Skip to content

Extract sidecar schema to a shared JSON Schema (single source for plugin + agent conformer) #31

Description

@bobthearsonist

Problem

The sidecar contract is enforced in two hand-maintained implementations that can drift:

  • Plugin (renderer): plugins/obsidian-plugin/src/schema.tsparseSidecar() 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

  • shared/schema.json is the only definition of the contract (zod in schema.ts is generated from it or replaced by ajv validation against it).
  • Plugin validates sidecars against shared/schema.json (preserving edge-endpoint referential check + strict top-level keys).
  • Python conformer validates/coerces against the same shared/schema.json (no hand-coded rule constants).
  • A shared fixture set of valid/invalid sidecars that both sides agree on (same accept/reject verdicts).

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions