@geraldmaron/construct 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +36 -0
- package/README.md +41 -7
- package/bin/construct +1027 -64
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/lib/acp/server.mjs +21 -7
- package/lib/adapters-sync.mjs +2 -1
- package/lib/audit-trail.mjs +185 -2
- package/lib/beads/auto-close.mjs +2 -1
- package/lib/beads/drift.mjs +2 -1
- package/lib/bridges/copilot-proxy.mjs +20 -5
- package/lib/certification/skill-inventory.mjs +10 -1
- package/lib/cli/approvals.mjs +147 -0
- package/lib/cli-commands.mjs +105 -14
- package/lib/comment-lint.mjs +127 -8
- package/lib/config/schema.mjs +6 -29
- package/lib/config/source-target-registry.mjs +70 -0
- package/lib/config/source-targets.mjs +179 -205
- package/lib/contracts/coverage.mjs +77 -0
- package/lib/contracts/validate.mjs +102 -13
- package/lib/contracts/violation-log.mjs +50 -4
- package/lib/db/migrate.mjs +69 -0
- package/lib/db/migrations/001_orchestration_runs.sql +9 -0
- package/lib/db/migrations/002_queue_provider.sql +50 -0
- package/lib/db/migrations/003_worker_registry.sql +17 -0
- package/lib/db/migrations/004_trace_events.sql +16 -0
- package/lib/db/migrations/005_shared_memory.sql +15 -0
- package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
- package/lib/decisions/enforced-baseline.json +0 -2
- package/lib/decisions/registry.mjs +3 -2
- package/lib/deployment/parity-contract.mjs +1 -1
- package/lib/deployment-mode.mjs +78 -2
- package/lib/diagram-export.mjs +10 -1
- package/lib/distill.mjs +5 -2
- package/lib/docs-verify.mjs +2 -1
- package/lib/doctor/cli.mjs +1 -0
- package/lib/doctor/command-on-path.mjs +24 -0
- package/lib/doctor/diagnosis.mjs +89 -0
- package/lib/doctor/embedding-health.mjs +50 -0
- package/lib/doctor/engine-health.mjs +93 -0
- package/lib/doctor/graph-validate.mjs +47 -0
- package/lib/doctor/index.mjs +18 -4
- package/lib/doctor/sidecar-providers.mjs +56 -0
- package/lib/doctor/watchers/consistency.mjs +93 -1
- package/lib/doctor/watchers/cx-budget.mjs +1 -1
- package/lib/doctor/watchers/graph-staleness.mjs +1 -1
- package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
- package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
- package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
- package/lib/doctor/watchers/provider-breaker.mjs +78 -0
- package/lib/document-export.mjs +52 -27
- package/lib/document-extract/docling-client.mjs +9 -5
- package/lib/document-extract/docling-sidecar.py +30 -0
- package/lib/document-extract.mjs +1 -1
- package/lib/document-ingest.mjs +9 -0
- package/lib/embed/approval-queue.mjs +184 -88
- package/lib/embed/authority-guard.mjs +65 -2
- package/lib/embed/capability-jobs.mjs +365 -0
- package/lib/embed/capability-lifecycle.mjs +219 -0
- package/lib/embed/capability-loader.mjs +303 -0
- package/lib/embed/capability-runtime.mjs +58 -0
- package/lib/embed/cli.mjs +185 -8
- package/lib/embed/config.mjs +4 -0
- package/lib/embed/daemon.mjs +125 -6
- package/lib/embed/demand-fetch.mjs +190 -54
- package/lib/embed/inbox.mjs +12 -10
- package/lib/embed/presets/ops-triage.mjs +236 -0
- package/lib/embed/presets/pm-feedback.mjs +341 -0
- package/lib/embed/presets/tpm.mjs +401 -0
- package/lib/embed/providers/jira.mjs +72 -1
- package/lib/embed/providers/registry.mjs +140 -33
- package/lib/embedded-contract/capability.mjs +4 -0
- package/lib/embedded-contract/execution.mjs +1 -1
- package/lib/embedded-contract/model-resolve.mjs +53 -3
- package/lib/embedded-contract/workflow-defs.mjs +48 -73
- package/lib/embedded-contract/workflow-invoke.mjs +144 -4
- package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
- package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
- package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
- package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
- package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
- package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
- package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
- package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
- package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
- package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
- package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
- package/lib/env-config.mjs +50 -9
- package/lib/extensions/index.mjs +27 -0
- package/lib/extensions/loader.mjs +120 -0
- package/lib/extensions/manifest-schema.mjs +141 -0
- package/lib/extensions/manifests/anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
- package/lib/extensions/manifests/directory.manifest.json +12 -0
- package/lib/extensions/manifests/docling.manifest.json +37 -0
- package/lib/extensions/manifests/echo.manifest.json +8 -0
- package/lib/extensions/manifests/feedback.manifest.json +12 -0
- package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
- package/lib/extensions/manifests/github.manifest.json +52 -0
- package/lib/extensions/manifests/linear.manifest.json +50 -0
- package/lib/extensions/manifests/local.manifest.json +12 -0
- package/lib/extensions/manifests/ollama.manifest.json +12 -0
- package/lib/extensions/manifests/openai.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter.manifest.json +12 -0
- package/lib/extensions/manifests/postgres.manifest.json +12 -0
- package/lib/extensions/manifests/salesforce.manifest.json +12 -0
- package/lib/extensions/manifests/slack.manifest.json +58 -0
- package/lib/extensions/manifests/whisper.manifest.json +36 -0
- package/lib/extensions/validate.mjs +175 -0
- package/lib/features.mjs +13 -9
- package/lib/flows/checkpoint.mjs +184 -0
- package/lib/flows/constants.mjs +27 -0
- package/lib/flows/define.mjs +146 -0
- package/lib/flows/engine.mjs +249 -0
- package/lib/flows/errors.mjs +19 -0
- package/lib/flows/index.mjs +27 -0
- package/lib/flows/joins.mjs +18 -0
- package/lib/flows/schema.mjs +90 -0
- package/lib/flows/state.mjs +31 -0
- package/lib/frameworks/loader.mjs +99 -0
- package/lib/frameworks/schema.mjs +157 -0
- package/lib/graph/build-from-corpus.mjs +67 -0
- package/lib/graph/build-from-embed.mjs +82 -0
- package/lib/graph/build-from-registry.mjs +164 -3
- package/lib/graph/cli.mjs +456 -12
- package/lib/graph/gap-queries.mjs +156 -0
- package/lib/graph/gaps.mjs +41 -0
- package/lib/graph/impacted.mjs +129 -0
- package/lib/graph/runtime-evidence.mjs +177 -0
- package/lib/graph/security-coverage.mjs +113 -0
- package/lib/graph/staleness.mjs +241 -10
- package/lib/graph/store.mjs +24 -7
- package/lib/graph/validate.mjs +161 -0
- package/lib/headhunt.mjs +9 -8
- package/lib/health-check.mjs +15 -7
- package/lib/hook-health.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +10 -4
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/graph-impact-advisory.mjs +2 -2
- package/lib/hooks/guard-bash.mjs +41 -15
- package/lib/hooks/mcp-health-check.mjs +11 -4
- package/lib/hooks/model-fallback.mjs +50 -6
- package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
- package/lib/hooks/pre-compact.mjs +181 -173
- package/lib/hooks/rule-verifier.mjs +2 -1
- package/lib/hooks/session-reflect.mjs +2 -1
- package/lib/hooks/session-start.mjs +3 -3
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +19 -7
- package/lib/identity.mjs +92 -0
- package/lib/ingest/degraded-extract.mjs +96 -0
- package/lib/ingest/docling-remote.mjs +2 -1
- package/lib/ingest/provider-extract.mjs +7 -19
- package/lib/ingest/sidecar-providers.mjs +196 -0
- package/lib/ingest-tooling.mjs +11 -5
- package/lib/init-unified.mjs +57 -45
- package/lib/init-update.mjs +2 -1
- package/lib/install/legacy-global-cleanup.mjs +25 -0
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/queue-registry.mjs +50 -0
- package/lib/intake/queue.mjs +133 -17
- package/lib/intake/session-prelude.mjs +57 -8
- package/lib/integrations/intake-integrations.mjs +61 -111
- package/lib/intent-classifier.mjs +43 -5
- package/lib/libreoffice-export.mjs +8 -8
- package/lib/logging/rotate.mjs +5 -4
- package/lib/mcp/broker.mjs +332 -17
- package/lib/mcp/denied-store.mjs +95 -0
- package/lib/mcp/destructive-gate.mjs +30 -0
- package/lib/mcp/dispatch-envelope.mjs +199 -0
- package/lib/mcp/memory-bridge.mjs +2 -1
- package/lib/mcp/server.mjs +154 -1411
- package/lib/mcp/tool-definitions-memory.mjs +376 -0
- package/lib/mcp/tool-definitions-project.mjs +271 -0
- package/lib/mcp/tool-definitions-skills.mjs +311 -0
- package/lib/mcp/tool-definitions-workflow.mjs +398 -0
- package/lib/mcp/tool-definitions.mjs +25 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +1 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +165 -25
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/provider-write.mjs +187 -0
- package/lib/mcp/tools/scope.mjs +0 -3
- package/lib/mcp/tools/skills.mjs +1 -1
- package/lib/mcp/tools/storage.mjs +0 -8
- package/lib/mcp/transport/auth.mjs +238 -0
- package/lib/mcp/transport/http.mjs +136 -0
- package/lib/mcp/transport/mode.mjs +31 -0
- package/lib/mcp/transport/stdio.mjs +22 -0
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +34 -23
- package/lib/mcp-platform-config.mjs +31 -7
- package/lib/mode-capabilities.mjs +109 -0
- package/lib/model-router.mjs +42 -9
- package/lib/models/catalog.mjs +40 -1
- package/lib/net-guard.mjs +247 -0
- package/lib/observation-store.mjs +6 -2
- package/lib/ollama/provision-context.mjs +2 -1
- package/lib/opencode-config.mjs +22 -3
- package/lib/opencode-runtime-plugin.mjs +120 -2
- package/lib/oracle/actions.mjs +204 -10
- package/lib/oracle/cli.mjs +24 -3
- package/lib/oracle/dispatch.mjs +2 -1
- package/lib/oracle/execute.mjs +2 -2
- package/lib/oracle/gaps.mjs +3 -3
- package/lib/oracle/heartbeat.mjs +53 -0
- package/lib/oracle/read-model.mjs +69 -13
- package/lib/oracle/reconcile.mjs +3 -46
- package/lib/oracle/routing.mjs +37 -31
- package/lib/oracle/synthesize.mjs +1 -1
- package/lib/orchestration/classification.mjs +434 -0
- package/lib/orchestration/delegation-flow.mjs +132 -0
- package/lib/orchestration/flow-selection.mjs +418 -0
- package/lib/orchestration/gates.mjs +244 -0
- package/lib/orchestration/host-sampling.mjs +121 -0
- package/lib/orchestration/policy-constants.mjs +48 -0
- package/lib/orchestration/provider-outcome.mjs +197 -0
- package/lib/orchestration/readiness.mjs +114 -13
- package/lib/orchestration/run-store-postgres.mjs +32 -26
- package/lib/orchestration/run-store-sqlite.mjs +8 -14
- package/lib/orchestration/run-store.mjs +25 -12
- package/lib/orchestration/runtime.mjs +446 -39
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +3 -2
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +528 -89
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/packs/cli.mjs +144 -0
- package/lib/packs/core-pack.mjs +112 -0
- package/lib/packs/enablement.mjs +187 -0
- package/lib/packs/index.mjs +23 -0
- package/lib/packs/loader.mjs +176 -0
- package/lib/packs/manifest-schema.mjs +58 -0
- package/lib/packs/prompts.mjs +120 -0
- package/lib/packs/validate.mjs +208 -0
- package/lib/plugin-registry.mjs +4 -5
- package/lib/policy/audit-gate.mjs +42 -0
- package/lib/policy/consumption-budget.mjs +149 -0
- package/lib/policy/engine.mjs +81 -6
- package/lib/policy/role-authority.mjs +89 -0
- package/lib/prompt-composer.js +19 -3
- package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
- package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
- package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
- package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
- package/lib/providers/contract/adapters/github/index.mjs +38 -3
- package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
- package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
- package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
- package/lib/providers/contract/adapters/jira/manifest.json +17 -0
- package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
- package/lib/providers/contract.mjs +207 -0
- package/lib/providers/credential-bootstrap.mjs +7 -4
- package/lib/providers/credential-sources.mjs +14 -2
- package/lib/providers/directory/index.mjs +273 -0
- package/lib/providers/feedback/index.mjs +392 -0
- package/lib/providers/filter-audit.mjs +68 -0
- package/lib/providers/filter-schema.mjs +54 -0
- package/lib/providers/github/index.mjs +31 -0
- package/lib/providers/instance-config.mjs +215 -0
- package/lib/providers/op-locate.mjs +96 -0
- package/lib/providers/op-run.mjs +64 -9
- package/lib/providers/registry.mjs +26 -3
- package/lib/providers/secret-audit-wiring.mjs +6 -0
- package/lib/providers/secret-resolver.mjs +240 -23
- package/lib/publish-template.mjs +6 -3
- package/lib/publish-tooling.mjs +4 -0
- package/lib/publish.mjs +32 -4
- package/lib/queue/pg-queue.mjs +395 -0
- package/lib/reflect.mjs +2 -1
- package/lib/registry/assemble.mjs +28 -2
- package/lib/registry/consolidation.mjs +8 -1
- package/lib/registry/custom-scaffold.mjs +200 -0
- package/lib/registry/custom-schema.mjs +137 -0
- package/lib/registry/loader.mjs +8 -3
- package/lib/registry/manifests/format-engines.default.json +15 -0
- package/lib/registry/manifests/surface-map.default.json +46 -0
- package/lib/registry/retired-paths.mjs +1 -0
- package/lib/registry/surface-map.mjs +100 -50
- package/lib/render-visual-check.mjs +240 -0
- package/lib/resources/budget.mjs +40 -17
- package/lib/roles/flavor-bindings.mjs +36 -14
- package/lib/roles/gateway.mjs +15 -8
- package/lib/roots.mjs +107 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -6
- package/lib/runtime/whisper-bootstrap.mjs +11 -3
- package/lib/runtime-env.mjs +5 -2
- package/lib/scheduler/index.mjs +8 -20
- package/lib/schema-infer.mjs +31 -2
- package/lib/scopes/lifecycle.mjs +29 -25
- package/lib/scopes/loader.mjs +49 -12
- package/lib/scopes/teams.mjs +1 -1
- package/lib/security/ingest-boundary.mjs +80 -0
- package/lib/security/recall-wrapper.mjs +99 -0
- package/lib/security/trust.mjs +132 -0
- package/lib/service-manager.mjs +38 -19
- package/lib/setup.mjs +41 -23
- package/lib/skills-apply.mjs +4 -2
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/state-root.mjs +147 -0
- package/lib/status.mjs +597 -6
- package/lib/storage/admin.mjs +77 -5
- package/lib/storage/backend-registry.mjs +73 -0
- package/lib/storage/backend.mjs +63 -9
- package/lib/storage/embeddings-openai.mjs +12 -2
- package/lib/storage/hybrid-query.mjs +11 -3
- package/lib/storage/retrieval-hardening.mjs +384 -0
- package/lib/storage/shared-memory.mjs +158 -0
- package/lib/storage/vector-client.mjs +69 -2
- package/lib/team/health.mjs +72 -0
- package/lib/telemetry/backends/local.mjs +9 -3
- package/lib/telemetry/client.mjs +3 -7
- package/lib/template-registry.mjs +10 -12
- package/lib/tenant/context.mjs +82 -0
- package/lib/tenant/isolation.mjs +129 -0
- package/lib/test-corpus-inventory.mjs +104 -2
- package/lib/uninstall/uninstall.mjs +78 -12
- package/lib/validator.mjs +4 -6
- package/lib/worker/entrypoint.mjs +2 -1
- package/lib/worker/run.mjs +2 -2
- package/lib/worker/trace.mjs +5 -11
- package/lib/workflow-state.mjs +52 -8
- package/lib/workflows/liveness.mjs +203 -0
- package/lib/workflows/loader.mjs +177 -0
- package/lib/workflows/manifest-schema.mjs +71 -0
- package/lib/workflows/surface-parity.mjs +118 -0
- package/lib/workflows/validate.mjs +127 -0
- package/lib/writes/control-plane.mjs +140 -0
- package/lib/writes/envelope.mjs +206 -0
- package/lib/writes/sent-log.mjs +86 -0
- package/lib/writes/write-intent.mjs +109 -0
- package/package.json +16 -8
- package/personas/construct.md +12 -3
- package/registry/capabilities.json +143 -36
- package/rules/common/comments.md +1 -0
- package/schemas/project-config.schema.json +0 -12
- package/schemas/unified-registry.schema.json +2 -4
- package/scripts/sync-specialists.mjs +284 -81
- package/skills/docs/adr-workflow.md +1 -1
- package/skills/docs/codebase-research-workflow.md +3 -3
- package/skills/docs/prd-workflow.md +5 -6
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/user-research-workflow.md +3 -3
- package/skills/operating/fleet-health-routing.md +77 -0
- package/skills/roles/ai-engineer.md +1 -1
- package/skills/roles/architect.md +0 -1
- package/skills/roles/business-strategist.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/data-engineer.md +1 -1
- package/skills/roles/data-engineer.pipeline.md +1 -1
- package/skills/roles/data-engineer.vector-retrieval.md +1 -2
- package/skills/roles/data-engineer.warehouse.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +0 -1
- package/skills/roles/devil-advocate.md +1 -1
- package/skills/roles/docs-keeper.md +1 -1
- package/skills/roles/evaluator.md +1 -1
- package/skills/roles/explorer.md +1 -1
- package/skills/roles/platform-engineer.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -2
- package/skills/roles/qa.api-contract.md +0 -1
- package/skills/roles/qa.data-pipeline.md +0 -1
- package/skills/roles/qa.md +0 -1
- package/skills/roles/qa.web-ui.md +0 -1
- package/skills/roles/release-manager.md +1 -1
- package/skills/roles/researcher.md +0 -2
- package/skills/roles/reviewer.md +0 -3
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +0 -1
- package/skills/roles/security.privacy.md +0 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/sre.md +1 -1
- package/skills/roles/test-automation.md +1 -1
- package/skills/roles/trace-reviewer.md +1 -1
- package/skills/roles/ux-researcher.md +1 -1
- package/specialists/artifact-manifest.json +36 -36
- package/specialists/org/contracts/accessibility-to-qa.json +11 -3
- package/specialists/org/contracts/any-to-business-strategist.json +19 -7
- package/specialists/org/contracts/any-to-debugger.json +7 -1
- package/specialists/org/contracts/any-to-designer.json +7 -1
- package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
- package/specialists/org/contracts/any-to-explorer.json +13 -4
- package/specialists/org/contracts/any-to-sre-incident.json +6 -2
- package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
- package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
- package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
- package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
- package/specialists/org/contracts/architect-to-engineer.json +20 -4
- package/specialists/org/contracts/architect-to-evaluator.json +15 -5
- package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
- package/specialists/org/contracts/architect-to-operations.json +17 -4
- package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
- package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
- package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
- package/specialists/org/contracts/engineer-to-qa.json +20 -4
- package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
- package/specialists/org/contracts/explorer-to-engineer.json +11 -3
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
- package/specialists/org/contracts/operations-to-user.json +53 -0
- package/specialists/org/contracts/operations-triage-output.json +53 -0
- package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
- package/specialists/org/contracts/product-manager-to-architect.json +30 -10
- package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
- package/specialists/org/contracts/qa-to-release-manager.json +11 -3
- package/specialists/org/contracts/researcher-to-architect.json +10 -2
- package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
- package/specialists/org/contracts/reviewer-to-security.json +17 -3
- package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
- package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
- package/specialists/org/contracts/user-to-construct.json +11 -4
- package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
- package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
- package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
- package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
- package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
- package/specialists/org/groups/engineering-group.json +2 -6
- package/specialists/org/groups/governance-group.json +2 -3
- package/specialists/org/groups/operations-group.json +5 -8
- package/specialists/org/groups/product-group.json +4 -8
- package/specialists/org/groups/quality-group.json +3 -7
- package/specialists/org/groups/strategy-group.json +6 -9
- package/specialists/org/models.json.example +8 -0
- package/specialists/org/scopes/creative.json +6 -1
- package/specialists/org/scopes/operations.json +7 -1
- package/specialists/org/scopes/research.json +6 -1
- package/specialists/org/scopes/rnd.json +7 -1
- package/specialists/org/specialists/cx-architect.json +27 -8
- package/specialists/org/specialists/cx-designer.json +22 -8
- package/specialists/org/specialists/cx-engineer.json +71 -7
- package/specialists/org/specialists/cx-operations.json +89 -15
- package/specialists/org/specialists/cx-orchestrator.json +16 -4
- package/specialists/org/specialists/cx-product-manager.json +28 -9
- package/specialists/org/specialists/cx-qa.json +16 -6
- package/specialists/org/specialists/cx-researcher.json +40 -7
- package/specialists/org/specialists/cx-reviewer.json +61 -11
- package/specialists/org/specialists/cx-security.json +30 -10
- package/specialists/org/teams/design-team.json +3 -4
- package/specialists/org/teams/engineering-team.json +3 -10
- package/specialists/org/teams/governance-team.json +3 -5
- package/specialists/org/teams/operations-team.json +6 -12
- package/specialists/org/teams/product-management-team.json +2 -3
- package/specialists/org/teams/quality-team.json +4 -12
- package/specialists/org/teams/research-team.json +3 -3
- package/specialists/org/teams/strategy-team.json +7 -14
- package/specialists/prompts/cx-architect.md +20 -1
- package/specialists/prompts/cx-data-analyst.md +1 -1
- package/specialists/prompts/cx-designer.md +12 -4
- package/specialists/prompts/cx-engineer.md +15 -1
- package/specialists/prompts/cx-operations.md +14 -2
- package/specialists/prompts/cx-orchestrator.md +9 -5
- package/specialists/prompts/cx-product-manager.md +5 -1
- package/specialists/prompts/cx-qa.md +6 -2
- package/specialists/prompts/cx-researcher.md +25 -30
- package/specialists/prompts/cx-reviewer.md +19 -1
- package/specialists/prompts/cx-security.md +5 -1
- package/templates/distribution/construct-brand.typ +123 -59
- package/templates/distribution/construct-decision.typ +6 -3
- package/templates/distribution/construct-pdf.typ +11 -4
- package/templates/distribution/construct-prd.typ +6 -3
- package/templates/distribution/construct-research.typ +7 -4
- package/lib/providers/contract/adapters/git/index.mjs +0 -115
- package/lib/providers/contract/adapters/slack/index.mjs +0 -175
- package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
- package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
- package/specialists/org/contracts/designer-to-accessibility.json +0 -36
- package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
- package/specialists/org/contracts/qa-to-test-automation.json +0 -42
- package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
- package/specialists/org/contracts/sre-to-release-manager.json +0 -36
- package/specialists/org/specialists/cx-accessibility.json +0 -69
- package/specialists/org/specialists/cx-ai-engineer.json +0 -75
- package/specialists/org/specialists/cx-business-strategist.json +0 -72
- package/specialists/org/specialists/cx-data-engineer.json +0 -71
- package/specialists/org/specialists/cx-devil-advocate.json +0 -71
- package/specialists/org/specialists/cx-docs-keeper.json +0 -82
- package/specialists/org/specialists/cx-evaluator.json +0 -69
- package/specialists/org/specialists/cx-explorer.json +0 -69
- package/specialists/org/specialists/cx-legal-compliance.json +0 -74
- package/specialists/org/specialists/cx-oracle.json +0 -46
- package/specialists/org/specialists/cx-platform-engineer.json +0 -80
- package/specialists/org/specialists/cx-rd-lead.json +0 -73
- package/specialists/org/specialists/cx-release-manager.json +0 -77
- package/specialists/org/specialists/cx-sre.json +0 -94
- package/specialists/org/specialists/cx-test-automation.json +0 -69
- package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
- package/specialists/org/specialists/cx-ux-researcher.json +0 -68
- package/specialists/org/teams/accessibility-team.json +0 -39
- package/specialists/org/teams/ux-research-team.json +0 -39
- package/specialists/prompts/cx-accessibility.md +0 -55
- package/specialists/prompts/cx-ai-engineer.md +0 -124
- package/specialists/prompts/cx-business-strategist.md +0 -58
- package/specialists/prompts/cx-data-engineer.md +0 -55
- package/specialists/prompts/cx-devil-advocate.md +0 -59
- package/specialists/prompts/cx-docs-keeper.md +0 -164
- package/specialists/prompts/cx-evaluator.md +0 -49
- package/specialists/prompts/cx-explorer.md +0 -71
- package/specialists/prompts/cx-legal-compliance.md +0 -58
- package/specialists/prompts/cx-oracle.md +0 -98
- package/specialists/prompts/cx-platform-engineer.md +0 -97
- package/specialists/prompts/cx-rd-lead.md +0 -59
- package/specialists/prompts/cx-release-manager.md +0 -54
- package/specialists/prompts/cx-sre.md +0 -111
- package/specialists/prompts/cx-test-automation.md +0 -55
- package/specialists/prompts/cx-trace-reviewer.md +0 -101
- package/specialists/prompts/cx-ux-researcher.md +0 -53
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/registry/custom-schema.mjs — validation contract for user-authored custom
|
|
3
|
+
* specialists and teams (construct-rf26.13).
|
|
4
|
+
*
|
|
5
|
+
* Hand-rolled validator, matching the style already established by
|
|
6
|
+
* lib/specialists/schema.mjs and lib/config/schema.mjs, rather than pulling in
|
|
7
|
+
* a schema-validation dependency: every error names the offending field and
|
|
8
|
+
* says what to do about it, so a scaffold failure is actionable without
|
|
9
|
+
* cross-referencing a generic validator dump. Reuses lib/specialists/schema.mjs's
|
|
10
|
+
* ALLOWED_TOOLS allowlist so a custom specialist is held to the same
|
|
11
|
+
* claudeTools contract as a built-in one.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { ALLOWED_TOOLS } from '../specialists/schema.mjs';
|
|
17
|
+
|
|
18
|
+
export const MODEL_TIERS = ['fast', 'standard', 'reasoning'];
|
|
19
|
+
const SKILL_REF_RE = /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/;
|
|
20
|
+
const ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate one custom specialist record. Returns an array of actionable error
|
|
24
|
+
* strings; empty means the record passes.
|
|
25
|
+
*/
|
|
26
|
+
export function validateCustomSpecialist(record, { rootDir, checkPromptFileExists = true } = {}) {
|
|
27
|
+
const errors = [];
|
|
28
|
+
const id = record?.name || '(unnamed)';
|
|
29
|
+
|
|
30
|
+
if (!record || typeof record !== 'object') {
|
|
31
|
+
return [`custom specialist: record must be an object`];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!record.name || !ID_RE.test(record.name)) {
|
|
35
|
+
errors.push(`${id}: "name" must be lowercase kebab-case (e.g. "widget-specialist"), got ${JSON.stringify(record.name)}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!record.description || String(record.description).trim().length < 20) {
|
|
39
|
+
errors.push(`${id}: "description" is required and must be at least 20 characters — it is load-bearing for orchestration routing`);
|
|
40
|
+
} else if (String(record.description).length > 500) {
|
|
41
|
+
errors.push(`${id}: "description" is too long (>500 chars) — keep it scannable`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!record.role || typeof record.role !== 'string') {
|
|
45
|
+
errors.push(`${id}: "role" is required (e.g. "widget-specialist") — a team's "owner" field must match a specialist's "role" for the team to validate`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!record.modelTier || !MODEL_TIERS.includes(record.modelTier)) {
|
|
49
|
+
errors.push(`${id}: "modelTier" must be one of ${MODEL_TIERS.join(' | ')}, got ${JSON.stringify(record.modelTier)}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const team = record.team || record.teamId;
|
|
53
|
+
if (!team || typeof team !== 'string') {
|
|
54
|
+
errors.push(`${id}: "team" (or "teamId") is required — reference an existing team id, or create one first with \`construct team create <id> --owner=${record.role || '<role>'}\``);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!Array.isArray(record.skills) || record.skills.length === 0) {
|
|
58
|
+
errors.push(`${id}: "skills" must be a non-empty array of skill bundle references (e.g. ["frontend-design/accessibility"])`);
|
|
59
|
+
} else {
|
|
60
|
+
record.skills.forEach((skill, i) => {
|
|
61
|
+
if (typeof skill !== 'string' || !SKILL_REF_RE.test(skill)) {
|
|
62
|
+
errors.push(`${id}: skills[${i}] "${skill}" is not a valid skill bundle reference — expected "<bundle>/<skill>" (e.g. "frontend-design/accessibility")`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!record.fence || typeof record.fence !== 'object' || Array.isArray(record.fence)) {
|
|
68
|
+
errors.push(`${id}: "fence" is required — at minimum { "allowedPaths": [...] } to declare the permission boundary`);
|
|
69
|
+
} else if (!Array.isArray(record.fence.allowedPaths) || record.fence.allowedPaths.length === 0) {
|
|
70
|
+
errors.push(`${id}: "fence.allowedPaths" must be a non-empty array of path globs this specialist may touch`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (record.handoffCandidates !== undefined && !Array.isArray(record.handoffCandidates)) {
|
|
74
|
+
errors.push(`${id}: "handoffCandidates" must be an array of role ids (the delegation spec — who this specialist hands off to)`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (record.claudeTools) {
|
|
78
|
+
const tools = String(record.claudeTools).split(',').map((t) => t.trim()).filter(Boolean);
|
|
79
|
+
for (const tool of tools) {
|
|
80
|
+
if (!ALLOWED_TOOLS.has(tool)) {
|
|
81
|
+
errors.push(`${id}: unknown tool "${tool}" in claudeTools — host platforms silently drop unknown tool names, causing this specialist to appear to work without it`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (checkPromptFileExists && record.promptFile && rootDir) {
|
|
87
|
+
const promptPath = path.join(rootDir, record.promptFile);
|
|
88
|
+
if (!fs.existsSync(promptPath)) {
|
|
89
|
+
errors.push(`${id}: promptFile "${record.promptFile}" does not exist under ${rootDir}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return errors;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate one custom team record. Returns an array of actionable error
|
|
98
|
+
* strings; empty means the record passes.
|
|
99
|
+
*/
|
|
100
|
+
export function validateCustomTeam(record) {
|
|
101
|
+
const errors = [];
|
|
102
|
+
const id = record?.id || record?.name || '(unnamed)';
|
|
103
|
+
|
|
104
|
+
if (!record || typeof record !== 'object') {
|
|
105
|
+
return [`custom team: record must be an object`];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!record.name || typeof record.name !== 'string') {
|
|
109
|
+
errors.push(`${id}: "name" is required (human-readable team name, e.g. "Widget Team")`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!record.owner || typeof record.owner !== 'string') {
|
|
113
|
+
errors.push(`${id}: "owner" is required — the role id that holds primary accountability (must match a specialist's "role" field)`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!Array.isArray(record.roles) || record.roles.length === 0) {
|
|
117
|
+
errors.push(`${id}: "roles" must be a non-empty array of role ids that constitute this team`);
|
|
118
|
+
} else if (record.owner && !record.roles.includes(record.owner)) {
|
|
119
|
+
errors.push(`${id}: "roles" must include the owner role "${record.owner}"`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!record.charter || String(record.charter).trim().length < 20) {
|
|
123
|
+
errors.push(`${id}: "charter" is required and must be at least 20 characters — a one-paragraph mission statement for what this team owns`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const field of ['decisionRights', 'forbiddenDecisions', 'escalationPath', 'specialists']) {
|
|
127
|
+
if (record[field] !== undefined && !Array.isArray(record[field])) {
|
|
128
|
+
errors.push(`${id}: "${field}" must be an array of strings when present`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (record.contact !== undefined && (typeof record.contact !== 'object' || Array.isArray(record.contact))) {
|
|
133
|
+
errors.push(`${id}: "contact" must be an object when present (e.g. { "slack": "#team", "owner": "role-id" })`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return errors;
|
|
137
|
+
}
|
package/lib/registry/loader.mjs
CHANGED
|
@@ -10,12 +10,17 @@
|
|
|
10
10
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import path from 'path';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
13
|
import { validate } from './validator.mjs';
|
|
15
14
|
import { assembleRegistry, orgDirMtime, listTeamsFromRegistry, listGroupsHierarchical } from './assemble.mjs';
|
|
15
|
+
import { packageRoot } from '../roots.mjs';
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// packageRoot (not a local __dirname-derived constant) because under a
|
|
18
|
+
// Bun-compiled binary every bundled module's import.meta.url collapses to
|
|
19
|
+
// the same virtual /$bunfs/root path, losing this file's real nesting
|
|
20
|
+
// under lib/registry/; packageRoot already falls back to the compiled
|
|
21
|
+
// binary's real on-disk directory (see lib/roots.mjs).
|
|
22
|
+
|
|
23
|
+
const ROOT_DIR = packageRoot;
|
|
19
24
|
|
|
20
25
|
let _registry = null;
|
|
21
26
|
let _rootDir = null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pdf": { "engine": "pandoc", "writer": null, "pdfEngine": "typst", "extraBinaries": ["typst"] },
|
|
3
|
+
"docx": { "engine": "pandoc", "writer": null, "pdfEngine": null, "extraBinaries": [] },
|
|
4
|
+
"doc": { "engine": "pandoc", "writer": null, "pdfEngine": null, "extraBinaries": [], "via": "libreoffice", "intermediate": "docx" },
|
|
5
|
+
"deck": { "engine": "pandoc", "writer": null, "pdfEngine": null, "extraBinaries": [] },
|
|
6
|
+
"pptx": { "engine": "pptxgenjs", "writer": null, "pdfEngine": null, "extraBinaries": [] },
|
|
7
|
+
"html": { "engine": "pandoc", "writer": null, "pdfEngine": null, "extraBinaries": [] },
|
|
8
|
+
"rtf": { "engine": "pandoc", "writer": "rtf", "pdfEngine": null, "extraBinaries": [] },
|
|
9
|
+
"odt": { "engine": "pandoc", "writer": "odt", "pdfEngine": null, "extraBinaries": [] },
|
|
10
|
+
"epub": { "engine": "pandoc", "writer": "epub", "pdfEngine": null, "extraBinaries": [] },
|
|
11
|
+
"tex": { "engine": "pandoc", "writer": "latex", "pdfEngine": null, "extraBinaries": [] },
|
|
12
|
+
"txt": { "engine": "pandoc", "writer": "plain", "pdfEngine": null, "extraBinaries": [] },
|
|
13
|
+
"md": { "engine": "copy", "writer": null, "pdfEngine": null, "extraBinaries": [] },
|
|
14
|
+
"mdx": { "engine": "copy", "writer": null, "pdfEngine": null, "extraBinaries": [] }
|
|
15
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tiers": ["agent-mcp", "thin-cli", "tui", "internal"],
|
|
3
|
+
"commands": {
|
|
4
|
+
"install": "thin-cli",
|
|
5
|
+
"init": "thin-cli",
|
|
6
|
+
"dev": "thin-cli",
|
|
7
|
+
"stop": "thin-cli",
|
|
8
|
+
"status": "thin-cli",
|
|
9
|
+
"doctor": "thin-cli",
|
|
10
|
+
"sync": "thin-cli",
|
|
11
|
+
"ingest": "agent-mcp",
|
|
12
|
+
"drop": "agent-mcp",
|
|
13
|
+
"distill": "agent-mcp",
|
|
14
|
+
"ask": "agent-mcp",
|
|
15
|
+
"search": "agent-mcp",
|
|
16
|
+
"knowledge": "agent-mcp",
|
|
17
|
+
"memory": "agent-mcp",
|
|
18
|
+
"reflect": "agent-mcp",
|
|
19
|
+
"intake": "tui",
|
|
20
|
+
"workflow": "agent-mcp",
|
|
21
|
+
"graph": "agent-mcp",
|
|
22
|
+
"capability": "agent-mcp",
|
|
23
|
+
"execution": "agent-mcp",
|
|
24
|
+
"orchestrate": "agent-mcp",
|
|
25
|
+
"models": "agent-mcp",
|
|
26
|
+
"profile": "tui",
|
|
27
|
+
"sandbox": "tui",
|
|
28
|
+
"review": "thin-cli",
|
|
29
|
+
"telemetry": "thin-cli",
|
|
30
|
+
"evals": "thin-cli",
|
|
31
|
+
"improvement": "thin-cli",
|
|
32
|
+
"ci": "thin-cli",
|
|
33
|
+
"docs": "thin-cli",
|
|
34
|
+
"export": "agent-mcp",
|
|
35
|
+
"diagram": "agent-mcp",
|
|
36
|
+
"demo": "agent-mcp",
|
|
37
|
+
"beads": "thin-cli",
|
|
38
|
+
"hook": "internal",
|
|
39
|
+
"lint:comments": "internal",
|
|
40
|
+
"lint:agents": "internal",
|
|
41
|
+
"registry:status": "internal",
|
|
42
|
+
"registry:validate": "internal",
|
|
43
|
+
"registry:generate-docs": "internal",
|
|
44
|
+
"rules": "thin-cli"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -29,6 +29,7 @@ export const RETIRED_REGISTRY_PATH_ALLOWLIST = Object.freeze([
|
|
|
29
29
|
'scripts/patch-registry-readers-v2.mjs',
|
|
30
30
|
'tests/no-monolithic-registry.test.mjs',
|
|
31
31
|
'tests/migration-round-trip.test.mjs',
|
|
32
|
+
'tests/fixtures/mcp-tool-schemas/results/project_context.result.json',
|
|
32
33
|
]);
|
|
33
34
|
|
|
34
35
|
export function isRetiredRegistryPathAllowed(relPath) {
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Declarative map from CLI command name to primary surface tier. The CLI registry
|
|
5
5
|
* remains the substrate; this file governs discovery posture without removing verbs.
|
|
6
|
+
* LMCP-B7: the tiers/command map ship as lib/registry/manifests/surface-map.default.json
|
|
7
|
+
* (loaded once at module init — this file's exports stay byte-identical to the prior
|
|
8
|
+
* hardcoded dict) with an optional `.cx/registry/surface-map.json` project override
|
|
9
|
+
* merged in per-call by resolveSurfaceMap()/surfaceForCommand()/validateSurfaceMap().
|
|
10
|
+
* An override may add or reassign command entries and replace the tier list; it can
|
|
11
|
+
* never remove a default entry outright (unset keys keep resolving to the default).
|
|
6
12
|
*
|
|
7
13
|
* Surfaces:
|
|
8
14
|
* agent-mcp — MCP tool canonical; CLI --json twin
|
|
@@ -11,64 +17,108 @@
|
|
|
11
17
|
* internal — CI/harness only
|
|
12
18
|
*/
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
import { packageRoot } from '../roots.mjs';
|
|
24
|
+
|
|
25
|
+
// packageRoot (not a local import.meta.url-derived HERE) because under a
|
|
26
|
+
// Bun-compiled binary every bundled module's import.meta.url collapses to
|
|
27
|
+
// the same virtual /$bunfs/root path, losing this file's real nesting
|
|
28
|
+
// under lib/registry/ — packageRoot already falls back to the compiled
|
|
29
|
+
// binary's real on-disk directory (see lib/roots.mjs), so rebuilding the
|
|
30
|
+
// known relative path from it resolves correctly in every run mode.
|
|
31
|
+
|
|
32
|
+
const DEFAULT_MANIFEST_PATH = path.join(packageRoot, 'lib', 'registry', 'manifests', 'surface-map.default.json');
|
|
33
|
+
|
|
34
|
+
function readJsonIfExists(filePath) {
|
|
35
|
+
try {
|
|
36
|
+
if (!fs.existsSync(filePath)) return null;
|
|
37
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadDefaultManifest() {
|
|
44
|
+
const manifest = readJsonIfExists(DEFAULT_MANIFEST_PATH);
|
|
45
|
+
if (!manifest || !Array.isArray(manifest.tiers) || typeof manifest.commands !== 'object') {
|
|
46
|
+
throw new Error(`lib/registry/surface-map.mjs: missing or malformed default manifest at ${DEFAULT_MANIFEST_PATH}`);
|
|
47
|
+
}
|
|
48
|
+
return manifest;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DEFAULT_MANIFEST = loadDefaultManifest();
|
|
52
|
+
|
|
53
|
+
// Byte-identical to the pre-manifest hardcoded exports: both are read once at
|
|
54
|
+
// module init from the default manifest, with no project override applied —
|
|
55
|
+
// a direct import of these two names sees exactly the shipped defaults.
|
|
56
|
+
|
|
57
|
+
export const SURFACE_TIERS = DEFAULT_MANIFEST.tiers;
|
|
15
58
|
|
|
16
59
|
/** @type {Record<string, string>} */
|
|
17
|
-
export const COMMAND_SURFACE =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
export: 'agent-mcp',
|
|
49
|
-
diagram: 'agent-mcp',
|
|
50
|
-
demo: 'agent-mcp',
|
|
51
|
-
beads: 'thin-cli',
|
|
52
|
-
hook: 'internal',
|
|
53
|
-
'lint:comments': 'internal',
|
|
54
|
-
'lint:agents': 'internal',
|
|
55
|
-
'registry:status': 'internal',
|
|
56
|
-
'registry:validate': 'internal',
|
|
57
|
-
'registry:generate-docs': 'internal',
|
|
58
|
-
rules: 'thin-cli',
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export function surfaceForCommand(name) {
|
|
62
|
-
if (COMMAND_SURFACE[name]) return COMMAND_SURFACE[name];
|
|
60
|
+
export const COMMAND_SURFACE = DEFAULT_MANIFEST.commands;
|
|
61
|
+
|
|
62
|
+
const overrideCache = new Map();
|
|
63
|
+
|
|
64
|
+
function loadOverride(cwd) {
|
|
65
|
+
if (overrideCache.has(cwd)) return overrideCache.get(cwd);
|
|
66
|
+
const override = readJsonIfExists(path.join(cwd, '.cx', 'registry', 'surface-map.json'));
|
|
67
|
+
overrideCache.set(cwd, override);
|
|
68
|
+
return override;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* resolveSurfaceMap({ cwd } = {})
|
|
73
|
+
*
|
|
74
|
+
* Merges the default manifest with a project's `.cx/registry/surface-map.json`
|
|
75
|
+
* override, when present. `commands` in the override are shallow-merged over
|
|
76
|
+
* the default (an override entry replaces the matching default; every other
|
|
77
|
+
* default entry survives untouched). `tiers` in the override replaces the
|
|
78
|
+
* default tier list wholesale when present.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveSurfaceMap({ cwd = process.cwd() } = {}) {
|
|
81
|
+
const override = loadOverride(cwd);
|
|
82
|
+
if (!override) return { tiers: SURFACE_TIERS, commands: COMMAND_SURFACE };
|
|
83
|
+
const tiers = Array.isArray(override.tiers) ? override.tiers : SURFACE_TIERS;
|
|
84
|
+
const commands = { ...COMMAND_SURFACE, ...(override.commands && typeof override.commands === 'object' ? override.commands : {}) };
|
|
85
|
+
return { tiers, commands };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function surfaceForCommand(name, { cwd = process.cwd() } = {}) {
|
|
89
|
+
const { commands } = resolveSurfaceMap({ cwd });
|
|
90
|
+
if (commands[name]) return commands[name];
|
|
63
91
|
if (name.includes(':')) return 'internal';
|
|
64
92
|
return 'thin-cli';
|
|
65
93
|
}
|
|
66
94
|
|
|
67
|
-
export function commandsBySurface(commands) {
|
|
68
|
-
const
|
|
95
|
+
export function commandsBySurface(commands, { cwd = process.cwd() } = {}) {
|
|
96
|
+
const { tiers } = resolveSurfaceMap({ cwd });
|
|
97
|
+
const grouped = Object.fromEntries(tiers.map((t) => [t, []]));
|
|
69
98
|
for (const cmd of commands) {
|
|
70
|
-
const surface = cmd.surface ?? surfaceForCommand(cmd.name);
|
|
99
|
+
const surface = cmd.surface ?? surfaceForCommand(cmd.name, { cwd });
|
|
71
100
|
grouped[surface]?.push(cmd.name);
|
|
72
101
|
}
|
|
73
102
|
return grouped;
|
|
74
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* validateSurfaceMap(commandNames, { cwd } = {})
|
|
107
|
+
*
|
|
108
|
+
* Stricter than surfaceForCommand's runtime fallback: every name must have an
|
|
109
|
+
* explicit entry in the resolved (default + override) command map. This is an
|
|
110
|
+
* opt-in registry/doctor-style check, separate from the CLI's own dispatch
|
|
111
|
+
* path, so the default fallback behavior for unregistered commands stays
|
|
112
|
+
* unaffected.
|
|
113
|
+
*/
|
|
114
|
+
export function validateSurfaceMap(commandNames, { cwd = process.cwd() } = {}) {
|
|
115
|
+
const { commands } = resolveSurfaceMap({ cwd });
|
|
116
|
+
const errors = commandNames
|
|
117
|
+
.filter((name) => !commands[name])
|
|
118
|
+
.map((name) => `command '${name}' has no surface-map entry (add one to .cx/registry/surface-map.json or lib/registry/manifests/surface-map.default.json)`);
|
|
119
|
+
return { valid: errors.length === 0, errors };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function __clearSurfaceMapOverrideCache() {
|
|
123
|
+
overrideCache.clear();
|
|
124
|
+
}
|
|
@@ -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
|
+
}
|