rcf-lite 0.31.0 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/blueprints/delivery-ci-workflows/CHANGELOG.md +5 -0
  3. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6102.json +5 -1
  4. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6103.json +5 -1
  5. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6108.json +5 -1
  6. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6111.json +10 -2
  7. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6121.json +5 -1
  8. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6122.json +5 -1
  9. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6123.json +5 -1
  10. package/blueprints/deploy-cloudflare-workers/CHANGELOG.md +5 -0
  11. package/blueprints/deploy-cloudflare-workers/contributions/user-stories/deploy-cloudflare-workers-us-12103.json +5 -1
  12. package/blueprints/deploy-cloudflare-workers/contributions/user-stories/deploy-cloudflare-workers-us-12110.json +5 -1
  13. package/blueprints/deploy-cloudflare-workers/contributions/user-stories/deploy-cloudflare-workers-us-12113.json +1 -1
  14. package/blueprints/email-smtp-resend/CHANGELOG.md +5 -0
  15. package/blueprints/email-smtp-resend/contributions/user-stories/email-smtp-resend-us-4104.json +5 -1
  16. package/blueprints/email-smtp-resend/contributions/user-stories/email-smtp-resend-us-4105.json +5 -1
  17. package/blueprints/email-smtp-resend/contributions/user-stories/email-smtp-resend-us-4106.json +5 -1
  18. package/blueprints/observability-probe-endpoints/CHANGELOG.md +5 -0
  19. package/blueprints/observability-probe-endpoints/contributions/user-stories/observability-probe-endpoints-us-14104.json +5 -1
  20. package/fixtures/canary-manifest.json +9 -9
  21. package/package.json +1 -1
  22. package/rcf/code-nodes/cn-017.json +4 -2
  23. package/rcf/code-nodes/cn-771.json +2 -1
  24. package/rcf/code-nodes/cn-778.json +14 -0
  25. package/rcf/fbs/fbs-004.json +4 -1
  26. package/rcf/fbs/fbs-201.json +2 -1
  27. package/rcf/tacs/tac-4127-readiness-view.json +3 -2
  28. package/rcf/test-suites/ts-004.json +82 -2
  29. package/rcf/test-suites/ts-223.json +24 -2
  30. package/rcf/user-stories/us-18001.json +6 -1
  31. package/rcf/user-stories/us-202.json +25 -1
  32. package/releases/releases.yaml +18 -1
  33. package/src/cli/freeze.js +8 -0
  34. package/src/cli/readiness.js +2 -38
  35. package/src/define/interface-paths.js +47 -0
  36. package/src/query/gates.js +57 -14
  37. package/src/view/html-page.js +29 -11
  38. package/src/view/index.js +16 -2
  39. package/src/view/live-client.js +18 -10
  40. package/src/view/style.css +10 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.32.1] - 2026-10-06
10
+
11
+ 0.32.1 ships two fixes merged since 0.32.0 (merge b6292f5) on top of the layout baseline chore. The viewer's Readiness tab now agrees with the CLI on D3 shapes:pathsResolve, and the D5 crosscut:operationalConcerns trigger reads the structured ac.scope field in addition to the legacy "[deployed]" marker. breaking: false; no behavioural changes on verbs consumers run in CI; fixes only. minAgentAction: null; the managed agent-instructions block hash did not move.
12
+
13
+ ### Fixed
14
+
15
+ - **Viewer Readiness tab D3 shapes:pathsResolve now agrees with the CLI (issue 307, w-2026-10-06-dave-026).** The viewer's render path (`src/view/index.js` `renderModelToPage`) called `computeReadiness` without the `resolvedPaths` pre-walk, so D3 shapes:pathsResolve false-failed on TAC interface `path:` tokens that resolve on disk and the Readiness tab disagreed with the CLI and freeze. Hit on ai-baseline define/kickoff: CLI reported D3 4/4 passing while the viewer tab showed D3 shapes:pathsResolve failing on four TAC interface paths. Fix: thread `resolvedPaths` through the viewer compute via the same `resolveInterfacePaths` seam the CLI (PR 304, issue 302) and freeze use. The viewer compute now runs the resolve walk against `projectRoot` before `computeReadiness` and passes the resulting `resolvedPaths` set to the compute. The pure gate reads it unchanged. Additive: `renderModelToPage` also returns the computed readiness so a regression test can assert the viewer's check state without parsing HTML. Regression test in `test/view/readiness-paths-resolve-307.test.js` seeds a TAC interface whose `path:` token points at a real file and asserts the viewer compute reports D3 ok; the negative control flips the path to a missing file and asserts D3 fails with the shapes:pathsResolve finding.
16
+
17
+ - **D5 crosscut:operationalConcerns trigger reads structured `ac.scope` (issue 306, w-2026-10-06-dave-031).** `computeOperationalConcernsCheck` in `src/query/gates.js` looked only for a literal `[deployed]` marker in AC description text, so trees that carry scope as the schema-canonical field `scope: "deployed"` (like ai-baseline, with 23 deployed-scope ACs and `TAD.operationalConcerns` absent) silently passed 0/0 and the gate never fired. Fix: the trigger now also reads `ac.scope === "deployed"`; a deployed-scope AC on either form must find a non-empty `TAD.operationalConcerns`. Legacy `[deployed]` markers are unchanged. Regression fixtures in `test/query/gates.test.js` cover the ai-baseline shape (structured `ac.scope` with empty `operationalConcerns` fails) and a negative control (non-deployed-scope ACs with empty `operationalConcerns` pass).
18
+
19
+ ## [0.32.0] - 2026-10-06
20
+
21
+ 0.32.0 ships four readiness and viewer fixes merged since 0.31.0 (merge ec9464db) as one release. D6 consistency:orphanInterfaces now accepts the schema-canonical object-shape REQ.deliveredBy (issue 301), D3 shapes:pathsResolve inside rcf define freeze pre-resolves TAC interface path tokens the way readiness does (issue 302), the deploy-cloudflare-workers blueprint reworded AC-12113-2 so D6 consistency:unsatisfiable stops reading "absent" as a field name (PR 300), the viewer's SSE tree-update resync preserves the active tab for every main tab including Readiness (issue 297), and the audit view header keeps the tabs visible under long project names and prefers prd.productName (issue 295). minAgentAction: null; the managed agent-instructions block hash did not move.
22
+
23
+
24
+ ### Fixed
25
+
26
+ - **D6 consistency:orphanInterfaces now accepts the schema-canonical object-shape REQ.deliveredBy (issue 301, w-2026-10-06-dave-020).** `collectReachedInterfaces` in `src/query/gates.js` previously accepted only array-shape `deliveredBy` and dropped the `{tacId, adrId?, field?}` object form that `@stravica-ai/rcf-schemas` 0.6.3 defines as canonical. Every schema-conforming REQ (including every blueprint-contributed REQ) therefore had its `deliveredBy` silently skipped by the orphanInterfaces reach-walk, and interfaces on blueprint TACs with no AC ownerRef read as orphans. Hit on ai-baseline define/kickoff (115/173 failing on nine applied blueprints). Fix: the walk now also handles the object form, routing `field: interfaces[<name>]` to a single interface via `parseOwnerRefInterfaceField` and a bare `tacId` to every interface on the named TAC (the "delivered by this TAC" meaning carried over from the array-shape bare pointer). Legacy array-shape pointers are unchanged. Regression fixtures in `test/query/gates.test.js` cover the object-shape interface pointer, the bare-tacId object form, and the legacy array shape.
27
+
28
+ - **D3 shapes:pathsResolve inside `rcf define freeze` now pre-resolves TAC interface `path:` tokens the way `rcf define readiness` does (issue 302, w-2026-10-06-dave-022).** `src/cli/freeze.js` called `computeReadiness` without `resolvedPaths`, so the D3 gate fell back to an empty set and false-failed every repo-relative `path:` token the engineer named on a TAC interface description, even when the file existed on disk and the readiness CLI (which pre-resolves via `resolveInterfacePaths` in `src/cli/readiness.js`) passed D3 4/4. Hit on ai-baseline define/kickoff: readiness green on D3, freeze refused with four shapes:pathsResolve findings. Fix: lifted `resolveInterfacePaths` out of `src/cli/readiness.js` into `src/define/interface-paths.js` so freeze can share it. Freeze now runs the resolve walk against `projectRoot` before `computeReadiness` and passes the resulting `resolvedPaths` set to the compute. The pure gate reads it unchanged. Regression test in `test/cli/freeze.test.js` seeds a TAC interface whose `path:` token points at a real file and asserts freeze succeeds, then flips to a missing path and asserts freeze refuses with exit 4 and surfaces the shapes:pathsResolve finding.
29
+
30
+ - **Blueprint deploy-cloudflare-workers: AC-12113-2 reworded so D6 consistency:unsatisfiable stops reading "absent" as a field name.** The description parenthetical "Cloudflare's runtime treats the absent field as the default asset-lookup-first shape" tripped extractThenFieldTokens pattern 2 (`<name> field`) which captured "absent" and no recordShape defines that field. Reworded to "treats a missing run_worker_first as the default asset-lookup-first shape"; meaning preserved, the field reference now names the real WranglerAssetsBlock field. Hit on ai-baseline define/kickoff.
31
+
32
+ - **Viewer: SSE tree-update preserves the active tab for every main tab, Readiness included (issue 297, w-2026-10-06-dave-003).** `resyncTabsAfterSwap` in `src/view/live-client.js` hard-coded a five-entry TABS array (`overview`, `requirements`, `architecture`, `build`, `product-map`) that omitted `readiness`. When Readiness was the active tab and an SSE tree-update swapped `#rcf-live-content` innerHTML (which happens about every 30s since issue 236 writes a heartbeat into `rcf/manifest.json`), the resync `indexOf` returned -1, fell back to Overview, unhid `#tab-overview` and never visited `#tab-readiness`, so both panels were visible. The viewer's standalone context still recovered a tick later when `rcfPage.init()` re-resolved the hash, but WESPA's admin SPA embed (`iframe /rcf-viewer/?embed=1&theme=...`, no hash) saw the Readiness tab silently switch to Overview. The parallel array was removed: the tab list is now derived from the server-rendered `nav.tabs` itself (every `[role="tab"][data-tab]` present at swap time) so a future tab added to the renderer cannot be forgotten by live-client. The overview fallback stays for a genuinely unknown or missing selection. AC-18001-8, CN-778. Issue 236 (heartbeat into git-tracked manifest.json) is unchanged.
33
+ - **Chain: live-client resync responsibility named on TAC-4127-readiness-view.** The TAC previously named `src/view/html-page.js` (now `src/view/page-init.js`) as the owner of the viewer-side tab list but did not name the live-client's post-swap resync. Added a responsibility bullet so a future reader finds both ends of the chain from one node. The TAC's substantive behaviour is unchanged.
34
+ - **Audit view header: tabs always visible under long project names (issue 295).** Dex (WSD) reported on the WESPA project (`manifest.projectName` = `WESPA - WSD Enterprise Semantic Platform for Analytics`, 54 chars) that the sixth main tab (Build) was clipped at 1440 and 1920 and the Find button in `.tools` sat on top of it. The .bar is `max-width: 1280px` with flex children `.brand`, `.product`, `[role=tablist]`, `.tools`; the pre-fix tablist carried `flex: 1 1 auto; min-width: 0; overflow-x: auto` so it was free to shrink below its intrinsic width while `.product` did not yield. The fix moves the shrink budget onto `.product` (via `min-width: 0` on both the flex item and on `.product .name`, so `text-overflow: ellipsis` triggers inside the flex container) and pins `nav.tabs` and `.tools` to `flex-shrink: 0` so the six tabs and the tools group hold their intrinsic widths. `.product .name` now also carries `title="<full projectName>"` so the ellipsis is reversible on hover; the attribute is HTML-escaped on the same path as the inner text. The fix holds in embed and non-embed, light and dark, with the 0.31.0 theme control present. `src/view/style.css`, `src/view/html-page.js`; US-202 gains AC-202-4 (layout rules) and AC-202-5 (title attribute); TS-004 gains the matching passing TCs; CN-017 and CN-771 updated to implement the new ACs; FBS-004 `acIds` updated.
35
+
36
+ ### Changed
37
+
38
+ - **Audit view header: project display name prefers `prd.productName` (issue 295).** Pre-fix the header read `manifest.projectName`, then fell back to `prd.productName` and `prd.productTitle`. `rcf define update MANIFEST` is not addressable (the manifest has no id in the family grammar), so `manifest.projectName` was a one-shot init value and the only way to rename the audit chrome without re-initialising was a hand edit of `manifest.json`. `prd.productName` is editable via `rcf define update PRD-001 --set productName=<value>`; preferring it in the header makes the display name lite-native without touching the manifest, which keeps owning ids and paths. `src/view/html-page.js` `resolveProductHeader`; US-202 gains AC-202-6; TS-004 gains the matching passing TCs; CN-017 updated. Follow-up for Dave: a lite-native `rcf define update MANIFEST --set projectName=<value>` (Part 2 cut a from the brief) is a separate, larger change (manifest is not a family id; the update verb would grow a MANIFEST special-case alongside the one `rcf define read` already carries) and is NOT shipped here.
39
+
9
40
  ## [0.31.0] - 2026-10-05
