Commit Graph
253 Commits
Author SHA1 Message Date
ehoandClaude Sonnet 5 42f7bd651d test(e2e): isolate runs and identities without a new backend endpoint (WP-74)
The three specs shared one mutable backend and said so in their own
comments ("Restart the backend between CI runs"). WP-70 recorded the fix as
a dev-only seed endpoint; it isn't needed. The DB path already routes
through IConfiguration, so playwright.config's webServer hands the backend a
throwaway SQLite file per invocation — the same trick TestWebApplicationFactory
already uses, with zero backend change. And StubIdentityProvider already
honoured X-Subject; the only gap was that nothing sent it. That matters
because the backend has no IsDevelopment() gate anywhere, so a seed endpoint
would have had to invent the codebase's first environment gate.

subjectInterceptor mirrors the existing roleInterceptor and is wired into the
same isDevMode()-only list. Interceptors alone were not enough: the raw XHR
upload and the hand-written letter-preview fetch bypass Angular's chain (as
CLAUDE.md documents), so both now stamp X-Subject explicitly — without that,
every uploaded document still landed under DemoOwner.

reuseExistingServer stays on: flipping it would break local runs for anyone
already serving the docker stack. Each run gets a unique DB filename and
global-setup sweeps only prior runs' leftovers — deleting a fixed path
mid-run risks SQLite silently recreating an empty, unmigrated file under
fullyParallel.

Verified: e2e passes twice back-to-back with no backend restart, and
X-Subject was observed on a real request, not merely wired.

brief-v2.spec.ts keeps the shared identity for now — see the KNOWN GAP note;
a backend staleness bug makes /brief/preview return a sent letter with the
draft watermark for any non-DemoOwner BSN. actors.ts reserves the actor for
whoever fixes it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 16:32:07 +02:00
ehoandClaude Sonnet 5 6bc00a917c refactor(backend): delete dead legacy endpoints, make domain types unions (WP-72 + WP-73)
Two work packages in one commit because both edit Program.cs and splitting
them would leave a commit that does not build.

WP-72 — deletes POST /api/v1/intakes and /herregistraties. Both were dead
from the UI (the wizard submits via /applications/{id}/submit) and strictly
less capable: they minted a bare reference and wrote no Aanvraag, made no
ZGW call, and did no document-ownership check. The shared Submit(...) helper
survives — /registrations and /change-requests still use it. WP-69 hardened
/intakes with a 400 last session; removing the surface is the stronger fix,
and WP-69's /applications/{id}/submit enforcement is untouched.

WP-73 — RegistrationStatus becomes an abstract record with three sealed
variants behind a private base ctor, so only Geregistreerd carries a
herregistratie deadline and reden is required on Geschorst/Doorgehaald
(matching the FE union, which was already right). HerregistratieRule
.IsStatusConsistent and its test are deleted: the type now guarantees what
the runtime check was for, and the test could no longer construct the
illegal state it existed to catch.

Aanvraag splits into a Concept | Submitted | Decided union with the EF row
demoted to AanvraagEntity behind a two-way mapper. Submitted carries a
non-null Referentie and SubmittedAt, and Decided.Afgewezen/MeerInfoGevraagd
require a Toelichting — so the five Referentie! null-forgiving derefs in
StatusAt are gone, not merely suppressed. IZaakSource.CreateZaak narrows to
Aanvraag.Submitted, removing the same class of deref in both zaak sources.

Draft is now cleared on submit rather than lingering: ApplicationStore's
doc-comment claimed "Concept only" but Submit never cleared it. Verified
nothing reads a submitted aanvraag's draft (draft-sync's applyResume only
resumes unsubmitted wizards), so the comment is now true instead of
aspirational.

No migration, no schema change, no wire change — RegistrationStatusDto and
the application DTOs are byte-identical, confirmed against a live swagger.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 16:31:38 +02:00
ehoandClaude Sonnet 5 edaf1360c5 docs: close WP-69, record verification result
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 1m6s
CI / frontend (push) Successful in 2m19s
CI / backend (push) Successful in 1m53s
CI / e2e (push) Successful in 3m13s
CI / semgrep (push) Successful in 1m4s
CI / api-client-drift (push) Successful in 1m51s
CI / storybook-a11y (push) Successful in 10m43s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 22:44:36 +02:00
ehoandClaude Sonnet 5 5d73ca21f6 feat(backend): enforce the scholing threshold server-side (WP-69)
ADR-0001's own canonical "config value" example was unenforced: GET
/intake/policy echoed ScholingThreshold, but no request DTO carried a
scholing answer, so the server had nothing to re-validate. A crafted
POST could skip a requirement the wizard presents as mandatory.

IntakePolicy.RejectIncompleteScholing is the authority — three-valued
completeness (below threshold an answer is required; "nee" is legal and
still submits; punten only belong to a followed scholing), living in the
class that owns the constant so scripts/check-seam.sh keeps guarding the
FE/BE literal pair. Both submit paths call it; a violation 400s with
ProblemDetails and leaves the aanvraag a Concept. Gated on
Type == "intake" (the endpoint's switch lumps herregistratie with
intake, which has no scholing question), and guarded by `reject is null`
so a zero-uren submission is still decided on its merits.

Also fixes a live FE bug in the same rule: validateStep required punten
whenever scholingGevolgd was 'ja' regardless of lageUren, while the
template renders those fields only when lageUren — so answering 'ja'
then raising uren either blocked the user on an invisible field or
emitted aanvullendeScholing: undefined alongside punten. punten now
derives from aanvullendeScholing, so that combination is unrepresentable
in ValidIntake.

