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.
Files changed (219) hide show
  1. package/CHANGELOG.md +310 -0
  2. package/bin/rcf.js +4 -0
  3. package/fixtures/canary-manifest.json +9 -9
  4. package/package.json +1 -1
  5. package/rcf/adrs/adr-4120-canonical-json-hashing.json +30 -0
  6. package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
  7. package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
  8. package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
  9. package/rcf/code-nodes/cn-602.json +3 -2
  10. package/rcf/code-nodes/cn-700.json +23 -0
  11. package/rcf/code-nodes/cn-701.json +14 -0
  12. package/rcf/code-nodes/cn-702.json +14 -0
  13. package/rcf/code-nodes/cn-703.json +16 -0
  14. package/rcf/code-nodes/cn-720.json +15 -0
  15. package/rcf/code-nodes/cn-721.json +15 -0
  16. package/rcf/code-nodes/cn-722.json +15 -0
  17. package/rcf/code-nodes/cn-723.json +18 -0
  18. package/rcf/code-nodes/cn-724.json +14 -0
  19. package/rcf/code-nodes/cn-725.json +21 -0
  20. package/rcf/code-nodes/cn-726.json +17 -0
  21. package/rcf/code-nodes/cn-727.json +14 -0
  22. package/rcf/code-nodes/cn-728.json +21 -0
  23. package/rcf/code-nodes/cn-729.json +18 -0
  24. package/rcf/code-nodes/cn-730.json +14 -0
  25. package/rcf/fbs/fbs-181.json +3 -2
  26. package/rcf/fbs/fbs-190.json +37 -0
  27. package/rcf/fbs/fbs-191.json +34 -0
  28. package/rcf/fbs/fbs-192.json +17 -0
  29. package/rcf/fbs/fbs-193.json +37 -0
  30. package/rcf/fbs/fbs-194.json +35 -0
  31. package/rcf/fbs/fbs-195.json +37 -0
  32. package/rcf/fbs/fbs-196.json +37 -0
  33. package/rcf/fbs/fbs-197.json +41 -0
  34. package/rcf/requirements/req-001.json +2 -1
  35. package/rcf/requirements/req-002.json +2 -1
  36. package/rcf/requirements/req-003.json +2 -1
  37. package/rcf/requirements/req-004.json +2 -1
  38. package/rcf/requirements/req-005.json +2 -1
  39. package/rcf/requirements/req-006.json +2 -1
  40. package/rcf/requirements/req-007.json +2 -1
  41. package/rcf/requirements/req-008.json +2 -1
  42. package/rcf/requirements/req-009.json +2 -1
  43. package/rcf/requirements/req-010.json +5 -2
  44. package/rcf/requirements/req-011.json +2 -1
  45. package/rcf/requirements/req-012.json +2 -1
  46. package/rcf/requirements/req-013.json +2 -1
  47. package/rcf/requirements/req-014.json +2 -1
  48. package/rcf/requirements/req-015.json +2 -1
  49. package/rcf/requirements/req-016.json +5 -2
  50. package/rcf/requirements/req-017.json +6 -2
  51. package/rcf/requirements/req-018.json +5 -2
  52. package/rcf/requirements/req-019.json +5 -2
  53. package/rcf/requirements/req-020.json +5 -2
  54. package/rcf/requirements/req-021.json +5 -2
  55. package/rcf/requirements/req-022.json +5 -2
  56. package/rcf/requirements/req-023.json +6 -4
  57. package/rcf/requirements/req-024.json +5 -2
  58. package/rcf/requirements/req-025.json +5 -2
  59. package/rcf/requirements/req-026.json +5 -2
  60. package/rcf/requirements/req-027.json +5 -2
  61. package/rcf/requirements/req-028.json +5 -2
  62. package/rcf/requirements/req-029.json +5 -2
  63. package/rcf/requirements/req-030.json +5 -2
  64. package/rcf/requirements/req-040.json +5 -2
  65. package/rcf/requirements/req-041.json +5 -2
  66. package/rcf/requirements/req-042.json +5 -2
  67. package/rcf/requirements/req-043.json +5 -2
  68. package/rcf/requirements/req-050.json +5 -2
  69. package/rcf/requirements/req-051.json +5 -2
  70. package/rcf/requirements/req-052.json +5 -2
  71. package/rcf/requirements/req-053.json +5 -2
  72. package/rcf/requirements/req-054.json +5 -2
  73. package/rcf/requirements/req-060.json +5 -2
  74. package/rcf/requirements/req-061.json +5 -2
  75. package/rcf/requirements/req-062.json +5 -2
  76. package/rcf/requirements/req-063.json +5 -2
  77. package/rcf/requirements/req-070.json +5 -2
  78. package/rcf/requirements/req-071.json +5 -2
  79. package/rcf/requirements/req-072.json +5 -2
  80. package/rcf/requirements/req-073.json +5 -2
  81. package/rcf/requirements/req-074.json +5 -2
  82. package/rcf/requirements/req-075.json +5 -2
  83. package/rcf/requirements/req-076.json +5 -2
  84. package/rcf/requirements/req-077.json +5 -2
  85. package/rcf/requirements/req-080.json +5 -2
  86. package/rcf/requirements/req-081.json +5 -2
  87. package/rcf/requirements/req-082.json +5 -2
  88. package/rcf/requirements/req-083.json +5 -2
  89. package/rcf/requirements/req-084.json +5 -2
  90. package/rcf/requirements/req-085.json +5 -2
  91. package/rcf/requirements/req-086.json +5 -2
  92. package/rcf/requirements/req-090.json +5 -2
  93. package/rcf/requirements/req-091.json +5 -2
  94. package/rcf/requirements/req-092.json +5 -2
  95. package/rcf/requirements/req-093.json +5 -2
  96. package/rcf/requirements/req-094.json +5 -2
  97. package/rcf/requirements/req-100.json +5 -2
  98. package/rcf/requirements/req-101.json +5 -2
  99. package/rcf/requirements/req-102.json +5 -2
  100. package/rcf/requirements/req-103.json +5 -2
  101. package/rcf/requirements/req-104.json +5 -2
  102. package/rcf/requirements/req-110.json +5 -2
  103. package/rcf/requirements/req-111.json +5 -2
  104. package/rcf/requirements/req-112.json +5 -2
  105. package/rcf/requirements/req-113.json +5 -2
  106. package/rcf/requirements/req-114.json +5 -2
  107. package/rcf/requirements/req-115.json +5 -2
  108. package/rcf/requirements/req-120.json +5 -2
  109. package/rcf/requirements/req-121.json +5 -2
  110. package/rcf/requirements/req-122.json +5 -2
  111. package/rcf/requirements/req-123.json +5 -2
  112. package/rcf/requirements/req-124.json +5 -2
  113. package/rcf/requirements/req-125.json +5 -2
  114. package/rcf/requirements/req-130.json +6 -2
  115. package/rcf/requirements/req-131.json +6 -2
  116. package/rcf/requirements/req-132.json +6 -2
  117. package/rcf/requirements/req-133.json +6 -2
  118. package/rcf/requirements/req-134.json +6 -2
  119. package/rcf/requirements/req-140.json +5 -2
  120. package/rcf/requirements/req-145.json +5 -2
  121. package/rcf/requirements/req-150.json +6 -2
  122. package/rcf/requirements/req-151.json +6 -2
  123. package/rcf/requirements/req-152.json +6 -2
  124. package/rcf/requirements/req-153.json +6 -2
  125. package/rcf/requirements/req-154.json +5 -2
  126. package/rcf/requirements/req-155.json +4 -1
  127. package/rcf/requirements/req-156.json +4 -1
  128. package/rcf/requirements/req-157.json +4 -1
  129. package/rcf/requirements/req-158.json +4 -1
  130. package/rcf/requirements/req-159.json +4 -1
  131. package/rcf/requirements/req-160.json +4 -1
  132. package/rcf/requirements/req-161.json +4 -1
  133. package/rcf/requirements/req-162.json +4 -1
  134. package/rcf/requirements/req-163.json +5 -2
  135. package/rcf/requirements/req-164.json +5 -2
  136. package/rcf/requirements/req-165.json +5 -2
  137. package/rcf/requirements/req-170.json +33 -0
  138. package/rcf/requirements/req-171.json +44 -0
  139. package/rcf/requirements/req-172.json +25 -0
  140. package/rcf/requirements/req-173.json +25 -0
  141. package/rcf/requirements/req-174.json +25 -0
  142. package/rcf/requirements/req-175.json +25 -0
  143. package/rcf/tacs/tac-4103-feedback-redact.json +2 -2
  144. package/rcf/tacs/tac-4120-define-delta-and-freeze.json +21 -0
  145. package/rcf/tacs/tac-4121-define-ledgers.json +23 -0
  146. package/rcf/tacs/tac-4122-define-gates.json +25 -0
  147. package/rcf/tacs/tac-4123-define-readiness.json +25 -0
  148. package/rcf/test-suites/ts-188.json +10 -2
  149. package/rcf/test-suites/ts-191.json +19 -5
  150. package/rcf/test-suites/ts-204.json +30 -0
  151. package/rcf/test-suites/ts-205.json +22 -0
  152. package/rcf/test-suites/ts-206.json +22 -0
  153. package/rcf/test-suites/ts-207.json +22 -0
  154. package/rcf/test-suites/ts-208.json +22 -0
  155. package/rcf/test-suites/ts-209.json +22 -0
  156. package/rcf/test-suites/ts-210.json +22 -0
  157. package/rcf/test-suites/ts-211.json +22 -0
  158. package/rcf/test-suites/ts-212.json +22 -0
  159. package/rcf/test-suites/ts-213.json +50 -0
  160. package/rcf/test-suites/ts-214.json +74 -0
  161. package/rcf/test-suites/ts-215.json +67 -0
  162. package/rcf/test-suites/ts-216.json +78 -0
  163. package/rcf/test-suites/ts-217.json +62 -0
  164. package/rcf/user-stories/us-15601.json +10 -1
  165. package/rcf/user-stories/us-15701.json +4 -4
  166. package/rcf/user-stories/us-17001.json +27 -0
  167. package/rcf/user-stories/us-17002.json +22 -0
  168. package/rcf/user-stories/us-17003.json +22 -0
  169. package/rcf/user-stories/us-17004.json +22 -0
  170. package/rcf/user-stories/us-17005.json +22 -0
  171. package/rcf/user-stories/us-17006.json +21 -0
  172. package/rcf/user-stories/us-17007.json +21 -0
  173. package/rcf/user-stories/us-17008.json +22 -0
  174. package/rcf/user-stories/us-17101.json +22 -0
  175. package/rcf/user-stories/us-17102.json +22 -0
  176. package/rcf/user-stories/us-17201.json +38 -0
  177. package/rcf/user-stories/us-17301.json +38 -0
  178. package/rcf/user-stories/us-17401.json +58 -0
  179. package/rcf/user-stories/us-17501.json +48 -0
  180. package/releases/releases.yaml +31 -1
  181. package/src/browser-verify/manifest-writer.js +9 -5
  182. package/src/cli/browser-verify.js +9 -1
  183. package/src/cli/feedback.js +316 -32
  184. package/src/cli/help.js +11 -0
  185. package/src/cli/intake.js +25 -4
  186. package/src/cli/ledger.js +399 -0
  187. package/src/cli/preflight.js +24 -3
  188. package/src/cli/readiness.js +280 -0
  189. package/src/cli/review.js +9 -1
  190. package/src/cli/ui-baseline.js +14 -1
  191. package/src/core/store/index.js +5 -0
  192. package/src/core/store/validate-composed-record.js +95 -0
  193. package/src/core/store/writer.js +86 -6
  194. package/src/define/freeze-record.js +232 -0
  195. package/src/define/ledgers.js +513 -0
  196. package/src/feedback/gh.js +27 -1
  197. package/src/feedback/redact-allowlist.json +7 -0
  198. package/src/feedback/redact.js +126 -11
  199. package/src/feedback/render.js +34 -12
  200. package/src/intake/manifest-writer.js +6 -3
  201. package/src/intake/orchestrator.js +44 -7
  202. package/src/preflight/manifest-writer.js +11 -11
  203. package/src/query/delta.js +276 -0
  204. package/src/query/gates.js +832 -0
  205. package/src/query/index.js +36 -0
  206. package/src/query/readiness.js +333 -0
  207. package/src/req-baseline/sweep.js +34 -5
  208. package/src/server/index.js +37 -0
  209. package/src/server/routes.js +65 -3
  210. package/src/ui-baseline/manifest-writer.js +11 -10
  211. package/src/view/doc-renderers/helpers.js +39 -2
  212. package/src/view/doc-renderers/req.js +10 -2
  213. package/src/view/doc-renderers/tad.js +67 -6
  214. package/src/view/doc-renderers/user-story.js +10 -5
  215. package/src/view/html-page.js +347 -28
  216. package/src/view/index.js +14 -1
  217. package/src/view/live-client.js +22 -2
  218. package/src/view/product-map.js +751 -0
  219. package/src/view/style.css +383 -0