10
41
 
11
42
  0.31.0 ships three themes from rcf-lite PRs 291, 292 and 293. The viewer gains a standalone Light | Dark | Auto theme control (ADR-4137), readable FIND and LookupModal hover and focus states in dark, and a WCAG 2.1 AA contrast audit test that blocks a merge dropping any colour pair below the floor in either theme. DEFINE step 3 rulings R1 to R8 and R13 land, with every tree-wide readiness check evaluated before any scope short-circuit through one shared helper (ADR-4138). And `--id` write-backs are idempotent: `rcf define ledger <name> add --id` and `rcf define create <kind> --id` treat a same-id replay with identical content as a no-op. Breaking under the pre-1.0 convention for three reasons: `rcf define create <kind> --id` and `rcf define ledger <name> add --id` now exit 3 on same-id-different-content; the readiness envelope for a scope-empty stage with a failing tree-wide check is `failing` (or `acknowledged` when acked at the current hash on a warn-with-ack stage), not `notApplicable`; and `shapes:draftSettled` is tree-wide, so a `[draft]` marker outside the D3 scope now fails the check. The managed agent-instructions block did not change, so no agent re-init is needed (minAgentAction null).
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - adds ownerRef on AC-6102-1 (interfaces[entry-point CLI]), AC-6103-1 (interfaces[gate-descriptor]), AC-6108-1 (interfaces[aggregate-report-schema]), AC-6111-1 (interfaces[entry-point CLI]), AC-6111-2 (interfaces[workflow-shape]), AC-6121-2 (interfaces[release-mode]), AC-6122-1 (interfaces[entry-point CLI]) and AC-6123-1 (interfaces[entry-point CLI]) so TAC-701 gate-descriptor and entry-point CLI, TAC-703 aggregate-report-schema, TAC-704 workflow-shape and entry-point CLI, TAC-705 release-mode and entry-point CLI, and TAC-706 entry-point CLI have delivering AC owners (D6 consistency:orphanInterfaces).
6
+
7
+
3
8
  ## 2.3.5 - 2026-09-11
4
9
 
5
10
  probe-utils.writeReport no longer promotes an all-accountBoundSkipped row set to PASS via a separate override; aggregate() already returns PASS for a pure-skip set on its own and returns WARN when any WARN row is present, so an all-skipped array carrying a WARN row is no longer silently promoted. Anatomy suite tracks the strengthened shared helper.
@@ -17,7 +17,11 @@
17
17
  "then": "the aggregate report's `runner.entryPoint` field records a project-relative path to a `.js` or `.mjs` file that exists at that path in the tree",
18
18
  "testable": true,
19
19
  "scope": "runtime",
20
- "disposition": "fixed"
20
+ "disposition": "fixed",
21
+ "ownerRef": {
22
+ "tacId": "TAC-701-delivery-ci-workflows-gate-runner",
23
+ "field": "interfaces[entry-point CLI]"
24
+ }
21
25
  },
22
26
  {
23
27
  "id": "AC-6102-2",
@@ -17,7 +17,11 @@
17
17
  "then": "the file exists, its `gate` field is `validate`, its `command` field names the `rcf define validate` invocation, its `exitCode` field records the underlying process exit code, and its `stdout` and `stderr` fields carry the captured streams",
18
18
  "testable": true,
19
19
  "scope": "runtime",
20
- "disposition": "template"
20
+ "disposition": "template",
21
+ "ownerRef": {
22
+ "tacId": "TAC-701-delivery-ci-workflows-gate-runner",
23
+ "field": "interfaces[gate-descriptor]"
24
+ }
21
25
  },
22
26
  {
23
27
  "id": "AC-6103-2",
@@ -17,7 +17,11 @@
17
17
  "then": "the aggregate report's `gates[]` array contains a `coverage-strict` entry with `outcome: missing`, its `reason` field carries a non-empty string, and the aggregate `verdict` is `failed`",
18
18
  "testable": true,
19
19
  "scope": "runtime",
20
- "disposition": "template"
20
+ "disposition": "template",
21
+ "ownerRef": {
22
+ "tacId": "TAC-703-delivery-ci-workflows-aggregate-report",
23
+ "field": "interfaces[aggregate-report-schema]"
24
+ }
21
25
  },
22
26
  {
23
27
  "id": "AC-6108-2",
@@ -17,7 +17,11 @@
17
17
  "then": "the materialiser reads the file at the documented path and treats its `workflowShape` as authoritative",
18
18
  "testable": true,
19
19
  "scope": "runtime",
20
- "disposition": "template"
20
+ "disposition": "template",
21
+ "ownerRef": {
22
+ "tacId": "TAC-704-delivery-ci-workflows-workflow-materialiser",
23
+ "field": "interfaces[entry-point CLI]"
24
+ }
21
25
  },
22
26
  {
23
27
  "id": "AC-6111-2",
@@ -27,7 +31,11 @@
27
31
  "then": "the materialiser processes the recognised fields (including the substitution-surface trio) and ignores the unknown fields without failing the boot-check",
28
32
  "testable": true,
29
33
  "scope": "runtime",
30
- "disposition": "template"
34
+ "disposition": "template",
35
+ "ownerRef": {
36
+ "tacId": "TAC-704-delivery-ci-workflows-workflow-materialiser",
37
+ "field": "interfaces[workflow-shape]"
38
+ }
31
39
  },
