heyspex 0.1.0 → 0.2.1
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/marketplace.json +14 -0
- package/README.md +21 -0
- package/bin/hs.mjs +1089 -104
- 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 +1127 -103
- package/dist/cli/hs-claim.mjs +981 -63
- package/dist/cli/hs-connect.mjs +10059 -1729
- package/dist/cli/hs-defer.mjs +5507 -3072
- package/dist/cli/hs-dev.mjs +576 -0
- package/dist/cli/hs-env.mjs +416 -302
- package/dist/cli/hs-index-rebuild.mjs +7334 -0
- package/dist/cli/hs-index-reconcile.mjs +8138 -0
- package/dist/cli/hs-init.mjs +2840 -149
- package/dist/cli/hs-install.mjs +7214 -594
- package/dist/cli/hs-join.mjs +10640 -2421
- package/dist/cli/hs-vault-pull.mjs +8157 -6370
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +4029 -1555
- package/dist/cli/hs-workspace.mjs +2702 -538
- 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 +30 -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 +28 -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 +33 -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 +32 -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/copilot-plugin-install.md +162 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +249 -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/README.md +309 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +80 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
- package/plugins/heyspex/hooks/hooks.json +418 -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 +456 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
- package/scripts/claude-hooks/_run.mjs +413 -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-execution-gate.mjs +51 -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 +577 -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/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -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 +86 -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 +279 -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 +451 -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/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
- package/scripts/cursor-hooks/manifest.mjs +281 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +288 -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 +496 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +272 -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 +199 -61
- package/scripts/lib/customer-kit.mjs +1203 -0
- package/scripts/lib/dev-mode.mjs +249 -32
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-branch.mjs +196 -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 +13 -22
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -307
- 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/lib/window-placement.mjs +109 -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 +449 -0
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -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/_shell-write-targets.mjs +77 -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 +275 -0
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -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/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +196 -0
- package/spexharness/src/intent-directive.mjs +626 -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 +849 -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 +96 -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 +77 -8
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
- package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
- package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
- package/vendor/heyspex-mcp/cli.mjs +207 -15524
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -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 +822 -17
- package/vendor/heyspex-mcp/console-transport.mjs +172 -0
- package/vendor/heyspex-mcp/credentials.mjs +585 -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 +131 -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 +723 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
- package/vendor/heyspex-mcp/http-transport.mjs +302 -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 +107 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -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 +908 -22
- 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 +327 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
- package/vendor/heyspex-mcp/promote-capture.mjs +388 -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 +666 -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 +191 -15
- 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 +83 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +625 -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/roadmap-default.mjs +70 -0
- 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 +109 -0
- package/vendor/heyspex-mcp/session-tools.mjs +162 -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 +453 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +304 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -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 +720 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +92 -0
- package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// spexharness/src/doctrine/inject.mjs
|
|
2
|
+
//
|
|
3
|
+
// DR3 (epic [[HS-KNHZ-8CMW-EPIC-spexcode-doctrine-v0-the-foundational]], task
|
|
4
|
+
// [[HS-X3S1-8CMW-TASK-dr3-doctrine-at-runtime-injected-into]]) — Doctrine AT RUNTIME. DR2
|
|
5
|
+
// (writer.mjs/merge.mjs/synthesize.mjs) produces the committed artifacts
|
|
6
|
+
// (spexharness/doctrine/SPEXCODE-DOCTRINE.json/.md); this module is the ONE seam every
|
|
7
|
+
// runtime surface (a SpexCode seat's bootstrap, the chat front's system message, the Claude
|
|
8
|
+
// Code plugin's SessionStart hook, the `/doctrine` built-in) reads that document through —
|
|
9
|
+
// none of them re-parses the JSON or re-implements the confidence-ordered, budget-capped
|
|
10
|
+
// rendering a second time.
|
|
11
|
+
//
|
|
12
|
+
// loadDoctrine() is CACHED (keyed by resolved path) and TOLERANT OF ABSENCE: a repo with no
|
|
13
|
+
// SPEXCODE-DOCTRINE.json yet (DR5's concurrent learner re-run notwithstanding — this reads
|
|
14
|
+
// whatever is on disk at call time, never bakes a snapshot into a test) returns `null` rather
|
|
15
|
+
// than throwing, and every caller downstream (renderDoctrineBlock) turns that into an
|
|
16
|
+
// explicit absence notice rather than silently injecting nothing. invalidateDoctrineCache()
|
|
17
|
+
// is the other half of that cache: `/doctrine refresh` calls it once `hs doctrine learn`
|
|
18
|
+
// finishes, so the NEXT read picks up the freshly written file instead of the stale cached
|
|
19
|
+
// one.
|
|
20
|
+
//
|
|
21
|
+
// renderDoctrineBlock() is measured against its OWN documented budget (DOCTRINE_MAX_TOKENS),
|
|
22
|
+
// deliberately separate from system-message.mjs's SYSTEM_MESSAGE_BYTE_CAP (2000 bytes,
|
|
23
|
+
// sent on every turn) — the Doctrine block rides the bootstrap injection (once per session,
|
|
24
|
+
// not per turn), so it gets real room; the system message only ever carries a one-line
|
|
25
|
+
// pointer to it (system-message.mjs's own `doctrine` option), never the body.
|
|
26
|
+
//
|
|
27
|
+
// recordDoctrineInjection() is the bridge to ../context-budget.mjs's ONE measurement path
|
|
28
|
+
// (recordContextInjection) — same pattern ../chat/system-message.mjs and
|
|
29
|
+
// ../gates/inject-vault-context.mjs already use, under `source: 'doctrine'`.
|
|
30
|
+
//
|
|
31
|
+
// layerProfileOverDoctrine() is Scope 5's pure helper: the SpexCode Profile layer (a later
|
|
32
|
+
// epic) sits ON TOP of Doctrine without silently overriding it — a profile entry with an
|
|
33
|
+
// APPROVED `supersedes` renders as a refinement; one without keeps the Doctrine line and is
|
|
34
|
+
// flagged, exactly the layering rule the doctrine writer's own DOCTRINE_HEADER documents
|
|
35
|
+
// (writer.mjs: "a per-developer layer on top, which may refine a principle here but may not
|
|
36
|
+
// silently contradict one").
|
|
37
|
+
//
|
|
38
|
+
// Pure aside from loadDoctrine's one fs.readFileSync — every other export here does no I/O.
|
|
39
|
+
|
|
40
|
+
import { readFileSync } from 'node:fs';
|
|
41
|
+
import path from 'node:path';
|
|
42
|
+
import { fileURLToPath } from 'node:url';
|
|
43
|
+
|
|
44
|
+
import { estimateTokens } from '../token-inventory.mjs';
|
|
45
|
+
import { recordContextInjection } from '../context-budget.mjs';
|
|
46
|
+
import { CATEGORY_ORDER, CATEGORY_LABELS } from './writer.mjs';
|
|
47
|
+
|
|
48
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
|
|
50
|
+
|
|
51
|
+
/** Default location of the committed Doctrine artifact, relative to a repo root. */
|
|
52
|
+
export function defaultDoctrinePath(repoRoot = REPO_ROOT) {
|
|
53
|
+
return path.join(repoRoot, 'spexharness', 'doctrine', 'SPEXCODE-DOCTRINE.json');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Audience: platform checkout vs customer repo (HS-A2PO, owner ruling 2026-10-03) ──────────
|
|
57
|
+
//
|
|
58
|
+
// "yes, inject-doctrine ships to customers. Baseline foundational doctrine on which user's
|
|
59
|
+
// doctrine will be built ontop." The full learned file (SPEXCODE-DOCTRINE.json, distilled from
|
|
60
|
+
// the owner's own sessions and full of platform rules) is NOT in the npm package; what ships is
|
|
61
|
+
// the curated BASELINE edition below. A session whose project is the package's own checkout
|
|
62
|
+
// (heyspex-app) keeps reading the learned file; every other project reads ONLY the baseline
|
|
63
|
+
// from the installed package — never a file under the customer's project, never the owner's
|
|
64
|
+
// learned set — and gets nothing at all (a clean no-op) when the baseline is absent.
|
|
65
|
+
|
|
66
|
+
/** The package root this module was loaded from (heyspex-app, or node_modules/heyspex). */
|
|
67
|
+
export const PACKAGE_ROOT = REPO_ROOT;
|
|
68
|
+
|
|
69
|
+
/** The shipped baseline edition, repo-relative (also listed in package.json `files`). */
|
|
70
|
+
export const BASELINE_DOCTRINE_REL = 'spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json';
|
|
71
|
+
|
|
72
|
+
/** Absolute path of the baseline edition inside a package root. */
|
|
73
|
+
export function baselineDoctrinePath(packageRoot = PACKAGE_ROOT) {
|
|
74
|
+
return path.join(packageRoot, ...BASELINE_DOCTRINE_REL.split('/'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function samePath(a, b) {
|
|
78
|
+
const norm = (p) => {
|
|
79
|
+
const s = path.resolve(p).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
80
|
+
return process.platform === 'win32' ? s.toLowerCase() : s;
|
|
81
|
+
};
|
|
82
|
+
return norm(a) === norm(b);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Which Doctrine a session in `projectRoot` reads. Pure (no fs).
|
|
87
|
+
* - project IS the package checkout → `platform`: the learned file under the project.
|
|
88
|
+
* - anything else → `customer`: the package's baseline edition only.
|
|
89
|
+
*
|
|
90
|
+
* @param {{ projectRoot: string, packageRoot?: string }} opts
|
|
91
|
+
* @returns {{ audience: 'platform'|'customer', path: string }}
|
|
92
|
+
*/
|
|
93
|
+
export function resolveDoctrineSource({ projectRoot, packageRoot = PACKAGE_ROOT }) {
|
|
94
|
+
if (projectRoot && samePath(projectRoot, packageRoot)) {
|
|
95
|
+
return { audience: 'platform', path: defaultDoctrinePath(projectRoot) };
|
|
96
|
+
}
|
|
97
|
+
return { audience: 'customer', path: baselineDoctrinePath(packageRoot) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The bootstrap-injection budget this module's own render function is held to — separate
|
|
102
|
+
* from, and much larger than, system-message.mjs's per-turn SYSTEM_MESSAGE_BYTE_CAP (see
|
|
103
|
+
* this file's own header). Chosen to comfortably hold every category's top principles
|
|
104
|
+
* without needing to be raised for the doctrine set this epic ships with; a caller with a
|
|
105
|
+
* tighter/looser seat budget passes its own `maxTokens` to {@link renderDoctrineBlock}.
|
|
106
|
+
*/
|
|
107
|
+
export const DOCTRINE_MAX_TOKENS = 800;
|
|
108
|
+
|
|
109
|
+
/** Shown (and injected) when no Doctrine document is readable at the resolved path. */
|
|
110
|
+
export const DOCTRINE_ABSENT_NOTICE =
|
|
111
|
+
'Doctrine: not yet learned — spexharness/doctrine/SPEXCODE-DOCTRINE.json is missing or empty. ' +
|
|
112
|
+
'Run `hs doctrine learn` (or `/doctrine refresh`) to generate it.';
|
|
113
|
+
|
|
114
|
+
/** loadDoctrine()'s cache: resolved path -> parsed doctrine (or `null` for absent/corrupt). */
|
|
115
|
+
const cache = new Map();
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Load the Doctrine JSON document, cached by resolved path. Tolerant of absence: a missing
|
|
119
|
+
* file, an unreadable one, or one that fails to parse as JSON all resolve to `null` rather
|
|
120
|
+
* than throwing — a runtime surface with no Doctrine yet must still start.
|
|
121
|
+
*
|
|
122
|
+
* @param {{ path?: string }} [opts] - `path` overrides the default
|
|
123
|
+
* `<repoRoot>/spexharness/doctrine/SPEXCODE-DOCTRINE.json` (tests point this at a fixture).
|
|
124
|
+
* @returns {object|null} the parsed document (writer.mjs's `buildDoctrineJson` shape), or null.
|
|
125
|
+
*/
|
|
126
|
+
export function loadDoctrine({ path: doctrinePath } = {}) {
|
|
127
|
+
const resolved = typeof doctrinePath === 'string' && doctrinePath ? doctrinePath : defaultDoctrinePath();
|
|
128
|
+
if (cache.has(resolved)) return cache.get(resolved);
|
|
129
|
+
let doctrine = null;
|
|
130
|
+
try {
|
|
131
|
+
doctrine = JSON.parse(readFileSync(resolved, 'utf8'));
|
|
132
|
+
} catch {
|
|
133
|
+
doctrine = null;
|
|
134
|
+
}
|
|
135
|
+
cache.set(resolved, doctrine);
|
|
136
|
+
return doctrine;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Invalidate loadDoctrine()'s cache — the whole thing, or just one resolved path. `/doctrine
|
|
141
|
+
* refresh` (builtins.mjs) calls this once `hs doctrine learn` finishes writing a fresh file,
|
|
142
|
+
* so the NEXT loadDoctrine() call re-reads instead of returning the stale cached document.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} [doctrinePath] - when given, only this path's cache entry is dropped.
|
|
145
|
+
*/
|
|
146
|
+
export function invalidateDoctrineCache(doctrinePath) {
|
|
147
|
+
if (typeof doctrinePath === 'string' && doctrinePath) cache.delete(doctrinePath);
|
|
148
|
+
else cache.clear();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** One principle rendered as a single line: `- [do] <text>` / `- [avoid] <text>`. */
|
|
152
|
+
function principleLine(p) {
|
|
153
|
+
const stance = p?.stance === 'avoid' ? 'avoid' : 'do';
|
|
154
|
+
return `- [${stance}] ${p?.text ?? ''}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Render the Doctrine document into the block every runtime surface injects: a header line
|
|
159
|
+
* (`learnedAt` + principle count — AC3's "/doctrine prints the document header + principle
|
|
160
|
+
* count + learnedAt" reads exactly this line), then the TOP principles by confidence,
|
|
161
|
+
* grouped by category (writer.mjs's own CATEGORY_ORDER/CATEGORY_LABELS — the same section
|
|
162
|
+
* order the committed Markdown document uses), one line each, kept within `maxTokens`.
|
|
163
|
+
*
|
|
164
|
+
* Absence (`doctrine` is null/has no principles) renders {@link DOCTRINE_ABSENT_NOTICE}
|
|
165
|
+
* instead — AC1's "absent (with a notice) when the file is missing".
|
|
166
|
+
*
|
|
167
|
+
* Budget: categories (and the principles inside them) are added in confidence order; the
|
|
168
|
+
* FIRST principle that would push the rendered text over `maxTokens` and everything ranked
|
|
169
|
+
* below it are dropped (a true tail cut, matching token-inventory.mjs's own budgeting
|
|
170
|
+
* convention) — `truncated: true` says so. A category with zero principles that fit is
|
|
171
|
+
* omitted entirely rather than rendered as an empty header.
|
|
172
|
+
*
|
|
173
|
+
* @param {object|null} doctrine - loadDoctrine()'s result.
|
|
174
|
+
* @param {{ maxTokens?: number }} [opts] - defaults to {@link DOCTRINE_MAX_TOKENS}.
|
|
175
|
+
* @returns {{ text: string, tokens: number, chars: number, principleCount: number,
|
|
176
|
+
* includedCount: number, learnedAt: string|null, truncated: boolean }}
|
|
177
|
+
*/
|
|
178
|
+
export function renderDoctrineBlock(doctrine, { maxTokens = DOCTRINE_MAX_TOKENS } = {}) {
|
|
179
|
+
const principles = Array.isArray(doctrine?.principles) ? doctrine.principles : [];
|
|
180
|
+
if (principles.length === 0) {
|
|
181
|
+
const text = DOCTRINE_ABSENT_NOTICE;
|
|
182
|
+
return { text, tokens: estimateTokens(text), chars: text.length, principleCount: 0, includedCount: 0, learnedAt: null, truncated: false };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const principleCount = principles.length;
|
|
186
|
+
const learnedAt = typeof doctrine?.learnedAt === 'string' ? doctrine.learnedAt : null;
|
|
187
|
+
const plural = `${principleCount} principle${principleCount === 1 ? '' : 's'}`;
|
|
188
|
+
const edition = typeof doctrine?.edition === 'string' && doctrine.edition ? doctrine.edition : null;
|
|
189
|
+
const header = edition
|
|
190
|
+
? `Doctrine (${edition} edition, ${plural}):`
|
|
191
|
+
: `Doctrine (learned ${learnedAt ?? 'unknown'}, ${plural}):`;
|
|
192
|
+
|
|
193
|
+
// Selection is GLOBAL confidence order (AC1's "top principles by confidence within the
|
|
194
|
+
// budget") — a true tail cut, same convention token-inventory.mjs's own budgeting uses:
|
|
195
|
+
// the first principle that would overflow, and every lower-confidence one after it, are
|
|
196
|
+
// dropped together, rather than letting a cheaper low-confidence line jump the queue.
|
|
197
|
+
const sorted = [...principles].sort((a, b) => (Number(b?.confidence) || 0) - (Number(a?.confidence) || 0));
|
|
198
|
+
|
|
199
|
+
let usedTokens = estimateTokens(header);
|
|
200
|
+
let truncated = false;
|
|
201
|
+
const includedCats = new Set();
|
|
202
|
+
const included = []; // { cat, line } in confidence order — display groups these by category.
|
|
203
|
+
|
|
204
|
+
for (const p of sorted) {
|
|
205
|
+
const cat = typeof p?.category === 'string' && p.category ? p.category : 'product-taste';
|
|
206
|
+
const line = principleLine(p);
|
|
207
|
+
const lineTokens = estimateTokens(line);
|
|
208
|
+
const needsCatHeader = !includedCats.has(cat);
|
|
209
|
+
const catHeaderTokens = needsCatHeader ? estimateTokens(`${CATEGORY_LABELS[cat] ?? cat}:`) : 0;
|
|
210
|
+
if (usedTokens + catHeaderTokens + lineTokens > maxTokens) {
|
|
211
|
+
truncated = true;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
usedTokens += catHeaderTokens + lineTokens;
|
|
215
|
+
includedCats.add(cat);
|
|
216
|
+
included.push({ cat, line });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Display grouping: the SELECTED principles, sectioned in writer.mjs's own canonical
|
|
220
|
+
// CATEGORY_ORDER (matching the committed Markdown document's section order), each section
|
|
221
|
+
// keeping the confidence order it was selected in.
|
|
222
|
+
const byCategory = new Map();
|
|
223
|
+
for (const item of included) {
|
|
224
|
+
if (!byCategory.has(item.cat)) byCategory.set(item.cat, []);
|
|
225
|
+
byCategory.get(item.cat).push(item.line);
|
|
226
|
+
}
|
|
227
|
+
const orderedCats = [
|
|
228
|
+
...CATEGORY_ORDER.filter((c) => byCategory.has(c)),
|
|
229
|
+
...[...byCategory.keys()].filter((c) => !CATEGORY_ORDER.includes(c)),
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
const lines = [header];
|
|
233
|
+
for (const cat of orderedCats) {
|
|
234
|
+
lines.push(`${CATEGORY_LABELS[cat] ?? cat}:`, ...byCategory.get(cat));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const text = lines.join('\n');
|
|
238
|
+
return {
|
|
239
|
+
text,
|
|
240
|
+
tokens: estimateTokens(text),
|
|
241
|
+
chars: text.length,
|
|
242
|
+
principleCount,
|
|
243
|
+
includedCount: included.length,
|
|
244
|
+
learnedAt,
|
|
245
|
+
truncated,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Record a rendered Doctrine block onto a session record through ../context-budget.mjs's ONE
|
|
251
|
+
* measurement path — `source: 'doctrine'`, same convention ../chat/system-message.mjs uses
|
|
252
|
+
* for its own piece. A no-op (returns null) when no `record` is supplied, exactly like
|
|
253
|
+
* buildSystemMessage's own optional `sessionRecord` — a caller with no session record yet
|
|
254
|
+
* still gets the block back from {@link renderDoctrineBlock} without erroring here.
|
|
255
|
+
*
|
|
256
|
+
* @param {ReturnType<import('../session-record.mjs').createSessionRecord>|null} record
|
|
257
|
+
* @param {{ text?: string, tokens?: number }|null} block - renderDoctrineBlock()'s result.
|
|
258
|
+
* @returns {import('../context-budget.mjs').ContextInjectionMeasurement|null}
|
|
259
|
+
*/
|
|
260
|
+
export function recordDoctrineInjection(record, block) {
|
|
261
|
+
if (!record || !block) return null;
|
|
262
|
+
return recordContextInjection(record, { source: 'doctrine', name: 'doctrine', text: block.text, tokens: block.tokens });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Scope 5's layering rule, as a pure helper: fold a Profile's entries over the Doctrine
|
|
267
|
+
* principles they target, WITHOUT the Profile module existing yet (it ships in a later
|
|
268
|
+
* epic — HS-HEBV per the task's own depends_on). Consumed today only by this module's own
|
|
269
|
+
* tests (fixtures stand in for real profile entries); `/profile` (the later epic) is the
|
|
270
|
+
* eventual real caller.
|
|
271
|
+
*
|
|
272
|
+
* A profile entry targets one Doctrine principle by exact `targetText` match against that
|
|
273
|
+
* principle's `text` (Doctrine principles carry no stable id today — matching on the
|
|
274
|
+
* authored text is the same identity DR2's own merge.mjs dedup uses). An entry with no
|
|
275
|
+
* matching principle is ignored (Profile-only additions are out of this task's scope).
|
|
276
|
+
*
|
|
277
|
+
* - entry.supersedes?.approved === true → the layered line is the PROFILE's own text,
|
|
278
|
+
* `source: 'profile-refines'`, carrying `refines` (the original Doctrine text) — "refines"
|
|
279
|
+
* the Scope 5 language.
|
|
280
|
+
* - anything else (no entry, entry present but not approved) → the Doctrine line is kept
|
|
281
|
+
* verbatim, `source: 'doctrine'`; a present-but-unapproved entry additionally sets
|
|
282
|
+
* `flagged: true` and `contradictedBy` — the `/profile` flag Scope 5 asks for.
|
|
283
|
+
*
|
|
284
|
+
* @param {object|null} doctrine - loadDoctrine()'s result.
|
|
285
|
+
* @param {Array<{ targetText: string, text?: string, stance?: 'do'|'avoid',
|
|
286
|
+
* supersedes?: { approved?: boolean } }>} [profileEntries]
|
|
287
|
+
* @returns {Array<{ category: string, stance: string, text: string,
|
|
288
|
+
* source: 'doctrine'|'profile-refines', flagged: boolean, refines?: string, contradictedBy?: string }>}
|
|
289
|
+
*/
|
|
290
|
+
export function layerProfileOverDoctrine(doctrine, profileEntries = []) {
|
|
291
|
+
const principles = Array.isArray(doctrine?.principles) ? doctrine.principles : [];
|
|
292
|
+
const entries = Array.isArray(profileEntries) ? profileEntries : [];
|
|
293
|
+
const byTarget = new Map();
|
|
294
|
+
for (const entry of entries) {
|
|
295
|
+
if (entry && typeof entry.targetText === 'string' && entry.targetText) byTarget.set(entry.targetText, entry);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return principles.map((p) => {
|
|
299
|
+
const entry = byTarget.get(p?.text);
|
|
300
|
+
if (!entry) {
|
|
301
|
+
return { category: p?.category ?? 'product-taste', stance: p?.stance ?? 'do', text: p?.text ?? '', source: 'doctrine', flagged: false };
|
|
302
|
+
}
|
|
303
|
+
const approved = entry?.supersedes?.approved === true;
|
|
304
|
+
if (approved) {
|
|
305
|
+
return {
|
|
306
|
+
category: p?.category ?? 'product-taste',
|
|
307
|
+
stance: entry.stance ?? p?.stance ?? 'do',
|
|
308
|
+
text: typeof entry.text === 'string' && entry.text ? entry.text : p?.text ?? '',
|
|
309
|
+
source: 'profile-refines',
|
|
310
|
+
refines: p?.text ?? '',
|
|
311
|
+
flagged: false,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
category: p?.category ?? 'product-taste',
|
|
316
|
+
stance: p?.stance ?? 'do',
|
|
317
|
+
text: p?.text ?? '',
|
|
318
|
+
source: 'doctrine',
|
|
319
|
+
flagged: true,
|
|
320
|
+
contradictedBy: typeof entry.text === 'string' ? entry.text : null,
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export default {
|
|
326
|
+
defaultDoctrinePath,
|
|
327
|
+
PACKAGE_ROOT,
|
|
328
|
+
BASELINE_DOCTRINE_REL,
|
|
329
|
+
baselineDoctrinePath,
|
|
330
|
+
resolveDoctrineSource,
|
|
331
|
+
DOCTRINE_MAX_TOKENS,
|
|
332
|
+
DOCTRINE_ABSENT_NOTICE,
|
|
333
|
+
loadDoctrine,
|
|
334
|
+
invalidateDoctrineCache,
|
|
335
|
+
renderDoctrineBlock,
|
|
336
|
+
recordDoctrineInjection,
|
|
337
|
+
layerProfileOverDoctrine,
|
|
338
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// spexharness/src/doctrine/merge.mjs
|
|
2
|
+
//
|
|
3
|
+
// DR2 (epic [[HS-KNHZ-8CMW-EPIC-spexcode-doctrine-v0-the-foundational]], task
|
|
4
|
+
// [[HS-YMPY-8CMW-TASK-dr2-doctrine-synthesis-a-routed]]) — pure merge of candidate
|
|
5
|
+
// principles (from synthesize.mjs's seat batches, plus the Command Code taste lines
|
|
6
|
+
// seeded in as principles) into the final set a Doctrine document is written from.
|
|
7
|
+
//
|
|
8
|
+
// DR5 ([[HS-7Z9E-8CMW-TASK-dr5-vault-knowledge-as-a-doctrine]]) adds a second seeded
|
|
9
|
+
// source: vault-source.mjs's curated vault rules (owner rulings, lessons, protocol
|
|
10
|
+
// enforcement, accepted ADRs, north star/operating model/principles-index, the
|
|
11
|
+
// naughty list, this repo's CLAUDE.md). Vault rules are seeded FIRST, ahead of the
|
|
12
|
+
// seat's synthesized candidates, so a matching session-derived principle dedupes INTO
|
|
13
|
+
// the vault rule (the vault rule's text stays canonical) and a contradicting one is
|
|
14
|
+
// headlined by the vault rule — the owner's written word wins over inferred behaviour.
|
|
15
|
+
//
|
|
16
|
+
// Pure: no I/O, no randomness, no clock reads except a caller-supplied `now` (unused
|
|
17
|
+
// today, reserved so a future "later ruling wins" tie-break can be date-aware without
|
|
18
|
+
// changing this module's shape).
|
|
19
|
+
//
|
|
20
|
+
// What this does, in order:
|
|
21
|
+
// 1. Dedupe near-identical principles (same category + stance, normalized-text
|
|
22
|
+
// similarity ≥ DEDUP_SIMILARITY_THRESHOLD) — union their evidence ids and raise
|
|
23
|
+
// confidence with each independent piece of evidence. The FIRST-seen candidate's
|
|
24
|
+
// text is kept as canonical; later matches only contribute evidence/confidence.
|
|
25
|
+
// 2. Detect a contradiction: same category, OPPOSITE stance, similarity above the
|
|
26
|
+
// same threshold (i.e. clearly the same topic). Neither side is dropped — both
|
|
27
|
+
// stay in `principles`, and the pair is recorded under `conflicts`. The headline
|
|
28
|
+
// is the vault-sourced side when either side is vault-sourced; otherwise the
|
|
29
|
+
// later-seen one (later ruling wins for the headline; both cited either way).
|
|
30
|
+
// 3. Seed the Command Code taste lines as principles at their own stated confidence,
|
|
31
|
+
// `evidence: ['taste:<...>']` (the observation id corpus.mjs already gives a
|
|
32
|
+
// taste line, which is itself prefixed `taste:` — see {@link seedTasteAsPrinciples}).
|
|
33
|
+
// 4. Seed vault-source.mjs's `vault-rule` observations as principles at their own
|
|
34
|
+
// `seedConfidence`, `evidence: [obs.id]` (already shaped `vault:<spec id>...`),
|
|
35
|
+
// category from {@link import('./vault-source.mjs').classifyVaultCategory} — see
|
|
36
|
+
// {@link seedVaultRulesAsPrinciples}.
|
|
37
|
+
|
|
38
|
+
import { classifyVaultCategory } from './vault-source.mjs';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {{
|
|
42
|
+
* text: string,
|
|
43
|
+
* category: string,
|
|
44
|
+
* confidence: number,
|
|
45
|
+
* evidence: string[],
|
|
46
|
+
* stance: 'do'|'avoid',
|
|
47
|
+
* supersedes?: string[],
|
|
48
|
+
* }} DoctrinePrinciple
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
const DEFAULT_CATEGORY = 'product-taste';
|
|
52
|
+
|
|
53
|
+
/** Lowercase, strip punctuation, collapse whitespace — the text dedup/contradiction compares on. */
|
|
54
|
+
export function normalizeForDedup(text) {
|
|
55
|
+
return String(text ?? '')
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
58
|
+
.replace(/\s+/g, ' ')
|
|
59
|
+
.trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Token-overlap (Jaccard) similarity of two principle texts, 0..1. Pure, no dependencies. */
|
|
63
|
+
export function textSimilarity(a, b) {
|
|
64
|
+
const ta = new Set(normalizeForDedup(a).split(' ').filter(Boolean));
|
|
65
|
+
const tb = new Set(normalizeForDedup(b).split(' ').filter(Boolean));
|
|
66
|
+
if (ta.size === 0 || tb.size === 0) return 0;
|
|
67
|
+
let intersection = 0;
|
|
68
|
+
for (const t of ta) if (tb.has(t)) intersection++;
|
|
69
|
+
const union = ta.size + tb.size - intersection;
|
|
70
|
+
return union === 0 ? 0 : intersection / union;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Similarity at/above this is "the same topic" for both dedup and contradiction detection. */
|
|
74
|
+
export const DEDUP_SIMILARITY_THRESHOLD = 0.5;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Seed the Command Code taste lines (corpus.mjs observations with `kind === 'taste'`)
|
|
78
|
+
* as candidate principles at their own stated confidence. `evidence` is the taste
|
|
79
|
+
* observation's own id (already shaped `taste:<project>:<session>:<seq>`), which both
|
|
80
|
+
* satisfies "cites an id that exists" and reads as `taste:<n>` per the task brief.
|
|
81
|
+
*
|
|
82
|
+
* @param {Array<{ id: string, text: string, confidence?: number }>} tasteObservations
|
|
83
|
+
* @returns {DoctrinePrinciple[]}
|
|
84
|
+
*/
|
|
85
|
+
export function seedTasteAsPrinciples(tasteObservations) {
|
|
86
|
+
return (tasteObservations ?? []).map((obs) => ({
|
|
87
|
+
text: obs.text,
|
|
88
|
+
category: DEFAULT_CATEGORY,
|
|
89
|
+
confidence: typeof obs.confidence === 'number' ? obs.confidence : 0.5,
|
|
90
|
+
evidence: [obs.id],
|
|
91
|
+
stance: 'do',
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Seed vault-source.mjs's `vault-rule` observations (owner decisions, lessons,
|
|
97
|
+
* protocol enforcement, accepted ADRs, north star/operating model/principles-index,
|
|
98
|
+
* the naughty list, CLAUDE.md) as candidate principles at their own `seedConfidence`.
|
|
99
|
+
* `evidence` is the observation's own id (already shaped `vault:<spec id>...`).
|
|
100
|
+
* `category` comes from {@link classifyVaultCategory} — never the seat (vault
|
|
101
|
+
* observations are never sent to it, see synthesize.mjs). Every seeded principle
|
|
102
|
+
* carries `vaultSourced: true` so {@link mergePrinciples} can headline it over a
|
|
103
|
+
* contradicting session-derived principle regardless of processing order.
|
|
104
|
+
*
|
|
105
|
+
* @param {Array<{ id: string, set: string, text: string, stance: 'do'|'avoid', seedConfidence: number }>} vaultObservations
|
|
106
|
+
* @returns {DoctrinePrinciple[]}
|
|
107
|
+
*/
|
|
108
|
+
export function seedVaultRulesAsPrinciples(vaultObservations) {
|
|
109
|
+
return (vaultObservations ?? [])
|
|
110
|
+
.filter((obs) => obs && obs.source === 'vault' && obs.kind === 'vault-rule')
|
|
111
|
+
.map((obs) => ({
|
|
112
|
+
text: obs.text,
|
|
113
|
+
category: classifyVaultCategory(obs),
|
|
114
|
+
confidence: typeof obs.seedConfidence === 'number' ? obs.seedConfidence : 0.5,
|
|
115
|
+
evidence: [obs.id],
|
|
116
|
+
stance: obs.stance === 'avoid' ? 'avoid' : 'do',
|
|
117
|
+
vaultSourced: true,
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Independent evidence nudges confidence up, asymptotic to 0.99 — never overridden downward by a merge. */
|
|
122
|
+
function raiseConfidence(baseConfidence, candidateConfidence) {
|
|
123
|
+
const bumped = baseConfidence + (1 - baseConfidence) * 0.15;
|
|
124
|
+
return Math.min(0.99, Math.max(bumped, candidateConfidence ?? baseConfidence));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function dedupeEvidence(evidence) {
|
|
128
|
+
return Array.from(new Set(evidence.filter((id) => typeof id === 'string' && id.length > 0)));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Merge candidate principles (freshly synthesized + any `existing` from a prior run,
|
|
133
|
+
* oldest first) into the final principle set, deduping, raising confidence, and
|
|
134
|
+
* recording contradictions.
|
|
135
|
+
*
|
|
136
|
+
* @param {DoctrinePrinciple[]} candidates
|
|
137
|
+
* @param {{ existing?: DoctrinePrinciple[] }} [opts] `existing` — principles from a
|
|
138
|
+
* prior incremental run, merged in FIRST (oldest), so a fresh candidate can still be
|
|
139
|
+
* detected as a contradiction of (and become the new headline over) one already on
|
|
140
|
+
* file.
|
|
141
|
+
* @returns {{ principles: DoctrinePrinciple[], conflicts: Array<{ category: string, a: string, b: string, headline: string }> }}
|
|
142
|
+
*/
|
|
143
|
+
export function mergePrinciples(candidates, { existing = [] } = {}) {
|
|
144
|
+
const ordered = [...existing, ...candidates];
|
|
145
|
+
const merged = [];
|
|
146
|
+
const conflicts = [];
|
|
147
|
+
|
|
148
|
+
for (const cand of ordered) {
|
|
149
|
+
if (!cand || typeof cand.text !== 'string' || !cand.text.trim()) continue;
|
|
150
|
+
|
|
151
|
+
const dupe = merged.find(
|
|
152
|
+
(m) => m.category === cand.category && m.stance === cand.stance && textSimilarity(m.text, cand.text) >= DEDUP_SIMILARITY_THRESHOLD,
|
|
153
|
+
);
|
|
154
|
+
if (dupe) {
|
|
155
|
+
dupe.evidence = dedupeEvidence([...dupe.evidence, ...(cand.evidence ?? [])]);
|
|
156
|
+
dupe.confidence = raiseConfidence(dupe.confidence, cand.confidence);
|
|
157
|
+
if (cand.vaultSourced) dupe.vaultSourced = true; // the vault-sourced flag propagates forward once true
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const opposite = merged.find(
|
|
162
|
+
(m) => m.category === cand.category && m.stance !== cand.stance && textSimilarity(m.text, cand.text) >= DEDUP_SIMILARITY_THRESHOLD,
|
|
163
|
+
);
|
|
164
|
+
if (opposite) {
|
|
165
|
+
// The owner's written word wins over inferred behaviour: when either side of a
|
|
166
|
+
// contradiction is vault-sourced, IT is the headline regardless of which was
|
|
167
|
+
// seen first; otherwise the later-seen candidate is the headline as before.
|
|
168
|
+
const headline = opposite.vaultSourced && !cand.vaultSourced ? opposite.text : cand.text;
|
|
169
|
+
conflicts.push({ category: cand.category, a: opposite.text, b: cand.text, headline });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
merged.push({
|
|
173
|
+
text: cand.text,
|
|
174
|
+
category: cand.category ?? DEFAULT_CATEGORY,
|
|
175
|
+
confidence: typeof cand.confidence === 'number' ? cand.confidence : 0.5,
|
|
176
|
+
evidence: dedupeEvidence(cand.evidence ?? []),
|
|
177
|
+
stance: cand.stance === 'avoid' ? 'avoid' : 'do',
|
|
178
|
+
...(Array.isArray(cand.supersedes) && cand.supersedes.length ? { supersedes: cand.supersedes.slice() } : {}),
|
|
179
|
+
...(cand.vaultSourced ? { vaultSourced: true } : {}),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { principles: merged, conflicts };
|
|
184
|
+
}
|