Problem
The sidecar contract is enforced in two hand-maintained implementations that can drift:
- Plugin (renderer):
plugins/obsidian-plugin/src/schema.ts — parseSidecar() is a hand-written zod schema (node id kebab regex, node classes regex ^(system|task|decision) (completed|active|context|blocked)$, edge classes enum strong-edge/weak-edge, required bounded position x[-200,5000]/y[-200,3000], strict top-level keys, _extractedBy/_schemaVersion/_pinned semantics, edge-endpoint referential check).
- Agent conformer (Engine A):
regenerate.py in the ai-private visual-notes skill — re-implements those same rules by hand in Python (NODE_ID_RE, NODE_CLASS_RE, EDGE_CLASSES, ALLOWED_TOP, position clamps, max nodes/edges). Its own docstring says it "mirrors plugins/obsidian-plugin/src/schema.ts."
Two hand-mirrored implementations of one contract drift. Recent near-misses all trace to this: edge classes must be strong-edge/weak-edge (not "edge"), strict top-level keys, the node classes regex, and position bounds.
Current mechanism
A JSON Schema already exists at shared/schema.json (draft-07, @visual-notes/shared) and is a complete mirror of the contract — but it is not the enforcement source of truth. Today it is only consumed by extractor.ts via toAnthropicToolSchema(sharedSidecarSchema) to shape the LLM tool input (what the model is asked to produce). The actual validation on both sides is still hand-mirrored:
- the plugin validates with hand-written zod in
schema.ts (not derived from shared/schema.json);
- the Python conformer validates/coerces with hard-coded constants (no reference to
shared/schema.json at all — it's in a separate repo).
So we have the schema file but neither validator is driven by it. The drift surface is unchanged.
Proposal
Promote shared/schema.json from "LLM tool-input shape" to the single enforcement source of truth for both consumers:
- TS plugin: validate against
shared/schema.json via ajv (or generate the zod schema from it via json-schema-to-zod), preserving parseSidecar()'s current behavior — including the referential edge-endpoint check (superRefine) and .strict() top-level semantics.
- Python conformer: drive coerce/validate from the same file via the
jsonschema package instead of the hand-coded NODE_ID_RE / NODE_CLASS_RE / EDGE_CLASSES / bounds constants. (Requires the conformer to read the schema from the visual-notes repo — vendor or pin it.)
The schema already has $id and rich descriptions, so it can carry the prose currently duplicated in the extraction prompt and the conformer comments.
Benefits
- Single source of truth — no drift between plugin, conformer, and LLM tool-input.
- Both engines provably conform to the same artifact.
_extractedBy / _schemaVersion / _pinned semantics centralized in one place.
- The LLM tool-input shape and the runtime validator can no longer disagree (they're the same file).
Acceptance
Notes
- The conformer lives in a separate repo (ai-private
visual-notes skill). Centralization requires a way to share the schema across repos (vendored copy with a version pin, published @visual-notes/shared, or a sync step) — worth deciding as part of this.
Problem
The sidecar contract is enforced in two hand-maintained implementations that can drift:
plugins/obsidian-plugin/src/schema.ts—parseSidecar()is a hand-written zod schema (node id kebab regex, nodeclassesregex^(system|task|decision) (completed|active|context|blocked)$, edgeclassesenumstrong-edge/weak-edge, required boundedpositionx[-200,5000]/y[-200,3000], strict top-level keys,_extractedBy/_schemaVersion/_pinnedsemantics, edge-endpoint referential check).regenerate.pyin the ai-privatevisual-notesskill — re-implements those same rules by hand in Python (NODE_ID_RE,NODE_CLASS_RE,EDGE_CLASSES,ALLOWED_TOP, position clamps, max nodes/edges). Its own docstring says it "mirrorsplugins/obsidian-plugin/src/schema.ts."Two hand-mirrored implementations of one contract drift. Recent near-misses all trace to this: edge
classesmust bestrong-edge/weak-edge(not"edge"), strict top-level keys, the nodeclassesregex, and position bounds.Current mechanism
A JSON Schema already exists at
shared/schema.json(draft-07,@visual-notes/shared) and is a complete mirror of the contract — but it is not the enforcement source of truth. Today it is only consumed byextractor.tsviatoAnthropicToolSchema(sharedSidecarSchema)to shape the LLM tool input (what the model is asked to produce). The actual validation on both sides is still hand-mirrored:schema.ts(not derived fromshared/schema.json);shared/schema.jsonat all — it's in a separate repo).So we have the schema file but neither validator is driven by it. The drift surface is unchanged.
Proposal
Promote
shared/schema.jsonfrom "LLM tool-input shape" to the single enforcement source of truth for both consumers:shared/schema.jsonvia ajv (or generate the zod schema from it via json-schema-to-zod), preservingparseSidecar()'s current behavior — including the referential edge-endpoint check (superRefine) and.strict()top-level semantics.jsonschemapackage instead of the hand-codedNODE_ID_RE/NODE_CLASS_RE/EDGE_CLASSES/ bounds constants. (Requires the conformer to read the schema from the visual-notes repo — vendor or pin it.)The schema already has
$idand rich descriptions, so it can carry the prose currently duplicated in the extraction prompt and the conformer comments.Benefits
_extractedBy/_schemaVersion/_pinnedsemantics centralized in one place.Acceptance
shared/schema.jsonis the only definition of the contract (zod inschema.tsis generated from it or replaced by ajv validation against it).shared/schema.json(preserving edge-endpoint referential check + strict top-level keys).shared/schema.json(no hand-coded rule constants).Notes
visual-notesskill). Centralization requires a way to share the schema across repos (vendored copy with a version pin, published@visual-notes/shared, or a sync step) — worth deciding as part of this.