Note: EndpointTests' Worked_hours_submission_succeeds was itself
asserting the vulnerable payload ({ uren: 40 }, no answer) and needed a
complete answer added; the zero-hours rows are the ordering regression
net and are unmodified.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 22:42:14 +02:00
ehoandClaude Sonnet 5 9da385311d docs: close WP-71, record verification result
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 1m11s
CI / frontend (push) Successful in 2m30s
CI / backend (push) Successful in 1m54s
CI / e2e (push) Successful in 3m9s
CI / semgrep (push) Successful in 1m7s
CI / api-client-drift (push) Successful in 1m53s
CI / storybook-a11y (push) Successful in 10m55s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 20:28:03 +02:00
ehoandClaude Sonnet 5 3652ff8d3f docs(test): make Given/When/Then the default BDD structure (WP-71)
bdd.mdx previously banned "Given/When/Then ceremony" outright, which
directly contradicted WP-70's own acceptance tests (Acceptance/
BesluitLifecycleTests.cs already used // Given/When/Then comments) and
the backend's organically-evolved PascalCase_snake_sentence convention,
which the doc gave zero guidance for. Reverses that rule: every test is
now structured Given -> When -> Then, with a genuinely empty phase
omitted rather than faked; present-tense declarative naming and the
one-behaviour-per-test rule are unchanged. ADR-0006 gets a cross-reference
so both documents agree everywhere, not just in acceptance tests.

Also closes out the doc's other named-but-unenforced rules found by the
audit: fixes the 5 files asserting rendered $localize copy instead of
the underlying tag/message-id (the compliant pattern already existed in
werkvoorraad-item-view.spec.ts), splits the multi-behaviour titles the
doc itself calls a smell (";", "and", "/"), and fixes bdd.mdx's own false
citation of registratie-wizard.machine.spec.ts as "one transition per
test" by actually splitting that test into one-transition-per-test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 20:25:30 +02:00
ehoandClaude Sonnet 5 306d002221 docs(test): generated living behaviour spec + FE/BE seam drift check (WP-71)
Gherkin/Cucumber was considered and rejected for business-readable BDD
scenarios: step-binding by runtime string match undoes the compile-time
guarantees WP-70 just added, and needs two frameworks for .NET+TS with
no non-technical co-author in view. Instead scripts/gen-behaviour-spec.mjs
(modeled on the existing gen-snippets.mjs) extracts every describe/it
and [Fact]/[Theory] name straight from the real suites into
libs/shared/docs/behaviour-spec.mdx, gated for drift in CI exactly like
gen-snippets/gen-api — the page can never diverge from the tests because
it's generated from them, and test names stay the single source of truth.

scripts/check-seam.sh guards the one FE/BE rule duplication most likely
to silently diverge: IntakePolicy.cs's ScholingThreshold vs
intake.machine.ts's SCHOLING_THRESHOLD_DEFAULT, two unlinked literals
pinned separately in each side's own tests but never against each other.

package.json/CI wiring for both (gen:behaviour-spec, check:seam) shipped
in the prior commit alongside the typecheck gate, since all three touch
the same few config files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 20:25:17 +02:00
ehoandClaude Sonnet 5 28c0a250e7 test(backend): split RuleTests.cs by aggregate, refresh DDD doc (WP-71)
RuleTests.cs held five aggregates' rules as nested classes in one file,
misaligned with Domain/<Aggregate>/ and with the Acceptance/Builders/
folder convention WP-70 started. Split into Domain/<Aggregate>RuleTests.cs
(pure move — same names, same bodies, same count) plus a new
ApplicationRuleTests.cs (the enum invariant moved out of the
WebApplicationFactory-booting ApplicationTests.cs, since it's a pure
Enum.GetNames check with no business needing a web host) and
OrgTemplateRuleTests.cs (RejectDraft had no direct unit test before,
only endpoint coverage).

libs/shared/docs/layers.mdx still taught the pre-WP-67 shape (six
contexts, no apps/libs split, enforcement via ESLint) — updated to the
real monorepo structure and to dependency-cruiser as the actual
enforcement mechanism. Adds specs for registration.policy.ts's
isStatusConsistent (untested; its backend mirror is) and both apps'
auth/domain/session.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 20:25:05 +02:00
ehoandClaude Sonnet 5 b937e55ad3 test: close illegal-state escape hatches in spec type-safety (WP-71)
ESLint blanket-exempted every *.spec.ts from the any ban, and no gate
type-checked spec files at all (ng test is transpile-only), so a wrong
cast in a test could never fail the build. 76 `as any` + 12 `as
Extract<>` state-narrowing casts in the three biggest wizard specs read
one variant's fields off a whole-union value: if the reducer returned
the wrong variant, the assertion silently read undefined instead of
failing.

