Files
atomic-design-poc/docs/project/archive/refactor-backlog-setup/refactor-backlog/implementation/rb-21.md
T
ehoandClaude Opus 5 12f17d9d73 docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every
WP done) and `docs/project/refactor-backlog-setup/` (the arc before it).
Move both under `docs/project/archive/` with `git mv`, so history stays
intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them,
because it points at the now-archived backlog README.

Add `docs/project/archive/README.md`. It states that these trees are
historical and names the two directories that are still live.

Repoint every inbound reference named in RD-30's Files table: CLAUDE.md,
the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the
`document-feature` and `new-ssp` skills, and the readable-codebase PLAN,
README, and RD-19 ticket. Fix two upward-relative links inside the moved
WP files (WP-68, WP-69) that gained a directory level and would otherwise
break. Repoint `.prettierignore`'s two agent-prompt exclusions to their
new path, so prettier keeps leaving those files' exact wording alone.

Mark RD-30 done and check off its acceptance criteria; flip its README
row to done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:00:38 +02:00

7.4 KiB

RB-21 — extract the read half of createDraftSync into find-concept.ts

Status: implemented · 2026-08-27 · Source finding: 04-cqrs-light.md CQ-001 · 00-baseline.md §4a (createDraftSync 143 lines, the largest function in the repo), §9 (fn > 40 threshold) · 99-backlog.md RB-21

What was wrong

createDraftSync (apps/ssp/src/app/registratie/application/draft-sync.ts) was registered as a command factory but owned three read paths (load, findConcept, and the read half of resume) mixed into the same function as the write path (ensureId, flush, submit, reset). CQ-001 named three pieces of shared mutable closure state — id, ensuring, resumeGate — as load-bearing: resumeGate exists only so the write path (ensureId) can wait for the read path (resume) to finish. That coupling is genuine and stays in place.

What changed

CQ-001's proposal, applied as a pure move, no redesign.

File Change
apps/ssp/src/app/registratie/application/find-concept.ts (new) findConcept(adapter, type) and loadConcept(adapter, id) — free functions taking ApplicationsAdapter, no inject(). loadConcept returns a LoadedConcept union ({tag:'concept', draft} | {tag:'not-concept'}) instead of the boolean-shaped branching the inline version had.
apps/ssp/src/app/registratie/application/find-concept.spec.ts (new) Direct spec, no TestBed — a fake ApplicationsAdapter object passed straight to the functions.
apps/ssp/src/app/registratie/application/draft-sync.ts Removed the inline findConcept closure and the body of load; both now call the free functions. createDraftSync keeps id, ensuring, resumeGate, and the whole write path, unchanged.
libs/shared/docs/behaviour-spec.mdx Regenerated (npm run gen:behaviour-spec) — picks up the new find-concept.spec.ts describe blocks.

createDraftSync shrank from 187 lines (export function createDraftSync to its closing brace, HEAD~1) to 169 lines. The whole file went from 236 to 216 lines.

The two call sites that used the old inline findConcept() now pass the adapter and type explicitly:

// ensureId's 409-recovery catch (WP-35)
const existing = await findConcept(adapter, deps.type);
// resume(), no ?aanvraag in the URL
const existing = await findConcept(adapter, deps.type);

load keeps setting the closure id and calling applyResume (both closure-dependent), but delegates the actual read to loadConcept:

const load = (linked: string): Promise<void> => {
  id = linked;
  return loadConcept(adapter, linked).then((result) => {
    if (result.tag === 'not-concept') {
      id = undefined;
      applyResume(null);
      return;
    }
    applyResume(result.draft);
  });
};

draft-sync.spec.ts — unchanged

draft-sync.spec.ts was not edited. It never called resume()/load() directly — its coverage is the debounce, submit() (including the 409-recovery path, which exercises the extracted findConcept indirectly through ensureId's catch), and flushPending. All of that stayed in createDraftSync, so the spec is unchanged and still exercises the wiring between createDraftSync and the two new free functions (the 409-recovery test would fail if that wiring were wrong). It passed unchanged, 8/8.

The new spec, and its verified red

find-concept.spec.ts covers the branches CQ-001 named:

  • findConcept: match found → id returned; no match of that type → undefined; match found but not Concept status → undefined; adapter.list() resolves to an unparsable shape (parseApplications fails) → undefined; adapter.list() rejects → undefined.
  • loadConcept: Concept with a draft → {tag:'concept', draft}; Concept with no draft → {tag:'concept', draft:null}; a non-Concept status (e.g. Ingediend, submitted) → {tag:'not-concept'}; adapter.detail() rejects (unknown/deleted id) → {tag:'not-concept'}.

Verified red without the fix. Used Edit (not git checkout) to invert one condition in loadConceptdto.status.tag !== 'Concept'dto.status.tag === 'Concept' — reran ng test ssp --include find-concept.spec.ts. Result: 3 of 9 tests failed —

loadConcept > reads the draft off a Concept
  AssertionError: expected { tag: 'not-concept' } to deeply equal { tag: 'concept', draft: { step: 1 } }
loadConcept > reports a missing draft as null
  AssertionError: expected { tag: 'not-concept' } to deeply equal { tag: 'concept', draft: null }
loadConcept > reports not-concept when the id has moved past Concept (submitted)
  AssertionError: expected { tag: 'concept', draft: null } to deeply equal { tag: 'not-concept' }

Then used Edit again to flip the condition back to !==, reran the same command: 9/9 green. findConcept's and loadConcept's other branches were not separately mutated — the inverted condition alone was enough to prove the spec is sensitive to the extraction being correct, and re-verifying full green after the revert confirmed no collateral change was left in the file.

Scope held

  • No restructuring of the write path (ensureId, flush, submit, reset) — untouched beyond the two call-site updates shown above.
  • applications.adapter.ts was not split (CQ-002's "Not filed" note rules that out for this design; out of scope here regardless).
  • resume()'s semantics (URL-param precedence, the resumeGate release-in-finally, the navigate-to-stamp-the-id side effect) are unchanged — only its two findConcept()/load() calls now go through the free functions.
  • No wire change, no DTO change, no behaviour change.

Verification

npm run ci (foreground): green — lint, typecheck, dep:check, format:check, check:tokens, check:seam, tests (ssp includes find-concept.spec.ts 9/9 new, draft-sync.spec.ts 8/8 unchanged), ng build --localize (both apps), npm audit, backend dotnet test (the known OpenZaakIntegrationTests.Admin_cases_… container-dependent failure is expected and outside npm run ci's scope), gen:snippets drift clean, gen:behaviour-spec drift clean once the regenerated file is committed alongside the code. Full counts are in the commit's npm run ci run — see the session note for the exact step-by-step output.