Read-only audit of #2224 at
fff8385ac, #3934 at9d6c596cfand #2781 at2d8b071b0. 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.csatfff8385achas 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.cschurn 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.CreateCustomGroupandDeleteCustomGroup.ProjectSecuritySettings.CreateCustomGrouprejects empty names and duplicate names, case-insensitively.DeleteCustomGroupcascades within the aggregate. It removes the group from project permissions, stage permissions and memberships.TryGetGroup, andProjectMembership.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}/groupswith body{name}. It returns 201 with aProjectGroupDto, 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 permissionsandPOST stages/{id}/permissionsendpoints. The PR only switches theirSavetoSaveAsync. - Code:
ProjectController.cs:772-835. - Persistence.
- It makes custom groups work on schema 0. A new membership field
_v0CustomGroupIdsis stored asRegistrations.CustomGroupIds. - The
CustomProjectGroupsmapping becomes schema-independent, withSetIgnoreIfDefault. - The schema-0
CustomProjectRolesmapping 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-53grantsAssignPermissionsto the seeded Administrators group by default.- Frontend.
- New:
manage-group-dialog(597 lines; a group × activity matrix and delete),custom-groups-tableandproject-group-badge. - Reworked:
create-project-group,edit-entity-dialog(multi-select toggles), themembership-tablebadges andproject-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), withTestPermissionHelper. - 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, themembership-tablefiles,angular.jsonandvitest.config.ts. - GitHub reports it CONFLICTING. Bots reviewed it; no person did.
- The local worktree
pr/pr2224.custom-project-groupsis atc5c1f97e1. It adds only July main merges:git log --no-merges fff8385ac..c5c1f97e1 ^origin/mainis 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}}underProjectDesignPolicy, behind the default-off flagannotationQuestionImport(QuestionImportController.cs:11-110). - Web. An
app-question-importtoolbar and dialog on the new editor's Design tab (signals, OnPush, an indented tree, a session-storage retry identity). - Docs.
ui-import-implementation.mdandhow-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 .NETandTest Summaryfailed 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.tsandswagger.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.mdsection and a newpr-tests.ymljob. - Behaviour.
- Every network command fails closed:
LIVE_API_COMMANDS_ENABLED = Falseatbulk_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 stagePUT .../questions(union-replace) and journal-drivenDELETErollback. 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-auditcopy, outside this PR. - The PR copy is hardened but fails closed.
- Activity. 51 reviews by Chris, 24 by
claude, 3 by Codex.claude-reviewfailed. 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.ymlandmkdocs.yml. - Main's
annotation-question-extensibility-architecture.mddelivery 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) andApply(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.jsonnames exactly 17 question-management specs. 25 spec files exist under that folder, andvitest.config.tsexcludes the folder wholesale. - CI runs
ng test, soangular.jsongoverns. 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:
- Draft first. Import writes new
QuestionDefinitionidentities atseq 1into a single-editorDesignDraftwith 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). - Identity.
- Question IDs stay server-minted; the deterministic operation-and-row scheme is fine.
- Stable option IDs are minted, and conditions and parent filters are rewritten from option values to option IDs (C4 option shape).
- The category string becomes an entity-type ID (DD-12).
@system:aliases resolve to pinned(systemGuid, SystemQuestionVersion, seq)stored as data (D2-06), not to code-rebuilt GUIDs.- 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).
- Provenance.
- A file import records C3 import provenance on the definition records: source "template file", file and plan hashes, format version, real actor.
- 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). - The catalogue never changes a copy (C4-T04). Catalogue administration uses an application role, never Design (T-AC-04).
- History. A C20
HistoryEventwithcause.kind = Import. The FEAT-024 definition-rewrite fence is admitted in publication phase 1, not by the import. - Capabilities lift. Distinct display labels, response modes and metadata fields become version content (H-IMP-06), and filtered options join the format (R1a-5).
- 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.Migrationsproject. - 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
reviewAccessGrantedcapture isn't on main). - Audit store.
authorizationAuditdoesn't exist on main; onlyruntimeFeatureFlagAuditdoes. 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.jsonedit 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 thanReplaceMemberGroups, idempotentAlreadyMember, and the same anti-escalation check at policy publication and at execution. #2224 supplies only the identity andTryGetGroup.
3. Defects not to carry over¶
Confirmed (#2224):
- D1. Schema-0 group persistence. It bypasses WP-M2. It activates stray
CustomProjectGroupson two production documents. Older binaries dropCustomProjectGroupson save, which orphans membership group IDs, andProjectMembership.GroupsusesGetGroup(Single), which throws on an unknown ID (H-GRP-03). - D2.
AssignPermissionsgranted to Administrators by default. It contradicts PM2 and SEC1 and fails main'sOwnerReservedActivityDefaultsTests:23(H-GRP-04). - D3. Cascade versus main's guard. On main, the cascade's
UpdatePermissionloop 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.Testsand 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-latestwith 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), withOwnerReservedActivitiesTestsandOwnerReservedActivityDefaultsTests. - 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) andCalculateRob(#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:39returns empty on schema 0). - WP1d: the members route still reads
editMembership(project-admin.routes.ts:36); the report key iseditMemberships. - 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.mdwas 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 = fileprovenance on the receipt. This goes in the T-RD-10 brief, aligned with ACD B5.