rcf-lite 0.32.1 → 0.32.2

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 CHANGED
@@ -6,6 +6,16 @@ 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.2] - 2026-10-06
10
+
11
+ 0.32.2 ships two Dex-lane fixes merged since 0.32.1 (merge b478a4e) as the WESPA readiness alignment release. D6 consistency:unsatisfiable now context-gates backticked identifiers in AC then-clauses so enum values, query-string parameter names, sort and filter literals, env-var names and config knobs stop being read as record-shape field names, and `rcf discover req-baseline opt-out` and `rcf define ledger concerns add` honour `--dry-run` by writing nothing. 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
+ - **D6 consistency:unsatisfiable stops reading backticked enum values, query-string parameter names, sort and filter literals, env-var and config-knob names as record-shape field names (issue 311, w-2026-10-06-dave-050).** `extractThenFieldTokens` in `src/query/gates.js` previously fired `consistency:unsatisfiable` on any backticked identifier in an AC `then` clause and treated every token it caught as a field some `recordShape` had to define. On WESPA (rcf-lite 0.31.0, main `df0fa47e`) 345 of 532 flagged tokens were not fields: HTTP error-envelope `code` enum values like `VALIDATION_ERROR` and `AUTH_TOKEN_INVALID_SIGNATURE`, pagination query-string keys like `pageSize`, `offset`, `cursors` and sort-order values like `asc`, `desc`, `next`, filter and match-mode literals like `query`, `exact`, `in`, env-var names like `BUILD_GIT_COMMIT_SHORT` and config knobs like `maxDiffPixelRatio`, `fullPath`, `workerAdminUser`. Fix: pattern 1 (bare backticked identifier) is now context-gated. ALL_CAPS tokens (enum values, env vars, constants) are skipped unconditionally. Other backticked tokens count only when a field-signalling word (a verb like `set`, `write`, `include`, `return`, `contain`; a predicate like `is set`, `is written`, `equals`, `holds`; or a context word like `field`, `record shape`, `response body`, `request body`, `payload`, `envelope`) sits within a short neighbourhood AND no explicit non-field context (`query parameter`, `query-string`, `sort order`, `sort direction`, `filter mode`, `match mode`, `enum value`, `env var`, `environment variable`, `config knob`, `config key`, `header name`, `cli flag`) overlaps the same neighbourhood. Pattern 4 added: `<ShapeName>.<fieldName>` dot notation is now read as a schema-style field reference. Patterns 2 (`<name> field`) and 3 (`field: <name>`) are preserved and the AC-12113-2 class is unchanged: a then-clause that names a real undefined field still fails. Regression fixtures in `test/query/gates.test.js` cover each of the five WESPA token patterns from the issue plus a dot-notation negative control and a plain-prose negative control. Expected drop on WESPA: 345 unsatisfiable findings removed; the 187 true-positive findings and all AC-12113-2 class failures remain.
16
+
17
+ - **`--dry-run` on `rcf discover req-baseline opt-out` and `rcf define ledger concerns add` writes nothing (issue 310, w-2026-10-06-dave-050).** Both verbs accepted the flag but wrote the opt-out record to `rcf/manifest.json` (`baselineAcOptOuts[]`) and created `rcf/define/concern-ledger.json` with the appended entry respectively, mutating viewer clones on rehearsal runs. Fix: `src/cli/req-baseline.js` opt-out verb and `src/cli/ledger.js` concerns handler both check `flags['dry-run']` before the write and emit a `[dry-run]` preview line (or a `{dryRun: true, ...}` JSON payload with `--json`) and exit 0. Help text updated on both verbs to document the flag. Regression tests in `test/cli/req-baseline-opt-out-dry-run.test.js` (new) and `test/cli/ledger.test.js` assert the on-disk bytes under `rcf/` are byte-equal before and after each dry-run and that the positive control (no `--dry-run`) still writes.
18
+
9
19
  ## [0.32.1] - 2026-10-06
10
20
 
11
21
  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.
