@mindrian_os/install 1.13.0-beta.11

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 (597) hide show
  1. package/.claude-plugin/plugin.json +21 -0
  2. package/.mcp.json +9 -0
  3. package/CHANGELOG.md +3333 -0
  4. package/LICENSE +123 -0
  5. package/README.md +673 -0
  6. package/agents/brain-query.md +80 -0
  7. package/agents/framework-runner.md +237 -0
  8. package/agents/grading.md +188 -0
  9. package/agents/investor.md +128 -0
  10. package/agents/larry-extended.md +135 -0
  11. package/agents/opportunity-scanner.md +91 -0
  12. package/agents/persona-analyst.md +132 -0
  13. package/agents/research.md +89 -0
  14. package/agents/reverse-salient-agent.md +27 -0
  15. package/bin/cli.js +142 -0
  16. package/bin/mindrian-mcp-server.cjs +182 -0
  17. package/bin/mindrian-tools.cjs +765 -0
  18. package/commands/act.md +439 -0
  19. package/commands/admin.md +404 -0
  20. package/commands/analyze-needs.md +42 -0
  21. package/commands/analyze-systems.md +39 -0
  22. package/commands/analyze-timing.md +42 -0
  23. package/commands/auto-explore.md +64 -0
  24. package/commands/beautiful-question.md +40 -0
  25. package/commands/brain-derive.md +78 -0
  26. package/commands/build-knowledge.md +42 -0
  27. package/commands/build-thesis.md +46 -0
  28. package/commands/causal.md +234 -0
  29. package/commands/challenge-assumptions.md +33 -0
  30. package/commands/compare-ventures.md +83 -0
  31. package/commands/dashboard.md +110 -0
  32. package/commands/deep-grade.md +82 -0
  33. package/commands/diagnose.md +58 -0
  34. package/commands/diagnostics.md +151 -0
  35. package/commands/doctor.md +151 -0
  36. package/commands/dominant-designs.md +40 -0
  37. package/commands/explain-decision.md +87 -0
  38. package/commands/explore-domains.md +42 -0
  39. package/commands/explore-futures.md +40 -0
  40. package/commands/explore-trends.md +42 -0
  41. package/commands/export.md +103 -0
  42. package/commands/file-meeting.md +724 -0
  43. package/commands/find-analogies.md +188 -0
  44. package/commands/find-bottlenecks.md +62 -0
  45. package/commands/find-connections.md +76 -0
  46. package/commands/funding.md +81 -0
  47. package/commands/grade.md +203 -0
  48. package/commands/graph.md +128 -0
  49. package/commands/hat-briefing.md +125 -0
  50. package/commands/heal.md +196 -0
  51. package/commands/help.md +399 -0
  52. package/commands/hmi-status.md +172 -0
  53. package/commands/jtbd.md +241 -0
  54. package/commands/leadership.md +73 -0
  55. package/commands/lean-canvas.md +40 -0
  56. package/commands/macro-trends.md +40 -0
  57. package/commands/map-unknowns.md +40 -0
  58. package/commands/memory.md +173 -0
  59. package/commands/models.md +175 -0
  60. package/commands/mos-reason.md +285 -0
  61. package/commands/mullins.md +120 -0
  62. package/commands/new-project.md +481 -0
  63. package/commands/onboard.md +434 -0
  64. package/commands/operator.md +149 -0
  65. package/commands/opportunities.md +144 -0
  66. package/commands/organize.md +497 -0
  67. package/commands/persona.md +198 -0
  68. package/commands/pipeline.md +112 -0
  69. package/commands/present.md +91 -0
  70. package/commands/publish.md +201 -0
  71. package/commands/query.md +124 -0
  72. package/commands/radar.md +72 -0
  73. package/commands/reanalyze.md +91 -0
  74. package/commands/research.md +196 -0
  75. package/commands/room.md +352 -0
  76. package/commands/rooms.md +598 -0
  77. package/commands/root-cause.md +40 -0
  78. package/commands/rs-experts.md +85 -0
  79. package/commands/rs-explain.md +100 -0
  80. package/commands/rs-fetch.md +94 -0
  81. package/commands/rs-thesis.md +85 -0
  82. package/commands/scenario-plan.md +40 -0
  83. package/commands/scheduled-tasks.md +285 -0
  84. package/commands/score-innovation.md +43 -0
  85. package/commands/scout.md +239 -0
  86. package/commands/setup.md +618 -0
  87. package/commands/snapshot.md +147 -0
  88. package/commands/speakers.md +84 -0
  89. package/commands/splash.md +28 -0
  90. package/commands/status.md +75 -0
  91. package/commands/structure-argument.md +42 -0
  92. package/commands/suggest-next.md +80 -0
  93. package/commands/systems-thinking.md +40 -0
  94. package/commands/think-hats.md +42 -0
  95. package/commands/update.md +181 -0
  96. package/commands/user-needs.md +40 -0
  97. package/commands/validate.md +40 -0
  98. package/commands/value-proposition.md +61 -0
  99. package/commands/vault.md +180 -0
  100. package/commands/visualize.md +52 -0
  101. package/commands/whitespace.md +507 -0
  102. package/commands/wiki.md +69 -0
  103. package/hooks/hooks.json +381 -0
  104. package/hooks/run-hook.cmd +64 -0
  105. package/lib/__init__.py +0 -0
  106. package/lib/__pycache__/__init__.cpython-312.pyc +0 -0
  107. package/lib/agents/auto-explore-agent.cjs +1043 -0
  108. package/lib/agents/reverse-salient-agent.cjs +679 -0
  109. package/lib/agents/tension-hook-agent.cjs +544 -0
  110. package/lib/brain/ROOM.md +44 -0
  111. package/lib/brain/chain-recommender.cjs +301 -0
  112. package/lib/chat/chat-context.js +185 -0
  113. package/lib/chat/chat-panel.js +721 -0
  114. package/lib/chat/fabric-chat.cjs +288 -0
  115. package/lib/chat/generative-tools.js +219 -0
  116. package/lib/conversation/ROOM.md +39 -0
  117. package/lib/conversation/classifier-rules.json +38 -0
  118. package/lib/conversation/classifier.cjs +264 -0
  119. package/lib/conversation/operator.cjs +287 -0
  120. package/lib/copy/115-spec-strings.cjs +55 -0
  121. package/lib/core/__init__.py +0 -0
  122. package/lib/core/__nav-stub.cjs +14 -0
  123. package/lib/core/__pycache__/__init__.cpython-312.pyc +0 -0
  124. package/lib/core/__pycache__/rs-math.cpython-312.pyc +0 -0
  125. package/lib/core/__pycache__/rs_cache.cpython-312.pyc +0 -0
  126. package/lib/core/__pycache__/rs_corpus.cpython-312.pyc +0 -0
  127. package/lib/core/__pycache__/rs_hybrid.cpython-312.pyc +0 -0
  128. package/lib/core/__pycache__/rs_math.cpython-312.pyc +0 -0
  129. package/lib/core/__pycache__/rs_rooms.cpython-312.pyc +0 -0
  130. package/lib/core/artifact-id.cjs +148 -0
  131. package/lib/core/asset-ops.cjs +151 -0
  132. package/lib/core/auto-commit-throttle.cjs +129 -0
  133. package/lib/core/bearer-token.cjs +199 -0
  134. package/lib/core/brain-client.cjs +865 -0
  135. package/lib/core/brain-derivation-prompts.cjs +326 -0
  136. package/lib/core/brain-derivation-queue.cjs +431 -0
  137. package/lib/core/brain-derivation.cjs +580 -0
  138. package/lib/core/brain-md-schema.cjs +528 -0
  139. package/lib/core/brain-md-staleness.cjs +357 -0
  140. package/lib/core/brain-response-sanitize.cjs +188 -0
  141. package/lib/core/bridge-writer.cjs +477 -0
  142. package/lib/core/chat-context-builder.cjs +253 -0
  143. package/lib/core/cross-room-aggregator.cjs +762 -0
  144. package/lib/core/daily-briefing.cjs +438 -0
  145. package/lib/core/decision-capture.cjs +618 -0
  146. package/lib/core/deep-links.cjs +82 -0
  147. package/lib/core/dispatch-optimizer.cjs +354 -0
  148. package/lib/core/dual-path-detector.cjs +84 -0
  149. package/lib/core/dual-path-detector.test.cjs +334 -0
  150. package/lib/core/exports-log.cjs +79 -0
  151. package/lib/core/feynman-minto-invariants.cjs +605 -0
  152. package/lib/core/folder-memory-async.cjs +338 -0
  153. package/lib/core/folder-memory-shared.cjs +890 -0
  154. package/lib/core/folder-memory.cjs +416 -0
  155. package/lib/core/framework-chain-composer.cjs +411 -0
  156. package/lib/core/frontmatter-schemas.cjs +330 -0
  157. package/lib/core/git-ops.cjs +141 -0
  158. package/lib/core/graph-ops.cjs +258 -0
  159. package/lib/core/hat-persistence.cjs +362 -0
  160. package/lib/core/index.cjs +60 -0
  161. package/lib/core/integration-registry.cjs +232 -0
  162. package/lib/core/intelligence-cascade.cjs +661 -0
  163. package/lib/core/lazygraph-ops.cjs +1057 -0
  164. package/lib/core/lru-cache.cjs +139 -0
  165. package/lib/core/mcp-profiles.cjs +182 -0
  166. package/lib/core/meeting-ops.cjs +54 -0
  167. package/lib/core/memory-ops.cjs +600 -0
  168. package/lib/core/migrations/ROOM.md +33 -0
  169. package/lib/core/migrations/phase-109-nodes-provenance.cjs +339 -0
  170. package/lib/core/migrations/phase-109-session-focus.cjs +99 -0
  171. package/lib/core/model-profiles.cjs +246 -0
  172. package/lib/core/mullins-scaffold.cjs +160 -0
  173. package/lib/core/nav-dial.cjs +316 -0
  174. package/lib/core/navigation/ROOM.md +15 -0
  175. package/lib/core/navigation/explanation.cjs +43 -0
  176. package/lib/core/navigation/focus.cjs +135 -0
  177. package/lib/core/navigation/ingestion.cjs +82 -0
  178. package/lib/core/navigation/insights.cjs +350 -0
  179. package/lib/core/navigation/memory-events.cjs +118 -0
  180. package/lib/core/navigation/neighborhood.cjs +78 -0
  181. package/lib/core/navigation/packet.cjs +182 -0
  182. package/lib/core/navigation/room-home.cjs +127 -0
  183. package/lib/core/navigation/transitions.cjs +82 -0
  184. package/lib/core/navigation-engine-shared.cjs +242 -0
  185. package/lib/core/navigation-engine.cjs +664 -0
  186. package/lib/core/navigation.cjs +60 -0
  187. package/lib/core/nl-graph-queries.cjs +164 -0
  188. package/lib/core/offer-presenter.cjs +406 -0
  189. package/lib/core/opportunity-extractor.cjs +183 -0
  190. package/lib/core/opportunity-ops.cjs +1371 -0
  191. package/lib/core/persona-ops.cjs +537 -0
  192. package/lib/core/persona-taxonomy.cjs +190 -0
  193. package/lib/core/platform-gates.cjs +120 -0
  194. package/lib/core/platform.cjs +257 -0
  195. package/lib/core/proactive-intelligence.cjs +528 -0
  196. package/lib/core/problem-type-router.cjs +315 -0
  197. package/lib/core/reasoning-ops.cjs +639 -0
  198. package/lib/core/reverse-salient-persona-suffix.cjs +115 -0
  199. package/lib/core/room-classifier-strict-mode.cjs +229 -0
  200. package/lib/core/room-db.cjs +127 -0
  201. package/lib/core/room-ops-async.cjs +92 -0
  202. package/lib/core/room-ops-shared.cjs +64 -0
  203. package/lib/core/room-ops-sync.cjs +70 -0
  204. package/lib/core/room-ops.cjs +32 -0
  205. package/lib/core/room-type-detector.cjs +386 -0
  206. package/lib/core/rs-brain-substrate-prompts.cjs +129 -0
  207. package/lib/core/rs-brain-substrate.cjs +570 -0
  208. package/lib/core/rs-breakthrough-scorer.cjs +255 -0
  209. package/lib/core/rs-canon-violations.cjs +82 -0
  210. package/lib/core/rs-chain-feeder.cjs +343 -0
  211. package/lib/core/rs-commercial-assessor.cjs +280 -0
  212. package/lib/core/rs-differential-scorer.cjs +376 -0
  213. package/lib/core/rs-domain-analyzer.cjs +385 -0
  214. package/lib/core/rs-egress-prompts.cjs +113 -0
  215. package/lib/core/rs-egress-telemetry.cjs +225 -0
  216. package/lib/core/rs-egress-violations.cjs +53 -0
  217. package/lib/core/rs-expert-mapper.cjs +467 -0
  218. package/lib/core/rs-fetcher-academic.cjs +697 -0
  219. package/lib/core/rs-fetcher-experts.cjs +314 -0
  220. package/lib/core/rs-fetcher-industry.cjs +731 -0
  221. package/lib/core/rs-fetcher-patents.cjs +564 -0
  222. package/lib/core/rs-innovation-classifier.cjs +194 -0
  223. package/lib/core/rs-mind-map.cjs +656 -0
  224. package/lib/core/rs-neo4j-writer.cjs +388 -0
  225. package/lib/core/rs-nl-to-query.cjs +425 -0
  226. package/lib/core/rs-pinecone-bridge.cjs +303 -0
  227. package/lib/core/rs-preprocessor.cjs +350 -0
  228. package/lib/core/rs-query-matrix.cjs +316 -0
  229. package/lib/core/rs-query-to-text.cjs +438 -0
  230. package/lib/core/rs-sqlite-mirror.cjs +443 -0
  231. package/lib/core/rs-thesis-generator.cjs +188 -0
  232. package/lib/core/rs_cache.py +479 -0
  233. package/lib/core/rs_corpus.py +468 -0
  234. package/lib/core/rs_hybrid.py +586 -0
  235. package/lib/core/rs_math.py +287 -0
  236. package/lib/core/rs_rooms.py +193 -0
  237. package/lib/core/scheduled-scanner.cjs +463 -0
  238. package/lib/core/scratchpad-ops.cjs +201 -0
  239. package/lib/core/section-8-trace-schema.cjs +138 -0
  240. package/lib/core/section-registry.cjs +111 -0
  241. package/lib/core/session-state.cjs +144 -0
  242. package/lib/core/shallow-doc-parser.cjs +174 -0
  243. package/lib/core/shallow-doc-parser.test.cjs +226 -0
  244. package/lib/core/skill-activation-router.cjs +284 -0
  245. package/lib/core/state-ops.cjs +46 -0
  246. package/lib/core/statusline-cache.cjs +266 -0
  247. package/lib/core/token-estimator.cjs +348 -0
  248. package/lib/core/user-archetype.cjs +239 -0
  249. package/lib/core/user-md-ops.cjs +524 -0
  250. package/lib/core/visual-ops.cjs +624 -0
  251. package/lib/core/write-lock.cjs +149 -0
  252. package/lib/graph/canvas-graph.js +467 -0
  253. package/lib/graph/constellation-config.cjs +299 -0
  254. package/lib/graph/graph-detail-panel.js +165 -0
  255. package/lib/hmi/ROOM.md +47 -0
  256. package/lib/hmi/across-session-memory.cjs +604 -0
  257. package/lib/hmi/cross-room-memory.cjs +575 -0
  258. package/lib/hmi/decoy-tier.cjs +395 -0
  259. package/lib/hmi/jtbd-classifier.cjs +219 -0
  260. package/lib/hmi/jtbd-state.cjs +199 -0
  261. package/lib/hmi/jtbd-taxonomy.json +392 -0
  262. package/lib/hmi/selector-dispatcher.cjs +546 -0
  263. package/lib/hmi/selector-telemetry.cjs +263 -0
  264. package/lib/hmi/shape-f0-renderer.cjs +139 -0
  265. package/lib/hmi/shape-f1-fallback.cjs +80 -0
  266. package/lib/hmi/shape-f1-renderer.cjs +138 -0
  267. package/lib/hmi/shape-f2-renderer.cjs +132 -0
  268. package/lib/hmi/shape-f3-renderer.cjs +66 -0
  269. package/lib/hmi/shape-f4-renderer.cjs +72 -0
  270. package/lib/hmi/shape-f5-renderer.cjs +155 -0
  271. package/lib/hmi/shape-f6-plan-review-renderer.cjs +312 -0
  272. package/lib/hmi/shape-f6-renderer.cjs +144 -0
  273. package/lib/hmi/shape-g-renderer.cjs +219 -0
  274. package/lib/hmi/shape-h-renderer.cjs +222 -0
  275. package/lib/hmi/tier-check.cjs +63 -0
  276. package/lib/import/PRECONDITIONS.md +41 -0
  277. package/lib/import/branding.cjs +210 -0
  278. package/lib/import/branding.test.cjs +235 -0
  279. package/lib/import/classifications-sync.cjs +104 -0
  280. package/lib/import/classifications-sync.test.cjs +129 -0
  281. package/lib/import/enricher.cjs +296 -0
  282. package/lib/import/enricher.test.cjs +273 -0
  283. package/lib/import/integration.test.cjs +376 -0
  284. package/lib/import/manifest.cjs +129 -0
  285. package/lib/import/manifest.schema.json +185 -0
  286. package/lib/import/manifest.test.cjs +123 -0
  287. package/lib/import/meeting-detector.cjs +92 -0
  288. package/lib/import/meeting-detector.test.cjs +100 -0
  289. package/lib/import/person-detector.cjs +229 -0
  290. package/lib/import/person-detector.test.cjs +149 -0
  291. package/lib/import/report.cjs +186 -0
  292. package/lib/import/report.test.cjs +186 -0
  293. package/lib/import/room-md-scaffolder.cjs +49 -0
  294. package/lib/import/router.cjs +224 -0
  295. package/lib/import/router.test.cjs +356 -0
  296. package/lib/import/run-all-tests.cjs +36 -0
  297. package/lib/import/smoke-test.cjs +213 -0
  298. package/lib/import/smoke-test.test.cjs +148 -0
  299. package/lib/import/test-fixtures/collision-vault/preexisting-room/STATE.md +8 -0
  300. package/lib/import/test-fixtures/collision-vault/preexisting-room/problem-definition/onboarding/onboarding.md +7 -0
  301. package/lib/import/test-fixtures/collision-vault/source/onboarding.md +5 -0
  302. package/lib/import/test-fixtures/obsidian-vault/.obsidian/workspace.json +1 -0
  303. package/lib/import/test-fixtures/obsidian-vault/notes/with-wikilinks.md +4 -0
  304. package/lib/import/test-fixtures/tiny-vault/notes/2026-01-15-team-sync.md +9 -0
  305. package/lib/import/test-fixtures/tiny-vault/notes/empty.md +3 -0
  306. package/lib/import/test-fixtures/tiny-vault/notes/onboarding.md +5 -0
  307. package/lib/import/test-fixtures/tiny-vault/notes/pricing.md +5 -0
  308. package/lib/import/test-fixtures/tiny-vault/notes/random.md +4 -0
  309. package/lib/import/undo.test.cjs +199 -0
  310. package/lib/import/vault-scanner.cjs +105 -0
  311. package/lib/import/vault-scanner.test.cjs +67 -0
  312. package/lib/mcp/app-html/dashboard.html +316 -0
  313. package/lib/mcp/app-html/graph.html +428 -0
  314. package/lib/mcp/app-html/mindrian-platform.html +1841 -0
  315. package/lib/mcp/app-html/wiki.html +383 -0
  316. package/lib/mcp/app-views.cjs +322 -0
  317. package/lib/mcp/brain-router.cjs +418 -0
  318. package/lib/mcp/capability-registry.cjs +62 -0
  319. package/lib/mcp/larry-context.cjs +46 -0
  320. package/lib/mcp/larry-server-instructions.md +114 -0
  321. package/lib/mcp/pipeline-state.cjs +275 -0
  322. package/lib/mcp/prompts.cjs +302 -0
  323. package/lib/mcp/resources.cjs +227 -0
  324. package/lib/mcp/session-catchup.cjs +327 -0
  325. package/lib/mcp/surface-detect.cjs +75 -0
  326. package/lib/mcp/tool-router.cjs +1034 -0
  327. package/lib/memory/aaak-compress.cjs +403 -0
  328. package/lib/memory/aaak-compress.test.cjs +288 -0
  329. package/lib/memory/async-artifact-auto-commit.test.cjs +223 -0
  330. package/lib/memory/bearer-token.test.cjs +315 -0
  331. package/lib/memory/brain-cache-lru.test.cjs +259 -0
  332. package/lib/memory/brain-client-query-shape.test.cjs +160 -0
  333. package/lib/memory/brain-derivation-graceful-degradation.test.cjs +1019 -0
  334. package/lib/memory/brain-derivation-queue.test.cjs +539 -0
  335. package/lib/memory/brain-derivation.test.cjs +634 -0
  336. package/lib/memory/brain-derive-command.test.cjs +534 -0
  337. package/lib/memory/brain-md-invariants-validator.test.cjs +704 -0
  338. package/lib/memory/brain-md-schema.test.cjs +467 -0
  339. package/lib/memory/brain-md-staleness.test.cjs +525 -0
  340. package/lib/memory/brain-server-resolution.test.cjs +314 -0
  341. package/lib/memory/chain-recommender.test.cjs +233 -0
  342. package/lib/memory/chat-context.test.cjs +128 -0
  343. package/lib/memory/command-registry.test.cjs +220 -0
  344. package/lib/memory/cross-room-aggregator.test.cjs +909 -0
  345. package/lib/memory/dashboard-server.test.cjs +256 -0
  346. package/lib/memory/debouncer-drain-at-prompt.test.cjs +389 -0
  347. package/lib/memory/decision-capture.test.cjs +632 -0
  348. package/lib/memory/decision-capture.worker.cjs +70 -0
  349. package/lib/memory/explain-decision-command.test.cjs +521 -0
  350. package/lib/memory/explain-decision-footer.test.cjs +316 -0
  351. package/lib/memory/explored-materials-store.cjs +392 -0
  352. package/lib/memory/feynman-minto-guardian.test.cjs +736 -0
  353. package/lib/memory/feynman-minto-invariants.test.cjs +511 -0
  354. package/lib/memory/feynman-prompts-drift.test.cjs +144 -0
  355. package/lib/memory/feynman-prompts.cjs +151 -0
  356. package/lib/memory/feynman-prompts.test.cjs +96 -0
  357. package/lib/memory/folder-memory-quadruple.test.cjs +548 -0
  358. package/lib/memory/folder-memory.test.cjs +503 -0
  359. package/lib/memory/framework-chain-composer.test.cjs +515 -0
  360. package/lib/memory/frontmatter-schema-validator.test.cjs +290 -0
  361. package/lib/memory/heal-command.test.cjs +604 -0
  362. package/lib/memory/index-artifact-transaction.test.cjs +333 -0
  363. package/lib/memory/lazygraph-rs-discoveries-view.test.cjs +122 -0
  364. package/lib/memory/mcp-input-validation.test.cjs +240 -0
  365. package/lib/memory/mcp-server-brain-deps.test.cjs +270 -0
  366. package/lib/memory/mcp-stack-fallback.test.cjs +433 -0
  367. package/lib/memory/minto-debouncer.test.cjs +407 -0
  368. package/lib/memory/minto-debouncer.worker.cjs +46 -0
  369. package/lib/memory/minto-migration-v88.test.cjs +265 -0
  370. package/lib/memory/minto-schema-v88.test.cjs +390 -0
  371. package/lib/memory/mos-status-renderer.test.cjs +631 -0
  372. package/lib/memory/narrative-schema.cjs +376 -0
  373. package/lib/memory/narrative-schema.test.cjs +209 -0
  374. package/lib/memory/nav-dial.test.cjs +414 -0
  375. package/lib/memory/navigation-engine-core.test.cjs +722 -0
  376. package/lib/memory/navigation-invariants.test.cjs +483 -0
  377. package/lib/memory/offer-presenter.test.cjs +554 -0
  378. package/lib/memory/on-stop-snapshot.test.cjs +404 -0
  379. package/lib/memory/pending-tension-store.cjs +373 -0
  380. package/lib/memory/post-compact-reinjection.test.cjs +854 -0
  381. package/lib/memory/post-write-triple.test.cjs +317 -0
  382. package/lib/memory/pre-compact-snapshot.test.cjs +495 -0
  383. package/lib/memory/problem-type-router.test.cjs +656 -0
  384. package/lib/memory/query-efficiency-telemetry.test.cjs +370 -0
  385. package/lib/memory/recompile-room-references.test.cjs +392 -0
  386. package/lib/memory/recompile-room-references.worker.cjs +42 -0
  387. package/lib/memory/record-decision-dual-write.test.cjs +454 -0
  388. package/lib/memory/room-classifier-strict-mode.test.cjs +417 -0
  389. package/lib/memory/room-minto-hook.test.cjs +398 -0
  390. package/lib/memory/rs-discovery-engine.test.cjs +323 -0
  391. package/lib/memory/run-feynman-tests.cjs +1247 -0
  392. package/lib/memory/security-trifecta.test.cjs +312 -0
  393. package/lib/memory/session-start-brain-staleness.test.cjs +363 -0
  394. package/lib/memory/session-start-triple-injection.test.cjs +514 -0
  395. package/lib/memory/sessionstart-banner-formatter.cjs +318 -0
  396. package/lib/memory/sessionstart-minto-banner.test.cjs +373 -0
  397. package/lib/memory/skill-activation-router.test.cjs +419 -0
  398. package/lib/memory/stamp-artifact-write.test.cjs +304 -0
  399. package/lib/memory/statusline-active-room.test.cjs +315 -0
  400. package/lib/memory/statusline-minto-segment.test.cjs +292 -0
  401. package/lib/memory/sync-async-entry-points.test.cjs +204 -0
  402. package/lib/memory/test-bridge-writer-enhanced.cjs +452 -0
  403. package/lib/memory/test-rs-brain-substrate-shape.cjs +529 -0
  404. package/lib/memory/test-rs-brain-substrate.cjs +636 -0
  405. package/lib/memory/test-rs-breakthrough-scorer.cjs +375 -0
  406. package/lib/memory/test-rs-canon-violations.cjs +218 -0
  407. package/lib/memory/test-rs-chain-feeder-core.cjs +344 -0
  408. package/lib/memory/test-rs-chain-feeder-skill-spawn.cjs +297 -0
  409. package/lib/memory/test-rs-commercial-assessor.cjs +385 -0
  410. package/lib/memory/test-rs-differential-scorer.cjs +480 -0
  411. package/lib/memory/test-rs-discovery-engine.cjs +603 -0
  412. package/lib/memory/test-rs-domain-analyzer.cjs +492 -0
  413. package/lib/memory/test-rs-egress-primitives.cjs +420 -0
  414. package/lib/memory/test-rs-expert-mapper.cjs +547 -0
  415. package/lib/memory/test-rs-explain-command.cjs +443 -0
  416. package/lib/memory/test-rs-fetcher-academic.cjs +848 -0
  417. package/lib/memory/test-rs-fetcher-experts.cjs +496 -0
  418. package/lib/memory/test-rs-fetcher-industry.cjs +702 -0
  419. package/lib/memory/test-rs-fetcher-patents.cjs +674 -0
  420. package/lib/memory/test-rs-innovation-classifier.cjs +301 -0
  421. package/lib/memory/test-rs-mind-map.cjs +646 -0
  422. package/lib/memory/test-rs-neo4j-writer.cjs +518 -0
  423. package/lib/memory/test-rs-nl-to-query.cjs +449 -0
  424. package/lib/memory/test-rs-pinecone-bridge.cjs +277 -0
  425. package/lib/memory/test-rs-preprocessor.cjs +433 -0
  426. package/lib/memory/test-rs-query-matrix.cjs +391 -0
  427. package/lib/memory/test-rs-query-to-text.cjs +551 -0
  428. package/lib/memory/test-rs-sqlite-mirror.cjs +649 -0
  429. package/lib/memory/test-rs-thesis-generator.cjs +360 -0
  430. package/lib/memory/triple-context-formatter.cjs +473 -0
  431. package/lib/memory/triple-context-formatter.test.cjs +442 -0
  432. package/lib/memory/user-md-persona.test.cjs +565 -0
  433. package/lib/memory/userpromptsubmit-integration.test.cjs +690 -0
  434. package/lib/memory/validators/README.md +157 -0
  435. package/lib/memory/validators/brain-md-invariants.cjs +475 -0
  436. package/lib/memory/validators/brain-substrate-invariants.cjs +285 -0
  437. package/lib/memory/validators/external-academic-invariants.cjs +249 -0
  438. package/lib/memory/validators/external-industry-invariants.cjs +271 -0
  439. package/lib/memory/validators/external-patents-invariants.cjs +266 -0
  440. package/lib/memory/validators/minto-invariants.cjs +62 -0
  441. package/lib/memory/validators/navigation-invariants.cjs +340 -0
  442. package/lib/memory/validators/queue-health.cjs +95 -0
  443. package/lib/memory/validators/snapshot-integrity.cjs +129 -0
  444. package/lib/memory/validators/stale-lifecycle.cjs +116 -0
  445. package/lib/memory/vault-section-minto-generator-atomic.test.cjs +556 -0
  446. package/lib/memory/vault-section-minto-generator-atomic.worker.cjs +73 -0
  447. package/lib/memory/write-lock-atomic.test.cjs +137 -0
  448. package/lib/memory/write-lock-atomic.worker.cjs +55 -0
  449. package/lib/parity/check-parity.cjs +83 -0
  450. package/lib/presentation/presentation-server.cjs +101 -0
  451. package/lib/presentation/presentation-watcher.cjs +123 -0
  452. package/lib/quickview/hub-server.cjs +719 -0
  453. package/lib/quickview/server.cjs +533 -0
  454. package/lib/render/JTBD-PALETTES.md +145 -0
  455. package/lib/render/ROOM.md +59 -0
  456. package/lib/render/render-v2.cjs +486 -0
  457. package/lib/render/render-v2.test.cjs +267 -0
  458. package/lib/render/render.cjs +65 -0
  459. package/lib/state/ROOM.md +46 -0
  460. package/lib/state/state-md-parser.cjs +215 -0
  461. package/lib/statusline/ROOM.md +38 -0
  462. package/lib/statusline/banner-suppression.cjs +50 -0
  463. package/lib/statusline/surface-detect.cjs +85 -0
  464. package/lib/update-bootstrap.sh.template +145 -0
  465. package/lib/vault/frontmatter-schema.cjs +297 -0
  466. package/lib/vault/room-scanner.cjs +352 -0
  467. package/lib/vault/wikilink-builder.cjs +231 -0
  468. package/lib/vault/wikilink-builder.test.cjs +182 -0
  469. package/lib/wiki/graph-links.cjs +281 -0
  470. package/lib/wiki/page-renderer.cjs +229 -0
  471. package/lib/wiki/wiki-chat.cjs +81 -0
  472. package/lib/wiki/wiki-layout.cjs +1459 -0
  473. package/lib/wiki/wiki-search.cjs +142 -0
  474. package/lib/wiki/wiki-server.cjs +678 -0
  475. package/lib/wiki/wiki-watcher.cjs +105 -0
  476. package/lib/workflow/ROOM.md +47 -0
  477. package/lib/workflow/command-resolver.cjs +155 -0
  478. package/lib/workflow/command-resolver.test.cjs +235 -0
  479. package/package.json +44 -0
  480. package/pipelines/analogy/01-decompose.md +80 -0
  481. package/pipelines/analogy/02-abstract.md +87 -0
  482. package/pipelines/analogy/03-search.md +135 -0
  483. package/pipelines/analogy/04-transfer.md +101 -0
  484. package/pipelines/analogy/05-validate.md +106 -0
  485. package/pipelines/analogy/CHAIN.md +56 -0
  486. package/pipelines/discovery/01-explore-domains.md +44 -0
  487. package/pipelines/discovery/02-think-hats.md +50 -0
  488. package/pipelines/discovery/03-analyze-needs.md +54 -0
  489. package/pipelines/discovery/CHAIN.md +37 -0
  490. package/pipelines/thesis/01-structure-argument.md +45 -0
  491. package/pipelines/thesis/02-challenge-assumptions.md +48 -0
  492. package/pipelines/thesis/03-build-thesis.md +54 -0
  493. package/pipelines/thesis/CHAIN.md +37 -0
  494. package/references/brain/causal-directives.md +91 -0
  495. package/references/brain/causal-enrichment.cypher +165 -0
  496. package/references/brain/command-triggers-schema.md +226 -0
  497. package/references/brain/graph-architecture.md +317 -0
  498. package/references/brain/query-patterns.md +460 -0
  499. package/references/brain/room-hierarchy-schema.md +218 -0
  500. package/references/brain/schema.md +76 -0
  501. package/references/capability-radar/capabilities-index.md +241 -0
  502. package/references/capability-radar/changelog-cache.md +81 -0
  503. package/references/causal/causal-schema.md +103 -0
  504. package/references/design/email-template-standard.md +155 -0
  505. package/references/design/graph-visualization-standard.md +178 -0
  506. package/references/document-generation.md +179 -0
  507. package/references/hsi/HSI-TOOLS-REFERENCE.md +222 -0
  508. package/references/import-config.md +141 -0
  509. package/references/integrations/detection-patterns.md +101 -0
  510. package/references/meeting/artifact-template.md +377 -0
  511. package/references/meeting/cross-meeting-intelligence.md +216 -0
  512. package/references/meeting/cross-relationship-patterns.md +202 -0
  513. package/references/meeting/live-join-interface.md +244 -0
  514. package/references/meeting/section-mapping.md +192 -0
  515. package/references/meeting/segment-classification.md +258 -0
  516. package/references/meeting/speaker-profile-template.md +219 -0
  517. package/references/meeting/summary-template.md +348 -0
  518. package/references/meeting/transcript-patterns.md +226 -0
  519. package/references/methodology/analyze-needs.md +135 -0
  520. package/references/methodology/analyze-systems.md +121 -0
  521. package/references/methodology/analyze-timing.md +149 -0
  522. package/references/methodology/beautiful-question.md +109 -0
  523. package/references/methodology/build-knowledge.md +161 -0
  524. package/references/methodology/build-thesis.md +237 -0
  525. package/references/methodology/challenge-assumptions.md +127 -0
  526. package/references/methodology/diagnose.md +169 -0
  527. package/references/methodology/dominant-designs.md +212 -0
  528. package/references/methodology/explore-domains.md +147 -0
  529. package/references/methodology/explore-futures.md +163 -0
  530. package/references/methodology/explore-trends.md +129 -0
  531. package/references/methodology/find-bottlenecks.md +131 -0
  532. package/references/methodology/grade.md +211 -0
  533. package/references/methodology/index.md +97 -0
  534. package/references/methodology/leadership.md +200 -0
  535. package/references/methodology/lean-canvas.md +116 -0
  536. package/references/methodology/macro-trends.md +192 -0
  537. package/references/methodology/map-unknowns.md +137 -0
  538. package/references/methodology/mullins-7-domains.md +104 -0
  539. package/references/methodology/problem-types.md +65 -0
  540. package/references/methodology/root-cause.md +178 -0
  541. package/references/methodology/sapphire-encoding.md +355 -0
  542. package/references/methodology/scenario-plan.md +178 -0
  543. package/references/methodology/score-innovation.md +154 -0
  544. package/references/methodology/structure-argument.md +158 -0
  545. package/references/methodology/systems-thinking.md +159 -0
  546. package/references/methodology/think-hats.md +147 -0
  547. package/references/methodology/triz-matrix.json +751 -0
  548. package/references/methodology/triz-principles.md +501 -0
  549. package/references/methodology/user-needs.md +199 -0
  550. package/references/methodology/validate.md +163 -0
  551. package/references/methodology/value-proposition.md +244 -0
  552. package/references/opportunities/funding-lifecycle.md +103 -0
  553. package/references/opportunities/grant-api-patterns.md +99 -0
  554. package/references/opportunities/opportunity-template.md +84 -0
  555. package/references/personality/assessment-philosophy.md +72 -0
  556. package/references/personality/lexicon.md +100 -0
  557. package/references/personality/persona-chains.md +56 -0
  558. package/references/personality/pws-lexicon-full.md +499 -0
  559. package/references/personality/voice-dna.md +156 -0
  560. package/references/personas/hat-perspectives.md +76 -0
  561. package/references/personas/persona-template.md +63 -0
  562. package/references/pipeline/act-output-contract.md +88 -0
  563. package/references/pipeline/chains-index.md +39 -0
  564. package/references/pws-profile-generation.md +79 -0
  565. package/references/reasoning/reasoning-schema.md +143 -0
  566. package/references/reasoning/reasoning-template.md +68 -0
  567. package/references/reasoning/run-template.md +38 -0
  568. package/references/research/RESEARCH_14_CLAUDE_CODE_SOURCE_ARCHITECTURE.md +209 -0
  569. package/references/research/RESEARCH_15_V1.8_OPTIMIZATION_JTBD.md +375 -0
  570. package/references/research/RESEARCH_16_NATIVE_FIRST_PLUGIN_ARCHITECTURE.md +575 -0
  571. package/references/research/RESEARCH_17_MCP_UI_FRAMEWORKS.md +272 -0
  572. package/references/taxonomy/TAXONOMY.md +192 -0
  573. package/references/templates/MINTO.md +36 -0
  574. package/references/user-research/2026-04-05-leah-lawrence-session.md +202 -0
  575. package/references/vault-kit/README.md +35 -0
  576. package/references/vault-kit/app.json +12 -0
  577. package/references/vault-kit/appearance.json +12 -0
  578. package/references/vault-kit/graph.json +35 -0
  579. package/references/vault-kit/snippets/mindrian-destijl.css +297 -0
  580. package/references/vault-kit/templates/new-artifact.md +37 -0
  581. package/references/vault-kit/templates/new-meeting-note.md +35 -0
  582. package/references/vault-kit/templates/new-team-profile.md +29 -0
  583. package/references/vault-kit/templates/new-xref.md +35 -0
  584. package/references/visual/symbol-system.md +151 -0
  585. package/skills/MOSDeckEngine/SKILL.md +325 -0
  586. package/skills/brain-connector/SKILL.md +114 -0
  587. package/skills/context-engine/SKILL.md +147 -0
  588. package/skills/conversation-mode/SKILL.md +102 -0
  589. package/skills/larry-personality/SKILL.md +219 -0
  590. package/skills/larry-personality/framework-chains.md +92 -0
  591. package/skills/larry-personality/mode-engine.md +185 -0
  592. package/skills/mullins-scaffold/SKILL.md +61 -0
  593. package/skills/mullins-scaffold/scaffold.json +146 -0
  594. package/skills/pws-methodology/SKILL.md +49 -0
  595. package/skills/room-passive/SKILL.md +165 -0
  596. package/skills/room-proactive/SKILL.md +250 -0
  597. package/skills/ui-system/SKILL.md +277 -0
