Read-only audit of #2573 at
271290115, #2986 at1c6799b00, #2987 atefd4b96ba, #2629 atd761ca442and #2812 at1c3869224. Saved verbatim as evidence for the harvest map. Feature implementation is on hold; nothing was ported, rebased or closed.
Harvest audit B: QM v2 PR-B services and the schema, profile and validation set¶
Read-only audit, 5 October 2026. Specifications read on the package in worktree pr4061 (RD, SP,
BC, contracts.md C4, C5, C14, versioning-model.md §3.4 to §3.6, §8, §12.6, consistency-model.md
§7.2 and §7.4, the tracker, the acceptance criteria, decision-register.md §5 and g0-dossier.md).
main means origin/main at 7673ed0. Nothing was built, run, ported or changed, so PR-B's
"13 / 13 pass" claim was not re-run.
Path abbreviations: PMC = src/libs/project-management/SyRF.ProjectManagement.Core, PMT =
src/libs/project-management/SyRF.ProjectManagement.Core.Tests. Short SHAs: B 2712901, PR-A
squash 3619043, #2986 1c6799b, #2987 efd4b96, #2629 d761ca4, #2812 1c38692, main 7673ed0.
1. Scope and state¶
1.1 What each PR contains¶
| PR | Base and relation | Own work (generated files excluded) | Conflicts and drift with main |
|---|---|---|---|
#2573 PR-B (2712901) |
One commit on PR-A's squash 36190433d, which sits on main f16b0a6a9 (17 April 2026, 798 first-parent commits behind main). B never merged main. |
8 files, +1,694/−13, no generated code. Production code (about 600 lines): AnnotationImpactService (246), SessionTransitionService (+359 over A's stub), StageTransitionJob aggregate (268), StageTransitionExceptions (60), IStageImpactReader and IStageTransitionJobRepository (28). Tests: 733 lines, 13 tests (8 impact, 5 transition). No tests cover StageTransitionJob or the exceptions. |
All 8 paths are absent on main, as are the AnnotationAggregate and session types B builds on, so B inherits PR-A's conflict. A's tip has 45 non-merge and 7 merge commits after the squash, and the four model folders B calls changed by +571/−340, although the methods B calls survive. StageTransitionWorker was deferred to PR-C and is not in B. |
#2986 (1c6799b) |
One commit on main f546357f8 (30 August 2026). |
8 files, +1,306. A AnnotationAnswerConformanceValidator (419 lines), API ProblemDetails (43), a malformed lookup ID rule in AnnotationRelationshipValidator (+8). Tests: 15 xUnit methods; one shared 11-case JSON fixture run by .NET, AF1 and AF2. |
None of its files changed on main since its base. It applies cleanly (GitHub says mergeable, blocked by review rules). It is not wired into any write path (§3). |
#2987 (efd4b96) |
One commit on f546357f8. |
One ADR: docs/decisions/ADR-016-annotation-schema-profile-boundary.md (258 lines). |
ADR-016 is already taken on main (preview TLS). |
#2629 (d761ca4) |
One commit on 568100fda (4 May 2026). |
One Draft feature doc, configurable-validation-strategy.md (320 lines). |
No file conflict. The text proposes "ADR-011 Project Template", and main's ADR-011 covers multi-option conditional parents. #2987 retires this proposal. |
#2812 (1c38692) |
Five commits plus a merge of main on 27 August 2026 (merge base 0c95c711b). |
27 authored files, +997/−82: ADR-017 (444 lines), an extensibility-doc rewrite (+61/−53), and inert flag plumbing for annotationResponseModesAndMetadata (env-mapping.yaml, both generators, FeatureFlags.cs, SyrfConfigureServices +51, six Helm chart tests, appsettings, values). It also touches 12 generated files (+115/−80). |
Dirty. 28 of its 39 paths changed on main since its base: env-mapping.yaml in 139 commits, the generated flag files in more than 60 each, docs/decisions/index.md in 18, the extensibility doc in 19. ADR-017 is already taken on main (bulk PDF notifier). |
1.2 What PR-B does¶
AnnotationImpactServicecounts annotations per question for one stage: the studies, the annotators, the sessions split into completed and incomplete, and the top 20 answer values. It also computes a SHA-256 fingerprint of the summary.SessionTransitionService.PlanStageTransitionruns when a stage's question set moves from one version to the next.- It skips completed sessions ("pinned").
- It rewrites every incomplete session in the stage, applying each question's
Keep/Map/ReAnswerdecision. - It drops annotations on removed questions from the new session version.
- It creates a replacement annotation when the decision targets a different question, and repoints outcome data to it.
- It appends one session version per session (trigger
AdminPublish). StageTransitionJobis a leased, retried, durable job keyed by(project, stage), and it snapshots the decisions.
1.3 The transition model against SP and RD¶
Under SP and RD, a stage owns no evidence, target or session (SP §3.2, RD-R04, RD-R31). Sessions are per Study × form and shared across stages, and question changes reach sessions only through a form publication (RD §4.8). So B's stage transition has no direct successor. Its parts split as follows.
| PR-B part | Fate | Where it goes |
|---|---|---|
| Per-stage question-set versions (SQS, PQS) driving session rewrites | Superseded by form versions that stages bind (RD-R31). The stage filter decides only which Studies are in a pool. | RD §3.5, C4, C6 |
StageTransitionJob (stage-keyed batch job) |
Superseded by two operations of the shared operation family (consistency §7.2): FormVersionIssue phase 2 for question changes (RD §4.8), and SweepStagePoolHistory for filter changes (SP §4.1). The filter sweep writes pool events only and never touches sessions. |
consistency §7.2, §7.4; SP §4.1 |
| Blocking review and export during a transition | Superseded. Reviewers keep saving, and only admission of new Studies and readiness pause for the form (RD-R35). Exports read pinned versions (C11). A filter change takes effect for selection immediately (SP §4.1). | RD §4.8 step 4; SP §4.1 |
| "Transition in progress" refusal | Kept as a concept. It becomes a typed PublicationInProgress per form (D2-11) and the stage settings fence (consistency §7.6). |
AC-R2c-14 |
| One session version per session per publication, combining every question's treatment, attributed to the publishing admin | Fits RD. The D2-01 reversal brought this back. Trigger AdminPublish maps to kind PublicationGenerated. |
RD §3.15, C5 |
Decision vocabulary (ChangeImpactClassification, Keep/Map/ReAnswer, mappings, note, decider and time) |
Fits in part, renamed and re-scoped. The classification becomes the publisher's immutable compatibility declaration on the question version. Keep becomes autoUpdate (same class and valid) or doNothing. Map becomes the mapped treatment, keyed by option ID. ReAnswer becomes requireReanswer, which keeps the pins and marks the answer Needs updating. Missing today: added and removed treatments, the per-category scope and the counting choice. |
versioning §8.2, §8.3; RD-R20, RD-R21 |
| Completed sessions always pinned; incomplete sessions always rewritten | Superseded by the generation table. Completed sessions may receive an incomplete generated version (requireReanswer, requireAnswerBeforeCounting). Sessions generate nothing under doNothing or countEarlierCompletes, or outside the chosen categories. A draft-only session is rebased and never versioned. |
RD §3.15 |
| Impact counts and fingerprint | Fit the Q-20 preview and its digest, once re-keyed to the form, to the three categories, to the prior version and to the route. | RD §4.8 steps 1 and 3 |
| Replacement across question identities | No counterpart. A data-type or multiplicity change is an incompatible version of the same question (D2-03, RD-R30), and nothing is carried across identities (versioning §3.1). | §12.6 already avoids ReplacementDraftLineagePlanner |
| Outdated-answer modes (D4-17) | Absent from B. Under the target model, outdatedAnswerCompletion lives in the form version that a generated version declares (RS-R39 to RS-R42, C5). |
T-RS-05 |
1.4 G0-D6 re-assessed (#2986)¶
The dossier keeps #2986 open outside the programme because "it fixes today's legacy writes, which R2a will not cover". Re-assessed against current evidence:
- The PR does not yet fix any write.
- There is no caller of
AnnotationAnswerConformanceValidator.Validateoutside tests at1c6799b. The PR's own checklist leaves "integrate after #2467" and "map the typed error to HTTP 400" unticked. -
2467 merged on 30 August 2026, so the blocker has gone.¶
- On main,
ReviewSubmissionService.AddSessionDataruns only the relationship validator, andAddAnnotationsruns no answer validation at all (PMC/Services/ReviewSubmissionService.cs:21-52@7673ed0). - Legacy writes still last the whole programme.
- R7 is the last milestone: after GA and the R6 waves, in window W8.
- Retirement has "no arbitrary date" (BC-R37).
- "For years" is weaker, but legacy writes do not end soon.
- A new reason has appeared. BC treats "an option value with no option" as an unmappable value, which gets a manifest disposition or quarantine (BC §8 failure table). E37's R6 part needs a manifest of unmatched values. Every non-conformant legacy write between now and R6 becomes conversion work, so the guard reduces BC findings. Its rules also make a ready inventory detector (H-VAL-02).
- R2a does not replace it. Canonical validation is keyed by option ID, runs under the pinned version and uses E23 applicability. #2986 validates by value against the current definition, with grandfathering, so only its fixtures and error vocabulary carry over to R2a.
Recommendation. Confirm the G0-D6 deviation, with the rationale corrected to items 1 to 4. After the hold lifts, finish #2986 as a small legacy-integrity PR: - wire it into both submission paths; - return every error, not only the first; - add endpoint tests.
Harvest its fixtures into E23 and its rules into the T-BC-01 inventory. This needs an owner ruling (§7).
2. Harvest entries¶
| ID | PR | Component (type or file at SHA) | What it does | Verdict | Why (rule IDs, decisions) | Target (spec §, contract, release, tracker row) | Adaptation needed | Tests to carry | Effort | Evidence (path:line @ short SHA) |
|---|---|---|---|---|---|---|---|---|---|---|
| H-SVC-01 | #2573 | AnnotationImpactService.ComputeStageImpact (extracted overload) and IStageImpactReader |
Counts annotations, studies, annotators and completed or incomplete sessions per question | Adapt | Q-20; RD-R22; FV2; QM-13 is retained and extended to every prior version, the three categories and cross-stage use (decision register §5) | RD §4.8 step 1; C4 publication preconditions; C8; F2 and R2c; T-RD-04, T-RD-05 |
Key by form, not stage. Count completed, saved_incomplete and draft_only (drafts from pmSessionDraft), by prior form version and by route. Read an authoritative pinned snapshot through the C8 boundary, not in-memory lists. Use a set for question membership; the current check is O(a×q). Drop the legacy embedded-annotation overload. |
8 impact tests as reference cases, re-keyed to the form | M | PMC/Services/AnnotationImpactService.cs:23-88 @ 2712901; PMC/Interfaces/IStageImpactReader.cs:8-11 @ 2712901 |
| H-SVC-02 | #2573 | StageImpactFingerprint.Compute |
Builds a SHA-256 digest of the summary for change detection | Adapt | RD §4.8 step 3 (PreviewDigestChanged); AC-R2c-06 |
RD §4.8; C4 publication; F2; T-RD-04 |
Digest identities and heads (session ID, head sequence, pinned form version), the draft-only count, the draft revision of the treatments and the usage-evidence identity. Counts alone miss compensating changes. Use a length-prefixed or canonical JSON encoding; today raw answer strings are joined with | and :. Leave answer values out. |
New tests; keep the order-insensitivity idea | S | PMC/Services/AnnotationImpactService.cs:206-245 @ 2712901 |
| H-SVC-03 | #2573 | Top-20 answer distribution per question | Shows how existing answers spread across values | Reference only | Q-34; RD §4.8 step 2 (per-option mapping choice) | Mapping step of the impact dialog; T-UX-06, T-RD-05 |
Count per option ID, show retired options only, and show it only to publishers. Not a code port. | None | S | PMC/Services/AnnotationImpactService.cs:153-162 @ 2712901 |
| H-SVC-04 | #2573 | SessionTransitionService.PlanStageTransition |
Plans one session version per session, applying the per-question decisions | Adapt (rewrite, using the logic as reference) | D2-01 amended; RD-R23; RD §3.15; C5 PublicationGenerated; consistency §7.4 |
RD §3.15, §4.8 phase 2; C4 publication command; C5; F2 and R2c; T-RD-04, T-RD-05 |
Keep: one combined version per session per generation; attribution to the publisher; removed questions leave the new version while history stays. Change: sessions keyed by form and shared across stages; the generation table per category; only affected sessions; every prior version handled; head compare-and-set against newer reviewer versions; deterministic IDs per (sessionId, operationId, generation); no invalid Complete; the reviewer stays the effective author; provenance {operationId, generation}; a pure planner that does not mutate loaded aggregates. |
5 transition tests as scenario references for RD-AE16 and FX-VM-46. Rewrite the mapping fixture: Mouse to Rat is a meaning change that Q-34 forbids. |
L | PMC/Services/SessionTransitionService.cs:36-186 @ 2712901 |
| H-SVC-05 | #2573 | Decision vocabulary as B consumes it (ChangeImpactClassification, AnswerHandlingStrategy, OptionMapping; StageTransitionQuestionDecision snapshot) |
Records a per-question admin decision with decider and time | Adapt | Versioning §8.2 and §8.3; D2-02 amended; RD-R20, RD-R21; §12.6 avoids AQVersion.PublishDecisions placement |
IssuePolicyRecord per question {changeClass, treatment, mappingRef} embedded in FormVersionIssue; compatibility on the question version; T-RD-04 |
Map Keep, Map and ReAnswer as in §1.3. Add added and removed, the per-category applies and countingChoice, and the rationale (SR-16). Drop the source and target question split. The classification moves to an immutable publisher declaration. B never reads Classification. |
New contract fixtures (C4-T13r) | M | PMC/Model/QuestionVersioning/VersioningValueObjects.cs:101-160 @ 3619043; PMC/Model/StageTransitionJobAggregate/StageTransitionJob.cs:259-268 @ 2712901 |
| H-SVC-06 | #2573 | Option mapping as executed (AnnotationAnswer.ApplyTransition, MapValue) |
Rewrites string answers by value | Avoid | RD-R21; Q-34 (explicit per option, meaning unchanged, by option ID, originals kept); C4 Options | n/a | It maps by value string. It accepts many-to-one mappings. Map with no mappings silently copies. A null answer becomes "". The classification is never checked. |
Do not carry the mapping test | — | PMC/Model/AnnotationAggregate/AnnotationAnswer.cs:46-85 @ 3619043; PMT/QuestionVersioning/SessionTransitionServiceTests.cs:91 @ 2712901 |
| H-SVC-07 | #2573 | Clearing on ReAnswer (answer and notes nulled in a new revision) |
Wipes the visible answer | Avoid | RD §3.15 (requireReanswer keeps the pins and shows Needs updating); VU1; SF6 |
n/a | Replace with a generated incomplete version that keeps the pins | — | — | PMC/Services/SessionTransitionService.cs:199-204 @ 2712901 |
| H-SVC-08 | #2573 | Replacement across question identities (CreatePublishTransitionReplacement, ResolveTargetParentAnnotationId, outcome-data repointing) |
Moves answers to a different question and re-parents them | Avoid | D2-03 and RD-R30 (same identity, incompatible version); versioning §3.1 ("nothing carried across"); §12.6 avoid list | n/a | None. Heads keyed by context (C1, C2) make repointing unnecessary. | Reference only: the sibling-order case, because C5 keeps entity order in a separate record | — | PMC/Services/SessionTransitionService.cs:223-294, :296-335 @ 2712901 |
| H-SVC-09 | #2573 | SessionTransitionService.PromoteDecision |
Promotes a mutable draft decision to an immutable one, stamped with decider and time | Reference only | RD §4.7, §4.8 steps 2 and 3 (treatments are attributed design-draft changes, committed in phase 1) | T-RD-04 |
Trivial and already implied by DesignDraftChange. Not worth porting. |
1 test (reference) | S | PMC/Services/SessionTransitionService.cs:22-34 @ 2712901 |
| H-SVC-10 | #2573 | StageTransitionSummary and the job counters |
Lists added and removed question IDs and counts transitioned, pinned and unchanged sessions and removed annotations | Reference only | Versioning §8.6 impact manifest; RD §4.8 step 5 | Impact manifest per-session categories and per-question states; T-RD-05 |
Use the manifest vocabulary (carriedForward, needsUpdatingVersion, mappedByPolicy, …). FrozenStudyCount and ProcessedSessions are never set by the planner. |
None | S | PMC/Services/SessionTransitionService.cs:366-381 @ 2712901 |
| H-SVC-11 | #2573 | StageTransitionJob aggregate and IStageTransitionJobRepository |
Claim, lease, heartbeat, retry and completion for a stage job | Avoid (as code) | Consistency §7.2: generation-fenced takeover, a stalled state and a stable cursor (VB-06f); superseded by main's ADR-020 operation family |
n/a (successor: pmCanonicalOperation of kind publication phase 2; T-RD-04) |
None. Use the main patterns cited in §4. Defects B-D2 to B-D4. | None (no tests exist) | — | PMC/Model/StageTransitionJobAggregate/StageTransitionJob.cs:10-241 @ 2712901; PMC/Interfaces/IStageTransitionJobRepository.cs:9-17 @ 2712901 |
| H-SVC-12 | #2573 | StageTransitionInProgressException |
Typed refusal of a second concurrent publish | Reference only | D2-11; AC-R2c-14 | The typed PublicationInProgress in the C18 error catalogue; T-RD-04 |
Key by form; carry the running operation and its progress (RD §8) | New | S | PMC/Services/StageTransitionExceptions.cs:6-22 @ 2712901 |
| H-SVC-13 | #2573 | ReviewStageTransitionBlockedException, DataExportStageTransitionBlockedException |
Locks a study's review, and current-state export, while a transition runs | Avoid | RD-R35 and consistency §7.4 (reviewers keep saving; only admission and readiness pause); C11 pinned exports | n/a | None | — | — | PMC/Services/StageTransitionExceptions.cs:24-60 @ 2712901 |
| H-VAL-01 | #2986 | The PR as a legacy-write integrity guard (AnnotationAnswerConformanceValidator with grandfathering) |
Checks submission boundary, question membership, persisted subtype and shape, authored option membership and conditional option availability (ADR-011 multi-option parents) | Reuse (outside the programme, per G0-D6) | G0-D6; BC §8 (unmappable values); plan principle 17 (legacy writers retire at R7) | Normal triage, not a programme row | Wire it into AddSessionData and AddAnnotations, map to HTTP 400 returning every error, and add endpoint and service tests |
All 15 xUnit tests, the AF1 and AF2 fixture spec and the 2 API test files | M | PMC/Services/Validation/AnnotationAnswerConformanceValidator.cs:104-397 @ 1c6799b |
| H-VAL-02 | #2986 | The same rules as a conversion-inventory detector | Finds option values with no option, type or shape mismatches and questions not in the stage | Adapt | BC §3.3 inventory; BC §8 unmappable values; E37 (R6 manifest of unmatched values) | BC §3.3; T-BC-01; E37 at R6 |
A read-only scan over all historical answers, with no grandfathering, that reports counts per rule code into the inventory and manifest | Reuse the fixture cases as detector tests | S–M | Same file, :142-266 @ 1c6799b |
| H-VAL-03 | #2986 | Shared fixture annotation-answer-conformance-v1.json and its three runners (.NET, AF1, AF2) |
One semantic corpus checked in backend and both forms | Adapt | E23 (shared fixtures run by .NET and AF2); AC-R2a-03 (FX-APPLIC) | E23 at F1a; R2a; T-RD-01, T-RD-02 |
Re-key to option IDs and pinned question versions. Add applicability, response-mode and metadata cases. Keep the three-runner pattern, with AF1 runners only for legacy parity. | 11 cases, annotation-answer-conformance.fixture.spec.ts |
S | src/services/web/src/app/shared/annotation/testing/annotation-answer-conformance-v1.json @ 1c6799b |
| H-VAL-04 | #2986 | Rule codes and AnnotationAnswerConformanceProblemDetails (urn:syrf:problem:…, ruleCode, questionId, expected, actual) |
Stable, field-addressable 400 errors | Adapt | C4 E23 ("typed errors name the blocking question and context"); RD §4.4 ValidationFailed; the VB typed-error catalogue |
C18 and E23 error catalogue; T-RD-01, T-RD-02 |
Return every error, not only Errors[0]. Add the context key (C2) and the question version. Keep the codes stable. |
1 ProblemDetails test | S | PMC/Services/Validation/AnnotationAnswerConformanceValidator.cs:10-37 and src/services/api/SyRF.API.Endpoint/Models/AnnotationAnswerConformanceProblemDetails.cs:8-42 @ 1c6799b |
| H-VAL-05 | #2986 | Validation against the current definition, with grandfathering of unchanged answers | Lets old invalid answers stand and validates edits against today's definition | Avoid (for canonical paths) | Versioning §3.6 (validity under the declared, pinned version); RD-R07; #2987 ADR §4 ("LatestVersion" prohibited) | n/a (kept only inside H-VAL-01) | — | — | — | Same file, :379-397 @ 1c6799b |
| H-VAL-06 | #2986 | LookupTargetInvalidId rule |
Rejects malformed lookup GUIDs instead of skipping them | Reuse (outside the programme, with H-VAL-01) | Integrity of legacy writes | Normal triage | None | 1 test | S | PMC/Services/Validation/AnnotationRelationshipValidator.cs:248-259 @ 1c6799b |
| H-VAL-07 | #2987 | ADR-016 §1, §3: server authority and five semantic categories (value kind, control, response mode, metadata, reference response); value XOR mode; suppression derived, never persisted | Separates concepts the legacy model conflates | Adapt | C4 Options and response modes; versioning §3.4; E37 payload contract; DD-12 | The F1a C4 ADR (a number from the programme block ADR-030 to 069, DOM §11.8); T-RD-01 |
Map onto question-version content. Resolve one conflict in E37: #2987 allows a metadata-only response, while C4 and §3.4 say "value XOR responseModeId plus metadata". A reference response belongs to C1 or C13 entity references, not options. | None (docs) | S | docs/decisions/ADR-016-annotation-schema-profile-boundary.md:57-114 @ efd4b96 |
| H-VAL-08 | #2987 | ADR-016 §4, §5: exact session → version chain; LatestVersion prohibited; lifecycle Draft, Published, In use, Retired |
Version-pinned validation and lifecycle | Reference only | C4 rules (QD1: published, never deleted, retire); RD §3.9 full pin map; already covered by the contracts | C4, C5 citations; T-RD-01 |
None beyond citing it as provenance | — | S | Same file, :116-150 @ efd4b96 |
| H-VAL-09 | #2987 | AnnotationSchema and AnnotationProfile split, the per-project and per-stage profile binding, delivery-map phases 2 to 8 |
A future aggregate boundary | Avoid | "Profile" collides with ScreeningProfile (RD §3.6); a stage owns no form settings (SP §3.2, RD §3.5); superseded by QuestionDefinitionVersion + FormVersion and the R2a, R2c releases |
n/a | — | — | — | Same file, :77-98, :195-217 @ efd4b96 |
| H-VAL-10 | #2629 | FEAT-027 problem statements: logic drift, the version-compatibility gap, hard-coded categories, system questions and lookup taxonomy | Problem framing | Reference only | E23 (shared fixtures, not a runtime rule payload); versioning §3.6; DD-12 entity-type IDs; E24; ACD catalogue | E23, E24 at F1a; T-RD-01, T-AC-04 |
None | — | S | docs/features/annotation-questions/configurable-validation-strategy.md:119-266 @ d761ca4 |
| H-VAL-11 | #2629 | FEAT-027 runtime design: boot-time YAML rule endpoint, NgRx rule cache, ProjectTemplate ADR-011, OptionValueAgainstQuestionVersion tier |
A proposed architecture | Avoid | Retired by #2987 ("Retired assumptions"); E23; ADR-011 is taken on main | n/a | — | — | — | Same file, :286-298 @ d761ca4 |
| H-VAL-12 | #2812 | ADR-017 response contract | Value XOR responseModeId; metadata scoped to the value or the mode; stable IDs and keys separate from labels; no global N/A enum; suppressDescendants keyed by ancestor instance; requiresReason; presence-sensitive update semantics (omitted, null, array) |
Adapt | C4 response modes; versioning §3.4; UA1 ("Not applicable" is an explicit mode); E37; C14 (unit and metadata types) | F1a C4 ADR and E37 (T-RD-01); unit and metadata field types into the C14 ADR at F-O |
definitionVersion becomes the revision's questionVersionRef. expectedDefinitionVersion becomes the session's declared form version plus StaleBase. Drop schema-v0 activation. Align metadata units with C14's unit vocabulary. |
Payload validation cases into H-VAL-03's corpus | M | docs/decisions/ADR-017-annotation-response-modes-and-metadata-contract.md:28-192, :252-273 @ 1c38692 |
| H-VAL-13 | #2812 | Freeze-on-first-use (Chris, 18 August 2026) and its atomic first-use lock | Makes used definitions immutable | Reference only | Versioning §3.4 adopts frozen versions (PH-06); C4 immutable versions | Provenance for C4 and versioning §3.4 | None. Main's Approved extensibility doc still says the choice is "not decided" (a docs follow-up, §4). | — | S | Same file, :209-250, :341-345, :374-417 @ 1c38692 |
| H-VAL-14 | #2812 | Schema-v0 activation machinery: live-definition counter, per-stage one-way AF2 lock, the three-service flag annotationResponseModesAndMetadata, chart and appsettings plumbing, generator scan of every section |
Gates response modes on legacy projects | Avoid | Response modes are canonical version content (C4, §3.4); canonical routes render on AF2 only (VB-08); a stage owns no settings (SP §3.2); legacy writers retire at R7; main's per-flag services: key supersedes the scan |
n/a | — | — | — | Same file, :275-308, :349-355 @ 1c38692 |
| H-VAL-15 | #2812 | Fail-closed boolean flag parsing (typeof parsed === 'boolean' in the generated parseBoolean; .NET JSON-boolean overlay) |
Treats a non-boolean flag value as the default instead of truthy | Reuse (outside the programme; platform hygiene) | Main's generator still returns JSON.parse(value), so "1" is truthy |
A separate small fix PR, not a programme row | Port the TypeScript change to main's generator; add a .NET test | The flag spec and .NET flag tests, generalised | S | src/services/web/scripts/generate-feature-flags.ts diff @ 1c38692; main src/services/web/scripts/generate-feature-flags.ts:389-398 @ 7673ed0 |
Count by verdict: 3 Reuse, 9 Adapt, 7 Reference only, 9 Avoid (28 entries).
Proposed additions to versioning-model §12.6.
- Add to the harvest column:
- the impact counts and the preview digest (H-SVC-01, H-SVC-02);
- the publication-generated planner shape: one combined version per session per generation
(H-SVC-04);
- #2986's fixture corpus and rule codes as E23 and E37 seeds (H-VAL-02 to H-VAL-04);
- #2812's response wire contract (H-VAL-12).
- Add to the avoid column:
- stage-keyed transitions and StageTransitionJob;
- review and export locks during publication;
- answer clearing on re-answer;
- value-based mapping;
- replacement across question identities.
- Correct one omission. The round-2 VB review (improvement 8, reviews/round-2/review-VB-…md:115)
harvested "the StageTransitionWorker pattern of lease plus batch writes plus a job CAS", but
that worker is in PR-C, not PR-B. Main's ADR-020 operation family has since
superseded the pattern, so §12.6 should not revive it.
3. Defects not to carry over¶
| ID | Status | Defect | Evidence |
|---|---|---|---|
| B-D1 | Confirmed, and wider than stated | SessionTransitionService never converts answers to the target type or shape. Keep and Map copy the source payload, and ReAnswer clears it while keeping the source type. AppendVersion then calls EnsureCompatible, which throws on any data-type or multiplicity change, so B cannot plan the D2-03 case at all. The throw comes mid-loop, after earlier sessions and annotations were already mutated in memory, so the "pure domain service" is neither pure nor atomic. |
PMC/Services/SessionTransitionService.cs:72-147, 199-221 @ 2712901; PMC/Model/AnnotationAggregate/Annotation.cs:286, 418 and AnnotationAnswer.cs:28-57 @ 3619043 |
| B-D2 | Confirmed | StageTransitionJob has no failure or stalled state (Queued, InProgress, Completed only). MarkQueuedRetry re-queues forever, and ClaimCount is never bounded. |
StageTransitionJob.cs:10-15, 197-204 @ 2712901 |
| B-D3 | New | Zombie writer. MarkBatchProcessed, MarkQueuedRetry and MarkCompleted take no lease owner or generation, so a worker whose lease expired can advance progress or complete the job after a takeover. MarkCompleted is allowed from Queued, and ProcessedSessions += count is not idempotent. |
StageTransitionJob.cs:178-215 @ 2712901 |
| B-D4 | New | The cursor is LastProcessedSessionId, a random GUID. This is the "Id > last over random GUIDs" pattern that consistency §7.2 forbids (VB-06f). Status is stored as an ordinal enum (VB improvement 9). |
StageTransitionJob.cs:46, 185 @ 2712901 |
| B-D5 | New | Over-generation. Any change gives every incomplete session in the stage a new version, even with no affected answer, so UnchangedSessions is always 0 on the change path. RD versions affected sessions only. |
SessionTransitionService.cs:135-149 @ 2712901 |
| B-D6 | New | Blind to prior versions. The planner never compares a session's pinned SQS with the request's previous SQS, so a session still on v1 receives the v2 → v3 decisions. C4 requires sessions under every prior version (AC-R2c-01). | SessionTransitionService.cs:72-147 @ 2712901 |
| B-D7 | New | No head check and no deterministic IDs. Replacement IDs come from Guid.NewGuid(), and no versionId is passed to AppendPublishTransitionRevision. A replay duplicates versions, and a newer reviewer version can be overwritten (RD-R23). |
SessionTransitionService.cs:85-90, 135-143 @ 2712901; PMC/Model/AnnotationSessionAggregate/AnnotationSession.cs:239-269 @ 3619043 |
| B-D8 | New | Unchecked decisions. Classification is never read. Map with no mappings silently copies. MapValue turns null into "". Many-to-one mappings are accepted. The test fixture maps Mouse to Rat, a meaning change. |
AnnotationAnswer.cs:59-85 @ 3619043; SessionTransitionServiceTests.cs:39, 91 @ 2712901 |
| B-D9 | New | Lost authorship. The publishing admin becomes CreatedBy of the reviewer's answer revisions; RD keeps the reviewer as the effective author. |
SessionTransitionService.cs:206-218 @ 2712901 |
| B-D10 | New (minor) | FrozenStudyCount and ProcessedSessions on the summary are never set. The commit message says partial was kept for the PR-A stub, but the diff removes it (partial class at :16 @ 3619043, class at :20 @ 2712901). |
SessionTransitionService.cs:366-381 @ 2712901 |
| B-D11 | New (minor) | The fingerprint encoding is ambiguous (raw answer strings joined by |, : and =). It digests counts, not identities. Question membership uses a list scan per annotation. |
AnnotationImpactService.cs:30, 67, 206-245 @ 2712901 |
| B-R1 | Refuted for B | "Each branch merged main independently, so GitHub shows spurious deletions." B is one commit on A's squash and never merged main. GitHub's file list holds exactly its 8 files. The fact may still hold for C and D. | gh api …/pulls/2573/files; git log --graph |
| V-D1 | New | #2986 is unwired. No production caller of Validate exists, so the exception and its ProblemDetails are never produced. |
git grep @ 1c6799b; PR body checklist |
| V-D2 | New (minor) | The ProblemDetails reports only the first error, although the validator collects all of them. | AnnotationAnswerConformanceProblemDetails.cs:40-42 @ 1c6799b |
| V-D3 | New (spec conflict) | #2987 allows a metadata-only response; C4 and versioning §3.4 define "value XOR responseModeId plus metadata". This must be settled in E37 and not inherited silently. | ADR-016…md:110-112 @ efd4b96; contracts.md C4 response modes |
| V-D4 | Confirmed (G0-D7) | ADR numbers collide with main: #2987 ADR-016 against ADR-016-preview-tls-certificate-and-readiness; #2812 ADR-017 against ADR-017-bulk-pdf-notifier-authority-deactivation; #2629's planned "ADR-011 Project Template" against ADR-011-schema-v0-multi-option-conditional-parent-answers. #2812 also edits docs/decisions/index.md. ADR-021 is now taken on main as well. Harvested content takes numbers from the programme block ADR-030 to 069 (DOM §11.8). |
git ls-tree origin/main docs/decisions/ @ 7673ed0 |
4. Superseded by main since the PRs were written¶
- Operation and lease mechanics (H-SVC-11): ADR-020 (2 October 2026) gives
BulkStudyUpdateOperationaGenerationandLeaseExpiresAtUtc, with generation-fenced takeover and Failed or Refused outcomes (PMC/Services/BulkStudyUpdate/Atomic/BulkStudyUpdateOperation.cs:10-79).ProjectStatisticsPublicationOperationhas loader and abort lease generations (PMC/Model/ProjectStatisticsAggregate/ProjectStatisticsPublicationOperation.cs:82-104), andBulkPdfUploadJobhas leases. Consistency §7.2 makes this one family. All @7673ed0. - #2467 (active-reviewer tracking) merged on 30 August 2026. This removes B's deferred worker dependency (
AssignmentBlockedReason) and #2986's integration blocker. Claims and presence now live inReviewSubmissionServiceon main. - Feature-flag targeting (H-VAL-14): main's
env-mapping.yamlhas a per-flagservices:list (:7) and the API'sSyRF.API.RuntimeFeatureFlags. Main did not take the fail-closed boolean check (generate-feature-flags.ts:389-398), hence H-VAL-15. - ADR-011 on main (multi-option conditional parent answers) is the rule that #2986's conditional option availability already follows. It makes #2629's ADR-011 plan obsolete.
- Docs drift: main's Approved
annotation-question-extensibility-architecture.md:87-93still lists frozen versus snapshot as "not decided". #2812 records Chris's 18 August freeze decision, and versioning §3.4 adopts frozen versions. This is a docs follow-up, not part of this audit. - None of B's classes exist on main (
git grepforAnnotationImpactService,SessionTransitionService,StageTransitionJobandPreviewDigestundersrc/returns nothing).
5. Closure-note drafts¶
- #2573 (PR-B). Harvested into the integrated review plan (harvest map H-SVC-01 to H-SVC-13). The
core idea, that a publication writes one attributable session version per affected session
combining every question's treatment, returned with the owner session's D2-01 reversal. It is now
specified in RD §3.15 and in C4 and C5 as
PublicationGenerated, and the impact counts and fingerprint inform the R2c preview and its digest (T-RD-04,T-RD-05). The stage-keyed job, the review and export locks, value-based mapping, answer clearing and replacement across questions are not carried forward, because under SP stages own no evidence and publication uses the shared operation family. - #2986.
- If G0-D6 is confirmed: stays open outside the programme as the legacy-write integrity fix, to be finished after the hold by wiring both submission paths. Its fixture corpus and rule codes are harvested into E23 and the T-BC-01 inventory (H-VAL-02 to H-VAL-04).
- If G0-D6 is declined: closed after harvesting the same items into
T-RD-01,T-RD-02andT-BC-01. Legacy writes stay unvalidated until R7. - #2987. Harvested into the F1a C4 draft: server authority, the five semantic categories, the
ban on
LatestVersionand the lifecycle (H-VAL-07, H-VAL-08). The schema and profile split and the per-stage binding are superseded by question and form versions (RD §3.4, §3.5). ADR-016 is taken on main; the content lands in a programme ADR numbered from 030 to 069. - #2629. Superseded by #2987 and #2986, and by E23's shared-fixture applicability specification. Its problem statements are kept as H-VAL-10.
- #2812. The response-mode wire contract is harvested into C4 and E37, with metadata types and units into C14 (H-VAL-12, H-VAL-13). The schema-v0 activation path, the per-stage lock and the three-service flag are not carried forward: response modes are canonical-only and R7 retires legacy writers. The fail-closed flag parsing is proposed as a separate small fix (H-VAL-15). ADR-017 is taken on main.
6. Proposed tracker rows¶
| Row | Scope added | Release | Dependencies | Acceptance evidence |
|---|---|---|---|---|
T-RD-04 (attach) |
Preview digest over identities and heads (H-SVC-02); generation-table planner semantics, using B's scenarios as fixtures (H-SVC-04); treatment vocabulary and policy-record mapping (H-SVC-05); typed PublicationInProgress (H-SVC-12) |
F2 | T-RD-00 (F2 part) |
RD-AE16, AC-R2c-06, AC-R2c-14, C4-T13r |
T-RD-05 (attach) |
Form-keyed impact counts by category, prior version and route (H-SVC-01); manifest counters (H-SVC-10) | R2c | T-RD-04; C8 boundary |
AC-R2c-01, AC-R2c-13, RD-AE06 |
T-UX-06 (attach) |
Per-option answer distribution in the mapping step (H-SVC-03) | R2c | T-RD-05 |
RD-AE15 (UI part) |
T-RD-01 (attach) |
E23 fixture corpus seeded from #2986 (H-VAL-03); typed error vocabulary (H-VAL-04); E37 payload with response modes and metadata from #2812 and #2987 (H-VAL-07, H-VAL-12), including the metadata-only ruling (V-D3) | F1a | T-RD-00 (F1a part) |
AC-R2a-03 (FX-APPLIC, both runners); C4 conformance |
T-RD-02 (attach) |
Run the FX-APPLIC runners in .NET and AF2; ValidationFailed carries rule codes |
R2a | T-RD-01 |
AC-R2a-03, RD-AE04 |
T-BC-01 (attach) |
Legacy-answer conformance detector in the read-only inventory: unmatched option values, type and shape mismatches, questions not in the stage (H-VAL-02) | BC inventory, before pilots | M0; T-BC-00 |
Inventory counts per rule code on an authorised copy; BC §8 unmappable-value finding |
T-RI-11 (new, only if F-O has no C14 ADR row) |
F-O C14 ADR: metadata field types and unit vocabulary, with #2812's metadata as input | F-O | T-SI-01 for event counts |
C14-T01 to C14-T06 |
| AC-M0-04 (attach) | Apply the §12.6 additions in §2 | M0-5 | Harvest map | AC-M0-04 |
The #2986 wiring (H-VAL-01, H-VAL-06) and the flag-parsing fix (H-VAL-15) are normal-triage issues outside the programme. They get no tracker row.
7. Owner-level questions¶
- G0-D6 with the corrected premise.
-
2986 does not yet validate any write; it needs a wiring slice now that #2467 has merged.¶
- Legacy writes last until R7, which has no date.
- Each invalid legacy answer becomes a conversion finding at R6.
- Recommendation: confirm "keep open outside the programme". Classify the wiring as a legacy-integrity fix, not feature work, so it can be authorised separately from the feature-implementation hold.