Merge branch 'main' into feature/techlog-ui-migration
Integrates the frontend template sync (a0fbafb → 5434760) into the TechLog UI migration. Merged in this direction so every conflict is resolved and proved in the worktree; main is only fast-forwarded afterwards and never holds a state that was not verified here. 15 conflicts. The rule throughout: keep the template's mechanism, keep the product's content, and never invent a third state neither branch would accept. The template's product manifest and its runtime feature kill switch are adopted. The route registry is deliberately not composed from contract.routes: the reference feature still declares screens this product deleted, and reducing over them would register paths with no component behind them. ROUTE_FEATURE_OWNER is narrowed to registered routes for the same reason. The first resolution did compose from contract.routes and was rejected by product-features.test.ts. Three files pinned counts and a digest describing the gate contract. Neither side's numbers describe the merged config/ci/gates.json, so they were recomputed from it rather than chosen: 27 gates, 82 commands, 94 command references, 107 evidence references, 128 artifacts, shape sha256 5063586d. README.md and docs/accessibility/manual-checklist.md now enumerate this product's 27 routes, which the template's own verify:documentation requires. product-feature-switch.test.tsx was rewritten around the invariant that still applies here — no registered route without a component — rather than deleted with the screens it used to exercise. docs/operations/template-merge-2026-08-17.md records every decision, the gate results, and the three follow-ups this merge deliberately did not decide. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -462,6 +462,103 @@ The full `tests/unit` + `tests/integration` run is **1,845 passed / 1,864**,
|
||||
cgroup, RLIMIT and `/tmp` permission behaviour already recorded above — the same
|
||||
file failed identically before this work. No adapter test fails.
|
||||
|
||||
## Operational contract review (2026-08-15)
|
||||
|
||||
A fourth review looked past the adapter layer at the operational contract:
|
||||
feature on/off, environment separation, folder boundaries, and which gates were
|
||||
actually green. It found five red gates and three structural gaps. Every row
|
||||
below names the defect, not the symptom.
|
||||
|
||||
| id | area | disposition | what was actually wrong |
|
||||
| --- | --- | --- | --- |
|
||||
| `OPS-01` | release | `FIXED` | `public/` is copied verbatim into `dist/`, so every build — production included — shipped the local runtime document. Runtime config now comes from `config/runtime/<profile>.json`. |
|
||||
| `OPS-02` | release | `FIXED` | Release coherence proved the artifacts agreed with each other, never that they belonged in production. `FE-GATE-027` refuses an artifact whose `APP_ENV`, auth mode, endpoints or build identity do not match a declared `RELEASE_TARGET`, and refuses an undeclared target outright. |
|
||||
| `OPS-03` | runtime | `FIXED` | `REQUEST_TIMEOUT_MS` was validated and then never passed to the V3 executor; every operation ran on its contract's own deadline. It is now a ceiling that may tighten a contract, never loosen one. |
|
||||
| `OPS-04` | build | `FIXED` | `VITE_ROUTER_BASE_PATH` drove the router and the Service Worker scope but not Vite's asset `base`, so a sub-path deployment emitted root-absolute assets. One value now feeds all three. |
|
||||
| `OPS-05` | provider | `FIXED` | bubblewrap 0.9.0 drops whatever follows the option stream inside an `--args` file, so the sandboxed command was never executed: bwrap printed usage and exited 1. Options stay hidden; the command travels on real argv. |
|
||||
| `OPS-06` | provider | `FIXED` | The scope wrapper read its liveness pipe through `fs`, a blocking `read(2)` on a pipe the supervisor never closes. `process.exit` deadlocked joining that thread, so a completed provider was reported as a timeout kill. |
|
||||
| `OPS-07` | release | `FIXED` | `mkdir`/`open` modes were left to the ambient umask, so a hardened runner produced directories it could not enter and handed `tar` a file it could not re-open. |
|
||||
| `OPS-08` | release | `FIXED` | Promotion cleanup deleted this promotion's exact five through a pinned descriptor and only then noticed the leaf had been substituted, leaving a half-emptied directory a retry could not distinguish from a completed one. |
|
||||
| `OPS-09` | removability | `FIXED` | The removal fixture was not a repository, had no `.gitignore`, and each removal script kept its own copy-target list that had drifted. Supply-chain generation therefore failed inside every fixture and took the whole provider suite down with it. |
|
||||
| `OPS-10` | removability | `FIXED` | A platform integration file asserted the reference feature's route ids, so removing the feature left it importing a deleted module. The assertion moved to the feature's own test tree. |
|
||||
| `OPS-11` | removability | `FIXED` | A removal fixture runs against a deliberately reduced CI contract; the canonical exact-count tests re-imposed the full authority on it and failed the fixture for the reduction it exists to prove. |
|
||||
| `OPS-12` | browser | `FIXED` | Four browser-capability specs answered capability requests without the `protocol` field the hardened envelope requires, so every capability was refused and the download and part-upload paths asserted against an empty transcript. |
|
||||
| `OPS-13` | browser | `FIXED` | A refused capability document answered `recovery: NONE`, contradicting both the design record and the vault, which already answers `REISSUE_CAPABILITY`. |
|
||||
| `OPS-14` | performance | `FIXED` | Playwright matches accessible names by substring, so the navigation entry matched the home page's call to action too; the run died on a strict-mode violation before the first measurement and produced no evidence at all. |
|
||||
| `OPS-15` | visual | `FIXED` | The platform overview baseline predated the reference routes moving from `integration-defined` to `session-required`, so the only visual gate covering that page failed for its own staleness. |
|
||||
| `OPS-16` | architecture | `FIXED` | `src/contracts` imported `src/application` for the shared `Result` and the compatibility predicate; neither package owned the shared vocabulary. Both moved down to contracts. |
|
||||
| `OPS-17` | architecture | `FIXED` | The documented "no adapter depends on another concrete adapter" rule had no executable form, and `diagnostics` imported a guard out of `telemetry`. The guard moved to the adapter kernel and the rule is now enforced with a same-directory backreference. |
|
||||
| `OPS-18` | architecture | `PARTIAL` | Generic presentation still reads the installed-feature registries. The rule freezes the exact set of modules doing so today; a new edge fails. Lifting the assembly into `bootstrap` is not done. |
|
||||
| `OPS-19` | documentation | `FIXED` | README and the manual accessibility checklist both claimed six routes while ten were registered, leaving four screens outside the declared manual review scope. The list is now derived from the route registry by `verify:documentation`. |
|
||||
|
||||
### Product feature selection (2026-08-15, second pass)
|
||||
|
||||
| id | disposition | what changed |
|
||||
| --- | --- | --- |
|
||||
| `OPS-20` | `FIXED` | Which features a build contains is now a declared manifest rather than five registries spreading a literal. `VITE_PRODUCT_FEATURES` narrows it at build time; a test fails if a new registry forgets to consult it. |
|
||||
| `OPS-21` | `FIXED` | `FEATURE_OVERRIDES` in the runtime document takes an installed feature out of service without a rebuild. The router refuses its routes, not just the navigation, so a typed deep link cannot still mount it. |
|
||||
| `OPS-22` | `FIXED` | Both inputs are subtractive by vocabulary: the override enum has no `ENABLED`, and a build-time selection naming a feature the source tree does not declare is refused rather than ignored. |
|
||||
| `OPS-23` | `FIXED` | A sandbox that fails to launch now reports why. The supervisor consumed the child's output only to enforce a byte cap and discarded it, so a host restriction surfaced as an unexplained `exit=1`. Lines the sandbox tooling itself emits are kept; provider output is still discarded. |
|
||||
|
||||
An env var does **not** shrink the bundle, and the code says so. A static import
|
||||
cannot be undone by a value, and making the import graph depend on a
|
||||
configuration string is what §3.5 exists to prevent. Measured: `none` changes
|
||||
the output by 58 bytes. Physical removal is FE-GATE-020's job.
|
||||
|
||||
### Host restriction discovered during this pass
|
||||
|
||||
`bwrap --unshare-net` no longer works on this machine:
|
||||
|
||||
```
|
||||
$ printf '%s\0' --unshare-net --ro-bind /usr /usr ... | bwrap --args 3 -- /bin/true
|
||||
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
|
||||
$ sysctl kernel.apparmor_restrict_unprivileged_userns
|
||||
kernel.apparmor_restrict_unprivileged_userns = 1
|
||||
```
|
||||
|
||||
That reproduction contains none of this repository's code. Earlier in the same
|
||||
session the identical sandbox ran to completion, so the restriction became
|
||||
active partway through. While it holds, 16 of the 108 provider tests cannot run
|
||||
here — they need a sandbox the kernel will not grant. They are not counted as
|
||||
green and not counted as product defects; under a host that permits the
|
||||
namespace the same file was 107/108.
|
||||
|
||||
### Still red after this pass
|
||||
|
||||
*applies effective aggregate cgroup limits without exposing command or
|
||||
credentials* was rewritten. It used to read the live process tree with one
|
||||
`ps` per pid and assert mid-run, which lost a race against a sandbox that now
|
||||
completes in a few hundred milliseconds; it records the tree from `/proc` every
|
||||
5ms and asserts on the recording after the run. That restructuring is also what
|
||||
revealed the host restriction above — the supervisor had been failing to launch
|
||||
the sandbox and the test was dying on the observation first.
|
||||
|
||||
Tests that spawn processes, build archives and sign evidence were given a
|
||||
30s budget instead of the 10s default sized for pure-JS unit tests. The default
|
||||
was not raised: that would hide a genuinely hung test.
|
||||
|
||||
### FE-GATE-020 after this pass
|
||||
|
||||
| fixture | before | after |
|
||||
| --- | ---: | ---: |
|
||||
| reference feature | failed before its first assertion | 1,612 pass / 1 fail |
|
||||
| optional recipe | 39 failures | 1,386 pass / 2 fail |
|
||||
| browser file + storage | 40 failures | 1,006 pass / 3 fail |
|
||||
| realtime | not reached | 1,159 pass / 1 fail |
|
||||
|
||||
Every remaining failure is one of the three environment-limited tests above.
|
||||
|
||||
Lab performance now produces evidence, and that evidence shows the
|
||||
named-interaction budget missed on this machine (367–724ms against 200ms). The
|
||||
metric measures a full lazy-route navigation while the budget is an
|
||||
INP-shaped 200ms, so the two do not describe the same thing. No budget was
|
||||
changed to make this green.
|
||||
|
||||
WebKit remains unavailable in this environment (`libevent-2.1-7t64`,
|
||||
`libavif16` are not installed), so 14 browser-capability specs and the WebKit
|
||||
E2E project are unverified here. Chromium and Firefox are 28/28 and visual is
|
||||
5/5.
|
||||
|
||||
## Rules for updating this ledger
|
||||
|
||||
- A row moves out of `NOT_STARTED` only with a linked red test, its green run, and the commit id.
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
# Template merge — `main` → `feature/techlog-ui-migration`
|
||||
|
||||
Record of how the frontend template sync on `main` was integrated into the
|
||||
TechLog UI migration branch, and of every decision taken to resolve a conflict.
|
||||
|
||||
## Why this direction first
|
||||
|
||||
The template sync landed on `main` while the UI migration ran in a worktree.
|
||||
Two orders were possible.
|
||||
|
||||
Merging the feature branch into `main` first would have resolved 15 conflicts
|
||||
directly on the integration branch: a bad resolution would already be on `main`,
|
||||
and it would have to be repaired forward, on the branch other work depends on.
|
||||
|
||||
Merging `main` into the worktree first keeps the resolution where the UI work
|
||||
lives. Every gate runs against the resolved tree before anything reaches `main`,
|
||||
and a resolution that turns out wrong is discarded by resetting one feature
|
||||
branch. `main` is then only ever fast-forwarded, so it never holds a state that
|
||||
was not already proved in the worktree.
|
||||
|
||||
That is the order used here.
|
||||
|
||||
## Starting state
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| merge base | `325a2a0` |
|
||||
| `main` | `bdee07a` — `chore: sync the frontend template from a0fbafb to 5434760`, 1 commit, 101 files, +3116/−448 |
|
||||
| `feature/techlog-ui-migration` | `0355b64` — 29 commits |
|
||||
| files changed by `main` | 101 |
|
||||
| files changed by the feature branch | 423 |
|
||||
| overlap | 23 |
|
||||
|
||||
Rollback refs were created before touching anything:
|
||||
|
||||
```
|
||||
backup/ui-before-template-merge → 0355b64
|
||||
backup/main-before-ff → bdee07a
|
||||
```
|
||||
|
||||
### Pre-merge baseline on the feature branch
|
||||
|
||||
Captured so that a pre-existing failure could not be mistaken for a merge
|
||||
regression.
|
||||
|
||||
| Gate | Result |
|
||||
| --- | --- |
|
||||
| `check:types` | pass |
|
||||
| `lint` | pass |
|
||||
| `verify:documentation` | `PASS_SCOPED` |
|
||||
| `tests/unit` + `tests/component` | 1815 passed / 101 failed |
|
||||
|
||||
The 101 failures were confined to `tests/unit/ci-artifact-contract.test.ts` (19)
|
||||
and `tests/unit/ci-workflow-generation.test.ts` (82) — sandbox subprocess gates
|
||||
that cannot run in this environment.
|
||||
|
||||
## Conflicts and how each was decided
|
||||
|
||||
15 conflicts. The rule applied throughout: **keep the template's mechanism, keep
|
||||
the product's content, and never invent a third state that neither branch
|
||||
would accept.**
|
||||
|
||||
### Deletions the UI migration made deliberately (2)
|
||||
|
||||
| Path | Decision |
|
||||
| --- | --- |
|
||||
| `src/presentation/examples/platform-overview-page.tsx` | deletion kept |
|
||||
| `tests/visual/.../platform-overview-light-chromium-visual-linux.png` | deletion kept |
|
||||
|
||||
`main` modified both; the feature branch deleted them in `c5c8b94`. Nothing on
|
||||
the branch references either, so the deletion stands.
|
||||
|
||||
### `src/features/installed-feature-contracts.ts`
|
||||
|
||||
The template introduced a product manifest: registries are composed from
|
||||
`INSTALLED_PRODUCT_FEATURES` so that narrowing the selection withdraws a
|
||||
feature's routes, operations, schemas and messages.
|
||||
|
||||
The manifest composition is kept for operations, schemas, mappers and
|
||||
invalidation. The **route registry is deliberately not composed from
|
||||
`contract.routes`**, because the reference feature still declares
|
||||
`REFERENCE_RESOURCE_*` routes whose screens this product deleted during the
|
||||
migration. Reducing over them would register paths with no component behind
|
||||
them — a typed deep link resolving to nothing.
|
||||
|
||||
The registry therefore lists the platform routes (empty on this branch) and
|
||||
TechLog's. This was caught by running the gates: the first resolution did
|
||||
compose from `contract.routes`, and `tests/unit/product-features.test.ts`
|
||||
rejected it.
|
||||
|
||||
`ROUTE_FEATURE_OWNER` was narrowed to registered routes for the same reason.
|
||||
Attributing an unregistered route to a feature claims the kill switch governs
|
||||
something no router can mount.
|
||||
|
||||
### `src/features/installed-feature-runtimes.tsx`
|
||||
|
||||
The template gates the reference feature's route codecs and components on the
|
||||
manifest. This product deleted that feature's presentation layer, so the import
|
||||
does not resolve and there is nothing to gate. The gating was removed and the
|
||||
reason recorded in the file; it belongs back the day those screens return.
|
||||
|
||||
Consequence: this file no longer references the manifest, so it was moved to the
|
||||
exempt list in `tests/unit/product-features.test.ts` with the same note.
|
||||
`tests/component/product-feature-switch.test.tsx` was rewritten to hold the
|
||||
invariant that still applies here — no registered route without a component —
|
||||
which is exactly the trap the first resolution fell into.
|
||||
|
||||
### `src/features/installed-feature-adapters.ts`
|
||||
|
||||
Both the manifest check and TechLog's input are kept. The reference feature's
|
||||
input stays `Partial` because the manifest may narrow it out; TechLog's is total
|
||||
because it is this product's own UI and is always installed. A build that
|
||||
narrows the reference feature out still ships TechLog.
|
||||
|
||||
### `src/features/installed-feature-messages.ts`
|
||||
|
||||
The template's rationale — message keys stay total so `message()` cannot become
|
||||
partial — is kept, and TechLog's catalog is merged in on the same terms.
|
||||
|
||||
### `src/presentation/routes/app-router.tsx`
|
||||
|
||||
The template looked the route definition and runtime component up by id inside
|
||||
the route element; this branch passes both as props from the grouped route
|
||||
contract. The prop-driven signature is kept.
|
||||
|
||||
The template's **feature kill switch is adopted**: a route whose owning feature
|
||||
the runtime document disabled renders the disabled surface instead of mounting.
|
||||
Withdrawing it from navigation alone would leave a working deep link. `getRoute`
|
||||
was dropped from the imports because the definition arrives as a prop.
|
||||
|
||||
### `vite.config.ts`
|
||||
|
||||
Two independent additions — TechLog route chunking and `routerBasePath` — both
|
||||
kept. A brace was lost in the first concatenation and caught by `check:types`.
|
||||
|
||||
### `scripts/build-frontend.ts`
|
||||
|
||||
Both new steps exist in the merged body, so the header comment was renumbered:
|
||||
runtime config becomes step 4, the TechLog serving boundary step 7, the release
|
||||
manifest step 8, and the inline step comments were corrected to match.
|
||||
|
||||
### `scripts/test-performance.ts`
|
||||
|
||||
The template clicked a navigation link before measuring; this product measures
|
||||
its own landing route, which `goto` already reached, and has no `targetLabel`.
|
||||
The click was dropped. The template's lesson was kept as a comment because it
|
||||
applies to the next click that lands here: Playwright matches accessible names
|
||||
by substring, so a nav entry can also match a call to action and resolve to two
|
||||
links — a strict-mode violation that produces no performance evidence at all.
|
||||
|
||||
### Derived baselines — recomputed, not chosen (3)
|
||||
|
||||
`scripts/contracts/ci-gates.ts`, `tests/unit/task3-selective-integration.test.ts`
|
||||
and `tests/unit/ci-workflow-generation.test.ts` each pin counts and a digest
|
||||
describing the gate contract. **Neither side's numbers describe the merged
|
||||
`config/ci/gates.json`**, so taking either would have been wrong. They were
|
||||
recomputed from the merged file:
|
||||
|
||||
| Value | Result |
|
||||
| --- | --- |
|
||||
| canonical gate shape SHA-256 | `5063586d799f51de94c0f0ddaf9b75e180825bba5051bc309550425013ea81ef` |
|
||||
| gates | 27 |
|
||||
| commands | 82 |
|
||||
| command references | 94 |
|
||||
| evidence artifact references | 107 |
|
||||
| artifacts | 128 (126 product + 2 from the template) |
|
||||
|
||||
### `README.md`, `docs/accessibility/manual-checklist.md`
|
||||
|
||||
The template enumerated its own example routes. Replacing them with generic
|
||||
prose broke `verify:documentation`, which requires both documents to name every
|
||||
installed route id — a rule the template sync itself introduced. Both documents
|
||||
now enumerate this product's 27 routes.
|
||||
|
||||
## Result
|
||||
|
||||
Merge commit parents: `0355b64` (UI) and `bdee07a` (template).
|
||||
|
||||
### Gates on the merged tree
|
||||
|
||||
| Gate | Result |
|
||||
| --- | --- |
|
||||
| `check:types` | pass (6 projects) |
|
||||
| `lint` | pass, `--max-warnings=0` |
|
||||
| `check:architecture` | 399 modules, 1232 dependencies, 12 fixtures pass |
|
||||
| `check:adapter-inventory` | 120 files, 5 importers of the shared abort primitive |
|
||||
| `check:remediation-ledger` | 25 dispositions, 0 open |
|
||||
| `check:diagnostics` | 8 diagnostics, 5 telemetry producers |
|
||||
| `check:i18n` | 197 keys, 4 locales |
|
||||
| `check:design-system` | 48 tokens |
|
||||
| `verify:documentation` | `PASS_SCOPED` |
|
||||
| `test:integration` | 81 passed |
|
||||
| `test:recipes` | 17 passed |
|
||||
| `test:runtime-schema` | 40 passed |
|
||||
| `tests/unit` + `tests/component` | 1851 passed / 98 failed |
|
||||
|
||||
The 98 failures are the same two sandbox files as the baseline —
|
||||
`ci-workflow-generation` (82) and `ci-artifact-contract` (16, down from 19). No
|
||||
file fails that did not fail before the merge.
|
||||
|
||||
## Mistake made during this merge
|
||||
|
||||
`git stash` was run inside the worktree while the merge was still in progress,
|
||||
to compare a gate against the pre-merge tree. That removed `MERGE_HEAD`: the
|
||||
resolved content survived, but git no longer knew a merge was underway, and
|
||||
committing then would have produced a single-parent commit — leaving `main` off
|
||||
the ancestry and breaking the fast-forward that step 2 depends on. `MERGE_HEAD`
|
||||
was restored to `bdee07a` before committing, and the resulting commit has both
|
||||
parents.
|
||||
|
||||
To compare against a pre-merge state, use a separate checkout rather than
|
||||
stashing an in-progress merge.
|
||||
|
||||
## Follow-ups this merge deliberately did not decide
|
||||
|
||||
1. **TechLog is outside the product manifest.** It is composed directly rather
|
||||
than as a `SelectableProductFeature`, so the runtime kill switch does not
|
||||
govern it. Promoting it is a product decision.
|
||||
2. **The runtime kill switch currently governs no route.** The only installed
|
||||
feature's screens were removed, so the switch still gates operations and
|
||||
schemas but has no user-visible effect. It starts asserting again as soon as
|
||||
a feature-owned route is registered.
|
||||
3. **The reference feature declares routes it cannot serve.** Either its screens
|
||||
return, or its route declarations should be withdrawn from its contract.
|
||||
Reference in New Issue
Block a user