@geraldmaron/construct 1.4.2 → 1.5.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/.env.example +36 -0
- package/README.md +41 -7
- package/bin/construct +1027 -64
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/lib/acp/server.mjs +21 -7
- package/lib/adapters-sync.mjs +2 -1
- package/lib/audit-trail.mjs +185 -2
- package/lib/beads/auto-close.mjs +2 -1
- package/lib/beads/drift.mjs +2 -1
- package/lib/bridges/copilot-proxy.mjs +20 -5
- package/lib/certification/skill-inventory.mjs +10 -1
- package/lib/cli/approvals.mjs +147 -0
- package/lib/cli-commands.mjs +105 -14
- package/lib/comment-lint.mjs +127 -8
- package/lib/config/schema.mjs +6 -29
- package/lib/config/source-target-registry.mjs +70 -0
- package/lib/config/source-targets.mjs +179 -205
- package/lib/contracts/coverage.mjs +77 -0
- package/lib/contracts/validate.mjs +102 -13
- package/lib/contracts/violation-log.mjs +50 -4
- package/lib/db/migrate.mjs +69 -0
- package/lib/db/migrations/001_orchestration_runs.sql +9 -0
- package/lib/db/migrations/002_queue_provider.sql +50 -0
- package/lib/db/migrations/003_worker_registry.sql +17 -0
- package/lib/db/migrations/004_trace_events.sql +16 -0
- package/lib/db/migrations/005_shared_memory.sql +15 -0
- package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
- package/lib/decisions/enforced-baseline.json +0 -2
- package/lib/decisions/registry.mjs +3 -2
- package/lib/deployment/parity-contract.mjs +1 -1
- package/lib/deployment-mode.mjs +78 -2
- package/lib/diagram-export.mjs +10 -1
- package/lib/distill.mjs +5 -2
- package/lib/docs-verify.mjs +2 -1
- package/lib/doctor/cli.mjs +1 -0
- package/lib/doctor/command-on-path.mjs +24 -0
- package/lib/doctor/diagnosis.mjs +89 -0
- package/lib/doctor/embedding-health.mjs +50 -0
- package/lib/doctor/engine-health.mjs +93 -0
- package/lib/doctor/graph-validate.mjs +47 -0
- package/lib/doctor/index.mjs +18 -4
- package/lib/doctor/sidecar-providers.mjs +56 -0
- package/lib/doctor/watchers/consistency.mjs +93 -1
- package/lib/doctor/watchers/cx-budget.mjs +1 -1
- package/lib/doctor/watchers/graph-staleness.mjs +1 -1
- package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
- package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
- package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
- package/lib/doctor/watchers/provider-breaker.mjs +78 -0
- package/lib/document-export.mjs +52 -27
- package/lib/document-extract/docling-client.mjs +9 -5
- package/lib/document-extract/docling-sidecar.py +30 -0
- package/lib/document-extract.mjs +1 -1
- package/lib/document-ingest.mjs +9 -0
- package/lib/embed/approval-queue.mjs +184 -88
- package/lib/embed/authority-guard.mjs +65 -2
- package/lib/embed/capability-jobs.mjs +365 -0
- package/lib/embed/capability-lifecycle.mjs +219 -0
- package/lib/embed/capability-loader.mjs +303 -0
- package/lib/embed/capability-runtime.mjs +58 -0
- package/lib/embed/cli.mjs +185 -8
- package/lib/embed/config.mjs +4 -0
- package/lib/embed/daemon.mjs +125 -6
- package/lib/embed/demand-fetch.mjs +190 -54
- package/lib/embed/inbox.mjs +12 -10
- package/lib/embed/presets/ops-triage.mjs +236 -0
- package/lib/embed/presets/pm-feedback.mjs +341 -0
- package/lib/embed/presets/tpm.mjs +401 -0
- package/lib/embed/providers/jira.mjs +72 -1
- package/lib/embed/providers/registry.mjs +140 -33
- package/lib/embedded-contract/capability.mjs +4 -0
- package/lib/embedded-contract/execution.mjs +1 -1
- package/lib/embedded-contract/model-resolve.mjs +53 -3
- package/lib/embedded-contract/workflow-defs.mjs +48 -73
- package/lib/embedded-contract/workflow-invoke.mjs +144 -4
- package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
- package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
- package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
- package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
- package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
- package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
- package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
- package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
- package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
- package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
- package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
- package/lib/env-config.mjs +50 -9
- package/lib/extensions/index.mjs +27 -0
- package/lib/extensions/loader.mjs +120 -0
- package/lib/extensions/manifest-schema.mjs +141 -0
- package/lib/extensions/manifests/anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
- package/lib/extensions/manifests/directory.manifest.json +12 -0
- package/lib/extensions/manifests/docling.manifest.json +37 -0
- package/lib/extensions/manifests/echo.manifest.json +8 -0
- package/lib/extensions/manifests/feedback.manifest.json +12 -0
- package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
- package/lib/extensions/manifests/github.manifest.json +52 -0
- package/lib/extensions/manifests/linear.manifest.json +50 -0
- package/lib/extensions/manifests/local.manifest.json +12 -0
- package/lib/extensions/manifests/ollama.manifest.json +12 -0
- package/lib/extensions/manifests/openai.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter.manifest.json +12 -0
- package/lib/extensions/manifests/postgres.manifest.json +12 -0
- package/lib/extensions/manifests/salesforce.manifest.json +12 -0
- package/lib/extensions/manifests/slack.manifest.json +58 -0
- package/lib/extensions/manifests/whisper.manifest.json +36 -0
- package/lib/extensions/validate.mjs +175 -0
- package/lib/features.mjs +13 -9
- package/lib/flows/checkpoint.mjs +184 -0
- package/lib/flows/constants.mjs +27 -0
- package/lib/flows/define.mjs +146 -0
- package/lib/flows/engine.mjs +249 -0
- package/lib/flows/errors.mjs +19 -0
- package/lib/flows/index.mjs +27 -0
- package/lib/flows/joins.mjs +18 -0
- package/lib/flows/schema.mjs +90 -0
- package/lib/flows/state.mjs +31 -0
- package/lib/frameworks/loader.mjs +99 -0
- package/lib/frameworks/schema.mjs +157 -0
- package/lib/graph/build-from-corpus.mjs +67 -0
- package/lib/graph/build-from-embed.mjs +82 -0
- package/lib/graph/build-from-registry.mjs +164 -3
- package/lib/graph/cli.mjs +456 -12
- package/lib/graph/gap-queries.mjs +156 -0
- package/lib/graph/gaps.mjs +41 -0
- package/lib/graph/impacted.mjs +129 -0
- package/lib/graph/runtime-evidence.mjs +177 -0
- package/lib/graph/security-coverage.mjs +113 -0
- package/lib/graph/staleness.mjs +241 -10
- package/lib/graph/store.mjs +24 -7
- package/lib/graph/validate.mjs +161 -0
- package/lib/headhunt.mjs +9 -8
- package/lib/health-check.mjs +15 -7
- package/lib/hook-health.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +10 -4
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/graph-impact-advisory.mjs +2 -2
- package/lib/hooks/guard-bash.mjs +41 -15
- package/lib/hooks/mcp-health-check.mjs +11 -4
- package/lib/hooks/model-fallback.mjs +50 -6
- package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
- package/lib/hooks/pre-compact.mjs +181 -173
- package/lib/hooks/rule-verifier.mjs +2 -1
- package/lib/hooks/session-reflect.mjs +2 -1
- package/lib/hooks/session-start.mjs +3 -3
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +19 -7
- package/lib/identity.mjs +92 -0
- package/lib/ingest/degraded-extract.mjs +96 -0
- package/lib/ingest/docling-remote.mjs +2 -1
- package/lib/ingest/provider-extract.mjs +7 -19
- package/lib/ingest/sidecar-providers.mjs +196 -0
- package/lib/ingest-tooling.mjs +11 -5
- package/lib/init-unified.mjs +55 -38
- package/lib/init-update.mjs +2 -1
- package/lib/install/legacy-global-cleanup.mjs +25 -0
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/queue-registry.mjs +50 -0
- package/lib/intake/queue.mjs +133 -17
- package/lib/intake/session-prelude.mjs +57 -8
- package/lib/integrations/intake-integrations.mjs +61 -111
- package/lib/intent-classifier.mjs +43 -5
- package/lib/libreoffice-export.mjs +8 -8
- package/lib/logging/rotate.mjs +5 -4
- package/lib/mcp/broker.mjs +332 -17
- package/lib/mcp/denied-store.mjs +95 -0
- package/lib/mcp/destructive-gate.mjs +30 -0
- package/lib/mcp/dispatch-envelope.mjs +199 -0
- package/lib/mcp/memory-bridge.mjs +2 -1
- package/lib/mcp/server.mjs +154 -1411
- package/lib/mcp/tool-definitions-memory.mjs +376 -0
- package/lib/mcp/tool-definitions-project.mjs +271 -0
- package/lib/mcp/tool-definitions-skills.mjs +311 -0
- package/lib/mcp/tool-definitions-workflow.mjs +398 -0
- package/lib/mcp/tool-definitions.mjs +25 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +1 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +165 -25
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/provider-write.mjs +187 -0
- package/lib/mcp/tools/scope.mjs +0 -3
- package/lib/mcp/tools/skills.mjs +1 -1
- package/lib/mcp/tools/storage.mjs +0 -8
- package/lib/mcp/transport/auth.mjs +238 -0
- package/lib/mcp/transport/http.mjs +136 -0
- package/lib/mcp/transport/mode.mjs +31 -0
- package/lib/mcp/transport/stdio.mjs +22 -0
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +34 -23
- package/lib/mcp-platform-config.mjs +31 -7
- package/lib/mode-capabilities.mjs +109 -0
- package/lib/model-router.mjs +42 -9
- package/lib/models/catalog.mjs +40 -1
- package/lib/net-guard.mjs +247 -0
- package/lib/observation-store.mjs +6 -2
- package/lib/ollama/provision-context.mjs +2 -1
- package/lib/opencode-config.mjs +22 -3
- package/lib/opencode-runtime-plugin.mjs +120 -2
- package/lib/oracle/actions.mjs +204 -10
- package/lib/oracle/cli.mjs +24 -3
- package/lib/oracle/dispatch.mjs +2 -1
- package/lib/oracle/execute.mjs +2 -2
- package/lib/oracle/gaps.mjs +3 -3
- package/lib/oracle/heartbeat.mjs +53 -0
- package/lib/oracle/read-model.mjs +69 -13
- package/lib/oracle/reconcile.mjs +3 -46
- package/lib/oracle/routing.mjs +37 -31
- package/lib/oracle/synthesize.mjs +1 -1
- package/lib/orchestration/classification.mjs +434 -0
- package/lib/orchestration/delegation-flow.mjs +132 -0
- package/lib/orchestration/flow-selection.mjs +418 -0
- package/lib/orchestration/gates.mjs +244 -0
- package/lib/orchestration/host-sampling.mjs +121 -0
- package/lib/orchestration/policy-constants.mjs +48 -0
- package/lib/orchestration/provider-outcome.mjs +197 -0
- package/lib/orchestration/readiness.mjs +114 -13
- package/lib/orchestration/run-store-postgres.mjs +32 -26
- package/lib/orchestration/run-store-sqlite.mjs +8 -14
- package/lib/orchestration/run-store.mjs +25 -12
- package/lib/orchestration/runtime.mjs +446 -39
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +3 -2
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +528 -89
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/packs/cli.mjs +144 -0
- package/lib/packs/core-pack.mjs +112 -0
- package/lib/packs/enablement.mjs +187 -0
- package/lib/packs/index.mjs +23 -0
- package/lib/packs/loader.mjs +176 -0
- package/lib/packs/manifest-schema.mjs +58 -0
- package/lib/packs/prompts.mjs +120 -0
- package/lib/packs/validate.mjs +208 -0
- package/lib/plugin-registry.mjs +4 -5
- package/lib/policy/audit-gate.mjs +42 -0
- package/lib/policy/consumption-budget.mjs +149 -0
- package/lib/policy/engine.mjs +81 -6
- package/lib/policy/role-authority.mjs +89 -0
- package/lib/prompt-composer.js +19 -3
- package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
- package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
- package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
- package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
- package/lib/providers/contract/adapters/github/index.mjs +38 -3
- package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
- package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
- package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
- package/lib/providers/contract/adapters/jira/manifest.json +17 -0
- package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
- package/lib/providers/contract.mjs +207 -0
- package/lib/providers/credential-bootstrap.mjs +7 -4
- package/lib/providers/credential-sources.mjs +14 -2
- package/lib/providers/directory/index.mjs +261 -0
- package/lib/providers/feedback/index.mjs +392 -0
- package/lib/providers/filter-audit.mjs +68 -0
- package/lib/providers/filter-schema.mjs +54 -0
- package/lib/providers/github/index.mjs +31 -0
- package/lib/providers/instance-config.mjs +215 -0
- package/lib/providers/op-locate.mjs +96 -0
- package/lib/providers/op-run.mjs +64 -9
- package/lib/providers/registry.mjs +26 -3
- package/lib/providers/secret-audit-wiring.mjs +6 -0
- package/lib/providers/secret-resolver.mjs +240 -23
- package/lib/publish-template.mjs +6 -3
- package/lib/publish-tooling.mjs +4 -0
- package/lib/publish.mjs +32 -4
- package/lib/queue/pg-queue.mjs +395 -0
- package/lib/reflect.mjs +2 -1
- package/lib/registry/assemble.mjs +28 -2
- package/lib/registry/consolidation.mjs +8 -1
- package/lib/registry/custom-scaffold.mjs +200 -0
- package/lib/registry/custom-schema.mjs +137 -0
- package/lib/registry/loader.mjs +8 -3
- package/lib/registry/manifests/format-engines.default.json +15 -0
- package/lib/registry/manifests/surface-map.default.json +46 -0
- package/lib/registry/retired-paths.mjs +1 -0
- package/lib/registry/surface-map.mjs +100 -50
- package/lib/render-visual-check.mjs +240 -0
- package/lib/resources/budget.mjs +40 -17
- package/lib/roles/flavor-bindings.mjs +36 -14
- package/lib/roles/gateway.mjs +15 -8
- package/lib/roots.mjs +107 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -6
- package/lib/runtime/whisper-bootstrap.mjs +11 -3
- package/lib/runtime-env.mjs +5 -2
- package/lib/scheduler/index.mjs +8 -20
- package/lib/schema-infer.mjs +31 -2
- package/lib/scopes/lifecycle.mjs +29 -25
- package/lib/scopes/loader.mjs +49 -12
- package/lib/scopes/teams.mjs +1 -1
- package/lib/security/ingest-boundary.mjs +80 -0
- package/lib/security/recall-wrapper.mjs +99 -0
- package/lib/security/trust.mjs +132 -0
- package/lib/service-manager.mjs +38 -19
- package/lib/setup.mjs +41 -23
- package/lib/skills-apply.mjs +4 -2
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/state-root.mjs +147 -0
- package/lib/status.mjs +597 -6
- package/lib/storage/admin.mjs +77 -5
- package/lib/storage/backend-registry.mjs +73 -0
- package/lib/storage/backend.mjs +63 -9
- package/lib/storage/embeddings-openai.mjs +12 -2
- package/lib/storage/hybrid-query.mjs +11 -3
- package/lib/storage/retrieval-hardening.mjs +384 -0
- package/lib/storage/shared-memory.mjs +158 -0
- package/lib/storage/vector-client.mjs +69 -2
- package/lib/team/health.mjs +72 -0
- package/lib/telemetry/backends/local.mjs +9 -3
- package/lib/telemetry/client.mjs +3 -7
- package/lib/template-registry.mjs +10 -12
- package/lib/tenant/context.mjs +82 -0
- package/lib/tenant/isolation.mjs +129 -0
- package/lib/test-corpus-inventory.mjs +103 -2
- package/lib/uninstall/uninstall.mjs +78 -12
- package/lib/validator.mjs +4 -6
- package/lib/worker/entrypoint.mjs +2 -1
- package/lib/worker/run.mjs +2 -2
- package/lib/worker/trace.mjs +5 -11
- package/lib/workflow-state.mjs +52 -8
- package/lib/workflows/liveness.mjs +203 -0
- package/lib/workflows/loader.mjs +177 -0
- package/lib/workflows/manifest-schema.mjs +71 -0
- package/lib/workflows/surface-parity.mjs +118 -0
- package/lib/workflows/validate.mjs +127 -0
- package/lib/writes/control-plane.mjs +140 -0
- package/lib/writes/envelope.mjs +206 -0
- package/lib/writes/sent-log.mjs +86 -0
- package/lib/writes/write-intent.mjs +109 -0
- package/package.json +16 -8
- package/personas/construct.md +12 -3
- package/registry/capabilities.json +143 -36
- package/rules/common/comments.md +1 -0
- package/schemas/project-config.schema.json +0 -12
- package/schemas/unified-registry.schema.json +2 -4
- package/scripts/sync-specialists.mjs +284 -81
- package/skills/docs/adr-workflow.md +1 -1
- package/skills/docs/codebase-research-workflow.md +3 -3
- package/skills/docs/prd-workflow.md +5 -6
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/user-research-workflow.md +3 -3
- package/skills/operating/fleet-health-routing.md +77 -0
- package/skills/roles/ai-engineer.md +1 -1
- package/skills/roles/architect.md +0 -1
- package/skills/roles/business-strategist.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/data-engineer.md +1 -1
- package/skills/roles/data-engineer.pipeline.md +1 -1
- package/skills/roles/data-engineer.vector-retrieval.md +1 -2
- package/skills/roles/data-engineer.warehouse.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +0 -1
- package/skills/roles/devil-advocate.md +1 -1
- package/skills/roles/docs-keeper.md +1 -1
- package/skills/roles/evaluator.md +1 -1
- package/skills/roles/explorer.md +1 -1
- package/skills/roles/platform-engineer.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -2
- package/skills/roles/qa.api-contract.md +0 -1
- package/skills/roles/qa.data-pipeline.md +0 -1
- package/skills/roles/qa.md +0 -1
- package/skills/roles/qa.web-ui.md +0 -1
- package/skills/roles/release-manager.md +1 -1
- package/skills/roles/researcher.md +0 -2
- package/skills/roles/reviewer.md +0 -3
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +0 -1
- package/skills/roles/security.privacy.md +0 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/sre.md +1 -1
- package/skills/roles/test-automation.md +1 -1
- package/skills/roles/trace-reviewer.md +1 -1
- package/skills/roles/ux-researcher.md +1 -1
- package/specialists/artifact-manifest.json +36 -36
- package/specialists/org/contracts/accessibility-to-qa.json +11 -3
- package/specialists/org/contracts/any-to-business-strategist.json +19 -7
- package/specialists/org/contracts/any-to-debugger.json +7 -1
- package/specialists/org/contracts/any-to-designer.json +7 -1
- package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
- package/specialists/org/contracts/any-to-explorer.json +13 -4
- package/specialists/org/contracts/any-to-sre-incident.json +6 -2
- package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
- package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
- package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
- package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
- package/specialists/org/contracts/architect-to-engineer.json +20 -4
- package/specialists/org/contracts/architect-to-evaluator.json +15 -5
- package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
- package/specialists/org/contracts/architect-to-operations.json +17 -4
- package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
- package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
- package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
- package/specialists/org/contracts/engineer-to-qa.json +20 -4
- package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
- package/specialists/org/contracts/explorer-to-engineer.json +11 -3
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
- package/specialists/org/contracts/operations-to-user.json +53 -0
- package/specialists/org/contracts/operations-triage-output.json +53 -0
- package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
- package/specialists/org/contracts/product-manager-to-architect.json +30 -10
- package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
- package/specialists/org/contracts/qa-to-release-manager.json +11 -3
- package/specialists/org/contracts/researcher-to-architect.json +10 -2
- package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
- package/specialists/org/contracts/reviewer-to-security.json +17 -3
- package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
- package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
- package/specialists/org/contracts/user-to-construct.json +11 -4
- package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
- package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
- package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
- package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
- package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
- package/specialists/org/groups/engineering-group.json +2 -6
- package/specialists/org/groups/governance-group.json +2 -3
- package/specialists/org/groups/operations-group.json +5 -8
- package/specialists/org/groups/product-group.json +4 -8
- package/specialists/org/groups/quality-group.json +3 -7
- package/specialists/org/groups/strategy-group.json +6 -9
- package/specialists/org/models.json.example +8 -0
- package/specialists/org/scopes/creative.json +6 -1
- package/specialists/org/scopes/operations.json +7 -1
- package/specialists/org/scopes/research.json +6 -1
- package/specialists/org/scopes/rnd.json +7 -1
- package/specialists/org/specialists/cx-architect.json +27 -8
- package/specialists/org/specialists/cx-designer.json +22 -8
- package/specialists/org/specialists/cx-engineer.json +71 -7
- package/specialists/org/specialists/cx-operations.json +89 -15
- package/specialists/org/specialists/cx-orchestrator.json +16 -4
- package/specialists/org/specialists/cx-product-manager.json +28 -9
- package/specialists/org/specialists/cx-qa.json +16 -6
- package/specialists/org/specialists/cx-researcher.json +40 -7
- package/specialists/org/specialists/cx-reviewer.json +61 -11
- package/specialists/org/specialists/cx-security.json +30 -10
- package/specialists/org/teams/design-team.json +3 -4
- package/specialists/org/teams/engineering-team.json +3 -10
- package/specialists/org/teams/governance-team.json +3 -5
- package/specialists/org/teams/operations-team.json +6 -12
- package/specialists/org/teams/product-management-team.json +2 -3
- package/specialists/org/teams/quality-team.json +4 -12
- package/specialists/org/teams/research-team.json +3 -3
- package/specialists/org/teams/strategy-team.json +7 -14
- package/specialists/prompts/cx-architect.md +20 -1
- package/specialists/prompts/cx-data-analyst.md +1 -1
- package/specialists/prompts/cx-designer.md +12 -4
- package/specialists/prompts/cx-engineer.md +15 -1
- package/specialists/prompts/cx-operations.md +14 -2
- package/specialists/prompts/cx-orchestrator.md +9 -5
- package/specialists/prompts/cx-product-manager.md +5 -1
- package/specialists/prompts/cx-qa.md +6 -2
- package/specialists/prompts/cx-researcher.md +25 -30
- package/specialists/prompts/cx-reviewer.md +19 -1
- package/specialists/prompts/cx-security.md +5 -1
- package/templates/distribution/construct-brand.typ +123 -59
- package/templates/distribution/construct-decision.typ +6 -3
- package/templates/distribution/construct-pdf.typ +11 -4
- package/templates/distribution/construct-prd.typ +6 -3
- package/templates/distribution/construct-research.typ +7 -4
- package/lib/providers/contract/adapters/git/index.mjs +0 -115
- package/lib/providers/contract/adapters/slack/index.mjs +0 -175
- package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
- package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
- package/specialists/org/contracts/designer-to-accessibility.json +0 -36
- package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
- package/specialists/org/contracts/qa-to-test-automation.json +0 -42
- package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
- package/specialists/org/contracts/sre-to-release-manager.json +0 -36
- package/specialists/org/specialists/cx-accessibility.json +0 -69
- package/specialists/org/specialists/cx-ai-engineer.json +0 -75
- package/specialists/org/specialists/cx-business-strategist.json +0 -72
- package/specialists/org/specialists/cx-data-engineer.json +0 -71
- package/specialists/org/specialists/cx-devil-advocate.json +0 -71
- package/specialists/org/specialists/cx-docs-keeper.json +0 -82
- package/specialists/org/specialists/cx-evaluator.json +0 -69
- package/specialists/org/specialists/cx-explorer.json +0 -69
- package/specialists/org/specialists/cx-legal-compliance.json +0 -74
- package/specialists/org/specialists/cx-oracle.json +0 -46
- package/specialists/org/specialists/cx-platform-engineer.json +0 -80
- package/specialists/org/specialists/cx-rd-lead.json +0 -73
- package/specialists/org/specialists/cx-release-manager.json +0 -77
- package/specialists/org/specialists/cx-sre.json +0 -94
- package/specialists/org/specialists/cx-test-automation.json +0 -69
- package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
- package/specialists/org/specialists/cx-ux-researcher.json +0 -68
- package/specialists/org/teams/accessibility-team.json +0 -39
- package/specialists/org/teams/ux-research-team.json +0 -39
- package/specialists/prompts/cx-accessibility.md +0 -55
- package/specialists/prompts/cx-ai-engineer.md +0 -124
- package/specialists/prompts/cx-business-strategist.md +0 -58
- package/specialists/prompts/cx-data-engineer.md +0 -55
- package/specialists/prompts/cx-devil-advocate.md +0 -59
- package/specialists/prompts/cx-docs-keeper.md +0 -164
- package/specialists/prompts/cx-evaluator.md +0 -49
- package/specialists/prompts/cx-explorer.md +0 -71
- package/specialists/prompts/cx-legal-compliance.md +0 -58
- package/specialists/prompts/cx-oracle.md +0 -98
- package/specialists/prompts/cx-platform-engineer.md +0 -97
- package/specialists/prompts/cx-rd-lead.md +0 -59
- package/specialists/prompts/cx-release-manager.md +0 -54
- package/specialists/prompts/cx-sre.md +0 -111
- package/specialists/prompts/cx-test-automation.md +0 -55
- package/specialists/prompts/cx-trace-reviewer.md +0 -101
- package/specialists/prompts/cx-ux-researcher.md +0 -53
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/storage/retrieval-hardening.mjs — Vector-store poisoning defenses for
|
|
3
|
+
* the retrieval/recall path.
|
|
4
|
+
*
|
|
5
|
+
* N1 (construct-9oi4.14.1, lib/security/trust.mjs) stamps ingested content
|
|
6
|
+
* with a trust label (`_trust.level`) but does nothing to the ranking or
|
|
7
|
+
* shape of what gets recalled — an adversarial embedding tuned to score high
|
|
8
|
+
* cosine similarity can still dominate assembled context. This module is the
|
|
9
|
+
* retrieval-side hardening layer: it takes an already similarity-ranked
|
|
10
|
+
* result array (from VectorClient.searchObservations/searchDocuments or the
|
|
11
|
+
* hybrid-query fusion) and applies, in order:
|
|
12
|
+
*
|
|
13
|
+
* 1. distance/similarity sanity filtering — drop results below a floor or
|
|
14
|
+
* carrying a non-finite/out-of-range similarity (malformed or
|
|
15
|
+
* adversarially-crafted embeddings can produce these).
|
|
16
|
+
* 2. duplicate-content collapse — near-identical bodies (same normalized
|
|
17
|
+
* text) keep only the highest-ranked copy, so an attacker cannot
|
|
18
|
+
* multiply one payload's presence in the assembled set.
|
|
19
|
+
* 3. trust-weighted re-ranking — at equal similarity, higher N1 trust wins;
|
|
20
|
+
* unstamped records are graded EXTERNAL_UNAUTHENTICATED per policy
|
|
21
|
+
* (lib/security/trust.mjs recallTrustGrade).
|
|
22
|
+
* 4. per-source recall cap — no single source may occupy more than a
|
|
23
|
+
* configurable fraction of the assembled set.
|
|
24
|
+
*
|
|
25
|
+
* `flagRetrievalAnomalies` is a separate, non-mutating check: it inspects
|
|
26
|
+
* per-source frequency across a result set (or a caller-supplied historical
|
|
27
|
+
* frequency table) and flags sources whose share is a statistical outlier,
|
|
28
|
+
* for callers that want to log/alert without altering ranking.
|
|
29
|
+
*
|
|
30
|
+
* Pure functions only — no I/O, no LanceDB dependency — so the module is
|
|
31
|
+
* unit-testable without a live vector store and composable into
|
|
32
|
+
* hybrid-query.mjs, observation-store.mjs, or any future retrieval caller.
|
|
33
|
+
*
|
|
34
|
+
* References: CX-AUDIT-LLMSEC-001, OWASP GenAI vector/embedding weaknesses,
|
|
35
|
+
* construct-9oi4.14.4 (depends on construct-9oi4.14.1).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { TRUST_LEVELS, recallTrustGrade } from '../security/trust.mjs';
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Defaults
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_HARDENING_OPTIONS = {
|
|
45
|
+
minSimilarity: 0.05,
|
|
46
|
+
maxSimilarity: 1.0001,
|
|
47
|
+
perSourceCapRatio: 0.34,
|
|
48
|
+
duplicateSimilarityThreshold: 0.995,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const TRUST_RANK = [
|
|
52
|
+
TRUST_LEVELS.EXTERNAL_UNAUTHENTICATED,
|
|
53
|
+
TRUST_LEVELS.EXTERNAL_AUTHENTICATED,
|
|
54
|
+
TRUST_LEVELS.TEAM_AUTHORED,
|
|
55
|
+
TRUST_LEVELS.TRUSTED_INTERNAL,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Trust grading
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
// Unstamped records are the highest-risk case (N1 policy: treat as the
|
|
63
|
+
// least-trusted level) rather than silently defaulting to a mid-tier grade.
|
|
64
|
+
|
|
65
|
+
function trustRankOf(record) {
|
|
66
|
+
const grade = recallTrustGrade(record);
|
|
67
|
+
const level = grade?.level ?? TRUST_LEVELS.EXTERNAL_UNAUTHENTICATED;
|
|
68
|
+
const idx = TRUST_RANK.indexOf(level);
|
|
69
|
+
return idx === -1 ? 0 : idx;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a stable source identity for a recalled record, for both the
|
|
74
|
+
* observations table (`source`) and the documents table (`source_path` /
|
|
75
|
+
* `sourcePath`), falling back to project scope so ungrouped records still
|
|
76
|
+
* collapse into one bucket rather than each counting as a unique source.
|
|
77
|
+
*
|
|
78
|
+
* @param {Record<string, unknown>} record
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
export function sourceIdentityOf(record) {
|
|
82
|
+
const raw = record?.source ?? record?.sourcePath ?? record?.source_path ?? record?.project ?? 'unknown';
|
|
83
|
+
return typeof raw === 'object' ? JSON.stringify(raw) : String(raw);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Distance / similarity sanity thresholds
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Drop results whose similarity is missing, non-finite, or outside the
|
|
92
|
+
* plausible [minSimilarity, maxSimilarity] band. Cosine similarity for a
|
|
93
|
+
* well-formed embedding pair lies in [-1, 1]; a value outside that band (or
|
|
94
|
+
* exactly saturating past 1) signals a malformed or adversarially crafted
|
|
95
|
+
* vector rather than a genuine semantic match.
|
|
96
|
+
*
|
|
97
|
+
* @param {Array<Record<string, unknown>>} results
|
|
98
|
+
* @param {{ minSimilarity?: number, maxSimilarity?: number }} [opts]
|
|
99
|
+
* @returns {Array<Record<string, unknown>>}
|
|
100
|
+
*/
|
|
101
|
+
export function applySanityThreshold(results, opts = {}) {
|
|
102
|
+
const minSimilarity = opts.minSimilarity ?? DEFAULT_HARDENING_OPTIONS.minSimilarity;
|
|
103
|
+
const maxSimilarity = opts.maxSimilarity ?? DEFAULT_HARDENING_OPTIONS.maxSimilarity;
|
|
104
|
+
if (!Array.isArray(results)) return [];
|
|
105
|
+
|
|
106
|
+
return results.filter((r) => {
|
|
107
|
+
const sim = r?.similarity;
|
|
108
|
+
return Number.isFinite(sim) && sim >= minSimilarity && sim <= maxSimilarity;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Duplicate-content collapse
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
function normalizeForDedup(record) {
|
|
117
|
+
const text = record?.content ?? record?.body ?? record?.summary ?? '';
|
|
118
|
+
return String(text).trim().toLowerCase().replace(/\s+/g, ' ');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Collapse near-identical content down to the single highest-similarity
|
|
123
|
+
* copy. Exact-normalized matches always collapse; near-duplicates above
|
|
124
|
+
* `duplicateSimilarityThreshold` collapse via a cheap Jaccard-on-shingles
|
|
125
|
+
* check so an attacker cannot pad recall share by injecting many
|
|
126
|
+
* near-identical restatements of one payload.
|
|
127
|
+
*
|
|
128
|
+
* @param {Array<Record<string, unknown>>} results Similarity-sorted, best first.
|
|
129
|
+
* @param {{ duplicateSimilarityThreshold?: number }} [opts]
|
|
130
|
+
* @returns {Array<Record<string, unknown>>}
|
|
131
|
+
*/
|
|
132
|
+
export function collapseDuplicates(results, opts = {}) {
|
|
133
|
+
const threshold = opts.duplicateSimilarityThreshold ?? DEFAULT_HARDENING_OPTIONS.duplicateSimilarityThreshold;
|
|
134
|
+
if (!Array.isArray(results) || results.length === 0) return [];
|
|
135
|
+
|
|
136
|
+
const kept = [];
|
|
137
|
+
const keptNormalized = [];
|
|
138
|
+
|
|
139
|
+
for (const candidate of results) {
|
|
140
|
+
const normalized = normalizeForDedup(candidate);
|
|
141
|
+
const isDuplicate = keptNormalized.some((existing) => {
|
|
142
|
+
if (existing === normalized) return true;
|
|
143
|
+
if (!existing || !normalized) return false;
|
|
144
|
+
return shingleSimilarity(existing, normalized) >= threshold;
|
|
145
|
+
});
|
|
146
|
+
if (isDuplicate) continue;
|
|
147
|
+
kept.push(candidate);
|
|
148
|
+
keptNormalized.push(normalized);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return kept;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Trigram-shingle Jaccard similarity — cheap, dependency-free near-duplicate
|
|
155
|
+
// detection that tolerates whitespace/punctuation noise between two restated
|
|
156
|
+
// copies of the same payload.
|
|
157
|
+
|
|
158
|
+
function shingleSimilarity(a, b) {
|
|
159
|
+
const shinglesA = shingles(a);
|
|
160
|
+
const shinglesB = shingles(b);
|
|
161
|
+
if (shinglesA.size === 0 || shinglesB.size === 0) return 0;
|
|
162
|
+
|
|
163
|
+
let intersection = 0;
|
|
164
|
+
for (const s of shinglesA) {
|
|
165
|
+
if (shinglesB.has(s)) intersection += 1;
|
|
166
|
+
}
|
|
167
|
+
const union = shinglesA.size + shinglesB.size - intersection;
|
|
168
|
+
return union === 0 ? 0 : intersection / union;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function shingles(text, size = 3) {
|
|
172
|
+
const set = new Set();
|
|
173
|
+
for (let i = 0; i <= text.length - size; i += 1) {
|
|
174
|
+
set.add(text.slice(i, i + size));
|
|
175
|
+
}
|
|
176
|
+
return set;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Trust-weighted ranking
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Re-rank results so that, at materially equal similarity, higher-trust
|
|
185
|
+
* records sort above lower-trust ones. Similarity remains the dominant sort
|
|
186
|
+
* key — trust only breaks ties within an `epsilon` similarity band — so a
|
|
187
|
+
* genuinely more relevant trusted result still loses to an overwhelmingly
|
|
188
|
+
* better-matching one; it only wins the recall competition an adversarial
|
|
189
|
+
* near-tie is designed to exploit.
|
|
190
|
+
*
|
|
191
|
+
* @param {Array<Record<string, unknown>>} results
|
|
192
|
+
* @param {{ epsilon?: number }} [opts]
|
|
193
|
+
* @returns {Array<Record<string, unknown>>}
|
|
194
|
+
*/
|
|
195
|
+
export function applyTrustWeightedRanking(results, opts = {}) {
|
|
196
|
+
const epsilon = opts.epsilon ?? 0.02;
|
|
197
|
+
if (!Array.isArray(results)) return [];
|
|
198
|
+
|
|
199
|
+
return [...results].sort((a, b) => {
|
|
200
|
+
const simA = Number(a?.similarity) || 0;
|
|
201
|
+
const simB = Number(b?.similarity) || 0;
|
|
202
|
+
if (Math.abs(simA - simB) > epsilon) return simB - simA;
|
|
203
|
+
|
|
204
|
+
const trustDelta = trustRankOf(b) - trustRankOf(a);
|
|
205
|
+
if (trustDelta !== 0) return trustDelta;
|
|
206
|
+
|
|
207
|
+
return simB - simA;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Per-source recall cap
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Enforce that no single source occupies more than `perSourceCapRatio` of
|
|
217
|
+
* the FINAL assembled context set — not of the raw candidate pool. The cap
|
|
218
|
+
* is defined against `assembledSize` (the caller's `limit`, i.e. how many
|
|
219
|
+
* results actually get assembled into context) because that is the quantity
|
|
220
|
+
* OWASP/N4 policy constrains: an attacker cannot monopolize the context a
|
|
221
|
+
* model actually sees, regardless of how many candidates were fetched
|
|
222
|
+
* upstream. When no explicit `assembledSize`/`limit` is given, the candidate
|
|
223
|
+
* pool size after this stage is the best available proxy.
|
|
224
|
+
*
|
|
225
|
+
* Applied after ranking, so the highest-ranked items from each source are
|
|
226
|
+
* kept and the cap only trims a source's excess once it would otherwise
|
|
227
|
+
* crowd out other sources. A single pass keeps up to `cap` per source, then
|
|
228
|
+
* a second pass backfills any remaining assembled slots from the trimmed
|
|
229
|
+
* overflow (best-ranked first) so the cap never shrinks the final set below
|
|
230
|
+
* `assembledSize` while other sources have eligible candidates left.
|
|
231
|
+
*
|
|
232
|
+
* @param {Array<Record<string, unknown>>} results Already ranked, best first.
|
|
233
|
+
* @param {{ perSourceCapRatio?: number, assembledSize?: number, limit?: number }} [opts]
|
|
234
|
+
* @returns {Array<Record<string, unknown>>}
|
|
235
|
+
*/
|
|
236
|
+
export function applyPerSourceCap(results, opts = {}) {
|
|
237
|
+
const ratio = opts.perSourceCapRatio ?? DEFAULT_HARDENING_OPTIONS.perSourceCapRatio;
|
|
238
|
+
if (!Array.isArray(results) || results.length === 0) return [];
|
|
239
|
+
|
|
240
|
+
const assembledSize = Math.min(
|
|
241
|
+
results.length,
|
|
242
|
+
opts.assembledSize ?? opts.limit ?? results.length,
|
|
243
|
+
);
|
|
244
|
+
const cap = Math.max(1, Math.floor(assembledSize * ratio));
|
|
245
|
+
|
|
246
|
+
const perSourceCount = new Map();
|
|
247
|
+
const kept = [];
|
|
248
|
+
const overflow = [];
|
|
249
|
+
|
|
250
|
+
for (const record of results) {
|
|
251
|
+
const source = sourceIdentityOf(record);
|
|
252
|
+
const count = perSourceCount.get(source) || 0;
|
|
253
|
+
if (count >= cap) {
|
|
254
|
+
overflow.push(record);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
perSourceCount.set(source, count + 1);
|
|
258
|
+
kept.push(record);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Backfill from overflow (still capped per source) only until the
|
|
262
|
+
// assembled set reaches its target size — the cap must keep binding, so
|
|
263
|
+
// a source already at its cap is never topped up again here.
|
|
264
|
+
|
|
265
|
+
for (const record of overflow) {
|
|
266
|
+
if (kept.length >= assembledSize) break;
|
|
267
|
+
const source = sourceIdentityOf(record);
|
|
268
|
+
const count = perSourceCount.get(source) || 0;
|
|
269
|
+
if (count >= cap) continue;
|
|
270
|
+
perSourceCount.set(source, count + 1);
|
|
271
|
+
kept.push(record);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return kept;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// Combined pipeline
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Apply the full retrieval-hardening pipeline to a similarity-ranked result
|
|
283
|
+
* set: sanity threshold -> duplicate collapse -> trust-weighted ranking ->
|
|
284
|
+
* per-source cap -> limit. Intended as the single call site for any recall
|
|
285
|
+
* path that assembles context from VectorClient search results.
|
|
286
|
+
*
|
|
287
|
+
* @param {Array<Record<string, unknown>>} results
|
|
288
|
+
* @param {{
|
|
289
|
+
* minSimilarity?: number, maxSimilarity?: number,
|
|
290
|
+
* duplicateSimilarityThreshold?: number,
|
|
291
|
+
* perSourceCapRatio?: number, epsilon?: number, limit?: number,
|
|
292
|
+
* }} [opts]
|
|
293
|
+
* @returns {Array<Record<string, unknown>>}
|
|
294
|
+
*/
|
|
295
|
+
export function hardenRetrieval(results, opts = {}) {
|
|
296
|
+
if (!Array.isArray(results)) return [];
|
|
297
|
+
|
|
298
|
+
const sane = applySanityThreshold(results, opts);
|
|
299
|
+
const deduped = collapseDuplicates(sane, opts);
|
|
300
|
+
const ranked = applyTrustWeightedRanking(deduped, opts);
|
|
301
|
+
const capped = applyPerSourceCap(ranked, opts);
|
|
302
|
+
|
|
303
|
+
return opts.limit != null ? capped.slice(0, opts.limit) : capped;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// Retrieval-frequency anomaly check
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Flag sources whose share of a result set is a statistical outlier versus
|
|
312
|
+
* the other sources present. Uses a leave-one-out z-score: each source's
|
|
313
|
+
* count is compared against the mean/stddev of every OTHER source's count,
|
|
314
|
+
* not the whole population — scoring against the full population lets a
|
|
315
|
+
* large flood inflate its own reference mean/stddev enough to mask itself,
|
|
316
|
+
* especially with only a handful of distinct sources. A source more than
|
|
317
|
+
* `zThreshold` standard deviations above the other sources' mean is flagged;
|
|
318
|
+
* if the other sources all tie (zero spread) any source strictly above that
|
|
319
|
+
* shared count is flagged outright. With fewer than 3 distinct sources the
|
|
320
|
+
* check is inconclusive (no meaningful distribution) and returns no flags
|
|
321
|
+
* rather than a false positive.
|
|
322
|
+
*
|
|
323
|
+
* Non-mutating — this is a detection signal for logging/alerting, not a
|
|
324
|
+
* ranking transform. Callers that want enforcement should also apply
|
|
325
|
+
* `applyPerSourceCap`.
|
|
326
|
+
*
|
|
327
|
+
* @param {Array<Record<string, unknown>>} results
|
|
328
|
+
* @param {{ zThreshold?: number }} [opts]
|
|
329
|
+
* @returns {{
|
|
330
|
+
* flagged: Array<{ source: string, count: number, share: number, zScore: number }>,
|
|
331
|
+
* sourceCounts: Record<string, number>,
|
|
332
|
+
* }}
|
|
333
|
+
*/
|
|
334
|
+
export function flagRetrievalAnomalies(results, opts = {}) {
|
|
335
|
+
const zThreshold = opts.zThreshold ?? 2;
|
|
336
|
+
const sourceCounts = {};
|
|
337
|
+
|
|
338
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
339
|
+
return { flagged: [], sourceCounts };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (const record of results) {
|
|
343
|
+
const source = sourceIdentityOf(record);
|
|
344
|
+
sourceCounts[source] = (sourceCounts[source] || 0) + 1;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const entries = Object.entries(sourceCounts);
|
|
348
|
+
if (entries.length < 3) {
|
|
349
|
+
return { flagged: [], sourceCounts };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Leave-one-out z-score: a flood source's own count inflates the
|
|
353
|
+
// population mean/stddev enough to mask itself in a plain z-score,
|
|
354
|
+
// especially with few distinct sources. Scoring each candidate against
|
|
355
|
+
// the mean/stddev of the OTHER sources only removes that self-masking.
|
|
356
|
+
|
|
357
|
+
const flagged = [];
|
|
358
|
+
for (const [source, count] of entries) {
|
|
359
|
+
const others = entries.filter(([s]) => s !== source).map(([, c]) => c);
|
|
360
|
+
const otherMean = others.reduce((sum, c) => sum + c, 0) / others.length;
|
|
361
|
+
const otherVariance = others.reduce((sum, c) => sum + (c - otherMean) ** 2, 0) / others.length;
|
|
362
|
+
const otherStddev = Math.sqrt(otherVariance);
|
|
363
|
+
|
|
364
|
+
// Zero-spread tie among the other sources leaves no stddev to divide by.
|
|
365
|
+
// Require the candidate to be a substantial multiple of that tied count
|
|
366
|
+
// (not merely +1) before flagging, or a one-off count on top of small
|
|
367
|
+
// uniform buckets (e.g. 4 vs 3,3,3) would falsely read as an outlier.
|
|
368
|
+
|
|
369
|
+
if (otherStddev === 0) {
|
|
370
|
+
if (otherMean > 0 && count >= otherMean * 2) {
|
|
371
|
+
flagged.push({ source, count, share: count / results.length, zScore: Infinity });
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const zScore = (count - otherMean) / otherStddev;
|
|
377
|
+
if (zScore >= zThreshold) {
|
|
378
|
+
flagged.push({ source, count, share: count / results.length, zScore });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
flagged.sort((a, b) => b.zScore - a.zScore);
|
|
383
|
+
return { flagged, sourceCounts };
|
|
384
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/storage/shared-memory.mjs — explicit shared/private memory boundary (LMCP-G10).
|
|
3
|
+
*
|
|
4
|
+
* lib/observation-store.mjs persists every observation to a local,
|
|
5
|
+
* project-scoped store only — there is no tier a team member on another
|
|
6
|
+
* machine can read, and nothing currently distinguishes a private session
|
|
7
|
+
* scratch note from curated, team-durable project knowledge. This module is
|
|
8
|
+
* that explicit tier: a record only ever reaches the shared, postgres-backed
|
|
9
|
+
* store when it opts in with `visibility: 'shared-project'` AND carries
|
|
10
|
+
* non-empty `provenance` — anything else (no visibility field, `visibility:
|
|
11
|
+
* 'private'`, or a `sessionScratch` marker) is refused, never silently
|
|
12
|
+
* promoted.
|
|
13
|
+
*
|
|
14
|
+
* Solo mode (no reachable Postgres) resolves to a `kind:'none'` store whose
|
|
15
|
+
* write/list are no-ops — private local memory (observation-store.mjs) is
|
|
16
|
+
* unaffected either way.
|
|
17
|
+
*
|
|
18
|
+
* Tenant scoping (LMCP-H4, ADR-0057/A7 stage 1): every row carries a
|
|
19
|
+
* `tenant_id` column and every query filters on it, matching the run store
|
|
20
|
+
* (run-store-postgres.mjs) and trace store (trace-store.mjs). resolveShared-
|
|
21
|
+
* MemoryStore resolves the tenant context up front and fails closed —
|
|
22
|
+
* TenantResolutionError propagates uncaught — when enterprise mode has no
|
|
23
|
+
* resolvable tenant id. Stage 2 (physical per-tenant isolation, per-tenant
|
|
24
|
+
* encryption) is explicitly deferred and not advertised; this remains one
|
|
25
|
+
* shared table with row-level tenant scoping only.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { createSqlClient } from './backend.mjs';
|
|
29
|
+
import { getDeploymentMode } from '../deployment-mode.mjs';
|
|
30
|
+
import { resolveTenantContext } from '../tenant/context.mjs';
|
|
31
|
+
|
|
32
|
+
export const SHARED_VISIBILITY = 'shared-project';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* isShareable(record)
|
|
36
|
+
*
|
|
37
|
+
* A record is shareable only when every one of these holds:
|
|
38
|
+
* - `visibility` is exactly 'shared-project' (default/absent means private)
|
|
39
|
+
* - `provenance` is a non-empty object (who/what produced this, re-checkable)
|
|
40
|
+
* - it does not carry a `sessionScratch` marker (an explicit private-session flag)
|
|
41
|
+
*
|
|
42
|
+
* @param {object} record
|
|
43
|
+
* @returns {{ shareable: true } | { shareable: false, reason: string }}
|
|
44
|
+
*/
|
|
45
|
+
export function isShareable(record) {
|
|
46
|
+
if (!record || typeof record !== 'object') {
|
|
47
|
+
return { shareable: false, reason: 'record must be an object' };
|
|
48
|
+
}
|
|
49
|
+
if (record.sessionScratch) {
|
|
50
|
+
return { shareable: false, reason: 'record is marked sessionScratch (private session state)' };
|
|
51
|
+
}
|
|
52
|
+
if (record.visibility !== SHARED_VISIBILITY) {
|
|
53
|
+
return { shareable: false, reason: `visibility must be '${SHARED_VISIBILITY}' (got ${JSON.stringify(record.visibility ?? null)})` };
|
|
54
|
+
}
|
|
55
|
+
if (!record.provenance || typeof record.provenance !== 'object' || Array.isArray(record.provenance) || Object.keys(record.provenance).length === 0) {
|
|
56
|
+
return { shareable: false, reason: 'provenance must be a non-empty object' };
|
|
57
|
+
}
|
|
58
|
+
if (!record.id) {
|
|
59
|
+
return { shareable: false, reason: 'record.id is required' };
|
|
60
|
+
}
|
|
61
|
+
if (!record.category) {
|
|
62
|
+
return { shareable: false, reason: 'record.category is required' };
|
|
63
|
+
}
|
|
64
|
+
return { shareable: true };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function noneStore() {
|
|
68
|
+
return {
|
|
69
|
+
kind: 'none',
|
|
70
|
+
writeSharedMemory: async () => ({ ok: false, reason: 'no-team-store-configured' }),
|
|
71
|
+
listSharedMemory: async () => [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function postgresStore(sql, defaultTenantId) {
|
|
76
|
+
let ensured = null;
|
|
77
|
+
const ready = () => {
|
|
78
|
+
if (!ensured) {
|
|
79
|
+
ensured = (async () => {
|
|
80
|
+
const { applyMigrations } = await import('../db/migrate.mjs');
|
|
81
|
+
await applyMigrations(sql);
|
|
82
|
+
})();
|
|
83
|
+
}
|
|
84
|
+
return ensured;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
kind: 'postgres',
|
|
89
|
+
tenantId: defaultTenantId,
|
|
90
|
+
async writeSharedMemory(record, { project, tenantId = defaultTenantId } = {}) {
|
|
91
|
+
if (!project) throw new Error('writeSharedMemory: project is required');
|
|
92
|
+
const check = isShareable(record);
|
|
93
|
+
if (!check.shareable) return { ok: false, reason: check.reason };
|
|
94
|
+
|
|
95
|
+
await ready();
|
|
96
|
+
await sql`
|
|
97
|
+
INSERT INTO construct_shared_memory (
|
|
98
|
+
id, project, tenant_id, category, summary, content, tags, provenance, created_at
|
|
99
|
+
) VALUES (
|
|
100
|
+
${record.id}, ${project}, ${tenantId}, ${record.category},
|
|
101
|
+
${record.summary || null}, ${record.content || null},
|
|
102
|
+
${sql.json(record.tags || [])}, ${sql.json(record.provenance)},
|
|
103
|
+
${record.createdAt || new Date().toISOString()}
|
|
104
|
+
)
|
|
105
|
+
ON CONFLICT (project, tenant_id, id) DO UPDATE SET
|
|
106
|
+
category = EXCLUDED.category,
|
|
107
|
+
summary = EXCLUDED.summary,
|
|
108
|
+
content = EXCLUDED.content,
|
|
109
|
+
tags = EXCLUDED.tags,
|
|
110
|
+
provenance = EXCLUDED.provenance
|
|
111
|
+
`;
|
|
112
|
+
return { ok: true };
|
|
113
|
+
},
|
|
114
|
+
async listSharedMemory({ project, tenantId = defaultTenantId, limit = 100 } = {}) {
|
|
115
|
+
if (!project) throw new Error('listSharedMemory: project is required');
|
|
116
|
+
await ready();
|
|
117
|
+
const rows = await sql`
|
|
118
|
+
SELECT id, category, summary, content, tags, provenance, created_at
|
|
119
|
+
FROM construct_shared_memory
|
|
120
|
+
WHERE project = ${project} AND tenant_id = ${tenantId}
|
|
121
|
+
ORDER BY created_at DESC
|
|
122
|
+
LIMIT ${limit}
|
|
123
|
+
`;
|
|
124
|
+
return rows.map((row) => ({
|
|
125
|
+
id: row.id,
|
|
126
|
+
category: row.category,
|
|
127
|
+
summary: row.summary,
|
|
128
|
+
content: row.content,
|
|
129
|
+
tags: row.tags,
|
|
130
|
+
provenance: row.provenance,
|
|
131
|
+
createdAt: row.created_at,
|
|
132
|
+
}));
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* resolveSharedMemoryStore(opts)
|
|
139
|
+
*
|
|
140
|
+
* `sql` accepts an injected client (matching lib/team/health.mjs's
|
|
141
|
+
* convention) so a caller — or a test backed by an in-memory fake — can
|
|
142
|
+
* supply a shared substrate directly instead of resolving one from env.
|
|
143
|
+
* Enterprise mode with no resolvable tenant id throws TenantResolutionError
|
|
144
|
+
* before any query runs (fail-closed).
|
|
145
|
+
*
|
|
146
|
+
* @param {{ env?: object, cwd?: string, config?: object, sql?: object }} [opts]
|
|
147
|
+
* @returns {{ kind: string, writeSharedMemory: Function, listSharedMemory: Function }}
|
|
148
|
+
*/
|
|
149
|
+
export function resolveSharedMemoryStore({ env = process.env, cwd = process.cwd(), config = null, sql = null } = {}) {
|
|
150
|
+
const mode = getDeploymentMode(env, { cwd });
|
|
151
|
+
if (mode === 'solo' && !sql) return noneStore();
|
|
152
|
+
|
|
153
|
+
const ownSql = sql || createSqlClient(env);
|
|
154
|
+
if (!ownSql) return noneStore();
|
|
155
|
+
|
|
156
|
+
const { tenantId } = resolveTenantContext({ env, config, mode });
|
|
157
|
+
return postgresStore(ownSql, tenantId);
|
|
158
|
+
}
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/storage/vector-client.mjs — Unified vector storage client for LanceDB.
|
|
3
|
+
*
|
|
4
|
+
* Unset CONSTRUCT_LANCEDB_PATH falls back to the calling project's
|
|
5
|
+
* machine-scoped state root (ADR-0066: `<stateRoot>/lancedb`, keyed off cwd).
|
|
6
|
+
* A managed install sets CONSTRUCT_LANCEDB_PATH to a single machine-wide
|
|
7
|
+
* index (lib/setup.mjs's defaultVectorIndexPath), which always takes
|
|
8
|
+
* precedence; the fallback path only applies to unmanaged/solo runs and tests.
|
|
9
|
+
*
|
|
10
|
+
* The index itself is lazy: _getDb() only connects/creates on the first
|
|
11
|
+
* actual store or search call, never at construction. pruneObservations()
|
|
12
|
+
* (lib/storage/admin.mjs's purgeExpiredData) is the TTL/size eviction path
|
|
13
|
+
* for the machine-scoped store and never creates observations_v1 as a side
|
|
14
|
+
* effect of checking or pruning it.
|
|
3
15
|
*/
|
|
4
16
|
|
|
5
|
-
import path from 'path';
|
|
6
17
|
import fs from 'fs';
|
|
7
18
|
import { getEmbeddingModelInfo } from './embeddings-engine.mjs';
|
|
19
|
+
import { resolveStateDir } from '../state-root.mjs';
|
|
8
20
|
|
|
9
21
|
const FALLBACK_DIMENSIONS = 384;
|
|
10
22
|
|
|
11
23
|
function getDbPath(env = process.env) {
|
|
12
24
|
if (env.CONSTRUCT_LANCEDB_PATH) return env.CONSTRUCT_LANCEDB_PATH;
|
|
13
|
-
|
|
25
|
+
// ensureDir:false — this path is computed on every call (including ones
|
|
26
|
+
// that never reach a real connect, e.g. lancedb module load failure), so
|
|
27
|
+
// directory creation stays owned by _getDb() right before it connects.
|
|
28
|
+
return resolveStateDir(process.cwd(), 'lancedb', { ensureDir: false });
|
|
14
29
|
}
|
|
15
30
|
|
|
16
31
|
// LanceDB uses optimistic concurrency: two writes racing the same table fail
|
|
@@ -175,6 +190,58 @@ export class VectorClient {
|
|
|
175
190
|
return this.isHealthy();
|
|
176
191
|
}
|
|
177
192
|
|
|
193
|
+
// Existence check that never creates the table as a side effect — a purge
|
|
194
|
+
// or doctor report must not conjure observations_v1 just to find it empty;
|
|
195
|
+
// _getObservationsTable() would create it on a missing-table catch.
|
|
196
|
+
|
|
197
|
+
async hasObservationsTable() {
|
|
198
|
+
const db = await this._getDb();
|
|
199
|
+
const names = await db.tableNames();
|
|
200
|
+
return names.includes('observations_v1');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Evict rows from observations_v1 older than `maxAgeDays` and/or beyond
|
|
205
|
+
* the `maxRows` most-recent cap. Either bound is optional; passing neither
|
|
206
|
+
* is a no-op. Returns eviction stats so callers (doctor, `construct prune`,
|
|
207
|
+
* the post-sync retention hook) can report on the machine-scoped store
|
|
208
|
+
* without re-deriving it themselves.
|
|
209
|
+
*/
|
|
210
|
+
async pruneObservations({ maxAgeDays, maxRows } = {}) {
|
|
211
|
+
if (!(await this.hasObservationsTable())) {
|
|
212
|
+
return { evictedCount: 0, remainingCount: 0, oldestRetainedAt: null };
|
|
213
|
+
}
|
|
214
|
+
const table = await this._getObservationsTable();
|
|
215
|
+
let evictedCount = 0;
|
|
216
|
+
|
|
217
|
+
if (typeof maxAgeDays === 'number' && maxAgeDays > 0) {
|
|
218
|
+
const cutoff = new Date(Date.now() - maxAgeDays * 24 * 60 * 60 * 1000).toISOString();
|
|
219
|
+
const before = await table.countRows();
|
|
220
|
+
await table.delete(`created_at < '${cutoff}'`);
|
|
221
|
+
evictedCount += Math.max(0, before - await table.countRows());
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (typeof maxRows === 'number' && maxRows > 0) {
|
|
225
|
+
const total = await table.countRows();
|
|
226
|
+
if (total > maxRows) {
|
|
227
|
+
const rows = await table.query().select(['created_at']).toArray();
|
|
228
|
+
const sortedTimestamps = rows.map((r) => r.created_at).sort();
|
|
229
|
+
const cutoff = sortedTimestamps[total - maxRows];
|
|
230
|
+
const before = await table.countRows();
|
|
231
|
+
await table.delete(`created_at < '${cutoff}'`);
|
|
232
|
+
evictedCount += Math.max(0, before - await table.countRows());
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const remaining = await table.query().select(['created_at']).toArray();
|
|
237
|
+
const remainingTimestamps = remaining.map((r) => r.created_at).filter(Boolean).sort();
|
|
238
|
+
return {
|
|
239
|
+
evictedCount,
|
|
240
|
+
remainingCount: remaining.length,
|
|
241
|
+
oldestRetainedAt: remainingTimestamps[0] ?? null,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
178
245
|
async storeObservation(record) {
|
|
179
246
|
return serializeWrite(getDbPath(this.env), () => withWriteRetry(() => this._storeObservation(record)));
|
|
180
247
|
}
|