32
40
  {
33
41
  "id": "AC-6111-3",
@@ -27,7 +27,11 @@
27
27
  "then": "the release workflow creates the release entity, runs the artefact-publish step, uploads the published artefact to the release entity, and records the just-published version identifier in the aggregate report",
28
28
  "testable": true,
29
29
  "scope": "runtime",
30
- "disposition": "template"
30
+ "disposition": "template",
31
+ "ownerRef": {
32
+ "tacId": "TAC-705-delivery-ci-workflows-release-workflow",
33
+ "field": "interfaces[release-mode]"
34
+ }
31
35
  },
32
36
  {
33
37
  "id": "AC-6121-3",
@@ -17,7 +17,11 @@
17
17
  "then": "the release workflow dispatches the promote workflow with `versionId` set to the just-published version identifier and waits on the promote workflow's outcome",
18
18
  "testable": true,
19
19
  "scope": "runtime",
20
- "disposition": "template"
20
+ "disposition": "template",
21
+ "ownerRef": {
22
+ "tacId": "TAC-705-delivery-ci-workflows-release-workflow",
23
+ "field": "interfaces[entry-point CLI]"
24
+ }
21
25
  },
22
26
  {
23
27
  "id": "AC-6122-2",
@@ -17,7 +17,11 @@
17
17
  "then": "a `scheduled-audit` workflow file lands at the provider's workflow path, its trigger set names a cron schedule, and its command line invokes the Node gate-runner entry point with the required check set writing to the distinct report path",
18
18
  "testable": true,
19
19
  "scope": "runtime",
20
- "disposition": "template"
20
+ "disposition": "template",
21
+ "ownerRef": {
22
+ "tacId": "TAC-706-delivery-ci-workflows-scheduled-audit",
23
+ "field": "interfaces[entry-point CLI]"
24
+ }
21
25
  },
22
26
  {
23
27
  "id": "AC-6123-2",
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - adds ownerRef on AC-12103-1 (interfaces[workerIdentity]) and AC-12110-1 (interfaces[probeUrl]) so the preview URL resolver's workerIdentity and probeUrl interfaces have delivering AC owners (D6 consistency:orphanInterfaces).
6
+
7
+
3
8
  ## 1.3.2 (register patch, 2026-09-10)
4
9
 
5
10
  - Register: neutral wording on the ADR-1306 consequences field (closes F-1); no capability change.
@@ -17,7 +17,11 @@
17
17
  "then": "both responses carry the `versionSha` field of V's build provenance in the health-probe body, and the two responses agree on that field",
18
18
  "testable": true,
19
19
  "scope": "deployed",
20
- "disposition": "template"
20
+ "disposition": "template",
21
+ "ownerRef": {
22
+ "tacId": "TAC-1303-deploy-cloudflare-workers-preview-url-resolver",
23
+ "field": "interfaces[workerIdentity]"
24
+ }
21
25
  },
22
26
  {
23
27
  "id": "AC-12103-2",
@@ -17,7 +17,11 @@
17
17
  "then": "at least one HTTP request to the production URL's `/healthz` completes within the sixty-second window; the request carries no `Authorization` header that would bypass a production auth layer; the retry count for any single probe does not exceed six",
18
18
  "testable": true,
19
19
  "scope": "deployed",
20
- "disposition": "template"
20
+ "disposition": "template",
21
+ "ownerRef": {
22
+ "tacId": "TAC-1303-deploy-cloudflare-workers-preview-url-resolver",
23
+ "field": "interfaces[probeUrl]"
24
+ }
21
25
  },
22
26
  {
23
27
  "id": "AC-12110-2",
@@ -21,7 +21,7 @@
21
21
  },
22
22
  {
23
23
  "id": "AC-12113-2",
24
- "description": "Given the operator answers run-worker-first with true alongside a non-empty assets-directory, when the manifest is generated, then the [assets] block carries run_worker_first = true. Given the operator answers run-worker-first with false or leaves it unanswered, when the manifest is generated, then the [assets] block does NOT carry a run_worker_first key (Cloudflare's runtime treats the absent field as the default asset-lookup-first shape). Anchored to TAC-1302; probed by assets-manifest-scan.mjs with anchorAcId AC-12113-1 (the same module surfaces both facets of the shape).",
24
+ "description": "Given the operator answers run-worker-first with true alongside a non-empty assets-directory, when the manifest is generated, then the [assets] block carries run_worker_first = true. Given the operator answers run-worker-first with false or leaves it unanswered, when the manifest is generated, then the [assets] block does NOT carry a run_worker_first key (Cloudflare's runtime treats a missing run_worker_first as the default asset-lookup-first shape). Anchored to TAC-1302; probed by assets-manifest-scan.mjs with anchorAcId AC-12113-1 (the same module surfaces both facets of the shape).",
25
25
  "given": "an applied fixture whose operator answered assets-directory = ./dist and run-worker-first = true",
26
26
  "when": "the probe module parses the fixture wrangler.toml",
27
27
  "then": "the parsed [assets] table carries run_worker_first equal to true; when the fixture is switched with SIMULATE_EMPTY_ASSETS=true the run_worker_first field is absent (the whole [assets] block is absent) and the probe still returns verdict pass because the bare-Worker shape is legal",
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - adds ownerRef on AC-4106-1 (interfaces[smtpClient]), AC-4104-2 (interfaces[auditSink]) and AC-4105-1 (interfaces[verifierClock]) so TAC-401 smtpClient and TAC-402 auditSink and verifierClock have delivering AC owners (D6 consistency:orphanInterfaces).
6
+
7
+
3
8
  ## 1.1.7 - 2026-09-11
4
9
 
5
10
  unverified-sender-refusal AC-4102-1 and AC-4102-2 rows are de-claimed to conformanceOnly. The refusal is driven by the local catch-all SMTP fixture (fixture-as-engine) and carries no Resend-returned message id, so the identifier half of the strict-evidence contract is not satisfied here. The live real-account-resend-send row (AC-4101-2) is unchanged.
@@ -27,7 +27,11 @@
27
27
  "then": "the response status is 401, the response body is empty, the handler invocation counter is unchanged, and the audit sink observes exactly one `webhookVerificationFailed` event",
28
28
  "testable": true,
29
29
  "scope": "runtime",
30
- "disposition": "fixed"
30
+ "disposition": "fixed",
31
+ "ownerRef": {
32
+ "tacId": "TAC-402-email-smtp-resend-webhook-verifier",
33
+ "field": "interfaces[auditSink]"
34
+ }
31
35
  },
32
36
  {
33
37
  "id": "AC-4104-3",
@@ -17,7 +17,11 @@
17
17
  "then": "the response status is 401, the response body is empty, the handler invocation counter is unchanged, and the audit sink observes exactly one `webhookReplayStale` event",
18
18
  "testable": true,
19
19
  "scope": "runtime",
20
- "disposition": "template"
20
+ "disposition": "template",
21
+ "ownerRef": {
22
+ "tacId": "TAC-402-email-smtp-resend-webhook-verifier",
23
+ "field": "interfaces[verifierClock]"
24
+ }
21
25
  },
22
26
  {
23
27
  "id": "AC-4105-2",
@@ -17,7 +17,11 @@
17
17
  "then": "the scan returns exactly one module and that module is the adapter module",
18
18
  "testable": true,
19
19
  "scope": "runtime",
20
- "disposition": "fixed"
20
+ "disposition": "fixed",
21
+ "ownerRef": {
22
+ "tacId": "TAC-401-email-smtp-resend-send-adapter",
23
+ "field": "interfaces[smtpClient]"
24
+ }
21
25
  },
22
26
  {
23
27
  "id": "AC-4106-2",
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - adds ownerRef on AC-14104-1 (interfaces[listenerFactory]) so the listener-topology's listenerFactory interface has a delivering AC owner (D6 consistency:orphanInterfaces).
6
+
7
+
3
8
  ## 1.2.5 - 2026-09-11
4
9
 
5
10
  Anatomy suite tracks the strengthened shared helper (WARN-only shortcut removed, log objects and content lists no longer identifiers, retained anchor existence check). No probe changes required.
@@ -17,7 +17,11 @@
17
17
  "then": "a GET against `/live` on port 9091 returns 200, a GET against `/live` on the user-traffic port returns 404 (or the project's default not-found response), and a GET against `/ready` on the user-traffic port returns 404",
18
18
  "testable": true,
19
19
  "scope": "runtime",
20
- "disposition": "template"
20
+ "disposition": "template",
21
+ "ownerRef": {
22
+ "tacId": "TAC-1503-observability-probe-endpoints-listener-topology",
23
+ "field": "interfaces[listenerFactory]"
24
+ }
21
25
  },
