harness-everything 0.40.0 → 0.41.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.
Files changed (160) hide show
  1. package/.agents/harness-everything/project-docs.json +9 -0
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/CHANGELOG.md +43 -0
  5. package/benchmarks/results/f1-doc-path-baseline/1-source-clean.stderr.txt +0 -0
  6. package/benchmarks/results/f1-doc-path-baseline/1-source-clean.stdout.json +20 -0
  7. package/benchmarks/results/f1-doc-path-baseline/2-injected-architecture.md +229 -0
  8. package/benchmarks/results/f1-doc-path-baseline/2-source-injected.stderr.txt +0 -0
  9. package/benchmarks/results/f1-doc-path-baseline/2-source-injected.stdout.json +33 -0
  10. package/benchmarks/results/f1-doc-path-baseline/3-source-reverted.stderr.txt +0 -0
  11. package/benchmarks/results/f1-doc-path-baseline/3-source-reverted.stdout.json +20 -0
  12. package/benchmarks/results/f1-doc-path-baseline/4a-packaged-clean.stderr.txt +0 -0
  13. package/benchmarks/results/f1-doc-path-baseline/4a-packaged-clean.stdout.json +20 -0
  14. package/benchmarks/results/f1-doc-path-baseline/4b-packaged-injected.stderr.txt +0 -0
  15. package/benchmarks/results/f1-doc-path-baseline/4b-packaged-injected.stdout.json +33 -0
  16. package/benchmarks/results/f1-doc-path-baseline/5-source-missing-config.stderr.txt +0 -0
  17. package/benchmarks/results/f1-doc-path-baseline/5-source-missing-config.stdout.json +15 -0
  18. package/benchmarks/results/f1-doc-path-baseline/5b-packaged-missing-config.stderr.txt +0 -0
  19. package/benchmarks/results/f1-doc-path-baseline/5b-packaged-missing-config.stdout.json +15 -0
  20. package/benchmarks/results/f1-doc-path-baseline/README.md +54 -0
  21. package/benchmarks/results/f1-doc-path-baseline/before-fix/1-source-clean.stderr.txt +0 -0
  22. package/benchmarks/results/f1-doc-path-baseline/before-fix/1-source-clean.stdout.json +115 -0
  23. package/benchmarks/results/f1-doc-path-baseline/before-fix/2-injected-document.md +328 -0
  24. package/benchmarks/results/f1-doc-path-baseline/before-fix/2-source-injected.stderr.txt +0 -0
  25. package/benchmarks/results/f1-doc-path-baseline/before-fix/2-source-injected.stdout.json +127 -0
  26. package/benchmarks/results/f1-doc-path-baseline/before-fix/3-source-reverted.stderr.txt +0 -0
  27. package/benchmarks/results/f1-doc-path-baseline/before-fix/3-source-reverted.stdout.json +115 -0
  28. package/benchmarks/results/f1-doc-path-baseline/before-fix/4a-packaged-clean.stderr.txt +0 -0
  29. package/benchmarks/results/f1-doc-path-baseline/before-fix/4a-packaged-clean.stdout.json +115 -0
  30. package/benchmarks/results/f1-doc-path-baseline/before-fix/4b-packaged-injected.stderr.txt +0 -0
  31. package/benchmarks/results/f1-doc-path-baseline/before-fix/4b-packaged-injected.stdout.json +127 -0
  32. package/benchmarks/results/f1-doc-path-baseline/before-fix/5-source-missing-config.stderr.txt +0 -0
  33. package/benchmarks/results/f1-doc-path-baseline/before-fix/5-source-missing-config.stdout.json +15 -0
  34. package/benchmarks/results/f1-doc-path-baseline/before-fix/5b-packaged-missing-config.stderr.txt +0 -0
  35. package/benchmarks/results/f1-doc-path-baseline/before-fix/5b-packaged-missing-config.stdout.json +15 -0
  36. package/benchmarks/results/f1-doc-path-baseline/before-fix/6-supplementary-fixture-ok.input.md +8 -0
  37. package/benchmarks/results/f1-doc-path-baseline/before-fix/6-supplementary-fixture-ok.stdout.json +16 -0
  38. package/benchmarks/results/f1-doc-path-baseline/before-fix/README.md +34 -0
  39. package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/after.json +248 -0
  40. package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/before.json +254 -0
  41. package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/probe.js +26 -0
  42. package/benchmarks/results/f2-doc-craft-baseline/README.md +32 -0
  43. package/benchmarks/results/f2-doc-craft-baseline/arm-f1-report.md +84 -0
  44. package/benchmarks/results/f2-doc-craft-baseline/arm-f1-task.md +21 -0
  45. package/benchmarks/results/f2-doc-craft-baseline/arm-f1.diff +19 -0
  46. package/benchmarks/results/f2-doc-craft-baseline/arm-f2-report.md +110 -0
  47. package/benchmarks/results/f2-doc-craft-baseline/arm-f2-task.md +21 -0
  48. package/benchmarks/results/f2-doc-craft-baseline/arm-f2.diff +19 -0
  49. package/benchmarks/results/f3-derived-views-baseline/README.md +34 -0
  50. package/benchmarks/results/f3-derived-views-baseline/arm-f2-report.md +43 -0
  51. package/benchmarks/results/f3-derived-views-baseline/arm-f2-task.md +21 -0
  52. package/benchmarks/results/f3-derived-views-baseline/arm-f2.diff +21 -0
  53. package/benchmarks/results/f3-derived-views-baseline/arm-f3-report.md +57 -0
  54. package/benchmarks/results/f3-derived-views-baseline/arm-f3-task.md +21 -0
  55. package/benchmarks/results/f3-derived-views-baseline/arm-f3.diff +21 -0
  56. package/benchmarks/results/live-host/claude-325-3b-s2b/README.md +99 -0
  57. package/benchmarks/results/live-host/claude-325-3b-s2b/capture.sanitized.jsonl +26 -0
  58. package/ci/archive/325-prep/README.md +71 -0
  59. package/ci/{prep → archive/325-prep}/doc-reconciliation-red.js +9 -7
  60. package/ci/archive/325-prep/f4-expected-red.json +34 -0
  61. package/ci/archive/325-prep/historical-qualification.test.js +143 -0
  62. package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +17 -4
  63. package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +8 -56
  64. package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +2 -2
  65. package/ci/fixtures/doc-obligations/reconciliation-s1/cases.json +17 -5
  66. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/harness.js +415 -0
  67. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/off-handoff-golden.json +16 -0
  68. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-failure.json +11 -0
  69. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-success-minimal.json +9 -0
  70. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-success.json +13 -0
  71. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-start.json +7 -0
  72. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop-freetext-reject.json +11 -0
  73. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop-reject.json +11 -0
  74. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop.json +11 -0
  75. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-task-result.json +19 -0
  76. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/off-capture-only.json +99 -0
  77. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/off-legacy.json +67 -0
  78. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-concurrent-writers.json +62 -0
  79. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-lookalikes-runtime-mutations.json +67 -0
  80. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-malformed.json +85 -0
  81. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-normal-session.json +74 -0
  82. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-runtime-drift-later-npm-test.json +58 -0
  83. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-valid-direct-quoted-review.json +85 -0
  84. package/ci/fixtures/doc-obligations/reconciliation-s2b/f4/cases.json +2267 -0
  85. package/ci/fixtures/doc-obligations/reconciliation-s2b/wiring/cases.json +1110 -0
  86. package/ci/fixtures/doc-obligations/reconciliation-s2b/wiring/legacy-off-golden.json +674 -0
  87. package/ci/lib/doc-reconciliation-file-spy.js +23 -0
  88. package/ci/lib/doc-reconciliation-prep-fixtures.js +136 -10
  89. package/ci/lib/doc-reconciliation-qualification-preload.js +37 -0
  90. package/ci/lib/doc-reconciliation-replay.js +104 -0
  91. package/ci/lib/doc-reconciliation-staged-tree.js +102 -0
  92. package/ci/mechanism-2p-opencode-v2-entrypoint.test.js +179 -0
  93. package/ci/mechanism-325-3b-preparation.test.js +27 -101
  94. package/ci/mechanism-325-3b-s2b-capture.test.js +561 -0
  95. package/ci/mechanism-325-3b-s2b-wiring.test.js +668 -0
  96. package/ci/mechanism-325-f1-doc-check.test.js +674 -0
  97. package/ci/mechanism-325-f2-doc-craft.test.js +597 -0
  98. package/ci/mechanism-325-f3-derived-views.test.js +798 -0
  99. package/ci/mechanism-325-f4-invariants.test.js +606 -0
  100. package/ci/mechanism-325-f4-projections.test.js +382 -0
  101. package/ci/mechanism-325-f4-qualification.test.js +228 -0
  102. package/ci/mechanism-325-f4-trust-boundary.test.js +880 -0
  103. package/ci/mutation-check.js +26 -3
  104. package/docs/architecture.md +5 -0
  105. package/docs/doc-workflow-reconciliation.md +331 -4
  106. package/docs/document-lifecycle.md +11 -0
  107. package/docs/governed-index.md +20 -0
  108. package/docs/platform-capabilities.md +22 -2
  109. package/grill-with-docs/SKILL.md +1 -1
  110. package/grill-with-docs/references/session-playbook.md +4 -0
  111. package/harness-everything/SKILL.md +1 -1
  112. package/harness-everything/references/doc-check.md +186 -0
  113. package/harness-everything/references/documentation/_shared/maintenance.md +41 -0
  114. package/harness-everything/references/documentation/_shared/writing-craft.md +59 -0
  115. package/harness-everything/references/triage-and-tiers.md +4 -0
  116. package/harness-everything/scripts/doc-check.js +442 -0
  117. package/harness-everything/scripts/doc-lifecycle.js +3 -0
  118. package/hooks/hooks.json +46 -0
  119. package/hooks/scripts/lib/doc-reconciliation-capture.sanitized.jsonl +26 -0
  120. package/hooks/scripts/lib/doc-reconciliation-pilot.js +171 -2
  121. package/hooks/scripts/lib/doc-reconciliation-provider.js +1059 -0
  122. package/hooks/scripts/lib/doc-reconciliation-qualification.json +34 -0
  123. package/hooks/scripts/lib/workflow-obligations.js +7 -0
  124. package/hooks/scripts/state-persist.js +78 -2
  125. package/hooks/scripts/workflow-stop-gate.js +7 -2
  126. package/opencode-plugin/README.md +15 -0
  127. package/opencode-plugin/index.mjs +77 -0
  128. package/opencode-plugin/plugin.json +1 -1
  129. package/package.json +1 -1
  130. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  131. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-capture.sanitized.jsonl +26 -0
  132. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +171 -2
  133. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +1059 -0
  134. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-qualification.json +34 -0
  135. package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +7 -0
  136. package/plugins/harness-everything/hooks/scripts/state-persist.js +78 -2
  137. package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +7 -2
  138. package/plugins/harness-everything/plugin.json +1 -1
  139. package/plugins/harness-everything/skills/grill-with-docs/SKILL.md +1 -1
  140. package/plugins/harness-everything/skills/grill-with-docs/references/session-playbook.md +4 -0
  141. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  142. package/plugins/harness-everything/skills/harness-everything/references/doc-check.md +186 -0
  143. package/plugins/harness-everything/skills/harness-everything/references/documentation/_shared/maintenance.md +41 -0
  144. package/plugins/harness-everything/skills/harness-everything/references/documentation/_shared/writing-craft.md +59 -0
  145. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +4 -0
  146. package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +442 -0
  147. package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +3 -0
  148. package/plugins/harness-everything/skills/repo-docs/SKILL.md +1 -1
  149. package/plugins/harness-everything/skills/repo-docs/references/process-guide.md +4 -0
  150. package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
  151. package/plugins/harness-everything/skills/to-spec/references/process.md +4 -0
  152. package/plugins/harness-everything/skills/to-tickets/SKILL.md +1 -1
  153. package/plugins/harness-everything/skills/to-tickets/references/publishing-and-templates.md +4 -0
  154. package/repo-docs/SKILL.md +1 -1
  155. package/repo-docs/references/process-guide.md +4 -0
  156. package/sbom.cdx.json +7 -7
  157. package/to-spec/SKILL.md +1 -1
  158. package/to-spec/references/process.md +4 -0
  159. package/to-tickets/SKILL.md +1 -1
  160. package/to-tickets/references/publishing-and-templates.md +4 -0
