diff --git a/src/oc-path/tests/scenarios/cross-kind-properties.test.ts b/src/oc-path/tests/scenarios/cross-kind-properties.test.ts index e2622f4d6c0..4547d0e7dc8 100644 --- a/src/oc-path/tests/scenarios/cross-kind-properties.test.ts +++ b/src/oc-path/tests/scenarios/cross-kind-properties.test.ts @@ -11,82 +11,89 @@ * 5. parse → emit → parse is fixpoint * 6. hostile inputs do not throw at parse time */ -import { describe, expect, it } from 'vitest'; -import { inferKind } from '../../dispatch.js'; -import { emitMd } from '../../emit.js'; -import { setMdOcPath } from '../../edit.js'; -import { resolveMdOcPath } from '../../resolve.js'; -import { emitJsonc } from '../../jsonc/emit.js'; -import { setJsoncOcPath } from '../../jsonc/edit.js'; -import { resolveJsoncOcPath } from '../../jsonc/resolve.js'; -import { parseJsonc } from '../../jsonc/parse.js'; -import { emitJsonl } from '../../jsonl/emit.js'; -import { setJsonlOcPath } from '../../jsonl/edit.js'; -import { resolveJsonlOcPath } from '../../jsonl/resolve.js'; -import { parseJsonl } from '../../jsonl/parse.js'; -import { parseOcPath } from '../../oc-path.js'; -import { parseMd } from '../../parse.js'; +import { describe, expect, it } from "vitest"; +import { inferKind } from "../../dispatch.js"; +import { setMdOcPath } from "../../edit.js"; +import { emitMd } from "../../emit.js"; +import { setJsoncOcPath } from "../../jsonc/edit.js"; +import { emitJsonc } from "../../jsonc/emit.js"; +import { parseJsonc } from "../../jsonc/parse.js"; +import { resolveJsoncOcPath } from "../../jsonc/resolve.js"; +import { setJsonlOcPath } from "../../jsonl/edit.js"; +import { emitJsonl } from "../../jsonl/emit.js"; +import { parseJsonl } from "../../jsonl/parse.js"; +import { resolveJsonlOcPath } from "../../jsonl/resolve.js"; +import { parseOcPath } from "../../oc-path.js"; +import { parseMd } from "../../parse.js"; +import { resolveMdOcPath } from "../../resolve.js"; -describe('wave-22 cross-kind property invariants', () => { - const mdRaw = '---\nname: x\n---\n\n## Boundaries\n\n- enabled: true\n'; +describe("wave-22 cross-kind property invariants", () => { + const mdRaw = "---\nname: x\n---\n\n## Boundaries\n\n- enabled: true\n"; const jsoncRaw = '// h\n{ "k": 1, "n": [1,2,3] }\n'; const jsonlRaw = '{"a":1}\n\nbroken\n{"b":2}\n'; - it('P-01 round-trip parse → emit is byte-stable across all kinds', () => { + it("P-01 round-trip parse → emit is byte-stable across all kinds", () => { expect(emitMd(parseMd(mdRaw).ast)).toBe(mdRaw); expect(emitJsonc(parseJsonc(jsoncRaw).ast)).toBe(jsoncRaw); expect(emitJsonl(parseJsonl(jsonlRaw).ast)).toBe(jsonlRaw); }); - it('P-02 resolve is non-mutating across all kinds', () => { + it("P-02 resolve is non-mutating across all kinds", () => { const md = parseMd(mdRaw).ast; let before = JSON.stringify(md); - resolveMdOcPath(md, parseOcPath('oc://X/[frontmatter]/name')); - resolveMdOcPath(md, parseOcPath('oc://X/boundaries')); + resolveMdOcPath(md, parseOcPath("oc://X/[frontmatter]/name")); + resolveMdOcPath(md, parseOcPath("oc://X/boundaries")); expect(JSON.stringify(md)).toBe(before); const jsonc = parseJsonc(jsoncRaw).ast; before = JSON.stringify(jsonc); - resolveJsoncOcPath(jsonc, parseOcPath('oc://X/k')); - resolveJsoncOcPath(jsonc, parseOcPath('oc://X/n.0')); + resolveJsoncOcPath(jsonc, parseOcPath("oc://X/k")); + resolveJsoncOcPath(jsonc, parseOcPath("oc://X/n.0")); expect(JSON.stringify(jsonc)).toBe(before); const jsonl = parseJsonl(jsonlRaw).ast; before = JSON.stringify(jsonl); - resolveJsonlOcPath(jsonl, parseOcPath('oc://X/L1')); - resolveJsonlOcPath(jsonl, parseOcPath('oc://X/$last')); + resolveJsonlOcPath(jsonl, parseOcPath("oc://X/L1")); + resolveJsonlOcPath(jsonl, parseOcPath("oc://X/$last")); expect(JSON.stringify(jsonl)).toBe(before); }); - it('P-03 unresolvable set never throws across all kinds', () => { - const ocPath = parseOcPath('oc://X/totally.missing.path'); - expect(() => - setMdOcPath(parseMd(mdRaw).ast, ocPath, 'x'), - ).not.toThrow(); - expect(() => + it("P-03 unresolvable set never throws across all kinds", () => { + const ocPath = parseOcPath("oc://X/totally.missing.path"); + expect(setMdOcPath(parseMd(mdRaw).ast, ocPath, "x")).toEqual({ + ok: false, + reason: "not-writable", + }); + expect( setJsoncOcPath(parseJsonc(jsoncRaw).ast, ocPath, { - kind: 'string', - value: 'x', + kind: "string", + value: "x", }), - ).not.toThrow(); - expect(() => + ).toEqual({ + ok: false, + reason: "unresolved", + }); + expect( setJsonlOcPath(parseJsonl(jsonlRaw).ast, ocPath, { - kind: 'string', - value: 'x', + kind: "string", + value: "x", }), - ).not.toThrow(); + ).toEqual({ + ok: false, + reason: "unresolved", + }); }); - it('P-04 inferKind aligns with the parser actually used', () => { - expect(inferKind('AGENTS.md')).toBe('md'); - expect(inferKind('SOUL.md')).toBe('md'); - expect(inferKind('config.jsonc')).toBe('jsonc'); - expect(inferKind('plugins.json')).toBe('jsonc'); - expect(inferKind('events.jsonl')).toBe('jsonl'); - expect(inferKind('audit.ndjson')).toBe('jsonl'); + it("P-04 inferKind aligns with the parser actually used", () => { + expect(inferKind("AGENTS.md")).toBe("md"); + expect(inferKind("SOUL.md")).toBe("md"); + expect(inferKind("config.jsonc")).toBe("jsonc"); + expect(inferKind("plugins.json")).toBe("jsonc"); + expect(inferKind("events.jsonl")).toBe("jsonl"); + expect(inferKind("audit.ndjson")).toBe("jsonl"); }); - it('P-05 parse → emit → parse is fixpoint across all kinds', () => { + it("P-05 parse → emit → parse is fixpoint across all kinds", () => { const md1 = emitMd(parseMd(mdRaw).ast); const md2 = emitMd(parseMd(md1).ast); expect(md1).toBe(md2); @@ -100,54 +107,52 @@ describe('wave-22 cross-kind property invariants', () => { expect(jl1).toBe(jl2); }); - it('P-06 hostile inputs do not throw at parse time across all kinds', () => { + it("P-06 hostile inputs do not throw at parse time across all kinds", () => { const hostile = [ - '\x00\x01\x02 binary garbage', + "\x00\x01\x02 binary garbage", '{ "unclosed":', - '## heading without anything', - '\n\n\n\n\n', + "## heading without anything", + "\n\n\n\n\n", ]; for (const raw of hostile) { - expect(() => parseMd(raw)).not.toThrow(); - expect(() => parseJsonc(raw)).not.toThrow(); - expect(() => parseJsonl(raw)).not.toThrow(); + expect(parseMd(raw).ast.raw).toBe(raw); + expect( + parseJsonc(raw).diagnostics.every((diagnostic) => diagnostic.severity === "error"), + ).toBe(true); + expect(parseJsonl(raw).ast.raw).toBe(raw); } }); - it('P-07 resolver returns null for paths past valid kinds (no throw)', () => { - const overlong = parseOcPath('oc://X/a/b/c.d.e.f.g.h'); - expect(() => resolveMdOcPath(parseMd(mdRaw).ast, overlong)).not.toThrow(); - expect(() => resolveJsoncOcPath(parseJsonc(jsoncRaw).ast, overlong)).not.toThrow(); - expect(() => resolveJsonlOcPath(parseJsonl(jsonlRaw).ast, overlong)).not.toThrow(); + it("P-07 resolver returns null for paths past valid kinds", () => { + const overlong = parseOcPath("oc://X/a/b/c.d.e.f.g.h"); + expect(resolveMdOcPath(parseMd(mdRaw).ast, overlong)).toBeNull(); + expect(resolveJsoncOcPath(parseJsonc(jsoncRaw).ast, overlong)).toBeNull(); + expect(resolveJsonlOcPath(parseJsonl(jsonlRaw).ast, overlong)).toBeNull(); }); - it('P-08 set-then-resolve produces the value just written (jsonc)', () => { + it("P-08 set-then-resolve produces the value just written (jsonc)", () => { const ast = parseJsonc('{ "k": 1 }').ast; - const r = setJsoncOcPath(ast, parseOcPath('oc://X/k'), { - kind: 'number', + const r = setJsoncOcPath(ast, parseOcPath("oc://X/k"), { + kind: "number", value: 42, }); if (r.ok) { - const m = resolveJsoncOcPath(r.ast, parseOcPath('oc://X/k')); - if (m?.kind === 'object-entry') { - expect(m.node.value).toEqual({ kind: 'number', value: 42 }); + const m = resolveJsoncOcPath(r.ast, parseOcPath("oc://X/k")); + if (m?.kind === "object-entry") { + expect(m.node.value).toEqual({ kind: "number", value: 42 }); } } }); - it('P-09 verbs are deterministic — same input twice produces same output', () => { + it("P-09 verbs are deterministic — same input twice produces same output", () => { expect(emitMd(parseMd(mdRaw).ast)).toBe(emitMd(parseMd(mdRaw).ast)); - expect(emitJsonc(parseJsonc(jsoncRaw).ast)).toBe( - emitJsonc(parseJsonc(jsoncRaw).ast), - ); - expect(emitJsonl(parseJsonl(jsonlRaw).ast)).toBe( - emitJsonl(parseJsonl(jsonlRaw).ast), - ); + expect(emitJsonc(parseJsonc(jsoncRaw).ast)).toBe(emitJsonc(parseJsonc(jsoncRaw).ast)); + expect(emitJsonl(parseJsonl(jsonlRaw).ast)).toBe(emitJsonl(parseJsonl(jsonlRaw).ast)); }); - it('P-10 inferKind returns null for unknown extensions', () => { - expect(inferKind('binary.bin')).toBeNull(); - expect(inferKind('no-ext')).toBeNull(); - expect(inferKind('archive.tar.gz')).toBeNull(); + it("P-10 inferKind returns null for unknown extensions", () => { + expect(inferKind("binary.bin")).toBeNull(); + expect(inferKind("no-ext")).toBeNull(); + expect(inferKind("archive.tar.gz")).toBeNull(); }); }); diff --git a/src/oc-path/tests/scenarios/jsonc-resolver-edges.test.ts b/src/oc-path/tests/scenarios/jsonc-resolver-edges.test.ts index 06001ddcb98..d9cc8cece70 100644 --- a/src/oc-path/tests/scenarios/jsonc-resolver-edges.test.ts +++ b/src/oc-path/tests/scenarios/jsonc-resolver-edges.test.ts @@ -5,128 +5,136 @@ * with mixed dotted / segment paths, returns null on any unresolvable * walk, and never throws on hostile inputs. */ -import { describe, expect, it } from 'vitest'; -import { parseJsonc } from '../../jsonc/parse.js'; -import { resolveJsoncOcPath } from '../../jsonc/resolve.js'; -import { parseOcPath } from '../../oc-path.js'; +import { describe, expect, it } from "vitest"; +import { parseJsonc } from "../../jsonc/parse.js"; +import { resolveJsoncOcPath } from "../../jsonc/resolve.js"; +import { parseOcPath } from "../../oc-path.js"; function rs(raw: string, ocPath: string) { return resolveJsoncOcPath(parseJsonc(raw).ast, parseOcPath(ocPath)); } -describe('wave-17 jsonc resolver edges', () => { - it('JR-01 root resolves on empty object', () => { - expect(rs('{}', 'oc://config')?.kind).toBe('root'); +describe("wave-17 jsonc resolver edges", () => { + it("JR-01 root resolves on empty object", () => { + expect(rs("{}", "oc://config")?.kind).toBe("root"); }); - it('JR-02 root resolves on scalar root', () => { - expect(rs('42', 'oc://config')?.kind).toBe('root'); + it("JR-02 root resolves on scalar root", () => { + expect(rs("42", "oc://config")?.kind).toBe("root"); }); - it('JR-03 root resolves on array root', () => { - expect(rs('[1,2,3]', 'oc://config')?.kind).toBe('root'); + it("JR-03 root resolves on array root", () => { + expect(rs("[1,2,3]", "oc://config")?.kind).toBe("root"); }); - it('JR-04 deep dotted descent within section', () => { - const m = rs('{"a":{"b":{"c":1}}}', 'oc://config/a.b.c'); - expect(m?.kind).toBe('object-entry'); + it("JR-04 deep dotted descent within section", () => { + const m = rs('{"a":{"b":{"c":1}}}', "oc://config/a.b.c"); + expect(m?.kind).toBe("object-entry"); }); - it('JR-05 missing intermediate key returns null', () => { - expect(rs('{"a":{"b":1}}', 'oc://config/a.x.b')).toBeNull(); + it("JR-05 missing intermediate key returns null", () => { + expect(rs('{"a":{"b":1}}', "oc://config/a.x.b")).toBeNull(); }); - it('JR-06 numeric segment indexes into array', () => { - const m = rs('{"items":["a","b","c"]}', 'oc://config/items.1'); - expect(m?.kind).toBe('value'); - if (m?.kind === 'value') { - expect(m.node).toMatchObject({ kind: 'string', value: 'b' }); + it("JR-06 numeric segment indexes into array", () => { + const m = rs('{"items":["a","b","c"]}', "oc://config/items.1"); + expect(m?.kind).toBe("value"); + if (m?.kind === "value") { + expect(m.node).toMatchObject({ kind: "string", value: "b" }); } }); - it('JR-07 negative array index resolves to Nth-from-last', () => { - expect(rs('{"x":[1,2]}', 'oc://config/x.-1')).toMatchObject({ kind: 'value', node: { kind: 'number', value: 2 } }); - expect(rs('{"x":[1,2]}', 'oc://config/x.-2')).toMatchObject({ kind: 'value', node: { kind: 'number', value: 1 } }); - expect(rs('{"x":[1,2]}', 'oc://config/x.-5')).toBeNull(); + it("JR-07 negative array index resolves to Nth-from-last", () => { + expect(rs('{"x":[1,2]}', "oc://config/x.-1")).toMatchObject({ + kind: "value", + node: { kind: "number", value: 2 }, + }); + expect(rs('{"x":[1,2]}', "oc://config/x.-2")).toMatchObject({ + kind: "value", + node: { kind: "number", value: 1 }, + }); + expect(rs('{"x":[1,2]}', "oc://config/x.-5")).toBeNull(); }); - it('JR-08 out-of-bounds array index returns null', () => { - expect(rs('{"x":[1,2]}', 'oc://config/x.99')).toBeNull(); + it("JR-08 out-of-bounds array index returns null", () => { + expect(rs('{"x":[1,2]}', "oc://config/x.99")).toBeNull(); }); - it('JR-09 non-integer index returns null (no NaN coercion)', () => { - expect(rs('{"x":[1,2]}', 'oc://config/x.foo')).toBeNull(); + it("JR-09 non-integer index returns null (no NaN coercion)", () => { + expect(rs('{"x":[1,2]}', "oc://config/x.foo")).toBeNull(); }); - it('JR-10 null AST root returns null on any path', () => { - expect(rs('', 'oc://config/x')).toBeNull(); + it("JR-10 null AST root returns null on any path", () => { + expect(rs("", "oc://config/x")).toBeNull(); }); - it('JR-11 descending past a primitive returns null', () => { - expect(rs('{"x":42}', 'oc://config/x.y')).toBeNull(); + it("JR-11 descending past a primitive returns null", () => { + expect(rs('{"x":42}', "oc://config/x.y")).toBeNull(); }); - it('JR-12 empty segment in dotted path throws OcPathError', () => { + it("JR-12 empty segment in dotted path throws OcPathError", () => { // v1 invariant: malformed paths fail loud at parse time, not silently null. - expect(() => rs('{"x":1}', 'oc://config/x..y')).toThrow(/Empty dotted sub-segment/); + expect(() => rs('{"x":1}', "oc://config/x..y")).toThrow(/Empty dotted sub-segment/); }); - it('JR-13 string value at leaf surfaces via object-entry shape', () => { - const m = rs('{"k":"v"}', 'oc://config/k'); - expect(m?.kind).toBe('object-entry'); - if (m?.kind === 'object-entry') {expect(m.node.key).toBe('k');} + it("JR-13 string value at leaf surfaces via object-entry shape", () => { + const m = rs('{"k":"v"}', "oc://config/k"); + expect(m?.kind).toBe("object-entry"); + if (m?.kind === "object-entry") { + expect(m.node.key).toBe("k"); + } }); - it('JR-14 boolean and null values resolve', () => { - const m1 = rs('{"k":true}', 'oc://config/k'); - expect(m1?.kind).toBe('object-entry'); - const m2 = rs('{"k":null}', 'oc://config/k'); - expect(m2?.kind).toBe('object-entry'); + it("JR-14 boolean and null values resolve", () => { + const m1 = rs('{"k":true}', "oc://config/k"); + expect(m1?.kind).toBe("object-entry"); + const m2 = rs('{"k":null}', "oc://config/k"); + expect(m2?.kind).toBe("object-entry"); }); - it('JR-15 mixed slash + dot segments resolve identically', () => { - const a = rs('{"a":{"b":{"c":1}}}', 'oc://config/a.b.c'); - const b = rs('{"a":{"b":{"c":1}}}', 'oc://config/a/b.c'); - const c = rs('{"a":{"b":{"c":1}}}', 'oc://config/a/b/c'); + it("JR-15 mixed slash + dot segments resolve identically", () => { + const a = rs('{"a":{"b":{"c":1}}}', "oc://config/a.b.c"); + const b = rs('{"a":{"b":{"c":1}}}', "oc://config/a/b.c"); + const c = rs('{"a":{"b":{"c":1}}}', "oc://config/a/b/c"); expect(a?.kind).toBe(b?.kind); expect(b?.kind).toBe(c?.kind); }); - it('JR-16 keys with special characters resolve', () => { - const m = rs('{"a-b_c":{"x":1}}', 'oc://config/a-b_c.x'); - expect(m?.kind).toBe('object-entry'); + it("JR-16 keys with special characters resolve", () => { + const m = rs('{"a-b_c":{"x":1}}', "oc://config/a-b_c.x"); + expect(m?.kind).toBe("object-entry"); }); - it('JR-17 unicode keys resolve', () => { - const m = rs('{"héllo":1}', 'oc://config/héllo'); - expect(m?.kind).toBe('object-entry'); + it("JR-17 unicode keys resolve", () => { + const m = rs('{"héllo":1}', "oc://config/héllo"); + expect(m?.kind).toBe("object-entry"); }); - it('JR-18 large nested structure (depth 20) resolves to leaf', () => { + it("JR-18 large nested structure (depth 20) resolves to leaf", () => { let json = '"leaf"'; const segs: string[] = []; for (let i = 19; i >= 0; i--) { json = `{"k${i}":${json}}`; segs.unshift(`k${i}`); } - const m = rs(json, `oc://config/${segs.join('.')}`); - expect(m?.kind).toBe('object-entry'); - if (m?.kind === 'object-entry') { - expect(m.node.value).toMatchObject({ kind: 'string', value: 'leaf' }); + const m = rs(json, `oc://config/${segs.join(".")}`); + expect(m?.kind).toBe("object-entry"); + if (m?.kind === "object-entry") { + expect(m.node.value).toMatchObject({ kind: "string", value: "leaf" }); } }); - it('JR-19 resolver is non-mutating across calls', () => { + it("JR-19 resolver is non-mutating across calls", () => { const { ast } = parseJsonc('{"x":{"y":1}}'); const before = JSON.stringify(ast); - rs('{"x":{"y":1}}', 'oc://config/x.y'); - rs('{"x":{"y":1}}', 'oc://config/x'); - rs('{"x":{"y":1}}', 'oc://config/missing'); + rs('{"x":{"y":1}}', "oc://config/x.y"); + rs('{"x":{"y":1}}', "oc://config/x"); + rs('{"x":{"y":1}}', "oc://config/missing"); expect(JSON.stringify(ast)).toBe(before); }); - it('JR-20 hostile input shapes do not throw', () => { - expect(() => rs('{garbage}', 'oc://config/x')).not.toThrow(); - expect(() => rs('{"a":', 'oc://config/a')).not.toThrow(); + it("JR-20 hostile input shapes do not throw", () => { + expect(rs("{garbage}", "oc://config/x")).toBeNull(); + expect(rs('{"a":', "oc://config/a")).toBeNull(); }); }); diff --git a/src/oc-path/tests/scenarios/jsonl-resolver-edges.test.ts b/src/oc-path/tests/scenarios/jsonl-resolver-edges.test.ts index edecb2cbb03..ef38bc8b51e 100644 --- a/src/oc-path/tests/scenarios/jsonl-resolver-edges.test.ts +++ b/src/oc-path/tests/scenarios/jsonl-resolver-edges.test.ts @@ -5,121 +5,125 @@ * deterministically; missing addresses, blank-line targets, and * malformed-line targets all surface as null without throwing. */ -import { describe, expect, it } from 'vitest'; -import { parseJsonl } from '../../jsonl/parse.js'; -import { resolveJsonlOcPath } from '../../jsonl/resolve.js'; -import { parseOcPath } from '../../oc-path.js'; +import { describe, expect, it } from "vitest"; +import { parseJsonl } from "../../jsonl/parse.js"; +import { resolveJsonlOcPath } from "../../jsonl/resolve.js"; +import { parseOcPath } from "../../oc-path.js"; function rs(raw: string, ocPath: string) { return resolveJsonlOcPath(parseJsonl(raw).ast, parseOcPath(ocPath)); } -describe('wave-18 jsonl resolver edges', () => { - it('JLR-01 root resolves with no segments', () => { - expect(rs('{"a":1}\n', 'oc://log')?.kind).toBe('root'); +describe("wave-18 jsonl resolver edges", () => { + it("JLR-01 root resolves with no segments", () => { + expect(rs('{"a":1}\n', "oc://log")?.kind).toBe("root"); }); - it('JLR-02 L1 resolves to a value line', () => { - const m = rs('{"a":1}\n', 'oc://log/L1'); - expect(m?.kind).toBe('line'); + it("JLR-02 L1 resolves to a value line", () => { + const m = rs('{"a":1}\n', "oc://log/L1"); + expect(m?.kind).toBe("line"); }); - it('JLR-03 L99 unknown line returns null', () => { - expect(rs('{"a":1}\n', 'oc://log/L99')).toBeNull(); + it("JLR-03 L99 unknown line returns null", () => { + expect(rs('{"a":1}\n', "oc://log/L99")).toBeNull(); }); - it('JLR-04 $last picks the most recent value line', () => { - const m = rs('{"a":1}\n{"a":2}\n{"a":3}\n', 'oc://log/$last/a'); - expect(m?.kind).toBe('object-entry'); - if (m?.kind === 'object-entry') { - expect(m.node.value).toMatchObject({ kind: 'number', value: 3 }); + it("JLR-04 $last picks the most recent value line", () => { + const m = rs('{"a":1}\n{"a":2}\n{"a":3}\n', "oc://log/$last/a"); + expect(m?.kind).toBe("object-entry"); + if (m?.kind === "object-entry") { + expect(m.node.value).toMatchObject({ kind: "number", value: 3 }); } }); - it('JLR-05 $last skips trailing blank lines', () => { - const m = rs('{"a":1}\n\n\n', 'oc://log/$last/a'); - expect(m?.kind).toBe('object-entry'); - if (m?.kind === 'object-entry') { - expect(m.node.value).toMatchObject({ kind: 'number', value: 1 }); + it("JLR-05 $last skips trailing blank lines", () => { + const m = rs('{"a":1}\n\n\n', "oc://log/$last/a"); + expect(m?.kind).toBe("object-entry"); + if (m?.kind === "object-entry") { + expect(m.node.value).toMatchObject({ kind: "number", value: 1 }); } }); - it('JLR-06 $last skips trailing malformed lines', () => { - const m = rs('{"a":1}\nbroken\n', 'oc://log/$last/a'); - expect(m?.kind).toBe('object-entry'); + it("JLR-06 $last skips trailing malformed lines", () => { + const m = rs('{"a":1}\nbroken\n', "oc://log/$last/a"); + expect(m?.kind).toBe("object-entry"); }); - it('JLR-07 $last on empty file returns null', () => { - expect(rs('', 'oc://log/$last/x')).toBeNull(); + it("JLR-07 $last on empty file returns null", () => { + expect(rs("", "oc://log/$last/x")).toBeNull(); }); - it('JLR-08 $last on all-blank file returns null', () => { - expect(rs('\n\n\n', 'oc://log/$last/x')).toBeNull(); + it("JLR-08 $last on all-blank file returns null", () => { + expect(rs("\n\n\n", "oc://log/$last/x")).toBeNull(); }); - it('JLR-09 $last on all-malformed file returns null', () => { - expect(rs('a\nb\nc\n', 'oc://log/$last/x')).toBeNull(); + it("JLR-09 $last on all-malformed file returns null", () => { + expect(rs("a\nb\nc\n", "oc://log/$last/x")).toBeNull(); }); - it('JLR-10 garbage line address returns null', () => { - expect(rs('{"a":1}\n', 'oc://log/garbage')).toBeNull(); - expect(rs('{"a":1}\n', 'oc://log/L')).toBeNull(); - expect(rs('{"a":1}\n', 'oc://log/Labc')).toBeNull(); + it("JLR-10 garbage line address returns null", () => { + expect(rs('{"a":1}\n', "oc://log/garbage")).toBeNull(); + expect(rs('{"a":1}\n', "oc://log/L")).toBeNull(); + expect(rs('{"a":1}\n', "oc://log/Labc")).toBeNull(); }); - it('JLR-11 descent into a blank line returns null', () => { - expect(rs('{"a":1}\n\n{"b":2}\n', 'oc://log/L2/anything')).toBeNull(); + it("JLR-11 descent into a blank line returns null", () => { + expect(rs('{"a":1}\n\n{"b":2}\n', "oc://log/L2/anything")).toBeNull(); }); - it('JLR-12 descent into a malformed line returns null', () => { - expect(rs('{"a":1}\nbroken\n{"b":2}\n', 'oc://log/L2/anything')).toBeNull(); + it("JLR-12 descent into a malformed line returns null", () => { + expect(rs('{"a":1}\nbroken\n{"b":2}\n', "oc://log/L2/anything")).toBeNull(); }); - it('JLR-13 missing field on a value line returns null', () => { - expect(rs('{"a":1}\n', 'oc://log/L1/missing')).toBeNull(); + it("JLR-13 missing field on a value line returns null", () => { + expect(rs('{"a":1}\n', "oc://log/L1/missing")).toBeNull(); }); - it('JLR-14 dotted descent through line value resolves', () => { - const m = rs('{"r":{"ok":true,"d":"x"}}\n', 'oc://log/L1/r.d'); - expect(m?.kind).toBe('object-entry'); - if (m?.kind === 'object-entry') { - expect(m.node.value).toMatchObject({ kind: 'string', value: 'x' }); + it("JLR-14 dotted descent through line value resolves", () => { + const m = rs('{"r":{"ok":true,"d":"x"}}\n', "oc://log/L1/r.d"); + expect(m?.kind).toBe("object-entry"); + if (m?.kind === "object-entry") { + expect(m.node.value).toMatchObject({ kind: "string", value: "x" }); } }); - it('JLR-15 array index inside a line resolves', () => { - const m = rs('{"items":["a","b","c"]}\n', 'oc://log/L1/items.2'); - expect(m?.kind).toBe('value'); - if (m?.kind === 'value') { - expect(m.node).toMatchObject({ kind: 'string', value: 'c' }); + it("JLR-15 array index inside a line resolves", () => { + const m = rs('{"items":["a","b","c"]}\n', "oc://log/L1/items.2"); + expect(m?.kind).toBe("value"); + if (m?.kind === "value") { + expect(m.node).toMatchObject({ kind: "string", value: "c" }); } }); - it('JLR-16 line numbers are 1-indexed', () => { - const m = rs('{"a":1}\n{"a":2}\n', 'oc://log/L1/a'); - if (m?.kind === 'object-entry') { - expect(m.node.value).toMatchObject({ kind: 'number', value: 1 }); + it("JLR-16 line numbers are 1-indexed", () => { + const m = rs('{"a":1}\n{"a":2}\n', "oc://log/L1/a"); + if (m?.kind === "object-entry") { + expect(m.node.value).toMatchObject({ kind: "number", value: 1 }); } }); - it('JLR-17 line numbers preserved across blank/malformed entries', () => { - const m = rs('{"a":1}\n\nbroken\n{"a":4}\n', 'oc://log/L4/a'); - expect(m?.kind).toBe('object-entry'); - if (m?.kind === 'object-entry') { - expect(m.node.value).toMatchObject({ kind: 'number', value: 4 }); + it("JLR-17 line numbers preserved across blank/malformed entries", () => { + const m = rs('{"a":1}\n\nbroken\n{"a":4}\n', "oc://log/L4/a"); + expect(m?.kind).toBe("object-entry"); + if (m?.kind === "object-entry") { + expect(m.node.value).toMatchObject({ kind: "number", value: 4 }); } }); - it('JLR-18 resolver is non-mutating', () => { + it("JLR-18 resolver is non-mutating", () => { const { ast } = parseJsonl('{"a":1}\n{"b":2}\n'); const before = JSON.stringify(ast); - rs('{"a":1}\n{"b":2}\n', 'oc://log/L1'); - rs('{"a":1}\n{"b":2}\n', 'oc://log/$last'); + rs('{"a":1}\n{"b":2}\n', "oc://log/L1"); + rs('{"a":1}\n{"b":2}\n', "oc://log/$last"); expect(JSON.stringify(ast)).toBe(before); }); - it('JLR-19 hostile inputs do not throw', () => { - expect(() => rs('not json\n', 'oc://log/L1')).not.toThrow(); - expect(() => rs('', 'oc://log/$last')).not.toThrow(); + it("JLR-19 hostile inputs do not throw", () => { + const malformed = rs("not json\n", "oc://log/L1"); + expect(malformed?.kind).toBe("line"); + if (malformed?.kind === "line") { + expect(malformed.node.kind).toBe("malformed"); + } + expect(rs("", "oc://log/$last")).toBeNull(); }); });