feat: serializer preserves unknown fields at every level (F5 SLICE-2, INV-15, #14)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+105
-59
@@ -121,6 +121,22 @@ export function newId(prefix: string): string {
|
||||
return `${prefix}_${randomUUID()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* INV-15 (round-trip preservation): after rebuilding the canonical known keys,
|
||||
* append any fields of `src` this writer does not recognize, sorted
|
||||
* lexicographically — no rung's writer may destroy another rung's data, and
|
||||
* placement stays deterministic (INV-2). Contract §2 rule 5 / §4 rule 3.
|
||||
*/
|
||||
function withUnknowns(known: Record<string, unknown>, src: unknown): Record<string, unknown> {
|
||||
const rec = src as Record<string, unknown>;
|
||||
for (const k of Object.keys(rec)
|
||||
.filter((k) => !(k in known))
|
||||
.sort()) {
|
||||
known[k] = rec[k];
|
||||
}
|
||||
return known;
|
||||
}
|
||||
|
||||
function serializeProvenance(p: Provenance): Record<string, unknown> {
|
||||
const known: Record<string, unknown> = {
|
||||
kind: p.kind,
|
||||
@@ -128,21 +144,27 @@ function serializeProvenance(p: Provenance): Record<string, unknown> {
|
||||
...(p.email !== undefined ? { email: p.email } : {}),
|
||||
};
|
||||
if (p.kind === "agent") {
|
||||
known.agent = {
|
||||
sdk: p.agent.sdk,
|
||||
model: p.agent.model,
|
||||
sessionId: p.agent.sessionId,
|
||||
...(p.agent.onBehalfOf !== undefined
|
||||
? {
|
||||
onBehalfOf: {
|
||||
id: p.agent.onBehalfOf.id,
|
||||
...(p.agent.onBehalfOf.email !== undefined ? { email: p.agent.onBehalfOf.email } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
known.agent = withUnknowns(
|
||||
{
|
||||
sdk: p.agent.sdk,
|
||||
model: p.agent.model,
|
||||
sessionId: p.agent.sessionId,
|
||||
...(p.agent.onBehalfOf !== undefined
|
||||
? {
|
||||
onBehalfOf: withUnknowns(
|
||||
{
|
||||
id: p.agent.onBehalfOf.id,
|
||||
...(p.agent.onBehalfOf.email !== undefined ? { email: p.agent.onBehalfOf.email } : {}),
|
||||
},
|
||||
p.agent.onBehalfOf,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
p.agent,
|
||||
);
|
||||
}
|
||||
return known;
|
||||
return withUnknowns(known, p);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,52 +173,76 @@ function serializeProvenance(p: Provenance): Record<string, unknown> {
|
||||
* ends with a trailing newline.
|
||||
*/
|
||||
export function serializeArtifact(a: Artifact): string {
|
||||
const canonical = {
|
||||
schemaVersion: a.schemaVersion,
|
||||
document: { path: a.document.path },
|
||||
anchors: Object.fromEntries(
|
||||
Object.keys(a.anchors)
|
||||
.sort()
|
||||
.map((k) => [
|
||||
k,
|
||||
const canonical = withUnknowns(
|
||||
{
|
||||
schemaVersion: a.schemaVersion,
|
||||
document: withUnknowns({ path: a.document.path }, a.document),
|
||||
anchors: Object.fromEntries(
|
||||
Object.keys(a.anchors)
|
||||
.sort()
|
||||
.map((k) => [
|
||||
k,
|
||||
withUnknowns(
|
||||
{
|
||||
fingerprint: withUnknowns(
|
||||
{
|
||||
text: a.anchors[k].fingerprint.text,
|
||||
before: a.anchors[k].fingerprint.before,
|
||||
after: a.anchors[k].fingerprint.after,
|
||||
lineHint: a.anchors[k].fingerprint.lineHint,
|
||||
},
|
||||
a.anchors[k].fingerprint,
|
||||
),
|
||||
},
|
||||
a.anchors[k],
|
||||
),
|
||||
]),
|
||||
),
|
||||
threads: a.threads.map((t) =>
|
||||
withUnknowns(
|
||||
{
|
||||
fingerprint: {
|
||||
text: a.anchors[k].fingerprint.text,
|
||||
before: a.anchors[k].fingerprint.before,
|
||||
after: a.anchors[k].fingerprint.after,
|
||||
lineHint: a.anchors[k].fingerprint.lineHint,
|
||||
},
|
||||
id: t.id,
|
||||
anchorId: t.anchorId,
|
||||
status: t.status,
|
||||
messages: t.messages.map((m) =>
|
||||
withUnknowns(
|
||||
{ id: m.id, author: serializeProvenance(m.author), body: m.body, createdAt: m.createdAt },
|
||||
m,
|
||||
),
|
||||
),
|
||||
},
|
||||
]),
|
||||
),
|
||||
threads: a.threads.map((t) => ({
|
||||
id: t.id,
|
||||
anchorId: t.anchorId,
|
||||
status: t.status,
|
||||
messages: t.messages.map((m) => ({
|
||||
id: m.id,
|
||||
author: serializeProvenance(m.author),
|
||||
body: m.body,
|
||||
createdAt: m.createdAt,
|
||||
})),
|
||||
})),
|
||||
attributions: a.attributions.map((at) => ({
|
||||
id: at.id,
|
||||
anchorId: at.anchorId,
|
||||
author: serializeProvenance(at.author),
|
||||
createdAt: at.createdAt,
|
||||
updatedAt: at.updatedAt,
|
||||
...(at.turnId !== undefined ? { turnId: at.turnId } : {}),
|
||||
})),
|
||||
proposals: a.proposals.map((p) => ({
|
||||
id: p.id,
|
||||
anchorId: p.anchorId,
|
||||
replacement: p.replacement,
|
||||
author: serializeProvenance(p.author),
|
||||
createdAt: p.createdAt,
|
||||
...(p.turnId !== undefined ? { turnId: p.turnId } : {}),
|
||||
...(p.instruction !== undefined ? { instruction: p.instruction } : {}),
|
||||
})),
|
||||
};
|
||||
t,
|
||||
),
|
||||
),
|
||||
attributions: a.attributions.map((at) =>
|
||||
withUnknowns(
|
||||
{
|
||||
id: at.id,
|
||||
anchorId: at.anchorId,
|
||||
author: serializeProvenance(at.author),
|
||||
createdAt: at.createdAt,
|
||||
updatedAt: at.updatedAt,
|
||||
...(at.turnId !== undefined ? { turnId: at.turnId } : {}),
|
||||
},
|
||||
at,
|
||||
),
|
||||
),
|
||||
proposals: a.proposals.map((p) =>
|
||||
withUnknowns(
|
||||
{
|
||||
id: p.id,
|
||||
anchorId: p.anchorId,
|
||||
replacement: p.replacement,
|
||||
author: serializeProvenance(p.author),
|
||||
createdAt: p.createdAt,
|
||||
...(p.turnId !== undefined ? { turnId: p.turnId } : {}),
|
||||
...(p.instruction !== undefined ? { instruction: p.instruction } : {}),
|
||||
},
|
||||
p,
|
||||
),
|
||||
),
|
||||
},
|
||||
a,
|
||||
);
|
||||
return JSON.stringify(canonical, null, 2) + "\n";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user