@@ -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 }`. A colon-separated
481
- * `path:line` pattern is 'file'; a bare identifier that looks like an
482
- * upper-cased id is 'id'; anything else is 'command'. The classification
483
- * is best-effort and does not gate the write.
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
- if (/^[^\s].*:\d+$/.test(value)) return { kind: 'file', value };
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
- const m = profile.match(/^##\s+Name\s*\n([^\n]+)/m);
1009
- if (m) {
1010
- const n = m[1].trim();
1011
- if (n && !/placeholder|todo|your name/i.test(n)) operatorName = n;
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). Zero hits -> create. One hit -> +1 comment. Many hits ->
1219
- * comment on the lowest number and mention the others. Search failure
1220
- * -> create with dedupe:unchecked. Closed search runs only after a
1221
- * zero-hit open search so the create body can reference the prior
1222
- * number.
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
- matches = searchOpen.value?.matches ?? [];
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
- const plan = matches.length === 1
1392
- ? `comment on #${matches[0].number}`
1393
- : matches.length > 1
1394
- ? `comment on #${matches.sort((a, b) => a.number - b.number)[0].number} (${matches.length} matches)`
1395
- : 'create';
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
- body: built.redacted.body,
1432
- ledger: built.redacted.ledger,
1433
- }, { fingerprint: built.fingerprint, includeBody: false }).body;
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 others.
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
- body: built.redacted.body,
1446
- ledger: built.redacted.ledger,
1447
- }, { fingerprint: built.fingerprint, includeBody: false }).body;
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
- stdout.write(`${e.id} -> ${result.url} (${dedupe})\n`);
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
- --dry-run Print the plan without writing the record
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 written manifest
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 {