22
26
  {
23
27
  "id": "AC-14104-2",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "registerCanary": [
3
3
  {
4
- "id": "rc-2026-10-05-001",
5
- "createdAt": "2026-10-05T15:28:27.972Z",
6
- "buildVersion": "0.31.0-mockdriver",
4
+ "id": "rc-2026-10-06-001",
5
+ "createdAt": "2026-10-06T18:28:43.472Z",
6
+ "buildVersion": "0.32.1-mockdriver",
7
7
  "fixturePromptId": "canary-prompt-01",
8
8
  "responseWordCount": 55,
9
9
  "grades": {
@@ -34,9 +34,9 @@
34
34
  "shipDespiteFailReason": "mock canary driver used; no real subagent was dispatched. This record verifies canary infrastructure only, not the register itself."
35
35
  },
36
36
  {
37
- "id": "rc-2026-10-05-002",
38
- "createdAt": "2026-10-05T15:28:27.972Z",
39
- "buildVersion": "0.31.0-mockdriver",
37
+ "id": "rc-2026-10-06-002",
38
+ "createdAt": "2026-10-06T18:28:43.473Z",
39
+ "buildVersion": "0.32.1-mockdriver",
40
40
  "fixturePromptId": "canary-prompt-02",
41
41
  "responseWordCount": 55,
42
42
  "grades": {
@@ -67,9 +67,9 @@
67
67
  "shipDespiteFailReason": "mock canary driver used; no real subagent was dispatched. This record verifies canary infrastructure only, not the register itself."
68
68
  },
69
69
  {
70
- "id": "rc-2026-10-05-003",
71
- "createdAt": "2026-10-05T15:28:27.972Z",
72
- "buildVersion": "0.31.0-mockdriver",
70
+ "id": "rc-2026-10-06-003",
71
+ "createdAt": "2026-10-06T18:28:43.473Z",
72
+ "buildVersion": "0.32.1-mockdriver",
73
73
  "fixturePromptId": "canary-prompt-03",
74
74
  "responseWordCount": 55,
75
75
  "grades": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rcf-lite",
3
- "version": "0.31.0",
3
+ "version": "0.32.1",
4
4
  "type": "module",
5
5
  "description": "One-install tooling for the Requirements Confidence Framework (RCF): the unified `rcf` CLI grouped into the five RCF tool groups (discover, define, build, verify, audit) plus a small core set (init, doctor, guidance, mcp), an MCP server, the live tree viewer and the fresh-context adversarial ship-gate verifier. Consumes @stravica-ai/rcf-schemas.",
6
6
  "license": "Apache-2.0",
@@ -6,11 +6,13 @@
6
6
  "implementsAcIds": [
7
7
  "AC-202-1",
8
8
  "AC-202-2",
9
- "AC-202-3"
9
+ "AC-202-3",
10
+ "AC-202-5",
11
+ "AC-202-6"
10
12
  ],
11
13
  "dependencies": [],
12
14
  "version": "0.1.0",
13
15
  "status": "draft",
14
16
  "createdAt": "2026-07-10T11:15:32.956Z",
15
- "updatedAt": "2026-07-10T11:15:32.956Z"
17
+ "updatedAt": "2026-10-06T10:24:19.149Z"
16
18
  }
@@ -4,11 +4,12 @@
4
4
  "title": "SearchButton hover/focus/active + three-state theme control styling (AA contrast in both themes)",
5
5
  "description": "Baz 2026-10-04 (w-2026-10-04-dave-001): the FIND SearchButton hover previously fell back to a hard-coded light grey (`var(--sv-surface-hover, #f0f3f8)`) because `--sv-surface-hover` was never declared, which painted dark ink on a light grey in dark mode. The hover + focus-visible + active states now use `--sv-active` with the `--sv-control` border and the `--sv-focus` outline so the pair stays above WCAG AA in both themes (AC-205-9). The three-state theme control (`.rcf-theme`, `.rcf-theme-btn`) sits right of the SearchButton in `.tools`; `aria-pressed=\"true\"` uses `--sv-button` / `--sv-onbutton`, hover and focus-visible use `--sv-active`, and the group is hidden under `html[data-embed=\"1\"]`.",
6
6
  "implementsAcIds": [
7
+ "AC-202-4",
7
8
  "AC-205-9"
8
9
  ],
9
10
  "dependencies": [],
10
11
  "version": "0.1.0",
11
12
  "status": "draft",
12
13
  "createdAt": "2026-10-05T09:59:33Z",
13
- "updatedAt": "2026-10-05T09:59:33Z"
14
+ "updatedAt": "2026-10-06T10:24:19.675Z"
14
15
  }
@@ -0,0 +1,14 @@
1
+ {
2
+ "cnId": "CN-778",
3
+ "path": "src/view/live-client.js#resyncTabsAfterSwap",
4
+ "title": "SSE tree-update tab resync derives the tab list from the rendered nav.tabs (SSOT)",
5
+ "description": "After the SSE tree-update swaps `#rcf-live-content` innerHTML, the newly-inserted panels carry the Phase 3.6 server-rendered default (one visible, the rest hidden). resyncTabsAfterSwap reads every [role=\"tab\"][data-tab] under the surviving `nav.tabs`, finds the aria-selected button, and aligns `#tab-<name>` visibility so exactly the active tab is visible. The tab list is derived from the DOM rather than a parallel array in this file: before issue 297 the parallel array omitted 'readiness' and a swap while Readiness was active fell back to Overview with both panels visible. The overview fallback is kept only for a genuinely unknown or missing selection. Pairs with CN-744 and CN-745 (which this file also hosts) and with page-init.js's activateTab for the non-swap routing path (AC-18001-8).",
6
+ "implementsAcIds": [
7
+ "AC-18001-8"
8
+ ],
9
+ "dependencies": [],
10
+ "version": "0.1.0",
11
+ "status": "draft",
12
+ "createdAt": "2026-10-06T12:00:00Z",
13
+ "updatedAt": "2026-10-06T12:00:00Z"
14
+ }
@@ -9,6 +9,9 @@
9
9
  "AC-202-1",
10
10
  "AC-202-2",
11
11
  "AC-202-3",
12
+ "AC-202-4",
13
+ "AC-202-5",
14
+ "AC-202-6",
12
15
  "AC-203-1",
13
16
  "AC-203-2"
14
17
  ],
@@ -30,7 +33,7 @@
30
33
  "domain": "view",
31
34
  "notes": "The page is generated fresh each run; no cached prior state lingers between regenerations.",
32
35
  "createdAt": "2026-06-25T00:00:00Z",
33
- "updatedAt": "2026-07-02T10:22:43Z",
36
+ "updatedAt": "2026-10-06T10:24:20.204Z",
34
37
  "buildOrder": 4,
35
38
  "executionStatus": "complete",
36
39
  "dependsOnFbsIds": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "createdAt": "2026-10-01T18:00:00Z",
3
- "updatedAt": "2026-10-01T18:00:00Z",
3
+ "updatedAt": "2026-10-06T12:00:00Z",
4
4
  "acIds": [
5
5
  "AC-18001-1",
6
6
  "AC-18001-2",
@@ -9,6 +9,7 @@
9
9
  "AC-18001-5",
10
10
  "AC-18001-6",
11
11
  "AC-18001-7",
12
+ "AC-18001-8",
12
13
  "AC-18101-1",
13
14
  "AC-18101-2",
14
15
  "AC-18101-3"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "createdAt": "2026-10-01T18:00:00Z",
3
- "updatedAt": "2026-10-01T18:00:00Z",
3
+ "updatedAt": "2026-10-06T12:00:00Z",
4
4
  "tacId": "TAC-4127-readiness-view",
5
5
  "prdId": "PRD-001",
6
6
  "tadId": "TAD-001",
@@ -15,7 +15,8 @@
15
15
  "src/view/components/findings-list.js: findingsList({ heading, count, items }) returns `<div class=\"rcf-findings-list\"><h4>{heading} <span class=\"rcf-findings-list__count\">{count}</span></h4><ul>...<li><a href={anchor}>{id}</a>: {why}</li>...</ul></div>` with ids capped at 20 and a `... N more suppressed` line beyond.",
16
16
  "src/view/components/diff.js: diff(before, after) returns a two-column rendering of two document bodies for the delta list; criterion-level before-and-after is deferred per ADR-4127.",
17
17
  "src/view/html-page.js: Readiness button and tabpanel are the first entries; the former Overview button and panel are relabelled PRD; the data-tab=overview and id=tab-overview identifiers are preserved so existing hash anchors still resolve; the inline-script TABS array gains 'readiness' as the first entry (default active tab) and keeps the existing four entries in order.",
18
- "src/view/index.js: renderModelToPage loads the freeze record, ledgers, test pointers and profile text once, invokes computeReadiness via the pure src/query/ seam, and threads the result into the model so renderPage and renderContent carry one computed object; the SSE tree-update broadcast rewalks and recomputes on every change under rcf/."
18
+ "src/view/index.js: renderModelToPage loads the freeze record, ledgers, test pointers and profile text once, invokes computeReadiness via the pure src/query/ seam, and threads the result into the model so renderPage and renderContent carry one computed object; the SSE tree-update broadcast rewalks and recomputes on every change under rcf/.",
19
+ "src/view/live-client.js: resyncTabsAfterSwap derives the list of tabs from the server-rendered nav.tabs (every [role=tab][data-tab] under it) rather than carrying a parallel array. The SSE swap re-emits every panel with the Phase 3.6 hidden-by-default layout; the resync reads the aria-selected button from the surviving header and makes exactly that panel visible for every tab the server rendered, Readiness included (AC-18001-8). The overview fallback stays for the genuinely-unknown-or-missing selection case."
19
20
  ],
20
21
  "internalStructure": "readiness.js is a sequence of eleven small block builders calling the three shared components; each block consumes exactly one top-level field of the readiness result so a wire-level JSON diff ties to a visible block. The register ordering is picked once at the top via pickRegister (same helper the CLI uses) and threaded down. Hash anchors are built with the same `#<docId>` convention the existing tabs use so blocker-id links open the right tab and expand the right <details> through the existing resolveHash routine.",
21
22
  "interfaces": [
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "createdAt": "2026-07-29T12:57:18.038Z",
3
- "updatedAt": "2026-07-29T13:35:14.688Z",
3
+ "updatedAt": "2026-10-06T10:12:36.226Z",
4
4
  "purpose": "Verify documents render as readable curated fields with working inter-document links. AC-202-1 is pending: its static-files-no-server clause predates the Phase 3.8 server-only view (see test-suites/PENDING.md).",
5
5
  "testLevel": "unit",
6
6
  "acIds": [
7
7
  "AC-202-2",
8
- "AC-202-3"
8
+ "AC-202-3",
9
+ "AC-202-4",
10
+ "AC-202-5",
11
+ "AC-202-6"
9
12
  ],
10
13
  "id": "TS-004",
11
14
  "usId": "US-202",
@@ -38,6 +41,83 @@
38
41
  "description": "the view serves from a local server process and never writes files to disk",
39
42
  "status": "pending",
40
43
  "testPointer": "test/view/cli.test.js::rcf audit view never writes any files to disk (regression against static mode)"
44
+ },
45
+ {
46
+ "id": "TC-004-tabs-cannot-shrink",
47
+ "acId": "AC-202-4",
48
+ "description": "style.css nav.tabs has flex-shrink: 0 and no min-width: 0, so the tablist never clips the Build tab under a long project name",
49
+ "status": "passing",
50
+ "testPointer": "test/view/shell-header-layout.test.js::AC-202-4: nav.tabs cannot shrink below its intrinsic width"
51
+ },
52
+ {
53
+ "id": "TC-004-tools-cannot-shrink",
54
+ "acId": "AC-202-4",
55
+ "description": "style.css .tools has flex-shrink: 0 so the Find button and theme control never shrink into the tablist",
56
+ "status": "passing",
57
+ "testPointer": "test/view/shell-header-layout.test.js::AC-202-4: .tools cannot shrink below its intrinsic width"
58
+ },
59
+ {
60
+ "id": "TC-004-product-yields-first",
61
+ "acId": "AC-202-4",
62
+ "description": "style.css .product has min-width: 0 so it yields to the tablist and tools before clipping anything",
63
+ "status": "passing",
64
+ "testPointer": "test/view/shell-header-layout.test.js::AC-202-4: .product yields first via min-width:0"
65
+ },
66
+ {
67
+ "id": "TC-004-name-min-width-zero",
68
+ "acId": "AC-202-4",
69
+ "description": "style.css .product .name has min-width: 0 so text-overflow: ellipsis triggers inside the flex .product",
70
+ "status": "passing",
71
+ "testPointer": "test/view/shell-header-layout.test.js::AC-202-4: .product .name has min-width:0 so the ellipsis triggers"
72
+ },
73
+ {
74
+ "id": "TC-004-name-title-attribute",
75
+ "acId": "AC-202-5",
76
+ "description": "renderPage emits a title=\"<full name>\" attribute on .product .name so hover reveals the whole name under ellipsis",
77
+ "status": "passing",
78
+ "testPointer": "test/view/shell-header-layout.test.js::AC-202-5: renderPage emits title=\"<full name>\" on .product .name"
79
+ },
80
+ {
81
+ "id": "TC-004-name-title-escaped",
82
+ "acId": "AC-202-5",
83
+ "description": "the title attribute shares the escapeHtml path with the inner text so a projectName of \"<script>\" renders as title=\"&lt;script&gt;\"",
84
+ "status": "passing",
85
+ "testPointer": "test/view/shell-header-layout.test.js::AC-202-5: the title attribute is HTML-escaped alongside the body"
86
+ },
87
+ {
88
+ "id": "TC-004-name-title-absent-on-collapse",
89
+ "acId": "AC-202-5",
90
+ "description": "no title attribute is emitted when the product block collapses (no projectName)",
91
+ "status": "passing",
92
+ "testPointer": "test/view/shell-header-layout.test.js::AC-202-5: no title attribute when the product block collapses"
93
+ },
94
+ {
95
+ "id": "TC-004-prd-productname-wins",
96
+ "acId": "AC-202-6",
97
+ "description": "resolveProductHeader prefers prd.productName over manifest.projectName when both are set",
98
+ "status": "passing",
99
+ "testPointer": "test/view/product-name-precedence.test.js::AC-202-6: prd.productName wins over manifest.projectName when both are set"
100
+ },
101
+ {
102
+ "id": "TC-004-manifest-fallback",
103
+ "acId": "AC-202-6",
104
+ "description": "resolveProductHeader falls back to manifest.projectName when the PRD carries no productName",
105
+ "status": "passing",
106
+ "testPointer": "test/view/product-name-precedence.test.js::AC-202-6: manifest.projectName is the first fallback when PRD has no productName"
107
+ },
108
+ {
109
+ "id": "TC-004-prd-producttitle-fallback",
110
+ "acId": "AC-202-6",
111
+ "description": "resolveProductHeader falls back to prd.productTitle when neither productName nor manifest is set",
112
+ "status": "passing",
113
+ "testPointer": "test/view/product-name-precedence.test.js::AC-202-6: prd.productTitle is the last fallback (brief-stage trees without productName or manifest)"
114
+ },
115
+ {
116
+ "id": "TC-004-render-reflects-precedence",
117
+ "acId": "AC-202-6",
118
+ "description": "the renderPage DOM reflects the flipped precedence; the manifest value does not reach the header slice when prd.productName is set",
119
+ "status": "passing",
120
+ "testPointer": "test/view/product-name-precedence.test.js::AC-202-6: renderPage reflects the flipped precedence in the DOM"
41
121
  }
42
122
  ],
43
123
  "status": "draft"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "createdAt": "2026-10-01T18:00:00Z",
3
- "updatedAt": "2026-10-01T18:00:00Z",
4
- "purpose": "Cover US-18001 (Readiness as tab 1 and the PRD rename) and US-18101 (shared pill, findings-list and diff components). Shares the readiness fixture builders with TS-222 so the tab and the CLI assertions ride the same shapes.",
3
+ "updatedAt": "2026-10-06T12:00:00Z",
4
+ "purpose": "Cover US-18001 (Readiness as tab 1 and the PRD rename, plus the live-client resync SSOT guard added for issue 297) and US-18101 (shared pill, findings-list and diff components). Shares the readiness fixture builders with TS-222 so the tab and the CLI assertions ride the same shapes.",
5
5
  "testLevel": "unit",
6
6
  "acIds": [
7
7
  "AC-18001-1",
@@ -11,6 +11,7 @@
11
11
  "AC-18001-5",
12
12
  "AC-18001-6",
13
13
  "AC-18001-7",
14
+ "AC-18001-8",
14
15
  "AC-18101-1",
15
16
  "AC-18101-2",
16
17
  "AC-18101-3"
@@ -88,6 +89,27 @@
88
89
  "description": "No existing Product Map or Requirements test expectation changes; only the Readiness-specific additions land.",
89
90
  "status": "pending",
90
91
  "testPointer": "test/view/readiness-tab.test.js::readiness tab: AC-18101-3 existing Product Map and Requirements tests retain their expectations"
92
+ },
93
+ {
94
+ "id": "TC-223-swap-preserves-readiness",
95
+ "acId": "AC-18001-8",
96
+ "description": "resyncTabsAfterSwap keeps tab-readiness as the sole visible panel when Readiness is aria-selected and the swap left tab-overview visible (the Phase 3.6 server default). Pins issue 297: before the fix, the hard-coded TABS list in live-client.js omitted 'readiness', indexOf returned -1, selected fell back to overview, and both tab-readiness and tab-overview went visible.",
97
+ "status": "passing",
98
+ "testPointer": "test/view/live-client.test.js::AC-18001-8 resyncTabsAfterSwap keeps Readiness visible on the embed-no-hash default (issue 297)"
99
+ },
100
+ {
101
+ "id": "TC-223-swap-ssot-every-tab",
102
+ "acId": "AC-18001-8",
103
+ "description": "resyncTabsAfterSwap honours every nav.tabs [role=tab][data-tab] the server rendered as the SSOT for the tab list. Iterating the six main tabs with each as aria-selected and the swap-default visible panel set to tab-overview, exactly the aria-selected tab's panel is visible after resync. Guards against future drift: a seventh tab added to the renderer survives a swap without an edit on live-client.",
104
+ "status": "passing",
105
+ "testPointer": "test/view/live-client.test.js::AC-18001-8 resyncTabsAfterSwap preserves every tab the DOM renders (SSOT invariant)"
106
+ },
107
+ {
108
+ "id": "TC-223-swap-product-map-regression",
109
+ "acId": "AC-18001-8",
110
+ "description": "resyncTabsAfterSwap preserves product-map as the sole visible panel through a swap. Guards the one tab that the pre-issue-297 TABS list did include, so the SSOT rewrite does not regress the behaviour it previously had right.",
111
+ "status": "passing",
112
+ "testPointer": "test/view/live-client.test.js::AC-18001-8 resyncTabsAfterSwap preserves Product Map through a swap (regression guard)"
91
113
  }
92
114
  ],
