epic #470 C13: core-decoupling ratchet reads 0, pinned permanently - #808
Merged
Conversation
…(C10) Epic #470 C10 — the flip. The Dev Platform now lives in byte5ai/omadia-dev-platform and installs via Hub/ZIP. Removed: - middleware/src/devplatform/ (62 files) — stores, worker, backends, LLM proxy, pipeline/gates, GitHub App, triggers, routers, wireDevPlatform - middleware/test/devplatform/ (58 files) - middleware/scripts/dev-transcript.ts Adversarial eval (#498): the `brief_delimiter` Tier A probe ran the real `composeBrief` out of src/devplatform/. A probe against a module core no longer ships measures a library, not a deployed defense — so the probe, its five corpus scenarios (direct-injection.jsonl, indirect-injection.jsonl) and their baseline rows leave with it. This is a real coverage reduction and is recorded as such in test/adversarial/README.md, with both ways to close it. Deterministic corpus: 12 scenarios -> 7.
…y (C10) Removed: - middleware/packages/dev-runner-shim/ (23 files) — the in-container agent shim. Never built by `npm run build`, yet index.ts resolved its dist/ at runtime; the extraction removes that inconsistency with it. - middleware/sidecars/dev-runner/ (2), dev-runner-daemon/ (30, dockerode), dev-dind/ (2) - docker-compose.dev-platform.yaml All four now live in byte5ai/omadia-dev-platform, which owns their GHCR publishing, SBOM and signing pipeline.
index.ts (-238 lines): - 15 devplatform imports + the now-dead DeviceFlowStore / ConductorRoleStore - the GitHub-webhook block mounted before express.json - the whole assembly block: assembleDevPlatform / mountDevPlatform, the GitHub-App routers, the three chat orchestrator tools, worker start + SIGTERM/SIGINT hooks, the uiRouteCatalog nav registration, the retention cron and the no-graphPool warning Side effect worth calling out: the `/api/v1/dev-runner/llm/` carve-out sat BEFORE the conductor inbound webhook router, so an express.json ran ahead of that router's route-level express.raw() and short-circuited it (body-parser marks the request `_body`). Deleting the carve-out restores the order the surrounding comments already document: pluginRawBodyMount -> conductor raw router -> global express.json. config.ts (-305 lines): all 43 DEV_*/FLY_* dev-platform schema keys, the `devPlatform` namespace, `buildDevPlatformConfig`, `csvList`, and the CORE_DEV_PREFIXED_KEYS / DevPlatformEnvKey / isDevPlatformEnvKey machinery that existed only to hold those keys out of the top level — `Config` is now `ParsedConfig`. `devPlatformBootRefusals` goes too: the plugin owns both interlocks (pluginConfig.ts, verified), so they became activation refusals rather than vanishing. KEPT: devFlag() (two PUBLIC_MCP_* call sites), DEV_ENDPOINTS_ENABLED, DEV_ENDPOINTS_LOOPBACK_ONLY, FLY_APP_NAME. .env.example: the 15-key block replaced with a pointer to the plugin repo. middleware/package.json: the §5 note — express/pg/zod stay as plugin peerDependencies resolved through the host node_modules symlink.
… (C10) - web-ui/app/admin/dev-platform/ (29 files) — the operator SPA. It ships from the plugin now as a compiled Vite bundle served through the C8 static host at /p/<pluginId>/ui/. - web-ui/app/_components/devjobs/ (6 files) + app/_lib/useDevJobEvents.ts - app/admin/page.tsx — the grid card. The generic `requiresNavFrom` mechanism stays; this was its only user. - app/_lib/i18n-structural.test.ts — the GateInbox.tsx path entry. H3 resolved by omission, as the plugin's ACCEPTANCE-RUN records. chat/page.tsx no longer special-cases `tool.name === 'dev_job_start'`; a dev-job start from the installed plugin now falls through to the generic long-running-task card (`isTaskStartToolName` / TaskChatCard), which is the accepted degradation. i18n: 299 leaf keys per locale removed (adminDevPlatform.* 288, chat.devJob.* 9, admin.index.cards.devPlatform.* 2) = 598 across en+de, plus the four orphaned `i18n-identical-allowlist.json` entries the validator flagged. `npm run i18n:check` OK — 3560 keys; `i18n:literals` translate=0.
… (C10)
publish-images.yml:
- the `dev-runner` and `dev-runner-daemon` matrix entries
- the runner-image supply chain: cosign install, syft SPDX-JSON SBOM,
keyless sign + attest (all guarded on `matrix.name == 'dev-runner'`)
- `id-token: write`, which existed solely for that keyless signing
auto-release.yml / release.yml: the matching caller-side `id-token: write`
grants, which a reusable workflow cannot self-grant and nothing else needs.
The `if: false` npm-provenance job in release.yml keeps its own grant.
byte5ai/omadia-dev-platform owns runner GHCR publishing, SBOM and signing
now. Per implementation.md §2.4 the cosign certificate identity binds to
repo + workflow + ref, so the new signer needs the transition
`--certificate-identity-regexp` landed before it publishes — that is P4's
job in the plugin repo, not core's.
The missing-Dockerfile guard stays: it also computes the version `stamp`
every remaining image consumes; only its dev-runner-daemon justification
comment is gone.
scripts/wave-{implement,verify}.workflow.mjs: the generic wave prompts baked
in "terminal transitions go through finalizeDevJob" — a dev-platform domain
rule in a domain-agnostic prompt, naming a function core no longer ships.
Genericised to "the subsystem's single finalizer"; the rule survives, the
implementor name does not. Same for the `docs/dev-platform/` example path.
`tsc` caught these, which is exactly why C10 has to be one PR. Three LONG_RUNNING_* keys sat INTERLEAVED with the dev-platform keys in config.ts, between DEV_PLATFORM_RUNNER_BASE_URL and DEV_PLATFORM_CLI_BIN, and went out with the block: LONG_RUNNING_SUBAGENT_TOOLS — W2-2 / issue #543, the generic LONG_RUNNING_TASK_STALE_MS `<tool>_start/_status/_list` seam that any LONG_RUNNING_TASK_RETAIN_MS slow tool opts into. Not dev-platform. FLY_APP_NAME went too, despite CORE_DEV_PREFIXED_KEYS documenting in prose that it "describes the host, not the feature, so it must survive the extraction". All four are restored under an explicit heading that says why they are core, so the next person reading config.ts does not have to re-derive it. Also drops the `node:os` import, now unused (it backed the workspace-dir default), and rewords the three comments this PR itself introduced that named the extracted subsystem — C10 must not add coupling references while removing them. Verified: middleware build + typecheck + adversarial/golden tsconfigs green; 6925 pass / 0 fail; typecheck:test ratchet 406 -> 371 (baseline lowered); lint 0 problems. web-ui typecheck green, 717 pass / 0 fail, i18n 3560 keys OK.
…(C10)
Ratchet: **3,300 → 214**, updated consistently in all three places the README
says must agree — `decoupling-baseline.json`, the README "Baseline **214**"
line, and the `acceptance.md` guard row.
Nine of fourteen zones read CLEAN. Every survivor is scheduled, not stranded:
migrations 69 C11 — 0022-0030 stay; core still applies them
middleware/test 62 C13 — fixture strings + the legacy-key regression test
scripts 27 C13 — the ratchet's own pattern list (needs a
self-exclusion before the total can reach 0)
middleware/src 19 C12 — publicPaths (6); rest are comments
web-ui/app 19 C13 — nav test fixtures, two comments
packages 17 C13 — plugin-api CHANGELOG recording the removal
env-example 1 the plugin repo URL, which cannot be reworded
Also: `test-typecheck-baseline.json` 406 → 371, and `package-lock.json`
regenerated (npm left `packages/dev-runner-shim` as an `extraneous: true`
stanza rather than dropping it; `npm ci` verified clean afterwards).
Docs:
- README gains a "C10 — the flip" status section: what left, what was kept and
why, H3 resolved by omission, the adversarial coverage reduction, and the
express.json ordering bug the deletion fixed. The H3 "decision before code"
is struck through — it is answered now.
- plan.md §4.2 gains a C10 note (and flags that its `devJobStepEffect.ts` line
went stale in the other direction: C5 deleted that port as dead code). P4 row
marked shipped.
- docs/CHANGELOG.md: "Dev Platform moved to byte5ai/omadia-dev-platform
(install via Hub/ZIP)" — written for operators, so it leads with how to get
it back, states that no data is touched, and that in-flight jobs survive the
upgrade because C12 has not run yet.
…sha_on_release matrix ref (C10 review) Cross-family review of PR #804. Three defects, no scope change to the deletion itself. 1. `.github/workflows/publish-images.yml` referenced `matrix.sha_on_release`, which nothing defines any more: the only two matrix entries that set it (dev-runner, dev-runner-daemon) left with the Dev Platform. actionlint errors on this branch and is clean on main. Dropping the clause also restores the behaviour the adjacent comment already claims — sha tags on the edge build only. 2. `middleware/package.json` — the `_dependencyNote` added by this PR was indented four spaces and pushed `"dependencies"` to column 0. Valid JSON, but the rest of the file is 2-space and any tool that rewrites it would reflow the diff. Text unchanged. 3. Removing the LLM-proxy carve-out moved `createConductorWebhooksInboundRouter` from *after* the global `express.json` to *before* it. That is correct — and it silently fixes a live bug: on main the router HMAC'd `Buffer.alloc(0)`, because body-parser stamps `_body` and the route-level `express.raw()` then short-circuits, so every correctly-signed inbound webhook answered 401. Measured on a standalone probe: json-first yields rawLen 0, router-first yields the full 24 bytes. Nothing locked that order. `conductorWebhookInbound.test.ts` mounts the router alone on a bare app, which passes under BOTH orders — which is exactly how the broken ordering shipped green. Added two behavioural tests (same signed request, both mount orders: 202 vs 401) plus a source-order assertion in `778RouteMounts.wiring.test.ts` that fails loudly if either anchor goes missing rather than passing vacuously. All four assertions mutation-checked: reordering the mount fails the wiring test, renaming the anchor fails it for the anchor reason, and forcing/removing the parser in the harness kills exactly the intended behavioural tests. Also corrects the C10 file count in the spec README (213 -> 217 measured deletions). The adjacent "43 config keys" claim was verified correct and left alone. Ratchet stays at 214 — the one comment that would have raised it was reworded, not excused.
Core stops exempting two URLs on a plugin's behalf. Since C4 the plugin declares them itself in `permissions.public_paths`, and the operator consents per prefix (`platform/publicPathGrants.ts`). The deletion is the load-bearing half of that handover, not tidy-up. `validateDeclaredPublicPath` REJECTS a declaration that collides with a live static exemption — "is already a static core public path … remove the core exemption first, or drop the declaration" — because two mechanisms claiming one URL is ambiguous and only one of them terminates. So while these entries existed the plugin's own grant could never validate. With them gone it does. Guarded by a new suite that pins `STATIC_PUBLIC_PATHS` as a CLOSED set: every entry must be accounted for by a named, justified core-owned exemption. Restore either deleted entry and the suite goes red on the unaccounted entry, which is what makes this commit cleanly revertible. The suite deliberately does not name the two deleted paths. Hardcoding a plugin's wire paths into core is a reference to that plugin whichever direction it asserts in — the coupling this epic removes and the thing `scripts/check-core-decoupling.mjs` counts — and it goes stale-but-green the day the plugin renames them. The closed set is also the stronger guard: it catches any unauthenticated surface being added, not just these two. The 401 follows as a corollary, since `requireAuth` runs on the `/api` mount before routing, and is asserted end-to-end against the same chain `src/index.ts` assembles. Ratchet: 214 → 209. Nothing raised.
Core carries no Dev Platform reference at all: no code path, config key,
i18n key, fixture string or comment. All 14 zones CLEAN.
214 -> 0 (3,300 at the epic's start, 214 after C10, 209 after C12)
WHAT MOVED
- middleware/src (14 -> 0), packages (17 -> 0), env-example (1 -> 0):
comments and one operator-facing pointer. Where the reference was to a
dead path (`devplatform/routes/devWebhooks.ts`, `src/devplatform/
devJobTypes.ts`) it was a stale link and is now corrected; where it named
the product it stays named, in prose.
- middleware/test (62 -> 0): fixture route prefixes, nav ids and channel
types now use a neutral example plugin. `manifestDevJobsLegacyKey.test.ts`
becomes `manifestRetiredPermissionKey.test.ts` and asserts the guarantee
that actually exists — unknown permission keys are ignored, not rejected —
which is key-agnostic, so pinning one historical key made it stale-but-
green the next time a different capability is retired.
- web-ui/app (19 -> 0): the same, plus a genuinely stale comment claiming
the nav entry appears "while DEV_PLATFORM_ENABLED" — an env var C10
deleted.
- scripts (27 -> 0): the ratchet was counting its own `PATTERNS` array. It
now excludes itself. A detector that counts its own definitions can never
reach zero, and "zero except for the 27 that are the tool" is not a
property anyone can check at a glance.
WHAT IS ALLOWLISTED, AND WHY IT IS NOT A DODGE
Two path-anchored entries, both historical record rather than coupling.
A record you are allowed to edit is not a record.
- `middleware/migrations/00{22..30}_*` — every deployment that ran them has
those FILENAMES in its `schema_migrations` ledger, and C11's plugin-side
migrator seeds from exactly those donor rows. Rename one and the handoff
stops matching on the installations that need it most.
- `packages/plugin-api/CHANGELOG.md` — a published entry for a released
version, written to be found by a consumer grepping for a removed type.
`services/githubAppJwt.ts` stays in core and needed no allowlist: it is
generic GitHub App auth and carries no matching identifier.
THE FLOOR IS NOW ABSOLUTE
A ratchet parked at zero still reads its floor out of a JSON file, and a
JSON file is editable. `EXTRACTION_COMPLETE` makes the script assert 0
outright and ignore the baseline for pass/fail; `--update` refuses to write
a non-zero baseline. The CI job additionally greps the report for `TOTAL 0`,
so a future refactor that made the script exit 0 while still counting hits
still fails. Bringing the Dev Platform back into core is an architectural
decision to argue in review, not a number to edit.
Zero counts IDENTIFIERS, not the English name: a comment may still say "the
dev platform plugin". Prose describes history; identifiers create coupling.
…oopback (C12 review) The suite self-skips its five unauthenticated 401 assertions when a sandbox refuses a loopback listener. Locally that is right — the closed-set assertion still runs. In CI it is not: a runner that denied bind(127.0.0.1:0) would delete the entire 401 half of the guard while the job stayed green, which is the same permanently-green failure family the decoupling job's own timeout comment warns about. Keep the local skip, but throw with a named reason when CI is set.
…fixture rename (C13 review) Three defects from cross-family review of the zero floor. 1. The self-exclusion was a BASENAME wildcard. `!**/check-core-decoupling.mjs` hides every file of that name anywhere in a scanned zone, not just the one detector. A probe at middleware/src/__probe/check-core-decoupling.mjs carrying three real identifiers scanned as "Core is free of Dev Platform references", exit 0. Anchored to the exact repo-relative path; the scripts zone still reports CLEAN, and the probe now trips the check. 2. The mutation test is now committed, not just performed. It plants that probe and asserts the checker goes red. Its own tokens are assembled at runtime so the test file cannot trip the detector it tests. 3. The C13 identifier rename left the plugin's HUMAN-READABLE label behind in four fixtures. PATTERNS are identifier-shaped, so `Dev Platform` and `Dev-Plattform` are invisible to the zero floor while the sweep claimed no fixture string survives. Renamed with their assertions, preserving the en/de/de-AT/fr fallback each test exercises. Deliberate prose mentions are untouched. The regression test is wired into the decoupling job. scripts/ has no package.json and no job globs *.test.mjs, so it would otherwise have been a test nobody runs — green because nothing executed it.
This was referenced Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Epic #470 C13. Stacked on #807 (C12). Ratchet 0.
Core carries no Dev Platform reference at all — no code path, config key, i18n key, fixture string or comment. All 14 zones CLEAN, and the floor is now enforced as an absolute rather than as a baseline.
Per-zone
middleware/srcdevplatform/routes/devWebhooks.ts,src/devplatform/devJobTypes.ts) — stale links, now correctedmiddleware/testmiddleware/packagespluginContext.ts+ one shared-pool comment;CHANGELOG.mdallowlistedmiddleware/migrationsmiddleware/env-exampleweb-ui/appDEV_PLATFORM_ENABLED" — an env var C10 deletedscriptsPATTERNSarraymiddleware/scripts,sidecars,package.json,web-ui/messages,web-ui/config,ci-workflows,composeid-token: writeand compose residue named in the C13 brief were handled thereThe floor is absolute, not a baseline
A ratchet parked at zero still reads its floor out of a JSON file, and a JSON file is editable. One hand-edit and core could legally re-acquire a reference — the outcome the whole epic exists to prevent.
So
EXTRACTION_COMPLETEinscripts/check-core-decoupling.mjsmakes the check assert0outright and ignore the baseline for pass/fail.--updaterefuses to write a non-zero baseline. The CI job (core decoupling ratchet (#470), unchanged name, still required) additionally greps the report forTOTAL 0, so even a refactor that made the script exit 0 while still counting hits fails the job.Mutation-checked — one probe comment added to
src/services/ssrfGuard.ts:Reversing the extraction is now an architectural decision to argue in review — flip the constant, restore a baseline — not a number to edit.
What is allowlisted, and why it is not a dodge
Two entries, each anchored on an exact path so the exemption cannot widen. Both are historical record rather than coupling — a record you are allowed to edit is not a record.
middleware/migrations/00{22..30}_*schema_migrationsledger, and C11's plugin-side migrator seeds its own ledger from exactly those donor rows. Rename one and the handoff stops matching on the installations that need it most. The DDL body names the real tables and columns living in those databases right now.middleware/packages/plugin-api/CHANGELOG.mdThe script also excludes itself.
PATTERNShas to spell out the 21 identifiers it hunts for, so an unfiltered scan counted 27 hits against the detector and made the target27instead of0. Self-exclusion is safe precisely because it is the detector — no runtime, ships in no image, and adding a pattern there can only make the check stricter. The narrower alternative (skipping the array by line range) breaks the moment the array moves.services/githubAppJwt.tsstays in core and needed no allowlist — it is generic GitHub App auth and carries no matching identifier. Its comment now says explicitly that moving it into the plugin repo would recreate the reverse dependency across a repo boundary.Zero counts identifiers, not the English name
Worth being explicit, because it is the difference between a sweep and a whitewash. The patterns are identifier-shaped (
devPlatform,dev-runner,DEV_JOB,djr_, …). A comment may still say "the dev platform plugin", and several deliberately do. What cannot survive is an import, a route, a config key, an i18n key or a fixture string. Prose describes history; identifiers create coupling. Where the old comment pointed at a real code artifact that no longer exists, it was corrected rather than reworded.One test re-scoped, not just renamed
manifestDevJobsLegacyKey.test.ts→manifestRetiredPermissionKey.test.ts. It pinned one historical permission key, but the guarantee it protects — unknown permission keys are ignored, not rejected — is key-agnostic. Pinned to one name it would go stale-but-green the next time a different capability is retired. It now exercises the shapes a retired key arrives in (baretrue, a block of options, alongside live keys). Which names were retired stays in the changelog, which is a changelog's job.A trap this sweep walked into
Renaming the nav fixture's de label left
never leaks the per-locale label map to the browserassertingres.text.includes('Dev-Plattform') === false— a string that no longer existed anywhere, so the guard was permanently green. Repointed at the real de label (Beispiel-Plugin), which the sibling locale test proves is present in the de response. Flagging it because it is the exact failure mode a mass-rename invites.Verification
middlewarefull suiteweb-uivitestmiddlewaretsc --noEmit+eslintmiddlewaretypecheck:testratchetweb-uitypecheck,eslinti18n:checki18n:literalscheck-core-decoupling.mjs--updateboth fail on one reintroduced referenceNothing was left above zero.
Do not merge yet; stacked on #807.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Cross-family review (Forge)
Reviewed against code, not against the description. GPT-5.4 (reasoning=high) via
codex exec— a different model family from the author, deliberately.Verdict: MERGE after
04fc1053. The zero floor is real and the mutation evidence reproduces. One genuine hole was found in the allowlisting, plus two gaps; all three are fixed on this branch.The enforcement itself reproduces
Re-ran the author's mutation independently (one
// probe: devplatforminsrc/services/ssrfGuard.ts):node scripts/check-core-decoupling.mjs--updategrep -qE '^ TOTAL 0'All three fail independently.
EXTRACTION_COMPLETEis not decorative.DEFECT (fixed) — the self-exclusion was a basename wildcard, not a path
'!**/check-core-decoupling.mjs'excludes every file of that name anywhere in a scanned zone, not just the detector. Proven:Three real identifiers inside
middleware/src, counted as zero. That is precisely the "reference hiding inside an allowlisted region" the floor exists to prevent, and it needed no edit to the script or the baseline — just a filename.Anchored to the exact repo-relative path. The
scriptszone still reportsCLEAN(self-exclusion intact) and the probe now exits 1.The mutation test is now committed rather than performed —
scripts/check-core-decoupling.test.mjsplants the probe and asserts the checker goes red. It builds its own pattern tokens at runtime (['dev','platform'].join('')) so the test file cannot trip the detector it tests. Mutation-checked in both directions: reverting the glob turns it red, restoring it turns it green.GAP (fixed) — the regression test would never have run
scripts/has nopackage.json, and no CI job globs*.test.mjs. A test file left there is green because nothing executes it — the same failure family this job's own timeout comment already names. Wired into thedecouplingjob as a step after the zero assertion.GAP (fixed) — the rename stopped at identifiers, not at the name
PATTERNSis identifier-shaped, soDev PlatformandDev-Plattformmatch nothing. Four fixtures still carried the plugin's display name while this PR states "what cannot survive is … a fixture string":middleware/test/uiRouteCatalogNav.test.ts·middleware/test/toolPluginRuntimeRouteDisposal.test.ts·web-ui/app/_lib/__tests__/navParse.test.ts·web-ui/app/_lib/__tests__/navMerge.test.tsRenamed to
Example Plugin/Beispiel-Plugintogether with their assertions, preserving the en / de / de-AT / fr fallback each test exercises. Invisible to the ratchet by construction, so this one only closes by reading.The identifiers-not-prose argument is otherwise correct and was not disturbed — deliberate prose in the detector,
.env.example,package.json_dependencyNoteandchat/page.tsxis untouched. Adding the English name toPATTERNSwould make the detector's own prose unfixable.Mass-rename trap — swept, one more angle
Extracted every string literal the C13 diff removed-and-did-not-re-add, then grepped each against the tree at HEAD. Besides the
Dev-Plattformleak the author already caught, no assertion compares against a string that no longer exists. The four fixtures above were self-consistent (fixture and assertion moved together), so they were a naming gap, not permanently-green tests.No behaviour hidden in the sweep
Classified every changed line in all 11 non-test source files (
pluginContext.ts,githubAppJwt.ts,manifestLoader.ts,awaitStore.ts,publicPaths.ts,ssrfGuard.ts,gateReevaluation.ts,adminEmbeddingProvider.ts,conductorWebhooksInbound.ts,0007_webhooks.sql,.env.example) plus bothweb-uifiles: comment-only, every one. The sole executable change in the whole residue sweep is the ratchet script itself. Claim (4) holds exactly.Allowlist judgement
Migrations
0022–0030and theplugin-apiCHANGELOG are anchored on exact paths and are genuinely historical record — rewording either breaks the artifact's one job. Left as-is. Worth knowing about the residual shape: a new line added inside those two regions would be allowlisted too. The migrations are frozen released DDL (editing one is already a violation with its own consequences) and the CHANGELOG is append-only per release, so the exposure is narrow and visible in review — unlike the basename hole, which was invisible.Gates (on the merged branch)
middleware 6,948 tests — 6,936 pass, 0 fail, 12 skipped · web-ui vitest 717 pass ·
typecheck:testratchet 371, no regressions · middlewareeslintclean · web-uitypecheckclean,eslint0 errors ·i18n:checkOK — 3,560 keys, en + de ·i18n:literalsexit 0 ·check-core-decoupling.mjsTOTAL 0 / baseline 0 · detector regression test 2/2 ·ci.ymlparses,decouplingjob has 5 steps.Note: C12's review fix (
f7acf93f) is merged forward into this branch, so theFixedchangelog entry here covers the whole C12+C13 review.