@@ -2,8 +2,8 @@
2
2
  "registerCanary": [
3
3
  {
4
4
  "id": "rc-2026-10-06-001",
5
- "createdAt": "2026-10-06T18:28:43.472Z",
6
- "buildVersion": "0.32.1-mockdriver",
5
+ "createdAt": "2026-10-06T19:49:58.281Z",
6
+ "buildVersion": "0.32.2-mockdriver",
7
7
  "fixturePromptId": "canary-prompt-01",
8
8
  "responseWordCount": 55,
9
9
  "grades": {
@@ -35,8 +35,8 @@
35
35
  },
36
36
  {
37
37
  "id": "rc-2026-10-06-002",
38
- "createdAt": "2026-10-06T18:28:43.473Z",
39
- "buildVersion": "0.32.1-mockdriver",
38
+ "createdAt": "2026-10-06T19:49:58.282Z",
39
+ "buildVersion": "0.32.2-mockdriver",
40
40
  "fixturePromptId": "canary-prompt-02",
41
41
  "responseWordCount": 55,
42
42
  "grades": {
@@ -68,8 +68,8 @@
68
68
  },
69
69
  {
70
70
  "id": "rc-2026-10-06-003",
71
- "createdAt": "2026-10-06T18:28:43.473Z",
72
- "buildVersion": "0.32.1-mockdriver",
71
+ "createdAt": "2026-10-06T19:49:58.282Z",
72
+ "buildVersion": "0.32.2-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.32.1",
3
+ "version": "0.32.2",
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",
@@ -40,8 +40,16 @@
40
40
  # `npm install rcf-lite`.
41
41
 
42
42
  feedVersion: 1
43
- latest: "0.32.1"
43
+ latest: "0.32.2"
44
44
  releases:
45
+ - version: "0.32.2"
46
+ date: "2026-10-06"
47
+ breaking: false
48
+ headlines:
49
+ - "rcf-lite 0.32.2 ships two Dex-lane fixes on top of 0.32.1. D6 consistency:unsatisfiable now context-gates backticked identifiers in AC then-clauses so enum values, query-string parameter names, sort and filter literals, env-var names and config knobs stop being read as record-shape field names (issue 311; expected drop on WESPA 345 unsatisfiable findings, 187 true positives remain, AC-12113-2 class unchanged), and `rcf discover req-baseline opt-out` and `rcf define ledger concerns add` honour `--dry-run` by writing nothing (issue 310)."
50
+ minAgentAction: null
51
+ notesUrl: "https://stravica.ai/docs/rcf/changelog/"
52
+
45
53
  - version: "0.32.1"
46
54
  date: "2026-10-06"
47
55
  breaking: false
package/src/cli/ledger.js CHANGED
@@ -112,6 +112,7 @@ concerns add flags:
112
112
  --disposition applied|waived
113
113
  Disposition (required)
114
114
  --reason <text> Reason (mandatory when disposition=waived)
115
+ --dry-run Preview the minted entry; write nothing
115
116
 
116
117
  probes add flags:
117
118
  --req <req-id> REQ the probe finding sits on (required)
@@ -523,6 +524,20 @@ async function runAdd({ projectRoot, name, flags, stdout, stderr }) {
523
524
  name: 'concerns', body, entry, now,
524
525
  ...(idOverride !== undefined ? { id: idOverride } : {}),
525
526
  });
527
+ // Issue 310 (0.32.2): --dry-run on concerns add previews the
528
+ // minted entry without writing the ledger. Prior behaviour
529
+ // accepted the flag and created `rcf/define/concern-ledger.json`
530
+ // anyway, mutating viewer clones on rehearsal runs.
531
+ if (flags['dry-run']) {
532
+ if (flags.json) {
533
+ stdout.write(`${JSON.stringify({ dryRun: true, status: step.status, id: step.entry.id, entry: step.entry }, null, 2)}\n`);
534
+ } else if (step.status === 'unchanged') {
535
+ stdout.write(`[dry-run] concern-ledger: would be unchanged ${step.entry.id}\n`);
536
+ } else {
537
+ stdout.write(`[dry-run] concern-ledger: would add concern ${step.entry.id}\n`);
538
+ }
539
+ return 0;
540
+ }
526
541
  if (step.status === 'unchanged') {
527
542
  if (flags.json) {
528
543
  stdout.write(`${JSON.stringify({ status: 'unchanged', id: step.entry.id, entry: step.entry }, null, 2)}\n`);
@@ -60,12 +60,14 @@ Verbs:
60
60
  (silence-refuses-build queue) without
61
61
  prompting.
62
62
 
63
- opt-out --req <id> --key <baselineKey> --reason "..." [--scope req|project]
63
+ opt-out --req <id> --key <baselineKey> --reason "..." [--scope req|project] [--dry-run]
64
64
  Record an operator ruling that removes a
65
65
  baseline AC from the REQ (or the whole
66
66
  project) and inherit the ruling on future
67
67
  sweeps. Reason is at least 20 characters
68
- (schema-enforced).
68
+ (schema-enforced). --dry-run prints the
69
+ opt-out that would be written and leaves
70
+ the manifest untouched.
69
71
 
70
72
  opt-out --remove --req <id> --key <baselineKey>
71
73
  Delete an existing opt-out; the next sweep
@@ -252,6 +254,18 @@ export async function main(argv, deps = {}) {
252
254
  stderr.write(`[error] usage req-baseline opt-out: --scope must be req or project (got ${scope})\n`);
253
255
  return 2;
254
256
  }
257
+ // Issue 310 (0.32.2): --dry-run on opt-out previews the opt-out
258
+ // record without writing to the manifest. Prior behaviour accepted
259
+ // the flag and wrote anyway, mutating viewer clones on rehearsal
260
+ // runs.
261
+ if (flags['dry-run']) {
262
+ if (flags.json) {
263
+ stdout.write(`${JSON.stringify({ dryRun: true, reqId: flags.req, baselineKey: flags.key, scope, reason: flags.reason }, null, 2)}\n`);
264
+ } else {
265
+ stdout.write(`[dry-run] would opt-out ${flags.req} ${flags.key} (scope=${scope})\n`);
266
+ }
267
+ return 0;
268
+ }
255
269
  const result = await writeOptOut({
256
270
  projectRoot,
257
271
  tree: walkResult.tree,
@@ -2033,9 +2033,23 @@ function collectRecordShapeFields(tree) {
2033
2033
  * that follows a verb like `set`, `write`, `include`, `return`,
2034
2034
  * `contain`, `name` or appears as the direct object of a `then`'s
2035
2035
  * verb phrase; a cheap approximation uses `then <subject> <verb>
2036
- * <field>` plus `field <name>`, `<name> field` and `field: <name>`
2037
- * patterns. The scan is deliberately conservative so a plain English
2038
- * sentence yields a small set.
2036
+ * <field>` plus `field <name>`, `<name> field`, `field: <name>` and
2037
+ * `<Shape>.<name>` patterns. The scan is deliberately conservative so
2038
+ * a plain English sentence yields a small set.
2039
+ *
2040
+ * Issue 311 (0.32.2): a bare backticked identifier (pattern 1) was
2041
+ * reading error-enum values, pagination query-string keys, sort and
2042
+ * filter-mode literals, env-var and config-knob names as record-shape
2043
+ * fields and false-positiving 345 of 532 ACs on WESPA. Pattern 1 is
2044
+ * now context-gated: ALL_CAPS tokens (enum values, env vars,
2045
+ * constants) are skipped unconditionally; other backticked tokens
2046
+ * count only when a field-signalling word sits within a short
2047
+ * neighbourhood AND no explicit non-field context (query parameter,
2048
+ * sort order, filter/match mode, env var, config knob, header name,
2049
+ * cli flag) overlaps the same neighbourhood. The AC-12113-2 class is
2050
+ * preserved: a then-clause that names a real missing field via
2051
+ * `<name> field`, `field: <name>` or an emit-verb context still fails
2052
+ * the check.
2039
2053
  *
2040
2054
  * @param {string} thenClause
2041
2055
  * @returns {string[]}
@@ -2045,27 +2059,60 @@ function extractThenFieldTokens(thenClause) {
2045
2059
  const text = stripQuotedSubstrings(thenClause);
2046
2060
  /** @type {Set<string>} */
2047
2061
  const tokens = new Set();
2048
- // Pattern 1: backticked identifier (`then `balance` is set`). The
2049
- // primary signal; prose that quotes a field name under backticks is
2050
- // the convention this check expects.
2051
- for (const m of text.matchAll(/`([A-Za-z_][A-Za-z0-9_]*)`/g)) {
2052
- if (m[1]) tokens.add(m[1]);
2053
- }
2054
- // Pattern 2: `<name> field` (adjective-form naming). An English
2055
- // "stop word" skip keeps "the field", "a field" and "its field" from
2056
- // tripping the check.
2057
2062
  const STOP = new Set(['the', 'a', 'an', 'this', 'that', 'its', 'our', 'their', 'any', 'no', 'some', 'each', 'every', 'another']);
2058
- for (const m of text.matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\s+field\b/g)) {
2063
+
2064
+ // Pattern 2 (preserved): `<name> field` (adjective-form naming). The
2065
+ // backtick around the name is optional; the stop-word skip keeps
2066
+ // "the field", "a field" and "its field" from tripping the check.
2067
+ for (const m of text.matchAll(/`?\b([A-Za-z_][A-Za-z0-9_]*)`?\s+field\b/g)) {
2059
2068
  const token = m[1];
2060
2069
  if (!token) continue;
2061
2070
  if (STOP.has(token.toLowerCase())) continue;
2062
2071
  tokens.add(token);
2063
2072
  }
2064
- // Pattern 3: `field: <name>` or `field = <name>` (schema-like
2065
- // notation inside a then clause).
2066
- for (const m of text.matchAll(/\bfield\s*[:=]\s*([A-Za-z_][A-Za-z0-9_]*)/g)) {
2073
+ // Pattern 3 (preserved): `field: <name>` or `field = <name>`
2074
+ // (schema-like notation inside a then clause). The name's backtick
2075
+ // is optional.
2076
+ for (const m of text.matchAll(/\bfields?\s*[:=]\s*`?([A-Za-z_][A-Za-z0-9_]*)`?/g)) {
2077
+ if (m[1]) tokens.add(m[1]);
2078
+ }
2079
+ // Pattern 4 (new, issue 311): `<ShapeName>.<fieldName>` dot
2080
+ // notation. Shape names are PascalCase; the field name after the
2081
+ // dot is the candidate.
2082
+ for (const m of text.matchAll(/\b[A-Z][A-Za-z0-9_]*\.([a-z_][A-Za-z0-9_]*)\b/g)) {
2067
2083
  if (m[1]) tokens.add(m[1]);
2068
2084
  }
2085
+ // Pattern 1 (narrowed, issue 311): backticked identifier WITH
2086
+ // explicit field evidence in its immediate neighbourhood AND no
2087
+ // overlapping non-field context. ALL_CAPS tokens (enum values, env
2088
+ // vars, constants) are skipped unconditionally; record-shape field
2089
+ // names in the WSD AI tree and the rcf-schemas canon are camelCase
2090
+ // or lower snake_case.
2091
+ const WINDOW = 36;
2092
+ const POS_BEFORE = /(?:\bsets?|\bsetting|\bwrites?|\bwriting|\bincludes?|\bincluding|\breturns?|\breturning|\bcontains?|\bcontaining|\bnames?|\bnaming|\bemits?|\bemitting|\bholds?|\bholding|\bcarries|\bcarrying|\badds?|\badding|\bhas|\bhave|\bhad|\bwith\s+(?:a|an|the))\s*$/i;
2093
+ const POS_AFTER = /^\s*(?:\bis\s+(?:set|written|included|returned|named|populated|present|absent|missing|required|optional|null|non-null|empty)|\bare\s+(?:set|written|included|returned|named|populated|present|absent|missing|required|optional)|\bequals?|\bholds?|\bcontains?|\bof\s+type|\bfields?\b)/i;
2094
+ const POS_CTX = /\bfields?\b|\brecord\s+shape\b|\brecord\s+body\b|\bresponse\s+body\b|\brequest\s+body\b|\bevent\s+body\b|\bpayload\b|\bevent\s+payload\b|\benvelope\b/i;
2095
+ const NEG_CTX = /\bquery\s+param(?:eter)?s?\b|\bquery[- ]?string\b|\bsort\s+(?:order|direction|by|key)\b|\bfilter\s+mode\b|\bmatch\s+mode\b|\benum\s+(?:value|member|variant|literal)s?\b|\benv(?:ironment)?\s*var(?:iable)?s?\b|\benvironment\s+variable\b|\bconfig(?:uration)?\s+(?:knob|key|option|value|setting|field|file)\b|\bheader\s+(?:name|value|key)\b|\b(?:cli|flag|option|argument|parameter)\s+name\b|\bconstant\b|\badmin\s+user\b|\bbuild\s+(?:info|metadata)\b|\broute\b|\bendpoint\b|\boperator\b|\bdirection\b/i;
2096
+
2097
+ for (const m of text.matchAll(/`([A-Za-z_][A-Za-z0-9_]*)`/g)) {
2098
+ const token = m[1];
2099
+ if (!token) continue;
2100
+ // Skip ALL_CAPS enum values, env vars, constants.
2101
+ if (/^[A-Z][A-Z0-9_]*$/.test(token) && token.length >= 2) continue;
2102
+ const start = m.index ?? 0;
2103
+ const end = start + m[0].length;
2104
+ const before = text.slice(Math.max(0, start - WINDOW), start);
2105
+ const after = text.slice(end, Math.min(text.length, end + WINDOW));
2106
+ if (NEG_CTX.test(before) || NEG_CTX.test(after)) continue;
2107
+ if (
2108
+ POS_BEFORE.test(before)
2109
+ || POS_AFTER.test(after)
2110
+ || POS_CTX.test(before)
2111
+ || POS_CTX.test(after)
2112
+ ) {
2113
+ tokens.add(token);
2114
+ }
2115
+ }
2069
2116
  return [...tokens];
2070
2117
  }
2071
2118