@mindrian_os/install 1.13.0-beta.17 → 1.13.0-beta.19

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 (182) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/commands/act.md +1 -0
  4. package/commands/admin.md +1 -0
  5. package/commands/analyze-needs.md +2 -0
  6. package/commands/analyze-systems.md +2 -0
  7. package/commands/analyze-timing.md +2 -0
  8. package/commands/auto-explore.md +2 -0
  9. package/commands/beautiful-question.md +2 -0
  10. package/commands/brain-derive.md +2 -0
  11. package/commands/build-knowledge.md +2 -0
  12. package/commands/build-thesis.md +2 -0
  13. package/commands/causal.md +2 -0
  14. package/commands/challenge-assumptions.md +2 -0
  15. package/commands/compare-ventures.md +2 -0
  16. package/commands/dashboard.md +2 -1
  17. package/commands/deep-grade.md +2 -0
  18. package/commands/diagnose.md +21 -1
  19. package/commands/diagnostics.md +14 -3
  20. package/commands/doctor.md +4 -1
  21. package/commands/dogfood-flush.md +92 -0
  22. package/commands/dominant-designs.md +2 -0
  23. package/commands/explain-decision.md +2 -0
  24. package/commands/explore-domains.md +2 -0
  25. package/commands/explore-futures.md +2 -0
  26. package/commands/explore-trends.md +2 -0
  27. package/commands/export.md +1 -0
  28. package/commands/feynman-timeline-refresh.md +2 -0
  29. package/commands/file-meeting.md +2 -0
  30. package/commands/find-analogies.md +1 -0
  31. package/commands/find-bottlenecks.md +2 -0
  32. package/commands/find-connections.md +2 -0
  33. package/commands/funding.md +1 -0
  34. package/commands/grade.md +2 -0
  35. package/commands/graph.md +1 -0
  36. package/commands/hat-briefing.md +1 -0
  37. package/commands/heal.md +22 -170
  38. package/commands/help.md +54 -334
  39. package/commands/hmi-status.md +23 -144
  40. package/commands/jtbd.md +1 -0
  41. package/commands/leadership.md +2 -0
  42. package/commands/lean-canvas.md +2 -0
  43. package/commands/macro-trends.md +2 -0
  44. package/commands/map-unknowns.md +2 -0
  45. package/commands/memory.md +1 -0
  46. package/commands/models.md +1 -0
  47. package/commands/mos-reason.md +2 -0
  48. package/commands/mos.md +139 -0
  49. package/commands/mullins.md +2 -0
  50. package/commands/mva-brief.md +2 -0
  51. package/commands/mva-option.md +2 -0
  52. package/commands/new-project.md +2 -0
  53. package/commands/onboard.md +20 -7
  54. package/commands/operator.md +1 -0
  55. package/commands/opportunities.md +1 -0
  56. package/commands/organize.md +22 -469
  57. package/commands/persona.md +1 -0
  58. package/commands/pipeline.md +2 -0
  59. package/commands/present.md +1 -0
  60. package/commands/publish.md +2 -0
  61. package/commands/query.md +24 -102
  62. package/commands/radar.md +2 -0
  63. package/commands/reanalyze.md +1 -0
  64. package/commands/research.md +2 -0
  65. package/commands/room.md +2 -0
  66. package/commands/rooms.md +1 -0
  67. package/commands/root-cause.md +2 -0
  68. package/commands/rs-experts.md +1 -0
  69. package/commands/rs-explain.md +1 -0
  70. package/commands/rs-fetch.md +1 -0
  71. package/commands/rs-thesis.md +1 -0
  72. package/commands/scenario-plan.md +2 -0
  73. package/commands/scheduled-tasks.md +1 -0
  74. package/commands/score-innovation.md +2 -0
  75. package/commands/scout.md +1 -0
  76. package/commands/setup.md +2 -0
  77. package/commands/snapshot.md +2 -0
  78. package/commands/speakers.md +1 -0
  79. package/commands/splash.md +5 -2
  80. package/commands/status.md +1 -0
  81. package/commands/structure-argument.md +2 -0
  82. package/commands/suggest-next.md +2 -0
  83. package/commands/systems-thinking.md +2 -0
  84. package/commands/think-hats.md +2 -0
  85. package/commands/update.md +2 -0
  86. package/commands/user-needs.md +2 -0
  87. package/commands/validate.md +2 -0
  88. package/commands/value-proposition.md +2 -0
  89. package/commands/vault.md +2 -0
  90. package/commands/visualize.md +24 -29
  91. package/commands/whitespace.md +2 -1
  92. package/commands/wiki.md +1 -0
  93. package/hooks/hooks.json +22 -88
  94. package/lib/agents/auto-explore-agent.cjs +82 -0
  95. package/lib/core/breakthrough/canary.cjs +134 -0
  96. package/lib/core/breakthrough/canary.test.cjs +136 -0
  97. package/lib/core/breakthrough/detectors.cjs +359 -0
  98. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  99. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  100. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  101. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  102. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  103. package/lib/core/breakthrough/review-queue.cjs +154 -0
  104. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  105. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  106. package/lib/core/breakthrough/scanner.cjs +426 -0
  107. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  108. package/lib/core/breakthrough/schema.cjs +164 -0
  109. package/lib/core/breakthrough/schema.test.cjs +256 -0
  110. package/lib/core/breakthrough/scoring.cjs +293 -0
  111. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  112. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  113. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  114. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  115. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  116. package/lib/core/first-touch-version-stamper.cjs +113 -0
  117. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  118. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  119. package/lib/core/llm-name-suggester.cjs +194 -0
  120. package/lib/core/llm-name-suggester.test.cjs +132 -0
  121. package/lib/core/mva-orchestrator.cjs +41 -0
  122. package/lib/core/mva-telemetry.cjs +31 -143
  123. package/lib/core/navigation/edges.cjs +35 -0
  124. package/lib/core/navigation/memory-events.cjs +126 -0
  125. package/lib/core/room-auto-create.cjs +318 -0
  126. package/lib/core/room-auto-create.test.cjs +198 -0
  127. package/lib/core/room-discard-cascade.cjs +225 -0
  128. package/lib/core/room-discard-cascade.test.cjs +135 -0
  129. package/lib/core/room-name-validator.cjs +132 -0
  130. package/lib/core/room-name-validator.test.cjs +156 -0
  131. package/lib/core/room-naming-selector.cjs +357 -0
  132. package/lib/core/room-naming-selector.test.cjs +277 -0
  133. package/lib/core/room-receipt-emit.cjs +63 -0
  134. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  135. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  136. package/lib/core/stale-copy-scanner.cjs +190 -0
  137. package/lib/core/state-aware-router.cjs +78 -0
  138. package/lib/core/telemetry/schema.cjs +168 -0
  139. package/lib/core/telemetry/schema.test.cjs +124 -0
  140. package/lib/core/telemetry/validator.cjs +197 -0
  141. package/lib/core/telemetry/validator.test.cjs +188 -0
  142. package/lib/core/telemetry/writer.cjs +141 -0
  143. package/lib/core/telemetry/writer.test.cjs +331 -0
  144. package/lib/core/terminal-capability.cjs +88 -0
  145. package/lib/core/venture-shape-nudge.cjs +163 -0
  146. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  147. package/lib/core/visual-ops.cjs +70 -2
  148. package/lib/hmi/selector-dispatcher.cjs +90 -1
  149. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  150. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  151. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  152. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  153. package/lib/memory/first-touch-version.test.cjs +198 -0
  154. package/lib/memory/help-coverage.test.cjs +108 -0
  155. package/lib/memory/help-renderer.test.cjs +145 -0
  156. package/lib/memory/palette-consistency.test.cjs +127 -0
  157. package/lib/memory/pending-tension-store.cjs +80 -0
  158. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  159. package/lib/memory/run-feynman-tests.cjs +213 -0
  160. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  161. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  162. package/lib/memory/soft-alias.test.cjs +144 -0
  163. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  164. package/lib/memory/state-aware-router.test.cjs +90 -0
  165. package/lib/memory/statusline-two-row.test.cjs +338 -0
  166. package/lib/memory/terminal-capability.test.cjs +155 -0
  167. package/lib/render/ROOM.md +74 -22
  168. package/lib/sessionstart/budget-compressor.cjs +130 -0
  169. package/lib/sessionstart/contributor-interface.cjs +134 -0
  170. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  171. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  172. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  173. package/lib/statusline/two-row-renderer.cjs +186 -0
  174. package/lib/statusline/version-resolver.cjs +81 -0
  175. package/package.json +1 -1
  176. package/references/visual/ROOM.md +55 -0
  177. package/references/visual/palette.json +54 -0
  178. package/skills/larry-personality/SKILL.md +34 -0
  179. package/skills/ui-system/SKILL.md +109 -1
  180. package/skills/ui-system/rules/dual-palette.md +156 -0
  181. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  182. package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
