5.6 KiB
Capability Spec Delta: diff (diff-stringify)
Package: @suaveplan/diff
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time. No exceptions. (Seeopenspec/AGENTS.mdSection 13.)
ADDED Requirements
Requirement: Multi-line patch rendering
The package SHALL expose stringifyPatch(patch: JsonPatch, options?: StringifyPatchOptions): string that renders the input JsonPatch as a multi-line string, one line per operation, joined by \n. The grammar for each line SHALL be:
add <path> -> <value>remove <path>replace <path> -> <value>move <from> -> <path>copy <from> -> <path>test <path> -> <value>
Path tokens SHALL be the operation's path (or from) field rendered verbatim. Value tokens SHALL be produced by options.valueFormatter (default: JSON.stringify).
When the input patch is empty, the function SHALL return the empty string.
Scenario: full RFC 6902 op set renders correctly
- WHEN a caller invokes
stringifyPatch([{ op: "add", path: "/a", value: 1 }, { op: "remove", path: "/b" }, { op: "move", from: "/c", path: "/d" }]) - THEN the result equals
"add /a -> 1\nremove /b\nmove /c -> /d"
Scenario: empty patch produces empty string
- WHEN a caller invokes
stringifyPatch([]) - THEN the result is the empty string
Verified by: src/utils/stringify-patch/stringify-patch.test.ts::"renders all six RFC 6902 operations" AND src/utils/stringify-patch/stringify-patch.test.ts::"returns empty string for empty patch"
Requirement: Value formatter injection point
StringifyPatchOptions.valueFormatter?: (value: unknown) => string SHALL be invoked exactly once per value-bearing operation (add, replace, test) when supplied. When omitted, the default formatter SHALL be JSON.stringify. Operations without a value field (remove, move, copy) SHALL NOT invoke the formatter.
Scenario: custom formatter applied
- WHEN a caller invokes
stringifyPatch([{ op: "add", path: "/secret", value: "abc" }], { valueFormatter: (v) =>***}) - THEN the result equals
"add /secret -> ***"
Scenario: formatter not invoked for value-less ops
- WHEN a caller invokes
stringifyPatch([{ op: "remove", path: "/a" }], { valueFormatter: spy }) - THEN
spyis invoked zero times
Verified by: src/utils/stringify-patch/stringify-patch.spec-conformance.test.ts::"valueFormatter receives every value-bearing op exactly once" AND src/utils/stringify-patch/stringify-patch.spec-conformance.test.ts::"valueFormatter not invoked for remove / move / copy"
Requirement: Indent prefix and uppercase op casing
StringifyPatchOptions.indent?: string SHALL be prepended to every rendered line (default: empty string). StringifyPatchOptions.uppercaseOps?: boolean SHALL render op names in uppercase when true (default: false, matching RFC 6902).
Scenario: indent prepended per line
- WHEN a caller invokes
stringifyPatch([{ op: "add", path: "/a", value: 1 }, { op: "remove", path: "/b" }], { indent: " " }) - THEN the result equals
" add /a -> 1\n remove /b"
Scenario: uppercase op casing
- WHEN a caller invokes
stringifyPatch([{ op: "add", path: "/a", value: 1 }], { uppercaseOps: true }) - THEN the result equals
"ADD /a -> 1"
Verified by: src/utils/stringify-patch/stringify-patch.spec-conformance.test.ts::"indent prefix prepended to every line" AND src/utils/stringify-patch/stringify-patch.spec-conformance.test.ts::"uppercaseOps flag flips op casing"
Requirement: Defensive guard for unknown ops
When the input patch contains an operation whose op field is not one of the six RFC 6902 operations, the function SHALL throw DiffError with code DIFF.INVALID_PATCH_OP. The DiffError message SHALL include the unknown op name for diagnostic purposes.
Scenario: unknown op throws structured error
- WHEN a caller invokes
stringifyPatch([{ op: "explode" as never, path: "/a" }]) - THEN the call throws
DiffErrorwith codeDIFF.INVALID_PATCH_OPAND the message contains"explode"
Verified by: src/utils/stringify-patch/stringify-patch.spec-conformance.test.ts::"throws DIFF.INVALID_PATCH_OP for unknown patch operation"
Requirement: OTel observability for stringify
stringifyPatch SHALL emit a passive OTel span event diff.stringify-patch carrying a numeric opCount attribute equal to patch.length. The span event MUST be emitted regardless of whether the caller has configured a tracer.
Scenario: span event emitted with correct opCount
- WHEN a caller invokes
stringifyPatch(patch)withpatch.length === 5under an in-memoryTracerProvider - THEN one span event named
diff.stringify-patchis recorded withopCount: 5
Verified by: src/utils/stringify-patch/stringify-patch.spec-conformance.test.ts::"emits diff.stringify-patch span event with opCount attribute"
Requirement: Subpath export
The package SHALL expose the new module at the ./utils/stringify-patch subpath in package.json::exports. Importing only stringifyPatch from that subpath SHALL NOT pull in unrelated @suaveplan/diff modules (diff/diff, patch/patch, history/history, sync/sync).
Scenario: subpath import resolves
- WHEN a caller writes
import { stringifyPatch } from "@suaveplan/diff/utils/stringify-patch" - THEN TypeScript resolves the import
Verified by: src/utils/stringify-patch/stringify-patch.spec-conformance.test.ts::"subpath ./utils/stringify-patch resolves and tree-shakes"