@intentius/chant 0.102.0 → 0.104.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/dist/audit/core.d.ts +2 -0
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/change-set.d.ts +232 -0
- package/dist/change-set.d.ts.map +1 -0
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/change-set.d.ts +11 -0
- package/dist/cli/handlers/change-set.d.ts.map +1 -0
- package/dist/cli/handlers/fan-out.d.ts +33 -0
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +7 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/pr.d.ts +31 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +3 -2
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +63 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +16 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +53 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/changed-files.d.ts +31 -0
- package/dist/components/changed-files.d.ts.map +1 -0
- package/dist/components/cli-support.d.ts +9 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +24 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/deploy-units.d.ts.map +1 -1
- package/dist/components/driver.d.ts +13 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/fan-out-output.d.ts +6 -0
- package/dist/components/fan-out-output.d.ts.map +1 -1
- package/dist/components/fan-out-record.d.ts +41 -0
- package/dist/components/fan-out-record.d.ts.map +1 -0
- package/dist/components/fan-out-run.d.ts +65 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-support.d.ts +2 -0
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/fan-out.d.ts +6 -0
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/gated-wave-pipeline.d.ts +50 -0
- package/dist/components/gated-wave-pipeline.d.ts.map +1 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layers.d.ts +25 -0
- package/dist/components/layers.d.ts.map +1 -0
- package/dist/components/pr-pipeline.d.ts +61 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -0
- package/dist/components/pr-run.d.ts +119 -0
- package/dist/components/pr-run.d.ts.map +1 -0
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/verbs/shell.d.ts.map +1 -1
- package/dist/components/wave-plan.d.ts +49 -0
- package/dist/components/wave-plan.d.ts.map +1 -0
- package/dist/gated-waves.d.ts +147 -0
- package/dist/gated-waves.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +48 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +7 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +46 -1
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lint/post-synth.d.ts +18 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.d.ts +126 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/change-set.d.ts +62 -0
- package/dist/op/activities/change-set.d.ts.map +1 -0
- package/dist/op/activities/decide.d.ts +2 -0
- package/dist/op/activities/decide.d.ts.map +1 -1
- package/dist/op/activities/factory.d.ts +137 -0
- package/dist/op/activities/factory.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +4 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/builders.d.ts +11 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts +5 -1
- package/dist/op/decide-backend.d.ts.map +1 -1
- package/dist/op/decide-read-inputs.d.ts +4 -2
- package/dist/op/decide-read-inputs.d.ts.map +1 -1
- package/dist/op/factory.d.ts +83 -0
- package/dist/op/factory.d.ts.map +1 -0
- package/dist/op/index.d.ts +4 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -0
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/types.d.ts +8 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/plan-summary.d.ts +207 -0
- package/dist/plan-summary.d.ts.map +1 -0
- package/dist/pr-forge.d.ts +105 -0
- package/dist/pr-forge.d.ts.map +1 -0
- package/dist/pr-loop.d.ts +178 -0
- package/dist/pr-loop.d.ts.map +1 -0
- package/dist/telemetry-attribution.d.ts +31 -0
- package/dist/telemetry-attribution.d.ts.map +1 -1
- package/dist/terraform/hcl2json.d.ts +45 -0
- package/dist/terraform/hcl2json.d.ts.map +1 -0
- package/dist/terraform/parse.d.ts +4 -40
- package/dist/terraform/parse.d.ts.map +1 -1
- package/dist/workspace/box-cli.d.ts +12 -0
- package/dist/workspace/box-cli.d.ts.map +1 -0
- package/dist/workspace/box-factory.d.ts +27 -3
- package/dist/workspace/box-factory.d.ts.map +1 -1
- package/dist/workspace/box-listing.d.ts +85 -0
- package/dist/workspace/box-listing.d.ts.map +1 -0
- package/dist/workspace/box-publish.d.ts +147 -0
- package/dist/workspace/box-publish.d.ts.map +1 -0
- package/dist/workspace/broker-protocol.d.ts +190 -0
- package/dist/workspace/broker-protocol.d.ts.map +1 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +2 -2
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
- package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
- package/dist/workspace/conformance/broker.d.ts +202 -0
- package/dist/workspace/conformance/broker.d.ts.map +1 -0
- package/dist/workspace/conformance/index.d.ts +2 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/vitest.d.ts +9 -2
- package/dist/workspace/conformance/vitest.d.ts.map +1 -1
- package/dist/workspace/conformance/writer.d.ts +108 -14
- package/dist/workspace/conformance/writer.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +126 -12
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/decision-points.schema.json +11 -4
- package/dist/workspace/declaration.d.ts +65 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +153 -4
- package/dist/workspace/environments.d.ts +36 -10
- package/dist/workspace/environments.d.ts.map +1 -1
- package/dist/workspace/factory-rules.d.ts +180 -0
- package/dist/workspace/factory-rules.d.ts.map +1 -0
- package/dist/workspace/init.d.ts +21 -0
- package/dist/workspace/init.d.ts.map +1 -1
- package/dist/workspace/intent-cli.d.ts +1 -1
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-record.d.ts +28 -1
- package/dist/workspace/intent-record.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts +102 -5
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/json-edit.d.ts +24 -0
- package/dist/workspace/json-edit.d.ts.map +1 -0
- package/dist/workspace/jsonc.d.ts +11 -0
- package/dist/workspace/jsonc.d.ts.map +1 -1
- package/dist/workspace/kinds.d.ts +48 -0
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/lexicon-symbols.d.ts +29 -0
- package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade.d.ts +31 -2
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/point-answer.schema.json +42 -2
- package/dist/workspace/points-cli.d.ts +10 -3
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +66 -5
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +33 -4
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-sessions.d.ts +5 -7
- package/dist/workspace/record-sessions.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +7 -0
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +7 -3
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +66 -6
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +28 -9
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/runs-cli.d.ts +138 -6
- package/dist/workspace/runs-cli.d.ts.map +1 -1
- package/dist/workspace/runs.d.ts +132 -11
- package/dist/workspace/runs.d.ts.map +1 -1
- package/dist/workspace/session-kinds.d.ts +1 -1
- package/dist/workspace/session-kinds.d.ts.map +1 -1
- package/dist/workspace/squash.d.ts +80 -0
- package/dist/workspace/squash.d.ts.map +1 -0
- package/dist/workspace/status-gates.d.ts +2 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +54 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/symbols.d.ts +16 -5
- package/dist/workspace/symbols.d.ts.map +1 -1
- package/dist/workspace/trust/run-commits.d.ts +46 -0
- package/dist/workspace/trust/run-commits.d.ts.map +1 -0
- package/dist/workspace/trust/run-statement.d.ts +128 -0
- package/dist/workspace/trust/run-statement.d.ts.map +1 -0
- package/dist/workspace/trust/seal.d.ts +6 -1
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/verify-cli.d.ts +1 -1
- package/dist/workspace/trust/verify.d.ts +16 -2
- package/dist/workspace/trust/verify.d.ts.map +1 -1
- package/dist/workspace/wip-cli.d.ts +21 -0
- package/dist/workspace/wip-cli.d.ts.map +1 -0
- package/dist/workspace/wip.d.ts +268 -0
- package/dist/workspace/wip.d.ts.map +1 -0
- package/dist/workspace/work-cli.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +1 -1
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/work-size.d.ts +55 -0
- package/dist/workspace/work-size.d.ts.map +1 -0
- package/dist/workspace/workspace-kinds.schema.json +122 -0
- package/dist/workspace/write-lock-cli.d.ts +60 -0
- package/dist/workspace/write-lock-cli.d.ts.map +1 -0
- package/dist/workspace/write-lock.d.ts +152 -0
- package/dist/workspace/write-lock.d.ts.map +1 -0
- package/dist/workspace/write-scope.d.ts +6 -2
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
- package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
- package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
- package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
- package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
- package/src/__fixtures__/change-set/warden.plan.json +44 -0
- package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
- package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
- package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
- package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
- package/src/audit/core.test.ts +15 -0
- package/src/audit/core.ts +8 -2
- package/src/audit/discover.test.ts +11 -0
- package/src/audit/discover.ts +7 -2
- package/src/change-set-bundle.test.ts +57 -0
- package/src/change-set.test.ts +215 -0
- package/src/change-set.ts +445 -0
- package/src/cli/commands/audit-opt-in.test.ts +54 -0
- package/src/cli/commands/audit.ts +29 -2
- package/src/cli/commands/build.test.ts +19 -0
- package/src/cli/commands/build.ts +3 -2
- package/src/cli/handlers/build.ts +13 -1
- package/src/cli/handlers/change-set.test.ts +56 -0
- package/src/cli/handlers/change-set.ts +44 -0
- package/src/cli/handlers/fan-out.test.ts +221 -2
- package/src/cli/handlers/fan-out.ts +165 -75
- package/src/cli/handlers/graph.ts +3 -0
- package/src/cli/handlers/operator.test.ts +21 -0
- package/src/cli/handlers/operator.ts +34 -3
- package/src/cli/handlers/pr.ts +373 -0
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/main.test.ts +20 -0
- package/src/cli/main.ts +185 -23
- package/src/cli/mcp/never-over-mcp.test.ts +101 -0
- package/src/cli/mcp/op-tools.ts +24 -2
- package/src/cli/mcp/workspace-tools.test.ts +1 -1
- package/src/cli/mcp/workspace-tools.ts +34 -6
- package/src/cli/registry.ts +63 -1
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability.ts +55 -0
- package/src/components/changed-files.ts +66 -0
- package/src/components/cli-support.ts +28 -5
- package/src/components/component.schema.json +17 -0
- package/src/components/component.ts +25 -0
- package/src/components/deploy-units.ts +3 -0
- package/src/components/driver.test.ts +52 -0
- package/src/components/driver.ts +31 -13
- package/src/components/fan-out-output.ts +42 -1
- package/src/components/fan-out-record.ts +69 -0
- package/src/components/fan-out-run.test.ts +17 -0
- package/src/components/fan-out-run.ts +304 -1
- package/src/components/fan-out-support.ts +3 -0
- package/src/components/fan-out-waves.test.ts +375 -0
- package/src/components/fan-out.ts +18 -17
- package/src/components/gated-wave-pipeline.ts +82 -0
- package/src/components/index.ts +1 -0
- package/src/components/layers.ts +30 -0
- package/src/components/pr-pipeline.ts +91 -0
- package/src/components/pr-run.test.ts +205 -0
- package/src/components/pr-run.ts +280 -0
- package/src/components/promote.ts +5 -0
- package/src/components/verbs/shell.test.ts +11 -0
- package/src/components/verbs/shell.ts +7 -3
- package/src/components/wave-plan.ts +124 -0
- package/src/discovery/sandbox/bundle.ts +1 -1
- package/src/gated-waves-bundle.test.ts +62 -0
- package/src/gated-waves.test.ts +118 -0
- package/src/gated-waves.ts +240 -0
- package/src/index.ts +1 -0
- package/src/lexicon.ts +51 -0
- package/src/lifecycle/gate-ledger.ts +7 -0
- package/src/lifecycle/git.ts +102 -16
- package/src/lint/post-synth.test.ts +19 -0
- package/src/lint/post-synth.ts +19 -1
- package/src/meta/declared-imports.test.ts +1 -1
- package/src/op/activities/activity-contracts.ts +84 -0
- package/src/op/activities/change-set.ts +114 -0
- package/src/op/activities/decide.ts +3 -0
- package/src/op/activities/factory.ts +675 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activity-contract.test.ts +21 -1
- package/src/op/activity-contract.ts +19 -9
- package/src/op/builders.ts +23 -0
- package/src/op/decide-backend.ts +5 -1
- package/src/op/decide-read-inputs.ts +33 -5
- package/src/op/factory.e2e.test.ts +232 -0
- package/src/op/factory.ts +118 -0
- package/src/op/index.ts +9 -1
- package/src/op/op-ir.test.ts +9 -0
- package/src/op/op-ir.ts +4 -0
- package/src/op/op-verb-class.ts +3 -0
- package/src/op/types.ts +8 -0
- package/src/op/work-lease-run.ts +3 -0
- package/src/plan-summary.test.ts +263 -0
- package/src/plan-summary.ts +845 -0
- package/src/pr-forge.test.ts +237 -0
- package/src/pr-forge.ts +332 -0
- package/src/pr-loop-bundle.test.ts +46 -0
- package/src/pr-loop.test.ts +171 -0
- package/src/pr-loop.ts +324 -0
- package/src/telemetry-attribution.test.ts +23 -1
- package/src/telemetry-attribution.ts +51 -0
- package/src/terraform/hcl2json.test.ts +21 -0
- package/src/terraform/hcl2json.ts +81 -0
- package/src/terraform/parse.ts +11 -74
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
- package/src/workspace/app-kind.test.ts +196 -0
- package/src/workspace/box-cli.ts +113 -0
- package/src/workspace/box-factory.test.ts +4 -1
- package/src/workspace/box-factory.ts +37 -4
- package/src/workspace/box-listing-write.schema.json +204 -0
- package/src/workspace/box-listing.test.ts +234 -0
- package/src/workspace/box-listing.ts +324 -0
- package/src/workspace/box-publish.schema.json +498 -0
- package/src/workspace/box-publish.test.ts +204 -0
- package/src/workspace/box-publish.ts +420 -0
- package/src/workspace/box-ship.test.ts +96 -0
- package/src/workspace/broker-protocol.schema.json +186 -0
- package/src/workspace/broker-protocol.test.ts +122 -0
- package/src/workspace/broker-protocol.ts +317 -0
- package/src/workspace/builder-tiers.test.ts +146 -0
- package/src/workspace/change-set.schema.json +460 -0
- package/src/workspace/chant-migrations.e2e.test.ts +17 -5
- package/src/workspace/checks/boxes.test.ts +3 -0
- package/src/workspace/checks.ts +10 -3
- package/src/workspace/component-environments.test.ts +105 -0
- package/src/workspace/composites.schema.json +23 -4
- package/src/workspace/composites.test.ts +8 -8
- package/src/workspace/composites.ts +13 -2
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
- package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
- package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
- package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
- package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
- package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
- package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
- package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
- package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
- package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
- package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
- package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
- package/src/workspace/conformance/broker.test.ts +122 -0
- package/src/workspace/conformance/broker.ts +792 -0
- package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
- package/src/workspace/conformance/index.mjs +22 -1
- package/src/workspace/conformance/index.ts +5 -1
- package/src/workspace/conformance/vitest.ts +33 -1
- package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
- package/src/workspace/conformance/writer.test.ts +32 -2
- package/src/workspace/conformance/writer.ts +339 -28
- package/src/workspace/decide.test.ts +208 -4
- package/src/workspace/decide.ts +427 -73
- package/src/workspace/decision-points.schema.json +11 -4
- package/src/workspace/declaration.schema.json +153 -4
- package/src/workspace/declaration.ts +141 -0
- package/src/workspace/environments.ts +66 -14
- package/src/workspace/factory-rules.test.ts +62 -0
- package/src/workspace/factory-rules.ts +218 -0
- package/src/workspace/graph-cli.ts +2 -2
- package/src/workspace/graph.schema.json +67 -0
- package/src/workspace/identity.test.ts +23 -0
- package/src/workspace/init.ts +82 -2
- package/src/workspace/intent-cli.ts +26 -9
- package/src/workspace/intent-patch-id.test.ts +162 -0
- package/src/workspace/intent-record.schema.json +130 -3
- package/src/workspace/intent-record.ts +74 -28
- package/src/workspace/intent-reviews.test.ts +76 -0
- package/src/workspace/intent-squash.test.ts +213 -0
- package/src/workspace/intent-why.test.ts +5 -5
- package/src/workspace/intent.schema.json +234 -4
- package/src/workspace/intent.ts +260 -66
- package/src/workspace/json-edit.test.ts +121 -0
- package/src/workspace/json-edit.ts +142 -0
- package/src/workspace/jsonc.ts +20 -0
- package/src/workspace/kinds.ts +157 -2
- package/src/workspace/lexicon-symbols.test.ts +148 -0
- package/src/workspace/lexicon-symbols.ts +125 -0
- package/src/workspace/lineage-upgrade.test.ts +63 -0
- package/src/workspace/lineage-upgrade.ts +146 -5
- package/src/workspace/plan-summary.schema.json +155 -0
- package/src/workspace/point-answer.schema.json +42 -2
- package/src/workspace/points-cli.ts +42 -16
- package/src/workspace/points-write.schema.json +162 -4
- package/src/workspace/points.schema.json +157 -2
- package/src/workspace/points.test.ts +13 -1
- package/src/workspace/points.ts +118 -12
- package/src/workspace/pr-report.schema.json +95 -0
- package/src/workspace/profiles.test.ts +159 -0
- package/src/workspace/read-contract.test.ts +2 -2
- package/src/workspace/reason-codes.test.ts +13 -1
- package/src/workspace/reason-codes.ts +38 -4
- package/src/workspace/record-sessions.ts +9 -8
- package/src/workspace/records-amend.schema.json +78 -0
- package/src/workspace/records-cli.ts +20 -6
- package/src/workspace/records-close.schema.json +78 -0
- package/src/workspace/records-close.ts +31 -7
- package/src/workspace/records-concurrent.e2e.test.ts +135 -0
- package/src/workspace/records-concurrent.test.ts +145 -0
- package/src/workspace/records-formats.test.ts +46 -10
- package/src/workspace/records-new.schema.json +7 -0
- package/src/workspace/records-review.schema.json +78 -0
- package/src/workspace/records-sessions-write.test.ts +69 -0
- package/src/workspace/records-sessions.test.ts +22 -0
- package/src/workspace/records-write.ts +192 -25
- package/src/workspace/records.schema.json +16 -0
- package/src/workspace/records.ts +50 -9
- package/src/workspace/reference-kinds/app/package.json +10 -0
- package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
- package/src/workspace/run-statement.schema.json +622 -0
- package/src/workspace/runs-cli.ts +343 -14
- package/src/workspace/runs-statement.test.ts +239 -0
- package/src/workspace/runs-write.schema.json +416 -5
- package/src/workspace/runs.schema.json +451 -4
- package/src/workspace/runs.test.ts +39 -0
- package/src/workspace/runs.ts +296 -35
- package/src/workspace/session-kinds.ts +3 -3
- package/src/workspace/squash.ts +198 -0
- package/src/workspace/status-contract.test.ts +5 -2
- package/src/workspace/status-gates.ts +3 -1
- package/src/workspace/status.schema.json +126 -3
- package/src/workspace/status.ts +90 -1
- package/src/workspace/symbols.ts +20 -9
- package/src/workspace/trust/run-commits.ts +115 -0
- package/src/workspace/trust/run-statement.ts +226 -0
- package/src/workspace/trust/seal.ts +8 -2
- package/src/workspace/trust/verify-cli.ts +6 -5
- package/src/workspace/trust/verify.ts +39 -6
- package/src/workspace/wip-cli.ts +97 -0
- package/src/workspace/wip-write.schema.json +504 -0
- package/src/workspace/wip.schema.json +334 -0
- package/src/workspace/wip.test.ts +294 -0
- package/src/workspace/wip.ts +753 -0
- package/src/workspace/work-cli.ts +3 -0
- package/src/workspace/work-evidence.schema.json +3 -0
- package/src/workspace/work-size.test.ts +77 -0
- package/src/workspace/work-size.ts +93 -0
- package/src/workspace/workspace-kinds.schema.json +122 -0
- package/src/workspace/write-lock-cli.ts +131 -0
- package/src/workspace/write-lock.schema.json +172 -0
- package/src/workspace/write-lock.test.ts +189 -0
- package/src/workspace/write-lock.ts +474 -0
- package/src/workspace/write-scope.test.ts +16 -1
- package/src/workspace/write-scope.ts +33 -11
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit one property of a JSON or JSONC document in place (#3308), for the
|
|
3
|
+
* write commands that change a field of the workspace declaration.
|
|
4
|
+
*
|
|
5
|
+
* Only the text of the property changed is touched: the rest of the file
|
|
6
|
+
* keeps its indentation, key order, line endings, comments and trailing
|
|
7
|
+
* commas. A new property goes after the last one of its object, indented
|
|
8
|
+
* like it. The caller re-parses the result and compares it with the value it
|
|
9
|
+
* meant to write, so an edit this module gets wrong is never written.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseJsonText, pointerToken } from "./jsonc";
|
|
13
|
+
|
|
14
|
+
export class JsonEditError extends Error {
|
|
15
|
+
constructor(message: string) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "JsonEditError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Parsed = Extract<ReturnType<typeof parseJsonText>, { ok: true }>;
|
|
22
|
+
|
|
23
|
+
function parse(text: string, jsonc: boolean): Parsed {
|
|
24
|
+
const r = parseJsonText(text, { jsonc });
|
|
25
|
+
if (!r.ok) throw new JsonEditError(`the file does not parse: ${r.message} at line ${r.location.line}, column ${r.location.column}`);
|
|
26
|
+
return r;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The value at a JSON Pointer, or undefined. */
|
|
30
|
+
export function valueAt(root: unknown, pointer: string): unknown {
|
|
31
|
+
if (pointer === "") return root;
|
|
32
|
+
let at: unknown = root;
|
|
33
|
+
for (const raw of pointer.slice(1).split("/")) {
|
|
34
|
+
const token = raw.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
35
|
+
if (at === null || typeof at !== "object") return undefined;
|
|
36
|
+
if (Array.isArray(at)) at = at[Number(token)];
|
|
37
|
+
else at = Object.prototype.hasOwnProperty.call(at, token) ? (at as Record<string, unknown>)[token] : undefined;
|
|
38
|
+
}
|
|
39
|
+
return at;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isObject = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
43
|
+
|
|
44
|
+
/** The text's line ending, and the indentation one level adds (the first indented line's, else two spaces). */
|
|
45
|
+
function style(text: string): { eol: string; unit: string } {
|
|
46
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
47
|
+
const m = /\n([ \t]+)\S/.exec(text);
|
|
48
|
+
return { eol, unit: m ? (m[1].startsWith("\t") ? "\t" : m[1]) : " " };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function lineStart(text: string, offset: number): number {
|
|
52
|
+
return text.lastIndexOf("\n", offset - 1) + 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function lineIndent(text: string, offset: number): string {
|
|
56
|
+
const start = lineStart(text, offset);
|
|
57
|
+
return /^[ \t]*/.exec(text.slice(start))![0];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The offset of the next character after `from` that is not whitespace or a comment. */
|
|
61
|
+
function skipTrivia(text: string, from: number): number {
|
|
62
|
+
let i = from;
|
|
63
|
+
for (;;) {
|
|
64
|
+
const c = text[i];
|
|
65
|
+
if (c === " " || c === "\t" || c === "\n" || c === "\r") i++;
|
|
66
|
+
else if (c === "/" && text[i + 1] === "/") {
|
|
67
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
68
|
+
} else if (c === "/" && text[i + 1] === "*") {
|
|
69
|
+
const end = text.indexOf("*/", i + 2);
|
|
70
|
+
i = end < 0 ? text.length : end + 2;
|
|
71
|
+
} else return i;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function render(value: unknown, indent: string, unit: string, eol: string): string {
|
|
76
|
+
return JSON.stringify(value, null, unit).split("\n").join(eol + indent);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Set `key` of the object at `objectPointer` to `value`: replace the value's
|
|
81
|
+
* text when the key is there, else add the key after the object's last one.
|
|
82
|
+
* Throws a {@link JsonEditError} when there is no object at the pointer.
|
|
83
|
+
*/
|
|
84
|
+
export function setProperty(text: string, jsonc: boolean, objectPointer: string, key: string, value: unknown): string {
|
|
85
|
+
const r = parse(text, jsonc);
|
|
86
|
+
const parent = valueAt(r.value, objectPointer);
|
|
87
|
+
if (!isObject(parent)) throw new JsonEditError(`there is no object at ${objectPointer || "the root"}`);
|
|
88
|
+
const { eol, unit } = style(text);
|
|
89
|
+
const child = r.span(`${objectPointer}/${pointerToken(key)}`);
|
|
90
|
+
if (child) return text.slice(0, child.start) + render(value, lineIndent(text, child.key ?? child.start), unit, eol) + text.slice(child.end);
|
|
91
|
+
|
|
92
|
+
const obj = r.span(objectPointer)!;
|
|
93
|
+
const parentIndent = lineIndent(text, obj.key ?? obj.start);
|
|
94
|
+
const keys = Object.keys(parent);
|
|
95
|
+
if (keys.length === 0) {
|
|
96
|
+
const indent = parentIndent + unit;
|
|
97
|
+
return `${text.slice(0, obj.start)}{${eol}${indent}${JSON.stringify(key)}: ${render(value, indent, unit, eol)}${eol}${parentIndent}}${text.slice(obj.end)}`;
|
|
98
|
+
}
|
|
99
|
+
const last = r.span(`${objectPointer}/${pointerToken(keys[keys.length - 1])}`)!;
|
|
100
|
+
const next = skipTrivia(text, last.end);
|
|
101
|
+
const trailingComma = text[next] === ",";
|
|
102
|
+
if (lineStart(text, last.key!) === lineStart(text, obj.start)) {
|
|
103
|
+
// An object on one line stays on one line.
|
|
104
|
+
const entry = `${JSON.stringify(key)}: ${JSON.stringify(value)}`;
|
|
105
|
+
return trailingComma ? `${text.slice(0, next + 1)} ${entry},${text.slice(next + 1)}` : `${text.slice(0, last.end)}, ${entry}${text.slice(last.end)}`;
|
|
106
|
+
}
|
|
107
|
+
const indent = lineIndent(text, last.key!);
|
|
108
|
+
const after = trailingComma ? next + 1 : last.end;
|
|
109
|
+
// After a comment that ends the last property's line, so the comment stays with it.
|
|
110
|
+
const eolAt = text.indexOf("\n", after);
|
|
111
|
+
const lineEnd = eolAt < 0 ? text.length : text[eolAt - 1] === "\r" ? eolAt - 1 : eolAt;
|
|
112
|
+
const at = /^[ \t]*(\/\/.*)?$/.test(text.slice(after, lineEnd)) ? lineEnd : after;
|
|
113
|
+
const entry = `${eol}${indent}${JSON.stringify(key)}: ${render(value, indent, unit, eol)}`;
|
|
114
|
+
if (trailingComma) return `${text.slice(0, at)}${entry},${text.slice(at)}`;
|
|
115
|
+
return `${text.slice(0, last.end)},${text.slice(last.end, at)}${entry}${text.slice(at)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Remove `key` from the object at `objectPointer`, with the comma that went with it. Unchanged when the key is not there. */
|
|
119
|
+
export function removeProperty(text: string, jsonc: boolean, objectPointer: string, key: string): string {
|
|
120
|
+
const r = parse(text, jsonc);
|
|
121
|
+
const parent = valueAt(r.value, objectPointer);
|
|
122
|
+
if (!isObject(parent)) throw new JsonEditError(`there is no object at ${objectPointer || "the root"}`);
|
|
123
|
+
const child = r.span(`${objectPointer}/${pointerToken(key)}`);
|
|
124
|
+
if (!child) return text;
|
|
125
|
+
const keys = Object.keys(parent);
|
|
126
|
+
const obj = r.span(objectPointer)!;
|
|
127
|
+
if (keys.length === 1) return `${text.slice(0, obj.start)}{}${text.slice(obj.end)}`;
|
|
128
|
+
const next = skipTrivia(text, child.end);
|
|
129
|
+
if (text[next] === ",") {
|
|
130
|
+
const start = lineStart(text, child.key!);
|
|
131
|
+
const ownLine = /^[ \t]*$/.test(text.slice(start, child.key!));
|
|
132
|
+
let to = next + 1;
|
|
133
|
+
const eolAt = text.indexOf("\n", to);
|
|
134
|
+
if (ownLine && /^[ \t\r]*$/.test(text.slice(to, eolAt < 0 ? text.length : eolAt))) to = eolAt < 0 ? text.length : eolAt + 1;
|
|
135
|
+
else while (text[to] === " ") to++;
|
|
136
|
+
return text.slice(0, ownLine ? start : child.key!) + text.slice(to);
|
|
137
|
+
}
|
|
138
|
+
// The last property, with no trailing comma: remove from the comma after the one before it.
|
|
139
|
+
const prev = r.span(`${objectPointer}/${pointerToken(keys[keys.indexOf(key) - 1])}`)!;
|
|
140
|
+
const comma = skipTrivia(text, prev.end);
|
|
141
|
+
return text.slice(0, comma) + text.slice(child.end);
|
|
142
|
+
}
|
package/src/workspace/jsonc.ts
CHANGED
|
@@ -31,6 +31,13 @@ export type JsonParseResult =
|
|
|
31
31
|
* `key` is set. Falls back to the nearest ancestor that has a position.
|
|
32
32
|
*/
|
|
33
33
|
locate(pointer: string, key?: boolean): TextLocation;
|
|
34
|
+
/**
|
|
35
|
+
* Where the value at `pointer` is in the text, as offsets: its key's
|
|
36
|
+
* opening quote (null for an array item or the root), the value's first
|
|
37
|
+
* character, and the offset just past its last. Undefined when there is
|
|
38
|
+
* no value at `pointer`. For editing the text in place (#3308).
|
|
39
|
+
*/
|
|
40
|
+
span(pointer: string): { key: number | null; start: number; end: number } | undefined;
|
|
34
41
|
}
|
|
35
42
|
| { ok: false; message: string; location: TextLocation };
|
|
36
43
|
|
|
@@ -63,6 +70,7 @@ export function parseJsonText(text: string, options: JsonParseOptions): JsonPars
|
|
|
63
70
|
};
|
|
64
71
|
|
|
65
72
|
const values = new Map<string, number>();
|
|
73
|
+
const ends = new Map<string, number>();
|
|
66
74
|
const keys = new Map<string, number>();
|
|
67
75
|
let i = text.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
68
76
|
|
|
@@ -113,6 +121,12 @@ export function parseJsonText(text: string, options: JsonParseOptions): JsonPars
|
|
|
113
121
|
function parseValue(pointer: string): unknown {
|
|
114
122
|
skip();
|
|
115
123
|
values.set(pointer, i);
|
|
124
|
+
const value = parseAt(pointer);
|
|
125
|
+
ends.set(pointer, i);
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseAt(pointer: string): unknown {
|
|
116
130
|
const c = text[i];
|
|
117
131
|
if (c === "{") return parseObject(pointer);
|
|
118
132
|
if (c === "[") return parseArray(pointer);
|
|
@@ -223,6 +237,12 @@ export function parseJsonText(text: string, options: JsonParseOptions): JsonPars
|
|
|
223
237
|
if (p === "") return toLocation(0);
|
|
224
238
|
}
|
|
225
239
|
},
|
|
240
|
+
span(pointer) {
|
|
241
|
+
const start = values.get(pointer);
|
|
242
|
+
const end = ends.get(pointer);
|
|
243
|
+
if (start === undefined || end === undefined) return undefined;
|
|
244
|
+
return { key: keys.get(pointer) ?? null, start, end };
|
|
245
|
+
},
|
|
226
246
|
};
|
|
227
247
|
} catch (err) {
|
|
228
248
|
if (!(err instanceof JsonSyntaxError)) throw err;
|
package/src/workspace/kinds.ts
CHANGED
|
@@ -45,8 +45,34 @@ export interface FileProbe {
|
|
|
45
45
|
* `live {` block is found without parsing HCL (#2545).
|
|
46
46
|
*/
|
|
47
47
|
anyBlock?: { in: string[]; blocks: string[] };
|
|
48
|
+
/**
|
|
49
|
+
* A file directly in the directory whose name matches one of `in` parses
|
|
50
|
+
* as JSON and has a value at one of `pointers`, each a JSON Pointer. This
|
|
51
|
+
* is how an app member's `package.json` with a `start` script is found
|
|
52
|
+
* without claiming every Node package (#3151).
|
|
53
|
+
*/
|
|
54
|
+
anyJsonKey?: { in: string[]; pointers: string[] };
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
/** A string, integer or boolean field a kind declares for its members (#3151). */
|
|
58
|
+
export interface ScalarKindField {
|
|
59
|
+
type: "string" | "integer" | "boolean";
|
|
60
|
+
description: string;
|
|
61
|
+
/** The value a member that doesn't set the field gets; null when the kind gives none. */
|
|
62
|
+
default?: string | number | boolean;
|
|
63
|
+
/** For a string field, a regular expression the value must match. */
|
|
64
|
+
pattern?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** An object field: named scalar fields, each with its own default (#3151). */
|
|
68
|
+
export interface ObjectKindField {
|
|
69
|
+
type: "object";
|
|
70
|
+
description: string;
|
|
71
|
+
properties: Record<string, ScalarKindField>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type KindField = ScalarKindField | ObjectKindField;
|
|
75
|
+
|
|
50
76
|
/** What a kind's probe checks in a directory. */
|
|
51
77
|
export type KindProbe =
|
|
52
78
|
/** Files, or blocks in files, sit directly in the directory. */
|
|
@@ -99,6 +125,8 @@ export interface MemberKind {
|
|
|
99
125
|
packageDir?: string;
|
|
100
126
|
/** How `workspace graph` reads a member of this kind (#2874); unset, the member is `kind-not-run`. */
|
|
101
127
|
graph?: KindGraph;
|
|
128
|
+
/** The fields a member entry of this kind may set, with their defaults (#3151); unset, the kind declares none. */
|
|
129
|
+
fields?: Record<string, KindField>;
|
|
102
130
|
}
|
|
103
131
|
|
|
104
132
|
export interface KindRegistry {
|
|
@@ -187,8 +215,18 @@ export function builtinKindRegistry(): KindRegistry {
|
|
|
187
215
|
export function probeKind(kind: MemberKind, tree: WorkspaceTree, dir: string): boolean {
|
|
188
216
|
if ("directory" in kind.probe) return true;
|
|
189
217
|
if ("chantProject" in kind.probe) return holdsChantProject(tree, dir);
|
|
190
|
-
const { anyFile, anyBlock } = kind.probe;
|
|
218
|
+
const { anyFile, anyBlock, anyJsonKey } = kind.probe;
|
|
191
219
|
if (anyFile && filesMatching(tree, dir, anyFile).length > 0) return true;
|
|
220
|
+
if (anyJsonKey) {
|
|
221
|
+
for (const file of filesMatching(tree, dir, anyJsonKey.in)) {
|
|
222
|
+
try {
|
|
223
|
+
const json = JSON.parse(tree.read(file)) as unknown;
|
|
224
|
+
if (anyJsonKey.pointers.some((p) => atPointer(json, p) !== undefined)) return true;
|
|
225
|
+
} catch {
|
|
226
|
+
// A file that isn't JSON is no evidence either way.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
192
230
|
if (anyBlock) {
|
|
193
231
|
const opener = new RegExp(`^\\s*(?:${anyBlock.blocks.map(escapeRegExp).join("|")})\\s*\\{`, "m");
|
|
194
232
|
for (const file of filesMatching(tree, dir, anyBlock.in)) {
|
|
@@ -227,6 +265,17 @@ function filesMatching(tree: WorkspaceTree, dir: string, names: readonly string[
|
|
|
227
265
|
return out.sort();
|
|
228
266
|
}
|
|
229
267
|
|
|
268
|
+
/** The value at JSON Pointer `pointer` in `json`, or undefined when there is none. */
|
|
269
|
+
function atPointer(json: unknown, pointer: string): unknown {
|
|
270
|
+
let at: unknown = json;
|
|
271
|
+
for (const raw of pointer.split("/").slice(1)) {
|
|
272
|
+
const token = raw.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
273
|
+
if (at === null || typeof at !== "object" || !Object.prototype.hasOwnProperty.call(at, token)) return undefined;
|
|
274
|
+
at = (at as Record<string, unknown>)[token];
|
|
275
|
+
}
|
|
276
|
+
return at;
|
|
277
|
+
}
|
|
278
|
+
|
|
230
279
|
function escapeRegExp(text: string): string {
|
|
231
280
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
232
281
|
}
|
|
@@ -337,7 +386,7 @@ export function parseKindData(text: string, source: string): KindData {
|
|
|
337
386
|
const problems: string[] = [];
|
|
338
387
|
const kinds: MemberKind[] = [];
|
|
339
388
|
const seen = new Set<string>();
|
|
340
|
-
for (const k of (raw as { kinds: { name: string; description: string; precedence: number; probe: FileProbe; outputs?: string[]; graph?: KindGraph }[] }).kinds) {
|
|
389
|
+
for (const k of (raw as { kinds: { name: string; description: string; precedence: number; probe: FileProbe; outputs?: string[]; graph?: KindGraph; fields?: Record<string, KindField> }[] }).kinds) {
|
|
341
390
|
if (BUILTIN_KIND_NAMES.includes(k.name)) {
|
|
342
391
|
problems.push(`${source}: kind ${k.name} is built in and can't be supplied by a package`);
|
|
343
392
|
continue;
|
|
@@ -353,13 +402,16 @@ export function parseKindData(text: string, source: string): KindData {
|
|
|
353
402
|
probe: {
|
|
354
403
|
...(k.probe.anyFile ? { anyFile: [...k.probe.anyFile] } : {}),
|
|
355
404
|
...(k.probe.anyBlock ? { anyBlock: { in: [...k.probe.anyBlock.in], blocks: [...k.probe.anyBlock.blocks] } } : {}),
|
|
405
|
+
...(k.probe.anyJsonKey ? { anyJsonKey: { in: [...k.probe.anyJsonKey.in], pointers: [...k.probe.anyJsonKey.pointers] } } : {}),
|
|
356
406
|
},
|
|
357
407
|
precedence: k.precedence,
|
|
358
408
|
shape: "member",
|
|
359
409
|
outputs: { from: "declared", names: [...(k.outputs ?? [])] },
|
|
360
410
|
source,
|
|
361
411
|
...(k.graph ? { graph: { lexicon: k.graph.lexicon, config: structuredClone(k.graph.config) } } : {}),
|
|
412
|
+
...(k.fields ? { fields: structuredClone(k.fields) } : {}),
|
|
362
413
|
});
|
|
414
|
+
for (const problem of fieldDefaultProblems(k.fields ?? {})) problems.push(`${source}: kind ${k.name} ${problem}`);
|
|
363
415
|
}
|
|
364
416
|
return { kinds, problems };
|
|
365
417
|
}
|
|
@@ -590,3 +642,106 @@ export function kindsFileShips(packageDir: string, file: string): boolean {
|
|
|
590
642
|
return rel === e || rel.startsWith(`${e}/`);
|
|
591
643
|
});
|
|
592
644
|
}
|
|
645
|
+
|
|
646
|
+
// ── Member fields (#3151) ────────────────────────────────────────────────────
|
|
647
|
+
|
|
648
|
+
/** A value a member sets, or a kind's default: what a scalar field holds. */
|
|
649
|
+
export type FieldValue = string | number | boolean;
|
|
650
|
+
|
|
651
|
+
/** Why a value doesn't fit `field`, phrased to follow "the value", or undefined when it fits. */
|
|
652
|
+
function scalarProblem(field: ScalarKindField, value: unknown): string | undefined {
|
|
653
|
+
const ok = field.type === "integer" ? typeof value === "number" && Number.isInteger(value) : typeof value === field.type;
|
|
654
|
+
if (!ok) return `is ${JSON.stringify(value)}, and the field is ${field.type === "integer" ? "an integer" : `a ${field.type}`}`;
|
|
655
|
+
if (field.pattern !== undefined && typeof value === "string") {
|
|
656
|
+
let re: RegExp;
|
|
657
|
+
try {
|
|
658
|
+
re = new RegExp(field.pattern, "u");
|
|
659
|
+
} catch {
|
|
660
|
+
return undefined; // Reported once, against the kind, by fieldDefaultProblems.
|
|
661
|
+
}
|
|
662
|
+
if (!re.test(value)) return `is ${JSON.stringify(value)}, which does not match ${field.pattern}`;
|
|
663
|
+
}
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** What is wrong with a kind's own field declarations: a pattern that isn't a regular expression, or a default of the wrong type. */
|
|
668
|
+
function fieldDefaultProblems(fields: Record<string, KindField>): string[] {
|
|
669
|
+
const out: string[] = [];
|
|
670
|
+
const scalar = (name: string, f: ScalarKindField) => {
|
|
671
|
+
if (f.pattern !== undefined) {
|
|
672
|
+
try {
|
|
673
|
+
new RegExp(f.pattern, "u");
|
|
674
|
+
} catch {
|
|
675
|
+
out.push(`declares field ${name} with pattern ${f.pattern}, which is not a regular expression`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (f.default === undefined) return;
|
|
680
|
+
const problem = scalarProblem(f, f.default);
|
|
681
|
+
if (problem) out.push(`declares field ${name} whose default ${problem}`);
|
|
682
|
+
};
|
|
683
|
+
for (const [name, f] of Object.entries(fields)) {
|
|
684
|
+
if (f.type === "object") for (const [inner, g] of Object.entries(f.properties)) scalar(`${name}.${inner}`, g);
|
|
685
|
+
else scalar(name, f);
|
|
686
|
+
}
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** One problem with a member's fields: the field's path under `fields` (`scripts/start`), and why. */
|
|
691
|
+
export interface MemberFieldProblem {
|
|
692
|
+
path: string;
|
|
693
|
+
message: string;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export interface MemberFields {
|
|
697
|
+
/** Every field the kind declares, set from the entry or its default (null when it has none); null when the kind declares no fields. */
|
|
698
|
+
fields: Record<string, FieldValue | null | Record<string, FieldValue | null>> | null;
|
|
699
|
+
problems: MemberFieldProblem[];
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* A member's fields (#3151): the ones its entry sets, checked against what
|
|
704
|
+
* its kind declares, with every other declared field at its default. A field
|
|
705
|
+
* the kind doesn't declare, or a value of the wrong type, is a problem, and
|
|
706
|
+
* the field reads as its default. A kind no registry knows has no fields to
|
|
707
|
+
* check against, so it reads as null with no problem (WSP003 reports it).
|
|
708
|
+
*/
|
|
709
|
+
export function resolveMemberFields(kind: MemberKind | undefined, given: Record<string, unknown> | null): MemberFields {
|
|
710
|
+
if (kind === undefined) return { fields: null, problems: [] };
|
|
711
|
+
const declared = kind.fields;
|
|
712
|
+
const problems: MemberFieldProblem[] = [];
|
|
713
|
+
if (declared === undefined) {
|
|
714
|
+
if (given !== null) problems.push({ path: "", message: `kind ${kind.name} declares no fields, and the entry sets ${Object.keys(given).join(", ") || "an empty fields block"}` });
|
|
715
|
+
return { fields: null, problems };
|
|
716
|
+
}
|
|
717
|
+
const set = given ?? {};
|
|
718
|
+
for (const name of Object.keys(set)) {
|
|
719
|
+
if (!(name in declared)) problems.push({ path: name, message: `kind ${kind.name} declares no field ${name}; its fields are ${Object.keys(declared).join(", ")}` });
|
|
720
|
+
}
|
|
721
|
+
const scalar = (path: string, field: ScalarKindField, value: unknown): FieldValue | null => {
|
|
722
|
+
if (value !== undefined) {
|
|
723
|
+
const problem = scalarProblem(field, value);
|
|
724
|
+
if (!problem) return value as FieldValue;
|
|
725
|
+
problems.push({ path, message: `field ${path.replace("/", ".")} ${problem}` });
|
|
726
|
+
}
|
|
727
|
+
return field.default ?? null;
|
|
728
|
+
};
|
|
729
|
+
const fields: NonNullable<MemberFields["fields"]> = {};
|
|
730
|
+
for (const [name, field] of Object.entries(declared)) {
|
|
731
|
+
if (field.type !== "object") {
|
|
732
|
+
fields[name] = scalar(name, field, set[name]);
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const raw = set[name];
|
|
736
|
+
let inner: Record<string, unknown> = {};
|
|
737
|
+
if (raw !== undefined) {
|
|
738
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) inner = raw as Record<string, unknown>;
|
|
739
|
+
else problems.push({ path: name, message: `field ${name} is ${JSON.stringify(raw)}, and the field is an object` });
|
|
740
|
+
}
|
|
741
|
+
for (const key of Object.keys(inner)) {
|
|
742
|
+
if (!(key in field.properties)) problems.push({ path: `${name}/${key}`, message: `kind ${kind.name} declares no field ${name}.${key}; ${name} has ${Object.keys(field.properties).join(", ")}` });
|
|
743
|
+
}
|
|
744
|
+
fields[name] = Object.fromEntries(Object.entries(field.properties).map(([key, f]) => [key, scalar(`${name}/${key}`, f, inner[key])]));
|
|
745
|
+
}
|
|
746
|
+
return { fields, problems };
|
|
747
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol resolvers from a member's lexicon (#3313): `graph --intent
|
|
3
|
+
* path#symbol` on a file in a member whose lexicon exports a resolver.
|
|
4
|
+
*
|
|
5
|
+
* The workspace has a chant member `db` whose chant.config.json declares a
|
|
6
|
+
* lexicon by module path, `lexicon/plugin.mjs`, which resolves `create table`
|
|
7
|
+
* and `create function` statements in `.sql` files. A loader stands in for
|
|
8
|
+
* the plugin in the unit cases; one case loads the module path the way any
|
|
9
|
+
* command does.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
14
|
+
import { cleanScratch, contract, git, repo } from "./__fixtures__/contract-repo";
|
|
15
|
+
import { intentGraph, type IntentDocument, type RegionNode } from "./intent";
|
|
16
|
+
import intentSchema from "./intent.schema.json";
|
|
17
|
+
import { memberSymbolResolvers, type SymbolPluginLoader } from "./lexicon-symbols";
|
|
18
|
+
import { resolveSymbol, type SymbolDeclaration, type SymbolResolver } from "./symbols";
|
|
19
|
+
|
|
20
|
+
afterAll(cleanScratch);
|
|
21
|
+
|
|
22
|
+
const intent = contract(intentSchema);
|
|
23
|
+
|
|
24
|
+
/** A tiny SQL resolver: each `create table|function <name>` up to the line ending in `;`. */
|
|
25
|
+
const SQL_RESOLVER_SOURCE = `
|
|
26
|
+
export const sqlResolver = {
|
|
27
|
+
language: "SQL",
|
|
28
|
+
extensions: [".sql"],
|
|
29
|
+
declarations(path, text) {
|
|
30
|
+
const lines = text.split("\\n");
|
|
31
|
+
const out = [];
|
|
32
|
+
lines.forEach((line, i) => {
|
|
33
|
+
const m = /^create\\s+(table|function)\\s+([a-z_.]+)/i.exec(line);
|
|
34
|
+
if (!m) return;
|
|
35
|
+
let end = i;
|
|
36
|
+
while (end < lines.length - 1 && !lines[end].trimEnd().endsWith(";")) end++;
|
|
37
|
+
out.push({ qualified: m[2], kind: m[1].toLowerCase(), lines: { start: i + 1, end: end + 1 } });
|
|
38
|
+
});
|
|
39
|
+
return out;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
const PLUGIN = `${SQL_RESOLVER_SOURCE}
|
|
45
|
+
export const plugin = {
|
|
46
|
+
name: "sqlish",
|
|
47
|
+
serializer: { name: "sqlish", rulePrefix: "SQI", serialize: () => "" },
|
|
48
|
+
async generate() {},
|
|
49
|
+
async validate() {},
|
|
50
|
+
async coverage() {},
|
|
51
|
+
async package() {},
|
|
52
|
+
symbolResolvers: () => [sqlResolver],
|
|
53
|
+
};
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const SCHEMA = ["-- the app's schema", "create table app.users (", " id int primary key", ");", "", "create function app.touch()", " returns void as $$ select 1 $$ language sql;", ""].join("\n");
|
|
57
|
+
|
|
58
|
+
async function sqlResolver(): Promise<SymbolResolver> {
|
|
59
|
+
const mod = (await import(`data:text/javascript,${encodeURIComponent(SQL_RESOLVER_SOURCE)}`)) as { sqlResolver: SymbolResolver };
|
|
60
|
+
return mod.sqlResolver;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function workspace(): string {
|
|
64
|
+
const root = repo(
|
|
65
|
+
{
|
|
66
|
+
"chant.workspace.json": `${JSON.stringify({
|
|
67
|
+
name: "demo",
|
|
68
|
+
schema: 1,
|
|
69
|
+
members: [
|
|
70
|
+
{ name: "db", dir: "db", kind: "chant" },
|
|
71
|
+
{ name: "notes", dir: "notes", kind: "other", because: "prose" },
|
|
72
|
+
],
|
|
73
|
+
})}\n`,
|
|
74
|
+
"db/chant.config.json": `${JSON.stringify({ lexicons: [{ name: "sqlish", module: "../lexicon/plugin.mjs" }] })}\n`,
|
|
75
|
+
"db/schema.sql": SCHEMA,
|
|
76
|
+
"db/main.ts": "export const x = 1;\n",
|
|
77
|
+
"lexicon/plugin.mjs": PLUGIN,
|
|
78
|
+
"notes/schema.sql": SCHEMA,
|
|
79
|
+
},
|
|
80
|
+
true,
|
|
81
|
+
);
|
|
82
|
+
git(root, "branch", "-M", "main");
|
|
83
|
+
return root;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const region = (doc: IntentDocument) => {
|
|
87
|
+
if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
|
|
88
|
+
return doc.nodes.find((n) => n.kind === "region") as RegionNode;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
describe("a lexicon's symbol resolvers (#3313)", () => {
|
|
92
|
+
test("resolveSymbol uses an extra resolver for its extensions, and core's for .ts", async () => {
|
|
93
|
+
const sql = await sqlResolver();
|
|
94
|
+
expect(resolveSymbol("db/schema.sql", SCHEMA, "app.users", [sql])).toEqual({ ok: true, declaration: { qualified: "app.users", kind: "table", lines: { start: 2, end: 4 } } });
|
|
95
|
+
expect(resolveSymbol("db/schema.sql", SCHEMA, "users", [sql])).toMatchObject({ ok: true, declaration: { qualified: "app.users" } });
|
|
96
|
+
expect(resolveSymbol("db/schema.sql", SCHEMA, "app.users")).toMatchObject({ ok: false, reason: "unsupported" });
|
|
97
|
+
// A lexicon's resolver never takes .ts from core's.
|
|
98
|
+
const greedy: SymbolResolver = { language: "greedy", extensions: [".ts"], declarations: (): SymbolDeclaration[] => [{ qualified: "x", kind: "lie", lines: { start: 9, end: 9 } }] };
|
|
99
|
+
expect(resolveSymbol("a.ts", "export const x = 1;\n", "x", [greedy])).toMatchObject({ ok: true, declaration: { kind: "variable", lines: { start: 1, end: 1 } } });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("graph --intent path#symbol resolves through the member's lexicon", async () => {
|
|
103
|
+
const root = workspace();
|
|
104
|
+
const sql = await sqlResolver();
|
|
105
|
+
const load: SymbolPluginLoader = async (name) => {
|
|
106
|
+
expect(name).toBe("sqlish");
|
|
107
|
+
return { name, symbolResolvers: () => [sql] };
|
|
108
|
+
};
|
|
109
|
+
const { doc } = await intentGraph({ cwd: root, region: "db/schema.sql#app.touch", loadLexicon: load });
|
|
110
|
+
intent.expectValid(doc);
|
|
111
|
+
expect(region(doc)).toMatchObject({ path: "db/schema.sql", lines: { start: 6, end: 7 }, member: "db", symbol: { name: "app.touch", qualified: "app.touch", kind: "function" } });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("loads the lexicon the member's config declares, by module path, as any command does", async () => {
|
|
115
|
+
const root = workspace();
|
|
116
|
+
const { doc } = await intentGraph({ cwd: root, region: "db/schema.sql#app.users" });
|
|
117
|
+
intent.expectValid(doc);
|
|
118
|
+
expect(region(doc)).toMatchObject({ lines: { start: 2, end: 4 }, symbol: { qualified: "app.users", kind: "table" } });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a file in a member with no such lexicon, or whose lexicon fails, stays intent-symbol-unsupported and says why", async () => {
|
|
122
|
+
const root = workspace();
|
|
123
|
+
const other = await intentGraph({ cwd: root, region: "notes/schema.sql#app.users" });
|
|
124
|
+
intent.expectValid(other.doc);
|
|
125
|
+
expect("error" in other.doc && other.doc.error.code).toBe("intent-symbol-unsupported");
|
|
126
|
+
const broken = await intentGraph({
|
|
127
|
+
cwd: root,
|
|
128
|
+
region: "db/schema.sql#app.users",
|
|
129
|
+
loadLexicon: async () => {
|
|
130
|
+
throw new Error("Cannot find package");
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
expect("error" in broken.doc && broken.doc.error).toMatchObject({ code: "intent-symbol-unsupported", message: expect.stringContaining('lexicon "sqlish" could not be loaded: Cannot find package') });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("a malformed resolver or bad declarations are dropped, not read as lines", async () => {
|
|
137
|
+
const root = workspace();
|
|
138
|
+
const got = await memberSymbolResolvers(join(root, "db"), async () => ({
|
|
139
|
+
symbolResolvers: () => [
|
|
140
|
+
{ language: "bad", extensions: ["sql"], declarations: () => [] },
|
|
141
|
+
{ language: "ok", extensions: [".sql"], declarations: () => [{ qualified: "t", kind: "table", lines: { start: 3, end: 1 } }, { qualified: "u", kind: "table", lines: { start: 1, end: 2 } }] },
|
|
142
|
+
],
|
|
143
|
+
}));
|
|
144
|
+
expect(got.problems).toEqual([expect.stringContaining("not { language, extensions")]);
|
|
145
|
+
expect(got.resolvers).toHaveLength(1);
|
|
146
|
+
expect(got.resolvers[0].declarations("x.sql", "")).toEqual([{ qualified: "u", kind: "table", lines: { start: 1, end: 2 } }]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol resolvers a member's lexicons contribute (#3313), for
|
|
3
|
+
* `chant workspace graph --intent <path>#<symbol>` on a file core's own
|
|
4
|
+
* resolver does not read (`symbols.ts`).
|
|
5
|
+
*
|
|
6
|
+
* The lexicons are the ones the member's `chant.config.ts` (or `.json`)
|
|
7
|
+
* names, read without running the config when it can be read statically
|
|
8
|
+
* (`../config-static.ts`), and by loading it as `chant run` does otherwise.
|
|
9
|
+
* Each is loaded as any command loads it (`loadPlugin`), and its plugin's
|
|
10
|
+
* `symbolResolvers()` answers. A lexicon that can't be loaded, or whose
|
|
11
|
+
* resolvers are malformed, contributes nothing, and the reason is kept so a
|
|
12
|
+
* refusal can say why the file has no resolver.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { lexiconNames } from "../lexicon-module";
|
|
17
|
+
import type { SymbolDeclaration, SymbolResolver } from "./symbols";
|
|
18
|
+
|
|
19
|
+
/** Loads a lexicon plugin by name; `loadPlugin` from the CLI unless a test swaps it. */
|
|
20
|
+
export type SymbolPluginLoader = (name: string) => Promise<{ name?: string; symbolResolvers?: () => unknown }>;
|
|
21
|
+
|
|
22
|
+
export interface MemberSymbolResolvers {
|
|
23
|
+
resolvers: SymbolResolver[];
|
|
24
|
+
/** Why a lexicon contributed nothing it may have meant to: its config or plugin couldn't be read, or a resolver was malformed. */
|
|
25
|
+
problems: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const firstLine = (err: unknown): string => (err instanceof Error ? err.message : String(err)).split("\n")[0];
|
|
29
|
+
|
|
30
|
+
/** A resolver a plugin returned, checked: a language, extensions starting with a dot, and a declarations function. */
|
|
31
|
+
function checked(lexicon: string, value: unknown, problems: string[]): SymbolResolver | undefined {
|
|
32
|
+
const r = value as Partial<SymbolResolver> | null;
|
|
33
|
+
const ok =
|
|
34
|
+
r !== null &&
|
|
35
|
+
typeof r === "object" &&
|
|
36
|
+
typeof r.language === "string" &&
|
|
37
|
+
Array.isArray(r.extensions) &&
|
|
38
|
+
r.extensions.length > 0 &&
|
|
39
|
+
r.extensions.every((e) => typeof e === "string" && /^\.[^./\\]+$/.test(e)) &&
|
|
40
|
+
typeof r.declarations === "function";
|
|
41
|
+
if (!ok) {
|
|
42
|
+
problems.push(`lexicon "${lexicon}" returned a symbol resolver that is not { language, extensions: [".ext", ...], declarations(path, text) }`);
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const declarations = r.declarations!.bind(r);
|
|
46
|
+
return {
|
|
47
|
+
language: `${r.language} (lexicon ${lexicon})`,
|
|
48
|
+
extensions: [...r.extensions!],
|
|
49
|
+
// A resolver's output is checked where it is used, so a bad entry is dropped rather than read as lines.
|
|
50
|
+
declarations(path: string, text: string): SymbolDeclaration[] {
|
|
51
|
+
const out = declarations(path, text);
|
|
52
|
+
if (!Array.isArray(out)) return [];
|
|
53
|
+
return out.filter(
|
|
54
|
+
(d) =>
|
|
55
|
+
d !== null &&
|
|
56
|
+
typeof d === "object" &&
|
|
57
|
+
typeof d.qualified === "string" &&
|
|
58
|
+
d.qualified !== "" &&
|
|
59
|
+
typeof d.kind === "string" &&
|
|
60
|
+
Number.isInteger(d.lines?.start) &&
|
|
61
|
+
Number.isInteger(d.lines?.end) &&
|
|
62
|
+
d.lines.start >= 1 &&
|
|
63
|
+
d.lines.end >= d.lines.start,
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The lexicon names a member's config declares: read statically when it can be, else by loading the config. */
|
|
70
|
+
async function memberLexicons(dir: string): Promise<string[]> {
|
|
71
|
+
const { readLexiconDeclarationsStatically } = await import("../config-static");
|
|
72
|
+
const read = readLexiconDeclarationsStatically(dir);
|
|
73
|
+
if (read.status === "no-config") return [];
|
|
74
|
+
if (read.status === "read") {
|
|
75
|
+
const { registerLexiconDeclarations } = await import("../lexicon-module");
|
|
76
|
+
const { dirname } = await import("node:path");
|
|
77
|
+
registerLexiconDeclarations(read.entries, dirname(read.configPath));
|
|
78
|
+
return lexiconNames(read.entries);
|
|
79
|
+
}
|
|
80
|
+
const { loadChantConfig } = await import("../config");
|
|
81
|
+
const { config } = await loadChantConfig(dir);
|
|
82
|
+
return lexiconNames(config.lexicons ?? []);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The symbol resolvers the lexicons of the member at `dir` (on disk) contribute, in config order. */
|
|
86
|
+
export async function memberSymbolResolvers(dir: string, load?: SymbolPluginLoader): Promise<MemberSymbolResolvers> {
|
|
87
|
+
const problems: string[] = [];
|
|
88
|
+
let names: string[];
|
|
89
|
+
try {
|
|
90
|
+
names = await memberLexicons(dir);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return { resolvers: [], problems: [`its chant.config could not be read: ${firstLine(err)}`] };
|
|
93
|
+
}
|
|
94
|
+
const loader = load ?? ((await import("../cli/plugins")).loadPlugin as SymbolPluginLoader);
|
|
95
|
+
const resolvers: SymbolResolver[] = [];
|
|
96
|
+
for (const name of names) {
|
|
97
|
+
let plugin: Awaited<ReturnType<SymbolPluginLoader>>;
|
|
98
|
+
try {
|
|
99
|
+
plugin = await loader(name);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
problems.push(`lexicon "${name}" could not be loaded: ${firstLine(err)}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (typeof plugin.symbolResolvers !== "function") continue;
|
|
105
|
+
let listed: unknown;
|
|
106
|
+
try {
|
|
107
|
+
listed = plugin.symbolResolvers();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
problems.push(`lexicon "${name}" symbolResolvers() threw: ${firstLine(err)}`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!Array.isArray(listed)) {
|
|
113
|
+
problems.push(`lexicon "${name}" symbolResolvers() did not return a list`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
for (const r of listed) {
|
|
117
|
+
const ok = checked(name, r, problems);
|
|
118
|
+
if (ok) resolvers.push(ok);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { resolvers, problems };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The member directory on disk for a member's `dir` under the workspace root. */
|
|
125
|
+
export const memberDirOnDisk = (root: string, dir: string): string => (dir === "." ? root : join(root, ...dir.split("/")));
|