diff --git a/src/did/+did2/+convert/+migrators_j/daqreader_epochdata_ingested.m b/src/did/+did2/+convert/+migrators_j/daqreader_epochdata_ingested.m index 1471c1ac..faf5ef6d 100644 --- a/src/did/+did2/+convert/+migrators_j/daqreader_epochdata_ingested.m +++ b/src/did/+did2/+convert/+migrators_j/daqreader_epochdata_ingested.m @@ -149,7 +149,14 @@ bodies = preBody; return; end -bodies = [{preBody}, refs]; +% the document is kept beside its clock references, so it is emitted +% complete: the superclass list of its class, from the schema +% (did2.build.superclasses; [early migration pieces fixed] (2), Jess Haley, +% 2026-10-26: "the builder produces a complete document"; before, +% v1_to_v2's ensureClassBlocks rewrote v1's list) +kept = preBody; +kept.document_class.superclasses = did2.build.superclasses('daqreader_epochdata_ingested'); +bodies = [{kept}, refs]; end % ===================== small helpers ======================================= diff --git a/src/did/+did2/+convert/+migrators_j/daqreader_image_epochdata_ingested.m b/src/did/+did2/+convert/+migrators_j/daqreader_image_epochdata_ingested.m index e8131c08..d2e9915d 100644 --- a/src/did/+did2/+convert/+migrators_j/daqreader_image_epochdata_ingested.m +++ b/src/did/+did2/+convert/+migrators_j/daqreader_image_epochdata_ingested.m @@ -343,7 +343,14 @@ % document rather than being deleted -- deciding it is redundant is the fold's % call to make, not this one's. -bodies = [{preBody}, refs]; +% the document is kept beside its clock references, so it is emitted +% complete: the superclass list of its class, from the schema +% (did2.build.superclasses; [early migration pieces fixed] (2), Jess Haley, +% 2026-10-26: "the builder produces a complete document"; before, +% v1_to_v2's ensureClassBlocks rewrote v1's list) +kept = preBody; +kept.document_class.superclasses = did2.build.superclasses('daqreader_image_epochdata_ingested'); +bodies = [{kept}, refs]; end % ===================== small helpers ======================================= diff --git a/src/did/+did2/+convert/+migrators_j/private/jDemoFold.m b/src/did/+did2/+convert/+migrators_j/private/jDemoFold.m index 24509ede..6025fa8a 100644 --- a/src/did/+did2/+convert/+migrators_j/private/jDemoFold.m +++ b/src/did/+did2/+convert/+migrators_j/private/jDemoFold.m @@ -144,6 +144,10 @@ % ---- the rename, and the consumed blocks ---- v2Body = preBody; v2Body.document_class.class_name = 'demo'; +% the superclass list of `demo`, from the schema (did2.build.superclasses), so +% the document is complete as emitted ([early migration pieces fixed] (2), +% Jess Haley, 2026-10-26; before, v1_to_v2's ensureClassBlocks rewrote it) +v2Body.document_class.superclasses = did2.build.superclasses('demo'); v2Body.demo = demoBlock; % `demo_ndi_mock` is the concrete block of the mock class and carries no fields; % it is listed so the removal does not depend on ensureClassBlocks' empty-block diff --git a/src/did/+did2/+convert/handOnSuperclasses.m b/src/did/+did2/+convert/handOnSuperclasses.m new file mode 100644 index 00000000..1fe4edf3 --- /dev/null +++ b/src/did/+did2/+convert/handOnSuperclasses.m @@ -0,0 +1,51 @@ +function body = handOnSuperclasses(body, schemaCacheOverride) +%HANDONSUPERCLASSES Write V2's superclass list on a document pass 1 hands on. +% +% BODY = did2.convert.handOnSuperclasses(BODY, SCHEMACACHE) (SCHEMACACHE [] +% for the shared cache). The one step on pass 1's hand-on path: a v1 +% document whose migrator hands it on unchanged to a later batch pass +% (did2.convert.v1_to_v2, HAND_ON_CLASSES: syncrule, epochfiles_ingested, +% stimulus_response_scalar, stimulus_response_scalar_parameters_basic, +% openminds) keeps every field as v1 wrote it; only its +% document_class.superclasses is rewritten from v1's form ({definition} +% paths) into V2's ({class_name, class_version}, did2.build.superclasses), +% and an EMPTY property block v1 wrote for a parent class that V2 hosts no +% block for (an abstract parent, e.g. `stimulus_response_scalar_parameters`) +% is dropped. A NON-empty such block is left for validation to report. +% +% Decided by Jess Haley, 2026-11-02 [handed-on classes carried at the end] +% (2) (did-schema docs/plans/did_v2_go_forward_class_audit.md; +% docs/V2_PLAN.md G98 (a)): "a small named step on the hand-on path writes +% V2's superclass list for the five classes pass 1 hands on unchanged ... +% and `ensureClassBlocks` is deleted". This is that step; it replaces what +% the padding did for these five (DID-matlab #364 measured: the superclass +% list, and one empty v1 parent block). +% +% Errors when the schema does not know the class (did2.schema.cache), so a +% hand-on class that lost its schema is a quarantine, not a silent pass. +% +% See also did2.build.superclasses, did2.convert.v1_to_v2. + +arguments + body (1,1) struct + schemaCacheOverride = [] +end + +cache = schemaCacheOverride; +if isempty(cache) + cache = did2.schema.cache.shared(); +end +className = char(body.document_class.class_name); +body.document_class.superclasses = did2.build.superclasses(className, ... + 'SchemaCache', cache); + +hosted = cache.resolvePlacement(className).blocksContributed; +chain = [reshape(cache.superclasses(className), 1, []), {className}]; +for cls = setdiff(chain, hosted) + name = cls{1}; + if isfield(body, name) && isstruct(body.(name)) ... + && (isempty(body.(name)) || isempty(fieldnames(body.(name)))) + body = rmfield(body, name); + end +end +end diff --git a/src/did/+did2/+convert/resolveValidIntervals.m b/src/did/+did2/+convert/resolveValidIntervals.m index 2d53b34b..0b3fa99c 100644 --- a/src/did/+did2/+convert/resolveValidIntervals.m +++ b/src/did/+did2/+convert/resolveValidIntervals.m @@ -996,9 +996,14 @@ if ~isempty(anchor.tolerance_seconds) % Written ONLY when the source clock stated a tolerance. An empty tolerance % cell would assert "the timeline is good to 0 s", which the source never - % said; with no block, ensureClassBlocks pads it. + % said; with none, the block is written empty here, so the document is + % complete as built ([early migration pieces fixed] (2), Jess Haley, + % 2026-10-26: "the builder produces a complete document"; before, + % ensureClassBlocks added it). ref.time_reference = struct('clock_tolerance', ... durationCell(anchor.tolerance_seconds)); +else + ref.time_reference = struct(); end value = struct('clock', struct('node', '', 'name', anchor.clock), ... 'start', durationCell(startSeconds)); diff --git a/src/did/+did2/+convert/v1_to_v2.m b/src/did/+did2/+convert/v1_to_v2.m index 8160521c..4edb37bf 100644 --- a/src/did/+did2/+convert/v1_to_v2.m +++ b/src/did/+did2/+convert/v1_to_v2.m @@ -176,6 +176,21 @@ % 2026-11-01 [last rename-pass classes]). PARENT_ONLY_CLASSES = {'app', 'calculator', 'epochid', 'stimulus_response', ... 'stimulus_response_scalar_parameters'}; +% v1 classes whose migrator HANDS ON a document unchanged to a later batch +% pass (resolveClockAlignment: syncrule; epochMint / resolveEpochProbemap: +% epochfiles_ingested; resolveResponseParameters: stimulus_response_scalar and +% its parameters; resolveOpenmindsCitations: openminds). Such a document gets +% V2's superclass list from its one named step, +% did2.convert.handOnSuperclasses (Decided by Jess Haley, 2026-11-02 +% [handed-on classes carried at the end] (2), which names those five, the +% ones the corpora hold). daqreader_epochdata_ingested and +% daqreader_image_epochdata_ingested are handed on the same way (to epochMint) +% when their source names no epoch document; no corpus holds one, and the +% padding probe on the unit fixtures found them (DID-matlab +% claude/W-padding-probe), so they take the same step. +HAND_ON_CLASSES = {'syncrule', 'epochfiles_ingested', 'stimulus_response_scalar', ... + 'stimulus_response_scalar_parameters_basic', 'openminds', ... + 'daqreader_epochdata_ingested', 'daqreader_image_epochdata_ingested'}; % a migrator transcribes the v1 source's own terms, which the term % registry (strictMode 'TermRegistry', tenet T19) need not hold yet: the check @@ -301,6 +316,8 @@ % yet produce a complete document. Denominator: the converted bodies padded. paddedInspected = 0; paddedNames = {}; +handOnNames = {}; +handOnValues = []; paddedValues = []; % DIMENSION TERMS WITH NO NODE (Jess Haley 2026-10-22 [covariate details], % answer "B" to 5.4): a dimension's term is bound `preferred` for now, and @@ -709,9 +726,18 @@ outBody.document_class.schema_version = 'did_v1'; end else - outBody = did2.convert.ensureClassBlocks(v2Bodies{bi}, options.SchemaCache); + outBody = v2Bodies{bi}; + if passedThrough && any(strcmp(className, HAND_ON_CLASSES)) + % the hand-on step ([handed-on classes carried at the + % end] (2)): V2's superclass list, nothing else + outBody = did2.convert.handOnSuperclasses(outBody, options.SchemaCache); + [handOnNames, handOnValues] = bumpClassCounter( ... + handOnNames, handOnValues, className); + end + beforePadding = outBody; + outBody = did2.convert.ensureClassBlocks(outBody, options.SchemaCache); paddedInspected = paddedInspected + 1; - if ~isequaln(outBody, v2Bodies{bi}) + if ~isequaln(outBody, beforePadding) [paddedNames, paddedValues] = bumpClassCounter( ... paddedNames, paddedValues, className); end @@ -925,6 +951,8 @@ 'carried_failing_veta_validation_by_class', buildByClassTable(carriedUnvalNames, carriedUnvalValues), ... 'legacy_ndi_document', legacy, ... 'carried_v1_template_check', carriedTemplateCheck, ... + 'hand_on_superclasses', struct('bodies_written', sum(handOnValues), ... + 'by_class', buildByClassTable(handOnNames, handOnValues)), ... 'class_blocks_padding', struct('bodies_inspected', paddedInspected, ... 'bodies_changed', sum(paddedValues), ... 'changed_by_class', buildByClassTable(paddedNames, paddedValues)), ...