rcf-lite 0.32.1 → 0.32.3
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 +18 -0
- package/fixtures/canary-manifest.json +9 -9
- package/package.json +1 -1
- package/releases/releases.yaml +17 -1
- package/src/cli/ledger.js +15 -0
- package/src/cli/req-baseline.js +16 -2
- package/src/query/gates.js +127 -18
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ 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.3] - 2026-10-07
|
|
10
|
+
|
|
11
|
+
0.32.3 is a hotfix for the 0.32.2 D6 consistency:unsatisfiable regression discovered by Barry on ai-on-record (define/kickoff, head 974b4c1) and second-sourced by Dex on WESPA (26 green-to-red findings in three classes, 2026-10-06 20:05Z). The regression came from pattern 4 of `extractThenFieldTokens` in `src/query/gates.js`, added in PR 313 as part of the issue 311 fix. breaking: false; the 0.32.1 and 0.32.2 fixes carry forward unchanged. minAgentAction: null.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **D6 consistency:unsatisfiable stops reading filename stems, non-shape PascalCase dot phrases and sentence-ending dots as record-field references (issue 315, w-2026-10-06-dave-059).** 0.32.2 added a pattern 4 to `extractThenFieldTokens` that fired `consistency:unsatisfiable` on any `<PascalCase>.<lowercaseWord>` match in an AC `then` clause. On ai-on-record define/kickoff that read `CLAUDE.md`, `AGENTS.md`, `rcf/evals/corpus-V.json`, `rcf/evals/golden-transcripts-V.json` and other filename stems as record fields and false-failed AC-1501-9:md, AC-2201-1:json, AC-2201-5:json and AC-2201-8:json. Dex's WESPA re-run on 2026-10-06 20:05Z (AI-1987) added 26 findings in three classes: filename stems with a filename-stem head (js x7, vue x5, md x1), PascalCase heads that are not recordShape names in the tree (`Math.random`, `TEST_USERS.admin`, `ToolExecutionContext.pushLlmCall`, `LoadedFilterSchema.distinctLiveSourceTables`, and backticked dot-phrases like `` `Something.listModelInfo` ``, `` `Something.userID` ``), and AC-130-1 producing the token `the` from a sentence-ending dot inside a dot-separated phrase (`Foo.the`). Fix: pattern 4 is now gated on the head to the left of the dot resolving to a recordShape name declared somewhere in the tree. `collectRecordShapeNames` walks the TAC interfaces alongside the existing `collectRecordShapeFields` and the resulting shape-name set is threaded through `extractThenFieldTokens` as a second argument. Three belt-and-braces guards stay in behind the shape-name gate so a hostile shape-name collision cannot reintroduce the class: an English-stop-word skip on the field token (so `Foo.the` is silent even if a shape named `Foo` were declared), a closed file-extension skip (md, json, jsonl, html, css, js, mjs, cjs, ts, vue, yaml, yml, txt, csv, pdf, png, jpg, svg, zip, wav, mp4, mov, log) and a two-char lookbehind path-context skip (preceded by `/` or `./`). Patterns 1 (narrowed backticked identifier), 2 (`<name> field`), 3 (`field: <name>`), the issue 311 fixtures and the AC-12113-2 class are preserved. Regression fixtures in `test/query/gates.test.js` cover each of the three new classes plus a positive control (`Session.lastSeenAt` where Session is declared without `lastSeenAt`) and a verbatim quote of the four ai-on-record ACs at head 974b4c1 as negative controls. Expected drop on WESPA: 26 unsatisfiable findings removed; the issue 311 drop and all AC-12113-2 class failures remain.
|
|
16
|
+
|
|
17
|
+
## [0.32.2] - 2026-10-06
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
25
|
+
- **`--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.
|
|
26
|
+
|
|
9
27
|
## [0.32.1] - 2026-10-06
|
|
10
28
|
|
|
11
29
|
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.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"registerCanary": [
|
|
3
3
|
{
|
|
4
|
-
"id": "rc-2026-10-
|
|
5
|
-
"createdAt": "2026-10-
|
|
6
|
-
"buildVersion": "0.32.
|
|
4
|
+
"id": "rc-2026-10-07-001",
|
|
5
|
+
"createdAt": "2026-10-07T10:44:10.684Z",
|
|
6
|
+
"buildVersion": "0.32.3-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-
|
|
38
|
-
"createdAt": "2026-10-
|
|
39
|
-
"buildVersion": "0.32.
|
|
37
|
+
"id": "rc-2026-10-07-002",
|
|
38
|
+
"createdAt": "2026-10-07T10:44:10.684Z",
|
|
39
|
+
"buildVersion": "0.32.3-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-
|
|
71
|
-
"createdAt": "2026-10-
|
|
72
|
-
"buildVersion": "0.32.
|
|
70
|
+
"id": "rc-2026-10-07-003",
|
|
71
|
+
"createdAt": "2026-10-07T10:44:10.684Z",
|
|
72
|
+
"buildVersion": "0.32.3-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.
|
|
3
|
+
"version": "0.32.3",
|
|
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",
|
package/releases/releases.yaml
CHANGED
|
@@ -40,8 +40,24 @@
|
|
|
40
40
|
# `npm install rcf-lite`.
|
|
41
41
|
|
|
42
42
|
feedVersion: 1
|
|
43
|
-
latest: "0.32.
|
|
43
|
+
latest: "0.32.3"
|
|
44
44
|
releases:
|
|
45
|
+
- version: "0.32.3"
|
|
46
|
+
date: "2026-10-07"
|
|
47
|
+
breaking: false
|
|
48
|
+
headlines:
|
|
49
|
+
- "rcf-lite 0.32.3 is a hotfix for the 0.32.2 D6 consistency:unsatisfiable regression on filename stems and non-shape dot phrases (issue 315). Pattern 4 of extractThenFieldTokens now emits a field token only when the head to the left of the dot is a declared recordShape name in the tree, so AC then-clauses that mention report.json, CLAUDE.md, scripts/seeds/README.md, Math.random, TEST_USERS.admin, ToolExecutionContext.pushLlmCall and sentence-ending dots like Foo.the stop reading as record-field references; file-extension, path-context and English-stop-word skips stay as belt and braces. The 0.32.1 and 0.32.2 fixes carry forward unchanged."
|
|
50
|
+
minAgentAction: null
|
|
51
|
+
notesUrl: "https://stravica.ai/docs/rcf/changelog/"
|
|
52
|
+
|
|
53
|
+
- version: "0.32.2"
|
|
54
|
+
date: "2026-10-06"
|
|
55
|
+
breaking: false
|
|
56
|
+
headlines:
|
|
57
|
+
- "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)."
|
|
58
|
+
minAgentAction: null
|
|
59
|
+
notesUrl: "https://stravica.ai/docs/rcf/changelog/"
|
|
60
|
+
|
|
45
61
|
- version: "0.32.1"
|
|
46
62
|
date: "2026-10-06"
|
|
47
63
|
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`);
|
package/src/cli/req-baseline.js
CHANGED
|
@@ -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,
|
package/src/query/gates.js
CHANGED
|
@@ -1823,6 +1823,7 @@ export function checkD6Consistency(ctx) {
|
|
|
1823
1823
|
// `[draft]` recordShape counts as defined (the engineer owns the
|
|
1824
1824
|
// draft; its `fields:` list is honest about what the then may name).
|
|
1825
1825
|
const definedFields = collectRecordShapeFields(tree);
|
|
1826
|
+
const definedShapes = collectRecordShapeNames(tree);
|
|
1826
1827
|
/** @type {Array<{ id: string, why: string }>} */
|
|
1827
1828
|
const unsatisfiableFindings = [];
|
|
1828
1829
|
let totalThenFieldChecks = 0;
|
|
@@ -1831,7 +1832,7 @@ export function checkD6Consistency(ctx) {
|
|
|
1831
1832
|
for (const ac of acs) {
|
|
1832
1833
|
const parts = parseWhenThen(ac?.description);
|
|
1833
1834
|
if (!parts) continue;
|
|
1834
|
-
const fields = extractThenFieldTokens(parts.then);
|
|
1835
|
+
const fields = extractThenFieldTokens(parts.then, definedShapes);
|
|
1835
1836
|
for (const field of fields) {
|
|
1836
1837
|
totalThenFieldChecks += 1;
|
|
1837
1838
|
if (definedFields.has(field)) continue;
|
|
@@ -2028,44 +2029,152 @@ function collectRecordShapeFields(tree) {
|
|
|
2028
2029
|
return out;
|
|
2029
2030
|
}
|
|
2030
2031
|
|
|
2032
|
+
/**
|
|
2033
|
+
* Collect every recordShape interface name declared anywhere in the
|
|
2034
|
+
* tree. Pattern 4 of `extractThenFieldTokens` (dot-notation
|
|
2035
|
+
* `<Head>.<field>`) uses this set to gate: a Head that is not a
|
|
2036
|
+
* declared shape name is not read as a record-field reference. This
|
|
2037
|
+
* closes the 0.32.2 class in issue 315 where filename stems, non-shape
|
|
2038
|
+
* PascalCase heads (Math.random, TEST_USERS.admin,
|
|
2039
|
+
* ToolExecutionContext.pushLlmCall) and sentence-ending dots (Foo.the)
|
|
2040
|
+
* were being read as record-field references.
|
|
2041
|
+
*
|
|
2042
|
+
* @param {import('#core/store/walker.js').TreeModel} tree
|
|
2043
|
+
* @returns {Set<string>}
|
|
2044
|
+
*/
|
|
2045
|
+
function collectRecordShapeNames(tree) {
|
|
2046
|
+
const out = new Set();
|
|
2047
|
+
for (const tac of tree.tacs ?? []) {
|
|
2048
|
+
for (const iface of tac.interfaces ?? []) {
|
|
2049
|
+
if (iface?.kind !== 'recordShape') continue;
|
|
2050
|
+
const name = iface?.name;
|
|
2051
|
+
if (typeof name !== 'string' || name.length === 0) continue;
|
|
2052
|
+
out.add(name);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return out;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// Closed list of file-extension tokens; pattern 4 (dot notation) never
|
|
2059
|
+
// emits a field token drawn from this set. Belt and braces behind the
|
|
2060
|
+
// shape-name gate: a tree with a recordShape literally named `Json` or
|
|
2061
|
+
// `Md` would still not read `foo.json`/`CLAUDE.md` as a field.
|
|
2062
|
+
const PATTERN_4_FILE_EXTENSION_SKIP = new Set([
|
|
2063
|
+
'md', 'json', 'jsonl', 'html', 'css', 'js', 'mjs', 'cjs', 'ts', 'vue',
|
|
2064
|
+
'yaml', 'yml', 'txt', 'csv', 'pdf', 'png', 'jpg', 'svg', 'zip', 'wav',
|
|
2065
|
+
'mp4', 'mov', 'log',
|
|
2066
|
+
]);
|
|
2067
|
+
|
|
2031
2068
|
/**
|
|
2032
2069
|
* Pull field tokens out of a `then` clause. A field token is a word
|
|
2033
2070
|
* that follows a verb like `set`, `write`, `include`, `return`,
|
|
2034
2071
|
* `contain`, `name` or appears as the direct object of a `then`'s
|
|
2035
2072
|
* verb phrase; a cheap approximation uses `then <subject> <verb>
|
|
2036
|
-
* <field>` plus `field <name>`, `<name> field
|
|
2037
|
-
* patterns. The scan is deliberately conservative so
|
|
2038
|
-
* sentence yields a small set.
|
|
2073
|
+
* <field>` plus `field <name>`, `<name> field`, `field: <name>` and
|
|
2074
|
+
* `<Shape>.<name>` patterns. The scan is deliberately conservative so
|
|
2075
|
+
* a plain English sentence yields a small set.
|
|
2076
|
+
*
|
|
2077
|
+
* Issue 311 (0.32.2): a bare backticked identifier (pattern 1) was
|
|
2078
|
+
* reading error-enum values, pagination query-string keys, sort and
|
|
2079
|
+
* filter-mode literals, env-var and config-knob names as record-shape
|
|
2080
|
+
* fields and false-positiving 345 of 532 ACs on WESPA. Pattern 1 is
|
|
2081
|
+
* now context-gated: ALL_CAPS tokens (enum values, env vars,
|
|
2082
|
+
* constants) are skipped unconditionally; other backticked tokens
|
|
2083
|
+
* count only when a field-signalling word sits within a short
|
|
2084
|
+
* neighbourhood AND no explicit non-field context (query parameter,
|
|
2085
|
+
* sort order, filter/match mode, env var, config knob, header name,
|
|
2086
|
+
* cli flag) overlaps the same neighbourhood. The AC-12113-2 class is
|
|
2087
|
+
* preserved: a then-clause that names a real missing field via
|
|
2088
|
+
* `<name> field`, `field: <name>` or an emit-verb context still fails
|
|
2089
|
+
* the check.
|
|
2039
2090
|
*
|
|
2040
2091
|
* @param {string} thenClause
|
|
2092
|
+
* @param {Set<string>} [shapeNames] - recordShape names declared in
|
|
2093
|
+
* the tree. Pattern 4 (`<Head>.<field>` dot notation) emits a field
|
|
2094
|
+
* token only when the head is a member of this set (issue 315).
|
|
2041
2095
|
* @returns {string[]}
|
|
2042
2096
|
*/
|
|
2043
|
-
function extractThenFieldTokens(thenClause) {
|
|
2097
|
+
function extractThenFieldTokens(thenClause, shapeNames = new Set()) {
|
|
2044
2098
|
if (typeof thenClause !== 'string' || thenClause.length === 0) return [];
|
|
2045
2099
|
const text = stripQuotedSubstrings(thenClause);
|
|
2046
2100
|
/** @type {Set<string>} */
|
|
2047
2101
|
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
2102
|
const STOP = new Set(['the', 'a', 'an', 'this', 'that', 'its', 'our', 'their', 'any', 'no', 'some', 'each', 'every', 'another']);
|
|
2058
|
-
|
|
2103
|
+
|
|
2104
|
+
// Pattern 2 (preserved): `<name> field` (adjective-form naming). The
|
|
2105
|
+
// backtick around the name is optional; the stop-word skip keeps
|
|
2106
|
+
// "the field", "a field" and "its field" from tripping the check.
|
|
2107
|
+
for (const m of text.matchAll(/`?\b([A-Za-z_][A-Za-z0-9_]*)`?\s+field\b/g)) {
|
|
2059
2108
|
const token = m[1];
|
|
2060
2109
|
if (!token) continue;
|
|
2061
2110
|
if (STOP.has(token.toLowerCase())) continue;
|
|
2062
2111
|
tokens.add(token);
|
|
2063
2112
|
}
|
|
2064
|
-
// Pattern 3: `field: <name>` or `field = <name>`
|
|
2065
|
-
// notation inside a then clause).
|
|
2066
|
-
|
|
2113
|
+
// Pattern 3 (preserved): `field: <name>` or `field = <name>`
|
|
2114
|
+
// (schema-like notation inside a then clause). The name's backtick
|
|
2115
|
+
// is optional.
|
|
2116
|
+
for (const m of text.matchAll(/\bfields?\s*[:=]\s*`?([A-Za-z_][A-Za-z0-9_]*)`?/g)) {
|
|
2067
2117
|
if (m[1]) tokens.add(m[1]);
|
|
2068
2118
|
}
|
|
2119
|
+
// Pattern 4 (narrowed, issue 315): `<ShapeName>.<fieldName>` dot
|
|
2120
|
+
// notation. Shape names are PascalCase. 0.32.2 emitted a field
|
|
2121
|
+
// token whenever a PascalCase-ish head sat next to a lowercase dot
|
|
2122
|
+
// tail, which read filename stems (`CLAUDE.md`, `*.json`,
|
|
2123
|
+
// `report.json`), non-shape PascalCase heads (`Math.random`,
|
|
2124
|
+
// `ToolExecutionContext.pushLlmCall`), bare-letter version tags
|
|
2125
|
+
// (`corpus-V.json`) and sentence-ending dots inside a dotted
|
|
2126
|
+
// phrase (`Foo.the`) as record fields. Narrowing: emit ONLY when
|
|
2127
|
+
// the head resolves to a recordShape name declared somewhere in
|
|
2128
|
+
// the tree. Three belt-and-braces guards remain regardless of the
|
|
2129
|
+
// shape-name set so a hostile shape-name collision cannot
|
|
2130
|
+
// reintroduce the class: the field is not an English stop word
|
|
2131
|
+
// (sentence-dot), not a known file extension (filename stems),
|
|
2132
|
+
// and the match does not sit inside a path context (preceded by
|
|
2133
|
+
// `/` or `./`, which also catches backticked paths like
|
|
2134
|
+
// `rcf/evals/corpus-V.json`).
|
|
2135
|
+
for (const m of text.matchAll(/\b([A-Z][A-Za-z0-9_]*)\.([a-z_][A-Za-z0-9_]*)\b/g)) {
|
|
2136
|
+
const head = m[1];
|
|
2137
|
+
const field = m[2];
|
|
2138
|
+
if (!head || !field) continue;
|
|
2139
|
+
if (!shapeNames.has(head)) continue;
|
|
2140
|
+
if (STOP.has(field.toLowerCase())) continue;
|
|
2141
|
+
if (PATTERN_4_FILE_EXTENSION_SKIP.has(field.toLowerCase())) continue;
|
|
2142
|
+
const start = m.index ?? 0;
|
|
2143
|
+
const beforeHead = text.slice(Math.max(0, start - 2), start);
|
|
2144
|
+
if (/[\/]/.test(beforeHead)) continue;
|
|
2145
|
+
tokens.add(field);
|
|
2146
|
+
}
|
|
2147
|
+
// Pattern 1 (narrowed, issue 311): backticked identifier WITH
|
|
2148
|
+
// explicit field evidence in its immediate neighbourhood AND no
|
|
2149
|
+
// overlapping non-field context. ALL_CAPS tokens (enum values, env
|
|
2150
|
+
// vars, constants) are skipped unconditionally; record-shape field
|
|
2151
|
+
// names in the WSD AI tree and the rcf-schemas canon are camelCase
|
|
2152
|
+
// or lower snake_case.
|
|
2153
|
+
const WINDOW = 36;
|
|
2154
|
+
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;
|
|
2155
|
+
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;
|
|
2156
|
+
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;
|
|
2157
|
+
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;
|
|
2158
|
+
|
|
2159
|
+
for (const m of text.matchAll(/`([A-Za-z_][A-Za-z0-9_]*)`/g)) {
|
|
2160
|
+
const token = m[1];
|
|
2161
|
+
if (!token) continue;
|
|
2162
|
+
// Skip ALL_CAPS enum values, env vars, constants.
|
|
2163
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(token) && token.length >= 2) continue;
|
|
2164
|
+
const start = m.index ?? 0;
|
|
2165
|
+
const end = start + m[0].length;
|
|
2166
|
+
const before = text.slice(Math.max(0, start - WINDOW), start);
|
|
2167
|
+
const after = text.slice(end, Math.min(text.length, end + WINDOW));
|
|
2168
|
+
if (NEG_CTX.test(before) || NEG_CTX.test(after)) continue;
|
|
2169
|
+
if (
|
|
2170
|
+
POS_BEFORE.test(before)
|
|
2171
|
+
|| POS_AFTER.test(after)
|
|
2172
|
+
|| POS_CTX.test(before)
|
|
2173
|
+
|| POS_CTX.test(after)
|
|
2174
|
+
) {
|
|
2175
|
+
tokens.add(token);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2069
2178
|
return [...tokens];
|
|
2070
2179
|
}
|
|
2071
2180
|
|