Skip to content

Read-only audit of #2224 at fff8385ac, #3934 at 9d6c596cf and #2781 at 2d8b071b0. Saved verbatim as evidence for the harvest map. Feature implementation is on hold; nothing was ported, rebased or closed. Correction (verifier V5, 5 October): ProjectSecuritySettingsTests.cs at fff8385ac has 20 [Fact] methods and no [Theory], not the 22 stated below.

Audit E: custom groups (#2224) and question template import (#3934, #2781)

Read-only audit, 5 October 2026. The PR heads are fff8385ac (#2224), 9d6c596cf (#3934) and 2d8b071b0 (#2781). Code on main is cited at origin/main 7673ed0d3. The specifications and package are cited from the pr4061 worktree (bc0c94734). PLAN.md means handover/2026-09-08-authorization-3335/PLAN.md, the #3335 handover plan. Nothing was checked out, fetched, built, commented on or closed.

1. Scope and state

1.1 #2224 custom project groups (author nurikarakaya; Chris merged main once, 5 May 2026)

  • Contents. 47 files. By area:
  • Backend: 6 files, +326/−72. Most of the ProjectRepository.cs churn is cast formatting.
  • .NET tests: 5 files, about 1,400 added lines.
  • Web: 24 files, +1,609/−280, plus about 1,370 lines of web specs.
  • One generated client (+132) and 6 config or agent files.
  • The real logic is about 300 backend lines and about 1,600 frontend lines.
  • Domain.
  • Project.CreateCustomGroup and DeleteCustomGroup.
  • ProjectSecuritySettings.CreateCustomGroup rejects empty names and duplicate names, case-insensitively.
  • DeleteCustomGroup cascades within the aggregate. It removes the group from project permissions, stage permissions and memberships.
  • TryGetGroup, and ProjectMembership.LeaveGroupInternal, which is idempotent and keeps the owner in the Administrators group.
  • Code: Security/ProjectSecuritySettings.cs:151-268, ProjectMembership.cs:223-235, Project.cs:595-607.
  • API.
  • POST api/projects/{projectId}/groups with body {name}. It returns 201 with a ProjectGroupDto, 400 for an empty or padded name, 404 for an unknown project and 409 for a duplicate name.
  • DELETE api/projects/{projectId}/groups/{groupId}. It returns 204, or 400 for an unknown group or a system group.
  • Both use ProjectEditMembershipsPolicy.
  • Member assignment and grants reuse the existing PUT investigators/{id}/groups, POST investigators, POST permissions and POST stages/{id}/permissions endpoints. The PR only switches their Save to SaveAsync.
  • Code: ProjectController.cs:772-835.
  • Persistence.
  • It makes custom groups work on schema 0. A new membership field _v0CustomGroupIds is stored as Registrations.CustomGroupIds.
  • The CustomProjectGroups mapping becomes schema-independent, with SetIgnoreIfDefault.
  • The schema-0 CustomProjectRoles mapping is removed.
  • Code: ProjectMembership.cs:85-114, ProjectRepository.cs:178,203,427-442,741-744.
  • Authorization.
  • The policy attributes are listed above.
  • ResourceSecurity.json:50-53 grants AssignPermissions to the seeded Administrators group by default.
  • Frontend.
  • New: manage-group-dialog (597 lines; a group × activity matrix and delete), custom-groups-table and project-group-badge.
  • Reworked: create-project-group, edit-entity-dialog (multi-select toggles), the membership-table badges and project-members.
  • Three state fixes: in the effects, the selector and the stage-permission-set entity id.
  • Tests.
  • 22 domain facts (ProjectSecuritySettingsTests).
  • 13 controller tests (11 for groups).
  • Permission isolation, two theories and two facts (ProjectLevelPermissionTests, ProjectStagePermissionTests), with TestPermissionHelper.
  • Web specs for the new components and the effects.
  • Drift.
  • Merge base 568100fda (4 May 2026). Main has 764 first-parent commits since then.
  • 25 of the 47 files changed on both sides. Among them are ProjectController.cs (+677 on main), ProjectMembership.cs (#2271, IsActive), ResourceSecurity.json (#3964, new activities), PermissionReportResolver.cs (rewritten), ProjectRepository.cs (+1,192), project-members.component.ts, the membership-table files, angular.json and vitest.config.ts.
  • GitHub reports it CONFLICTING. Bots reviewed it; no person did.
  • The local worktree pr/pr2224.custom-project-groups is at c5c1f97e1. It adds only July main merges: git log --no-merges fff8385ac..c5c1f97e1 ^origin/main is empty, so there is nothing extra to harvest.

1.2 #3934 question template import through the UI (Chris, 2 October 2026)

  • Contents. 42 files. By area:
  • Backend: 7 files, +1,071.
  • API tests: 7 files, +1,405.
  • Web: 6 files, +714, including a 594-line spec.
  • Docs and config: 8 files.
  • Generated or vendored: 14 files, +2,665. These are the OpenAPI document, the client, the flag artefacts and the Unicode CaseFolding-17.0.0.txt.
  • The real logic is about 1,800 lines.
  • Backend (SyRF.API.Endpoint/Services/QuestionImport):
  • A bounded CSV and XLSX reader with OpenXML hardening.
  • A canonical plan builder for standard format v1 plus an adapter for the legacy pilot workbooks.
  • QuestionImportService:
    • preview on a private uncached Project;
    • apply in one snapshot, majority-write transaction, with an insert-only receipt and an optimistic Project save;
    • an operation-ID, file-hash, plan-hash and snapshot-hash retry identity;
    • source-free attempt history with a 30-day TTL.
  • Controller. api/projects/{id}/question-import/{template|history|preview|apply|receipt/{op}} under ProjectDesignPolicy, behind the default-off flag annotationQuestionImport (QuestionImportController.cs:11-110).
  • Web. An app-question-import toolbar and dialog on the new editor's Design tab (signals, OnPush, an indented tree, a session-storage retry identity).
  • Docs. ui-import-implementation.md and how-to/import-question-templates.md.
  • Activity, from gh pr view.
  • Four commits between 17:05Z and 19:01Z on 2 October; last updated at 19:32Z that day.
  • No reviews at all, from people or bots.
  • Checks: Test .NET and Test Summary failed on one test the PR doesn't touch (Mongo.Data.Tests.BsonClassMapRoundtripTests.Study_RandomId_NotSerialized_WhenSchemaVersionIsZero; 1 of 2,063). 37 checks passed and the preview deployed.
  • SonarCloud: the API quality gate comment reports 71.4% new-code coverage against 80%; the web gate passed.
  • Verdict on activity. Recent, authorised work, not abandoned. Its doc records Chris's authorisation of 2 October. It is frozen by the 5 October hold, not dormant.
  • Drift.
  • Merge base c708f83a9 (2 October, #3931). Main has 64 commits since then.
  • The overlapping files are all generated or manifest files: .generated-checksums.json, consumer-manifest.json, mkdocs.yml, env-mapping.yaml, api-client.generated.ts and swagger.json.
  • So the conflict is shallow: regenerate on rebase.

1.3 #2781 guarded annotation question import foundation (Chris, August 2026)

  • Contents. 19 files, all Python tooling under tools/question-template-importer/:
  • parser: 1,069 lines;
  • bulk_importer.py: 1,921 lines;
  • generic_uploader.py: 1,911 lines;
  • tests: 4,752 lines;
  • a synthetic 4-row contract fixture (CSV and expected JSON).
  • Also an operator guide, a CLAUDE.md section and a new pr-tests.yml job.
  • Behaviour.
  • Every network command fails closed: LIVE_API_COMMANDS_ENABLED = False at bulk_importer.py:55, with the message "until the Annotation Extensibility follow-up proves answerLabel/provenance survival".
  • The live path sequences per-question PUT annotationQuestion/{id} calls, a stage PUT .../questions (union-replace) and journal-driven DELETE rollback. It targets the hard-coded production API base and a hard-coded impersonated investigator ID (bulk_importer.py:33,38; the value isn't reproduced here).
  • Known fact confirmed.
  • The uploader that actually uploaded in August is the detached importer-2b-audit copy, outside this PR.
  • The PR copy is hardened but fails closed.
  • Activity. 51 reviews by Chris, 24 by claude, 3 by Codex. claude-review failed. Last updated 1 September.
  • Drift.
  • Merge base 5ddc7a0a0 (12 August). Main has 686 commits since then.
  • Overlapping files: CLAUDE.md, docs/how-to/index.md, pr-tests.yml and mkdocs.yml.
  • Main's annotation-question-extensibility-architecture.md delivery map lists #2781 as step "2c0" and retires the Python live writer at "2g".

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-GRP-01 #2224 Group endpoints in ProjectController plus CreateCustomGroupDto Creates and deletes custom groups under EditMemberships; 201, 400, 404, 409 and 204 mapping Adapt Q-09; AC-R1c-01 and 12; PLAN D10 ("reuse its API shape and tests in WP11"); C10 R1c, as #3335's WP11; C10; proposed T-AC-10 Add rename (AC-R1c-01 says create, rename and delete). Return ProblemDetails or ValidationProblem as main's newer endpoints do. Return 404, not 400, for an unknown group, and 400, not a NullReferenceException, for a null body. Use CreatedAtRoute. Register the routes in the endpoint catalogue (C10-T07). Put them behind an R1c flag, after X-AUTH-SCHEMA. Write authorizationAudit entries (AC-R1c-06). Group tests from H-GRP-10 M Controllers/ProjectController.cs:772-835,935 @fff8385ac; EndpointAuthorizationCatalogTests.cs:81 @7673ed0d3
H-GRP-02 #2224 ProjectSecuritySettings.CreateCustomGroup, DeleteCustomGroup and TryGetGroup; ProjectMembership.LeaveGroupInternal Name validation and uniqueness; deletion strips the group from project grants, stage grants and memberships in one aggregate save; the owner never leaves Administrators Adapt PM1; C10 anti-escalation and revocation rules; AC-R1c-08 and 10; TI-R17 R1c; C10; T-AC-10; consumers are T-TI-04 (admission) and T-RS-07 (group adjudicators) Implement only on the schema-1 field _v1CustomProjectGroups; drop the schema-0 branch (see H-GRP-03). The cascade calls UpdatePermission for every activity that lists the group, but main's GuardStorage throws for owner-reserved activities, so legacy reserved grants must be skipped or cleared without the guard. Raise GroupCreated, GroupRenamed and GroupDeleted events for audit and for #3941's capture (AC-R1c-04). Deleting a group revokes Review grants, so drafts and claims go through the claim-revocation outbox (AC-R1c-10, RT-20). Queued work by affected actors is denied at execution under M5b's DelegatedWorkAuthorityCheck, so show an active-work impact preview first (T-AC-09). Refuse or convert deletion while a training policy (TI-R17) or an adjudicator assignment references the group. Replace System.Data.DuplicateNameException with a domain exception. H-GRP-09 M Security/ProjectSecuritySettings.cs:151-268, ProjectMembership.cs:223-235 @fff8385ac; ProjectPermissionsWithDefaults.cs:29-36 and DelegatedWork/DelegatedWorkAuthorityCheck.cs:14-22 @7673ed0d3
H-GRP-03 #2224 Schema-0 group persistence (_v0CustomGroupIds and Registrations.CustomGroupIds; universal CustomProjectGroups mapping; CustomProjectRoles unmapped) Lets schema-0 projects (all of production) hold custom groups Avoid PLAN D10, G-D and WP-M2 (custom groups wait for schema 1; WP-M2 renames CustomProjectRoles to CustomProjectGroups); X-AUTH-SCHEMA; C16 — Not ported. It invents a schema-0 shape that WP-M2 would also have to migrate. It would turn the two production documents with stray CustomProjectGroups (PLAN:119) into live groups. Main and any rollback image map CustomProjectGroups only when schema > 0, so an older binary drops the group definitions on its next save and leaves membership references pointing at nothing. — — ProjectMembership.cs:85-114, ProjectRepository.cs:427-442,741-744 @fff8385ac; ProjectRepository.cs:1385-1391 @7673ed0d3; PLAN.md:65,89,119
H-GRP-04 #2224 ResourceSecurity.json default AssignPermissions → Administrators Lets non-owner administrators assign grants Avoid PM2; SEC1; C10 ("AssignPermissions only through R1d's envelope"); D1-01 (#3964); AC-R1c-03 — Not ported. Main's OwnerReservedActivityDefaultsTests would fail on it, and so would the owner-reserved refusal and storage guard. Until R1d, only the owner assigns grants to custom groups. — — ResourceSecurity.json:50-53 @fff8385ac; OwnerReservedActivityDefaultsTests.cs:23 @7673ed0d3
H-GRP-05 #2224 FixAdminGroupIds inverted guard and TODO notes ("zero legacy Groups in 27 memberships") Changes when legacy group repair runs on schema 0 Reference only WP-M2 and WP-M3 (schema-0 branches deleted after migration) WP-M2 preflight input (#3335); not R1c Don't change legacy runtime behaviour before migration. Record the observation as a preflight query for WP-M2. — S ProjectMembership.cs:266-275, Project.cs:880-887 @fff8385ac
H-GRP-06 #2224 CustomProjectPermissions getter: schema-0 ??= new HashSet and the EndInit parent fix Makes schema-0 permission edits persist Reference only C10 baseline Follow-up for the authorization programme, outside R1c On main the schema-0 getter returns ImmutableHashSet.Empty as the editable collection, and _v0CustomProjectResourceSecurity is never initialised. So UpdateProjectPermission on a schema-0 project with no stored overrides probably throws NotSupportedException. That would affect today's chart-visibility dialog. Unverified: main's tests use schema 1. Write a failing schema-0 test first. New: a schema-0 permission update test S ProjectSecuritySettings.cs:80 @fff8385ac; ProjectSecuritySettings.cs:60,82 and PermissionCollectionWithDefaults.cs:45-47,88-102 @7673ed0d3
H-GRP-07 #2224 Save changed to await SaveAsync in the membership and permission endpoints Async save in async actions Reference only Modernise touched code Whichever slice touches those endpoints (R1c) Its stated reason, "silent data loss", is refuted: the synchronous Save blocks on ReplaceOne and dispatches events before the response. This is hygiene only. — S ProjectController.cs:1223,1297,1318,1342 @7673ed0d3; MongoExtensions.cs:328, MongoUnitOfWorkBase.cs:248-273 @7673ed0d3
H-GRP-08 #2224 PermissionReportResolver null guard on application groups Avoids a null reference in the report Avoid #3642, #3723, #3879, authority M3a — Superseded: main's resolver returns the evaluator's report when enforced and uses EffectiveApplicationGroups otherwise. — — PermissionReportResolver.cs:19-28 @fff8385ac; PermissionReportResolver.cs:30-47 @7673ed0d3
H-GRP-09 #2224 ProjectSecuritySettingsTests (22 facts) Create validation and case-insensitive duplicates; delete cascade across memberships, several stages and other groups; TryGetGroup; LeaveGroupInternal Adapt AC-R1c-12; Q-09 R1c; T-AC-10 Build schema-1 projects with an Audit (main's OwnerReservedActivitiesTests.CreateProject). Replace the global ResourceSecurity.Instance set-up with main's [Collection] security scope. Add cases for an owner-reserved legacy grant during cascade, rename, emitted events, and deletion refused while a policy references the group. All 22, ported S ProjectSecuritySettingsTests.cs:15-500 @fff8385ac; OwnerReservedActivitiesTests.cs:302-309 @7673ed0d3
H-GRP-10 #2224 ProjectControllerTests group section (11 tests plus 2 membership-save tests) HTTP status mapping, a single save, the group added to settings Adapt AC-R1c-01 and 12; C10-T07 R1c; T-AC-10 Main's ProjectController constructor now takes 7 required arguments. Moq unit tests never exercise authorization, so add endpoint authorization tests (403 without EditMemberships; the owner and administrators allowed) on main's Authorization/* harness. Update expectations to ProblemDetails, 404 for an unknown group and 400 for a null body. Drop the SaveAsync-count assertions. The 11 group tests, rewritten; new 403 and catalogue tests M ProjectControllerTests.cs:30-345 @fff8385ac; ProjectController.cs:69-90 @7673ed0d3
H-GRP-11 #2224 ProjectLevelPermissionTests, ProjectStagePermissionTests and TestPermissionHelper A custom-group member holding one grant gets only that activity, project and stage Adapt AC-R1c-05 (parity, A-17); AC-R1c-02 and 09; C10-T02 R1c X-AUTH-ENFORCE parity suite; FX-PERM; T-AC-10 Run every case through both the legacy IsAuthorizedForProjectActivity and ProjectAuthorityEvaluator (which matches member.GroupIds at :213). Enumerate activities from the catalogue: 22 grantable project activities including BulkPdfUpload and CalculateRob, plus 4 stage activities. Add the FX-PERM anti-escalation matrix, which is new. Delete TestPermissionHelper, which sets a process-wide singleton. Both theories and both facts, as parity tests M ProjectLevelPermissionTests.cs:16-167, ProjectStagePermissionTests.cs:17-184 @fff8385ac; ProjectAuthorityEvaluator.cs:213 @7673ed0d3
H-GRP-12 #2224 manage-group-dialog (component, template and spec) Group × activity checkboxes by category, a stage accordion and delete confirmation Reference only AC-R1c-07; U8 (not designed); new UI must be Material 3; C17 U8 dialog design (W1); R1c WP11 dialog Its activity list is hard-coded and covers 13 of the 22 grantable project activities. Saving sends one POST per changed activity, each a client-side read-modify-write of the whole group list, sequentially, so it can lose updates and fail part-way. R1c needs one atomic group-grants command with a base version and a dialog driven by the catalogue. Keep it as design and test-case input. Spec cases as an inventory — manage-group-dialog.component.ts:145-193,310-345,420-470 @fff8385ac
H-GRP-13 #2224 custom-groups-table, project-group-badge, membership-table badges, edit-entity toggles, create-project-group and the project-members rework, with specs Lists groups with member counts and avatars, numbered badges and multi-select group toggles Reference only U8 (R1b visibility, R1c CRUD); Material 3 rule R1b Members & groups page; R1c Main rewrote membership-table (M3, #2771 and #3459), project-members (#2271 disable) and the invite dialog, so these conflict. Rebuild against U8. Carry the specs' behaviours, not the files. Spec cases (about 1,070 lines) as an inventory — custom-groups-table.component.ts:31-36, project-group-badge.component.ts @fff8385ac
H-GRP-14 #2224 Web state fixes: the selector drops unknown groups, stagePermissionSetSchema keys on stageId, and the effects normalise group objects Stops crashes and wrong keys when membership groups reference missing or keyed entities Adapt AC-R1b-04 (the page shows exactly what is enforced) R1b read-only page; proposed T-AC-11 Reproduce each on main with a failing spec first. Main still has all three: effects.ts:1039, selectors.ts:49-51 and the entity schema with no idAttribute. normalizr passes primitive IDs through unchanged, so the effects change may be unnecessary. StagePermissionSetDto has both id and stageId, so confirm which one consumers use. New regression specs S project-detail.effects.ts:795-806, membership.selectors.ts:50, stage-permission-set.entity.ts:12 @fff8385ac
H-GRP-15 #2224 angular.json and vitest.config.ts exclusion edits, AGENTS.md and .gitignore Narrows the project-members spec exclusions; unrelated edits Avoid Superseded — Main already narrowed the project-members exclusions to four subfolders. The other edits are unrelated. — — angular.json:181-184, vitest.config.ts:32-35 @7673ed0d3
H-IMP-01 #3934 QuestionImportFileReader plus the vendored Unicode case folding Bounded CSV and XLSX parsing (5 MiB, 500 questions, 100 options, depth 20; zip and XML limits; DTDs off; formula, macro, external-link and date-cell refusal) Reuse AC-R1a-01 to 04; ADR-009 R1a-3 (G0-X9 slices); proposed T-RD-10 Move it to the Application layer, next to the plan. No logic change. QuestionImportReaderEdgeTests (6); the reader cases in QuestionImportTests (strict CSV, CSV and XLSX equivalence, formulas, limits, case folding) S QuestionImportFileReader.cs:24-27,131-159,239-246 @9d6c596cf
H-IMP-02 #3934 QuestionImportPlanBuilder (standard v1, legacy adapter, deterministic IDs, parent and condition resolution, plan hash) Turns rows into one canonical plan; resolves @row, @system and @question within this project; validates the control matrix and conditions Adapt DS-20 (import-target port); AC-R1a-01 and 02; C4 identity rules; DD-12 R1a-3; C4; T-RD-10 Split it into a target-neutral rows-to-plan step and target resolution behind IQuestionImportTarget. @question and @system lookup and category placement move into the legacy adapter. Error messages must name the unresolved reference (AC-R1a-02). Lookups are neither imported nor remapped, but AC-R1a-01 and plan R1a promise "lookup remap": add them, or refuse them explicitly and amend the AC. There are no filtered-options columns; R1a-5 needs them in format v2. QuestionImportPlanEdgeTests (7); QuestionImportTests plan cases M QuestionImportPlan.cs:32-38,194,224-241,272-290 @9d6c596cf
H-IMP-03 #3934 QuestionImportService and QuestionImportAuditStore Preview on a private uncached Project; one transaction holding the receipt and the optimistic Project save; retry identity; bulk-lock check; unknown-commit retry; 30-day attempt history Adapt AC-R1a-01, 03, 04 and 08; R0 writer inventory and composite write guard; V2-16 R1a-3 (preview), R1a-4 (legacy apply); T-RD-10; T-BC-01 inventory Expose it as LegacyProjectImportTarget. List it in R0's writer inventory and refuse projects admitted to the canonical model. Move the bulk-lock check inside the transaction or onto the registered guard. Create indexes at start-up, not on every attempt (:57). Rename the collections to pmQuestionImportReceipt and pmQuestionImportAttempt. Store receipts as BSON fields, not a JSON string. Add source {kind: file, catalogue or project, ...} for AC-R1a-08. Preview writes an attempt record, so stop calling it read-only in the docs. QuestionImportTransactionTests (5, real replica set) M QuestionImportService.cs:41-57,96-187 @9d6c596cf
H-IMP-04 #3934 QuestionImportController, QuestionImportUploadOperationProcessor and the annotationQuestionImport flag wiring Design-policy routes; flag checked before the body is read; 413, 422 and 409 mapping; exact-one-file envelope Reuse C10-T07; flag rules R1a-3 and R1a-4; T-RD-10 Regenerate OpenAPI, the client, checksums and flag artefacts on rebase. Add the routes to EndpointAuthorizationCatalogTests. QuestionImportControllerTests (10) S QuestionImportController.cs:11-110 @9d6c596cf
H-IMP-05 #3934 Angular question-import.component.ts, question-import.service.ts, the dialog template and SCSS, and the spec Download, upload, preview tree, confirm, receipt recovery and history on the Design tab Adapt Plan R1a ("new Design/Assign/Preview editor"); U19; Material 3 rule; AC-R1a-07 R1a-3 and R1a-4 UI; C17 and F1c placement; T-RD-10 Validate it against U19 and Material 3 and place it as C17 says. Under the canonical adapter, Confirm creates a draft, not live questions. R1a-6 removes the "Focused question" debug text, which sits directly under the new toolbar. question-import.component.spec.ts (594 lines; it runs under ng test because it isn't in angular.json's exclusions) M question-import.component.ts:35-75,90-240, design.component.html:14-15 @9d6c596cf
H-IMP-06 #3934 Refusal of unsupported fields (CAPABILITY_NOT_AVAILABLE for distinct option display labels; non-empty metadata_fields_json and response_modes_json) Never silently drops data the writer can't store Reuse Extensibility architecture steps 2c2 and 6; C4 version content (displayLabel?, modes, metadata) R1a legacy adapter; lift per capability in the R2a canonical adapter (T-RD-02) Keep it for the legacy target. The canonical target lifts each refusal once C4 version content and the AF2 renderer support the field. DistinctLabelsAreRejectedRatherThanSilentlyLost S QuestionImportPlan.cs:154-161 @9d6c596cf
H-IMP-07 #3934 docs/features/question-management/ui-import-implementation.md and docs/how-to/import-question-templates.md Implementation contract and user guide Adapt Stale docs block merges R1a-3 and R1a-4 PRs Re-cut it to the R1a slices and the import-target port. Mark the canonical adapter as an R2a follow-on. validate-docs S ui-import-implementation.md @9d6c596cf
H-IMP-08 #2781 contract-fixtures/annotation-questions-v1.csv and .expected.json A synthetic legacy-format golden fixture: a system anchor, a condition, a checkbox and options with labels Adapt Extensibility map 2c (fixture parity); AC-R1a-01 R1a-3 .NET golden test; T-RD-10 Map the expected payloads to the .NET plan. Conflict: #2781 puts option labels into option description (the ROUTE row's "Oral" and "Intravenous"), while #3934 refuses distinct labels. Keep #3934's refusal until DisplayLabel persistence lands (2c2), and record the ROUTE row as an expected refusal or relabel it. The fixture as a .NET parity test S contract-fixtures/annotation-questions-v1.expected.json:42-52 @2d8b071b0
H-IMP-09 #2781 question_template_parser.py and the three Python test suites (4,752 lines) Executable behavioural specification of the legacy workbook contract Reference only Extensibility map 2g (Python live-writer retirement) R1a-1 audit evidence; T-RD-10 Compare its cases (cycles, answer_mode, Unicode, sibling order, booleans) with #3934's 45 API tests, and port any gaps as .NET tests. No Python is carried. Gap cases only S question_template_parser.py, test_question_template_parser.py @2d8b071b0
H-IMP-10 #2781 Live uploader (bulk_importer.py and generic_uploader.py upload, preflight and rollback) Per-question PUTs, a stage union-replace and journal DELETE rollback against production, as an impersonated investigator Avoid AC-R1a-03 (no partial questions); C10 and the impersonation read-only gate G-C; real-actor provenance (C3); extensibility "never sequence public per-question PUTs" — Superseded by #3934's server-side atomic apply. The hard-coded production base and impersonation identity must not enter the repository. — — bulk_importer.py:33,38,55,284-370 @2d8b071b0
H-IMP-11 #2781 pr-tests.yml test-question-template-importer job and the CLAUDE.md boundary section Python CI lane on ubuntu-latest; agent instructions Avoid CI rule (default juniper-ci; no valid hosted reason); validate-workflows.sh route pins; CLAUDE.md kept slim (#3301) — Drop both. If any Python survives until 2g, route it to juniper-ci and document it under docs/. — — pr-tests.yml:322-326 @2d8b071b0
H-IMP-12 #2781 docs/how-to/bulk-import-annotation-questions.md Operator guide for the gated Python tool Reference only Extensibility map 2g Input to H-IMP-07 #3934's how-to supersedes it. — — docs/how-to/bulk-import-annotation-questions.md @2d8b071b0

2.1 How the import fits R1a, and what changes once versioned questions exist

R1a's legacy path:

  • Pipeline. reader (H-IMP-01) → target-neutral plan (H-IMP-02) → IQuestionImportTarget.Preview (R1a-3) and Apply (R1a-4, legacy adapter) → receipt and history (H-IMP-03).
  • What #3934 already meets.
  • Preview equals apply: same plan, same writer, and apply checks the plan hash and snapshot hash.
  • No partial questions (transaction test).
  • No deletes.
  • Flag-off behaviour is identical.
  • Gaps.
  • The lookup remap (AC-R1a-01).
  • A named cross-project reference (AC-R1a-02).
  • AC-R1a-04 wording. Under the legacy writer, adding a child to an existing parent appends the child's ID to that parent's child list (Project.UpsertCustomAnnotationQuestion, "Add annotation to parent"). AC-R1a-04 should therefore say "never changes existing question content".
  • Settles #3655's count.
  • At 7673ed0d3, angular.json names exactly 17 question-management specs. 25 spec files exist under that folder, and vitest.config.ts excludes the folder wholesale.
  • CI runs ng test, so angular.json governs. AC-R1a-07's "17" is correct today; #3655's "18 of 23" predates later changes.

The R2a canonical adapter is a slice of T-RD-02 (DS-20). Seven things change:

  1. Draft first. Import writes new QuestionDefinition identities at seq 1 into a single-editor DesignDraft with base checks (R2a). They become live only when a form version that pins them, with ancestors, is published through the ordinary publication protocol (C4; F2 and R2c impact for existing forms).
  2. Identity.
  3. Question IDs stay server-minted; the deterministic operation-and-row scheme is fine.
  4. Stable option IDs are minted, and conditions and parent filters are rewritten from option values to option IDs (C4 option shape).
  5. The category string becomes an entity-type ID (DD-12).
  6. @system: aliases resolve to pinned (systemGuid, SystemQuestionVersion, seq) stored as data (D2-06), not to code-rebuilt GUIDs.
  7. One idempotency authority. The receipt's operation-ID idempotency moves into the canonical command ledger (E50). The project-version and snapshot drift fence becomes per-aggregate CAS, with no Project document in the transaction (E25, CR-2).
  8. Provenance.
  9. A file import records C3 import provenance on the definition records: source "template file", file and plan hashes, format version, real actor.
  10. A catalogue copy records copiedFrom {catalogueId, itemId, versionId, copiedBy, copiedAt}, and a project copy uses the same shape (ACD §3.4 and §4.7; B5 pending).
  11. The catalogue never changes a copy (C4-T04). Catalogue administration uses an application role, never Design (T-AC-04).
  12. History. A C20 HistoryEvent with cause.kind = Import. The FEAT-024 definition-rewrite fence is admitted in publication phase 1, not by the import.
  13. Capabilities lift. Distinct display labels, response modes and metadata fields become version content (H-IMP-06), and filtered options join the format (R1a-5).
  14. Neither PR delivers R1a-5 or R1a-6. On main the new editor writes parentFilter: null (design.store.ts:364) and still shows the debug text (design.component.html:14).

2.2 How #2224 fits R1c, C10, the #3335 gates and training admission

  • Order. Group CRUD lands after X-AUTH-SCHEMA (G-D: WP-M1 runner, then WP-M2 in production). Main has no SyRF.ProjectManagement.Migrations project.
  • Enforcement. It also needs X-AUTH-ENFORCE (M6 cutover) or the parity suite (H-GRP-11), plus X-AUTH-WP9, Q-03a and X-NOTIF (#3941 is still open, so its reviewAccessGranted capture isn't on main).
  • Audit store. authorizationAudit doesn't exist on main; only runtimeFeatureFlagAudit does. R1c's audit depends on #3335 creating it in WP4, WP10 or WP11.
  • What #2224 lacks for C10. Rename, anti-escalation (AC-R1c-02), audit, the notification path, claim release and impact previews. Its ResourceSecurity.json edit contradicts PM2.
  • What it supplies. Training admission (TI §5.4, T-TI-04) needs from R1c a stable group identity, an existence re-check (TryGetGroup), an additive join (ProjectMembership.JoinGroup, which exists on main at :189) rather than ReplaceMemberGroups, idempotent AlreadyMember, and the same anti-escalation check at policy publication and at execution. #2224 supplies only the identity and TryGetGroup.

3. Defects not to carry over

Confirmed (#2224):

  • D1. Schema-0 group persistence. It bypasses WP-M2. It activates stray CustomProjectGroups on two production documents. Older binaries drop CustomProjectGroups on save, which orphans membership group IDs, and ProjectMembership.Groups uses GetGroup (Single), which throws on an unknown ID (H-GRP-03).
  • D2. AssignPermissions granted to Administrators by default. It contradicts PM2 and SEC1 and fails main's OwnerReservedActivityDefaultsTests:23 (H-GRP-04).
  • D3. Cascade versus main's guard. On main, the cascade's UpdatePermission loop throws for owner-reserved legacy grants (ProjectPermissionsWithDefaults.cs:29-36), so deleting a group fails on projects that already hold such a grant (H-GRP-02).
  • D4. Non-atomic multi-request grant save in the dialog. It does client-side read-modify-write of the group lists with no base version, so updates can be lost and a save can partly fail (H-GRP-12).
  • D5. No anti-escalation, audit, notification capture or claim release on group changes. These predate #3941, RT-20 and Q-03a.
  • D6. Incomplete activity coverage. The activity list is hard-coded to 13 of 22 grantable project activities (AC-R1c-07).
  • D7. Wrong status codes and bodies. Unknown group gives 400; a null body gives a NullReferenceException, which a test enshrines; the bodies are strings, not ProblemDetails.

Refuted (#2224): "Save() causes silent data loss". The synchronous save completes before the response (H-GRP-07).

New, on main and unverified: a schema-0 UpdateProjectPermission with no stored overrides probably throws NotSupportedException (H-GRP-06). This is a follow-up candidate, not part of R1c.

#3934:

  • D8. Indexes created per request inside RecordAttemptAsync (:57). Failures are swallowed as best-effort.
  • D9. Collection names break the pm{Entity} rule (mongodb-reference.md:73-92).
  • D10. The receipt is stored as a JSON string inside BSON.
  • D11. The bulk-lock check sits outside the transaction (TOCTOU against R0's composite guard).
  • D12. Placement. The domain logic lives in the API project (ADR-009).
  • D13. No lookup remap (AC-R1a-01).
  • Not a defect: the one red .NET test is in Mongo.Data.Tests and isn't touched by the PR.

#2781 (known facts confirmed):

  • D14. The uploader fails closed, pending answer-label survival and atomic rollback.
  • D15. The live path is non-atomic sequential PUTs with DELETE rollback.
  • D16. It hard-codes a production base URL and an impersonated investigator.
  • D17. Its CI lane is on ubuntu-latest with no valid reason.
  • D18 (new). Its fixture's option-label mapping conflicts with #3934 (H-IMP-08).

4. Superseded by main since the PRs were written

  • #2224, superseded:
  • Permission report and effective groups: #3642/#3723 (28 September) and #3879 (1 October, shared protected-resource decisions); application-authority M3a makes the evaluator the report source when enforced.
  • Owner-only ownership transfer and owner-reserved refusal in the API and domain: #3964 (3 October, 85e6facf7), with OwnerReservedActivitiesTests and OwnerReservedActivityDefaultsTests.
  • Membership disable and IsActive: #2271 (15 September).
  • Members UI rework: #2771, #3459 and #3273 (M3 theming), the invite dialog rewrite, and the removal of project-members.mock-data.ts.
  • New activities BulkPdfUpload (#2788) and CalculateRob (#3060).
  • Endpoint catalogue coverage: EndpointAuthorizationCatalogTests.
  • ProjectAuthorityEvaluator, dark until M6; M5b's queued-work authority (DelegatedWorkAuthorityCheck, 2 October), which re-checks group-derived authority when queued work runs.
  • Narrower spec exclusions.
  • #2224, not superseded:
  • Custom group CRUD (nothing on main creates a custom group; ProjectSecuritySettings.cs:39 returns empty on schema 0).
  • WP1d: the members route still reads editMembership (project-admin.routes.ts:36); the report key is editMemberships.
  • WP9, WP11, WP-M1 and WP-M2, authorizationAudit, and the R1b read-only page.
  • #3934: only the generated artefacts. It builds on main's extensibility map (steps 2c to 2f) and on #2772's answer labels (merged 28 August).
  • #2781:
  • Its production purpose is superseded by #3934 (map step 2g).
  • 2779, the planning contract, merged on 12 August.

  • CLAUDE.md was slimmed by #3301.
  • Its synthetic fixture is the only artefact still worth porting.

5. Closure-note drafts (not to be posted during the hold)

#2224. Thank you, Nuri Karakaya (@nurikarakaya), for the custom project groups work (December 2025 to January 2026). Its API shape (POST/DELETE api/projects/{projectId}/groups under EditMemberships), its create-and-cascade-delete rules and its domain, controller and permission-isolation tests are carried into R1c of the integrated review plan. R1c is delivered with the authorization programme's WP11 once memberships are on schema 1, and cites this PR (harvest map H-GRP-01, -02 and -09 to -11). The schema-0 persistence path and the AssignPermissions default are not carried, because memberships migrate to schema 1 first and AssignPermissions stays owner-reserved. The members-page components inform the new Members & groups design.

#3934. Harvested into R1a. The bounded CSV and XLSX reader, the canonical plan and legacy adapter, the controller and the transactional receipt with its tests are split into R1a-3 (preview behind the import-target port) and R1a-4 (legacy apply), with the dialog adapted to the new editor's design (H-IMP-01 to -07). The canonical adapter, which writes versioned question drafts for publication with import or copiedFrom provenance, follows in R2a.

#2781. The reference parser, shared fixture and Python tests shaped #3934's legacy adapter. The fixture becomes a .NET golden test in R1a-3, and the test cases are used to find coverage gaps (H-IMP-08 and -09). The live uploader, its production impersonation path and its CI lane are not carried, because the production path is the server-side atomic import.

6. Proposed tracker rows

Suggested ID Scope Release Depends on Acceptance evidence
T-RD-10 (new) R1a import pipeline harvested from #3934 and #2781: readers, a target-neutral plan, the IQuestionImportTarget port, legacy preview (R1a-3) and legacy apply with receipt and history (R1a-4); writer-inventory entry; the #2781 fixture as a golden test (H-IMP-01 to -08) R1a (T2) R1a-1 (this audit); U19; T-BC-01 inventory AC-R1a-01 to 04 and 08; AC-R1a-CONF (C4-T04); QuestionImport*Tests; flag-off test
T-RD-11 (new; G0-X9) R1a-5 filtered-options authoring in the new editor, with a parent_filter column in import format v2 R1a R1a-1 AC-R1a-06
T-RD-12 (new; G0-X9) R1a-6: remove the 17 angular.json exclusions and the question-management/** exclusion in vitest.config.ts; remove the "Focused question" text R1a R1a-1 AC-R1a-07
Attach to T-RD-02 Canonical import adapter: drafts at seq 1, option IDs, pinned system anchors, ledger idempotency, C3 provenance, C20 Import events (§2.1) R2a T-RD-10; T-RD-01 AC-R2a definitions criteria; C4-T04
Attach to T-AC-04 Catalogue and project copy as import sources sharing the pipeline; copiedFrom R1a T-RD-10; ACD B5 AC-R1a-10r; ACD-AE19 and AE20
T-AC-10 (new) R1c group create, rename and delete and member assignment from #2224's shape on schema 1, with events, audit, impact preview, claim release, the parity suite and the FX-PERM matrix (H-GRP-01, -02 and -09 to -11); jointly with #3335 WP11 R1c X-AUTH-SCHEMA; X-AUTH-ENFORCE or parity; X-AUTH-WP9; Q-03a; X-NOTIF; U8 dialog AC-R1c-01 to 12; C10-T02, T07 and T09
T-AC-11 (new, if #3335 doesn't track it) R1b read-only Members & groups page with the WP1d guard fix and the state fixes from H-GRP-14 R1b U8 visibility; #3335 WP1d AC-R1b-04 to 06
Attach to T-TI-04 Admission uses the additive JoinGroup, AlreadyMember, a group existence re-check and the shared anti-escalation check; group deletion refuses or converts referencing policies TR1 T-AC-10 TI-AE07 to AE09
Follow-up (outside the programme) Repro test, then a fix, for the schema-0 UpdateProjectPermission failure (H-GRP-06) — — A failing test, then a passing one

7. Owner-level questions

None. Two items belong in briefs:

  • AC-R1a-01 promises a lookup remap that neither PR implements. Implement it, or refuse lookups and amend the AC. This goes in the T-RD-10 brief.
  • Should a file import count as a "copy" for AC-R1a-08? Recommendation: yes, with source.kind = file provenance on the receipt. This goes in the T-RD-10 brief, aligned with ACD B5.