expectTag(state, tag) (libs/shared/src/testing/expect-tag.ts) asserts
and narrows in one call, replacing every one of those casts. Removes
the spec-file any exemption, adds `npm run typecheck` (tsc --noEmit
over each project's tsconfig.spec.json) to CI, and forbids production
code from importing libs/shared/src/testing via dependency-cruiser.
Backend: AanvraagBuilder now models ZaakUrl (closing the last
post-Build() mutation) and guards AtStep; null-forgiving `!` on
endpoint assertions replaced with Assert.NotNull so a null DTO fails by
name, not NullReferenceException.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 20:24:53 +02:00
ehoandClaude Sonnet 5 66224b1644 docs: record WP-70 commit range
CI / changes (push) Successful in 10s
CI / lint (push) Successful in 1m58s
CI / frontend (push) Successful in 3m40s
CI / backend (push) Successful in 2m6s
CI / e2e (push) Successful in 3m36s
CI / semgrep (push) Successful in 1m4s
CI / api-client-drift (push) Successful in 1m48s
CI / storybook-a11y (push) Successful in 13m50s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 15:31:32 +02:00
ehoandClaude Sonnet 5 a82332fa20 docs: ADR-0006 test-data builders, close out WP-70
Writes up the principle behind WP-70's three tracks ("build test data
through the same door production code uses") as ADR-0006, with a decision
table for which fixture idiom fits which test type. Updates the
test-strategy skill (adds the Fixtures rule, fixes its stale pre-monorepo
src/app/... worked-example paths) and the shared Storybook testing.mdx page
to match. Closes WP-70 with the signatures/counts as actually shipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 15:31:20 +02:00
ehoandClaude Sonnet 5 a7bf228ca8 test(e2e): shared Actors/SeedRefs/loginAs, kill duplicated magic strings (WP-70)
The demo BSN, password, and DigiD login sequence were copy-pasted verbatim
into all three specs; the diploma id #diploma-d1 was coupled to SeedData.cs's
ordering by comment only, with no compile-time check if the seed shape
changed. e2e/support/actors.ts names both: Actors.zorgverlener + loginAs()
for the login sequence, SeedRefs.diplomaZonderPolicyVragen for the seed
coupling (with the "why d1" reasoning attached to the name, not scattered
across specs). Zero assertions changed — pure extract-and-rename of test
setup. e2e test-isolation (the shared mutable backend) is a documented
follow-up, not fixed here — see ADR-0006.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 15:31:12 +02:00
ehoandClaude Sonnet 5 a73a1c6f1e test(frontend): replay real messages instead of hand-built state literals (WP-70)
Every machine spec redefined its own throwaway fixture helper (editing1/2/3,
editingWith), hardcoding fields like errors: {} that assert against shapes
the reducer may never actually produce. given(reduce, initial)(...msgs)
(libs/shared/src/testing/machine.ts) replaces them by replaying real Msgs
through the real reduce, so a fixture is provably reachable. Adds the same
idiom for value objects (unwrapOk) and RemoteData (loading/success/failure),
plus intake.acceptance.spec.ts as a worked full-journey example.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 15:31:06 +02:00
ehoandClaude Sonnet 5 2eea860efe test(backend): type-state Aanvraag builder, illegal fixtures unrepresentable (WP-70)
RuleTests/OpenZaakZaakSourceTests hand-built Aanvraag fixtures by initializer,
keeping Submitted/Referentie/SubmittedAt/BesluitStatus consistent by hand. A
type-state builder (Given.Concept().Submitted().Decided()) makes an illegal
sequence a compile error instead, and delegates the toelichting-required rule
to the real BeoordelingRules so it can't drift from production. Adds
BesluitLifecycleTests covering the WP-68 besluit invariants end to end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 15:30:57 +02:00
ehoandClaude Opus 5 868fb55783 docs: close WP-68, record verification result
CI / changes (push) Successful in 9s
CI / lint (push) Successful in 1m0s
CI / frontend (push) Successful in 2m43s
CI / backend (push) Successful in 2m14s
CI / e2e (push) Successful in 3m22s
CI / semgrep (push) Successful in 1m12s
CI / api-client-drift (push) Successful in 2m4s
CI / storybook-a11y (push) Successful in 11m46s
npm run ci passed fully green (lint, format, tokens, all four test suites, both
localized builds, audit, backend dotnet test at 216 passing, snippet + api-client
drift checks). npm run e2e could not be verified this session: port 4200 was
occupied by an unrelated container from a different repo, so Playwright reused it
instead of starting this app — a pre-existing local port collision, not a
regression, and not part of the local GREEN gate per CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:47:35 +02:00
ehoandClaude Opus 5 472a49f19f docs(backend): correct IntakePolicy's false re-validation claim (WP-68 F5)
The doc-comment claimed "the backend re-validates on submit as the authority" —
it doesn't. Neither SubmitApplicationRequest nor IntakeRequest carries a scholing
answer at all, so there's nothing to re-validate; both submit paths only apply
SubmissionRules.RejectZeroUren. A crafted POST can bypass the scholing requirement
entirely. States the gap and points to WP-69 (opened, not yet planned) for the
enforcement, which needs a wire change.

Also strengthens the F2 concurrency test to assert the persisted status matches
whichever request actually won the race, and updates WP-68's own Decisions/
acceptance-criteria text to reflect two implementation choices that improved on
the original write-up once real constraints surfaced: ProcessingWindow stays on
ApplicationStore (StatusAt is already in the same file), and AanvraagStatusTag is
not given a Concept member (would have broken
AanvraagStatusTag_covers_the_published_lifecycle) — AanvraagStatus.Tag is nullable
instead, null exactly for Concept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:42:21 +02:00
ehoandClaude Opus 5 31d4aa1848 test(backend): cover WP-68 F2/F6/T3 (besluit concurrency, toelichting rule, transitions)
- Concurrent_besluiten_on_the_same_aanvraag_yield_exactly_one_success: races two
  besluiten on the same open aanvraag, asserts exactly one 200 and one 409 — the
  behavior F2's in-lock guard exists to guarantee.
- Only_a_non_approval_requires_a_toelichting: unit test for
  BeoordelingRules.RequiresToelichting (F6).
- A_terminal_decision_refuses_any_further_besluit /
  MeerInfoOpvragen_is_not_terminal_a_further_besluit_is_still_legal: the transition
  table at the aggregate level (T3) — an Aanvraag whose BesluitStatus already records
  a decision computes a terminal StatusAt, and CanDecide refuses a further besluit,
  independent of the endpoint-level equivalent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:38:30 +02:00
ehoandClaude Opus 5 fc6e73806a refactor(backend): move aanvraag status lifecycle into the domain (WP-68 F3)
The status was derived in Contracts/Mappers.ToStatusDto, not the domain; Concept was
a magic "Concept" string with no AanvraagStatusTag member; and the besluit endpoint
re-derived its own guard by reading the status back out of the DTO and Enum.Parse-ing
it. New Domain/Applications/AanvraagStatus.cs models the full status (Concept
included, via a null Tag rather than a sixth enum member) as a closed type,
constructible only through its factories. Aanvraag.StatusAt(now) carries the logic
verbatim; Mappers.ToStatusDto and ZgwZaakMapper's two status producers become
one-line projections onto the same wire DTO, so the wire shape is unchanged (gen:api
shows zero diff beyond F1's). The one remaining Enum.Parse (the beoordeling GET,
which crosses the IZaakSource wire boundary) is now non-throwing on an unrecognised
tag.

Also, WP-68 F2: the besluit transition-legality check now runs inside
ApplicationStore.RecordBesluit's write lock instead of in the endpoint beforehand —
two concurrent besluiten used to both pass the check before either wrote, letting
the second silently overwrite a terminal decision. RecordBesluit returns an
Ok/NotFound/Conflict outcome, mirroring DocumentStore.DeleteResult.

Also, WP-68 F6: the "toelichting required" rule moves from an inline endpoint check
into BeoordelingRules.RequiresToelichting, alongside CanDecide.

The three tests naming this refactor's regression net
(AanvraagStatusTag_covers_the_published_lifecycle,
AutoApprovable_flips_to_goedgekeurd_after_the_window, ZgwZaakMapperTests) pass
unmodified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:34:10 +02:00
ehoandClaude Opus 5 fd04221d2f chore(api): regenerate client for F1's new 400 responses
npm run gen:api after WP-68 F1 — draft-sync and submit now document their
document-ownership 400 ProblemDetails response.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:33:50 +02:00
ehoandClaude Opus 5 a394950a1d fix(backend): reject foreign documentIds on submit and draft-sync (WP-68 F1)
submit and draft-sync took document ids straight from the request body with no
ownership check: a caller who knew a foreign document's id could attach another
citizen's upload to their own aanvraag (surfacing on the behandelaar's beoordeling
screen, POSTed to OpenZaak as their zaakinformatieobject) and permanently block the
victim's own delete by flipping Linked=true. ADR-0001 holds the FE has no authority;
this trusted it anyway.

