@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.
- package/.claude-plugin/plugin.json +21 -0
- package/.mcp.json +9 -0
- package/CHANGELOG.md +3333 -0
- package/LICENSE +123 -0
- package/README.md +673 -0
- package/agents/brain-query.md +80 -0
- package/agents/framework-runner.md +237 -0
- package/agents/grading.md +188 -0
- package/agents/investor.md +128 -0
- package/agents/larry-extended.md +135 -0
- package/agents/opportunity-scanner.md +91 -0
- package/agents/persona-analyst.md +132 -0
- package/agents/research.md +89 -0
- package/agents/reverse-salient-agent.md +27 -0
- package/bin/cli.js +142 -0
- package/bin/mindrian-mcp-server.cjs +182 -0
- package/bin/mindrian-tools.cjs +765 -0
- package/commands/act.md +439 -0
- package/commands/admin.md +404 -0
- package/commands/analyze-needs.md +42 -0
- package/commands/analyze-systems.md +39 -0
- package/commands/analyze-timing.md +42 -0
- package/commands/auto-explore.md +64 -0
- package/commands/beautiful-question.md +40 -0
- package/commands/brain-derive.md +78 -0
- package/commands/build-knowledge.md +42 -0
- package/commands/build-thesis.md +46 -0
- package/commands/causal.md +234 -0
- package/commands/challenge-assumptions.md +33 -0
- package/commands/compare-ventures.md +83 -0
- package/commands/dashboard.md +110 -0
- package/commands/deep-grade.md +82 -0
- package/commands/diagnose.md +58 -0
- package/commands/diagnostics.md +151 -0
- package/commands/doctor.md +151 -0
- package/commands/dominant-designs.md +40 -0
- package/commands/explain-decision.md +87 -0
- package/commands/explore-domains.md +42 -0
- package/commands/explore-futures.md +40 -0
- package/commands/explore-trends.md +42 -0
- package/commands/export.md +103 -0
- package/commands/file-meeting.md +724 -0
- package/commands/find-analogies.md +188 -0
- package/commands/find-bottlenecks.md +62 -0
- package/commands/find-connections.md +76 -0
- package/commands/funding.md +81 -0
- package/commands/grade.md +203 -0
- package/commands/graph.md +128 -0
- package/commands/hat-briefing.md +125 -0
- package/commands/heal.md +196 -0
- package/commands/help.md +399 -0
- package/commands/hmi-status.md +172 -0
- package/commands/jtbd.md +241 -0
- package/commands/leadership.md +73 -0
- package/commands/lean-canvas.md +40 -0
- package/commands/macro-trends.md +40 -0
- package/commands/map-unknowns.md +40 -0
- package/commands/memory.md +173 -0
- package/commands/models.md +175 -0
- package/commands/mos-reason.md +285 -0
- package/commands/mullins.md +120 -0
- package/commands/new-project.md +481 -0
- package/commands/onboard.md +434 -0
- package/commands/operator.md +149 -0
- package/commands/opportunities.md +144 -0
- package/commands/organize.md +497 -0
- package/commands/persona.md +198 -0
- package/commands/pipeline.md +112 -0
- package/commands/present.md +91 -0
- package/commands/publish.md +201 -0
- package/commands/query.md +124 -0
- package/commands/radar.md +72 -0
- package/commands/reanalyze.md +91 -0
- package/commands/research.md +196 -0
- package/commands/room.md +352 -0
- package/commands/rooms.md +598 -0
- package/commands/root-cause.md +40 -0
- package/commands/rs-experts.md +85 -0
- package/commands/rs-explain.md +100 -0
- package/commands/rs-fetch.md +94 -0
- package/commands/rs-thesis.md +85 -0
- package/commands/scenario-plan.md +40 -0
- package/commands/scheduled-tasks.md +285 -0
- package/commands/score-innovation.md +43 -0
- package/commands/scout.md +239 -0
- package/commands/setup.md +618 -0
- package/commands/snapshot.md +147 -0
- package/commands/speakers.md +84 -0
- package/commands/splash.md +28 -0
- package/commands/status.md +75 -0
- package/commands/structure-argument.md +42 -0
- package/commands/suggest-next.md +80 -0
- package/commands/systems-thinking.md +40 -0
- package/commands/think-hats.md +42 -0
- package/commands/update.md +181 -0
- package/commands/user-needs.md +40 -0
- package/commands/validate.md +40 -0
- package/commands/value-proposition.md +61 -0
- package/commands/vault.md +180 -0
- package/commands/visualize.md +52 -0
- package/commands/whitespace.md +507 -0
- package/commands/wiki.md +69 -0
- package/hooks/hooks.json +381 -0
- package/hooks/run-hook.cmd +64 -0
- package/lib/__init__.py +0 -0
- package/lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/lib/agents/auto-explore-agent.cjs +1043 -0
- package/lib/agents/reverse-salient-agent.cjs +679 -0
- package/lib/agents/tension-hook-agent.cjs +544 -0
- package/lib/brain/ROOM.md +44 -0
- package/lib/brain/chain-recommender.cjs +301 -0
- package/lib/chat/chat-context.js +185 -0
- package/lib/chat/chat-panel.js +721 -0
- package/lib/chat/fabric-chat.cjs +288 -0
- package/lib/chat/generative-tools.js +219 -0
- package/lib/conversation/ROOM.md +39 -0
- package/lib/conversation/classifier-rules.json +38 -0
- package/lib/conversation/classifier.cjs +264 -0
- package/lib/conversation/operator.cjs +287 -0
- package/lib/copy/115-spec-strings.cjs +55 -0
- package/lib/core/__init__.py +0 -0
- package/lib/core/__nav-stub.cjs +14 -0
- package/lib/core/__pycache__/__init__.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs-math.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs_cache.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs_corpus.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs_hybrid.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs_math.cpython-312.pyc +0 -0
- package/lib/core/__pycache__/rs_rooms.cpython-312.pyc +0 -0
- package/lib/core/artifact-id.cjs +148 -0
- package/lib/core/asset-ops.cjs +151 -0
- package/lib/core/auto-commit-throttle.cjs +129 -0
- package/lib/core/bearer-token.cjs +199 -0
- package/lib/core/brain-client.cjs +865 -0
- package/lib/core/brain-derivation-prompts.cjs +326 -0
- package/lib/core/brain-derivation-queue.cjs +431 -0
- package/lib/core/brain-derivation.cjs +580 -0
- package/lib/core/brain-md-schema.cjs +528 -0
- package/lib/core/brain-md-staleness.cjs +357 -0
- package/lib/core/brain-response-sanitize.cjs +188 -0
- package/lib/core/bridge-writer.cjs +477 -0
- package/lib/core/chat-context-builder.cjs +253 -0
- package/lib/core/cross-room-aggregator.cjs +762 -0
- package/lib/core/daily-briefing.cjs +438 -0
- package/lib/core/decision-capture.cjs +618 -0
- package/lib/core/deep-links.cjs +82 -0
- package/lib/core/dispatch-optimizer.cjs +354 -0
- package/lib/core/dual-path-detector.cjs +84 -0
- package/lib/core/dual-path-detector.test.cjs +334 -0
- package/lib/core/exports-log.cjs +79 -0
- package/lib/core/feynman-minto-invariants.cjs +605 -0
- package/lib/core/folder-memory-async.cjs +338 -0
- package/lib/core/folder-memory-shared.cjs +890 -0
- package/lib/core/folder-memory.cjs +416 -0
- package/lib/core/framework-chain-composer.cjs +411 -0
- package/lib/core/frontmatter-schemas.cjs +330 -0
- package/lib/core/git-ops.cjs +141 -0
- package/lib/core/graph-ops.cjs +258 -0
- package/lib/core/hat-persistence.cjs +362 -0
- package/lib/core/index.cjs +60 -0
- package/lib/core/integration-registry.cjs +232 -0
- package/lib/core/intelligence-cascade.cjs +661 -0
- package/lib/core/lazygraph-ops.cjs +1057 -0
- package/lib/core/lru-cache.cjs +139 -0
- package/lib/core/mcp-profiles.cjs +182 -0
- package/lib/core/meeting-ops.cjs +54 -0
- package/lib/core/memory-ops.cjs +600 -0
- package/lib/core/migrations/ROOM.md +33 -0
- package/lib/core/migrations/phase-109-nodes-provenance.cjs +339 -0
- package/lib/core/migrations/phase-109-session-focus.cjs +99 -0
- package/lib/core/model-profiles.cjs +246 -0
- package/lib/core/mullins-scaffold.cjs +160 -0
- package/lib/core/nav-dial.cjs +316 -0
- package/lib/core/navigation/ROOM.md +15 -0
- package/lib/core/navigation/explanation.cjs +43 -0
- package/lib/core/navigation/focus.cjs +135 -0
- package/lib/core/navigation/ingestion.cjs +82 -0
- package/lib/core/navigation/insights.cjs +350 -0
- package/lib/core/navigation/memory-events.cjs +118 -0
- package/lib/core/navigation/neighborhood.cjs +78 -0
- package/lib/core/navigation/packet.cjs +182 -0
- package/lib/core/navigation/room-home.cjs +127 -0
- package/lib/core/navigation/transitions.cjs +82 -0
- package/lib/core/navigation-engine-shared.cjs +242 -0
- package/lib/core/navigation-engine.cjs +664 -0
- package/lib/core/navigation.cjs +60 -0
- package/lib/core/nl-graph-queries.cjs +164 -0
- package/lib/core/offer-presenter.cjs +406 -0
- package/lib/core/opportunity-extractor.cjs +183 -0
- package/lib/core/opportunity-ops.cjs +1371 -0
- package/lib/core/persona-ops.cjs +537 -0
- package/lib/core/persona-taxonomy.cjs +190 -0
- package/lib/core/platform-gates.cjs +120 -0
- package/lib/core/platform.cjs +257 -0
- package/lib/core/proactive-intelligence.cjs +528 -0
- package/lib/core/problem-type-router.cjs +315 -0
- package/lib/core/reasoning-ops.cjs +639 -0
- package/lib/core/reverse-salient-persona-suffix.cjs +115 -0
- package/lib/core/room-classifier-strict-mode.cjs +229 -0
- package/lib/core/room-db.cjs +127 -0
- package/lib/core/room-ops-async.cjs +92 -0
- package/lib/core/room-ops-shared.cjs +64 -0
- package/lib/core/room-ops-sync.cjs +70 -0
- package/lib/core/room-ops.cjs +32 -0
- package/lib/core/room-type-detector.cjs +386 -0
- package/lib/core/rs-brain-substrate-prompts.cjs +129 -0
- package/lib/core/rs-brain-substrate.cjs +570 -0
- package/lib/core/rs-breakthrough-scorer.cjs +255 -0
- package/lib/core/rs-canon-violations.cjs +82 -0
- package/lib/core/rs-chain-feeder.cjs +343 -0
- package/lib/core/rs-commercial-assessor.cjs +280 -0
- package/lib/core/rs-differential-scorer.cjs +376 -0
- package/lib/core/rs-domain-analyzer.cjs +385 -0
- package/lib/core/rs-egress-prompts.cjs +113 -0
- package/lib/core/rs-egress-telemetry.cjs +225 -0
- package/lib/core/rs-egress-violations.cjs +53 -0
- package/lib/core/rs-expert-mapper.cjs +467 -0
- package/lib/core/rs-fetcher-academic.cjs +697 -0
- package/lib/core/rs-fetcher-experts.cjs +314 -0
- package/lib/core/rs-fetcher-industry.cjs +731 -0
- package/lib/core/rs-fetcher-patents.cjs +564 -0
- package/lib/core/rs-innovation-classifier.cjs +194 -0
- package/lib/core/rs-mind-map.cjs +656 -0
- package/lib/core/rs-neo4j-writer.cjs +388 -0
- package/lib/core/rs-nl-to-query.cjs +425 -0
- package/lib/core/rs-pinecone-bridge.cjs +303 -0
- package/lib/core/rs-preprocessor.cjs +350 -0
- package/lib/core/rs-query-matrix.cjs +316 -0
- package/lib/core/rs-query-to-text.cjs +438 -0
- package/lib/core/rs-sqlite-mirror.cjs +443 -0
- package/lib/core/rs-thesis-generator.cjs +188 -0
- package/lib/core/rs_cache.py +479 -0
- package/lib/core/rs_corpus.py +468 -0
- package/lib/core/rs_hybrid.py +586 -0
- package/lib/core/rs_math.py +287 -0
- package/lib/core/rs_rooms.py +193 -0
- package/lib/core/scheduled-scanner.cjs +463 -0
- package/lib/core/scratchpad-ops.cjs +201 -0
- package/lib/core/section-8-trace-schema.cjs +138 -0
- package/lib/core/section-registry.cjs +111 -0
- package/lib/core/session-state.cjs +144 -0
- package/lib/core/shallow-doc-parser.cjs +174 -0
- package/lib/core/shallow-doc-parser.test.cjs +226 -0
- package/lib/core/skill-activation-router.cjs +284 -0
- package/lib/core/state-ops.cjs +46 -0
- package/lib/core/statusline-cache.cjs +266 -0
- package/lib/core/token-estimator.cjs +348 -0
- package/lib/core/user-archetype.cjs +239 -0
- package/lib/core/user-md-ops.cjs +524 -0
- package/lib/core/visual-ops.cjs +624 -0
- package/lib/core/write-lock.cjs +149 -0
- package/lib/graph/canvas-graph.js +467 -0
- package/lib/graph/constellation-config.cjs +299 -0
- package/lib/graph/graph-detail-panel.js +165 -0
- package/lib/hmi/ROOM.md +47 -0
- package/lib/hmi/across-session-memory.cjs +604 -0
- package/lib/hmi/cross-room-memory.cjs +575 -0
- package/lib/hmi/decoy-tier.cjs +395 -0
- package/lib/hmi/jtbd-classifier.cjs +219 -0
- package/lib/hmi/jtbd-state.cjs +199 -0
- package/lib/hmi/jtbd-taxonomy.json +392 -0
- package/lib/hmi/selector-dispatcher.cjs +546 -0
- package/lib/hmi/selector-telemetry.cjs +263 -0
- package/lib/hmi/shape-f0-renderer.cjs +139 -0
- package/lib/hmi/shape-f1-fallback.cjs +80 -0
- package/lib/hmi/shape-f1-renderer.cjs +138 -0
- package/lib/hmi/shape-f2-renderer.cjs +132 -0
- package/lib/hmi/shape-f3-renderer.cjs +66 -0
- package/lib/hmi/shape-f4-renderer.cjs +72 -0
- package/lib/hmi/shape-f5-renderer.cjs +155 -0
- package/lib/hmi/shape-f6-plan-review-renderer.cjs +312 -0
- package/lib/hmi/shape-f6-renderer.cjs +144 -0
- package/lib/hmi/shape-g-renderer.cjs +219 -0
- package/lib/hmi/shape-h-renderer.cjs +222 -0
- package/lib/hmi/tier-check.cjs +63 -0
- package/lib/import/PRECONDITIONS.md +41 -0
- package/lib/import/branding.cjs +210 -0
- package/lib/import/branding.test.cjs +235 -0
- package/lib/import/classifications-sync.cjs +104 -0
- package/lib/import/classifications-sync.test.cjs +129 -0
- package/lib/import/enricher.cjs +296 -0
- package/lib/import/enricher.test.cjs +273 -0
- package/lib/import/integration.test.cjs +376 -0
- package/lib/import/manifest.cjs +129 -0
- package/lib/import/manifest.schema.json +185 -0
- package/lib/import/manifest.test.cjs +123 -0
- package/lib/import/meeting-detector.cjs +92 -0
- package/lib/import/meeting-detector.test.cjs +100 -0
- package/lib/import/person-detector.cjs +229 -0
- package/lib/import/person-detector.test.cjs +149 -0
- package/lib/import/report.cjs +186 -0
- package/lib/import/report.test.cjs +186 -0
- package/lib/import/room-md-scaffolder.cjs +49 -0
- package/lib/import/router.cjs +224 -0
- package/lib/import/router.test.cjs +356 -0
- package/lib/import/run-all-tests.cjs +36 -0
- package/lib/import/smoke-test.cjs +213 -0
- package/lib/import/smoke-test.test.cjs +148 -0
- package/lib/import/test-fixtures/collision-vault/preexisting-room/STATE.md +8 -0
- package/lib/import/test-fixtures/collision-vault/preexisting-room/problem-definition/onboarding/onboarding.md +7 -0
- package/lib/import/test-fixtures/collision-vault/source/onboarding.md +5 -0
- package/lib/import/test-fixtures/obsidian-vault/.obsidian/workspace.json +1 -0
- package/lib/import/test-fixtures/obsidian-vault/notes/with-wikilinks.md +4 -0
- package/lib/import/test-fixtures/tiny-vault/notes/2026-01-15-team-sync.md +9 -0
- package/lib/import/test-fixtures/tiny-vault/notes/empty.md +3 -0
- package/lib/import/test-fixtures/tiny-vault/notes/onboarding.md +5 -0
- package/lib/import/test-fixtures/tiny-vault/notes/pricing.md +5 -0
- package/lib/import/test-fixtures/tiny-vault/notes/random.md +4 -0
- package/lib/import/undo.test.cjs +199 -0
- package/lib/import/vault-scanner.cjs +105 -0
- package/lib/import/vault-scanner.test.cjs +67 -0
- package/lib/mcp/app-html/dashboard.html +316 -0
- package/lib/mcp/app-html/graph.html +428 -0
- package/lib/mcp/app-html/mindrian-platform.html +1841 -0
- package/lib/mcp/app-html/wiki.html +383 -0
- package/lib/mcp/app-views.cjs +322 -0
- package/lib/mcp/brain-router.cjs +418 -0
- package/lib/mcp/capability-registry.cjs +62 -0
- package/lib/mcp/larry-context.cjs +46 -0
- package/lib/mcp/larry-server-instructions.md +114 -0
- package/lib/mcp/pipeline-state.cjs +275 -0
- package/lib/mcp/prompts.cjs +302 -0
- package/lib/mcp/resources.cjs +227 -0
- package/lib/mcp/session-catchup.cjs +327 -0
- package/lib/mcp/surface-detect.cjs +75 -0
- package/lib/mcp/tool-router.cjs +1034 -0
- package/lib/memory/aaak-compress.cjs +403 -0
- package/lib/memory/aaak-compress.test.cjs +288 -0
- package/lib/memory/async-artifact-auto-commit.test.cjs +223 -0
- package/lib/memory/bearer-token.test.cjs +315 -0
- package/lib/memory/brain-cache-lru.test.cjs +259 -0
- package/lib/memory/brain-client-query-shape.test.cjs +160 -0
- package/lib/memory/brain-derivation-graceful-degradation.test.cjs +1019 -0
- package/lib/memory/brain-derivation-queue.test.cjs +539 -0
- package/lib/memory/brain-derivation.test.cjs +634 -0
- package/lib/memory/brain-derive-command.test.cjs +534 -0
- package/lib/memory/brain-md-invariants-validator.test.cjs +704 -0
- package/lib/memory/brain-md-schema.test.cjs +467 -0
- package/lib/memory/brain-md-staleness.test.cjs +525 -0
- package/lib/memory/brain-server-resolution.test.cjs +314 -0
- package/lib/memory/chain-recommender.test.cjs +233 -0
- package/lib/memory/chat-context.test.cjs +128 -0
- package/lib/memory/command-registry.test.cjs +220 -0
- package/lib/memory/cross-room-aggregator.test.cjs +909 -0
- package/lib/memory/dashboard-server.test.cjs +256 -0
- package/lib/memory/debouncer-drain-at-prompt.test.cjs +389 -0
- package/lib/memory/decision-capture.test.cjs +632 -0
- package/lib/memory/decision-capture.worker.cjs +70 -0
- package/lib/memory/explain-decision-command.test.cjs +521 -0
- package/lib/memory/explain-decision-footer.test.cjs +316 -0
- package/lib/memory/explored-materials-store.cjs +392 -0
- package/lib/memory/feynman-minto-guardian.test.cjs +736 -0
- package/lib/memory/feynman-minto-invariants.test.cjs +511 -0
- package/lib/memory/feynman-prompts-drift.test.cjs +144 -0
- package/lib/memory/feynman-prompts.cjs +151 -0
- package/lib/memory/feynman-prompts.test.cjs +96 -0
- package/lib/memory/folder-memory-quadruple.test.cjs +548 -0
- package/lib/memory/folder-memory.test.cjs +503 -0
- package/lib/memory/framework-chain-composer.test.cjs +515 -0
- package/lib/memory/frontmatter-schema-validator.test.cjs +290 -0
- package/lib/memory/heal-command.test.cjs +604 -0
- package/lib/memory/index-artifact-transaction.test.cjs +333 -0
- package/lib/memory/lazygraph-rs-discoveries-view.test.cjs +122 -0
- package/lib/memory/mcp-input-validation.test.cjs +240 -0
- package/lib/memory/mcp-server-brain-deps.test.cjs +270 -0
- package/lib/memory/mcp-stack-fallback.test.cjs +433 -0
- package/lib/memory/minto-debouncer.test.cjs +407 -0
- package/lib/memory/minto-debouncer.worker.cjs +46 -0
- package/lib/memory/minto-migration-v88.test.cjs +265 -0
- package/lib/memory/minto-schema-v88.test.cjs +390 -0
- package/lib/memory/mos-status-renderer.test.cjs +631 -0
- package/lib/memory/narrative-schema.cjs +376 -0
- package/lib/memory/narrative-schema.test.cjs +209 -0
- package/lib/memory/nav-dial.test.cjs +414 -0
- package/lib/memory/navigation-engine-core.test.cjs +722 -0
- package/lib/memory/navigation-invariants.test.cjs +483 -0
- package/lib/memory/offer-presenter.test.cjs +554 -0
- package/lib/memory/on-stop-snapshot.test.cjs +404 -0
- package/lib/memory/pending-tension-store.cjs +373 -0
- package/lib/memory/post-compact-reinjection.test.cjs +854 -0
- package/lib/memory/post-write-triple.test.cjs +317 -0
- package/lib/memory/pre-compact-snapshot.test.cjs +495 -0
- package/lib/memory/problem-type-router.test.cjs +656 -0
- package/lib/memory/query-efficiency-telemetry.test.cjs +370 -0
- package/lib/memory/recompile-room-references.test.cjs +392 -0
- package/lib/memory/recompile-room-references.worker.cjs +42 -0
- package/lib/memory/record-decision-dual-write.test.cjs +454 -0
- package/lib/memory/room-classifier-strict-mode.test.cjs +417 -0
- package/lib/memory/room-minto-hook.test.cjs +398 -0
- package/lib/memory/rs-discovery-engine.test.cjs +323 -0
- package/lib/memory/run-feynman-tests.cjs +1247 -0
- package/lib/memory/security-trifecta.test.cjs +312 -0
- package/lib/memory/session-start-brain-staleness.test.cjs +363 -0
- package/lib/memory/session-start-triple-injection.test.cjs +514 -0
- package/lib/memory/sessionstart-banner-formatter.cjs +318 -0
- package/lib/memory/sessionstart-minto-banner.test.cjs +373 -0
- package/lib/memory/skill-activation-router.test.cjs +419 -0
- package/lib/memory/stamp-artifact-write.test.cjs +304 -0
- package/lib/memory/statusline-active-room.test.cjs +315 -0
- package/lib/memory/statusline-minto-segment.test.cjs +292 -0
- package/lib/memory/sync-async-entry-points.test.cjs +204 -0
- package/lib/memory/test-bridge-writer-enhanced.cjs +452 -0
- package/lib/memory/test-rs-brain-substrate-shape.cjs +529 -0
- package/lib/memory/test-rs-brain-substrate.cjs +636 -0
- package/lib/memory/test-rs-breakthrough-scorer.cjs +375 -0
- package/lib/memory/test-rs-canon-violations.cjs +218 -0
- package/lib/memory/test-rs-chain-feeder-core.cjs +344 -0
- package/lib/memory/test-rs-chain-feeder-skill-spawn.cjs +297 -0
- package/lib/memory/test-rs-commercial-assessor.cjs +385 -0
- package/lib/memory/test-rs-differential-scorer.cjs +480 -0
- package/lib/memory/test-rs-discovery-engine.cjs +603 -0
- package/lib/memory/test-rs-domain-analyzer.cjs +492 -0
- package/lib/memory/test-rs-egress-primitives.cjs +420 -0
- package/lib/memory/test-rs-expert-mapper.cjs +547 -0
- package/lib/memory/test-rs-explain-command.cjs +443 -0
- package/lib/memory/test-rs-fetcher-academic.cjs +848 -0
- package/lib/memory/test-rs-fetcher-experts.cjs +496 -0
- package/lib/memory/test-rs-fetcher-industry.cjs +702 -0
- package/lib/memory/test-rs-fetcher-patents.cjs +674 -0
- package/lib/memory/test-rs-innovation-classifier.cjs +301 -0
- package/lib/memory/test-rs-mind-map.cjs +646 -0
- package/lib/memory/test-rs-neo4j-writer.cjs +518 -0
- package/lib/memory/test-rs-nl-to-query.cjs +449 -0
- package/lib/memory/test-rs-pinecone-bridge.cjs +277 -0
- package/lib/memory/test-rs-preprocessor.cjs +433 -0
- package/lib/memory/test-rs-query-matrix.cjs +391 -0
- package/lib/memory/test-rs-query-to-text.cjs +551 -0
- package/lib/memory/test-rs-sqlite-mirror.cjs +649 -0
- package/lib/memory/test-rs-thesis-generator.cjs +360 -0
- package/lib/memory/triple-context-formatter.cjs +473 -0
- package/lib/memory/triple-context-formatter.test.cjs +442 -0
- package/lib/memory/user-md-persona.test.cjs +565 -0
- package/lib/memory/userpromptsubmit-integration.test.cjs +690 -0
- package/lib/memory/validators/README.md +157 -0
- package/lib/memory/validators/brain-md-invariants.cjs +475 -0
- package/lib/memory/validators/brain-substrate-invariants.cjs +285 -0
- package/lib/memory/validators/external-academic-invariants.cjs +249 -0
- package/lib/memory/validators/external-industry-invariants.cjs +271 -0
- package/lib/memory/validators/external-patents-invariants.cjs +266 -0
- package/lib/memory/validators/minto-invariants.cjs +62 -0
- package/lib/memory/validators/navigation-invariants.cjs +340 -0
- package/lib/memory/validators/queue-health.cjs +95 -0
- package/lib/memory/validators/snapshot-integrity.cjs +129 -0
- package/lib/memory/validators/stale-lifecycle.cjs +116 -0
- package/lib/memory/vault-section-minto-generator-atomic.test.cjs +556 -0
- package/lib/memory/vault-section-minto-generator-atomic.worker.cjs +73 -0
- package/lib/memory/write-lock-atomic.test.cjs +137 -0
- package/lib/memory/write-lock-atomic.worker.cjs +55 -0
- package/lib/parity/check-parity.cjs +83 -0
- package/lib/presentation/presentation-server.cjs +101 -0
- package/lib/presentation/presentation-watcher.cjs +123 -0
- package/lib/quickview/hub-server.cjs +719 -0
- package/lib/quickview/server.cjs +533 -0
- package/lib/render/JTBD-PALETTES.md +145 -0
- package/lib/render/ROOM.md +59 -0
- package/lib/render/render-v2.cjs +486 -0
- package/lib/render/render-v2.test.cjs +267 -0
- package/lib/render/render.cjs +65 -0
- package/lib/state/ROOM.md +46 -0
- package/lib/state/state-md-parser.cjs +215 -0
- package/lib/statusline/ROOM.md +38 -0
- package/lib/statusline/banner-suppression.cjs +50 -0
- package/lib/statusline/surface-detect.cjs +85 -0
- package/lib/update-bootstrap.sh.template +145 -0
- package/lib/vault/frontmatter-schema.cjs +297 -0
- package/lib/vault/room-scanner.cjs +352 -0
- package/lib/vault/wikilink-builder.cjs +231 -0
- package/lib/vault/wikilink-builder.test.cjs +182 -0
- package/lib/wiki/graph-links.cjs +281 -0
- package/lib/wiki/page-renderer.cjs +229 -0
- package/lib/wiki/wiki-chat.cjs +81 -0
- package/lib/wiki/wiki-layout.cjs +1459 -0
- package/lib/wiki/wiki-search.cjs +142 -0
- package/lib/wiki/wiki-server.cjs +678 -0
- package/lib/wiki/wiki-watcher.cjs +105 -0
- package/lib/workflow/ROOM.md +47 -0
- package/lib/workflow/command-resolver.cjs +155 -0
- package/lib/workflow/command-resolver.test.cjs +235 -0
- package/package.json +44 -0
- package/pipelines/analogy/01-decompose.md +80 -0
- package/pipelines/analogy/02-abstract.md +87 -0
- package/pipelines/analogy/03-search.md +135 -0
- package/pipelines/analogy/04-transfer.md +101 -0
- package/pipelines/analogy/05-validate.md +106 -0
- package/pipelines/analogy/CHAIN.md +56 -0
- package/pipelines/discovery/01-explore-domains.md +44 -0
- package/pipelines/discovery/02-think-hats.md +50 -0
- package/pipelines/discovery/03-analyze-needs.md +54 -0
- package/pipelines/discovery/CHAIN.md +37 -0
- package/pipelines/thesis/01-structure-argument.md +45 -0
- package/pipelines/thesis/02-challenge-assumptions.md +48 -0
- package/pipelines/thesis/03-build-thesis.md +54 -0
- package/pipelines/thesis/CHAIN.md +37 -0
- package/references/brain/causal-directives.md +91 -0
- package/references/brain/causal-enrichment.cypher +165 -0
- package/references/brain/command-triggers-schema.md +226 -0
- package/references/brain/graph-architecture.md +317 -0
- package/references/brain/query-patterns.md +460 -0
- package/references/brain/room-hierarchy-schema.md +218 -0
- package/references/brain/schema.md +76 -0
- package/references/capability-radar/capabilities-index.md +241 -0
- package/references/capability-radar/changelog-cache.md +81 -0
- package/references/causal/causal-schema.md +103 -0
- package/references/design/email-template-standard.md +155 -0
- package/references/design/graph-visualization-standard.md +178 -0
- package/references/document-generation.md +179 -0
- package/references/hsi/HSI-TOOLS-REFERENCE.md +222 -0
- package/references/import-config.md +141 -0
- package/references/integrations/detection-patterns.md +101 -0
- package/references/meeting/artifact-template.md +377 -0
- package/references/meeting/cross-meeting-intelligence.md +216 -0
- package/references/meeting/cross-relationship-patterns.md +202 -0
- package/references/meeting/live-join-interface.md +244 -0
- package/references/meeting/section-mapping.md +192 -0
- package/references/meeting/segment-classification.md +258 -0
- package/references/meeting/speaker-profile-template.md +219 -0
- package/references/meeting/summary-template.md +348 -0
- package/references/meeting/transcript-patterns.md +226 -0
- package/references/methodology/analyze-needs.md +135 -0
- package/references/methodology/analyze-systems.md +121 -0
- package/references/methodology/analyze-timing.md +149 -0
- package/references/methodology/beautiful-question.md +109 -0
- package/references/methodology/build-knowledge.md +161 -0
- package/references/methodology/build-thesis.md +237 -0
- package/references/methodology/challenge-assumptions.md +127 -0
- package/references/methodology/diagnose.md +169 -0
- package/references/methodology/dominant-designs.md +212 -0
- package/references/methodology/explore-domains.md +147 -0
- package/references/methodology/explore-futures.md +163 -0
- package/references/methodology/explore-trends.md +129 -0
- package/references/methodology/find-bottlenecks.md +131 -0
- package/references/methodology/grade.md +211 -0
- package/references/methodology/index.md +97 -0
- package/references/methodology/leadership.md +200 -0
- package/references/methodology/lean-canvas.md +116 -0
- package/references/methodology/macro-trends.md +192 -0
- package/references/methodology/map-unknowns.md +137 -0
- package/references/methodology/mullins-7-domains.md +104 -0
- package/references/methodology/problem-types.md +65 -0
- package/references/methodology/root-cause.md +178 -0
- package/references/methodology/sapphire-encoding.md +355 -0
- package/references/methodology/scenario-plan.md +178 -0
- package/references/methodology/score-innovation.md +154 -0
- package/references/methodology/structure-argument.md +158 -0
- package/references/methodology/systems-thinking.md +159 -0
- package/references/methodology/think-hats.md +147 -0
- package/references/methodology/triz-matrix.json +751 -0
- package/references/methodology/triz-principles.md +501 -0
- package/references/methodology/user-needs.md +199 -0
- package/references/methodology/validate.md +163 -0
- package/references/methodology/value-proposition.md +244 -0
- package/references/opportunities/funding-lifecycle.md +103 -0
- package/references/opportunities/grant-api-patterns.md +99 -0
- package/references/opportunities/opportunity-template.md +84 -0
- package/references/personality/assessment-philosophy.md +72 -0
- package/references/personality/lexicon.md +100 -0
- package/references/personality/persona-chains.md +56 -0
- package/references/personality/pws-lexicon-full.md +499 -0
- package/references/personality/voice-dna.md +156 -0
- package/references/personas/hat-perspectives.md +76 -0
- package/references/personas/persona-template.md +63 -0
- package/references/pipeline/act-output-contract.md +88 -0
- package/references/pipeline/chains-index.md +39 -0
- package/references/pws-profile-generation.md +79 -0
- package/references/reasoning/reasoning-schema.md +143 -0
- package/references/reasoning/reasoning-template.md +68 -0
- package/references/reasoning/run-template.md +38 -0
- package/references/research/RESEARCH_14_CLAUDE_CODE_SOURCE_ARCHITECTURE.md +209 -0
- package/references/research/RESEARCH_15_V1.8_OPTIMIZATION_JTBD.md +375 -0
- package/references/research/RESEARCH_16_NATIVE_FIRST_PLUGIN_ARCHITECTURE.md +575 -0
- package/references/research/RESEARCH_17_MCP_UI_FRAMEWORKS.md +272 -0
- package/references/taxonomy/TAXONOMY.md +192 -0
- package/references/templates/MINTO.md +36 -0
- package/references/user-research/2026-04-05-leah-lawrence-session.md +202 -0
- package/references/vault-kit/README.md +35 -0
- package/references/vault-kit/app.json +12 -0
- package/references/vault-kit/appearance.json +12 -0
- package/references/vault-kit/graph.json +35 -0
- package/references/vault-kit/snippets/mindrian-destijl.css +297 -0
- package/references/vault-kit/templates/new-artifact.md +37 -0
- package/references/vault-kit/templates/new-meeting-note.md +35 -0
- package/references/vault-kit/templates/new-team-profile.md +29 -0
- package/references/vault-kit/templates/new-xref.md +35 -0
- package/references/visual/symbol-system.md +151 -0
- package/skills/MOSDeckEngine/SKILL.md +325 -0
- package/skills/brain-connector/SKILL.md +114 -0
- package/skills/context-engine/SKILL.md +147 -0
- package/skills/conversation-mode/SKILL.md +102 -0
- package/skills/larry-personality/SKILL.md +219 -0
- package/skills/larry-personality/framework-chains.md +92 -0
- package/skills/larry-personality/mode-engine.md +185 -0
- package/skills/mullins-scaffold/SKILL.md +61 -0
- package/skills/mullins-scaffold/scaffold.json +146 -0
- package/skills/pws-methodology/SKILL.md +49 -0
- package/skills/room-passive/SKILL.md +165 -0
- package/skills/room-proactive/SKILL.md +250 -0
- package/skills/ui-system/SKILL.md +277 -0
|
@@ -0,0 +1,1371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindrianOS Plugin -- Opportunity Bank + Funding Operations
|
|
3
|
+
* Core operations for opportunity-bank/ and funding/ room sections.
|
|
4
|
+
* Pure Node.js built-ins only (zero npm deps per Phase 10 decision).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { discoverSections } = require('./section-registry.cjs');
|
|
12
|
+
const { opportunityHash, OPPORTUNITY_SCHEMA_FIELDS } = require('./opportunity-extractor.cjs');
|
|
13
|
+
const graphOps = require('./graph-ops.cjs');
|
|
14
|
+
const brain = require('./brain-client.cjs');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse YAML frontmatter from a markdown string.
|
|
18
|
+
* Simple regex/split parsing (no yaml library -- follows existing codebase pattern).
|
|
19
|
+
* Handles scalar values, simple lists (- item), and nested objects.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} content - Markdown file content
|
|
22
|
+
* @returns {Object} Parsed frontmatter key-value pairs
|
|
23
|
+
*/
|
|
24
|
+
function parseFrontmatter(content) {
|
|
25
|
+
if (!content || typeof content !== 'string') return {};
|
|
26
|
+
|
|
27
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
28
|
+
if (!match) return {};
|
|
29
|
+
|
|
30
|
+
const yaml = match[1];
|
|
31
|
+
const result = {};
|
|
32
|
+
const lines = yaml.split('\n');
|
|
33
|
+
|
|
34
|
+
let currentKey = null;
|
|
35
|
+
let currentList = null;
|
|
36
|
+
let currentObj = null;
|
|
37
|
+
let currentObjKey = null;
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
const line = lines[i];
|
|
41
|
+
|
|
42
|
+
// Top-level key: value
|
|
43
|
+
const topMatch = line.match(/^([a-z_]+):\s*(.*)$/);
|
|
44
|
+
if (topMatch) {
|
|
45
|
+
// Flush any pending list/object
|
|
46
|
+
if (currentList !== null && currentKey) {
|
|
47
|
+
result[currentKey] = currentList;
|
|
48
|
+
currentList = null;
|
|
49
|
+
}
|
|
50
|
+
if (currentObj !== null && currentObjKey !== null) {
|
|
51
|
+
if (!result[currentKey]) result[currentKey] = [];
|
|
52
|
+
result[currentKey].push(currentObj);
|
|
53
|
+
currentObj = null;
|
|
54
|
+
currentObjKey = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
currentKey = topMatch[1];
|
|
58
|
+
const val = topMatch[2].trim();
|
|
59
|
+
|
|
60
|
+
if (val === '' || val === 'null') {
|
|
61
|
+
// Could be a list or object starting on next line
|
|
62
|
+
result[currentKey] = null;
|
|
63
|
+
} else if (val === 'true') {
|
|
64
|
+
result[currentKey] = true;
|
|
65
|
+
} else if (val === 'false') {
|
|
66
|
+
result[currentKey] = false;
|
|
67
|
+
} else if (/^-?\d+(\.\d+)?$/.test(val)) {
|
|
68
|
+
result[currentKey] = parseFloat(val);
|
|
69
|
+
} else {
|
|
70
|
+
// Remove surrounding quotes if present
|
|
71
|
+
result[currentKey] = val.replace(/^["']|["']$/g, '');
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// List item ( - value)
|
|
77
|
+
const listMatch = line.match(/^\s+-\s+(.+)$/);
|
|
78
|
+
if (listMatch && currentKey) {
|
|
79
|
+
if (currentList === null) currentList = [];
|
|
80
|
+
|
|
81
|
+
const itemVal = listMatch[1].trim();
|
|
82
|
+
|
|
83
|
+
// Check if this is a nested object field ( - key: value)
|
|
84
|
+
const nestedMatch = itemVal.match(/^([a-z_]+):\s*(.+)$/);
|
|
85
|
+
if (nestedMatch) {
|
|
86
|
+
// Flush previous object if starting a new one
|
|
87
|
+
if (currentObj !== null) {
|
|
88
|
+
if (!Array.isArray(result[currentKey])) result[currentKey] = [];
|
|
89
|
+
result[currentKey].push(currentObj);
|
|
90
|
+
}
|
|
91
|
+
currentObj = {};
|
|
92
|
+
currentObjKey = currentKey;
|
|
93
|
+
currentObj[nestedMatch[1]] = nestedMatch[2].replace(/^["']|["']$/g, '').trim();
|
|
94
|
+
} else {
|
|
95
|
+
// Simple list item
|
|
96
|
+
currentList.push(itemVal.replace(/^["']|["']$/g, ''));
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Nested object field ( key: value) -- continuation of a list object
|
|
102
|
+
const nestedFieldMatch = line.match(/^\s{4,}([a-z_]+):\s*(.+)$/);
|
|
103
|
+
if (nestedFieldMatch && currentObj !== null) {
|
|
104
|
+
currentObj[nestedFieldMatch[1]] = nestedFieldMatch[2].replace(/^["']|["']$/g, '').trim();
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Flush any pending list/object
|
|
110
|
+
if (currentList !== null && currentKey) {
|
|
111
|
+
result[currentKey] = currentList;
|
|
112
|
+
}
|
|
113
|
+
if (currentObj !== null && currentKey) {
|
|
114
|
+
if (!Array.isArray(result[currentKey])) result[currentKey] = [];
|
|
115
|
+
result[currentKey].push(currentObj);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parse opportunity artifact frontmatter.
|
|
123
|
+
* @param {string} content - Opportunity markdown file content
|
|
124
|
+
* @returns {Object} Parsed opportunity fields
|
|
125
|
+
*/
|
|
126
|
+
function parseOpportunityFrontmatter(content) {
|
|
127
|
+
return parseFrontmatter(content);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse funding STATUS.md frontmatter.
|
|
132
|
+
* @param {string} content - STATUS.md file content
|
|
133
|
+
* @returns {{ stage: string|null, outcome: string|null, source_opportunity: string|null, deadline: string|null, last_updated: string|null, transition_history: Array|null }}
|
|
134
|
+
*/
|
|
135
|
+
function parseFundingStatus(content) {
|
|
136
|
+
const fm = parseFrontmatter(content);
|
|
137
|
+
return {
|
|
138
|
+
stage: fm.stage || null,
|
|
139
|
+
outcome: fm.outcome || null,
|
|
140
|
+
source_opportunity: fm.source_opportunity || null,
|
|
141
|
+
deadline: fm.deadline || null,
|
|
142
|
+
last_updated: fm.last_updated || null,
|
|
143
|
+
transition_history: fm.transition_history || null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* List opportunities in room/opportunity-bank/.
|
|
149
|
+
* Scans for .md files (excluding STATE.md), parses frontmatter for each.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} roomDir - Path to room directory
|
|
152
|
+
* @returns {{ opportunities: Array<{filename, funder, program, deadline, relevance_score, status}>, count: number }}
|
|
153
|
+
*/
|
|
154
|
+
function listOpportunities(roomDir) {
|
|
155
|
+
const oppDir = path.join(path.resolve(roomDir), 'opportunity-bank');
|
|
156
|
+
if (!fs.existsSync(oppDir)) return { opportunities: [], count: 0 };
|
|
157
|
+
|
|
158
|
+
let files;
|
|
159
|
+
try {
|
|
160
|
+
files = fs.readdirSync(oppDir).filter(f => f.endsWith('.md') && f !== 'STATE.md');
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return { opportunities: [], count: 0 };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const opportunities = files.map(filename => {
|
|
166
|
+
const filePath = path.join(oppDir, filename);
|
|
167
|
+
try {
|
|
168
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
169
|
+
const fm = parseOpportunityFrontmatter(content);
|
|
170
|
+
return {
|
|
171
|
+
filename,
|
|
172
|
+
funder: fm.funder || null,
|
|
173
|
+
program: fm.program || null,
|
|
174
|
+
deadline: fm.deadline || null,
|
|
175
|
+
relevance_score: fm.relevance_score != null ? fm.relevance_score : null,
|
|
176
|
+
status: fm.status || null,
|
|
177
|
+
};
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return { filename, funder: null, program: null, deadline: null, relevance_score: null, status: null };
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return { opportunities, count: opportunities.length };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* List funding entries in room/funding/.
|
|
188
|
+
* Scans for subdirectories, reads STATUS.md from each.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} roomDir - Path to room directory
|
|
191
|
+
* @returns {{ entries: Array<{name, stage, outcome, deadline, source_opportunity}>, count: number }}
|
|
192
|
+
*/
|
|
193
|
+
function listFunding(roomDir) {
|
|
194
|
+
const fundDir = path.join(path.resolve(roomDir), 'funding');
|
|
195
|
+
if (!fs.existsSync(fundDir)) return { entries: [], count: 0 };
|
|
196
|
+
|
|
197
|
+
let dirEntries;
|
|
198
|
+
try {
|
|
199
|
+
dirEntries = fs.readdirSync(fundDir, { withFileTypes: true })
|
|
200
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'));
|
|
201
|
+
} catch (e) {
|
|
202
|
+
return { entries: [], count: 0 };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const entries = dirEntries.map(entry => {
|
|
206
|
+
const statusPath = path.join(fundDir, entry.name, 'STATUS.md');
|
|
207
|
+
try {
|
|
208
|
+
const content = fs.readFileSync(statusPath, 'utf-8');
|
|
209
|
+
const st = parseFundingStatus(content);
|
|
210
|
+
return {
|
|
211
|
+
name: entry.name,
|
|
212
|
+
stage: st.stage,
|
|
213
|
+
outcome: st.outcome,
|
|
214
|
+
deadline: st.deadline,
|
|
215
|
+
source_opportunity: st.source_opportunity,
|
|
216
|
+
};
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return { name: entry.name, stage: null, outcome: null, deadline: null, source_opportunity: null };
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return { entries, count: entries.length };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Read opportunity-bank/STATE.md content.
|
|
227
|
+
* @param {string} roomDir - Path to room directory
|
|
228
|
+
* @returns {string|null} STATE.md content or null if missing
|
|
229
|
+
*/
|
|
230
|
+
function getOpportunityBankState(roomDir) {
|
|
231
|
+
const statePath = path.join(path.resolve(roomDir), 'opportunity-bank', 'STATE.md');
|
|
232
|
+
try {
|
|
233
|
+
return fs.readFileSync(statePath, 'utf-8');
|
|
234
|
+
} catch (e) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Read funding/STATE.md content.
|
|
241
|
+
* @param {string} roomDir - Path to room directory
|
|
242
|
+
* @returns {string|null} STATE.md content or null if missing
|
|
243
|
+
*/
|
|
244
|
+
function getFundingState(roomDir) {
|
|
245
|
+
const statePath = path.join(path.resolve(roomDir), 'funding', 'STATE.md');
|
|
246
|
+
try {
|
|
247
|
+
return fs.readFileSync(statePath, 'utf-8');
|
|
248
|
+
} catch (e) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Domain-to-funding-category mapping for API queries
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
const DOMAIN_CATEGORY_MAP = {
|
|
257
|
+
'artificial-intelligence': 'ST',
|
|
258
|
+
'machine-learning': 'ST',
|
|
259
|
+
'natural-language-processing': 'ST',
|
|
260
|
+
'software': 'ST',
|
|
261
|
+
'robotics': 'ST',
|
|
262
|
+
'biotech': 'HL',
|
|
263
|
+
'health': 'HL',
|
|
264
|
+
'healthcare': 'HL',
|
|
265
|
+
'medical': 'HL',
|
|
266
|
+
'clean-energy': 'EN',
|
|
267
|
+
'energy': 'EN',
|
|
268
|
+
'climate': 'EN',
|
|
269
|
+
'environment': 'ENV',
|
|
270
|
+
'education': 'ED',
|
|
271
|
+
'agriculture': 'AG',
|
|
272
|
+
'food': 'AG',
|
|
273
|
+
'transportation': 'T',
|
|
274
|
+
'infrastructure': 'ISS',
|
|
275
|
+
'housing': 'HU',
|
|
276
|
+
'community-development': 'CD',
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Geography to eligibility mapping
|
|
280
|
+
const GEO_ELIGIBILITY_MAP = {
|
|
281
|
+
'United States': ['us-entity'],
|
|
282
|
+
'US': ['us-entity'],
|
|
283
|
+
'Israel': ['international'],
|
|
284
|
+
'EU': ['international'],
|
|
285
|
+
'UK': ['international'],
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Build a grant query from room context.
|
|
290
|
+
* Reads room STATE.md, problem-definition/ for domain context.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} roomDir - Path to room directory
|
|
293
|
+
* @returns {{ keyword: string, fundingCategories: string[], eligibilities: string[], geography: string, ventureStage: string } | { insufficient: true, reason: string }}
|
|
294
|
+
*/
|
|
295
|
+
function buildGrantQuery(roomDir) {
|
|
296
|
+
const resolved = path.resolve(roomDir);
|
|
297
|
+
|
|
298
|
+
// Read room STATE.md for venture context
|
|
299
|
+
const statePath = path.join(resolved, 'STATE.md');
|
|
300
|
+
let stateContent = '';
|
|
301
|
+
try {
|
|
302
|
+
stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
303
|
+
} catch (_e) {
|
|
304
|
+
// No state file
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const stateFm = parseFrontmatter(stateContent);
|
|
308
|
+
|
|
309
|
+
// Read problem-definition for domain context
|
|
310
|
+
const probDir = path.join(resolved, 'problem-definition');
|
|
311
|
+
let problemText = '';
|
|
312
|
+
if (fs.existsSync(probDir)) {
|
|
313
|
+
try {
|
|
314
|
+
const files = fs.readdirSync(probDir).filter(f => f.endsWith('.md') && f !== 'STATE.md' && f !== 'ROOM.md');
|
|
315
|
+
for (const f of files) {
|
|
316
|
+
try {
|
|
317
|
+
problemText += ' ' + fs.readFileSync(path.join(probDir, f), 'utf-8');
|
|
318
|
+
} catch (_e) { /* skip */ }
|
|
319
|
+
}
|
|
320
|
+
} catch (_e) { /* skip */ }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Check for sufficient context
|
|
324
|
+
const domainKeywords = stateFm.domain_keywords || [];
|
|
325
|
+
if ((!domainKeywords || domainKeywords.length === 0) && problemText.trim().length < 50) {
|
|
326
|
+
return {
|
|
327
|
+
insufficient: true,
|
|
328
|
+
reason: 'Room needs domain_keywords in STATE.md or content in problem-definition/ for context-driven grant discovery. Add your venture domain, geography, and team type to STATE.md.',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Build keyword from domain keywords + problem text extraction
|
|
333
|
+
const keywordParts = Array.isArray(domainKeywords) ? [...domainKeywords] : [];
|
|
334
|
+
|
|
335
|
+
// Extract key terms from problem text (first 200 chars of body, after frontmatter)
|
|
336
|
+
if (problemText) {
|
|
337
|
+
const body = problemText.replace(/^---[\s\S]*?---/, '').trim();
|
|
338
|
+
const firstSentence = body.split(/[.!?\n]/).filter(s => s.trim().length > 10)[0] || '';
|
|
339
|
+
if (firstSentence) {
|
|
340
|
+
// Extract significant words (5+ chars, not common words)
|
|
341
|
+
const stopWords = new Set(['about', 'their', 'these', 'those', 'which', 'where', 'through', 'between', 'using', 'based', 'should', 'would', 'could']);
|
|
342
|
+
const terms = firstSentence.toLowerCase().match(/[a-z]{5,}/g) || [];
|
|
343
|
+
const significant = terms.filter(t => !stopWords.has(t)).slice(0, 3);
|
|
344
|
+
keywordParts.push(...significant);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Deduplicate and build keyword string (max 100 chars for API compat)
|
|
349
|
+
const uniqueKeywords = [...new Set(keywordParts)];
|
|
350
|
+
let keyword = uniqueKeywords.join(' ');
|
|
351
|
+
if (keyword.length > 100) keyword = keyword.slice(0, 97) + '...';
|
|
352
|
+
|
|
353
|
+
// Map domain keywords to funding categories
|
|
354
|
+
const fundingCategories = [];
|
|
355
|
+
for (const kw of (Array.isArray(domainKeywords) ? domainKeywords : [])) {
|
|
356
|
+
const cat = DOMAIN_CATEGORY_MAP[kw];
|
|
357
|
+
if (cat && !fundingCategories.includes(cat)) fundingCategories.push(cat);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Geography and eligibility
|
|
361
|
+
const geography = stateFm.geography || 'United States';
|
|
362
|
+
const eligibilities = GEO_ELIGIBILITY_MAP[geography] || [];
|
|
363
|
+
|
|
364
|
+
// Venture stage
|
|
365
|
+
const ventureStage = stateFm.venture_stage || 'unknown';
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
keyword,
|
|
369
|
+
fundingCategories,
|
|
370
|
+
eligibilities,
|
|
371
|
+
geography,
|
|
372
|
+
ventureStage,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Search Grants.gov API (v1).
|
|
378
|
+
* POST to https://api.grants.gov/v1/api/search2.
|
|
379
|
+
*
|
|
380
|
+
* @param {{ keyword: string, fundingCategories?: string[] }} query
|
|
381
|
+
* @returns {Promise<{ results: Array, error: string|null }>}
|
|
382
|
+
*/
|
|
383
|
+
async function searchGrantsGov(query) {
|
|
384
|
+
const url = 'https://api.grants.gov/v1/api/search2';
|
|
385
|
+
const body = {
|
|
386
|
+
keyword: query.keyword || '',
|
|
387
|
+
oppStatuses: 'posted',
|
|
388
|
+
rows: 25,
|
|
389
|
+
sortBy: 'openDate|desc',
|
|
390
|
+
};
|
|
391
|
+
if (query.fundingCategories && query.fundingCategories.length > 0) {
|
|
392
|
+
body.fundingCategories = query.fundingCategories.join('|');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const controller = new AbortController();
|
|
397
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
398
|
+
|
|
399
|
+
const resp = await fetch(url, {
|
|
400
|
+
method: 'POST',
|
|
401
|
+
headers: { 'Content-Type': 'application/json' },
|
|
402
|
+
body: JSON.stringify(body),
|
|
403
|
+
signal: controller.signal,
|
|
404
|
+
});
|
|
405
|
+
clearTimeout(timeout);
|
|
406
|
+
|
|
407
|
+
if (!resp.ok) {
|
|
408
|
+
return { results: [], error: `Grants.gov API returned ${resp.status}` };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const data = await resp.json();
|
|
412
|
+
const hits = (data.oppHits || []).map(h => ({
|
|
413
|
+
title: h.title || h.oppTitle || '',
|
|
414
|
+
funder: h.agencyName || h.agency || '',
|
|
415
|
+
program: h.oppNumber || '',
|
|
416
|
+
amount: h.awardCeiling || null,
|
|
417
|
+
deadline: h.closeDate || null,
|
|
418
|
+
source: 'grants-gov',
|
|
419
|
+
source_url: `https://grants.gov/search-results-detail/${h.id || h.oppId || ''}`,
|
|
420
|
+
opportunity_id: h.oppNumber || h.id || '',
|
|
421
|
+
}));
|
|
422
|
+
|
|
423
|
+
return { results: hits, error: null };
|
|
424
|
+
} catch (e) {
|
|
425
|
+
const msg = e.name === 'AbortError' ? 'Grants.gov API timeout (10s)' : `Grants.gov API error: ${e.message}`;
|
|
426
|
+
return { results: [], error: msg };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Search Simpler Grants API.
|
|
432
|
+
* POST to https://api.simpler.grants.gov/v1/opportunities/search.
|
|
433
|
+
*
|
|
434
|
+
* @param {{ keyword: string }} query
|
|
435
|
+
* @returns {Promise<{ results: Array, error: string|null }>}
|
|
436
|
+
*/
|
|
437
|
+
async function searchSimplerGrants(query) {
|
|
438
|
+
const url = 'https://api.simpler.grants.gov/v1/opportunities/search';
|
|
439
|
+
const keyword = (query.keyword || '').slice(0, 100);
|
|
440
|
+
const body = {
|
|
441
|
+
query: keyword,
|
|
442
|
+
filters: { opportunity_status: { one_of: ['posted'] } },
|
|
443
|
+
pagination: { page_size: 25, sort_by: [{ order_by: 'relevancy' }] },
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const controller = new AbortController();
|
|
448
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
449
|
+
|
|
450
|
+
const resp = await fetch(url, {
|
|
451
|
+
method: 'POST',
|
|
452
|
+
headers: { 'Content-Type': 'application/json' },
|
|
453
|
+
body: JSON.stringify(body),
|
|
454
|
+
signal: controller.signal,
|
|
455
|
+
});
|
|
456
|
+
clearTimeout(timeout);
|
|
457
|
+
|
|
458
|
+
if (!resp.ok) {
|
|
459
|
+
return { results: [], error: `Simpler Grants API returned ${resp.status}` };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const data = await resp.json();
|
|
463
|
+
const items = (data.data || []).map(item => ({
|
|
464
|
+
title: item.opportunity_title || '',
|
|
465
|
+
funder: item.agency_name || item.agency || '',
|
|
466
|
+
program: item.opportunity_number || '',
|
|
467
|
+
amount: item.award_ceiling || null,
|
|
468
|
+
deadline: item.close_date || null,
|
|
469
|
+
source: 'simpler-grants',
|
|
470
|
+
source_url: `https://simpler.grants.gov/opportunity/${item.opportunity_id || ''}`,
|
|
471
|
+
opportunity_id: item.opportunity_number || item.opportunity_id || '',
|
|
472
|
+
}));
|
|
473
|
+
|
|
474
|
+
return { results: items, error: null };
|
|
475
|
+
} catch (e) {
|
|
476
|
+
const msg = e.name === 'AbortError' ? 'Simpler Grants API timeout (10s)' : `Simpler Grants API error: ${e.message}`;
|
|
477
|
+
return { results: [], error: msg };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Compute relevance score for an opportunity against room context.
|
|
483
|
+
*
|
|
484
|
+
* @param {Object} opp - Opportunity data (title, funder, program, etc.)
|
|
485
|
+
* @param {Object} queryContext - Output from buildGrantQuery
|
|
486
|
+
* @returns {{ score: number, reasoning: string }}
|
|
487
|
+
*/
|
|
488
|
+
function computeRelevance(opp, queryContext) {
|
|
489
|
+
let score = 0;
|
|
490
|
+
const reasons = [];
|
|
491
|
+
|
|
492
|
+
// Domain fit: check if title/program contains domain keywords
|
|
493
|
+
const titleLower = ((opp.title || '') + ' ' + (opp.program || '')).toLowerCase();
|
|
494
|
+
const keywords = queryContext.keyword ? queryContext.keyword.toLowerCase().split(/\s+/) : [];
|
|
495
|
+
let domainHits = 0;
|
|
496
|
+
for (const kw of keywords) {
|
|
497
|
+
if (kw.length >= 4 && titleLower.includes(kw)) domainHits++;
|
|
498
|
+
}
|
|
499
|
+
if (domainHits >= 2) {
|
|
500
|
+
score += 0.35;
|
|
501
|
+
reasons.push('Strong domain keyword match');
|
|
502
|
+
} else if (domainHits >= 1) {
|
|
503
|
+
score += 0.2;
|
|
504
|
+
reasons.push('Partial domain keyword match');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Eligibility check (if geography matches)
|
|
508
|
+
if (queryContext.geography === 'United States') {
|
|
509
|
+
score += 0.15;
|
|
510
|
+
reasons.push('US entity eligible');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Has deadline (actionable)
|
|
514
|
+
if (opp.deadline) {
|
|
515
|
+
score += 0.1;
|
|
516
|
+
reasons.push('Has defined deadline');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Has funding amount (quantifiable)
|
|
520
|
+
if (opp.amount && opp.amount > 0) {
|
|
521
|
+
score += 0.1;
|
|
522
|
+
reasons.push('Funding amount specified');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Stage match: early-stage grants (SBIR/STTR/seed) match pre-revenue
|
|
526
|
+
if (queryContext.ventureStage && /pre-revenue|seed|early/i.test(queryContext.ventureStage)) {
|
|
527
|
+
if (/sbir|sttr|seed|phase\s*i|early.stage/i.test(titleLower)) {
|
|
528
|
+
score += 0.2;
|
|
529
|
+
reasons.push('Stage-appropriate (early-stage grant)');
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Baseline relevance (it was returned by keyword search)
|
|
534
|
+
score += 0.1;
|
|
535
|
+
|
|
536
|
+
// Cap at 1.0
|
|
537
|
+
score = Math.min(1.0, Math.round(score * 100) / 100);
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
score,
|
|
541
|
+
reasoning: reasons.join('. ') || 'Returned by keyword search',
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Scan for opportunities using room context.
|
|
547
|
+
* Calls buildGrantQuery, then both grant APIs, merges + deduplicates + scores.
|
|
548
|
+
*
|
|
549
|
+
* @param {string} roomDir - Path to room directory
|
|
550
|
+
* @returns {Promise<{ query_context: Object, results: Array, api_errors: string[] }>}
|
|
551
|
+
*/
|
|
552
|
+
async function scanOpportunities(roomDir) {
|
|
553
|
+
const queryContext = buildGrantQuery(roomDir);
|
|
554
|
+
|
|
555
|
+
if (queryContext.insufficient) {
|
|
556
|
+
return {
|
|
557
|
+
query_context: queryContext,
|
|
558
|
+
results: [],
|
|
559
|
+
api_errors: [],
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Call both APIs concurrently
|
|
564
|
+
const [grantsGovResult, simplerResult] = await Promise.allSettled([
|
|
565
|
+
searchGrantsGov(queryContext),
|
|
566
|
+
searchSimplerGrants(queryContext),
|
|
567
|
+
]);
|
|
568
|
+
|
|
569
|
+
const allResults = [];
|
|
570
|
+
const apiErrors = [];
|
|
571
|
+
|
|
572
|
+
// Collect Grants.gov results
|
|
573
|
+
if (grantsGovResult.status === 'fulfilled') {
|
|
574
|
+
allResults.push(...grantsGovResult.value.results);
|
|
575
|
+
if (grantsGovResult.value.error) apiErrors.push(grantsGovResult.value.error);
|
|
576
|
+
} else {
|
|
577
|
+
apiErrors.push(`Grants.gov: ${grantsGovResult.reason}`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Collect Simpler Grants results
|
|
581
|
+
if (simplerResult.status === 'fulfilled') {
|
|
582
|
+
allResults.push(...simplerResult.value.results);
|
|
583
|
+
if (simplerResult.value.error) apiErrors.push(simplerResult.value.error);
|
|
584
|
+
} else {
|
|
585
|
+
apiErrors.push(`Simpler Grants: ${simplerResult.reason}`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Deduplicate by opportunity_id (prefer first seen)
|
|
589
|
+
const seen = new Set();
|
|
590
|
+
const deduped = [];
|
|
591
|
+
for (const opp of allResults) {
|
|
592
|
+
const key = opp.opportunity_id || opp.title;
|
|
593
|
+
if (!key || seen.has(key)) continue;
|
|
594
|
+
seen.add(key);
|
|
595
|
+
deduped.push(opp);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Compute relevance scores
|
|
599
|
+
const scored = deduped.map(opp => {
|
|
600
|
+
const rel = computeRelevance(opp, queryContext);
|
|
601
|
+
return {
|
|
602
|
+
...opp,
|
|
603
|
+
relevance_score: rel.score,
|
|
604
|
+
relevance_reasoning: rel.reasoning,
|
|
605
|
+
};
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// Sort by relevance descending
|
|
609
|
+
scored.sort((a, b) => b.relevance_score - a.relevance_score);
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
query_context: queryContext,
|
|
613
|
+
results: scored,
|
|
614
|
+
api_errors: apiErrors,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* File an opportunity to room/opportunity-bank/.
|
|
620
|
+
* Creates a dated artifact file following opportunity-template.md schema.
|
|
621
|
+
*
|
|
622
|
+
* @param {string} roomDir - Path to room directory
|
|
623
|
+
* @param {Object} opportunityData - Opportunity data to file
|
|
624
|
+
* @returns {{ filed: boolean, path: string }}
|
|
625
|
+
*/
|
|
626
|
+
function fileOpportunity(roomDir, opportunityData) {
|
|
627
|
+
const resolved = path.resolve(roomDir);
|
|
628
|
+
const oppDir = path.join(resolved, 'opportunity-bank');
|
|
629
|
+
|
|
630
|
+
// Create directory if needed
|
|
631
|
+
if (!fs.existsSync(oppDir)) {
|
|
632
|
+
fs.mkdirSync(oppDir, { recursive: true });
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const today = new Date().toISOString().split('T')[0];
|
|
636
|
+
const slug = (opportunityData.program || opportunityData.title || 'unknown')
|
|
637
|
+
.toLowerCase()
|
|
638
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
639
|
+
.replace(/^-|-$/g, '')
|
|
640
|
+
.slice(0, 50);
|
|
641
|
+
const filename = `${today}-${slug}.md`;
|
|
642
|
+
const filePath = path.join(oppDir, filename);
|
|
643
|
+
|
|
644
|
+
// Build frontmatter
|
|
645
|
+
const fm = [
|
|
646
|
+
'---',
|
|
647
|
+
'methodology: opportunity-scan',
|
|
648
|
+
`created: ${today}`,
|
|
649
|
+
`source: ${opportunityData.source || 'manual'}`,
|
|
650
|
+
];
|
|
651
|
+
if (opportunityData.source_url) fm.push(`source_url: ${opportunityData.source_url}`);
|
|
652
|
+
if (opportunityData.opportunity_id) fm.push(`opportunity_id: "${opportunityData.opportunity_id}"`);
|
|
653
|
+
fm.push(`funder: ${opportunityData.funder || 'Unknown'}`);
|
|
654
|
+
fm.push(`program: ${opportunityData.program || opportunityData.title || 'Unknown'}`);
|
|
655
|
+
fm.push(`amount_floor: ${opportunityData.amount_floor || 0}`);
|
|
656
|
+
fm.push(`amount_ceiling: ${opportunityData.amount_ceiling || opportunityData.amount || 0}`);
|
|
657
|
+
if (opportunityData.deadline) fm.push(`deadline: ${opportunityData.deadline}`);
|
|
658
|
+
fm.push(`relevance_score: ${opportunityData.relevance_score || 0}`);
|
|
659
|
+
fm.push(`relevance_reasoning: "${(opportunityData.relevance_reasoning || '').replace(/"/g, '\\"')}"`);
|
|
660
|
+
fm.push('status: filed');
|
|
661
|
+
fm.push('rejection: null');
|
|
662
|
+
fm.push('---');
|
|
663
|
+
|
|
664
|
+
// Build body
|
|
665
|
+
const body = [
|
|
666
|
+
'',
|
|
667
|
+
`# ${opportunityData.title || opportunityData.program || 'Opportunity'}`,
|
|
668
|
+
'',
|
|
669
|
+
'## Overview',
|
|
670
|
+
'',
|
|
671
|
+
`Filed from ${opportunityData.source || 'manual'} scan on ${today}.`,
|
|
672
|
+
];
|
|
673
|
+
if (opportunityData.funder) body.push(`Funder: ${opportunityData.funder}`);
|
|
674
|
+
if (opportunityData.amount) body.push(`Award: up to $${Number(opportunityData.amount).toLocaleString()}`);
|
|
675
|
+
if (opportunityData.deadline) body.push(`Deadline: ${opportunityData.deadline}`);
|
|
676
|
+
|
|
677
|
+
const content = fm.join('\n') + '\n' + body.join('\n') + '\n';
|
|
678
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
679
|
+
|
|
680
|
+
return { filed: true, path: filePath };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Reject an opportunity, capturing the reason as data.
|
|
685
|
+
* Appends rejection record to opportunity-bank/STATE.md.
|
|
686
|
+
*
|
|
687
|
+
* @param {string} roomDir - Path to room directory
|
|
688
|
+
* @param {Object} opportunityData - Opportunity data being rejected
|
|
689
|
+
* @param {string} reason - Rejection reason (rejection IS data)
|
|
690
|
+
* @returns {{ rejected: boolean, reason: string }}
|
|
691
|
+
*/
|
|
692
|
+
function rejectOpportunity(roomDir, opportunityData, reason) {
|
|
693
|
+
const resolved = path.resolve(roomDir);
|
|
694
|
+
const oppDir = path.join(resolved, 'opportunity-bank');
|
|
695
|
+
|
|
696
|
+
// Create directory if needed
|
|
697
|
+
if (!fs.existsSync(oppDir)) {
|
|
698
|
+
fs.mkdirSync(oppDir, { recursive: true });
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const statePath = path.join(oppDir, 'STATE.md');
|
|
702
|
+
let stateContent = '';
|
|
703
|
+
try {
|
|
704
|
+
stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
705
|
+
} catch (_e) {
|
|
706
|
+
stateContent = '---\nsection: opportunity-bank\n---\n\n# Opportunity Bank\n';
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Append rejection record
|
|
710
|
+
const today = new Date().toISOString().split('T')[0];
|
|
711
|
+
const title = opportunityData.title || opportunityData.program || 'Unknown';
|
|
712
|
+
const record = `\n## Rejections\n\n- **${today}** -- ${title}: ${reason}\n`;
|
|
713
|
+
|
|
714
|
+
if (stateContent.includes('## Rejections')) {
|
|
715
|
+
// Append to existing rejections section
|
|
716
|
+
stateContent = stateContent.replace(
|
|
717
|
+
/(## Rejections\n)/,
|
|
718
|
+
`$1\n- **${today}** -- ${title}: ${reason}\n`
|
|
719
|
+
);
|
|
720
|
+
} else {
|
|
721
|
+
stateContent += record;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
fs.writeFileSync(statePath, stateContent, 'utf-8');
|
|
725
|
+
|
|
726
|
+
return { rejected: true, reason };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ---------------------------------------------------------------------------
|
|
730
|
+
// Funding lifecycle stages (sequential, no skipping, no backward)
|
|
731
|
+
// ---------------------------------------------------------------------------
|
|
732
|
+
const FUNDING_STAGES = ['discovered', 'researched', 'applying', 'submitted'];
|
|
733
|
+
const VALID_OUTCOMES = ['awarded', 'rejected', 'withdrawn'];
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Create a funding entry from an opportunity-bank source.
|
|
737
|
+
* Creates room/funding/{slug}/ with STATUS.md and metadata.yaml.
|
|
738
|
+
*
|
|
739
|
+
* @param {string} roomDir - Path to room directory
|
|
740
|
+
* @param {string} slug - Slug for the funding folder name
|
|
741
|
+
* @param {string} sourceOpportunityPath - Filename (without extension) of source in opportunity-bank
|
|
742
|
+
* @returns {{ created: boolean, path: string, slug: string }}
|
|
743
|
+
*/
|
|
744
|
+
function createFunding(roomDir, slug, sourceOpportunityPath) {
|
|
745
|
+
const resolved = path.resolve(roomDir);
|
|
746
|
+
const fundDir = path.join(resolved, 'funding', slug);
|
|
747
|
+
|
|
748
|
+
// Create directory tree if needed
|
|
749
|
+
if (!fs.existsSync(fundDir)) {
|
|
750
|
+
fs.mkdirSync(fundDir, { recursive: true });
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const today = new Date().toISOString().split('T')[0];
|
|
754
|
+
|
|
755
|
+
// Read source opportunity for metadata
|
|
756
|
+
const sourceFile = sourceOpportunityPath.endsWith('.md')
|
|
757
|
+
? sourceOpportunityPath
|
|
758
|
+
: `${sourceOpportunityPath}.md`;
|
|
759
|
+
const sourcePath = path.join(resolved, 'opportunity-bank', sourceFile);
|
|
760
|
+
let sourceFm = {};
|
|
761
|
+
try {
|
|
762
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
|
|
763
|
+
sourceFm = parseFrontmatter(sourceContent);
|
|
764
|
+
} catch (_e) {
|
|
765
|
+
// Source may not exist — proceed with defaults
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Wikilink reference (without .md extension)
|
|
769
|
+
const sourceRef = sourceOpportunityPath.replace(/\.md$/, '');
|
|
770
|
+
|
|
771
|
+
// Build STATUS.md
|
|
772
|
+
const statusContent = [
|
|
773
|
+
'---',
|
|
774
|
+
'stage: discovered',
|
|
775
|
+
'outcome: null',
|
|
776
|
+
`source_opportunity: "[[opportunity-bank/${sourceRef}]]"`,
|
|
777
|
+
sourceFm.deadline ? `deadline: ${sourceFm.deadline}` : 'deadline: null',
|
|
778
|
+
`last_updated: ${today}`,
|
|
779
|
+
'transition_history:',
|
|
780
|
+
' - stage: discovered',
|
|
781
|
+
` date: ${today}`,
|
|
782
|
+
' note: "Created from opportunity scan"',
|
|
783
|
+
'---',
|
|
784
|
+
'',
|
|
785
|
+
`# ${sourceFm.program || slug} -- Funding Lifecycle`,
|
|
786
|
+
'',
|
|
787
|
+
`## Current Stage: Discovered`,
|
|
788
|
+
'',
|
|
789
|
+
`Promoted from [[opportunity-bank/${sourceRef}]] on ${today}.`,
|
|
790
|
+
'',
|
|
791
|
+
].join('\n');
|
|
792
|
+
|
|
793
|
+
fs.writeFileSync(path.join(fundDir, 'STATUS.md'), statusContent, 'utf-8');
|
|
794
|
+
|
|
795
|
+
// Build metadata.yaml
|
|
796
|
+
const metadataContent = [
|
|
797
|
+
`funder: ${sourceFm.funder || 'Unknown'}`,
|
|
798
|
+
`program: ${sourceFm.program || slug}`,
|
|
799
|
+
`amount_floor: ${sourceFm.amount_floor || 0}`,
|
|
800
|
+
`amount_ceiling: ${sourceFm.amount_ceiling || 0}`,
|
|
801
|
+
`deadline: ${sourceFm.deadline || 'null'}`,
|
|
802
|
+
`source_url: ${sourceFm.source_url || 'null'}`,
|
|
803
|
+
`created: ${today}`,
|
|
804
|
+
].join('\n') + '\n';
|
|
805
|
+
|
|
806
|
+
fs.writeFileSync(path.join(fundDir, 'metadata.yaml'), metadataContent, 'utf-8');
|
|
807
|
+
|
|
808
|
+
return { created: true, path: fundDir, slug };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Update a funding entry's stage.
|
|
813
|
+
* Validates sequential stage transition (no skipping, no backward).
|
|
814
|
+
*
|
|
815
|
+
* @param {string} roomDir - Path to room directory
|
|
816
|
+
* @param {string} slug - Funding entry slug
|
|
817
|
+
* @param {string} newStage - Target stage
|
|
818
|
+
* @param {string} [note] - Transition note
|
|
819
|
+
* @returns {{ updated: boolean, previousStage?: string, newStage?: string, error?: string }}
|
|
820
|
+
*/
|
|
821
|
+
function updateFundingStage(roomDir, slug, newStage, note) {
|
|
822
|
+
const resolved = path.resolve(roomDir);
|
|
823
|
+
const statusPath = path.join(resolved, 'funding', slug, 'STATUS.md');
|
|
824
|
+
|
|
825
|
+
let content;
|
|
826
|
+
try {
|
|
827
|
+
content = fs.readFileSync(statusPath, 'utf-8');
|
|
828
|
+
} catch (_e) {
|
|
829
|
+
return { updated: false, error: `Funding entry not found: ${slug}` };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const fm = parseFundingStatus(content);
|
|
833
|
+
const currentStage = fm.stage;
|
|
834
|
+
|
|
835
|
+
// Validate stage transition
|
|
836
|
+
const currentIdx = FUNDING_STAGES.indexOf(currentStage);
|
|
837
|
+
const newIdx = FUNDING_STAGES.indexOf(newStage);
|
|
838
|
+
|
|
839
|
+
if (newIdx === -1) {
|
|
840
|
+
return { updated: false, error: `Invalid stage: ${newStage}. Valid stages: ${FUNDING_STAGES.join(', ')}` };
|
|
841
|
+
}
|
|
842
|
+
if (currentIdx === -1) {
|
|
843
|
+
return { updated: false, error: `Current stage unknown: ${currentStage}` };
|
|
844
|
+
}
|
|
845
|
+
if (newIdx !== currentIdx + 1) {
|
|
846
|
+
return { updated: false, error: `Cannot transition from ${currentStage} to ${newStage}. Next valid stage: ${FUNDING_STAGES[currentIdx + 1] || 'none (already at final stage)'}` };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const today = new Date().toISOString().split('T')[0];
|
|
850
|
+
const transitionNote = note || `Advanced to ${newStage}`;
|
|
851
|
+
|
|
852
|
+
// Update frontmatter in content
|
|
853
|
+
// Replace stage line
|
|
854
|
+
content = content.replace(/^stage:\s*\S+/m, `stage: ${newStage}`);
|
|
855
|
+
// Replace last_updated line
|
|
856
|
+
content = content.replace(/^last_updated:\s*\S+/m, `last_updated: ${today}`);
|
|
857
|
+
|
|
858
|
+
// Append to transition_history (insert before the closing ---)
|
|
859
|
+
const historyEntry = ` - stage: ${newStage}\n date: ${today}\n note: "${transitionNote}"`;
|
|
860
|
+
|
|
861
|
+
// Find the end of frontmatter and insert before it
|
|
862
|
+
const fmEndMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
863
|
+
if (fmEndMatch) {
|
|
864
|
+
const fmBody = fmEndMatch[1];
|
|
865
|
+
const updatedFmBody = fmBody + '\n' + historyEntry;
|
|
866
|
+
content = content.replace(fmEndMatch[0], `---\n${updatedFmBody}\n---`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Update body heading
|
|
870
|
+
const stageTitle = newStage.charAt(0).toUpperCase() + newStage.slice(1);
|
|
871
|
+
content = content.replace(/## Current Stage:\s*\S+/, `## Current Stage: ${stageTitle}`);
|
|
872
|
+
|
|
873
|
+
fs.writeFileSync(statusPath, content, 'utf-8');
|
|
874
|
+
|
|
875
|
+
return { updated: true, previousStage: currentStage, newStage };
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Set the outcome attribute on a funding entry.
|
|
880
|
+
* Outcome is separate from stage per CONTEXT.md decision.
|
|
881
|
+
*
|
|
882
|
+
* @param {string} roomDir - Path to room directory
|
|
883
|
+
* @param {string} slug - Funding entry slug
|
|
884
|
+
* @param {string} outcome - One of: awarded, rejected, withdrawn
|
|
885
|
+
* @returns {{ set: boolean, outcome?: string, error?: string }}
|
|
886
|
+
*/
|
|
887
|
+
function setFundingOutcome(roomDir, slug, outcome) {
|
|
888
|
+
const resolved = path.resolve(roomDir);
|
|
889
|
+
const statusPath = path.join(resolved, 'funding', slug, 'STATUS.md');
|
|
890
|
+
|
|
891
|
+
if (!VALID_OUTCOMES.includes(outcome)) {
|
|
892
|
+
return { set: false, error: `Invalid outcome: ${outcome}. Valid: ${VALID_OUTCOMES.join(', ')}` };
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
let content;
|
|
896
|
+
try {
|
|
897
|
+
content = fs.readFileSync(statusPath, 'utf-8');
|
|
898
|
+
} catch (_e) {
|
|
899
|
+
return { set: false, error: `Funding entry not found: ${slug}` };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const fm = parseFundingStatus(content);
|
|
903
|
+
|
|
904
|
+
// 'awarded' and 'rejected' only valid after Submitted (or any stage for withdrawn)
|
|
905
|
+
if (outcome !== 'withdrawn') {
|
|
906
|
+
if (fm.stage !== 'submitted') {
|
|
907
|
+
return { set: false, error: `Outcome '${outcome}' can only be set at 'submitted' stage (current: ${fm.stage}). Use 'withdrawn' to exit at any stage.` };
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const today = new Date().toISOString().split('T')[0];
|
|
912
|
+
|
|
913
|
+
// Update outcome in frontmatter
|
|
914
|
+
content = content.replace(/^outcome:\s*\S+/m, `outcome: ${outcome}`);
|
|
915
|
+
content = content.replace(/^last_updated:\s*\S+/m, `last_updated: ${today}`);
|
|
916
|
+
|
|
917
|
+
fs.writeFileSync(statusPath, content, 'utf-8');
|
|
918
|
+
|
|
919
|
+
return { set: true, outcome };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Compute and write funding/STATE.md from all funding entries.
|
|
924
|
+
* Aggregates pipeline: count by stage, upcoming deadlines, stale entries.
|
|
925
|
+
*
|
|
926
|
+
* @param {string} roomDir - Path to room directory
|
|
927
|
+
* @returns {{ total: number, by_stage: Object, upcoming_deadlines: Array, stale_entries: Array }}
|
|
928
|
+
*/
|
|
929
|
+
function computeFundingState(roomDir) {
|
|
930
|
+
const resolved = path.resolve(roomDir);
|
|
931
|
+
const fundDir = path.join(resolved, 'funding');
|
|
932
|
+
|
|
933
|
+
if (!fs.existsSync(fundDir)) {
|
|
934
|
+
fs.mkdirSync(fundDir, { recursive: true });
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const { entries, count } = listFunding(roomDir);
|
|
938
|
+
|
|
939
|
+
const byStage = {};
|
|
940
|
+
for (const stage of FUNDING_STAGES) byStage[stage] = 0;
|
|
941
|
+
const upcomingDeadlines = [];
|
|
942
|
+
const staleEntries = [];
|
|
943
|
+
const today = new Date();
|
|
944
|
+
const staleThreshold = 14; // days
|
|
945
|
+
|
|
946
|
+
for (const entry of entries) {
|
|
947
|
+
if (entry.stage && byStage[entry.stage] !== undefined) {
|
|
948
|
+
byStage[entry.stage]++;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Track upcoming deadlines
|
|
952
|
+
if (entry.deadline) {
|
|
953
|
+
const dl = new Date(entry.deadline);
|
|
954
|
+
if (dl > today) {
|
|
955
|
+
upcomingDeadlines.push({ name: entry.name, deadline: entry.deadline, stage: entry.stage });
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Check staleness by reading last_updated from STATUS.md
|
|
960
|
+
const statusPath = path.join(fundDir, entry.name, 'STATUS.md');
|
|
961
|
+
try {
|
|
962
|
+
const content = fs.readFileSync(statusPath, 'utf-8');
|
|
963
|
+
const fm = parseFundingStatus(content);
|
|
964
|
+
if (fm.last_updated) {
|
|
965
|
+
const lastUpdate = new Date(fm.last_updated);
|
|
966
|
+
const daysSince = Math.floor((today - lastUpdate) / (1000 * 60 * 60 * 24));
|
|
967
|
+
if (daysSince > staleThreshold) {
|
|
968
|
+
staleEntries.push({ name: entry.name, stage: entry.stage, days_since_update: daysSince });
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
} catch (_e) { /* skip */ }
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Sort deadlines by date ascending
|
|
975
|
+
upcomingDeadlines.sort((a, b) => new Date(a.deadline) - new Date(b.deadline));
|
|
976
|
+
|
|
977
|
+
// Build STATE.md content
|
|
978
|
+
const todayStr = today.toISOString().split('T')[0];
|
|
979
|
+
const stateLines = [
|
|
980
|
+
'---',
|
|
981
|
+
'section: funding',
|
|
982
|
+
`last_computed: ${todayStr}`,
|
|
983
|
+
`total_entries: ${count}`,
|
|
984
|
+
'---',
|
|
985
|
+
'',
|
|
986
|
+
'# Funding Pipeline',
|
|
987
|
+
'',
|
|
988
|
+
'## Pipeline Summary',
|
|
989
|
+
'',
|
|
990
|
+
`Total entries: ${count}`,
|
|
991
|
+
'',
|
|
992
|
+
'| Stage | Count |',
|
|
993
|
+
'|-------|-------|',
|
|
994
|
+
];
|
|
995
|
+
|
|
996
|
+
for (const stage of FUNDING_STAGES) {
|
|
997
|
+
stateLines.push(`| ${stage} | ${byStage[stage]} |`);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (upcomingDeadlines.length > 0) {
|
|
1001
|
+
stateLines.push('', '## Upcoming Deadlines', '');
|
|
1002
|
+
for (const d of upcomingDeadlines) {
|
|
1003
|
+
stateLines.push(`- **${d.deadline}** -- ${d.name} (${d.stage})`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (staleEntries.length > 0) {
|
|
1008
|
+
stateLines.push('', '## Needs Attention (stale > 14 days)', '');
|
|
1009
|
+
for (const s of staleEntries) {
|
|
1010
|
+
stateLines.push(`- **${s.name}** -- ${s.days_since_update} days since update (${s.stage})`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
stateLines.push('');
|
|
1015
|
+
|
|
1016
|
+
fs.writeFileSync(path.join(fundDir, 'STATE.md'), stateLines.join('\n'), 'utf-8');
|
|
1017
|
+
|
|
1018
|
+
return { total: count, by_stage: byStage, upcoming_deadlines: upcomingDeadlines, stale_entries: staleEntries };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Compute and write opportunity-bank/STATE.md from all opportunity artifacts.
|
|
1023
|
+
* Aggregates: total, count by status, recent additions, top by relevance.
|
|
1024
|
+
*
|
|
1025
|
+
* @param {string} roomDir - Path to room directory
|
|
1026
|
+
* @returns {{ total: number, by_status: Object, recent: Array, top_relevance: Array }}
|
|
1027
|
+
*/
|
|
1028
|
+
function computeOpportunityBankState(roomDir) {
|
|
1029
|
+
const resolved = path.resolve(roomDir);
|
|
1030
|
+
const oppDir = path.join(resolved, 'opportunity-bank');
|
|
1031
|
+
|
|
1032
|
+
if (!fs.existsSync(oppDir)) {
|
|
1033
|
+
fs.mkdirSync(oppDir, { recursive: true });
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const { opportunities, count } = listOpportunities(roomDir);
|
|
1037
|
+
|
|
1038
|
+
const byStatus = {};
|
|
1039
|
+
const recent = [];
|
|
1040
|
+
const topRelevance = [];
|
|
1041
|
+
|
|
1042
|
+
for (const opp of opportunities) {
|
|
1043
|
+
const status = opp.status || 'unknown';
|
|
1044
|
+
byStatus[status] = (byStatus[status] || 0) + 1;
|
|
1045
|
+
|
|
1046
|
+
// Read created date from frontmatter
|
|
1047
|
+
const filePath = path.join(oppDir, opp.filename);
|
|
1048
|
+
try {
|
|
1049
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1050
|
+
const fm = parseFrontmatter(content);
|
|
1051
|
+
if (fm.created) {
|
|
1052
|
+
recent.push({ filename: opp.filename, created: fm.created, funder: opp.funder });
|
|
1053
|
+
}
|
|
1054
|
+
} catch (_e) { /* skip */ }
|
|
1055
|
+
|
|
1056
|
+
if (opp.relevance_score != null) {
|
|
1057
|
+
topRelevance.push({ filename: opp.filename, relevance_score: opp.relevance_score, funder: opp.funder, program: opp.program });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Sort recent by date descending
|
|
1062
|
+
recent.sort((a, b) => (b.created || '').localeCompare(a.created || ''));
|
|
1063
|
+
// Sort top relevance descending
|
|
1064
|
+
topRelevance.sort((a, b) => (b.relevance_score || 0) - (a.relevance_score || 0));
|
|
1065
|
+
|
|
1066
|
+
// Build STATE.md
|
|
1067
|
+
const todayStr = new Date().toISOString().split('T')[0];
|
|
1068
|
+
const stateLines = [
|
|
1069
|
+
'---',
|
|
1070
|
+
'section: opportunity-bank',
|
|
1071
|
+
`last_computed: ${todayStr}`,
|
|
1072
|
+
`total_opportunities: ${count}`,
|
|
1073
|
+
'---',
|
|
1074
|
+
'',
|
|
1075
|
+
'# Opportunity Bank',
|
|
1076
|
+
'',
|
|
1077
|
+
`## Summary`,
|
|
1078
|
+
'',
|
|
1079
|
+
`Total opportunities: ${count}`,
|
|
1080
|
+
'',
|
|
1081
|
+
'| Status | Count |',
|
|
1082
|
+
'|--------|-------|',
|
|
1083
|
+
];
|
|
1084
|
+
|
|
1085
|
+
for (const [status, cnt] of Object.entries(byStatus)) {
|
|
1086
|
+
stateLines.push(`| ${status} | ${cnt} |`);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (recent.length > 0) {
|
|
1090
|
+
stateLines.push('', '## Recent Additions', '');
|
|
1091
|
+
for (const r of recent.slice(0, 5)) {
|
|
1092
|
+
stateLines.push(`- ${r.created} -- ${r.funder || r.filename}`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (topRelevance.length > 0) {
|
|
1097
|
+
stateLines.push('', '## Top by Relevance', '');
|
|
1098
|
+
for (const t of topRelevance.slice(0, 5)) {
|
|
1099
|
+
stateLines.push(`- **${t.relevance_score}** -- ${t.program || t.filename} (${t.funder || 'Unknown'})`);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
stateLines.push('');
|
|
1104
|
+
|
|
1105
|
+
fs.writeFileSync(path.join(oppDir, 'STATE.md'), stateLines.join('\n'), 'utf-8');
|
|
1106
|
+
|
|
1107
|
+
return { total: count, by_status: byStatus, recent: recent.slice(0, 5), top_relevance: topRelevance.slice(0, 5) };
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// ---------------------------------------------------------------------------
|
|
1111
|
+
// Bank an opportunity from the extraction engine (with dedup by problem hash)
|
|
1112
|
+
// ---------------------------------------------------------------------------
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Bank an opportunity to room/opportunity-bank/ with full schema YAML frontmatter.
|
|
1116
|
+
* Deduplicates by problem_hash: if a matching hash exists, appends evidence and
|
|
1117
|
+
* updates confidence if higher. Otherwise creates a new .md file.
|
|
1118
|
+
*
|
|
1119
|
+
* @param {string} roomDir - Absolute path to room directory
|
|
1120
|
+
* @param {Object} opportunity - Opportunity object with all OPPORTUNITY_SCHEMA_FIELDS
|
|
1121
|
+
* @returns {{ banked: boolean, updated: boolean, path: string, error?: string }}
|
|
1122
|
+
*/
|
|
1123
|
+
function bankOpportunity(roomDir, opportunity) {
|
|
1124
|
+
// Validate input
|
|
1125
|
+
if (!opportunity || typeof opportunity !== 'object') {
|
|
1126
|
+
return { banked: false, updated: false, path: '', error: 'Invalid opportunity object' };
|
|
1127
|
+
}
|
|
1128
|
+
if (!opportunity.problem) {
|
|
1129
|
+
return { banked: false, updated: false, path: '', error: 'Opportunity missing required field: problem' };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const resolved = path.resolve(roomDir);
|
|
1133
|
+
const oppDir = path.join(resolved, 'opportunity-bank');
|
|
1134
|
+
|
|
1135
|
+
// Create directory if needed
|
|
1136
|
+
if (!fs.existsSync(oppDir)) {
|
|
1137
|
+
fs.mkdirSync(oppDir, { recursive: true });
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const hash = opportunityHash(opportunity.problem);
|
|
1141
|
+
const hashPrefix = hash.slice(0, 8);
|
|
1142
|
+
|
|
1143
|
+
// DEDUP CHECK: scan existing files for matching problem_hash
|
|
1144
|
+
let existingPath = null;
|
|
1145
|
+
try {
|
|
1146
|
+
const files = fs.readdirSync(oppDir).filter(f => f.endsWith('.md') && f !== 'STATE.md');
|
|
1147
|
+
for (const filename of files) {
|
|
1148
|
+
const filePath = path.join(oppDir, filename);
|
|
1149
|
+
try {
|
|
1150
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
1151
|
+
const fm = parseFrontmatter(content);
|
|
1152
|
+
if (fm.problem_hash === hashPrefix) {
|
|
1153
|
+
existingPath = filePath;
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
} catch (_e) { /* skip unreadable files */ }
|
|
1157
|
+
}
|
|
1158
|
+
} catch (_e) { /* directory read error -- proceed to create */ }
|
|
1159
|
+
|
|
1160
|
+
if (existingPath) {
|
|
1161
|
+
// Dedup hit: append evidence and update confidence if higher
|
|
1162
|
+
try {
|
|
1163
|
+
let content = fs.readFileSync(existingPath, 'utf8');
|
|
1164
|
+
const fm = parseFrontmatter(content);
|
|
1165
|
+
|
|
1166
|
+
// Update confidence if new is higher
|
|
1167
|
+
const existingConf = parseFloat(fm.confidence) || 0;
|
|
1168
|
+
const newConf = parseFloat(opportunity.confidence) || 0;
|
|
1169
|
+
if (newConf > existingConf) {
|
|
1170
|
+
content = content.replace(
|
|
1171
|
+
/^confidence:\s*[\d.]+/m,
|
|
1172
|
+
`confidence: ${newConf}`
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Append new evidence to the Evidence section
|
|
1177
|
+
const newEvidence = opportunity.evidence || '';
|
|
1178
|
+
if (newEvidence) {
|
|
1179
|
+
const evidenceMarker = '## Evidence';
|
|
1180
|
+
const evidenceIdx = content.indexOf(evidenceMarker);
|
|
1181
|
+
if (evidenceIdx !== -1) {
|
|
1182
|
+
const insertPoint = evidenceIdx + evidenceMarker.length;
|
|
1183
|
+
const before = content.slice(0, insertPoint);
|
|
1184
|
+
const after = content.slice(insertPoint);
|
|
1185
|
+
content = before + `\n\n[${opportunity.source_framework || 'unknown'} @ ${opportunity.created || 'unknown'}]: ${newEvidence}` + after;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
fs.writeFileSync(existingPath, content, 'utf8');
|
|
1190
|
+
return { banked: false, updated: true, path: existingPath };
|
|
1191
|
+
} catch (_e) {
|
|
1192
|
+
return { banked: false, updated: false, path: existingPath, error: `Failed to update existing: ${_e.message}` };
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// No dedup match -- create new file
|
|
1197
|
+
const created = opportunity.created || new Date().toISOString().split('T')[0];
|
|
1198
|
+
const filename = `${created}-${hashPrefix}.md`;
|
|
1199
|
+
const filePath = path.join(oppDir, filename);
|
|
1200
|
+
|
|
1201
|
+
// Build YAML frontmatter with all schema fields
|
|
1202
|
+
const fmLines = [
|
|
1203
|
+
'---',
|
|
1204
|
+
`problem: "${(opportunity.problem || '').replace(/"/g, '\\"')}"`,
|
|
1205
|
+
`mirror_solution: "${(opportunity.mirror_solution || '').replace(/"/g, '\\"')}"`,
|
|
1206
|
+
`domain: "${(opportunity.domain || '').replace(/"/g, '\\"')}"`,
|
|
1207
|
+
`evidence: "${(opportunity.evidence || '').replace(/"/g, '\\"')}"`,
|
|
1208
|
+
`source_framework: "${opportunity.source_framework || 'unknown'}"`,
|
|
1209
|
+
`knight_position: "${opportunity.knight_position || 'uncertainty'}"`,
|
|
1210
|
+
`confidence: ${opportunity.confidence || 0}`,
|
|
1211
|
+
`created: "${created}"`,
|
|
1212
|
+
`status: "${opportunity.status || 'banked'}"`,
|
|
1213
|
+
`problem_hash: "${hashPrefix}"`,
|
|
1214
|
+
'---',
|
|
1215
|
+
];
|
|
1216
|
+
|
|
1217
|
+
// Build markdown body
|
|
1218
|
+
const bodyLines = [
|
|
1219
|
+
'',
|
|
1220
|
+
`# ${opportunity.problem || 'Opportunity'}`,
|
|
1221
|
+
'',
|
|
1222
|
+
'## Evidence',
|
|
1223
|
+
'',
|
|
1224
|
+
opportunity.evidence || '',
|
|
1225
|
+
'',
|
|
1226
|
+
'## Mirror Solution',
|
|
1227
|
+
'',
|
|
1228
|
+
opportunity.mirror_solution || '',
|
|
1229
|
+
'',
|
|
1230
|
+
];
|
|
1231
|
+
|
|
1232
|
+
const content = fmLines.join('\n') + '\n' + bodyLines.join('\n');
|
|
1233
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
1234
|
+
|
|
1235
|
+
// Index opportunity in SQLite graph (non-blocking -- graph failure must not break banking)
|
|
1236
|
+
try {
|
|
1237
|
+
graphOps.indexOpportunity(roomDir, opportunity).catch(() => {});
|
|
1238
|
+
} catch (_e) {
|
|
1239
|
+
// Graph indexing is enhancement, not requirement -- Tier 0 principle
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Brain enrichment: suggest validation steps (non-blocking, Tier 0)
|
|
1243
|
+
try {
|
|
1244
|
+
enrichOpportunity(roomDir, filePath, opportunity).catch(() => {});
|
|
1245
|
+
} catch (_e) {
|
|
1246
|
+
// Brain enrichment is optional -- Tier 0 principle
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
return { banked: true, updated: false, path: filePath };
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Filter opportunities by domain, knight position, and minimum confidence.
|
|
1254
|
+
* Reads full frontmatter from each opportunity file for filtering.
|
|
1255
|
+
*
|
|
1256
|
+
* @param {string} roomDir - Path to room directory
|
|
1257
|
+
* @param {{ domain?: string, knight?: string, minConfidence?: number|string }} filters - Filter criteria
|
|
1258
|
+
* @returns {{ opportunities: Array, count: number, filtered_from: number }}
|
|
1259
|
+
*/
|
|
1260
|
+
function filterOpportunities(roomDir, filters = {}) {
|
|
1261
|
+
const { opportunities } = listOpportunities(roomDir);
|
|
1262
|
+
const oppDir = path.join(path.resolve(roomDir), 'opportunity-bank');
|
|
1263
|
+
|
|
1264
|
+
// Enrich with full frontmatter for filtering
|
|
1265
|
+
const enriched = opportunities.map(opp => {
|
|
1266
|
+
try {
|
|
1267
|
+
const content = fs.readFileSync(path.join(oppDir, opp.filename), 'utf8');
|
|
1268
|
+
const fm = parseOpportunityFrontmatter(content);
|
|
1269
|
+
return { ...opp, ...fm };
|
|
1270
|
+
} catch (_e) {
|
|
1271
|
+
return opp;
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
let result = enriched;
|
|
1276
|
+
|
|
1277
|
+
if (filters.domain) {
|
|
1278
|
+
result = result.filter(o => o.domain && o.domain.toLowerCase().includes(filters.domain.toLowerCase()));
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
if (filters.knight) {
|
|
1282
|
+
result = result.filter(o => o.knight_position === filters.knight);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (filters.minConfidence != null) {
|
|
1286
|
+
const min = parseFloat(filters.minConfidence);
|
|
1287
|
+
result = result.filter(o => o.confidence != null && parseFloat(o.confidence) >= min);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
return { opportunities: result, count: result.length, filtered_from: enriched.length };
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* Enrich a banked opportunity with Brain validation step suggestions.
|
|
1295
|
+
* Reads the opportunity file, queries Brain for framework chains, and appends
|
|
1296
|
+
* a "## Suggested Validation" section to the file.
|
|
1297
|
+
*
|
|
1298
|
+
* Graceful degradation: if Brain is unavailable or returns no steps, does nothing.
|
|
1299
|
+
*
|
|
1300
|
+
* @param {string} roomDir - Absolute path to room directory
|
|
1301
|
+
* @param {string} oppFilePath - Absolute path to the opportunity .md file
|
|
1302
|
+
* @param {Object} opportunity - Opportunity object with problem, domain, knight_position
|
|
1303
|
+
* @returns {Promise<{ enriched: boolean, steps: number, error?: string }>}
|
|
1304
|
+
*/
|
|
1305
|
+
async function enrichOpportunity(roomDir, oppFilePath, opportunity) {
|
|
1306
|
+
try {
|
|
1307
|
+
// Check Brain availability first (fast -- no network call)
|
|
1308
|
+
if (!brain.isAvailable()) {
|
|
1309
|
+
return { enriched: false, steps: 0 };
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const result = await brain.suggestValidationSteps(opportunity);
|
|
1313
|
+
if (!result || !result.steps || result.steps.length === 0) {
|
|
1314
|
+
return { enriched: false, steps: 0 };
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Build the validation section markdown
|
|
1318
|
+
const lines = [
|
|
1319
|
+
'',
|
|
1320
|
+
'## Suggested Validation',
|
|
1321
|
+
'',
|
|
1322
|
+
`_Source: Brain framework chains (${result.chain_source})_`,
|
|
1323
|
+
'',
|
|
1324
|
+
];
|
|
1325
|
+
|
|
1326
|
+
for (const step of result.steps) {
|
|
1327
|
+
lines.push(`${step.order}. **${step.framework}** -- ${step.reason}`);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
lines.push('');
|
|
1331
|
+
|
|
1332
|
+
// Read existing file and check if already enriched
|
|
1333
|
+
const existing = fs.readFileSync(oppFilePath, 'utf8');
|
|
1334
|
+
if (existing.includes('## Suggested Validation')) {
|
|
1335
|
+
// Already enriched -- skip to avoid duplicates
|
|
1336
|
+
return { enriched: false, steps: 0 };
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Append validation section
|
|
1340
|
+
fs.appendFileSync(oppFilePath, lines.join('\n'), 'utf8');
|
|
1341
|
+
|
|
1342
|
+
return { enriched: true, steps: result.steps.length };
|
|
1343
|
+
} catch (err) {
|
|
1344
|
+
return { enriched: false, steps: 0, error: err.message };
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
module.exports = {
|
|
1349
|
+
listOpportunities,
|
|
1350
|
+
listFunding,
|
|
1351
|
+
parseOpportunityFrontmatter,
|
|
1352
|
+
parseFundingStatus,
|
|
1353
|
+
getOpportunityBankState,
|
|
1354
|
+
getFundingState,
|
|
1355
|
+
buildGrantQuery,
|
|
1356
|
+
searchGrantsGov,
|
|
1357
|
+
searchSimplerGrants,
|
|
1358
|
+
scanOpportunities,
|
|
1359
|
+
fileOpportunity,
|
|
1360
|
+
rejectOpportunity,
|
|
1361
|
+
createFunding,
|
|
1362
|
+
updateFundingStage,
|
|
1363
|
+
setFundingOutcome,
|
|
1364
|
+
computeFundingState,
|
|
1365
|
+
computeOpportunityBankState,
|
|
1366
|
+
bankOpportunity,
|
|
1367
|
+
enrichOpportunity,
|
|
1368
|
+
filterOpportunities,
|
|
1369
|
+
FUNDING_STAGES,
|
|
1370
|
+
VALID_OUTCOMES,
|
|
1371
|
+
};
|