@geraldmaron/construct 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +36 -0
- package/README.md +41 -7
- package/bin/construct +1029 -65
- package/bin/construct-postinstall.mjs +27 -2
- package/config/tag-vocabulary.json +264 -0
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/lib/acp/server.mjs +21 -7
- package/lib/adapters-sync.mjs +2 -1
- package/lib/audit-trail.mjs +185 -2
- package/lib/beads/auto-close.mjs +2 -1
- package/lib/beads/drift.mjs +2 -1
- package/lib/bridges/copilot-proxy.mjs +20 -5
- package/lib/certification/skill-inventory.mjs +10 -1
- package/lib/cli/approvals.mjs +147 -0
- package/lib/cli-commands.mjs +105 -14
- package/lib/comment-lint.mjs +127 -8
- package/lib/config/schema.mjs +7 -30
- package/lib/config/source-target-registry.mjs +70 -0
- package/lib/config/source-targets.mjs +179 -205
- package/lib/contracts/coverage.mjs +77 -0
- package/lib/contracts/validate.mjs +102 -13
- package/lib/contracts/violation-log.mjs +50 -4
- package/lib/db/migrate.mjs +69 -0
- package/lib/db/migrations/001_orchestration_runs.sql +9 -0
- package/lib/db/migrations/002_queue_provider.sql +50 -0
- package/lib/db/migrations/003_worker_registry.sql +17 -0
- package/lib/db/migrations/004_trace_events.sql +16 -0
- package/lib/db/migrations/005_shared_memory.sql +15 -0
- package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
- package/lib/decisions/enforced-baseline.json +0 -2
- package/lib/decisions/registry.mjs +3 -2
- package/lib/deployment/parity-contract.mjs +1 -1
- package/lib/deployment-mode.mjs +78 -2
- package/lib/diagram-export.mjs +10 -1
- package/lib/distill.mjs +5 -2
- package/lib/docs-verify.mjs +2 -1
- package/lib/doctor/cli.mjs +1 -0
- package/lib/doctor/command-on-path.mjs +24 -0
- package/lib/doctor/diagnosis.mjs +109 -0
- package/lib/doctor/embedding-health.mjs +50 -0
- package/lib/doctor/engine-health.mjs +93 -0
- package/lib/doctor/graph-validate.mjs +47 -0
- package/lib/doctor/index.mjs +23 -4
- package/lib/doctor/sidecar-providers.mjs +56 -0
- package/lib/doctor/watchers/consistency.mjs +93 -1
- package/lib/doctor/watchers/cx-budget.mjs +1 -1
- package/lib/doctor/watchers/graph-staleness.mjs +1 -1
- package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
- package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
- package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
- package/lib/doctor/watchers/provider-breaker.mjs +78 -0
- package/lib/document-export.mjs +52 -27
- package/lib/document-extract/docling-client.mjs +9 -5
- package/lib/document-extract/docling-sidecar.py +30 -0
- package/lib/document-extract.mjs +1 -1
- package/lib/document-ingest.mjs +9 -0
- package/lib/embed/approval-queue.mjs +184 -88
- package/lib/embed/authority-guard.mjs +65 -2
- package/lib/embed/capability-jobs.mjs +365 -0
- package/lib/embed/capability-lifecycle.mjs +219 -0
- package/lib/embed/capability-loader.mjs +303 -0
- package/lib/embed/capability-runtime.mjs +58 -0
- package/lib/embed/cli.mjs +185 -8
- package/lib/embed/config.mjs +4 -0
- package/lib/embed/daemon.mjs +125 -6
- package/lib/embed/demand-fetch.mjs +190 -54
- package/lib/embed/inbox.mjs +12 -10
- package/lib/embed/presets/ops-triage.mjs +236 -0
- package/lib/embed/presets/pm-feedback.mjs +341 -0
- package/lib/embed/presets/tpm.mjs +401 -0
- package/lib/embed/providers/jira.mjs +72 -1
- package/lib/embed/providers/registry.mjs +140 -33
- package/lib/embed/worker.mjs +5 -0
- package/lib/embedded-contract/capability.mjs +4 -0
- package/lib/embedded-contract/execution.mjs +1 -1
- package/lib/embedded-contract/model-resolve.mjs +53 -3
- package/lib/embedded-contract/workflow-defs.mjs +48 -73
- package/lib/embedded-contract/workflow-invoke.mjs +144 -4
- package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
- package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
- package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
- package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
- package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
- package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
- package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
- package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
- package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
- package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
- package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
- package/lib/env-config.mjs +60 -11
- package/lib/export-validate.mjs +34 -2
- package/lib/extensions/index.mjs +27 -0
- package/lib/extensions/loader.mjs +120 -0
- package/lib/extensions/manifest-schema.mjs +141 -0
- package/lib/extensions/manifests/anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
- package/lib/extensions/manifests/directory.manifest.json +12 -0
- package/lib/extensions/manifests/docling.manifest.json +37 -0
- package/lib/extensions/manifests/echo.manifest.json +8 -0
- package/lib/extensions/manifests/feedback.manifest.json +12 -0
- package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
- package/lib/extensions/manifests/github.manifest.json +52 -0
- package/lib/extensions/manifests/linear.manifest.json +50 -0
- package/lib/extensions/manifests/local.manifest.json +12 -0
- package/lib/extensions/manifests/ollama.manifest.json +12 -0
- package/lib/extensions/manifests/openai.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter.manifest.json +12 -0
- package/lib/extensions/manifests/postgres.manifest.json +12 -0
- package/lib/extensions/manifests/salesforce.manifest.json +12 -0
- package/lib/extensions/manifests/slack.manifest.json +58 -0
- package/lib/extensions/manifests/whisper.manifest.json +36 -0
- package/lib/extensions/validate.mjs +175 -0
- package/lib/features.mjs +13 -9
- package/lib/flows/checkpoint.mjs +184 -0
- package/lib/flows/constants.mjs +27 -0
- package/lib/flows/define.mjs +146 -0
- package/lib/flows/engine.mjs +249 -0
- package/lib/flows/errors.mjs +19 -0
- package/lib/flows/index.mjs +27 -0
- package/lib/flows/joins.mjs +18 -0
- package/lib/flows/schema.mjs +90 -0
- package/lib/flows/state.mjs +31 -0
- package/lib/frameworks/loader.mjs +99 -0
- package/lib/frameworks/schema.mjs +157 -0
- package/lib/graph/build-from-corpus.mjs +67 -0
- package/lib/graph/build-from-embed.mjs +82 -0
- package/lib/graph/build-from-registry.mjs +164 -3
- package/lib/graph/cli.mjs +456 -12
- package/lib/graph/gap-queries.mjs +156 -0
- package/lib/graph/gaps.mjs +41 -0
- package/lib/graph/impacted.mjs +129 -0
- package/lib/graph/runtime-evidence.mjs +177 -0
- package/lib/graph/security-coverage.mjs +113 -0
- package/lib/graph/staleness.mjs +241 -10
- package/lib/graph/store.mjs +24 -7
- package/lib/graph/validate.mjs +161 -0
- package/lib/headhunt.mjs +9 -8
- package/lib/health-check.mjs +15 -7
- package/lib/hook-health.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +10 -4
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/graph-impact-advisory.mjs +2 -2
- package/lib/hooks/guard-bash.mjs +41 -15
- package/lib/hooks/mcp-health-check.mjs +11 -4
- package/lib/hooks/model-fallback.mjs +50 -6
- package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
- package/lib/hooks/pre-compact.mjs +181 -173
- package/lib/hooks/rule-verifier.mjs +2 -1
- package/lib/hooks/session-reflect.mjs +2 -1
- package/lib/hooks/session-start.mjs +3 -3
- package/lib/host/readiness.mjs +109 -0
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +29 -8
- package/lib/identity.mjs +92 -0
- package/lib/ingest/degraded-extract.mjs +96 -0
- package/lib/ingest/docling-remote.mjs +2 -1
- package/lib/ingest/provider-extract.mjs +7 -19
- package/lib/ingest/sidecar-providers.mjs +196 -0
- package/lib/ingest-tooling.mjs +11 -5
- package/lib/init-unified.mjs +55 -38
- package/lib/init-update.mjs +2 -1
- package/lib/install/legacy-global-cleanup.mjs +25 -0
- package/lib/install/stage-project.mjs +41 -4
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/prepare.mjs +2 -0
- package/lib/intake/queue-registry.mjs +50 -0
- package/lib/intake/queue.mjs +133 -17
- package/lib/intake/session-prelude.mjs +57 -8
- package/lib/integrations/intake-integrations.mjs +61 -111
- package/lib/intent-classifier.mjs +43 -5
- package/lib/libreoffice-export.mjs +8 -8
- package/lib/logging/rotate.mjs +5 -4
- package/lib/mcp/broker.mjs +332 -17
- package/lib/mcp/denied-store.mjs +95 -0
- package/lib/mcp/destructive-approval.mjs +57 -0
- package/lib/mcp/destructive-gate.mjs +30 -0
- package/lib/mcp/dispatch-envelope.mjs +199 -0
- package/lib/mcp/memory-bridge.mjs +2 -1
- package/lib/mcp/server.mjs +185 -1268
- package/lib/mcp/tool-definitions-memory.mjs +376 -0
- package/lib/mcp/tool-definitions-project.mjs +271 -0
- package/lib/mcp/tool-definitions-skills.mjs +311 -0
- package/lib/mcp/tool-definitions-workflow.mjs +398 -0
- package/lib/mcp/tool-definitions.mjs +25 -0
- package/lib/mcp/tool-rate-limit.mjs +47 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +95 -0
- package/lib/mcp/tool-surface-parity.mjs +60 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +197 -19
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/project.mjs +25 -8
- package/lib/mcp/tools/provider-write.mjs +187 -0
- package/lib/mcp/tools/scope.mjs +0 -3
- package/lib/mcp/tools/skills.mjs +1 -1
- package/lib/mcp/tools/storage.mjs +2 -2
- package/lib/mcp/tools/web-search-governance.mjs +96 -0
- package/lib/mcp/tools/web-search.mjs +5 -76
- package/lib/mcp/transport/auth.mjs +238 -0
- package/lib/mcp/transport/http.mjs +136 -0
- package/lib/mcp/transport/mode.mjs +31 -0
- package/lib/mcp/transport/stdio.mjs +22 -0
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +34 -23
- package/lib/mcp-platform-config.mjs +53 -20
- package/lib/mode-capabilities.mjs +109 -0
- package/lib/model-router.mjs +42 -9
- package/lib/models/catalog.mjs +40 -1
- package/lib/net-guard.mjs +247 -0
- package/lib/observation-store.mjs +6 -2
- package/lib/ollama/provision-context.mjs +2 -1
- package/lib/opencode-config.mjs +22 -3
- package/lib/opencode-runtime-plugin.mjs +120 -2
- package/lib/oracle/actions.mjs +204 -10
- package/lib/oracle/cli.mjs +24 -3
- package/lib/oracle/daemon-entry.mjs +6 -0
- package/lib/oracle/dispatch.mjs +2 -1
- package/lib/oracle/execute.mjs +2 -2
- package/lib/oracle/gaps.mjs +3 -3
- package/lib/oracle/heartbeat.mjs +53 -0
- package/lib/oracle/read-model.mjs +69 -13
- package/lib/oracle/reconcile.mjs +3 -46
- package/lib/oracle/routing.mjs +37 -31
- package/lib/oracle/synthesize.mjs +1 -1
- package/lib/orchestration/classification.mjs +434 -0
- package/lib/orchestration/delegation-flow.mjs +132 -0
- package/lib/orchestration/flow-selection.mjs +418 -0
- package/lib/orchestration/gates.mjs +244 -0
- package/lib/orchestration/host-sampling.mjs +121 -0
- package/lib/orchestration/policy-constants.mjs +48 -0
- package/lib/orchestration/provider-outcome.mjs +197 -0
- package/lib/orchestration/readiness.mjs +114 -13
- package/lib/orchestration/run-store-postgres.mjs +32 -26
- package/lib/orchestration/run-store-sqlite.mjs +8 -14
- package/lib/orchestration/run-store.mjs +25 -12
- package/lib/orchestration/runtime.mjs +508 -62
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +60 -0
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +763 -80
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/output-quality.mjs +61 -2
- package/lib/packs/cli.mjs +144 -0
- package/lib/packs/core-pack.mjs +112 -0
- package/lib/packs/enablement.mjs +187 -0
- package/lib/packs/index.mjs +23 -0
- package/lib/packs/loader.mjs +176 -0
- package/lib/packs/manifest-schema.mjs +58 -0
- package/lib/packs/prompts.mjs +120 -0
- package/lib/packs/validate.mjs +208 -0
- package/lib/path-policy.mjs +56 -0
- package/lib/plugin-registry.mjs +4 -5
- package/lib/policy/audit-gate.mjs +42 -0
- package/lib/policy/consumption-budget.mjs +149 -0
- package/lib/policy/engine.mjs +81 -6
- package/lib/policy/role-authority.mjs +89 -0
- package/lib/prompt-composer.js +19 -3
- package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
- package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
- package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
- package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
- package/lib/providers/contract/adapters/github/index.mjs +38 -3
- package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
- package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
- package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
- package/lib/providers/contract/adapters/jira/manifest.json +17 -0
- package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
- package/lib/providers/contract.mjs +207 -0
- package/lib/providers/credential-bootstrap.mjs +7 -4
- package/lib/providers/credential-sources.mjs +14 -2
- package/lib/providers/directory/index.mjs +261 -0
- package/lib/providers/feedback/index.mjs +392 -0
- package/lib/providers/filter-audit.mjs +68 -0
- package/lib/providers/filter-schema.mjs +54 -0
- package/lib/providers/github/index.mjs +31 -0
- package/lib/providers/instance-config.mjs +215 -0
- package/lib/providers/op-locate.mjs +96 -0
- package/lib/providers/op-run.mjs +64 -9
- package/lib/providers/registry.mjs +26 -3
- package/lib/providers/secret-audit-wiring.mjs +41 -18
- package/lib/providers/secret-resolver.mjs +263 -31
- package/lib/publish-template.mjs +6 -3
- package/lib/publish-tooling.mjs +4 -0
- package/lib/publish.mjs +32 -4
- package/lib/queue/pg-queue.mjs +395 -0
- package/lib/reflect.mjs +2 -1
- package/lib/registry/assemble.mjs +28 -2
- package/lib/registry/catalog.mjs +6 -0
- package/lib/registry/consolidation.mjs +8 -1
- package/lib/registry/custom-scaffold.mjs +200 -0
- package/lib/registry/custom-schema.mjs +137 -0
- package/lib/registry/loader.mjs +15 -4
- package/lib/registry/manifests/format-engines.default.json +15 -0
- package/lib/registry/manifests/surface-map.default.json +46 -0
- package/lib/registry/retired-paths.mjs +1 -0
- package/lib/registry/surface-map.mjs +100 -50
- package/lib/registry/validate.mjs +3 -3
- package/lib/render-visual-check.mjs +240 -0
- package/lib/resources/budget.mjs +40 -17
- package/lib/roles/flavor-bindings.mjs +36 -14
- package/lib/roles/gateway.mjs +15 -8
- package/lib/roots.mjs +107 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -6
- package/lib/runtime/whisper-bootstrap.mjs +11 -3
- package/lib/runtime-env.mjs +5 -2
- package/lib/sandbox.mjs +1 -1
- package/lib/scheduler/index.mjs +8 -20
- package/lib/schema-infer.mjs +31 -2
- package/lib/scopes/lifecycle.mjs +29 -25
- package/lib/scopes/loader.mjs +49 -12
- package/lib/scopes/teams.mjs +1 -1
- package/lib/security/ingest-boundary.mjs +80 -0
- package/lib/security/recall-wrapper.mjs +99 -0
- package/lib/security/trust.mjs +132 -0
- package/lib/service-manager.mjs +93 -24
- package/lib/setup.mjs +41 -23
- package/lib/skills-apply.mjs +4 -2
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/state-root.mjs +147 -0
- package/lib/status.mjs +597 -6
- package/lib/storage/admin.mjs +84 -7
- package/lib/storage/backend-registry.mjs +73 -0
- package/lib/storage/backend.mjs +63 -9
- package/lib/storage/embeddings-openai.mjs +12 -2
- package/lib/storage/hybrid-query.mjs +11 -3
- package/lib/storage/retrieval-hardening.mjs +384 -0
- package/lib/storage/shared-memory.mjs +158 -0
- package/lib/storage/vector-client.mjs +69 -2
- package/lib/team/health.mjs +72 -0
- package/lib/telemetry/backends/local.mjs +9 -3
- package/lib/telemetry/client.mjs +3 -7
- package/lib/template-registry.mjs +10 -12
- package/lib/tenant/context.mjs +82 -0
- package/lib/tenant/isolation.mjs +129 -0
- package/lib/test-corpus-inventory.mjs +103 -2
- package/lib/uninstall/uninstall.mjs +78 -12
- package/lib/validator.mjs +4 -6
- package/lib/worker/entrypoint.mjs +2 -1
- package/lib/worker/run.mjs +2 -2
- package/lib/worker/trace.mjs +5 -11
- package/lib/workflow-state.mjs +52 -8
- package/lib/workflows/liveness.mjs +203 -0
- package/lib/workflows/loader.mjs +177 -0
- package/lib/workflows/manifest-schema.mjs +71 -0
- package/lib/workflows/surface-parity.mjs +118 -0
- package/lib/workflows/validate.mjs +127 -0
- package/lib/writes/control-plane.mjs +140 -0
- package/lib/writes/envelope.mjs +206 -0
- package/lib/writes/sent-log.mjs +86 -0
- package/lib/writes/write-intent.mjs +109 -0
- package/package.json +20 -7
- package/personas/construct.md +12 -3
- package/registry/agent-manifest.json +117 -0
- package/registry/capabilities.json +1987 -0
- package/rules/common/comments.md +1 -0
- package/schemas/brand-voice.schema.json +24 -0
- package/schemas/capability-registry.schema.json +72 -0
- package/schemas/certification-run.schema.json +130 -0
- package/schemas/demo-recording.schema.json +46 -0
- package/schemas/eval-dataset.schema.json +79 -0
- package/schemas/execution-capability-profile.schema.json +46 -0
- package/schemas/execution-policy.schema.json +114 -0
- package/schemas/improvement-proposal.schema.json +65 -0
- package/schemas/mcp-tool-output.schema.json +61 -0
- package/schemas/platform-capabilities.schema.json +83 -0
- package/schemas/project-config.schema.json +215 -0
- package/schemas/project-demo.schema.json +60 -0
- package/schemas/provider-behavior-matrix.schema.json +91 -0
- package/schemas/scope.schema.json +197 -0
- package/schemas/specialist-trace.schema.json +107 -0
- package/schemas/team.schema.json +99 -0
- package/schemas/unified-registry.schema.json +546 -0
- package/scripts/sync-specialists.mjs +336 -106
- package/skills/docs/adr-workflow.md +1 -1
- package/skills/docs/codebase-research-workflow.md +3 -3
- package/skills/docs/prd-workflow.md +5 -6
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/user-research-workflow.md +3 -3
- package/skills/operating/fleet-health-routing.md +77 -0
- package/skills/roles/ai-engineer.md +1 -1
- package/skills/roles/architect.md +0 -1
- package/skills/roles/business-strategist.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/data-engineer.md +1 -1
- package/skills/roles/data-engineer.pipeline.md +1 -1
- package/skills/roles/data-engineer.vector-retrieval.md +1 -2
- package/skills/roles/data-engineer.warehouse.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +0 -1
- package/skills/roles/devil-advocate.md +1 -1
- package/skills/roles/docs-keeper.md +1 -1
- package/skills/roles/evaluator.md +1 -1
- package/skills/roles/explorer.md +1 -1
- package/skills/roles/platform-engineer.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -2
- package/skills/roles/qa.api-contract.md +0 -1
- package/skills/roles/qa.data-pipeline.md +0 -1
- package/skills/roles/qa.md +0 -1
- package/skills/roles/qa.web-ui.md +0 -1
- package/skills/roles/release-manager.md +1 -1
- package/skills/roles/researcher.md +0 -2
- package/skills/roles/reviewer.md +0 -3
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +0 -1
- package/skills/roles/security.privacy.md +0 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/sre.md +1 -1
- package/skills/roles/test-automation.md +1 -1
- package/skills/roles/trace-reviewer.md +1 -1
- package/skills/roles/ux-researcher.md +1 -1
- package/specialists/artifact-manifest.json +36 -36
- package/specialists/org/contracts/accessibility-to-qa.json +11 -3
- package/specialists/org/contracts/any-to-business-strategist.json +19 -7
- package/specialists/org/contracts/any-to-debugger.json +7 -1
- package/specialists/org/contracts/any-to-designer.json +7 -1
- package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
- package/specialists/org/contracts/any-to-explorer.json +13 -4
- package/specialists/org/contracts/any-to-sre-incident.json +6 -2
- package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
- package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
- package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
- package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
- package/specialists/org/contracts/architect-to-engineer.json +20 -4
- package/specialists/org/contracts/architect-to-evaluator.json +15 -5
- package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
- package/specialists/org/contracts/architect-to-operations.json +17 -4
- package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
- package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
- package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
- package/specialists/org/contracts/engineer-to-qa.json +20 -4
- package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
- package/specialists/org/contracts/explorer-to-engineer.json +11 -3
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
- package/specialists/org/contracts/operations-to-user.json +53 -0
- package/specialists/org/contracts/operations-triage-output.json +53 -0
- package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
- package/specialists/org/contracts/product-manager-to-architect.json +30 -10
- package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
- package/specialists/org/contracts/qa-to-release-manager.json +11 -3
- package/specialists/org/contracts/researcher-to-architect.json +10 -2
- package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
- package/specialists/org/contracts/reviewer-to-security.json +17 -3
- package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
- package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
- package/specialists/org/contracts/user-to-construct.json +11 -4
- package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
- package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
- package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
- package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
- package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
- package/specialists/org/groups/engineering-group.json +2 -6
- package/specialists/org/groups/governance-group.json +2 -3
- package/specialists/org/groups/operations-group.json +5 -8
- package/specialists/org/groups/product-group.json +4 -8
- package/specialists/org/groups/quality-group.json +3 -7
- package/specialists/org/groups/strategy-group.json +6 -9
- package/specialists/org/models.json.example +8 -0
- package/specialists/org/scopes/creative.json +6 -1
- package/specialists/org/scopes/operations.json +7 -1
- package/specialists/org/scopes/research.json +6 -1
- package/specialists/org/scopes/rnd.json +7 -1
- package/specialists/org/specialists/cx-architect.json +27 -8
- package/specialists/org/specialists/cx-designer.json +22 -8
- package/specialists/org/specialists/cx-engineer.json +71 -7
- package/specialists/org/specialists/cx-operations.json +89 -15
- package/specialists/org/specialists/cx-orchestrator.json +16 -4
- package/specialists/org/specialists/cx-product-manager.json +28 -9
- package/specialists/org/specialists/cx-qa.json +16 -6
- package/specialists/org/specialists/cx-researcher.json +41 -7
- package/specialists/org/specialists/cx-reviewer.json +61 -11
- package/specialists/org/specialists/cx-security.json +30 -10
- package/specialists/org/teams/design-team.json +3 -4
- package/specialists/org/teams/engineering-team.json +3 -10
- package/specialists/org/teams/governance-team.json +3 -5
- package/specialists/org/teams/operations-team.json +6 -12
- package/specialists/org/teams/product-management-team.json +2 -3
- package/specialists/org/teams/quality-team.json +4 -12
- package/specialists/org/teams/research-team.json +3 -3
- package/specialists/org/teams/strategy-team.json +7 -14
- package/specialists/prompts/cx-architect.md +20 -1
- package/specialists/prompts/cx-data-analyst.md +1 -1
- package/specialists/prompts/cx-designer.md +12 -4
- package/specialists/prompts/cx-engineer.md +15 -1
- package/specialists/prompts/cx-operations.md +14 -2
- package/specialists/prompts/cx-orchestrator.md +9 -5
- package/specialists/prompts/cx-product-manager.md +5 -1
- package/specialists/prompts/cx-qa.md +6 -2
- package/specialists/prompts/cx-researcher.md +27 -31
- package/specialists/prompts/cx-reviewer.md +19 -1
- package/specialists/prompts/cx-security.md +5 -1
- package/templates/distribution/construct-brand.typ +123 -59
- package/templates/distribution/construct-decision.typ +6 -3
- package/templates/distribution/construct-pdf.typ +11 -4
- package/templates/distribution/construct-prd.typ +6 -3
- package/templates/distribution/construct-research.typ +7 -4
- package/vendor/pandoc-ext/README.md +3 -0
- package/vendor/pandoc-ext/diagram.lua +687 -0
- package/lib/providers/contract/adapters/git/index.mjs +0 -115
- package/lib/providers/contract/adapters/slack/index.mjs +0 -175
- package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
- package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
- package/specialists/org/contracts/designer-to-accessibility.json +0 -36
- package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
- package/specialists/org/contracts/qa-to-test-automation.json +0 -42
- package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
- package/specialists/org/contracts/sre-to-release-manager.json +0 -36
- package/specialists/org/specialists/cx-accessibility.json +0 -69
- package/specialists/org/specialists/cx-ai-engineer.json +0 -75
- package/specialists/org/specialists/cx-business-strategist.json +0 -72
- package/specialists/org/specialists/cx-data-engineer.json +0 -71
- package/specialists/org/specialists/cx-devil-advocate.json +0 -71
- package/specialists/org/specialists/cx-docs-keeper.json +0 -82
- package/specialists/org/specialists/cx-evaluator.json +0 -69
- package/specialists/org/specialists/cx-explorer.json +0 -69
- package/specialists/org/specialists/cx-legal-compliance.json +0 -74
- package/specialists/org/specialists/cx-oracle.json +0 -46
- package/specialists/org/specialists/cx-platform-engineer.json +0 -80
- package/specialists/org/specialists/cx-rd-lead.json +0 -73
- package/specialists/org/specialists/cx-release-manager.json +0 -77
- package/specialists/org/specialists/cx-sre.json +0 -94
- package/specialists/org/specialists/cx-test-automation.json +0 -69
- package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
- package/specialists/org/specialists/cx-ux-researcher.json +0 -68
- package/specialists/org/teams/accessibility-team.json +0 -39
- package/specialists/org/teams/ux-research-team.json +0 -39
- package/specialists/prompts/cx-accessibility.md +0 -55
- package/specialists/prompts/cx-ai-engineer.md +0 -124
- package/specialists/prompts/cx-business-strategist.md +0 -58
- package/specialists/prompts/cx-data-engineer.md +0 -55
- package/specialists/prompts/cx-devil-advocate.md +0 -59
- package/specialists/prompts/cx-docs-keeper.md +0 -164
- package/specialists/prompts/cx-evaluator.md +0 -49
- package/specialists/prompts/cx-explorer.md +0 -71
- package/specialists/prompts/cx-legal-compliance.md +0 -58
- package/specialists/prompts/cx-oracle.md +0 -98
- package/specialists/prompts/cx-platform-engineer.md +0 -97
- package/specialists/prompts/cx-rd-lead.md +0 -59
- package/specialists/prompts/cx-release-manager.md +0 -54
- package/specialists/prompts/cx-sre.md +0 -111
- package/specialists/prompts/cx-test-automation.md +0 -55
- package/specialists/prompts/cx-trace-reviewer.md +0 -101
- package/specialists/prompts/cx-ux-researcher.md +0 -53
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/render-visual-check.mjs — rendered-artifact visual gate for the test:visual script.
|
|
3
|
+
*
|
|
4
|
+
* Exports a fixture through the real document-export engines (Pandoc/Typst for PDF, pptxgenjs for
|
|
5
|
+
* PPTX, Pandoc for DOCX) and asserts what a source-level check cannot: (1) for pdf/pptx — the same
|
|
6
|
+
* RENDER_REQUIRED_FORMATS output-quality.mjs already treats as render-required — every rasterized
|
|
7
|
+
* page carries real ink, not a blank/near-blank canvas from a broken template or missing font,
|
|
8
|
+
* decoded straight from the produced PNG's own IDAT stream with zlib inflate plus PNG filter
|
|
9
|
+
* reconstruction (no image-decoding dependency needed); (2) runOutputQuality's text-extraction-
|
|
10
|
+
* parity and on-disk reference-integrity checks pass against the produced file for every format,
|
|
11
|
+
* reusing the same gate publish.mjs enforces; (3) a renderer required for the requested format that
|
|
12
|
+
* is absent is reported as a typed, CI-visible failure reason, never a silent skip, because a
|
|
13
|
+
* visual claim that rests on a tool that was never invoked is not a visual claim. docx has no
|
|
14
|
+
* raster path in render-pipeline.mjs (only pdf/html/pptx/mermaid/d2), so it is validated via export
|
|
15
|
+
* plus text/reference checks only, matching the existing render-required contract.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import zlib from 'node:zlib';
|
|
23
|
+
|
|
24
|
+
import { exportMarkdown, whichBin } from './document-export.mjs';
|
|
25
|
+
import { runOutputQuality } from './output-quality.mjs';
|
|
26
|
+
|
|
27
|
+
// Below this ink ratio a rasterized page is treated as blank: a faithful export of a
|
|
28
|
+
// multi-paragraph fixture always covers more of the canvas than antialiasing noise or a stray
|
|
29
|
+
// hairline would produce.
|
|
30
|
+
|
|
31
|
+
const MIN_INK_RATIO = 0.01;
|
|
32
|
+
|
|
33
|
+
// Formats output-quality.mjs already treats as render-required (RENDER_REQUIRED_FORMATS); only
|
|
34
|
+
// these get rasterized and blank-page checked. docx has no renderer in render-pipeline.mjs.
|
|
35
|
+
|
|
36
|
+
const RASTER_FORMATS = new Set(['pdf', 'pptx']);
|
|
37
|
+
|
|
38
|
+
function readPngChunks(pngPath) {
|
|
39
|
+
const buf = fs.readFileSync(pngPath);
|
|
40
|
+
const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
41
|
+
if (!buf.subarray(0, 8).equals(sig)) throw new Error(`${pngPath}: not a PNG (bad signature)`);
|
|
42
|
+
|
|
43
|
+
let offset = 8;
|
|
44
|
+
let header = null;
|
|
45
|
+
const idatParts = [];
|
|
46
|
+
while (offset < buf.length) {
|
|
47
|
+
const len = buf.readUInt32BE(offset);
|
|
48
|
+
const type = buf.toString('ascii', offset + 4, offset + 8);
|
|
49
|
+
const data = buf.subarray(offset + 8, offset + 8 + len);
|
|
50
|
+
if (type === 'IHDR') {
|
|
51
|
+
header = {
|
|
52
|
+
width: data.readUInt32BE(0),
|
|
53
|
+
height: data.readUInt32BE(4),
|
|
54
|
+
bitDepth: data.readUInt8(8),
|
|
55
|
+
colorType: data.readUInt8(9),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (type === 'IDAT') idatParts.push(data);
|
|
59
|
+
offset += 8 + len + 4;
|
|
60
|
+
if (type === 'IEND') break;
|
|
61
|
+
}
|
|
62
|
+
if (!header) throw new Error(`${pngPath}: missing IHDR chunk`);
|
|
63
|
+
if (header.bitDepth !== 8) throw new Error(`${pngPath}: unsupported bit depth ${header.bitDepth} (need 8)`);
|
|
64
|
+
const channels = { 0: 1, 2: 3, 4: 2, 6: 4 }[header.colorType];
|
|
65
|
+
if (!channels) throw new Error(`${pngPath}: unsupported color type ${header.colorType}`);
|
|
66
|
+
return { ...header, channels, raw: zlib.inflateSync(Buffer.concat(idatParts)) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function paeth(a, b, c) {
|
|
70
|
+
const p = a + b - c;
|
|
71
|
+
const pa = Math.abs(p - a);
|
|
72
|
+
const pb = Math.abs(p - b);
|
|
73
|
+
const pc = Math.abs(p - c);
|
|
74
|
+
if (pa <= pb && pa <= pc) return a;
|
|
75
|
+
if (pb <= pc) return b;
|
|
76
|
+
return c;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Reverses the per-scanline PNG filter (none/sub/up/average/paeth) so pixel values are real
|
|
80
|
+
// sample bytes rather than filter-encoded deltas.
|
|
81
|
+
|
|
82
|
+
function unfilterPng({ width, height, channels, raw }) {
|
|
83
|
+
const stride = width * channels;
|
|
84
|
+
const bpp = channels;
|
|
85
|
+
const out = Buffer.alloc(height * stride);
|
|
86
|
+
let pos = 0;
|
|
87
|
+
for (let y = 0; y < height; y++) {
|
|
88
|
+
const filter = raw[pos];
|
|
89
|
+
pos += 1;
|
|
90
|
+
const rowStart = y * stride;
|
|
91
|
+
for (let x = 0; x < stride; x++) {
|
|
92
|
+
const val = raw[pos + x];
|
|
93
|
+
const a = x >= bpp ? out[rowStart + x - bpp] : 0;
|
|
94
|
+
const b = y > 0 ? out[rowStart - stride + x] : 0;
|
|
95
|
+
const c = x >= bpp && y > 0 ? out[rowStart - stride + x - bpp] : 0;
|
|
96
|
+
let recon;
|
|
97
|
+
if (filter === 0) recon = val;
|
|
98
|
+
else if (filter === 1) recon = val + a;
|
|
99
|
+
else if (filter === 2) recon = val + b;
|
|
100
|
+
else if (filter === 3) recon = val + Math.floor((a + b) / 2);
|
|
101
|
+
else if (filter === 4) recon = val + paeth(a, b, c);
|
|
102
|
+
else throw new Error(`unsupported PNG filter type ${filter} at row ${y}`);
|
|
103
|
+
out[rowStart + x] = recon & 0xff;
|
|
104
|
+
}
|
|
105
|
+
pos += stride;
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fraction of pixels that are not near-white (allows antialiasing/JPEG-adjacent noise near 255).
|
|
111
|
+
|
|
112
|
+
export function inkRatio(pngPath) {
|
|
113
|
+
const decoded = readPngChunks(pngPath);
|
|
114
|
+
const pixels = unfilterPng(decoded);
|
|
115
|
+
const { channels } = decoded;
|
|
116
|
+
let inked = 0;
|
|
117
|
+
let total = 0;
|
|
118
|
+
for (let i = 0; i < pixels.length; i += channels) {
|
|
119
|
+
total += 1;
|
|
120
|
+
const r = pixels[i];
|
|
121
|
+
const g = channels >= 2 ? pixels[i + 1] : r;
|
|
122
|
+
const b = channels >= 3 ? pixels[i + 2] : r;
|
|
123
|
+
if (r < 245 || g < 245 || b < 245) inked += 1;
|
|
124
|
+
}
|
|
125
|
+
return total === 0 ? 0 : inked / total;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function rasterizePdf(pdfPath, outDir) {
|
|
129
|
+
const result = spawnSync('pdftoppm', ['-png', '-r', '72', pdfPath, path.join(outDir, 'page')], { encoding: 'utf8' });
|
|
130
|
+
if (result.status !== 0) throw new Error(`pdftoppm failed: ${(result.stderr || '').trim()}`);
|
|
131
|
+
return fs.readdirSync(outDir)
|
|
132
|
+
.filter((f) => f.startsWith('page') && f.endsWith('.png'))
|
|
133
|
+
.sort()
|
|
134
|
+
.map((f) => path.join(outDir, f));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// pptx has no direct rasterizer; render-pipeline.mjs's own pptx path goes soffice -> pdf ->
|
|
138
|
+
// pdftoppm, so this mirrors that exact chain rather than inventing a second one.
|
|
139
|
+
|
|
140
|
+
function rasterizePptx(pptxPath, outDir, env) {
|
|
141
|
+
const soffice = whichBin('soffice', env) || whichBin('libreoffice', env);
|
|
142
|
+
if (!soffice) return { ok: false, reason: 'missing-dependency', message: 'soffice/libreoffice not on PATH' };
|
|
143
|
+
const conv = spawnSync(soffice, ['--headless', '--convert-to', 'pdf', '--outdir', outDir, pptxPath], { encoding: 'utf8' });
|
|
144
|
+
if (conv.status !== 0) return { ok: false, reason: 'unavailable-renderer', message: (conv.stderr || '').trim() || 'soffice convert failed' };
|
|
145
|
+
const pdf = path.join(outDir, `${path.parse(pptxPath).name}.pdf`);
|
|
146
|
+
if (!fs.existsSync(pdf)) return { ok: false, reason: 'unavailable-renderer', message: 'soffice produced no pdf' };
|
|
147
|
+
return { ok: true, images: rasterizePdf(pdf, outDir) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const REQUIRED_ENGINES = {
|
|
151
|
+
pdf: ['pandoc', 'typst', 'pdftoppm'],
|
|
152
|
+
pptx: ['pandoc', 'soffice', 'pdftoppm'],
|
|
153
|
+
docx: ['pandoc'],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export function requiredEngineStatus(format, env = process.env) {
|
|
157
|
+
const names = REQUIRED_ENGINES[format] || [];
|
|
158
|
+
const missing = names.filter((name) => {
|
|
159
|
+
if (name === 'soffice') return !whichBin('soffice', env) && !whichBin('libreoffice', env);
|
|
160
|
+
return !whichBin(name, env);
|
|
161
|
+
});
|
|
162
|
+
return { format, required: names, missing, present: missing.length === 0 };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Runs the full rendered-artifact gate for one format: export via the real engine, rasterize when
|
|
166
|
+
// the format is render-required, assert non-blank pages, and run the shared output-quality checks
|
|
167
|
+
// (text parity + references) against the produced file. Returns a typed failure (never throws)
|
|
168
|
+
// when a required engine is absent, so the caller can fail the process loudly instead of skipping.
|
|
169
|
+
|
|
170
|
+
export function checkRenderedArtifact({ fixturePath, format, workDir, env = process.env } = {}) {
|
|
171
|
+
const engineStatus = requiredEngineStatus(format, env);
|
|
172
|
+
if (!engineStatus.present) {
|
|
173
|
+
const message = `test:visual requires real engines for ${format} but ${engineStatus.missing.join(', ')} ${engineStatus.missing.length > 1 ? 'are' : 'is'} not on PATH. Install: ${engineStatus.missing.join(', ')}. This is a hard failure: a rendered-artifact claim cannot rest on a tool that never ran.`;
|
|
174
|
+
return { ok: false, format, skipped: false, reason: 'missing-dependency', message, failures: [message] };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const dir = workDir || fs.mkdtempSync(path.join(os.tmpdir(), `cx-visual-${format}-`));
|
|
178
|
+
const outputPath = path.join(dir, `artifact.${format}`);
|
|
179
|
+
const sourceMarkdown = fs.readFileSync(fixturePath, 'utf8');
|
|
180
|
+
|
|
181
|
+
const exported = exportMarkdown({ inputPath: fixturePath, outputPath, format, env });
|
|
182
|
+
if (!exported.ok) {
|
|
183
|
+
const message = `export failed for ${format}: ${exported.message}`;
|
|
184
|
+
return { ok: false, format, skipped: false, reason: 'export-failed', message, failures: [message] };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let images = [];
|
|
188
|
+
if (RASTER_FORMATS.has(format)) {
|
|
189
|
+
if (format === 'pdf') {
|
|
190
|
+
images = rasterizePdf(outputPath, dir);
|
|
191
|
+
} else {
|
|
192
|
+
const raster = rasterizePptx(outputPath, dir, env);
|
|
193
|
+
if (!raster.ok) {
|
|
194
|
+
const message = `rasterize failed for ${format}: ${raster.message}`;
|
|
195
|
+
return { ok: false, format, skipped: false, reason: raster.reason, message, failures: [message] };
|
|
196
|
+
}
|
|
197
|
+
images = raster.images;
|
|
198
|
+
}
|
|
199
|
+
if (images.length === 0) {
|
|
200
|
+
const message = `${format} export produced zero rasterizable pages`;
|
|
201
|
+
return { ok: false, format, skipped: false, reason: 'no-pages', message, failures: [message] };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const blankPages = [];
|
|
206
|
+
const ratios = [];
|
|
207
|
+
for (const image of images) {
|
|
208
|
+
const ratio = inkRatio(image);
|
|
209
|
+
ratios.push(ratio);
|
|
210
|
+
if (ratio < MIN_INK_RATIO) blankPages.push({ image, ratio });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const quality = runOutputQuality({
|
|
214
|
+
exportPath: outputPath,
|
|
215
|
+
format,
|
|
216
|
+
sourceMarkdown,
|
|
217
|
+
baseDir: path.dirname(fixturePath),
|
|
218
|
+
gateLevel: 'render-smoke',
|
|
219
|
+
env,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const failures = [];
|
|
223
|
+
if (blankPages.length > 0) {
|
|
224
|
+
failures.push(`${blankPages.length}/${images.length} rendered page(s) are blank (ink ratio < ${MIN_INK_RATIO}): ${blankPages.map((p) => `${path.basename(p.image)}=${p.ratio.toFixed(4)}`).join(', ')}`);
|
|
225
|
+
}
|
|
226
|
+
for (const f of quality.failures) failures.push(f);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
ok: failures.length === 0,
|
|
230
|
+
format,
|
|
231
|
+
skipped: false,
|
|
232
|
+
pageCount: images.length,
|
|
233
|
+
inkRatios: ratios,
|
|
234
|
+
quality,
|
|
235
|
+
failures,
|
|
236
|
+
message: failures.length === 0
|
|
237
|
+
? `${format}: ${images.length ? `${images.length} page(s) rendered, all non-blank; ` : ''}text parity and references verified`
|
|
238
|
+
: failures.join('; '),
|
|
239
|
+
};
|
|
240
|
+
}
|
package/lib/resources/budget.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/resources/budget.mjs — disk-budget enforcement for
|
|
2
|
+
* lib/resources/budget.mjs — disk-budget enforcement for Construct's
|
|
3
|
+
* generated project state.
|
|
3
4
|
*
|
|
4
5
|
* Construct generates data continuously: traces, intake archives, task
|
|
5
6
|
* graphs, worker logs, session records, backups. None of it has a
|
|
6
|
-
* natural ceiling, so left unchecked the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* natural ceiling, so left unchecked the tree grows until it threatens
|
|
8
|
+
* the operator's disk. The budget primitive caps each category against
|
|
9
|
+
* a config-defined ceiling.
|
|
9
10
|
*
|
|
10
11
|
* Two enforcement modes:
|
|
11
12
|
* - **Hard-reject** (traces, worker logs): observability is
|
|
@@ -17,6 +18,13 @@
|
|
|
17
18
|
* load-bearing — never reject; emit a warning the operator can
|
|
18
19
|
* see in the doctor surface, and pruning gets aggressive.
|
|
19
20
|
*
|
|
21
|
+
* Category locations (ADR-0066): traces and worker-logs are the two
|
|
22
|
+
* hard-reject, highest-volume categories, so they resolve through the
|
|
23
|
+
* machine-scoped state root (`resolveStateDir`) rather than the
|
|
24
|
+
* project's own `.cx/`. The remaining soft-warn categories are smaller
|
|
25
|
+
* and still project-local. "Total .cx/ cap" therefore really means
|
|
26
|
+
* "total generated-state cap" and sums bytes across both locations.
|
|
27
|
+
*
|
|
20
28
|
* Defaults live in DEFAULT_PROJECT_CONFIG.resources. The operator
|
|
21
29
|
* raises them in construct.config.json or via the dashboard. Doctor
|
|
22
30
|
* surfaces usage vs cap at 80% (warn) and 100% (fail).
|
|
@@ -24,13 +32,17 @@
|
|
|
24
32
|
import fs from 'node:fs';
|
|
25
33
|
import path from 'node:path';
|
|
26
34
|
import { loadProjectConfig } from '../config/project-config.mjs';
|
|
35
|
+
import { resolveStateDir } from '../state-root.mjs';
|
|
27
36
|
|
|
28
37
|
export const HARD_REJECT_CATEGORIES = new Set(['traces', 'worker-logs']);
|
|
29
38
|
export const SOFT_WARN_CATEGORIES = new Set(['intake-archive', 'task-graphs', 'sessions', 'backups', 'handoffs']);
|
|
30
39
|
|
|
31
|
-
const
|
|
32
|
-
traces: '
|
|
33
|
-
'worker-logs': '
|
|
40
|
+
const STATE_ROOT_CATEGORY_SEGMENTS = {
|
|
41
|
+
traces: ['traces'],
|
|
42
|
+
'worker-logs': ['runtime', 'worker'],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const PROJECT_LOCAL_CATEGORY_PATHS = {
|
|
34
46
|
'intake-archive': '.cx/intake/processed',
|
|
35
47
|
'intake-skipped': '.cx/intake/skipped',
|
|
36
48
|
'task-graphs': '.cx/task-graphs',
|
|
@@ -39,6 +51,19 @@ const CATEGORY_PATHS = {
|
|
|
39
51
|
handoffs: '.cx/handoffs',
|
|
40
52
|
};
|
|
41
53
|
|
|
54
|
+
const CATEGORY_NAMES = [...Object.keys(STATE_ROOT_CATEGORY_SEGMENTS), ...Object.keys(PROJECT_LOCAL_CATEGORY_PATHS)];
|
|
55
|
+
|
|
56
|
+
// ensureDir:false — this module only measures/prunes usage, it never itself
|
|
57
|
+
// writes a trace or worker-log file, so a measurement call must not conjure
|
|
58
|
+
// the directory it is merely checking.
|
|
59
|
+
|
|
60
|
+
function categoryDir(projectRoot, name) {
|
|
61
|
+
const stateSegments = STATE_ROOT_CATEGORY_SEGMENTS[name];
|
|
62
|
+
if (stateSegments) return resolveStateDir(projectRoot, ...stateSegments, { ensureDir: false });
|
|
63
|
+
const rel = PROJECT_LOCAL_CATEGORY_PATHS[name];
|
|
64
|
+
return rel ? path.join(projectRoot, rel) : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
function loadResourceBudgets(projectRoot, env) {
|
|
43
68
|
const { config } = loadProjectConfig(projectRoot, env);
|
|
44
69
|
return config?.resources ?? {};
|
|
@@ -67,13 +92,13 @@ function walkDirSize(dir) {
|
|
|
67
92
|
export function measureUsage(projectRoot, env = process.env) {
|
|
68
93
|
const budgets = loadResourceBudgets(projectRoot, env);
|
|
69
94
|
const result = { categories: {}, totalCxBytes: 0, totalCxCap: (budgets?.disk?.totalCxMaxMb ?? 2000) * 1024 * 1024 };
|
|
70
|
-
for (const
|
|
71
|
-
const full =
|
|
95
|
+
for (const name of CATEGORY_NAMES) {
|
|
96
|
+
const full = categoryDir(projectRoot, name);
|
|
72
97
|
const { bytes, files } = walkDirSize(full);
|
|
73
98
|
result.categories[name] = {
|
|
74
99
|
bytes,
|
|
75
100
|
files,
|
|
76
|
-
path:
|
|
101
|
+
path: full,
|
|
77
102
|
enforcement: HARD_REJECT_CATEGORIES.has(name) ? 'hard-reject' : 'soft-warn',
|
|
78
103
|
};
|
|
79
104
|
result.totalCxBytes += bytes;
|
|
@@ -93,7 +118,7 @@ export function warnBudgetOnce(message, env = process.env) {
|
|
|
93
118
|
}
|
|
94
119
|
|
|
95
120
|
export function reserveOrReject(projectRoot, category, sizeBytes, env = process.env) {
|
|
96
|
-
if (!
|
|
121
|
+
if (!CATEGORY_NAMES.includes(category)) {
|
|
97
122
|
return { ok: true, source: 'unknown-category' };
|
|
98
123
|
}
|
|
99
124
|
const usage = measureUsage(projectRoot, env);
|
|
@@ -123,10 +148,8 @@ export function reserveOrReject(projectRoot, category, sizeBytes, env = process.
|
|
|
123
148
|
function listHardRejectFiles(projectRoot) {
|
|
124
149
|
const files = [];
|
|
125
150
|
for (const cat of HARD_REJECT_CATEGORIES) {
|
|
126
|
-
const
|
|
127
|
-
if (!
|
|
128
|
-
const dir = path.join(projectRoot, rel);
|
|
129
|
-
if (!fs.existsSync(dir)) continue;
|
|
151
|
+
const dir = categoryDir(projectRoot, cat);
|
|
152
|
+
if (!dir || !fs.existsSync(dir)) continue;
|
|
130
153
|
const walk = (current) => {
|
|
131
154
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
132
155
|
const full = path.join(current, entry.name);
|
|
@@ -199,7 +222,7 @@ export function planPrune(projectRoot, env = process.env, opts = {}) {
|
|
|
199
222
|
const actions = [];
|
|
200
223
|
|
|
201
224
|
const tracesMaxDays = disk.tracesMaxDays ?? 30;
|
|
202
|
-
const tracesDir =
|
|
225
|
+
const tracesDir = categoryDir(projectRoot, 'traces');
|
|
203
226
|
if (fs.existsSync(tracesDir)) {
|
|
204
227
|
const cutoff = now - tracesMaxDays * 24 * 60 * 60 * 1000;
|
|
205
228
|
for (const f of fs.readdirSync(tracesDir)) {
|
|
@@ -258,7 +281,7 @@ export function planPrune(projectRoot, env = process.env, opts = {}) {
|
|
|
258
281
|
}
|
|
259
282
|
}
|
|
260
283
|
|
|
261
|
-
const workerDir =
|
|
284
|
+
const workerDir = categoryDir(projectRoot, 'worker-logs');
|
|
262
285
|
const workerMaxMb = disk.workerLogsMaxMb ?? 100;
|
|
263
286
|
const workerMaxDays = disk.workerLogsMaxDays ?? 14;
|
|
264
287
|
if (fs.existsSync(workerDir)) {
|
|
@@ -7,33 +7,39 @@
|
|
|
7
7
|
*
|
|
8
8
|
* baseOnly: classifier value "core" loads the base role file (no .flavor suffix).
|
|
9
9
|
* See ADR-0047 for when to split specialists vs add flavors.
|
|
10
|
+
*
|
|
11
|
+
* construct-rf26.11 folded 15 of the 29 legacy specialists into 11 surviving
|
|
12
|
+
* worker anchors as skill bundles rather than separate personas. rolePrefix
|
|
13
|
+
* and classifierKey are unchanged (the skills/roles/*.md overlay files and
|
|
14
|
+
* the classifier heuristics that detect them are untouched); only
|
|
15
|
+
* specialistId is repointed to the anchor that now loads that overlay.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
export const SPECIALIST_FLAVOR_BINDINGS = {
|
|
13
19
|
engineer: { classifierKey: 'engineer', rolePrefix: 'engineer', specialistId: 'cx-engineer' },
|
|
14
|
-
'ai-engineer': { classifierKey: 'aiEngineer', rolePrefix: 'ai-engineer', specialistId: 'cx-
|
|
15
|
-
'platform-engineer': { classifierKey: 'platformEngineer', rolePrefix: 'platform-engineer', specialistId: 'cx-
|
|
16
|
-
'data-engineer': { classifierKey: 'dataEngineer', rolePrefix: 'data-engineer', specialistId: 'cx-
|
|
20
|
+
'ai-engineer': { classifierKey: 'aiEngineer', rolePrefix: 'ai-engineer', specialistId: 'cx-engineer', baseOnly: true },
|
|
21
|
+
'platform-engineer': { classifierKey: 'platformEngineer', rolePrefix: 'platform-engineer', specialistId: 'cx-engineer', baseOnly: true },
|
|
22
|
+
'data-engineer': { classifierKey: 'dataEngineer', rolePrefix: 'data-engineer', specialistId: 'cx-engineer' },
|
|
17
23
|
architect: { classifierKey: 'architect', rolePrefix: 'architect', specialistId: 'cx-architect' },
|
|
18
24
|
'product-manager': { classifierKey: 'productManager', rolePrefix: 'product-manager', specialistId: 'cx-product-manager' },
|
|
19
|
-
'business-strategist': { classifierKey: 'businessStrategist', rolePrefix: 'business-strategist', specialistId: 'cx-
|
|
25
|
+
'business-strategist': { classifierKey: 'businessStrategist', rolePrefix: 'business-strategist', specialistId: 'cx-product-manager', baseOnly: true },
|
|
20
26
|
qa: { classifierKey: 'qa', rolePrefix: 'qa', specialistId: 'cx-qa' },
|
|
21
|
-
'test-automation': { classifierKey: 'testAutomation', rolePrefix: 'test-automation', specialistId: 'cx-
|
|
27
|
+
'test-automation': { classifierKey: 'testAutomation', rolePrefix: 'test-automation', specialistId: 'cx-qa', baseOnly: true },
|
|
22
28
|
security: { classifierKey: 'security', rolePrefix: 'security', specialistId: 'cx-security' },
|
|
23
29
|
'data-analyst': { classifierKey: 'dataAnalyst', rolePrefix: 'data-analyst', specialistId: 'cx-data-analyst' },
|
|
24
|
-
sre: { classifierKey: 'sre', rolePrefix: 'sre', specialistId: 'cx-
|
|
30
|
+
sre: { classifierKey: 'sre', rolePrefix: 'sre', specialistId: 'cx-operations', baseOnly: true },
|
|
25
31
|
operations: { classifierKey: 'operations', rolePrefix: 'operations', specialistId: 'cx-operations', baseOnly: true },
|
|
26
|
-
'release-manager': { classifierKey: 'releaseManager', rolePrefix: 'release-manager', specialistId: 'cx-
|
|
27
|
-
'docs-keeper': { classifierKey: 'docsKeeper', rolePrefix: 'docs-keeper', specialistId: 'cx-
|
|
32
|
+
'release-manager': { classifierKey: 'releaseManager', rolePrefix: 'release-manager', specialistId: 'cx-operations', baseOnly: true },
|
|
33
|
+
'docs-keeper': { classifierKey: 'docsKeeper', rolePrefix: 'docs-keeper', specialistId: 'cx-operations', baseOnly: true },
|
|
28
34
|
reviewer: { classifierKey: 'reviewer', rolePrefix: 'reviewer', specialistId: 'cx-reviewer' },
|
|
29
|
-
'devil-advocate': { classifierKey: 'devilAdvocate', rolePrefix: 'devil-advocate', specialistId: 'cx-
|
|
30
|
-
evaluator: { classifierKey: 'evaluator', rolePrefix: 'evaluator', specialistId: 'cx-
|
|
31
|
-
'trace-reviewer': { classifierKey: 'traceReviewer', rolePrefix: 'trace-reviewer', specialistId: 'cx-
|
|
35
|
+
'devil-advocate': { classifierKey: 'devilAdvocate', rolePrefix: 'devil-advocate', specialistId: 'cx-reviewer', baseOnly: true },
|
|
36
|
+
evaluator: { classifierKey: 'evaluator', rolePrefix: 'evaluator', specialistId: 'cx-reviewer', baseOnly: true },
|
|
37
|
+
'trace-reviewer': { classifierKey: 'traceReviewer', rolePrefix: 'trace-reviewer', specialistId: 'cx-reviewer', baseOnly: true },
|
|
32
38
|
researcher: { classifierKey: 'researcher', rolePrefix: 'researcher', specialistId: 'cx-researcher' },
|
|
33
|
-
'ux-researcher': { classifierKey: 'uxResearcher', rolePrefix: 'ux-researcher', specialistId: 'cx-
|
|
34
|
-
explorer: { classifierKey: 'explorer', rolePrefix: 'explorer', specialistId: 'cx-
|
|
39
|
+
'ux-researcher': { classifierKey: 'uxResearcher', rolePrefix: 'ux-researcher', specialistId: 'cx-researcher', baseOnly: true },
|
|
40
|
+
explorer: { classifierKey: 'explorer', rolePrefix: 'explorer', specialistId: 'cx-researcher', baseOnly: true },
|
|
35
41
|
designer: { classifierKey: 'designer', rolePrefix: 'designer', specialistId: 'cx-designer' },
|
|
36
|
-
accessibility: { classifierKey: 'accessibility', rolePrefix: 'designer.accessibility', specialistId: 'cx-
|
|
42
|
+
accessibility: { classifierKey: 'accessibility', rolePrefix: 'designer.accessibility', specialistId: 'cx-designer', baseOnly: true },
|
|
37
43
|
debugger: { classifierKey: 'debugger', rolePrefix: 'debugger', specialistId: 'cx-debugger', baseOnly: true },
|
|
38
44
|
};
|
|
39
45
|
|
|
@@ -45,10 +51,26 @@ const BY_SPECIALIST = Object.fromEntries(
|
|
|
45
51
|
Object.entries(SPECIALIST_FLAVOR_BINDINGS).map(([name, b]) => [name, b]),
|
|
46
52
|
);
|
|
47
53
|
|
|
54
|
+
// Reverse index: since construct-rf26.11, several short-name keys above share
|
|
55
|
+
// a single specialistId (e.g. engineer/ai-engineer/platform-engineer/
|
|
56
|
+
// data-engineer all point at cx-engineer). bindingForSpecialist(shortName)
|
|
57
|
+
// only ever returns the entry whose OWN key matches the short name, so a
|
|
58
|
+
// caller resolving a flavor by specialistId (rather than by the historical
|
|
59
|
+
// short name) needs every candidate binding for that id, not just one.
|
|
60
|
+
const BY_SPECIALIST_ID = new Map();
|
|
61
|
+
for (const binding of Object.values(SPECIALIST_FLAVOR_BINDINGS)) {
|
|
62
|
+
if (!BY_SPECIALIST_ID.has(binding.specialistId)) BY_SPECIALIST_ID.set(binding.specialistId, []);
|
|
63
|
+
BY_SPECIALIST_ID.get(binding.specialistId).push(binding);
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
export function bindingForSpecialist(shortName) {
|
|
49
67
|
return BY_SPECIALIST[shortName] ?? null;
|
|
50
68
|
}
|
|
51
69
|
|
|
70
|
+
export function bindingsForSpecialistId(specialistId) {
|
|
71
|
+
return BY_SPECIALIST_ID.get(specialistId) ?? [];
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
export function bindingForClassifier(classifierKey) {
|
|
53
75
|
return BY_CLASSIFIER[classifierKey] ?? null;
|
|
54
76
|
}
|
package/lib/roles/gateway.mjs
CHANGED
|
@@ -196,7 +196,14 @@ export function shouldEscalate(event, manifest, now = Date.now()) {
|
|
|
196
196
|
return { escalate: false, reason: 'below-threshold', count: hits };
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
// The bd client is injectable so tests can intercept the escalation path
|
|
200
|
+
// without reaching the real, shared bd/dolt database (construct-y4iv) --
|
|
201
|
+
// CONSTRUCT_ROLES_ROOT isolates the dedup-fingerprint state but is also set
|
|
202
|
+
// legitimately in production (lib/oracle/execute.mjs), so it can't double as
|
|
203
|
+
// a "we're in a test" signal. Callers that don't inject a client get the real
|
|
204
|
+
// one, so production behavior (and every other existing caller) is unchanged.
|
|
205
|
+
|
|
206
|
+
async function createBdIncident(event, routeResult, { runBd: runBdImpl = runBd } = {}) {
|
|
200
207
|
// Dedup: check if this fingerprint has already been raised as a bead
|
|
201
208
|
if (fingerprintAlreadyRaised(event.fingerprint)) {
|
|
202
209
|
return { success: true, skipped: true, reason: 'fingerprint-already-raised', fingerprint: event.fingerprint };
|
|
@@ -221,7 +228,7 @@ async function createBdIncident(event, routeResult) {
|
|
|
221
228
|
null,
|
|
222
229
|
2
|
|
223
230
|
);
|
|
224
|
-
const result = await
|
|
231
|
+
const result = await runBdImpl(
|
|
225
232
|
['create', title, '-t', 'bug', '-l', labels, '-d', body],
|
|
226
233
|
{ actor: `roles:${routeResult.personaId}`, silent: true, timeoutSeconds: 10, commandTimeoutSeconds: 30 }
|
|
227
234
|
);
|
|
@@ -246,7 +253,7 @@ async function emitToastBestEffort(payload) {
|
|
|
246
253
|
} catch { /* dashboard not loaded; toast is best-effort */ }
|
|
247
254
|
}
|
|
248
255
|
|
|
249
|
-
export async function tryEscalate(event) {
|
|
256
|
+
export async function tryEscalate(event, deps = {}) {
|
|
250
257
|
const r = routeEvent(event);
|
|
251
258
|
if (!r) return { escalated: false, event, reason: 'no-owner' };
|
|
252
259
|
|
|
@@ -267,7 +274,7 @@ export async function tryEscalate(event) {
|
|
|
267
274
|
}
|
|
268
275
|
} catch { /* cost ledger optional — never block on missing module */ }
|
|
269
276
|
|
|
270
|
-
const bd = await createBdIncident(event, r);
|
|
277
|
+
const bd = await createBdIncident(event, r, deps);
|
|
271
278
|
if (!bd.success) {
|
|
272
279
|
return { escalated: false, event, reason: 'bd-create-failed', error: bd.error };
|
|
273
280
|
}
|
|
@@ -299,12 +306,12 @@ export async function tryEscalate(event) {
|
|
|
299
306
|
return { escalated: true, event, personaId: r.personaId, bdIssueId: bd.issueId, decision };
|
|
300
307
|
}
|
|
301
308
|
|
|
302
|
-
export async function recordAndMaybeInvoke(eventType, payload = {}) {
|
|
309
|
+
export async function recordAndMaybeInvoke(eventType, payload = {}, deps = {}) {
|
|
303
310
|
if (process.env.CONSTRUCT_ROLES === 'off') {
|
|
304
311
|
return { recorded: false, escalated: false, reason: 'global-off' };
|
|
305
312
|
}
|
|
306
313
|
const event = emit(eventType, payload);
|
|
307
|
-
const r = await tryEscalate(event);
|
|
314
|
+
const r = await tryEscalate(event, deps);
|
|
308
315
|
return { recorded: true, ...r };
|
|
309
316
|
}
|
|
310
317
|
|
|
@@ -366,7 +373,7 @@ export function prunePending({ now = Date.now() } = {}) {
|
|
|
366
373
|
// events.jsonl, runs each unescalated one through tryEscalate. Bounded by
|
|
367
374
|
// `sinceMs` and `maxProcess` so a long-quiet machine can't trigger a flood.
|
|
368
375
|
|
|
369
|
-
export async function processBacklog({ sinceMs = 60 * 60 * 1000, maxProcess = 10 } = {}) {
|
|
376
|
+
export async function processBacklog({ sinceMs = 60 * 60 * 1000, maxProcess = 10, deps = {} } = {}) {
|
|
370
377
|
if (process.env.CONSTRUCT_ROLES === 'off') return { processed: 0, escalated: 0, reason: 'global-off' };
|
|
371
378
|
|
|
372
379
|
const ep = _paths.eventsPath();
|
|
@@ -390,7 +397,7 @@ export async function processBacklog({ sinceMs = 60 * 60 * 1000, maxProcess = 10
|
|
|
390
397
|
for (const e of candidates.reverse()) {
|
|
391
398
|
if (processed >= maxProcess) break;
|
|
392
399
|
processed++;
|
|
393
|
-
const r = await tryEscalate(e);
|
|
400
|
+
const r = await tryEscalate(e, deps);
|
|
394
401
|
if (r.escalated) escalated++;
|
|
395
402
|
}
|
|
396
403
|
return { processed, escalated };
|
package/lib/roots.mjs
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/roots.mjs — canonical resolution of the three directory concepts in Construct.
|
|
3
|
+
*
|
|
4
|
+
* packageRoot — where the construct npm package is installed. Resolved from
|
|
5
|
+
* import.meta.url, stable across global install, npx, and local
|
|
6
|
+
* dev symlinks. Points to the directory that contains
|
|
7
|
+
* construct's own package.json. Locate bundled assets here:
|
|
8
|
+
* personas, skills, templates, schemas, hooks.
|
|
9
|
+
*
|
|
10
|
+
* projectRoot — the user's repo being operated on. Discovered at runtime by
|
|
11
|
+
* inspecting --project <dir> in argv, then walking up from cwd
|
|
12
|
+
* looking for .cx/ or package.json. Falls back to cwd when no
|
|
13
|
+
* marker is found. Services (oracle, embed, telemetry) that
|
|
14
|
+
* read or write to the user's repo must use this, not
|
|
15
|
+
* packageRoot.
|
|
16
|
+
*
|
|
17
|
+
* cwd — process.cwd(). Useful for relative-path resolution before
|
|
18
|
+
* projectRoot is determined. Callers should prefer
|
|
19
|
+
* projectRoot once it is resolved.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
// Under `bun build --compile`, every module's import.meta.url/dirname resolves
|
|
27
|
+
// inside the bundle's virtual filesystem (`/$bunfs/root/...`) rather than a
|
|
28
|
+
// real on-disk path, so any install-root resolution derived from it collapses
|
|
29
|
+
// to a directory that does not exist and every sibling-data-dir read (skills/,
|
|
30
|
+
// specialists/, templates/, config/, registry/) throws ENOENT. process.execPath
|
|
31
|
+
// still reports the compiled binary's real on-disk location, so callers whose
|
|
32
|
+
// naive resolution lands inside /$bunfs fall back to the binary's own
|
|
33
|
+
// directory. This assumes the data directories ship next to the binary
|
|
34
|
+
// (true for the CI/dev smoke path — dist/<binary> sits one level below the
|
|
35
|
+
// checkout root, the same depth bin/construct and lib/roots.mjs assume); a
|
|
36
|
+
// fully standalone end-user install still needs its own asset story
|
|
37
|
+
// (embedded assets or a co-installed data tree), tracked separately.
|
|
38
|
+
|
|
39
|
+
const BUN_VIRTUAL_FS_PREFIX = '/$bunfs';
|
|
40
|
+
|
|
41
|
+
export function isBunCompiledVirtualPath(candidatePath) {
|
|
42
|
+
return typeof process.versions?.bun === 'string' && candidatePath.startsWith(BUN_VIRTUAL_FS_PREFIX);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolveInstallRoot(naiveRoot, { execPath = process.execPath, upFromBinary = 1 } = {}) {
|
|
46
|
+
if (!isBunCompiledVirtualPath(naiveRoot)) return naiveRoot;
|
|
47
|
+
return path.resolve(path.dirname(execPath), ...Array(upFromBinary).fill('..'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Two dozen lib/*.mjs files use the standard Node "was I run directly"
|
|
51
|
+
// idiom (`import.meta.url === file://${process.argv[1]}`) so they double as
|
|
52
|
+
// standalone scripts. Under a Bun-compiled binary, every bundled module's
|
|
53
|
+
// import.meta.url collapses to the same virtual bunfs path as process.argv[1]
|
|
54
|
+
// (both point at the single compiled executable), so that comparison is true
|
|
55
|
+
// for every one of those files simultaneously — each runs its own top-level
|
|
56
|
+
// CLI logic as an unwanted side effect of merely being imported, and the
|
|
57
|
+
// first one whose logic throws (e.g. lib/headhunt.mjs's bare invocation
|
|
58
|
+
// requiring --for) takes the whole binary down before bin/construct's real
|
|
59
|
+
// dispatch ever executes. There is exactly one legitimate entry point inside
|
|
60
|
+
// a compiled binary (bin/construct itself), so this always resolves false
|
|
61
|
+
// under Bun-compile regardless of the module asking.
|
|
62
|
+
|
|
63
|
+
export function isMainModule(moduleUrl) {
|
|
64
|
+
if (typeof process.versions?.bun === 'string' && isBunCompiledVirtualPath(process.argv[1] || '')) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return moduleUrl === `file://${process.argv[1]}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// lib/roots.mjs lives one level below the package root (<package>/lib/roots.mjs).
|
|
71
|
+
|
|
72
|
+
export const packageRoot = resolveInstallRoot(path.resolve(fileURLToPath(import.meta.url), '..', '..'));
|
|
73
|
+
|
|
74
|
+
// Walk upward from `start`, looking for .cx/ first (Construct project marker),
|
|
75
|
+
// then package.json (generic JS project root). Stops at filesystem root.
|
|
76
|
+
|
|
77
|
+
function walkUp(start) {
|
|
78
|
+
let dir = path.resolve(start);
|
|
79
|
+
while (true) {
|
|
80
|
+
if (fs.existsSync(path.join(dir, '.cx'))) return dir;
|
|
81
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
|
|
82
|
+
const parent = path.dirname(dir);
|
|
83
|
+
if (parent === dir) return null;
|
|
84
|
+
dir = parent;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the project root for the user's repo.
|
|
90
|
+
*
|
|
91
|
+
* Resolution order:
|
|
92
|
+
* 1. --project <dir> in process.argv (explicit override)
|
|
93
|
+
* 2. Walk upward from cwd looking for .cx/ or package.json
|
|
94
|
+
* 3. cwd itself as a last resort
|
|
95
|
+
*
|
|
96
|
+
* Pass a cwd argument when the caller controls the starting directory (tests,
|
|
97
|
+
* sub-process workers). Omit it in top-level CLI code to use process.cwd().
|
|
98
|
+
*/
|
|
99
|
+
export function resolveProjectRoot(cwd = process.cwd()) {
|
|
100
|
+
const argv = process.argv;
|
|
101
|
+
const flagIndex = argv.indexOf('--project');
|
|
102
|
+
if (flagIndex !== -1 && argv[flagIndex + 1]) {
|
|
103
|
+
return path.resolve(argv[flagIndex + 1]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return walkUp(cwd) ?? path.resolve(cwd);
|
|
107
|
+
}
|