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

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 (199) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.mcp.json +6 -1
  3. package/CHANGELOG.md +31 -0
  4. package/README.md +51 -56
  5. package/bin/mindrian-brain-mcp-client.cjs +152 -0
  6. package/commands/act.md +1 -0
  7. package/commands/admin.md +1 -0
  8. package/commands/analyze-needs.md +2 -0
  9. package/commands/analyze-systems.md +2 -0
  10. package/commands/analyze-timing.md +2 -0
  11. package/commands/auto-explore.md +2 -0
  12. package/commands/beautiful-question.md +2 -0
  13. package/commands/brain-derive.md +2 -0
  14. package/commands/build-knowledge.md +2 -0
  15. package/commands/build-thesis.md +2 -0
  16. package/commands/causal.md +2 -0
  17. package/commands/challenge-assumptions.md +2 -0
  18. package/commands/compare-ventures.md +2 -0
  19. package/commands/dashboard.md +2 -1
  20. package/commands/deep-grade.md +2 -0
  21. package/commands/diagnose.md +21 -1
  22. package/commands/diagnostics.md +14 -3
  23. package/commands/doctor.md +6 -2
  24. package/commands/dogfood-flush.md +92 -0
  25. package/commands/dominant-designs.md +2 -0
  26. package/commands/explain-decision.md +2 -0
  27. package/commands/explore-domains.md +2 -0
  28. package/commands/explore-futures.md +2 -0
  29. package/commands/explore-trends.md +2 -0
  30. package/commands/export.md +1 -0
  31. package/commands/feynman-timeline-refresh.md +2 -0
  32. package/commands/file-meeting.md +2 -0
  33. package/commands/find-analogies.md +1 -0
  34. package/commands/find-bottlenecks.md +2 -0
  35. package/commands/find-connections.md +2 -0
  36. package/commands/funding.md +1 -0
  37. package/commands/grade.md +2 -0
  38. package/commands/graph.md +1 -0
  39. package/commands/hat-briefing.md +1 -0
  40. package/commands/heal.md +22 -170
  41. package/commands/help.md +54 -334
  42. package/commands/hmi-status.md +23 -144
  43. package/commands/jtbd.md +1 -0
  44. package/commands/leadership.md +2 -0
  45. package/commands/lean-canvas.md +2 -0
  46. package/commands/macro-trends.md +2 -0
  47. package/commands/map-unknowns.md +2 -0
  48. package/commands/memory.md +1 -0
  49. package/commands/models.md +1 -0
  50. package/commands/mos-reason.md +2 -0
  51. package/commands/mos.md +139 -0
  52. package/commands/mullins.md +2 -0
  53. package/commands/mva-brief.md +2 -0
  54. package/commands/mva-option.md +2 -0
  55. package/commands/new-project.md +2 -0
  56. package/commands/onboard.md +20 -7
  57. package/commands/operator.md +1 -0
  58. package/commands/opportunities.md +1 -0
  59. package/commands/organize.md +22 -469
  60. package/commands/persona.md +1 -0
  61. package/commands/pipeline.md +2 -0
  62. package/commands/present.md +1 -0
  63. package/commands/publish.md +2 -0
  64. package/commands/query.md +24 -102
  65. package/commands/radar.md +2 -0
  66. package/commands/reanalyze.md +1 -0
  67. package/commands/research.md +2 -0
  68. package/commands/room.md +2 -0
  69. package/commands/rooms.md +1 -0
  70. package/commands/root-cause.md +2 -0
  71. package/commands/rs-experts.md +1 -0
  72. package/commands/rs-explain.md +1 -0
  73. package/commands/rs-fetch.md +1 -0
  74. package/commands/rs-thesis.md +1 -0
  75. package/commands/scenario-plan.md +2 -0
  76. package/commands/scheduled-tasks.md +1 -0
  77. package/commands/score-innovation.md +2 -0
  78. package/commands/scout.md +1 -0
  79. package/commands/setup.md +2 -0
  80. package/commands/snapshot.md +2 -0
  81. package/commands/speakers.md +1 -0
  82. package/commands/splash.md +5 -2
  83. package/commands/status.md +1 -0
  84. package/commands/structure-argument.md +2 -0
  85. package/commands/suggest-next.md +2 -0
  86. package/commands/systems-thinking.md +2 -0
  87. package/commands/think-hats.md +2 -0
  88. package/commands/update.md +2 -0
  89. package/commands/user-needs.md +2 -0
  90. package/commands/validate.md +2 -0
  91. package/commands/value-proposition.md +2 -0
  92. package/commands/vault.md +2 -0
  93. package/commands/visualize.md +24 -29
  94. package/commands/whitespace.md +2 -1
  95. package/commands/wiki.md +1 -0
  96. package/hooks/hooks.json +22 -88
  97. package/lib/agents/auto-explore-agent.cjs +82 -0
  98. package/lib/core/breakthrough/canary.cjs +134 -0
  99. package/lib/core/breakthrough/canary.test.cjs +136 -0
  100. package/lib/core/breakthrough/detectors.cjs +359 -0
  101. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  102. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  103. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  104. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  105. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  106. package/lib/core/breakthrough/review-queue.cjs +154 -0
  107. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  108. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  109. package/lib/core/breakthrough/scanner.cjs +426 -0
  110. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  111. package/lib/core/breakthrough/schema.cjs +164 -0
  112. package/lib/core/breakthrough/schema.test.cjs +256 -0
  113. package/lib/core/breakthrough/scoring.cjs +293 -0
  114. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  115. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  116. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  117. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  118. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  119. package/lib/core/directive-envelope.cjs +175 -0
  120. package/lib/core/directive-envelope.test.cjs +225 -0
  121. package/lib/core/doctor/class-m-brain-smoke.cjs +278 -0
  122. package/lib/core/doctor/class-m-brain-smoke.test.cjs +310 -0
  123. package/lib/core/first-touch-version-stamper.cjs +113 -0
  124. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  125. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  126. package/lib/core/llm-name-suggester.cjs +194 -0
  127. package/lib/core/llm-name-suggester.test.cjs +132 -0
  128. package/lib/core/mcp-profiles.cjs +1 -1
  129. package/lib/core/migration-snapshot.cjs +172 -0
  130. package/lib/core/migration-snapshot.test.cjs +174 -0
  131. package/lib/core/mindrian-brain-shim.test.cjs +214 -0
  132. package/lib/core/mva-orchestrator.cjs +41 -0
  133. package/lib/core/mva-telemetry.cjs +31 -143
  134. package/lib/core/navigation/edges.cjs +35 -0
  135. package/lib/core/navigation/memory-events.cjs +126 -0
  136. package/lib/core/room-auto-create.cjs +318 -0
  137. package/lib/core/room-auto-create.test.cjs +198 -0
  138. package/lib/core/room-discard-cascade.cjs +225 -0
  139. package/lib/core/room-discard-cascade.test.cjs +135 -0
  140. package/lib/core/room-name-validator.cjs +132 -0
  141. package/lib/core/room-name-validator.test.cjs +156 -0
  142. package/lib/core/room-naming-selector.cjs +357 -0
  143. package/lib/core/room-naming-selector.test.cjs +277 -0
  144. package/lib/core/room-receipt-emit.cjs +63 -0
  145. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  146. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  147. package/lib/core/rs-nl-to-query.cjs +1 -1
  148. package/lib/core/stale-copy-scanner.cjs +190 -0
  149. package/lib/core/state-aware-router.cjs +78 -0
  150. package/lib/core/telemetry/schema.cjs +168 -0
  151. package/lib/core/telemetry/schema.test.cjs +124 -0
  152. package/lib/core/telemetry/validator.cjs +200 -0
  153. package/lib/core/telemetry/validator.test.cjs +188 -0
  154. package/lib/core/telemetry/writer.cjs +141 -0
  155. package/lib/core/telemetry/writer.test.cjs +331 -0
  156. package/lib/core/terminal-capability.cjs +88 -0
  157. package/lib/core/tier0-messaging.cjs +109 -0
  158. package/lib/core/tier0-messaging.test.cjs +218 -0
  159. package/lib/core/venture-shape-nudge.cjs +163 -0
  160. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  161. package/lib/core/visual-ops.cjs +70 -2
  162. package/lib/hmi/selector-dispatcher.cjs +90 -1
  163. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  164. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  165. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  166. package/lib/memory/brain-derivation-graceful-degradation.test.cjs +2 -2
  167. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  168. package/lib/memory/first-touch-version.test.cjs +198 -0
  169. package/lib/memory/help-coverage.test.cjs +108 -0
  170. package/lib/memory/help-renderer.test.cjs +145 -0
  171. package/lib/memory/mos-status-renderer.test.cjs +2 -2
  172. package/lib/memory/navigation-engine-core.test.cjs +1 -1
  173. package/lib/memory/palette-consistency.test.cjs +127 -0
  174. package/lib/memory/pending-tension-store.cjs +80 -0
  175. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  176. package/lib/memory/run-feynman-tests.cjs +223 -0
  177. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  178. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  179. package/lib/memory/soft-alias.test.cjs +144 -0
  180. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  181. package/lib/memory/state-aware-router.test.cjs +90 -0
  182. package/lib/memory/statusline-two-row.test.cjs +338 -0
  183. package/lib/memory/terminal-capability.test.cjs +155 -0
  184. package/lib/render/ROOM.md +74 -22
  185. package/lib/sessionstart/budget-compressor.cjs +130 -0
  186. package/lib/sessionstart/contributor-interface.cjs +134 -0
  187. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  188. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  189. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  190. package/lib/statusline/two-row-renderer.cjs +186 -0
  191. package/lib/statusline/version-resolver.cjs +81 -0
  192. package/package.json +1 -1
  193. package/references/visual/ROOM.md +55 -0
  194. package/references/visual/palette.json +54 -0
  195. package/skills/larry-personality/SKILL.md +34 -0
  196. package/skills/ui-system/SKILL.md +109 -1
  197. package/skills/ui-system/rules/dual-palette.md +156 -0
  198. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  199. 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,175 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Phase 127-00 (Task 1) -- DirectiveEnvelope wrapper module.