93
115
  "status": "draft"
@@ -44,6 +44,11 @@
44
44
  "id": "AC-18001-7",
45
45
  "description": "[happy] Given the tab and the CLI --json output for the same tree hash, when compared, then every id rendered on the tab appears in the JSON for that hash.",
46
46
  "testable": true
47
+ },
48
+ {
49
+ "id": "AC-18001-8",
50
+ "description": "[must-not] Given any main tab (readiness, overview, requirements, architecture, build, product-map) is the active tab, when an SSE tree-update swaps #rcf-live-content innerHTML and resyncTabsAfterSwap runs, then exactly one tabpanel is visible and it is the active tab's panel. The live-client derives the tab list from the server-rendered nav.tabs rather than carrying a parallel array that can drift from page-init: every nav.tabs [role=tab][data-tab] present at swap time is honoured. Readiness is covered as issue 297's regression.",
51
+ "testable": true
47
52
  }
48
53
  ],
49
54
  "tacIds": [
@@ -51,5 +56,5 @@
51
56
  ],
52
57
  "tags": [],
53
58
  "createdAt": "2026-10-01T18:00:00Z",
54
- "updatedAt": "2026-10-01T18:00:00Z"
59
+ "updatedAt": "2026-10-06T12:00:00Z"
55
60
  }
