rcf-lite 0.30.0 → 0.31.0
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.
- package/CHANGELOG.md +46 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/define-draft-shapes.md +15 -10
- package/package.json +1 -1
- package/rcf/adrs/adr-4131-gates-bite-ack-recorded-override.json +3 -3
- package/rcf/adrs/adr-4137-standalone-theme-persistence.json +30 -0
- package/rcf/adrs/adr-4138-tree-wide-checks-before-scope-shortcircuit.json +35 -0
- package/rcf/code-nodes/cn-771.json +14 -0
- package/rcf/code-nodes/cn-772.json +14 -0
- package/rcf/code-nodes/cn-773.json +16 -0
- package/rcf/code-nodes/cn-774.json +20 -0
- package/rcf/code-nodes/cn-775.json +15 -0
- package/rcf/code-nodes/cn-776.json +17 -0
- package/rcf/code-nodes/cn-777.json +21 -0
- package/rcf/fbs/fbs-202.json +18 -2
- package/rcf/fbs/fbs-203.json +12 -4
- package/rcf/requirements/req-002.json +2 -2
- package/rcf/requirements/req-173.json +2 -2
- package/rcf/requirements/req-174.json +2 -2
- package/rcf/tacs/tac-4122-define-gates.json +7 -2
- package/rcf/test-suites/ts-007.json +1 -1
- package/rcf/test-suites/ts-215.json +64 -1
- package/rcf/test-suites/ts-216.json +18 -2
- package/rcf/test-suites/ts-219.json +10 -2
- package/rcf/test-suites/ts-220.json +18 -2
- package/rcf/test-suites/ts-225.json +10 -2
- package/rcf/test-suites/ts-229.json +49 -3
- package/rcf/test-suites/ts-230.json +26 -2
- package/rcf/test-suites/ts-231.json +16 -1
- package/rcf/test-suites/ts-236.json +44 -0
- package/rcf/user-stories/us-17302.json +36 -1
- package/rcf/user-stories/us-17401.json +11 -1
- package/rcf/user-stories/us-17402.json +6 -1
- package/rcf/user-stories/us-17403.json +11 -1
- package/rcf/user-stories/us-17404.json +16 -1
- package/rcf/user-stories/us-17405.json +7 -2
- package/rcf/user-stories/us-205.json +6 -1
- package/rcf/user-stories/us-206.json +25 -4
- package/rcf/user-stories/us-207.json +36 -0
- package/releases/releases.yaml +11 -1
- package/src/cli/create.js +22 -2
- package/src/cli/ledger.js +96 -4
- package/src/cli/readiness.js +15 -1
- package/src/core/errors/index.js +7 -1
- package/src/core/store/writer.js +181 -13
- package/src/define/ledgers.js +86 -7
- package/src/mcp/tools.js +1 -1
- package/src/query/gates.js +383 -150
- package/src/view/html-page.js +14 -2
- package/src/view/page-init.js +149 -8
- package/src/view/style.css +97 -18
- package/src/view/view-index.js +22 -0
|
@@ -49,6 +49,11 @@
|
|
|
49
49
|
"id": "AC-205-8",
|
|
50
50
|
"description": "[must-not] Given the LookupModal, when the user types a query, picks a result or closes the modal, then no `postMessage` is sent to the parent window, no outbound network request beyond the `./index.json` fetch is made, and no search term is written to `localStorage` / `sessionStorage` / cookies.",
|
|
51
51
|
"testable": true
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "AC-205-9",
|
|
55
|
+
"description": "[happy] Given the SearchButton (`.rcf-search-btn`) in the shell's `.tools` group, when it is rendered in either theme and in each of its states (default, `:hover`, `:focus-visible`, `:active`), then the computed foreground / background contrast ratio for the button text, the icon stroke against the background and the focus-ring colour against the adjacent surface meets WCAG 2.1 AA (>= 4.5:1 for the text, >= 3:1 for the icon and the focus-ring boundary) in both the light (`:root`) and dark (`:root[data-theme=\"dark\"]`) token sets.",
|
|
56
|
+
"testable": true
|
|
52
57
|
}
|
|
53
58
|
],
|
|
54
59
|
"tacIds": [
|
|
@@ -60,5 +65,5 @@
|
|
|
60
65
|
"navigation"
|
|
61
66
|
],
|
|
62
67
|
"createdAt": "2026-10-02T19:00:00Z",
|
|
63
|
-
"updatedAt": "2026-10-
|
|
68
|
+
"updatedAt": "2026-10-04T14:00:00Z"
|
|
64
69
|
}
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"version": "0.1.0",
|
|
6
6
|
"status": "draft",
|
|
7
7
|
"title": "wespa's embedding contract is proved by a CI host fixture",
|
|
8
|
-
"description": "A same-origin test-host fixture served by the viewer's dev server (route `/test-host.html` behind a `--test-host` flag or an env guard; off by default so wespa's proxy allow-list is unaffected) mimics the wespa shell: a 56px header, a resizable sidebar (64px collapsed, 200-400px expanded, user-resizable live via a drag handle), a content pane carrying an `<iframe src=\"./?embed=1&theme=light\">`. The host posts `{ type: \"rcf-view-theme\", theme: \"light\"|\"dark\" }` on a toggle control; it stores no theme itself so a passing run proves the iframe reads the message. The viewer sends no outbound postMessage. Three lints run in the test suite: (1) framing lint (`GET /` returns no `X-Frame-Options`, no CSP `frame-ancestors` that forbids same-origin, and the fixture loads the iframe without a browser error); (2) target lint (no `target=\"_top\"` string in the server-rendered HTML); (3) localStorage namespace lint (every `localStorage.{setItem,getItem,removeItem}` and shared `storage.*` call site in `src/view/**` uses a key starting with `rcf-view:v1:`). See viewer UI refresh design doc section 9 and Dex's wespa requirements of 2026-10-02.",
|
|
8
|
+
"description": "A same-origin test-host fixture served by the viewer's dev server (route `/test-host.html` behind a `--test-host` flag or an env guard; off by default so wespa's proxy allow-list is unaffected) mimics the wespa shell: a 56px header, a resizable sidebar (64px collapsed, 200-400px expanded, user-resizable live via a drag handle), a content pane carrying an `<iframe src=\"./?embed=1&theme=light\">`. The host posts `{ type: \"rcf-view-theme\", theme: \"light\"|\"dark\" }` on a toggle control; it stores no theme itself so a passing run proves the iframe reads the message. The viewer sends no outbound postMessage. Three lints run in the test suite: (1) framing lint (`GET /` returns no `X-Frame-Options`, no CSP `frame-ancestors` that forbids same-origin, and the fixture loads the iframe without a browser error); (2) target lint (no `target=\"_top\"` string in the server-rendered HTML); (3) localStorage namespace lint (every `localStorage.{setItem,getItem,removeItem}` and shared `storage.*` call site in `src/view/**` uses a key starting with `rcf-view:v1:`). Amended 2026-10-04 (w-2026-10-04-dave-001): the viewer's own three-state theme control (Light|Dark|Auto) sits right of the SearchButton in non-embed mode, is hidden entirely under `embed=1` and in non-embed mode persists its setting under `rcf-view:v1:theme` per ADR-4137 (Dave's storage ruling 2026-10-04). See viewer UI refresh design doc section 9 and section 11 recommendation 6, and Dex's wespa requirements of 2026-10-02.",
|
|
9
9
|
"asA": "Dex integrating the rcf-lite viewer behind wespa's reverse proxy",
|
|
10
|
-
"iWant": "a CI host fixture that proves the viewer honours the embedding contract and three lints that keep framing, target='_top' and localStorage namespace in place over time",
|
|
11
|
-
"soThat": "regressions in the embedding contract are caught in the viewer's own suite before they reach wespa",
|
|
10
|
+
"iWant": "a CI host fixture that proves the viewer honours the embedding contract and three lints that keep framing, target='_top' and localStorage namespace in place over time; and a visible three-state theme control in the standalone viewer that is hidden under embed=1",
|
|
11
|
+
"soThat": "regressions in the embedding contract are caught in the viewer's own suite before they reach wespa, and standalone viewer users can switch theme without a URL edit",
|
|
12
12
|
"acceptanceCriteria": [
|
|
13
13
|
{
|
|
14
14
|
"id": "AC-206-1",
|
|
@@ -39,6 +39,26 @@
|
|
|
39
39
|
"id": "AC-206-6",
|
|
40
40
|
"description": "[must-not] Given the test-host fixture and the viewer page, when either is loaded, then neither posts any outbound message beyond the host's `rcf-view-theme`; the viewer sends no `postMessage` of any shape, and the test-host adds no new postMessage shape.",
|
|
41
41
|
"testable": true
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "AC-206-7",
|
|
45
|
+
"description": "[happy] Given the standalone viewer loaded without `embed=1`, when the shell paints, then a three-state theme control (`role=\"group\"`, three buttons Light|Dark|Auto with `aria-pressed`) renders in the header's `.tools` group right of the SearchButton; when a button is pressed, `<html data-theme>` is set to `light` or `dark` (or the attribute is removed for Auto so the `prefers-color-scheme` media query decides) live without a reload, and the pressed button's `aria-pressed` becomes `true` while the others become `false`.",
|
|
46
|
+
"testable": true
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "AC-206-8",
|
|
50
|
+
"description": "[happy] Given the standalone (non-embed) viewer, when a theme button is pressed, then the chosen value (`light`, `dark` or `auto`) is written to `localStorage['rcf-view:v1:theme']`; on the next page load the stored value is reapplied; the boot precedence is `?theme=` wins first, then the stored value, then the default (`auto`); and the postMessage `rcf-view-theme` listener still applies live but writes nothing to the stored key.",
|
|
51
|
+
"testable": true
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "AC-206-9",
|
|
55
|
+
"description": "[must-not] Given the viewer loaded with `?embed=1`, when the shell paints, then the three-state theme control is absent from the DOM entirely (no `[data-rcf-theme-control]` element rendered, not merely hidden by CSS), and no read or write of `localStorage['rcf-view:v1:theme']` happens on load, on a postMessage theme change, or on any user action inside the viewer.",
|
|
56
|
+
"testable": true
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "AC-206-10",
|
|
60
|
+
"description": "[happy] Given the standalone viewer with a stored value `rcf-view:v1:theme = dark`, when the URL carries `?theme=light`, then on load `<html data-theme>` is `light` (the explicit query wins) and the Light button is pressed; the stored value is left as `dark` and is reused on the next load without a `?theme=` query.",
|
|
61
|
+
"testable": true
|
|
42
62
|
}
|
|
43
63
|
],
|
|
44
64
|
"tacIds": [
|
|
@@ -47,8 +67,9 @@
|
|
|
47
67
|
"tags": [
|
|
48
68
|
"view",
|
|
49
69
|
"embedding",
|
|
70
|
+
"theme",
|
|
50
71
|
"lint"
|
|
51
72
|
],
|
|
52
73
|
"createdAt": "2026-10-02T20:00:00Z",
|
|
53
|
-
"updatedAt": "2026-10-
|
|
74
|
+
"updatedAt": "2026-10-04T14:00:00Z"
|
|
54
75
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"usId": "US-207",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"reqId": "REQ-002",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"status": "draft",
|
|
7
|
+
"title": "Every viewer text and control colour pair meets WCAG 2.1 AA in both themes",
|
|
8
|
+
"description": "A static CSS-contrast audit test parses `packages/rcf-lite/src/view/style.css`, resolves the `--sv-*` custom properties for the light (`:root`) and dark (`:root[data-theme=\"dark\"]`) token blocks, and enumerates every rule that sets `color` together with a reachable `background` / `background-color` (own rule, then the nearest ancestor surface token, default `--sv-canvas` page background). Each pair is enumerated in both themes and across every reachable state variant: default, `:hover`, `:focus`, `:focus-visible`, `:active`, `[aria-pressed=\"true\"]`, `[aria-selected=\"true\"]`, `.is-active`, `:disabled`. For each pair the WCAG relative-luminance contrast ratio is computed and asserted against the WCAG 2.1 AA thresholds (>= 4.5:1 for normal text, >= 3:1 for large text and non-text UI boundaries per WCAG 1.4.3 and 1.4.11). Pairs that cannot be resolved statically (gradients, rgba over an unknown background) are computed against the two most likely surfaces (`--sv-canvas` and the ancestor surface token) and recorded as approximate rather than skipped. The test emits a BEFORE/AFTER table under `./output/contrast-report.md` and refuses the suite on any failing pair. Scope covers the whole viewer shell, every tab, components, SearchButton and LookupModal, Readiness, Requirements, Architecture, Build (Specs and DAG), ID lookup; Mermaid and Product Map themed palettes are audited on a best-effort basis and anything bound to a third-party default is listed as a known gap with its ratio. See viewer UI refresh design doc section 9 (theme) and Baz's 2026-10-04 review (`w-2026-10-04-dave-001`).",
|
|
9
|
+
"asA": "an engineer or reviewer looking at the review surface in either theme",
|
|
10
|
+
"iWant": "every text and control colour pair in the viewer to meet WCAG 2.1 AA in both themes, pinned by a test that fails the suite when a new rule breaks the floor",
|
|
11
|
+
"soThat": "the review surface stays readable in both themes and nobody has to re-do the audit by hand on every CSS change",
|
|
12
|
+
"acceptanceCriteria": [
|
|
13
|
+
{
|
|
14
|
+
"id": "AC-207-1",
|
|
15
|
+
"description": "[happy] Given the viewer stylesheet `src/view/style.css` and the two token blocks (`:root` and `:root[data-theme=\"dark\"]`), when the contrast test runs, then it enumerates every CSS rule that sets `color` (and every state rule merged over its base selector, so a state that changes only the background is paired with the inherited colour) paired with every reachable background (own rule, nearest ancestor surface token, page background `--sv-canvas`) and every state variant (default, `:hover`, `:focus`, `:focus-visible`, `:active`, `[aria-pressed=\"true\"]`, `[aria-selected=\"true\"]`, `.is-active`, `:disabled`), computes the WCAG relative-luminance contrast ratio for each, and asserts >= 4.5:1 for text, >= 3:1 for large text (>= 18.66px bold or >= 24px) and for non-text UI boundaries (control borders and outlines, focus rings against the adjacent colour, each enumerated as its own pair) per WCAG 1.4.3 and 1.4.11 in both the light and dark token sets.",
|
|
16
|
+
"testable": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "AC-207-2",
|
|
20
|
+
"description": "[failure] Given a contrast pair that falls below the applicable threshold, when the test runs, then the failure message names the selector, state, theme, foreground colour, background colour, computed ratio and the threshold applied, and the test exits non-zero so the suite blocks a merge that drops a pair below AA.",
|
|
21
|
+
"testable": true
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"tacIds": [
|
|
25
|
+
"TAC-4134-wespa-host-fixture"
|
|
26
|
+
],
|
|
27
|
+
"tags": [
|
|
28
|
+
"view",
|
|
29
|
+
"contrast",
|
|
30
|
+
"accessibility",
|
|
31
|
+
"a11y",
|
|
32
|
+
"theme"
|
|
33
|
+
],
|
|
34
|
+
"createdAt": "2026-10-04T14:00:00Z",
|
|
35
|
+
"updatedAt": "2026-10-05T12:30:00Z"
|
|
36
|
+
}
|
package/releases/releases.yaml
CHANGED
|
@@ -40,8 +40,18 @@
|
|
|
40
40
|
# `npm install rcf-lite`.
|
|
41
41
|
|
|
42
42
|
feedVersion: 1
|
|
43
|
-
latest: "0.
|
|
43
|
+
latest: "0.31.0"
|
|
44
44
|
releases:
|
|
45
|
+
- version: "0.31.0"
|
|
46
|
+
date: "2026-10-05"
|
|
47
|
+
breaking: true
|
|
48
|
+
headlines:
|
|
49
|
+
- "rcf-lite 0.31.0 gives the viewer a Light, Dark and Auto theme control in standalone mode and brings its text and control colours up to WCAG AA contrast in both themes, with a test that blocks any regression. Under embed=1 the control is absent, nothing is stored and the rcf-view-theme message contract is unchanged."
|
|
50
|
+
- "DEFINE step 3 rulings R1 to R13 land: tree-wide readiness checks now run before any scope short-circuit, so a narrowed scope still reports a draft marker, an unjoined core entity, an uncited standards pack, a missing Deploy ADR or an empty security or operational section anywhere in the tree. A stage whose scope is empty but whose tree-wide check fails now reads failing rather than not applicable, and the draft-settled check is tree-wide."
|
|
51
|
+
- "Write-backs that carry an id are idempotent: rcf define create <kind> --id and rcf define ledger <name> add --id exit 0 and write nothing when the same id is replayed with the same content, and exit 3 naming the differing fields when the content differs."
|
|
52
|
+
minAgentAction: null
|
|
53
|
+
notesUrl: "https://stravica.ai/docs/rcf/changelog/"
|
|
54
|
+
|
|
45
55
|
- version: "0.30.0"
|
|
46
56
|
date: "2026-10-03"
|
|
47
57
|
breaking: true
|
package/src/cli/create.js
CHANGED
|
@@ -64,8 +64,11 @@ Options:
|
|
|
64
64
|
every non-root child carries a mandatory
|
|
65
65
|
parentId-style field). See the parent table
|
|
66
66
|
above for which id each kind expects.
|
|
67
|
-
--id <id> Override auto-assigned id
|
|
68
|
-
|
|
67
|
+
--id <id> Override auto-assigned id. Idempotent by id:
|
|
68
|
+
an existing id with identical content is a
|
|
69
|
+
no-op ('<id>: unchanged', exit 0); with
|
|
70
|
+
different content it refuses (exit 3, names
|
|
71
|
+
the differing fields). Use update to patch
|
|
69
72
|
--title <string> Required for req / us / tac / adr / fbs / ts
|
|
70
73
|
(ac / tc use --description)
|
|
71
74
|
--description <string> Body description; required for ac / tc
|
|
@@ -297,6 +300,15 @@ export async function main(argv, deps = {}) {
|
|
|
297
300
|
if (isRcfError(result)) {
|
|
298
301
|
return handleWriterError(result, stderr);
|
|
299
302
|
}
|
|
303
|
+
// Ruling R1 extension 2026-10-05 (PR 292 follow-up, w-2026-10-05-dave-013):
|
|
304
|
+
// a same-id replay with identical content exits 0 with 'unchanged';
|
|
305
|
+
// the file is not rewritten. Mirrors `rcf define ledger add --id`.
|
|
306
|
+
if (result.status === 'unchanged') {
|
|
307
|
+
if (!flags.quiet) {
|
|
308
|
+
stdout.write(`${result.id}: unchanged (${result.filePath})\n`);
|
|
309
|
+
}
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
300
312
|
if (options.dryRun) {
|
|
301
313
|
if (!flags.quiet) stdout.write(`[dry-run] would create ${result.id} at ${result.filePath}\n`);
|
|
302
314
|
return 0;
|
|
@@ -358,6 +370,10 @@ const ERROR_KINDS = new Set([
|
|
|
358
370
|
'parseFailure',
|
|
359
371
|
'ioFailure',
|
|
360
372
|
'usage',
|
|
373
|
+
// Ruling R1 extension 2026-10-05 (PR 292 follow-up): a same-id
|
|
374
|
+
// create whose would-be body differs from the existing doc refuses
|
|
375
|
+
// with `kind: 'conflict'` and exits 3 (never silently overwrites).
|
|
376
|
+
'conflict',
|
|
361
377
|
]);
|
|
362
378
|
|
|
363
379
|
function isRcfError(value) {
|
|
@@ -412,6 +428,10 @@ function handleWriterError(err, stderr) {
|
|
|
412
428
|
stderr.write(`[error] ${kind} ${err.message}\n`);
|
|
413
429
|
if (kind === 'usage') return 2;
|
|
414
430
|
if (kind === 'validation' || kind === 'brokenReference') return 3;
|
|
431
|
+
// Ruling R1 extension 2026-10-05 (PR 292 follow-up): a same-id
|
|
432
|
+
// create with different content refuses with exit 3; `update` is
|
|
433
|
+
// the verb for patching.
|
|
434
|
+
if (kind === 'conflict') return 3;
|
|
415
435
|
if (kind === 'missingFile' || kind === 'parseFailure') return 2;
|
|
416
436
|
return 1;
|
|
417
437
|
}
|
package/src/cli/ledger.js
CHANGED
|
@@ -27,6 +27,7 @@ import { runIntakeScansOnDelta } from '../intake/orchestrator.js';
|
|
|
27
27
|
import { findProjectRoot } from '../view/index.js';
|
|
28
28
|
|
|
29
29
|
const OPTION_SPEC = {
|
|
30
|
+
id: { type: 'string' },
|
|
30
31
|
kind: { type: 'string' },
|
|
31
32
|
text: { type: 'string' },
|
|
32
33
|
from: { type: 'string' },
|
|
@@ -74,6 +75,15 @@ Common flags:
|
|
|
74
75
|
--help Print this help
|
|
75
76
|
|
|
76
77
|
brief add flags:
|
|
78
|
+
--id <n> Optional positive integer. When set, the add
|
|
79
|
+
is idempotent by id (ruling R1 2026-10-05):
|
|
80
|
+
an entry at that id with identical content
|
|
81
|
+
exits 0 printing 'unchanged' (--json emits
|
|
82
|
+
{status:"unchanged",id:n}); different content
|
|
83
|
+
at the same id exits 3 naming the differing
|
|
84
|
+
fields; a free id is honoured as the override.
|
|
85
|
+
The harness carries --id back when it replays
|
|
86
|
+
a turn so the add is a no-op on replay.
|
|
77
87
|
--kind <${BRIEF_KINDS.join('|')}>
|
|
78
88
|
Statement kind (default: capability)
|
|
79
89
|
--text <text> The statement text (mutually exclusive with --from)
|
|
@@ -193,6 +203,14 @@ export async function main(argv, deps = {}) {
|
|
|
193
203
|
return await runResolve({ projectRoot, name, positionals, flags, stdout, stderr });
|
|
194
204
|
} catch (err) {
|
|
195
205
|
if (err instanceof LedgerError) {
|
|
206
|
+
// Ruling R1 2026-10-05: a same-id-different-content refusal
|
|
207
|
+
// exits 3 (not 1 and not 2) so a replaying harness can
|
|
208
|
+
// distinguish 'you tried to overwrite a prior answer' from a
|
|
209
|
+
// schema failure or a usage error. Update is the verb for that.
|
|
210
|
+
if (err.code === 'conflict') {
|
|
211
|
+
stderr.write(`[error] ledger: ${err.message}\n`);
|
|
212
|
+
return 3;
|
|
213
|
+
}
|
|
196
214
|
stderr.write(`[error] ${err.code === 'usage' ? 'usage ' : ''}ledger: ${err.message}\n`);
|
|
197
215
|
return err.code === 'usage' ? 2 : 1;
|
|
198
216
|
}
|
|
@@ -285,6 +303,19 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
|
|
|
285
303
|
const body = await loadLedger({ projectRoot, name });
|
|
286
304
|
const now = new Date().toISOString();
|
|
287
305
|
|
|
306
|
+
// Ruling R1 2026-10-05: parse --id up front; a non-integer or negative
|
|
307
|
+
// value is a usage error BEFORE loading / scanning the ledger.
|
|
308
|
+
/** @type {number | undefined} */
|
|
309
|
+
let idOverride;
|
|
310
|
+
if (flags.id !== undefined) {
|
|
311
|
+
const parsed = Number(flags.id);
|
|
312
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
313
|
+
stderr.write(`[error] usage ledger: --id must be a positive integer, got '${flags.id}'\n`);
|
|
314
|
+
return 2;
|
|
315
|
+
}
|
|
316
|
+
idOverride = parsed;
|
|
317
|
+
}
|
|
318
|
+
|
|
288
319
|
if (name === 'brief') {
|
|
289
320
|
const kind = /** @type {string} */ (flags.kind ?? 'capability');
|
|
290
321
|
if (!BRIEF_KINDS.includes(/** @type {any} */ (kind))) {
|
|
@@ -299,6 +330,12 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
|
|
|
299
330
|
stderr.write('[error] usage ledger: brief add requires --text <text> or --from <path>\n');
|
|
300
331
|
return 2;
|
|
301
332
|
}
|
|
333
|
+
if (idOverride !== undefined && flags.from) {
|
|
334
|
+
// R1 identity is per entry; --from mints N entries, so --id has no
|
|
335
|
+
// single target. Refuse the combination to keep the harness honest.
|
|
336
|
+
stderr.write('[error] usage ledger: brief add --id applies to --text only (--from mints one entry per line and has no single id)\n');
|
|
337
|
+
return 2;
|
|
338
|
+
}
|
|
302
339
|
if (flags['no-scan'] && flags.findings) {
|
|
303
340
|
stderr.write('[error] usage ledger: brief add --no-scan and --findings cannot be combined\n');
|
|
304
341
|
return 2;
|
|
@@ -336,10 +373,32 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
|
|
|
336
373
|
// Compute what WOULD be minted (for both the real path and dry-run).
|
|
337
374
|
let cur = body;
|
|
338
375
|
const added = [];
|
|
376
|
+
/** @type {'added' | 'unchanged'} */
|
|
377
|
+
let primaryStatus = 'added';
|
|
339
378
|
for (const d of drafts) {
|
|
340
|
-
|
|
379
|
+
// Only the single --text path reaches here with an idOverride (we
|
|
380
|
+
// refused --id with --from above); a per-call idOverride is passed
|
|
381
|
+
// on the first and only draft.
|
|
382
|
+
const step = addEntry({
|
|
383
|
+
name: 'brief', body: cur, entry: d, now,
|
|
384
|
+
...(idOverride !== undefined ? { id: idOverride } : {}),
|
|
385
|
+
});
|
|
341
386
|
cur = step.body;
|
|
342
387
|
added.push(step.entry);
|
|
388
|
+
if (step.status === 'unchanged') primaryStatus = 'unchanged';
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Ruling R1 2026-10-05 short-circuit: when --id landed on an entry
|
|
392
|
+
// that already carried identical content, exit 0 with the unchanged
|
|
393
|
+
// signal before any scan / finding work. A replayed turn is a no-op.
|
|
394
|
+
if (primaryStatus === 'unchanged' && added.length === 1) {
|
|
395
|
+
const existing = added[0];
|
|
396
|
+
if (flags.json) {
|
|
397
|
+
stdout.write(`${JSON.stringify({ status: 'unchanged', id: existing.id, entry: existing }, null, 2)}\n`);
|
|
398
|
+
} else {
|
|
399
|
+
stdout.write(`brief-ledger: unchanged ${existing.id}\n`);
|
|
400
|
+
}
|
|
401
|
+
return 0;
|
|
343
402
|
}
|
|
344
403
|
|
|
345
404
|
// Convert operator-supplied --findings into openQuestion statements.
|
|
@@ -428,7 +487,18 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
|
|
|
428
487
|
default: flags.default ? String(flags.default) : null,
|
|
429
488
|
};
|
|
430
489
|
if (flags.blocks) entry.blocks = String(flags.blocks);
|
|
431
|
-
const step = addEntry({
|
|
490
|
+
const step = addEntry({
|
|
491
|
+
name: 'decisions', body, entry, now,
|
|
492
|
+
...(idOverride !== undefined ? { id: idOverride } : {}),
|
|
493
|
+
});
|
|
494
|
+
if (step.status === 'unchanged') {
|
|
495
|
+
if (flags.json) {
|
|
496
|
+
stdout.write(`${JSON.stringify({ status: 'unchanged', id: step.entry.id, entry: step.entry }, null, 2)}\n`);
|
|
497
|
+
} else {
|
|
498
|
+
stdout.write(`decisions-ledger: unchanged ${step.entry.id}\n`);
|
|
499
|
+
}
|
|
500
|
+
return 0;
|
|
501
|
+
}
|
|
432
502
|
await saveLedger({ projectRoot, name: 'decisions', body: step.body });
|
|
433
503
|
stdout.write(`decisions-ledger: added decision ${step.entry.id}\n`);
|
|
434
504
|
return 0;
|
|
@@ -449,7 +519,18 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
|
|
|
449
519
|
disposition: String(flags.disposition),
|
|
450
520
|
};
|
|
451
521
|
if (flags.reason) entry.reason = String(flags.reason);
|
|
452
|
-
const step = addEntry({
|
|
522
|
+
const step = addEntry({
|
|
523
|
+
name: 'concerns', body, entry, now,
|
|
524
|
+
...(idOverride !== undefined ? { id: idOverride } : {}),
|
|
525
|
+
});
|
|
526
|
+
if (step.status === 'unchanged') {
|
|
527
|
+
if (flags.json) {
|
|
528
|
+
stdout.write(`${JSON.stringify({ status: 'unchanged', id: step.entry.id, entry: step.entry }, null, 2)}\n`);
|
|
529
|
+
} else {
|
|
530
|
+
stdout.write(`concern-ledger: unchanged ${step.entry.id}\n`);
|
|
531
|
+
}
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
453
534
|
await saveLedger({ projectRoot, name: 'concerns', body: step.body });
|
|
454
535
|
stdout.write(`concern-ledger: added concern ${step.entry.id}\n`);
|
|
455
536
|
return 0;
|
|
@@ -465,7 +546,18 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
|
|
|
465
546
|
finding: String(flags.finding),
|
|
466
547
|
severity: flags.severity ? String(flags.severity) : 'medium',
|
|
467
548
|
};
|
|
468
|
-
const step = addEntry({
|
|
549
|
+
const step = addEntry({
|
|
550
|
+
name: 'probes', body, entry, now,
|
|
551
|
+
...(idOverride !== undefined ? { id: idOverride } : {}),
|
|
552
|
+
});
|
|
553
|
+
if (step.status === 'unchanged') {
|
|
554
|
+
if (flags.json) {
|
|
555
|
+
stdout.write(`${JSON.stringify({ status: 'unchanged', id: step.entry.id, entry: step.entry }, null, 2)}\n`);
|
|
556
|
+
} else {
|
|
557
|
+
stdout.write(`probe-ledger: unchanged ${step.entry.id}\n`);
|
|
558
|
+
}
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
469
561
|
await saveLedger({ projectRoot, name: 'probes', body: step.body });
|
|
470
562
|
stdout.write(`probe-ledger: added probe ${step.entry.id}\n`);
|
|
471
563
|
return 0;
|
package/src/cli/readiness.js
CHANGED
|
@@ -390,7 +390,7 @@ function renderText(stdout, stderr, result, wallMs, checkStage, level, persona,
|
|
|
390
390
|
|
|
391
391
|
for (const stage of result.stages) {
|
|
392
392
|
if (checkStage && checkStage !== 'all' && stage.stage !== checkStage) continue;
|
|
393
|
-
stdout.write(`\n${
|
|
393
|
+
stdout.write(`\n${formatStageLine(stage)}\n`);
|
|
394
394
|
for (const c of stage.checks) {
|
|
395
395
|
if (persona && c.persona !== persona) continue;
|
|
396
396
|
const mark = c.ok ? 'ok' : 'FAIL';
|
|
@@ -405,6 +405,20 @@ function renderText(stdout, stderr, result, wallMs, checkStage, level, persona,
|
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Format the per-stage header line the text report prints, e.g.
|
|
410
|
+
* `D3 (define.shapes): failing`. A `reason` trailer is appended only
|
|
411
|
+
* when the envelope carries one (the `notApplicable` envelopes do; the
|
|
412
|
+
* ADR-4138 tree-wide failing envelope does not). Exported so the
|
|
413
|
+
* printed line is testable without driving the whole CLI.
|
|
414
|
+
*
|
|
415
|
+
* @param {{ stage: string, gate: string, state: string, reason?: string }} stage
|
|
416
|
+
* @returns {string}
|
|
417
|
+
*/
|
|
418
|
+
export function formatStageLine(stage) {
|
|
419
|
+
return `${stage.stage} (${stage.gate}): ${STATE_LABEL[stage.state] ?? stage.state}${stage.reason ? ` (${stage.reason})` : ''}`;
|
|
420
|
+
}
|
|
421
|
+
|
|
408
422
|
/**
|
|
409
423
|
* Render the per-persona next-action lines (spec section 4). When a
|
|
410
424
|
* `--level` is given the matching persona's line heads the pair; when
|
package/src/core/errors/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// renders them inline in the page so an owner can see what is broken.
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* @typedef {('validation'|'missingFile'|'brokenReference'|'parseFailure'|'ioFailure'|'usage'|'staleCode'|'missingCodeNodes'|'duplicateId')} ErrorKind
|
|
10
|
+
* @typedef {('validation'|'missingFile'|'brokenReference'|'parseFailure'|'ioFailure'|'usage'|'staleCode'|'missingCodeNodes'|'duplicateId'|'conflict')} ErrorKind
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -49,6 +49,12 @@ const VALID_KINDS = new Set([
|
|
|
49
49
|
// with the spec-verbatim refusal message; the CLI translates it to
|
|
50
50
|
// exit 3 (not exit 2) with the message printed as-is on stderr.
|
|
51
51
|
'requiresAppliedCapabilities',
|
|
52
|
+
// Ruling R1 extension 2026-10-05 (DEFINE step 3, PR 292 follow-up):
|
|
53
|
+
// a same-id `rcf define create` replay whose would-be body differs
|
|
54
|
+
// from the on-disk entry refuses with `kind: 'conflict'`; the CLI
|
|
55
|
+
// translates it to exit 3 with the id and the differing field keys
|
|
56
|
+
// named in the message. Mirrors `src/define/ledgers.js` R1 add path.
|
|
57
|
+
'conflict',
|
|
52
58
|
]);
|
|
53
59
|
|
|
54
60
|
/**
|