Adds DocumentStore.ForeignIds(ids, owner) and calls it from both write paths before
any write, 400 ProblemDetails on a mismatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:24:32 +02:00
ehoandClaude Opus 5 6a4a0ad435 docs: add WP-68, aggregate invariants + status modelling
Architecture review found the context boundaries, FP/TEA idioms and read/write
separation sound, and rejected explicit CQRS as the fix for anything found. It
located four real defects clustered in one place: the backend's aggregate roots
don't guard their own invariants, and the aanvraag status lifecycle is a computed
string living in the contracts layer instead of the domain. Full Decisions block
pre-made so implementation can proceed without re-litigating scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:20:55 +02:00
ehoandClaude Opus 5 6cfd70eeeb fix(backend): resolve besluit endpoint's id via Referentie, not local PK
POST /beoordeling/{id}/besluit always 404'd against a real OpenZaak: {id} is the
FE-facing case id from IZaakSource.ListCases, which under OpenZaakZaakSource is the
ZGW zaak's own uuid, not ApplicationStore's primary key. Resolve the case through
ListCases first (same seam the GET sibling already uses), then to the local Aanvraag
via its Referentie — the one identifier stable across both sources.

Adds ApplicationStore.GetByReferentie and a regression test that reproduces the
divergence with a decorating IZaakSource test double instead of a live OpenZaak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:20:36 +02:00
eho d2c2cffc1f Merge pull request 'Behandelportal: monorepo merge + WP-64..67 backoffice arc (OpenZaak write closes it out)' (#1) from monorepo-behandelportal into main
CI / changes (push) Successful in 9s
CI / lint (push) Successful in 56s
CI / frontend (push) Successful in 2m40s
CI / backend (push) Successful in 2m6s
CI / e2e (push) Successful in 3m20s
CI / semgrep (push) Successful in 1m11s
CI / api-client-drift (push) Successful in 2m10s
CI / storybook-a11y (push) Successful in 11m59s
2026-08-04 12:56:33 +00:00
ehoandClaude Sonnet 5 a2e301060e docs: record WP-66 commit hash
CI / changes (pull_request) Successful in 18s
CI / lint (pull_request) Successful in 1m9s
CI / frontend (pull_request) Successful in 3m28s
CI / backend (pull_request) Successful in 2m59s
CI / e2e (pull_request) Successful in 4m33s
CI / semgrep (pull_request) Successful in 1m23s
CI / api-client-drift (pull_request) Successful in 2m25s
CI / storybook-a11y (pull_request) Successful in 14m48s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 09:41:38 +02:00
ehoandClaude Sonnet 5 d996ca2463 feat(behandelportal): WP-66 wire the decision into OpenZaak
Extends IZaakSource with RecordBesluit, mirroring WP-50's CreateZaak write
pattern: OpenZaakZaakSource POSTs a new Statussen entry (highest-volgnummer
statustype, since the harness catalogus has no per-outcome besluittype),
carrying the besluit + toelichting in statustoelichting; LocalZaakSource
no-ops. The beoordeling endpoint calls it after the local decision commits,
flagging a failure via RecordZgwDivergence the same way submit's
create-zaak/document writes do — closing WP-60's "second write pair" gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 09:41:28 +02:00
ehoandClaude Sonnet 5 39409bdf76 ci: fix test-storybook config-dir + add missing behandelportal a11y coverage
CI / changes (pull_request) Successful in 17s
CI / lint (pull_request) Successful in 56s
CI / frontend (pull_request) Successful in 2m31s
CI / backend (pull_request) Successful in 1m57s
CI / e2e (pull_request) Successful in 3m32s
CI / semgrep (pull_request) Successful in 1m13s
CI / api-client-drift (pull_request) Successful in 2m2s
CI / storybook-a11y (pull_request) Successful in 11m42s
WP-67's monorepo split renamed .storybook to .storybook-ssp/
.storybook-behandelportal, but test-storybook still defaulted to the
(now nonexistent) plain .storybook dir -- it loads <config-dir>/main.js
for test-runner hooks even in --url mode, so every invocation failed
with "Could not load main.js in .storybook".

Also: test-storybook:ci only ever built+served+tested the ssp instance.
Since the split, behandelportal's stories (werkvoorraad, beoordeling,
besluit-form, ...) were never axe-tested in CI at all. Added the
:behandelportal siblings (mirroring the existing storybook/
build-storybook naming) and wired them into ci-local.sh's --full step
and the storybook-a11y GitHub Actions job.