@@ -0,0 +1,1019 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
6
+ *
7
+ * Phase 90-08 -- Graceful Degradation End-to-End Suite
8
+ * ====================================================
9
+ * Third-line defense for Phase 90. Proves the Brain derivation layer
10
+ * survives every realistic failure mode with four invariants intact:
11
+ *
12
+ * A. NO CRASH -- no uncaught exception; deriveSection / aggregator /
13
+ * dispatcher NEVER throw; every path returns a structured result.
14
+ * B. NO CORRUPTION -- no partial BRAIN.md left on disk; no
15
+ * BRAIN.md.tmp.*.brain orphans; atomic rename contract honored.
16
+ * C. USER-VISIBLE STATUS -- caller learns what happened via
17
+ * {success:false, reason:'...'} OR stderr / stdout markers.
18
+ * D. RETRY PATH -- after the failure clears (Brain back online, etc.),
19
+ * re-running the same invocation produces a correct BRAIN.md at
20
+ * the same path.
21
+ *
22
+ * Canon Part 8 is asserted AS A FAILURE-MODE GUARANTEE: every scenario
23
+ * that lands a BRAIN.md on disk is grep-scanned against the frozen
24
+ * FORBIDDEN_PATTERNS set; every captured Brain query payload is scanned
25
+ * too. If the boundary is real, it survives every failure.
26
+ *
27
+ * Scenarios (14):
28
+ * 01 Brain offline permanent
29
+ * 02 Brain offline intermittent (succeeds then fails)
30
+ * 03 API quota exhausted (rate_limited)
31
+ * 04 Timeout mid-derivation (hanging promise)
32
+ * 05 Schema drift mid-session (brain_graph_version shifts)
33
+ * 06 Malformed Brain response
34
+ * 07 Network partition (ECONNREFUSED)
35
+ * 08 Filesystem EACCES on section dir
36
+ * 09 Disk full on atomic rename (ENOSPC)
37
+ * 10 Concurrent deriveSection on same section
38
+ * 11 Canon Part 8 regression under ordinary operation
39
+ * (dangerous fixture; Brain succeeds normally)
40
+ * 12 Canon Part 8 regression under timeout
41
+ * (dangerous fixture; Brain times out)
42
+ * 13 Cross-room aggregator survives corrupt peer room
43
+ * 14 Concurrent session-start staleness scans (queue idempotency)
44
+ *
45
+ * Cross-cutting audits (after all scenarios):
46
+ * A1 Canon Part 8 forbidden-regex sweep across every BRAIN.md landed
47
+ * A2 Orphan tmpfile sweep across every tmp dir touched
48
+ *
49
+ * Runtime budget: total suite < 30 seconds. Each scenario self-contained
50
+ * via mkdtempSync + require.cache mock.
51
+ *
52
+ * Pure CJS, node built-ins only, zero npm deps.
53
+ */
54
+
55
+ const assert = require('node:assert/strict');
56
+ const fs = require('node:fs');
57
+ const os = require('node:os');
58
+ const path = require('node:path');
59
+ const crypto = require('node:crypto');
60
+
61
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
62
+
63
+ // ---------- Suite-wide state ----------
64
+
65
+ let passed = 0;
66
+ let failed = 0;
67
+ const failures = [];
68
+
69
+ // Track every tmp root created so A2 can sweep for orphans.
70
+ const TMP_ROOTS = [];
71
+ // Track every BRAIN.md written so A1 can scan for forbidden regex hits.
72
+ const LANDED_BRAIN_FILES = [];
73
+
74
+ // Suite-wide wall clock.
75
+ const SUITE_START = Date.now();
76
+
77
+ // ---------- Canon Part 8 forbidden-regex set ----------
78
+ //
79
+ // Byte-for-byte parity with lib/core/cross-room-aggregator.cjs
80
+ // FORBIDDEN_PATTERNS + the same set used by Plan 90-05 invariants.
81
+ // If this drifts from the canonical set, downstream tests will fail
82
+ // open. Keep in sync.
83
+
84
+ const FORBIDDEN_PATTERNS = Object.freeze([
85
+ /@[a-zA-Z0-9._-]+\.[a-zA-Z]{2,}/, // email
86
+ /\$[0-9][\d,.]*[KMB]?\b/, // currency magnitude
87
+ /\b(Lawrence|Jonathan|Nimrod|Oren|Dror)\s+(said|revealed|told|mentioned)\b/i, // quoted-person
88
+ /\bmeeting with\b/i, // meeting fragment
89
+ /\b\d{3}-\d{2}-\d{4}\b/, // SSN-like
90
+ /\b\+?1?[\s.\-]?\(?\d{3}\)?[\s.\-]?\d{3}[\s.\-]?\d{4}\b/, // phone-like
91
+ ]);
92
+
93
+ // Extra forbidden tokens used by the adversarial fixtures (hard subset;
94
+ // not regex-reachable from FORBIDDEN_PATTERNS alone).
95
+ const DANGEROUS_SUBSTRINGS = Object.freeze([
96
+ 'Lawrence',
97
+ '5M',
98
+ 'revenue',
99
+ 'Q4 meeting',
100
+ 'jonathan@mindrian.com',
101
+ '123-45-6789',
102
+ ]);
103
+
104
+ // ---------- Tmp + filesystem helpers ----------
105
+
106
+ function mkTmp(tag) {
107
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'gd-' + tag + '-'));
108
+ TMP_ROOTS.push(d);
109
+ return d;
110
+ }
111
+
112
+ function writeFile(p, body) {
113
+ fs.mkdirSync(path.dirname(p), { recursive: true });
114
+ fs.writeFileSync(p, body, 'utf8');
115
+ }
116
+
117
+ // Walk + chmod to restore before rmSync (needed for Scenario 08).
118
+ function walkChmod(root, mode) {
119
+ try {
120
+ fs.chmodSync(root, mode);
121
+ const entries = fs.readdirSync(root, { withFileTypes: true });
122
+ for (const e of entries) {
123
+ const p = path.join(root, e.name);
124
+ if (e.isDirectory()) walkChmod(p, mode);
125
+ else {
126
+ try { fs.chmodSync(p, mode); } catch (_e) { /* best effort */ }
127
+ }
128
+ }
129
+ } catch (_e) { /* best effort */ }
130
+ }
131
+
132
+ process.on('exit', function () {
133
+ for (const d of TMP_ROOTS) {
134
+ try {
135
+ walkChmod(d, 0o755);
136
+ fs.rmSync(d, { recursive: true, force: true });
137
+ } catch (_e) { /* best effort */ }
138
+ }
139
+ });
140
+
141
+ // Count orphan tmpfiles under a directory (recursive).
142
+ function countOrphanTmpfiles(root) {
143
+ let count = 0;
144
+ const orphans = [];
145
+ function walk(dir) {
146
+ let entries;
147
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
148
+ catch (_e) { return; }
149
+ for (const e of entries) {
150
+ const p = path.join(dir, e.name);
151
+ if (e.isDirectory()) walk(p);
152
+ else if (/BRAIN\.md\.tmp\..*\.brain$/.test(e.name)) {
153
+ count += 1;
154
+ orphans.push(p);
155
+ }
156
+ }
157
+ }
158
+ walk(root);
159
+ return { count: count, paths: orphans };
160
+ }
161
+
162
+ // ---------- Fixture builders ----------
163
+
164
+ function buildSection(sectionPath, opts) {
165
+ opts = opts || {};
166
+ fs.mkdirSync(sectionPath, { recursive: true });
167
+ const section = path.basename(sectionPath);
168
+ writeFile(path.join(sectionPath, 'ROOM.md'), [
169
+ '---',
170
+ 'name: "' + section + '"',
171
+ 'kind: section',
172
+ 'created_at: "2026-04-20T00:00:00Z"',
173
+ 'updated_at: "2026-04-20T00:00:00Z"',
174
+ '---',
175
+ '',
176
+ '# ' + section,
177
+ '',
178
+ opts.identity_text || 'A generic venture identity paragraph with zero personal data.',
179
+ '',
180
+ '## Section links',
181
+ '',
182
+ ].join('\n'));
183
+ writeFile(path.join(sectionPath, 'STATE.md'), [
184
+ '---',
185
+ 'artifact_count: ' + (opts.artifact_count || 2),
186
+ 'gap_status: ' + (opts.gap_status || 'partial'),
187
+ 'completeness_score: ' + (opts.completeness_score || 0.5),
188
+ 'minto_health: ' + (opts.minto_health || 'ok'),
189
+ 'last_activity_at: "2026-04-20T00:00:00Z"',
190
+ '---',
191
+ '',
192
+ '## State',
193
+ '',
194
+ ].join('\n'));
195
+ const governingThought = opts.governing_thought === undefined
196
+ ? 'The venture needs to validate its primary assumption within 90 days.'
197
+ : opts.governing_thought;
198
+ const argsCount = opts.arguments_count == null ? 4 : opts.arguments_count;
199
+ const evDensity = opts.evidence_density == null ? 0.6 : opts.evidence_density;
200
+ const meceStatus = opts.mece_status || 'pass';
201
+ const rhs = opts.reasoning_health_score == null ? 0.7 : opts.reasoning_health_score;
202
+ writeFile(path.join(sectionPath, 'MINTO.md'), [
203
+ '---',
204
+ 'schema_version: 88',
205
+ 'version: 1',
206
+ 'section: "' + section + '"',
207
+ 'last_generated_at: "2026-04-20T00:00:00Z"',
208
+ 'last_artifact_write_seen_at: "2026-04-20T00:00:00Z"',
209
+ 'arguments_count: ' + argsCount,
210
+ 'evidence_density: ' + evDensity,
211
+ 'mece_status: ' + meceStatus,
212
+ 'reasoning_health_score: ' + rhs,
213
+ 'flagged_weaknesses: []',
214
+ 'decision_log: []',
215
+ '---',
216
+ '',
217
+ '## Governing Thought',
218
+ '',
219
+ governingThought || '(empty)',
220
+ '',
221
+ '## Arguments',
222
+ '',
223
+ '- arg 1',
224
+ '- arg 2',
225
+ '',
226
+ ].join('\n'));
227
+ }
228
+
229
+ // Build a room with N sections. Returns { root, sections: [absolute paths] }.
230
+ function buildRoom(opts) {
231
+ opts = opts || {};
232
+ const root = mkTmp(opts.tag || 'room');
233
+ const count = Number.isFinite(opts.count) ? opts.count : 3;
234
+ const sections = [];
235
+ for (let i = 1; i <= count; i++) {
236
+ const name = opts.names && opts.names[i - 1] ? opts.names[i - 1] : ('section-' + i);
237
+ const p = path.join(root, name);
238
+ buildSection(p, opts.sectionOpts ? opts.sectionOpts(i, name) : {});
239
+ sections.push(p);
240
+ }
241
+ return { root: root, sections: sections };
242
+ }
243
+
244
+ // Dangerous-content fixture used by Scenarios 11 + 12.
245
+ function buildDangerousSection(sectionPath) {
246
+ buildSection(sectionPath, {
247
+ identity_text: 'The room identity mentions Lawrence said 5M revenue at the Q4 meeting with jonathan@mindrian.com.',
248
+ governing_thought: 'Lawrence said we will hit 5M revenue by Q4 if pricing holds. SSN 123-45-6789 was leaked.',
249
+ });
250
+ }
251
+
252
+ // ---------- Brain-client mock helpers ----------
253
+
254
+ function installMockBrainClient(opts) {
255
+ opts = opts || {};
256
+ const audit = {
257
+ isAvailable_calls: 0,
258
+ schema_calls: 0,
259
+ query_calls: [],
260
+ search_calls: [],
261
+ smartSearch_calls: [],
262
+ };
263
+ const brainClientPath = require.resolve('../core/brain-client.cjs');
264
+ const realClient = require('../core/brain-client.cjs');
265
+ const fakeExports = {
266
+ isAvailable: function () {
267
+ audit.isAvailable_calls += 1;
268
+ if (typeof opts.isAvailable === 'function') return opts.isAvailable(audit.isAvailable_calls);
269
+ return opts.isAvailable === undefined ? true : !!opts.isAvailable;
270
+ },
271
+ schema: async function () {
272
+ audit.schema_calls += 1;
273
+ if (typeof opts.schemaReturn === 'function') return opts.schemaReturn(audit.schema_calls);
274
+ if (opts.schemaThrows) { throw new Error(opts.schemaThrows); }
275
+ return opts.schemaReturn || { brain_graph_version: 21432 };
276
+ },
277
+ query: async function (cypher) {
278
+ audit.query_calls.push({ cypher: cypher });
279
+ if (typeof opts.query === 'function') return opts.query(cypher, audit.query_calls.length);
280
+ return { records: [{ name: 'SWOT', description: 'Classic framework' }] };
281
+ },
282
+ search: async function (q, o) {
283
+ audit.search_calls.push({ query: q, options: o });
284
+ if (typeof opts.search === 'function') return opts.search(q, o, audit.search_calls.length);
285
+ return { matches: [{ title: 'Analogy 1', score: 0.8 }] };
286
+ },
287
+ smartSearch: async function (q, o) {
288
+ audit.smartSearch_calls.push({ query: q, options: o });
289
+ return null;
290
+ },
291
+ _test: realClient._test,
292
+ };
293
+ require.cache[brainClientPath] = {
294
+ id: brainClientPath,
295
+ filename: brainClientPath,
296
+ loaded: true,
297
+ exports: fakeExports,
298
+ };
299
+ return audit;
300
+ }
301
+
302
+ function resetRequireCache() {
303
+ const targets = [
304
+ '../core/brain-client.cjs',
305
+ '../core/brain-derivation.cjs',
306
+ '../core/brain-derivation-prompts.cjs',
307
+ '../core/brain-derivation-queue.cjs',
308
+ '../core/cross-room-aggregator.cjs',
309
+ '../core/folder-memory.cjs',
310
+ '../core/folder-memory-shared.cjs',
311
+ ];
312
+ for (const t of targets) {
313
+ try {
314
+ const p = require.resolve(t);
315
+ delete require.cache[p];
316
+ } catch (_e) { /* best effort */ }
317
+ }
318
+ }
319
+
320
+ // Capture a deriveSection result's on-disk BRAIN.md (if any) for the A1
321
+ // cross-cutting audit.
322
+ function recordLandedBrainMd(sectionPath) {
323
+ const p = path.join(sectionPath, 'BRAIN.md');
324
+ if (fs.existsSync(p)) LANDED_BRAIN_FILES.push(p);
325
+ }
326
+
327
+ // Run a deriveSection call and assert invariants A + B + C. Returns the
328
+ // result object for scenario-specific assertions.
329
+ //
330
+ // Invariant A: never throws. Promise rejection counts as a crash.
331
+ // Invariant B: no orphan tmpfile in sectionPath after the call.
332
+ // Invariant C: result is a structured object with success boolean.
333
+ async function runDerive(sectionPath, options) {
334
+ const { deriveSection } = require('../core/brain-derivation.cjs');
335
+ const roomPath = path.dirname(sectionPath);
336
+ const section = path.basename(sectionPath);
337
+ let result;
338
+ let threw = false;
339
+ let err = null;
340
+ try {
341
+ result = await deriveSection(roomPath, section, options || {});
342
+ } catch (e) {
343
+ threw = true;
344
+ err = e;
345
+ }
346
+ // Invariant A.
347
+ assert.equal(threw, false, 'deriveSection must NEVER throw; got: ' + (err && err.stack ? err.stack : String(err)));
348
+ // Invariant B.
349
+ const orphans = countOrphanTmpfiles(sectionPath);
350
+ assert.equal(orphans.count, 0, 'orphan tmpfile detected after deriveSection: ' + orphans.paths.join(','));
351
+ // Invariant C.
352
+ assert.equal(typeof result, 'object', 'deriveSection must return an object');
353
+ assert.equal(typeof result.success, 'boolean', 'result.success must be a boolean');
354
+ // Record for A1.
355
+ recordLandedBrainMd(sectionPath);
356
+ return result;
357
+ }
358
+
359
+ // ---------- Scenario runner ----------
360
+
361
+ async function runScenario(name, fn) {
362
+ resetRequireCache();
363
+ const start = Date.now();
364
+ try {
365
+ await fn();
366
+ passed += 1;
367
+ process.stdout.write(' ok ' + name + ' (' + (Date.now() - start) + 'ms)\n');
368
+ } catch (err) {
369
+ failed += 1;
370
+ failures.push({ name: name, err: err });
371
+ process.stderr.write(' FAIL ' + name + '\n');
372
+ process.stderr.write(' ' + (err && err.stack ? err.stack : err) + '\n');
373
+ } finally {
374
+ // Leave tmp dirs for the post-suite A2 sweep; cleaned on process exit.
375
+ }
376
+ }
377
+
378
+ // ---------- 14 SCENARIOS ----------
379
+
380
+ async function main() {
381
+
382
+ // ------------------------------------------------------------------
383
+ // Scenario 01: Brain offline PERMANENT
384
+ // ------------------------------------------------------------------
385
+ // Setup: isAvailable returns false for every call.
386
+ // Action: deriveSection on 5 sections.
387
+ // Assert: no BRAIN.md written; every call returns {success:false,
388
+ // reason:'brain_unavailable'}; no crash; no tmpfile debris;
389
+ // audit shows zero query/search calls fired (boundary holds
390
+ // under the offline failure mode).
391
+ //
392
+ // Retry path (D): re-run with Brain online -> success.
393
+ await runScenario('Scenario 01: Brain offline permanent', async function () {
394
+ const audit = installMockBrainClient({ isAvailable: false });
395
+ const { root, sections } = buildRoom({ tag: 's01', count: 5 });
396
+ const results = [];
397
+ for (const s of sections) {
398
+ const r = await runDerive(s, {});
399
+ results.push(r);
400
+ }
401
+ // Invariant A (no crash) + B (no corruption) asserted by runDerive.
402
+ // Invariant C: user-visible status.
403
+ for (const r of results) {
404
+ assert.equal(r.success, false);
405
+ assert.equal(r.reason, 'brain_unavailable', 'offline scenario must yield brain_unavailable');
406
+ assert.equal(r.cost_tokens, 0);
407
+ }
408
+ // Zero BRAIN.md on disk.
409
+ for (const s of sections) {
410
+ assert.equal(fs.existsSync(path.join(s, 'BRAIN.md')), false, 'no BRAIN.md when Brain is offline');
411
+ }
412
+ // Part 8 under failure: zero Brain payload traffic.
413
+ assert.equal(audit.query_calls.length, 0, 'zero query calls under offline');
414
+ assert.equal(audit.search_calls.length, 0, 'zero search calls under offline');
415
+ assert.equal(audit.schema_calls, 0, 'zero schema calls under offline');
416
+ // Invariant D (retry path): Brain back online -> success.
417
+ resetRequireCache();
418
+ installMockBrainClient({});
419
+ const r2 = await runDerive(sections[0], {});
420
+ assert.equal(r2.success, true, 'retry after Brain online must succeed');
421
+ assert.ok(fs.existsSync(path.join(sections[0], 'BRAIN.md')), 'retry writes BRAIN.md');
422
+ });
423
+
424
+ // ------------------------------------------------------------------
425
+ // Scenario 02: Brain offline INTERMITTENT
426
+ // ------------------------------------------------------------------
427
+ // Setup: isAvailable -> true, but a throw on the third section's
428
+ // schema() fires. The first two must derive cleanly.
429
+ // Action: sequentially deriveSection on 5 sections.
430
+ // Assert: exactly 2 BRAIN.md files written; 3 failures report
431
+ // structured reason; no tmpfile debris anywhere.
432
+ await runScenario('Scenario 02: Brain offline intermittent', async function () {
433
+ // State machine: schema_calls >= 3 throws brain_unavailable.
434
+ let schemaCalls = 0;
435
+ installMockBrainClient({
436
+ isAvailable: true,
437
+ schemaReturn: function () {
438
+ schemaCalls += 1;
439
+ if (schemaCalls >= 3) {
440
+ throw new Error('brain_unavailable; backend dropped');
441
+ }
442
+ return { brain_graph_version: 21432 };
443
+ },
444
+ });
445
+ const { root, sections } = buildRoom({ tag: 's02', count: 5 });
446
+ const results = [];
447
+ for (const s of sections) {
448
+ results.push(await runDerive(s, {}));
449
+ }
450
+ // First 2 succeed; sections 3-5 fail (derivation_error/brain_unavailable).
451
+ assert.equal(results[0].success, true);
452
+ assert.equal(results[1].success, true);
453
+ for (let i = 2; i < 5; i++) {
454
+ assert.equal(results[i].success, false);
455
+ assert.ok(
456
+ results[i].reason === 'derivation_error' ||
457
+ results[i].reason === 'brain_unavailable' ||
458
+ results[i].reason === 'derivation_timeout',
459
+ 'intermittent failure reason expected; got ' + results[i].reason
460
+ );
461
+ }
462
+ // Exactly 2 BRAIN.md on disk.
463
+ let landed = 0;
464
+ for (const s of sections) {
465
+ if (fs.existsSync(path.join(s, 'BRAIN.md'))) landed += 1;
466
+ }
467
+ assert.equal(landed, 2, 'exactly 2 BRAIN.md under intermittent offline');
468
+ });
469
+
470
+ // ------------------------------------------------------------------
471
+ // Scenario 03: API quota exhausted (rate_limited after 1 call)
472
+ // ------------------------------------------------------------------
473
+ // Setup: the first section's query pipeline succeeds, second section's
474
+ // FIRST cypher call throws 429.
475
+ // Action: sequential deriveSection on 5 sections.
476
+ // Assert: exactly 1 BRAIN.md (section 1); sections 2..5 all report
477
+ // reason=rate_limited (subsequent ones also because the mock
478
+ // persists the 429 for every section 2+ query).
479
+ await runScenario('Scenario 03: API quota exhausted', async function () {
480
+ let sectionCount = 0;
481
+ // Track which section boundary we are in by schema() calls (one per
482
+ // deriveSection). After the first deriveSection, flip the rate-limit.
483
+ installMockBrainClient({
484
+ schemaReturn: function () {
485
+ sectionCount += 1;
486
+ return { brain_graph_version: 21432 };
487
+ },
488
+ query: function (cypher) {
489
+ if (sectionCount >= 2) {
490
+ throw new Error('429 rate_limited too many requests');
491
+ }
492
+ return { records: [{ name: 'SWOT' }] };
493
+ },
494
+ search: function () {
495
+ if (sectionCount >= 2) {
496
+ throw new Error('429 rate_limited too many requests');
497
+ }
498
+ return { matches: [{ title: 'Analogy 1' }] };
499
+ },
500
+ });
501
+ const { sections } = buildRoom({ tag: 's03', count: 5 });
502
+ const results = [];
503
+ for (const s of sections) {
504
+ results.push(await runDerive(s, {}));
505
+ }
506
+ assert.equal(results[0].success, true, 'first section must succeed before rate-limit');
507
+ for (let i = 1; i < 5; i++) {
508
+ assert.equal(results[i].success, false, 'section ' + (i + 1) + ' should fail under rate limit');
509
+ assert.equal(results[i].reason, 'rate_limited',
510
+ 'expected rate_limited; got ' + results[i].reason + ' for section ' + (i + 1));
511
+ }
512
+ let landed = 0;
513
+ for (const s of sections) {
514
+ if (fs.existsSync(path.join(s, 'BRAIN.md'))) landed += 1;
515
+ }
516
+ assert.equal(landed, 1, 'exactly 1 BRAIN.md after rate-limit mid-batch');
517
+ });
518
+
519
+ // ------------------------------------------------------------------
520
+ // Scenario 04: Timeout mid-derivation (hanging promise)
521
+ // ------------------------------------------------------------------
522
+ // Setup: query returns a promise that rejects after 200ms with ETIMEDOUT.
523
+ // Action: deriveSection; assert we come back promptly.
524
+ // Assert: success:false; reason is timeout-flavored; no BRAIN.md.
525
+ //
526
+ // Invariant D (retry): remove the timeout behavior, re-run -> success.
527
+ await runScenario('Scenario 04: Timeout mid-derivation', async function () {
528
+ installMockBrainClient({
529
+ query: function () {
530
+ return new Promise(function (_resolve, reject) {
531
+ setTimeout(function () { reject(new Error('ETIMEDOUT brain query timed out')); }, 50);
532
+ });
533
+ },
534
+ });
535
+ const { sections } = buildRoom({ tag: 's04', count: 1 });
536
+ const s = sections[0];
537
+ const start = Date.now();
538
+ const r = await runDerive(s, {});
539
+ const elapsed = Date.now() - start;
540
+ assert.ok(elapsed < 5000, 'deriveSection timeout path must not hang (elapsed ' + elapsed + 'ms)');
541
+ assert.equal(r.success, false);
542
+ assert.ok(
543
+ r.reason === 'derivation_timeout' || r.reason === 'derivation_error',
544
+ 'timeout reason expected; got ' + r.reason
545
+ );
546
+ assert.equal(fs.existsSync(path.join(s, 'BRAIN.md')), false);
547
+ // Retry path (D).
548
+ resetRequireCache();
549
+ installMockBrainClient({});
550
+ const r2 = await runDerive(s, {});
551
+ assert.equal(r2.success, true, 'retry after timeout clears must succeed');
552
+ });
553
+
554
+ // ------------------------------------------------------------------
555
+ // Scenario 05: Schema drift mid-session
556
+ // ------------------------------------------------------------------
557
+ // Setup: schema() returns brain_graph_version=100 for first section,
558
+ // 101 for second, 100 again for third.
559
+ // Action: 3 deriveSection calls.
560
+ // Assert: all 3 write BRAIN.md; each BRAIN.md records the version that
561
+ // was live at derivation time; no corruption. (Plan 90-05
562
+ // staleness re-check is a separate concern.)
563
+ await runScenario('Scenario 05: Schema drift mid-session', async function () {
564
+ let idx = 0;
565
+ const versions = [100, 101, 100];
566
+ installMockBrainClient({
567
+ schemaReturn: function () {
568
+ const v = versions[idx] || 100;
569
+ idx += 1;
570
+ return { brain_graph_version: v };
571
+ },
572
+ });
573
+ const { sections } = buildRoom({ tag: 's05', count: 3 });
574
+ const results = [];
575
+ for (const s of sections) {
576
+ results.push(await runDerive(s, {}));
577
+ }
578
+ for (const r of results) assert.equal(r.success, true);
579
+ // Each BRAIN.md carries its frontmatter brain_graph_version.
580
+ const bodies = sections.map(function (s) { return fs.readFileSync(path.join(s, 'BRAIN.md'), 'utf8'); });
581
+ assert.ok(/brain_graph_version:\s*100/.test(bodies[0]), 'section 1 must record v100');
582
+ assert.ok(/brain_graph_version:\s*101/.test(bodies[1]), 'section 2 must record v101');
583
+ assert.ok(/brain_graph_version:\s*100/.test(bodies[2]), 'section 3 must record v100');
584
+ });
585
+
586
+ // ------------------------------------------------------------------
587
+ // Scenario 06: Malformed Brain response
588
+ // ------------------------------------------------------------------
589
+ // Setup: query returns {results: 'not-an-array'} (wrong shape).
590
+ // Action: deriveSection.
591
+ // Assert: deriveSection does NOT crash; renderRecords coerces to
592
+ // empty / '(no signal)'; BRAIN.md STILL lands with empty
593
+ // section bodies (the derivation layer tolerates malformed
594
+ // records the same as empty results).
595
+ //
596
+ // (The canonical "malformed" case the Brain API can emit in practice
597
+ // is a well-formed JSON payload with an unexpected array shape; the
598
+ // renderer is defensive by design. This scenario proves that.)
599
+ await runScenario('Scenario 06: Malformed Brain response', async function () {
600
+ installMockBrainClient({
601
+ query: function () { return { results: 'not-an-array-as-expected' }; },
602
+ search: function () { return { hits: 'also-not-an-array' }; },
603
+ });
604
+ const { sections } = buildRoom({ tag: 's06', count: 1 });
605
+ const s = sections[0];
606
+ const r = await runDerive(s, {});
607
+ assert.equal(r.success, true, 'malformed responses must not crash the derivation');
608
+ const body = fs.readFileSync(path.join(s, 'BRAIN.md'), 'utf8');
609
+ // Every section body falls through to (no signal) since records are
610
+ // not in the expected shape.
611
+ assert.ok(/no signal/.test(body), 'body must mark sections as no signal under malformed response');
612
+ });
613
+
614
+ // ------------------------------------------------------------------
615
+ // Scenario 07: Network partition (ECONNREFUSED)
616
+ // ------------------------------------------------------------------
617
+ // Setup: query throws Error('ECONNREFUSED ...') synchronously.
618
+ // Action: deriveSection.
619
+ // Assert: success:false; reason derivation_error (no more specific
620
+ // category); no BRAIN.md; no tmpfile.
621
+ await runScenario('Scenario 07: Network partition', async function () {
622
+ installMockBrainClient({
623
+ query: function () { throw new Error('ECONNREFUSED brain.mindrian.ai:443'); },
624
+ search: function () { throw new Error('ECONNREFUSED brain.mindrian.ai:443'); },
625
+ });
626
+ const { sections } = buildRoom({ tag: 's07', count: 1 });
627
+ const s = sections[0];
628
+ const r = await runDerive(s, {});
629
+ assert.equal(r.success, false);
630
+ assert.ok(r.reason === 'derivation_error' || r.reason === 'brain_unavailable' || r.reason === 'derivation_timeout',
631
+ 'network partition reason; got ' + r.reason);
632
+ assert.equal(fs.existsSync(path.join(s, 'BRAIN.md')), false);
633
+ });
634
+
635
+ // ------------------------------------------------------------------
636
+ // Scenario 08: Filesystem EACCES
637
+ // ------------------------------------------------------------------
638
+ // Setup: chmod section dir to 0o555 (read+execute, no write). rename
639
+ // into the dir should fail with EACCES.
640
+ // Action: deriveSection.
641
+ // Assert: success:false; reason fs_error; no BRAIN.md; no tmpfile.
642
+ //
643
+ // Retry (D): restore permissions and re-derive -> success.
644
+ await runScenario('Scenario 08: Filesystem EACCES', async function () {
645
+ // Skip on root (tests cannot simulate EACCES as root -- bypass perms).
646
+ if (typeof process.getuid === 'function' && process.getuid() === 0) {
647
+ process.stdout.write(' skip EACCES scenario skipped (running as root)\n');
648
+ return;
649
+ }
650
+ installMockBrainClient({});
651
+ const { sections } = buildRoom({ tag: 's08', count: 1 });
652
+ const s = sections[0];
653
+ // Make the section dir read-only.
654
+ fs.chmodSync(s, 0o555);
655
+ try {
656
+ const r = await runDerive(s, {});
657
+ // Either fs_error or write path blocked; assert no BRAIN.md.
658
+ assert.equal(r.success, false, 'expected failure under EACCES');
659
+ assert.ok(r.reason === 'fs_error' || r.reason === 'schema_rejected' || /fs|access|perm/i.test(r.reason || ''),
660
+ 'expected fs_error-flavored reason; got ' + r.reason);
661
+ assert.equal(fs.existsSync(path.join(s, 'BRAIN.md')), false);
662
+ } finally {
663
+ // Restore before cleanup (so rmSync can enter) and for retry path.
664
+ fs.chmodSync(s, 0o755);
665
+ }
666
+ // Retry (D).
667
+ resetRequireCache();
668
+ installMockBrainClient({});
669
+ const r2 = await runDerive(s, {});
670
+ assert.equal(r2.success, true, 'retry after perms restored must succeed');
671
+ });
672
+
673
+ // ------------------------------------------------------------------
674
+ // Scenario 09: Disk full on atomic rename (ENOSPC)
675
+ // ------------------------------------------------------------------
676
+ // Setup: monkey-patch fs.renameSync to throw ENOSPC the first call.
677
+ // Action: deriveSection.
678
+ // Assert: success:false; reason fs_error; tmpfile cleaned; no BRAIN.md.
679
+ //
680
+ // Retry (D): restore fs.renameSync, re-run -> success.
681
+ await runScenario('Scenario 09: Disk full on atomic rename (ENOSPC)', async function () {
682
+ installMockBrainClient({});
683
+ const { sections } = buildRoom({ tag: 's09', count: 1 });
684
+ const s = sections[0];
685
+ const origRename = fs.renameSync;
686
+ let thrown = false;
687
+ fs.renameSync = function (from, to) {
688
+ if (!thrown && typeof from === 'string' && from.indexOf('BRAIN.md.tmp') !== -1) {
689
+ thrown = true;
690
+ const err = new Error('ENOSPC: no space left on device');
691
+ err.code = 'ENOSPC';
692
+ throw err;
693
+ }
694
+ return origRename.apply(fs, arguments);
695
+ };
696
+ try {
697
+ const r = await runDerive(s, {});
698
+ assert.equal(r.success, false);
699
+ assert.equal(r.reason, 'fs_error', 'ENOSPC must surface as fs_error; got ' + r.reason);
700
+ assert.equal(fs.existsSync(path.join(s, 'BRAIN.md')), false);
701
+ } finally {
702
+ fs.renameSync = origRename;
703
+ }
704
+ // Retry (D).
705
+ resetRequireCache();
706
+ installMockBrainClient({});
707
+ const r2 = await runDerive(s, {});
708
+ assert.equal(r2.success, true, 'retry after ENOSPC clears must succeed');
709
+ });
710
+
711
+ // ------------------------------------------------------------------
712
+ // Scenario 10: Concurrent deriveSection on same section
713
+ // ------------------------------------------------------------------
714
+ // Setup: launch two deriveSection calls via Promise.all on same path.
715
+ // Action: Promise.all([deriveSection, deriveSection]).
716
+ // Assert: at least one succeeds; BRAIN.md lands; no tmpfile debris.
717
+ // If one loses the race, its reason is fs_error (EEXIST from
718
+ // exclusive-create wx-flag openSync on the same tmpfile).
719
+ await runScenario('Scenario 10: Concurrent deriveSection on same section', async function () {
720
+ installMockBrainClient({});
721
+ const { sections } = buildRoom({ tag: 's10', count: 1 });
722
+ const s = sections[0];
723
+ const { deriveSection } = require('../core/brain-derivation.cjs');
724
+ const roomPath = path.dirname(s);
725
+ const section = path.basename(s);
726
+ const [a, b] = await Promise.all([
727
+ deriveSection(roomPath, section, {}),
728
+ deriveSection(roomPath, section, {}),
729
+ ]);
730
+ // At least one must have succeeded.
731
+ assert.ok(a.success || b.success, 'at least one concurrent deriveSection must succeed');
732
+ // BRAIN.md present on disk.
733
+ assert.ok(fs.existsSync(path.join(s, 'BRAIN.md')));
734
+ // No tmpfile debris.
735
+ const orphans = countOrphanTmpfiles(s);
736
+ assert.equal(orphans.count, 0, 'no orphan tmpfiles after concurrent calls');
737
+ recordLandedBrainMd(s);
738
+ });
739
+
740
+ // ------------------------------------------------------------------
741
+ // Scenario 11: Canon Part 8 under ORDINARY operation (dangerous fixture)
742
+ // ------------------------------------------------------------------
743
+ // Setup: dangerous triple ("Lawrence said 5M", emails, SSN).
744
+ // Brain succeeds normally.
745
+ // Action: deriveSection.
746
+ // Assert: BRAIN.md lands; forbidden regex set finds ZERO hits in
747
+ // BRAIN.md body; every captured Brain payload (cypher +
748
+ // pinecone search) is scrubbed of the same forbidden
749
+ // patterns. Canon Part 8 holds under normal operation.
750
+ await runScenario('Scenario 11: Canon Part 8 under ordinary operation', async function () {
751
+ const audit = installMockBrainClient({});
752
+ const { sections } = buildRoom({
753
+ tag: 's11',
754
+ count: 1,
755
+ names: ['market-analysis'],
756
+ sectionOpts: function () { return {}; }, // placeholder
757
+ });
758
+ // Overwrite with dangerous content.
759
+ buildDangerousSection(sections[0]);
760
+ const r = await runDerive(sections[0], {});
761
+ assert.equal(r.success, true, 'ordinary derivation must succeed');
762
+ const body = fs.readFileSync(path.join(sections[0], 'BRAIN.md'), 'utf8');
763
+ for (const bad of DANGEROUS_SUBSTRINGS) {
764
+ assert.equal(body.indexOf(bad), -1, 'BRAIN.md leaks dangerous substring: ' + bad);
765
+ }
766
+ for (const re of FORBIDDEN_PATTERNS) {
767
+ assert.equal(re.test(body), false, 'BRAIN.md matches forbidden regex: ' + re);
768
+ }
769
+ // Brain payload audit.
770
+ const payloads = [
771
+ ...audit.query_calls.map(function (c) { return c.cypher; }),
772
+ ...audit.search_calls.map(function (c) { return String(c.query || ''); }),
773
+ ];
774
+ assert.ok(payloads.length >= 1, 'at least 1 Brain payload expected');
775
+ for (const p of payloads) {
776
+ for (const bad of DANGEROUS_SUBSTRINGS) {
777
+ assert.equal(p.indexOf(bad), -1, 'Brain payload leaks ' + bad + ': ' + p);
778
+ }
779
+ for (const re of FORBIDDEN_PATTERNS) {
780
+ assert.equal(re.test(p), false, 'Brain payload matches forbidden regex: ' + re);
781
+ }
782
+ }
783
+ });
784
+
785
+ // ------------------------------------------------------------------
786
+ // Scenario 12: Canon Part 8 under TIMEOUT (dangerous fixture)
787
+ // ------------------------------------------------------------------
788
+ // Setup: dangerous triple + query rejects with ETIMEDOUT.
789
+ // Action: deriveSection.
790
+ // Assert: NO BRAIN.md on disk; captured Brain payloads (if any) are
791
+ // scrubbed of forbidden content. The failure path does NOT
792
+ // open a backdoor into the user-content egress boundary.
793
+ await runScenario('Scenario 12: Canon Part 8 under timeout', async function () {
794
+ const audit = installMockBrainClient({
795
+ query: function () {
796
+ return new Promise(function (_r, reject) {
797
+ setTimeout(function () { reject(new Error('ETIMEDOUT')); }, 30);
798
+ });
799
+ },
800
+ });
801
+ const { sections } = buildRoom({ tag: 's12', count: 1 });
802
+ buildDangerousSection(sections[0]);
803
+ const r = await runDerive(sections[0], {});
804
+ assert.equal(r.success, false);
805
+ assert.equal(fs.existsSync(path.join(sections[0], 'BRAIN.md')), false,
806
+ 'timeout path must not write BRAIN.md');
807
+ // Brain payloads captured before the timeout fired: every one scrubbed.
808
+ const payloads = [
809
+ ...audit.query_calls.map(function (c) { return c.cypher; }),
810
+ ...audit.search_calls.map(function (c) { return String(c.query || ''); }),
811
+ ];
812
+ for (const p of payloads) {
813
+ for (const bad of DANGEROUS_SUBSTRINGS) {
814
+ assert.equal(p.indexOf(bad), -1, 'payload leaks ' + bad + ' under timeout');
815
+ }
816
+ for (const re of FORBIDDEN_PATTERNS) {
817
+ assert.equal(re.test(p), false, 'payload matches forbidden regex under timeout: ' + re);
818
+ }
819
+ }
820
+ });
821
+
822
+ // ------------------------------------------------------------------
823
+ // Scenario 13: Cross-room aggregator survives corrupt peer room
824
+ // ------------------------------------------------------------------
825
+ // Setup: current room valid; peer room has BRAIN.md with corrupt
826
+ // frontmatter (missing required fields + garbled YAML).
827
+ // Action: aggregateContradictions.
828
+ // Assert: aggregator never throws; peer room skip_reasons shows
829
+ // unreadable OR empty >= 1; other peers still scanned;
830
+ // output sanitized.
831
+ await runScenario('Scenario 13: Cross-room aggregator survives corrupt peer room', async function () {
832
+ // Set MINDRIAN_ROOMS_ROOT env to a tmp dir and seed a registry.
833
+ const hostRoot = mkTmp('s13');
834
+ const envRoot = hostRoot;
835
+ const prev = process.env.MINDRIAN_ROOMS_ROOT;
836
+ process.env.MINDRIAN_ROOMS_ROOT = envRoot;
837
+ try {
838
+ // Build three rooms under envRoot.
839
+ const roomA = path.join(envRoot, 'room-a'); // current
840
+ const roomB = path.join(envRoot, 'room-b'); // corrupt peer
841
+ const roomC = path.join(envRoot, 'room-c'); // healthy peer
842
+ fs.mkdirSync(roomA, { recursive: true });
843
+ fs.mkdirSync(roomB, { recursive: true });
844
+ fs.mkdirSync(roomC, { recursive: true });
845
+ // Seed current room with a valid section.
846
+ buildSection(path.join(roomA, 'shared-section'), {});
847
+ // Seed ROOM.md on every room so aggregator does not opt-out.
848
+ for (const r of [roomA, roomB, roomC]) {
849
+ writeFile(path.join(r, 'ROOM.md'), [
850
+ '---', 'name: "' + path.basename(r) + '"', 'kind: room', '---', '', '# ' + path.basename(r), '',
851
+ ].join('\n'));
852
+ }
853
+ // Seed peer room B with corrupt BRAIN.md.
854
+ buildSection(path.join(roomB, 'shared-section'), {});
855
+ writeFile(path.join(roomB, 'shared-section', 'BRAIN.md'), [
856
+ '---',
857
+ 'section: "shared-section"',
858
+ '!!!corrupt-yaml-body not an actual frontmatter scalar {{',
859
+ 'brain_generated_at: not-an-iso-date',
860
+ '---',
861
+ '',
862
+ '## Pattern Matches',
863
+ '',
864
+ '(corrupt body)',
865
+ '',
866
+ ].join('\n'));
867
+ // Seed peer room C with valid BRAIN.md.
868
+ buildSection(path.join(roomC, 'shared-section'), {});
869
+ writeFile(path.join(roomC, 'shared-section', 'BRAIN.md'), [
870
+ '---',
871
+ 'section: "shared-section"',
872
+ 'brain_generated_at: "2026-04-21T00:00:00Z"',
873
+ 'brain_graph_version: 21432',
874
+ 'governing_thought_hash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaa"',
875
+ 'staleness: "fresh"',
876
+ 'author: "brain"',
877
+ 'stale_reason: null',
878
+ 'prompt_version: "90-01-v1"',
879
+ 'cost_tokens: 60',
880
+ 'brain_query_count: 1',
881
+ '---',
882
+ '',
883
+ '## Pattern Matches',
884
+ '',
885
+ '- SWOT',
886
+ '',
887
+ ].join('\n'));
888
+ // Seed .rooms/registry.json under envRoot.
889
+ writeFile(path.join(envRoot, '.rooms', 'registry.json'), JSON.stringify({
890
+ version: 1,
891
+ rooms: {
892
+ 'room-a': { path: roomA, active: true },
893
+ 'room-b': { path: roomB, active: false },
894
+ 'room-c': { path: roomC, active: false },
895
+ },
896
+ }, null, 2));
897
+ // Resolve the aggregator with the env override.
898
+ resetRequireCache();
899
+ const xroom = require('../core/cross-room-aggregator.cjs');
900
+ let threw = false;
901
+ let result;
902
+ try {
903
+ result = await xroom.aggregateContradictions(roomA, { opt_in: true });
904
+ } catch (e) {
905
+ threw = true;
906
+ }
907
+ assert.equal(threw, false, 'aggregator must not throw on corrupt peer');
908
+ assert.equal(typeof result, 'object', 'aggregator must return object');
909
+ assert.ok(Array.isArray(result.contradictions), 'contradictions must be array');
910
+ // Either unreadable OR empty OR no contradictions detected -- the
911
+ // critical assertion is that scanning proceeds.
912
+ const scannedOrSkipped = (result.scanned_rooms || 0) + (result.skipped_rooms || 0);
913
+ assert.ok(scannedOrSkipped >= 1, 'at least one peer must be processed (scanned or skipped)');
914
+ // JSON.stringify output must contain no forbidden content (Canon Part 8).
915
+ const ser = JSON.stringify(result);
916
+ for (const re of FORBIDDEN_PATTERNS) {
917
+ assert.equal(re.test(ser), false, 'aggregator output leaks forbidden regex under corrupt peer: ' + re);
918
+ }
919
+ } finally {
920
+ if (prev === undefined) delete process.env.MINDRIAN_ROOMS_ROOT;
921
+ else process.env.MINDRIAN_ROOMS_ROOT = prev;
922
+ }
923
+ });
924
+
925
+ // ------------------------------------------------------------------
926
+ // Scenario 14: Concurrent session-start staleness scans
927
+ // ------------------------------------------------------------------
928
+ // Setup: two simulated session-start staleness scans that both
929
+ // enqueue the same section with the SAME new hash.
930
+ // Action: parallel enqueue().
931
+ // Assert: queue has a SINGLE entry for the section (idempotency from
932
+ // Plan 90-02); queue JSON is valid; no lost entries.
933
+ await runScenario('Scenario 14: Concurrent session-start staleness scans', async function () {
934
+ const roomDir = mkTmp('s14-queue');
935
+ const q = require('../core/brain-derivation-queue.cjs');
936
+ const emptyHash = 'sha256:' + crypto.createHash('sha256').update('').digest('hex');
937
+ const newHash = 'sha256:' + crypto.createHash('sha256').update('governing thought v2').digest('hex');
938
+ // Fire 5 parallel enqueues on the same section + same new hash.
939
+ const targets = [];
940
+ for (let i = 0; i < 5; i++) {
941
+ targets.push(Promise.resolve().then(function () {
942
+ return q.enqueue(roomDir, 'market-analysis', emptyHash, newHash, q.ALLOWED_REASONS.SESSION_START_STALE);
943
+ }));
944
+ }
945
+ const results = await Promise.all(targets);
946
+ // All five must return queued:true.
947
+ for (const r of results) {
948
+ assert.equal(r.queued, true, 'every enqueue must return queued:true; got ' + JSON.stringify(r));
949
+ }
950
+ // Queue file is valid JSON with exactly one entry for this section.
951
+ const final = q.readQueue(roomDir);
952
+ const matches = final.entries.filter(function (e) { return e.section === 'market-analysis'; });
953
+ assert.equal(matches.length, 1, 'expected exactly 1 entry for section; got ' + matches.length);
954
+ assert.equal(matches[0].new_governing_thought_hash, newHash);
955
+ // Queue file is valid JSON.
956
+ const raw = fs.readFileSync(path.join(roomDir, '.mindrian', 'brain-derivation-queue.json'), 'utf8');
957
+ let parsed;
958
+ assert.doesNotThrow(function () { parsed = JSON.parse(raw); }, 'queue JSON must be valid after concurrent writes');
959
+ assert.equal(Array.isArray(parsed.entries), true);
960
+ });
961
+
962
+ // ------------------------------------------------------------------
963
+ // A1 Cross-cutting: Canon Part 8 forbidden-regex sweep across every
964
+ // BRAIN.md landed during the suite.
965
+ // ------------------------------------------------------------------
966
+ await runScenario('Audit A1: Canon Part 8 forbidden-regex sweep across all landed BRAIN.md', function () {
967
+ const hits = [];
968
+ const seen = new Set(LANDED_BRAIN_FILES);
969
+ for (const p of seen) {
970
+ if (!fs.existsSync(p)) continue;
971
+ const body = fs.readFileSync(p, 'utf8');
972
+ for (const re of FORBIDDEN_PATTERNS) {
973
+ if (re.test(body)) hits.push({ path: p, regex: String(re) });
974
+ }
975
+ for (const bad of DANGEROUS_SUBSTRINGS) {
976
+ if (body.indexOf(bad) !== -1) hits.push({ path: p, token: bad });
977
+ }
978
+ }
979
+ assert.equal(hits.length, 0, 'Canon Part 8 sweep found hits: ' + JSON.stringify(hits));
980
+ });
981
+
982
+ // ------------------------------------------------------------------
983
+ // A2 Cross-cutting: orphan tmpfile sweep across every tmp root.
984
+ // ------------------------------------------------------------------
985
+ await runScenario('Audit A2: orphan tmpfile sweep across all suite tmp roots', function () {
986
+ let total = 0;
987
+ const paths = [];
988
+ for (const root of TMP_ROOTS) {
989
+ const orphans = countOrphanTmpfiles(root);
990
+ total += orphans.count;
991
+ for (const p of orphans.paths) paths.push(p);
992
+ }
993
+ assert.equal(total, 0, 'orphan tmpfile(s) remain after suite: ' + paths.join(','));
994
+ });
995
+
996
+ // ---------- Final ----------
997
+
998
+ const suiteMs = Date.now() - SUITE_START;
999
+ process.stdout.write(
1000
+ '\nbrain-derivation-graceful-degradation: ' + passed + '/' + (passed + failed) +
1001
+ ' passed (' + suiteMs + 'ms total)\n'
1002
+ );
1003
+ if (failed > 0) {
1004
+ process.stderr.write('\nFAILURES:\n');
1005
+ for (const f of failures) {
1006
+ process.stderr.write(' - ' + f.name + '\n');
1007
+ }
1008
+ }
1009
+ // Performance budget assertion: < 30s.
1010
+ if (suiteMs > 30000) {
1011
+ process.stderr.write('WARN: suite exceeded 30000ms budget (actual: ' + suiteMs + 'ms)\n');
1012
+ }
1013
+ process.exit(failed === 0 ? 0 : 1);
1014
+ }
1015
+
1016
+ main().catch(function (e) {
1017
+ process.stderr.write('Uncaught in main: ' + (e && e.stack ? e.stack : e) + '\n');
1018
+ process.exit(1);
1019
+ });