@@ -0,0 +1,233 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 120-01 Task 1 -- F.7 Breakthrough Surface renderer test suite.
5
+ *
6
+ * Per 120-01-PLAN.md behavior tests (11 tests):
7
+ * T1 F7_VERBS verbatim lock (5 strings, exact order, Object.freeze invariant)
8
+ * T2 F7_DISMISS_INDEX === 3 (zero-indexed position of 'Dismiss')
9
+ * T3 renderShapeF7Breakthrough happy path (contract envelope)
10
+ * T4 Two-row layout (title + verbs)
11
+ * T5 [Dismiss] MANDATORY guard (D-10 -- assertContractInvariants refuses without 'Dismiss')
12
+ * T6 D-20 HARD FLOOR -- empty artifact_ids returns {error: 'provenance_required'}
13
+ * T7 "More breakthroughs (N)" affordance (D-11; only when more_count > 0)
14
+ * T8 Closed-vocab carve-out -- contract.freeTextOffered === false; no Free-Text
15
+ * T9 formatTimeAnchor buckets (in the last hour / today / this week / etc.)
16
+ * T10 Canon Part 8 source-grep (zero brain-client / fetch brain.mindrian / cross-room aggregat*)
17
+ * T11 Em-dash invariant (HARD RULE; zero U+2014 in the renderer source)
18
+ *
19
+ * Pure CJS, node built-ins only, zero deps (Phase 87 invariant).
20
+ */
21
+ 'use strict';
22
+
23
+ const assert = require('node:assert/strict');
24
+ const test = require('node:test');
25
+ const fs = require('node:fs');
26
+ const path = require('node:path');
27
+
28
+ const RENDERER_PATH = path.resolve(__dirname, 'shape-f7-breakthrough-renderer.cjs');
29
+ const renderer = require('./shape-f7-breakthrough-renderer.cjs');
30
+ const {
31
+ renderShapeF7Breakthrough,
32
+ F7_VERBS,
33
+ F7_DISMISS_INDEX,
34
+ formatTimeAnchor,
35
+ assertContractInvariants,
36
+ KIND_DISPLAY_NAMES,
37
+ } = renderer;
38
+
39
+ // --- T1: F7_VERBS verbatim lock ---
40
+ test('T1: F7_VERBS is the 5 verbs verbatim in exact order', () => {
41
+ assert.deepEqual(F7_VERBS, ['Explore deeper', 'Confirm', 'File as decision', 'Dismiss', 'Back']);
42
+ assert.equal(F7_VERBS.length, 5);
43
+ });
44
+
45
+ test('T1b: F7_VERBS is Object.freeze-d -- mutation does not change the array', () => {
46
+ const before = F7_VERBS.slice();
47
+ // Frozen arrays: push throws in strict mode OR silently no-ops in sloppy mode.
48
+ // Either way, the canonical array contents must be unchanged after the attempt.
49
+ try { F7_VERBS.push('Free-Text'); } catch (_e) { /* expected in strict */ }
50
+ assert.deepEqual(F7_VERBS.slice(0, 5), before);
51
+ assert.equal(F7_VERBS.length, 5, 'mutation must not extend the frozen array');
52
+ });
53
+
54
+ // --- T2: F7_DISMISS_INDEX ---
55
+ test('T2: F7_DISMISS_INDEX === 3 (zero-indexed slot of Dismiss in F7_VERBS)', () => {
56
+ assert.equal(F7_DISMISS_INDEX, 3);
57
+ assert.equal(F7_VERBS[F7_DISMISS_INDEX], 'Dismiss');
58
+ });
59
+
60
+ // --- T3: happy path ---
61
+ test('T3: renderShapeF7Breakthrough returns canonical {zones, contract} envelope', () => {
62
+ const now = Date.now();
63
+ const out = renderShapeF7Breakthrough({
64
+ tier: 1,
65
+ breakthrough: {
66
+ id: 'bk:1',
67
+ kind: 'convergence',
68
+ theme: 'X',
69
+ confidence: 0.55,
70
+ artifact_ids: ['a1', 'a2', 'a3', 'a4'],
71
+ detected_at: now - 3600 * 1000,
72
+ },
73
+ more_count: 2,
74
+ });
75
+ assert.equal(typeof out, 'object');
76
+ assert.equal(out.contract.shape, 'F.7');
77
+ assert.deepEqual(out.contract.verbs, F7_VERBS);
78
+ assert.equal(out.contract.freeTextOffered, false);
79
+ assert.equal(out.contract.mode, 'closed');
80
+ assert.equal(out.contract.recommended, null);
81
+ assert.equal(out.contract.breakthrough_id, 'bk:1');
82
+ assert.equal(out.contract.more_count, 2);
83
+ assert.equal(typeof out.zones.header, 'string');
84
+ assert.equal(typeof out.zones.body, 'string');
85
+ });
86
+
87
+ // --- T4: two-row layout ---
88
+ test('T4: zones.body contains title row + 5-verb row with [X] brackets', () => {
89
+ const now = Date.now();
90
+ const out = renderShapeF7Breakthrough({
91
+ tier: 1,
92
+ breakthrough: {
93
+ id: 'bk:1',
94
+ kind: 'convergence',
95
+ theme: 'X',
96
+ artifact_ids: ['a1', 'a2', 'a3'],
97
+ detected_at: now - 1800 * 1000,
98
+ },
99
+ more_count: 0,
100
+ });
101
+ // Row 1: title with kind capitalized + theme + time anchor
102
+ assert.match(out.zones.body, /Convergence: X/);
103
+ assert.match(out.zones.body, /\(in the last hour\)/i);
104
+ // Row 2: 5 verbs in [X] brackets in the locked order
105
+ assert.match(out.zones.body, /\[Explore deeper\]/);
106
+ assert.match(out.zones.body, /\[Confirm\]/);
107
+ assert.match(out.zones.body, /\[File as decision\]/);
108
+ assert.match(out.zones.body, /\[Dismiss\]/);
109
+ assert.match(out.zones.body, /\[Back\]/);
110
+ });
111
+
112
+ // --- T5: D-10 mandatory Dismiss guard ---
113
+ test('T5: assertContractInvariants refuses a verbs array missing Dismiss (D-10 MANDATORY)', () => {
114
+ // Direct invariant call: a verbs array with the same length but Dismiss replaced.
115
+ const broken = ['Explore deeper', 'Confirm', 'File as decision', 'NotDismiss', 'Back'];
116
+ const result = assertContractInvariants(broken);
117
+ assert.equal(result.ok, false);
118
+ assert.equal(result.reason, 'dismiss_required');
119
+ });
120
+
121
+ test('T5b: assertContractInvariants refuses a wrong-count verbs array', () => {
122
+ const tooShort = ['Explore deeper', 'Confirm', 'File as decision', 'Dismiss'];
123
+ const r1 = assertContractInvariants(tooShort);
124
+ assert.equal(r1.ok, false);
125
+ assert.equal(r1.reason, 'verb_count_mismatch');
126
+
127
+ const wrong = ['Explore deeper', 'wrongverb', 'File as decision', 'Dismiss', 'Back'];
128
+ const r2 = assertContractInvariants(wrong);
129
+ assert.equal(r2.ok, false);
130
+ assert.equal(r2.reason, 'verb_order_mismatch');
131
+ });
132
+
133
+ // --- T6: D-20 HARD FLOOR -- empty artifact_ids ---
134
+ test('T6: D-20 HARD FLOOR -- empty artifact_ids returns {error: provenance_required}', () => {
135
+ const out = renderShapeF7Breakthrough({
136
+ tier: 1,
137
+ breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: [], detected_at: Date.now() },
138
+ more_count: 0,
139
+ });
140
+ assert.equal(typeof out, 'object');
141
+ assert.equal(out.error, 'provenance_required');
142
+ assert.equal(out.zones, undefined);
143
+ assert.equal(out.contract, undefined);
144
+ });
145
+
146
+ test('T6b: missing breakthrough returns provenance_required', () => {
147
+ const out = renderShapeF7Breakthrough({ tier: 1, more_count: 0 });
148
+ assert.equal(out.error, 'provenance_required');
149
+ });
150
+
151
+ test('T6c: artifact_ids not an array returns provenance_required', () => {
152
+ const out = renderShapeF7Breakthrough({
153
+ tier: 1,
154
+ breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: 'not-array', detected_at: Date.now() },
155
+ more_count: 0,
156
+ });
157
+ assert.equal(out.error, 'provenance_required');
158
+ });
159
+
160
+ // --- T7: More breakthroughs (N) affordance ---
161
+ test('T7: more_count > 0 renders "More breakthroughs (N)" in zones.footer', () => {
162
+ const out = renderShapeF7Breakthrough({
163
+ tier: 1,
164
+ breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: ['a1'], detected_at: Date.now() },
165
+ more_count: 2,
166
+ });
167
+ assert.match(out.zones.footer, /More breakthroughs \(2\)/);
168
+ });
169
+
170
+ test('T7b: more_count === 0 leaves zones.footer empty (no affordance)', () => {
171
+ const out = renderShapeF7Breakthrough({
172
+ tier: 1,
173
+ breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: ['a1'], detected_at: Date.now() },
174
+ more_count: 0,
175
+ });
176
+ assert.equal(out.zones.footer.indexOf('More breakthroughs'), -1);
177
+ });
178
+
179
+ // --- T8: closed-vocab carve-out ---
180
+ test('T8: contract.freeTextOffered === false; verbs does NOT contain Free-Text', () => {
181
+ const out = renderShapeF7Breakthrough({
182
+ tier: 1,
183
+ breakthrough: { id: 'bk:1', kind: 'convergence', theme: 'X', artifact_ids: ['a1'], detected_at: Date.now() },
184
+ more_count: 0,
185
+ });
186
+ assert.equal(out.contract.freeTextOffered, false);
187
+ assert.equal(out.contract.verbs.indexOf('Free-Text'), -1);
188
+ assert.equal(out.contract.verbs.length, 5);
189
+ });
190
+
191
+ // --- T9: formatTimeAnchor buckets ---
192
+ test('T9: formatTimeAnchor returns the correct bucket string per age', () => {
193
+ const now = Date.now();
194
+ // < 1 hour -> "in the last hour"
195
+ assert.equal(formatTimeAnchor(now - 30 * 60 * 1000), 'in the last hour');
196
+ // 1-8 hours -> "in the last N hours"
197
+ assert.match(formatTimeAnchor(now - 4 * 3600 * 1000), /in the last \d+ hours/);
198
+ // 8-24 hours -> "today"
199
+ assert.equal(formatTimeAnchor(now - 12 * 3600 * 1000), 'today');
200
+ // 1-2 days -> "in the last day"
201
+ assert.equal(formatTimeAnchor(now - 36 * 3600 * 1000), 'in the last day');
202
+ // 2-7 days -> "this week"
203
+ assert.equal(formatTimeAnchor(now - 4 * 24 * 3600 * 1000), 'this week');
204
+ // 7-14 days -> "in the last two weeks"
205
+ assert.equal(formatTimeAnchor(now - 10 * 24 * 3600 * 1000), 'in the last two weeks');
206
+ // > 14 days -> "on YYYY-MM-DD"
207
+ assert.match(formatTimeAnchor(now - 30 * 24 * 3600 * 1000), /^on \d{4}-\d{2}-\d{2}$/);
208
+ });
209
+
210
+ // --- T10: Canon Part 8 source-grep ---
211
+ test('T10: Canon Part 8 invariant -- zero brain-client / brain.mindrian / cross-room aggregat in renderer source', () => {
212
+ const src = fs.readFileSync(RENDERER_PATH, 'utf8');
213
+ assert.equal(/require\([^)]*brain-client[^)]*\)/.test(src), false, 'must not require brain-client');
214
+ assert.equal(/fetch\([^)]*brain\.mindrian[^)]*\)/.test(src), false, 'must not fetch brain.mindrian');
215
+ assert.equal(/cross.{0,3}room.{0,3}aggregat/i.test(src), false, 'must not mention cross-room aggregat');
216
+ });
217
+
218
+ // --- T11: em-dash invariant ---
219
+ test('T11: em-dash invariant -- zero U+2014 in renderer source (HARD RULE)', () => {
220
+ const src = fs.readFileSync(RENDERER_PATH, 'utf8');
221
+ // U+2014 is the em-dash. The test file itself avoids the literal char so this
222
+ // suite stays em-dash-free.
223
+ const emDash = String.fromCharCode(0x2014);
224
+ assert.equal(src.indexOf(emDash), -1, 'renderer source must contain zero em-dashes');
225
+ });
226
+
227
+ // --- Bonus: KIND_DISPLAY_NAMES sanity ---
228
+ test('Bonus: KIND_DISPLAY_NAMES covers the four known detector kinds', () => {
229
+ assert.equal(KIND_DISPLAY_NAMES.convergence, 'Convergence');
230
+ assert.equal(KIND_DISPLAY_NAMES.contradiction_resolved, 'Contradiction resolved');
231
+ assert.equal(KIND_DISPLAY_NAMES.cross_domain_analogy, 'Cross-domain analogy');
232
+ assert.equal(KIND_DISPLAY_NAMES.reverse_salient_closed, 'Reverse salient closed');
233
+ });
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
6
+ *
7
+ * Phase 121.5-01 Task 2 -- Body-shape coverage acceptance tests.
8
+ *
9
+ * Verifies scripts/audit-body-shape-coverage.cjs and output-styles/destijl.md.
10
+ * The audit script is the CI tripwire that scans commands/*.md frontmatter for
11
+ * body_shape: presence and a valid value from the locked vocabulary. The
12
+ * output style is the system-prompt-enforced 4-zone De Stijl format that lifts
13
+ * SKILL.md from skill-instruction-suggested to force-for-plugin-enforced.
14
+ *
15
+ * Canon references:
16
+ * Part 3 UI Ruling System -- this test enforces the body_shape contract
17
+ * that the ruling system depends on.
18
+ * Part 7 Reuse Before Build -- no new commands are created; this is a
19
+ * pure compliance + infrastructure pass.
20
+ * Part 8 Graph Boundary -- audit script + output style add zero network
21
+ * surface, zero Brain calls, zero telemetry egress.
22
+ *
23
+ * Test map (7 cases, one-to-one with PLAN Task 2 <behavior>):
24
+ *
25
+ * 1. output-styles/destijl.md exists and parses as valid YAML frontmatter
26
+ * + markdown body.
27
+ * 2. Frontmatter contains exactly `force-for-plugin: true` AND
28
+ * `keep-coding-instructions: true` (literal string match).
29
+ * 3. Body documents the four zones in fixed order matching SKILL.md
30
+ * Section 1: Header Panel / Content Body / Intelligence Strip /
31
+ * Action Footer.
32
+ * 4. Body documents the 5+2 body shapes (A, B, C, D, E + F family)
33
+ * mapping to the SKILL.md Section 2 vocabulary.
34
+ * 5. scripts/audit-body-shape-coverage.cjs exits 0 against the live
35
+ * commands/ directory (post-Task-1 state). Exits 1 against a
36
+ * synthetic fixture with a command missing body_shape:.
37
+ * 6. The audit script reports per-category counts (A / B / C / D / E /
38
+ * F.x / methodology) in --json mode.
39
+ * 7. Every body_shape: value in commands/*.md is one of the locked
40
+ * vocabulary {A, B, C, D, E, F.0..F.6, methodology}. The audit
41
+ * script flags any other value.
42
+ */
43
+
44
+ const assert = require('node:assert/strict');
45
+ const fs = require('node:fs');
46
+ const os = require('node:os');
47
+ const path = require('node:path');
48
+ const { execFileSync } = require('node:child_process');
49
+
50
+ const REPO = path.resolve(__dirname, '..', '..');
51
+ const DESTIJL_PATH = path.join(REPO, 'output-styles', 'destijl.md');
52
+ const AUDIT_PATH = path.join(REPO, 'scripts', 'audit-body-shape-coverage.cjs');
53
+ const COMMANDS_DIR = path.join(REPO, 'commands');
54
+
55
+ // ---------- Fixture helpers ----------
56
+
57
+ const TMP_ROOTS = [];
58
+ function mkTmp(prefix) {
59
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
60
+ TMP_ROOTS.push(d);
61
+ return d;
62
+ }
63
+ process.on('exit', () => {
64
+ for (const d of TMP_ROOTS) {
65
+ try { fs.rmSync(d, { recursive: true, force: true }); } catch (_) {}
66
+ }
67
+ });
68
+
69
+ function readDestijl() {
70
+ return fs.readFileSync(DESTIJL_PATH, 'utf8');
71
+ }
72
+
73
+ function loadAudit() {
74
+ try { delete require.cache[require.resolve(AUDIT_PATH)]; } catch (_) {}
75
+ return require(AUDIT_PATH);
76
+ }
77
+
78
+ // Run audit script as a subprocess (mirrors how CI will invoke it).
79
+ // Returns { code, stdout, stderr }.
80
+ function runAudit(args, env) {
81
+ const result = { code: 0, stdout: '', stderr: '' };
82
+ try {
83
+ result.stdout = execFileSync('node', [AUDIT_PATH, ...(args || [])], {
84
+ env: Object.assign({}, process.env, env || {}),
85
+ encoding: 'utf8',
86
+ stdio: ['ignore', 'pipe', 'pipe'],
87
+ });
88
+ } catch (e) {
89
+ result.code = (e && typeof e.status === 'number') ? e.status : 1;
90
+ result.stdout = (e && e.stdout) ? e.stdout.toString() : '';
91
+ result.stderr = (e && e.stderr) ? e.stderr.toString() : '';
92
+ }
93
+ return result;
94
+ }
95
+
96
+ // ---------- Tests ----------
97
+
98
+ let passed = 0;
99
+ let failed = 0;
100
+ const failures = [];
101
+
102
+ function test(name, fn) {
103
+ try {
104
+ fn();
105
+ passed++;
106
+ console.log(' ok ' + name);
107
+ } catch (e) {
108
+ failed++;
109
+ failures.push({ name, error: e });
110
+ console.log(' FAIL ' + name);
111
+ console.log(' ' + (e && e.message ? e.message : String(e)));
112
+ }
113
+ }
114
+
115
+ console.log('Phase 121.5-01 Task 2 -- body-shape-coverage acceptance suite');
116
+ console.log('');
117
+
118
+ // Test 1: destijl.md exists and parses
119
+ test('Test 1: output-styles/destijl.md exists with frontmatter + body', () => {
120
+ assert.ok(fs.existsSync(DESTIJL_PATH), 'output-styles/destijl.md missing');
121
+ const text = readDestijl();
122
+ const fm = text.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
123
+ assert.ok(fm, 'destijl.md does not parse as frontmatter + body');
124
+ assert.ok(fm[1].length > 0, 'frontmatter is empty');
125
+ assert.ok(fm[2].length > 100, 'body is too short (< 100 chars)');
126
+ });
127
+
128
+ // Test 2: force-for-plugin + keep-coding-instructions present
129
+ test('Test 2: frontmatter has force-for-plugin: true AND keep-coding-instructions: true', () => {
130
+ const text = readDestijl();
131
+ const fm = text.match(/^---\n([\s\S]*?)\n---/);
132
+ assert.ok(fm, 'no frontmatter found');
133
+ assert.ok(/^force-for-plugin:\s*true\s*$/m.test(fm[1]),
134
+ 'force-for-plugin: true not found in frontmatter');
135
+ assert.ok(/^keep-coding-instructions:\s*true\s*$/m.test(fm[1]),
136
+ 'keep-coding-instructions: true not found in frontmatter');
137
+ });
138
+
139
+ // Test 3: 4 zones documented in fixed order
140
+ test('Test 3: body documents 4 zones in SKILL.md Section 1 order', () => {
141
+ const text = readDestijl();
142
+ const idxHeader = text.indexOf('Header Panel');
143
+ const idxBody = text.indexOf('Content Body');
144
+ const idxStrip = text.indexOf('Intelligence Strip');
145
+ const idxFooter = text.indexOf('Action Footer');
146
+ assert.ok(idxHeader > 0, 'Header Panel zone not documented');
147
+ assert.ok(idxBody > idxHeader, 'Content Body must come after Header Panel');
148
+ assert.ok(idxStrip > idxBody, 'Intelligence Strip must come after Content Body');
149
+ assert.ok(idxFooter > idxStrip, 'Action Footer must come after Intelligence Strip');
150
+ });
151
+
152
+ // Test 4: 5+2 body shapes documented
153
+ test('Test 4: body documents shapes A, B, C, D, E + F family + methodology', () => {
154
+ const text = readDestijl();
155
+ // Each shape letter must appear in the shape table or shape documentation.
156
+ // Use regex to find the shape table row pattern: "| <letter> |" or "Shape <letter>".
157
+ for (const shape of ['A', 'B', 'C', 'D', 'E']) {
158
+ const re = new RegExp('(\\|\\s*' + shape + '\\s*\\||Shape\\s+' + shape + '\\b)');
159
+ assert.ok(re.test(text), 'Shape ' + shape + ' not documented in body');
160
+ }
161
+ // F family members
162
+ for (const fSub of ['F.0', 'F.1', 'F.2', 'F.3', 'F.4', 'F.5', 'F.6']) {
163
+ const re = new RegExp('\\b' + fSub.replace('.', '\\.') + '\\b');
164
+ assert.ok(re.test(text), fSub + ' not documented in body');
165
+ }
166
+ assert.ok(/methodology/i.test(text), 'methodology not documented in body');
167
+ });
168
+
169
+ // Test 5a: audit script exits 0 against live commands/ (post-Task-1 state)
170
+ test('Test 5a: audit exits 0 against live commands/ (100% coverage)', () => {
171
+ assert.ok(fs.existsSync(AUDIT_PATH), 'audit script missing');
172
+ const r = runAudit([]);
173
+ if (r.code !== 0) {
174
+ console.log(' stdout: ' + r.stdout);
175
+ console.log(' stderr: ' + r.stderr);
176
+ }
177
+ assert.strictEqual(r.code, 0, 'audit script exited non-zero on live repo');
178
+ });
179
+
180
+ // Test 5b: audit exits 1 on synthetic fixture missing body_shape
181
+ test('Test 5b: audit exits 1 on synthetic command missing body_shape', () => {
182
+ const tmp = mkTmp('body-shape-audit-');
183
+ const cmdsDir = path.join(tmp, 'commands');
184
+ fs.mkdirSync(cmdsDir, { recursive: true });
185
+ // Good file
186
+ fs.writeFileSync(path.join(cmdsDir, 'good.md'),
187
+ '---\nname: good\ndescription: test\nbody_shape: A\n---\n# good\n');
188
+ // Bad file (missing body_shape)
189
+ fs.writeFileSync(path.join(cmdsDir, 'bad.md'),
190
+ '---\nname: bad\ndescription: test\n---\n# bad\n');
191
+
192
+ // Audit accepts a --dir override.
193
+ const r = runAudit(['--dir', cmdsDir]);
194
+ assert.notStrictEqual(r.code, 0,
195
+ 'audit must fail when a command is missing body_shape (got exit 0)');
196
+ });
197
+
198
+ // Test 6: --json mode reports per-category counts
199
+ test('Test 6: --json mode reports histogram per category', () => {
200
+ const r = runAudit(['--json']);
201
+ assert.strictEqual(r.code, 0, 'audit --json exited non-zero');
202
+ let parsed;
203
+ try {
204
+ parsed = JSON.parse(r.stdout);
205
+ } catch (e) {
206
+ assert.fail('--json output not valid JSON: ' + e.message);
207
+ }
208
+ assert.ok(parsed && typeof parsed === 'object', '--json must emit object');
209
+ assert.ok(typeof parsed.total === 'number', 'total missing in --json output');
210
+ assert.ok(parsed.histogram && typeof parsed.histogram === 'object',
211
+ 'histogram missing in --json output');
212
+ // At least 3 distinct shape categories should appear in a healthy repo
213
+ // (proves the sweep is a spread, not a monoculture).
214
+ const keys = Object.keys(parsed.histogram);
215
+ assert.ok(keys.length >= 3,
216
+ 'histogram has < 3 distinct shapes -- monoculture detected: ' + keys.join(','));
217
+ });
218
+
219
+ // Test 7: all body_shape values in live repo are in the locked vocabulary
220
+ test('Test 7: every body_shape value in commands/*.md is in locked vocab', () => {
221
+ const { VALID_SHAPES } = loadAudit();
222
+ assert.ok(VALID_SHAPES && typeof VALID_SHAPES.has === 'function',
223
+ 'audit must export VALID_SHAPES Set');
224
+ // Sanity-check the vocabulary itself
225
+ for (const v of ['A', 'B', 'C', 'D', 'E',
226
+ 'F.0', 'F.1', 'F.2', 'F.3', 'F.4', 'F.5', 'F.6',
227
+ 'methodology']) {
228
+ assert.ok(VALID_SHAPES.has(v), 'VALID_SHAPES missing required value: ' + v);
229
+ }
230
+ // Now scan live commands and validate each
231
+ const files = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md'));
232
+ const offenders = [];
233
+ for (const f of files) {
234
+ const text = fs.readFileSync(path.join(COMMANDS_DIR, f), 'utf8');
235
+ const fm = text.match(/^---\n([\s\S]*?)\n---/);
236
+ if (!fm) { offenders.push(f + ' (no frontmatter)'); continue; }
237
+ const m = fm[1].match(/^body_shape:\s*(.+)$/m);
238
+ if (!m) { offenders.push(f + ' (no body_shape)'); continue; }
239
+ // Strip trailing parenthetical (e.g. "B (Semantic Tree)" -> "B")
240
+ let v = m[1].trim().replace(/\s*\(.*\)\s*$/, '');
241
+ // Strip surrounding quotes if present
242
+ v = v.replace(/^['"]|['"]$/g, '');
243
+ if (!VALID_SHAPES.has(v)) {
244
+ offenders.push(f + ' = "' + m[1].trim() + '" (extracted: "' + v + '")');
245
+ }
246
+ }
247
+ if (offenders.length) {
248
+ assert.fail('commands with invalid body_shape values:\n ' + offenders.join('\n '));
249
+ }
250
+ });
251
+
252
+ // ---------- Summary ----------
253
+
254
+ console.log('');
255
+ console.log('========================================');
256
+ console.log('Tests: ' + (passed + failed) + ' total, ' + passed + ' passed, ' + failed + ' failed');
257
+ console.log('========================================');
258
+
259
+ if (failed > 0) {
260
+ console.log('');
261
+ console.log('Failures:');
262
+ for (const f of failures) {
263
+ console.log(' - ' + f.name);
264
+ }
265
+ process.exit(1);
266
+ }
267
+
268
+ process.exit(0);