Skip to content

Add per-field placement attribute (declaring_class / concrete_class)#56

Merged
stevevanhooser merged 2 commits into
mainfrom
claude/lucid-heisenberg-fSrVB
May 24, 2026
Merged

Add per-field placement attribute (declaring_class / concrete_class)#56
stevevanhooser merged 2 commits into
mainfrom
claude/lucid-heisenberg-fSrVB

Conversation

@stevevanhooser
Copy link
Copy Markdown
Contributor

Summary

Introduce a per-field placement attribute on field definitions, with values "declaring_class" (default; current behavior preserved) and "concrete_class" (route the field into the concrete subclass's body block on instance documents, only valid on fields declared by an abstract class). Apply it to V_delta calculator.input_parameters.

This captures intent that was always implicit in V_gamma's abstract-class design but was never articulated. The schema-author phrase "every calculator subclass instance must supply input_parameters" naturally describes a field that lives in the concrete subclass's block — exactly where did_v1 documents already store it (<calc_class>.input_parameters), and where every NDI calculator writer (ndi.element.addepoch, ndi.element.oneepoch, the mocks) puts it. The previous spec text mandated the field live in calculator.input_parameters (a parent-named block), which forced the migrator to perform a move and produced shape drift between the calc's in-memory output and the gate-normalized read-back of expected-doc JSON. That mismatch is the proximate cause of testCalcTuningCurve failing on the NDI-matlab claude/776-flip-did2-normalize-gate branch.

Design

placement is per-field, not per-class. Different fields on the same abstract class can choose differently. Allowed values:

placement value Where the field lives on a document instance
"declaring_class" (default) In the property block named after the declaring class — current V_gamma rule.
"concrete_class" In the property block named after the document's concrete class (document_class.class_name). The declaring abstract class does not host the field on the instance body.

Restrictions:

  • "concrete_class" is only valid when the declaring class has document_class.abstract: true. A concrete class with placement: "concrete_class" on one of its own fields is a schema error.
  • Field-name collisions on the concrete-class block (two ancestors place the same name, or a concrete class redeclares an ancestor-placed name) are detected at class-cache build time as a hard schema error. No silent precedence rule.
  • An abstract class whose declared fields are all "concrete_class"-placed contributes no property block on instance bodies.

Changes

V_gamma_SPEC.md (the authoritative spec; V_delta inherits):

  • "Abstract Classes": note the placement-driven body-block omission.
  • "Field Definition Object": add placement row to the field-definition key table; add new "Field placement (new in V_gamma)" subsection covering values, restrictions, multi-inheritance collision rules, and the effect on instance body shape.
  • "JSON Format: Document Instances": relax "exactly one block per chain class" to "one block per chain class that contributes a block"; update the "Provenance is structural", "Required vs. empty blocks", and "No cross-block field movement" rules to reflect placement-driven exceptions.

V_delta_SPEC.md: new differences-from-V_gamma section noting that V_delta is the first set to exercise placement, applied to calculator.input_parameters.

Schemas:

  • schemas/V_delta/stable/calculator.json: add "placement": "concrete_class" to the input_parameters field with documentation pointing at the spec section.
  • schemas/V_delta/stable/did_schema_meta.json and schemas/V_gamma/did_schema_meta.json: register placement as an allowed optional field-definition property (enum: ["declaring_class", "concrete_class"]). Required because additionalProperties: false on the field-definition object would otherwise reject the new key.

What this does not change

  • No DID-matlab implementation changes are in this PR. The corresponding calcCommon / ensureClassBlocks / class-cache updates will follow in a separate PR against vh-lab/did-matlab@claude/lucid-heisenberg-fSrVB, gated on this spec landing.
  • No V_gamma or V_delta concrete *_calc schemas change; simple_calc, tuningcurve_calc, etc. continue to declare their own non-inherited fields in the concrete class block. The input_parameters field is inherited from the abstract calculator parent and now lives in the same per-subclass block via placement routing, matching the on-disk layout of legacy did_v1 documents and removing the need for the calc migrator to perform a move.

Test plan

  • CI on this PR: schema set parses; meta-schema accepts placement on the calculator.input_parameters field and rejects it on a concrete-class field (if a meta-schema test fixture exercises that path).
  • After merge: follow-up DID-matlab PR (claude/lucid-heisenberg-fSrVB) lands the cache / migrator / ensureClassBlocks side; once that merges to V2, NDI-matlab testCalcTuningCurve on claude/776-flip-did2-normalize-gate goes green and we clean up the diagnostic dumps.

Background

  • NDI-matlab issue: [did2 #3] Route v1→V_delta normalization through ndi.database (not session/dataset) VH-Lab/NDI-matlab#776 (flip did2 read-normalization gate).
  • Diagnostic trace that surfaced this issue: the gate-flipped tuningcurve test fails because expected docs carry calculator.input_parameters.X (post-migrator) while actual in-memory calc output carries <class>.input_parameters.X, and the comparison config uses the latter path. Root cause is the spec/migrator combination forcing the move; correct semantics is to keep the field in the concrete subclass block where every legacy and live writer already places it.

Generated by Claude Code

claude added 2 commits May 23, 2026 20:02
Introduce a per-field `placement` attribute on field definition
objects, with allowed values `"declaring_class"` (default, current
behavior) and `"concrete_class"`. `placement: "concrete_class"` is
only valid on fields declared by an abstract class; it routes the
field into the concrete subclass's property block on instance bodies,
instead of materializing the abstract class as a body block.

This was always the intended semantics for abstract-class fields whose
"required-ness" really means "every concrete subclass must supply
this" -- the V_gamma SPEC text did not articulate the choice and so
defaulted everything to declaring-class placement. Captured now so
that:

- V_delta's abstract `calculator.input_parameters` can be marked
  `placement: "concrete_class"`, matching where the field already
  lives in did_v1 documents (`<calc_class>.input_parameters`) and on
  the schema-required-field reading "every calculator subclass
  instance must supply input_parameters."
- Future abstract-class fields can choose declaration-block or
  concrete-block placement on a per-field basis. Different fields on
  the same abstract class may pick differently.

V_gamma_SPEC.md changes:
- "Abstract Classes": note that abstract classes whose fields are all
  placed on the concrete subclass contribute no property block on
  instance bodies.
- "Field Definition Object": add `placement` to the field-definition
  table (optional, default `declaring_class`) and a new "Field
  placement" subsection covering the two allowed values, the
  abstract-only constraint, the effect on the abstract class's body
  block, and the collision rules.
- "JSON Format: Document Instances": relax "exactly one block per
  chain class" to "one block per chain class that contributes a
  block"; update "Provenance is structural", "Required vs. empty
  blocks", and "No cross-block field movement" rules to reflect the
  placement-driven exceptions.

V_delta_SPEC.md changes:
- New differences-from-V_gamma section noting that V_delta is the
  first set to exercise `placement`, applied to
  `calculator.input_parameters`.

Schema changes:
- `schemas/V_delta/stable/calculator.json`: add
  `"placement": "concrete_class"` to the `input_parameters` field,
  with documentation pointing at the spec section.
- `schemas/V_delta/stable/did_schema_meta.json` and
  `schemas/V_gamma/did_schema_meta.json`: register `placement` as an
  allowed optional field-definition property, with enum
  `["declaring_class", "concrete_class"]`. Required because
  `additionalProperties: false` on the field-definition object would
  otherwise reject the new key.

Collision rule (per V_gamma SPEC): a schema is invalid at class-cache
build time if two ancestors place the same field name into the
concrete-class block, or if a concrete subclass's own field name
collides with an ancestor's placement-routed name. Hard error, not
silent precedence.

No DID-matlab implementation changes are made here; this PR carries
the spec contract. The did-matlab `calcCommon` migrator and
`ensureClassBlocks` updates will follow in a separate PR against
DID-matlab `claude/lucid-heisenberg-fSrVB`, gated on this spec
landing.
Drop the "unless same placement and shape" carve-out from the
subclass-redeclaration collision rule. Same-name match on any class in
the chain (abstract or concrete, higher or lower) is now an
unconditional hard error once any ancestor has declared the field with
placement: concrete_class. No "verbatim restatement" exception.

Two reasons:
- The carve-out was hard to define precisely (what counts as "same
  shape" -- constraints object equality? enum ordering? type-coercion
  tolerance?) and invited the very accidental name-collision footgun
  the collision rules are meant to catch.
- Verbatim restatement of an inherited field has no functional value.
  V_gamma has no override mechanism today; "reaffirmation" is pure
  noise. Authors who want the inherited field simply inherit it.

V_gamma_SPEC.md: collapse the previous points 2 and 3 into a single
"No subclass redeclaration of a placed name" rule.

Meta-schemas: simplify the placement key description to match.
@stevevanhooser stevevanhooser merged commit a57017a into main May 24, 2026
4 checks passed
@stevevanhooser stevevanhooser deleted the claude/lucid-heisenberg-fSrVB branch May 24, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants