Spec 0026 - Admissions migration, Gate 4
Status: operator-approved (2026-10-04) for local implementation under the rulings at the end of this document; no landing, deployment or cutover authority. This is a read-only design against the clean, unlanded mono-web integration revision 9850665245b6280a693e6947d90ed1b57cb322a5 (unpublished), the revision where spec 0027 was accepted locally. It is 41 commits on top of the Contact-accepted 101ffb036ea8f6b10f0eb03c9970da45d3a9b2c4 (unpublished), which sits on the first-design baseline 8152c389f1176ede38b70417bab1332b0a311c21 (unpublished). mw/ denotes mono-web repository-relative paths; unprefixed paths belong to effx. The operator-owned mono-web main remains read-only at f433ea90. This revision replaces the first design, which described fourteen operations and the three-value outcome. No Admissions code, vendored pack, tests, migrations, provider, credentials or deployed environment were changed for this design (docs/specs/0004-monoweb-migration-plan.md:316-327; docs/research/admission-outcome-policy-evidence.md; docs/research/contact-slice-evidence.md; mw/AGENTS.md#verification-and-resources).
What changed since the first design
Three accepted facts replace the first design's premises. Each was observed at the revision above; the evidence files record the commands, exits and resources.
- Spec 0027 repaired the handwritten source. An admission outcome is
AdmittedorRejected;Substituteis no outcome and no role.recordOutcomebecamedecideOutcome, and theadmissionOutcomesgroup gainedannounceOutcomeand three template operations. The group now has 8 operations, Admissions 18, and the external index 111 (107 + 4) plus one excluded internal receipt. Migrations0081and0082, an outcome mail worker and an editable-template dashboard route came with it (docs/specs/0027-admission-outcome-policy.md;docs/research/admission-outcome-policy-evidence.md). - Contact (spec 0028) is migrated. The mono-web integration now pins effx pack
e04c8d2a16636f81e65d478a4d06a07933f6896band has seven generated groups (Profile, Directory, SocialEvents, Content, TeamApplications, Organization, Contact). Effx main has since added theeffx cedarprojection (spec 0017: a new compiler module, a CLI command and pack peer-dependency metadata) plus tests, CI and removed scrub tooling; it changed nothing underpackages/compiler/src/generateorpackages/compiler/src/extensions, so generated contracts and handlers are not affected. The earlier conditional S5 re-vendor step is therefore closed (docs/research/contact-slice-evidence.md;docs/specs/0017-effx-cedar.md). - Counts. Effx has locally accepted 38 of 111 external operations (Profile 2, Directory 4, SocialEvents 3, Content 8, TeamApplications 7, Organization 13, Contact 1). 73 external plus one internal remain, of which 18 are Admissions (
mw/packages/sdk/native-api-operations.json).
Admissions' ten non-outcome operations are unchanged in source: mw/packages/http-api/src/admissions.ts and the five mw/apps/backend/src/admission/{http,http-reads,http-commands,http-access,http-context}.ts handler files have no diff between 8152c389 (unpublished) and the revision above. Only admission-outcomes.ts, http-semantics.ts, a single added problem code in placements.ts, and the outcome handlers changed.
| Remaining bounded context (the §5 sequence) | HTTP operations still handwritten | Current Foldkit programs | Authority and custody to retain | Relevant unmerged evidence, not approved behavior |
|---|---|---|---|---|
| Admissions - proposed here | 18 external: admissions 10 + admissionOutcomes 8 | 0 | High: anonymous rate-limited submit; person/owner, department/semester and time-scoped decisions; delegation-only decide, announce and template authority; fresh snapshots, preconditions, two receipts, outbox, immutable mail envelope and mail worker. | effx/admissions-outcome-0027 (accepted locally, unlanded); design-time refs fix/admissions-core-0926 (5d05e6e2 (unpublished)) and fix/admissions-{affiliation,onboarding,ownership,returning}-sub; recruitment review overlaps. |
| Recruitment/onboarding | 18 external: recruitment 16 + Recruitment's two onboarding operations; Identity's claim is excluded | 4: recruitment, recruitment-maintenance, interview, scheduling | Very high: invitation-only bearer capability versus Person credentials, interviewer/board/department scope, corrections, durable delivery/replay. | fix/recruitment-review-0926 (27265998 (unpublished)), fix/admissions-onboarding-sub, admissions-core readiness. |
| Placements/certificates | 16 external: placements 10 + certificates 6 | 1 accepted dated-school-service; branch-only certificate editor is not current | High: current affiliation, scoped boards/teams, ETag/CAS, commitments/coverage, immutable issued certificate and private PDF replay. | fix/placements-review-0926 (31b4435f (unpublished)), feat/certificates-0926 (512a6bc0 (unpublished)), fix/admissions-affiliation-sub and admissions-core. |
| Economy/receipts | 13 external receipts plus one internal receipt, not in the external SDK | 0; owner/approval/settlement are React Router | Very high: encrypted destination and private file custody, scoped delegations, external payment evidence, atomic receipts/outbox versus after-commit provider/file work. | fix/economy-review-0926 (31b4b5af (unpublished)), fix/delivery-review-0926 (31c82272 (unpublished)). |
| Identity/system | 7 external Identity: system 6 + Identity's onboarding claim; shared system health is separate | 0 | Critical: Better Auth session/OAuth, one-principal credential algebra, one-use invitation claim, refresh/replay and durable provider boundaries. | fix/oauth-review-0926 (44bc3416 (unpublished)); related Recruitment/onboarding proposals are not claim migration. |
Counts are allocations, not claims that those endpoints do not run today. The five rows are the §5 sequence after Organization and sum to 72 external plus one internal; the shared health endpoint makes 73 external plus one internal, matching 111 minus the 38 accepted. Contact, the omission the first design appended to this table, is migrated by spec 0028 and is no longer a row. Branch tips were re-read at design time against current mono-web refs and must be rechecked at implementation time; no branch was checked out or merged (docs/specs/0004-monoweb-migration-plan.md:16-33,72-90; docs/research/monoweb-branches.md:57-78; mw/packages/http-api/src/api.ts).
Observable Admissions outcome and exact accepted contract
A visitor sees currently open periods and application options, submits one bounded application and can follow its confirmation. An authenticated applicant sees only their progress; an authorized period manager creates/revises a department-semester window. For an interviewed applicant Ja derives Admitted and Nei derives Rejected; a Kanskje or returning applicant needs an explicit decision by an authorized Rekruttering decider. The department's delegated school coordinator then announces the outcome once, which is final and queues one immutable mail; a delegated coordinator or a global administrator edits, previews and saves the versioned message templates. A returning assistant reads options and registers through their own account, without turning Identity's separate invitation claim into an Admissions endpoint. Keep both existing external group names admissions and admissionOutcomes, their current OpenAPI title/description/tag, and all 18 qualified operation IDs. Do not collapse groups into a new admissions superset, add a synthetic nineteenth operation or restore recordOutcome (mw/packages/http-api/src/{admissions,admission-outcomes}.ts; mw/packages/sdk/native-api-operations.json; mw/packages/http-api/src/api.ts:34-70; mw/docs/system.md:164-218).
A below is anonymous External/None/Anonymous without PersonSecurity; P is PersonSecurity with one Better Auth cookie or delegated Person OAuth bearer, never both. All remain External/Reveal. CR is optional If-Match/If-None-Match; IK is required Idempotency-Key; IK+M additionally requires strong If-Match. D is dynamic public conditional 200/bodyless 304 with strong ETag, Vary: Origin and the existing bounded equal max-age/s-maxage (0–30 seconds); PC is private/no-store conditional 200/304; PR is private/no-store 200; NR is no-store 200. Creates return 201/no-store/ETag/origin-relative Location/Vary; entity mutations return 200/no-store/new ETag/Vary; an announcement returns 200 with the private/no-store entity-mutation headers (PrivateEntityMutationResponseHeaders). These are exact existing header-schema/response branches, not a proposed simplification (mw/packages/http-api/src/{access,common,http-semantics}.ts; mw/packages/http-api/src/http-semantics.ts:205-212; mw/packages/http-api/src/admissions.ts:68-324; mw/packages/http-api/src/admission-outcomes.ts:41-356).
| Qualified operation | Current method/path and input | Credential; exact capability / canonical resolver; decision time | Current success and cache/headers |
|---|---|---|---|
admissions.listOpenAdmissionPeriods | GET /api/open-admission-periods; CR | A; none / admissions.public-open-periods; SnapshotRead | OpenAdmissionPeriodListResponse; D. |
admissions.listApplicationOptions | GET /api/application-options; CR | A; none / admissions.public-application-options; SnapshotRead | PublicApplicationCatalogSchema; D. |
admissions.submitApplication | POST /api/applications; IK; strict SubmitApplicationRequest JSON | A; none / admissions.application-create; Transaction | 201 ApplicationConfirmed {applicationId}; created headers. |
admissions.readApplicationConfirmation | GET /api/applications/:applicationId; typed PublicApplicationIdSchema param; no conditional header | A; none / admissions.public-application-by-id; SnapshotRead | 200 ApplicationConfirmed; NR, not a public cache or staff response. |
admissions.readApplicantProgress | GET /api/applicant-progress; no query/body | P; admissions.read-applicant-progress / profile.current-person, requirement profile.owner; SnapshotRead | 200 ApplicantProgressResponseSchema; PR. |
admissions.listAdmissionPeriods | GET /api/admission-periods; CR | P; admissions.read-periods / admissions.management-periods; SnapshotRead | AdmissionPeriodManagementListResponse; PC. |
admissions.createAdmissionPeriod | POST /api/admission-periods; IK; CreateAdmissionPeriodRequest JSON | P; admissions.create-period / admissions.period-create; Transaction | 201 AdmissionPeriodManagementItem; created headers. |
admissions.reviseAdmissionPeriod | PATCH /api/admission-periods/:admissionPeriodId; typed ID; IK+M; AdmissionPeriodMergePatch with application/merge-patch+json | P; admissions.revise-period / admissions.period-by-id; Transaction | 200 AdmissionPeriodManagementItem; entity headers; absent versus null patch fields remain distinct. |
admissions.readReturningAssistantOptions | GET /api/returning-assistant/options; no query/body | P; placements.self / profile.current-person; SnapshotRead | 200 ReturningAssistantOptionsSchema; NR. |
admissions.registerReturningAssistant | POST /api/returning-assistant/registrations; IK; ReturningAssistantRegistrationInputSchema with body-carried expected revision | P; placements.self / profile.current-person; Transaction | 201 ReturningAssistantRegistrationResponseSchema; created headers; no invented HTTP If-Match. |
admissionOutcomes.listScopes | GET /api/admission-outcomes/scopes | P; admissions.outcomes.read or alternative admissions.outcomes.announce / admissions.application-outcomes; SnapshotRead | 200 AdmissionOutcomeScopes; PR. |
admissionOutcomes.readOutcomes | GET /api/admission-outcomes; required departmentId and semesterId query | P; same read-or-announce access and resolver; SnapshotRead | 200 tagged AdmissionOutcomeBoardResource (Decide or minimal Announce); PR. |
admissionOutcomes.readOutcome | GET /api/admission-outcomes/:applicationId; typed ID; CR | P; declared admissions.outcomes.read / admissions.application-outcomes; SnapshotRead; backend also requires current Decide authority | 200 AdmissionOutcomeResource or bodyless 304; PC. |
admissionOutcomes.decideOutcome | POST /api/admission-outcomes/:applicationId:decide; typed ID; IK+M; AdmissionOutcomeCommand (Admitted, Rejected only) | P; admissions.outcomes.decide / admissions.application-outcomes; Transaction | 200 AdmissionOutcomeResource with new ETag; entity headers. |
admissionOutcomes.announceOutcome | POST /api/admission-outcomes/:applicationId:announce; typed ID; IK+M; empty {} AdmissionOutcomeAnnounceCommand | P; admissions.outcomes.announce with requirement admissions.department-coordinator / admissions.outcome-announcement; Transaction | 200 AdmissionOutcomeAnnounceResource with new ETag; private/no-store entity-mutation headers. |
admissionOutcomes.readOutcomeTemplates | GET /api/admission-outcomes/templates; optional departmentId query (omitted selects the global default); CR | P; admissions.outcome-templates.read / admissions.outcome-templates; SnapshotRead | 200 OutcomeTemplateResource or bodyless 304; PC. |
admissionOutcomes.previewOutcomeTemplate | POST /api/admission-outcomes/templates/:kind:preview; kind param; optional departmentId query; OutcomeTemplatePreviewRequest | P; admissions.outcome-templates.edit / admissions.outcome-templates; SnapshotRead; reads, writes no receipt | 200 RenderedOutcomeMail {subject, html, text}; PR. |
admissionOutcomes.saveOutcomeTemplate | PUT /api/admission-outcomes/templates/:kind; kind param; optional departmentId query; IK+M; OutcomeTemplateDraft | P; admissions.outcome-templates.edit / admissions.outcome-templates; Transaction | 200 OutcomeTemplateResource with new ETag; entity headers. |
All names, schemas, statuses and security rows above come from current exports, not only the SDK index (mw/packages/http-api/src/admissions.ts:68-324; mw/packages/http-api/src/admission-outcomes.ts:41-356; mw/packages/http-api/src/v2-schemas.ts:132-162,274-302; mw/packages/domain/src/{application/schema,application/returning,admissions/outcome,admissions/template}.ts). Preserve every endpoint's declared ordered closed problem union, including intentional shared references, rather than a copied group-wide superset. The ten Admissions*Problem unions stay in mw/packages/http-api/src/endpoint-problems.ts:408-598. The outcome unions now live in admission-outcomes.ts:99-143: the three board/entry reads share the 17-code AdmissionOutcomeProblem; decideOutcome, announceOutcome and saveOutcomeTemplate share the 24-code AdmissionOutcomeCommandProblem (those 17, then idempotency.unavailable, then the six admission-outcome.{undecided,placement-active,final,derived,unreviewed,template-invalid} codes); readOutcomeTemplates and previewOutcomeTemplate share the 18-code AdmissionOutcomeTemplateProblem (the 17 plus admission-outcome.template-invalid). Spec 0027 expected an 18-member command union; the accepted source appends six policy codes instead of one, so the 24-code order is the contract. NativeProblemRegistry owns per-code status/body/headers (http-semantics.ts:475-513 for the six new codes). Credential middleware separately declares missing/invalid responses; Origin ingress can reject before endpoint logic. Keep schema annotations/examples, operation summaries/descriptions, request media, dynamic cache limits, public 201 Location and exact problem ordering byte-identical (mw/packages/http-api/src/http-semantics.ts:53-113,136-212,1418-1548; mw/packages/http-api/src/common.ts:50-108).
Authority and application-owned behavior
- Public reads and submission. Public catalog/open-period validators depend on current period/department/field facts and a short dynamic cache; evaluate access and preconditions before a 304. The anonymous submit is still a transactional command. Keep the existing body bound, strict decode, origin policy and public rate limit before body parsing and receipt lookup, including replay. Within the existing database transaction, lock the command/target/email, check active eligibility, reject duplicate applicant-period submissions, and commit applicant/application/command receipt/audit/outbox atomically. A generated guard cannot become a new identity grant or move rate limiting after a replay (
mw/apps/backend/src/admission/{http-reads,http-commands}.ts;mw/packages/database/src/application/postgres.ts:478-648;mw/packages/database/migrations/{0002-admission-period-authority,0003-public-applicant-admission}.sql). - Current staff decisions. Period managers resolve current Organization reach for one department-semester under the read snapshot or write transaction. Create/revise compare current revision and strong If-Match where declared; a successful replay retains the original HTTP capsule and cannot redeliver outbox effects. Applicant progress is confined to the authenticated
profile.owner. Returning options/registration useplacements.selfwith an existing linked person/applicant and history; their command revision lives in the body. Do not merge returning registration into onboarding account claim or infer authority from the UI's navigation role (mw/apps/backend/src/admission/{http-access,http-commands,http-reads}.ts;mw/packages/database/src/{admission-period/postgres,application/returning-postgres}.ts;mw/packages/http-api/src/admissions.ts:151-324). - Outcome decision and announcement. Decide rides the delegable capability
admissions.outcomesand announce ridesplacements.coordinate, each only through an active team delegation that reaches the department or an active global administrator. A department-board leader, a national-board leader, a team leader and a plain member hold neither, and a board-leader reach is never inherited. Rekruttering may re-decide aKanskjeor returning outcome until the announcement;JaandNeiderive Admitted and Rejected and cannot be overridden.Rejectedis refused while the applicant has an active placement in the same department and semester, and a later placement for a Rejected applicant is refused; both orders contend on the same department row lock. Each handler resolves the credential and current authority inside the preparation of the committing transaction, before identity and receipt lookup, so a revoked actor cannot replay a formerly accepted decision, announcement or save. Protected reads use a repeatable-read snapshot (outcomeSnapshotRead); the read-one handler's extra Decide check remains despite the broader declared read AccessSpec. The board isDecide(every application, withcanAnnouncefor an administrator who may also announce) or the minimalAnnounceprojection with name, state and ETag but no recipient, phone or study data; a plain member is refused. An announcement is final, stores one immutable{subject, html, text}envelope rendered from the effective template and queues it once; replay, rollback and denial queue none. Rekruttering's team name, a UI button, an inferred applicant result or a previously observed grant is never authority (mw/apps/backend/src/admission/outcome-http.ts:99-130,255-300;mw/apps/backend/src/admission/outcome-http-support.ts:97-112;mw/packages/database/src/admissions/{outcome,outcome-mail-queue}.ts;mw/packages/domain/src/{admissions/outcome,authz/delegation}.ts;mw/apps/backend/src/http-api/receipt-transaction.ts:196-321;docs/research/admission-outcome-policy-evidence.md). - Templates, rendering and mail. Global defaults are editable only by an active global administrator, and a department override only by that department's delegated coordinator; board membership and the dashboard role confer neither. A global default never masks an invalid selected override. Drafts are a closed ProseMirror document with placeholders, validated by one strict Schema at preview, save and announce, and rendered by a code-owned fixed layout with escaping. Preview and the sent bytes come from the same renderer. The renderer, the validator, the editor component and the mail worker are application code: effx generates none of them, and a generated guard cannot move validation after storage (
mw/apps/backend/src/admission/{outcome-template-http,template-render,outcome-worker}.ts;mw/packages/domain/src/admissions/template.ts;mw/packages/database/src/admissions/outcome-template-store.ts). - UI and delivery stay put. The homepage
/assistenterform and confirmation, dashboard/opptaksperioder,/soknad,/tidligere-assistenter, the outcome board/opptaksutfall(renamed from/vikarer) with its/opptaksutfall/malertemplate editor, current React Router loaders/actions and their same-origin/SSR generated SDK bridges stay application-owned. Keep existing request identity, error display, privacy/no-store, disabled/retry controls, browser disposal and axe assertions. Admissions has zero Foldkit programs, so do not add a fake Foldkit adapter for parity. Period changes, application/returning commands and the outcome announcement retain their current outbox/worker and provider Layers after commit. A loopback mailbox proves transport acceptance of the exact bytes, not inbox delivery, and neither an HTTP success nor a generated SDK call proves delivery (mw/apps/homepage/src/routes/_home.assistenter.tsx;mw/apps/dashboard/app/routes/{dashboard.opptaksperioder._index,dashboard.soknad._index,dashboard.tidligere-assistenter._index,dashboard.opptaksutfall._index,dashboard.opptaksutfall.maler}.tsx;mw/packages/domain/src/{admission-period/effects,application/effects}.ts;mw/docs/system.md:200-218).
Policy is settled by spec 0027 and is not a gate here. The accepted revision has the two-value outcome, the separate announcement and the editable templates; this spec migrates that behavior unchanged. Do not reintroduce Substitute as an outcome, a role or a coverage kind. Dated per-absence school coverage stays Placements-owned and out of this slice, apart from the one added placement.person-rejected problem code that changed ten placements operations' OpenAPI bytes (mw/packages/http-api/src/placements.ts:81; docs/specs/0027-admission-outcome-policy.md).
Unmerged proposals are not an implementation shortcut
Read-only live refs at design time: fix/admissions-core-0926 at 5d05e6e2 (unpublished); fix/admissions-affiliation-sub at db4ce5ac (unpublished); fix/admissions-onboarding-sub at 0ae7a9eb (unpublished); fix/admissions-ownership-sub at 9f7c48da (unpublished); fix/admissions-returning-sub at b7a52b86 (unpublished); related fix/recruitment-review-0926 at 27265998 (unpublished). None is merged into mono-web main or the accepted integration. core proposes shared readiness evidence for onboarding and Placements, placement-coherent outcomes, a current-semester Substitute read, returning identity/replay and new problem codes. Its Substitute and outcome proposals are superseded by the operator's correction in spec 0027. The subrefs propose overlapping but nonidentical ownership/returning/onboarding/affiliation changes. Core and subrefs share patch IDs/blobs; their migration numbers 0081–0084 collide among them, with Recruitment's proposed interview-correction fence and now with the accepted 0081-admission-outcome-policy.sql and 0082-admission-outcome-mail.sql. Core's old local proof note is not full just test, both PostgreSQL majors, hosted CI or approval. Treat every branch diff as research only, not a schema migration, receipt policy, new endpoint or source for generated parity. Compare exact tips again after approval and do not merge/cherry-pick wholesale (mw/packages/database/src/admissions/outcome.ts; branch-only fix/admissions-core-0926:docs/specs/admissions-core-review-fixes.md:42-101; docs/research/monoweb-branches.md:57-78; mw/AGENTS.md#construction-over-trust).
Proposed generated seam
Follow the current Profile/Directory/SocialEvents/Content/TeamApplications/Organization/Contact pattern: one declaration-only mw/packages/http-api/src/admissions.effx.ts and one admission-outcomes.effx.ts, each with a distinct Http.group on ExternalNativeApi, plus app-owned adapters re-exporting the existing SchemaRefs, ordered problem tuples, native header schemas (including PrivateEntityMutationResponseHeaders), resolver symbols and metadata/access annotators. Defaults contain only facts shared by every operation in that group. In particular, admissions cannot have group-wide PersonSecurity: four public endpoints use None/Anonymous, while six staff operations use Person, endpoint-local capability/resolver/time; admissionOutcomes uses the current Person credential alternatives throughout, but per-operation capability, requirement, resolver, decision time and command headers still differ, and the three problem unions differ by endpoint. Choose Operation.query for actual read-only behavior and Operation.command for mutations; HTTP POST alone is not evidence of a read or a grant. Of the 18 operations, 11 are Queries and 7 are Commands (submitApplication, createAdmissionPeriod, reviseAdmissionPeriod, registerReturningAssistant, decideOutcome, announceOutcome, saveOutcomeTemplate). previewOutcomeTemplate is the single Query-over-POST: it reads, carries a private draft and sample in the body, and writes no receipt, so it is a Query with payloadIsQuery: true under ADR 0010 and EFFX2402, not a Command. Exact paths, group titles, statuses, conditional branches, middleware and annotated schemas remain owned by the native contracts. This is a proposed seam, not committed files (mw/packages/http-api/src/{organization.effx,organization-effx-adapters,contact.effx,contact-effx-adapters,admissions,admission-outcomes}.ts; docs/decisions/{0004-schema-ref,0006-query-is-a-semantic-claim,0010-query-over-post}.md; mw/apps/backend/.effx/generated/organization-handlers.ts).
Add both declarations to the same contract and backend compiler projects that currently emit seven groups (mw/packages/http-api/tsconfig.effx.json, mw/apps/backend/tsconfig.effx.json). Bind the generated typed raw factories at all 18 existing handler functions in mw/apps/backend/src/admission/{http,http-reads,http-commands,outcome-http,outcome-template-http}.ts, with deferred guards at each existing access decision, preserving public rate limit, snapshot, domain and transactional receipt order (the outcome commands authorize before identity and receipt lookup; a generated guard may not move that). Replace both handwritten HttpApiGroup registrations in the root, migrate all backend/homepage/dashboard/type-test imports to the generated canonical exports, and remove obsolete uppercase endpoint names, comments, duplicate tuples and handwritten HttpApiBuilder.group branches in one clean cutover. No aliases, dual router, test-only SDK echo, hand-edited .effx/generated output or effx runtime DI (mw/packages/http-api/src/{api,index,admissions,admission-outcomes}.ts; mw/apps/backend/src/admission/{http,outcome-http}.ts; mw/apps/{dashboard,homepage}/; mw/packages/sdk/src/effect-client.ts).
Declarations no accepted slice has generated yet. Observed in the accepted generated contracts, none uses these three; each is a first use and must be proved by its actual strict diagnostic and a minimal real reproducer, not assumed: (1) a PUT endpoint (saveOutcomeTemplate; the emitter documents HttpApiEndpoint.put and the transport table maps Http.Put, but no mono-web group emits one); (2) a Query-over-POST with payloadIsQuery: true (previewOutcomeTemplate; covered by compiler tests, never used by a generated mono-web group); and (3) an access with alternative capabilities (Capability.any("admissions.outcomes.read", "admissions.outcomes.announce") for the two board reads; the IR carries One | Any but no accepted declaration uses Any). The anonymous commands have a generated precedent in SubmitTeamApplication (None credential, Command), but Admissions' body bound, origin policy and public rate limit before body parsing and receipt lookup are application code that must be observed unchanged. A gap is reported to the effx owner with its diagnostic and reproducer. It is never worked around with a hand-maintained output, a relaxed --strict-access, or a declaration that misstates a read as a command.
The pack is already vendored. Mono-web tools/effx-dist/manifest.json pins e04c8d2a16636f81e65d478a4d06a07933f6896b (four tarballs, with snapshotDecisionForCommand and the S5 generator change). Effx main has not changed the generator or extension source since that commit; it added the Cedar projection, which does not affect generated output. Before implementation, compare the then-current effx main against the mono-web pin; if a later change under the generator or the extensions lands, re-vendor the exact clean pack with effx's committed scripts/pack.ts output and mono-web's lockfile procedure, never a guessed or floating package, and repeat the clean-revision gates. Admissions needs no snapshot-decision opt-in: all seven Commands decide in a Transaction (docs/decisions/0013-capability-only-command-snapshot-decisions.md; mw/tools/effx-dist/manifest.json; docs/research/contact-slice-evidence.md).
Implementation and acceptance gates - after approval only
- Source/pack preflight and real defects. Obtain explicit operator approval of this design before implementation. Inspect the then-clean mono-web integration revision (the 0027-accepted tip or its descendant; operator-owned
mainread-only), current Admissions branch diffs, compiler manifest and frozen Bun install in its owndevenv shell. Regenerate the existing seven groups only, in both compiler projects, and require the baseline hashes: complete external OpenAPI SHA-2569e0b0035244ca89c758455b3bc7c44dc178a2b8ca78ad828ff4c2cb2475b2e2c(6,325,574 bytes), complete SDK index SHA-256df0a424f5d685bb5bc27da33241536cf5425049f3367c1eb8a73d383df240ba4(18,784 bytes), 111 external IDs and one internal exclusion, and the seven-group semantic hash2901cf833cad441f246e430c942a6441ac7959a44afb36e3a5a98e700e17190e(identical at the Contact revision and at the 0027 tip, because the repair was handwritten). If a true accepted-source defect is observed, commit a focused browser/real-PostgreSQL regression red at the source before a separate repair, with supported-major upgrade proof for any migration; do not import conflicting numbered migrations or silently alter app policy. A compiler gap needs its actual strict eighteen-declaration diagnostic and a minimal real reproducer reported to the effx owner (mw/AGENTS.md#verification-and-resources;docs/specs/0004-monoweb-migration-plan.md:324-327). - One IR, two existing groups, exact SDK bytes, semantically equal OpenAPI. Run
just effx-nativeand strict contract and raw-handlereffx checkwith--strict-access. Both projects must share one semantic hash; it changes from the seven-group baseline only because two groups are added. Generate the entire external OpenAPI and SDK from mono-web's recipes, count 111 external operation IDs, the exact tenadmissions.*and eightadmissionOutcomes.*, and the one separately excluded internal receipt. Compare the SDK index byte-for-byte against the baseline hash above. Compare the OpenAPI under a component-name-canonical digest, which renames every component to a name derived from its content, rewrites every$refand merges identical entries, so that any operation/path/status, short dynamic cache header, public conditional 304, required query/If-Match/idempotency, security, tagged response, schema or 17/24/18-code problem difference fails and only a component name does not (operator ruling, see below). Investigate every mismatch at source. The seven earlier groups' generated contract and handler files stay byte-identical to the accepted revision, not only a sampled Admissions subtree; remove all obsolete handwritten declarations, aliases and raw registration in the same source commit (mw/packages/http-api/src/{api,admissions,admission-outcomes}.ts;mw/packages/sdk/scripts/generate-operations.ts;mw/justfile:29-34). - Focused contract, authority, SQL and clients. Run native contract tests plus eighteen-operation declaration/adapter negative tests, generated SDK client/index tests (the SDK index test pins 111), existing
mw/apps/backend/src/admission/{admission-period.http,submit-application.http,outcome-policy.http}.test.ts,returning-registration.test.ts,mw/packages/database/src/{admission-period,application,admissions}focused suites including the outcome-policy upgrade proof on PGlite and PostgreSQL 17 and 18, and current homepage/dashboard route tests. Add a regression only for an observable omission, such as public 304 before authorization, owner leakage, no current authority on replay, a board leader without a delegation reaching decide, announce or a template, missing required scope, changed 201/200/304 headers (including the private/no-store announcement), stale ETag, duplicate applicant, returning identity, a preview that writes, or a board that discloses contact data to a coordinator. Use real disposable PostgreSQL where locks, races and receipt atomicity matter; do not replace it with PGlite alone or mocked SDK echoes. Keep source-based OpenAPI assertions derived from the canonical problem registry; no copied literal bounds or statuses (mw/packages/http-api/test/native-api.test.ts;mw/packages/sdk/src/__tests__/generated-native-client.test.ts;mw/apps/backend/src/admission/;mw/packages/database/src/admissions/;mw/AGENTS.md#verification-and-resources). - Every actual Admissions-facing journey, not a compiler fixture. Run
just e2e applicant(homepage public application/confirmation),just e2e admission-periods(scoped staff window create/revise), andjust e2e admission-outcomes(decide board, coordinator announcement, template preview/save, denial, replay, loopback HTML+text mailbox and browser) as the three central existing suites against generated groups, real Better Auth/Chromium/SQL and their original assertions. Runjust e2e recommendation-applicant-progressandjust e2e recommendation-returningfor the authenticated progress and returning surfaces; neither adds an Admissions Foldkit program. Inspect actual coverage of all 18 routes; strengthen an existing registered named runner where a route or denial/replay assertion is absent rather than treating three unrelated green summaries as proof. Keep accessibility, canonical browser origin, private/no-store responses, HTTP receipts and process/port cleanup.just e2e contactandjust golden school-serviceshare the built homepage Worker, generated SDK and the dated coverage that follows announcements;just e2e placement,just e2e onboardingandjust golden recruitmentare adjacent dependency regressions if shared readiness, account or placement seams change. They do not replace Admissions suites. Register any new journey withjust layout/hosted matrix (mw/justfile:127-160;mw/apps/homepage/e2e/run-real-public-applicant-admission.mjs;mw/apps/dashboard/e2e/{run-real-admission-period-management,run-real-native-admission-outcomes}.mjs;mw/apps/dashboard/e2e/native-admission-outcomes.spec.ts;mw/tools/acceptance/recommendation-check.ts;mw/AGENTS.md#delegation-and-landing). - Prior accepted and aggregate gates on one clean commit. On the same clean committed mono-web revision run the named Profile, Schools, SocialEvents, Content, TeamApplications golden (normal and its two expected intentional fault teardowns), Organization, Contact and receipt-owner cross-context journeys separately, plus full serial
just check --concurrency=1andjust test --concurrency=1. Includejust proof authorization-rulesandjust model checkif this slice actually changes relationship reach or the Alloy model; the transport-only design implies no authority-code change, and the accepted Alloy run (43 of 43 checks, 53 of 53 mutants, 30 of 30 scenarios) belongs to the handwritten repair, not to this migration. All heavy commands run indevenv shell, one machine-wide job at a time throughjust measure --class <class> -- ...(just modelmeasures itself). Record each command's exit, ledger wall, peak RSS, source revision, clean tree and unverified boundaries. Intentional fault exits 1/143 are never described as a passing normal golden (mw/AGENTS.md#verification-and-resources;docs/research/organization-slice-evidence.md). - Source accounting and custody. Baseline at the 0027-accepted revision: physical
wc -lof the ten transport filesmw/packages/http-api/src/{admissions,admission-outcomes}.ts(350 + 356) andmw/apps/backend/src/admission/{http,http-reads,http-commands,http-access,http-context,outcome-http,outcome-http-support,outcome-template-http}.ts(62 + 321 + 504 + 138 + 92 + 444 + 112 + 282) is 2,661. At the final accepted commit count the same files, new.effx.ts/adapters and every changed caller, affected domain/SQL and homepage/dashboard route/bridge responsibilities, and count generated contracts/handlers and whole OpenAPI/SDK separately. The renderer, validator, editor, mail worker and SQL stay maintained and are not transport. This is descriptive, not a LOC target. Keep runtime receipts/source manifests outside the public repo, publish only redactedmw/-relative findings in effx after a clean passing revision, and leave mono-web's operator-ownedmain, unmerged branch refs, provider, credentials and production writer untouched. Approval of this spec would authorize only local implementation/testing, not mono-web branch landing or deployment (docs/specs/0004-monoweb-migration-plan.md:305-336;mw/AGENTS.md#verification-and-resources).
Definition of done after approval: exactly the 18 external Admissions/Outcomes operations are generated and bound with current runtime guards, a byte-identical 111-operation SDK index, an external OpenAPI that is semantically equal to the accepted one under component-name canonicalization (one excluded internal receipt), the seven earlier groups' generated files unchanged, app-owned authority/SQL/receipts/templates/mail/UI, observed denial/stale/replay/privacy across all five named Admissions-facing browser/progress/returning journeys and adjacent affected contexts, and full check/test on one clean unpublished mono-web revision. This document itself claims none of those implementation checks passed. It authorizes no deployment, provider or credential change, mono-web landing or production cutover.
Operator rulings during implementation (2026-10-04)
- Pin. Implement against the pinned pack
e04c8d2a16636f81e65d478a4d06a07933f6896b; do not re-vendor during this slice, even if effx main lands more generator work. A later pack upgrade is its own slice with its own seven-group identity proof. - First uses. Probe
PUT, Query over POST andCapability.anyfirst, each alone with a minimal fixture against the pinned pack. A gap goes to the operator as a reproducer and strict diagnostic, never a workaround or a weakened contract. - One source for problem codes. The spread-composed outcome problem lists in
mw/packages/http-api/src/admission-outcomes.tsstay the single source. The declarations use flat literal tuples, and a type-level and runtime test compares each flat tuple with its composed source element for element and in order. The instance is recorded in mono-webSTATE.mdto retire when the effx frontend resolves spreads of const tuples. - OpenAPI gate. Whole-document byte equality is not the hard gate; wire, status, security and schema semantics are, and a
$refcomponent-name suffix swap is an accepted non-semantic projection delta (the SocialEvents precedent). The generator sorts a group's endpoints by key (packages/compiler/src/generate/http.ts:323) and Effect names colliding identifiers by encounter order, which swapsAdmissionPeriodManagementItemandAdmissionPeriodManagementItem_1at three$refsites with identical contents. The test canonicalizes component names, with negative controls. The SDK index stays byte-identical, and the new whole-document SHA-256572094dafeef770356b3e18c25ac2a7e40280142dcf2d6e9e31320f5281f2d7dis the post-0026 baseline. Deterministic content-derived component naming, or declaration-order emit, is a compiler improvement for a later pack.