@mindrian_os/install 1.13.0-beta.16 → 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 (219) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +36 -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 +4 -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 +4 -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 +58 -0
  51. package/commands/mva-option.md +91 -0
  52. package/commands/new-project.md +4 -0
  53. package/commands/onboard.md +22 -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 +31 -88
  94. package/lib/agents/auto-explore-agent.cjs +82 -0
  95. package/lib/agents/mva/brain-classic-traps.cjs +77 -0
  96. package/lib/agents/mva/brain-cross-domain.cjs +79 -0
  97. package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
  98. package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
  99. package/lib/agents/mva/index.cjs +42 -0
  100. package/lib/agents/mva/six-hats-red-black.cjs +137 -0
  101. package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
  102. package/lib/agents/mva/test-all-six-agents.cjs +467 -0
  103. package/lib/conversation/operator.cjs +64 -0
  104. package/lib/conversation/operator.test.cjs +160 -0
  105. package/lib/core/breakthrough/canary.cjs +134 -0
  106. package/lib/core/breakthrough/canary.test.cjs +136 -0
  107. package/lib/core/breakthrough/detectors.cjs +359 -0
  108. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  109. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  110. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  111. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  112. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  113. package/lib/core/breakthrough/review-queue.cjs +154 -0
  114. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  115. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  116. package/lib/core/breakthrough/scanner.cjs +426 -0
  117. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  118. package/lib/core/breakthrough/schema.cjs +164 -0
  119. package/lib/core/breakthrough/schema.test.cjs +256 -0
  120. package/lib/core/breakthrough/scoring.cjs +293 -0
  121. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  122. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  123. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  124. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  125. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  126. package/lib/core/first-touch-version-stamper.cjs +113 -0
  127. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  128. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  129. package/lib/core/llm-name-suggester.cjs +194 -0
  130. package/lib/core/llm-name-suggester.test.cjs +132 -0
  131. package/lib/core/mva-agent-contract.cjs +170 -0
  132. package/lib/core/mva-agent-contract.test.cjs +169 -0
  133. package/lib/core/mva-budget.cjs +75 -0
  134. package/lib/core/mva-budget.test.cjs +68 -0
  135. package/lib/core/mva-classifier.cjs +370 -0
  136. package/lib/core/mva-classifier.test.cjs +248 -0
  137. package/lib/core/mva-deck-builder.cjs +452 -0
  138. package/lib/core/mva-deck-builder.test.cjs +287 -0
  139. package/lib/core/mva-detect.smoke.test.cjs +197 -0
  140. package/lib/core/mva-dispatcher.cjs +110 -0
  141. package/lib/core/mva-dispatcher.test.cjs +216 -0
  142. package/lib/core/mva-option-router.cjs +292 -0
  143. package/lib/core/mva-option-router.test.cjs +483 -0
  144. package/lib/core/mva-orchestrator.cjs +365 -0
  145. package/lib/core/mva-orchestrator.test.cjs +908 -0
  146. package/lib/core/mva-progressive-renderer.cjs +194 -0
  147. package/lib/core/mva-progressive-renderer.test.cjs +157 -0
  148. package/lib/core/mva-rule-linter.cjs +213 -0
  149. package/lib/core/mva-rule-linter.test.cjs +336 -0
  150. package/lib/core/mva-state.cjs +159 -0
  151. package/lib/core/mva-telemetry.cjs +58 -0
  152. package/lib/core/mva-telemetry.test.cjs +196 -0
  153. package/lib/core/mva-vercel-deploy.cjs +168 -0
  154. package/lib/core/mva-vercel-deploy.test.cjs +239 -0
  155. package/lib/core/navigation/dashboard-helpers.cjs +145 -0
  156. package/lib/core/navigation/edges.cjs +35 -0
  157. package/lib/core/navigation/memory-events.cjs +126 -0
  158. package/lib/core/navigation.cjs +11 -0
  159. package/lib/core/resolve-vercel-key.cjs +107 -0
  160. package/lib/core/resolve-vercel-key.test.cjs +137 -0
  161. package/lib/core/room-auto-create.cjs +318 -0
  162. package/lib/core/room-auto-create.test.cjs +198 -0
  163. package/lib/core/room-discard-cascade.cjs +225 -0
  164. package/lib/core/room-discard-cascade.test.cjs +135 -0
  165. package/lib/core/room-name-validator.cjs +132 -0
  166. package/lib/core/room-name-validator.test.cjs +156 -0
  167. package/lib/core/room-naming-selector.cjs +357 -0
  168. package/lib/core/room-naming-selector.test.cjs +277 -0
  169. package/lib/core/room-receipt-emit.cjs +63 -0
  170. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  171. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  172. package/lib/core/stale-copy-scanner.cjs +190 -0
  173. package/lib/core/state-aware-router.cjs +78 -0
  174. package/lib/core/telemetry/schema.cjs +168 -0
  175. package/lib/core/telemetry/schema.test.cjs +124 -0
  176. package/lib/core/telemetry/validator.cjs +197 -0
  177. package/lib/core/telemetry/validator.test.cjs +188 -0
  178. package/lib/core/telemetry/writer.cjs +141 -0
  179. package/lib/core/telemetry/writer.test.cjs +331 -0
  180. package/lib/core/terminal-capability.cjs +88 -0
  181. package/lib/core/venture-shape-nudge.cjs +163 -0
  182. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  183. package/lib/core/visual-ops.cjs +70 -2
  184. package/lib/hmi/selector-dispatcher.cjs +90 -1
  185. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  186. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  187. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  188. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  189. package/lib/memory/first-touch-version.test.cjs +198 -0
  190. package/lib/memory/help-coverage.test.cjs +108 -0
  191. package/lib/memory/help-renderer.test.cjs +145 -0
  192. package/lib/memory/palette-consistency.test.cjs +127 -0
  193. package/lib/memory/pending-tension-store.cjs +80 -0
  194. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  195. package/lib/memory/run-feynman-tests.cjs +240 -0
  196. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  197. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  198. package/lib/memory/soft-alias.test.cjs +144 -0
  199. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  200. package/lib/memory/state-aware-router.test.cjs +90 -0
  201. package/lib/memory/statusline-two-row.test.cjs +338 -0
  202. package/lib/memory/terminal-capability.test.cjs +155 -0
  203. package/lib/render/ROOM.md +74 -22
  204. package/lib/sessionstart/budget-compressor.cjs +130 -0
  205. package/lib/sessionstart/contributor-interface.cjs +134 -0
  206. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  207. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  208. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  209. package/lib/statusline/two-row-renderer.cjs +186 -0
  210. package/lib/statusline/version-resolver.cjs +81 -0
  211. package/package.json +1 -1
  212. package/references/visual/ROOM.md +55 -0
  213. package/references/visual/palette.json +54 -0
  214. package/skills/larry-personality/SKILL.md +34 -0
  215. package/skills/mva-pipeline/SKILL.md +129 -0
  216. package/skills/ui-system/SKILL.md +109 -1
  217. package/skills/ui-system/rules/dual-palette.md +156 -0
  218. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  219. package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
