@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,848 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
6
+ * Phase 89.2 Plan 02 -- academic fetcher fixture suite.
7
+ *
8
+ * 18 scenarios total: 12 fetcher tests + 6 validator tests.
9
+ *
10
+ * Test 1 happy path 6 sources (mock fetch returns paper batches per source)
11
+ * Test 2 dedup determinism (same input twice -> identical output)
12
+ * Test 3 API-key-missing graceful (scopus + ieee + nature env unset)
13
+ * Test 4 429 rate-limit graceful (openalex returns 429; fetcher continues)
14
+ * Test 5 timeout graceful (AbortError; fetcher continues with remaining sources)
15
+ * Test 6 malformed response graceful (arXiv returns non-XML; api_error logged)
16
+ * Test 7 per-source budget exhausted (seed 100 entries; openalex skipped)
17
+ * Test 8 CANON PART 8 adversarial: leaked-artifact-body in query throws
18
+ * Test 9 CANON PART 8 adversarial: leaked-venture-name in query throws
19
+ * Test 10 CANON PART 8 adversarial: leaked-meeting-fragment in query throws
20
+ * Test 11 CANON PART 8 adversarial: leaked-financial-figure in query throws
21
+ * Test 12 chokepoint exclusivity: every fetch() call site is inside a
22
+ * per-source dispatcher invoked by fetchAcademic via
23
+ * buildAcademicQuery; static grep enforces this.
24
+ *
25
+ * V1 validator Check A: telemetry file absent -> {severity: null}
26
+ * V2 validator Check B: per-source budget exceeded -> warning
27
+ * V3 validator Check C: status enum violation -> warning
28
+ * V4 validator Check D: query_text literal present -> CRITICAL canon_boundary
29
+ * V5 validator Check E: query_text_hash format violation -> warning
30
+ * V6 validator Check F: fetched_at malformed ISO-8601 -> warning
31
+ *
32
+ * Suite-end audits:
33
+ * A1 every captured outbound URL + every captured telemetry record
34
+ * JSON.stringify scanned against FORBIDDEN_PATTERNS; ZERO hits
35
+ * A2 parity gate: rs-egress-prompts FORBIDDEN_PATTERNS byte-for-byte
36
+ * === cross-room-aggregator FORBIDDEN_PATTERNS at every regex.source
37
+ *
38
+ * Pure CJS, zero npm deps, node built-ins only (assert, fs, os, path, crypto).
39
+ */
40
+
41
+ const assert = require('node:assert/strict');
42
+ const fs = require('node:fs');
43
+ const os = require('node:os');
44
+ const path = require('node:path');
45
+ const crypto = require('node:crypto');
46
+
47
+ // ---------- Suite bookkeeping ----------
48
+
49
+ let passed = 0;
50
+ let failed = 0;
51
+ const failures = [];
52
+
53
+ // Captured outputs for end-of-suite A1 sweep.
54
+ const SCENARIO_RESULTS = [];
55
+
56
+ // Captured outbound URLs from mock fetch (for chokepoint + A1 audit).
57
+ // Two ledgers: per-scenario (cleared on each installMockFetch) + cumulative
58
+ // (never cleared; A1 sweep scans this).
59
+ const CAPTURED_URLS = [];
60
+ const CAPTURED_URLS_ALL = [];
61
+
62
+ let testHomeDir = null;
63
+ let originalHome = null;
64
+
65
+ const ALL_TMP_ROOTS = [];
66
+
67
+ process.on('exit', function () {
68
+ if (originalHome !== null) {
69
+ process.env.HOME = originalHome;
70
+ }
71
+ for (const d of ALL_TMP_ROOTS) {
72
+ try { fs.rmSync(d, { recursive: true, force: true }); } catch (_e) { /* best effort */ }
73
+ }
74
+ });
75
+
76
+ function setupScopedHome() {
77
+ if (originalHome === null) {
78
+ originalHome = process.env.HOME;
79
+ }
80
+ testHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rsfetacad-home-'));
81
+ ALL_TMP_ROOTS.push(testHomeDir);
82
+ process.env.HOME = testHomeDir;
83
+ resetRequireCache();
84
+ }
85
+
86
+ function resetRequireCache() {
87
+ const targets = [
88
+ '../core/rs-egress-violations.cjs',
89
+ '../core/rs-egress-prompts.cjs',
90
+ '../core/rs-egress-telemetry.cjs',
91
+ '../core/rs-fetcher-academic.cjs',
92
+ '../core/cross-room-aggregator.cjs',
93
+ './validators/external-academic-invariants.cjs',
94
+ ];
95
+ for (const t of targets) {
96
+ try {
97
+ const p = require.resolve(t);
98
+ delete require.cache[p];
99
+ } catch (_e) { /* best effort */ }
100
+ }
101
+ }
102
+
103
+ // ---------- Mock fetch ----------
104
+ //
105
+ // Map<urlPrefix, handler(url, opts) -> {ok, status, headers?, json?, text?}>
106
+ // Wildcard: any prefix matches as substring (.indexOf >= 0). First match wins.
107
+
108
+ let originalFetch = null;
109
+ const mockResponses = new Map();
110
+
111
+ function installMockFetch(responses) {
112
+ if (originalFetch === null) {
113
+ originalFetch = global.fetch;
114
+ }
115
+ mockResponses.clear();
116
+ // Clear captured URLs per install so per-scenario assertions see only
117
+ // this scenario's traffic. Cross-scenario aggregation is preserved by
118
+ // scenarios that explicitly read CAPTURED_URLS before the next install.
119
+ CAPTURED_URLS.length = 0;
120
+ for (const [k, v] of Object.entries(responses)) {
121
+ mockResponses.set(k, v);
122
+ }
123
+ global.fetch = async function (url, opts) {
124
+ CAPTURED_URLS.push(String(url));
125
+ CAPTURED_URLS_ALL.push(String(url));
126
+ for (const [prefix, handler] of mockResponses.entries()) {
127
+ if (String(url).indexOf(prefix) >= 0) {
128
+ return handler(url, opts);
129
+ }
130
+ }
131
+ return { ok: false, status: 404, headers: new Map(), async json() { return {}; }, async text() { return ''; } };
132
+ };
133
+ }
134
+
135
+ function restoreFetch() {
136
+ if (originalFetch !== null) {
137
+ global.fetch = originalFetch;
138
+ }
139
+ mockResponses.clear();
140
+ }
141
+
142
+ // ---------- Scenario runner ----------
143
+
144
+ async function runScenario(name, fn) {
145
+ const start = Date.now();
146
+ try {
147
+ await fn();
148
+ passed += 1;
149
+ process.stdout.write(' ok ' + name + ' (' + (Date.now() - start) + 'ms)\n');
150
+ } catch (err) {
151
+ failed += 1;
152
+ failures.push({ name: name, err: err });
153
+ process.stderr.write(' FAIL ' + name + '\n');
154
+ process.stderr.write(' ' + (err && err.stack ? err.stack : err) + '\n');
155
+ } finally {
156
+ restoreFetch();
157
+ resetRequireCache();
158
+ }
159
+ }
160
+
161
+ // ---------- Helpers ----------
162
+
163
+ function clearApiKeys() {
164
+ delete process.env.SCOPUS_API_KEY;
165
+ delete process.env.IEEE_API_KEY;
166
+ delete process.env.NATURE_API_KEY;
167
+ }
168
+
169
+ function setApiKeys() {
170
+ process.env.SCOPUS_API_KEY = 'test-scopus-key';
171
+ process.env.IEEE_API_KEY = 'test-ieee-key';
172
+ process.env.NATURE_API_KEY = 'test-nature-key';
173
+ }
174
+
175
+ function makeOpenAlexResponse(query, n) {
176
+ // Simulated OpenAlex shape with abstract_inverted_index.
177
+ const works = [];
178
+ for (let i = 0; i < n; i++) {
179
+ works.push({
180
+ id: 'https://openalex.org/W' + i + 'q' + crypto.createHash('sha256').update(query).digest('hex').slice(0, 6),
181
+ title: 'OpenAlex paper ' + i + ' for ' + query,
182
+ abstract_inverted_index: { 'cancer': [0], 'biomarker': [1], 'study': [2] },
183
+ publication_year: 2024,
184
+ authorships: [{ author: { display_name: 'Author A' + i }, institutions: [{ display_name: 'Inst-' + i }] }],
185
+ doi: 'https://doi.org/10.1000/oa-' + i + '-' + crypto.createHash('sha256').update(query).digest('hex').slice(0, 6),
186
+ });
187
+ }
188
+ return { results: works };
189
+ }
190
+
191
+ function makeArxivXml(query, n) {
192
+ let entries = '';
193
+ for (let i = 0; i < n; i++) {
194
+ const tag = crypto.createHash('sha256').update(query).digest('hex').slice(0, 6);
195
+ entries += '<entry><id>http://arxiv.org/abs/2401.000' + i + tag + '</id>'
196
+ + '<title>arXiv paper ' + i + ' for ' + query + '</title>'
197
+ + '<summary>Abstract for arxiv paper ' + i + '.</summary>'
198
+ + '<published>2024-01-' + ((i % 28) + 1).toString().padStart(2, '0') + 'T00:00:00Z</published>'
199
+ + '<author><name>Arxiv Author ' + i + '</name></author>'
200
+ + '</entry>';
201
+ }
202
+ return '<?xml version="1.0" encoding="UTF-8"?>'
203
+ + '<feed xmlns="http://www.w3.org/2005/Atom">'
204
+ + entries
205
+ + '</feed>';
206
+ }
207
+
208
+ function makePubMedResponse(query, n) {
209
+ const idlist = [];
210
+ for (let i = 0; i < n; i++) {
211
+ idlist.push('PM' + i + crypto.createHash('sha256').update(query).digest('hex').slice(0, 6));
212
+ }
213
+ return { esearchresult: { idlist: idlist, count: String(n) } };
214
+ }
215
+
216
+ function makeScopusResponse(query, n) {
217
+ const entries = [];
218
+ for (let i = 0; i < n; i++) {
219
+ entries.push({
220
+ 'dc:identifier': 'SCOPUS_ID:' + i + crypto.createHash('sha256').update(query).digest('hex').slice(0, 6),
221
+ 'dc:title': 'Scopus paper ' + i + ' for ' + query,
222
+ 'dc:description': 'Scopus abstract ' + i,
223
+ 'dc:creator': 'Scopus Author ' + i,
224
+ 'prism:doi': '10.1234/scopus-' + i,
225
+ 'affiliation': [{ 'affilname': 'Scopus Inst ' + i }],
226
+ });
227
+ }
228
+ return { 'search-results': { entry: entries } };
229
+ }
230
+
231
+ function makeIeeeResponse(query, n) {
232
+ const articles = [];
233
+ for (let i = 0; i < n; i++) {
234
+ articles.push({
235
+ article_number: 'IEEE' + i + crypto.createHash('sha256').update(query).digest('hex').slice(0, 6),
236
+ title: 'IEEE paper ' + i + ' for ' + query,
237
+ abstract: 'IEEE abstract ' + i,
238
+ authors: { authors: [{ full_name: 'IEEE Author ' + i, affiliation: 'IEEE Inst ' + i }] },
239
+ doi: '10.1109/ieee-' + i,
240
+ });
241
+ }
242
+ return { articles: articles, total_records: n };
243
+ }
244
+
245
+ function makeNatureResponse(query, n) {
246
+ const records = [];
247
+ for (let i = 0; i < n; i++) {
248
+ records.push({
249
+ identifier: 'doi:10.1038/nat-' + i + crypto.createHash('sha256').update(query).digest('hex').slice(0, 6),
250
+ title: 'Nature paper ' + i + ' for ' + query,
251
+ abstract: 'Nature abstract ' + i,
252
+ creators: [{ creator: 'Nature Author ' + i }],
253
+ doi: '10.1038/nat-' + i,
254
+ });
255
+ }
256
+ return { records: records };
257
+ }
258
+
259
+ function buildAllSourcesMockOk(query, perSource) {
260
+ return {
261
+ 'https://api.openalex.org/works': async function (url) {
262
+ return {
263
+ ok: true,
264
+ status: 200,
265
+ headers: new Map([['x-ratelimit-remaining', '99']]),
266
+ async json() { return makeOpenAlexResponse(query, perSource); },
267
+ async text() { return JSON.stringify(makeOpenAlexResponse(query, perSource)); },
268
+ };
269
+ },
270
+ 'export.arxiv.org/api/query': async function (url) {
271
+ return {
272
+ ok: true,
273
+ status: 200,
274
+ headers: new Map(),
275
+ async text() { return makeArxivXml(query, perSource); },
276
+ async json() { throw new Error('arxiv returns XML'); },
277
+ };
278
+ },
279
+ 'eutils.ncbi.nlm.nih.gov': async function (url) {
280
+ return {
281
+ ok: true,
282
+ status: 200,
283
+ headers: new Map(),
284
+ async json() { return makePubMedResponse(query, perSource); },
285
+ async text() { return JSON.stringify(makePubMedResponse(query, perSource)); },
286
+ };
287
+ },
288
+ 'api.elsevier.com/content/search/scopus': async function (url) {
289
+ return {
290
+ ok: true,
291
+ status: 200,
292
+ headers: new Map(),
293
+ async json() { return makeScopusResponse(query, perSource); },
294
+ async text() { return JSON.stringify(makeScopusResponse(query, perSource)); },
295
+ };
296
+ },
297
+ 'ieeexploreapi.ieee.org': async function (url) {
298
+ return {
299
+ ok: true,
300
+ status: 200,
301
+ headers: new Map(),
302
+ async json() { return makeIeeeResponse(query, perSource); },
303
+ async text() { return JSON.stringify(makeIeeeResponse(query, perSource)); },
304
+ };
305
+ },
306
+ 'api.springernature.com': async function (url) {
307
+ return {
308
+ ok: true,
309
+ status: 200,
310
+ headers: new Map(),
311
+ async json() { return makeNatureResponse(query, perSource); },
312
+ async text() { return JSON.stringify(makeNatureResponse(query, perSource)); },
313
+ };
314
+ },
315
+ };
316
+ }
317
+
318
+ // ---------- A1/A2 sweep helpers ----------
319
+
320
+ function getForbiddenPatternsFromAggregator() {
321
+ const aggregator = require('../core/cross-room-aggregator.cjs');
322
+ return aggregator.FORBIDDEN_PATTERNS;
323
+ }
324
+
325
+ function scanAgainstForbidden(stringified) {
326
+ const patterns = getForbiddenPatternsFromAggregator();
327
+ for (const re of patterns) {
328
+ if (re.test(stringified)) {
329
+ return { hit: true, pattern: re.source };
330
+ }
331
+ }
332
+ return { hit: false };
333
+ }
334
+
335
+ // ---------- Begin scenarios ----------
336
+
337
+ console.log('=== 89.2-02 fetcher-academic suite: starting ===');
338
+
339
+ (async function main() {
340
+
341
+ // ---------- Test 1: happy path 6 sources ----------
342
+ await runScenario('Test 1: happy path 6 sources (with API keys set)', async function () {
343
+ setupScopedHome();
344
+ setApiKeys();
345
+ const queries = ['cancer immunotherapy biomarkers'];
346
+ installMockFetch(buildAllSourcesMockOk(queries[0], 5));
347
+ const fetcher = require('../core/rs-fetcher-academic.cjs');
348
+ const out = await fetcher.fetchAcademic(queries, {});
349
+ assert.ok(out && Array.isArray(out.papers), 'papers is array');
350
+ assert.ok(out.papers.length >= 20,
351
+ 'expected >= 20 papers across 6 sources after dedup; got ' + out.papers.length);
352
+ const sample = out.papers[0];
353
+ for (const f of ['id', 'title', 'abstract', 'authors', 'institution', 'doi', 'source', 'fetched_at']) {
354
+ assert.ok(Object.prototype.hasOwnProperty.call(sample, f),
355
+ 'paper missing field: ' + f);
356
+ }
357
+ assert.ok(Array.isArray(sample.authors), 'authors is array');
358
+ const sources = new Set(out.papers.map(p => p.source));
359
+ for (const s of ['openalex', 'arxiv', 'pubmed', 'scopus', 'ieee', 'nature']) {
360
+ assert.ok(sources.has(s), 'source coverage missing: ' + s);
361
+ }
362
+ SCENARIO_RESULTS.push({ test: 'T1', surface: 'academic', payload: out });
363
+ });
364
+
365
+ // ---------- Test 2: dedup determinism ----------
366
+ await runScenario('Test 2: dedup determinism (same input twice == identical output)', async function () {
367
+ setupScopedHome();
368
+ setApiKeys();
369
+ const queries = ['CRISPR off-target detection'];
370
+ installMockFetch(buildAllSourcesMockOk(queries[0], 5));
371
+ const fetcher = require('../core/rs-fetcher-academic.cjs');
372
+ const r1 = await fetcher.fetchAcademic(queries, {});
373
+ // Reset captured URLs and HOME to avoid budget contamination.
374
+ CAPTURED_URLS.length = 0;
375
+ setupScopedHome();
376
+ installMockFetch(buildAllSourcesMockOk(queries[0], 5));
377
+ const fetcher2 = require('../core/rs-fetcher-academic.cjs');
378
+ const r2 = await fetcher2.fetchAcademic(queries, {});
379
+ // Strip fetched_at because timestamps will diff. Compare structural shape.
380
+ const stripT = function (papers) {
381
+ return papers.map(p => {
382
+ const c = Object.assign({}, p);
383
+ delete c.fetched_at;
384
+ return c;
385
+ });
386
+ };
387
+ assert.equal(JSON.stringify(stripT(r1.papers)), JSON.stringify(stripT(r2.papers)),
388
+ 'dedup output must be deterministic across runs');
389
+ SCENARIO_RESULTS.push({ test: 'T2', surface: 'academic', payload: stripT(r1.papers) });
390
+ });
391
+
392
+ // ---------- Test 3: API-key-missing graceful ----------
393
+ await runScenario('Test 3: API-key-missing graceful (scopus + ieee + nature unset)', async function () {
394
+ setupScopedHome();
395
+ clearApiKeys();
396
+ const queries = ['quantum brain imaging'];
397
+ installMockFetch(buildAllSourcesMockOk(queries[0], 3));
398
+ const fetcher = require('../core/rs-fetcher-academic.cjs');
399
+ const out = await fetcher.fetchAcademic(queries, {});
400
+ assert.ok(Array.isArray(out.papers), 'papers is array');
401
+ // Only openalex + arxiv + pubmed should populate papers.
402
+ const sources = new Set(out.papers.map(p => p.source));
403
+ assert.ok(sources.has('openalex'), 'openalex present');
404
+ assert.ok(sources.has('arxiv'), 'arxiv present');
405
+ assert.ok(sources.has('pubmed'), 'pubmed present');
406
+ assert.ok(!sources.has('scopus'), 'scopus must be skipped (no API key)');
407
+ assert.ok(!sources.has('ieee'), 'ieee must be skipped (no API key)');
408
+ assert.ok(!sources.has('nature'), 'nature must be skipped (no API key)');
409
+
410
+ // Check telemetry has 3 api_key_missing entries.
411
+ const telemetry = require('../core/rs-egress-telemetry.cjs');
412
+ const file = telemetry.TELEMETRY_FILE;
413
+ assert.ok(fs.existsSync(file), 'telemetry exists at ' + file);
414
+ const payload = JSON.parse(fs.readFileSync(file, 'utf8'));
415
+ const keyMissing = payload.entries.filter(e => e.status === 'api_key_missing');
416
+ assert.equal(keyMissing.length, 3,
417
+ 'expected 3 api_key_missing entries; got ' + keyMissing.length);
418
+ SCENARIO_RESULTS.push({ test: 'T3', surface: 'academic', payload: out });
419
+ });
420
+
421
+ // ---------- Test 4: 429 rate-limit graceful ----------
422
+ await runScenario('Test 4: 429 rate-limit graceful (openalex returns 429)', async function () {
423
+ setupScopedHome();
424
+ clearApiKeys();
425
+ const queries = ['graphene heat conductivity'];
426
+ const mocks = buildAllSourcesMockOk(queries[0], 4);
427
+ mocks['https://api.openalex.org/works'] = async function () {
428
+ return {
429
+ ok: false,
430
+ status: 429,
431
+ headers: new Map([['retry-after', '60']]),
432
+ async json() { return { error: 'rate limited' }; },
433
+ async text() { return 'rate limited'; },
434
+ };
435
+ };
436
+ installMockFetch(mocks);
437
+ const fetcher = require('../core/rs-fetcher-academic.cjs');
438
+ const out = await fetcher.fetchAcademic(queries, {});
439
+ // Should not throw. Should still have papers from arxiv + pubmed.
440
+ const sources = new Set(out.papers.map(p => p.source));
441
+ assert.ok(!sources.has('openalex'), 'openalex must be empty after 429');
442
+ assert.ok(sources.has('arxiv'), 'arxiv populates');
443
+ assert.ok(sources.has('pubmed'), 'pubmed populates');
444
+
445
+ const telemetry = require('../core/rs-egress-telemetry.cjs');
446
+ const payload = JSON.parse(fs.readFileSync(telemetry.TELEMETRY_FILE, 'utf8'));
447
+ const rateLimited = payload.entries.filter(e => e.status === 'rate_limited');
448
+ assert.ok(rateLimited.length >= 1, 'rate_limited telemetry recorded');
449
+ assert.equal(rateLimited[0].http_status, 429, 'http_status 429 recorded');
450
+ SCENARIO_RESULTS.push({ test: 'T4', surface: 'academic', payload: out });
451
+ });
452
+
453
+ // ---------- Test 5: timeout graceful ----------
454
+ await runScenario('Test 5: timeout graceful (openalex throws AbortError)', async function () {
455
+ setupScopedHome();
456
+ clearApiKeys();
457
+ const queries = ['CAR-T cell therapy'];
458
+ const mocks = buildAllSourcesMockOk(queries[0], 3);
459
+ mocks['https://api.openalex.org/works'] = async function () {
460
+ const err = new Error('aborted');
461
+ err.name = 'AbortError';
462
+ throw err;
463
+ };
464
+ installMockFetch(mocks);
465
+ const fetcher = require('../core/rs-fetcher-academic.cjs');
466
+ const out = await fetcher.fetchAcademic(queries, {});
467
+ // Should not throw. Should still have papers from arxiv + pubmed.
468
+ const sources = new Set(out.papers.map(p => p.source));
469
+ assert.ok(!sources.has('openalex'), 'openalex empty after timeout');
470
+ assert.ok(sources.has('arxiv'), 'arxiv populates');
471
+
472
+ const telemetry = require('../core/rs-egress-telemetry.cjs');
473
+ const payload = JSON.parse(fs.readFileSync(telemetry.TELEMETRY_FILE, 'utf8'));
474
+ const timed = payload.entries.filter(e => e.status === 'timeout');
475
+ assert.ok(timed.length >= 1, 'timeout telemetry recorded');
476
+ SCENARIO_RESULTS.push({ test: 'T5', surface: 'academic', payload: out });
477
+ });
478
+
479
+ // ---------- Test 6: malformed response graceful ----------
480
+ await runScenario('Test 6: malformed response graceful (arXiv returns non-XML)', async function () {
481
+ setupScopedHome();
482
+ clearApiKeys();
483
+ const queries = ['mRNA vaccine technology'];
484
+ const mocks = buildAllSourcesMockOk(queries[0], 3);
485
+ mocks['export.arxiv.org/api/query'] = async function () {
486
+ return {
487
+ ok: true,
488
+ status: 200,
489
+ headers: new Map(),
490
+ async text() { return '<<<not-xml>>>'; },
491
+ async json() { throw new Error('not json'); },
492
+ };
493
+ };
494
+ installMockFetch(mocks);
495
+ const fetcher = require('../core/rs-fetcher-academic.cjs');
496
+ const out = await fetcher.fetchAcademic(queries, {});
497
+ const sources = new Set(out.papers.map(p => p.source));
498
+ assert.ok(!sources.has('arxiv'), 'arxiv empty after parse failure');
499
+ assert.ok(sources.has('openalex'), 'openalex still populates');
500
+
501
+ const telemetry = require('../core/rs-egress-telemetry.cjs');
502
+ const payload = JSON.parse(fs.readFileSync(telemetry.TELEMETRY_FILE, 'utf8'));
503
+ const errd = payload.entries.filter(e => e.status === 'api_error');
504
+ assert.ok(errd.length >= 1, 'api_error telemetry recorded for arxiv');
505
+ SCENARIO_RESULTS.push({ test: 'T6', surface: 'academic', payload: out });
506
+ });
507
+
508
+ // ---------- Test 7: per-source budget exhausted ----------
509
+ await runScenario('Test 7: per-source budget exhausted (openalex skipped)', async function () {
510
+ setupScopedHome();
511
+ clearApiKeys();
512
+ const telemetry = require('../core/rs-egress-telemetry.cjs');
513
+ // Seed 100 openalex entries within last 24h.
514
+ const now = Date.now();
515
+ const entries = [];
516
+ for (let i = 0; i < 100; i++) {
517
+ entries.push({
518
+ source: 'openalex',
519
+ query_text_hash: 'aaaaaaaaaaaaaaaa',
520
+ status: 'ok',
521
+ fetched_at: new Date(now - (i * 1000)).toISOString(),
522
+ });
523
+ }
524
+ fs.mkdirSync(path.dirname(telemetry.TELEMETRY_FILE), { recursive: true });
525
+ fs.writeFileSync(telemetry.TELEMETRY_FILE,
526
+ JSON.stringify({ schema_version: '1.0', entries: entries }, null, 2));
527
+
528
+ const queries = ['plasma confinement tokamak'];
529
+ installMockFetch(buildAllSourcesMockOk(queries[0], 3));
530
+ const fetcher = require('../core/rs-fetcher-academic.cjs');
531
+ const out = await fetcher.fetchAcademic(queries, {});
532
+ const sources = new Set(out.papers.map(p => p.source));
533
+ assert.ok(!sources.has('openalex'), 'openalex skipped after budget exhausted');
534
+ assert.ok(sources.has('arxiv'), 'arxiv runs normally');
535
+
536
+ const payload = JSON.parse(fs.readFileSync(telemetry.TELEMETRY_FILE, 'utf8'));
537
+ // Note: 'budget_exhausted' is in our extended status enum below; if telemetry
538
+ // module doesn't accept it, fetcher should still skip openalex without error.
539
+ // We assert the behavior (skip), not the telemetry status name strictly here.
540
+ // OpenAlex URL must NOT appear in CAPTURED_URLS for this run.
541
+ const ourUrls = CAPTURED_URLS.filter(u => u.indexOf('openalex.org/works') >= 0);
542
+ assert.equal(ourUrls.length, 0, 'no openalex URL fetched after budget exhaust');
543
+ SCENARIO_RESULTS.push({ test: 'T7', surface: 'academic', payload: out });
544
+ });
545
+
546
+ // ---------- Test 8: CANON PART 8 adversarial: leaked-artifact-body ----------
547
+ await runScenario('Test 8: CANON PART 8 adversarial leaked-artifact-body throws', async function () {
548
+ setupScopedHome();
549
+ clearApiKeys();
550
+ // Pattern 3 (meeting with) is the only short way to leak an artifact body in 1 line.
551
+ const queries = ['cancer biomarkers <<artifact: meeting with Dr Smith Q4 financials>>'];
552
+ installMockFetch(buildAllSourcesMockOk('clean', 1));
553
+ const fetcher = require('../core/rs-fetcher-academic.cjs');
554
+ let threw = null;
555
+ try {
556
+ await fetcher.fetchAcademic(queries, {});
557
+ } catch (e) {
558
+ threw = e;
559
+ }
560
+ assert.ok(threw, 'expected throw on adversarial query');
561
+ assert.equal(threw.name, 'ExternalEgressViolation', 'class name');
562
+ assert.equal(threw.meta.surface, 'academic', 'meta.surface');
563
+ assert.ok(typeof threw.meta.matched_pattern === 'string'
564
+ && threw.meta.matched_pattern.length > 0, 'meta.matched_pattern present');
565
+
566
+ // ZERO outbound URL captures for this query.
567
+ assert.equal(CAPTURED_URLS.length, 0,
568
+ 'NO fetch() calls allowed before throw; got ' + CAPTURED_URLS.length);
569
+
570
+ // ZERO telemetry entries for this query.
571
+ const telemetry = require('../core/rs-egress-telemetry.cjs');
572
+ if (fs.existsSync(telemetry.TELEMETRY_FILE)) {
573
+ const payload = JSON.parse(fs.readFileSync(telemetry.TELEMETRY_FILE, 'utf8'));
574
+ assert.equal(payload.entries.length, 0,
575
+ 'NO telemetry entries allowed; got ' + payload.entries.length);
576
+ }
577
+ SCENARIO_RESULTS.push({ test: 'T8', surface: 'academic', payload: { threw: threw.meta.matched_pattern } });
578
+ });
579
+
580
+ // ---------- Test 9: CANON PART 8 adversarial: leaked-venture-name (currency) ----------
581
+ await runScenario('Test 9: CANON PART 8 adversarial leaked-venture-financials throws', async function () {
582
+ setupScopedHome();
583
+ clearApiKeys();
584
+ // Pattern 1 (currency $5.2M) leak.
585
+ const queries = ['oncology venture valuation $5.2M cancer treatment'];
586
+ installMockFetch(buildAllSourcesMockOk('clean', 1));
587
+ const fetcher = require('../core/rs-fetcher-academic.cjs');
588
+ let threw = null;
589
+ try {
590
+ await fetcher.fetchAcademic(queries, {});
591
+ } catch (e) {
592
+ threw = e;
593
+ }
594
+ assert.ok(threw, 'expected throw on adversarial query');
595
+ assert.equal(threw.name, 'ExternalEgressViolation', 'class name');
596
+ assert.equal(CAPTURED_URLS.length, 0, 'NO fetch() calls allowed');
597
+ SCENARIO_RESULTS.push({ test: 'T9', surface: 'academic', payload: { threw: threw.meta.matched_pattern } });
598
+ });
599
+
600
+ // ---------- Test 10: CANON PART 8 adversarial: leaked-meeting-fragment ----------
601
+ await runScenario('Test 10: CANON PART 8 adversarial leaked-meeting-fragment throws', async function () {
602
+ setupScopedHome();
603
+ clearApiKeys();
604
+ const queries = ['oncology meeting with Mayo Clinic Q3'];
605
+ installMockFetch(buildAllSourcesMockOk('clean', 1));
606
+ const fetcher = require('../core/rs-fetcher-academic.cjs');
607
+ let threw = null;
608
+ try {
609
+ await fetcher.fetchAcademic(queries, {});
610
+ } catch (e) {
611
+ threw = e;
612
+ }
613
+ assert.ok(threw, 'expected throw on adversarial query');
614
+ assert.equal(threw.name, 'ExternalEgressViolation', 'class name');
615
+ assert.equal(CAPTURED_URLS.length, 0, 'NO fetch() calls allowed');
616
+ SCENARIO_RESULTS.push({ test: 'T10', surface: 'academic', payload: { threw: threw.meta.matched_pattern } });
617
+ });
618
+
619
+ // ---------- Test 11: CANON PART 8 adversarial: leaked-financial-figure (currency variant) ----------
620
+ await runScenario('Test 11: CANON PART 8 adversarial leaked-financial-figure throws', async function () {
621
+ setupScopedHome();
622
+ clearApiKeys();
623
+ const queries = ['oncology partnerships under $750K threshold'];
624
+ installMockFetch(buildAllSourcesMockOk('clean', 1));
625
+ const fetcher = require('../core/rs-fetcher-academic.cjs');
626
+ let threw = null;
627
+ try {
628
+ await fetcher.fetchAcademic(queries, {});
629
+ } catch (e) {
630
+ threw = e;
631
+ }
632
+ assert.ok(threw, 'expected throw on adversarial query');
633
+ assert.equal(threw.name, 'ExternalEgressViolation', 'class name');
634
+ assert.equal(CAPTURED_URLS.length, 0, 'NO fetch() calls allowed');
635
+ SCENARIO_RESULTS.push({ test: 'T11', surface: 'academic', payload: { threw: threw.meta.matched_pattern } });
636
+ });
637
+
638
+ // ---------- Test 12: chokepoint exclusivity (static grep) ----------
639
+ await runScenario('Test 12: chokepoint exclusivity (every fetch() inside per-source dispatcher)', async function () {
640
+ const fetcherPath = path.resolve(__dirname, '..', 'core', 'rs-fetcher-academic.cjs');
641
+ const src = fs.readFileSync(fetcherPath, 'utf8');
642
+ // Find all fetch( occurrences (excluding comments).
643
+ const lines = src.split('\n');
644
+ let fetchCount = 0;
645
+ let inBlockComment = false;
646
+ for (let i = 0; i < lines.length; i++) {
647
+ let line = lines[i];
648
+ if (inBlockComment) {
649
+ if (line.indexOf('*/') >= 0) inBlockComment = false;
650
+ continue;
651
+ }
652
+ // Strip trailing line comments.
653
+ const slashIdx = line.indexOf('//');
654
+ if (slashIdx >= 0) line = line.slice(0, slashIdx);
655
+ if (line.indexOf('/*') >= 0 && line.indexOf('*/') < 0) {
656
+ inBlockComment = true;
657
+ continue;
658
+ }
659
+ // Match 'fetch(' or 'fetchWithTimeout(' or 'global.fetch(' but not function names.
660
+ // Production code MUST go through fetchWithTimeout helper.
661
+ const matches = line.match(/\bfetch\s*\(/g);
662
+ if (matches) {
663
+ // Allow only if it's INSIDE the fetchWithTimeout helper (which is the
664
+ // ONE function that calls native fetch).
665
+ fetchCount += matches.length;
666
+ }
667
+ }
668
+ // We expect exactly 1 native fetch call: inside fetchWithTimeout.
669
+ assert.ok(fetchCount === 1,
670
+ 'expected exactly 1 native fetch() call (inside fetchWithTimeout helper); got ' + fetchCount);
671
+
672
+ // Also verify auditQueryString call count >= 1 (chokepoint enforcement).
673
+ const auditCount = (src.match(/auditQueryString/g) || []).length;
674
+ assert.ok(auditCount >= 1, 'auditQueryString must be invoked at least once; got ' + auditCount);
675
+
676
+ // Verify buildAcademicQuery is defined exactly once.
677
+ const buildDefCount = (src.match(/function buildAcademicQuery/g) || []).length;
678
+ assert.equal(buildDefCount, 1, 'buildAcademicQuery defined exactly once');
679
+ });
680
+
681
+ // ---------- V1: validator Check A telemetry-file-absent ----------
682
+ await runScenario('V1: validator Check A telemetry-file-absent -> {severity: null}', async function () {
683
+ setupScopedHome();
684
+ const validator = require('./validators/external-academic-invariants.cjs');
685
+ // Telemetry file does not exist in this scoped HOME.
686
+ const result = validator.validate('/dev/null', {});
687
+ assert.equal(result.severity, null, 'severity null when telemetry absent');
688
+ assert.equal(result.violations.length, 0, 'no violations when telemetry absent');
689
+ });
690
+
691
+ // ---------- V2: validator Check B per-source budget exceeded ----------
692
+ await runScenario('V2: validator Check B per-source budget exceeded -> warning', async function () {
693
+ setupScopedHome();
694
+ const telemetry = require('../core/rs-egress-telemetry.cjs');
695
+ // Seed 150 openalex entries within last 24h (over default 100 budget).
696
+ const now = Date.now();
697
+ const entries = [];
698
+ for (let i = 0; i < 150; i++) {
699
+ entries.push({
700
+ source: 'openalex',
701
+ query_text_hash: 'aaaaaaaaaaaaaaaa',
702
+ status: 'ok',
703
+ fetched_at: new Date(now - (i * 1000)).toISOString(),
704
+ });
705
+ }
706
+ fs.mkdirSync(path.dirname(telemetry.TELEMETRY_FILE), { recursive: true });
707
+ fs.writeFileSync(telemetry.TELEMETRY_FILE,
708
+ JSON.stringify({ schema_version: '1.0', entries: entries }, null, 2));
709
+
710
+ const validator = require('./validators/external-academic-invariants.cjs');
711
+ const result = validator.validate('/dev/null', {});
712
+ assert.ok(result.violations.length >= 1, 'expected at least one violation');
713
+ const budgetViolations = result.violations.filter(v => v.category === 'budget_exceeded');
714
+ assert.ok(budgetViolations.length >= 1, 'expected budget_exceeded violation');
715
+ assert.equal(budgetViolations[0].severity, 'warning', 'budget_exceeded severity is warning');
716
+ });
717
+
718
+ // ---------- V3: validator Check C status enum violation ----------
719
+ await runScenario('V3: validator Check C status enum violation -> warning', async function () {
720
+ setupScopedHome();
721
+ const telemetry = require('../core/rs-egress-telemetry.cjs');
722
+ const entries = [{
723
+ source: 'openalex',
724
+ query_text_hash: 'aaaaaaaaaaaaaaaa',
725
+ status: 'unknown_status',
726
+ fetched_at: new Date().toISOString(),
727
+ }];
728
+ fs.mkdirSync(path.dirname(telemetry.TELEMETRY_FILE), { recursive: true });
729
+ fs.writeFileSync(telemetry.TELEMETRY_FILE,
730
+ JSON.stringify({ schema_version: '1.0', entries: entries }, null, 2));
731
+
732
+ const validator = require('./validators/external-academic-invariants.cjs');
733
+ const result = validator.validate('/dev/null', {});
734
+ const statusViolations = result.violations.filter(v => v.category === 'status_enum_violation');
735
+ assert.ok(statusViolations.length >= 1, 'expected status_enum_violation');
736
+ assert.equal(statusViolations[0].severity, 'warning', 'status_enum severity is warning');
737
+ });
738
+
739
+ // ---------- V4: validator Check D query_text literal -> CRITICAL ----------
740
+ await runScenario('V4: validator Check D query_text literal present -> CRITICAL canon_boundary', async function () {
741
+ setupScopedHome();
742
+ const telemetry = require('../core/rs-egress-telemetry.cjs');
743
+ const entries = [{
744
+ source: 'openalex',
745
+ query_text: 'literal cancer biomarkers query', // FORBIDDEN: literal field
746
+ query_text_hash: 'aaaaaaaaaaaaaaaa',
747
+ status: 'ok',
748
+ fetched_at: new Date().toISOString(),
749
+ }];
750
+ fs.mkdirSync(path.dirname(telemetry.TELEMETRY_FILE), { recursive: true });
751
+ fs.writeFileSync(telemetry.TELEMETRY_FILE,
752
+ JSON.stringify({ schema_version: '1.0', entries: entries }, null, 2));
753
+
754
+ const validator = require('./validators/external-academic-invariants.cjs');
755
+ const result = validator.validate('/dev/null', {});
756
+ const canonViolations = result.violations.filter(v => v.category === 'canon_boundary');
757
+ assert.ok(canonViolations.length >= 1, 'expected canon_boundary violation');
758
+ assert.equal(canonViolations[0].severity, 'critical',
759
+ 'canon_boundary severity is critical');
760
+ });
761
+
762
+ // ---------- V5: validator Check E query_text_hash format ----------
763
+ await runScenario('V5: validator Check E query_text_hash format violation -> warning', async function () {
764
+ setupScopedHome();
765
+ const telemetry = require('../core/rs-egress-telemetry.cjs');
766
+ const entries = [{
767
+ source: 'openalex',
768
+ query_text_hash: 'NOT-A-HEX-HASH-XYZ', // wrong format
769
+ status: 'ok',
770
+ fetched_at: new Date().toISOString(),
771
+ }];
772
+ fs.mkdirSync(path.dirname(telemetry.TELEMETRY_FILE), { recursive: true });
773
+ fs.writeFileSync(telemetry.TELEMETRY_FILE,
774
+ JSON.stringify({ schema_version: '1.0', entries: entries }, null, 2));
775
+
776
+ const validator = require('./validators/external-academic-invariants.cjs');
777
+ const result = validator.validate('/dev/null', {});
778
+ const hashViolations = result.violations.filter(v => v.category === 'hash_format_invalid');
779
+ assert.ok(hashViolations.length >= 1, 'expected hash_format_invalid violation');
780
+ assert.equal(hashViolations[0].severity, 'warning', 'severity is warning');
781
+ });
782
+
783
+ // ---------- V6: validator Check F fetched_at malformed ISO-8601 ----------
784
+ await runScenario('V6: validator Check F fetched_at malformed -> warning', async function () {
785
+ setupScopedHome();
786
+ const telemetry = require('../core/rs-egress-telemetry.cjs');
787
+ const entries = [{
788
+ source: 'openalex',
789
+ query_text_hash: 'aaaaaaaaaaaaaaaa',
790
+ status: 'ok',
791
+ fetched_at: 'NOT-A-VALID-DATE',
792
+ }];
793
+ fs.mkdirSync(path.dirname(telemetry.TELEMETRY_FILE), { recursive: true });
794
+ fs.writeFileSync(telemetry.TELEMETRY_FILE,
795
+ JSON.stringify({ schema_version: '1.0', entries: entries }, null, 2));
796
+
797
+ const validator = require('./validators/external-academic-invariants.cjs');
798
+ const result = validator.validate('/dev/null', {});
799
+ const dateViolations = result.violations.filter(v => v.category === 'fetched_at_malformed');
800
+ assert.ok(dateViolations.length >= 1, 'expected fetched_at_malformed violation');
801
+ assert.equal(dateViolations[0].severity, 'warning', 'severity is warning');
802
+ });
803
+
804
+ // ---------- A1 sweep: zero forbidden matches in any captured output ----------
805
+ await runScenario('A1: zero forbidden matches across captured payloads + URLs', async function () {
806
+ for (const rec of SCENARIO_RESULTS) {
807
+ const stringified = JSON.stringify(rec.payload);
808
+ const hit = scanAgainstForbidden(stringified);
809
+ assert.equal(hit.hit, false,
810
+ 'A1 violation in ' + rec.test + '/' + rec.surface +
811
+ ' against pattern ' + (hit.pattern || 'n/a'));
812
+ }
813
+ // Also scan every captured outbound URL across the ENTIRE suite.
814
+ // CAPTURED_URLS is per-scenario; CAPTURED_URLS_ALL is the cumulative
815
+ // ledger that captures every URL ever issued during the suite.
816
+ for (const u of CAPTURED_URLS_ALL) {
817
+ const hit = scanAgainstForbidden(u);
818
+ assert.equal(hit.hit, false,
819
+ 'A1 outbound URL leaked forbidden pattern ' + (hit.pattern || 'n/a') +
820
+ ' in ' + u.slice(0, 100));
821
+ }
822
+ });
823
+
824
+ // ---------- A2 sweep: parity gate ----------
825
+ await runScenario('A2: FORBIDDEN_PATTERNS parity (rs-egress-prompts === cross-room-aggregator)', async function () {
826
+ const promptsModule = require('../core/rs-egress-prompts.cjs');
827
+ const aggregator = require('../core/cross-room-aggregator.cjs');
828
+ const ours = promptsModule.FORBIDDEN_PATTERNS;
829
+ const truth = aggregator.FORBIDDEN_PATTERNS;
830
+ assert.equal(ours.length, truth.length, 'lengths match');
831
+ for (let i = 0; i < truth.length; i++) {
832
+ assert.equal(ours[i].source, truth[i].source, 'pattern source mismatch at index ' + i);
833
+ assert.equal(ours[i].flags, truth[i].flags, 'pattern flags mismatch at index ' + i);
834
+ }
835
+ });
836
+
837
+ // ---------- Final report ----------
838
+ if (failed > 0) {
839
+ console.error('\n=== ' + failed + ' FAILURES ===');
840
+ for (const f of failures) {
841
+ console.error(' ' + f.name);
842
+ }
843
+ process.exit(1);
844
+ }
845
+
846
+ console.log('=== 89.2-02 fetcher-academic suite: 18/18 passed ===');
847
+ process.exit(0);
848
+ })();