heyspex 0.1.0 → 0.2.0
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/README.md +21 -0
- package/bin/hs.mjs +931 -98
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1085 -103
- package/dist/cli/hs-claim.mjs +936 -63
- package/dist/cli/hs-connect.mjs +9786 -1910
- package/dist/cli/hs-defer.mjs +5267 -3053
- package/dist/cli/hs-dev.mjs +564 -0
- package/dist/cli/hs-env.mjs +385 -299
- package/dist/cli/hs-index-rebuild.mjs +7129 -0
- package/dist/cli/hs-index-reconcile.mjs +7852 -0
- package/dist/cli/hs-init.mjs +777 -138
- package/dist/cli/hs-install.mjs +6583 -485
- package/dist/cli/hs-join.mjs +10137 -2368
- package/dist/cli/hs-vault-pull.mjs +4659 -5882
- package/dist/cli/hs-vault.mjs +3793 -1537
- package/dist/cli/hs-workspace.mjs +2550 -524
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +231 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +62 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
- package/plugins/heyspex/hooks/hooks.json +361 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
- package/scripts/claude-hooks/_run.mjs +120 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +449 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
- package/scripts/cursor-hooks/manifest.mjs +235 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +268 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +333 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +270 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +197 -60
- package/scripts/lib/customer-kit.mjs +886 -0
- package/scripts/lib/dev-mode.mjs +244 -28
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +11 -21
- package/scripts/lib/hs-source-push.mjs +107 -58
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +441 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +194 -0
- package/spexharness/src/intent-directive.mjs +541 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +819 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +95 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
- package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
- package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
- package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
- package/vendor/heyspex-mcp/cli.mjs +201 -15524
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
- package/vendor/heyspex-mcp/console-transport.mjs +166 -0
- package/vendor/heyspex-mcp/credentials.mjs +584 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
- package/vendor/heyspex-mcp/http-transport.mjs +300 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +906 -21
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
- package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +660 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +29 -9
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +59 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +108 -0
- package/vendor/heyspex-mcp/session-tools.mjs +152 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +301 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +724 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +91 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
// spexharness/src/doctrine/vault-source.mjs
|
|
2
|
+
//
|
|
3
|
+
// DR5 (epic [[HS-KNHZ-8CMW-EPIC-spexcode-doctrine-v0-the-foundational]], task
|
|
4
|
+
// [[HS-7Z9E-8CMW-TASK-dr5-vault-knowledge-as-a-doctrine]]) — the Doctrine learns from
|
|
5
|
+
// the project's own CURATED knowledge, not only from session transcripts: owner
|
|
6
|
+
// rulings (memory/decisions), lessons, agent-facing protocol enforcement rules,
|
|
7
|
+
// accepted ADRs, the north star / operating model / principles index, the naughty
|
|
8
|
+
// list, this app repo's own always-loaded CLAUDE.md contract, and UI-pattern taste.
|
|
9
|
+
//
|
|
10
|
+
// Every extracted rule is emitted at a SEEDED confidence (never routed through the
|
|
11
|
+
// `research_synthesis` seat — see synthesize.mjs's `source !== 'vault'` filter and
|
|
12
|
+
// merge.mjs's {@link import('./merge.mjs').seedVaultRulesAsPrinciples}): the owner's
|
|
13
|
+
// own written word does not need an LLM to re-derive it, and re-synthesizing it would
|
|
14
|
+
// risk drifting the text away from what was actually decided.
|
|
15
|
+
//
|
|
16
|
+
// Supersession rules (survey table, [[HS-7Z9E-8CMW-TASK-dr5-vault-knowledge-as-a-doctrine]]):
|
|
17
|
+
// - A file whose frontmatter `status` is `superseded` or `closed` is skipped
|
|
18
|
+
// entirely — zero observations.
|
|
19
|
+
// - A body carrying a `> [!amendment]` or `> [!deprecated]` callout (regardless of
|
|
20
|
+
// `status`, e.g. a still-`open` decision the owner later reversed in-body) uses
|
|
21
|
+
// the amendment/deprecation text as the CURRENT rule and drops the original
|
|
22
|
+
// paragraph — the callout is what is actually still true.
|
|
23
|
+
//
|
|
24
|
+
// Full-recompute per run (the vault is git-tracked and small), with a per-file mtime
|
|
25
|
+
// cache ({@link loadVaultCache}/{@link saveVaultCache}, `.heyspex/doctrine/vault-cache.json`)
|
|
26
|
+
// so an unchanged file's content is never re-read — see {@link extractWithCache}.
|
|
27
|
+
// Every observation's `text` passes through `redact.mjs` before it is ever returned.
|
|
28
|
+
|
|
29
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
30
|
+
import { dirname, join } from 'node:path';
|
|
31
|
+
|
|
32
|
+
import yaml from 'js-yaml';
|
|
33
|
+
|
|
34
|
+
import { redactText } from '../redact.mjs';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {{
|
|
38
|
+
* id: string,
|
|
39
|
+
* source: 'vault',
|
|
40
|
+
* set: string,
|
|
41
|
+
* kind: 'vault-rule',
|
|
42
|
+
* stance: 'do'|'avoid',
|
|
43
|
+
* seedConfidence: number,
|
|
44
|
+
* text: string,
|
|
45
|
+
* path: string,
|
|
46
|
+
* updated: string|null,
|
|
47
|
+
* }} VaultObservation
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
// ── paths ─────────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/** The sibling vault repo checkout — see reference memory "the-spec-vault-lives-in-the-sibling". */
|
|
53
|
+
export function defaultVaultRoot(repoRoot) {
|
|
54
|
+
return join(repoRoot, '..', 'heyspex-specs');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function defaultVaultSpecsDir(vaultRoot) {
|
|
58
|
+
return join(vaultRoot, 'specs');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function defaultVaultCachePath(repoRoot) {
|
|
62
|
+
return join(repoRoot, '.heyspex', 'doctrine', 'vault-cache.json');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── frontmatter + markdown helpers ──────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
68
|
+
|
|
69
|
+
/** Split a vault file's raw text into `{ data, body }`. Tolerant of missing/corrupt frontmatter. */
|
|
70
|
+
export function parseFrontmatter(raw) {
|
|
71
|
+
const text = String(raw ?? '').replace(/\r\n/g, '\n');
|
|
72
|
+
const m = FRONTMATTER_RE.exec(text);
|
|
73
|
+
if (!m) return { data: {}, body: text };
|
|
74
|
+
let data = {};
|
|
75
|
+
try {
|
|
76
|
+
const parsed = yaml.load(m[1]);
|
|
77
|
+
if (parsed && typeof parsed === 'object') data = parsed;
|
|
78
|
+
} catch {
|
|
79
|
+
// corrupt frontmatter — treat as no frontmatter, body is the whole (unparsed) file
|
|
80
|
+
}
|
|
81
|
+
return { data, body: text.slice(m[0].length) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A file whose frontmatter `status` marks it retired — skip entirely, per the module header. */
|
|
85
|
+
export function isSuperseded(fm) {
|
|
86
|
+
const status = typeof fm?.status === 'string' ? fm.status.toLowerCase() : '';
|
|
87
|
+
return status === 'superseded' || status === 'closed';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A `> [!amendment] ...` / `> [!deprecated] ...` callout anywhere in the body — captures
|
|
92
|
+
* the marker's own same-line text plus every following `>`-prefixed line, joined into
|
|
93
|
+
* one string. `null` when no such callout is present.
|
|
94
|
+
*/
|
|
95
|
+
export function extractAmendmentCallout(body) {
|
|
96
|
+
const re = /^>\s*\[!(?:amendment|deprecated)\]\s?([^\n]*)\n((?:>.*(?:\n|$))*)/im;
|
|
97
|
+
const m = re.exec(body ?? '');
|
|
98
|
+
if (!m) return null;
|
|
99
|
+
const firstLine = m[1].trim();
|
|
100
|
+
const restLines = m[2]
|
|
101
|
+
.split('\n')
|
|
102
|
+
.map((l) => l.replace(/^>\s?/, '').trim())
|
|
103
|
+
.filter(Boolean);
|
|
104
|
+
const all = [firstLine, ...restLines].filter(Boolean);
|
|
105
|
+
const joined = all.join(' ').trim();
|
|
106
|
+
return joined || null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** First non-empty, non-heading-only paragraph of a body — skips a `# Title` line AND any leading `##`+ section heading with no prose of its own (e.g. a body opening straight into `## Lesson\n\n<content>` with no summary). */
|
|
110
|
+
export function firstParagraph(body) {
|
|
111
|
+
const paras = String(body ?? '')
|
|
112
|
+
.split(/\n{2,}/)
|
|
113
|
+
.map((p) => p.trim())
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
for (const p of paras) {
|
|
116
|
+
if (/^#{1,6}\s+/.test(p)) continue;
|
|
117
|
+
return p;
|
|
118
|
+
}
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Content of a `## <heading>` section (case-insensitive, exact heading text), up to the next `##`/`---`/EOF. */
|
|
123
|
+
export function extractHeadingSection(body, headingName) {
|
|
124
|
+
const re = new RegExp(`^##\\s+${headingName}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|\\n---\\s*$|$)`, 'im');
|
|
125
|
+
const m = re.exec(body ?? '');
|
|
126
|
+
return m ? m[1].trim() : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The **How to apply** block, either as a `## How to apply` / `## The general shape`
|
|
131
|
+
* heading section, or (the decisions-set shape) an inline `**How to apply:** ...`
|
|
132
|
+
* paragraph. `null` when neither is present.
|
|
133
|
+
*/
|
|
134
|
+
export function findHowToApplyBlock(body) {
|
|
135
|
+
const heading = extractHeadingSection(body, '(?:How to apply|The general shape)');
|
|
136
|
+
if (heading) return heading;
|
|
137
|
+
const paras = String(body ?? '').split(/\n{2,}/);
|
|
138
|
+
for (const p of paras) {
|
|
139
|
+
const m = /^\*\*How to apply:?\*\*\s*([\s\S]*)$/i.exec(p.trim());
|
|
140
|
+
if (m && m[1].trim()) return m[1].trim();
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** `fm.summary` when present, else the body's first paragraph. */
|
|
146
|
+
function openingSpan(fm, body) {
|
|
147
|
+
const summary = typeof fm?.summary === 'string' ? fm.summary.trim() : '';
|
|
148
|
+
return summary || firstParagraph(body);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Shared decisions/lessons text builder: opening span (+ How to apply block when present), OR the amendment text when the body carries one. */
|
|
152
|
+
function extractRulingSpan(fm, body) {
|
|
153
|
+
const amendment = extractAmendmentCallout(body);
|
|
154
|
+
if (amendment) return amendment;
|
|
155
|
+
const opening = openingSpan(fm, body);
|
|
156
|
+
const block = findHowToApplyBlock(body);
|
|
157
|
+
if (!opening && !block) return null;
|
|
158
|
+
return block ? `${opening}\n\n${block}`.trim() : opening;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** `##`-level sections of a document, in order, `{ heading, content }`. Nested (`###`+) headings stay inside their parent's `content`. */
|
|
162
|
+
export function splitTopHeadingSections(body) {
|
|
163
|
+
const text = String(body ?? '');
|
|
164
|
+
const re = /^##[ \t]+(.+?)\s*$/gm;
|
|
165
|
+
const matches = [...text.matchAll(re)];
|
|
166
|
+
const sections = [];
|
|
167
|
+
for (let i = 0; i < matches.length; i++) {
|
|
168
|
+
const heading = matches[i][1].trim();
|
|
169
|
+
const start = matches[i].index + matches[i][0].length;
|
|
170
|
+
const end = i + 1 < matches.length ? matches[i + 1].index : text.length;
|
|
171
|
+
sections.push({ heading, content: text.slice(start, end).trim() });
|
|
172
|
+
}
|
|
173
|
+
return sections;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Content between a named heading (any level 1-6, matched by `namePattern`) and the next heading of the same-or-shallower level. `null` when not found. */
|
|
177
|
+
export function extractNamedSection(raw, namePattern) {
|
|
178
|
+
const lines = String(raw ?? '').split('\n');
|
|
179
|
+
const headingRe = /^(#{1,6})\s+(.*)$/;
|
|
180
|
+
let start = -1;
|
|
181
|
+
let level = 0;
|
|
182
|
+
for (let i = 0; i < lines.length; i++) {
|
|
183
|
+
const m = headingRe.exec(lines[i]);
|
|
184
|
+
if (m && namePattern.test(m[2])) {
|
|
185
|
+
start = i + 1;
|
|
186
|
+
level = m[1].length;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (start === -1) return null;
|
|
191
|
+
let end = lines.length;
|
|
192
|
+
for (let i = start; i < lines.length; i++) {
|
|
193
|
+
const m = headingRe.exec(lines[i]);
|
|
194
|
+
if (m && m[1].length <= level) {
|
|
195
|
+
end = i;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return lines.slice(start, end).join('\n').trim();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Top-level (non-indented) `- ` bullets within a block of text, in order. */
|
|
203
|
+
function topLevelBullets(block) {
|
|
204
|
+
if (!block) return [];
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const line of block.split('\n')) {
|
|
207
|
+
const m = /^-\s+(.+)$/.exec(line);
|
|
208
|
+
if (m && m[1].trim()) out.push(m[1].trim());
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function specIdFor(fm, filePath) {
|
|
214
|
+
if (typeof fm?.id === 'string' && fm.id.trim()) return fm.id.trim();
|
|
215
|
+
const base = filePath.split(/[\\/]/).pop() ?? filePath;
|
|
216
|
+
return base.replace(/\.md$/i, '');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function listMarkdownFiles(dir) {
|
|
220
|
+
let entries;
|
|
221
|
+
try {
|
|
222
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
223
|
+
} catch {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
return entries
|
|
227
|
+
.filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.md'))
|
|
228
|
+
.map((e) => join(dir, e.name))
|
|
229
|
+
.sort();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── per-set extractors — each returns zero or more `{ setId, stance, seedConfidence, text }` (unredacted) ──
|
|
233
|
+
|
|
234
|
+
const SET = {
|
|
235
|
+
DECISIONS: 'decisions',
|
|
236
|
+
LESSONS: 'lessons',
|
|
237
|
+
PROTOCOLS_ENFORCEMENT: 'protocols-enforcement',
|
|
238
|
+
PROTOCOLS: 'protocols',
|
|
239
|
+
ADR: 'adr',
|
|
240
|
+
PRINCIPLES: 'principles',
|
|
241
|
+
NAUGHTY_LIST: 'naughty-list',
|
|
242
|
+
CLAUDE_MD: 'claude-md',
|
|
243
|
+
UI_PATTERNS: 'ui-patterns',
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
function extractDecisionFile(filePath, raw) {
|
|
247
|
+
const { data: fm, body } = parseFrontmatter(raw);
|
|
248
|
+
if (isSuperseded(fm)) return [];
|
|
249
|
+
const text = extractRulingSpan(fm, body);
|
|
250
|
+
if (!text) return [];
|
|
251
|
+
return [
|
|
252
|
+
{
|
|
253
|
+
id: `vault:${specIdFor(fm, filePath)}`,
|
|
254
|
+
set: SET.DECISIONS,
|
|
255
|
+
stance: 'do',
|
|
256
|
+
seedConfidence: 0.9,
|
|
257
|
+
text,
|
|
258
|
+
updated: fm?.updated ?? fm?.created ?? null,
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function extractLessonFile(filePath, raw) {
|
|
264
|
+
const { data: fm, body } = parseFrontmatter(raw);
|
|
265
|
+
if (isSuperseded(fm)) return [];
|
|
266
|
+
const text = extractRulingSpan(fm, body);
|
|
267
|
+
if (!text) return [];
|
|
268
|
+
return [
|
|
269
|
+
{
|
|
270
|
+
id: `vault:${specIdFor(fm, filePath)}`,
|
|
271
|
+
set: SET.LESSONS,
|
|
272
|
+
stance: 'do',
|
|
273
|
+
seedConfidence: 0.8,
|
|
274
|
+
text,
|
|
275
|
+
updated: fm?.updated ?? fm?.created ?? null,
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function extractProtocolFile(filePath, raw) {
|
|
281
|
+
const { data: fm, body } = parseFrontmatter(raw);
|
|
282
|
+
if (isSuperseded(fm)) return [];
|
|
283
|
+
const specId = specIdFor(fm, filePath);
|
|
284
|
+
const enforcement = Array.isArray(fm?.enforcement) ? fm.enforcement : [];
|
|
285
|
+
const injectRules = enforcement.filter((e) => e && typeof e.inject === 'string' && e.inject.trim());
|
|
286
|
+
|
|
287
|
+
if (injectRules.length) {
|
|
288
|
+
return injectRules.map((entry, i) => ({
|
|
289
|
+
id: `vault:${specId}#${entry.rule ?? i}`,
|
|
290
|
+
set: SET.PROTOCOLS_ENFORCEMENT,
|
|
291
|
+
stance: 'do',
|
|
292
|
+
seedConfidence: 0.9,
|
|
293
|
+
text: entry.inject.trim(),
|
|
294
|
+
updated: fm?.updated ?? fm?.created ?? null,
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const summary = typeof fm?.summary === 'string' ? fm.summary.trim() : '';
|
|
299
|
+
if (!summary) return [];
|
|
300
|
+
return [
|
|
301
|
+
{
|
|
302
|
+
id: `vault:${specId}`,
|
|
303
|
+
set: SET.PROTOCOLS,
|
|
304
|
+
stance: 'do',
|
|
305
|
+
seedConfidence: 0.6,
|
|
306
|
+
text: summary,
|
|
307
|
+
updated: fm?.updated ?? fm?.created ?? null,
|
|
308
|
+
},
|
|
309
|
+
];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function extractAdrFile(filePath, raw) {
|
|
313
|
+
const { data: fm, body } = parseFrontmatter(raw);
|
|
314
|
+
const status = typeof fm?.status === 'string' ? fm.status.toLowerCase() : '';
|
|
315
|
+
if (status !== 'accepted') return [];
|
|
316
|
+
const decision = extractHeadingSection(body, 'Decision');
|
|
317
|
+
const text = decision || firstParagraph(body);
|
|
318
|
+
if (!text) return [];
|
|
319
|
+
return [
|
|
320
|
+
{
|
|
321
|
+
id: `vault:${specIdFor(fm, filePath)}`,
|
|
322
|
+
set: SET.ADR,
|
|
323
|
+
stance: 'do',
|
|
324
|
+
seedConfidence: 0.85,
|
|
325
|
+
text: text.slice(0, 2000),
|
|
326
|
+
updated: fm?.updated ?? fm?.created ?? null,
|
|
327
|
+
},
|
|
328
|
+
];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const BOILERPLATE_HEADING_RE = /^(related specs?|change log|sources?)$/i;
|
|
332
|
+
const LIST_ITEM_RE = /^(?:\d+\.|[-*])\s+\*\*/;
|
|
333
|
+
|
|
334
|
+
function extractPrinciplesFile(filePath, raw) {
|
|
335
|
+
const { data: fm, body } = parseFrontmatter(raw);
|
|
336
|
+
if (isSuperseded(fm)) return [];
|
|
337
|
+
const specId = specIdFor(fm, filePath);
|
|
338
|
+
const sections = splitTopHeadingSections(body).filter((s) => !BOILERPLATE_HEADING_RE.test(s.heading));
|
|
339
|
+
|
|
340
|
+
const out = [];
|
|
341
|
+
let n = 0;
|
|
342
|
+
for (const section of sections) {
|
|
343
|
+
const paras = section.content
|
|
344
|
+
.split(/\n{2,}/)
|
|
345
|
+
.map((p) => p.trim())
|
|
346
|
+
.filter(Boolean);
|
|
347
|
+
const listItems = paras.filter((p) => LIST_ITEM_RE.test(p));
|
|
348
|
+
const texts = listItems.length ? listItems.map((p) => p.replace(/^(?:\d+\.|[-*])\s+/, '').trim()) : paras.length ? [paras[0]] : [];
|
|
349
|
+
for (const text of texts) {
|
|
350
|
+
n += 1;
|
|
351
|
+
out.push({
|
|
352
|
+
id: `vault:${specId}#${n}`,
|
|
353
|
+
set: SET.PRINCIPLES,
|
|
354
|
+
stance: 'do',
|
|
355
|
+
seedConfidence: 0.95,
|
|
356
|
+
text,
|
|
357
|
+
updated: fm?.updated ?? fm?.created ?? null,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const TABLE_ROW_RE = /^\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*$/;
|
|
365
|
+
|
|
366
|
+
function extractNaughtyListFile(filePath, raw) {
|
|
367
|
+
const { data: fm, body } = parseFrontmatter(raw);
|
|
368
|
+
const specId = specIdFor(fm, filePath);
|
|
369
|
+
const sections = splitTopHeadingSections(body).filter((s) => /^banned/i.test(s.heading));
|
|
370
|
+
|
|
371
|
+
const out = [];
|
|
372
|
+
let n = 0;
|
|
373
|
+
for (const section of sections) {
|
|
374
|
+
for (const line of section.content.split('\n')) {
|
|
375
|
+
const m = TABLE_ROW_RE.exec(line.trim());
|
|
376
|
+
if (!m) continue;
|
|
377
|
+
const term = m[1].replace(/\*\*/g, '').trim();
|
|
378
|
+
if (/^-+$/.test(term) || /^banned term$/i.test(term)) continue; // header/separator row
|
|
379
|
+
const why = m[2].replace(/\*\*/g, '').trim();
|
|
380
|
+
const useInstead = m[3].replace(/\*\*/g, '').trim();
|
|
381
|
+
n += 1;
|
|
382
|
+
out.push({
|
|
383
|
+
id: `vault:${specId}#${n}`,
|
|
384
|
+
set: SET.NAUGHTY_LIST,
|
|
385
|
+
stance: 'avoid',
|
|
386
|
+
seedConfidence: 0.95,
|
|
387
|
+
text: `Avoid "${term}" (${why}) — use ${useInstead} instead.`,
|
|
388
|
+
updated: fm?.updated ?? fm?.created ?? null,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function extractUiPatternFile(filePath, raw) {
|
|
396
|
+
const { data: fm, body } = parseFrontmatter(raw);
|
|
397
|
+
if (isSuperseded(fm)) return [];
|
|
398
|
+
const pattern = extractHeadingSection(body, 'Pattern');
|
|
399
|
+
const whenToApply = extractHeadingSection(body, 'When to apply');
|
|
400
|
+
const text = [pattern, whenToApply].filter(Boolean).join('\n\n').trim();
|
|
401
|
+
if (!text) return [];
|
|
402
|
+
return [
|
|
403
|
+
{
|
|
404
|
+
id: `vault:${specIdFor(fm, filePath)}`,
|
|
405
|
+
set: SET.UI_PATTERNS,
|
|
406
|
+
stance: 'do',
|
|
407
|
+
seedConfidence: 0.75,
|
|
408
|
+
text,
|
|
409
|
+
updated: fm?.updated ?? fm?.created ?? null,
|
|
410
|
+
},
|
|
411
|
+
];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const CLAUDE_MD_SECTIONS = [/session start/i, /agent claim contract/i, /^conventions$/i];
|
|
415
|
+
|
|
416
|
+
function extractClaudeMdFile(filePath, raw) {
|
|
417
|
+
const out = [];
|
|
418
|
+
let n = 0;
|
|
419
|
+
for (const pattern of CLAUDE_MD_SECTIONS) {
|
|
420
|
+
const section = extractNamedSection(raw, pattern);
|
|
421
|
+
for (const bullet of topLevelBullets(section)) {
|
|
422
|
+
n += 1;
|
|
423
|
+
out.push({
|
|
424
|
+
id: `vault:claude-md#${n}`,
|
|
425
|
+
set: SET.CLAUDE_MD,
|
|
426
|
+
stance: 'do',
|
|
427
|
+
seedConfidence: 0.95,
|
|
428
|
+
text: bullet,
|
|
429
|
+
updated: null,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ── mtime cache ──────────────────────────────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
export function loadVaultCache(path) {
|
|
439
|
+
if (!existsSync(path)) return {};
|
|
440
|
+
try {
|
|
441
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
442
|
+
return parsed && typeof parsed === 'object' && parsed.files && typeof parsed.files === 'object' ? parsed.files : {};
|
|
443
|
+
} catch {
|
|
444
|
+
return {};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function saveVaultCache(path, files) {
|
|
449
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
450
|
+
writeFileSync(path, `${JSON.stringify({ version: 1, files }, null, 2)}\n`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function statMtime(filePath) {
|
|
454
|
+
try {
|
|
455
|
+
return statSync(filePath).mtimeMs;
|
|
456
|
+
} catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Extract `filePath` via `extractor(raw, filePath)`, reusing `cache[filePath]` when its
|
|
463
|
+
* `mtimeMs` still matches the file on disk — the file is never re-read in that case.
|
|
464
|
+
* Always writes an entry into `newCache` for every file that still exists, so a file
|
|
465
|
+
* removed from the vault since the last run naturally drops out of the cache.
|
|
466
|
+
*/
|
|
467
|
+
function extractWithCache(filePath, cache, newCache, extractor, stats) {
|
|
468
|
+
const mtimeMs = statMtime(filePath);
|
|
469
|
+
if (mtimeMs === null) return [];
|
|
470
|
+
const prior = cache[filePath];
|
|
471
|
+
if (prior && prior.mtimeMs === mtimeMs) {
|
|
472
|
+
newCache[filePath] = prior;
|
|
473
|
+
stats.filesCached += 1;
|
|
474
|
+
return prior.observations;
|
|
475
|
+
}
|
|
476
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
477
|
+
const observations = (extractor(filePath, raw) ?? []).map((o) => ({ ...o, path: filePath }));
|
|
478
|
+
newCache[filePath] = { mtimeMs, observations };
|
|
479
|
+
stats.filesRead += 1;
|
|
480
|
+
return observations;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ── top-level entrypoint ────────────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Extract every vault-sourced Doctrine observation.
|
|
487
|
+
*
|
|
488
|
+
* @param {{
|
|
489
|
+
* vaultRoot?: string,
|
|
490
|
+
* repoRoot?: string,
|
|
491
|
+
* cache?: object,
|
|
492
|
+
* redact?: (text: string) => string,
|
|
493
|
+
* }} [opts]
|
|
494
|
+
* @returns {{
|
|
495
|
+
* observations: VaultObservation[],
|
|
496
|
+
* cache: object,
|
|
497
|
+
* countsBySet: Record<string, number>,
|
|
498
|
+
* stats: { filesRead: number, filesCached: number },
|
|
499
|
+
* }}
|
|
500
|
+
*/
|
|
501
|
+
export function extractVaultObservations({ vaultRoot, repoRoot = process.cwd(), cache = {}, redact = redactText } = {}) {
|
|
502
|
+
const resolvedVaultRoot = vaultRoot ?? defaultVaultRoot(repoRoot);
|
|
503
|
+
const specsDir = defaultVaultSpecsDir(resolvedVaultRoot);
|
|
504
|
+
const newCache = {};
|
|
505
|
+
const stats = { filesRead: 0, filesCached: 0 };
|
|
506
|
+
const rawObservations = [];
|
|
507
|
+
|
|
508
|
+
const run = (files, extractor) => {
|
|
509
|
+
for (const f of files) rawObservations.push(...extractWithCache(f, cache, newCache, extractor, stats));
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
run(listMarkdownFiles(join(specsDir, 'memory', 'decisions')), extractDecisionFile);
|
|
513
|
+
run(listMarkdownFiles(join(specsDir, 'memory', 'lessons')), extractLessonFile);
|
|
514
|
+
run(listMarkdownFiles(join(specsDir, 'protocols')), extractProtocolFile);
|
|
515
|
+
run(listMarkdownFiles(join(specsDir, 'adr')), extractAdrFile);
|
|
516
|
+
run(listMarkdownFiles(join(specsDir, 'memory', 'ui-patterns')), extractUiPatternFile);
|
|
517
|
+
|
|
518
|
+
for (const name of ['heyspex-north-star.md', 'heyspex-operating-model.md', 'principles-index.md']) {
|
|
519
|
+
run([join(specsDir, name)], extractPrinciplesFile);
|
|
520
|
+
}
|
|
521
|
+
run([join(specsDir, 'naughty-list.md')], extractNaughtyListFile);
|
|
522
|
+
run([join(repoRoot, 'CLAUDE.md')], extractClaudeMdFile);
|
|
523
|
+
|
|
524
|
+
const observations = rawObservations.map((o) => ({
|
|
525
|
+
id: o.id,
|
|
526
|
+
source: 'vault',
|
|
527
|
+
set: o.set,
|
|
528
|
+
kind: 'vault-rule',
|
|
529
|
+
stance: o.stance,
|
|
530
|
+
seedConfidence: o.seedConfidence,
|
|
531
|
+
text: redact(o.text),
|
|
532
|
+
path: o.path ?? '',
|
|
533
|
+
updated: o.updated ?? null,
|
|
534
|
+
}));
|
|
535
|
+
|
|
536
|
+
const countsBySet = {};
|
|
537
|
+
for (const o of observations) countsBySet[o.set] = (countsBySet[o.set] ?? 0) + 1;
|
|
538
|
+
|
|
539
|
+
return { observations, cache: newCache, countsBySet, stats };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ── category classification (used by merge.mjs — never the seat, see module header) ──
|
|
543
|
+
|
|
544
|
+
const CATEGORY_KEYWORDS = [
|
|
545
|
+
['safety-permissions', ['permission', 'destructive', 'safety', 'credential', 'secret', 'redact', 'guardrail', 'settings write', 'write-permission']],
|
|
546
|
+
['verification', ['verify', 'verification', 'acceptance', 'test', 'gate', 'proof', 'validate']],
|
|
547
|
+
['delegation', ['delegat', 'subagent', 'seat', 'routing', 'route ', 'dispatch', 'fleet', 'model council', 'assign']],
|
|
548
|
+
['research-grounding', ['research', 'ground', 'citation', 'evidence', 'cite', 'web search']],
|
|
549
|
+
['naming', ['naming', 'brand', 'terminology', 'vocabulary', 'glossary', 'name the']],
|
|
550
|
+
['communication', ['report', 'communicat', 'status update', 'concise', 'progress']],
|
|
551
|
+
['session-hygiene', ['session', 'commit', 'handoff', 'close-out', 'claim', 'hygiene', 'wip']],
|
|
552
|
+
];
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Category for a vault-rule observation — never the `research_synthesis` seat (vault
|
|
556
|
+
* observations are never sent to it, see synthesize.mjs). `naughty-list` and
|
|
557
|
+
* `ui-patterns` are fixed by set per the DR5 survey table; everything else is a
|
|
558
|
+
* keyword heuristic over the set's own protocol-domain vocabulary and the rule text,
|
|
559
|
+
* falling back to `session-hygiene`.
|
|
560
|
+
* @param {{ set: string, text: string }} observation
|
|
561
|
+
* @returns {string}
|
|
562
|
+
*/
|
|
563
|
+
export function classifyVaultCategory(observation) {
|
|
564
|
+
const set = observation?.set;
|
|
565
|
+
if (set === SET.NAUGHTY_LIST) return 'communication';
|
|
566
|
+
if (set === SET.UI_PATTERNS) return 'product-taste';
|
|
567
|
+
const haystack = String(observation?.text ?? '').toLowerCase();
|
|
568
|
+
for (const [category, keywords] of CATEGORY_KEYWORDS) {
|
|
569
|
+
if (keywords.some((kw) => haystack.includes(kw))) return category;
|
|
570
|
+
}
|
|
571
|
+
return 'session-hygiene';
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export { SET as VAULT_SETS };
|