@@ -0,0 +1,251 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 120-03 Wave 2 Task 1 -- voice-scaffold unit tests (Tests 1-14).
4
+ *
5
+ * Per CONTEXT.md D-17 LOCKED VERBATIM 4-rule voice scaffold:
6
+ * Rule 1: Evidence requirement -- artifact ids OR graph edges cited
7
+ * Rule 2: Mechanism clause -- "by Y" user action
8
+ * Rule 3: Time anchor -- "in the last N hours/days" / "this week" / etc.
9
+ * Rule 4: No unbacked superlatives -- 6 forbidden + 3 frequency words guarded
10
+ *
11
+ * The auditor IS the structural enforcement (D-20 meta-principle).
12
+ */
13
+
14
+ const test = require('node:test');
15
+ const { strict: assert } = require('node:assert');
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+
19
+ const SCAFFOLD_PATH = path.resolve(__dirname, 'voice-scaffold.cjs');
20
+ const voiceScaffold = require('./voice-scaffold.cjs');
21
+ const {
22
+ composeBreakthroughVoiceLine,
23
+ auditVoiceLine,
24
+ citeEvidence,
25
+ includeMechanism,
26
+ anchorTime,
27
+ noUnbackedSuperlatives,
28
+ formatTimeAnchor,
29
+ VOICE_RULE_NAMES,
30
+ FORBIDDEN_SUPERLATIVES,
31
+ FREQUENCY_WORDS,
32
+ KIND_DISPLAY,
33
+ } = voiceScaffold;
34
+
35
+ // --- T1: VOICE_RULE_NAMES verbatim ---
36
+ test('T1: VOICE_RULE_NAMES verbatim + Object.freeze invariant', () => {
37
+ assert.deepEqual(VOICE_RULE_NAMES, [
38
+ 'evidence_requirement',
39
+ 'mechanism_clause',
40
+ 'time_anchor',
41
+ 'no_unbacked_superlatives',
42
+ ]);
43
+ assert.equal(VOICE_RULE_NAMES.length, 4);
44
+ // Object.freeze invariant
45
+ const before = VOICE_RULE_NAMES.slice();
46
+ try { VOICE_RULE_NAMES.push('extra'); } catch (_e) { /* strict mode throws */ }
47
+ assert.deepEqual(VOICE_RULE_NAMES.slice(0, 4), before);
48
+ });
49
+
50
+ // --- T2: FORBIDDEN_SUPERLATIVES verbatim ---
51
+ test('T2: FORBIDDEN_SUPERLATIVES contains the 6 D-17 rule 4 words verbatim', () => {
52
+ assert.deepEqual(FORBIDDEN_SUPERLATIVES, [
53
+ 'breakthrough', 'biggest', 'first', 'unprecedented', 'major', 'massive',
54
+ ]);
55
+ assert.equal(FORBIDDEN_SUPERLATIVES.length, 6);
56
+ });
57
+
58
+ // --- T3: composeBreakthroughVoiceLine happy path -- convergence ---
59
+ test('T3: composeBreakthroughVoiceLine produces D-17-compliant line for convergence', () => {
60
+ const twoDaysAgoMs = Date.now() - 2 * 86400 * 1000;
61
+ const bk = {
62
+ kind: 'convergence',
63
+ theme: 'operator-state machines',
64
+ artifact_ids: ['art:1', 'art:2', 'art:3', 'art:4'],
65
+ detected_at: twoDaysAgoMs,
66
+ };
67
+ const roomState = {
68
+ mechanism_phrase: 'by uploading the four notes on Mode A and Mode B',
69
+ };
70
+ const line = composeBreakthroughVoiceLine(bk, roomState);
71
+ assert.match(
72
+ line,
73
+ /^You're seeing a convergence on operator-state machines \(artifacts art:1, art:2, art:3, art:4\) -- by uploading the four notes on Mode A and Mode B -- in the last two days\.$/,
74
+ );
75
+ });
76
+
77
+ // --- T4: composeBreakthroughVoiceLine default mechanism when roomState absent ---
78
+ test('T4: composeBreakthroughVoiceLine uses structural default mechanism when roomState absent', () => {
79
+ const bk = {
80
+ kind: 'convergence',
81
+ theme: 'X',
82
+ artifact_ids: ['a1', 'a2', 'a3'],
83
+ detected_at: Date.now(),
84
+ };
85
+ const line = composeBreakthroughVoiceLine(bk, null);
86
+ // Default: "by adding N artifacts to this section"
87
+ assert.ok(/by adding 3 artifacts/.test(line),
88
+ 'expected default mechanism with artifact count, got: ' + line);
89
+ // The default must still pass auditor (rule 2 satisfied)
90
+ const audit = auditVoiceLine(line);
91
+ assert.equal(audit.ok, true, 'default-mechanism line must pass auditor, violations: ' + JSON.stringify(audit.violations));
92
+ });
93
+
94
+ // --- T5: auditVoiceLine -- D-17 rule 1 violation -- no evidence cite ---
95
+ test('T5: auditVoiceLine catches D-17 rule 1 violation (no evidence cite)', () => {
96
+ const bad = "You're seeing a convergence on X -- by uploading -- in the last two days.";
97
+ const r = auditVoiceLine(bad);
98
+ assert.equal(r.ok, false);
99
+ assert.ok(r.violations.indexOf('evidence_requirement') >= 0,
100
+ 'expected evidence_requirement violation, got: ' + JSON.stringify(r.violations));
101
+ });
102
+
103
+ // --- T6: auditVoiceLine -- D-17 rule 2 violation -- no mechanism ---
104
+ test('T6: auditVoiceLine catches D-17 rule 2 violation (no mechanism)', () => {
105
+ const bad = "You're seeing a convergence on X (artifacts a1, a2, a3) -- in the last two days.";
106
+ const r = auditVoiceLine(bad);
107
+ assert.equal(r.ok, false);
108
+ assert.ok(r.violations.indexOf('mechanism_clause') >= 0,
109
+ 'expected mechanism_clause violation, got: ' + JSON.stringify(r.violations));
110
+ });
111
+
112
+ // --- T7: auditVoiceLine -- D-17 rule 3 violation -- no time anchor ---
113
+ test('T7: auditVoiceLine catches D-17 rule 3 violation (no time anchor)', () => {
114
+ const bad = "You're seeing a convergence on X (artifacts a1, a2, a3) -- by uploading the notes.";
115
+ const r = auditVoiceLine(bad);
116
+ assert.equal(r.ok, false);
117
+ assert.ok(r.violations.indexOf('time_anchor') >= 0,
118
+ 'expected time_anchor violation, got: ' + JSON.stringify(r.violations));
119
+ });
120
+
121
+ // --- T8: auditVoiceLine -- D-17 rule 4 violation -- unbacked superlative ---
122
+ test('T8: auditVoiceLine catches D-17 rule 4 violation (unbacked superlative)', () => {
123
+ const bad = "You're seeing the BIGGEST breakthrough on X (artifacts a1, a2, a3) -- by uploading -- in the last hour.";
124
+ const r = auditVoiceLine(bad);
125
+ assert.equal(r.ok, false);
126
+ assert.ok(r.violations.indexOf('no_unbacked_superlatives') >= 0,
127
+ 'expected no_unbacked_superlatives violation, got: ' + JSON.stringify(r.violations));
128
+ });
129
+
130
+ // --- T9: auditVoiceLine -- D-17 rule 4 SATISFIED with numeric backing ---
131
+ test('T9: auditVoiceLine accepts D-17 rule 4 when superlative has numeric backing', () => {
132
+ const good = "You're seeing a convergence on X (artifacts a1, a2, a3) -- by uploading -- in the last hour. This is the highest differential score (0.78) in this room's history.";
133
+ const r = auditVoiceLine(good);
134
+ assert.equal(r.ok, true,
135
+ 'expected no violations, got: ' + JSON.stringify(r.violations));
136
+ });
137
+
138
+ // --- T10: auditVoiceLine happy path passes all 4 rules ---
139
+ test('T10: auditVoiceLine passes for T3-composed convergence line', () => {
140
+ const twoDaysAgoMs = Date.now() - 2 * 86400 * 1000;
141
+ const bk = {
142
+ kind: 'convergence',
143
+ theme: 'operator-state machines',
144
+ artifact_ids: ['art:1', 'art:2', 'art:3', 'art:4'],
145
+ detected_at: twoDaysAgoMs,
146
+ };
147
+ const roomState = {
148
+ mechanism_phrase: 'by uploading the four notes on Mode A and Mode B',
149
+ };
150
+ const line = composeBreakthroughVoiceLine(bk, roomState);
151
+ const r = auditVoiceLine(line);
152
+ assert.equal(r.ok, true,
153
+ 'composer + auditor must be aligned, violations: ' + JSON.stringify(r.violations));
154
+ assert.deepEqual(r.violations, []);
155
+ });
156
+
157
+ // --- T11: auditVoiceLine -- multiple violations reported ---
158
+ test('T11: auditVoiceLine reports multiple violations on maximally bad line', () => {
159
+ const veryBad = "You're seeing a convergence on X.";
160
+ const r = auditVoiceLine(veryBad);
161
+ assert.equal(r.ok, false);
162
+ // expect at least 3 violations: evidence, mechanism, time_anchor (no superlative present so rule 4 OK)
163
+ assert.ok(r.violations.indexOf('evidence_requirement') >= 0);
164
+ assert.ok(r.violations.indexOf('mechanism_clause') >= 0);
165
+ assert.ok(r.violations.indexOf('time_anchor') >= 0);
166
+ assert.ok(r.violations.length >= 3);
167
+ });
168
+
169
+ // --- T12: composeBreakthroughVoiceLine for EVERY kind passes auditor ---
170
+ test('T12: composeBreakthroughVoiceLine output passes auditor for every kind', () => {
171
+ const kinds = [
172
+ 'convergence',
173
+ 'contradiction_resolved',
174
+ 'cross_domain_analogy',
175
+ 'reverse_salient_closed',
176
+ ];
177
+ for (const kind of kinds) {
178
+ const bk = {
179
+ kind: kind,
180
+ theme: 'sample-theme',
181
+ artifact_ids: ['art:1', 'art:2', 'art:3'],
182
+ detected_at: Date.now() - 6 * 3600 * 1000, // 6 hours ago
183
+ };
184
+ const line = composeBreakthroughVoiceLine(bk, null);
185
+ const r = auditVoiceLine(line);
186
+ assert.equal(r.ok, true,
187
+ 'kind ' + kind + ': composer output failed audit, line: ' + line + ', violations: ' + JSON.stringify(r.violations));
188
+ }
189
+ });
190
+
191
+ // --- T13: Canon Part 8 source-grep -- zero Brain coupling ---
192
+ test('T13: Canon Part 8 source-grep -- voice-scaffold.cjs has zero Brain coupling', () => {
193
+ const src = fs.readFileSync(SCAFFOLD_PATH, 'utf8');
194
+ // brain-client require
195
+ assert.ok(!/require\(.+brain-client/.test(src),
196
+ 'voice-scaffold must not require brain-client');
197
+ // direct fetch to brain.mindrian
198
+ assert.ok(!/fetch.+brain\.mindrian/.test(src),
199
+ 'voice-scaffold must not fetch brain.mindrian');
200
+ // cross-room aggregation
201
+ assert.ok(!/cross-room/i.test(src.split('\n').filter(l => !l.match(/^\s*\/\//)).join('\n')) ||
202
+ src.indexOf('Canon Part 8') >= 0,
203
+ 'cross-room mentions require Canon Part 8 context comment');
204
+ });
205
+
206
+ // --- T14: em-dash invariant ---
207
+ test('T14: zero U+2014 em-dashes in voice-scaffold.cjs', () => {
208
+ const src = fs.readFileSync(SCAFFOLD_PATH, 'utf8');
209
+ const idx = src.indexOf('—');
210
+ assert.equal(idx, -1, 'em-dash found at index ' + idx);
211
+ });
212
+
213
+ // --- Bonus: formatTimeAnchor buckets ---
214
+ test('T15: formatTimeAnchor returns expected buckets', () => {
215
+ const now = Date.now();
216
+ assert.equal(formatTimeAnchor(now - 30 * 60 * 1000, now), 'in the last hour');
217
+ assert.equal(formatTimeAnchor(now - 4 * 3600 * 1000, now), 'in the last 4 hours');
218
+ assert.equal(formatTimeAnchor(now - 12 * 3600 * 1000, now), 'today');
219
+ assert.equal(formatTimeAnchor(now - 36 * 3600 * 1000, now), 'in the last day');
220
+ assert.equal(formatTimeAnchor(now - 2 * 86400 * 1000, now), 'in the last two days');
221
+ assert.equal(formatTimeAnchor(now - 5 * 86400 * 1000, now), 'this week');
222
+ assert.equal(formatTimeAnchor(now - 10 * 86400 * 1000, now), 'in the last two weeks');
223
+ });
224
+
225
+ // --- Bonus: rule predicates exposed ---
226
+ test('T16: rule predicates exposed individually for unit-level testing', () => {
227
+ assert.equal(typeof citeEvidence, 'function');
228
+ assert.equal(typeof includeMechanism, 'function');
229
+ assert.equal(typeof anchorTime, 'function');
230
+ assert.equal(typeof noUnbackedSuperlatives, 'function');
231
+ // citeEvidence positive cases
232
+ assert.equal(citeEvidence('foo (artifacts a1, a2)'), true);
233
+ assert.equal(citeEvidence('foo [[wiki-link]]'), true);
234
+ assert.equal(citeEvidence('foo (see edges X, Y)'), true);
235
+ // citeEvidence negative
236
+ assert.equal(citeEvidence('foo bar baz'), false);
237
+ });
238
+
239
+ // --- Bonus: FREQUENCY_WORDS exposed ---
240
+ test('T17: FREQUENCY_WORDS exposed verbatim', () => {
241
+ assert.deepEqual(FREQUENCY_WORDS, ['consistent', 'repeated', 'always']);
242
+ });
243
+
244
+ // --- Bonus: KIND_DISPLAY exposed ---
245
+ test('T18: KIND_DISPLAY exposed with the 4 detector kinds', () => {
246
+ assert.equal(typeof KIND_DISPLAY, 'object');
247
+ assert.ok(KIND_DISPLAY.convergence);
248
+ assert.ok(KIND_DISPLAY.contradiction_resolved);
249
+ assert.ok(KIND_DISPLAY.cross_domain_analogy);
250
+ assert.ok(KIND_DISPLAY.reverse_salient_closed);
251
+ });
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
4
+ *
5
+ * first-touch-version-stamper.cjs
6
+ * --------------------------------
7
+ * Phase 121.5-05 Sub-plan F (SEED-007 absorption).
8
+ *
9
+ * Pure function module that returns the canonical version stamp for each
10
+ * first-touch surface (banner / splash / onboard / sessionstart /
11
+ * operator-update / larry-extended). The string the user sees -- "MindrianOS
12
+ * v<version>" -- is derived once, here, so the per-surface emission stays in
13
+ * lock-step with .claude-plugin/plugin.json.
14
+ *
15
+ * Source finding (SEED-007, 2026-05-09): a v1.12.0 "Three ways to do this"
16
+ * greeting was rendering on a stale Windows install with NO version-of-record
17
+ * visible. The user could not answer "what version am I running?" by LOOKING
18
+ * at the terminal. This module is the canonical answer to that question.
19
+ *
20
+ * Canon Part 7 (Reuse Before Build): the version-of-record source is
21
+ * .claude-plugin/plugin.json -- the same one scripts/banner already reads via
22
+ * lib/core/platform.cjs::readPluginJsonVersion. This module wraps that source
23
+ * with the per-surface rendering contract; it does NOT introduce a second
24
+ * version-of-record surface.
25
+ *
26
+ * Canon Part 8: local-only; no fetch, no Brain, no telemetry egress.
27
+ *
28
+ * No emoji. No em-dashes. ASCII-clean source.
29
+ */
30
+
31
+ 'use strict';
32
+
33
+ const fs = require('fs');
34
+ const path = require('path');
35
+
36
+ const SURFACES_PATH = path.join(__dirname, '..', '..', 'data', 'first-touch-surfaces.json');
37
+ const PLUGIN_JSON_PATH = path.join(__dirname, '..', '..', '.claude-plugin', 'plugin.json');
38
+
39
+ const KNOWN_SURFACES = new Set([
40
+ 'banner',
41
+ 'splash',
42
+ 'onboard',
43
+ 'sessionstart',
44
+ 'operator-update',
45
+ 'larry-extended',
46
+ ]);
47
+
48
+ function readSurfaces() {
49
+ return JSON.parse(fs.readFileSync(SURFACES_PATH, 'utf8'));
50
+ }
51
+
52
+ function resolveCurrentVersion() {
53
+ try {
54
+ return JSON.parse(fs.readFileSync(PLUGIN_JSON_PATH, 'utf8')).version;
55
+ } catch (_e) {
56
+ return 'unknown';
57
+ }
58
+ }
59
+
60
+ function stampVersion(surfaceId, version) {
61
+ if (!KNOWN_SURFACES.has(surfaceId)) {
62
+ throw new Error('unknown_surface: ' + surfaceId);
63
+ }
64
+ const v = version || resolveCurrentVersion();
65
+ switch (surfaceId) {
66
+ case 'banner':
67
+ return 'MindrianOS v' + v;
68
+ case 'splash':
69
+ return 'MindrianOS v' + v + ' -- conversation as the product surface';
70
+ case 'onboard':
71
+ return 'Welcome to MindrianOS v' + v + '. Let me show you around.';
72
+ case 'sessionstart':
73
+ case 'operator-update':
74
+ case 'larry-extended':
75
+ return 'MindrianOS v' + v;
76
+ default:
77
+ // unreachable: KNOWN_SURFACES gates entry
78
+ throw new Error('unknown_surface: ' + surfaceId);
79
+ }
80
+ }
81
+
82
+ // U+2014 EM DASH detector -- the no-em-dash hard rule scanner uses this
83
+ // (CLAUDE.md / MEMORY feedback_no_emdashes). Written as an escape so this
84
+ // source file stays ASCII-clean.
85
+ const EM_DASH = '\u2014';
86
+
87
+ function hasEmDash(text) {
88
+ return typeof text === 'string' && text.indexOf(EM_DASH) >= 0;
89
+ }
90
+
91
+ module.exports = {
92
+ stampVersion,
93
+ resolveCurrentVersion,
94
+ readSurfaces,
95
+ hasEmDash,
96
+ KNOWN_SURFACES,
97
+ };
98
+
99
+ // Allow CLI invocation: `node lib/core/first-touch-version-stamper.cjs banner`
100
+ if (require.main === module) {
101
+ const surfaceArg = process.argv[2];
102
+ if (!surfaceArg) {
103
+ process.stdout.write(stampVersion('banner') + '\n');
104
+ process.exit(0);
105
+ }
106
+ try {
107
+ process.stdout.write(stampVersion(surfaceArg) + '\n');
108
+ process.exit(0);
109
+ } catch (e) {
110
+ process.stderr.write('first-touch-version-stamper: ' + e.message + '\n');
111
+ process.exit(2);
112
+ }
113
+ }
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 119-02 -- Larry thinness-acknowledgment voice + thin-finding detector.
4
+ *
5
+ * Per CONTEXT.md D-05: when Phase 117's composeAutoExploreFinding returns
6
+ * thin output (null OR <= 1 finding), the room is still scaffolded but
7
+ * Larry's voice acknowledges thinness honestly via the verbatim copy below.
8
+ *
9
+ * The verbatim copy is LOCKED per .planning/phases/119-room-as-receipt-invariant/
10
+ * 119-02-PLAN.md objective block (lines 89-93). Do NOT paraphrase.
11
+ *
12
+ * Em-dash HARD RULE: uses `--` (two ASCII hyphens) NEVER `-` (U+2014).
13
+ * Smart-quote BAN: uses ASCII apostrophe `'` (0x27) only -- never `’`.
14
+ *
15
+ * Canon Part 8 invariant: pure-local module; no Brain call, no telemetry
16
+ * egress; the voice line is a static string with zero outbound surface.
17
+ * Canon Part 10 sub-claim 1: Larry IS the product -- the voice line is
18
+ * the conversational anchor that makes "rooms are receipts" feel honest
19
+ * rather than apologetic when the source material is thin (D-05).
20
+ */
21
+
22
+ // The verbatim D-05 voice line. Locked per the plan. Length: 152 chars (ASCII).
23
+ // Composed via two string literals (concat) for readability; the runtime
24
+ // value is one continuous string.
25
+ const THINNESS_VOICE_LINE =
26
+ "I made a room around this -- it's mostly empty until we have more to work with. " +
27
+ "Want to keep going and see what fills in?";
28
+
29
+ /**
30
+ * Decide whether the room should surface the thinness acknowledgment to the user.
31
+ *
32
+ * Per .planning/phases/119-room-as-receipt-invariant/119-02-PLAN.md Task 1
33
+ * <behavior> Test 6: D-05 threshold is "2+ findings = substantive; <=1 finding
34
+ * = thin". The detector treats null / undefined / non-object / missing
35
+ * findings array / empty findings array / single-finding all as thin.
36
+ *
37
+ * @param {object|null} autoExploreFinding the JSON shape Phase 117 writes
38
+ * to .mindrian/auto-explore-<material_id>.json
39
+ * (shape: { material_id, findings: [...], composed_at_ms })
40
+ * @returns {boolean} true if Larry should render the thinness voice line
41
+ */
42
+ function shouldAcknowledgeThinness(autoExploreFinding) {
43
+ if (autoExploreFinding === null || autoExploreFinding === undefined) return true;
44
+ if (typeof autoExploreFinding !== 'object') return true;
45
+ const findings = Array.isArray(autoExploreFinding.findings) ? autoExploreFinding.findings : [];
46
+ return findings.length <= 1;
47
+ }
48
+
49
+ /**
50
+ * Return the verbatim D-05 thinness voice line. Single source of truth -- callers
51
+ * MUST use this function (or the THINNESS_VOICE_LINE constant) rather than
52
+ * inlining the string. This avoids cross-file string drift.
53
+ *
54
+ * @returns {string} the locked verbatim voice line
55
+ */
56
+ function voiceLine() {
57
+ return THINNESS_VOICE_LINE;
58
+ }
59
+
60
+ module.exports = {
61
+ THINNESS_VOICE_LINE: THINNESS_VOICE_LINE,
62
+ shouldAcknowledgeThinness: shouldAcknowledgeThinness,
63
+ voiceLine: voiceLine,
64
+ };
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 119-02 Task 1 -- test suite for larry-thinness-acknowledgment.cjs.
4
+ *
5
+ * Verifies (per CONTEXT.md D-05 verbatim copy lock + the 11 behavior tests in
6
+ * .planning/phases/119-room-as-receipt-invariant/119-02-PLAN.md Task 1):
7
+ * 1. THINNESS_VOICE_LINE returns exact verbatim string.
8
+ * 2. voiceLine() returns the same string as THINNESS_VOICE_LINE.
9
+ * 3. shouldAcknowledgeThinness(null) -> true.
10
+ * 4. shouldAcknowledgeThinness({findings:[]}) -> true.
11
+ * 5. shouldAcknowledgeThinness({findings:[a,b]}) -> false (substantive).
12
+ * 6. shouldAcknowledgeThinness({findings:[a]}) -> true (D-05 threshold 2+).
13
+ * 7-10. Template files exist + valid frontmatter (verified by Task 2 + bash gate).
14
+ * 11. No smart quotes / em-dashes in the verbatim string (ASCII only).
15
+ */
16
+
17
+ const assert = require('node:assert');
18
+ const test = require('node:test');
19
+
20
+ const {
21
+ THINNESS_VOICE_LINE,
22
+ shouldAcknowledgeThinness,
23
+ voiceLine,
24
+ } = require('./larry-thinness-acknowledgment.cjs');
25
+
26
+ const EXPECTED =
27
+ "I made a room around this -- it's mostly empty until we have more to work with. " +
28
+ "Want to keep going and see what fills in?";
29
+
30
+ test('Test 1: THINNESS_VOICE_LINE is the verbatim D-05 string', () => {
31
+ assert.strictEqual(THINNESS_VOICE_LINE, EXPECTED);
32
+ });
33
+
34
+ test('Test 2: voiceLine() returns same string as THINNESS_VOICE_LINE (single source of truth)', () => {
35
+ assert.strictEqual(voiceLine(), THINNESS_VOICE_LINE);
36
+ });
37
+
38
+ test('Test 3: shouldAcknowledgeThinness(null) returns true', () => {
39
+ assert.strictEqual(shouldAcknowledgeThinness(null), true);
40
+ });
41
+
42
+ test('Test 4: shouldAcknowledgeThinness(undefined) returns true', () => {
43
+ assert.strictEqual(shouldAcknowledgeThinness(undefined), true);
44
+ });
45
+
46
+ test('Test 5: shouldAcknowledgeThinness({findings: []}) returns true (empty findings)', () => {
47
+ assert.strictEqual(shouldAcknowledgeThinness({ findings: [] }), true);
48
+ });
49
+
50
+ test('Test 6: shouldAcknowledgeThinness({findings: [a, b]}) returns false (substantive, 2+ findings)', () => {
51
+ const finding = {
52
+ findings: [
53
+ { source_pipeline: 'whitespace', hsi_score: 0.7 },
54
+ { source_pipeline: 'cross-domain', hsi_score: 0.6 },
55
+ ],
56
+ };
57
+ assert.strictEqual(shouldAcknowledgeThinness(finding), false);
58
+ });
59
+
60
+ test('Test 7: shouldAcknowledgeThinness({findings: [a]}) returns true (1-finding edge case, D-05 threshold)', () => {
61
+ const finding = {
62
+ findings: [{ source_pipeline: 'whitespace', hsi_score: 0.3 }],
63
+ };
64
+ assert.strictEqual(shouldAcknowledgeThinness(finding), true);
65
+ });
66
+
67
+ test('Test 8: shouldAcknowledgeThinness on non-object returns true (defensive)', () => {
68
+ assert.strictEqual(shouldAcknowledgeThinness('not an object'), true);
69
+ assert.strictEqual(shouldAcknowledgeThinness(42), true);
70
+ });
71
+
72
+ test('Test 9: shouldAcknowledgeThinness on object without findings array returns true', () => {
73
+ assert.strictEqual(shouldAcknowledgeThinness({}), true);
74
+ assert.strictEqual(shouldAcknowledgeThinness({ findings: null }), true);
75
+ });
76
+
77
+ test('Test 10: THINNESS_VOICE_LINE has no em-dash (em-dash HARD RULE)', () => {
78
+ // Construct via charCode so this test source itself contains no literal
79
+ // em-dash (which would self-trip the shell-harness em-dash invariant).
80
+ const EMDASH = String.fromCharCode(0x2014);
81
+ assert.strictEqual(THINNESS_VOICE_LINE.indexOf(EMDASH), -1, 'em-dash U+2014 present');
82
+ });
83
+
84
+ test('Test 11: THINNESS_VOICE_LINE has no smart-quote characters (ASCII only)', () => {
85
+ // Right single quote U+2019, left single quote U+2018, right double U+201D, left double U+201C
86
+ assert.strictEqual(THINNESS_VOICE_LINE.indexOf('’'), -1, 'right single quote U+2019 present');
87
+ assert.strictEqual(THINNESS_VOICE_LINE.indexOf('‘'), -1, 'left single quote U+2018 present');
88
+ assert.strictEqual(THINNESS_VOICE_LINE.indexOf('”'), -1, 'right double quote U+201D present');
89
+ assert.strictEqual(THINNESS_VOICE_LINE.indexOf('“'), -1, 'left double quote U+201C present');
90
+ });
91
+
92
+ test('Test 12: THINNESS_VOICE_LINE contains the locked phrase fragments', () => {
93
+ assert.ok(THINNESS_VOICE_LINE.includes('I made a room around this'), 'opener missing');
94
+ assert.ok(THINNESS_VOICE_LINE.includes("it's mostly empty"), 'middle (ASCII apostrophe) missing');
95
+ assert.ok(THINNESS_VOICE_LINE.includes('Want to keep going'), 'continuation invite missing');
96
+ assert.ok(THINNESS_VOICE_LINE.includes('see what fills in?'), 'closing question missing');
97
+ });