5
+ *
6
+ * Builds the typed packet returned by `brain_ask` (via the stdio shim) and
7
+ * consumed by Larry's surface. CANONICAL DEFAULT mode: GUIDED.
8
+ *
9
+ * Default: GUIDED per feedback_larry_pedagogical_guided_first.md (HARD RULE).
10
+ * AUTONOMOUS only on explicit user invitation, non-judgment prep, or explicit
11
+ * "run" command. Cold start ALWAYS forces GUIDED. Mature-room commit-phase
12
+ * gates default to HYBRID.
13
+ *
14
+ * Canon parts: 2 (Larry pedagogy intrinsic, not Brain-dependent),
15
+ * 3 (Decision Gate F-shape next_gate handoff), 7 (pure data shaping,
16
+ * zero network surface; envelope wrapper sits ABOVE brain-client).
17
+ *
18
+ * Constraints: CJS, Node built-ins only, zero new runtime deps, ZERO network
19
+ * surface (no fetch/http/brain-client require). HARD RULE: no em-dashes.
20
+ */
21
+
22
+ const DEFAULT_MODE = 'GUIDED';
23
+
24
+ /**
25
+ * Mode classifier. Signal precedence (first match wins; explicit user signals
26
+ * beat heuristic context):
27
+ * 1. user_said_just_tell_me | user_said_bottom_line -> AUTONOMOUS
28
+ * 2. is_first_material | is_cold_start -> GUIDED (forced)
29
+ * 3. session_count>=8 + room_mature + in_commit_phase -> HYBRID
30
+ * 4. is_prep_work && !requires_judgment -> AUTONOMOUS
31
+ * 5. user_explicitly_said_run -> AUTONOMOUS
32
+ * 6. default -> GUIDED
33
+ *
34
+ * Non-object input is coerced to empty signals (default GUIDED applies).
35
+ * @param {object} signals
36
+ * @returns {{mode: string, rationale: string}}
37
+ */
38
+ function selectMode(signals) {
39
+ const s = (signals && typeof signals === 'object') ? signals : {};
40
+
41
+ // 1. Explicit user invitation (highest precedence).
42
+ if (s.user_said_just_tell_me || s.user_said_bottom_line) {
43
+ return { mode: 'AUTONOMOUS', rationale: 'explicit_user_invitation' };
44
+ }
45
+
46
+ // 2. Cold start FORCES GUIDED (per feedback_larry_pedagogical_guided_first.md).
47
+ if (s.is_first_material || s.is_cold_start) {
48
+ return { mode: 'GUIDED', rationale: 'cold_start_never_autonomous' };
49
+ }
50
+
51
+ // 3. Mature-room commit-phase: HYBRID for non-trivial methodology run.
52
+ if (typeof s.session_count === 'number' && s.session_count >= 8
53
+ && s.room_mature && s.in_commit_phase) {
54
+ return { mode: 'HYBRID', rationale: 'mature_room_commit_gate' };
55
+ }
56
+
57
+ // 4. Non-judgment prep work.
58
+ if (s.is_prep_work && !s.requires_judgment) {
59
+ return { mode: 'AUTONOMOUS', rationale: 'non_judgment_prep_work' };
60
+ }
61
+
62
+ // 5. Explicit execute command.
63
+ if (s.user_explicitly_said_run) {
64
+ return { mode: 'AUTONOMOUS', rationale: 'explicit_execute_command' };
65
+ }
66
+
67
+ // 6. Default: GUIDED (Larry pedagogical canon).
68
+ return { mode: 'GUIDED', rationale: 'default_guided_pedagogical_canon' };
69
+ }
70
+
71
+ // The 3 documented user-override keys. Frozen template; envelope build
72
+ // shallow-copies so caller mutations do not leak across envelopes.
73
+ const USER_OVERRIDE_TEMPLATE = Object.freeze({
74
+ 'just tell me': 'switch to AUTONOMOUS, run framework end-to-end',
75
+ 'let me think': 'switch to GUIDED, ask the next reframing question',
76
+ 'stop': 'halt, return control',
77
+ });
78
+
79
+ /**
80
+ * Build directive body. Pass through Brain-supplied `directive` when present;
81
+ * otherwise produce a mode-appropriate empty scaffold.
82
+ * @param {object|null} brainResponse
83
+ * @param {string} mode
84
+ * @returns {object}
85
+ */
86
+ function buildDirective(brainResponse, mode) {
87
+ // Pass-through if Brain already supplied a typed directive payload.
88
+ if (brainResponse && typeof brainResponse === 'object'
89
+ && brainResponse.directive && typeof brainResponse.directive === 'object') {
90
+ return brainResponse.directive;
91
+ }
92
+
93
+ if (mode === 'AUTONOMOUS') {
94
+ return { autonomous: { plan: [], framework: null } };
95
+ }
96
+ if (mode === 'HYBRID') {
97
+ return {
98
+ hybrid: {
99
+ autonomous_prep: [],
100
+ guided_commit: { questions: [] },
101
+ framework: null,
102
+ },
103
+ };
104
+ }
105
+ // GUIDED default scaffold.
106
+ return { guided: { questions: [], framework: null, stage: null } };
107
+ }
108
+
109
+ /**
110
+ * Build next_gate handoff. Brain-supplied next_gate passes through when its
111
+ * sub_shape matches F.[1-5]; otherwise defaults to F.1 Next Move (Canon Part 3).
112
+ * @param {object|null} brainResponse
113
+ * @returns {{sub_shape: string, options: Array}}
114
+ */
115
+ function buildNextGate(brainResponse) {
116
+ if (brainResponse && typeof brainResponse === 'object'
117
+ && brainResponse.next_gate && typeof brainResponse.next_gate === 'object') {
118
+ const ng = brainResponse.next_gate;
119
+ if (typeof ng.sub_shape === 'string' && /^F\.[1-5]$/.test(ng.sub_shape)) {
120
+ return {
121
+ sub_shape: ng.sub_shape,
122
+ options: Array.isArray(ng.options) ? ng.options : [],
123
+ };
124
+ }
125
+ }
126
+ return { sub_shape: 'F.1', options: [] };
127
+ }
128
+
129
+ /**
130
+ * Wrap a Brain response (or null) in a typed DirectiveEnvelope.
131
+ * brainResponse null/undefined => Tier-0 sentinel (mode GUIDED, rationale
132
+ * "brain_unreachable", directive.guided.stage "tier_0_brain_unreachable").
133
+ * Otherwise selectMode applies + directive/next_gate pass through when supplied.
134
+ * @param {object|null} brainResponse
135
+ * @param {object} signals
136
+ * @returns {object} DirectiveEnvelope per CAPABILITY-MAP spec.
137
+ */
138
+ function wrapDirective(brainResponse, signals) {
139
+ if (brainResponse === null || brainResponse === undefined) {
140
+ // Tier-0 sentinel.
141
+ return {
142
+ packet_version: '1.0',
143
+ packet_type: 'DirectiveEnvelope',
144
+ mode: 'GUIDED',
145
+ mode_rationale: 'brain_unreachable',
146
+ directive: {
147
+ guided: {
148
+ questions: [],
149
+ framework: null,
150
+ stage: 'tier_0_brain_unreachable',
151
+ },
152
+ },
153
+ user_override: Object.assign({}, USER_OVERRIDE_TEMPLATE),
154
+ next_gate: { sub_shape: 'F.1', options: [] },
155
+ };
156
+ }
157
+
158
+ const { mode, rationale } = selectMode(signals || {});
159
+
160
+ return {
161
+ packet_version: '1.0',
162
+ packet_type: 'DirectiveEnvelope',
163
+ mode: mode,
164
+ mode_rationale: rationale,
165
+ directive: buildDirective(brainResponse, mode),
166
+ user_override: Object.assign({}, USER_OVERRIDE_TEMPLATE),
167
+ next_gate: buildNextGate(brainResponse),
168
+ };
169
+ }
170
+
171
+ module.exports = {
172
+ DEFAULT_MODE,
173
+ selectMode,
174
+ wrapDirective,
175
+ };
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
6
+ *
7
+ * Phase 127-00 (Task 1 RED) -- DirectiveEnvelope wrapper tests.
8
+ *
9
+ * 9 behavior tests covering:
10
+ * - Tier-0 sentinel (null brainResponse)
11
+ * - Default GUIDED mode for ordinary responses
12
+ * - Explicit AUTONOMOUS via "just tell me" / "bottom line"
13
+ * - HYBRID for mature-room commit-phase signals
14
+ * - Cold-start FORCES GUIDED (never autonomous)
15
+ * - user_override map present + the 3 documented keys
16
+ * - next_gate sub_shape regex + options array
17
+ * - selectMode default invariant
18
+ * - DEFAULT_MODE constant exported and locked
19
+ *
20
+ * Canon parts: 2 (Larry pedagogy), 3 (Decision Gate F-shape next_gate),
21
+ * 7 (Reuse Before Build: pure data shaping, zero network surface).
22
+ *
23
+ * Constraints (Phase 87 invariant):
24
+ * - Node built-ins only (node:assert/strict).
25
+ * - CJS only.
26
+ * - Zero new runtime dependencies.
27
+ *
28
+ * HARD RULE: no em-dashes (hyphens only).
29
+ */
30
+
31
+ const assert = require('node:assert/strict');
32
+
33
+ let passed = 0;
34
+ let failed = 0;
35
+
36
+ function ok(name) {
37
+ passed += 1;
38
+ process.stdout.write(' ok ' + name + '\n');
39
+ }
40
+
41
+ function fail(name, err) {
42
+ failed += 1;
43
+ process.stdout.write(' FAIL ' + name + '\n');
44
+ if (err) process.stdout.write(' ' + (err.message || String(err)) + '\n');
45
+ }
46
+
47
+ const mod = require('./directive-envelope.cjs');
48
+ const { wrapDirective, selectMode, DEFAULT_MODE } = mod;
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Test 1: wrapDirective(null, {}) returns Tier-0 sentinel envelope.
52
+ // ---------------------------------------------------------------------------
53
+ (function test1_tier0Sentinel() {
54
+ const label = 'wrapDirective(null, {}) returns Tier-0 sentinel envelope';
55
+ try {
56
+ const env = wrapDirective(null, {});
57
+ assert.equal(env.packet_version, '1.0', 'packet_version must be "1.0"');
58
+ assert.equal(env.packet_type, 'DirectiveEnvelope', 'packet_type must be "DirectiveEnvelope"');
59
+ assert.equal(env.mode, 'GUIDED', 'Tier-0 mode must be GUIDED');
60
+ assert.equal(env.mode_rationale, 'brain_unreachable',
61
+ 'Tier-0 mode_rationale must be "brain_unreachable"; got ' + env.mode_rationale);
62
+ assert.ok(env.directive && typeof env.directive === 'object', 'directive must be an object');
63
+ assert.ok(env.directive.guided && typeof env.directive.guided === 'object',
64
+ 'Tier-0 directive.guided must be present');
65
+ assert.ok(Array.isArray(env.directive.guided.questions),
66
+ 'Tier-0 directive.guided.questions must be an array');
67
+ ok(label);
68
+ } catch (err) { fail(label, err); }
69
+ })();
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Test 2: ordinary brainResponse with empty signals defaults to GUIDED.
73
+ // ---------------------------------------------------------------------------
74
+ (function test2_defaultGuided() {
75
+ const label = 'wrapDirective({text:"..."}, {}) defaults to mode=GUIDED';
76
+ try {
77
+ const env = wrapDirective({ text: 'raw brain response' }, {});
78
+ assert.equal(env.mode, 'GUIDED', 'default mode must be GUIDED');
79
+ assert.ok(typeof env.mode_rationale === 'string' && env.mode_rationale.length > 0,
80
+ 'mode_rationale must be a non-empty string');
81
+ assert.ok(env.mode_rationale.includes('guided') || env.mode_rationale.includes('pedagogical')
82
+ || env.mode_rationale.includes('default'),
83
+ 'mode_rationale for default GUIDED should reference guided/pedagogical/default; got ' + env.mode_rationale);
84
+ ok(label);
85
+ } catch (err) { fail(label, err); }
86
+ })();
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Test 3: explicit AUTONOMOUS via "just tell me" signal.
90
+ // ---------------------------------------------------------------------------
91
+ (function test3_explicitAutonomous() {
92
+ const label = 'wrapDirective(resp, {user_said_just_tell_me:true}) returns AUTONOMOUS';
93
+ try {
94
+ const env = wrapDirective({ text: 'r' }, { user_said_just_tell_me: true });
95
+ assert.equal(env.mode, 'AUTONOMOUS', 'mode must be AUTONOMOUS');
96
+ assert.equal(env.mode_rationale, 'explicit_user_invitation',
97
+ 'mode_rationale must be "explicit_user_invitation"; got ' + env.mode_rationale);
98
+ ok(label);
99
+ } catch (err) { fail(label, err); }
100
+ })();
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Test 4: HYBRID for mature-room commit-phase.
104
+ // ---------------------------------------------------------------------------
105
+ (function test4_hybridMatureCommit() {
106
+ const label = 'wrapDirective(resp, {session_count:9, room_mature:true, in_commit_phase:true}) returns HYBRID';
107
+ try {
108
+ const env = wrapDirective({ text: 'r' }, {
109
+ session_count: 9,
110
+ room_mature: true,
111
+ in_commit_phase: true,
112
+ });
113
+ assert.equal(env.mode, 'HYBRID', 'mode must be HYBRID; got ' + env.mode);
114
+ assert.equal(env.mode_rationale, 'mature_room_commit_gate',
115
+ 'mode_rationale must be "mature_room_commit_gate"; got ' + env.mode_rationale);
116
+ ok(label);
117
+ } catch (err) { fail(label, err); }
118
+ })();
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Test 5: cold start FORCES GUIDED (never autonomous on first material).
122
+ // ---------------------------------------------------------------------------
123
+ (function test5_coldStartForcesGuided() {
124
+ const label = 'wrapDirective(resp, {is_first_material:true}) returns GUIDED (never autonomous on cold start)';
125
+ try {
126
+ const env = wrapDirective({ text: 'r' }, { is_first_material: true });
127
+ assert.equal(env.mode, 'GUIDED', 'cold-start mode must be GUIDED; got ' + env.mode);
128
+ assert.ok(env.mode_rationale.includes('cold_start'),
129
+ 'cold-start mode_rationale must include "cold_start"; got ' + env.mode_rationale);
130
+ ok(label);
131
+ } catch (err) { fail(label, err); }
132
+ })();
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Test 6: every envelope contains user_override with the 3 documented keys.
136
+ // ---------------------------------------------------------------------------
137
+ (function test6_userOverrideMap() {
138
+ const label = 'every envelope contains user_override map with the 3 documented overrides';
139
+ try {
140
+ const envs = [
141
+ wrapDirective(null, {}),
142
+ wrapDirective({ text: 'r' }, {}),
143
+ wrapDirective({ text: 'r' }, { user_said_just_tell_me: true }),
144
+ wrapDirective({ text: 'r' }, { is_first_material: true }),
145
+ ];
146
+ for (const env of envs) {
147
+ assert.ok(env.user_override && typeof env.user_override === 'object',
148
+ 'user_override must be an object');
149
+ assert.ok(Object.prototype.hasOwnProperty.call(env.user_override, 'just tell me'),
150
+ 'user_override must have "just tell me" key');
151
+ assert.ok(Object.prototype.hasOwnProperty.call(env.user_override, 'let me think'),
152
+ 'user_override must have "let me think" key');
153
+ assert.ok(Object.prototype.hasOwnProperty.call(env.user_override, 'stop'),
154
+ 'user_override must have "stop" key');
155
+ }
156
+ ok(label);
157
+ } catch (err) { fail(label, err); }
158
+ })();
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Test 7: every envelope contains next_gate.sub_shape F.[1-5] + options array.
162
+ // ---------------------------------------------------------------------------
163
+ (function test7_nextGateShape() {
164
+ const label = 'every envelope contains next_gate.sub_shape matching /^F\\.[1-5]$/ and options array';
165
+ try {
166
+ const re = /^F\.[1-5]$/;
167
+ const envs = [
168
+ wrapDirective(null, {}),
169
+ wrapDirective({ text: 'r' }, {}),
170
+ wrapDirective({ text: 'r' }, { user_said_just_tell_me: true }),
171
+ wrapDirective({ text: 'r' }, { session_count: 9, room_mature: true, in_commit_phase: true }),
172
+ ];
173
+ for (const env of envs) {
174
+ assert.ok(env.next_gate && typeof env.next_gate === 'object', 'next_gate must be an object');
175
+ assert.ok(re.test(env.next_gate.sub_shape),
176
+ 'next_gate.sub_shape must match F.[1-5]; got ' + env.next_gate.sub_shape);
177
+ assert.ok(Array.isArray(env.next_gate.options),
178
+ 'next_gate.options must be an array');
179
+ }
180
+ ok(label);
181
+ } catch (err) { fail(label, err); }
182
+ })();
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Test 8: selectMode returns GUIDED by default with no signals.
186
+ // ---------------------------------------------------------------------------
187
+ (function test8_selectModeDefault() {
188
+ const label = 'selectMode({}) returns mode=GUIDED by default (DEFAULT_MODE invariant)';
189
+ try {
190
+ const r = selectMode({});
191
+ assert.ok(r && typeof r === 'object', 'selectMode must return an object');
192
+ assert.equal(r.mode, 'GUIDED', 'default selectMode mode must be GUIDED; got ' + r.mode);
193
+ assert.ok(typeof r.rationale === 'string' && r.rationale.length > 0,
194
+ 'rationale must be a non-empty string');
195
+ // Non-object input must be treated as empty signals (closed vocabulary).
196
+ const r2 = selectMode(null);
197
+ assert.equal(r2.mode, 'GUIDED', 'selectMode(null) must default to GUIDED');
198
+ const r3 = selectMode(undefined);
199
+ assert.equal(r3.mode, 'GUIDED', 'selectMode(undefined) must default to GUIDED');
200
+ const r4 = selectMode('string');
201
+ assert.equal(r4.mode, 'GUIDED', 'selectMode(non-object) must default to GUIDED');
202
+ ok(label);
203
+ } catch (err) { fail(label, err); }
204
+ })();
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Test 9: DEFAULT_MODE constant exported and locked to 'GUIDED'.
208
+ // ---------------------------------------------------------------------------
209
+ (function test9_defaultModeConstant() {
210
+ const label = 'DEFAULT_MODE constant exported and === "GUIDED" (Larry pedagogical canon lock)';
211
+ try {
212
+ assert.equal(DEFAULT_MODE, 'GUIDED',
213
+ 'DEFAULT_MODE must be exported and equal "GUIDED"; got ' + DEFAULT_MODE);
214
+ assert.equal(typeof DEFAULT_MODE, 'string', 'DEFAULT_MODE must be a string');
215
+ ok(label);
216
+ } catch (err) { fail(label, err); }
217
+ })();
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // Summary.
221
+ // ---------------------------------------------------------------------------
222
+ process.stdout.write('\n');
223
+ process.stdout.write('PASSED: ' + passed + '\n');
224
+ process.stdout.write('FAILED: ' + failed + '\n');
225
+ process.exit(failed === 0 ? 0 : 1);