@@ -33,8 +33,32 @@
33
33
  "when": "a document links to another",
34
34
  "then": "the link navigates to that document's section",
35
35
  "testable": true
36
+ },
37
+ {
38
+ "id": "AC-202-4",
39
+ "description": "[happy] The audit view header keeps every main tab visible and the .tools group right of them at any viewport the chrome supports: the tablist and .tools cannot shrink below their intrinsic width (flex-shrink: 0) and .product carries the shrink budget via min-width: 0 (both on the flex item and on .product .name) so the project name ellipsizes first.",
40
+ "given": "the viewer rendered in the standalone (non-embed) shell",
41
+ "when": "any viewport >= 721px the chrome supports (above the responsive @media that re-rows the tabs under 720px)",
42
+ "then": "the six main tabs are fully visible and the .tools group sits right of them; .product .name ellipsizes when the project name is longer than its slot and nothing in .tools overlaps a clipped tab",
43
+ "testable": true
44
+ },
45
+ {
46
+ "id": "AC-202-5",
47
+ "description": "[happy] The .product .name span carries a title=\"<full name>\" attribute so hovering reveals the whole name when the ellipsis has clipped it; the title is HTML-escaped on the same path as the inner text, and when the product block collapses (no project name on the tree) no title attribute is emitted.",
48
+ "given": "a project tree with a projectName",
49
+ "when": "the viewer renders the header",
50
+ "then": "the .name span has title=\"<the full projectName>\" with the same HTML-escaping as the inner text",
51
+ "testable": true
52
+ },
53
+ {
54
+ "id": "AC-202-6",
55
+ "description": "[happy] The header display name prefers prd.productName, then manifest.projectName, then prd.productTitle. A rename via `rcf define update PRD-001 --set productName=...` therefore reaches the audit chrome without touching manifest.projectName (which the chain keeps for ids and paths).",
56
+ "given": "a tree where both manifest.projectName and prd.productName are set",
57
+ "when": "resolveProductHeader runs",
58
+ "then": "the header projectName is prd.productName; when the PRD carries no productName the fallback is manifest.projectName, then prd.productTitle",
59
+ "testable": true
36
60
  }
37
61
  ],
38
62
  "createdAt": "2026-06-25T00:00:00Z",
39
- "updatedAt": "2026-07-29T13:34:38.914Z"
63
+ "updatedAt": "2026-10-06T10:12:07.288Z"
40
64
  }
@@ -40,8 +40,25 @@
40
40
  # `npm install rcf-lite`.
41
41
 
42
42
  feedVersion: 1
43
- latest: "0.31.0"
43
+ latest: "0.32.1"
44
44
  releases:
45
+ - version: "0.32.1"
46
+ date: "2026-10-06"
47
+ breaking: false
48
+ headlines:
49
+ - "rcf-lite 0.32.1 fixes two readiness gates that false-paired under schema-canonical content on top of 0.32.0. The viewer's Readiness tab D3 shapes:pathsResolve now agrees with the CLI by pre-resolving TAC interface path tokens through the same resolveInterfacePaths seam (issue 307), and the D5 crosscut:operationalConcerns trigger reads the structured ac.scope field in addition to the legacy '[deployed]' marker so trees that carry scope as a schema field no longer silently pass 0/0 (issue 306)."
50
+ minAgentAction: null
51
+ notesUrl: "https://stravica.ai/docs/rcf/changelog/"
52
+
53
+ - version: "0.32.0"
54
+ date: "2026-10-06"
55
+ breaking: false
56
+ headlines:
57
+ - "rcf-lite 0.32.0 fixes two readiness and freeze gates that false-tripped under schema-canonical content: D6 consistency:orphanInterfaces now accepts the object-shape REQ.deliveredBy (issue 301), and D3 shapes:pathsResolve inside rcf define freeze pre-resolves TAC interface path tokens the way readiness does (issue 302). The deploy-cloudflare-workers blueprint reworded AC-12113-2 so D6 consistency:unsatisfiable stops reading 'absent' as a field name (PR 300)."
58
+ - "Viewer fixes: SSE tree-update resync preserves the active tab for every main tab including Readiness (issue 297), and the audit view header keeps the six tabs visible under long project names and prefers prd.productName (issue 295)."
59
+ minAgentAction: null
60
+ notesUrl: "https://stravica.ai/docs/rcf/changelog/"
61
+
45
62
  - version: "0.31.0"
46
63
  date: "2026-10-05"
47
64
  breaking: true
package/src/cli/freeze.js CHANGED
@@ -35,6 +35,7 @@ import { checkCodeNodeResolution } from '#core/store';
35
35
  import { findProjectRoot } from '../view/index.js';
36
36
  import { loadFreezeRecord, saveFreezeRecord } from '../define/freeze-record.js';
37
37
  import { loadAllLedgers } from '../define/ledgers.js';
