@geraldmaron/construct 1.2.2 → 1.3.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 +7 -10
- package/bin/construct +286 -220
- package/bin/construct-postinstall.mjs +0 -21
- package/commands/work/optimize-prompts.md +1 -1
- package/examples/distribution/sources/adr.md +2 -2
- package/examples/seed-observations/decisions.md +1 -1
- package/lib/artifact-loop-core.mjs +412 -0
- package/lib/artifact-manifest.mjs +6 -0
- package/lib/artifact-release-gate.mjs +115 -49
- package/lib/audit-rules.mjs +2 -4
- package/lib/audit-skills.mjs +32 -20
- package/lib/audit-specialists.mjs +44 -26
- package/lib/auto-docs.mjs +6 -5
- package/lib/brand-prose.mjs +3 -3
- package/lib/brand-tokens.mjs +2 -2
- package/lib/certification/artifact-fixtures.mjs +4 -1
- package/lib/certification/demo-parity.mjs +6 -32
- package/lib/certification/real-llm-scenarios.mjs +58 -13
- package/lib/certification/role-cards.mjs +7 -6
- package/lib/certification/role-overlays.mjs +4 -4
- package/lib/certification/runner.mjs +9 -2
- package/lib/certification/skill-inventory.mjs +34 -22
- package/lib/certification/skill-scenarios.mjs +21 -8
- package/lib/certification/specialist-contracts.mjs +4 -3
- package/lib/certification/specialist-scenarios.mjs +7 -6
- package/lib/certification/status.mjs +5 -4
- package/lib/cli-commands.mjs +56 -57
- package/lib/comment-lint.mjs +1 -5
- package/lib/completions.mjs +20 -2
- package/lib/config/legacy-config-migration.mjs +125 -0
- package/lib/config/schema.mjs +5 -5
- package/lib/config/source-targets.mjs +80 -0
- package/lib/contracts/validate.mjs +119 -51
- package/lib/decisions/golden.mjs +3 -2
- package/lib/decisions/registry.mjs +11 -6
- package/lib/demo-project.mjs +188 -0
- package/lib/demo-recording.mjs +9 -34
- package/lib/demo-script.mjs +2 -2
- package/lib/demo-surface.mjs +13 -131
- package/lib/demo-tour-renderer.mjs +92 -0
- package/lib/demo.mjs +69 -54
- package/lib/diagram-export.mjs +63 -14
- package/lib/doctor/source-checkout.mjs +3 -4
- package/lib/doctor/watchers/consistency.mjs +90 -22
- package/lib/doctor/watchers/credential-parity.mjs +97 -0
- package/lib/doctor/watchers/mcp-protocol.mjs +1 -1
- package/lib/doctor/watchers/service-health.mjs +4 -32
- package/lib/document-export.mjs +73 -9
- package/lib/document-extract.mjs +10 -198
- package/lib/embed/artifact.mjs +2 -3
- package/lib/embed/cli.mjs +1 -1
- package/lib/embed/config.mjs +1 -2
- package/lib/embed/customer-profiles.mjs +1 -1
- package/lib/embed/daemon.mjs +5 -6
- package/lib/embed/demand-fetch.mjs +114 -2
- package/lib/embed/docs-lifecycle.mjs +9 -5
- package/lib/embed/intake-metrics.mjs +2 -2
- package/lib/embed/notifications.mjs +2 -3
- package/lib/embed/recommendation-store.mjs +25 -22
- package/lib/embed/role-framing.mjs +4 -9
- package/lib/embedded-contract/capability.mjs +4 -2
- package/lib/embedded-contract/contract-version.mjs +1 -1
- package/lib/env-config.mjs +2 -2
- package/lib/flavors/loader.mjs +21 -19
- package/lib/graph/build-from-registry.mjs +31 -7
- package/lib/graph/staleness.mjs +1 -1
- package/lib/headhunt.mjs +82 -35
- package/lib/hooks/agent-tracker.mjs +2 -2
- package/lib/hooks/config-protection.mjs +1 -1
- package/lib/hooks/registry-sync.mjs +3 -3
- package/lib/host-capabilities.mjs +5 -5
- package/lib/improvement/controller.mjs +2 -2
- package/lib/init-unified.mjs +19 -21
- package/lib/intake/classify.mjs +26 -0
- package/lib/intake/daemon.mjs +2 -3
- package/lib/intake/prepare.mjs +10 -3
- package/lib/intake/session-prelude.mjs +1 -1
- package/lib/intake/tables/creative.mjs +9 -9
- package/lib/intake/tables/operations.mjs +4 -4
- package/lib/intake/tables/research.mjs +2 -2
- package/lib/integrations/intake-integrations.mjs +9 -10
- package/lib/knowledge/research-store.mjs +2 -2
- package/lib/mcp/server.mjs +104 -61
- package/lib/mcp/tool-budget.mjs +1 -46
- package/lib/mcp/tool-recovery.mjs +44 -0
- package/lib/mcp/tools/artifact-author.mjs +36 -0
- package/lib/mcp/tools/find-tool.mjs +86 -0
- package/lib/mcp/tools/orchestration-run.mjs +107 -69
- package/lib/mcp/tools/project.mjs +4 -3
- package/lib/mcp/tools/{profile.mjs → scope.mjs} +54 -52
- package/lib/mcp/tools/skills.mjs +39 -22
- package/lib/mcp/tools/telemetry.mjs +2 -1
- package/lib/mcp/tools/workflow.mjs +1 -1
- package/lib/mcp-platform-config.mjs +7 -5
- package/lib/migrations/index.mjs +4 -2
- package/lib/migrations/v2-unified-registry.mjs +35 -0
- package/lib/model-router.mjs +203 -36
- package/lib/models/catalog.mjs +25 -9
- package/lib/models/execution-capability-profile.mjs +3 -3
- package/lib/models/execution-policy.mjs +7 -6
- package/lib/models/provider-poll.mjs +18 -4
- package/lib/opencode-config.mjs +56 -1
- package/lib/opencode-runtime-plugin.mjs +13 -10
- package/lib/oracle/artifact-gate.mjs +14 -4
- package/lib/oracle/cli.mjs +2 -0
- package/lib/oracle/dispatch.mjs +18 -1
- package/lib/oracle/execute.mjs +39 -7
- package/lib/oracle/index.mjs +1 -1
- package/lib/oracle/org-graph.mjs +10 -5
- package/lib/oracle/policy.mjs +6 -0
- package/lib/oracle/read-model.mjs +103 -6
- package/lib/oracle/reconcile.mjs +31 -4
- package/lib/oracle/remediation-dispatch.mjs +53 -0
- package/lib/oracle/routing.mjs +5 -0
- package/lib/oracle/synthesize.mjs +80 -3
- package/lib/orchestration/routing-tables.mjs +44 -10
- package/lib/orchestration/runtime.mjs +2 -0
- package/lib/orchestration/worker.mjs +1 -1
- package/lib/orchestration-policy.mjs +351 -62
- package/lib/overrides/resolver.mjs +1 -12
- package/lib/parity.mjs +37 -11
- package/lib/policy/engine.mjs +80 -15
- package/lib/prompt-composer.js +51 -15
- package/lib/prompt-metadata.mjs +3 -2
- package/lib/providers/connection-probe.mjs +58 -0
- package/lib/providers/copilot-auth.mjs +1 -1
- package/lib/providers/credential-bootstrap.mjs +1 -1
- package/lib/providers/op-run.mjs +3 -4
- package/lib/providers/secret-resolver.mjs +31 -0
- package/lib/publish-tooling.mjs +3 -11
- package/lib/publish.mjs +12 -29
- package/lib/reconcile/mcp-entry-reconcile.mjs +14 -9
- package/lib/reflect.mjs +2 -2
- package/lib/registry/agent-manifest.mjs +190 -0
- package/lib/registry/assemble.mjs +133 -0
- package/lib/registry/catalog.mjs +171 -0
- package/lib/registry/cli.mjs +590 -6
- package/lib/registry/consolidation.mjs +5 -4
- package/lib/registry/docs-sync.mjs +67 -0
- package/lib/registry/loader.mjs +147 -0
- package/lib/registry/org-io.mjs +76 -0
- package/lib/registry/retired-paths.mjs +71 -0
- package/lib/registry/surface-map.mjs +5 -7
- package/lib/registry/validator.mjs +438 -0
- package/lib/research-execution-policy.mjs +173 -0
- package/lib/roles/catalog.mjs +4 -9
- package/lib/roles/fence.mjs +107 -1
- package/lib/roles/flavor-bindings.mjs +68 -0
- package/lib/roles/gateway.mjs +140 -29
- package/lib/roles/manifest.mjs +22 -13
- package/lib/roles/router.mjs +1 -1
- package/lib/runtime-env.mjs +1 -1
- package/lib/scopes/enrich.mjs +67 -0
- package/lib/scopes/hooks.mjs +12 -0
- package/lib/{profiles → scopes}/lifecycle.mjs +70 -70
- package/lib/scopes/loader.mjs +104 -0
- package/lib/scopes/rebrand.mjs +32 -0
- package/lib/scopes/research-profile.mjs +16 -0
- package/lib/scopes/teams.mjs +98 -0
- package/lib/service-manager.mjs +1 -117
- package/lib/setup-prompts.mjs +1 -1
- package/lib/setup.mjs +17 -28
- package/lib/skills/composition-graph.mjs +98 -0
- package/lib/skills/router.mjs +80 -0
- package/lib/specialist-contracts.mjs +17 -18
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/specialists/prompt-schema.mjs +6 -5
- package/lib/specialists/roster.mjs +7 -12
- package/lib/specialists/schema.mjs +9 -6
- package/lib/status.mjs +25 -90
- package/lib/storage/file-lock.mjs +6 -3
- package/lib/telemetry/skill-calls.mjs +1 -1
- package/lib/templates/doc-presentation.mjs +63 -0
- package/lib/templates/visual-requirements.mjs +35 -2
- package/lib/term-format.mjs +107 -2
- package/lib/test-env-setup.mjs +21 -0
- package/lib/ui/components.mjs +42 -0
- package/lib/ui/glyphs.mjs +58 -0
- package/lib/ui/links.mjs +115 -0
- package/lib/ui/theme.mjs +108 -0
- package/lib/uninstall/uninstall.mjs +2 -1
- package/lib/validator.mjs +45 -8
- package/lib/validators/skill-effectiveness.mjs +174 -0
- package/lib/validators/skills.mjs +1 -1
- package/package.json +12 -7
- package/personas/construct.md +12 -3
- package/platforms/claude/CLAUDE.md +1 -1
- package/rules/common/beads-hygiene.md +52 -0
- package/rules/common/neurodivergent-output.md +4 -7
- package/rules/common/research.md +2 -0
- package/scripts/sync-specialists.mjs +260 -49
- package/skills/ai/prompt-optimizer.md +3 -3
- package/skills/brand/output-vibe.md +48 -0
- package/skills/docs/adr-workflow.md +3 -0
- package/skills/docs/backlog-proposal-workflow.md +3 -0
- package/skills/docs/codebase-research-workflow.md +5 -2
- package/skills/docs/customer-profile-workflow.md +3 -0
- package/skills/docs/document-ingest-workflow.md +3 -0
- package/skills/docs/evidence-ingest-workflow.md +4 -1
- package/skills/docs/init-project.md +1 -1
- package/skills/docs/prd-workflow.md +1 -1
- package/skills/docs/prfaq-workflow.md +3 -0
- package/skills/docs/product-intelligence-workflow.md +4 -1
- package/skills/docs/product-signal-workflow.md +3 -0
- package/skills/docs/research-workflow.md +13 -6
- package/skills/docs/runbook-workflow.md +3 -0
- package/skills/docs/strategy-workflow.md +3 -0
- package/skills/docs/user-research-workflow.md +6 -3
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/{engineer.ai.md → ai-engineer.md} +2 -2
- package/skills/roles/architect.ai-systems.md +1 -1
- package/skills/roles/architect.data.md +1 -1
- package/skills/roles/architect.enterprise.md +1 -1
- package/skills/roles/architect.integration.md +1 -1
- package/skills/roles/architect.md +1 -1
- package/skills/roles/architect.platform.md +1 -1
- package/skills/roles/{product-manager.business-strategy.md → business-strategist.md} +3 -3
- package/skills/roles/data-analyst.experiment.md +1 -1
- package/skills/roles/data-analyst.md +1 -1
- package/skills/roles/data-analyst.product-intelligence.md +1 -1
- package/skills/roles/data-analyst.product.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/{engineer.data.md → data-engineer.md} +2 -2
- package/skills/roles/data-engineer.pipeline.md +2 -2
- package/skills/roles/data-engineer.vector-retrieval.md +2 -2
- package/skills/roles/data-engineer.warehouse.md +2 -2
- package/skills/roles/debugger.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +1 -1
- package/skills/roles/{reviewer.devil-advocate.md → devil-advocate.md} +2 -2
- package/skills/roles/{operator.docs.md → docs-keeper.md} +3 -3
- package/skills/roles/engineer.md +3 -7
- package/skills/roles/{reviewer.evaluator.md → evaluator.md} +2 -2
- package/skills/roles/{researcher.explorer.md → explorer.md} +2 -2
- package/skills/roles/{operator.md → operations.md} +2 -5
- package/skills/roles/orchestrator.md +1 -1
- package/skills/roles/{engineer.platform.md → platform-engineer.md} +2 -2
- package/skills/roles/product-manager.ai-product.md +1 -1
- package/skills/roles/product-manager.enterprise.md +1 -1
- package/skills/roles/product-manager.growth.md +1 -2
- package/skills/roles/product-manager.md +1 -2
- package/skills/roles/product-manager.platform.md +1 -1
- package/skills/roles/product-manager.product.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -1
- package/skills/roles/qa.api-contract.md +1 -1
- package/skills/roles/qa.data-pipeline.md +1 -1
- package/skills/roles/qa.md +1 -1
- package/skills/roles/qa.web-ui.md +1 -1
- package/skills/roles/{operator.release.md → release-manager.md} +3 -3
- package/skills/roles/researcher.md +1 -1
- package/skills/roles/reviewer.md +1 -1
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.appsec.md +1 -1
- package/skills/roles/security.cloud.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +1 -1
- package/skills/roles/security.privacy.md +1 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/{operator.sre.md → sre.md} +3 -3
- package/skills/roles/{qa.test-automation.md → test-automation.md} +2 -2
- package/skills/roles/{reviewer.trace.md → trace-reviewer.md} +2 -2
- package/skills/roles/{researcher.ux.md → ux-researcher.md} +2 -2
- package/skills/routing.json +18 -0
- package/skills/routing.md +1 -1
- package/specialists/artifact-manifest.json +2 -1
- package/specialists/audit-enrichments.json +14 -14
- package/specialists/org/contracts/accessibility-to-qa.json +37 -0
- package/specialists/org/contracts/any-to-business-strategist.json +57 -0
- package/specialists/org/contracts/any-to-debugger.json +38 -0
- package/specialists/org/contracts/any-to-designer.json +33 -0
- package/specialists/org/contracts/any-to-docs-keeper.json +34 -0
- package/specialists/org/contracts/any-to-explorer.json +39 -0
- package/specialists/org/contracts/any-to-sre-incident.json +33 -0
- package/specialists/org/contracts/any-to-trace-reviewer.json +32 -0
- package/specialists/org/contracts/architect-to-ai-engineer.json +32 -0
- package/specialists/org/contracts/architect-to-data-engineer.json +52 -0
- package/specialists/org/contracts/architect-to-devil-advocate.json +38 -0
- package/specialists/org/contracts/architect-to-engineer.json +34 -0
- package/specialists/org/contracts/architect-to-evaluator.json +59 -0
- package/specialists/org/contracts/architect-to-legal-compliance.json +55 -0
- package/specialists/org/contracts/architect-to-operations.json +45 -0
- package/specialists/org/contracts/architect-to-platform-engineer.json +42 -0
- package/specialists/org/contracts/business-strategist-to-product-manager.json +39 -0
- package/specialists/org/contracts/construct-to-orchestrator.json +36 -0
- package/specialists/org/contracts/construct-to-rd-lead.json +53 -0
- package/specialists/org/contracts/data-analyst-to-product-manager.json +43 -0
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +36 -0
- package/specialists/org/contracts/designer-to-accessibility.json +36 -0
- package/specialists/org/contracts/engineer-to-qa.json +34 -0
- package/specialists/org/contracts/engineer-to-reviewer.json +52 -0
- package/specialists/org/contracts/explorer-to-engineer.json +38 -0
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +43 -0
- package/specialists/org/contracts/platform-engineer-to-engineer.json +31 -0
- package/specialists/org/contracts/product-manager-to-architect.json +71 -0
- package/specialists/org/contracts/product-manager-to-data-analyst.json +49 -0
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +51 -0
- package/specialists/org/contracts/qa-to-release-manager.json +42 -0
- package/specialists/org/contracts/qa-to-test-automation.json +42 -0
- package/specialists/org/contracts/rd-lead-to-architect.json +38 -0
- package/specialists/org/contracts/researcher-to-architect.json +40 -0
- package/specialists/org/contracts/researcher-to-product-manager.json +57 -0
- package/specialists/org/contracts/reviewer-to-security.json +41 -0
- package/specialists/org/contracts/sre-to-release-manager.json +36 -0
- package/specialists/org/contracts/test-automation-to-engineer.json +34 -0
- package/specialists/org/contracts/trace-reviewer-to-sre.json +41 -0
- package/specialists/org/contracts/user-to-construct.json +30 -0
- package/specialists/org/groups/engineering-group.json +43 -0
- package/specialists/org/groups/governance-group.json +38 -0
- package/specialists/org/groups/operations-group.json +41 -0
- package/specialists/org/groups/product-group.json +46 -0
- package/specialists/org/groups/quality-group.json +42 -0
- package/specialists/org/groups/strategy-group.json +41 -0
- package/specialists/org/policies/action-approval.json +13 -0
- package/specialists/org/policies/agents-routing.json +13 -0
- package/specialists/org/policies/architecture.json +20 -0
- package/specialists/org/policies/bash-safety.json +13 -0
- package/specialists/org/policies/beads-hygiene.json +13 -0
- package/specialists/org/policies/bootstrap-state.json +13 -0
- package/specialists/org/policies/code-review.json +13 -0
- package/specialists/org/policies/coding-style.json +13 -0
- package/specialists/org/policies/comments.json +13 -0
- package/specialists/org/policies/commit-approval.json +13 -0
- package/specialists/org/policies/contract-preconditions.json +13 -0
- package/specialists/org/policies/deployment.json +20 -0
- package/specialists/org/policies/description.json +14 -0
- package/specialists/org/policies/design-approval.json +19 -0
- package/specialists/org/policies/doc-ownership.json +13 -0
- package/specialists/org/policies/file-path-fence.json +13 -0
- package/specialists/org/policies/framing.json +13 -0
- package/specialists/org/policies/git-workflow.json +13 -0
- package/specialists/org/policies/incident-response.json +15 -0
- package/specialists/org/policies/intake-triage.json +15 -0
- package/specialists/org/policies/neurodivergent-output.json +13 -0
- package/specialists/org/policies/no-fabrication.json +13 -0
- package/specialists/org/policies/patterns.json +13 -0
- package/specialists/org/policies/quality-gate-approval.json +17 -0
- package/specialists/org/policies/release-gates.json +13 -0
- package/specialists/org/policies/research-evidence.json +13 -0
- package/specialists/org/policies/review-before-change.json +13 -0
- package/specialists/org/policies/rollback.json +15 -0
- package/specialists/org/policies/scope-change.json +20 -0
- package/specialists/org/policies/secret-scan.json +13 -0
- package/specialists/org/policies/security-approval.json +17 -0
- package/specialists/org/policies/security.json +13 -0
- package/specialists/org/policies/session-efficiency.json +13 -0
- package/specialists/org/policies/skill-routing.json +13 -0
- package/specialists/org/policies/strategic-prioritization.json +17 -0
- package/specialists/org/policies/testing.json +13 -0
- package/specialists/org/policies/tool-invisibility.json +14 -0
- package/specialists/org/scopes/creative.json +54 -0
- package/specialists/org/scopes/operations.json +51 -0
- package/specialists/org/scopes/research.json +60 -0
- package/specialists/org/scopes/rnd.json +81 -0
- package/specialists/org/specialists/cx-accessibility.json +69 -0
- package/specialists/org/specialists/cx-ai-engineer.json +75 -0
- package/specialists/org/specialists/cx-architect.json +89 -0
- package/specialists/org/specialists/cx-business-strategist.json +72 -0
- package/specialists/org/specialists/cx-data-analyst.json +68 -0
- package/specialists/org/specialists/cx-data-engineer.json +71 -0
- package/specialists/org/specialists/cx-debugger.json +75 -0
- package/specialists/org/specialists/cx-designer.json +85 -0
- package/specialists/org/specialists/cx-devil-advocate.json +71 -0
- package/specialists/org/specialists/cx-docs-keeper.json +82 -0
- package/specialists/org/specialists/cx-engineer.json +106 -0
- package/specialists/org/specialists/cx-evaluator.json +69 -0
- package/specialists/org/specialists/cx-explorer.json +69 -0
- package/specialists/org/specialists/cx-legal-compliance.json +74 -0
- package/specialists/org/specialists/cx-operations.json +72 -0
- package/specialists/org/specialists/cx-oracle.json +46 -0
- package/specialists/org/specialists/cx-orchestrator.json +81 -0
- package/specialists/org/specialists/cx-platform-engineer.json +80 -0
- package/specialists/org/specialists/cx-product-manager.json +102 -0
- package/specialists/org/specialists/cx-qa.json +79 -0
- package/specialists/org/specialists/cx-rd-lead.json +73 -0
- package/specialists/org/specialists/cx-release-manager.json +77 -0
- package/specialists/org/specialists/cx-researcher.json +82 -0
- package/specialists/org/specialists/cx-reviewer.json +71 -0
- package/specialists/org/specialists/cx-security.json +91 -0
- package/specialists/org/specialists/cx-sre.json +94 -0
- package/specialists/org/specialists/cx-test-automation.json +69 -0
- package/specialists/org/specialists/cx-trace-reviewer.json +69 -0
- package/specialists/org/specialists/cx-ux-researcher.json +68 -0
- package/specialists/org/teams/accessibility-team.json +39 -0
- package/specialists/org/teams/design-team.json +40 -0
- package/specialists/org/teams/engineering-team.json +50 -0
- package/specialists/org/teams/governance-team.json +41 -0
- package/specialists/org/teams/operations-team.json +46 -0
- package/specialists/org/teams/product-management-team.json +45 -0
- package/specialists/org/teams/quality-team.json +49 -0
- package/specialists/org/teams/research-team.json +39 -0
- package/specialists/org/teams/strategy-team.json +48 -0
- package/specialists/org/teams/ux-research-team.json +39 -0
- package/specialists/prompts/_shared/validation-contract.md +1 -1
- package/specialists/prompts/_team-template.md +10 -0
- package/specialists/prompts/cx-accessibility.md +1 -0
- package/specialists/prompts/cx-ai-engineer.md +1 -1
- package/specialists/prompts/cx-business-strategist.md +1 -1
- package/specialists/prompts/cx-data-engineer.md +1 -1
- package/specialists/prompts/cx-designer.md +1 -0
- package/specialists/prompts/cx-devil-advocate.md +1 -1
- package/specialists/prompts/cx-docs-keeper.md +1 -1
- package/specialists/prompts/cx-evaluator.md +1 -1
- package/specialists/prompts/cx-explorer.md +1 -1
- package/specialists/prompts/cx-operations.md +1 -1
- package/specialists/prompts/cx-oracle.md +7 -3
- package/specialists/prompts/cx-orchestrator.md +17 -1
- package/specialists/prompts/cx-platform-engineer.md +1 -1
- package/specialists/prompts/cx-product-manager.md +2 -0
- package/specialists/prompts/cx-release-manager.md +1 -1
- package/specialists/prompts/cx-researcher.md +7 -4
- package/specialists/prompts/cx-sre.md +1 -1
- package/specialists/prompts/cx-test-automation.md +2 -2
- package/specialists/prompts/cx-trace-reviewer.md +3 -3
- package/specialists/prompts/cx-ux-researcher.md +2 -1
- package/templates/demos/scripts/architecture-review-adr.json +30 -0
- package/templates/demos/scripts/capability-contract.json +25 -0
- package/templates/demos/scripts/intake-triage.json +25 -0
- package/templates/demos/scripts/profile-doctor-health.json +25 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +2 -2
- package/templates/demos/tapes/architecture-review-adr.tape +44 -0
- package/templates/demos/tapes/capability-contract.tape +39 -0
- package/templates/demos/tapes/intake-triage.tape +39 -0
- package/templates/demos/tapes/profile-doctor-health.tape +39 -0
- package/templates/distribution/construct-brand.typ +23 -18
- package/templates/distribution/construct-decision.typ +1 -1
- package/templates/distribution/construct-pdf.typ +1 -1
- package/templates/distribution/construct-prd.typ +1 -1
- package/templates/distribution/construct-research.typ +1 -1
- package/templates/distribution/mermaid-puppeteer.json +8 -0
- package/templates/docs/persona-artifact.md +1 -1
- package/templates/docs/prd.md +6 -0
- package/lib/boundary.mjs +0 -127
- package/lib/certification/dashboard-api.mjs +0 -71
- package/lib/chat/cli.mjs +0 -333
- package/lib/chat/command-suggest.mjs +0 -161
- package/lib/chat/commands.mjs +0 -215
- package/lib/chat/config.mjs +0 -142
- package/lib/chat/context-compactor.mjs +0 -250
- package/lib/chat/context-continuation.mjs +0 -253
- package/lib/chat/continuation-source.mjs +0 -58
- package/lib/chat/demo-guide.mjs +0 -61
- package/lib/chat/design-tokens.mjs +0 -91
- package/lib/chat/desktop-binary.mjs +0 -81
- package/lib/chat/desktop-build.mjs +0 -130
- package/lib/chat/desktop-launcher.mjs +0 -133
- package/lib/chat/evidence.mjs +0 -145
- package/lib/chat/export.mjs +0 -74
- package/lib/chat/harness/driver.mjs +0 -91
- package/lib/chat/list-picker.mjs +0 -112
- package/lib/chat/model-picker.mjs +0 -356
- package/lib/chat/openrouter-fallback.mjs +0 -151
- package/lib/chat/permission-prompt.mjs +0 -33
- package/lib/chat/picker-catalog.mjs +0 -45
- package/lib/chat/policy-telemetry.mjs +0 -34
- package/lib/chat/present.mjs +0 -246
- package/lib/chat/session-context.mjs +0 -39
- package/lib/chat/session-persist.mjs +0 -73
- package/lib/chat/session-restore.mjs +0 -71
- package/lib/chat/session-settings.mjs +0 -53
- package/lib/chat/system-prompt.mjs +0 -52
- package/lib/chat/transparency.mjs +0 -93
- package/lib/chat/tui/color-scheme.mjs +0 -42
- package/lib/chat/tui/markdown.mjs +0 -123
- package/lib/chat/tui/presentation.mjs +0 -100
- package/lib/chat/tui/render.mjs +0 -500
- package/lib/chat/tui/turn-block.mjs +0 -284
- package/lib/chat/tui/turn-present.mjs +0 -18
- package/lib/chat/tui/turn-state.mjs +0 -88
- package/lib/chat/tui/usage.mjs +0 -122
- package/lib/chat/web-commands.mjs +0 -146
- package/lib/chat/web-launcher.mjs +0 -63
- package/lib/chat/web-picker-keys.mjs +0 -46
- package/lib/chat/web-session.mjs +0 -159
- package/lib/config/alias.mjs +0 -57
- package/lib/dashboard-demo.mjs +0 -71
- package/lib/dashboard-static.mjs +0 -175
- package/lib/install/desktop-binary-download.mjs +0 -88
- package/lib/profiles/loader.mjs +0 -123
- package/lib/profiles/rebrand.mjs +0 -46
- package/lib/server/auth.mjs +0 -169
- package/lib/server/chat-loop.mjs +0 -622
- package/lib/server/chat.mjs +0 -336
- package/lib/server/cors.mjs +0 -77
- package/lib/server/csrf.mjs +0 -103
- package/lib/server/demo-preview.mjs +0 -63
- package/lib/server/index.mjs +0 -2641
- package/lib/server/insights.mjs +0 -780
- package/lib/server/langfuse-login.mjs +0 -58
- package/lib/server/rate-limit.mjs +0 -93
- package/lib/server/telemetry-login.mjs +0 -100
- package/lib/server/webhook.mjs +0 -512
- package/specialists/contracts.json +0 -1032
- package/specialists/contracts.schema.json +0 -83
- package/specialists/policy-inventory.json +0 -188
- package/specialists/registry.json +0 -1412
- package/specialists/role-manifests.json +0 -217
- package/specialists/teams.json +0 -94
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/doctor/watchers/credential-parity.mjs — advisory credential alignment checks.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces when a pinned OpenRouter model or OpenCode default expects
|
|
5
|
+
* OPENROUTER_API_KEY but the canonical store and op-run catalog both lack it,
|
|
6
|
+
* or when OpenCode's openrouter provider has no auth wiring.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { parseEnvFile, getUserEnvPath } from '../../env-config.mjs';
|
|
14
|
+
import { hasSecret } from '../../providers/secret-resolver.mjs';
|
|
15
|
+
import { ensureOpenRouterProviderAuth } from '../../opencode-config.mjs';
|
|
16
|
+
import { findOpenCodeConfigPath } from '../../opencode-config.mjs';
|
|
17
|
+
|
|
18
|
+
export const name = 'credential-parity';
|
|
19
|
+
export const intervalMs = 30 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
function openRouterModelId(modelId) {
|
|
22
|
+
return typeof modelId === 'string' && /^openrouter\//.test(modelId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function openCodeNeedsOpenRouter(homeDir) {
|
|
26
|
+
const configPath = findOpenCodeConfigPath();
|
|
27
|
+
if (!fs.existsSync(configPath)) return { needs: false, configPath };
|
|
28
|
+
try {
|
|
29
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
30
|
+
const defaultModel = config.model || config.defaultModel || '';
|
|
31
|
+
return { needs: openRouterModelId(defaultModel), configPath, config };
|
|
32
|
+
} catch {
|
|
33
|
+
return { needs: false, configPath };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function openRouterAuthWired(config) {
|
|
38
|
+
const probe = { provider: { openrouter: config?.provider?.openrouter || {} } };
|
|
39
|
+
ensureOpenRouterProviderAuth(probe);
|
|
40
|
+
const provider = probe.provider.openrouter;
|
|
41
|
+
return Boolean(
|
|
42
|
+
provider.apiKey
|
|
43
|
+
|| provider.options?.headers?.Authorization,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runCredentialParityChecks({ rootDir = process.cwd(), homeDir = os.homedir(), env = process.env } = {}) {
|
|
48
|
+
const violations = [];
|
|
49
|
+
const configPath = getUserEnvPath(homeDir);
|
|
50
|
+
const openRouterConfigured = hasSecret('OPENROUTER_API_KEY', { env, cwd: rootDir });
|
|
51
|
+
|
|
52
|
+
const openCode = openCodeNeedsOpenRouter(homeDir);
|
|
53
|
+
if (openCode.needs && !openRouterConfigured) {
|
|
54
|
+
violations.push({
|
|
55
|
+
category: 'credential-parity',
|
|
56
|
+
severity: 'advisory',
|
|
57
|
+
summary: `OpenCode default model uses OpenRouter but OPENROUTER_API_KEY is not configured (${openCode.configPath})`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (openCode.config && openCode.needs && !openRouterAuthWired(openCode.config)) {
|
|
62
|
+
violations.push({
|
|
63
|
+
category: 'credential-parity',
|
|
64
|
+
severity: 'advisory',
|
|
65
|
+
summary: 'OpenCode provider.openrouter has no apiKey env ref — run `construct sync --global`',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const xdgEnv = parseEnvFile(configPath);
|
|
70
|
+
const legacyEnv = parseEnvFile(path.join(homeDir, '.construct', 'config.env'));
|
|
71
|
+
if (legacyEnv.CONSTRUCT_OP_ENV_FILE && !xdgEnv.CONSTRUCT_OP_ENV_FILE) {
|
|
72
|
+
violations.push({
|
|
73
|
+
category: 'credential-parity',
|
|
74
|
+
severity: 'advisory',
|
|
75
|
+
summary: 'CONSTRUCT_OP_ENV_FILE is still in ~/.construct/config.env — run `construct install --scope=user` to migrate to XDG config',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
summary: violations.length
|
|
81
|
+
? `credential-parity: ${violations.length} advisory finding(s)`
|
|
82
|
+
: 'credential-parity: ok',
|
|
83
|
+
violations,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function tick({ rootDir = process.cwd(), homeDir = os.homedir(), env = process.env } = {}) {
|
|
88
|
+
const result = await runCredentialParityChecks({ rootDir, homeDir, env });
|
|
89
|
+
return {
|
|
90
|
+
actions: [],
|
|
91
|
+
escalations: [],
|
|
92
|
+
findings: result.violations,
|
|
93
|
+
passed: result.violations.length === 0
|
|
94
|
+
? [{ category: 'credential-parity', summary: result.summary }]
|
|
95
|
+
: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `initialize`, parse the response, send `tools/list`, report tool count.
|
|
7
7
|
* cass-memory v0.2.x (the original cause of construct-1qt) rejects the
|
|
8
8
|
* handshake outright; this watcher flags servers that fail the handshake so
|
|
9
|
-
*
|
|
9
|
+
* doctor surfaces a protocol problem rather than a port-up false
|
|
10
10
|
* positive.
|
|
11
11
|
*
|
|
12
12
|
* Severity defaults to warning. Set `CONSTRUCT_MCP_PROTOCOL_BLOCKING=1` to
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/doctor/watchers/service-health.mjs — continuous service probes.
|
|
3
3
|
*
|
|
4
|
-
* Probes the services `construct dev` starts (postgres,
|
|
5
|
-
*
|
|
4
|
+
* Probes the services `construct dev` starts (postgres, cm).
|
|
5
|
+
* On 2 consecutive probe failures, attempts a restart of
|
|
6
6
|
* docker-backed services (postgres). On 2 consecutive failed
|
|
7
7
|
* restarts, escalates `service.down` for cx-sre. Non-docker services log +
|
|
8
8
|
* escalate after 3 consecutive failures — auto-restart needs original spawn
|
|
@@ -13,12 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import { spawnSync, spawn } from 'node:child_process';
|
|
15
15
|
import { existsSync, readFileSync, openSync, mkdirSync } from 'node:fs';
|
|
16
|
-
import {
|
|
17
|
-
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { join } from 'node:path';
|
|
18
17
|
|
|
19
18
|
import { record } from '../audit.mjs';
|
|
20
19
|
import { escalate } from '../escalate.mjs';
|
|
21
|
-
import { configDir,
|
|
20
|
+
import { configDir, cacheDir } from '../../config/xdg.mjs';
|
|
22
21
|
|
|
23
22
|
export const name = 'service-health';
|
|
24
23
|
export const intervalMs = 60 * 1000;
|
|
@@ -26,9 +25,6 @@ export const intervalMs = 60 * 1000;
|
|
|
26
25
|
const failureCounts = {};
|
|
27
26
|
const restartAttempts = {};
|
|
28
27
|
|
|
29
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
-
const REPO_ROOT = join(__dirname, '..', '..', '..');
|
|
31
|
-
|
|
32
28
|
const services = [
|
|
33
29
|
{
|
|
34
30
|
id: 'postgres',
|
|
@@ -36,12 +32,6 @@ const services = [
|
|
|
36
32
|
restart: () => dockerRestart('construct-postgres'),
|
|
37
33
|
restartable: true,
|
|
38
34
|
},
|
|
39
|
-
{
|
|
40
|
-
id: 'dashboard',
|
|
41
|
-
probe: () => probeDashboardPort(),
|
|
42
|
-
restart: () => respawnViaConstructUp('dashboard'),
|
|
43
|
-
restartable: true,
|
|
44
|
-
},
|
|
45
35
|
{
|
|
46
36
|
id: 'cm',
|
|
47
37
|
probe: () => probeMemoryHealth(),
|
|
@@ -55,18 +45,6 @@ function probeTcp(host, port, timeoutMs = 1500) {
|
|
|
55
45
|
return r.status === 0;
|
|
56
46
|
}
|
|
57
47
|
|
|
58
|
-
function probeDashboardPort() {
|
|
59
|
-
const statePath = join(stateDir(), 'dashboard.json');
|
|
60
|
-
if (!existsSync(statePath)) return true;
|
|
61
|
-
try {
|
|
62
|
-
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
63
|
-
if (!state.port) return true;
|
|
64
|
-
return probeTcp('127.0.0.1', state.port);
|
|
65
|
-
} catch {
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
48
|
function probeMemoryHealth() {
|
|
71
49
|
const envPath = join(configDir(), 'config.env');
|
|
72
50
|
if (!existsSync(envPath)) return true;
|
|
@@ -99,12 +77,6 @@ function detachSpawn(command, args, logName) {
|
|
|
99
77
|
}
|
|
100
78
|
}
|
|
101
79
|
|
|
102
|
-
function respawnViaConstructUp() {
|
|
103
|
-
const bin = join(REPO_ROOT, 'bin', 'construct');
|
|
104
|
-
if (!existsSync(bin)) return { ok: false, error: 'bin/construct not found' };
|
|
105
|
-
return detachSpawn('node', [bin, 'up'], 'doctor-respawn-up.log');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
80
|
function respawnCm() {
|
|
109
81
|
const envPath = join(configDir(), 'config.env');
|
|
110
82
|
let port = '4231';
|
package/lib/document-export.mjs
CHANGED
|
@@ -105,6 +105,50 @@ export function pdfRenderedDiagrams(pdfPath, sourceContent) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function rawDiagramSourcePattern() {
|
|
109
|
+
return /(?:flowchart\s+(?:TD|LR|BT|RL)|sequenceDiagram|classDiagram|stateDiagram|erDiagram|journey|gantt|pie\s+title|direction:\s*(?:right|down|left|up))/i;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function zipEntryText(zipPath, entryName, env = process.env) {
|
|
113
|
+
if (!whichBin('unzip', env)) return null;
|
|
114
|
+
const result = spawnSync('unzip', ['-p', zipPath, entryName], { encoding: 'utf8', env });
|
|
115
|
+
return result.status === 0 ? String(result.stdout || '') : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function zipEntryNames(zipPath, env = process.env) {
|
|
119
|
+
if (!whichBin('unzip', env)) return [];
|
|
120
|
+
const result = spawnSync('unzip', ['-Z1', zipPath], { encoding: 'utf8', env });
|
|
121
|
+
return result.status === 0
|
|
122
|
+
? String(result.stdout || '').split('\n').map((line) => line.trim()).filter(Boolean)
|
|
123
|
+
: [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function docxRenderedDiagrams(docxPath, sourceContent, env = process.env) {
|
|
127
|
+
const expected = countDiagramFences(sourceContent);
|
|
128
|
+
if (expected === 0) return true;
|
|
129
|
+
const documentXml = zipEntryText(docxPath, 'word/document.xml', env);
|
|
130
|
+
if (!documentXml) return false;
|
|
131
|
+
if (rawDiagramSourcePattern().test(documentXml)) return false;
|
|
132
|
+
const rels = zipEntryText(docxPath, 'word/_rels/document.xml.rels', env) || '';
|
|
133
|
+
const entries = zipEntryNames(docxPath, env);
|
|
134
|
+
const mediaCount = entries.filter((entry) => entry.startsWith('word/media/')).length;
|
|
135
|
+
return mediaCount >= expected || /relationships\/image/i.test(rels);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function htmlRenderedDiagrams(htmlPath, sourceContent) {
|
|
139
|
+
const expected = countDiagramFences(sourceContent);
|
|
140
|
+
if (expected === 0) return true;
|
|
141
|
+
let html = '';
|
|
142
|
+
try {
|
|
143
|
+
html = fs.readFileSync(htmlPath, 'utf8');
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (rawDiagramSourcePattern().test(html)) return false;
|
|
148
|
+
const renderedCount = (html.match(/<(?:img|svg)\b/gi) || []).length;
|
|
149
|
+
return renderedCount >= expected || /data:image\//i.test(html);
|
|
150
|
+
}
|
|
151
|
+
|
|
108
152
|
export { pdfUsesBundledBrandSans } from './brand-fonts.mjs';
|
|
109
153
|
|
|
110
154
|
/** @deprecated use pdfUsesBundledBrandSans */
|
|
@@ -441,17 +485,37 @@ export function exportMarkdown({
|
|
|
441
485
|
};
|
|
442
486
|
}
|
|
443
487
|
|
|
444
|
-
if (
|
|
488
|
+
if (figures) {
|
|
445
489
|
const source = fs.readFileSync(inputPath, 'utf8');
|
|
446
490
|
const expectedDiagrams = countDiagramFences(source);
|
|
447
|
-
if (expectedDiagrams > 0
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
491
|
+
if (expectedDiagrams > 0) {
|
|
492
|
+
if (format === 'pdf' && !pdfRenderedDiagrams(target, source)) {
|
|
493
|
+
return {
|
|
494
|
+
ok: false,
|
|
495
|
+
format,
|
|
496
|
+
inputPath,
|
|
497
|
+
outputPath: target,
|
|
498
|
+
message: `Export wrote PDF but rendered 0/${expectedDiagrams} diagram(s). Ensure d2 and mmdc are installed; mmdc needs Chrome (set PUPPETEER_EXECUTABLE_PATH or install Google Chrome). Pandoc stderr: ${(result.stderr || '').trim().slice(0, 300)}`,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (format === 'docx' && !docxRenderedDiagrams(target, source, effectiveEnv)) {
|
|
502
|
+
return {
|
|
503
|
+
ok: false,
|
|
504
|
+
format,
|
|
505
|
+
inputPath,
|
|
506
|
+
outputPath: target,
|
|
507
|
+
message: `Export wrote DOCX but left diagram source unresolved. Ensure d2 and mermaid-cli rendered the figures before distributing the document. Pandoc stderr: ${(result.stderr || '').trim().slice(0, 300)}`,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
if ((format === 'html' || format === 'deck') && !htmlRenderedDiagrams(target, source)) {
|
|
511
|
+
return {
|
|
512
|
+
ok: false,
|
|
513
|
+
format,
|
|
514
|
+
inputPath,
|
|
515
|
+
outputPath: target,
|
|
516
|
+
message: `Export wrote ${format.toUpperCase()} but left diagram source unresolved. Ensure d2 and mermaid-cli rendered the figures before distributing the document. Pandoc stderr: ${(result.stderr || '').trim().slice(0, 300)}`,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
455
519
|
}
|
|
456
520
|
}
|
|
457
521
|
|
package/lib/document-extract.mjs
CHANGED
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
* - Async (preferred): `extractDocumentTextAsync` routes PDF/Office/HTML
|
|
6
6
|
* through the docling Python sidecar and audio/video through whisper.cpp.
|
|
7
7
|
* Returns { text, markdown, metadata, droppedInfo, ... }.
|
|
8
|
-
* - Sync (legacy): `extractDocumentText`
|
|
9
|
-
* PDF/Office
|
|
10
|
-
* dropped, scanned pages return empty). Audio/video throws
|
|
11
|
-
* { code: 'ASR_REQUIRED' } on this path.
|
|
8
|
+
* - Sync (legacy): `extractDocumentText` for text/transcript/calendar/email;
|
|
9
|
+
* PDF/Office zip formats require docling or the node-native unpdf/mammoth path.
|
|
12
10
|
*
|
|
13
11
|
* `extractDocumentMetadata` returns frontmatter + title/authors/dates.
|
|
14
12
|
*
|
|
@@ -46,17 +44,12 @@ export const AUDIO_VIDEO_EXTS = new Set(['.mp3', '.wav', '.m4a', '.mp4', '.mov',
|
|
|
46
44
|
|
|
47
45
|
export const ZIP_DOCUMENT_EXTS = new Set(['.docx', '.xlsx', '.pptx', '.odt', '.ods']);
|
|
48
46
|
|
|
49
|
-
// Office zip formats with no Node-native backend
|
|
50
|
-
// or opt into the legacy zip-xml path via CONSTRUCT_ALLOW_LEGACY_EXTRACT=1.
|
|
47
|
+
// Office zip formats with no Node-native backend require docling.
|
|
51
48
|
const OFFICE_REQUIRES_DOCLING_EXTS = new Set(['.xlsx', '.pptx', '.odt', '.ods']);
|
|
52
49
|
|
|
53
|
-
function legacyExtractAllowed(env = process.env) {
|
|
54
|
-
return env.CONSTRUCT_ALLOW_LEGACY_EXTRACT === '1';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
50
|
function makeOfficeRequiresDoclingError(extension) {
|
|
58
51
|
const err = new Error(
|
|
59
|
-
`${extension} extraction requires docling. Run \`construct install --with-docling
|
|
52
|
+
`${extension} extraction requires docling. Run \`construct install --with-docling\` or use high-fidelity ingest.`,
|
|
60
53
|
);
|
|
61
54
|
err.code = 'OFFICE_REQUIRES_DOCLING';
|
|
62
55
|
err.extension = extension;
|
|
@@ -65,7 +58,7 @@ function makeOfficeRequiresDoclingError(extension) {
|
|
|
65
58
|
|
|
66
59
|
function makePdfRequiresDoclingError() {
|
|
67
60
|
const err = new Error(
|
|
68
|
-
'PDF extraction requires docling
|
|
61
|
+
'PDF extraction requires docling or the node-native unpdf backend. Run `construct install --with-docling` or ensure unpdf is installed.',
|
|
69
62
|
);
|
|
70
63
|
err.code = 'PDF_REQUIRES_DOCLING';
|
|
71
64
|
return err;
|
|
@@ -85,9 +78,6 @@ export const EXTRACTABLE_DOCUMENT_EXTS = new Set([
|
|
|
85
78
|
'.pdf',
|
|
86
79
|
]);
|
|
87
80
|
|
|
88
|
-
// Minimum extracted characters per page that distinguishes text-bearing PDFs
|
|
89
|
-
// from scanned images. Below this threshold droppedInfo is emitted.
|
|
90
|
-
const PDF_MIN_TEXT_THRESHOLD = 50;
|
|
91
81
|
|
|
92
82
|
function normalizeText(value) {
|
|
93
83
|
return String(value)
|
|
@@ -149,103 +139,6 @@ function extractUtf8(filePath, extension) {
|
|
|
149
139
|
return { text, method: 'utf8', droppedInfo: [], structured: null };
|
|
150
140
|
}
|
|
151
141
|
|
|
152
|
-
function zipEntries(filePath) {
|
|
153
|
-
return run('unzip', ['-Z1', filePath]).split('\n').map((line) => line.trim()).filter(Boolean);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function unzipEntry(filePath, entryPath) {
|
|
157
|
-
return run('unzip', ['-p', filePath, entryPath]);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function extractZipDocument(filePath, extension) {
|
|
161
|
-
const entries = zipEntries(filePath);
|
|
162
|
-
let targets = [];
|
|
163
|
-
const droppedInfo = [];
|
|
164
|
-
|
|
165
|
-
if (extension === '.docx') {
|
|
166
|
-
targets = entries.filter((entry) => entry === 'word/document.xml' || entry.startsWith('word/header') || entry.startsWith('word/footer'));
|
|
167
|
-
|
|
168
|
-
// Count embedded images (word/media/ folder entries)
|
|
169
|
-
const imageCount = entries.filter((e) => e.startsWith('word/media/')).length;
|
|
170
|
-
if (imageCount > 0) {
|
|
171
|
-
droppedInfo.push(makeDropInfo({ kind: 'inline-image', count: imageCount, reason: 'Embedded images not extracted from DOCX', recoverable: true }));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Count tables in the main document XML
|
|
175
|
-
try {
|
|
176
|
-
const docXml = unzipEntry(filePath, 'word/document.xml');
|
|
177
|
-
const tableCount = (docXml.match(/<w:tbl[\s>]/g) || []).length;
|
|
178
|
-
if (tableCount > 0) {
|
|
179
|
-
droppedInfo.push(makeDropInfo({ kind: 'table', count: tableCount, reason: 'Table structure flattened; cell boundaries and column headers not preserved', recoverable: true }));
|
|
180
|
-
}
|
|
181
|
-
} catch { /* ignore — main extraction still proceeds */ }
|
|
182
|
-
|
|
183
|
-
// Count comments
|
|
184
|
-
if (entries.includes('word/comments.xml')) {
|
|
185
|
-
try {
|
|
186
|
-
const commentsXml = unzipEntry(filePath, 'word/comments.xml');
|
|
187
|
-
const commentCount = (commentsXml.match(/<w:comment\s/g) || []).length;
|
|
188
|
-
if (commentCount > 0) {
|
|
189
|
-
droppedInfo.push(makeDropInfo({ kind: 'comment', count: commentCount, reason: 'Document review comments not included in extracted text', recoverable: true }));
|
|
190
|
-
}
|
|
191
|
-
} catch { /* ignore */ }
|
|
192
|
-
}
|
|
193
|
-
} else if (extension === '.xlsx') {
|
|
194
|
-
targets = entries.filter((entry) => entry === 'xl/sharedStrings.xml' || /^xl\/worksheets\/sheet\d+\.xml$/.test(entry));
|
|
195
|
-
|
|
196
|
-
// Count formula cells across all worksheets
|
|
197
|
-
let formulaCount = 0;
|
|
198
|
-
for (const sheetEntry of entries.filter((e) => /^xl\/worksheets\/sheet\d+\.xml$/.test(e))) {
|
|
199
|
-
try {
|
|
200
|
-
const sheetXml = unzipEntry(filePath, sheetEntry);
|
|
201
|
-
formulaCount += (sheetXml.match(/<f\b/g) || []).length;
|
|
202
|
-
} catch { /* ignore */ }
|
|
203
|
-
}
|
|
204
|
-
if (formulaCount > 0) {
|
|
205
|
-
droppedInfo.push(makeDropInfo({ kind: 'formula', count: formulaCount, reason: 'Spreadsheet formulas not evaluated; only cell string values extracted', recoverable: true }));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Count embedded images
|
|
209
|
-
const imageCount = entries.filter((e) => e.startsWith('xl/media/')).length;
|
|
210
|
-
if (imageCount > 0) {
|
|
211
|
-
droppedInfo.push(makeDropInfo({ kind: 'inline-image', count: imageCount, reason: 'Embedded images not extracted from XLSX', recoverable: true }));
|
|
212
|
-
}
|
|
213
|
-
} else if (extension === '.pptx') {
|
|
214
|
-
targets = entries.filter((entry) => /^ppt\/slides\/slide\d+\.xml$/.test(entry));
|
|
215
|
-
|
|
216
|
-
// Count speaker notes slides
|
|
217
|
-
const notesCount = entries.filter((e) => /^ppt\/notesSlides\/notesSlide\d+\.xml$/.test(e)).length;
|
|
218
|
-
if (notesCount > 0) {
|
|
219
|
-
droppedInfo.push(makeDropInfo({ kind: 'speaker-notes', count: notesCount, reason: 'Speaker notes slides not included in extracted text', recoverable: true }));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Count slides with animation sequences
|
|
223
|
-
let animationCount = 0;
|
|
224
|
-
for (const slideEntry of targets) {
|
|
225
|
-
try {
|
|
226
|
-
const slideXml = unzipEntry(filePath, slideEntry);
|
|
227
|
-
if (/<p:timing\b/.test(slideXml)) animationCount++;
|
|
228
|
-
} catch { /* ignore */ }
|
|
229
|
-
}
|
|
230
|
-
if (animationCount > 0) {
|
|
231
|
-
droppedInfo.push(makeDropInfo({ kind: 'animation', count: animationCount, reason: 'Slide animation timing sequences not preserved in extracted text', recoverable: false }));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Count embedded images
|
|
235
|
-
const imageCount = entries.filter((e) => e.startsWith('ppt/media/')).length;
|
|
236
|
-
if (imageCount > 0) {
|
|
237
|
-
droppedInfo.push(makeDropInfo({ kind: 'inline-image', count: imageCount, reason: 'Embedded images not extracted from PPTX', recoverable: true }));
|
|
238
|
-
}
|
|
239
|
-
} else if (extension === '.odt' || extension === '.ods') {
|
|
240
|
-
targets = entries.filter((entry) => entry === 'content.xml');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (targets.length === 0) throw new Error(`No readable document entries found for ${extension}`);
|
|
244
|
-
|
|
245
|
-
const text = normalizeText(targets.map((entry) => stripMarkup(unzipEntry(filePath, entry))).join('\n\n'));
|
|
246
|
-
return { text, method: 'zip-xml', droppedInfo, structured: null };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
142
|
// Minimal RTF-to-text: strip control words, groups, and escapes. RTF is a
|
|
250
143
|
// text format, so this works everywhere; it is the cross-platform fallback when
|
|
251
144
|
// textutil (macOS only) is absent. Lossy on complex documents, but never throws.
|
|
@@ -280,84 +173,12 @@ function extractRichText(filePath, extension) {
|
|
|
280
173
|
throw new Error('textutil not available for rich-text extraction');
|
|
281
174
|
}
|
|
282
175
|
|
|
283
|
-
function extractPdfWithPdftotext(filePath) {
|
|
284
|
-
return {
|
|
285
|
-
text: normalizeText(run('pdftotext', ['-layout', '-nopgbrk', filePath, '-'])),
|
|
286
|
-
method: 'pdftotext',
|
|
287
|
-
droppedInfo: [],
|
|
288
|
-
structured: null,
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function extractPdfWithSwift(filePath) {
|
|
293
|
-
const script = `
|
|
294
|
-
import Foundation
|
|
295
|
-
import PDFKit
|
|
296
|
-
|
|
297
|
-
let path = CommandLine.arguments[1]
|
|
298
|
-
let url = URL(fileURLWithPath: path)
|
|
299
|
-
guard let document = PDFDocument(url: url) else {
|
|
300
|
-
fputs("Unable to open PDF\\n", stderr)
|
|
301
|
-
exit(1)
|
|
302
|
-
}
|
|
303
|
-
print(document.string ?? "")
|
|
304
|
-
`;
|
|
305
|
-
return {
|
|
306
|
-
text: normalizeText(run('swift', ['-e', script, filePath])),
|
|
307
|
-
method: 'swift-pdfkit',
|
|
308
|
-
droppedInfo: [],
|
|
309
|
-
structured: null,
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function extractPdfWithMdls(filePath) {
|
|
314
|
-
const text = extractWithMdls(filePath);
|
|
315
|
-
return { text, method: 'mdls', droppedInfo: [], structured: null };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
176
|
function extractWithMdls(filePath) {
|
|
319
177
|
const text = normalizeText(run('mdls', ['-raw', '-name', 'kMDItemTextContent', filePath]));
|
|
320
178
|
if (!text || text === '(null)') throw new Error('Spotlight text extraction unavailable');
|
|
321
179
|
return text;
|
|
322
180
|
}
|
|
323
181
|
|
|
324
|
-
function extractPdf(filePath) {
|
|
325
|
-
let extracted;
|
|
326
|
-
if (commandExists('pdftotext')) {
|
|
327
|
-
extracted = extractPdfWithPdftotext(filePath);
|
|
328
|
-
} else if (process.platform === 'darwin' && commandExists('swift')) {
|
|
329
|
-
try {
|
|
330
|
-
extracted = extractPdfWithSwift(filePath);
|
|
331
|
-
} catch {
|
|
332
|
-
extracted = extractPdfWithMdls(filePath);
|
|
333
|
-
}
|
|
334
|
-
} else if (process.platform === 'darwin' && commandExists('mdls')) {
|
|
335
|
-
extracted = extractPdfWithMdls(filePath);
|
|
336
|
-
} else {
|
|
337
|
-
throw new Error('No PDF extraction backend available');
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Scanned-PDF heuristic: if extracted text is very short relative to file size,
|
|
341
|
-
// the PDF probably contains only images and needs OCR to be useful.
|
|
342
|
-
const stat = statSync(filePath);
|
|
343
|
-
const pages = Math.max(1, Math.ceil(stat.size / 50_000));
|
|
344
|
-
const threshold = PDF_MIN_TEXT_THRESHOLD * pages;
|
|
345
|
-
if (extracted.text.length < threshold) {
|
|
346
|
-
const ocrHint = process.env.CONSTRUCT_PDF_OCR_HINT || 'none';
|
|
347
|
-
const hintMsg = ocrHint !== 'none'
|
|
348
|
-
? `OCR backend hint: ${ocrHint}. See docs/guides/intake/scanned-pdfs.md.`
|
|
349
|
-
: 'Set CONSTRUCT_PDF_OCR_HINT=tesseract|pdfocr to enable OCR. See docs/guides/intake/scanned-pdfs.md.';
|
|
350
|
-
extracted.droppedInfo.push(makeDropInfo({
|
|
351
|
-
kind: 'scanned-pdf',
|
|
352
|
-
count: pages,
|
|
353
|
-
reason: `Extracted text (${extracted.text.length} chars) is below minimum threshold for ${pages} page(s); document may be a scanned image. ${hintMsg}`,
|
|
354
|
-
recoverable: true,
|
|
355
|
-
}));
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return extracted;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
182
|
export function isExtractableDocumentPath(filePath) {
|
|
362
183
|
return EXTRACTABLE_DOCUMENT_EXTS.has(extname(filePath).toLowerCase());
|
|
363
184
|
}
|
|
@@ -710,10 +531,7 @@ export function extractDocumentText(filePath, { maxChars = null } = {}) {
|
|
|
710
531
|
const envelope = extractCalendar(resolvedPath);
|
|
711
532
|
extracted = { text: envelope.text, method: 'ics', droppedInfo: envelope.droppedInfo, structured: envelope.structured };
|
|
712
533
|
} else if (ZIP_DOCUMENT_EXTS.has(extension)) {
|
|
713
|
-
|
|
714
|
-
throw makeOfficeRequiresDoclingError(extension);
|
|
715
|
-
}
|
|
716
|
-
extracted = extractZipDocument(resolvedPath, extension);
|
|
534
|
+
throw makeOfficeRequiresDoclingError(extension);
|
|
717
535
|
} else if (RICH_TEXT_EXTS.has(extension)) {
|
|
718
536
|
extracted = extractRichText(resolvedPath, extension);
|
|
719
537
|
} else if (MDLS_DOCUMENT_EXTS.has(extension) && process.platform === 'darwin' && commandExists('mdls')) {
|
|
@@ -721,10 +539,7 @@ export function extractDocumentText(filePath, { maxChars = null } = {}) {
|
|
|
721
539
|
} else if (EMAIL_DOCUMENT_EXTS.has(extension)) {
|
|
722
540
|
extracted = extractEml(resolvedPath);
|
|
723
541
|
} else if (extension === '.pdf') {
|
|
724
|
-
|
|
725
|
-
throw makePdfRequiresDoclingError();
|
|
726
|
-
}
|
|
727
|
-
extracted = extractPdf(resolvedPath);
|
|
542
|
+
throw makePdfRequiresDoclingError();
|
|
728
543
|
} else {
|
|
729
544
|
throw new Error(`Unsupported document type: ${extension || 'unknown'}`);
|
|
730
545
|
}
|
|
@@ -798,12 +613,9 @@ export async function extractDocumentTextNodeNative(filePath, { maxChars = null
|
|
|
798
613
|
if (text) {
|
|
799
614
|
return buildExtractionResult(resolvedPath, extension, { text, method: nodeBackend.method, droppedInfo: [], structured: null }, maxChars);
|
|
800
615
|
}
|
|
801
|
-
} catch { /* optional dep absent or parse failure
|
|
802
|
-
if (
|
|
803
|
-
|
|
804
|
-
throw makePdfRequiresDoclingError();
|
|
805
|
-
}
|
|
806
|
-
return extractDocumentText(resolvedPath, { maxChars });
|
|
616
|
+
} catch { /* optional dep absent or parse failure */ }
|
|
617
|
+
if (extension === '.docx') throw makeOfficeRequiresDoclingError(extension);
|
|
618
|
+
throw makePdfRequiresDoclingError();
|
|
807
619
|
}
|
|
808
620
|
|
|
809
621
|
return extractDocumentText(resolvedPath, { maxChars });
|
package/lib/embed/artifact.mjs
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generates PRDs, RFCs, and ADRs as structured markdown files under docs/.
|
|
5
5
|
* Each artifact type has a fixed schema and front-matter. Numbering is auto-
|
|
6
|
-
* incremented by scanning the target directory. Entry points cover the CLI
|
|
7
|
-
* the embed daemon when analysis triggers a recommendation
|
|
8
|
-
* API.
|
|
6
|
+
* incremented by scanning the target directory. Entry points cover the CLI
|
|
7
|
+
* and the embed daemon when analysis triggers a recommendation.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'fs';
|
package/lib/embed/cli.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Exports:
|
|
11
11
|
* resolveEmbedStatus(env, homeDir) — returns { level, label, detail }
|
|
12
12
|
* level: 'running' | 'stopped' | 'none'
|
|
13
|
-
* Consumed by session-start hook and
|
|
13
|
+
* Consumed by session-start hook and `construct embed status`.
|
|
14
14
|
*
|
|
15
15
|
* autoStartEmbedIfNeeded(env, rootDir, homeDir) — spawns daemon when
|
|
16
16
|
* provider credentials are present but daemon is stopped. No-op if already
|
package/lib/embed/config.mjs
CHANGED
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
* - type: slack # post to a Slack channel
|
|
39
39
|
* provider: slack
|
|
40
40
|
* channel: C12345
|
|
41
|
-
* - type: dashboard # push to the Construct dashboard
|
|
42
41
|
*
|
|
43
42
|
* approval:
|
|
44
43
|
* require: # action types that need approval before dispatch
|
|
@@ -138,7 +137,7 @@ export const DEFAULT_OPERATING_PROFILE = {
|
|
|
138
137
|
},
|
|
139
138
|
riskModel: {
|
|
140
139
|
detect: ['blocked work', 'missing owner', 'stale cadence', 'source errors', 'missing focal resource', 'unapproved high-risk action'],
|
|
141
|
-
|
|
140
|
+
riskStatus: { ok: 'no known gaps', attention: 'gaps or source errors exist', blocked: 'required focal resource or approval path missing' },
|
|
142
141
|
},
|
|
143
142
|
};
|
|
144
143
|
|
|
@@ -128,7 +128,7 @@ export function createCustomerProfile({ name, owner, workspace, status = 'active
|
|
|
128
128
|
updatedAt: new Date().toISOString(),
|
|
129
129
|
};
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
const content = `---
|
|
132
132
|
cx_doc_id: ${randomUUID()}
|
|
133
133
|
created_at: ${profile.createdAt}
|
|
134
134
|
updated_at: ${profile.updatedAt}
|
package/lib/embed/daemon.mjs
CHANGED
|
@@ -737,7 +737,7 @@ this.#scheduler.register(
|
|
|
737
737
|
async () => {
|
|
738
738
|
const result = await this.#inboxWatcher.poll();
|
|
739
739
|
if (result.processed.length) {
|
|
740
|
-
const { getRebrand } = await import('../
|
|
740
|
+
const { getRebrand } = await import('../scopes/rebrand.mjs');
|
|
741
741
|
const { signalNoun } = getRebrand(this.#rootDir);
|
|
742
742
|
const noun = result.processed.length === 1 ? signalNoun : `${signalNoun}s`;
|
|
743
743
|
process.stderr.write(`[embed] Classified ${result.processed.length} ${noun} from [${result.dirs.join(', ')}]\n`);
|
|
@@ -932,9 +932,8 @@ this.#scheduler.register(
|
|
|
932
932
|
);
|
|
933
933
|
|
|
934
934
|
// ── Job 13: telemetry-heartbeat ──────────────────────────────────────
|
|
935
|
-
// Every 5 min — emit a daemon.heartbeat trace event so
|
|
936
|
-
//
|
|
937
|
-
// has occurred. The heartbeat is fire-and-forget; failure is non-fatal.
|
|
935
|
+
// Every 5 min — emit a daemon.heartbeat trace event so telemetry backends
|
|
936
|
+
// always have recent data, even when no agent/worker activity has occurred.
|
|
938
937
|
this.#scheduler.register(
|
|
939
938
|
'telemetry-heartbeat',
|
|
940
939
|
5 * 60_000,
|
|
@@ -962,8 +961,8 @@ this.#scheduler.register(
|
|
|
962
961
|
|
|
963
962
|
// ── Job 14: telemetry-generation ─────────────────────────────────────────
|
|
964
963
|
// Every 5 min — make a tiny LLM call and record it as a telemetry generation
|
|
965
|
-
// observation so
|
|
966
|
-
//
|
|
964
|
+
// observation so external telemetry backends always have recent data to display,
|
|
965
|
+
// even when no agent sessions are active.
|
|
967
966
|
this.#scheduler.register(
|
|
968
967
|
'telemetry-generation',
|
|
969
968
|
5 * 60_000,
|