@geraldmaron/construct 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -18
- package/bin/construct +392 -91
- package/bin/construct-postinstall.mjs +30 -2
- package/examples/distribution/README.md +42 -0
- package/examples/distribution/manifest.json +53 -0
- package/examples/distribution/sources/adr.md +84 -0
- package/examples/distribution/sources/deck-one-pager.md +65 -0
- package/examples/distribution/sources/prd-platform.md +161 -0
- package/examples/distribution/sources/research-brief.md +80 -0
- package/examples/distribution/sources/rfc-platform.md +82 -0
- package/examples/distribution/sources/runbook.md +103 -0
- package/examples/distribution/sources/strategy.md +88 -0
- package/lib/adapters-sync.mjs +67 -0
- package/lib/artifact-gate-notice.mjs +38 -0
- package/lib/artifact-manifest.mjs +112 -0
- package/lib/artifact-release-gate.mjs +171 -0
- package/lib/artifact-reviewers.mjs +68 -0
- package/lib/artifact-type-from-path.mjs +79 -0
- package/lib/audit-skills.mjs +4 -0
- package/lib/audit-specialists.mjs +285 -0
- package/lib/audit-trail.mjs +77 -10
- package/lib/auto-docs.mjs +87 -18
- package/lib/beads/drift.mjs +27 -6
- package/lib/beads-client.mjs +49 -121
- package/lib/beads-optimistic.mjs +5 -12
- package/lib/brand-fonts.mjs +93 -0
- package/lib/brand-prose.mjs +214 -0
- package/lib/brand-tokens.mjs +92 -0
- package/lib/bridges/copilot-proxy.mjs +13 -26
- package/lib/capability-ledger.mjs +156 -0
- package/lib/certification/artifact-fixtures.mjs +132 -0
- package/lib/certification/artifact-gates.mjs +63 -0
- package/lib/certification/artifact-provenance.mjs +97 -0
- package/lib/certification/canonical-scenarios.mjs +78 -0
- package/lib/certification/cli.mjs +191 -0
- package/lib/certification/dashboard-api.mjs +71 -0
- package/lib/certification/demo-parity.mjs +116 -0
- package/lib/certification/document-io-fixtures.mjs +246 -0
- package/lib/certification/document-workflow.mjs +97 -0
- package/lib/certification/eval-bridge.mjs +77 -0
- package/lib/certification/model-routing.mjs +149 -0
- package/lib/certification/prompt-budget.mjs +119 -0
- package/lib/certification/rc-gate.mjs +206 -0
- package/lib/certification/real-llm-scenarios.mjs +303 -0
- package/lib/certification/role-cards.mjs +113 -0
- package/lib/certification/role-overlays.mjs +117 -0
- package/lib/certification/run.mjs +122 -0
- package/lib/certification/runner.mjs +323 -0
- package/lib/certification/scenarios.mjs +51 -0
- package/lib/certification/skill-inventory.mjs +289 -0
- package/lib/certification/skill-scenarios.mjs +147 -0
- package/lib/certification/specialist-contracts.mjs +85 -0
- package/lib/certification/specialist-scenarios.mjs +175 -0
- package/lib/certification/stale-impact.mjs +146 -0
- package/lib/certification/status.mjs +252 -0
- package/lib/certification/store.mjs +77 -0
- package/lib/chat/cli.mjs +333 -0
- package/lib/chat/command-suggest.mjs +161 -0
- package/lib/chat/commands.mjs +215 -0
- package/lib/chat/config.mjs +142 -0
- package/lib/chat/context-compactor.mjs +250 -0
- package/lib/chat/context-continuation.mjs +253 -0
- package/lib/chat/continuation-source.mjs +58 -0
- package/lib/chat/demo-guide.mjs +61 -0
- package/lib/chat/design-tokens.mjs +91 -0
- package/lib/chat/desktop-binary.mjs +79 -0
- package/lib/chat/desktop-build.mjs +130 -0
- package/lib/chat/desktop-launcher.mjs +133 -0
- package/lib/chat/evidence.mjs +145 -0
- package/lib/chat/export.mjs +74 -0
- package/lib/chat/harness/driver.mjs +91 -0
- package/lib/chat/list-picker.mjs +112 -0
- package/lib/chat/model-picker.mjs +356 -0
- package/lib/chat/openrouter-fallback.mjs +151 -0
- package/lib/chat/permission-prompt.mjs +33 -0
- package/lib/chat/picker-catalog.mjs +45 -0
- package/lib/chat/policy-telemetry.mjs +34 -0
- package/lib/chat/present.mjs +245 -0
- package/lib/chat/session-context.mjs +39 -0
- package/lib/chat/session-persist.mjs +73 -0
- package/lib/chat/session-restore.mjs +71 -0
- package/lib/chat/session-settings.mjs +53 -0
- package/lib/chat/system-prompt.mjs +52 -0
- package/lib/chat/transparency.mjs +93 -0
- package/lib/chat/tui/color-scheme.mjs +42 -0
- package/lib/chat/tui/markdown.mjs +123 -0
- package/lib/chat/tui/presentation.mjs +100 -0
- package/lib/chat/tui/render.mjs +500 -0
- package/lib/chat/tui/turn-block.mjs +284 -0
- package/lib/chat/tui/turn-present.mjs +18 -0
- package/lib/chat/tui/turn-state.mjs +88 -0
- package/lib/chat/tui/usage.mjs +122 -0
- package/lib/chat/web-commands.mjs +146 -0
- package/lib/chat/web-launcher.mjs +63 -0
- package/lib/chat/web-picker-keys.mjs +46 -0
- package/lib/chat/web-session.mjs +159 -0
- package/lib/cli-commands.mjs +232 -11
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/schema.mjs +36 -5
- package/lib/contract-schemas/decision.json +50 -0
- package/lib/contract-schemas/implementation.json +51 -0
- package/lib/contract-schemas/review-report.json +32 -0
- package/lib/contract-schemas/test-report.json +43 -0
- package/lib/contracts/construct-handoff.mjs +60 -0
- package/lib/contracts/validate.mjs +32 -3
- package/lib/contracts/violation-log.mjs +58 -5
- package/lib/dashboard-demo.mjs +71 -0
- package/lib/dashboard-static.mjs +19 -5
- package/lib/decisions/registry.mjs +5 -3
- package/lib/deck-export-pptx.mjs +1152 -0
- package/lib/demo-recording.mjs +142 -0
- package/lib/demo-script.mjs +114 -0
- package/lib/demo-surface.mjs +249 -0
- package/lib/demo.mjs +703 -0
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +302 -0
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +4 -2
- package/lib/doctor/project-adapters.mjs +44 -0
- package/lib/doctor/watchers/cx-budget.mjs +98 -0
- package/lib/doctor/watchers/graph-staleness.mjs +52 -0
- package/lib/doctor/watchers/process-pressure.mjs +4 -3
- package/lib/doctor/watchers/service-health.mjs +1 -1
- package/lib/document-export.mjs +360 -36
- package/lib/document-extract.mjs +88 -0
- package/lib/document-ingest.mjs +32 -18
- package/lib/embed/cli.mjs +2 -2
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/semantic.mjs +5 -2
- package/lib/embed/worker.mjs +2 -2
- package/lib/embedded-contract/model-resolve.mjs +10 -9
- package/lib/env-config.mjs +26 -0
- package/lib/evals/dataset.mjs +137 -0
- package/lib/evals/gates.mjs +175 -0
- package/lib/graph/build-co-change.mjs +54 -0
- package/lib/graph/build-from-registry.mjs +136 -0
- package/lib/graph/build-import-graph.mjs +153 -0
- package/lib/graph/cli.mjs +110 -0
- package/lib/graph/impact-cli.mjs +89 -0
- package/lib/graph/impact.mjs +108 -0
- package/lib/graph/staleness.mjs +44 -0
- package/lib/graph/store.mjs +172 -0
- package/lib/handoffs/cleanup.mjs +1 -1
- package/lib/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- package/lib/hooks/audit-trail.mjs +14 -28
- package/lib/hooks/brand-prose-lint.mjs +38 -0
- package/lib/hooks/graph-impact-advisory.mjs +62 -0
- package/lib/hooks/session-optimize.mjs +84 -207
- package/lib/hooks/session-start.mjs +4 -5
- package/lib/host-disposition.mjs +7 -0
- package/lib/improvement/cli.mjs +189 -0
- package/lib/improvement/controller.mjs +137 -0
- package/lib/improvement/proposal.mjs +120 -0
- package/lib/improvement/specialist-loop.mjs +192 -0
- package/lib/improvement/store.mjs +89 -0
- package/lib/improvement/surface.mjs +219 -0
- package/lib/ingest-tooling.mjs +97 -0
- package/lib/init/doc-lanes.mjs +165 -0
- package/lib/init-docs.mjs +31 -178
- package/lib/init-unified.mjs +27 -97
- package/lib/init-update-guide.mjs +102 -0
- package/lib/init-update.mjs +32 -1
- package/lib/init.mjs +8 -0
- package/lib/install/desktop-binary-download.mjs +85 -0
- package/lib/install/legacy-global-cleanup.mjs +189 -0
- package/lib/intake/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- package/lib/intake/queue.mjs +2 -1
- package/lib/intake/session-prelude.mjs +90 -1
- package/lib/libreoffice-export.mjs +97 -0
- package/lib/logging/rotate.mjs +9 -0
- package/lib/maintenance/docker-reclaim.mjs +206 -0
- package/lib/mcp/external-schema-cost.mjs +74 -0
- package/lib/mcp/server.mjs +84 -9
- package/lib/mcp/stdio-mcp-probe.mjs +188 -0
- package/lib/mcp/tool-budget.mjs +55 -4
- package/lib/mcp/tools/document.mjs +18 -4
- package/lib/mcp/tools/skills.mjs +32 -3
- package/lib/mcp/tools/workflow.mjs +13 -1
- package/lib/model-free-selector.mjs +18 -0
- package/lib/model-registry.mjs +13 -229
- package/lib/model-router.mjs +425 -93
- package/lib/models/behavior-matrix.mjs +289 -0
- package/lib/models/catalog.mjs +209 -0
- package/lib/models/execution-capability-profile.mjs +196 -0
- package/lib/models/execution-policy.mjs +307 -0
- package/lib/models/provider-poll.mjs +383 -0
- package/lib/npm-spawn-env.mjs +17 -0
- package/lib/ollama/installed-models.mjs +129 -0
- package/lib/oracle/actions.mjs +187 -0
- package/lib/oracle/artifact-gate.mjs +99 -0
- package/lib/oracle/cli.mjs +204 -0
- package/lib/oracle/daemon-entry.mjs +14 -0
- package/lib/oracle/dispatch.mjs +81 -0
- package/lib/oracle/execute.mjs +143 -0
- package/lib/oracle/gaps.mjs +76 -0
- package/lib/oracle/index.mjs +84 -0
- package/lib/oracle/issues.mjs +164 -0
- package/lib/oracle/org-graph.mjs +170 -0
- package/lib/oracle/policy.mjs +80 -0
- package/lib/oracle/read-model.mjs +398 -0
- package/lib/oracle/reconcile.mjs +191 -0
- package/lib/oracle/routing.mjs +89 -0
- package/lib/oracle/synthesize.mjs +384 -0
- package/lib/oracle/verdicts.mjs +51 -0
- package/lib/orchestration/worker.mjs +88 -27
- package/lib/orchestration-policy.mjs +50 -0
- package/lib/parity.mjs +96 -2
- package/lib/persona-sections.mjs +66 -0
- package/lib/playwright-demo.mjs +252 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +9 -4
- package/lib/prompt-validation-contract.mjs +24 -0
- package/lib/provider-capabilities.js +57 -11
- package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
- package/lib/providers/contract/adapters/git/index.mjs +115 -0
- package/lib/providers/contract/adapters/github/index.mjs +166 -0
- package/lib/providers/contract/adapters/jira/index.mjs +187 -0
- package/lib/providers/contract/adapters/slack/index.mjs +175 -0
- package/lib/providers/contract/contract-tests.mjs +57 -0
- package/lib/providers/contract/errors.mjs +48 -0
- package/lib/providers/contract/interface.mjs +50 -0
- package/lib/providers/contract/registry.mjs +102 -0
- package/lib/providers/copilot-auth.mjs +297 -0
- package/lib/providers/credential-bootstrap.mjs +178 -0
- package/lib/providers/credential-catalog.mjs +46 -0
- package/lib/providers/credential-sources.mjs +63 -0
- package/lib/providers/creds.mjs +5 -2
- package/lib/providers/op-run.mjs +59 -0
- package/lib/providers/secret-resolver.mjs +159 -0
- package/lib/publish-template.mjs +163 -0
- package/lib/publish-tooling.mjs +119 -0
- package/lib/publish.mjs +305 -0
- package/lib/registry/cli.mjs +82 -0
- package/lib/registry/consolidation.mjs +147 -0
- package/lib/registry/generate-docs.mjs +75 -0
- package/lib/registry/skill-verification.mjs +57 -0
- package/lib/registry/surface-map.mjs +76 -0
- package/lib/registry/validate.mjs +135 -0
- package/lib/resources/budget.mjs +82 -0
- package/lib/resources/process-budget.mjs +45 -0
- package/lib/rules-delivery.mjs +9 -2
- package/lib/rules-read.mjs +26 -0
- package/lib/runtime-env.mjs +23 -0
- package/lib/runtime-pressure.mjs +51 -7
- package/lib/schema-infer.mjs +13 -2
- package/lib/server/chat-loop.mjs +622 -0
- package/lib/server/demo-preview.mjs +63 -0
- package/lib/server/index.mjs +229 -8
- package/lib/server/langfuse-login.mjs +3 -3
- package/lib/service-manager.mjs +61 -9
- package/lib/session-store.mjs +1 -1
- package/lib/setup.mjs +102 -2
- package/lib/specialists/prompt-schema.mjs +19 -10
- package/lib/specialists/roster.mjs +43 -0
- package/lib/specialists/scaffold.mjs +56 -0
- package/lib/storage/backend.mjs +6 -6
- package/lib/storage/embeddings-local.mjs +6 -3
- package/lib/storage/file-lock.mjs +18 -11
- package/lib/storage/hybrid-query.mjs +7 -4
- package/lib/storage/state-source.mjs +5 -5
- package/lib/storage/sync.mjs +2 -3
- package/lib/telemetry/rule-calls.mjs +52 -0
- package/lib/template-registry.mjs +2 -2
- package/lib/templates/visual-requirements.mjs +27 -51
- package/lib/test-corpus-inventory.mjs +313 -0
- package/lib/uninstall/uninstall.mjs +19 -7
- package/lib/update.mjs +12 -0
- package/lib/upgrade.mjs +14 -0
- package/lib/wireframe.mjs +20 -14
- package/lib/worker/run.mjs +17 -4
- package/lib/worker/trace.mjs +11 -3
- package/package.json +29 -13
- package/personas/construct.md +13 -13
- package/platforms/claude/settings.template.json +36 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/release-gates.md +4 -3
- package/scripts/sync-specialists.mjs +187 -60
- package/skills/devops/data-engineering.md +1 -1
- package/skills/docs/adr-workflow.md +1 -0
- package/skills/docs/backlog-proposal-workflow.md +1 -0
- package/skills/docs/codebase-research-workflow.md +40 -0
- package/skills/docs/customer-profile-workflow.md +1 -0
- package/skills/docs/document-ingest-workflow.md +1 -0
- package/skills/docs/evidence-ingest-workflow.md +1 -0
- package/skills/docs/init-docs.md +2 -2
- package/skills/docs/init-project.md +8 -3
- package/skills/docs/prd-workflow.md +24 -1
- package/skills/docs/prfaq-workflow.md +1 -0
- package/skills/docs/product-intelligence-workflow.md +1 -0
- package/skills/docs/product-signal-workflow.md +1 -0
- package/skills/docs/research-workflow.md +54 -37
- package/skills/docs/runbook-workflow.md +1 -0
- package/skills/docs/strategy-workflow.md +1 -0
- package/skills/docs/user-research-workflow.md +40 -0
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/architect.md +5 -0
- package/skills/roles/operator.docs.md +4 -0
- package/skills/routing.md +4 -2
- package/specialists/artifact-manifest.json +480 -0
- package/specialists/artifact-manifest.schema.json +53 -0
- package/specialists/audit-enrichments.json +454 -0
- package/specialists/contracts.json +37 -25
- package/specialists/prompts/_shared/validation-contract.md +26 -0
- package/specialists/prompts/cx-accessibility.md +21 -2
- package/specialists/prompts/cx-ai-engineer.md +24 -1
- package/specialists/prompts/cx-architect.md +4 -1
- package/specialists/prompts/cx-business-strategist.md +23 -2
- package/specialists/prompts/cx-data-analyst.md +22 -1
- package/specialists/prompts/cx-data-engineer.md +23 -2
- package/specialists/prompts/cx-debugger.md +21 -2
- package/specialists/prompts/cx-designer.md +26 -3
- package/specialists/prompts/cx-devil-advocate.md +19 -2
- package/specialists/prompts/cx-docs-keeper.md +28 -1
- package/specialists/prompts/cx-engineer.md +22 -1
- package/specialists/prompts/cx-evaluator.md +18 -1
- package/specialists/prompts/cx-explorer.md +21 -2
- package/specialists/prompts/cx-legal-compliance.md +21 -2
- package/specialists/prompts/cx-operations.md +21 -2
- package/specialists/prompts/cx-oracle.md +94 -0
- package/specialists/prompts/cx-orchestrator.md +20 -1
- package/specialists/prompts/cx-platform-engineer.md +25 -2
- package/specialists/prompts/cx-product-manager.md +32 -1
- package/specialists/prompts/cx-qa.md +24 -2
- package/specialists/prompts/cx-rd-lead.md +23 -2
- package/specialists/prompts/cx-release-manager.md +23 -2
- package/specialists/prompts/cx-researcher.md +22 -2
- package/specialists/prompts/cx-reviewer.md +18 -1
- package/specialists/prompts/cx-security.md +22 -2
- package/specialists/prompts/cx-sre.md +30 -3
- package/specialists/prompts/cx-test-automation.md +7 -1
- package/specialists/prompts/cx-trace-reviewer.md +22 -3
- package/specialists/prompts/cx-ux-researcher.md +21 -2
- package/specialists/registry.json +52 -173
- package/specialists/tone-profiles.json +42 -0
- package/templates/demos/playwright/demo-recording.config.mjs +47 -0
- package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
- package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
- package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
- package/templates/demos/tapes/resource-guard-rails.tape +49 -0
- package/templates/demos/vhs/construct-cockpit.json +24 -0
- package/templates/distribution/construct-brand.typ +446 -0
- package/templates/distribution/construct-decision.typ +38 -0
- package/templates/distribution/construct-deck.html +95 -0
- package/templates/distribution/construct-pdf.typ +38 -0
- package/templates/distribution/construct-prd.typ +38 -0
- package/templates/distribution/construct-research.typ +38 -0
- package/templates/distribution/construct-web.html +92 -0
- package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
- package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
- package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
- package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/README.md +36 -0
- package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
- package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
- package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
- package/templates/distribution/icons/activity.svg +15 -0
- package/templates/distribution/icons/alert-triangle.svg +17 -0
- package/templates/distribution/icons/book-open.svg +16 -0
- package/templates/distribution/icons/bot.svg +20 -0
- package/templates/distribution/icons/brain.svg +22 -0
- package/templates/distribution/icons/circle-check.svg +16 -0
- package/templates/distribution/icons/clipboard-check.svg +17 -0
- package/templates/distribution/icons/cpu.svg +28 -0
- package/templates/distribution/icons/database.svg +17 -0
- package/templates/distribution/icons/eye.svg +16 -0
- package/templates/distribution/icons/file-text.svg +19 -0
- package/templates/distribution/icons/gauge.svg +16 -0
- package/templates/distribution/icons/git-branch.svg +17 -0
- package/templates/distribution/icons/key.svg +17 -0
- package/templates/distribution/icons/layers.svg +17 -0
- package/templates/distribution/icons/list-checks.svg +19 -0
- package/templates/distribution/icons/lock.svg +16 -0
- package/templates/distribution/icons/message-square.svg +15 -0
- package/templates/distribution/icons/network.svg +19 -0
- package/templates/distribution/icons/route.svg +17 -0
- package/templates/distribution/icons/scale.svg +19 -0
- package/templates/distribution/icons/search.svg +16 -0
- package/templates/distribution/icons/send.svg +16 -0
- package/templates/distribution/icons/server.svg +18 -0
- package/templates/distribution/icons/shield-check.svg +16 -0
- package/templates/distribution/icons/users.svg +18 -0
- package/templates/distribution/icons/webhook.svg +17 -0
- package/templates/distribution/icons/workflow.svg +17 -0
- package/templates/distribution/icons/wrench.svg +15 -0
- package/templates/distribution/run.mjs +18 -1
- package/templates/docs/adr.md +7 -0
- package/templates/docs/construct_guide.md +10 -10
- package/templates/docs/customer-profile.md +4 -0
- package/templates/docs/one-pager.md +4 -0
- package/templates/docs/postmortem.md +31 -0
- package/templates/docs/prd-platform.md +19 -0
- package/templates/docs/prd.md +15 -0
- package/templates/docs/prfaq.md +7 -0
- package/templates/docs/qa-strategy.md +103 -0
- package/templates/docs/research-brief.md +8 -0
- package/templates/docs/rfc-platform.md +12 -0
- package/templates/docs/test-plan.md +7 -0
- package/lib/ingest/chunker.mjs +0 -94
- package/lib/ingest/pipeline.mjs +0 -53
- package/lib/ingest/store.mjs +0 -82
- package/lib/mode-commands.mjs +0 -122
- package/lib/policy/unified-gates.mjs +0 -96
- package/lib/profiles/validate-custom.mjs +0 -114
- package/lib/services/telemetry-backend.mjs +0 -177
- package/lib/storage/fusion.mjs +0 -95
package/lib/document-extract.mjs
CHANGED
|
@@ -45,6 +45,31 @@ export const CALENDAR_EXTS = new Set(['.ics']);
|
|
|
45
45
|
export const AUDIO_VIDEO_EXTS = new Set(['.mp3', '.wav', '.m4a', '.mp4', '.mov', '.avi', '.mkv', '.flac', '.ogg', '.webm', '.m4v']);
|
|
46
46
|
|
|
47
47
|
export const ZIP_DOCUMENT_EXTS = new Set(['.docx', '.xlsx', '.pptx', '.odt', '.ods']);
|
|
48
|
+
|
|
49
|
+
// Office zip formats with no Node-native backend — callers must route through docling
|
|
50
|
+
// or opt into the legacy zip-xml path via CONSTRUCT_ALLOW_LEGACY_EXTRACT=1.
|
|
51
|
+
const OFFICE_REQUIRES_DOCLING_EXTS = new Set(['.xlsx', '.pptx', '.odt', '.ods']);
|
|
52
|
+
|
|
53
|
+
function legacyExtractAllowed(env = process.env) {
|
|
54
|
+
return env.CONSTRUCT_ALLOW_LEGACY_EXTRACT === '1';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function makeOfficeRequiresDoclingError(extension) {
|
|
58
|
+
const err = new Error(
|
|
59
|
+
`${extension} extraction requires docling. Run \`construct install --with-docling\`, use high-fidelity ingest, or set CONSTRUCT_ALLOW_LEGACY_EXTRACT=1 for the legacy zip-xml path.`,
|
|
60
|
+
);
|
|
61
|
+
err.code = 'OFFICE_REQUIRES_DOCLING';
|
|
62
|
+
err.extension = extension;
|
|
63
|
+
return err;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makePdfRequiresDoclingError() {
|
|
67
|
+
const err = new Error(
|
|
68
|
+
'PDF extraction requires docling, the node-native unpdf backend, or CONSTRUCT_ALLOW_LEGACY_EXTRACT=1 for pdftotext/PDFKit.',
|
|
69
|
+
);
|
|
70
|
+
err.code = 'PDF_REQUIRES_DOCLING';
|
|
71
|
+
return err;
|
|
72
|
+
}
|
|
48
73
|
export const RICH_TEXT_EXTS = new Set(['.doc', '.rtf']);
|
|
49
74
|
export const MDLS_DOCUMENT_EXTS = new Set(['.xls', '.ppt', '.pages', '.numbers', '.key']);
|
|
50
75
|
export const EMAIL_DOCUMENT_EXTS = new Set(['.eml', '.msg']);
|
|
@@ -685,6 +710,9 @@ export function extractDocumentText(filePath, { maxChars = null } = {}) {
|
|
|
685
710
|
const envelope = extractCalendar(resolvedPath);
|
|
686
711
|
extracted = { text: envelope.text, method: 'ics', droppedInfo: envelope.droppedInfo, structured: envelope.structured };
|
|
687
712
|
} else if (ZIP_DOCUMENT_EXTS.has(extension)) {
|
|
713
|
+
if (!legacyExtractAllowed()) {
|
|
714
|
+
throw makeOfficeRequiresDoclingError(extension);
|
|
715
|
+
}
|
|
688
716
|
extracted = extractZipDocument(resolvedPath, extension);
|
|
689
717
|
} else if (RICH_TEXT_EXTS.has(extension)) {
|
|
690
718
|
extracted = extractRichText(resolvedPath, extension);
|
|
@@ -693,11 +721,21 @@ export function extractDocumentText(filePath, { maxChars = null } = {}) {
|
|
|
693
721
|
} else if (EMAIL_DOCUMENT_EXTS.has(extension)) {
|
|
694
722
|
extracted = extractEml(resolvedPath);
|
|
695
723
|
} else if (extension === '.pdf') {
|
|
724
|
+
if (!legacyExtractAllowed()) {
|
|
725
|
+
throw makePdfRequiresDoclingError();
|
|
726
|
+
}
|
|
696
727
|
extracted = extractPdf(resolvedPath);
|
|
697
728
|
} else {
|
|
698
729
|
throw new Error(`Unsupported document type: ${extension || 'unknown'}`);
|
|
699
730
|
}
|
|
700
731
|
|
|
732
|
+
return buildExtractionResult(resolvedPath, extension, extracted, maxChars);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Shared envelope construction so the sync, async, and Node-native paths emit
|
|
736
|
+
// an identical result shape (callers route on extractionMethod and droppedInfo).
|
|
737
|
+
|
|
738
|
+
function buildExtractionResult(resolvedPath, extension, extracted, maxChars) {
|
|
701
739
|
const text = extracted.text;
|
|
702
740
|
const limit = Number.isFinite(Number(maxChars)) && Number(maxChars) > 0
|
|
703
741
|
? Math.min(Number(maxChars), 200_000)
|
|
@@ -714,13 +752,63 @@ export function extractDocumentText(filePath, { maxChars = null } = {}) {
|
|
|
714
752
|
droppedInfo: extracted.droppedInfo ?? [],
|
|
715
753
|
structured: extracted.structured ?? null,
|
|
716
754
|
};
|
|
755
|
+
|
|
717
756
|
// Surface eml-specific metadata so callers (intake, ingest) can route
|
|
718
757
|
// attachment filenames and oversize-skip markers without re-parsing.
|
|
758
|
+
|
|
719
759
|
if (extracted.attachments) result.attachments = extracted.attachments;
|
|
720
760
|
if (extracted.skipped) result.skipped = extracted.skipped;
|
|
721
761
|
return result;
|
|
722
762
|
}
|
|
723
763
|
|
|
764
|
+
// Node-native extraction for the everyday adapter path: unpdf (PDF) and mammoth
|
|
765
|
+
// (DOCX) are optional pure-JS deps. XLSX/PPTX/ODT/ODS have no Node backend and
|
|
766
|
+
// fail loud with OFFICE_REQUIRES_DOCLING. Legacy zip-xml and pdftotext paths run
|
|
767
|
+
// only when CONSTRUCT_ALLOW_LEGACY_EXTRACT=1; otherwise extraction errors clearly.
|
|
768
|
+
|
|
769
|
+
async function extractWithUnpdf(filePath) {
|
|
770
|
+
const { extractText, getDocumentProxy } = await import('unpdf');
|
|
771
|
+
const proxy = await getDocumentProxy(new Uint8Array(readFileSync(filePath)));
|
|
772
|
+
const { text } = await extractText(proxy, { mergePages: true });
|
|
773
|
+
return normalizeText(typeof text === 'string' ? text : (Array.isArray(text) ? text.join('\n') : ''));
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function extractWithMammoth(filePath) {
|
|
777
|
+
const mammoth = (await import('mammoth')).default;
|
|
778
|
+
const { value } = await mammoth.extractRawText({ path: filePath });
|
|
779
|
+
return normalizeText(value || '');
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
export async function extractDocumentTextNodeNative(filePath, { maxChars = null } = {}) {
|
|
783
|
+
const resolvedPath = resolve(filePath);
|
|
784
|
+
if (!existsSync(resolvedPath)) throw new Error(`File not found: ${resolvedPath}`);
|
|
785
|
+
const extension = extname(resolvedPath).toLowerCase();
|
|
786
|
+
|
|
787
|
+
if (OFFICE_REQUIRES_DOCLING_EXTS.has(extension)) {
|
|
788
|
+
throw makeOfficeRequiresDoclingError(extension);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const nodeBackend = extension === '.pdf' ? { fn: extractWithUnpdf, method: 'unpdf' }
|
|
792
|
+
: extension === '.docx' ? { fn: extractWithMammoth, method: 'mammoth' }
|
|
793
|
+
: null;
|
|
794
|
+
|
|
795
|
+
if (nodeBackend) {
|
|
796
|
+
try {
|
|
797
|
+
const text = await nodeBackend.fn(resolvedPath);
|
|
798
|
+
if (text) {
|
|
799
|
+
return buildExtractionResult(resolvedPath, extension, { text, method: nodeBackend.method, droppedInfo: [], structured: null }, maxChars);
|
|
800
|
+
}
|
|
801
|
+
} catch { /* optional dep absent or parse failure — legacy path only when explicitly allowed */ }
|
|
802
|
+
if (!legacyExtractAllowed()) {
|
|
803
|
+
if (extension === '.docx') throw makeOfficeRequiresDoclingError(extension);
|
|
804
|
+
throw makePdfRequiresDoclingError();
|
|
805
|
+
}
|
|
806
|
+
return extractDocumentText(resolvedPath, { maxChars });
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return extractDocumentText(resolvedPath, { maxChars });
|
|
810
|
+
}
|
|
811
|
+
|
|
724
812
|
// Async high-fidelity extraction path. Routes PDF/Office/HTML through the
|
|
725
813
|
// docling Python sidecar (layout-aware markdown) and audio/video through
|
|
726
814
|
// whisper.cpp (Metal-accelerated on macOS). Text/transcript/calendar/email
|
package/lib/document-ingest.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from 'node:path';
|
|
12
|
-
import {
|
|
12
|
+
import { extractDocumentTextAsync, extractDocumentTextNodeNative, extractDocumentMetadata, isExtractableDocumentPath } from './document-extract.mjs';
|
|
13
13
|
import { syncFileStateToSql } from './storage/sync.mjs';
|
|
14
14
|
import { stampFrontmatter } from './doc-stamp.mjs';
|
|
15
15
|
import { KNOWLEDGE_ROOT, KNOWLEDGE_SUBDIRS } from './knowledge/layout.mjs';
|
|
@@ -157,9 +157,9 @@ function resolveOutputPath(sourcePath, { cwd, outputPath, outputDir, target }) {
|
|
|
157
157
|
// can stall (a large PDF, a slow model download) or fail. Without a bound the
|
|
158
158
|
// caller hangs: the CLI never returns, and the ingest_document MCP tool blocks
|
|
159
159
|
// until the client times out and surfaces an opaque error. Bound the whole
|
|
160
|
-
// docling attempt and fall back to the
|
|
161
|
-
// a usable result, with the fallback recorded in droppedInfo. Override
|
|
162
|
-
// with CONSTRUCT_DOCLING_TIMEOUT_MS (0 disables the timeout).
|
|
160
|
+
// docling attempt and fall back to the node-native extractor so ingest always
|
|
161
|
+
// returns a usable result, with the fallback recorded in droppedInfo. Override
|
|
162
|
+
// the bound with CONSTRUCT_DOCLING_TIMEOUT_MS (0 disables the timeout).
|
|
163
163
|
|
|
164
164
|
const DOCLING_TIMEOUT_MS = (() => {
|
|
165
165
|
const raw = Number(process.env.CONSTRUCT_DOCLING_TIMEOUT_MS);
|
|
@@ -176,15 +176,15 @@ export function withTimeout(promise, ms, onTimeoutMessage) {
|
|
|
176
176
|
|
|
177
177
|
/**
|
|
178
178
|
* Run the high-fidelity (docling) extractor with a bound, falling back to the
|
|
179
|
-
*
|
|
179
|
+
* node-native extractor on timeout or any failure so a caller never hangs. The
|
|
180
180
|
* extractor functions are injectable for testing; production uses the real
|
|
181
|
-
* docling-async and
|
|
181
|
+
* docling-async and node-native extractors.
|
|
182
182
|
*/
|
|
183
183
|
export async function extractWithDoclingFallback(sourcePath, {
|
|
184
184
|
maxChars = null,
|
|
185
185
|
timeoutMs = DOCLING_TIMEOUT_MS,
|
|
186
186
|
asyncExtract = extractDocumentTextAsync,
|
|
187
|
-
|
|
187
|
+
nodeNativeExtract = extractDocumentTextNodeNative,
|
|
188
188
|
} = {}) {
|
|
189
189
|
try {
|
|
190
190
|
return await withTimeout(
|
|
@@ -193,24 +193,28 @@ export async function extractWithDoclingFallback(sourcePath, {
|
|
|
193
193
|
`docling extraction exceeded ${Math.round(timeoutMs / 1000)}s`,
|
|
194
194
|
);
|
|
195
195
|
} catch (err) {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
...(
|
|
196
|
+
const fallback = await nodeNativeExtract(sourcePath, { maxChars });
|
|
197
|
+
fallback.droppedInfo = [
|
|
198
|
+
...(fallback.droppedInfo ?? []),
|
|
199
199
|
{
|
|
200
200
|
kind: 'docling-fallback',
|
|
201
201
|
count: 1,
|
|
202
|
-
reason: `High-fidelity (docling) extraction failed or timed out (${err.message}); used the
|
|
202
|
+
reason: `High-fidelity (docling) extraction failed or timed out (${err.message}); used the node-native extractor. Re-run after \`construct install --with-docling\`, or pass --fidelity=fast to skip docling.`,
|
|
203
203
|
recoverable: true,
|
|
204
204
|
},
|
|
205
205
|
];
|
|
206
|
-
return
|
|
206
|
+
return fallback;
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// The default adapter path prefers the Node-native extractor (unpdf/mammoth) so
|
|
211
|
+
// plain PDF/DOCX ingest needs no Python venv; high-fidelity stays on docling for
|
|
212
|
+
// scanned/layout-critical content and formats without a Node backend.
|
|
213
|
+
|
|
210
214
|
function extractViaAdapter(sourcePath, { highFidelity, maxChars }) {
|
|
211
215
|
return highFidelity
|
|
212
216
|
? extractWithDoclingFallback(sourcePath, { maxChars })
|
|
213
|
-
:
|
|
217
|
+
: extractDocumentTextNodeNative(sourcePath, { maxChars });
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
async function extractWithStrategy(sourcePath, { strategy, fallback, model, provider, highFidelity, maxChars, env }) {
|
|
@@ -349,6 +353,7 @@ export async function ingestDocuments(inputPaths, {
|
|
|
349
353
|
droppedInfo: results.flatMap((r) => r.droppedInfo.map((d) => ({ ...d, sourcePath: r.sourcePath }))),
|
|
350
354
|
droppedCount: totalDrops,
|
|
351
355
|
highFidelity,
|
|
356
|
+
fidelity: highFidelity ? 'high' : 'fast',
|
|
352
357
|
ingestion: {
|
|
353
358
|
strategy: resolvedStrategy.strategy,
|
|
354
359
|
fallback: resolvedStrategy.fallback,
|
|
@@ -368,7 +373,7 @@ export async function runIngestCli(argv = process.argv.slice(2), { cwd = process
|
|
|
368
373
|
let target = 'knowledge/internal';
|
|
369
374
|
let sync = false;
|
|
370
375
|
let strict = false;
|
|
371
|
-
let
|
|
376
|
+
let fidelity = 'high';
|
|
372
377
|
let strategy = null;
|
|
373
378
|
let orchestration = null;
|
|
374
379
|
|
|
@@ -379,21 +384,30 @@ export async function runIngestCli(argv = process.argv.slice(2), { cwd = process
|
|
|
379
384
|
else if (arg.startsWith('--strategy=')) strategy = arg.split('=').slice(1).join('=');
|
|
380
385
|
else if (arg === '--strategy') strategy = '';
|
|
381
386
|
else if (arg.startsWith('--orchestration=')) orchestration = arg.split('=').slice(1).join('=');
|
|
382
|
-
else if (arg
|
|
387
|
+
else if (arg.startsWith('--fidelity=')) {
|
|
388
|
+
const value = arg.split('=').slice(1).join('=');
|
|
389
|
+
if (value !== 'fast' && value !== 'high') {
|
|
390
|
+
throw new Error(`Unsupported fidelity: ${value}. Valid values: fast, high`);
|
|
391
|
+
}
|
|
392
|
+
fidelity = value;
|
|
393
|
+
} else if (arg === '--sync') sync = true;
|
|
383
394
|
else if (arg === '--strict') strict = true;
|
|
384
|
-
else if (arg === '--legacy-extractor')
|
|
395
|
+
else if (arg === '--legacy-extractor') fidelity = 'fast';
|
|
385
396
|
else if (strategy === '') strategy = arg;
|
|
386
397
|
else inputs.push(arg);
|
|
387
398
|
}
|
|
388
399
|
|
|
400
|
+
const highFidelity = fidelity === 'high';
|
|
401
|
+
|
|
389
402
|
if (inputs.length === 0) {
|
|
390
403
|
throw new Error(
|
|
391
404
|
`Usage: construct ingest <file-or-dir> [more paths] [--out=FILE] [--out-dir=DIR] ` +
|
|
392
|
-
`[--target=sibling|knowledge/<subdir>] [--strategy=adapter|provider] [--orchestration=prompt-only|orchestrated] [--sync] [--strict] [--
|
|
405
|
+
`[--target=sibling|knowledge/<subdir>] [--strategy=adapter|provider] [--orchestration=prompt-only|orchestrated] [--sync] [--strict] [--fidelity=fast|high]\n` +
|
|
393
406
|
` --strategy: extraction strategy override (adapter = local extractors; provider = configured provider/model)\n` +
|
|
394
407
|
` --orchestration: prompt-only (deterministic extraction) or orchestrated (engage the specialist chain)\n` +
|
|
395
408
|
` --strict: exit non-zero if any extraction drops occur\n` +
|
|
396
|
-
` --
|
|
409
|
+
` --fidelity=fast|high: fast = node-native extractors; high = docling (default)\n` +
|
|
410
|
+
` --legacy-extractor: deprecated alias for --fidelity=fast\n` +
|
|
397
411
|
` knowledge subdirs: ${KNOWLEDGE_SUBDIRS.join(', ')}`,
|
|
398
412
|
);
|
|
399
413
|
}
|
package/lib/embed/cli.mjs
CHANGED
|
@@ -392,9 +392,9 @@ async function cmdEmbedSnapshot(args, { homeDir = os.homedir() } = {}) {
|
|
|
392
392
|
const { loadEmbedConfig, EMPTY_CONFIG } = await import('./config.mjs');
|
|
393
393
|
const { SnapshotEngine, renderMarkdown } = await import('./snapshot.mjs');
|
|
394
394
|
const { ProviderRegistry } = await import('./providers/registry.mjs');
|
|
395
|
-
const {
|
|
395
|
+
const { prepareConstructEnv } = await import('../runtime-env.mjs');
|
|
396
396
|
|
|
397
|
-
const env =
|
|
397
|
+
const env = prepareConstructEnv();
|
|
398
398
|
const registry = await ProviderRegistry.fromEnv(env);
|
|
399
399
|
|
|
400
400
|
let cfg;
|
package/lib/embed/daemon.mjs
CHANGED
|
@@ -58,54 +58,11 @@ function logCatch(source, err) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* Resolve an LLM API key
|
|
62
|
-
* Supports 1Password op:// refs in .zshrc or other shell config files for the
|
|
63
|
-
* telemetry-generation probe.
|
|
61
|
+
* Resolve an LLM API key via the shared secret resolver (cached op read per process).
|
|
64
62
|
*/
|
|
65
63
|
async function resolveLLMKey(varName, env) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const files = [join(homeDir, '.construct', 'config.env'), join(homeDir, '.env')];
|
|
69
|
-
for (const file of files) {
|
|
70
|
-
try {
|
|
71
|
-
if (!existsSync(file)) continue;
|
|
72
|
-
const content = readFileSync(file, 'utf8');
|
|
73
|
-
const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
|
|
74
|
-
if (m && m[1]) return m[1].trim();
|
|
75
|
-
} catch { /* skip */ }
|
|
76
|
-
}
|
|
77
|
-
// Check shell rc files for exports and command substitutions (including op:// refs)
|
|
78
|
-
const rcs = [join(homeDir, '.zshrc'), join(homeDir, '.bashrc'), join(homeDir, '.bash_profile'), join(homeDir, '.profile')];
|
|
79
|
-
const { spawnSync } = await import('child_process');
|
|
80
|
-
for (const rc of rcs) {
|
|
81
|
-
if (!existsSync(rc)) continue;
|
|
82
|
-
try {
|
|
83
|
-
const content = readFileSync(rc, 'utf8');
|
|
84
|
-
|
|
85
|
-
// op:// ref via 1Password CLI
|
|
86
|
-
const opRe = new RegExp(`export\\s+${varName}=["']?\\$\\(op read '([^']+)'\\)["']?`, 'm');
|
|
87
|
-
const opMatch = content.match(opRe);
|
|
88
|
-
if (opMatch) {
|
|
89
|
-
try {
|
|
90
|
-
const r = spawnSync('op', ['read', opMatch[1]], { encoding: 'utf8', timeout: 5000 });
|
|
91
|
-
if (r.status === 0 && r.stdout?.trim()) return r.stdout.trim();
|
|
92
|
-
} catch { /* op read failed */ }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Source the rc file and echo — handles any $(...) command substitution
|
|
96
|
-
const anyExportRe = new RegExp(`^\\s*export\\s+${varName}=`, 'm');
|
|
97
|
-
if (anyExportRe.test(content)) {
|
|
98
|
-
try {
|
|
99
|
-
const r = spawnSync('zsh', ['-c', `source "${rc}" 2>/dev/null; echo "\${${varName}}"`], { encoding: 'utf8', timeout: 5000 });
|
|
100
|
-
if (r.status === 0 && r.stdout?.trim()) {
|
|
101
|
-
const val = r.stdout.trim().split('\n').pop()?.trim();
|
|
102
|
-
if (val && !val.includes('ERROR') && !val.includes('op read') && !val.startsWith('$(')) return val;
|
|
103
|
-
}
|
|
104
|
-
} catch { /* shell eval failed */ }
|
|
105
|
-
}
|
|
106
|
-
} catch { /* skip */ }
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
64
|
+
const { resolveSecret } = await import('../providers/secret-resolver.mjs');
|
|
65
|
+
return resolveSecret(varName, { env, cwd: process.cwd() });
|
|
109
66
|
}
|
|
110
67
|
|
|
111
68
|
// ─── Root dir resolution ─────────────────────────────────────────────────────
|
|
@@ -614,6 +571,21 @@ export class EmbedDaemon {
|
|
|
614
571
|
{ runImmediately: true },
|
|
615
572
|
);
|
|
616
573
|
|
|
574
|
+
// ── Job 7: rss-cap ────────────────────────────────────────────────────────
|
|
575
|
+
const { memoryCapMbFor, rssOverCap, currentRssMb } = await import('../resources/process-budget.mjs');
|
|
576
|
+
const rssCapMb = memoryCapMbFor('embed-daemon', this.#rootDir, this.#env);
|
|
577
|
+
this.#scheduler.register(
|
|
578
|
+
'rss-cap',
|
|
579
|
+
60_000,
|
|
580
|
+
async () => {
|
|
581
|
+
if (!rssOverCap(rssCapMb)) return;
|
|
582
|
+
process.stderr.write(
|
|
583
|
+
`[embed] RSS ${Math.round(currentRssMb())}MB exceeds cap ${rssCapMb}MB — stopping daemon\n`,
|
|
584
|
+
);
|
|
585
|
+
this.stop();
|
|
586
|
+
},
|
|
587
|
+
);
|
|
588
|
+
|
|
617
589
|
// ── Job 0: telemetry-setup ───────────────────────────────────────────────────
|
|
618
590
|
// On startup — ensure annotation queues and eval configs exist (idempotent)
|
|
619
591
|
this.#scheduler.register(
|
package/lib/embed/semantic.mjs
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generates embeddings via @huggingface/transformers, caches them to disk,
|
|
5
5
|
* computes cosine similarity, and clusters related signals by topic.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Inference is local and offline (allowRemoteModels=false): the model loads
|
|
7
|
+
* from local cache only and returns null when no cached weights are present,
|
|
8
|
+
* never reaching the network. Default model is all-MiniLM-L6-v2 (384-dimensional
|
|
9
|
+
* vectors, ~50MB on disk, fast inference).
|
|
8
10
|
*
|
|
9
11
|
* Storage:
|
|
10
12
|
* ~/.cx/cache/embeddings/<sha256>.json — single embedding vector
|
|
@@ -37,6 +39,7 @@ async function getEmbedder() {
|
|
|
37
39
|
try {
|
|
38
40
|
const { pipeline, env: hfEnv } = await import('@huggingface/transformers');
|
|
39
41
|
hfEnv.allowLocalModels = true;
|
|
42
|
+
hfEnv.allowRemoteModels = false;
|
|
40
43
|
hfEnv.useBrowserCache = false;
|
|
41
44
|
embedder = await pipeline('feature-extraction', MODEL_NAME, {
|
|
42
45
|
quantized: true,
|
package/lib/embed/worker.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import {
|
|
11
|
+
import { prepareConstructEnv } from '../runtime-env.mjs';
|
|
12
12
|
import { EmbedDaemon } from './daemon.mjs';
|
|
13
13
|
|
|
14
14
|
function parseArgv(argv) {
|
|
@@ -19,7 +19,7 @@ function parseArgv(argv) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const configPath = parseArgv(process.argv.slice(2));
|
|
22
|
-
const env =
|
|
22
|
+
const env = prepareConstructEnv();
|
|
23
23
|
|
|
24
24
|
// If no config file, build an auto-discovered config inline and inject it
|
|
25
25
|
let daemonOpts = { env };
|
|
@@ -61,15 +61,15 @@ function matchCapabilities(modelId, requested, warnings) {
|
|
|
61
61
|
return matched;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function familyByProvider(hostProvider, env) {
|
|
64
|
+
function familyByProvider(hostProvider, env, allowAmbient) {
|
|
65
65
|
if (!hostProvider || typeof hostProvider !== 'string') return null;
|
|
66
|
-
const families = listModelFamilies({ env });
|
|
66
|
+
const families = listModelFamilies({ env, allowAmbient });
|
|
67
67
|
return families.find((f) => f.id === hostProvider)
|
|
68
68
|
|| families.find((f) => f.id === `openrouter-${hostProvider}`)
|
|
69
69
|
|| null;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function success({ selectedModel, family, tier, resolutionSource, fallbackReason = null, tierSource = null, capabilities, warnings, env }) {
|
|
72
|
+
function success({ selectedModel, family, tier, resolutionSource, fallbackReason = null, tierSource = null, capabilities, warnings, env, allowAmbient }) {
|
|
73
73
|
return {
|
|
74
74
|
selectedModel,
|
|
75
75
|
selectedProvider: family ? family.id : null,
|
|
@@ -123,28 +123,28 @@ function configError({ tier, reason, remediation, warnings }) {
|
|
|
123
123
|
* @param {string} [opts.registryPath]
|
|
124
124
|
* @returns {object}
|
|
125
125
|
*/
|
|
126
|
-
export function resolveEmbeddedModel(request = {}, { env = process.env, registryPath = null } = {}) {
|
|
126
|
+
export function resolveEmbeddedModel(request = {}, { env = process.env, registryPath = null, allowAmbient = env === process.env } = {}) {
|
|
127
127
|
const { workflowType, requestedTier, hostModel, hostProvider, capabilities, allowCrossProviderFallback = false } = request;
|
|
128
128
|
const tier = resolveTier({ requestedTier, workflowType });
|
|
129
129
|
const warnings = [];
|
|
130
130
|
|
|
131
|
-
const hostFamily = hostModel ? describeModelFamily(hostModel, { env }) : null;
|
|
131
|
+
const hostFamily = hostModel ? describeModelFamily(hostModel, { env, allowAmbient }) : null;
|
|
132
132
|
|
|
133
133
|
if (hostModel && hostFamily) {
|
|
134
|
-
return success({ selectedModel: hostModel, family: hostFamily, tier, resolutionSource: 'host-model', capabilities, warnings, env });
|
|
134
|
+
return success({ selectedModel: hostModel, family: hostFamily, tier, resolutionSource: 'host-model', capabilities, warnings, env, allowAmbient });
|
|
135
135
|
}
|
|
136
136
|
if (hostModel && !hostFamily) {
|
|
137
137
|
warnings.push(`Host model "${hostModel}" is not a recognized provider family; attempting same-family or tier fallback.`);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
const family = hostFamily || familyByProvider(hostProvider, env);
|
|
140
|
+
const family = hostFamily || familyByProvider(hostProvider, env, allowAmbient);
|
|
141
141
|
if (family) {
|
|
142
142
|
const model = family.tiers[tier];
|
|
143
143
|
if (model) {
|
|
144
144
|
const fallbackReason = hostModel
|
|
145
145
|
? `Exact host model unavailable; resolved the ${tier} model in the same provider family (${family.id}).`
|
|
146
146
|
: `Resolved the ${tier} model within the host provider family (${family.id}).`;
|
|
147
|
-
return success({ selectedModel: model, family, tier, resolutionSource: 'same-family-fallback', fallbackReason, capabilities, warnings, env });
|
|
147
|
+
return success({ selectedModel: model, family, tier, resolutionSource: 'same-family-fallback', fallbackReason, capabilities, warnings, env, allowAmbient });
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -166,7 +166,7 @@ export function resolveEmbeddedModel(request = {}, { env = process.env, registry
|
|
|
166
166
|
: 'No host context supplied; used the Construct tier default.';
|
|
167
167
|
return success({
|
|
168
168
|
selectedModel: defaultModel,
|
|
169
|
-
family: describeModelFamily(defaultModel, { env }),
|
|
169
|
+
family: describeModelFamily(defaultModel, { env, allowAmbient }),
|
|
170
170
|
tier,
|
|
171
171
|
resolutionSource: 'tier-default',
|
|
172
172
|
fallbackReason,
|
|
@@ -174,6 +174,7 @@ export function resolveEmbeddedModel(request = {}, { env = process.env, registry
|
|
|
174
174
|
capabilities,
|
|
175
175
|
warnings,
|
|
176
176
|
env,
|
|
177
|
+
allowAmbient,
|
|
177
178
|
});
|
|
178
179
|
}
|
|
179
180
|
|
package/lib/env-config.mjs
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { API_KEY_CREDENTIALS } from './providers/credential-catalog.mjs';
|
|
12
|
+
import { extractOpRef } from './providers/secret-resolver.mjs';
|
|
11
13
|
|
|
12
14
|
export function parseEnvFile(filePath) {
|
|
13
15
|
if (!filePath || !fs.existsSync(filePath)) return {};
|
|
@@ -88,6 +90,22 @@ const ENV_SHADOW_WARN = new Set([
|
|
|
88
90
|
'CONSTRUCT_DEPLOYMENT_MODE',
|
|
89
91
|
]);
|
|
90
92
|
|
|
93
|
+
const CREDENTIAL_ENV_KEYS = new Set(
|
|
94
|
+
API_KEY_CREDENTIALS.flatMap((entry) => entry.envVars),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
function isOpReference(value) {
|
|
98
|
+
return Boolean(extractOpRef(value));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function preferInjectedCredential(envValue, fileValue) {
|
|
102
|
+
return Boolean(
|
|
103
|
+
envValue
|
|
104
|
+
&& !isOpReference(envValue)
|
|
105
|
+
&& isOpReference(fileValue),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
91
109
|
export function loadConstructEnv({ rootDir, homeDir = os.homedir(), env = process.env, warn = true } = {}) {
|
|
92
110
|
const rootEnv = rootDir ? parseEnvFile(path.join(rootDir, '.env')) : {};
|
|
93
111
|
const userEnv = parseEnvFile(getUserEnvPath(homeDir));
|
|
@@ -103,6 +121,7 @@ export function loadConstructEnv({ rootDir, homeDir = os.homedir(), env = proces
|
|
|
103
121
|
// silently and no user action is needed.
|
|
104
122
|
if (!env[key]) continue;
|
|
105
123
|
if (key in fileEnv && fileEnv[key] !== env[key]) {
|
|
124
|
+
if (preferInjectedCredential(env[key], fileEnv[key])) continue;
|
|
106
125
|
process.stderr.write(
|
|
107
126
|
`[construct] WARNING: process.env.${key} (${env[key].slice(0, 6)}…) ` +
|
|
108
127
|
`differs from config.env value (${fileEnv[key].slice(0, 6)}…). ` +
|
|
@@ -115,7 +134,14 @@ export function loadConstructEnv({ rootDir, homeDir = os.homedir(), env = proces
|
|
|
115
134
|
|
|
116
135
|
// process.env is the base; config.env (file) values override shell values
|
|
117
136
|
// so that credentials set in the dashboard always win over stale shell exports.
|
|
137
|
+
// Materialized credentials from `op run` (or an explicit export) win over stored
|
|
138
|
+
// op:// references so one 1Password auth per invocation is enough.
|
|
118
139
|
const merged = { ...env, ...fileEnv };
|
|
140
|
+
for (const key of CREDENTIAL_ENV_KEYS) {
|
|
141
|
+
if (preferInjectedCredential(env[key], fileEnv[key])) {
|
|
142
|
+
merged[key] = env[key];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
119
145
|
const databaseUrl = resolveDatabaseUrl(merged);
|
|
120
146
|
if (databaseUrl && !cleanEnvValue(merged.DATABASE_URL)) {
|
|
121
147
|
merged.DATABASE_URL = databaseUrl;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/evals/dataset.mjs — provenance-backed evaluation dataset: validation,
|
|
3
|
+
* leakage-safe splitting, expiry, and held-out selection (construct-6zga.1.6).
|
|
4
|
+
*
|
|
5
|
+
* A dataset item carries the provenance the governed improvement loop needs to
|
|
6
|
+
* evaluate a candidate on data it did not generate: source trace ids, human-label
|
|
7
|
+
* provenance, redaction state, a capability snapshot, the allowed tools, the
|
|
8
|
+
* expected evidence and contract behavior, a split assignment, and an expiry.
|
|
9
|
+
* Split assignment is deterministic and keyed on task family plus source trace, so
|
|
10
|
+
* every item from one trace lands in the same split — a candidate's generating
|
|
11
|
+
* trace can never leak across the train/dev/test boundary. selectEvalSet then
|
|
12
|
+
* strips the generating trace, near-duplicates, and the same user correction from
|
|
13
|
+
* the evaluation set so a candidate is never scored on its own origin. Reference
|
|
14
|
+
* shape: schemas/eval-dataset.schema.json.
|
|
15
|
+
*/
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
|
|
18
|
+
export const EVAL_DATASET_SCHEMA_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
export const DATASET_SPLITS = Object.freeze(['train', 'dev', 'test']);
|
|
21
|
+
export const REDACTION_STATES = Object.freeze(['raw', 'redacted', 'synthetic']);
|
|
22
|
+
export const EVIDENCE_REQUIREMENTS = Object.freeze(['none', 'preferred', 'required']);
|
|
23
|
+
export const LABEL_PROVENANCE = Object.freeze(['human', 'model-assisted', 'unlabeled']);
|
|
24
|
+
export const CONTRACT_OUTCOMES = Object.freeze(['pass', 'fail', 'blocker', 'handoff']);
|
|
25
|
+
export const CAPABILITY_CLASSES = Object.freeze(['hosted-direct', 'hosted-routed', 'local-capable', 'local-constrained', 'unknown']);
|
|
26
|
+
|
|
27
|
+
function hashBucket(text) {
|
|
28
|
+
const digest = crypto.createHash('sha256').update(String(text)).digest();
|
|
29
|
+
return digest.readUInt32BE(0) % 100;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizePrompt(text) {
|
|
33
|
+
return String(text || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function promptOf(item) {
|
|
37
|
+
return item?.taskInput?.prompt ?? item?.prompt ?? '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function nearDuplicateKey(item) {
|
|
41
|
+
const normalized = normalizePrompt(promptOf(item));
|
|
42
|
+
if (!normalized) return null;
|
|
43
|
+
return crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 16);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function primarySourceTrace(item) {
|
|
47
|
+
return Array.isArray(item?.sourceTraceIds) && item.sourceTraceIds.length ? item.sourceTraceIds[0] : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Deterministically assign a split. The bucket is keyed on task family plus the
|
|
52
|
+
* item's primary source trace, so all items sharing a trace co-locate in one
|
|
53
|
+
* split. devRatio/testRatio are fractions of the bucket space; the remainder is
|
|
54
|
+
* train.
|
|
55
|
+
*/
|
|
56
|
+
export function assignSplit(item, { devRatio = 0.15, testRatio = 0.15 } = {}) {
|
|
57
|
+
const trace = primarySourceTrace(item) || item?.id || '';
|
|
58
|
+
const bucket = hashBucket(`${item?.taskFamily || ''}:${trace}`);
|
|
59
|
+
const testCut = Math.round(testRatio * 100);
|
|
60
|
+
const devCut = testCut + Math.round(devRatio * 100);
|
|
61
|
+
if (bucket < testCut) return 'test';
|
|
62
|
+
if (bucket < devCut) return 'dev';
|
|
63
|
+
return 'train';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isExpired(item, nowIso) {
|
|
67
|
+
if (!item?.expiry || !nowIso) return false;
|
|
68
|
+
return String(item.expiry) <= String(nowIso);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Strip every item that would leak into a candidate's evaluation: the candidate's
|
|
73
|
+
* own generating / source traces, a near-duplicate task input, and the same user
|
|
74
|
+
* correction. This is the held-out guarantee (construct-6zga.1.6 AC2).
|
|
75
|
+
*/
|
|
76
|
+
export function excludeLeakage(items, candidate = {}) {
|
|
77
|
+
const list = Array.isArray(items) ? items : [];
|
|
78
|
+
const generatingTraces = new Set([
|
|
79
|
+
...(Array.isArray(candidate.generatingTraceIds) ? candidate.generatingTraceIds : []),
|
|
80
|
+
...(Array.isArray(candidate.sourceTraceIds) ? candidate.sourceTraceIds : []),
|
|
81
|
+
]);
|
|
82
|
+
const candidateDupKey = nearDuplicateKey(candidate);
|
|
83
|
+
const candidateCorrection = candidate.correctionId ?? candidate.humanLabel?.correctionId ?? null;
|
|
84
|
+
|
|
85
|
+
return list.filter((item) => {
|
|
86
|
+
const traces = Array.isArray(item?.sourceTraceIds) ? item.sourceTraceIds : [];
|
|
87
|
+
if (traces.some((t) => generatingTraces.has(t))) return false;
|
|
88
|
+
if (candidateDupKey && nearDuplicateKey(item) === candidateDupKey) return false;
|
|
89
|
+
const correction = item?.humanLabel?.correctionId ?? null;
|
|
90
|
+
if (candidateCorrection && correction && correction === candidateCorrection) return false;
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build the evaluation set for a candidate: the items in the requested split,
|
|
97
|
+
* minus expired items, minus anything that would leak (excludeLeakage).
|
|
98
|
+
*/
|
|
99
|
+
export function selectEvalSet(items, candidate = {}, { split = 'test', nowIso = null } = {}) {
|
|
100
|
+
const list = Array.isArray(items) ? items : [];
|
|
101
|
+
const inSplit = list.filter((item) => item?.split === split && !isExpired(item, nowIso));
|
|
102
|
+
return excludeLeakage(inSplit, candidate);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Hand-rolled validator (no ajv — Construct stays dependency-free at startup).
|
|
107
|
+
* Returns { valid, errors } against schemas/eval-dataset.schema.json.
|
|
108
|
+
*/
|
|
109
|
+
export function validateDatasetItem(item) {
|
|
110
|
+
const errors = [];
|
|
111
|
+
if (!item || typeof item !== 'object') return { valid: false, errors: ['item is not an object'] };
|
|
112
|
+
if (item.schemaVersion !== EVAL_DATASET_SCHEMA_VERSION) errors.push(`schemaVersion must be ${EVAL_DATASET_SCHEMA_VERSION}`);
|
|
113
|
+
if (typeof item.id !== 'string' || !item.id) errors.push('id required');
|
|
114
|
+
if (typeof item.taskFamily !== 'string' || !item.taskFamily) errors.push('taskFamily required');
|
|
115
|
+
if (!item.taskInput || typeof item.taskInput.prompt !== 'string') errors.push('taskInput.prompt required');
|
|
116
|
+
|
|
117
|
+
if (!item.capabilitySnapshot || !CAPABILITY_CLASSES.includes(item.capabilitySnapshot.capabilityClass)) {
|
|
118
|
+
errors.push('capabilitySnapshot.capabilityClass invalid');
|
|
119
|
+
}
|
|
120
|
+
if (!Array.isArray(item.allowedTools)) errors.push('allowedTools must be an array');
|
|
121
|
+
|
|
122
|
+
const ev = item.expectedEvidenceBehavior;
|
|
123
|
+
if (!ev || !EVIDENCE_REQUIREMENTS.includes(ev.requirement) || typeof ev.citationsRequired !== 'boolean') {
|
|
124
|
+
errors.push('expectedEvidenceBehavior invalid');
|
|
125
|
+
}
|
|
126
|
+
if (!item.expectedContractResult || !CONTRACT_OUTCOMES.includes(item.expectedContractResult.outcome)) {
|
|
127
|
+
errors.push('expectedContractResult.outcome invalid');
|
|
128
|
+
}
|
|
129
|
+
if (!item.redaction || !REDACTION_STATES.includes(item.redaction.state)) errors.push('redaction.state invalid');
|
|
130
|
+
|
|
131
|
+
if (!Array.isArray(item.sourceTraceIds) || item.sourceTraceIds.length < 1) errors.push('sourceTraceIds must be non-empty');
|
|
132
|
+
if (!item.humanLabel || !LABEL_PROVENANCE.includes(item.humanLabel.provenance)) errors.push('humanLabel.provenance invalid');
|
|
133
|
+
if (!DATASET_SPLITS.includes(item.split)) errors.push(`split invalid: ${item.split}`);
|
|
134
|
+
if (item.expiry !== null && typeof item.expiry !== 'string') errors.push('expiry must be an ISO string or null');
|
|
135
|
+
|
|
136
|
+
return { valid: errors.length === 0, errors };
|
|
137
|
+
}
|