@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,291 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
4
+ *
5
+ * lib/memory/stale-copy-scanner.test.cjs
6
+ * --------------------------------------
7
+ * Phase 121.5-05 Sub-plan F Task 2 acceptance tests.
8
+ *
9
+ * 8 test groups covering the stale-copy-scanner + class K wiring:
10
+ *
11
+ * 1. scanForStaleCopy() returns {violations: []} on a clean repo.
12
+ * 2. Synthetic fixture: temp banner with "MindrianOS v1.12.0" hardcoded
13
+ * while current is v1.13.0 -> violations[].length >= 1 with kind
14
+ * === 'stale_version_hardcode'.
15
+ * 3. Synthetic fixture: temp file with em-dash in greeting and
16
+ * em_dash_check: true -> violations[] with kind === 'em_dash_violation'.
17
+ * 4. allowed_hardcoded_versions[] suppresses stale_version_hardcode hits.
18
+ * 5. version_stamp_required: false skips stale_version_hardcode (but
19
+ * em_dash_check still applies if true).
20
+ * 6. scanForStaleCopy({surfaces: [...]}) accepts an override.
21
+ * 7. scripts/doctor.cjs --stale-first-touch invokes the scanner and the
22
+ * class K row appears in the human-readable output.
23
+ * 8. scripts/doctor.cjs --all activates --stale-first-touch (class K
24
+ * participates in the cascade).
25
+ *
26
+ * Pure CJS, node built-ins only. No emoji. No em-dashes (the EM_DASH
27
+ * literal used in Test 3 is written via the U+2014 escape sequence).
28
+ */
29
+
30
+ 'use strict';
31
+
32
+ const fs = require('fs');
33
+ const os = require('os');
34
+ const path = require('path');
35
+ const { execSync } = require('child_process');
36
+
37
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
38
+ const scanner = require(path.join(REPO_ROOT, 'lib', 'core', 'stale-copy-scanner.cjs'));
39
+
40
+ let passed = 0;
41
+ let failed = 0;
42
+ const failures = [];
43
+
44
+ function assert(name, cond, detail) {
45
+ if (cond) {
46
+ passed++;
47
+ console.log(' PASS ' + name);
48
+ } else {
49
+ failed++;
50
+ failures.push({ name, detail });
51
+ console.log(' FAIL ' + name + (detail ? ' :: ' + detail : ''));
52
+ }
53
+ }
54
+
55
+ function mkTmpDir() {
56
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'stale-copy-scanner-'));
57
+ }
58
+
59
+ // EM DASH escape (U+2014). Used in synthetic fixtures to test the em-dash
60
+ // detector without putting a literal em-dash in this source file.
61
+ const EM_DASH = '\u2014';
62
+
63
+ // --- Test 1: clean repo -------------------------------------------------
64
+ {
65
+ const result = scanner.scanForStaleCopy();
66
+ assert(
67
+ 'Test 1: scanForStaleCopy() on live repo returns {valid: true, violations: []}',
68
+ result.valid === true && Array.isArray(result.violations) && result.violations.length === 0,
69
+ 'violations=' + JSON.stringify(result.violations),
70
+ );
71
+ }
72
+
73
+ // --- Test 2: stale version hardcode detected ----------------------------
74
+ {
75
+ const tmp = mkTmpDir();
76
+ const filePath = path.join(tmp, 'fake-banner');
77
+ fs.writeFileSync(filePath, 'MindrianOS v1.12.0 welcome line\n', 'utf8');
78
+ const fakeSurface = {
79
+ id: 'banner',
80
+ file: filePath,
81
+ kind: 'shell_script',
82
+ version_stamp_required: true,
83
+ allowed_hardcoded_versions: [],
84
+ em_dash_check: true,
85
+ };
86
+ const result = scanner.scanForStaleCopy({
87
+ surfaces: [fakeSurface],
88
+ currentVersion: '1.13.0',
89
+ repoRoot: tmp,
90
+ });
91
+ const staleHits = result.violations.filter(v => v.kind === 'stale_version_hardcode');
92
+ assert(
93
+ 'Test 2: stale v1.12.0 against current 1.13.0 fires stale_version_hardcode',
94
+ staleHits.length >= 1 && staleHits[0].found === '1.12.0',
95
+ 'hits=' + JSON.stringify(staleHits),
96
+ );
97
+ fs.rmSync(tmp, { recursive: true, force: true });
98
+ }
99
+
100
+ // --- Test 3: em-dash violation detected ---------------------------------
101
+ {
102
+ const tmp = mkTmpDir();
103
+ const filePath = path.join(tmp, 'fake-greeting');
104
+ fs.writeFileSync(filePath, 'MindrianOS v1.13.0 ' + EM_DASH + ' the surface\n', 'utf8');
105
+ const fakeSurface = {
106
+ id: 'splash',
107
+ file: filePath,
108
+ kind: 'command_md',
109
+ version_stamp_required: true,
110
+ allowed_hardcoded_versions: [],
111
+ em_dash_check: true,
112
+ };
113
+ const result = scanner.scanForStaleCopy({
114
+ surfaces: [fakeSurface],
115
+ currentVersion: '1.13.0',
116
+ repoRoot: tmp,
117
+ });
118
+ const emDashHits = result.violations.filter(v => v.kind === 'em_dash_violation');
119
+ assert(
120
+ 'Test 3: U+2014 em-dash on em_dash_check surface fires em_dash_violation',
121
+ emDashHits.length === 1 && emDashHits[0].surface === 'splash',
122
+ 'hits=' + JSON.stringify(emDashHits),
123
+ );
124
+ fs.rmSync(tmp, { recursive: true, force: true });
125
+ }
126
+
127
+ // --- Test 4: allowed_hardcoded_versions suppresses stale hits -----------
128
+ {
129
+ const tmp = mkTmpDir();
130
+ const filePath = path.join(tmp, 'fake-banner');
131
+ // Two stale literals; only one is allowlisted.
132
+ fs.writeFileSync(filePath, '# v1.4.0 and v1.5.1 are doc examples\n', 'utf8');
133
+ const fakeSurface = {
134
+ id: 'banner',
135
+ file: filePath,
136
+ kind: 'shell_script',
137
+ version_stamp_required: true,
138
+ allowed_hardcoded_versions: ['1.4.0', '1.5.1'],
139
+ em_dash_check: false,
140
+ };
141
+ const result = scanner.scanForStaleCopy({
142
+ surfaces: [fakeSurface],
143
+ currentVersion: '1.13.0',
144
+ repoRoot: tmp,
145
+ });
146
+ assert(
147
+ 'Test 4: allowlisted literals suppress stale_version_hardcode',
148
+ result.valid === true && result.violations.length === 0,
149
+ 'violations=' + JSON.stringify(result.violations),
150
+ );
151
+ fs.rmSync(tmp, { recursive: true, force: true });
152
+ }
153
+
154
+ // --- Test 5: version_stamp_required: false skips stale-version-check ----
155
+ {
156
+ const tmp = mkTmpDir();
157
+ const filePath = path.join(tmp, 'fake-persona');
158
+ // Both a stale version literal AND an em-dash. Surface declares
159
+ // version_stamp_required: false (e.g. larry-extended persona file) and
160
+ // em_dash_check: true. Only the em-dash should fire.
161
+ fs.writeFileSync(filePath, 'v1.0.0 baseline ' + EM_DASH + ' persona\n', 'utf8');
162
+ const fakeSurface = {
163
+ id: 'larry-extended',
164
+ file: filePath,
165
+ kind: 'agent_md',
166
+ version_stamp_required: false,
167
+ allowed_hardcoded_versions: [],
168
+ em_dash_check: true,
169
+ };
170
+ const result = scanner.scanForStaleCopy({
171
+ surfaces: [fakeSurface],
172
+ currentVersion: '1.13.0',
173
+ repoRoot: tmp,
174
+ });
175
+ const staleHits = result.violations.filter(v => v.kind === 'stale_version_hardcode');
176
+ const emDashHits = result.violations.filter(v => v.kind === 'em_dash_violation');
177
+ assert(
178
+ 'Test 5a: version_stamp_required: false skips stale-version check',
179
+ staleHits.length === 0,
180
+ 'staleHits=' + JSON.stringify(staleHits),
181
+ );
182
+ assert(
183
+ 'Test 5b: em_dash_check: true still fires when version_stamp_required: false',
184
+ emDashHits.length === 1,
185
+ 'emDashHits=' + JSON.stringify(emDashHits),
186
+ );
187
+ fs.rmSync(tmp, { recursive: true, force: true });
188
+ }
189
+
190
+ // --- Test 6: opts.surfaces override accepted ----------------------------
191
+ {
192
+ // Pass an empty surfaces[] and verify scanned_count reflects the override.
193
+ const result = scanner.scanForStaleCopy({ surfaces: [], currentVersion: '1.13.0' });
194
+ assert(
195
+ 'Test 6: opts.surfaces override accepted (empty list -> scanned_count 0)',
196
+ result.scanned_count === 0 && result.valid === true,
197
+ 'result=' + JSON.stringify(result),
198
+ );
199
+ }
200
+
201
+ // --- Test 7: doctor.cjs --stale-first-touch surfaces class K ------------
202
+ {
203
+ let out = '';
204
+ let err = null;
205
+ try {
206
+ out = execSync('node ' + JSON.stringify(path.join(REPO_ROOT, 'scripts', 'doctor.cjs')) + ' --stale-first-touch', {
207
+ cwd: REPO_ROOT,
208
+ encoding: 'utf8',
209
+ env: Object.assign({}, process.env, { COLUMNS: '120' }),
210
+ });
211
+ } catch (e) {
212
+ err = e.message;
213
+ }
214
+ assert(
215
+ 'Test 7a: doctor.cjs --stale-first-touch runs without error',
216
+ err === null,
217
+ err || '',
218
+ );
219
+ assert(
220
+ 'Test 7b: output contains "class K stale-first-touch" row',
221
+ /class K stale-first-touch/.test(out),
222
+ 'first 300 chars: ' + out.slice(0, 300),
223
+ );
224
+ }
225
+
226
+ // --- Test 8: doctor.cjs --all activates class K -------------------------
227
+ {
228
+ let out = '';
229
+ let err = null;
230
+ try {
231
+ out = execSync('node ' + JSON.stringify(path.join(REPO_ROOT, 'scripts', 'doctor.cjs')) + ' --all --json', {
232
+ cwd: REPO_ROOT,
233
+ encoding: 'utf8',
234
+ env: Object.assign({}, process.env, { COLUMNS: '120' }),
235
+ });
236
+ } catch (e) {
237
+ err = e.message;
238
+ }
239
+ let report;
240
+ let parseErr = null;
241
+ try { report = JSON.parse(out); } catch (e) { parseErr = e.message; }
242
+ assert(
243
+ 'Test 8a: doctor.cjs --all --json parses',
244
+ err === null && parseErr === null && report && typeof report === 'object',
245
+ 'err=' + err + ' parseErr=' + parseErr,
246
+ );
247
+ assert(
248
+ 'Test 8b: report.checks["stale-first-touch-copy"] present under --all',
249
+ report && report.checks && report.checks['stale-first-touch-copy']
250
+ && report.checks['stale-first-touch-copy'].class === 'K',
251
+ 'checks keys: ' + (report && report.checks ? Object.keys(report.checks).join(',') : 'n/a'),
252
+ );
253
+ }
254
+
255
+ // --- Test 9 (Canon Part 8 check): scanner has no network / Brain surface ---
256
+ // Strip C-style block comments + line comments before scanning. The forbidden
257
+ // patterns should not appear in EXECUTABLE source -- doc strings that mention
258
+ // "no fetch / no Brain" are exactly what we want documented and must not
259
+ // trigger the audit.
260
+ {
261
+ let scannerSource = fs.readFileSync(path.join(REPO_ROOT, 'lib', 'core', 'stale-copy-scanner.cjs'), 'utf8');
262
+ scannerSource = scannerSource.replace(/\/\*[\s\S]*?\*\//g, ''); // strip /* ... */
263
+ scannerSource = scannerSource.replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // strip // ... (preserve :// in URLs, though we have none)
264
+ const forbiddenPatterns = [
265
+ { name: 'fetch(', re: /\bfetch\s*\(/ },
266
+ { name: 'require https', re: /require\(['"]https?['"]\)/ },
267
+ { name: 'brain client', re: /brain[._-]client/i },
268
+ { name: 'brain url', re: /brain\.mindrian\.ai/i },
269
+ { name: 'telemetry', re: /telemetry/i },
270
+ ];
271
+ let bad = null;
272
+ for (const p of forbiddenPatterns) {
273
+ if (p.re.test(scannerSource)) { bad = p.name; break; }
274
+ }
275
+ assert(
276
+ 'Test 9: scanner executable source has no fetch / http / Brain / telemetry surface (Canon Part 8 audit)',
277
+ bad === null,
278
+ 'forbidden pattern matched: ' + bad,
279
+ );
280
+ }
281
+
282
+ // --- Summary ------------------------------------------------------------
283
+ console.log('');
284
+ console.log('stale-copy-scanner.test.cjs: ' + passed + ' passed, ' + failed + ' failed');
285
+ if (failed > 0) {
286
+ for (const f of failures) {
287
+ console.log(' - ' + f.name + (f.detail ? ' :: ' + f.detail : ''));
288
+ }
289
+ process.exit(1);
290
+ }
291
+ process.exit(0);
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Phase 121.5-08 Plan -- state-aware-router unit tests.
4
+ *
5
+ * Behavior contract (per plan, D-10 LOCKED):
6
+ * T5: resolveNextSurface({roomState: {exists: false}})
7
+ * -> {route: '/mos:onboard', reason: 'no_room'}
8
+ * T6: resolveNextSurface({roomState: {exists: true, sectionsCount: 0}})
9
+ * -> {route: '/mos:status', reason: 'mostly_empty', addendum: 'suggest next move'}
10
+ * T6b: resolveNextSurface({roomState: {exists: true, stage: 'Pre-Opportunity', sectionsCount: 4}})
11
+ * -> {route: '/mos:status', reason: 'mostly_empty', addendum: 'suggest next move'}
12
+ * (stage signal counts as mostly-empty even when sections > 0)
13
+ * T7: resolveNextSurface({roomState: {exists: true, sectionsCount: 5}})
14
+ * -> {route: '/mos:suggest-next', reason: 'populated'}
15
+ *
16
+ * Hermetic: no fs reads, no network. Pure function test.
17
+ */
18
+ 'use strict';
19
+
20
+ const path = require('path');
21
+
22
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
23
+ const ROUTER = require(path.join(REPO_ROOT, 'lib', 'core', 'state-aware-router.cjs'));
24
+
25
+ let pass = 0;
26
+ let fail = 0;
27
+ const failures = [];
28
+
29
+ function assert(cond, name, detail) {
30
+ if (cond) {
31
+ pass++;
32
+ console.log('PASS ' + name);
33
+ } else {
34
+ fail++;
35
+ failures.push(name + (detail ? ' -- ' + detail : ''));
36
+ console.log('FAIL ' + name + (detail ? ' -- ' + detail : ''));
37
+ }
38
+ }
39
+
40
+ // --- T5: no room -> /mos:onboard ---
41
+ {
42
+ const r = ROUTER.resolveNextSurface({ roomState: { exists: false } });
43
+ assert(r.route === '/mos:onboard', 'T5: no_room -> /mos:onboard', r.route);
44
+ assert(r.reason === 'no_room', 'T5: reason=no_room', r.reason);
45
+ }
46
+
47
+ // --- T5b: defensive: no opts at all -> still /mos:onboard ---
48
+ {
49
+ const r1 = ROUTER.resolveNextSurface();
50
+ assert(r1.route === '/mos:onboard', 'T5b: no opts -> /mos:onboard', r1.route);
51
+ const r2 = ROUTER.resolveNextSurface({});
52
+ assert(r2.route === '/mos:onboard', 'T5b: empty opts -> /mos:onboard', r2.route);
53
+ }
54
+
55
+ // --- T6: room exists, sectionsCount=0 -> /mos:status mostly_empty ---
56
+ {
57
+ const r = ROUTER.resolveNextSurface({ roomState: { exists: true, sectionsCount: 0 } });
58
+ assert(r.route === '/mos:status', 'T6: sections=0 -> /mos:status', r.route);
59
+ assert(r.reason === 'mostly_empty', 'T6: reason=mostly_empty', r.reason);
60
+ assert(r.addendum === 'suggest next move', 'T6: addendum=suggest next move', r.addendum);
61
+ }
62
+
63
+ // --- T6b: room exists, stage=Pre-Opportunity (sections>0 still counts as mostly empty) ---
64
+ {
65
+ const r = ROUTER.resolveNextSurface({ roomState: { exists: true, sectionsCount: 4, stage: 'Pre-Opportunity' } });
66
+ assert(r.route === '/mos:status', 'T6b: stage=Pre-Opportunity -> /mos:status', r.route);
67
+ assert(r.reason === 'mostly_empty', 'T6b: stage signal counts as mostly_empty', r.reason);
68
+ }
69
+
70
+ // --- T7: room populated -> /mos:suggest-next ---
71
+ {
72
+ const r = ROUTER.resolveNextSurface({ roomState: { exists: true, sectionsCount: 5 } });
73
+ assert(r.route === '/mos:suggest-next', 'T7: populated -> /mos:suggest-next', r.route);
74
+ assert(r.reason === 'populated', 'T7: reason=populated', r.reason);
75
+ }
76
+
77
+ // --- T7b: clearly populated room ---
78
+ {
79
+ const r = ROUTER.resolveNextSurface({ roomState: { exists: true, sectionsCount: 12, stage: 'Validation' } });
80
+ assert(r.route === '/mos:suggest-next', 'T7b: many sections -> /mos:suggest-next');
81
+ }
82
+
83
+ console.log('');
84
+ console.log('state-aware-router.test.cjs: ' + pass + ' passed, ' + fail + ' failed');
85
+ if (fail > 0) {
86
+ console.log('FAILURES:');
87
+ for (const f of failures) console.log(' - ' + f);
88
+ process.exit(1);
89
+ }
90
+ process.exit(0);