38
+ import { resolveInterfacePaths } from '../define/interface-paths.js';
38
39
  import {
39
40
  STAGE_ALIASES,
40
41
  STAGE_GATES,
@@ -368,6 +369,12 @@ async function runFreeze({
368
369
  // freezeableAfterAck check below is the real gate (freeze still
369
370
  // exits 4 on an unfrozen tree); the wrap's refusal is informational
370
371
  // here as it is on readiness.
372
+ // Issue 302 (2026-10-06): freeze now pre-resolves the TAC
373
+ // interface `path:` tokens the way readiness does, so the D3
374
+ // `shapes:pathsResolve` check inside freeze agrees with the
375
+ // readiness verdict instead of defaulting to an empty set and
376
+ // false-failing resolvable paths.
377
+ const resolvedPaths = await resolveInterfacePaths(projectRoot, tree);
371
378
  /** @type {import('../query/readiness.js').ReadinessResult} */
372
379
  const readiness = computeReadiness(tree, {
373
380
  freeze: freezeForCompute,
@@ -375,6 +382,7 @@ async function runFreeze({
375
382
  profileText,
376
383
  testPointers,
377
384
  validateErrors,
385
+ resolvedPaths,
378
386
  });
379
387
  const gated = await runWithAdmissibilityGate({
380
388
  tree,
@@ -16,7 +16,7 @@
16
16
  // every other query verb has.
17
17
 
18
18
  import { parseArgs } from 'node:util';
19
- import { readFile, stat } from 'node:fs/promises';
19
+ import { readFile } from 'node:fs/promises';
20
20
  import { join } from 'node:path';
21
21
 
22
22
  import { resolveTestPointers, walkTree } from '#core/store';
@@ -30,9 +30,9 @@ import {
30
30
  STAGE_ORDER,
31
31
  STAGE_SHORT_NAMES,
32
32
  ackable,
33
- extractInterfacePathTokens,
34
33
  stagePolicy,
35
34
  } from '../query/gates.js';
35
+ import { resolveInterfacePaths } from '../define/interface-paths.js';
36
36
  import {
37
37
  computeReadiness,
38
38
  countLitmusReadersAtHash,
@@ -129,42 +129,6 @@ const STATE_LABEL = {
129
129
  notApplicable: 'notApplicable',
130
130
  };
131
131
 
132
- /**
133
- * ADR-4131 (0.30.0 PR 5): walk the tree's TAC interfaces, extract the
134
- * `path:` tokens the engineer named, resolve each one against
135
- * `projectRoot` on disk, and return the set of tokens that resolve.
136
- * `shapes:pathsResolve` reads the set from the stage context and
137
- * decides the D3 bite without I/O. The compute is pure; this helper is
138
- * the one place I/O happens for the check.
139
- *
140
- * @param {string} projectRoot
141
- * @param {import('#core/store/walker.js').TreeModel} tree
142
- * @returns {Promise<Set<string>>}
143
- */
144
- async function resolveInterfacePaths(projectRoot, tree) {
145
- const resolved = new Set();
146
- /** @type {Set<string>} */
147
- const candidates = new Set();
148
- for (const tac of tree.tacs ?? []) {
149
- for (const iface of tac.interfaces ?? []) {
150
- const desc = typeof iface?.description === 'string' ? iface.description : '';
151
- for (const token of extractInterfacePathTokens(desc)) {
152
- candidates.add(token);
153
- }
154
- }
155
- }
156
- await Promise.all([...candidates].map(async (token) => {
157
- try {
158
- await stat(join(projectRoot, token));
159
- resolved.add(token);
160
- } catch {
161
- // Missing path stays out of the set; the gate decides whether
162
- // `authoredAt: D3` lets it through.
163
- }
164
- }));
165
- return resolved;
166
- }
167
-
168
132
  /**
169
133
  * Read `rcf/.identity/profile.md` when it exists; return null on
170
134
  * ENOENT. Any other read error surfaces as null so the profile
@@ -0,0 +1,47 @@
1
+ // Shared define-time helper: resolve the `path:` tokens an engineer
2
+ // named on TAC interfaces against the project root on disk. The pure
3
+ // D3 `shapes:pathsResolve` check reads the resulting set from the
4
+ // stage context; this is the one place I/O happens for the check.
5
+ //
6
+ // Lifted out of `src/cli/readiness.js` in issue 302 (2026-10-06) so
7
+ // the freeze CLI can pre-resolve the same tokens and D3 agrees with
8
+ // readiness instead of false-failing from an empty fallback set.
9
+ // ADR-4131 (0.30.0 PR 5) still names readiness as the origin.
10
+
11
+ import { stat } from 'node:fs/promises';
12
+ import { join } from 'node:path';
13
+
14
+ import { extractInterfacePathTokens } from '../query/gates.js';
15
+
16
+ /**
17
+ * Walk the tree's TAC interfaces, extract the `path:` tokens the
18
+ * engineer named, resolve each one against `projectRoot` on disk, and
19
+ * return the set of tokens that resolve.
20
+ *
21
+ * @param {string} projectRoot
22
+ * @param {import('#core/store/walker.js').TreeModel} tree
23
+ * @returns {Promise<Set<string>>}
24
+ */
25
+ export async function resolveInterfacePaths(projectRoot, tree) {
26
+ const resolved = new Set();
27
+ /** @type {Set<string>} */
28
+ const candidates = new Set();
29
+ for (const tac of tree.tacs ?? []) {
30
+ for (const iface of tac.interfaces ?? []) {
31
+ const desc = typeof iface?.description === 'string' ? iface.description : '';
32
+ for (const token of extractInterfacePathTokens(desc)) {
33
+ candidates.add(token);
34
+ }
35
+ }
36
+ }
37
+ await Promise.all([...candidates].map(async (token) => {
38
+ try {
39
+ await stat(join(projectRoot, token));
40
+ resolved.add(token);
41
+ } catch {
42
+ // Missing path stays out of the set; the gate decides whether
43
+ // `authoredAt: D3` lets it through.
44
+ }
45
+ }));
46
+ return resolved;
47
+ }
@@ -1582,7 +1582,15 @@ function computeSecurityArchitectureCheck(ctx) {
1582
1582
  function computeOperationalConcernsCheck(ctx) {
1583
1583
  const tree = ctx.tree;
1584
1584
  const tad = /** @type {any} */ (tree.tad ?? {});
1585
- const hasDeployedAc = (tree.userStories ?? []).some((us) => (us.acceptanceCriteria ?? []).some((ac) => typeof ac?.description === 'string' && ac.description.includes('[deployed]')));
1585
+ // Issue 306 (2026-10-06): schemas carry AC scope as the structured
1586
+ // field `scope: "deployed"`; the legacy `[deployed]` description
1587
+ // marker (R13 2026-10-05) is kept so older trees still trigger. A
1588
+ // deployed-scope AC on either form must find a non-empty
1589
+ // TAD.operationalConcerns.
1590
+ const hasDeployedAc = (tree.userStories ?? []).some((us) => (us.acceptanceCriteria ?? []).some((ac) => {
1591
+ if (ac?.scope === 'deployed') return true;
1592
+ return typeof ac?.description === 'string' && ac.description.includes('[deployed]');
1593
+ }));
1586
1594
  const opFail = [];
1587
1595
  if (hasDeployedAc) {
1588
1596
  const oc = tad.operationalConcerns;
@@ -2064,10 +2072,24 @@ function extractThenFieldTokens(thenClause) {
2064
2072
  /**
2065
2073
  * Collect every interface the tree "reaches" from an AC `ownerRef` or
2066
2074
  * a REQ `deliveredBy` (REQ-174 companion field). The result is a Set
2067
- * of `<tacId>:<name>` keys. A `deliveredBy` on a REQ can take two
2068
- * shapes: an array of `TAC-nnnn:<name>` pointer strings, or an array
2069
- * of plain TAC ids ("delivered by this TAC"); the second shape reaches
2070
- * every interface on the named TAC.
2075
+ * of `<tacId>:<name>` keys.
2076
+ *
2077
+ * REQ `deliveredBy` takes two canonical shapes, both accepted here:
2078
+ * 1. The schema-canonical object form `{tacId, adrId?, field?}`
2079
+ * (`@stravica-ai/rcf-schemas` 0.6.3 `$defs.deliveredBy`). When
2080
+ * `tacId` is present:
2081
+ * - `field: interfaces[<name>]` reaches `{tacId}:{name}` (one
2082
+ * interface), via `parseOwnerRefInterfaceField`.
2083
+ * - any other `field`, or no `field`, reaches every interface
2084
+ * on the named TAC (the "delivered by this TAC" meaning).
2085
+ * An `adrId`-only deliveredBy does not reach any TAC interface.
2086
+ * 2. A legacy array of pointer strings: `TAC-nnnn:<name>` reaches
2087
+ * one interface, a bare `TAC-nnnn` reaches every interface on
2088
+ * that TAC.
2089
+ *
2090
+ * Issue 301 (2026-10-06): the array-only guard dropped every
2091
+ * schema-canonical object deliveredBy and false-positived every
2092
+ * blueprint TAC interface.
2071
2093
  *
2072
2094
  * @param {import('#core/store/walker.js').TreeModel} tree
2073
2095
  * @returns {Set<string>}
@@ -2085,19 +2107,40 @@ function collectReachedInterfaces(tree) {
2085
2107
  out.add(`${ref.tacId}:${name}`);
2086
2108
  }
2087
2109
  }
2088
- // REQ deliveredBy (optional field; shapes above).
2110
+ // REQ deliveredBy. Both shapes documented above.
2089
2111
  for (const req of tree.requirements ?? []) {
2090
2112
  const deliveredBy = /** @type {any} */ (req)?.deliveredBy;
2091
- if (!Array.isArray(deliveredBy)) continue;
2092
- for (const entry of deliveredBy) {
2093
- if (typeof entry !== 'string' || entry.length === 0) continue;
2094
- const m = entry.match(/^(TAC-[A-Za-z0-9-]+):(.+)$/);
2095
- if (m) {
2096
- out.add(`${m[1]}:${m[2]}`);
2113
+ if (deliveredBy == null) continue;
2114
+ if (Array.isArray(deliveredBy)) {
2115
+ for (const entry of deliveredBy) {
2116
+ if (typeof entry !== 'string' || entry.length === 0) continue;
2117
+ const m = entry.match(/^(TAC-[A-Za-z0-9-]+):(.+)$/);
2118
+ if (m) {
2119
+ out.add(`${m[1]}:${m[2]}`);
2120
+ continue;
2121
+ }
2122
+ // Plain TAC id: every interface on that TAC is reached.
2123
+ const tac = (tree.tacs ?? []).find((t) => t?.tacId === entry);
2124
+ if (!tac) continue;
2125
+ for (const iface of tac.interfaces ?? []) {
2126
+ if (typeof iface?.name === 'string') out.add(`${tac.tacId}:${iface.name}`);
2127
+ }
2128
+ }
2129
+ continue;
2130
+ }
2131
+ if (typeof deliveredBy === 'object') {
2132
+ const tacId = /** @type {any} */ (deliveredBy).tacId;
2133
+ if (typeof tacId !== 'string' || tacId.length === 0) continue;
2134
+ const field = /** @type {any} */ (deliveredBy).field;
2135
+ const name = parseOwnerRefInterfaceField(field);
2136
+ if (name) {
2137
+ out.add(`${tacId}:${name}`);
2097
2138
  continue;
2098
2139
  }
2099
- // Plain TAC id: every interface on that TAC is reached.
2100
- const tac = (tree.tacs ?? []).find((t) => t?.tacId === entry);
2140
+ // No field, or a field that does not name an interface: reach
2141
+ // every interface on the named TAC (the "delivered by this TAC"
2142
+ // meaning carried over from the array-shape bare TAC pointer).
2143
+ const tac = (tree.tacs ?? []).find((t) => t?.tacId === tacId);
2101
2144
  if (!tac) continue;
2102
2145
  for (const iface of tac.interfaces ?? []) {
2103
2146
  if (typeof iface?.name === 'string') out.add(`${tac.tacId}:${iface.name}`);
@@ -843,21 +843,33 @@ function buildSpecFilterText(fbs) {
843
843
  }
844
844
 
845
845
  /**
846
- * Viewer UI refresh PR 2 (shell polish, Baz 2026-10-02; decision 17):
847
- * the one-bar header carries the project name only. The project name
848
- * is sourced from `manifest.projectName`, then `prd.productName`,
849
- * then `prd.productTitle` (the PRD title the brief names). Decision 17
850
- * is explicit: project name prominent after the icon mark, tool name
851
- * out of the bar, no technology chip. When the tree exposes no
852
- * project name, the whole product block collapses and the brand icon
853
- * stands on its own (silent empty state; CSS `.product:empty { display:none; }`).
846
+ * Viewer UI refresh PR 2 (shell polish, Baz 2026-10-02; decision 17)
847
+ * with the issue-295 flip (w-2026-10-06-dave-001): the one-bar header
848
+ * carries the project name only, sourced from `prd.productName` first
849
+ * (the human-readable name an owner edits via `rcf define update
850
+ * PRD-001 --set productName=...`), then `manifest.projectName` (the
851
+ * init-time value kept for ids / paths), then `prd.productTitle` (the
852
+ * PRD title the brief names). Decision 17 remains: project name
853
+ * prominent after the icon mark, tool name out of the bar, no
854
+ * technology chip. When the tree exposes no project name, the whole
855
+ * product block collapses and the brand icon stands on its own
856
+ * (silent empty state; CSS `.product:empty { display:none; }`).
857
+ *
858
+ * The precedence flip is a lite-native cut (US-202, Part 2 cut b):
859
+ * `rcf define update MANIFEST` is not addressable in the current CLI,
860
+ * so before this change manifest.projectName was a one-shot init value
861
+ * and the only way to rename the audit surface was a hand edit of
862
+ * manifest.json. prd.productName is already updatable via `rcf define
863
+ * update PRD-001 --set productName=...`; preferring it in the header
864
+ * means the display name is lite-native without touching the manifest
865
+ * (ids and paths are untouched).
854
866
  *
855
867
  * @param {import('./tree-model.js').BuiltTreeModel} model
856
868
  * @returns {{ projectName: string | null }}
857
869
  */
858
870
  export function resolveProductHeader(model) {
859
- const projectName = model.manifest?.projectName
860
- ?? model.prd?.productName
871
+ const projectName = model.prd?.productName
872
+ ?? model.manifest?.projectName
861
873
  ?? model.prd?.productTitle
862
874
  ?? null;
863
875
  return { projectName };
@@ -870,7 +882,13 @@ function renderProductBlock(header) {
870
882
  // empty state (brief) survives screen-reader output too.
871
883
  return '<div class="product"></div>';
872
884
  }
873
- return `<div class="product"><span class="name">${escapeHtml(header.projectName)}</span>
885
+ // w-2026-10-06-dave-001 (issue 295): .product .name clips its text
886
+ // with text-overflow:ellipsis so a long project name yields rather
887
+ // than pushing the tablist off the bar; the title attribute carries
888
+ // the full name so hover reveals it. Both the title and the body go
889
+ // through escapeHtml so the attribute is safe against injection.
890
+ const safe = escapeHtml(header.projectName);
891
+ return `<div class="product"><span class="name" title="${safe}">${safe}</span>
874
892
  <span class="sub">review surface</span></div>`;
875
893
  }
876
894
 
package/src/view/index.js CHANGED
@@ -12,6 +12,7 @@ import { resolveTestPointers, walkTree } from '#core/store';
12
12
  import { computeReadiness } from '../query/readiness.js';
13
13
  import { computeQuestions } from '../query/questions.js';
14
14
  import { loadFreezeRecord } from '../define/freeze-record.js';
15
+ import { resolveInterfacePaths } from '../define/interface-paths.js';
15
16
  import { loadAllLedgers } from '../define/ledgers.js';
16
17
  import { renderContent, renderPage } from './html-page.js';
17
18
  import { renderProductMapGrouping } from './product-map.js';
@@ -77,11 +78,18 @@ export async function renderModelToPage({ projectRoot }) {
77
78
  // Load the readiness inputs once per rewalk; the viewer is pure
78
79
  // (never recomputes readiness). Failures load as `null` so the tab
79
80
  // still renders when a tree is partial.
80
- const [freezeRecord, ledgers, testPointers, profileText] = await Promise.all([
81
+ const [freezeRecord, ledgers, testPointers, profileText, resolvedPaths] = await Promise.all([
81
82
  loadFreezeRecord({ projectRoot }).catch(() => null),
82
83
  loadAllLedgers({ projectRoot }).catch(() => ({})),
83
84
  resolveTestPointers({ projectRoot, tree }).catch(() => undefined),
84
85
  readProfileText(projectRoot),
86
+ // Issue 307 (2026-10-06): run the same TAC-interface path
87
+ // resolver the CLI and freeze run so the viewer's D3
88
+ // `shapes:pathsResolve` check reads the resolved set from the
89
+ // same seam. Before this, the viewer called computeReadiness
90
+ // without `resolvedPaths`, D3 false-failed on paths that resolve
91
+ // on disk, and the Readiness tab disagreed with the CLI.
92
+ resolveInterfacePaths(projectRoot, tree).catch(() => undefined),
85
93
  ]);
86
94
 
87
95
  let readiness = null;
@@ -93,6 +101,7 @@ export async function renderModelToPage({ projectRoot }) {
93
101
  profileText,
94
102
  testPointers,
95
103
  validateErrors: errors,
104
+ resolvedPaths,
96
105
  });
97
106
  } catch (err) {
98
107
  // A malformed tree should not block the viewer; the Readiness tab
@@ -149,7 +158,12 @@ export async function renderModelToPage({ projectRoot }) {
149
158
  // list is extended before the version pin bump). One row per doc id
150
159
  // plus one row per AC under each US.
151
160
  const indexJson = serialiseViewIndex(model);
152
- return { fullPageHtml, contentHtml, errors, tree, pmPartials, indexJson };
161
+ // Issue 307 (2026-10-06): expose the computed readiness alongside
162
+ // the rendered HTML so tests can assert the viewer's D3
163
+ // shapes:pathsResolve agrees with the CLI without parsing HTML. The
164
+ // viewer HTML is still the operator-facing surface; this field is
165
+ // additive and consumed by test code only.
166
+ return { fullPageHtml, contentHtml, errors, tree, pmPartials, indexJson, readiness };
153
167
  }
154
168
 
155
169
  /**
@@ -507,19 +507,27 @@
507
507
  // the panels back in line with the aria-selected button and re-run
508
508
  // Mermaid in the visible one.
509
509
  function resyncTabsAfterSwap(doc, win) {
510
- // Phase 3.8 tabs; Phase 3.9 added product-map. Product Map MUST be
511
- // in this list so an SSE swap while it is the active tab keeps the
512
- // panel visible; without it, resolveActiveTab falls back to
513
- // overview and the freshly-swapped pm listeners have already been
514
- // rewired by the rcfPage.init() call below.
515
- var TABS = ['overview', 'requirements', 'architecture', 'build', 'product-map'];
516
- // Scope to the top-level nav.tabs to avoid matching pm-group-btn
510
+ // Issue 297 (w-2026-10-06-dave-003, AC-18001-8): the authoritative
511
+ // tab list is the server-rendered nav.tabs itself, not a parallel
512
+ // array that can drift. Earlier the list was hand-copied here and
513
+ // omitted 'readiness' (added as tab 1 in US-18001); a Readiness
514
+ // SSE swap fell back to Overview and both panels went visible.
515
+ // Deriving from the DOM means a future tab cannot be forgotten
516
+ // the same way. Scope to nav.tabs to avoid matching pm-group-btn
517
517
  // nodes (they also carry role="tab").
518
+ var buttons = doc.querySelectorAll('nav.tabs [role="tab"][data-tab]');
519
+ var names = [];
520
+ for (var b = 0; b < buttons.length; b += 1) {
521
+ var n = buttons[b].getAttribute ? buttons[b].getAttribute('data-tab') : null;
522
+ if (n) names.push(n);
523
+ }
518
524
  var selectedBtn = doc.querySelector('nav.tabs [role="tab"][aria-selected="true"]');
519
525
  var selected = selectedBtn && selectedBtn.getAttribute ? selectedBtn.getAttribute('data-tab') : null;
520
- if (!selected || TABS.indexOf(selected) === -1) selected = 'overview';
521
- for (var i = 0; i < TABS.length; i += 1) {
522
- var name = TABS[i];
526
+ if (!selected || names.indexOf(selected) === -1) {
527
+ selected = names.indexOf('overview') !== -1 ? 'overview' : (names[0] || 'overview');
528
+ }
529
+ for (var i = 0; i < names.length; i += 1) {
530
+ var name = names[i];
523
531
  var panel = doc.getElementById('tab-' + name);
524
532
  if (!panel) continue;
525
533
  if (name === selected) panel.removeAttribute('hidden');
@@ -236,6 +236,10 @@ header.app-header .bar {
236
236
  color: var(--sv-ink);
237
237
  overflow: hidden;
238
238
  text-overflow: ellipsis;
239
+ /* w-2026-10-06-dave-001 (issue 295): .product is a flex container; the
240
+ ellipsis only triggers when its child is itself free to shrink below
241
+ intrinsic. min-width:0 unlocks that. */
242
+ min-width: 0;
239
243
  }
240
244
 
241
245
  .product .sub {
@@ -248,8 +252,12 @@ nav.tabs {
248
252
  display: flex;
249
253
  gap: 0.25rem;
250
254
  margin-left: 1rem;
251
- flex: 1 1 auto;
252
- min-width: 0;
255
+ /* w-2026-10-06-dave-001 (issue 295): the tablist grows to fill the
256
+ bar but never shrinks below its intrinsic width (flex-shrink:0),
257
+ so every main tab stays visible and the Find button in .tools
258
+ never sits on top of a clipped tab. .product carries the shrink
259
+ budget via its own min-width:0 and the ellipsis on .product .name. */
260
+ flex: 1 0 auto;
253
261
  overflow-x: auto;
254
262
  -webkit-overflow-scrolling: touch;
255
263
  }