@geraldmaron/construct 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -18
- package/bin/construct +392 -91
- package/bin/construct-postinstall.mjs +30 -2
- package/examples/distribution/README.md +42 -0
- package/examples/distribution/manifest.json +53 -0
- package/examples/distribution/sources/adr.md +84 -0
- package/examples/distribution/sources/deck-one-pager.md +65 -0
- package/examples/distribution/sources/prd-platform.md +161 -0
- package/examples/distribution/sources/research-brief.md +80 -0
- package/examples/distribution/sources/rfc-platform.md +82 -0
- package/examples/distribution/sources/runbook.md +103 -0
- package/examples/distribution/sources/strategy.md +88 -0
- package/lib/adapters-sync.mjs +67 -0
- package/lib/artifact-gate-notice.mjs +38 -0
- package/lib/artifact-manifest.mjs +112 -0
- package/lib/artifact-release-gate.mjs +171 -0
- package/lib/artifact-reviewers.mjs +68 -0
- package/lib/artifact-type-from-path.mjs +79 -0
- package/lib/audit-skills.mjs +4 -0
- package/lib/audit-specialists.mjs +285 -0
- package/lib/audit-trail.mjs +77 -10
- package/lib/auto-docs.mjs +87 -18
- package/lib/beads/drift.mjs +27 -6
- package/lib/beads-client.mjs +49 -121
- package/lib/beads-optimistic.mjs +5 -12
- package/lib/brand-fonts.mjs +93 -0
- package/lib/brand-prose.mjs +214 -0
- package/lib/brand-tokens.mjs +92 -0
- package/lib/bridges/copilot-proxy.mjs +13 -26
- package/lib/capability-ledger.mjs +156 -0
- package/lib/certification/artifact-fixtures.mjs +132 -0
- package/lib/certification/artifact-gates.mjs +63 -0
- package/lib/certification/artifact-provenance.mjs +97 -0
- package/lib/certification/canonical-scenarios.mjs +78 -0
- package/lib/certification/cli.mjs +191 -0
- package/lib/certification/dashboard-api.mjs +71 -0
- package/lib/certification/demo-parity.mjs +116 -0
- package/lib/certification/document-io-fixtures.mjs +246 -0
- package/lib/certification/document-workflow.mjs +97 -0
- package/lib/certification/eval-bridge.mjs +77 -0
- package/lib/certification/model-routing.mjs +149 -0
- package/lib/certification/prompt-budget.mjs +119 -0
- package/lib/certification/rc-gate.mjs +206 -0
- package/lib/certification/real-llm-scenarios.mjs +303 -0
- package/lib/certification/role-cards.mjs +113 -0
- package/lib/certification/role-overlays.mjs +117 -0
- package/lib/certification/run.mjs +122 -0
- package/lib/certification/runner.mjs +323 -0
- package/lib/certification/scenarios.mjs +51 -0
- package/lib/certification/skill-inventory.mjs +289 -0
- package/lib/certification/skill-scenarios.mjs +147 -0
- package/lib/certification/specialist-contracts.mjs +85 -0
- package/lib/certification/specialist-scenarios.mjs +175 -0
- package/lib/certification/stale-impact.mjs +146 -0
- package/lib/certification/status.mjs +252 -0
- package/lib/certification/store.mjs +77 -0
- package/lib/chat/cli.mjs +333 -0
- package/lib/chat/command-suggest.mjs +161 -0
- package/lib/chat/commands.mjs +215 -0
- package/lib/chat/config.mjs +142 -0
- package/lib/chat/context-compactor.mjs +250 -0
- package/lib/chat/context-continuation.mjs +253 -0
- package/lib/chat/continuation-source.mjs +58 -0
- package/lib/chat/demo-guide.mjs +61 -0
- package/lib/chat/design-tokens.mjs +91 -0
- package/lib/chat/desktop-binary.mjs +79 -0
- package/lib/chat/desktop-build.mjs +130 -0
- package/lib/chat/desktop-launcher.mjs +133 -0
- package/lib/chat/evidence.mjs +145 -0
- package/lib/chat/export.mjs +74 -0
- package/lib/chat/harness/driver.mjs +91 -0
- package/lib/chat/list-picker.mjs +112 -0
- package/lib/chat/model-picker.mjs +356 -0
- package/lib/chat/openrouter-fallback.mjs +151 -0
- package/lib/chat/permission-prompt.mjs +33 -0
- package/lib/chat/picker-catalog.mjs +45 -0
- package/lib/chat/policy-telemetry.mjs +34 -0
- package/lib/chat/present.mjs +245 -0
- package/lib/chat/session-context.mjs +39 -0
- package/lib/chat/session-persist.mjs +73 -0
- package/lib/chat/session-restore.mjs +71 -0
- package/lib/chat/session-settings.mjs +53 -0
- package/lib/chat/system-prompt.mjs +52 -0
- package/lib/chat/transparency.mjs +93 -0
- package/lib/chat/tui/color-scheme.mjs +42 -0
- package/lib/chat/tui/markdown.mjs +123 -0
- package/lib/chat/tui/presentation.mjs +100 -0
- package/lib/chat/tui/render.mjs +500 -0
- package/lib/chat/tui/turn-block.mjs +284 -0
- package/lib/chat/tui/turn-present.mjs +18 -0
- package/lib/chat/tui/turn-state.mjs +88 -0
- package/lib/chat/tui/usage.mjs +122 -0
- package/lib/chat/web-commands.mjs +146 -0
- package/lib/chat/web-launcher.mjs +63 -0
- package/lib/chat/web-picker-keys.mjs +46 -0
- package/lib/chat/web-session.mjs +159 -0
- package/lib/cli-commands.mjs +232 -11
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/schema.mjs +36 -5
- package/lib/contract-schemas/decision.json +50 -0
- package/lib/contract-schemas/implementation.json +51 -0
- package/lib/contract-schemas/review-report.json +32 -0
- package/lib/contract-schemas/test-report.json +43 -0
- package/lib/contracts/construct-handoff.mjs +60 -0
- package/lib/contracts/validate.mjs +32 -3
- package/lib/contracts/violation-log.mjs +58 -5
- package/lib/dashboard-demo.mjs +71 -0
- package/lib/dashboard-static.mjs +19 -5
- package/lib/decisions/registry.mjs +5 -3
- package/lib/deck-export-pptx.mjs +1152 -0
- package/lib/demo-recording.mjs +142 -0
- package/lib/demo-script.mjs +114 -0
- package/lib/demo-surface.mjs +249 -0
- package/lib/demo.mjs +703 -0
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +302 -0
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +4 -2
- package/lib/doctor/project-adapters.mjs +44 -0
- package/lib/doctor/watchers/cx-budget.mjs +98 -0
- package/lib/doctor/watchers/graph-staleness.mjs +52 -0
- package/lib/doctor/watchers/process-pressure.mjs +4 -3
- package/lib/doctor/watchers/service-health.mjs +1 -1
- package/lib/document-export.mjs +360 -36
- package/lib/document-extract.mjs +88 -0
- package/lib/document-ingest.mjs +32 -18
- package/lib/embed/cli.mjs +2 -2
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/semantic.mjs +5 -2
- package/lib/embed/worker.mjs +2 -2
- package/lib/embedded-contract/model-resolve.mjs +10 -9
- package/lib/env-config.mjs +26 -0
- package/lib/evals/dataset.mjs +137 -0
- package/lib/evals/gates.mjs +175 -0
- package/lib/graph/build-co-change.mjs +54 -0
- package/lib/graph/build-from-registry.mjs +136 -0
- package/lib/graph/build-import-graph.mjs +153 -0
- package/lib/graph/cli.mjs +110 -0
- package/lib/graph/impact-cli.mjs +89 -0
- package/lib/graph/impact.mjs +108 -0
- package/lib/graph/staleness.mjs +44 -0
- package/lib/graph/store.mjs +172 -0
- package/lib/handoffs/cleanup.mjs +1 -1
- package/lib/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- package/lib/hooks/audit-trail.mjs +14 -28
- package/lib/hooks/brand-prose-lint.mjs +38 -0
- package/lib/hooks/graph-impact-advisory.mjs +62 -0
- package/lib/hooks/session-optimize.mjs +84 -207
- package/lib/hooks/session-start.mjs +4 -5
- package/lib/host-disposition.mjs +7 -0
- package/lib/improvement/cli.mjs +189 -0
- package/lib/improvement/controller.mjs +137 -0
- package/lib/improvement/proposal.mjs +120 -0
- package/lib/improvement/specialist-loop.mjs +192 -0
- package/lib/improvement/store.mjs +89 -0
- package/lib/improvement/surface.mjs +219 -0
- package/lib/ingest-tooling.mjs +97 -0
- package/lib/init/doc-lanes.mjs +165 -0
- package/lib/init-docs.mjs +31 -178
- package/lib/init-unified.mjs +27 -97
- package/lib/init-update-guide.mjs +102 -0
- package/lib/init-update.mjs +32 -1
- package/lib/init.mjs +8 -0
- package/lib/install/desktop-binary-download.mjs +85 -0
- package/lib/install/legacy-global-cleanup.mjs +189 -0
- package/lib/intake/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- package/lib/intake/queue.mjs +2 -1
- package/lib/intake/session-prelude.mjs +90 -1
- package/lib/libreoffice-export.mjs +97 -0
- package/lib/logging/rotate.mjs +9 -0
- package/lib/maintenance/docker-reclaim.mjs +206 -0
- package/lib/mcp/external-schema-cost.mjs +74 -0
- package/lib/mcp/server.mjs +84 -9
- package/lib/mcp/stdio-mcp-probe.mjs +188 -0
- package/lib/mcp/tool-budget.mjs +55 -4
- package/lib/mcp/tools/document.mjs +18 -4
- package/lib/mcp/tools/skills.mjs +32 -3
- package/lib/mcp/tools/workflow.mjs +13 -1
- package/lib/model-free-selector.mjs +18 -0
- package/lib/model-registry.mjs +13 -229
- package/lib/model-router.mjs +425 -93
- package/lib/models/behavior-matrix.mjs +289 -0
- package/lib/models/catalog.mjs +209 -0
- package/lib/models/execution-capability-profile.mjs +196 -0
- package/lib/models/execution-policy.mjs +307 -0
- package/lib/models/provider-poll.mjs +383 -0
- package/lib/npm-spawn-env.mjs +17 -0
- package/lib/ollama/installed-models.mjs +129 -0
- package/lib/oracle/actions.mjs +187 -0
- package/lib/oracle/artifact-gate.mjs +99 -0
- package/lib/oracle/cli.mjs +204 -0
- package/lib/oracle/daemon-entry.mjs +14 -0
- package/lib/oracle/dispatch.mjs +81 -0
- package/lib/oracle/execute.mjs +143 -0
- package/lib/oracle/gaps.mjs +76 -0
- package/lib/oracle/index.mjs +84 -0
- package/lib/oracle/issues.mjs +164 -0
- package/lib/oracle/org-graph.mjs +170 -0
- package/lib/oracle/policy.mjs +80 -0
- package/lib/oracle/read-model.mjs +398 -0
- package/lib/oracle/reconcile.mjs +191 -0
- package/lib/oracle/routing.mjs +89 -0
- package/lib/oracle/synthesize.mjs +384 -0
- package/lib/oracle/verdicts.mjs +51 -0
- package/lib/orchestration/worker.mjs +88 -27
- package/lib/orchestration-policy.mjs +50 -0
- package/lib/parity.mjs +96 -2
- package/lib/persona-sections.mjs +66 -0
- package/lib/playwright-demo.mjs +252 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +9 -4
- package/lib/prompt-validation-contract.mjs +24 -0
- package/lib/provider-capabilities.js +57 -11
- package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
- package/lib/providers/contract/adapters/git/index.mjs +115 -0
- package/lib/providers/contract/adapters/github/index.mjs +166 -0
- package/lib/providers/contract/adapters/jira/index.mjs +187 -0
- package/lib/providers/contract/adapters/slack/index.mjs +175 -0
- package/lib/providers/contract/contract-tests.mjs +57 -0
- package/lib/providers/contract/errors.mjs +48 -0
- package/lib/providers/contract/interface.mjs +50 -0
- package/lib/providers/contract/registry.mjs +102 -0
- package/lib/providers/copilot-auth.mjs +297 -0
- package/lib/providers/credential-bootstrap.mjs +178 -0
- package/lib/providers/credential-catalog.mjs +46 -0
- package/lib/providers/credential-sources.mjs +63 -0
- package/lib/providers/creds.mjs +5 -2
- package/lib/providers/op-run.mjs +59 -0
- package/lib/providers/secret-resolver.mjs +159 -0
- package/lib/publish-template.mjs +163 -0
- package/lib/publish-tooling.mjs +119 -0
- package/lib/publish.mjs +305 -0
- package/lib/registry/cli.mjs +82 -0
- package/lib/registry/consolidation.mjs +147 -0
- package/lib/registry/generate-docs.mjs +75 -0
- package/lib/registry/skill-verification.mjs +57 -0
- package/lib/registry/surface-map.mjs +76 -0
- package/lib/registry/validate.mjs +135 -0
- package/lib/resources/budget.mjs +82 -0
- package/lib/resources/process-budget.mjs +45 -0
- package/lib/rules-delivery.mjs +9 -2
- package/lib/rules-read.mjs +26 -0
- package/lib/runtime-env.mjs +23 -0
- package/lib/runtime-pressure.mjs +51 -7
- package/lib/schema-infer.mjs +13 -2
- package/lib/server/chat-loop.mjs +622 -0
- package/lib/server/demo-preview.mjs +63 -0
- package/lib/server/index.mjs +229 -8
- package/lib/server/langfuse-login.mjs +3 -3
- package/lib/service-manager.mjs +61 -9
- package/lib/session-store.mjs +1 -1
- package/lib/setup.mjs +102 -2
- package/lib/specialists/prompt-schema.mjs +19 -10
- package/lib/specialists/roster.mjs +43 -0
- package/lib/specialists/scaffold.mjs +56 -0
- package/lib/storage/backend.mjs +6 -6
- package/lib/storage/embeddings-local.mjs +6 -3
- package/lib/storage/file-lock.mjs +18 -11
- package/lib/storage/hybrid-query.mjs +7 -4
- package/lib/storage/state-source.mjs +5 -5
- package/lib/storage/sync.mjs +2 -3
- package/lib/telemetry/rule-calls.mjs +52 -0
- package/lib/template-registry.mjs +2 -2
- package/lib/templates/visual-requirements.mjs +27 -51
- package/lib/test-corpus-inventory.mjs +313 -0
- package/lib/uninstall/uninstall.mjs +19 -7
- package/lib/update.mjs +12 -0
- package/lib/upgrade.mjs +14 -0
- package/lib/wireframe.mjs +20 -14
- package/lib/worker/run.mjs +17 -4
- package/lib/worker/trace.mjs +11 -3
- package/package.json +29 -13
- package/personas/construct.md +13 -13
- package/platforms/claude/settings.template.json +36 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/release-gates.md +4 -3
- package/scripts/sync-specialists.mjs +187 -60
- package/skills/devops/data-engineering.md +1 -1
- package/skills/docs/adr-workflow.md +1 -0
- package/skills/docs/backlog-proposal-workflow.md +1 -0
- package/skills/docs/codebase-research-workflow.md +40 -0
- package/skills/docs/customer-profile-workflow.md +1 -0
- package/skills/docs/document-ingest-workflow.md +1 -0
- package/skills/docs/evidence-ingest-workflow.md +1 -0
- package/skills/docs/init-docs.md +2 -2
- package/skills/docs/init-project.md +8 -3
- package/skills/docs/prd-workflow.md +24 -1
- package/skills/docs/prfaq-workflow.md +1 -0
- package/skills/docs/product-intelligence-workflow.md +1 -0
- package/skills/docs/product-signal-workflow.md +1 -0
- package/skills/docs/research-workflow.md +54 -37
- package/skills/docs/runbook-workflow.md +1 -0
- package/skills/docs/strategy-workflow.md +1 -0
- package/skills/docs/user-research-workflow.md +40 -0
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/architect.md +5 -0
- package/skills/roles/operator.docs.md +4 -0
- package/skills/routing.md +4 -2
- package/specialists/artifact-manifest.json +480 -0
- package/specialists/artifact-manifest.schema.json +53 -0
- package/specialists/audit-enrichments.json +454 -0
- package/specialists/contracts.json +37 -25
- package/specialists/prompts/_shared/validation-contract.md +26 -0
- package/specialists/prompts/cx-accessibility.md +21 -2
- package/specialists/prompts/cx-ai-engineer.md +24 -1
- package/specialists/prompts/cx-architect.md +4 -1
- package/specialists/prompts/cx-business-strategist.md +23 -2
- package/specialists/prompts/cx-data-analyst.md +22 -1
- package/specialists/prompts/cx-data-engineer.md +23 -2
- package/specialists/prompts/cx-debugger.md +21 -2
- package/specialists/prompts/cx-designer.md +26 -3
- package/specialists/prompts/cx-devil-advocate.md +19 -2
- package/specialists/prompts/cx-docs-keeper.md +28 -1
- package/specialists/prompts/cx-engineer.md +22 -1
- package/specialists/prompts/cx-evaluator.md +18 -1
- package/specialists/prompts/cx-explorer.md +21 -2
- package/specialists/prompts/cx-legal-compliance.md +21 -2
- package/specialists/prompts/cx-operations.md +21 -2
- package/specialists/prompts/cx-oracle.md +94 -0
- package/specialists/prompts/cx-orchestrator.md +20 -1
- package/specialists/prompts/cx-platform-engineer.md +25 -2
- package/specialists/prompts/cx-product-manager.md +32 -1
- package/specialists/prompts/cx-qa.md +24 -2
- package/specialists/prompts/cx-rd-lead.md +23 -2
- package/specialists/prompts/cx-release-manager.md +23 -2
- package/specialists/prompts/cx-researcher.md +22 -2
- package/specialists/prompts/cx-reviewer.md +18 -1
- package/specialists/prompts/cx-security.md +22 -2
- package/specialists/prompts/cx-sre.md +30 -3
- package/specialists/prompts/cx-test-automation.md +7 -1
- package/specialists/prompts/cx-trace-reviewer.md +22 -3
- package/specialists/prompts/cx-ux-researcher.md +21 -2
- package/specialists/registry.json +52 -173
- package/specialists/tone-profiles.json +42 -0
- package/templates/demos/playwright/demo-recording.config.mjs +47 -0
- package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
- package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
- package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
- package/templates/demos/tapes/resource-guard-rails.tape +49 -0
- package/templates/demos/vhs/construct-cockpit.json +24 -0
- package/templates/distribution/construct-brand.typ +446 -0
- package/templates/distribution/construct-decision.typ +38 -0
- package/templates/distribution/construct-deck.html +95 -0
- package/templates/distribution/construct-pdf.typ +38 -0
- package/templates/distribution/construct-prd.typ +38 -0
- package/templates/distribution/construct-research.typ +38 -0
- package/templates/distribution/construct-web.html +92 -0
- package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
- package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
- package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
- package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/README.md +36 -0
- package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
- package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
- package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
- package/templates/distribution/icons/activity.svg +15 -0
- package/templates/distribution/icons/alert-triangle.svg +17 -0
- package/templates/distribution/icons/book-open.svg +16 -0
- package/templates/distribution/icons/bot.svg +20 -0
- package/templates/distribution/icons/brain.svg +22 -0
- package/templates/distribution/icons/circle-check.svg +16 -0
- package/templates/distribution/icons/clipboard-check.svg +17 -0
- package/templates/distribution/icons/cpu.svg +28 -0
- package/templates/distribution/icons/database.svg +17 -0
- package/templates/distribution/icons/eye.svg +16 -0
- package/templates/distribution/icons/file-text.svg +19 -0
- package/templates/distribution/icons/gauge.svg +16 -0
- package/templates/distribution/icons/git-branch.svg +17 -0
- package/templates/distribution/icons/key.svg +17 -0
- package/templates/distribution/icons/layers.svg +17 -0
- package/templates/distribution/icons/list-checks.svg +19 -0
- package/templates/distribution/icons/lock.svg +16 -0
- package/templates/distribution/icons/message-square.svg +15 -0
- package/templates/distribution/icons/network.svg +19 -0
- package/templates/distribution/icons/route.svg +17 -0
- package/templates/distribution/icons/scale.svg +19 -0
- package/templates/distribution/icons/search.svg +16 -0
- package/templates/distribution/icons/send.svg +16 -0
- package/templates/distribution/icons/server.svg +18 -0
- package/templates/distribution/icons/shield-check.svg +16 -0
- package/templates/distribution/icons/users.svg +18 -0
- package/templates/distribution/icons/webhook.svg +17 -0
- package/templates/distribution/icons/workflow.svg +17 -0
- package/templates/distribution/icons/wrench.svg +15 -0
- package/templates/distribution/run.mjs +18 -1
- package/templates/docs/adr.md +7 -0
- package/templates/docs/construct_guide.md +10 -10
- package/templates/docs/customer-profile.md +4 -0
- package/templates/docs/one-pager.md +4 -0
- package/templates/docs/postmortem.md +31 -0
- package/templates/docs/prd-platform.md +19 -0
- package/templates/docs/prd.md +15 -0
- package/templates/docs/prfaq.md +7 -0
- package/templates/docs/qa-strategy.md +103 -0
- package/templates/docs/research-brief.md +8 -0
- package/templates/docs/rfc-platform.md +12 -0
- package/templates/docs/test-plan.md +7 -0
- package/lib/ingest/chunker.mjs +0 -94
- package/lib/ingest/pipeline.mjs +0 -53
- package/lib/ingest/store.mjs +0 -82
- package/lib/mode-commands.mjs +0 -122
- package/lib/policy/unified-gates.mjs +0 -96
- package/lib/profiles/validate-custom.mjs +0 -114
- package/lib/services/telemetry-backend.mjs +0 -177
- package/lib/storage/fusion.mjs +0 -95
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/certification/runner.mjs — execute certification scenarios and persist run records.
|
|
3
|
+
*
|
|
4
|
+
* Hermetic scenarios run deterministic gates only. Live scenarios require
|
|
5
|
+
* CONSTRUCT_CERTIFY_LIVE=1; without opt-in the runner records inconclusive and
|
|
6
|
+
* never promotes a skipped provider call to pass. Paid reference models require
|
|
7
|
+
* CONSTRUCT_CERTIFY_ALLOW_PAID=1 with operator ack recorded on the run artifact.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import { validateCapabilityLedger } from '../capability-ledger.mjs';
|
|
13
|
+
import { validateCorpusInventory } from '../test-corpus-inventory.mjs';
|
|
14
|
+
import { validateArtifactRelease } from '../artifact-release-gate.mjs';
|
|
15
|
+
import { validateRoleCards } from './role-cards.mjs';
|
|
16
|
+
import { auditSpecialistContracts } from './specialist-contracts.mjs';
|
|
17
|
+
import { validateAllGoldenArtifactGates } from './artifact-gates.mjs';
|
|
18
|
+
import { validateSkillScenarioFixture } from './skill-scenarios.mjs';
|
|
19
|
+
import { validateSpecialistScenarioFixture } from './specialist-scenarios.mjs';
|
|
20
|
+
import { validateAllRoleOverlays } from './role-overlays.mjs';
|
|
21
|
+
import { measurePromptBudgetChains } from './prompt-budget.mjs';
|
|
22
|
+
import { validateAllArtifactProvenance } from './artifact-provenance.mjs';
|
|
23
|
+
import { validateDocumentWorkflowCertification } from './document-workflow.mjs';
|
|
24
|
+
import { buildDemoParityReport } from './demo-parity.mjs';
|
|
25
|
+
import { runRealLlmS3, runRealLlmS8 } from './real-llm-scenarios.mjs';
|
|
26
|
+
import { decidePromotion } from '../evals/gates.mjs';
|
|
27
|
+
import { buildScenarioEvaluationReport, runScenarioEvalGates } from './eval-bridge.mjs';
|
|
28
|
+
import {
|
|
29
|
+
formatModelRoutingSummary,
|
|
30
|
+
listCertificationModels,
|
|
31
|
+
resolveCertificationModel,
|
|
32
|
+
} from './model-routing.mjs';
|
|
33
|
+
import { deriveVerdictFromExecution } from './run.mjs';
|
|
34
|
+
import { fixtureDigest, getScenario, newRunId } from './scenarios.mjs';
|
|
35
|
+
import { writeCertificationRun } from './store.mjs';
|
|
36
|
+
|
|
37
|
+
export const LIVE_OPT_IN_ENV = 'CONSTRUCT_CERTIFY_LIVE';
|
|
38
|
+
|
|
39
|
+
function liveOptInEnabled(env = process.env) {
|
|
40
|
+
return env[LIVE_OPT_IN_ENV] === '1';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function runGate(gate, { root, scenario }) {
|
|
44
|
+
if (gate.type === 'capability-ledger-audit') {
|
|
45
|
+
const result = validateCapabilityLedger({ rootDir: root });
|
|
46
|
+
return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
|
|
47
|
+
}
|
|
48
|
+
if (gate.type === 'corpus-inventory-audit') {
|
|
49
|
+
const result = validateCorpusInventory({ rootDir: root });
|
|
50
|
+
return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
|
|
51
|
+
}
|
|
52
|
+
if (gate.type === 'artifact-release-gate') {
|
|
53
|
+
const rel = scenario.fixture.path;
|
|
54
|
+
const result = validateArtifactRelease({
|
|
55
|
+
filePath: path.join(root, rel),
|
|
56
|
+
type: gate.artifactType ?? 'prd',
|
|
57
|
+
rootDir: root,
|
|
58
|
+
});
|
|
59
|
+
return { id: gate.id, pass: result.ok === true, detail: result.errors?.[0] ?? null };
|
|
60
|
+
}
|
|
61
|
+
if (gate.type === 'role-cards-audit') {
|
|
62
|
+
const result = validateRoleCards({ rootDir: root });
|
|
63
|
+
return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
|
|
64
|
+
}
|
|
65
|
+
if (gate.type === 'specialist-contracts-audit') {
|
|
66
|
+
const result = auditSpecialistContracts({ rootDir: root });
|
|
67
|
+
return { id: gate.id, pass: result.pass, detail: result.failures[0]?.specialistId ?? null };
|
|
68
|
+
}
|
|
69
|
+
if (gate.type === 'skill-scenario-audit') {
|
|
70
|
+
const rel = scenario.fixture.path;
|
|
71
|
+
const fixture = JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8'));
|
|
72
|
+
const result = validateSkillScenarioFixture(fixture, { rootDir: root });
|
|
73
|
+
return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
|
|
74
|
+
}
|
|
75
|
+
if (gate.type === 'artifact-golden-audit') {
|
|
76
|
+
const result = validateAllGoldenArtifactGates({ rootDir: root });
|
|
77
|
+
return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
|
|
78
|
+
}
|
|
79
|
+
if (gate.type === 'specialist-scenario-audit') {
|
|
80
|
+
const rel = scenario.fixture.path;
|
|
81
|
+
const fixture = JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8'));
|
|
82
|
+
const result = validateSpecialistScenarioFixture(fixture, { rootDir: root });
|
|
83
|
+
return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
|
|
84
|
+
}
|
|
85
|
+
if (gate.type === 'role-overlays-audit') {
|
|
86
|
+
const result = validateAllRoleOverlays({ rootDir: root });
|
|
87
|
+
return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
|
|
88
|
+
}
|
|
89
|
+
if (gate.type === 'prompt-budget-audit') {
|
|
90
|
+
const result = measurePromptBudgetChains({ rootDir: root });
|
|
91
|
+
return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
|
|
92
|
+
}
|
|
93
|
+
if (gate.type === 'artifact-provenance-audit') {
|
|
94
|
+
const result = validateAllArtifactProvenance({ rootDir: root, strict: gate.strict !== false });
|
|
95
|
+
return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
|
|
96
|
+
}
|
|
97
|
+
if (gate.type === 'document-workflow-audit') {
|
|
98
|
+
const result = validateDocumentWorkflowCertification({ rootDir: root });
|
|
99
|
+
return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
|
|
100
|
+
}
|
|
101
|
+
if (gate.type === 'demo-parity-audit') {
|
|
102
|
+
const result = buildDemoParityReport({ rootDir: root });
|
|
103
|
+
return { id: gate.id, pass: result.pass, detail: result.mismatches[0]?.detail ?? null };
|
|
104
|
+
}
|
|
105
|
+
if (gate.type === 'real-llm-scenario') {
|
|
106
|
+
const runner = gate.scenario === 's8' ? runRealLlmS8 : runRealLlmS3;
|
|
107
|
+
const result = await runner({ env: process.env });
|
|
108
|
+
if (result.status === 'inconclusive') {
|
|
109
|
+
return { id: gate.id, pass: false, detail: result.skip ?? result.detail ?? 'inconclusive', inconclusive: true };
|
|
110
|
+
}
|
|
111
|
+
return { id: gate.id, pass: result.status === 'pass', detail: result.detail ?? result.status };
|
|
112
|
+
}
|
|
113
|
+
if (gate.type === 'live-provider-smoke') {
|
|
114
|
+
return { id: gate.id, pass: false, detail: 'live provider gate not executed in hermetic mode' };
|
|
115
|
+
}
|
|
116
|
+
return { id: gate.id, pass: false, detail: `unknown gate type: ${gate.type}` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function deriveVerdictWithEval({ scenarioGates = [], evalReport = null, providerSkipped = false, qualitative = null, error = null } = {}) {
|
|
120
|
+
if (providerSkipped || error) {
|
|
121
|
+
return deriveVerdictFromExecution({ providerSkipped, error });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const scenarioFailed = scenarioGates.some((gate) => gate?.pass === false && !gate?.inconclusive);
|
|
125
|
+
const inconclusiveGate = scenarioGates.find((gate) => gate?.inconclusive);
|
|
126
|
+
if (inconclusiveGate) {
|
|
127
|
+
return deriveVerdictFromExecution({ providerSkipped: true, error: inconclusiveGate.detail });
|
|
128
|
+
}
|
|
129
|
+
if (scenarioFailed) {
|
|
130
|
+
return deriveVerdictFromExecution({ gates: scenarioGates });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const promotion = evalReport?.decision ?? decidePromotion({
|
|
134
|
+
deterministic: evalReport?.blocked != null
|
|
135
|
+
? { blocked: evalReport.blocked, regressions: (evalReport.gates ?? []).filter((g) => !g.pass).map((g) => g.gate), gates: evalReport.gates ?? [] }
|
|
136
|
+
: null,
|
|
137
|
+
judges: evalReport?.evaluators ?? [],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!promotion.promotable) {
|
|
141
|
+
const source = evalReport?.blocked ? 'deterministic' : (evalReport?.evaluators?.some((j) => j.verdict === 'fail') ? 'qualitative' : 'deterministic');
|
|
142
|
+
return {
|
|
143
|
+
status: 'fail',
|
|
144
|
+
source,
|
|
145
|
+
reason: promotion.reason,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (qualitative?.abstained) {
|
|
150
|
+
return deriveVerdictFromExecution({ gates: scenarioGates, qualitative });
|
|
151
|
+
}
|
|
152
|
+
if (typeof qualitative?.score === 'number' && qualitative.score < 0.5) {
|
|
153
|
+
return deriveVerdictFromExecution({ gates: scenarioGates, qualitative });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return deriveVerdictFromExecution({ gates: scenarioGates, qualitative });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function previewCertificationRun(scenarioId, { repoRoot = process.cwd(), env = process.env, now = () => new Date().toISOString() } = {}) {
|
|
160
|
+
const { scenario } = getScenario(scenarioId, { repoRoot });
|
|
161
|
+
const model = resolveCertificationModel(scenario.model ?? {}, { env, now });
|
|
162
|
+
const mode = scenario.mode ?? 'hermetic';
|
|
163
|
+
const requiresLive = mode === 'live' || scenario.requiresEnv === LIVE_OPT_IN_ENV;
|
|
164
|
+
return {
|
|
165
|
+
scenarioId,
|
|
166
|
+
mode,
|
|
167
|
+
model,
|
|
168
|
+
modelSummary: formatModelRoutingSummary(model),
|
|
169
|
+
requiresLive,
|
|
170
|
+
liveOptIn: liveOptInEnabled(env),
|
|
171
|
+
modelsAvailable: listCertificationModels({ env }),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function runCertificationScenario(scenarioId, {
|
|
176
|
+
projectDir = process.cwd(),
|
|
177
|
+
repoRoot = projectDir,
|
|
178
|
+
env = process.env,
|
|
179
|
+
now = () => new Date().toISOString(),
|
|
180
|
+
dryRun = false,
|
|
181
|
+
judges = [],
|
|
182
|
+
} = {}) {
|
|
183
|
+
const startedAt = now();
|
|
184
|
+
const startMs = Date.now();
|
|
185
|
+
const { root, scenario } = getScenario(scenarioId, { repoRoot });
|
|
186
|
+
const fixturePath = scenario.fixture.path;
|
|
187
|
+
const sha256 = fixtureDigest(root, fixturePath);
|
|
188
|
+
const mode = scenario.mode ?? 'hermetic';
|
|
189
|
+
const requiresLive = mode === 'live' || scenario.requiresEnv === LIVE_OPT_IN_ENV;
|
|
190
|
+
const resolvedModel = resolveCertificationModel(scenario.model ?? {}, { env, now });
|
|
191
|
+
|
|
192
|
+
if (resolvedModel.blocked) {
|
|
193
|
+
const verdict = deriveVerdictFromExecution({
|
|
194
|
+
providerSkipped: true,
|
|
195
|
+
error: resolvedModel.blockReason,
|
|
196
|
+
});
|
|
197
|
+
const run = {
|
|
198
|
+
schemaVersion: 1,
|
|
199
|
+
id: newRunId(scenarioId),
|
|
200
|
+
scenarioId,
|
|
201
|
+
capabilityId: scenario.capabilityId,
|
|
202
|
+
model: {
|
|
203
|
+
provider: resolvedModel.provider,
|
|
204
|
+
requestedId: resolvedModel.requestedId,
|
|
205
|
+
resolvedId: resolvedModel.resolvedId,
|
|
206
|
+
tier: resolvedModel.tier,
|
|
207
|
+
paidOptIn: resolvedModel.paidOptIn,
|
|
208
|
+
operatorAckAt: resolvedModel.operatorAckAt,
|
|
209
|
+
},
|
|
210
|
+
fixture: { path: fixturePath, sha256 },
|
|
211
|
+
verdict: { ...verdict, reason: resolvedModel.blockReason },
|
|
212
|
+
gates: [],
|
|
213
|
+
evaluation: null,
|
|
214
|
+
qualitative: null,
|
|
215
|
+
timing: { latencyMs: Date.now() - startMs, startedAt, finishedAt: now() },
|
|
216
|
+
cost: null,
|
|
217
|
+
artifacts: null,
|
|
218
|
+
evidenceVersion: 'corpus-inventory:1',
|
|
219
|
+
createdAt: startedAt,
|
|
220
|
+
};
|
|
221
|
+
if (dryRun) return { dryRun: true, run, exitCode: 2, modelSummary: formatModelRoutingSummary(resolvedModel) };
|
|
222
|
+
writeCertificationRun(run, { rootDir: projectDir });
|
|
223
|
+
return { run, exitCode: 2, modelSummary: formatModelRoutingSummary(resolvedModel) };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (requiresLive && !liveOptInEnabled(env)) {
|
|
227
|
+
const verdict = deriveVerdictFromExecution({
|
|
228
|
+
providerSkipped: true,
|
|
229
|
+
gates: [],
|
|
230
|
+
});
|
|
231
|
+
const run = {
|
|
232
|
+
schemaVersion: 1,
|
|
233
|
+
id: newRunId(scenarioId),
|
|
234
|
+
scenarioId,
|
|
235
|
+
capabilityId: scenario.capabilityId,
|
|
236
|
+
model: {
|
|
237
|
+
provider: resolvedModel.provider,
|
|
238
|
+
requestedId: resolvedModel.requestedId,
|
|
239
|
+
resolvedId: resolvedModel.resolvedId,
|
|
240
|
+
tier: resolvedModel.tier,
|
|
241
|
+
paidOptIn: resolvedModel.paidOptIn,
|
|
242
|
+
operatorAckAt: resolvedModel.operatorAckAt,
|
|
243
|
+
},
|
|
244
|
+
fixture: { path: fixturePath, sha256 },
|
|
245
|
+
verdict: { ...verdict, reason: `${LIVE_OPT_IN_ENV}=1 required for live scenarios` },
|
|
246
|
+
gates: [],
|
|
247
|
+
evaluation: null,
|
|
248
|
+
qualitative: null,
|
|
249
|
+
timing: { latencyMs: Date.now() - startMs, startedAt, finishedAt: now() },
|
|
250
|
+
cost: null,
|
|
251
|
+
artifacts: null,
|
|
252
|
+
evidenceVersion: 'corpus-inventory:1',
|
|
253
|
+
createdAt: startedAt,
|
|
254
|
+
};
|
|
255
|
+
if (dryRun) return { dryRun: true, run, exitCode: 2, modelSummary: formatModelRoutingSummary(resolvedModel) };
|
|
256
|
+
writeCertificationRun(run, { rootDir: projectDir });
|
|
257
|
+
return { run, exitCode: 2, modelSummary: formatModelRoutingSummary(resolvedModel) };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const scenarioGates = [];
|
|
261
|
+
for (const gate of scenario.gates ?? []) {
|
|
262
|
+
scenarioGates.push(await runGate(gate, { root, scenario }));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const { deterministic, evalGates } = runScenarioEvalGates(scenario);
|
|
266
|
+
const evaluation = buildScenarioEvaluationReport({
|
|
267
|
+
scenario,
|
|
268
|
+
deterministic,
|
|
269
|
+
judges,
|
|
270
|
+
});
|
|
271
|
+
const gates = [...scenarioGates, ...evalGates];
|
|
272
|
+
|
|
273
|
+
const qualitative = judges.length > 0
|
|
274
|
+
? {
|
|
275
|
+
judgeModel: judges[0]?.model ?? null,
|
|
276
|
+
score: judges.every((j) => j.verdict !== 'fail') ? 1 : 0,
|
|
277
|
+
abstained: judges.every((j) => j.verdict === 'abstain'),
|
|
278
|
+
}
|
|
279
|
+
: null;
|
|
280
|
+
|
|
281
|
+
const verdict = deriveVerdictWithEval({
|
|
282
|
+
scenarioGates,
|
|
283
|
+
evalReport: evaluation,
|
|
284
|
+
qualitative,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const run = {
|
|
288
|
+
schemaVersion: 1,
|
|
289
|
+
id: newRunId(scenarioId),
|
|
290
|
+
scenarioId,
|
|
291
|
+
capabilityId: scenario.capabilityId,
|
|
292
|
+
model: {
|
|
293
|
+
provider: resolvedModel.provider,
|
|
294
|
+
requestedId: resolvedModel.requestedId,
|
|
295
|
+
resolvedId: resolvedModel.resolvedId,
|
|
296
|
+
tier: resolvedModel.tier,
|
|
297
|
+
paidOptIn: resolvedModel.paidOptIn,
|
|
298
|
+
operatorAckAt: resolvedModel.operatorAckAt,
|
|
299
|
+
},
|
|
300
|
+
fixture: { path: fixturePath, sha256 },
|
|
301
|
+
verdict,
|
|
302
|
+
gates,
|
|
303
|
+
evaluation,
|
|
304
|
+
qualitative,
|
|
305
|
+
timing: { latencyMs: Date.now() - startMs, startedAt, finishedAt: now() },
|
|
306
|
+
cost: null,
|
|
307
|
+
artifacts: null,
|
|
308
|
+
evidenceVersion: 'corpus-inventory:1',
|
|
309
|
+
createdAt: startedAt,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
if (dryRun) {
|
|
313
|
+
return {
|
|
314
|
+
dryRun: true,
|
|
315
|
+
run,
|
|
316
|
+
exitCode: verdict.status === 'pass' ? 0 : verdict.status === 'fail' ? 1 : 2,
|
|
317
|
+
modelSummary: formatModelRoutingSummary(resolvedModel),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const persisted = writeCertificationRun(run, { rootDir: projectDir });
|
|
321
|
+
const exitCode = verdict.status === 'pass' ? 0 : verdict.status === 'fail' ? 1 : 2;
|
|
322
|
+
return { ...persisted, exitCode, modelSummary: formatModelRoutingSummary(resolvedModel) };
|
|
323
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/certification/scenarios.mjs — load certification scenario catalog and fixtures.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
function findConstructRoot(startPath = process.cwd()) {
|
|
10
|
+
let current = path.resolve(startPath);
|
|
11
|
+
while (true) {
|
|
12
|
+
if (fs.existsSync(path.join(current, 'package.json')) && fs.existsSync(path.join(current, 'tests'))) return current;
|
|
13
|
+
const parent = path.dirname(current);
|
|
14
|
+
if (parent === current) return path.resolve(startPath);
|
|
15
|
+
current = parent;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function defaultScenarioCatalogPath(rootDir = process.cwd()) {
|
|
20
|
+
return path.join(findConstructRoot(rootDir), 'tests', 'certification', 'scenarios', 'catalog.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadScenarioCatalog({ repoRoot } = {}) {
|
|
24
|
+
const root = findConstructRoot(repoRoot);
|
|
25
|
+
const filePath = defaultScenarioCatalogPath(root);
|
|
26
|
+
const catalog = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
27
|
+
return { root, filePath, catalog };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function listScenarios({ repoRoot } = {}) {
|
|
31
|
+
const { catalog } = loadScenarioCatalog({ repoRoot });
|
|
32
|
+
return catalog.scenarios ?? [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getScenario(scenarioId, { repoRoot } = {}) {
|
|
36
|
+
const { root, catalog } = loadScenarioCatalog({ repoRoot });
|
|
37
|
+
const scenario = (catalog.scenarios ?? []).find((entry) => entry.id === scenarioId);
|
|
38
|
+
if (!scenario) throw new Error(`unknown certification scenario: ${scenarioId}`);
|
|
39
|
+
return { root, scenario };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function fixtureDigest(root, relPath) {
|
|
43
|
+
const absolute = path.join(root, relPath);
|
|
44
|
+
if (!fs.existsSync(absolute)) throw new Error(`fixture does not exist: ${relPath}`);
|
|
45
|
+
return createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function newRunId(scenarioId) {
|
|
49
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
50
|
+
return `cert-${scenarioId.replace(/[^a-z0-9.-]+/gi, '-')}-${stamp}`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/certification/skill-inventory.mjs — machine-readable skill corpus inventory.
|
|
3
|
+
*
|
|
4
|
+
* Walks skills/**, extracts id, owner specialist, activation triggers, inputs,
|
|
5
|
+
* outputs, and verification hooks. Flags orphan skills and conflicting output
|
|
6
|
+
* contracts for manifest workflow types. Output feeds tests/certification/skills/inventory.json.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { artifactTypes, loadArtifactManifest } from '../artifact-manifest.mjs';
|
|
13
|
+
|
|
14
|
+
function findConstructRoot(startPath = process.cwd()) {
|
|
15
|
+
let current = path.resolve(startPath);
|
|
16
|
+
while (true) {
|
|
17
|
+
if (fs.existsSync(path.join(current, 'specialists', 'registry.json'))) return current;
|
|
18
|
+
const parent = path.dirname(current);
|
|
19
|
+
if (parent === current) return path.resolve(startPath);
|
|
20
|
+
current = parent;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function collectSkillFiles(skillsDir) {
|
|
25
|
+
const results = [];
|
|
26
|
+
const walk = (dir, prefix = '') => {
|
|
27
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
walk(path.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
30
|
+
} else if (entry.name.endsWith('.md') && entry.name !== 'routing.md') {
|
|
31
|
+
const rel = prefix ? `${prefix}/${entry.name.replace(/\.md$/, '')}` : entry.name.replace(/\.md$/, '');
|
|
32
|
+
results.push(rel);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
try { walk(skillsDir); } catch { /* skills dir missing */ }
|
|
37
|
+
return results.sort();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function collectProfileRoles(root) {
|
|
41
|
+
const dir = path.join(root, 'profiles');
|
|
42
|
+
const roles = new Set();
|
|
43
|
+
const scan = (node) => {
|
|
44
|
+
if (Array.isArray(node)) { node.forEach(scan); return; }
|
|
45
|
+
if (node && typeof node === 'object') {
|
|
46
|
+
if (Array.isArray(node.roles)) for (const r of node.roles) if (typeof r === 'string') roles.add(r);
|
|
47
|
+
for (const v of Object.values(node)) scan(v);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
for (const f of fs.readdirSync(dir)) {
|
|
52
|
+
if (!f.endsWith('.json')) continue;
|
|
53
|
+
try { scan(JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'))); } catch { /* skip malformed */ }
|
|
54
|
+
}
|
|
55
|
+
} catch { /* no profiles dir */ }
|
|
56
|
+
return roles;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function roleBase(skillId) {
|
|
60
|
+
const m = skillId.match(/^roles\/([^.]+)(?:\..*)?$/);
|
|
61
|
+
return m ? m[1] : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseFrontmatter(content) {
|
|
65
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
66
|
+
if (!match) return { meta: {}, body: content };
|
|
67
|
+
const meta = {};
|
|
68
|
+
for (const line of match[1].split('\n')) {
|
|
69
|
+
const kv = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
|
|
70
|
+
if (!kv) continue;
|
|
71
|
+
const [, key, raw] = kv;
|
|
72
|
+
if (raw.startsWith('[')) {
|
|
73
|
+
try { meta[key] = JSON.parse(raw); } catch { meta[key] = raw; }
|
|
74
|
+
} else {
|
|
75
|
+
meta[key] = raw.replace(/^["']|["']$/g, '');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { meta, body: content.slice(match[0].length) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function activationTriggers(meta, body) {
|
|
82
|
+
const triggers = [];
|
|
83
|
+
if (meta.description) triggers.push(meta.description);
|
|
84
|
+
const useWhen = body.match(/^Use when:\s*(.+)$/m)?.[1];
|
|
85
|
+
if (useWhen) triggers.push(useWhen.trim());
|
|
86
|
+
return [...new Set(triggers.filter(Boolean))];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function verificationHooks(meta, body) {
|
|
90
|
+
const hooks = [];
|
|
91
|
+
if (meta.verificationBar) hooks.push({ kind: 'verificationBar', value: meta.verificationBar });
|
|
92
|
+
if (/construct artifact validate/i.test(body)) hooks.push({ kind: 'cli', value: 'construct artifact validate' });
|
|
93
|
+
if (/construct certify/i.test(body)) hooks.push({ kind: 'cli', value: 'construct certify' });
|
|
94
|
+
if (/get_template\(/i.test(body)) hooks.push({ kind: 'mcp', value: 'get_template' });
|
|
95
|
+
return hooks;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveOwners(skillId, { registry, declaredSkills, ownerBases }) {
|
|
99
|
+
const owners = new Set();
|
|
100
|
+
for (const agent of registry.specialists ?? []) {
|
|
101
|
+
if ((agent.skills ?? []).includes(skillId)) owners.add(`cx-${agent.name}`);
|
|
102
|
+
}
|
|
103
|
+
const base = roleBase(skillId);
|
|
104
|
+
if (base && ownerBases.has(base)) {
|
|
105
|
+
for (const agent of registry.specialists ?? []) {
|
|
106
|
+
if (agent.name === base || agent.name.replace(/^cx-/, '') === base) owners.add(`cx-${agent.name}`);
|
|
107
|
+
}
|
|
108
|
+
if (!owners.size) owners.add(`profile-role:${base}`);
|
|
109
|
+
}
|
|
110
|
+
if (declaredSkills.has(skillId) && owners.size === 0) {
|
|
111
|
+
for (const agent of registry.specialists ?? []) {
|
|
112
|
+
if ((agent.skills ?? []).includes(skillId)) owners.add(`cx-${agent.name}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [...owners].sort();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function workflowSkillIds(manifest) {
|
|
119
|
+
const ids = new Set();
|
|
120
|
+
for (const entry of Object.values(manifest.artifacts ?? {})) {
|
|
121
|
+
if (entry.workflowSkill) ids.add(entry.workflowSkill);
|
|
122
|
+
}
|
|
123
|
+
return ids;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function detectConflictingOutputs(skills) {
|
|
127
|
+
const manifestTypes = new Set(artifactTypes());
|
|
128
|
+
const byType = new Map();
|
|
129
|
+
for (const skill of skills) {
|
|
130
|
+
const type = skill.outputs?.artifactType;
|
|
131
|
+
if (!type || !manifestTypes.has(type)) continue;
|
|
132
|
+
if (!skill.id.startsWith('docs/') && !skill.id.includes('-workflow')) continue;
|
|
133
|
+
const list = byType.get(type) ?? [];
|
|
134
|
+
list.push(skill);
|
|
135
|
+
byType.set(type, list);
|
|
136
|
+
}
|
|
137
|
+
const conflicts = [];
|
|
138
|
+
for (const [artifactType, entries] of byType) {
|
|
139
|
+
if (entries.length < 2) continue;
|
|
140
|
+
const bars = new Set(entries.map((e) => e.verificationHooks.find((h) => h.kind === 'verificationBar')?.value ?? ''));
|
|
141
|
+
conflicts.push({
|
|
142
|
+
artifactType,
|
|
143
|
+
skillIds: entries.map((e) => e.id),
|
|
144
|
+
reason: bars.size > 1 ? 'divergent verificationBar across workflow skills' : 'multiple workflow skills claim the same manifest artifact type',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return conflicts;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function defaultSkillInventoryPath(rootDir = process.cwd()) {
|
|
151
|
+
return path.join(findConstructRoot(rootDir), 'tests', 'certification', 'skills', 'inventory.json');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function buildSkillInventory({ rootDir } = {}) {
|
|
155
|
+
const root = findConstructRoot(rootDir);
|
|
156
|
+
const skillsDir = path.join(root, 'skills');
|
|
157
|
+
const registry = JSON.parse(fs.readFileSync(path.join(root, 'specialists', 'registry.json'), 'utf8'));
|
|
158
|
+
const manifest = loadArtifactManifest({ rootDir: root });
|
|
159
|
+
const declaredSkills = new Set();
|
|
160
|
+
for (const agent of registry.specialists ?? []) {
|
|
161
|
+
for (const skill of agent.skills ?? []) declaredSkills.add(skill);
|
|
162
|
+
}
|
|
163
|
+
const ownerBases = new Set([
|
|
164
|
+
...(registry.specialists ?? []).map((a) => a.name.replace(/^cx-/, '')),
|
|
165
|
+
...collectProfileRoles(root),
|
|
166
|
+
]);
|
|
167
|
+
const workflowIds = workflowSkillIds(manifest);
|
|
168
|
+
|
|
169
|
+
const skills = collectSkillFiles(skillsDir).map((id) => {
|
|
170
|
+
const filePath = path.join(skillsDir, `${id}.md`);
|
|
171
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
172
|
+
const { meta, body } = parseFrontmatter(content);
|
|
173
|
+
const owners = resolveOwners(id, { registry, declaredSkills, ownerBases });
|
|
174
|
+
const hooks = verificationHooks(meta, body);
|
|
175
|
+
return {
|
|
176
|
+
id,
|
|
177
|
+
owners,
|
|
178
|
+
activationTriggers: activationTriggers(meta, body),
|
|
179
|
+
inputs: Array.isArray(meta.inputs) ? meta.inputs : (meta.inputs ? [meta.inputs] : []),
|
|
180
|
+
outputs: {
|
|
181
|
+
artifactType: meta.artifactType ?? null,
|
|
182
|
+
name: meta.name ?? null,
|
|
183
|
+
},
|
|
184
|
+
verificationHooks: hooks,
|
|
185
|
+
workflowSkill: workflowIds.has(id),
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const blockingFindings = [];
|
|
190
|
+
for (const skill of skills) {
|
|
191
|
+
if (skill.owners.length === 0) {
|
|
192
|
+
blockingFindings.push({ kind: 'no-owner', skillId: skill.id, severity: 'blocking' });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const conflict of detectConflictingOutputs(skills)) {
|
|
196
|
+
blockingFindings.push({
|
|
197
|
+
kind: 'conflicting-output-contract',
|
|
198
|
+
severity: 'blocking',
|
|
199
|
+
artifactType: conflict.artifactType,
|
|
200
|
+
skillIds: conflict.skillIds,
|
|
201
|
+
reason: conflict.reason,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const payload = {
|
|
206
|
+
version: 1,
|
|
207
|
+
generatedAt: new Date().toISOString(),
|
|
208
|
+
generator: 'lib/certification/skill-inventory.mjs',
|
|
209
|
+
skillCount: skills.length,
|
|
210
|
+
skills,
|
|
211
|
+
blockingFindings,
|
|
212
|
+
digest: null,
|
|
213
|
+
};
|
|
214
|
+
const canonical = { ...payload, digest: undefined };
|
|
215
|
+
payload.digest = createHash('sha256').update(JSON.stringify(canonical)).digest('hex');
|
|
216
|
+
return payload;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function validateSkillInventory({ rootDir, inventory: supplied, checkFreshness = true } = {}) {
|
|
220
|
+
const root = findConstructRoot(rootDir);
|
|
221
|
+
const inventoryPath = defaultSkillInventoryPath(root);
|
|
222
|
+
const inventory = supplied ?? JSON.parse(fs.readFileSync(inventoryPath, 'utf8'));
|
|
223
|
+
const errors = [];
|
|
224
|
+
const warnings = [];
|
|
225
|
+
|
|
226
|
+
if (inventory.version !== 1) errors.push('inventory.version must equal 1');
|
|
227
|
+
if (!Array.isArray(inventory.skills) || inventory.skills.length === 0) errors.push('inventory.skills must be non-empty');
|
|
228
|
+
|
|
229
|
+
const onDisk = new Set(collectSkillFiles(path.join(root, 'skills')));
|
|
230
|
+
const indexed = new Set((inventory.skills ?? []).map((s) => s.id));
|
|
231
|
+
for (const id of onDisk) {
|
|
232
|
+
if (!indexed.has(id)) errors.push(`missing inventory entry: ${id}`);
|
|
233
|
+
}
|
|
234
|
+
for (const skill of inventory.skills ?? []) {
|
|
235
|
+
if (!onDisk.has(skill.id)) errors.push(`stale inventory entry: ${skill.id}`);
|
|
236
|
+
if (!Array.isArray(skill.owners)) errors.push(`${skill.id}: owners must be an array`);
|
|
237
|
+
if (!Array.isArray(skill.activationTriggers)) errors.push(`${skill.id}: activationTriggers must be an array`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for (const finding of inventory.blockingFindings ?? []) {
|
|
241
|
+
if (finding.severity === 'blocking') warnings.push(`blocking finding recorded: ${finding.kind} ${finding.skillId ?? finding.artifactType ?? ''}`.trim());
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (checkFreshness) {
|
|
245
|
+
const fresh = buildSkillInventory({ rootDir: root });
|
|
246
|
+
const freshCanonical = { ...fresh, digest: undefined, generatedAt: undefined };
|
|
247
|
+
const committedCanonical = { ...inventory, digest: undefined, generatedAt: undefined };
|
|
248
|
+
if (JSON.stringify(freshCanonical) !== JSON.stringify(committedCanonical)) {
|
|
249
|
+
errors.push('inventory is stale — regenerate with node scripts/generate-skill-inventory.mjs');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
filePath: inventoryPath,
|
|
255
|
+
skillCount: inventory.skills?.length ?? 0,
|
|
256
|
+
blockingCount: (inventory.blockingFindings ?? []).filter((f) => f.severity === 'blocking').length,
|
|
257
|
+
errors,
|
|
258
|
+
warnings,
|
|
259
|
+
pass: errors.length === 0,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function writeSkillInventory({ rootDir } = {}) {
|
|
264
|
+
const root = findConstructRoot(rootDir);
|
|
265
|
+
const inventory = buildSkillInventory({ rootDir: root });
|
|
266
|
+
const inventoryPath = defaultSkillInventoryPath(root);
|
|
267
|
+
fs.mkdirSync(path.dirname(inventoryPath), { recursive: true });
|
|
268
|
+
fs.writeFileSync(inventoryPath, `${JSON.stringify(inventory, null, 2)}\n`);
|
|
269
|
+
return { inventoryPath, inventory };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function runSkillInventoryAuditCli(args = [], { rootDir } = {}) {
|
|
273
|
+
if (args.includes('--write')) {
|
|
274
|
+
const { inventoryPath, inventory } = writeSkillInventory({ rootDir });
|
|
275
|
+
process.stdout.write(`Wrote ${inventoryPath} (${inventory.skillCount} skills, ${inventory.blockingFindings.length} blocking findings)\n`);
|
|
276
|
+
return validateSkillInventory({ rootDir, inventory, checkFreshness: false });
|
|
277
|
+
}
|
|
278
|
+
const checkFreshness = !args.includes('--no-freshness');
|
|
279
|
+
const result = validateSkillInventory({ rootDir, checkFreshness });
|
|
280
|
+
if (args.includes('--json')) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
281
|
+
else {
|
|
282
|
+
process.stdout.write(`Skill inventory: ${result.skillCount} skills (${result.blockingCount} blocking findings recorded)\n`);
|
|
283
|
+
if (result.errors.length) result.errors.forEach((error) => process.stderr.write(` ✗ ${error}\n`));
|
|
284
|
+
if (result.warnings.length) result.warnings.forEach((warning) => process.stderr.write(` ⚠ ${warning}\n`));
|
|
285
|
+
process.stdout.write(result.pass ? ' Result: PASS\n' : ' Result: FAIL\n');
|
|
286
|
+
}
|
|
287
|
+
if (!result.pass) process.exitCode = 1;
|
|
288
|
+
return result;
|
|
289
|
+
}
|