Add per-field placement attribute (declaring_class / concrete_class)#56
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduce a per-field
placementattribute 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 toV_deltacalculator.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 incalculator.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 oftestCalcTuningCurvefailing on the NDI-matlabclaude/776-flip-did2-normalize-gatebranch.Design
placementis per-field, not per-class. Different fields on the same abstract class can choose differently. Allowed values:placementvalue"declaring_class"(default)"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 hasdocument_class.abstract: true. A concrete class withplacement: "concrete_class"on one of its own fields is a schema error."concrete_class"-placed contributes no property block on instance bodies.Changes
V_gamma_SPEC.md (the authoritative spec; V_delta inherits):
placementrow 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.V_delta_SPEC.md: new differences-from-V_gamma section noting that V_delta is the first set to exercise
placement, applied tocalculator.input_parameters.Schemas:
schemas/V_delta/stable/calculator.json: add"placement": "concrete_class"to theinput_parametersfield with documentation pointing at the spec section.schemas/V_delta/stable/did_schema_meta.jsonandschemas/V_gamma/did_schema_meta.json: registerplacementas an allowed optional field-definition property (enum: ["declaring_class", "concrete_class"]). Required becauseadditionalProperties: falseon the field-definition object would otherwise reject the new key.What this does not change
calcCommon/ensureClassBlocks/ class-cache updates will follow in a separate PR againstvh-lab/did-matlab@claude/lucid-heisenberg-fSrVB, gated on this spec landing.*_calcschemas change;simple_calc,tuningcurve_calc, etc. continue to declare their own non-inherited fields in the concrete class block. Theinput_parametersfield is inherited from the abstractcalculatorparent 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
placementon thecalculator.input_parametersfield and rejects it on a concrete-class field (if a meta-schema test fixture exercises that path).claude/lucid-heisenberg-fSrVB) lands the cache / migrator / ensureClassBlocks side; once that merges toV2, NDI-matlabtestCalcTuningCurveonclaude/776-flip-did2-normalize-gategoes green and we clean up the diagnostic dumps.Background
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