@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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/artifact-type-from-path.mjs — Infer manifest doc type from artifact file paths.
|
|
3
|
+
*
|
|
4
|
+
* Resolves a manifest doc type when --type is omitted.
|
|
5
|
+
* Resolution order: YAML frontmatter cx_doc_type → directory heuristics → filename hints.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { artifactTypes } from './artifact-manifest.mjs';
|
|
11
|
+
|
|
12
|
+
const KNOWN = new Set(artifactTypes());
|
|
13
|
+
|
|
14
|
+
function readFrontmatterType(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
const head = fs.readFileSync(filePath, 'utf8').slice(0, 4096);
|
|
17
|
+
const match = head.match(/^---\n([\s\S]*?)\n---/);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
for (const line of match[1].split('\n')) {
|
|
20
|
+
const m = line.match(/^(?:cx_doc_type|artifactType|doc_type)\s*:\s*(.+)$/);
|
|
21
|
+
if (m) {
|
|
22
|
+
const value = m[1].trim().replace(/^['"]|['"]$/g, '');
|
|
23
|
+
return KNOWN.has(value) ? value : null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch { /* unreadable */ }
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function inferPrdSubtype(rel, base) {
|
|
31
|
+
const blob = `${rel} ${base}`.toLowerCase();
|
|
32
|
+
if (/meta[\s-]?prd/.test(blob)) return 'meta-prd';
|
|
33
|
+
if (/platform/.test(blob)) return 'prd-platform';
|
|
34
|
+
if (/business/.test(blob)) return 'prd-business';
|
|
35
|
+
return 'prd';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function inferRfcSubtype(rel, base) {
|
|
39
|
+
const blob = `${rel} ${base}`.toLowerCase();
|
|
40
|
+
if (/platform/.test(blob)) return 'rfc-platform';
|
|
41
|
+
return 'rfc';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PREFIX_RULES = [
|
|
45
|
+
[/^docs\/prd\//, inferPrdSubtype],
|
|
46
|
+
[/^docs\/adr\//, () => 'adr'],
|
|
47
|
+
[/^docs\/rfc\//, inferRfcSubtype],
|
|
48
|
+
[/^docs\/research\//, () => 'research-brief'],
|
|
49
|
+
[/^docs\/runbooks?\//, () => 'runbook'],
|
|
50
|
+
[/^\.cx\/research\//, () => 'research-brief'],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export function isArtifactGatePath(relPath) {
|
|
54
|
+
const rel = relPath.replace(/\\/g, '/');
|
|
55
|
+
if (!rel.endsWith('.md')) return false;
|
|
56
|
+
if (rel.startsWith('templates/docs/')) return false;
|
|
57
|
+
if (/^docs\//.test(rel) || /^\.cx\/research\//.test(rel)) return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function inferArtifactTypeFromPath(filePath, { rootDir = process.cwd() } = {}) {
|
|
62
|
+
const rel = path.relative(rootDir, filePath).replace(/\\/g, '/');
|
|
63
|
+
if (!rel.endsWith('.md')) return null;
|
|
64
|
+
|
|
65
|
+
const fromFm = readFrontmatterType(filePath);
|
|
66
|
+
if (fromFm) return fromFm;
|
|
67
|
+
|
|
68
|
+
if (!isArtifactGatePath(rel)) return null;
|
|
69
|
+
|
|
70
|
+
const base = path.basename(filePath, path.extname(filePath));
|
|
71
|
+
for (const [re, resolver] of PREFIX_RULES) {
|
|
72
|
+
if (re.test(rel)) {
|
|
73
|
+
const type = typeof resolver === 'function' ? resolver(rel, base) : resolver;
|
|
74
|
+
return KNOWN.has(type) ? type : null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
package/lib/audit-skills.mjs
CHANGED
|
@@ -155,6 +155,10 @@ export function auditSkills({ rootDir, silent = false } = {}) {
|
|
|
155
155
|
|
|
156
156
|
export async function runAuditSkillsCli(args = []) {
|
|
157
157
|
const rootDir = args.find((a) => a.startsWith('--root='))?.split('=')[1] ?? undefined;
|
|
158
|
+
if (args.includes('--inventory')) {
|
|
159
|
+
const { runSkillInventoryAuditCli } = await import('./certification/skill-inventory.mjs');
|
|
160
|
+
return runSkillInventoryAuditCli(args.filter((a) => a !== '--inventory'), { rootDir });
|
|
161
|
+
}
|
|
158
162
|
const result = auditSkills({ rootDir });
|
|
159
163
|
if (!result.pass) process.exit(1);
|
|
160
164
|
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/audit-specialists.mjs — Specialist/skill corpus audit matrix.
|
|
3
|
+
*
|
|
4
|
+
* Merges live registry, contracts, skills, prompts, and audit-enrichments into
|
|
5
|
+
* one machine-readable matrix. Powers `construct audit specialists` and the
|
|
6
|
+
* human-readable docs/concepts/specialist-skill-audit.md companion.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { auditSkills } from './audit-skills.mjs';
|
|
12
|
+
import { loadArtifactManifest } from './artifact-manifest.mjs';
|
|
13
|
+
import { resolvePerspectiveFromPrompt } from './specialists/prompt-schema.mjs';
|
|
14
|
+
import { validateRoleCards } from './certification/role-cards.mjs';
|
|
15
|
+
import { auditSpecialistContracts } from './certification/specialist-contracts.mjs';
|
|
16
|
+
import { validateAllRoleOverlays } from './certification/role-overlays.mjs';
|
|
17
|
+
import { ROLE_DIRECTIVE_RE } from './role-preload.mjs';
|
|
18
|
+
|
|
19
|
+
function findConstructRoot(startPath = process.cwd()) {
|
|
20
|
+
let current = path.resolve(startPath);
|
|
21
|
+
while (true) {
|
|
22
|
+
if (fs.existsSync(path.join(current, 'specialists', 'registry.json'))) return current;
|
|
23
|
+
const parent = path.dirname(current);
|
|
24
|
+
if (parent === current) return path.resolve(startPath);
|
|
25
|
+
current = parent;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readJson(p) {
|
|
30
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectSkillFiles(skillsDir) {
|
|
34
|
+
const results = [];
|
|
35
|
+
function walk(dir, prefix = '') {
|
|
36
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
walk(path.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
39
|
+
} else if (entry.name.endsWith('.md') && entry.name !== 'routing.md') {
|
|
40
|
+
results.push(prefix ? `${prefix}/${entry.name.replace(/\.md$/, '')}` : entry.name.replace(/\.md$/, ''));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
try { walk(skillsDir); } catch { /* missing */ }
|
|
45
|
+
return results.sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseRoleOverlay(promptPath) {
|
|
49
|
+
if (!fs.existsSync(promptPath)) return null;
|
|
50
|
+
const text = fs.readFileSync(promptPath, 'utf8');
|
|
51
|
+
const m = text.match(ROLE_DIRECTIVE_RE);
|
|
52
|
+
return m ? `roles/${m[1]}` : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function contractsForSpecialist(contracts, name) {
|
|
56
|
+
const id = name.startsWith('cx-') ? name : `cx-${name}`;
|
|
57
|
+
const related = [];
|
|
58
|
+
for (const c of contracts) {
|
|
59
|
+
if (c.producer === id || c.consumer === id) related.push(c.id);
|
|
60
|
+
}
|
|
61
|
+
return related;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function docTypesFromContracts(contracts) {
|
|
65
|
+
const types = new Set();
|
|
66
|
+
for (const c of contracts) {
|
|
67
|
+
const dt = c.trigger?.docAuthoring?.docType;
|
|
68
|
+
if (Array.isArray(dt)) dt.forEach((t) => types.add(t));
|
|
69
|
+
else if (typeof dt === 'string') types.add(dt);
|
|
70
|
+
}
|
|
71
|
+
return [...types];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function enforcementForAgent(agent, contracts) {
|
|
75
|
+
const id = `cx-${agent.name}`;
|
|
76
|
+
const items = ['no-fabrication.md', 'neurodivergent-output.md'];
|
|
77
|
+
const related = contracts.filter((c) => c.producer === id || c.consumer === id);
|
|
78
|
+
if (related.some((c) => c.postconditions?.length)) items.push('contracts.json postconditions');
|
|
79
|
+
if (agent.docArtifacts?.length) items.push('artifact structure lint', 'comment-lint on artifact paths');
|
|
80
|
+
if (agent.watchConditions?.length) items.push('watchConditions routing');
|
|
81
|
+
return items;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function crossChecks({ registry, manifest, skillAudit, allSkills, contractDocTypes }) {
|
|
85
|
+
const issues = [];
|
|
86
|
+
const manifestTypes = new Set(Object.keys(manifest.artifacts ?? {}));
|
|
87
|
+
|
|
88
|
+
for (const agent of registry.specialists ?? []) {
|
|
89
|
+
for (const skill of agent.skills ?? []) {
|
|
90
|
+
if (!allSkills.includes(skill)) {
|
|
91
|
+
issues.push({ kind: 'missing-skill-file', agent: agent.name, skill });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const doc of agent.docArtifacts ?? []) {
|
|
95
|
+
if (!manifestTypes.has(doc)) {
|
|
96
|
+
issues.push({ kind: 'doc-artifact-no-manifest', agent: agent.name, docType: doc });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const dt of contractDocTypes) {
|
|
102
|
+
if (!manifestTypes.has(dt)) {
|
|
103
|
+
issues.push({ kind: 'contract-doc-no-manifest', docType: dt });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (skillAudit.missingSkillFiles.length) {
|
|
108
|
+
for (const { agent, skill } of skillAudit.missingSkillFiles) {
|
|
109
|
+
issues.push({ kind: 'registry-missing-skill', agent, skill });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return issues;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function auditSpecialists({ rootDir, silent = false } = {}) {
|
|
117
|
+
const root = rootDir ?? findConstructRoot();
|
|
118
|
+
const registry = readJson(path.join(root, 'specialists', 'registry.json'));
|
|
119
|
+
const contractsDoc = readJson(path.join(root, 'specialists', 'contracts.json'));
|
|
120
|
+
const enrichments = readJson(path.join(root, 'specialists', 'audit-enrichments.json'));
|
|
121
|
+
const manifest = loadArtifactManifest({ rootDir: root });
|
|
122
|
+
const skillAudit = auditSkills({ rootDir: root, silent: true });
|
|
123
|
+
const allSkills = collectSkillFiles(path.join(root, 'skills'));
|
|
124
|
+
const contractDocTypes = docTypesFromContracts(contractsDoc.contracts ?? []);
|
|
125
|
+
const crossCheckIssues = crossChecks({ registry, manifest, skillAudit, allSkills, contractDocTypes });
|
|
126
|
+
|
|
127
|
+
const specialists = (registry.specialists ?? []).map((agent) => {
|
|
128
|
+
const promptPath = agent.promptFile
|
|
129
|
+
? path.join(root, agent.promptFile)
|
|
130
|
+
: null;
|
|
131
|
+
const meta = enrichments.specialists?.[agent.name] ?? {};
|
|
132
|
+
const perspective = resolvePerspectiveFromPrompt(agent.name, { rootDir: root, registry });
|
|
133
|
+
return {
|
|
134
|
+
name: agent.name,
|
|
135
|
+
id: `cx-${agent.name}`,
|
|
136
|
+
humanEquivalent: meta.humanEquivalent ?? 'unknown',
|
|
137
|
+
primaryOutputs: agent.docArtifacts ?? [],
|
|
138
|
+
skillsBound: agent.skills ?? [],
|
|
139
|
+
roleOverlay: promptPath ? parseRoleOverlay(promptPath) : null,
|
|
140
|
+
researchProfile: meta.researchProfile ?? null,
|
|
141
|
+
toneProfile: meta.toneProfile ?? 'direct',
|
|
142
|
+
challengeModel: {
|
|
143
|
+
tension: perspective?.tension ?? null,
|
|
144
|
+
failureMode: perspective?.failureMode ?? null,
|
|
145
|
+
watchConditions: agent.watchConditions ?? [],
|
|
146
|
+
},
|
|
147
|
+
enforcementToday: enforcementForAgent(agent, contractsDoc.contracts ?? []),
|
|
148
|
+
contracts: contractsForSpecialist(contractsDoc.contracts ?? [], agent.name),
|
|
149
|
+
grade: meta.grade ?? 'adequate',
|
|
150
|
+
gap: meta.gap ?? 'Not assessed',
|
|
151
|
+
beadId: meta.beadId ?? 'construct-hcr9',
|
|
152
|
+
skillCount: (agent.skills ?? []).length,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const roleOverlays = [];
|
|
157
|
+
const rolesDir = path.join(root, 'skills', 'roles');
|
|
158
|
+
if (fs.existsSync(rolesDir)) {
|
|
159
|
+
for (const f of fs.readdirSync(rolesDir).filter((n) => n.endsWith('.md')).sort()) {
|
|
160
|
+
const key = f.replace(/\.md$/, '');
|
|
161
|
+
const meta = enrichments.roleOverlays?.[key] ?? {};
|
|
162
|
+
roleOverlays.push({
|
|
163
|
+
path: `roles/${key}`,
|
|
164
|
+
grade: meta.grade ?? 'adequate',
|
|
165
|
+
gap: meta.gap ?? 'Not assessed',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const workflowSkills = allSkills
|
|
171
|
+
.filter((s) => !s.startsWith('roles/'))
|
|
172
|
+
.map((skillPath) => {
|
|
173
|
+
const manifestEntry = Object.entries(manifest.artifacts ?? {}).find(([, v]) => v.workflowSkill === skillPath);
|
|
174
|
+
return {
|
|
175
|
+
path: skillPath,
|
|
176
|
+
artifactType: manifestEntry?.[0] ?? null,
|
|
177
|
+
toneDefault: manifestEntry?.[1]?.toneDefault ?? null,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const roleCards = validateRoleCards({ rootDir: root });
|
|
182
|
+
const specialistContracts = auditSpecialistContracts({ rootDir: root });
|
|
183
|
+
const roleOverlayAudit = validateAllRoleOverlays({ rootDir: root });
|
|
184
|
+
if (!roleCards.pass) {
|
|
185
|
+
for (const err of roleCards.errors) crossCheckIssues.push({ kind: 'role-card-missing', detail: err });
|
|
186
|
+
}
|
|
187
|
+
if (!specialistContracts.pass) {
|
|
188
|
+
for (const fail of specialistContracts.failures) {
|
|
189
|
+
crossCheckIssues.push({ kind: 'specialist-contract-fail', specialist: fail.specialistId });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!roleOverlayAudit.pass) {
|
|
193
|
+
for (const err of roleOverlayAudit.errors.slice(0, 5)) {
|
|
194
|
+
crossCheckIssues.push({ kind: 'role-overlay-fail', detail: err });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const result = {
|
|
199
|
+
version: 1,
|
|
200
|
+
generatedAt: new Date().toISOString(),
|
|
201
|
+
specialistCount: specialists.length,
|
|
202
|
+
roleOverlayCount: roleOverlays.length,
|
|
203
|
+
workflowSkillCount: workflowSkills.length,
|
|
204
|
+
specialists,
|
|
205
|
+
roleOverlays,
|
|
206
|
+
workflowSkills,
|
|
207
|
+
crossCheckIssues,
|
|
208
|
+
roleCards,
|
|
209
|
+
specialistContracts,
|
|
210
|
+
roleOverlayAudit,
|
|
211
|
+
pass: crossCheckIssues.length === 0 && skillAudit.pass,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (!silent) {
|
|
215
|
+
const line = (msg) => process.stdout.write(`${msg}\n`);
|
|
216
|
+
line('Construct Specialist Audit');
|
|
217
|
+
line('═══════════════════════════');
|
|
218
|
+
line(` Specialists: ${result.specialistCount}`);
|
|
219
|
+
line(` Role overlays: ${result.roleOverlayCount}`);
|
|
220
|
+
line(` Workflow skills: ${result.workflowSkillCount}`);
|
|
221
|
+
line('');
|
|
222
|
+
if (crossCheckIssues.length === 0) {
|
|
223
|
+
line(' ✓ Cross-checks passed (skills, docArtifacts, manifest, contracts)');
|
|
224
|
+
} else {
|
|
225
|
+
line(` ✗ Cross-check issues (${crossCheckIssues.length}):`);
|
|
226
|
+
for (const issue of crossCheckIssues) line(` - ${JSON.stringify(issue)}`);
|
|
227
|
+
}
|
|
228
|
+
line('');
|
|
229
|
+
line(result.pass ? ' Result: PASS' : ' Result: FAIL');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function formatAuditMarkdown(result) {
|
|
236
|
+
const lines = [
|
|
237
|
+
'# Specialist & skill audit',
|
|
238
|
+
'',
|
|
239
|
+
`Generated: ${result.generatedAt}. Re-run \`construct audit specialists --json\` for the live matrix.`,
|
|
240
|
+
'',
|
|
241
|
+
'## Specialists',
|
|
242
|
+
'',
|
|
243
|
+
'| Specialist | Human equivalent | Outputs | Skills | Role overlay | Research | Tone | Grade | Gap |',
|
|
244
|
+
'|---|---|---|---|---|---|---|---|---|',
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
for (const s of result.specialists) {
|
|
248
|
+
lines.push(
|
|
249
|
+
`| cx-${s.name} | ${s.humanEquivalent} | ${s.primaryOutputs.join(', ') || '—'} | ${s.skillCount} | ${s.roleOverlay ?? '—'} | ${s.researchProfile ?? '—'} | ${s.toneProfile} | ${s.grade} | ${s.gap} |`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
lines.push('', '## Role overlays', '', '| Overlay | Grade | Gap |', '|---|---|---|');
|
|
254
|
+
for (const r of result.roleOverlays) {
|
|
255
|
+
lines.push(`| ${r.path} | ${r.grade} | ${r.gap} |`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
lines.push('', '## Cross-check issues', '');
|
|
259
|
+
if (result.crossCheckIssues.length === 0) {
|
|
260
|
+
lines.push('None.');
|
|
261
|
+
} else {
|
|
262
|
+
for (const issue of result.crossCheckIssues) {
|
|
263
|
+
lines.push(`- \`${issue.kind}\`: ${JSON.stringify(issue)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return `${lines.join('\n')}\n`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function runAuditSpecialistsCli(args = []) {
|
|
271
|
+
const json = args.includes('--json');
|
|
272
|
+
const markdown = args.includes('--markdown');
|
|
273
|
+
const rootDir = args.find((a) => a.startsWith('--root='))?.split('=')[1];
|
|
274
|
+
const result = auditSpecialists({ rootDir, silent: true });
|
|
275
|
+
|
|
276
|
+
if (markdown) {
|
|
277
|
+
process.stdout.write(formatAuditMarkdown(result));
|
|
278
|
+
} else if (json) {
|
|
279
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
280
|
+
} else {
|
|
281
|
+
auditSpecialists({ rootDir, silent: false });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!result.pass) process.exit(1);
|
|
285
|
+
}
|
package/lib/audit-trail.mjs
CHANGED
|
@@ -1,22 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/audit-trail.mjs —
|
|
3
|
-
* lib/hooks/audit-trail.mjs and
|
|
2
|
+
* lib/audit-trail.mjs — owns the append-only audit log written by
|
|
3
|
+
* lib/hooks/audit-trail.mjs and rendered for `construct audit trail`.
|
|
4
4
|
*
|
|
5
5
|
* The log is JSONL at ~/.cx/audit-trail.jsonl. Each line carries a
|
|
6
6
|
* prev_line_hash pointing at the SHA-256 of the previous line — tampering
|
|
7
7
|
* (reorder, delete, edit) breaks the chain and is surfaced by --verify.
|
|
8
|
+
*
|
|
9
|
+
* A serial hash chain cannot survive concurrent appends: a read-prev-hash +
|
|
10
|
+
* append done outside a critical section lets two processes (hook, daemon,
|
|
11
|
+
* parallel tool calls in one session) both chain off the same predecessor, so
|
|
12
|
+
* the second link points at a stale tail. appendAuditRecord
|
|
13
|
+
* closes that window by computing prev_line_hash and appending inside a single
|
|
14
|
+
* cross-process file lock, making every writer effectively serialized.
|
|
15
|
+
*
|
|
16
|
+
* Corrupted links cannot be re-derived, so a `chain_reset` boundary line seals
|
|
17
|
+
* the damaged prefix as immutable legacy and re-bases the live chain;
|
|
18
|
+
* verifyChain validates only from the last boundary forward. See
|
|
19
|
+
* docs/concepts/observability.md.
|
|
8
20
|
*/
|
|
9
|
-
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
21
|
+
import { readFileSync, existsSync, statSync, mkdirSync } from 'node:fs';
|
|
10
22
|
import { createHash } from 'node:crypto';
|
|
11
|
-
import { join } from 'node:path';
|
|
23
|
+
import { join, dirname } from 'node:path';
|
|
12
24
|
import { homedir } from 'node:os';
|
|
13
25
|
|
|
26
|
+
import { appendBounded, readLastLineAcrossSegments } from './logging/rotate.mjs';
|
|
27
|
+
import { withFileLockSync } from './storage/file-lock.mjs';
|
|
28
|
+
|
|
14
29
|
const AUDIT_FILE = join(homedir(), '.cx', 'audit-trail.jsonl');
|
|
15
30
|
|
|
16
31
|
function sha256(input) {
|
|
17
32
|
return createHash('sha256').update(input).digest('hex');
|
|
18
33
|
}
|
|
19
34
|
|
|
35
|
+
// prev_line_hash and the append must be one atomic step or concurrent writers
|
|
36
|
+
// race; the file lock serializes them across processes.
|
|
37
|
+
|
|
38
|
+
export function appendAuditRecord(partial, { file = AUDIT_FILE } = {}) {
|
|
39
|
+
return withFileLockSync(file, () => {
|
|
40
|
+
let prev = null;
|
|
41
|
+
try {
|
|
42
|
+
const last = readLastLineAcrossSegments(file);
|
|
43
|
+
if (last !== null) prev = sha256(last);
|
|
44
|
+
} catch { /* first record, or unreadable tail */ }
|
|
45
|
+
const record = { ...partial, prev_line_hash: prev };
|
|
46
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
47
|
+
appendBounded('audit-trail', file, JSON.stringify(record) + '\n');
|
|
48
|
+
return record;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A reset boundary seals the records it follows as legacy excluded from
|
|
53
|
+
// verification and re-bases the live chain from this line.
|
|
54
|
+
|
|
55
|
+
export function writeChainReset({ file = AUDIT_FILE, reason = 'manual reset' } = {}) {
|
|
56
|
+
return appendAuditRecord({
|
|
57
|
+
ts: new Date().toISOString(),
|
|
58
|
+
chain_reset: true,
|
|
59
|
+
reason,
|
|
60
|
+
agent: 'construct',
|
|
61
|
+
tool: null,
|
|
62
|
+
target: null,
|
|
63
|
+
}, { file });
|
|
64
|
+
}
|
|
65
|
+
|
|
20
66
|
export function readAuditTrail({ limit = 50, since = null, agent = null, tool = null } = {}) {
|
|
21
67
|
if (!existsSync(AUDIT_FILE)) return [];
|
|
22
68
|
const lines = readFileSync(AUDIT_FILE, 'utf8').split('\n').filter(Boolean);
|
|
@@ -34,19 +80,29 @@ export function readAuditTrail({ limit = 50, since = null, agent = null, tool =
|
|
|
34
80
|
return filtered.slice(-limit);
|
|
35
81
|
}
|
|
36
82
|
|
|
37
|
-
export function verifyChain() {
|
|
38
|
-
if (!existsSync(
|
|
39
|
-
const lines = readFileSync(
|
|
83
|
+
export function verifyChain(file = AUDIT_FILE) {
|
|
84
|
+
if (!existsSync(file)) return { ok: true, verified: 0, broken: [], legacy: 0 };
|
|
85
|
+
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
86
|
+
|
|
87
|
+
// Validation starts at the last reset boundary; the prefix it seals is legacy
|
|
88
|
+
// and exempt, so concurrency damage predating the migration can't fail the chain.
|
|
89
|
+
|
|
90
|
+
let start = 0;
|
|
91
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
92
|
+
try { if (JSON.parse(lines[i]).chain_reset) start = i; } catch { /* legacy malformed */ }
|
|
93
|
+
}
|
|
94
|
+
|
|
40
95
|
const broken = [];
|
|
41
96
|
let prevHash = null;
|
|
42
|
-
for (let i =
|
|
97
|
+
for (let i = start; i < lines.length; i += 1) {
|
|
43
98
|
const line = lines[i];
|
|
44
99
|
let row;
|
|
45
100
|
try { row = JSON.parse(line); } catch {
|
|
46
101
|
broken.push({ line: i + 1, reason: 'malformed JSON' });
|
|
102
|
+
prevHash = sha256(line);
|
|
47
103
|
continue;
|
|
48
104
|
}
|
|
49
|
-
if (i >
|
|
105
|
+
if (i > start && !row.chain_reset && row.prev_line_hash !== prevHash) {
|
|
50
106
|
broken.push({
|
|
51
107
|
line: i + 1,
|
|
52
108
|
expected: prevHash,
|
|
@@ -56,7 +112,7 @@ export function verifyChain() {
|
|
|
56
112
|
}
|
|
57
113
|
prevHash = sha256(line);
|
|
58
114
|
}
|
|
59
|
-
return { ok: broken.length === 0, verified: lines.length, broken };
|
|
115
|
+
return { ok: broken.length === 0, verified: lines.length - start, broken, legacy: start };
|
|
60
116
|
}
|
|
61
117
|
|
|
62
118
|
function formatRow(row) {
|
|
@@ -74,6 +130,9 @@ export async function runAuditTrailCli(args = []) {
|
|
|
74
130
|
for (let i = 0; i < args.length; i += 1) {
|
|
75
131
|
const arg = args[i];
|
|
76
132
|
if (arg === '--verify') flags.add('verify');
|
|
133
|
+
else if (arg === '--reset') flags.add('reset');
|
|
134
|
+
else if (arg.startsWith('--reason=')) options.reason = arg.slice(9);
|
|
135
|
+
else if (arg === '--reason') options.reason = args[++i];
|
|
77
136
|
else if (arg === '--json') flags.add('json');
|
|
78
137
|
else if (arg === '--all') flags.add('all');
|
|
79
138
|
else if (arg.startsWith('--limit=')) options.limit = Number(arg.slice(8));
|
|
@@ -97,6 +156,8 @@ Options:
|
|
|
97
156
|
--agent <name> Filter to a specific agent (e.g. cx-engineer)
|
|
98
157
|
--tool <name> Filter to a specific tool (Edit, Write, MultiEdit, NotebookEdit, Bash)
|
|
99
158
|
--verify Verify the tamper-evidence chain and exit
|
|
159
|
+
--reset Seal the current chain as legacy and re-base a fresh chain
|
|
160
|
+
--reason <text> Reason recorded on the --reset boundary
|
|
100
161
|
--json Emit raw JSONL lines instead of formatted output
|
|
101
162
|
-h, --help Show this message
|
|
102
163
|
|
|
@@ -105,6 +166,12 @@ Log location: ${AUDIT_FILE}
|
|
|
105
166
|
return;
|
|
106
167
|
}
|
|
107
168
|
|
|
169
|
+
if (flags.has('reset')) {
|
|
170
|
+
writeChainReset({ reason: options.reason || 'manual reset' });
|
|
171
|
+
console.log(`Audit chain re-based at ${AUDIT_FILE}. Prior records sealed as legacy; verification resumes from the new boundary.`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
108
175
|
if (flags.has('verify')) {
|
|
109
176
|
const result = verifyChain();
|
|
110
177
|
if (result.ok) {
|
package/lib/auto-docs.mjs
CHANGED
|
@@ -90,20 +90,25 @@ function buildCoreDocsContract() {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
const DIR_DESCRIPTIONS = {
|
|
93
|
-
|
|
94
|
-
apps: 'User-facing apps shipped from this repo (e.g. apps/docs/, the Next.js docs site)',
|
|
93
|
+
apps: 'User-facing apps shipped from this repo (chat, dashboard, docs)',
|
|
95
94
|
bin: 'CLI entrypoint (`construct`)',
|
|
96
|
-
claude: 'Claude Code integration (agents, settings template)',
|
|
97
95
|
commands: 'Command prompt assets',
|
|
98
|
-
|
|
96
|
+
config: 'Repo-wide controlled vocabulary (tag-vocabulary.json)',
|
|
97
|
+
deploy: 'Terraform and deployment configs',
|
|
99
98
|
docs: 'Architecture notes, runbooks, and documentation contract',
|
|
100
|
-
|
|
101
|
-
lib: 'Core runtime: CLI, hooks, MCP,
|
|
102
|
-
|
|
99
|
+
examples: 'Example projects and persona fixtures',
|
|
100
|
+
lib: 'Core runtime: CLI, hooks, MCP, providers, oracle, sync',
|
|
101
|
+
packages: 'Shared workspace packages (e.g. cx-ui)',
|
|
103
102
|
personas: 'Persona prompt definitions',
|
|
103
|
+
platforms: 'Host adapter capability configs',
|
|
104
|
+
profiles: 'Profile catalog',
|
|
105
|
+
registry: 'Product capability registry',
|
|
104
106
|
rules: 'Coding and quality standards',
|
|
105
|
-
|
|
107
|
+
schemas: 'Registry and config JSON Schema',
|
|
108
|
+
scripts: 'Audit, alignment, release, and sync scripts',
|
|
106
109
|
skills: 'Reusable domain knowledge files',
|
|
110
|
+
specialists: 'Org registry, contracts, and specialist prompts',
|
|
111
|
+
templates: 'Doc and workflow templates',
|
|
107
112
|
tests: 'Test suite',
|
|
108
113
|
};
|
|
109
114
|
|
|
@@ -164,16 +169,16 @@ function buildAgentsTable(rootDir) {
|
|
|
164
169
|
let registry;
|
|
165
170
|
try { registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')); } catch { return ''; }
|
|
166
171
|
|
|
167
|
-
const agents =
|
|
168
|
-
|
|
172
|
+
const agents = [
|
|
173
|
+
...(registry.orchestrator ? [{ ...registry.orchestrator, name: registry.orchestrator.name }] : []),
|
|
174
|
+
...(registry.specialists ?? []),
|
|
175
|
+
];
|
|
176
|
+
if (!agents.length) return '';
|
|
169
177
|
|
|
170
178
|
const rows = ['| Specialist | Tier | Purpose |', '|---|---|---|'];
|
|
171
179
|
for (const agent of agents.slice(0, 30)) {
|
|
172
180
|
const name = agent.name ?? agent.id ?? 'n/a';
|
|
173
181
|
const tier = agent.modelTier ?? agent.tier ?? agent.model_tier ?? 'n/a';
|
|
174
|
-
// Strip em-dashes from descriptions so the prose lint stays clean across
|
|
175
|
-
// AUTO regen. Em-dashes are allowed in the registry's source descriptions
|
|
176
|
-
// but never in the rendered markdown.
|
|
177
182
|
const purpose = (agent.description ?? agent.purpose ?? '')
|
|
178
183
|
.split('\n')[0].slice(0, 80)
|
|
179
184
|
.replace(/ — /g, '. ').replace(/—/g, ',');
|
|
@@ -474,13 +479,77 @@ export function buildFumadocsReference({ rootDir } = {}) {
|
|
|
474
479
|
written.push(path.relative(rootDir, hooksPage));
|
|
475
480
|
}
|
|
476
481
|
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
479
|
-
const
|
|
480
|
-
if (writeIfChanged(
|
|
481
|
-
written.push(path.relative(rootDir,
|
|
482
|
+
const specialistsContent = renderAgentsPage(rootDir);
|
|
483
|
+
if (specialistsContent) {
|
|
484
|
+
const specialistsPage = path.join(refDir, 'specialists.md');
|
|
485
|
+
if (writeIfChanged(specialistsPage, specialistsContent)) {
|
|
486
|
+
written.push(path.relative(rootDir, specialistsPage));
|
|
487
|
+
}
|
|
488
|
+
const legacyAgentsPage = path.join(refDir, 'agents.md');
|
|
489
|
+
if (fs.existsSync(legacyAgentsPage)) {
|
|
490
|
+
fs.unlinkSync(legacyAgentsPage);
|
|
491
|
+
written.push(`- ${path.relative(rootDir, legacyAgentsPage)}`);
|
|
482
492
|
}
|
|
483
493
|
}
|
|
484
494
|
|
|
485
495
|
return { written };
|
|
486
496
|
}
|
|
497
|
+
|
|
498
|
+
export function checkFumadocsReferenceDrift({ rootDir } = {}) {
|
|
499
|
+
rootDir = rootDir ?? process.cwd();
|
|
500
|
+
const refDir = path.join(rootDir, 'docs', 'reference');
|
|
501
|
+
const cliDir = path.join(refDir, 'cli');
|
|
502
|
+
const drift = [];
|
|
503
|
+
|
|
504
|
+
for (const category of REFERENCE_CATEGORIES) {
|
|
505
|
+
const commands = CLI_COMMANDS_BY_CATEGORY[category] ?? [];
|
|
506
|
+
if (!commands.length) continue;
|
|
507
|
+
const slug = slugify(category);
|
|
508
|
+
const out = path.join(cliDir, `${slug}.md`);
|
|
509
|
+
const content = renderCommandPage(category, commands);
|
|
510
|
+
if (readFile(out) !== content) drift.push(path.relative(rootDir, out));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const cliPages = REFERENCE_CATEGORIES
|
|
514
|
+
.map((cat) => ({ cat, commands: CLI_COMMANDS_BY_CATEGORY[cat] ?? [] }))
|
|
515
|
+
.filter(({ commands }) => commands.length)
|
|
516
|
+
.map(({ cat }) => slugify(cat));
|
|
517
|
+
const cliMetaContent = JSON.stringify({ title: 'CLI', pages: ['index', ...cliPages] }, null, 2) + '\n';
|
|
518
|
+
const cliMeta = path.join(cliDir, 'meta.json');
|
|
519
|
+
if (readFile(cliMeta) !== cliMetaContent) drift.push(path.relative(rootDir, cliMeta));
|
|
520
|
+
|
|
521
|
+
const cliIndexContent = [
|
|
522
|
+
'---',
|
|
523
|
+
'title: CLI',
|
|
524
|
+
'description: Every construct command, grouped by category. Generated from lib/cli-commands.mjs.',
|
|
525
|
+
'---',
|
|
526
|
+
'',
|
|
527
|
+
'> Generated from `lib/cli-commands.mjs`. Re-run `construct docs:site` to refresh.',
|
|
528
|
+
'',
|
|
529
|
+
'Commands are grouped by what they do. Pick a category below.',
|
|
530
|
+
'',
|
|
531
|
+
...REFERENCE_CATEGORIES.map((cat) => {
|
|
532
|
+
const slug = slugify(cat);
|
|
533
|
+
const commands = CLI_COMMANDS_BY_CATEGORY[cat] ?? [];
|
|
534
|
+
if (!commands.length) return null;
|
|
535
|
+
return `- [${cat}](/reference/cli/${slug}) — ${commands.length} command${commands.length === 1 ? '' : 's'}`;
|
|
536
|
+
}).filter(Boolean),
|
|
537
|
+
'',
|
|
538
|
+
].join('\n');
|
|
539
|
+
const cliIndex = path.join(cliDir, 'index.md');
|
|
540
|
+
if (readFile(cliIndex) !== cliIndexContent) drift.push(path.relative(rootDir, cliIndex));
|
|
541
|
+
|
|
542
|
+
const hooksPage = path.join(refDir, 'hooks.md');
|
|
543
|
+
const hooksContent = renderHooksPage(rootDir);
|
|
544
|
+
if (hooksContent && readFile(hooksPage) !== hooksContent) drift.push(path.relative(rootDir, hooksPage));
|
|
545
|
+
|
|
546
|
+
const specialistsContent = renderAgentsPage(rootDir);
|
|
547
|
+
const specialistsPage = path.join(refDir, 'specialists.md');
|
|
548
|
+
if (specialistsContent && readFile(specialistsPage) !== specialistsContent) {
|
|
549
|
+
drift.push(path.relative(rootDir, specialistsPage));
|
|
550
|
+
}
|
|
551
|
+
const legacyAgentsPage = path.join(refDir, 'agents.md');
|
|
552
|
+
if (fs.existsSync(legacyAgentsPage)) drift.push(path.relative(rootDir, legacyAgentsPage));
|
|
553
|
+
|
|
554
|
+
return { drift };
|
|
555
|
+
}
|