@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,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/graph/build-import-graph.mjs — static import-graph derivation.
|
|
3
|
+
*
|
|
4
|
+
* Walks lib/, bin/, scripts/, and tests/ for ESM/CJS source, parses each
|
|
5
|
+
* file's relative import/export-from/dynamic-import/require specifiers, and
|
|
6
|
+
* resolves them to repo-relative paths. Produces:
|
|
7
|
+
* file|test --imports--> file|test (directed module dependency)
|
|
8
|
+
* file --realizes--> capability (derived: a capability's declared
|
|
9
|
+
* verification tests transitively reach
|
|
10
|
+
* the implementation files that realize it)
|
|
11
|
+
* Test files (*.test.mjs) are typed `test` so they merge with the registry's
|
|
12
|
+
* declared verification-test nodes. Bare and node: specifiers are ignored.
|
|
13
|
+
* Pure-JS, no AST library, per ADR-0001 — a tolerant regex over import forms
|
|
14
|
+
* is sufficient for this codebase's conventions.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
const SOURCE_ROOTS = ['lib', 'bin', 'scripts', 'tests'];
|
|
21
|
+
const SOURCE_EXT = new Set(['.mjs', '.js', '.cjs']);
|
|
22
|
+
const SKIP_DIRS = new Set(['node_modules', 'fixtures', '.git', '.cx']);
|
|
23
|
+
const RESOLVE_ORDER = ['', '.mjs', '.js', '.cjs', '.json', '/index.mjs', '/index.js'];
|
|
24
|
+
|
|
25
|
+
const IMPORT_RE = /(?:import|export)\s+(?:[^'"]*?\sfrom\s+)?['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)|require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
26
|
+
|
|
27
|
+
function isTestPath(rel) {
|
|
28
|
+
return rel.endsWith('.test.mjs') || rel.endsWith('.test.js');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function nodeFor(rel) {
|
|
32
|
+
return isTestPath(rel) ? { type: 'test', id: `test:${rel}` } : { type: 'file', id: `file:${rel}` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function walk(rootDir, dirRel, acc) {
|
|
36
|
+
const abs = path.join(rootDir, dirRel);
|
|
37
|
+
let entries;
|
|
38
|
+
try { entries = readdirSync(abs, { withFileTypes: true }); } catch { return; }
|
|
39
|
+
for (const e of entries) {
|
|
40
|
+
if (e.name.startsWith('.') && e.name !== '.') { if (SKIP_DIRS.has(e.name)) continue; }
|
|
41
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
42
|
+
const rel = dirRel ? path.join(dirRel, e.name) : e.name;
|
|
43
|
+
if (e.isDirectory()) { walk(rootDir, rel, acc); continue; }
|
|
44
|
+
if (!e.isFile()) continue;
|
|
45
|
+
const ext = path.extname(e.name);
|
|
46
|
+
if (SOURCE_EXT.has(ext) || (dirRel === 'bin' && ext === '')) acc.push(rel);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function extractSpecifiers(content) {
|
|
51
|
+
const specs = [];
|
|
52
|
+
let m;
|
|
53
|
+
IMPORT_RE.lastIndex = 0;
|
|
54
|
+
while ((m = IMPORT_RE.exec(content)) !== null) {
|
|
55
|
+
const spec = m[1] || m[2] || m[3];
|
|
56
|
+
if (spec) specs.push(spec);
|
|
57
|
+
}
|
|
58
|
+
return specs;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveSpecifier(rootDir, importerRel, spec) {
|
|
62
|
+
if (!spec.startsWith('.') && !spec.startsWith('/')) return null;
|
|
63
|
+
const baseAbs = path.resolve(path.dirname(path.join(rootDir, importerRel)), spec);
|
|
64
|
+
for (const suffix of RESOLVE_ORDER) {
|
|
65
|
+
const cand = baseAbs + suffix;
|
|
66
|
+
try {
|
|
67
|
+
if (existsSync(cand) && statSync(cand).isFile()) return path.relative(rootDir, cand).split(path.sep).join('/');
|
|
68
|
+
} catch { /* keep trying */ }
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildForwardAdj(edges) {
|
|
74
|
+
const adj = new Map();
|
|
75
|
+
for (const e of edges) {
|
|
76
|
+
if (e.rel !== 'imports') continue;
|
|
77
|
+
if (!adj.has(e.from)) adj.set(e.from, []);
|
|
78
|
+
adj.get(e.from).push(e.to);
|
|
79
|
+
}
|
|
80
|
+
return adj;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function closure(adj, startId) {
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const stack = [startId];
|
|
86
|
+
while (stack.length) {
|
|
87
|
+
const id = stack.pop();
|
|
88
|
+
for (const to of adj.get(id) || []) {
|
|
89
|
+
if (seen.has(to)) continue;
|
|
90
|
+
seen.add(to);
|
|
91
|
+
stack.push(to);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return seen;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {object} opts
|
|
99
|
+
* @param {string} opts.rootDir
|
|
100
|
+
* @param {object[]} [opts.validates] — validates edges (test→capability) from the
|
|
101
|
+
* registry build; these derive realizes (file→capability) via test closures.
|
|
102
|
+
* @returns {{ nodes: object[], edges: object[] }}
|
|
103
|
+
*/
|
|
104
|
+
export function buildImportGraph({ rootDir, validates = [] }) {
|
|
105
|
+
const files = [];
|
|
106
|
+
for (const root of SOURCE_ROOTS) walk(rootDir, root, files);
|
|
107
|
+
files.sort();
|
|
108
|
+
|
|
109
|
+
const nodes = [];
|
|
110
|
+
const edges = [];
|
|
111
|
+
const known = new Set(files);
|
|
112
|
+
|
|
113
|
+
for (const rel of files) {
|
|
114
|
+
const self = nodeFor(rel);
|
|
115
|
+
nodes.push({ id: self.id, type: self.type, name: rel, attrs: { path: rel } });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const rel of files) {
|
|
119
|
+
let content;
|
|
120
|
+
try { content = readFileSync(path.join(rootDir, rel), 'utf8'); } catch { continue; }
|
|
121
|
+
const fromNode = nodeFor(rel);
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
for (const spec of extractSpecifiers(content)) {
|
|
124
|
+
const target = resolveSpecifier(rootDir, rel, spec);
|
|
125
|
+
if (!target || target === rel || seen.has(target)) continue;
|
|
126
|
+
seen.add(target);
|
|
127
|
+
const toNode = nodeFor(target);
|
|
128
|
+
if (!known.has(target)) nodes.push({ id: toNode.id, type: toNode.type, name: target, attrs: { path: target } });
|
|
129
|
+
edges.push({ from: fromNode.id, to: toNode.id, rel: 'imports', source: 'import-graph' });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Derive realizes: a capability's declared tests transitively reach the
|
|
134
|
+
// implementation files that realize it. Intentionally over-inclusive — a
|
|
135
|
+
// broad edge only widens impact, which is the safe direction for advisory use.
|
|
136
|
+
|
|
137
|
+
const adj = buildForwardAdj(edges);
|
|
138
|
+
const realizesSeen = new Set();
|
|
139
|
+
for (const v of validates) {
|
|
140
|
+
if (v.rel !== 'validates') continue;
|
|
141
|
+
const testId = v.from;
|
|
142
|
+
const capId = v.to;
|
|
143
|
+
for (const reached of closure(adj, testId)) {
|
|
144
|
+
if (!reached.startsWith('file:')) continue;
|
|
145
|
+
const key = `${reached}->${capId}`;
|
|
146
|
+
if (realizesSeen.has(key)) continue;
|
|
147
|
+
realizesSeen.add(key);
|
|
148
|
+
edges.push({ from: reached, to: capId, rel: 'realizes', source: 'import-graph' });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { nodes, edges };
|
|
153
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/graph/cli.mjs — `construct matrix` command surface.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* build Regenerate the dependency graph from the registry seeds
|
|
6
|
+
* (and, once available, the static import graph) into
|
|
7
|
+
* <projectDir>/.cx/graph/.
|
|
8
|
+
* stat Print node/edge counts by type and relation.
|
|
9
|
+
* query <id> Print a node's dependencies and dependents.
|
|
10
|
+
*
|
|
11
|
+
* Seeds are read from the Construct package root (rootDir); the graph is
|
|
12
|
+
* persisted under the active project (projectDir) so it travels with .cx/.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { buildFromRegistry } from './build-from-registry.mjs';
|
|
16
|
+
import { buildImportGraph } from './build-import-graph.mjs';
|
|
17
|
+
import { buildCoChange } from './build-co-change.mjs';
|
|
18
|
+
import { writeGraph, loadGraph, dependenciesOf, dependentsOf } from './store.mjs';
|
|
19
|
+
|
|
20
|
+
function isoNow() {
|
|
21
|
+
return new Date().toISOString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function runBuild({ rootDir, projectDir, json, coChange }) {
|
|
25
|
+
const reg = buildFromRegistry({ rootDir });
|
|
26
|
+
const validates = reg.edges.filter((e) => e.rel === 'validates');
|
|
27
|
+
const imp = buildImportGraph({ rootDir, validates });
|
|
28
|
+
const nodes = [...reg.nodes, ...imp.nodes];
|
|
29
|
+
const edges = [...reg.edges, ...imp.edges];
|
|
30
|
+
|
|
31
|
+
if (coChange) {
|
|
32
|
+
const sourceRels = imp.nodes.filter((n) => n.type === 'file' || n.type === 'test').map((n) => n.name);
|
|
33
|
+
const co = buildCoChange({ rootDir, sourceRels });
|
|
34
|
+
nodes.push(...co.nodes);
|
|
35
|
+
edges.push(...co.edges);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const sourceHash = reg.sourceHash;
|
|
39
|
+
const result = writeGraph(projectDir, { nodes, edges, generatedAt: isoNow(), sourceHash });
|
|
40
|
+
const meta = loadGraph(projectDir).meta;
|
|
41
|
+
if (json) {
|
|
42
|
+
process.stdout.write(JSON.stringify({ ok: true, ...result, meta }, null, 2) + '\n');
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
process.stdout.write(`✓ graph built: ${result.nodeCount} nodes, ${result.edgeCount} edges → ${result.dir}\n`);
|
|
46
|
+
process.stdout.write(` nodes: ${JSON.stringify(meta.nodesByType)}\n`);
|
|
47
|
+
process.stdout.write(` edges: ${JSON.stringify(meta.edgesByRel)}\n`);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runStat({ projectDir, json }) {
|
|
52
|
+
const graph = loadGraph(projectDir);
|
|
53
|
+
if (!graph.exists) {
|
|
54
|
+
process.stderr.write('No graph found. Run `construct matrix build` first.\n');
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
if (json) {
|
|
58
|
+
process.stdout.write(JSON.stringify(graph.meta, null, 2) + '\n');
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
process.stdout.write(`graph: ${graph.nodes.size} nodes, ${graph.edges.length} edges (generated ${graph.meta?.generatedAt ?? 'unknown'})\n`);
|
|
62
|
+
process.stdout.write(` nodes: ${JSON.stringify(graph.meta?.nodesByType ?? {})}\n`);
|
|
63
|
+
process.stdout.write(` edges: ${JSON.stringify(graph.meta?.edgesByRel ?? {})}\n`);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runQuery({ projectDir, id, json }) {
|
|
68
|
+
const graph = loadGraph(projectDir);
|
|
69
|
+
if (!graph.exists) {
|
|
70
|
+
process.stderr.write('No graph found. Run `construct matrix build` first.\n');
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
const node = graph.nodes.get(id);
|
|
74
|
+
const deps = dependenciesOf(graph, id);
|
|
75
|
+
const dependents = dependentsOf(graph, id);
|
|
76
|
+
if (json) {
|
|
77
|
+
process.stdout.write(JSON.stringify({ id, found: !!node, node: node ?? null, dependencies: deps, dependents }, null, 2) + '\n');
|
|
78
|
+
return node ? 0 : 1;
|
|
79
|
+
}
|
|
80
|
+
if (!node) {
|
|
81
|
+
process.stderr.write(`node not found: ${id}\n`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
process.stdout.write(`${id} (${node.type})\n`);
|
|
85
|
+
process.stdout.write(` → dependencies (${deps.length}): ${deps.join(', ') || '(none)'}\n`);
|
|
86
|
+
process.stdout.write(` ← dependents (${dependents.length}): ${dependents.join(', ') || '(none)'}\n`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string[]} args
|
|
92
|
+
* @param {{ rootDir: string, projectDir: string }} ctx
|
|
93
|
+
* @returns {number} exit code
|
|
94
|
+
*/
|
|
95
|
+
export function runGraphCli(args, { rootDir, projectDir }) {
|
|
96
|
+
const sub = args[0] || 'stat';
|
|
97
|
+
const json = args.includes('--json');
|
|
98
|
+
if (sub === 'build') return runBuild({ rootDir, projectDir, json, coChange: !args.includes('--no-co-change') });
|
|
99
|
+
if (sub === 'stat') return runStat({ projectDir, json });
|
|
100
|
+
if (sub === 'query') {
|
|
101
|
+
const id = args.slice(1).find((a) => !a.startsWith('--'));
|
|
102
|
+
if (!id) {
|
|
103
|
+
process.stderr.write('Usage: construct matrix query <node-id> [--json]\n');
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
return runQuery({ projectDir, id, json });
|
|
107
|
+
}
|
|
108
|
+
process.stderr.write(`Unknown graph subcommand: ${sub}. Available: build, stat, query\n`);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/graph/impact-cli.mjs — `construct impact` command surface.
|
|
3
|
+
*
|
|
4
|
+
* Maps changed files to the tests that should run and the capabilities and
|
|
5
|
+
* workflows they touch, using the living dependency graph. Advisory by default;
|
|
6
|
+
* `--run` executes the selected tests via `node --test`. Files come from
|
|
7
|
+
* positional args, or — when none are given — from `git diff --name-only HEAD`.
|
|
8
|
+
*
|
|
9
|
+
* construct impact lib/oracle/synthesize.mjs
|
|
10
|
+
* construct impact --run
|
|
11
|
+
* git diff --name-only origin/main | construct impact --stdin --json
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
import { computeImpact } from './impact.mjs';
|
|
19
|
+
|
|
20
|
+
function readStdin() {
|
|
21
|
+
try { return readFileSync(0, 'utf8'); } catch { return ''; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function gitChangedFiles(projectDir) {
|
|
25
|
+
const res = spawnSync('git', ['diff', '--name-only', 'HEAD'], { cwd: projectDir, encoding: 'utf8' });
|
|
26
|
+
if (res.status !== 0) return [];
|
|
27
|
+
return res.stdout.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string[]} args
|
|
32
|
+
* @param {{ rootDir: string, projectDir: string }} ctx
|
|
33
|
+
* @returns {Promise<number>} exit code
|
|
34
|
+
*/
|
|
35
|
+
export async function runImpactCli(args, { projectDir }) {
|
|
36
|
+
const json = args.includes('--json');
|
|
37
|
+
const run = args.includes('--run');
|
|
38
|
+
const useStdin = args.includes('--stdin');
|
|
39
|
+
let files = args.filter((a) => !a.startsWith('--'));
|
|
40
|
+
|
|
41
|
+
if (useStdin) files = readStdin().split('\n').map((l) => l.trim()).filter(Boolean);
|
|
42
|
+
if (files.length === 0) files = gitChangedFiles(projectDir);
|
|
43
|
+
|
|
44
|
+
if (files.length === 0) {
|
|
45
|
+
process.stderr.write('No changed files given (args, --stdin, or git diff). Nothing to analyze.\n');
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = computeImpact({ rootDir: projectDir, changedFiles: files });
|
|
50
|
+
|
|
51
|
+
if (!result.graphPresent) {
|
|
52
|
+
process.stderr.write('No dependency graph found. Run `construct matrix build` first.\n');
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (json && !run) {
|
|
57
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!json) {
|
|
62
|
+
process.stdout.write(`Changed: ${result.changed.length} file(s)\n`);
|
|
63
|
+
process.stdout.write(`Impacted capabilities (${result.impactedCapabilities.length}): ${result.impactedCapabilities.join(', ') || '(none)'}\n`);
|
|
64
|
+
process.stdout.write(`Stale capabilities (${result.staleCapabilities.length}): ${result.staleCapabilities.join(', ') || '(none)'}\n`);
|
|
65
|
+
process.stdout.write(`Impacted workflows (${result.impactedWorkflows.length}): ${result.impactedWorkflows.join(', ') || '(none)'}\n`);
|
|
66
|
+
process.stdout.write(`Affected tests (${result.affectedTests.length}):\n`);
|
|
67
|
+
for (const t of result.affectedTests) process.stdout.write(` ${t}\n`);
|
|
68
|
+
if (result.coverageGaps.length) {
|
|
69
|
+
process.stdout.write(`\n⚠ Coverage gap — changed files realizing no capability (${result.coverageGaps.length}):\n`);
|
|
70
|
+
for (const f of result.coverageGaps) process.stdout.write(` ${f}\n`);
|
|
71
|
+
}
|
|
72
|
+
if (result.unknown.length) {
|
|
73
|
+
process.stdout.write(`\nNot in graph (${result.unknown.length}): ${result.unknown.join(', ')}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (run) {
|
|
78
|
+
const runnable = result.affectedTests.filter((t) => existsSync(path.join(projectDir, t)));
|
|
79
|
+
if (runnable.length === 0) {
|
|
80
|
+
process.stdout.write('\nNo runnable affected tests on disk.\n');
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
process.stdout.write(`\n▶ Running ${runnable.length} affected test(s)…\n`);
|
|
84
|
+
const res = spawnSync(process.execPath, ['--test', ...runnable], { cwd: projectDir, stdio: 'inherit' });
|
|
85
|
+
return res.status ?? 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/graph/impact.mjs — forward change-impact over the dependency graph.
|
|
3
|
+
*
|
|
4
|
+
* Given a set of changed files, computes the tests that should run, the
|
|
5
|
+
* capabilities and workflows impacted, and any changed file that realizes no
|
|
6
|
+
* capability (a coverage gap). Test selection is Test Impact Analysis over the
|
|
7
|
+
* static import graph: a test is affected if it transitively imports a changed
|
|
8
|
+
* file, or if it validates a capability that a changed file realizes. The
|
|
9
|
+
* union is intentionally conservative — over-selection is safe, under-selection
|
|
10
|
+
* silently ships untested change.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { loadGraph, dependentsOf, dependenciesOf } from './store.mjs';
|
|
14
|
+
import { rollupStaleImpact } from '../certification/stale-impact.mjs';
|
|
15
|
+
|
|
16
|
+
function isTestRel(rel) {
|
|
17
|
+
return rel.endsWith('.test.mjs') || rel.endsWith('.test.js');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toNodeId(rel) {
|
|
21
|
+
return isTestRel(rel) ? `test:${rel}` : `file:${rel}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function reverseImportClosure(graph, startId) {
|
|
25
|
+
const reached = new Set();
|
|
26
|
+
const stack = [startId];
|
|
27
|
+
while (stack.length) {
|
|
28
|
+
const id = stack.pop();
|
|
29
|
+
for (const importer of dependentsOf(graph, id, 'imports')) {
|
|
30
|
+
if (reached.has(importer)) continue;
|
|
31
|
+
reached.add(importer);
|
|
32
|
+
stack.push(importer);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return reached;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {object} opts
|
|
40
|
+
* @param {string} opts.rootDir — project root holding .cx/graph/.
|
|
41
|
+
* @param {string[]} opts.changedFiles — repo-relative paths.
|
|
42
|
+
* @returns {{ changed: string[], unknown: string[], affectedTests: string[], impactedCapabilities: string[], impactedWorkflows: string[], staleCapabilities: string[], coverageGaps: string[], graphPresent: boolean }}
|
|
43
|
+
*/
|
|
44
|
+
export function computeImpact({ rootDir, changedFiles }) {
|
|
45
|
+
const graph = loadGraph(rootDir);
|
|
46
|
+
const norm = [...new Set((changedFiles || []).map((f) => f.split('\\').join('/').replace(/^\.\//, '')))].filter(Boolean);
|
|
47
|
+
if (!graph.exists) {
|
|
48
|
+
return {
|
|
49
|
+
changed: norm,
|
|
50
|
+
unknown: norm,
|
|
51
|
+
affectedTests: [],
|
|
52
|
+
impactedCapabilities: [],
|
|
53
|
+
impactedWorkflows: [],
|
|
54
|
+
staleCapabilities: rollupStaleImpact({ rootDir, changedFiles: norm }),
|
|
55
|
+
coverageGaps: [],
|
|
56
|
+
graphPresent: false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const tests = new Set();
|
|
61
|
+
const capabilities = new Set();
|
|
62
|
+
const unknown = [];
|
|
63
|
+
const coverageGaps = [];
|
|
64
|
+
|
|
65
|
+
for (const rel of norm) {
|
|
66
|
+
const id = toNodeId(rel);
|
|
67
|
+
if (!graph.nodes.has(id)) { unknown.push(rel); continue; }
|
|
68
|
+
|
|
69
|
+
if (id.startsWith('test:')) tests.add(id);
|
|
70
|
+
|
|
71
|
+
for (const importer of reverseImportClosure(graph, id)) {
|
|
72
|
+
if (importer.startsWith('test:')) tests.add(importer);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const caps = dependenciesOf(graph, id, 'realizes');
|
|
76
|
+
for (const c of caps) capabilities.add(c);
|
|
77
|
+
if (id.startsWith('file:') && caps.length === 0) coverageGaps.push(rel);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Capabilities reached transitively also pull in their declared verification
|
|
81
|
+
// tests; their embedded workflows complete the requirement trace.
|
|
82
|
+
|
|
83
|
+
for (const cap of [...capabilities]) {
|
|
84
|
+
for (const t of dependentsOf(graph, cap, 'validates')) tests.add(t);
|
|
85
|
+
}
|
|
86
|
+
for (const t of tests) {
|
|
87
|
+
for (const cap of dependenciesOf(graph, t, 'validates')) capabilities.add(cap);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const workflows = new Set();
|
|
91
|
+
for (const cap of capabilities) {
|
|
92
|
+
for (const wf of dependenciesOf(graph, cap, 'embeds')) workflows.add(wf);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const strip = (set, prefix) => [...set].map((id) => id.slice(prefix.length)).sort();
|
|
96
|
+
const impactedCapabilityIds = strip(capabilities, 'capability:');
|
|
97
|
+
const staleCapabilities = rollupStaleImpact({ rootDir, changedFiles: norm });
|
|
98
|
+
return {
|
|
99
|
+
changed: norm,
|
|
100
|
+
unknown,
|
|
101
|
+
affectedTests: [...tests].map((id) => id.slice('test:'.length)).sort(),
|
|
102
|
+
impactedCapabilities: impactedCapabilityIds,
|
|
103
|
+
impactedWorkflows: strip(workflows, 'workflow:'),
|
|
104
|
+
staleCapabilities,
|
|
105
|
+
coverageGaps: coverageGaps.sort(),
|
|
106
|
+
graphPresent: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/graph/staleness.mjs — dependency graph seed-hash staleness checks.
|
|
3
|
+
*
|
|
4
|
+
* Compares the hash of registry/contracts/workflow seed files against the
|
|
5
|
+
* hash stored at last graph build (.cx/graph/meta.json). Shared by Oracle,
|
|
6
|
+
* the doctor graph-staleness watcher, and PostToolUse advisories.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { hashFiles } from './build-from-registry.mjs';
|
|
10
|
+
import { loadGraph } from './store.mjs';
|
|
11
|
+
|
|
12
|
+
export const GRAPH_SEED_FILES = [
|
|
13
|
+
'registry/capabilities.json',
|
|
14
|
+
'specialists/contracts.json',
|
|
15
|
+
'lib/embedded-contract/workflow-defs.mjs',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} rootDir — project root holding .cx/graph/.
|
|
20
|
+
* @returns {{ present: boolean, stale: boolean, staleReason: string|null, currentHash?: string, storedHash?: string|null }}
|
|
21
|
+
*/
|
|
22
|
+
export function checkGraphStaleness(rootDir) {
|
|
23
|
+
const graph = loadGraph(rootDir);
|
|
24
|
+
if (!graph.exists) {
|
|
25
|
+
return { present: false, stale: false, staleReason: null };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const current = hashFiles(rootDir, GRAPH_SEED_FILES);
|
|
30
|
+
const stored = graph.meta?.sourceHash ?? null;
|
|
31
|
+
if (stored && current !== stored) {
|
|
32
|
+
return {
|
|
33
|
+
present: true,
|
|
34
|
+
stale: true,
|
|
35
|
+
staleReason: 'registry/contracts/workflow seeds changed since last build',
|
|
36
|
+
currentHash: current,
|
|
37
|
+
storedHash: stored,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { present: true, stale: false, staleReason: null, currentHash: current, storedHash: stored };
|
|
41
|
+
} catch {
|
|
42
|
+
return { present: true, stale: false, staleReason: null };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/graph/store.mjs — typed, directed dependency-graph store.
|
|
3
|
+
*
|
|
4
|
+
* The substrate for Construct's living dependency matrix. Nodes are typed
|
|
5
|
+
* entities (file, module, workflow, capability, test, contract, surface,
|
|
6
|
+
* skill, rule) addressed by canonical ids of the form `type:key`. Edges are
|
|
7
|
+
* directed and carry a relation kind (imports, realizes, validates, covers,
|
|
8
|
+
* exposes, governed_by, uses, embeds, co_changes) and a provenance source
|
|
9
|
+
* (registry, import-graph, co-change, override).
|
|
10
|
+
*
|
|
11
|
+
* Persisted under `.cx/graph/` as deterministic JSONL so a graph round-trips
|
|
12
|
+
* losslessly and diffs cleanly: nodes.jsonl (sorted by id), edges.jsonl
|
|
13
|
+
* (sorted by from,rel,to), meta.json (generatedAt, sourceHash, counts).
|
|
14
|
+
* Dependency-free and pure-JS per ADR-0001.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
export const NODE_TYPES = new Set([
|
|
21
|
+
'file', 'module', 'workflow', 'capability', 'test', 'contract', 'surface', 'skill', 'rule',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export const EDGE_RELS = new Set([
|
|
25
|
+
'imports', 'realizes', 'validates', 'covers', 'exposes', 'governed_by', 'uses', 'embeds', 'co_changes', 'contains',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export const EDGE_SOURCES = new Set(['registry', 'import-graph', 'co-change', 'override']);
|
|
29
|
+
|
|
30
|
+
const STORE_SUBDIR = path.join('.cx', 'graph');
|
|
31
|
+
|
|
32
|
+
export function graphDir(rootDir) {
|
|
33
|
+
return path.join(rootDir, STORE_SUBDIR);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function nodeId(type, key) {
|
|
37
|
+
return `${type}:${key}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nodesPath(rootDir) { return path.join(graphDir(rootDir), 'nodes.jsonl'); }
|
|
41
|
+
function edgesPath(rootDir) { return path.join(graphDir(rootDir), 'edges.jsonl'); }
|
|
42
|
+
function metaPath(rootDir) { return path.join(graphDir(rootDir), 'meta.json'); }
|
|
43
|
+
|
|
44
|
+
function readJsonl(filePath) {
|
|
45
|
+
if (!existsSync(filePath)) return [];
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const line of readFileSync(filePath, 'utf8').split('\n')) {
|
|
48
|
+
if (!line.trim()) continue;
|
|
49
|
+
try { out.push(JSON.parse(line)); } catch { /* skip malformed line */ }
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeAtomic(filePath, contents) {
|
|
55
|
+
const tmp = `${filePath}.tmp`;
|
|
56
|
+
writeFileSync(tmp, contents, 'utf8');
|
|
57
|
+
renameSync(tmp, filePath);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Canonical de-dup: nodes keyed by id (last write wins on attrs), edges keyed
|
|
61
|
+
// by from|rel|to (weights summed so repeated co-change observations accumulate).
|
|
62
|
+
|
|
63
|
+
function normalizeNodes(nodes) {
|
|
64
|
+
const byId = new Map();
|
|
65
|
+
for (const n of nodes) {
|
|
66
|
+
if (!n?.id || !n?.type) continue;
|
|
67
|
+
const prev = byId.get(n.id);
|
|
68
|
+
byId.set(n.id, prev ? { ...prev, ...n, attrs: { ...(prev.attrs || {}), ...(n.attrs || {}) } } : { id: n.id, type: n.type, name: n.name ?? n.id, attrs: n.attrs || {} });
|
|
69
|
+
}
|
|
70
|
+
return [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function edgeKey(e) { return `${e.from}|${e.rel}|${e.to}`; }
|
|
74
|
+
|
|
75
|
+
function normalizeEdges(edges) {
|
|
76
|
+
const byKey = new Map();
|
|
77
|
+
for (const e of edges) {
|
|
78
|
+
if (!e?.from || !e?.to || !e?.rel) continue;
|
|
79
|
+
const key = edgeKey(e);
|
|
80
|
+
const prev = byKey.get(key);
|
|
81
|
+
if (prev) {
|
|
82
|
+
prev.weight = (prev.weight || 1) + (e.weight || 1);
|
|
83
|
+
if (e.source && !prev.sources.includes(e.source)) prev.sources.push(e.source);
|
|
84
|
+
} else {
|
|
85
|
+
byKey.set(key, { from: e.from, to: e.to, rel: e.rel, weight: e.weight || 1, sources: e.source ? [e.source] : (e.sources || []) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [...byKey.values()].sort((a, b) => (edgeKey(a) < edgeKey(b) ? -1 : 1));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function countBy(items, key) {
|
|
92
|
+
const out = {};
|
|
93
|
+
for (const it of items) { const k = it[key]; out[k] = (out[k] || 0) + 1; }
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Persist a full graph (regenerate semantics). Nodes and edges are normalized
|
|
99
|
+
* and sorted; meta records counts and the caller-supplied source hash.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} rootDir
|
|
102
|
+
* @param {{ nodes: object[], edges: object[], generatedAt?: string, sourceHash?: string }} graph
|
|
103
|
+
* @returns {{ nodeCount: number, edgeCount: number, dir: string }}
|
|
104
|
+
*/
|
|
105
|
+
export function writeGraph(rootDir, { nodes = [], edges = [], generatedAt = null, sourceHash = null } = {}) {
|
|
106
|
+
const dir = graphDir(rootDir);
|
|
107
|
+
mkdirSync(dir, { recursive: true });
|
|
108
|
+
const normNodes = normalizeNodes(nodes);
|
|
109
|
+
const normEdges = normalizeEdges(edges);
|
|
110
|
+
writeAtomic(nodesPath(rootDir), normNodes.map((n) => JSON.stringify(n)).join('\n') + (normNodes.length ? '\n' : ''));
|
|
111
|
+
writeAtomic(edgesPath(rootDir), normEdges.map((e) => JSON.stringify(e)).join('\n') + (normEdges.length ? '\n' : ''));
|
|
112
|
+
const meta = {
|
|
113
|
+
generatedAt,
|
|
114
|
+
sourceHash,
|
|
115
|
+
nodeCount: normNodes.length,
|
|
116
|
+
edgeCount: normEdges.length,
|
|
117
|
+
nodesByType: countBy(normNodes, 'type'),
|
|
118
|
+
edgesByRel: countBy(normEdges, 'rel'),
|
|
119
|
+
};
|
|
120
|
+
writeAtomic(metaPath(rootDir), JSON.stringify(meta, null, 2) + '\n');
|
|
121
|
+
return { nodeCount: normNodes.length, edgeCount: normEdges.length, dir };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Load the graph into an indexed, queryable view. Out/in adjacency maps make
|
|
126
|
+
* forward (dependencies) and reverse (dependents) traversal O(degree).
|
|
127
|
+
*
|
|
128
|
+
* @param {string} rootDir
|
|
129
|
+
* @returns {{ nodes: Map<string,object>, edges: object[], out: Map<string,object[]>, in: Map<string,object[]>, meta: object|null, exists: boolean }}
|
|
130
|
+
*/
|
|
131
|
+
export function loadGraph(rootDir) {
|
|
132
|
+
const nodeList = readJsonl(nodesPath(rootDir));
|
|
133
|
+
const edgeList = readJsonl(edgesPath(rootDir));
|
|
134
|
+
let meta = null;
|
|
135
|
+
try { meta = JSON.parse(readFileSync(metaPath(rootDir), 'utf8')); } catch { /* no meta */ }
|
|
136
|
+
|
|
137
|
+
const nodes = new Map();
|
|
138
|
+
for (const n of nodeList) nodes.set(n.id, n);
|
|
139
|
+
const out = new Map();
|
|
140
|
+
const inc = new Map();
|
|
141
|
+
for (const e of edgeList) {
|
|
142
|
+
if (!out.has(e.from)) out.set(e.from, []);
|
|
143
|
+
if (!inc.has(e.to)) inc.set(e.to, []);
|
|
144
|
+
out.get(e.from).push(e);
|
|
145
|
+
inc.get(e.to).push(e);
|
|
146
|
+
}
|
|
147
|
+
return { nodes, edges: edgeList, out, in: inc, meta, exists: existsSync(nodesPath(rootDir)) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function filterRel(edges, rel) {
|
|
151
|
+
if (!rel) return edges;
|
|
152
|
+
const rels = Array.isArray(rel) ? new Set(rel) : new Set([rel]);
|
|
153
|
+
return edges.filter((e) => rels.has(e.rel));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Nodes that `id` points to along `rel` (its dependencies).
|
|
158
|
+
*/
|
|
159
|
+
export function dependenciesOf(graph, id, rel = null) {
|
|
160
|
+
return filterRel(graph.out.get(id) || [], rel).map((e) => e.to);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Nodes that point to `id` along `rel` (its dependents).
|
|
165
|
+
*/
|
|
166
|
+
export function dependentsOf(graph, id, rel = null) {
|
|
167
|
+
return filterRel(graph.in.get(id) || [], rel).map((e) => e.from);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function nodesByType(graph, type) {
|
|
171
|
+
return [...graph.nodes.values()].filter((n) => n.type === type);
|
|
172
|
+
}
|