Verified directly: ssp 62/180 stories green, behandelportal 45/112
green (including the new besluit-form story), full `ci-local.sh --full`
green end-to-end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 10:24:49 +02:00
ehoandClaude Sonnet 5 6c6bef45f3 docs: fix stale WP-65 status in backlog README, closing the format:check gap
CI / changes (pull_request) Successful in 16s
CI / lint (pull_request) Successful in 54s
CI / frontend (pull_request) Successful in 2m27s
CI / storybook-a11y (pull_request) Failing after 3m16s
CI / backend (pull_request) Successful in 1m53s
CI / semgrep (pull_request) Successful in 1m7s
CI / e2e (pull_request) Successful in 2m45s
CI / api-client-drift (pull_request) Successful in 1m57s
WP-65's summary-table row still said "in progress (65a done)" from before
65b shipped, and that one oversized cell was forcing prettier to want to
re-pad the entire ~65-row table. Correcting it to "done" (its actual
status, per the WP-65 file's own outcome notes) removes the outlier —
npm run format:check is green again with zero other rows touched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 09:52:34 +02:00
ehoandClaude Sonnet 5 af8a011819 feat(behandelportal): WP-65b beoordeling besluit (decision write)
CI / changes (pull_request) Successful in 17s
CI / lint (pull_request) Failing after 56s
CI / frontend (pull_request) Successful in 2m36s
CI / storybook-a11y (pull_request) Failing after 3m19s
CI / backend (pull_request) Failing after 1m55s
CI / api-client-drift (pull_request) Canceled after 0s
CI / e2e (pull_request) Canceled after 40s
CI / semgrep (pull_request) Canceled after 24s
Adds POST /beoordeling/{id}/besluit: a Besluit enum (Goedkeuren/Afwijzen/
MeerInfoOpvragen) backed by new Aanvraag.BesluitStatus/BesluitToelichting
columns, gated by the same BeoordelingRules.CanDecide the read side's
canBesluiten flag already uses (409 on an illegal transition, 400 on a
missing required toelichting). Mappers.ToStatusDto gains the "a recorded
decision wins" branch. FE: besluit.machine.ts + besluit-form organism
(same form idiom as change-request-form), wired into the beoordeling page
behind the server's canBesluiten flag.

Completes WP-65 (65a + 65b) — verified end-to-end against a running
backend (werkvoorraad -> beoordeling -> besluit -> status reflected back).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 09:46:20 +02:00
ehoandClaude Sonnet 5 4133b30e5d feat(behandelportal): WP-65a beoordeling detail (read) + fix unreachable medewerker login
CI / changes (pull_request) Successful in 17s
CI / lint (pull_request) Failing after 54s
CI / frontend (pull_request) Successful in 2m38s
CI / storybook-a11y (pull_request) Failing after 3m28s
CI / backend (pull_request) Successful in 2m1s
CI / semgrep (pull_request) Successful in 1m9s
CI / e2e (pull_request) Successful in 2m55s
CI / api-client-drift (pull_request) Successful in 2m1s
New GET /beoordeling/{id} shows one aanvraag's status, linked documents, and a
canBesluiten decision flag, gated by the same CanBeoordelen capability as the
werkvoorraad list. Reads through IZaakSource.ListCases rather than a new seam
method (WP-66 needs one anyway for the real write); owner BSN is masked.

Fixes a real gap found while wiring this up: the behandelportal's login was still
WP-61's copied citizen/BSN DigiD flow, so nothing ever sent X-Medewerker and the
werkvoorraad screen (WP-64) always denied in a real browser. A dev-only
medewerkerInterceptor (mirrors the existing ?role= stand-in as ?rollen=) fixes that.

WP-65's own Risks note authorized splitting read from write across sessions given
its size; this is the read half. The decision-recording mutation is next (65b).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 09:01:09 +02:00
ehoandClaude Sonnet 5 fe69caee63 feat(behandelportal): WP-64 werkvoorraad (queue) screen
CI / changes (pull_request) Successful in 15s
CI / lint (pull_request) Successful in 57s
CI / frontend (pull_request) Successful in 2m36s
CI / storybook-a11y (pull_request) Failing after 3m14s
CI / backend (pull_request) Successful in 2m1s
CI / semgrep (pull_request) Successful in 1m10s
CI / e2e (pull_request) Successful in 3m3s
CI / api-client-drift (pull_request) Successful in 2m1s
New GET /werkvoorraad endpoint lists aanvragen still open (Ingediend/InBehandeling),
gated by the medewerker capability (CanBeoordelen) rather than the admin role — reuses
the existing ApplicationSummaryDto, no new DTO. GET /me now surfaces aanvraag:beoordelen
for a behandelaar so the FE can gate with the same AccessStore/capabilityGuard idiom
every other page uses.

FE: a behandeling domain type deliberately narrower than ssp's full AanvraagStatus
union (only the two open tags — illegal states unrepresentable), composed into a
werkvoorraad-list organism from existing shared/ui molecules. Replaces WP-61's
scaffold placeholder as the app's real landing page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 22:02:35 +02:00
ehoandClaude Sonnet 5 e7156c5132 feat(WP-67): merge behandelportal into this repo as a monorepo
Restructures into apps/ssp + apps/behandelportal (two Angular projects)
plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's
separate sibling repo. That split had already produced real drift: a
hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree
forked and silently diverging (7 files), and beheer + the styles.scss
token bridge duplicated byte-for-byte across both repos.

- git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/,
  environments/, the Storybook docs/*.mdx, and styles.scss into
  libs/shared + libs/beheer (all confirmed identical between the two
  repos before merging). auth stays deliberately duplicated per
  ADR-0002 (actor-specific, expected to diverge) - amended there.
- One generated API client (libs/shared), no more vendored swagger.json.
- .dependency-cruiser split into a base factory + one config per app,
  and Storybook into .storybook-ssp/.storybook-behandelportal - both
  forced by the @auth/* alias resolving to different directories per app.
- SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/
  HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies
  its own nav/admin-links/dev-panel instead of one being hardcoded.
- CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated;
  WP-67 backlog entry documents the full decision trail.

npm run ci green (lint, dep:check x2, 360 tests across ssp/
behandelportal/shared/beheer, both localized builds, backend tests,
snippet + api-client drift); both dev servers, both Storybook
instances, and docker compose verified working.

The old sibling repo (/home/eho/repos/behandelportal) is left
untouched, not deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 21:01:57 +02:00
ehoandClaude Sonnet 5 d3f3b13345 feat(behandelportal): WP-63 aanvraag status lifecycle enum
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 50s
CI / frontend (push) Successful in 1m32s
CI / backend (push) Successful in 1m50s
CI / e2e (push) Successful in 3m7s
CI / storybook-a11y (push) Successful in 6m53s
CI / semgrep (push) Successful in 1m12s
CI / api-client-drift (push) Successful in 1m43s
Model the full ADR-0002 lifecycle (Ingediend/InBehandeling/
MeerInfoGevraagd/Goedgekeurd/Afgewezen) as a backend enum backing the
existing AanvraagStatusDto.Tag string, and widen the FE union/parse
boundary/switches to match. Ingediend/MeerInfoGevraagd aren't reachable
yet (no behandelaar transition exists) — that's WP-65. Zero DTO shape
change, so gen:api has no drift.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-01 08:56:47 +02:00
ehoandClaude Sonnet 5 a09c4ed87b feat(behandelportal): WP-62 medewerker caller identity + authz seam
Splits backend CallerIdentity into the two ADR-0002 §3 actor kinds
(ZorgverlenerCaller/MedewerkerCaller), a stub X-Medewerker/X-Rollen header
path mirroring WP-53's citizen stub, and Authz.CanBeoordelen as the first
medewerker capability — backend-only, no consumer until WP-64. Also fixes
the backlog README's stale WP-61 status (done, but table said todo).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 23:47:43 +02:00
ehoandClaude Sonnet 5 e4ea75414b docs(WP-61): mark done, record the shared-backend deviation
CI / changes (push) Successful in 7s
CI / lint (push) Successful in 56s
CI / frontend (push) Successful in 1m32s
CI / backend (push) Successful in 10s
CI / e2e (push) Successful in 2m56s
CI / semgrep (push) Successful in 1m10s
CI / storybook-a11y (push) Successful in 6m38s
CI / api-client-drift (push) Successful in 1m56s
WP-61 (bootstrap the behandelportal app) is done — a separate sibling repo
at /home/eho/repos/behandelportal, not a commit in this one. Records the
one real kickoff deviation from the vanilla new-ssp recipe: create-ssp.mjs
unconditionally renames the backend project even with --skip-backend, which
conflicts with the WP's "no new backend service" decision. Resolved by
deleting backend/ from the new repo and vendoring its swagger.json into
api-contract/ instead, with gen:api regenerating only the client against
that vendored doc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 23:26:37 +02:00
ehoandClaude Sonnet 5 920ce138cb fix(scaffolding): rename create-ssp to create-frontend, fix 2 bugs it surfaced
Renamed scripts/create-ssp.mjs -> create-frontend.mjs (+ its WP-45 doc, npm
script, and every prose/command reference) since "ssp" reads as an acronym
where "create-frontend" says what it does.

Also fixes two real bugs found while running it for real during WP-61:
scripts/ci-local.sh was missing from RENAME_CONTENT_FILES (any --name'd
clone that keeps a backend would break `npm run ci`, still hardcoding
BigRegister.slnx), and plopfile.mjs's `gen:context` insertion into
.dependency-cruiser.js anchored on the `showcase: null,` line, which
create-ssp/create-frontend has already stripped by the time gen:context
runs in the same invocation — silently leaving a freshly scaffolded
context with no CONTEXT_ALLOWED fence entry at all. Re-anchored on the
`const CONTEXT_ALLOWED = {` line instead, which never moves.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 23:26:28 +02:00
ehoandClaude Sonnet 5 ba24784586 feat(openzaak): one-command UI-OpenZaak bridge, real BSN fix, flake mitigation
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 55s
CI / frontend (push) Successful in 1m33s
CI / backend (push) Successful in 1m46s
CI / e2e (push) Successful in 3m14s
CI / storybook-a11y (push) Successful in 6m50s
CI / semgrep (push) Successful in 1m13s
CI / api-client-drift (push) Successful in 1m47s
scripts/openzaak-ui-up.sh brings up the root app and the OpenZaak harness
together, wires them onto one docker network, seeds the catalogus, grants
the container-alias zaaktype scope, and verifies a real aanvraag submitted
through the UI lands in OpenZaak.

Along the way: DocumentStore.DemoOwner was reusing the seeded doctor's
11-digit BIG-nummer as a stand-in BSN, which isn't a valid 9-digit BSN shape
— OpenZaak rejects it, breaking both submit's rol-creation step and the
citizen's own applications list under Zgw:Enabled=true. Fixed to a real
elfproef-valid BSN.

Also adds mitigation for a still-unexplained per-container flake (every
outbound ZGW POST fails as if the body were empty, for that container's
whole lifetime) that correlates with host memory pressure: the script now
warns when host swap is heavily used, and an opt-in ZgwDiagnosticHandler
(ZGW_DEBUG_HTTP=1) logs Content-Length vs. actual bytes sent so the next
reproduction can confirm or rule out client-side body corruption.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 08:23:01 +02:00
ehoandClaude Sonnet 5 3ff80c124f feat(openzaak): bounded retry + flagged write divergence (WP-60)
Local aanvraag/document writes and their paired ZGW writes aren't
transactional; a ZGW failure after the local write succeeds used to
diverge silently. ZgwHttpClient now retries transport-shaped failures
(not 500, which can follow a partial commit on the non-idempotent
statussen/rollen POSTs), and a ZGW failure that survives retry sets
Aanvraag.ZgwError plus a zgw:divergence audit row instead of failing
or diverging quietly. No outbox/reconcile job: three request-triggered
write paths don't justify a persisted queue that would also need to
carry citizen PII for the JWT audit claims.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 18:11:55 +02:00
eho 67abc58052 feat(openzaak): per-document-type confidentialiteit config (WP-59)
Drives the DRC upload's vertrouwelijkheidaanduiding from a new stamdata
table instead of the hardcoded "openbaar", following the existing
config-as-code pattern (ADR-0004). Adds the referential-integrity check
StamdataValidationTests was missing for the new table.
2026-07-30 17:30:25 +02:00
ehoandClaude Sonnet 5 3e983bd2cc feat(openzaak): real notification delivery to the BFF webhook (WP-58)
OpenZaak doesn't serve the Notificaties API itself (it's a separate app,
open-notificaties) — standing one up for a real abonnement would triple
this harness for a benefit it doesn't need (exactly one subscriber, this
repo's own BFF). Instead, an opt-in compose overlay adds a celery worker
and points OpenZaak's NotificationsConfig straight at the BFF's webhook
via a zgw_consumers Service; bootstrap-notificaties.sh configures it
idempotently and verify-notificatie.sh proves a real write delivers to
the BFF's audit trail end-to-end.

Verified live: preflight proves the webhook's shared-secret gate both
ways (204/401), a zaak PATCH triggers real celery delivery, and rerunning
both scripts against an already-configured harness stays idempotent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 15:33:16 +02:00
ehoandClaude Sonnet 5 1e87997ea0 feat(openzaak): least-privilege client scopes (WP-57)
setup_configuration has no YAML field for granular autorisaties, so
bigregister-test now starts at heeft_alle_autorisaties: false (dev + prod
template) and bootstrap-catalogus.sh grants exactly the ztc/zrc scopes the
harness needs via the Django ORM, sidestepping the zero-scope
chicken-and-egg with the JWT-authenticated Autorisaties REST API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 14:05:54 +02:00
ehoandClaude Sonnet 5 89ad3490b0 feat(openzaak): idempotent catalogus/zaaktype/zaak provisioning (WP-56)
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 53s
CI / frontend (push) Successful in 1m42s
CI / backend (push) Successful in 2m11s
CI / e2e (push) Successful in 3m58s
CI / storybook-a11y (push) Successful in 8m8s
CI / semgrep (push) Successful in 1m17s
CI / api-client-drift (push) Successful in 1m50s
bootstrap-catalogus.sh now looks up every resource by its natural key before
creating it (catalogus by domein+rsin, zaaktype by catalogus+identificatie,
statustype by zaaktype+volgnummer, roltype by zaaktype+omschrijvingGeneriek,
zaaktype-publish by checking `concept` first, zaak by identificatie,
status/rol by existence-under-the-zaak), so rerunning against an
already-seeded instance reuses what's there instead of erroring.

The WP's original plan (move this into OpenZaak's `setup_configuration`
mechanism) turned out not to be achievable: reading the actual
django_setup_configuration steps installed inside the open-zaak image shows
no step exists for Catalogi/Zaken content anywhere in this OpenZaak version
— only sites/credentials/applicaties/selectielijst. Documented as a
deviation; the WP's own Risks section already anticipated this and sanctioned
falling back to an idempotent script.

Verified live: fresh instance -> full run (all created) -> integration test
green -> reran the script twice more against the same instance (all reused,
identical URLs, no duplicates) -> integration test still green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 12:57:37 +02:00
ehoandClaude Sonnet 5 4d5ed7ca36 fix(styles): drop dead RijksSans font-face + stack from vendored CIBG CSS
The rijks-sans-regular/italic.woff2 files were never vendored (only
CIBG-icons-1.2.woff2 exists), so the @font-face rules 404'd on every page
load and the font stacks named a font that could never load anyway. Matches
the already-documented decision to run a system-font stack (licensed RO/Rijks
fonts not shipped).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 12:57:30 +02:00
ehoandClaude Sonnet 5 3588057a75 feat(openzaak): real secrets + TLS for the production OpenZaak harness (WP-55)
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 12s
CI / frontend (push) Successful in 14s
CI / storybook-a11y (push) Successful in 17s
CI / backend (push) Successful in 1m51s
CI / semgrep (push) Successful in 1m13s
CI / e2e (push) Successful in 2m56s
CI / api-client-drift (push) Successful in 1m41s
docker-compose.openzaak.prod.yml layers real SECRET_KEY/DB password/site
domain/allowed-hosts (all required, fail-fast via ${VAR:?...}) on top of the
WP-54 dev harness, switches Postgres off trust auth, and sets IS_HTTPS for a
front-facing reverse-proxy TLS setup. The ZGW client secret lives inside a
file setup_configuration reads rather than a compose env var, so it's
templated (data.prod.yaml.template, no secret) and rendered host-side via
render-prod-secrets.sh into a gitignored data.prod.yaml, mounted over the
container's dev data.yaml. ZgwOptions.cs already binds from IConfiguration,
so the BFF side needed no code change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 12:27:21 +02:00
ehoandClaude Sonnet 5 f21c3c7ca2 docs(backlog): add phase 10 (OpenZaak hardening) and phase 11 (behandelportal)
WP-55..60 harden the OpenZaak integration for production (secrets/TLS,
idempotent provisioning, least-privilege scopes, real notifications,
confidentialiteit config, write-divergence resilience). WP-61..66 stand up
a staff-facing behandelportal per ADR-0002, wired to the same backend via
BFF-lite decision DTOs. Both phases are independent tracks; WP-60's
Decisions block is deliberately left open for a planner-agent kickoff.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 11:59:11 +02:00
ehoandClaude Sonnet 5 66f8125ccd docs(backlog): WP-30 done — confirmed GREEN on a real Gitea run
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 9s
CI / backend (push) Successful in 11s
CI / storybook-a11y (push) Successful in 17s
CI / e2e (push) Successful in 19s
CI / api-client-drift (push) Successful in 16s
CI / frontend (push) Successful in 12s
CI / semgrep (push) Successful in 6s
The watched push came back all-green after ebf1f8f (the non-root Dockerfile
fix semgrep's live run caught). Marks the entire showcase/ZGW backlog
(WP-05..54) done.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 10:58:30 +02:00
ehoandClaude Sonnet 5 ebf1f8f8b4 fix(backend): run the prod image as non-root (semgrep, live Gitea finding)
CI / changes (push) Successful in 8s
CI / lint (push) Successful in 10s
CI / frontend (push) Successful in 13s
CI / storybook-a11y (push) Successful in 17s
CI / backend (push) Successful in 1m45s
CI / semgrep (push) Successful in 1m9s
CI / e2e (push) Successful in 2m53s
CI / api-client-drift (push) Successful in 1m46s
The pushed WP-30 item-5 Dockerfile predated the semgrep triage's local run —
CI's now-blocking semgrep gate caught what local verification couldn't:
dockerfile.security.missing-user-entrypoint (no USER, container runs as root).

mcr.microsoft.com/dotnet/aspnet:10.0 ships a pre-created non-root user for
exactly this ($APP_UID, uid/gid 1654) — switched to it, with --chown on both
COPY layers so the app can still create/write bigregister.db (WP-22, a
relative-path SQLite connection string resolved against the container's /app
cwd) as that user.

Verified for real: rebuilt, confirmed `whoami` is `app` inside the container,
ran it and curled a live GET /api/v1/brief/preview (200), confirmed
bigregister.db was created and is actually owned by app:app. Full semgrep
re-run (this file didn't exist during the original triage) is now 0 findings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 10:11:54 +02:00
ehoandClaude Sonnet 5 c2e06cc8d7 docs(backlog): WP-30 status update — 5 of 6 items landed
CI / changes (push) Successful in 30s
CI / lint (push) Successful in 4m0s
CI / frontend (push) Successful in 4m42s
CI / backend (push) Successful in 2m27s
CI / e2e (push) Successful in 3m36s
CI / semgrep (push) Failing after 1m11s
CI / storybook-a11y (push) Successful in 8m38s
CI / api-client-drift (push) Successful in 1m48s
Records what's implemented (items 1/3/4/5/6), what's deliberately skipped
this round (item 2, blocked on act_runner access), and that the WP can't be
marked fully done until a real Gitea run confirms the CI-timing/path-filter
behavior this environment can't observe. npm run ci confirmed green locally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:36:36 +02:00
ehoandClaude Sonnet 5 a0d8804a53 feat(backend): optional lean deployable image (WP-30 #5)
Multi-stage backend/Dockerfile (sdk build -> aspnet:10.0 runtime, ~312MB) +
docker-compose.prod.yml, additive only — not wired into CI or the existing
dev docker-compose.yml (which keeps the SDK image for dotnet run hot-reload).
New .dockerignore keeps the build context lean (node_modules alone is
~750MB) since the Dockerfile COPYs from the repo root to pick up
public/letter.css (WP-25's FE<->BE letter contract) as a sibling of backend/.

Verified for real: built the image, ran it, and curled a live
GET /api/v1/brief/preview against the running container — got back the
actual rendered letter HTML with letter.css inlined, confirming the
walk-up-from-BaseDirectory lookup resolves inside this image layout too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:35:21 +02:00
ehoandClaude Sonnet 5 e7db69d8a9 perf(ci): path-filter jobs by which side changed (WP-30 #3)
New `changes` job (dorny/paths-filter, pinned to SHA) computes frontend/
backend outputs; every downstream job gates its real steps on the relevant
output(s) instead of being skipped as a whole job. Conservative "skip steps,
not jobs" variant: every job still runs and reports a status (checkout always
executes) even when its side is untouched, so a required-status-check never
waits on a job that never started — the tradeoff the WP itself flagged as the
open risk of this item. e2e/semgrep/api-client-drift gate on either side
(they exercise both). `.github/workflows/**` counts as both sides, so a CI
change always gets a full run. Validated with `actionlint` (0 issues) and a
local YAML parse; the actual skip behavior can only be confirmed on a real
Gitea PR run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:28:55 +02:00
ehoandClaude Sonnet 5 e02e8ce058 perf(ci): split lint into its own fast-fail job (WP-30 #4)
New `lint` job (lint + format:check + check:tokens) runs in parallel with
`frontend`, reporting in ~5 min instead of waiting on the full
test:coverage/ng build --localize/npm audit chain. Depends on the item-1
node_modules cache (otherwise this would duplicate a full npm ci for no
speed benefit, per the WP's own note).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:26:41 +02:00