fix(oc-path): quote positional JSONC keys (#130499)

This commit is contained in:
Vincent Koc
2026-08-27 13:56:02 +08:00
committed by GitHub
parent be8b6ff3c4
commit d36d90ba62
4 changed files with 36 additions and 28 deletions
+7 -13
View File
@@ -11,6 +11,7 @@ import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
import { isMap, isScalar, isSeq, type Node, type Pair } from "yaml";
import type { MdAst } from "./ast.js";
import type { JsoncValue } from "./jsonc/ast.js";
import { resolveJsoncPositionalSegment } from "./jsonc/resolve-value.js";
import type { JsonlAst, JsonlLine } from "./jsonl/ast.js";
import { pickJsonlLine } from "./jsonl/line.js";
import type { OcPath, PredicateSpec } from "./oc-path.js";
@@ -303,11 +304,15 @@ const jsoncOps: WalkOps<JsoncValue> = {
return null;
},
positional(node, seg) {
const concrete = positionalForJsoncNode(node, seg);
const concrete = resolveJsoncPositionalSegment(node, seg);
if (concrete === null) {
return null;
}
return jsoncOps.lookup(node, concrete);
const match = jsoncOps.lookup(node, concrete);
if (match === null || node.kind !== "object") {
return match;
}
return { keySub: quoteSeg(concrete), child: match.child };
},
*predicate(node, pred) {
if (node.kind === "object") {
@@ -327,17 +332,6 @@ const jsoncOps: WalkOps<JsoncValue> = {
walk: walkJsonc,
};
function positionalForJsoncNode(node: JsoncValue, seg: string): string | null {
if (node.kind === "object") {
const keys = node.entries.map((e) => e.key);
return resolvePositionalSeg(seg, { indexable: false, size: keys.length, keys });
}
if (node.kind === "array") {
return resolvePositionalSeg(seg, { indexable: true, size: node.items.length });
}
return null;
}
// ---------- JSONL walker ---------------------------------------------------
// First slot is a line address; subsequent slots descend into the
+2 -13
View File
@@ -5,13 +5,13 @@ import {
isPositionalSeg,
isQuotedSeg,
parseArrayIndexSegment,
resolvePositionalSeg,
splitRespectingBrackets,
unquoteSeg,
} from "../oc-path.js";
import { OcEmitSentinelError, REDACTED_SENTINEL } from "../sentinel.js";
import type { JsoncAst, JsoncValue } from "./ast.js";
import { parseJsonc } from "./parse.js";
import { resolveJsoncPositionalSegment } from "./resolve-value.js";
type JsoncEditPath = Array<string | number>;
type JsoncEditTarget = { readonly path: JsoncEditPath; readonly value: JsoncValue };
@@ -126,7 +126,7 @@ function resolveEditTarget(root: JsoncValue, segments: readonly string[]): Jsonc
return null;
}
if (isPositionalSeg(segment)) {
const concrete = positionalForJsonc(current, segment);
const concrete = resolveJsoncPositionalSegment(current, segment);
if (concrete !== null) {
segment = concrete;
}
@@ -154,17 +154,6 @@ function resolveEditTarget(root: JsoncValue, segments: readonly string[]): Jsonc
return { path: out, value: current };
}
function positionalForJsonc(node: JsoncValue, segment: string): string | null {
if (node.kind === "object") {
const keys = node.entries.map((entry) => entry.key);
return resolvePositionalSeg(segment, { indexable: false, size: keys.length, keys });
}
if (node.kind === "array") {
return resolvePositionalSeg(segment, { indexable: true, size: node.items.length });
}
return null;
}
function jsoncValueToJson(value: JsoncValue): unknown {
switch (value.kind) {
case "object":
@@ -23,7 +23,7 @@ export function resolveJsoncValueOcPath(
return null;
}
if (isPositionalSeg(seg)) {
const concrete = positionalForJsonc(current, seg);
const concrete = resolveJsoncPositionalSegment(current, seg);
if (concrete !== null) {
seg = concrete;
}
@@ -60,7 +60,7 @@ export function resolveJsoncValueOcPath(
return { kind: "value", node: current, path: walked };
}
function positionalForJsonc(node: JsoncValue, seg: string): string | null {
export function resolveJsoncPositionalSegment(node: JsoncValue, seg: string): string | null {
if (node.kind === "object") {
const keys = node.entries.map((e) => e.key);
return resolvePositionalSeg(seg, { indexable: false, size: keys.length, keys });
@@ -117,6 +117,31 @@ describe("findOcPaths — JSONC kind", () => {
}
}
});
it.each([
{
name: "quotes positional object keys",
raw: '{"items":{"zeta.key":10,"alpha":20}}',
pattern: "oc://config/items/$first",
expectedPath: 'oc://config/items/"zeta.key"',
expectedValue: "10",
},
{
name: "emits positional array indexes",
raw: '{"items":[10,20,30]}',
pattern: "oc://config/items/$last",
expectedPath: "oc://config/items/2",
expectedValue: "30",
},
])("$name", ({ raw, pattern, expectedPath, expectedValue }) => {
const ast = parseJsonc(raw).ast;
const out = findOcPaths(ast, parseOcPath(pattern));
expect(out).toHaveLength(1);
const result = requireFirstResult(out);
expect(formatOcPath(result.path)).toBe(expectedPath);
expect(result.match.kind === "leaf" && result.match.valueText).toBe(expectedValue);
});
});
describe("findOcPaths — slash-deep JSONC paths", () => {