@@ -0,0 +1,597 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // F2 doc-craft lock suite: the prose-share reminder and the shared-craft carrier pointers.
5
+ //
6
+ // R cases run the doc-check command on both legs (canonical scripts + packaged mirror) and assert the
7
+ // prose-share reminder contract from the A1 §3 spec: top-level `reminders`, exact line classification,
8
+ // `share = proseLines/eligibleLines`, trigger strictly when `2*proseLines > eligibleLines`, no exit effect.
9
+ // Expected values come from the spec and the packet table, never from running the code.
10
+ // C cases pin the shared-craft carrier pointers; P pins doc-check byte parity.
11
+ //
12
+ // RED is expected until the implementer lands: R* must fail at an assertion (missing `reminders`), never crash.
13
+
14
+ const crypto = require('crypto');
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+ const { spawnSync } = require('child_process');
19
+
20
+ const ROOT = path.resolve(__dirname, '..');
21
+ const MODULE = 'doc-check.js';
22
+ const CONFIG = '.agents/harness-everything/project-docs.json';
23
+ const LEGS = [
24
+ { name: 'source', dir: path.join(ROOT, 'harness-everything', 'scripts') },
25
+ { name: 'packaged', dir: path.join(ROOT, 'plugins', 'harness-everything', 'skills', 'harness-everything', 'scripts') }
26
+ ];
27
+
28
+ let passed = 0;
29
+ let failed = 0;
30
+ let skipped = 0;
31
+ const failures = [];
32
+ const tmpDirs = [];
33
+
34
+ function record(status, id, leg, detail) {
35
+ const where = leg ? ` [${leg}]` : '';
36
+ if (status === 'PASS') passed += 1;
37
+ else if (status === 'SKIP') skipped += 1;
38
+ else { failed += 1; failures.push(`${id}${where}`); }
39
+ console.log(` ${status} ${id}${where}${detail ? ` — ${detail}` : ''}`);
40
+ }
41
+
42
+ function truncate(text) {
43
+ const s = text === undefined ? 'undefined' : String(text);
44
+ return s.length > 300 ? `${s.slice(0, 300)}…` : s;
45
+ }
46
+
47
+ function same(actual, expected, label) {
48
+ const a = JSON.stringify(actual);
49
+ const e = JSON.stringify(expected);
50
+ if (a !== e) throw new Error(`${label}: got ${truncate(String(a))} expected ${truncate(String(e))}`);
51
+ }
52
+
53
+ // ---- Fixture helpers -----------------------------------------------------------------------------------------------
54
+ function removeTree(dir) {
55
+ fs.rmSync(dir, { recursive: true, force: true });
56
+ }
57
+
58
+ function writeDoc(ws, relative, text) {
59
+ const absolute = path.join(ws, relative);
60
+ fs.mkdirSync(path.dirname(absolute), { recursive: true });
61
+ fs.writeFileSync(absolute, text);
62
+ }
63
+
64
+ function makeWorkspace(config) {
65
+ const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'f2 doc craft '));
66
+ tmpDirs.push(ws);
67
+ if (config) writeDoc(ws, CONFIG, `${JSON.stringify(config, null, 2)}\n`);
68
+ return ws;
69
+ }
70
+
71
+ const REFERENCE = config => ({ governed: [{ path: 'docs/x.md', role: 'reference' }], ...config });
72
+
73
+ function refDoc(body) {
74
+ return `---\nrole: reference\nstatus: active\n---\n\n${body}\n`;
75
+ }
76
+
77
+ function runCheck(scriptDir, options) {
78
+ return spawnSync(process.execPath, [
79
+ path.join(scriptDir, MODULE), 'check',
80
+ '--workspace', options.workspace,
81
+ '--config', options.config,
82
+ ...(options.args || [])
83
+ ], { encoding: 'utf8', cwd: os.tmpdir() });
84
+ }
85
+
86
+ function jsonOut(result) {
87
+ try { return JSON.parse(result.stdout); } catch {
88
+ throw new Error(`stdout is not JSON (exit ${result.status}): ${truncate(result.stdout || result.stderr)}`);
89
+ }
90
+ }
91
+
92
+ function assertPerLeg(id, fn) {
93
+ for (const leg of LEGS) {
94
+ if (!fs.existsSync(path.join(leg.dir, MODULE))) {
95
+ record('FAIL', id, leg.name, `module-missing: ${path.relative(ROOT, path.join(leg.dir, MODULE))}`);
96
+ continue;
97
+ }
98
+ try { fn(leg); record('PASS', id, leg.name); } catch (error) {
99
+ record('FAIL', id, leg.name, error.message.split('\n')[0]);
100
+ }
101
+ }
102
+ }
103
+
104
+ // The reminder oracle per A1 §3: one prose-share reminder with exactly these fields.
105
+ function assertReminder(out, pathValue, prose, eligible, idLabel) {
106
+ if (!Array.isArray(out.reminders) || out.reminders.length !== 1) {
107
+ throw new Error(`${idLabel}: expected exactly one reminder, got ${truncate(JSON.stringify(out.reminders))}`);
108
+ }
109
+ const item = out.reminders[0];
110
+ same(item.code, 'prose-share', 'reminder code');
111
+ same(item.path, pathValue, 'reminder path');
112
+ same(item.strength, 'SHOULD', 'reminder strength');
113
+ same(item.proseLines, prose, 'proseLines');
114
+ same(item.eligibleLines, eligible, 'eligibleLines');
115
+ same(item.share, prose / eligible, 'share');
116
+ same(item.threshold, 0.5, 'threshold');
117
+ }
118
+
119
+ // ---- R cases -------------------------------------------------------------------------------------------------------
120
+
121
+ // R1: heading + one prose line. Exact 50% must not trigger.
122
+ assertPerLeg('case-r1:exact-half-no-reminder', leg => {
123
+ const ws = makeWorkspace(REFERENCE());
124
+ writeDoc(ws, 'docs/x.md', refDoc('# Title\n\nOnly prose line.\n'));
125
+ const result = runCheck(leg.dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
126
+ const out = jsonOut(result);
127
+ same(out.reminders, [], 'reminders');
128
+ same(result.status, 0, 'exit');
129
+ });
130
+
131
+ // R2: heading + prose + label line. 2/3 triggers; the label line counts as prose.
132
+ assertPerLeg('case-r2:label-line-counts', leg => {
133
+ const ws = makeWorkspace(REFERENCE());
134
+ writeDoc(ws, 'docs/x.md', refDoc('# Title\n\nIntro prose sentence.\n\n**Problem:** More\n'));
135
+ const result = runCheck(leg.dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
136
+ same(result.status, 0, 'exit');
137
+ assertReminder(jsonOut(result), 'docs/x.md', 2, 3, 'case-r2');
138
+ });
139
+
140
+ // R3: heading + blank + fenced code. 0/1, no reminder.
141
+ assertPerLeg('case-r3:fenced-code-0-of-1', leg => {
142
+ const ws = makeWorkspace(REFERENCE());
143
+ writeDoc(ws, 'docs/x.md', refDoc('# Title\n\n```text\nconst code = 1;\n```\n'));
144
+ const result = runCheck(leg.dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
145
+ const out = jsonOut(result);
146
+ same(out.reminders, [], 'reminders');
147
+ same(result.status, 0, 'exit');
148
+ });
149
+
150
+ // R4: blockquote CJK + CJK prose pipe table row. 2/3, one reminder.
151
+ assertPerLeg('case-r4:cjk-blockquote-and-table', leg => {
152
+ const ws = makeWorkspace(REFERENCE());
153
+ writeDoc(ws, 'docs/x.md', refDoc('> 引用された行も散文として数えます\n\n本文の行は散文です。\n\n| before | after |\n'));
154
+ const result = runCheck(leg.dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
155
+ const out = jsonOut(result);
156
+ assertReminder(out, 'docs/x.md', 2, 3, 'case-r4');
157
+ });
158
+
159
+ // R5: fence only. 0/0, share 0, no reminder.
160
+ assertPerLeg('case-r5:fence-only-0-of-0', leg => {
161
+ const ws = makeWorkspace(REFERENCE());
162
+ writeDoc(ws, 'docs/x.md', refDoc('```text\nonly fence content\n```\n'));
163
+ const result = runCheck(leg.dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
164
+ const out = jsonOut(result);
165
+ same(out.reminders, [], 'reminders');
166
+ same(result.status, 0, 'exit');
167
+ });
168
+
169
+ // R6: frontmatter lines are not counted. The body sits below the threshold (1/2, exact half);
170
+ // counting the 5 frontmatter lines would push the share above (prose 5, eligible 9 → 2*5 > 9), so a
171
+ // mutant that counts frontmatter fires here while the documented behaviour must not.
172
+ assertPerLeg('case-r6:frontmatter-not-counted', leg => {
173
+ const ws = makeWorkspace(REFERENCE());
174
+ writeDoc(ws, 'docs/x.md', '---\nrole: reference\nstatus: active\ntitle: A longer title field\nowner: The doc owner\n---\n\n# Title\n\nOnly prose line.\n');
175
+ const result = runCheck(leg.dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
176
+ const out = jsonOut(result);
177
+ same(out.reminders, [], 'reminders');
178
+ });
179
+
180
+ // R7: an unclosed fence consumes to EOF; three prose-like lines inside stay excluded (0/1, no reminder).
181
+ assertPerLeg('case-r7:unclosed-fence-consumes-to-eof', leg => {
182
+ const ws = makeWorkspace(REFERENCE());
183
+ writeDoc(ws, 'docs/x.md', refDoc('# Title\n\n```\nplain sentence one inside fence\nplain sentence two inside fence\nplain sentence three inside fence\n'));
184
+ const result = runCheck(leg.dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
185
+ const out = jsonOut(result);
186
+ same(out.reminders, [], 'reminders');
187
+ same(result.status, 0, 'exit');
188
+ });
189
+
190
+ // R8: list items are eligible but not prose; a 4-space-indented line is excluded. 0/2, no reminder.
191
+ assertPerLeg('case-r8:lists-eligible-not-prose', leg => {
192
+ const ws = makeWorkspace(REFERENCE());
193
+ writeDoc(ws, 'docs/x.md', refDoc('- bullet item one\n\n1. ordered item alpha\n\n indented code line\n'));
194
+ const result = runCheck(leg.dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
195
+ const out = jsonOut(result);
196
+ same(out.reminders, [], 'reminders');
197
+ same(result.status, 0, 'exit');
198
+ });
199
+
200
+ // R9: a CRLF body classifies identically to R2.
201
+ assertPerLeg('case-r9:crlf-same-counts', leg => {
202
+ const ws = makeWorkspace(REFERENCE());
203
+ writeDoc(ws, 'docs/x.md', `${refDoc('# Title\n\nIntro prose sentence.\n\n**Problem:** More\n').replace(/\n/g, '\r\n')}`);
204
+ const result = runCheck(leg.dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
205
+ const out = jsonOut(result);
206
+ assertReminder(out, 'docs/x.md', 2, 3, 'case-r9');
207
+ });
208
+
209
+ // R10: the same changed path passed twice yields exactly one reminder.
210
+ assertPerLeg('case-r10:duplicate-path-one-reminder', leg => {
211
+ const ws = makeWorkspace(REFERENCE());
212
+ writeDoc(ws, 'docs/x.md', refDoc('# Title\n\nIntro prose sentence.\n\n**Problem:** More\n'));
213
+ const result = runCheck(leg.dir, {
214
+ workspace: ws, config: CONFIG,
215
+ args: ['--changed-path', 'docs/x.md', '--changed-path', 'docs/x.md']
216
+ });
217
+ const out = jsonOut(result);
218
+ if (!Array.isArray(out.reminders) || out.reminders.length !== 1) {
219
+ throw new Error(`expected exactly one reminder, got ${truncate(JSON.stringify(out.reminders))}`);
220
+ }
221
+ same(out.reminders[0].path, 'docs/x.md', 'reminder path');
222
+ });
223
+
224
+ // R11: an ungoverned file and a legacy file with high prose both stay reminder-free.
225
+ assertPerLeg('case-r11:ungoverned-and-legacy-skipped', leg => {
226
+ const ungoverned = makeWorkspace(REFERENCE());
227
+ writeDoc(ungoverned, 'docs/x.md', refDoc('Plain text only.\n'));
228
+ writeDoc(ungoverned, 'README.md', '# Readme\n\nprose one\nprose two\nprose three\n');
229
+ const outU = jsonOut(runCheck(leg.dir, { workspace: ungoverned, config: CONFIG, args: ['--changed-path', 'README.md'] }));
230
+ if ((outU.reminders || []).length > 0) throw new Error(`reminders for an ungoverned file: ${truncate(JSON.stringify(outU.reminders))}`);
231
+
232
+ const legacy = makeWorkspace(REFERENCE({ legacy: ['docs/legacy/**'] }));
233
+ writeDoc(legacy, 'docs/x.md', refDoc('Plain text only.\n'));
234
+ writeDoc(legacy, 'docs/legacy/old.md', 'legacy prose one\nlegacy prose two\nlegacy prose three\n');
235
+ const outL = jsonOut(runCheck(leg.dir, { workspace: legacy, config: CONFIG, args: ['--changed-path', 'docs/legacy/old.md'] }));
236
+ if ((outL.reminders || []).length > 0) throw new Error(`reminders for a legacy file: ${truncate(JSON.stringify(outL.reminders))}`);
237
+ });
238
+
239
+ // R12: high prose AND a broken link stays exit 1; the reminder does not hide the defect.
240
+ assertPerLeg('case-r12:reminder-never-masks-broken-link', leg => {
241
+ const ws = makeWorkspace(REFERENCE());
242
+ writeDoc(ws, 'docs/x.md', refDoc('# Title\n\nIntro prose sentence.\n\n**Problem:** More\n\nA broken [link](missing.md).\n'));
243
+ const result = runCheck(leg.dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
244
+ same(result.status, 1, 'exit');
245
+ const out = jsonOut(result);
246
+ if (!(out.links.broken || []).some(item => item.target === 'missing.md')) {
247
+ throw new Error(`broken_link missing: ${truncate(JSON.stringify(out.links))}`);
248
+ }
249
+ if (!(Array.isArray(out.reminders) && out.reminders.length === 1)) {
250
+ throw new Error(`expected one reminder alongside the failure: ${truncate(JSON.stringify(out.reminders))}`);
251
+ }
252
+ });
253
+
254
+ // R13: high prose and a valid doc exits 0; the reminder never causes failure.
255
+ assertPerLeg('case-r13:reminder-never-causes-failure', leg => {
256
+ const ws = makeWorkspace(REFERENCE());
257
+ writeDoc(ws, 'docs/x.md', refDoc('# Title\n\nIntro prose sentence.\n\n**Problem:** More\n\nAnd another plain sentence.\n'));
258
+ const result = runCheck(leg.dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
259
+ same(result.status, 0, 'exit');
260
+ const out = jsonOut(result);
261
+ if (!(Array.isArray(out.reminders) && out.reminders.length === 1)) {
262
+ throw new Error(`expected one reminder on a valid doc: ${truncate(JSON.stringify(out.reminders))}`);
263
+ }
264
+ });
265
+
266
+ // R14: a missing config exits 2 with no reminder analysis.
267
+ assertPerLeg('case-r14:missing-config-exit-2-no-reminders', leg => {
268
+ const ws = makeWorkspace(null);
269
+ writeDoc(ws, 'docs/x.md', refDoc('Plain text only.\n'));
270
+ const result = runCheck(leg.dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
271
+ same(result.status, 2, 'exit');
272
+ const out = jsonOut(result);
273
+ if (Object.prototype.hasOwnProperty.call(out, 'reminders')) {
274
+ throw new Error(`reminders present on the exit-2 lane: ${truncate(JSON.stringify(out.reminders))}`);
275
+ }
276
+ });
277
+
278
+ // R15: governed prd and adr docs with high prose stay reminder-free — the reminder scope is
279
+ // governed reference docs only (D6.5). Asserts on reminders only; prd/adr fixtures may lint.
280
+ function caseR15(dir) {
281
+ const ws = makeWorkspace({
282
+ domains: ['DOCA'],
283
+ governed: [
284
+ { path: 'docs/plan.md', role: 'prd' },
285
+ { path: 'docs/decision.md', role: 'adr' }
286
+ ]
287
+ });
288
+ writeDoc(ws, 'docs/plan.md', '---\nid: PRD-DOCA-001\nrevision: 1\nstatus: draft\ndrafted_by: prd author\n---\n\n# Title\n\nIntro prose sentence.\n\n**Problem:** More\n');
289
+ writeDoc(ws, 'docs/decision.md', '---\nid: DOCA-001\nstatus: accepted\n---\n\n# Decision\n\nAnother prose sentence.\n\nAnd one more plain sentence.\n');
290
+ const out = jsonOut(runCheck(dir, {
291
+ workspace: ws, config: CONFIG,
292
+ args: ['--changed-path', 'docs/plan.md', '--changed-path', 'docs/decision.md']
293
+ }));
294
+ if ((out.reminders || []).length > 0) throw new Error(`reminders for prd/adr docs: ${truncate(JSON.stringify(out.reminders))}`);
295
+ }
296
+ assertPerLeg('case-r15:prd-and-adr-no-reminder', leg => caseR15(leg.dir));
297
+
298
+ // R16: a legacy file with high prose counts no reminder even when it is governed
299
+ // with the reference role — the legacy guard, not the role, is what keeps it out.
300
+ function caseR16(dir) {
301
+ const ws = makeWorkspace({ governed: [{ path: 'docs/legacy/old.md', role: 'reference' }], legacy: ['docs/legacy/**'] });
302
+ writeDoc(ws, 'docs/legacy/old.md', refDoc('# Title\n\nIntro prose sentence.\n\n**Problem:** More\n'));
303
+ const out = jsonOut(runCheck(dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/legacy/old.md'] }));
304
+ if ((out.reminders || []).length > 0) throw new Error(`reminders for a legacy file: ${truncate(JSON.stringify(out.reminders))}`);
305
+ }
306
+ assertPerLeg('case-r16:legacy-governed-no-reminder', leg => caseR16(leg.dir));
307
+
308
+ // R17: reminders are advisory-only. A valid high-prose reference still produces exactly one
309
+ // reminder, and nothing with the prose-share code may leak into lint.diagnostics.
310
+ function caseR17(dir) {
311
+ const ws = makeWorkspace(REFERENCE());
312
+ writeDoc(ws, 'docs/x.md', refDoc('# Title\n\nIntro prose sentence.\n\n**Problem:** More\n'));
313
+ const out = jsonOut(runCheck(dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] }));
314
+ assertReminder(out, 'docs/x.md', 2, 3, 'case-r17');
315
+ const diagnosticsText = JSON.stringify(out.lint.diagnostics);
316
+ if (diagnosticsText.includes('prose-share')) throw new Error(`reminder leaked into lint.diagnostics: ${truncate(diagnosticsText)}`);
317
+ }
318
+ assertPerLeg('case-r17:reminder-never-in-diagnostics', leg => caseR17(leg.dir));
319
+
320
+ // R18: a doc with malformed frontmatter (doc.error) gets no reminder even with high prose.
321
+ function caseR18(dir) {
322
+ const ws = makeWorkspace(REFERENCE());
323
+ writeDoc(ws, 'docs/x.md', '---\nrole: reference\n\nPlain prose sentence one.\nPlain prose sentence two.\nPlain prose sentence three.\n');
324
+ const out = jsonOut(runCheck(dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] }));
325
+ if ((out.reminders || []).length > 0) throw new Error(`reminders for a malformed-frontmatter doc: ${truncate(JSON.stringify(out.reminders))}`);
326
+ }
327
+ assertPerLeg('case-r18:malformed-frontmatter-no-reminder', leg => caseR18(leg.dir));
328
+
329
+ // R19: a closing fence must match the opening character. A ~~~ line inside a ``` fence does not
330
+ // close it, so the fence stays open to EOF and the prose-like lines after ~~~ stay excluded (0/1).
331
+ function caseR19(dir) {
332
+ const ws = makeWorkspace(REFERENCE());
333
+ writeDoc(ws, 'docs/x.md', refDoc('# Title\n\n```text\ncode inside the fence\n~~~\nplain prose sentence after the mismatched closer\nplain prose sentence two after the closer\n'));
334
+ const out = jsonOut(runCheck(dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] }));
335
+ if (out.reminders.length !== 0) throw new Error(`~~~ closed a backtick fence: ${truncate(JSON.stringify(out.reminders))}`);
336
+ }
337
+ assertPerLeg('case-r19:mismatched-fence-chars-not-closed', leg => caseR19(leg.dir));
338
+
339
+ // R20: a line with 4 leading spaces before a backtick run is an indented line, not an opening
340
+ // fence (0–3 spaces per the A1 §3 table), so it is excluded from eligible and the lines after
341
+ // it count: prose 2 of eligible 2 triggers one reminder.
342
+ function caseR20(dir) {
343
+ const ws = makeWorkspace(REFERENCE());
344
+ writeDoc(ws, 'docs/x.md', refDoc(' ```\nPlain prose sentence one.\nPlain prose sentence two.\n'));
345
+ const out = jsonOut(runCheck(dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] }));
346
+ assertReminder(out, 'docs/x.md', 2, 2, 'case-r20');
347
+ }
348
+ assertPerLeg('case-r20:indented-backticks-not-a-fence', leg => caseR20(leg.dir));
349
+
350
+ // ---- C cases: shared-reference carriers -----------------------------------------------------------------------------
351
+
352
+ const SHARED = 'harness-everything/references/documentation/_shared';
353
+ const C1_FILES = {
354
+ 'to-spec': 'process.md',
355
+ 'to-tickets': 'publishing-and-templates.md',
356
+ 'repo-docs': 'process-guide.md',
357
+ 'grill-with-docs': 'session-playbook.md'
358
+ };
359
+ const C1_LEGS = [
360
+ { name: 'source', root: ROOT, shared: path.join(ROOT, SHARED) },
361
+ {
362
+ name: 'packaged',
363
+ root: path.join(ROOT, 'plugins', 'harness-everything', 'skills'),
364
+ shared: path.join(ROOT, 'plugins', 'harness-everything', 'skills', SHARED)
365
+ }
366
+ ];
367
+
368
+ // C1: the four thin consumers name both shared references through <skills-repo-root>, and both targets
369
+ // exist in the source and packaged skills trees.
370
+ {
371
+ const id = 'case-c1:consumer-pointers-resolve';
372
+ for (const leg of C1_LEGS) {
373
+ const missing = [];
374
+ try {
375
+ for (const [skill, fileName] of Object.entries(C1_FILES)) {
376
+ const file = path.join(leg.root, skill, 'references', fileName);
377
+ if (!fs.existsSync(file)) { missing.push(`${skill}: reference file missing`); continue; }
378
+ const text = fs.readFileSync(file, 'utf8');
379
+ for (const sharedName of ['writing-craft.md', 'maintenance.md']) {
380
+ if (!text.includes(`<skills-repo-root>/harness-everything/references/documentation/_shared/${sharedName}`)) {
381
+ missing.push(`${skill}: ${sharedName} pointer missing`);
382
+ }
383
+ }
384
+ }
385
+ for (const sharedName of ['writing-craft.md', 'maintenance.md']) {
386
+ if (!fs.existsSync(path.join(leg.shared, sharedName))) missing.push(`${sharedName} target missing`);
387
+ }
388
+ if (missing.length) throw new Error(missing.join(' | '));
389
+ record('PASS', id, leg.name);
390
+ } catch (error) {
391
+ record('FAIL', id, leg.name, error.message.split('\n')[0]);
392
+ }
393
+ }
394
+ }
395
+
396
+ // C3: shared references use stable D-number and section citations, never drifting source line numbers.
397
+ {
398
+ const id = 'case-c3:shared-citations-use-stable-anchors';
399
+ for (const leg of C1_LEGS) {
400
+ const missing = [];
401
+ try {
402
+ for (const sharedName of ['writing-craft.md', 'maintenance.md']) {
403
+ const file = path.join(leg.shared, sharedName);
404
+ if (!fs.existsSync(file)) { missing.push(`${sharedName} target missing`); continue; }
405
+ if (/\b(?:lines?\s+\d+|L\d+|[A-Za-z0-9.]+:\d+)\b/i.test(fs.readFileSync(file, 'utf8'))) {
406
+ missing.push(`${sharedName} contains a line-number citation`);
407
+ }
408
+ }
409
+ if (missing.length) throw new Error(missing.join(' | '));
410
+ record('PASS', id, leg.name);
411
+ } catch (error) {
412
+ record('FAIL', id, leg.name, error.message.split('\n')[0]);
413
+ }
414
+ }
415
+ }
416
+
417
+ // C2: the doc-check reference links both shared files, and both exist in the source and packaged layouts.
418
+ {
419
+ const id = 'case-c2:doc-check-links-shared-pair';
420
+ const missing = [];
421
+ try {
422
+ for (const [skillRel, refFile] of [['harness', 'harness-everything'], ['packaged', 'plugins/harness-everything/skills/harness-everything']]) {
423
+ const docFile = path.join(ROOT, refFile, 'references', 'doc-check.md');
424
+ if (!fs.existsSync(docFile)) { missing.push(`${skillRel}: doc-check.md missing`); continue; }
425
+ const text = fs.readFileSync(docFile, 'utf8');
426
+ for (const sharedName of ['writing-craft.md', 'maintenance.md']) {
427
+ if (!text.includes(`documentation/_shared/${sharedName}`)) missing.push(`${skillRel}: link to _shared/${sharedName} missing`);
428
+ if (!fs.existsSync(path.join(ROOT, refFile, 'references', 'documentation', '_shared', sharedName))) {
429
+ missing.push(`${skillRel}: shared file _shared/${sharedName} missing`);
430
+ }
431
+ }
432
+ }
433
+ if (missing.length) throw new Error(missing.join(' | '));
434
+ record('PASS', id, 'both');
435
+ } catch (error) {
436
+ record('FAIL', id, 'both', error.message.split('\n')[0]);
437
+ }
438
+ }
439
+
440
+ // ---- P case: byte parity -------------------------------------------------------------------------------------------
441
+
442
+ {
443
+ const id = 'case-p1:doc-check-byte-parity';
444
+ try {
445
+ const digest = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
446
+ same(digest(path.join(LEGS[1].dir, MODULE)), digest(path.join(LEGS[0].dir, MODULE)), 'sha256 doc-check.js source = packaged');
447
+ record('PASS', id, 'both');
448
+ } catch (error) {
449
+ record('FAIL', id, 'both', error.message.split('\n')[0]);
450
+ }
451
+ }
452
+
453
+ // ---- Mutation probes (run with --mutation-probes once the suite is GREEN) -------------------------------------------
454
+ // Both probes apply their mutation to a tmp copy of the source scripts, rerun the case assertion against the copy,
455
+ // and expect the assertion to throw (the mutation must be detected).
456
+
457
+ function scriptCopy(prefix) {
458
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
459
+ tmpDirs.push(dir);
460
+ fs.cpSync(LEGS[0].dir, dir, { recursive: true });
461
+ return dir;
462
+ }
463
+
464
+ // MP1: flip the strict trigger (>) to >=; case R1 (exact 50%) must then fail.
465
+ // ProbeApplyError extends the probe set and applyMutation; only doc-check.js is mutated.
466
+ class ProbeApplyError extends Error {}
467
+
468
+ // Each probe edits one guard by a unique substring locator. 0 or 2+ matches → "did not apply", never a guess.
469
+ function applyMutation(dir, needle, replacement, label) {
470
+ const file = path.join(dir, MODULE);
471
+ const original = fs.readFileSync(file, 'utf8');
472
+ const occurrences = original.split(needle).length - 1;
473
+ if (occurrences !== 1) throw new ProbeApplyError(`locator "${needle}" (${label}) matched ${occurrences} lines; probe did not apply`);
474
+ fs.writeFileSync(file, original.replace(needle, replacement));
475
+ }
476
+
477
+ function mp1Apply() {
478
+ const dir = scriptCopy('f2 doc craft mp1 ');
479
+ const file = path.join(dir, MODULE);
480
+ const original = fs.readFileSync(file, 'utf8');
481
+ const lines = original.split('\n');
482
+ const hits = lines.map((text, index) => ({ text, index }))
483
+ .filter(({ text }) => /\b2\s*\*\s*\w/.test(text) && !/=\s*2\s*\*/.test(text) && />/.test(text) && !/>=/.test(text));
484
+ if (hits.length !== 1) throw new ProbeApplyError(`could not uniquely locate the > trigger line (${hits.length} candidates); update this probe for the green shape`);
485
+ const { text, index } = hits[0];
486
+ lines[index] = text.replace('>', '>=');
487
+ fs.writeFileSync(file, lines.join('\n'));
488
+ return dir;
489
+ }
490
+
491
+ function mp1Assert(dir) {
492
+ const ws = makeWorkspace(REFERENCE());
493
+ writeDoc(ws, 'docs/x.md', refDoc('# Title\n\nOnly prose line.\n'));
494
+ const result = runCheck(dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
495
+ // The R1 assertion: with >=, 2*1 >= 2 triggers and must violate the empty-reminders contract.
496
+ const out = jsonOut(result);
497
+ same(out.reminders, [], 'reminders (mutated >= must violate this)');
498
+ }
499
+
500
+ // MP2: force exit 1 whenever any reminder exists; case R13 (high prose, valid doc) must then fail.
501
+ function mp2Apply() {
502
+ const dir = scriptCopy('f2 doc craft mp2 ');
503
+ const file = path.join(dir, MODULE);
504
+ const original = fs.readFileSync(file, 'utf8');
505
+ const emitMatches = [...original.matchAll(/return emit\(result[^;]*\);/g)];
506
+ if (emitMatches.length !== 1) throw new ProbeApplyError(`could not uniquely locate the final emit (${emitMatches.length} candidates); update this probe for the green shape`);
507
+ const injected = `if (result.reminders && result.reminders.length) return emit(result, 1);\n ${emitMatches[0][0]}`;
508
+ fs.writeFileSync(file, original.replace(emitMatches[0][0], injected));
509
+ return dir;
510
+ }
511
+
512
+ function mp2Assert(dir) {
513
+ const ws = makeWorkspace(REFERENCE());
514
+ writeDoc(ws, 'docs/x.md', refDoc('# Title\n\nIntro prose sentence.\n\n**Problem:** More\n\nAnd another plain sentence.\n'));
515
+ const result = runCheck(dir, { workspace: ws, config: CONFIG, args: ['--changed-path', 'docs/x.md'] });
516
+ // The R13 assertion: the mutated forced failure must flip exit 0 to 1.
517
+ same(result.status, 0, 'exit (mutated forced failure must violate this)');
518
+ }
519
+
520
+ // MP3: drop the role === 'reference' guard; case R15 (prd/adr high prose) must then fail.
521
+ function mp3Apply() {
522
+ const dir = scriptCopy('f2 doc craft mp3 ');
523
+ applyMutation(dir, "doc.role !== 'reference' || ", '', 'R15 role guard');
524
+ return dir;
525
+ }
526
+
527
+ function mp3Assert(dir) {
528
+ caseR15(dir);
529
+ }
530
+
531
+ // MP4: drop the legacy guard; case R16 (legacy governed reference) must then fail.
532
+ function mp4Apply() {
533
+ const dir = scriptCopy('f2 doc craft mp4 ');
534
+ applyMutation(dir, " !== 'reference' || doc.legacy", " !== 'reference'", 'R16 legacy guard');
535
+ return dir;
536
+ }
537
+
538
+ function mp4Assert(dir) {
539
+ caseR16(dir);
540
+ }
541
+
542
+ // MP5: also push the reminders into lint.diagnostics; case R17 must then fail.
543
+ function mp5Apply() {
544
+ const dir = scriptCopy('f2 doc craft mp5 ');
545
+ applyMutation(dir, 'diagnostics: analysis.diagnostics',
546
+ "diagnostics: analysis.diagnostics.concat(reminders.map(item => ({ ...item, severity: 'error' })))",
547
+ 'R17 reminders-never-diagnostics');
548
+ return dir;
549
+ }
550
+
551
+ function mp5Assert(dir) {
552
+ caseR17(dir);
553
+ }
554
+
555
+ // MP6: drop the doc.error guard; case R18 (malformed frontmatter) must then fail.
556
+ function mp6Apply() {
557
+ const dir = scriptCopy('f2 doc craft mp6 ');
558
+ applyMutation(dir, ' || doc.error', '', 'R18 doc.error guard');
559
+ return dir;
560
+ }
561
+
562
+ function mp6Assert(dir) {
563
+ caseR18(dir);
564
+ }
565
+
566
+ if (process.argv.includes('--mutation-probes')) {
567
+ for (const leg of LEGS) {
568
+ for (const [id, apply, assertFn] of [
569
+ ['mp1:threshold-ge-mutation-detected-by-r1', mp1Apply, mp1Assert],
570
+ ['mp2:forced-exit-mutation-detected-by-r13', mp2Apply, mp2Assert],
571
+ ['mp3:role-guard-mutation-detected-by-r15', mp3Apply, mp3Assert],
572
+ ['mp4:legacy-guard-mutation-detected-by-r16', mp4Apply, mp4Assert],
573
+ ['mp5:reminder-in-diagnostics-mutation-detected-by-r17', mp5Apply, mp5Assert],
574
+ ['mp6:doc-error-guard-mutation-detected-by-r18', mp6Apply, mp6Assert]
575
+ ]) {
576
+ // A probe PASSES when the case assertion throws on the mutant (mutation detected).
577
+ let dir;
578
+ try { dir = apply(); } catch (error) {
579
+ record('FAIL', id, leg.name, `probe did not apply: ${error.message.split('\n')[0]}`);
580
+ continue;
581
+ }
582
+ try { assertFn(dir); record('FAIL', id, leg.name, 'mutation survived — the case passed on the mutant'); } catch (error) {
583
+ record('PASS', id, leg.name, `mutation detected: ${error.message.split('\n')[0]}`);
584
+ }
585
+ }
586
+ }
587
+ }
588
+
589
+ // ---- Summary -------------------------------------------------------------------------------------------------------
590
+
591
+ console.log(`\nF2 doc-craft summary: ${passed} PASS, ${failed} FAIL, ${skipped} SKIP${failed ? `\nFAILED assertions (${failures.length}):\n ${failures.join('\n ')}` : ''}${failed === 0 && skipped > 0 ? ' (skips are environment-gated cases only)' : ''}`);
592
+
593
+ for (const dir of tmpDirs) {
594
+ try { removeTree(dir); } catch { /* tmp leftovers are harmless */ }
595
+ }
596
+
597
+ process.exitCode = failed > 0 ? 1 : 0;