@geraldmaron/construct 1.1.1 → 1.2.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/README.md +30 -19
- package/bin/construct +487 -87
- 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 +258 -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/artifact-workflow.mjs +252 -0
- package/lib/audit-skills.mjs +4 -0
- package/lib/audit-specialists.mjs +285 -0
- package/lib/audit-trail.mjs +1 -1
- package/lib/auto-docs.mjs +90 -19
- 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 +246 -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 +146 -15
- package/lib/cli-service-inventory.mjs +42 -0
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/intake-policy.mjs +209 -0
- package/lib/config/project-config.mjs +11 -2
- package/lib/config/schema.mjs +86 -6
- package/lib/config/source-targets.mjs +311 -0
- 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 +598 -140
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +13 -11
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +3 -1
- package/lib/doctor/project-adapters.mjs +44 -0
- package/lib/doctor/source-checkout.mjs +16 -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/document-export.mjs +384 -38
- package/lib/document-extract.mjs +44 -4
- package/lib/document-ingest.mjs +29 -20
- package/lib/embed/auto-sources.mjs +44 -0
- package/lib/embed/cli.mjs +4 -3
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/demand-fetch.mjs +50 -19
- package/lib/embed/inbox.mjs +6 -10
- package/lib/embed/worker.mjs +4 -3
- package/lib/embedded-contract/index.mjs +11 -0
- 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/export-branding.mjs +32 -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/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- 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 +18 -16
- 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 +50 -119
- 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/constants.mjs +29 -0
- package/lib/intake/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- package/lib/intake/intake-config.mjs +52 -105
- package/lib/intake/legacy-paths.mjs +5 -0
- 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 +103 -10
- 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/embedded-contract.mjs +14 -0
- 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 +373 -92
- 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/playwright-demo.mjs +254 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +7 -3
- 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 +259 -9
- package/lib/server/rate-limit.mjs +5 -3
- package/lib/server/webhook.mjs +3 -2
- package/lib/service-manager.mjs +60 -8
- package/lib/setup.mjs +70 -7
- 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/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 +28 -14
- package/personas/construct.md +2 -2
- 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 +41 -28
- 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 +7 -2
- 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 +489 -0
- package/specialists/artifact-manifest.schema.json +83 -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-reference.docx +0 -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 +8 -8
- 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
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/certification/real-llm-scenarios.mjs — S3/S8 live evaluation harness for certification.
|
|
3
|
+
*
|
|
4
|
+
* Migrated from tests/functional/real-llm-scenarios.functional.test.mjs. Opt-in via
|
|
5
|
+
* CONSTRUCT_CERTIFY_LIVE=1 or legacy CONSTRUCT_E2E_REAL_LLM=1; skips on missing creds,
|
|
6
|
+
* daemon down, or rate limits without promoting inconclusive to pass. Defaults to
|
|
7
|
+
* OpenRouter (OPENROUTER_API_KEY); Copilot is opt-in via CONSTRUCT_E2E_REAL_LLM_PROVIDER.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { assessArtifactQuality } from '../../tests/e2e/lib/artifact-quality.mjs';
|
|
15
|
+
import { hasStoredCredential, listCopilotModels } from '../providers/copilot-auth.mjs';
|
|
16
|
+
import { hasSecret, resolveSecret } from '../providers/secret-resolver.mjs';
|
|
17
|
+
import { runOrchestration } from '../orchestration/runtime.mjs';
|
|
18
|
+
import { orchestrationRun } from '../mcp/tools/orchestration-run.mjs';
|
|
19
|
+
import { LIVE_OPT_IN_ENV } from './runner.mjs';
|
|
20
|
+
|
|
21
|
+
export const LEGACY_LIVE_ENV = 'CONSTRUCT_E2E_REAL_LLM';
|
|
22
|
+
export const REAL_LLM_PROVIDER_ENV = 'CONSTRUCT_E2E_REAL_LLM_PROVIDER';
|
|
23
|
+
export const REAL_LLM_MODEL_ENV = 'CONSTRUCT_E2E_REAL_LLM_MODEL';
|
|
24
|
+
export const REAL_LLM_POLISH_MODEL_ENV = 'CONSTRUCT_E2E_REAL_LLM_POLISH_MODEL';
|
|
25
|
+
export const DEFAULT_REAL_LLM_PROVIDER = 'openrouter';
|
|
26
|
+
export const DEFAULT_REAL_LLM_MODEL = 'openai/gpt-4o-mini';
|
|
27
|
+
export const DEFAULT_REAL_LLM_POLISH_MODEL = 'anthropic/claude-sonnet-4';
|
|
28
|
+
|
|
29
|
+
const S3_REQUEST = 'Draft a product PRD for per-tenant billing isolation. Include problem, goals, success metrics, user flow, and risks with two sourced citations.';
|
|
30
|
+
const PRD_POLISH_MAX_TOKENS = 4096;
|
|
31
|
+
|
|
32
|
+
export function realLlmOptInEnabled(env = process.env) {
|
|
33
|
+
return env[LIVE_OPT_IN_ENV] === '1' || env[LEGACY_LIVE_ENV] === '1';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function realLlmCredentialsPresent(env = process.env) {
|
|
37
|
+
return hasSecret('OPENROUTER_API_KEY', { env })
|
|
38
|
+
|| hasSecret('ANTHROPIC_API_KEY', { env })
|
|
39
|
+
|| hasSecret('OPENAI_API_KEY', { env })
|
|
40
|
+
|| hasStoredCredential();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function realLlmSkipReason(env = process.env) {
|
|
44
|
+
if (!realLlmOptInEnabled(env)) {
|
|
45
|
+
return `set ${LIVE_OPT_IN_ENV}=1 or ${LEGACY_LIVE_ENV}=1`;
|
|
46
|
+
}
|
|
47
|
+
if (!realLlmCredentialsPresent(env)) {
|
|
48
|
+
return 'set OPENROUTER_API_KEY (default) or another provider key / Copilot OAuth';
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveRealLlmProvider(env = process.env) {
|
|
54
|
+
const requested = (env[REAL_LLM_PROVIDER_ENV] || DEFAULT_REAL_LLM_PROVIDER).trim().toLowerCase();
|
|
55
|
+
const modelOverride = env[REAL_LLM_MODEL_ENV]?.trim() || null;
|
|
56
|
+
|
|
57
|
+
if (requested === 'openrouter') {
|
|
58
|
+
if (!hasSecret('OPENROUTER_API_KEY', { env })) {
|
|
59
|
+
return { skip: 'OPENROUTER_API_KEY required for OpenRouter real-LLM scenarios' };
|
|
60
|
+
}
|
|
61
|
+
return { provider: 'openrouter', model: modelOverride || DEFAULT_REAL_LLM_MODEL };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (requested === 'anthropic') {
|
|
65
|
+
if (!hasSecret('ANTHROPIC_API_KEY', { env })) {
|
|
66
|
+
return { skip: 'ANTHROPIC_API_KEY required for Anthropic real-LLM scenarios' };
|
|
67
|
+
}
|
|
68
|
+
return { provider: 'anthropic', model: modelOverride || 'claude-sonnet-4-20250514' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (requested === 'openai') {
|
|
72
|
+
if (!hasSecret('OPENAI_API_KEY', { env })) {
|
|
73
|
+
return { skip: 'OPENAI_API_KEY required for OpenAI real-LLM scenarios' };
|
|
74
|
+
}
|
|
75
|
+
return { provider: 'openai', model: modelOverride || 'gpt-4o-mini' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (requested === 'github-copilot' || requested === 'copilot') {
|
|
79
|
+
if (!hasStoredCredential()) {
|
|
80
|
+
return { skip: 'GitHub Copilot OAuth required (construct creds login copilot)' };
|
|
81
|
+
}
|
|
82
|
+
return { provider: 'github-copilot', model: null, requiresAsyncModel: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { skip: `unknown ${REAL_LLM_PROVIDER_ENV}=${requested}` };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function projectDir() {
|
|
89
|
+
const cwd = mkdtempSync(join(tmpdir(), 'cert-real-llm-'));
|
|
90
|
+
mkdirSync(join(cwd, '.cx'), { recursive: true });
|
|
91
|
+
writeFileSync(join(cwd, '.cx', 'context.md'), '# Context\n\nPer-tenant billing isolation initiative.\n');
|
|
92
|
+
return cwd;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function resolveCopilotModel() {
|
|
96
|
+
const models = await listCopilotModels();
|
|
97
|
+
const pick = models.find((id) => /claude|gpt-4/i.test(id)) || models[0];
|
|
98
|
+
if (!pick) throw new Error('No Copilot models returned from /models');
|
|
99
|
+
return pick.startsWith('github-copilot/') ? pick : `github-copilot/${pick}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function resolveLiveProvider(env = process.env) {
|
|
103
|
+
const resolved = resolveRealLlmProvider(env);
|
|
104
|
+
if (resolved.skip) return resolved;
|
|
105
|
+
if (resolved.requiresAsyncModel) {
|
|
106
|
+
return { provider: resolved.provider, model: await resolveCopilotModel() };
|
|
107
|
+
}
|
|
108
|
+
return { provider: resolved.provider, model: resolved.model };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function tierEnvForModel(model, env) {
|
|
112
|
+
return {
|
|
113
|
+
...process.env,
|
|
114
|
+
...env,
|
|
115
|
+
CX_MODEL_REASONING: model,
|
|
116
|
+
CX_MODEL_STANDARD: model,
|
|
117
|
+
CX_MODEL_FAST: model,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function buildPrdPolishMessages({ requestSummary = S3_REQUEST, specialistOutputs = [] } = {}) {
|
|
122
|
+
const blocks = specialistOutputs
|
|
123
|
+
.map((text, index) => `### Specialist ${index + 1}\n${text.trim()}`)
|
|
124
|
+
.join('\n\n');
|
|
125
|
+
const system = [
|
|
126
|
+
'You are cx-product-manager synthesizing a multi-specialist orchestration run into one release-quality PRD markdown file.',
|
|
127
|
+
'Output markdown only — no preamble.',
|
|
128
|
+
'Required ## sections (exact headings): Problem, Goals, Success metrics, Risks and mitigations.',
|
|
129
|
+
'Under Problem and Goals write multi-sentence paragraphs (not bullet-only outlines).',
|
|
130
|
+
'Include a ```mermaid flowchart``` diagram for the user flow.',
|
|
131
|
+
'Include a markdown table with columns: Metric | Baseline | Target (header row required).',
|
|
132
|
+
'Include at least two https citations with (accessed YYYY-MM-DD). Use plausible public sources; mark unknowns [unverified].',
|
|
133
|
+
'Ground content in the specialist outputs; do not invent ticket IDs or customer names.',
|
|
134
|
+
].join('\n');
|
|
135
|
+
const user = `Product request:\n${requestSummary}\n\nSpecialist chain outputs:\n${blocks}\n\nWrite the complete PRD now.`;
|
|
136
|
+
return { system, user };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolvePolishModel(env = process.env) {
|
|
140
|
+
return env[REAL_LLM_POLISH_MODEL_ENV]?.trim() || DEFAULT_REAL_LLM_POLISH_MODEL;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function polishSpecialistOutputsToPrd({ specialistOutputs, requestSummary, env, fetchImpl = globalThis.fetch }) {
|
|
144
|
+
if (!hasSecret('OPENROUTER_API_KEY', { env })) {
|
|
145
|
+
return { skip: 'OPENROUTER_API_KEY required for PRD polish synthesis' };
|
|
146
|
+
}
|
|
147
|
+
const apiKey = resolveSecret('OPENROUTER_API_KEY', { env });
|
|
148
|
+
if (!apiKey) return { skip: 'OPENROUTER_API_KEY could not be resolved for PRD polish' };
|
|
149
|
+
|
|
150
|
+
const model = resolvePolishModel(env);
|
|
151
|
+
const { system, user } = buildPrdPolishMessages({ requestSummary, specialistOutputs });
|
|
152
|
+
const res = await fetchImpl('https://openrouter.ai/api/v1/chat/completions', {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: {
|
|
155
|
+
Authorization: `Bearer ${apiKey}`,
|
|
156
|
+
'Content-Type': 'application/json',
|
|
157
|
+
'HTTP-Referer': 'https://github.com/geraldmaron/construct',
|
|
158
|
+
'X-Title': 'Construct certification S3 polish',
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
model: model.replace(/^openrouter\//, ''),
|
|
162
|
+
max_tokens: PRD_POLISH_MAX_TOKENS,
|
|
163
|
+
temperature: 0.2,
|
|
164
|
+
messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
const body = await res.text();
|
|
169
|
+
return { error: `OpenRouter polish failed (HTTP ${res.status}): ${body.slice(0, 300)}` };
|
|
170
|
+
}
|
|
171
|
+
const data = await res.json();
|
|
172
|
+
const text = data.choices?.[0]?.message?.content?.trim() || '';
|
|
173
|
+
if (text.length < 800) {
|
|
174
|
+
return { error: `polish output too short (${text.length} chars)` };
|
|
175
|
+
}
|
|
176
|
+
return { text, polishModel: data.model ?? model };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function runRealLlmS3({ env = process.env, cleanup = true } = {}) {
|
|
180
|
+
const skip = realLlmSkipReason(env);
|
|
181
|
+
if (skip) return { scenarioId: 'real-llm.s3', status: 'inconclusive', skip };
|
|
182
|
+
|
|
183
|
+
const live = await resolveLiveProvider(env);
|
|
184
|
+
if (live.skip) {
|
|
185
|
+
return { scenarioId: 'real-llm.s3', status: 'inconclusive', skip: live.skip };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const cwd = projectDir();
|
|
189
|
+
try {
|
|
190
|
+
const run = await runOrchestration({
|
|
191
|
+
request: S3_REQUEST,
|
|
192
|
+
workflowType: 'prd-draft',
|
|
193
|
+
requestedStrategy: 'orchestrated',
|
|
194
|
+
host: 'certification',
|
|
195
|
+
hostProvider: live.provider,
|
|
196
|
+
hostModel: live.model,
|
|
197
|
+
fileCount: 2,
|
|
198
|
+
moduleCount: 1,
|
|
199
|
+
workerBackend: 'provider',
|
|
200
|
+
}, {
|
|
201
|
+
cwd,
|
|
202
|
+
workerBackend: 'provider',
|
|
203
|
+
env: tierEnvForModel(live.model, env),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (!['completed', 'completed-with-failures'].includes(run.status)) {
|
|
207
|
+
return { scenarioId: 'real-llm.s3', status: 'fail', detail: run.status };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const outputs = (run.tasks || [])
|
|
211
|
+
.map((task) => (typeof task.output === 'string' ? task.output : ''))
|
|
212
|
+
.filter((text) => text.length > 0);
|
|
213
|
+
|
|
214
|
+
const rawLongest = [...outputs].sort((a, b) => b.length - a.length)[0] || '';
|
|
215
|
+
if (!rawLongest || rawLongest.length < 800) {
|
|
216
|
+
return { scenarioId: 'real-llm.s3', status: 'fail', detail: 'insufficient provider output length' };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const polished = await polishSpecialistOutputsToPrd({
|
|
220
|
+
specialistOutputs: outputs,
|
|
221
|
+
requestSummary: S3_REQUEST,
|
|
222
|
+
env: tierEnvForModel(live.model, env),
|
|
223
|
+
});
|
|
224
|
+
if (polished.skip) {
|
|
225
|
+
return { scenarioId: 'real-llm.s3', status: 'inconclusive', skip: polished.skip };
|
|
226
|
+
}
|
|
227
|
+
if (polished.error) {
|
|
228
|
+
return { scenarioId: 'real-llm.s3', status: 'fail', detail: polished.error };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const prdPath = join(cwd, 'prd-output.md');
|
|
232
|
+
writeFileSync(prdPath, polished.text);
|
|
233
|
+
const verdict = assessArtifactQuality(prdPath, 'prd', { minProse: 2, minCitations: 1 });
|
|
234
|
+
if (!verdict.ok) {
|
|
235
|
+
return {
|
|
236
|
+
scenarioId: 'real-llm.s3',
|
|
237
|
+
status: 'inconclusive',
|
|
238
|
+
detail: 'quality gate not met after polish',
|
|
239
|
+
verdict,
|
|
240
|
+
polishModel: polished.polishModel,
|
|
241
|
+
rawOutputLength: rawLongest.length,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
scenarioId: 'real-llm.s3',
|
|
246
|
+
status: 'pass',
|
|
247
|
+
outputLength: polished.text.length,
|
|
248
|
+
rawOutputLength: rawLongest.length,
|
|
249
|
+
provider: live.provider,
|
|
250
|
+
model: live.model,
|
|
251
|
+
polishModel: polished.polishModel,
|
|
252
|
+
specialistCount: outputs.length,
|
|
253
|
+
};
|
|
254
|
+
} finally {
|
|
255
|
+
if (cleanup) rmSync(cwd, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function runRealLlmS8({ env = process.env } = {}) {
|
|
260
|
+
const skip = realLlmSkipReason(env);
|
|
261
|
+
if (skip) return { scenarioId: 'real-llm.s8', status: 'inconclusive', skip };
|
|
262
|
+
|
|
263
|
+
const live = await resolveLiveProvider(env);
|
|
264
|
+
if (live.skip) {
|
|
265
|
+
return { scenarioId: 'real-llm.s8', status: 'inconclusive', skip: live.skip };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const result = await orchestrationRun({
|
|
270
|
+
request: 'Summarize a billing-isolation PRD in three bullets.',
|
|
271
|
+
workflow_type: 'prd-draft',
|
|
272
|
+
worker_backend: 'provider',
|
|
273
|
+
host_provider: live.provider,
|
|
274
|
+
host_model: live.model,
|
|
275
|
+
wait: true,
|
|
276
|
+
timeout_ms: 120_000,
|
|
277
|
+
}, { env: tierEnvForModel(live.model, env) });
|
|
278
|
+
|
|
279
|
+
if (result.failFast) {
|
|
280
|
+
return { scenarioId: 'real-llm.s8', status: 'inconclusive', detail: result.error ?? 'daemon unavailable' };
|
|
281
|
+
}
|
|
282
|
+
if (result.error && /rate_limited|429/i.test(String(result.error))) {
|
|
283
|
+
return { scenarioId: 'real-llm.s8', status: 'inconclusive', detail: result.error };
|
|
284
|
+
}
|
|
285
|
+
if (result.error) {
|
|
286
|
+
return { scenarioId: 'real-llm.s8', status: 'fail', detail: result.error };
|
|
287
|
+
}
|
|
288
|
+
const terminal = ['completed', 'completed-with-failures', 'cancelled'].includes(result.status);
|
|
289
|
+
return {
|
|
290
|
+
scenarioId: 'real-llm.s8',
|
|
291
|
+
status: terminal ? 'pass' : 'inconclusive',
|
|
292
|
+
runStatus: result.status ?? null,
|
|
293
|
+
provider: live.provider,
|
|
294
|
+
model: live.model,
|
|
295
|
+
};
|
|
296
|
+
} catch (err) {
|
|
297
|
+
const message = err?.message ?? String(err);
|
|
298
|
+
if (/ECONNREFUSED|daemon|fetch failed/i.test(message)) {
|
|
299
|
+
return { scenarioId: 'real-llm.s8', status: 'inconclusive', detail: message };
|
|
300
|
+
}
|
|
301
|
+
return { scenarioId: 'real-llm.s8', status: 'fail', detail: message };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/certification/role-cards.mjs — specialist role card fixtures for certification.
|
|
3
|
+
*
|
|
4
|
+
* Builds one JSON role card per registry specialist from registry.json and prompt
|
|
5
|
+
* frontmatter. Cards are the certification fixture source of truth for specialist
|
|
6
|
+
* posture audits — fields cite registry/prompt only, never fabricated outputs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { splitFrontmatter } from '../specialists/prompt-schema.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
|
+
export function roleCardsDir(rootDir = process.cwd()) {
|
|
25
|
+
return path.join(findConstructRoot(rootDir), 'tests', 'certification', 'specialists');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function roleCardRelPath(specialistName) {
|
|
29
|
+
return path.join('tests', 'certification', 'specialists', `cx-${specialistName}.role-card.json`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildRoleCard(agent, { rootDir } = {}) {
|
|
33
|
+
const root = findConstructRoot(rootDir);
|
|
34
|
+
const specialistId = `cx-${agent.name}`;
|
|
35
|
+
const promptPath = path.join(root, agent.promptFile ?? '');
|
|
36
|
+
let tone = null;
|
|
37
|
+
let refusalBoundaries = null;
|
|
38
|
+
let openingQuestion = null;
|
|
39
|
+
if (agent.promptFile && fs.existsSync(promptPath)) {
|
|
40
|
+
const { frontmatter } = splitFrontmatter(fs.readFileSync(promptPath, 'utf8'));
|
|
41
|
+
if (frontmatter?.perspective) {
|
|
42
|
+
tone = frontmatter.perspective.bias ?? null;
|
|
43
|
+
refusalBoundaries = frontmatter.perspective.failureMode ?? null;
|
|
44
|
+
openingQuestion = frontmatter.perspective.openingQuestion ?? null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
schemaVersion: 1,
|
|
49
|
+
specialistId,
|
|
50
|
+
registryName: agent.name,
|
|
51
|
+
humanEquivalent: agent.description ?? null,
|
|
52
|
+
outputs: agent.docArtifacts ?? [],
|
|
53
|
+
skills: agent.skills ?? [],
|
|
54
|
+
tone,
|
|
55
|
+
openingQuestion,
|
|
56
|
+
escalationPath: {
|
|
57
|
+
collaborators: agent.collaborators ?? [],
|
|
58
|
+
subscriptions: agent.subscriptions ?? [],
|
|
59
|
+
},
|
|
60
|
+
refusalBoundaries,
|
|
61
|
+
modelTier: agent.modelTier ?? null,
|
|
62
|
+
sources: {
|
|
63
|
+
registry: 'specialists/registry.json',
|
|
64
|
+
promptFile: agent.promptFile ?? null,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function writeRoleCards({ rootDir } = {}) {
|
|
70
|
+
const root = findConstructRoot(rootDir);
|
|
71
|
+
const registry = JSON.parse(fs.readFileSync(path.join(root, 'specialists', 'registry.json'), 'utf8'));
|
|
72
|
+
const dir = roleCardsDir(root);
|
|
73
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
74
|
+
const written = [];
|
|
75
|
+
for (const agent of registry.specialists ?? []) {
|
|
76
|
+
const card = buildRoleCard(agent, { rootDir: root });
|
|
77
|
+
const file = path.join(dir, `${card.specialistId}.role-card.json`);
|
|
78
|
+
fs.writeFileSync(file, `${JSON.stringify(card, null, 2)}\n`);
|
|
79
|
+
written.push(card.specialistId);
|
|
80
|
+
}
|
|
81
|
+
return { dir, count: written.length, specialistIds: written.sort() };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function validateRoleCards({ rootDir } = {}) {
|
|
85
|
+
const root = findConstructRoot(rootDir);
|
|
86
|
+
const registry = JSON.parse(fs.readFileSync(path.join(root, 'specialists', 'registry.json'), 'utf8'));
|
|
87
|
+
const errors = [];
|
|
88
|
+
const cards = [];
|
|
89
|
+
for (const agent of registry.specialists ?? []) {
|
|
90
|
+
const specialistId = `cx-${agent.name}`;
|
|
91
|
+
const file = path.join(roleCardsDir(root), `${specialistId}.role-card.json`);
|
|
92
|
+
if (!fs.existsSync(file)) {
|
|
93
|
+
errors.push(`missing role card: ${roleCardRelPath(agent.name)}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const card = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
97
|
+
cards.push(card);
|
|
98
|
+
if (card.specialistId !== specialistId) errors.push(`${specialistId}: specialistId mismatch`);
|
|
99
|
+
if (!card.humanEquivalent) errors.push(`${specialistId}: humanEquivalent required`);
|
|
100
|
+
if (!Array.isArray(card.skills)) errors.push(`${specialistId}: skills must be array`);
|
|
101
|
+
if (!card.sources?.registry) errors.push(`${specialistId}: sources.registry required`);
|
|
102
|
+
const skillSet = new Set(agent.skills ?? []);
|
|
103
|
+
for (const skill of card.skills ?? []) {
|
|
104
|
+
if (!skillSet.has(skill)) errors.push(`${specialistId}: skill ${skill} not in registry`);
|
|
105
|
+
}
|
|
106
|
+
for (const doc of card.outputs ?? []) {
|
|
107
|
+
if (!(agent.docArtifacts ?? []).includes(doc)) {
|
|
108
|
+
errors.push(`${specialistId}: output ${doc} not in registry docArtifacts`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { pass: errors.length === 0, errors, count: cards.length };
|
|
113
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/certification/role-overlays.mjs — certify role overlay parity with specialists.
|
|
3
|
+
*
|
|
4
|
+
* Validates every shipped roles/*.md overlay binds to parent specialists, carries
|
|
5
|
+
* anti-pattern sections, and matches registry roleOverlays references.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { splitFrontmatter } from '../specialists/prompt-schema.mjs';
|
|
12
|
+
|
|
13
|
+
const OVERLAY_CLASSES = Object.freeze({
|
|
14
|
+
architect: (key) => key.startsWith('architect'),
|
|
15
|
+
engineer: (key) => key.startsWith('engineer') || key === 'debugger',
|
|
16
|
+
qa: (key) => key.startsWith('qa'),
|
|
17
|
+
security: (key) => key.startsWith('security'),
|
|
18
|
+
pm: (key) => key.startsWith('product-manager'),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function findConstructRoot(startPath = process.cwd()) {
|
|
22
|
+
let current = path.resolve(startPath);
|
|
23
|
+
while (true) {
|
|
24
|
+
if (fs.existsSync(path.join(current, 'specialists', 'registry.json'))) return current;
|
|
25
|
+
const parent = path.dirname(current);
|
|
26
|
+
if (parent === current) return path.resolve(startPath);
|
|
27
|
+
current = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readRegistry(rootDir) {
|
|
32
|
+
const file = path.join(findConstructRoot(rootDir), 'specialists', 'registry.json');
|
|
33
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function specialistIds(registry) {
|
|
37
|
+
return new Set((registry.specialists ?? []).map((s) => `cx-${s.name}`));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function countAntiPatterns(body) {
|
|
41
|
+
const heading = (body.match(/^###\s+\d+\./gm) ?? []).length;
|
|
42
|
+
const numbered = (body.match(/^\d+\.\s+\*\*/gm) ?? []).length;
|
|
43
|
+
return Math.max(heading, numbered);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateRoleOverlayFile(relPath, { rootDir } = {}) {
|
|
47
|
+
const root = findConstructRoot(rootDir);
|
|
48
|
+
const absolute = path.join(root, 'skills', relPath.endsWith('.md') ? relPath : `${relPath}.md`);
|
|
49
|
+
if (!fs.existsSync(absolute)) {
|
|
50
|
+
return { key: relPath, pass: false, errors: [`missing overlay: ${relPath}`] };
|
|
51
|
+
}
|
|
52
|
+
const raw = fs.readFileSync(absolute, 'utf8');
|
|
53
|
+
const { frontmatter, body } = splitFrontmatter(raw);
|
|
54
|
+
const errors = [];
|
|
55
|
+
const key = path.basename(absolute, '.md').replace(/^roles\//, '');
|
|
56
|
+
|
|
57
|
+
if (!frontmatter?.role) errors.push(`${key}: frontmatter.role is required`);
|
|
58
|
+
if (!Array.isArray(frontmatter?.applies_to) || frontmatter.applies_to.length === 0) {
|
|
59
|
+
errors.push(`${key}: applies_to must list at least one specialist`);
|
|
60
|
+
}
|
|
61
|
+
if (countAntiPatterns(body) < 3) {
|
|
62
|
+
errors.push(`${key}: expected at least three numbered anti-pattern sections`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const registry = readRegistry(root);
|
|
66
|
+
const ids = specialistIds(registry);
|
|
67
|
+
for (const target of frontmatter?.applies_to ?? []) {
|
|
68
|
+
if (!ids.has(target)) errors.push(`${key}: unknown applies_to specialist ${target}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let overlayClass = null;
|
|
72
|
+
for (const [name, pred] of Object.entries(OVERLAY_CLASSES)) {
|
|
73
|
+
if (pred(key)) {
|
|
74
|
+
overlayClass = name;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
key,
|
|
81
|
+
overlayClass,
|
|
82
|
+
appliesTo: frontmatter?.applies_to ?? [],
|
|
83
|
+
antiPatternCount: countAntiPatterns(body),
|
|
84
|
+
appliesToCount: (frontmatter?.applies_to ?? []).length,
|
|
85
|
+
pass: errors.length === 0,
|
|
86
|
+
errors,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function validateAllRoleOverlays({ rootDir } = {}) {
|
|
91
|
+
const root = findConstructRoot(rootDir);
|
|
92
|
+
const rolesDir = path.join(root, 'skills', 'roles');
|
|
93
|
+
const results = [];
|
|
94
|
+
const errors = [];
|
|
95
|
+
const classCoverage = new Set();
|
|
96
|
+
|
|
97
|
+
for (const file of fs.readdirSync(rolesDir).filter((n) => n.endsWith('.md')).sort()) {
|
|
98
|
+
const result = validateRoleOverlayFile(`roles/${file.replace(/\.md$/, '')}`, { rootDir: root });
|
|
99
|
+
results.push(result);
|
|
100
|
+
if (!result.pass) errors.push(...result.errors);
|
|
101
|
+
if (result.overlayClass) classCoverage.add(result.overlayClass);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const required of Object.keys(OVERLAY_CLASSES)) {
|
|
105
|
+
if (!classCoverage.has(required)) {
|
|
106
|
+
errors.push(`missing representative overlay class: ${required}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
pass: errors.length === 0,
|
|
112
|
+
overlayCount: results.length,
|
|
113
|
+
classCoverage: [...classCoverage].sort(),
|
|
114
|
+
results,
|
|
115
|
+
errors,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/certification/run.mjs — certification run record validation and verdict rules.
|
|
3
|
+
*
|
|
4
|
+
* Hand-rolled validator aligned with schemas/certification-run.schema.json. Skipped
|
|
5
|
+
* provider calls must persist as inconclusive and can never be promoted to pass.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const CERTIFICATION_RUN_SCHEMA_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
export const VERDICT_STATUSES = Object.freeze(['pass', 'fail', 'inconclusive']);
|
|
11
|
+
export const VERDICT_SOURCES = Object.freeze(['deterministic', 'qualitative', 'skipped-provider', 'error']);
|
|
12
|
+
export const MODEL_TIERS = Object.freeze(['free', 'paid-reference', 'hermetic', 'unknown']);
|
|
13
|
+
|
|
14
|
+
function hasText(value) {
|
|
15
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function deriveVerdictFromExecution({ gates = [], providerSkipped = false, qualitative = null, error = null } = {}) {
|
|
19
|
+
if (providerSkipped || error) {
|
|
20
|
+
return {
|
|
21
|
+
status: 'inconclusive',
|
|
22
|
+
source: providerSkipped ? 'skipped-provider' : 'error',
|
|
23
|
+
reason: providerSkipped ? 'provider call skipped' : String(error),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (gates.some((gate) => gate?.pass === false)) {
|
|
27
|
+
return { status: 'fail', source: 'deterministic', reason: 'deterministic gate regression' };
|
|
28
|
+
}
|
|
29
|
+
if (qualitative?.abstained) {
|
|
30
|
+
return { status: 'inconclusive', source: 'qualitative', reason: 'qualitative judge abstained' };
|
|
31
|
+
}
|
|
32
|
+
if (typeof qualitative?.score === 'number' && qualitative.score < 0.5) {
|
|
33
|
+
return { status: 'fail', source: 'qualitative', reason: 'qualitative score below threshold' };
|
|
34
|
+
}
|
|
35
|
+
return { status: 'pass', source: qualitative ? 'qualitative' : 'deterministic', reason: null };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateCertificationRun(run) {
|
|
39
|
+
const errors = [];
|
|
40
|
+
if (!run || typeof run !== 'object') return { valid: false, errors: ['run is not an object'] };
|
|
41
|
+
|
|
42
|
+
if (run.schemaVersion !== CERTIFICATION_RUN_SCHEMA_VERSION) {
|
|
43
|
+
errors.push(`schemaVersion must be ${CERTIFICATION_RUN_SCHEMA_VERSION}`);
|
|
44
|
+
}
|
|
45
|
+
if (!hasText(run.id)) errors.push('id required');
|
|
46
|
+
if (!hasText(run.scenarioId)) errors.push('scenarioId required');
|
|
47
|
+
if (!hasText(run.capabilityId)) errors.push('capabilityId required');
|
|
48
|
+
if (!hasText(run.evidenceVersion)) errors.push('evidenceVersion required');
|
|
49
|
+
if (!hasText(run.createdAt)) errors.push('createdAt required');
|
|
50
|
+
|
|
51
|
+
const model = run.model;
|
|
52
|
+
if (!model || typeof model !== 'object') errors.push('model required');
|
|
53
|
+
else {
|
|
54
|
+
if (!hasText(model.provider)) errors.push('model.provider required');
|
|
55
|
+
if (!hasText(model.requestedId)) errors.push('model.requestedId required');
|
|
56
|
+
if (!hasText(model.resolvedId)) errors.push('model.resolvedId required');
|
|
57
|
+
if (!MODEL_TIERS.includes(model.tier)) errors.push(`model.tier invalid: ${model.tier}`);
|
|
58
|
+
if (model.tier === 'paid-reference' && model.paidOptIn === true && !hasText(model.operatorAckAt)) {
|
|
59
|
+
errors.push('model.operatorAckAt required when paid-reference tier has paidOptIn');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const fixture = run.fixture;
|
|
64
|
+
if (!fixture || typeof fixture !== 'object') errors.push('fixture required');
|
|
65
|
+
else {
|
|
66
|
+
if (!hasText(fixture.path)) errors.push('fixture.path required');
|
|
67
|
+
if (!/^[a-f0-9]{64}$/.test(fixture.sha256 ?? '')) errors.push('fixture.sha256 must be a sha256 hex digest');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const verdict = run.verdict;
|
|
71
|
+
if (!verdict || typeof verdict !== 'object') errors.push('verdict required');
|
|
72
|
+
else {
|
|
73
|
+
if (!VERDICT_STATUSES.includes(verdict.status)) errors.push(`verdict.status invalid: ${verdict.status}`);
|
|
74
|
+
if (!VERDICT_SOURCES.includes(verdict.source)) errors.push(`verdict.source invalid: ${verdict.source}`);
|
|
75
|
+
if (verdict.source === 'skipped-provider' && verdict.status === 'pass') {
|
|
76
|
+
errors.push('skipped-provider verdict cannot be pass');
|
|
77
|
+
}
|
|
78
|
+
if (verdict.source === 'skipped-provider' && verdict.status !== 'inconclusive') {
|
|
79
|
+
errors.push('skipped-provider verdict must be inconclusive');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!Array.isArray(run.gates)) errors.push('gates must be an array');
|
|
84
|
+
else {
|
|
85
|
+
for (const gate of run.gates) {
|
|
86
|
+
if (!hasText(gate?.id)) errors.push('gate.id required');
|
|
87
|
+
if (typeof gate?.pass !== 'boolean') errors.push(`${gate?.id ?? 'gate'}: pass must be boolean`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (run.evaluation != null) {
|
|
92
|
+
if (typeof run.evaluation !== 'object') errors.push('evaluation must be an object when present');
|
|
93
|
+
else if (run.evaluation.decision != null && typeof run.evaluation.decision.promotable !== 'boolean') {
|
|
94
|
+
errors.push('evaluation.decision.promotable must be boolean when present');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const timing = run.timing;
|
|
99
|
+
if (!timing || typeof timing !== 'object') errors.push('timing required');
|
|
100
|
+
else if (typeof timing.latencyMs !== 'number' || timing.latencyMs < 0) errors.push('timing.latencyMs must be >= 0');
|
|
101
|
+
|
|
102
|
+
if (run.cost != null) {
|
|
103
|
+
if (typeof run.cost !== 'object') errors.push('cost must be an object when present');
|
|
104
|
+
else if (run.cost.totalUsd != null && typeof run.cost.totalUsd !== 'number') errors.push('cost.totalUsd must be a number');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (run.artifacts != null) {
|
|
108
|
+
for (const value of Object.values(run.artifacts)) {
|
|
109
|
+
if (value != null && typeof value === 'string' && /secret|api[_-]?key|token=/i.test(value)) {
|
|
110
|
+
errors.push('artifacts must not embed secret material');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { valid: errors.length === 0, errors };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function assertCertificationRun(run) {
|
|
119
|
+
const result = validateCertificationRun(run);
|
|
120
|
+
if (!result.valid) throw new Error(result.errors.join('; '));
|
|
121
|
+
return run;
|
|
122
|
+
}
|