rcf-lite 0.28.1 → 0.28.4
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 +310 -0
- package/bin/rcf.js +4 -0
- package/fixtures/canary-manifest.json +9 -9
- package/package.json +1 -1
- package/rcf/adrs/adr-4120-canonical-json-hashing.json +30 -0
- package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
- package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
- package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
- package/rcf/code-nodes/cn-602.json +3 -2
- package/rcf/code-nodes/cn-700.json +23 -0
- package/rcf/code-nodes/cn-701.json +14 -0
- package/rcf/code-nodes/cn-702.json +14 -0
- package/rcf/code-nodes/cn-703.json +16 -0
- package/rcf/code-nodes/cn-720.json +15 -0
- package/rcf/code-nodes/cn-721.json +15 -0
- package/rcf/code-nodes/cn-722.json +15 -0
- package/rcf/code-nodes/cn-723.json +18 -0
- package/rcf/code-nodes/cn-724.json +14 -0
- package/rcf/code-nodes/cn-725.json +21 -0
- package/rcf/code-nodes/cn-726.json +17 -0
- package/rcf/code-nodes/cn-727.json +14 -0
- package/rcf/code-nodes/cn-728.json +21 -0
- package/rcf/code-nodes/cn-729.json +18 -0
- package/rcf/code-nodes/cn-730.json +14 -0
- package/rcf/fbs/fbs-181.json +3 -2
- package/rcf/fbs/fbs-190.json +37 -0
- package/rcf/fbs/fbs-191.json +34 -0
- package/rcf/fbs/fbs-192.json +17 -0
- package/rcf/fbs/fbs-193.json +37 -0
- package/rcf/fbs/fbs-194.json +35 -0
- package/rcf/fbs/fbs-195.json +37 -0
- package/rcf/fbs/fbs-196.json +37 -0
- package/rcf/fbs/fbs-197.json +41 -0
- package/rcf/requirements/req-001.json +2 -1
- package/rcf/requirements/req-002.json +2 -1
- package/rcf/requirements/req-003.json +2 -1
- package/rcf/requirements/req-004.json +2 -1
- package/rcf/requirements/req-005.json +2 -1
- package/rcf/requirements/req-006.json +2 -1
- package/rcf/requirements/req-007.json +2 -1
- package/rcf/requirements/req-008.json +2 -1
- package/rcf/requirements/req-009.json +2 -1
- package/rcf/requirements/req-010.json +5 -2
- package/rcf/requirements/req-011.json +2 -1
- package/rcf/requirements/req-012.json +2 -1
- package/rcf/requirements/req-013.json +2 -1
- package/rcf/requirements/req-014.json +2 -1
- package/rcf/requirements/req-015.json +2 -1
- package/rcf/requirements/req-016.json +5 -2
- package/rcf/requirements/req-017.json +6 -2
- package/rcf/requirements/req-018.json +5 -2
- package/rcf/requirements/req-019.json +5 -2
- package/rcf/requirements/req-020.json +5 -2
- package/rcf/requirements/req-021.json +5 -2
- package/rcf/requirements/req-022.json +5 -2
- package/rcf/requirements/req-023.json +6 -4
- package/rcf/requirements/req-024.json +5 -2
- package/rcf/requirements/req-025.json +5 -2
- package/rcf/requirements/req-026.json +5 -2
- package/rcf/requirements/req-027.json +5 -2
- package/rcf/requirements/req-028.json +5 -2
- package/rcf/requirements/req-029.json +5 -2
- package/rcf/requirements/req-030.json +5 -2
- package/rcf/requirements/req-040.json +5 -2
- package/rcf/requirements/req-041.json +5 -2
- package/rcf/requirements/req-042.json +5 -2
- package/rcf/requirements/req-043.json +5 -2
- package/rcf/requirements/req-050.json +5 -2
- package/rcf/requirements/req-051.json +5 -2
- package/rcf/requirements/req-052.json +5 -2
- package/rcf/requirements/req-053.json +5 -2
- package/rcf/requirements/req-054.json +5 -2
- package/rcf/requirements/req-060.json +5 -2
- package/rcf/requirements/req-061.json +5 -2
- package/rcf/requirements/req-062.json +5 -2
- package/rcf/requirements/req-063.json +5 -2
- package/rcf/requirements/req-070.json +5 -2
- package/rcf/requirements/req-071.json +5 -2
- package/rcf/requirements/req-072.json +5 -2
- package/rcf/requirements/req-073.json +5 -2
- package/rcf/requirements/req-074.json +5 -2
- package/rcf/requirements/req-075.json +5 -2
- package/rcf/requirements/req-076.json +5 -2
- package/rcf/requirements/req-077.json +5 -2
- package/rcf/requirements/req-080.json +5 -2
- package/rcf/requirements/req-081.json +5 -2
- package/rcf/requirements/req-082.json +5 -2
- package/rcf/requirements/req-083.json +5 -2
- package/rcf/requirements/req-084.json +5 -2
- package/rcf/requirements/req-085.json +5 -2
- package/rcf/requirements/req-086.json +5 -2
- package/rcf/requirements/req-090.json +5 -2
- package/rcf/requirements/req-091.json +5 -2
- package/rcf/requirements/req-092.json +5 -2
- package/rcf/requirements/req-093.json +5 -2
- package/rcf/requirements/req-094.json +5 -2
- package/rcf/requirements/req-100.json +5 -2
- package/rcf/requirements/req-101.json +5 -2
- package/rcf/requirements/req-102.json +5 -2
- package/rcf/requirements/req-103.json +5 -2
- package/rcf/requirements/req-104.json +5 -2
- package/rcf/requirements/req-110.json +5 -2
- package/rcf/requirements/req-111.json +5 -2
- package/rcf/requirements/req-112.json +5 -2
- package/rcf/requirements/req-113.json +5 -2
- package/rcf/requirements/req-114.json +5 -2
- package/rcf/requirements/req-115.json +5 -2
- package/rcf/requirements/req-120.json +5 -2
- package/rcf/requirements/req-121.json +5 -2
- package/rcf/requirements/req-122.json +5 -2
- package/rcf/requirements/req-123.json +5 -2
- package/rcf/requirements/req-124.json +5 -2
- package/rcf/requirements/req-125.json +5 -2
- package/rcf/requirements/req-130.json +6 -2
- package/rcf/requirements/req-131.json +6 -2
- package/rcf/requirements/req-132.json +6 -2
- package/rcf/requirements/req-133.json +6 -2
- package/rcf/requirements/req-134.json +6 -2
- package/rcf/requirements/req-140.json +5 -2
- package/rcf/requirements/req-145.json +5 -2
- package/rcf/requirements/req-150.json +6 -2
- package/rcf/requirements/req-151.json +6 -2
- package/rcf/requirements/req-152.json +6 -2
- package/rcf/requirements/req-153.json +6 -2
- package/rcf/requirements/req-154.json +5 -2
- package/rcf/requirements/req-155.json +4 -1
- package/rcf/requirements/req-156.json +4 -1
- package/rcf/requirements/req-157.json +4 -1
- package/rcf/requirements/req-158.json +4 -1
- package/rcf/requirements/req-159.json +4 -1
- package/rcf/requirements/req-160.json +4 -1
- package/rcf/requirements/req-161.json +4 -1
- package/rcf/requirements/req-162.json +4 -1
- package/rcf/requirements/req-163.json +5 -2
- package/rcf/requirements/req-164.json +5 -2
- package/rcf/requirements/req-165.json +5 -2
- package/rcf/requirements/req-170.json +33 -0
- package/rcf/requirements/req-171.json +44 -0
- package/rcf/requirements/req-172.json +25 -0
- package/rcf/requirements/req-173.json +25 -0
- package/rcf/requirements/req-174.json +25 -0
- package/rcf/requirements/req-175.json +25 -0
- package/rcf/tacs/tac-4103-feedback-redact.json +2 -2
- package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
- package/rcf/tacs/tac-4121-define-ledgers.json +23 -0
- package/rcf/tacs/tac-4122-define-gates.json +25 -0
- package/rcf/tacs/tac-4123-define-readiness.json +25 -0
- package/rcf/test-suites/ts-188.json +10 -2
- package/rcf/test-suites/ts-191.json +19 -5
- package/rcf/test-suites/ts-204.json +30 -0
- package/rcf/test-suites/ts-205.json +22 -0
- package/rcf/test-suites/ts-206.json +22 -0
- package/rcf/test-suites/ts-207.json +22 -0
- package/rcf/test-suites/ts-208.json +22 -0
- package/rcf/test-suites/ts-209.json +22 -0
- package/rcf/test-suites/ts-210.json +22 -0
- package/rcf/test-suites/ts-211.json +22 -0
- package/rcf/test-suites/ts-212.json +22 -0
- package/rcf/test-suites/ts-213.json +50 -0
- package/rcf/test-suites/ts-214.json +74 -0
- package/rcf/test-suites/ts-215.json +67 -0
- package/rcf/test-suites/ts-216.json +78 -0
- package/rcf/test-suites/ts-217.json +62 -0
- package/rcf/user-stories/us-15601.json +10 -1
- package/rcf/user-stories/us-15701.json +4 -4
- package/rcf/user-stories/us-17001.json +27 -0
- package/rcf/user-stories/us-17002.json +22 -0
- package/rcf/user-stories/us-17003.json +22 -0
- package/rcf/user-stories/us-17004.json +22 -0
- package/rcf/user-stories/us-17005.json +22 -0
- package/rcf/user-stories/us-17006.json +21 -0
- package/rcf/user-stories/us-17007.json +21 -0
- package/rcf/user-stories/us-17008.json +22 -0
- package/rcf/user-stories/us-17101.json +22 -0
- package/rcf/user-stories/us-17102.json +22 -0
- package/rcf/user-stories/us-17201.json +38 -0
- package/rcf/user-stories/us-17301.json +38 -0
- package/rcf/user-stories/us-17401.json +58 -0
- package/rcf/user-stories/us-17501.json +48 -0
- package/releases/releases.yaml +31 -1
- package/src/browser-verify/manifest-writer.js +9 -5
- package/src/cli/browser-verify.js +9 -1
- package/src/cli/feedback.js +316 -32
- package/src/cli/help.js +11 -0
- package/src/cli/intake.js +25 -4
- package/src/cli/ledger.js +399 -0
- package/src/cli/preflight.js +24 -3
- package/src/cli/readiness.js +280 -0
- package/src/cli/review.js +9 -1
- package/src/cli/ui-baseline.js +14 -1
- package/src/core/store/index.js +5 -0
- package/src/core/store/validate-composed-record.js +95 -0
- package/src/core/store/writer.js +86 -6
- package/src/define/freeze-record.js +232 -0
- package/src/define/ledgers.js +513 -0
- package/src/feedback/gh.js +27 -1
- package/src/feedback/redact-allowlist.json +7 -0
- package/src/feedback/redact.js +126 -11
- package/src/feedback/render.js +34 -12
- package/src/intake/manifest-writer.js +6 -3
- package/src/intake/orchestrator.js +44 -7
- package/src/preflight/manifest-writer.js +11 -11
- package/src/query/delta.js +276 -0
- package/src/query/gates.js +832 -0
- package/src/query/index.js +36 -0
- package/src/query/readiness.js +333 -0
- package/src/req-baseline/sweep.js +34 -5
- package/src/server/index.js +37 -0
- package/src/server/routes.js +65 -3
- package/src/ui-baseline/manifest-writer.js +11 -10
- package/src/view/doc-renderers/helpers.js +39 -2
- package/src/view/doc-renderers/req.js +10 -2
- package/src/view/doc-renderers/tad.js +67 -6
- package/src/view/doc-renderers/user-story.js +10 -5
- package/src/view/html-page.js +347 -28
- package/src/view/index.js +14 -1
- package/src/view/live-client.js +22 -2
- package/src/view/product-map.js +751 -0
- package/src/view/style.css +383 -0
package/src/cli/feedback.js
CHANGED
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
emitSessionStart,
|
|
55
55
|
shouldAsk,
|
|
56
56
|
} from '../feedback/hook.js';
|
|
57
|
-
import { redact, allowedHosts, findResidualSecrets } from '../feedback/redact.js';
|
|
57
|
+
import { redact, allowedHosts, findResidualSecrets, RESERVED_IDENTITY_TOKENS } from '../feedback/redact.js';
|
|
58
58
|
import { fingerprint } from '../feedback/fingerprint.js';
|
|
59
59
|
import { renderIssue, renderComment, renderBundle } from '../feedback/render.js';
|
|
60
60
|
import { resolve as resolveDestination, listUnresolvedLibraries, findBlueprintRecord } from '../feedback/destination.js';
|
|
@@ -477,17 +477,58 @@ async function handleAdd(argv, ctx) {
|
|
|
477
477
|
}
|
|
478
478
|
|
|
479
479
|
/**
|
|
480
|
-
* Classify an evidence pointer into `{ kind, value }`.
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
480
|
+
* Classify an evidence pointer into `{ kind, value }`. The help text
|
|
481
|
+
* promises three shapes: command / path:line / id. The classifier
|
|
482
|
+
* distinguishes them by cheap shape rules:
|
|
483
|
+
*
|
|
484
|
+
* - `id`: a canonical uppercase-prefix RCF artefact id (`AC-15501-1`,
|
|
485
|
+
* `REQ-155`, `FBS-181`, `US-15701`).
|
|
486
|
+
* - `file`: `path:line` (a non-space token ending in `:<digits>`) OR
|
|
487
|
+
* a bare filesystem path shape - a slash-bearing, non-whitespace
|
|
488
|
+
* token that ends in a known file extension. This is the shape a
|
|
489
|
+
* `--evidence rcf/.blueprint-libraries/wsd/1.0.0/library.json`
|
|
490
|
+
* argument carries (issue #245).
|
|
491
|
+
* - `command`: anything else. This is the fallback shape for a bare
|
|
492
|
+
* verb invocation like `rcf feedback preview` or `pnpm --filter
|
|
493
|
+
* rcf-lite test`.
|
|
494
|
+
*
|
|
495
|
+
* Classification is best-effort and never gates the write; the value
|
|
496
|
+
* itself is always preserved verbatim. The rendered preview label
|
|
497
|
+
* uses this kind so the operator sees which shape the pointer carries
|
|
498
|
+
* without having to re-run it.
|
|
484
499
|
*
|
|
485
500
|
* @param {string} value
|
|
486
501
|
* @returns {{ kind: 'file' | 'id' | 'command', value: string }}
|
|
487
502
|
*/
|
|
503
|
+
// 0.28.3 (issue #245): extension shortlist used by `classifyEvidence`
|
|
504
|
+
// to recognise bare file-path evidence. Kept in lockstep with the
|
|
505
|
+
// redactor's default extension allowlist so operators see the same
|
|
506
|
+
// treatment on both sides. Duplicated deliberately (not imported) to
|
|
507
|
+
// keep the CLI light on cross-module reads for a per-call classifier.
|
|
508
|
+
const EVIDENCE_FILE_EXTENSIONS = new Set([
|
|
509
|
+
'md', 'mdx', 'json', 'yml', 'yaml', 'txt',
|
|
510
|
+
'js', 'mjs', 'cjs', 'ts', 'tsx',
|
|
511
|
+
'html', 'css', 'png', 'svg', 'sh', 'csv', 'log', 'lock',
|
|
512
|
+
]);
|
|
513
|
+
|
|
488
514
|
export function classifyEvidence(value) {
|
|
515
|
+
if (typeof value !== 'string' || value.length === 0) return { kind: 'command', value };
|
|
516
|
+
// Canonical RCF id shape: uppercase prefix, then digits (e.g. AC-15501-1).
|
|
489
517
|
if (/^[A-Z]+-\d[\w-]*$/.test(value)) return { kind: 'id', value };
|
|
490
|
-
|
|
518
|
+
// path:line - a non-whitespace token ending in ":<digits>".
|
|
519
|
+
if (/^[^\s]+:\d+$/.test(value)) return { kind: 'file', value };
|
|
520
|
+
// Bare filesystem path or filename: no whitespace, ends in a known
|
|
521
|
+
// extension (issue #245). Whitespace anywhere disqualifies the
|
|
522
|
+
// candidate; that is almost certainly a shell command. A bare
|
|
523
|
+
// filename without a slash still counts because a --evidence
|
|
524
|
+
// pointer to README.md or CHANGELOG.md is common.
|
|
525
|
+
if (!/\s/.test(value)) {
|
|
526
|
+
const lastDot = value.lastIndexOf('.');
|
|
527
|
+
if (lastDot > 0 && lastDot < value.length - 1) {
|
|
528
|
+
const ext = value.slice(lastDot + 1).toLowerCase();
|
|
529
|
+
if (EVIDENCE_FILE_EXTENSIONS.has(ext)) return { kind: 'file', value };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
491
532
|
return { kind: 'command', value };
|
|
492
533
|
}
|
|
493
534
|
|
|
@@ -624,6 +665,14 @@ async function handleStatus(argv, ctx) {
|
|
|
624
665
|
visibility: dest.visibility ?? 'unresolved',
|
|
625
666
|
derived: !!dest.derived,
|
|
626
667
|
reason: dest.reason ?? null,
|
|
668
|
+
// Issue #244 (0.28.3): the preview surface prints
|
|
669
|
+
// `source: <library-manifest|package-bugs-url|derived|...>` so
|
|
670
|
+
// the operator can see WHICH resolver path picked the destination.
|
|
671
|
+
// The --json surface must expose the same field for programmatic
|
|
672
|
+
// verifiers per AC-15502-2 (--json emits the same as text). The
|
|
673
|
+
// field is null when the resolver did not attach one (e.g. an
|
|
674
|
+
// unresolved library).
|
|
675
|
+
source: dest.source ?? null,
|
|
627
676
|
});
|
|
628
677
|
}
|
|
629
678
|
|
|
@@ -672,6 +721,12 @@ async function handleStatus(argv, ctx) {
|
|
|
672
721
|
? `${row.repo} (${row.visibility}${row.derived ? ', derived' : ''})`
|
|
673
722
|
: `(unresolved${row.reason ? `: ${row.reason}` : ''})`;
|
|
674
723
|
stdout.write(` - ${row.ref} -> ${cell}\n`);
|
|
724
|
+
// Issue #244 (0.28.3): print the resolver source so text and
|
|
725
|
+
// --json carry the same disclosure. Preview already prints this;
|
|
726
|
+
// status now matches per AC-15502-2 parity.
|
|
727
|
+
if (row.source) {
|
|
728
|
+
stdout.write(` source: ${row.source}\n`);
|
|
729
|
+
}
|
|
675
730
|
}
|
|
676
731
|
}
|
|
677
732
|
return 0;
|
|
@@ -1003,15 +1058,76 @@ async function handlePreview(argv, ctx) {
|
|
|
1003
1058
|
*/
|
|
1004
1059
|
async function buildRedactionContext(projectRoot) {
|
|
1005
1060
|
let operatorName;
|
|
1061
|
+
// 0.28.3 (R5g, issue #246): track why identity folding might be
|
|
1062
|
+
// disabled so we can emit exactly one visible stderr warning
|
|
1063
|
+
// instead of leaving the operator to guess. `disabledReason` is
|
|
1064
|
+
// one of 'no-profile' (rcf/.identity/profile.md is missing or
|
|
1065
|
+
// unreadable), 'no-name-line' (present but no `## Name` heading),
|
|
1066
|
+
// 'blank-name' (whitespace-only), 'placeholder' (matches the
|
|
1067
|
+
// placeholder/todo regex), 'stopword-only' (Name contains only
|
|
1068
|
+
// reserved-noun tokens), or null when a foldable identity was
|
|
1069
|
+
// extracted.
|
|
1070
|
+
let disabledReason = 'no-profile';
|
|
1006
1071
|
try {
|
|
1007
1072
|
const profile = await readFile(resolve(projectRoot, 'rcf', '.identity', 'profile.md'), 'utf8');
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1073
|
+
// Codex P1-2: extract the Name-section body up to the next `## `
|
|
1074
|
+
// heading or EOF, using ONLY horizontal whitespace on the heading
|
|
1075
|
+
// line so an empty section (`## Name\n\n## Role\n...`) does not
|
|
1076
|
+
// greedy-match into the next section's heading. Bind the search
|
|
1077
|
+
// to the first `## Name` header only.
|
|
1078
|
+
// JS regex has no `\Z`; use a lookahead for the next `## ` header
|
|
1079
|
+
// OR end of string. `[\s\S]*?` is lazy so it stops at the first
|
|
1080
|
+
// matching lookahead.
|
|
1081
|
+
const nameSection = profile.match(/^##[ \t]+Name[ \t]*\n([\s\S]*?)(?=^##[ \t]+|$(?![\s\S]))/m);
|
|
1082
|
+
if (!nameSection) {
|
|
1083
|
+
disabledReason = 'no-name-line';
|
|
1084
|
+
} else {
|
|
1085
|
+
// The first non-empty non-comment line inside the Name section is
|
|
1086
|
+
// the operator's identity. Strip whitespace and leading/trailing
|
|
1087
|
+
// markdown emphasis so a shipped placeholder like
|
|
1088
|
+
// `_(who you are; how you want the agent to address you)_` is
|
|
1089
|
+
// seen as text, not folded as a name token.
|
|
1090
|
+
const nonEmpty = nameSection[1].split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
|
|
1091
|
+
const first = nonEmpty[0];
|
|
1092
|
+
if (!first) {
|
|
1093
|
+
disabledReason = 'blank-name';
|
|
1094
|
+
} else if (/^_\(.*\)_$/i.test(first) || /placeholder|todo|your name/i.test(first) || /^_.*(who you are|how you want).*_$/i.test(first)) {
|
|
1095
|
+
// Codex P1-3: recognise the shipped seed placeholder shape
|
|
1096
|
+
// `_(who you are; how you want the agent to address you)_`
|
|
1097
|
+
// as a placeholder so the untouched generated profile emits
|
|
1098
|
+
// the disabled-folding warning rather than folding the
|
|
1099
|
+
// placeholder text as a name.
|
|
1100
|
+
disabledReason = 'placeholder';
|
|
1101
|
+
} else {
|
|
1102
|
+
operatorName = first;
|
|
1103
|
+
const rawTokens = first.split(/\s+/);
|
|
1104
|
+
const foldable = rawTokens.filter((t) => t.length > 3 && !RESERVED_IDENTITY_TOKENS.has(t.toLowerCase()));
|
|
1105
|
+
if (foldable.length === 0) {
|
|
1106
|
+
disabledReason = 'stopword-only';
|
|
1107
|
+
} else {
|
|
1108
|
+
disabledReason = null;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1012
1111
|
}
|
|
1013
1112
|
} catch { /* absent is fine */ }
|
|
1014
1113
|
|
|
1114
|
+
// Emit the one-line warning once per context build. All disabled
|
|
1115
|
+
// reasons carry the same fix pointer so the operator knows how to
|
|
1116
|
+
// arm folding without having to read the source.
|
|
1117
|
+
if (disabledReason) {
|
|
1118
|
+
const path = 'rcf/.identity/profile.md';
|
|
1119
|
+
const detail = {
|
|
1120
|
+
'no-profile': `${path} is missing`,
|
|
1121
|
+
'no-name-line': `${path} has no \`## Name\` heading`,
|
|
1122
|
+
'blank-name': `${path} \`## Name\` line is empty`,
|
|
1123
|
+
'placeholder': `${path} \`## Name\` line is a placeholder`,
|
|
1124
|
+
'stopword-only': `${path} \`## Name\` line contains only reserved nouns`,
|
|
1125
|
+
}[disabledReason] ?? path;
|
|
1126
|
+
process.stderr.write(
|
|
1127
|
+
`[warn] operator-identity: ${detail}; identity folding is disabled. Add a real \`## Name\` line to arm the redactor.\n`
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1015
1131
|
let projectName;
|
|
1016
1132
|
const remotes = new Set();
|
|
1017
1133
|
try {
|
|
@@ -1033,11 +1149,21 @@ async function buildRedactionContext(projectRoot) {
|
|
|
1033
1149
|
} catch { /* no git or no remotes is fine */ }
|
|
1034
1150
|
|
|
1035
1151
|
let allowHostsExt = [];
|
|
1152
|
+
let allowExtensionsExt = [];
|
|
1036
1153
|
try {
|
|
1037
1154
|
const settings = JSON.parse(await readFile(resolve(projectRoot, 'rcf', 'feedback-settings.json'), 'utf8'));
|
|
1038
1155
|
if (Array.isArray(settings?.redaction?.allowHosts)) {
|
|
1039
1156
|
allowHostsExt = settings.redaction.allowHosts;
|
|
1040
1157
|
}
|
|
1158
|
+
// 0.28.2 (Codex review follow-up on #233): the operator's
|
|
1159
|
+
// per-project extension shortlist must reach the redactor the
|
|
1160
|
+
// same way `allowHosts` does. Previously the CLI dropped
|
|
1161
|
+
// `settings.redaction.allowExtensions` on the floor, so an
|
|
1162
|
+
// operator adding `xyz` to keep `file.xyz` intact saw the fold
|
|
1163
|
+
// fire in preview and submit.
|
|
1164
|
+
if (Array.isArray(settings?.redaction?.allowExtensions)) {
|
|
1165
|
+
allowExtensionsExt = settings.redaction.allowExtensions;
|
|
1166
|
+
}
|
|
1041
1167
|
} catch { /* absent is fine */ }
|
|
1042
1168
|
// Assemble the extended allow-list once so downstream callers can
|
|
1043
1169
|
// print it if they want to; the redactor itself accepts the raw
|
|
@@ -1059,6 +1185,7 @@ async function buildRedactionContext(projectRoot) {
|
|
|
1059
1185
|
operatorName,
|
|
1060
1186
|
gitRemote: [...remotes].filter((r) => typeof r === 'string' && r.length > 0),
|
|
1061
1187
|
allowHosts: allowHostsExt,
|
|
1188
|
+
allowExtensions: allowExtensionsExt,
|
|
1062
1189
|
};
|
|
1063
1190
|
}
|
|
1064
1191
|
|
|
@@ -1215,11 +1342,21 @@ const SUBMIT_OPTIONS = /** @type {const} */ ({
|
|
|
1215
1342
|
*
|
|
1216
1343
|
* Dedupe (per entry passing preflight): gh search issues on the
|
|
1217
1344
|
* destination for `rcf-feedback-fingerprint: <fp>` (open, in:body,
|
|
1218
|
-
* limit 5).
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
1345
|
+
* limit 5). Every returned candidate is then re-read via
|
|
1346
|
+
* ghIssueGetBody, and only candidates whose body carries
|
|
1347
|
+
* `rcf-feedback-fingerprint: <fp>` on its own line AND whose anchor /
|
|
1348
|
+
* kind / target markers do not disagree with the entry's are counted
|
|
1349
|
+
* as verified matches (0.28.2, issue #234, Barry ruling 2026-09-21).
|
|
1350
|
+
* A search hit that does not verify is a NON-MATCH and is dropped
|
|
1351
|
+
* before the fold decision. Zero verified hits -> create; one -> fold
|
|
1352
|
+
* comment on that number; many -> fold on the lowest and mention the
|
|
1353
|
+
* others. The fold comment carries the full report (title, body,
|
|
1354
|
+
* evidence, environment) prefaced with "Apparent duplicate of the
|
|
1355
|
+
* issue subject; posting the full report so a human can judge." The
|
|
1356
|
+
* retired "+1 from another reporter." payload is gone. Search
|
|
1357
|
+
* failure -> create with dedupe:unchecked. Closed search runs only
|
|
1358
|
+
* after a zero-hit open search so the create body can reference the
|
|
1359
|
+
* prior number.
|
|
1223
1360
|
*
|
|
1224
1361
|
* Create/comment failure (network/4xx/5xx/ratelimit or unexpected
|
|
1225
1362
|
* label rejection): retry once WITHOUT labels; still failing, bundle
|
|
@@ -1381,18 +1518,47 @@ async function handleSubmit(argv, ctx) {
|
|
|
1381
1518
|
let dedupe = 'new';
|
|
1382
1519
|
let closedRef = null;
|
|
1383
1520
|
let matches = [];
|
|
1521
|
+
let searchMatches = [];
|
|
1384
1522
|
if (!searchOpen.ok) {
|
|
1385
1523
|
dedupe = 'unchecked';
|
|
1386
1524
|
} else {
|
|
1387
|
-
|
|
1525
|
+
searchMatches = searchOpen.value?.matches ?? [];
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// 0.28.2 (issue #234, Barry ruling 2026-09-21): the search hit
|
|
1529
|
+
// alone is not a fold decision. Re-read each candidate body via
|
|
1530
|
+
// ghIssueGetBody and keep only those whose body carries
|
|
1531
|
+
// `rcf-feedback-fingerprint: <fp>` on its own line AND whose
|
|
1532
|
+
// anchor / kind / target markers do not disagree with the
|
|
1533
|
+
// entry's. A search hit whose body does not verify is a
|
|
1534
|
+
// NON-MATCH and is dropped before the fold decision.
|
|
1535
|
+
if (searchMatches.length > 0 && typeof gh.ghIssueGetBody === 'function') {
|
|
1536
|
+
const verified = [];
|
|
1537
|
+
for (const m of searchMatches) {
|
|
1538
|
+
const v = await gh.ghIssueGetBody({ repo: destination.repo, number: m.number });
|
|
1539
|
+
if (!v.ok || typeof v.value?.body !== 'string') continue;
|
|
1540
|
+
const body = v.value.body;
|
|
1541
|
+
if (!hasFingerprintLine(body, built.fingerprint)) continue;
|
|
1542
|
+
if (!candidateMatchesEntry(body, e)) continue;
|
|
1543
|
+
verified.push({ ...m, title: m.title ?? extractTitle(body) ?? null });
|
|
1544
|
+
}
|
|
1545
|
+
matches = verified;
|
|
1546
|
+
} else {
|
|
1547
|
+
matches = searchMatches;
|
|
1388
1548
|
}
|
|
1389
1549
|
|
|
1390
1550
|
if (flags['dry-run']) {
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1551
|
+
let plan;
|
|
1552
|
+
if (matches.length === 1) {
|
|
1553
|
+
const t = matches[0].title;
|
|
1554
|
+
plan = `commented on #${matches[0].number} (fingerprint match${t ? `: ${t}` : ''})`;
|
|
1555
|
+
} else if (matches.length > 1) {
|
|
1556
|
+
const sorted = matches.slice().sort((a, b) => a.number - b.number);
|
|
1557
|
+
const t = sorted[0]?.title;
|
|
1558
|
+
plan = `commented on #${sorted[0].number} (fingerprint match${t ? `: ${t}` : ''}, ${matches.length} verified)`;
|
|
1559
|
+
} else {
|
|
1560
|
+
plan = `created (title: ${built.rendered.title})`;
|
|
1561
|
+
}
|
|
1396
1562
|
stdout.write(`${e.id} -> dry-run ${plan} (${dedupe === 'unchecked' ? 'unchecked' : matches.length === 0 ? 'new' : 'comment'})\n`);
|
|
1397
1563
|
continue;
|
|
1398
1564
|
}
|
|
@@ -1425,26 +1591,28 @@ async function handleSubmit(argv, ctx) {
|
|
|
1425
1591
|
labels: usedLabels,
|
|
1426
1592
|
});
|
|
1427
1593
|
} else if (matches.length === 1) {
|
|
1428
|
-
// Comment
|
|
1594
|
+
// Verified fingerprint match: comment. Comment carries the
|
|
1595
|
+
// full report per Barry's 2026-09-21 ruling on #234.
|
|
1429
1596
|
dedupe = 'comment';
|
|
1430
|
-
const commentBody = renderComment(e, {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
}
|
|
1597
|
+
const commentBody = renderComment(e, built.redacted, {
|
|
1598
|
+
fingerprint: built.fingerprint,
|
|
1599
|
+
matchedTitle: matches[0].title ?? null,
|
|
1600
|
+
}).body;
|
|
1434
1601
|
result = await commentWithRetry(gh, {
|
|
1435
1602
|
repo: destination.repo,
|
|
1436
1603
|
number: matches[0].number,
|
|
1437
1604
|
body: commentBody,
|
|
1438
1605
|
});
|
|
1439
1606
|
} else {
|
|
1440
|
-
// Many: comment on lowest, mention the
|
|
1607
|
+
// Many verified matches: comment on lowest, mention the
|
|
1608
|
+
// others. The comment still carries the full report.
|
|
1441
1609
|
dedupe = 'comment';
|
|
1442
1610
|
const sorted = matches.slice().sort((a, b) => a.number - b.number);
|
|
1443
1611
|
const others = sorted.slice(1).map((m) => `#${m.number}`).join(', ');
|
|
1444
|
-
const base = renderComment(e, {
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
}
|
|
1612
|
+
const base = renderComment(e, built.redacted, {
|
|
1613
|
+
fingerprint: built.fingerprint,
|
|
1614
|
+
matchedTitle: sorted[0].title ?? null,
|
|
1615
|
+
}).body;
|
|
1448
1616
|
const commentBody = `${base}\nalso see ${others}\n`;
|
|
1449
1617
|
result = await commentWithRetry(gh, {
|
|
1450
1618
|
repo: destination.repo,
|
|
@@ -1463,7 +1631,23 @@ async function handleSubmit(argv, ctx) {
|
|
|
1463
1631
|
droppedLabels,
|
|
1464
1632
|
sessionId,
|
|
1465
1633
|
});
|
|
1466
|
-
|
|
1634
|
+
// 0.28.2 (issue #234): row shape names the outcome and the
|
|
1635
|
+
// matched issue's title so a wrong fold is visible on the
|
|
1636
|
+
// same line a human already sees. The create path prints the
|
|
1637
|
+
// title of the entry we just filed; the comment path prints
|
|
1638
|
+
// the matched candidate's title.
|
|
1639
|
+
const number = extractIssueNumberFromUrl(result.url);
|
|
1640
|
+
const numberSuffix = number ? `#${number}` : '';
|
|
1641
|
+
if (dedupe === 'comment') {
|
|
1642
|
+
const sorted = matches.slice().sort((a, b) => a.number - b.number);
|
|
1643
|
+
const matchedTitle = sorted[0]?.title ?? null;
|
|
1644
|
+
const foldTail = matchedTitle ? `: ${matchedTitle}` : '';
|
|
1645
|
+
stdout.write(`${e.id} -> commented on ${numberSuffix} (fingerprint match${foldTail}) ${result.url}\n`);
|
|
1646
|
+
} else if (dedupe === 'unchecked') {
|
|
1647
|
+
stdout.write(`${e.id} -> created ${numberSuffix} (${built.rendered.title}, dedupe unchecked) ${result.url}\n`);
|
|
1648
|
+
} else {
|
|
1649
|
+
stdout.write(`${e.id} -> created ${numberSuffix} (${built.rendered.title}) ${result.url}\n`);
|
|
1650
|
+
}
|
|
1467
1651
|
summary.submitted += 1;
|
|
1468
1652
|
} else {
|
|
1469
1653
|
// Retry-without-labels failed too: bundle this one entry.
|
|
@@ -1793,6 +1977,106 @@ function at(now) {
|
|
|
1793
1977
|
return now().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
1794
1978
|
}
|
|
1795
1979
|
|
|
1980
|
+
// -- 0.28.2 (issue #234) fold-decision helpers ----------------------------
|
|
1981
|
+
//
|
|
1982
|
+
// A search hit alone is not a fold: submit re-reads the candidate
|
|
1983
|
+
// body and folds only when the fingerprint line is literally present
|
|
1984
|
+
// on its own line AND the anchor / kind / target markers do not
|
|
1985
|
+
// contradict the entry's. This is the code side of Barry's ruling on
|
|
1986
|
+
// #234 (2026-09-21) and of the amended AC-15701-2.
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* True when `body` contains a line reading exactly
|
|
1990
|
+
* `rcf-feedback-fingerprint: <fp>` (case-sensitive, own line).
|
|
1991
|
+
*
|
|
1992
|
+
* @param {string} body
|
|
1993
|
+
* @param {string} fp
|
|
1994
|
+
* @returns {boolean}
|
|
1995
|
+
*/
|
|
1996
|
+
function hasFingerprintLine(body, fp) {
|
|
1997
|
+
if (typeof body !== 'string' || typeof fp !== 'string' || fp.length === 0) return false;
|
|
1998
|
+
const target = `rcf-feedback-fingerprint: ${fp}`;
|
|
1999
|
+
const lines = body.split(/\r?\n/);
|
|
2000
|
+
for (const l of lines) {
|
|
2001
|
+
if (l.trim() === target) return true;
|
|
2002
|
+
}
|
|
2003
|
+
return false;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/**
|
|
2007
|
+
* Defence-in-depth: with a truncated fingerprint (12 hex chars) a
|
|
2008
|
+
* collision is negligible but not zero. We refuse to fold when the
|
|
2009
|
+
* candidate body's environment-table markers for anchor, kind or the
|
|
2010
|
+
* blueprint slug disagree with the entry's own values.
|
|
2011
|
+
*
|
|
2012
|
+
* @param {string} body
|
|
2013
|
+
* @param {object} entry
|
|
2014
|
+
* @returns {boolean}
|
|
2015
|
+
*/
|
|
2016
|
+
function candidateMatchesEntry(body, entry) {
|
|
2017
|
+
if (typeof body !== 'string') return false;
|
|
2018
|
+
const marker = (label) => {
|
|
2019
|
+
const re = new RegExp(`\\|\\s*${label}\\s*\\|\\s*([^|\\n]+?)\\s*\\|`, 'i');
|
|
2020
|
+
const m = body.match(re);
|
|
2021
|
+
return m ? m[1].trim() : null;
|
|
2022
|
+
};
|
|
2023
|
+
const bodyAnchor = marker('anchor');
|
|
2024
|
+
const bodyKind = marker('kind');
|
|
2025
|
+
const bodyBlueprint = marker('blueprint');
|
|
2026
|
+
const bodyTarget = marker('target');
|
|
2027
|
+
const entryAnchor = (entry?.anchor ?? '-').trim();
|
|
2028
|
+
const entryKind = (entry?.kind ?? 'core').trim();
|
|
2029
|
+
const entryTargetRef = typeof entry?.target?.ref === 'string' ? entry.target.ref.trim() : '';
|
|
2030
|
+
const entryBlueprintSlug = entry?.kind === 'blueprint'
|
|
2031
|
+
? String(entry?.target?.effectiveSlug ?? entry?.target?.ref ?? '').trim()
|
|
2032
|
+
: null;
|
|
2033
|
+
if (bodyAnchor !== null && bodyAnchor.toUpperCase() !== entryAnchor.toUpperCase()) return false;
|
|
2034
|
+
if (bodyKind !== null && bodyKind.toLowerCase() !== entryKind.toLowerCase()) return false;
|
|
2035
|
+
if (entryBlueprintSlug && bodyBlueprint !== null && bodyBlueprint !== '-') {
|
|
2036
|
+
// The blueprint cell carries the slug as its first whitespace-
|
|
2037
|
+
// separated token, followed by an optional version and library
|
|
2038
|
+
// trail. Compare only the head.
|
|
2039
|
+
const head = bodyBlueprint.split(/\s+/)[0];
|
|
2040
|
+
if (head && head !== entryBlueprintSlug) return false;
|
|
2041
|
+
}
|
|
2042
|
+
// 0.28.2 (Codex review follow-up on #234): core entries also carry
|
|
2043
|
+
// a "target" line on the environment table. Refuse to fold across a
|
|
2044
|
+
// differing target (verb path) even when kind and anchor agree,
|
|
2045
|
+
// matching the amended AC-15701-2 anchor-and-target rule for both
|
|
2046
|
+
// kinds.
|
|
2047
|
+
if (entryKind.toLowerCase() === 'core' && entryTargetRef && bodyTarget !== null && bodyTarget !== '-') {
|
|
2048
|
+
if (bodyTarget !== entryTargetRef) return false;
|
|
2049
|
+
}
|
|
2050
|
+
return true;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/**
|
|
2054
|
+
* Pull the first `## Report: <title>` header from a candidate body,
|
|
2055
|
+
* used to name the matched issue on the stdout row when the search
|
|
2056
|
+
* response did not carry one.
|
|
2057
|
+
*
|
|
2058
|
+
* @param {string} body
|
|
2059
|
+
* @returns {string | null}
|
|
2060
|
+
*/
|
|
2061
|
+
function extractTitle(body) {
|
|
2062
|
+
if (typeof body !== 'string') return null;
|
|
2063
|
+
const m = body.match(/^##\s+Report:\s+(.+)$/m);
|
|
2064
|
+
return m ? m[1].trim() : null;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
/**
|
|
2068
|
+
* Extract the numeric issue id from a github.com issue URL. Used only
|
|
2069
|
+
* to shape the stdout row.
|
|
2070
|
+
*
|
|
2071
|
+
* @param {string | undefined} url
|
|
2072
|
+
* @returns {number | null}
|
|
2073
|
+
*/
|
|
2074
|
+
function extractIssueNumberFromUrl(url) {
|
|
2075
|
+
if (typeof url !== 'string') return null;
|
|
2076
|
+
const m = url.match(/\/issues\/(\d+)/);
|
|
2077
|
+
return m ? Number(m[1]) : null;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
1796
2080
|
// -- opt-in / opt-out (slice 5, FBS-184) ----------------------------------
|
|
1797
2081
|
|
|
1798
2082
|
const OPT_OPTIONS = /** @type {const} */ ({
|
package/src/cli/help.js
CHANGED
|
@@ -45,6 +45,8 @@ import { HELP as REQ_BASELINE_HELP } from './req-baseline.js';
|
|
|
45
45
|
import { HELP as INTAKE_HELP } from './intake.js';
|
|
46
46
|
import { HELP as BLUEPRINT_HELP } from './blueprint.js';
|
|
47
47
|
import { HELP as STANDARDS_HELP } from './standards.js';
|
|
48
|
+
import { HELP as LEDGER_HELP } from './ledger.js';
|
|
49
|
+
import { HELP as READINESS_HELP } from './readiness.js';
|
|
48
50
|
|
|
49
51
|
// Verify group members (from the verify-suite subtree).
|
|
50
52
|
import { HELP as VERIFY_RUN_HELP } from '../verify/cli/run.js';
|
|
@@ -93,6 +95,8 @@ define Author the RCF tree.
|
|
|
93
95
|
design <fbs-id> <verb> Design substage for a UI-bearing FBS.
|
|
94
96
|
blueprint <verb> Compose blueprints onto the project.
|
|
95
97
|
standards <verb> Register standards packs against the project.
|
|
98
|
+
ledger <name> <verb> Sidecar-ledger CRUD under rcf/define/.
|
|
99
|
+
readiness DEFINE stage-gate readiness view.
|
|
96
100
|
|
|
97
101
|
build Drive the FBS queue and the five-stage build cycle.
|
|
98
102
|
queue Show the FBS build queue.
|
|
@@ -189,6 +193,11 @@ Verbs:
|
|
|
189
193
|
design <fbs-id> <verb> Design substage for a UI-bearing FBS.
|
|
190
194
|
blueprint <verb> Compose blueprints onto the project.
|
|
191
195
|
standards <verb> Register standards packs against the project.
|
|
196
|
+
ledger <name> <verb> Sidecar-ledger CRUD under rcf/define/
|
|
197
|
+
(brief | decisions | concerns | probes;
|
|
198
|
+
verbs: add | resolve | list).
|
|
199
|
+
readiness DEFINE stage-gate readiness view; twin of
|
|
200
|
+
the viewer's Readiness tab (slice 5).
|
|
192
201
|
|
|
193
202
|
Run 'rcf help define <verb>' for per-verb help.
|
|
194
203
|
`;
|
|
@@ -284,6 +293,8 @@ export const HELP_MAP = {
|
|
|
284
293
|
design: DESIGN_HELP,
|
|
285
294
|
blueprint: BLUEPRINT_HELP,
|
|
286
295
|
standards: STANDARDS_HELP,
|
|
296
|
+
ledger: LEDGER_HELP,
|
|
297
|
+
readiness: READINESS_HELP,
|
|
287
298
|
},
|
|
288
299
|
build: {
|
|
289
300
|
queue: BUILD_HELP,
|
package/src/cli/intake.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import { readFile } from 'node:fs/promises';
|
|
13
13
|
import { parseArgs } from 'node:util';
|
|
14
14
|
|
|
15
|
-
import { walkTree } from '#core/store';
|
|
15
|
+
import { walkTree, validateComposedRecord } from '#core/store';
|
|
16
|
+
import { FIDELITY_LEVELS } from '../intake/fidelity.js';
|
|
16
17
|
|
|
17
18
|
import { findProjectRoot } from '../view/index.js';
|
|
18
19
|
import {
|
|
@@ -44,16 +45,25 @@ Options:
|
|
|
44
45
|
--kind <kind> Hint the artefact kind: napkin |
|
|
45
46
|
productBrief | prd | prdPlusTad | other
|
|
46
47
|
--input <config.json> Non-interactive mode: pre-filled artefacts
|
|
47
|
-
+ findings + acknowledgements
|
|
48
|
-
|
|
48
|
+
+ findings + acknowledgements. The
|
|
49
|
+
fidelity in the input file must be one
|
|
50
|
+
of the allowed values below.
|
|
51
|
+
--dry-run Print the plan without writing the record;
|
|
52
|
+
the composed record is still run through
|
|
53
|
+
the same manifest schema pass the write
|
|
54
|
+
path runs (0.28.2 fix for #230), so a
|
|
55
|
+
preview fails on the same schema misses.
|
|
49
56
|
--json Emit the intakeClassification block as JSON
|
|
50
57
|
--quiet Suppress non-error confirmations
|
|
51
58
|
--help Print this help
|
|
52
59
|
|
|
60
|
+
Fidelity enum (allowed values on --input.fidelity):
|
|
61
|
+
${FIDELITY_LEVELS.join(', ')}
|
|
62
|
+
|
|
53
63
|
Exit codes:
|
|
54
64
|
0 success
|
|
55
65
|
2 usage error (missing --artefact/--input, unreadable file)
|
|
56
|
-
3 validation failure on the
|
|
66
|
+
3 validation failure on the composed record (including --dry-run)
|
|
57
67
|
`;
|
|
58
68
|
|
|
59
69
|
/**
|
|
@@ -124,6 +134,17 @@ export async function main(argv, deps = {}) {
|
|
|
124
134
|
}
|
|
125
135
|
|
|
126
136
|
if (flags['dry-run']) {
|
|
137
|
+
// 0.28.2 (issue #230): run the same schema pass the writer runs so
|
|
138
|
+
// a preview no longer passes on a value the real run refuses.
|
|
139
|
+
const dryValidation = validateComposedRecord({
|
|
140
|
+
tree: walkResult.tree,
|
|
141
|
+
record: outcome.record,
|
|
142
|
+
verb: 'intake',
|
|
143
|
+
});
|
|
144
|
+
if (dryValidation) {
|
|
145
|
+
stderr.write(`[error] ${dryValidation.kind} ${dryValidation.message}\n`);
|
|
146
|
+
return 3;
|
|
147
|
+
}
|
|
127
148
|
if (flags.json) {
|
|
128
149
|
stdout.write(`${JSON.stringify(outcome.record, null, 2)}\n`);
|
|
129
150
|
} else {
|