rcf-lite 0.28.4 → 0.29.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 +107 -1
- package/bin/rcf.js +4 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/define-draft-shapes.md +89 -0
- package/guidance/define-intake.md +97 -0
- package/guidance/define-intent.md +107 -0
- package/guidance/discover-prototype.md +67 -0
- package/guidance/discovery-prototypes.md +69 -0
- package/guidance/harness-template.md +34 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +34 -0
- package/guidance/manifest.json +9 -2
- package/package.json +1 -1
- package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
- package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
- package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
- package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
- package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
- package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
- package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
- package/rcf/adrs/adr-4134-index-json-route.json +35 -0
- package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
- package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
- package/rcf/code-nodes/cn-725.json +18 -2
- package/rcf/code-nodes/cn-726.json +10 -2
- package/rcf/code-nodes/cn-727.json +13 -4
- package/rcf/code-nodes/cn-728.json +18 -2
- package/rcf/code-nodes/cn-729.json +10 -2
- package/rcf/code-nodes/cn-730.json +13 -3
- package/rcf/code-nodes/cn-731.json +20 -0
- package/rcf/code-nodes/cn-732.json +14 -0
- package/rcf/code-nodes/cn-733.json +14 -0
- package/rcf/code-nodes/cn-734.json +14 -0
- package/rcf/code-nodes/cn-735.json +20 -0
- package/rcf/code-nodes/cn-736.json +14 -0
- package/rcf/code-nodes/cn-737.json +14 -0
- package/rcf/code-nodes/cn-738.json +14 -0
- package/rcf/code-nodes/cn-739.json +18 -0
- package/rcf/code-nodes/cn-740.json +19 -0
- package/rcf/code-nodes/cn-741.json +16 -0
- package/rcf/code-nodes/cn-742.json +16 -0
- package/rcf/code-nodes/cn-743.json +15 -0
- package/rcf/code-nodes/cn-744.json +22 -0
- package/rcf/code-nodes/cn-745.json +16 -0
- package/rcf/code-nodes/cn-746.json +14 -0
- package/rcf/code-nodes/cn-747.json +18 -0
- package/rcf/code-nodes/cn-748.json +16 -0
- package/rcf/code-nodes/cn-749.json +16 -0
- package/rcf/code-nodes/cn-750.json +15 -0
- package/rcf/code-nodes/cn-751.json +18 -0
- package/rcf/code-nodes/cn-752.json +14 -0
- package/rcf/fbs/fbs-196.json +13 -5
- package/rcf/fbs/fbs-198.json +42 -0
- package/rcf/fbs/fbs-199.json +59 -0
- package/rcf/fbs/fbs-200.json +46 -0
- package/rcf/fbs/fbs-201.json +49 -0
- package/rcf/fbs/fbs-202.json +110 -0
- package/rcf/fbs/fbs-203.json +100 -0
- package/rcf/requirements/req-002.json +2 -2
- package/rcf/requirements/req-173.json +2 -2
- package/rcf/requirements/req-174.json +10 -4
- package/rcf/requirements/req-175.json +10 -4
- package/rcf/requirements/req-176.json +25 -0
- package/rcf/requirements/req-180.json +31 -0
- package/rcf/requirements/req-181.json +31 -0
- package/rcf/requirements/req-182.json +25 -0
- package/rcf/requirements/req-186.json +25 -0
- package/rcf/requirements/req-187.json +31 -0
- package/rcf/requirements/req-188.json +25 -0
- package/rcf/tacs/tac-4121-define-ledgers.json +15 -3
- package/rcf/tacs/tac-4122-define-gates.json +40 -4
- package/rcf/tacs/tac-4123-define-readiness.json +39 -3
- package/rcf/tacs/tac-4124-define-freeze.json +28 -0
- package/rcf/tacs/tac-4127-readiness-view.json +49 -0
- package/rcf/tacs/tac-4128-define-guidance.json +53 -0
- package/rcf/tacs/tac-4129-define-questions.json +45 -0
- package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
- package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
- package/rcf/tacs/tac-4132-id-lookup.json +42 -0
- package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
- package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
- package/rcf/test-suites/ts-016.json +3 -3
- package/rcf/test-suites/ts-215.json +148 -4
- package/rcf/test-suites/ts-216.json +19 -3
- package/rcf/test-suites/ts-218.json +78 -0
- package/rcf/test-suites/ts-219.json +70 -0
- package/rcf/test-suites/ts-220.json +78 -0
- package/rcf/test-suites/ts-221.json +78 -0
- package/rcf/test-suites/ts-222.json +100 -0
- package/rcf/test-suites/ts-223.json +94 -0
- package/rcf/test-suites/ts-224.json +70 -0
- package/rcf/test-suites/ts-225.json +78 -0
- package/rcf/test-suites/ts-226.json +158 -0
- package/rcf/test-suites/ts-227.json +38 -0
- package/rcf/test-suites/ts-228.json +86 -0
- package/rcf/test-suites/ts-229.json +83 -0
- package/rcf/user-stories/us-17302.json +50 -0
- package/rcf/user-stories/us-17401.json +14 -2
- package/rcf/user-stories/us-17402.json +55 -0
- package/rcf/user-stories/us-17403.json +60 -0
- package/rcf/user-stories/us-17502.json +60 -0
- package/rcf/user-stories/us-17503.json +65 -0
- package/rcf/user-stories/us-17601.json +58 -0
- package/rcf/user-stories/us-18001.json +55 -0
- package/rcf/user-stories/us-18002.json +35 -0
- package/rcf/user-stories/us-18101.json +35 -0
- package/rcf/user-stories/us-18202.json +40 -0
- package/rcf/user-stories/us-18601.json +55 -0
- package/rcf/user-stories/us-18602.json +50 -0
- package/rcf/user-stories/us-18603.json +45 -0
- package/rcf/user-stories/us-18701.json +50 -0
- package/rcf/user-stories/us-18702.json +50 -0
- package/rcf/user-stories/us-18801.json +45 -0
- package/rcf/user-stories/us-204.json +59 -0
- package/rcf/user-stories/us-205.json +64 -0
- package/rcf/user-stories/us-206.json +54 -0
- package/releases/releases.yaml +11 -1
- package/src/cli/freeze.js +730 -0
- package/src/cli/help.js +12 -0
- package/src/cli/ledger.js +228 -12
- package/src/cli/questions.js +288 -0
- package/src/cli/readiness.js +267 -49
- package/src/cli/view.js +29 -0
- package/src/define/freeze-record.js +30 -15
- package/src/define/ledgers.js +210 -11
- package/src/intake/index.js +1 -1
- package/src/intake/orchestrator.js +117 -0
- package/src/mcp/tools.js +354 -0
- package/src/query/delta.js +6 -0
- package/src/query/gates.js +314 -39
- package/src/query/index.js +16 -2
- package/src/query/questions.js +438 -0
- package/src/query/readiness.js +248 -7
- package/src/server/index.js +24 -2
- package/src/server/routes.js +68 -1
- package/src/setup/identity-seed.js +17 -0
- package/src/view/build-dag.js +563 -0
- package/src/view/components/badge.js +41 -0
- package/src/view/components/diff.js +72 -0
- package/src/view/components/doc-row.js +62 -0
- package/src/view/components/empty-state.js +35 -0
- package/src/view/components/entity-selector.js +137 -0
- package/src/view/components/filter-bar.js +97 -0
- package/src/view/components/findings-list.js +49 -0
- package/src/view/components/index.js +12 -0
- package/src/view/components/pill.js +46 -0
- package/src/view/components/sub-tab-strip.js +43 -0
- package/src/view/components/toast.js +25 -0
- package/src/view/components-fixture.js +265 -0
- package/src/view/doc-renderers/architecture.js +304 -0
- package/src/view/doc-renderers/build.js +160 -0
- package/src/view/doc-renderers/index.js +2 -0
- package/src/view/doc-renderers/prd.js +35 -6
- package/src/view/html-page.js +700 -602
- package/src/view/index.js +96 -3
- package/src/view/live-client.js +39 -6
- package/src/view/needs-work.js +79 -0
- package/src/view/page-init.js +2193 -0
- package/src/view/readiness/phrasebook.js +115 -0
- package/src/view/readiness/po-layer.js +288 -0
- package/src/view/readiness/question-adapter.js +329 -0
- package/src/view/readiness/stage-legend.js +71 -0
- package/src/view/readiness.js +505 -0
- package/src/view/style.css +2017 -382
- package/src/view/test-host/embed-client.js +121 -0
- package/src/view/test-host/fixture.js +183 -0
- package/src/view/test-host/lints.js +220 -0
- package/src/view/view-index.js +323 -0
package/src/define/ledgers.js
CHANGED
|
@@ -189,6 +189,11 @@ function validateEntry(name, entry, filePath, where = 'entry') {
|
|
|
189
189
|
filePath, field: `${where}.source`,
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
|
+
if (e.resolvedBy !== undefined && typeof e.resolvedBy !== 'string') {
|
|
193
|
+
throw new LedgerError(`brief ${where}.resolvedBy must be a string when present.`, {
|
|
194
|
+
filePath, field: `${where}.resolvedBy`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
192
197
|
} else if (name === 'decisions') {
|
|
193
198
|
if (typeof e.question !== 'string' || e.question.length === 0) {
|
|
194
199
|
throw new LedgerError(`decisions ${where}.question must be non-empty.`, {
|
|
@@ -443,37 +448,231 @@ export function resolveEntry({ name, body, id, patch = {}, now }) {
|
|
|
443
448
|
return { body: nextBody, entry: updated };
|
|
444
449
|
}
|
|
445
450
|
|
|
451
|
+
const RESOLVED_BY_GRAMMAR_RE = /^(REQ-\d+|TAD\.entity:.+|PRD\.user:.+|TAD\.system:.+|TAC-\d+(?:-[A-Za-z0-9-]+)?|omitted:.+)$/;
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* True when `pointer` matches the closed grammar for a `resolvedBy`
|
|
455
|
+
* value. Shape-only; does NOT verify that the pointed-to id or name
|
|
456
|
+
* resolves against a tree (that is D2's `skeleton:resolvedBy` check).
|
|
457
|
+
*
|
|
458
|
+
* @param {string} pointer
|
|
459
|
+
* @returns {boolean}
|
|
460
|
+
*/
|
|
461
|
+
export function isResolvedByGrammar(pointer) {
|
|
462
|
+
if (typeof pointer !== 'string') return false;
|
|
463
|
+
return RESOLVED_BY_GRAMMAR_RE.test(pointer.trim());
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const KIND_MARKER_RE = /^\[([^\]]+)\]\s*/;
|
|
467
|
+
const SOURCE_MARKER_RE = /\s*\(source:\s*([^)]+)\)\s*$/;
|
|
468
|
+
const QUESTION_PREFIX_RE = /^(TBC|TBD|Open|Question)\b[:.]?\s*/i;
|
|
469
|
+
|
|
446
470
|
/**
|
|
447
471
|
* Parse `--from <file>` content into brief-statement drafts: one entry
|
|
448
|
-
* per non-empty line
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
472
|
+
* per non-empty line. Each line optionally carries a leading `[kind]`
|
|
473
|
+
* marker and a trailing `(source: ...)` marker. A line without
|
|
474
|
+
* `[kind]` takes `opts.kind` (default `capability`); a line without
|
|
475
|
+
* `(source: ...)` takes `opts.source` or no source. Heuristics kind a
|
|
476
|
+
* line `openQuestion` when it ends with `?` or begins with `TBC`,
|
|
477
|
+
* `TBD`, `Open:` or `Question:`, unless a `[kind]` marker says
|
|
478
|
+
* otherwise. Unknown `[kind]` values raise `LedgerError` (code
|
|
479
|
+
* `usage`) before any write.
|
|
452
480
|
*
|
|
453
481
|
* @param {string} content
|
|
454
482
|
* @param {object} [opts]
|
|
455
|
-
* @param {string} [opts.kind] default kind for every
|
|
456
|
-
* @param {string} [opts.source]
|
|
483
|
+
* @param {string} [opts.kind] default kind for every unmarked line
|
|
484
|
+
* @param {string} [opts.source] default source for every unmarked line
|
|
457
485
|
* @returns {Array<{ text: string, kind: string, source?: string }>}
|
|
458
486
|
*/
|
|
459
487
|
export function parseBriefFromFile(content, opts = {}) {
|
|
460
|
-
const
|
|
488
|
+
const defaultKind = opts.kind ?? 'capability';
|
|
461
489
|
/** @type {Array<{ text: string, kind: string, source?: string }>} */
|
|
462
490
|
const drafts = [];
|
|
463
491
|
for (const raw of content.split(/\r?\n/)) {
|
|
464
492
|
const trimmed = raw.trim();
|
|
465
493
|
if (!trimmed) continue;
|
|
466
|
-
|
|
494
|
+
let line = trimmed
|
|
467
495
|
.replace(/^[-*]\s+/, '')
|
|
468
496
|
.replace(/^\d+[.)]\s+/, '');
|
|
469
|
-
if (!
|
|
470
|
-
|
|
471
|
-
|
|
497
|
+
if (!line) continue;
|
|
498
|
+
|
|
499
|
+
// Trailing (source: ...) marker.
|
|
500
|
+
let source = opts.source;
|
|
501
|
+
const srcMatch = line.match(SOURCE_MARKER_RE);
|
|
502
|
+
if (srcMatch) {
|
|
503
|
+
source = srcMatch[1].trim();
|
|
504
|
+
line = line.slice(0, srcMatch.index).trimEnd();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Leading [kind] marker.
|
|
508
|
+
let kind = null;
|
|
509
|
+
const kindMatch = line.match(KIND_MARKER_RE);
|
|
510
|
+
if (kindMatch) {
|
|
511
|
+
kind = kindMatch[1].trim();
|
|
512
|
+
line = line.slice(kindMatch[0].length).trim();
|
|
513
|
+
if (!BRIEF_KINDS.includes(/** @type {any} */ (kind))) {
|
|
514
|
+
throw new LedgerError(
|
|
515
|
+
`brief --from: unknown kind '${kind}' (expected one of: ${BRIEF_KINDS.join(', ')})`,
|
|
516
|
+
{ field: 'kind', code: 'usage' },
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (!line) continue;
|
|
522
|
+
|
|
523
|
+
// Heuristics for openQuestion when no [kind] marker was given.
|
|
524
|
+
if (!kind) {
|
|
525
|
+
if (line.endsWith('?') || QUESTION_PREFIX_RE.test(line)) {
|
|
526
|
+
kind = 'openQuestion';
|
|
527
|
+
} else {
|
|
528
|
+
kind = defaultKind;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const entry = { text: line, kind };
|
|
533
|
+
if (source) entry.source = source;
|
|
472
534
|
drafts.push(entry);
|
|
473
535
|
}
|
|
474
536
|
return drafts;
|
|
475
537
|
}
|
|
476
538
|
|
|
539
|
+
/**
|
|
540
|
+
* Parse a `resolvedBy` pointer against the walker tree. The six
|
|
541
|
+
* accepted forms are `REQ-nnn`, `TAD.entity:<name>`, `PRD.user:<name>`,
|
|
542
|
+
* `TAD.system:<name>`, `TAC-nnnn`, and `omitted:<reason>`. Resolution
|
|
543
|
+
* checks the pointed-to id or name actually exists in the tree
|
|
544
|
+
* (except for `omitted:`, which always passes once the reason is
|
|
545
|
+
* non-empty).
|
|
546
|
+
*
|
|
547
|
+
* @param {string} pointer
|
|
548
|
+
* @param {object} tree walker tree (TreeModel-like)
|
|
549
|
+
* @returns {{ ok: true, kind: string, target: string } | { ok: false, why: string }}
|
|
550
|
+
*/
|
|
551
|
+
export function parseResolvedBy(pointer, tree) {
|
|
552
|
+
if (typeof pointer !== 'string' || pointer.length === 0) {
|
|
553
|
+
return { ok: false, why: 'pointer must be a non-empty string' };
|
|
554
|
+
}
|
|
555
|
+
const p = pointer.trim();
|
|
556
|
+
|
|
557
|
+
// omitted:<reason>
|
|
558
|
+
const omittedMatch = p.match(/^omitted:(.+)$/);
|
|
559
|
+
if (omittedMatch) {
|
|
560
|
+
const reason = omittedMatch[1].trim();
|
|
561
|
+
if (!reason) return { ok: false, why: 'omitted: pointer needs a reason' };
|
|
562
|
+
return { ok: true, kind: 'omitted', target: reason };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// REQ-nnn
|
|
566
|
+
if (/^REQ-\d+$/.test(p)) {
|
|
567
|
+
const found = (tree?.requirements ?? []).some((r) => r?.reqId === p);
|
|
568
|
+
if (!found) return { ok: false, why: 'pointer does not resolve' };
|
|
569
|
+
return { ok: true, kind: 'req', target: p };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// TAC-nnnn (short form, resolves to a tree TAC whose id starts
|
|
573
|
+
// with that prefix) or TAC-nnnn-<slug> (exact-match long form).
|
|
574
|
+
// Codex review 2026-10-02: an unanchored /^TAC-\d+/ let arbitrary
|
|
575
|
+
// suffixes (TAC-4130-made-up) pass as long as the numeric prefix
|
|
576
|
+
// matched an existing TAC; anchor the regex and gate the prefix
|
|
577
|
+
// fallback to the short form only.
|
|
578
|
+
const tacMatch = p.match(/^TAC-\d+(?:-[A-Za-z0-9-]+)?$/);
|
|
579
|
+
if (tacMatch) {
|
|
580
|
+
const tacs = tree?.tacs ?? [];
|
|
581
|
+
const isShortForm = /^TAC-\d+$/.test(p);
|
|
582
|
+
const found = tacs.some((t) => {
|
|
583
|
+
if (!t?.tacId) return false;
|
|
584
|
+
if (t.tacId === p) return true;
|
|
585
|
+
if (!isShortForm) return false;
|
|
586
|
+
return t.tacId.split('-').slice(0, 2).join('-') === p;
|
|
587
|
+
});
|
|
588
|
+
if (!found) return { ok: false, why: 'pointer does not resolve' };
|
|
589
|
+
return { ok: true, kind: 'tac', target: p };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// TAD.entity:<name>
|
|
593
|
+
const entityMatch = p.match(/^TAD\.entity:(.+)$/);
|
|
594
|
+
if (entityMatch) {
|
|
595
|
+
const name = entityMatch[1].trim();
|
|
596
|
+
if (!name) return { ok: false, why: 'TAD.entity: pointer needs a name' };
|
|
597
|
+
const entities = tree?.tad?.dataArchitecture?.coreEntities ?? [];
|
|
598
|
+
const found = Array.isArray(entities)
|
|
599
|
+
&& entities.some((e) => (typeof e === 'string' ? e : e?.name) === name);
|
|
600
|
+
if (!found) return { ok: false, why: 'pointer does not resolve' };
|
|
601
|
+
return { ok: true, kind: 'tadEntity', target: name };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// PRD.user:<name>
|
|
605
|
+
const userMatch = p.match(/^PRD\.user:(.+)$/);
|
|
606
|
+
if (userMatch) {
|
|
607
|
+
const name = userMatch[1].trim();
|
|
608
|
+
if (!name) return { ok: false, why: 'PRD.user: pointer needs a name' };
|
|
609
|
+
// Codex review 2026-10-02: the production PRD schema uses
|
|
610
|
+
// `targetUsers`; keep `users` as a fallback for the test
|
|
611
|
+
// fixtures but prefer the real path.
|
|
612
|
+
const users = tree?.prd?.targetUsers ?? tree?.prd?.users ?? [];
|
|
613
|
+
const found = Array.isArray(users)
|
|
614
|
+
&& users.some((u) => (typeof u === 'string' ? u : u?.name) === name);
|
|
615
|
+
if (!found) return { ok: false, why: 'pointer does not resolve' };
|
|
616
|
+
return { ok: true, kind: 'prdUser', target: name };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// TAD.system:<name>
|
|
620
|
+
const systemMatch = p.match(/^TAD\.system:(.+)$/);
|
|
621
|
+
if (systemMatch) {
|
|
622
|
+
const name = systemMatch[1].trim();
|
|
623
|
+
if (!name) return { ok: false, why: 'TAD.system: pointer needs a name' };
|
|
624
|
+
// Codex review 2026-10-02: the production TAD schema puts
|
|
625
|
+
// externalSystems inside integrationArchitecture; keep the
|
|
626
|
+
// top-level path as a fallback for the test fixtures.
|
|
627
|
+
const systems = tree?.tad?.integrationArchitecture?.externalSystems ?? tree?.tad?.externalSystems ?? [];
|
|
628
|
+
const found = Array.isArray(systems)
|
|
629
|
+
&& systems.some((s) => (typeof s === 'string' ? s : s?.name) === name);
|
|
630
|
+
if (!found) return { ok: false, why: 'pointer does not resolve' };
|
|
631
|
+
return { ok: true, kind: 'tadSystem', target: name };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return { ok: false, why: 'pointer outside grammar (REQ-nnn, TAD.entity:<name>, PRD.user:<name>, TAD.system:<name>, TAC-nnnn, omitted:<reason>)' };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Patch a ledger entry in place. Field patches honour `add`-side
|
|
639
|
+
* validation by re-running `validateLedger` on the resulting body.
|
|
640
|
+
* Throws `LedgerError` (code `usage`) when the id is unknown. The
|
|
641
|
+
* entry's `id` and `addedAt` are never touched; `status`, `addedAt`
|
|
642
|
+
* and `resolvedAt` are passed through from the current entry unless
|
|
643
|
+
* explicitly patched.
|
|
644
|
+
*
|
|
645
|
+
* @param {object} args
|
|
646
|
+
* @param {LedgerName} args.name
|
|
647
|
+
* @param {Record<string, unknown[]>} args.body
|
|
648
|
+
* @param {number} args.id
|
|
649
|
+
* @param {Record<string, unknown>} args.patch
|
|
650
|
+
* @returns {{ body: Record<string, unknown[]>, entry: Record<string, unknown> }}
|
|
651
|
+
*/
|
|
652
|
+
export function updateEntry({ name, body, id, patch }) {
|
|
653
|
+
const cfg = LEDGER_CONFIG[name];
|
|
654
|
+
if (!cfg) throw new LedgerError(`Unknown ledger '${name}'.`, { code: 'usage' });
|
|
655
|
+
const list = /** @type {any[]} */ (body[cfg.arrayKey] ?? []);
|
|
656
|
+
const idx = list.findIndex((e) => Number(e?.id) === Number(id));
|
|
657
|
+
if (idx < 0) {
|
|
658
|
+
throw new LedgerError(`${name} ledger has no entry with id ${id}.`, {
|
|
659
|
+
filePath: ledgerRelPath(name), field: `id ${id}`, code: 'usage',
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
const current = list[idx];
|
|
663
|
+
const next = { ...current };
|
|
664
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
665
|
+
if (value === undefined) continue;
|
|
666
|
+
if (key === 'id' || key === 'addedAt') continue;
|
|
667
|
+
next[key] = value;
|
|
668
|
+
}
|
|
669
|
+
const nextList = [...list];
|
|
670
|
+
nextList[idx] = next;
|
|
671
|
+
const nextBody = { ...body, [cfg.arrayKey]: nextList };
|
|
672
|
+
validateLedger(name, nextBody, ledgerRelPath(name));
|
|
673
|
+
return { body: nextBody, entry: next };
|
|
674
|
+
}
|
|
675
|
+
|
|
477
676
|
/**
|
|
478
677
|
* Load every ledger the project has actually authored, keyed by name.
|
|
479
678
|
*
|
package/src/intake/index.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
export { classifyFidelity, FIDELITY_LEVELS } from './fidelity.js';
|
|
4
4
|
export { scanArtefactForFindings } from './validate.js';
|
|
5
|
-
export { runIntakePhases } from './orchestrator.js';
|
|
5
|
+
export { runIntakePhases, runIntakeScansOnDelta } from './orchestrator.js';
|
|
6
6
|
export { writeIntakeRecord, composeIntakeRecord, nextIntakeId } from './manifest-writer.js';
|
|
@@ -18,6 +18,123 @@ import { classifyFidelity } from './fidelity.js';
|
|
|
18
18
|
import { scanArtefactForFindings } from './validate.js';
|
|
19
19
|
import { composeIntakeRecord } from './manifest-writer.js';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Run the mechanical intake scans over newly minted brief statements
|
|
23
|
+
* against every pre-existing brief statement. The scans operate on
|
|
24
|
+
* the raw text of the statements; a finding is attributed to the
|
|
25
|
+
* ids of every statement whose text the scan matched (an
|
|
26
|
+
* `impliedButNotStated` cue in one new statement with the matching
|
|
27
|
+
* negated-login phrase elsewhere attributes to both). On a frozen
|
|
28
|
+
* tree the caller passes every pre-existing statement as `existing`;
|
|
29
|
+
* the comparison set is `existing` by definition.
|
|
30
|
+
*
|
|
31
|
+
* Each returned finding carries:
|
|
32
|
+
* { kind, detail, scanName, ids: number[] }
|
|
33
|
+
* where `kind` is one of the three mechanical kinds from
|
|
34
|
+
* `scanArtefactForFindings`, `ids` lists the involved statement ids
|
|
35
|
+
* from `newStatements` first and then `existing` (duplicates
|
|
36
|
+
* removed), and `scanName` is `kind` for the DEFINE PR 1 wiring.
|
|
37
|
+
*
|
|
38
|
+
* @param {Array<{ id: number, text?: string }>} newStatements
|
|
39
|
+
* @param {Array<{ id: number, text?: string }>} existing
|
|
40
|
+
* @returns {Array<{ kind: string, detail: string, scanName: string, ids: number[] }>}
|
|
41
|
+
*/
|
|
42
|
+
export function runIntakeScansOnDelta(newStatements, existing) {
|
|
43
|
+
const safeNew = Array.isArray(newStatements) ? newStatements : [];
|
|
44
|
+
const safeExisting = Array.isArray(existing) ? existing : [];
|
|
45
|
+
if (safeNew.length === 0) return [];
|
|
46
|
+
|
|
47
|
+
const combined = [...safeNew, ...safeExisting];
|
|
48
|
+
const combinedText = combined.map((s) => (typeof s?.text === 'string' ? s.text : '')).join('\n');
|
|
49
|
+
const rawFindings = scanArtefactForFindings(combinedText);
|
|
50
|
+
if (rawFindings.length === 0) return [];
|
|
51
|
+
|
|
52
|
+
// A finding only counts when at least one of the newStatements
|
|
53
|
+
// participates. We attribute by re-running the scan over the
|
|
54
|
+
// `newStatements` alone (if that yields the same kind of finding,
|
|
55
|
+
// the new side is sufficient) OR by running the scan over any
|
|
56
|
+
// (new, existing) pair that contains at least one new statement:
|
|
57
|
+
// the second case detects a contradiction between a new and an
|
|
58
|
+
// existing statement.
|
|
59
|
+
/** @type {Array<{ kind: string, detail: string, scanName: string, ids: number[] }>} */
|
|
60
|
+
const out = [];
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
for (const f of rawFindings) {
|
|
63
|
+
const key = `${f.kind}::${f.detail}`;
|
|
64
|
+
if (seen.has(key)) continue;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
const ids = attributeFinding(f, safeNew, safeExisting);
|
|
67
|
+
if (ids.length === 0) continue;
|
|
68
|
+
out.push({ kind: f.kind, detail: f.detail, scanName: f.kind, ids });
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {{ kind: string, detail: string }} finding
|
|
75
|
+
* @param {Array<{ id: number, text?: string }>} newStatements
|
|
76
|
+
* @param {Array<{ id: number, text?: string }>} existing
|
|
77
|
+
* @returns {number[]}
|
|
78
|
+
*/
|
|
79
|
+
function attributeFinding(finding, newStatements, existing) {
|
|
80
|
+
/** @type {number[]} */
|
|
81
|
+
const ids = [];
|
|
82
|
+
for (const s of newStatements) {
|
|
83
|
+
if (statementParticipates(finding, s, [...newStatements, ...existing])) {
|
|
84
|
+
ids.push(Number(s.id));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (ids.length === 0) return ids;
|
|
88
|
+
// Also attribute an existing statement that participates on the other side.
|
|
89
|
+
for (const s of existing) {
|
|
90
|
+
if (statementParticipates(finding, s, [...newStatements, ...existing])) {
|
|
91
|
+
if (!ids.includes(Number(s.id))) ids.push(Number(s.id));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return ids;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Decide whether `statement.text` contributes to `finding`. The scan
|
|
99
|
+
* is pattern-based and finds the same finding kind on the text in
|
|
100
|
+
* isolation if the statement itself carries the full pattern; for
|
|
101
|
+
* cross-statement findings (a contradiction between two statements)
|
|
102
|
+
* the statement contributes when it carries at least one half of the
|
|
103
|
+
* pattern and the other half appears somewhere in the combined set.
|
|
104
|
+
*
|
|
105
|
+
* @param {{ kind: string, detail: string }} finding
|
|
106
|
+
* @param {{ id: number, text?: string }} statement
|
|
107
|
+
* @param {Array<{ id: number, text?: string }>} combined
|
|
108
|
+
*/
|
|
109
|
+
function statementParticipates(finding, statement, combined) {
|
|
110
|
+
const text = typeof statement?.text === 'string' ? statement.text : '';
|
|
111
|
+
if (!text) return false;
|
|
112
|
+
const soloFindings = scanArtefactForFindings(text).map((f) => f.kind);
|
|
113
|
+
if (soloFindings.includes(finding.kind)) return true;
|
|
114
|
+
// Cross-statement: pair this statement's text with every other
|
|
115
|
+
// combined text and see whether the pair raises the same finding.
|
|
116
|
+
for (const other of combined) {
|
|
117
|
+
if (other === statement) continue;
|
|
118
|
+
const otherText = typeof other?.text === 'string' ? other.text : '';
|
|
119
|
+
if (!otherText) continue;
|
|
120
|
+
const pairFindings = scanArtefactForFindings(`${text}\n${otherText}`).map((f) => f.kind);
|
|
121
|
+
// Codex review 2026-10-02: a pre-existing finding already
|
|
122
|
+
// raised by `other` alone must not be re-attributed to an
|
|
123
|
+
// unrelated `statement` merely because concatenation leaves
|
|
124
|
+
// the pattern intact. Require the pair to raise the finding
|
|
125
|
+
// that neither side raises alone.
|
|
126
|
+
const otherSolo = scanArtefactForFindings(otherText).map((f) => f.kind);
|
|
127
|
+
if (
|
|
128
|
+
pairFindings.includes(finding.kind)
|
|
129
|
+
&& !soloFindings.includes(finding.kind)
|
|
130
|
+
&& !otherSolo.includes(finding.kind)
|
|
131
|
+
) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
21
138
|
/**
|
|
22
139
|
* @typedef {'napkin'|'productBrief'|'prd'|'tad'|'other'} ArtefactKind
|
|
23
140
|
*/
|