@intentius/chant 0.91.0 → 0.93.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/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +6 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +2 -1
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +12 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/lexicon-module.d.ts +22 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +19 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.d.ts +2 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/symptoms.d.ts +51 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +4 -2
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.d.ts +14 -3
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
- package/dist/op/activities/activity-contracts.d.ts +201 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/apply.d.ts.map +1 -1
- package/dist/op/activities/change-coverage.d.ts +44 -0
- package/dist/op/activities/change-coverage.d.ts.map +1 -0
- package/dist/op/activities/converge.d.ts +24 -1
- package/dist/op/activities/converge.d.ts.map +1 -1
- package/dist/op/activities/decide.d.ts +107 -0
- package/dist/op/activities/decide.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +10 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/shell.d.ts +32 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/source-release.d.ts +86 -0
- package/dist/op/activities/source-release.d.ts.map +1 -0
- package/dist/op/activities/source-rollback.d.ts +100 -0
- package/dist/op/activities/source-rollback.d.ts.map +1 -0
- package/dist/op/activities/work-evidence.d.ts +42 -0
- package/dist/op/activities/work-evidence.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +1 -0
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/activity-profiles.d.ts +16 -0
- package/dist/op/activity-profiles.d.ts.map +1 -1
- package/dist/op/builders.d.ts +84 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/composites/converge-op.d.ts +31 -4
- package/dist/op/composites/converge-op.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts +116 -0
- package/dist/op/decide-backend.d.ts.map +1 -0
- package/dist/op/decide-config.d.ts +120 -0
- package/dist/op/decide-config.d.ts.map +1 -0
- package/dist/op/decide-read-inputs.d.ts +40 -0
- package/dist/op/decide-read-inputs.d.ts.map +1 -0
- package/dist/op/gate-wait.d.ts +43 -0
- package/dist/op/gate-wait.d.ts.map +1 -0
- package/dist/op/gate.d.ts +5 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +11 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +29 -4
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts +2 -0
- package/dist/op/local-output.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/operator.d.ts +97 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +37 -5
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/source-archive.d.ts +63 -0
- package/dist/op/source-archive.d.ts.map +1 -0
- package/dist/op/steward-points.d.ts +136 -0
- package/dist/op/steward-points.d.ts.map +1 -0
- package/dist/op/steward-turn.d.ts +53 -0
- package/dist/op/steward-turn.d.ts.map +1 -0
- package/dist/op/steward.d.ts +22 -1
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/types.d.ts +7 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts +1 -1
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
- package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
- package/dist/workspace/answers-ledger.d.ts +76 -0
- package/dist/workspace/answers-ledger.d.ts.map +1 -0
- package/dist/workspace/changes-cli.d.ts +16 -0
- package/dist/workspace/changes-cli.d.ts.map +1 -0
- package/dist/workspace/changes.d.ts +200 -0
- package/dist/workspace/changes.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
- package/dist/workspace/chant-migrations.d.ts +99 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -0
- package/dist/workspace/checks/boxes.d.ts +34 -1
- package/dist/workspace/checks/boxes.d.ts.map +1 -1
- package/dist/workspace/checks/diagrams.d.ts +31 -0
- package/dist/workspace/checks/diagrams.d.ts.map +1 -0
- package/dist/workspace/checks/records.d.ts +7 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +3 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +2 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +49 -4
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/declaration.d.ts +52 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +114 -1
- package/dist/workspace/declared-kinds.d.ts +20 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/generated-files.d.ts +7 -0
- package/dist/workspace/generated-files.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/lineage-check.d.ts +3 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +6 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +39 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/point-answer.schema.json +95 -0
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts +14 -2
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +11 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +1 -1
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +11 -5
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +34 -4
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/status-gates.d.ts +7 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +59 -1
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +16 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +113 -2
- package/dist/workspace/work-cli.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +74 -0
- package/dist/workspace/work-evidence.d.ts.map +1 -0
- package/dist/workspace/work.d.ts +43 -1
- package/dist/workspace/work.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/audit.test.ts +9 -1
- package/src/cli/commands/build.ts +3 -2
- package/src/cli/commands/lint.ts +7 -1
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +16 -2
- package/src/cli/handlers/operator.ts +34 -2
- package/src/cli/handlers/run.test.ts +154 -2
- package/src/cli/handlers/run.ts +121 -2
- package/src/cli/main.test.ts +10 -0
- package/src/cli/main.ts +19 -0
- package/src/cli/mcp/workspace-tools.test.ts +35 -19
- package/src/cli/mcp/workspace-tools.ts +62 -4
- package/src/cli/plugins.ts +11 -3
- package/src/cli/registry.ts +12 -2
- package/src/codegen/pinned-upgrade.ts +2 -1
- package/src/codegen/release-wiring.test.ts +49 -2
- package/src/components/capability-plugin-loader.ts +3 -3
- package/src/components/cli-support.ts +2 -2
- package/src/config.ts +11 -0
- package/src/lexicon-module.ts +56 -1
- package/src/lexicon-package.test.ts +96 -0
- package/src/lifecycle/converge-ledger.ts +20 -0
- package/src/lifecycle/run-ledger.ts +6 -0
- package/src/lifecycle/symptoms.ts +95 -0
- package/src/lint/rules/index.ts +6 -2
- package/src/lint/rules/op/index.ts +17 -4
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
- package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
- package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
- package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
- package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
- package/src/lint/rules/sys001-no-literal-key.ts +67 -0
- package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
- package/src/op/activities/activity-contracts.ts +190 -1
- package/src/op/activities/apply.ts +7 -6
- package/src/op/activities/change-coverage.ts +60 -0
- package/src/op/activities/converge.test.ts +55 -1
- package/src/op/activities/converge.ts +208 -29
- package/src/op/activities/decide.live.test.ts +78 -0
- package/src/op/activities/decide.test.ts +300 -0
- package/src/op/activities/decide.ts +267 -0
- package/src/op/activities/index.ts +21 -0
- package/src/op/activities/shell.test.ts +91 -1
- package/src/op/activities/shell.ts +91 -2
- package/src/op/activities/source-release.test.ts +118 -0
- package/src/op/activities/source-release.ts +168 -0
- package/src/op/activities/source-rollback.test.ts +131 -0
- package/src/op/activities/source-rollback.ts +239 -0
- package/src/op/activities/work-evidence.ts +62 -0
- package/src/op/activity-contract-registry.test.ts +2 -0
- package/src/op/activity-contract-registry.ts +2 -2
- package/src/op/activity-contract.test.ts +12 -0
- package/src/op/activity-contract.ts +6 -1
- package/src/op/activity-profiles.ts +29 -0
- package/src/op/activity-registry.test.ts +1 -1
- package/src/op/activity-registry.ts +2 -2
- package/src/op/builders.ts +119 -5
- package/src/op/command-gate.test.ts +192 -0
- package/src/op/composites/converge-op.test.ts +27 -1
- package/src/op/composites/converge-op.ts +69 -15
- package/src/op/decide-backend.test.ts +44 -0
- package/src/op/decide-backend.ts +212 -0
- package/src/op/decide-config.test.ts +52 -0
- package/src/op/decide-config.ts +80 -0
- package/src/op/decide-read-inputs.ts +117 -0
- package/src/op/gate-approval.ts +2 -2
- package/src/op/gate-wait.ts +57 -0
- package/src/op/gate.ts +8 -3
- package/src/op/generate-pipeline.ts +2 -2
- package/src/op/index.ts +18 -3
- package/src/op/local-executor.ts +151 -15
- package/src/op/local-output.test.ts +33 -0
- package/src/op/local-output.ts +20 -2
- package/src/op/op-ir.test.ts +10 -0
- package/src/op/op-ir.ts +4 -0
- package/src/op/op-verb-class.ts +1 -0
- package/src/op/operator.ts +290 -11
- package/src/op/runtime.ts +31 -8
- package/src/op/runtimes/local.ts +3 -1
- package/src/op/source-archive.ts +161 -0
- package/src/op/steward-points.test.ts +313 -0
- package/src/op/steward-points.ts +194 -0
- package/src/op/steward-turn.ts +79 -0
- package/src/op/steward.test.ts +87 -3
- package/src/op/steward.ts +25 -1
- package/src/op/types.ts +7 -0
- package/src/op/work-lease-run.test.ts +20 -1
- package/src/op/work-lease-run.ts +2 -2
- package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
- package/src/workspace/answers-ledger.ts +118 -0
- package/src/workspace/changes-cli.ts +56 -0
- package/src/workspace/changes.schema.json +472 -0
- package/src/workspace/changes.test.ts +315 -0
- package/src/workspace/changes.ts +512 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
- package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
- package/src/workspace/chant-migrations.ts +164 -0
- package/src/workspace/check-contract.test.ts +4 -2
- package/src/workspace/check.schema.json +15 -5
- package/src/workspace/checks/boxes.test.ts +80 -1
- package/src/workspace/checks/boxes.ts +136 -1
- package/src/workspace/checks/diagrams.test.ts +134 -0
- package/src/workspace/checks/diagrams.ts +108 -0
- package/src/workspace/checks/records.ts +47 -0
- package/src/workspace/checks.ts +8 -1
- package/src/workspace/compose-graph.ts +2 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
- package/src/workspace/conformance/index.ts +5 -0
- package/src/workspace/decide.test.ts +1 -1
- package/src/workspace/decide.ts +156 -23
- package/src/workspace/declaration.schema.json +114 -1
- package/src/workspace/declaration.test.ts +58 -0
- package/src/workspace/declaration.ts +104 -0
- package/src/workspace/declared-kinds.test.ts +5 -5
- package/src/workspace/declared-kinds.ts +46 -1
- package/src/workspace/generated-files.ts +14 -0
- package/src/workspace/graph-cli.ts +1 -1
- package/src/workspace/graph.schema.json +9 -1
- package/src/workspace/intent.schema.json +8 -2
- package/src/workspace/intent.ts +3 -10
- package/src/workspace/lineage-check.ts +16 -1
- package/src/workspace/lineage-upgrade-cli.ts +1 -1
- package/src/workspace/lineage-upgrade.test.ts +70 -0
- package/src/workspace/lineage-upgrade.ts +81 -10
- package/src/workspace/ls-contract.test.ts +57 -0
- package/src/workspace/ls.schema.json +46 -1
- package/src/workspace/ls.ts +62 -3
- package/src/workspace/points-cli.ts +14 -3
- package/src/workspace/points-write.schema.json +11 -1
- package/src/workspace/points.schema.json +33 -0
- package/src/workspace/points.test.ts +1 -1
- package/src/workspace/points.ts +1 -1
- package/src/workspace/read-contract.test.ts +1 -1
- package/src/workspace/reason-codes.test.ts +11 -1
- package/src/workspace/reason-codes.ts +18 -2
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-cli.ts +13 -3
- package/src/workspace/records-close.schema.json +2 -0
- package/src/workspace/records-new.schema.json +2 -0
- package/src/workspace/records-review.schema.json +2 -0
- package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
- package/src/workspace/records-sessions-write.test.ts +2 -34
- package/src/workspace/records-write.e2e.test.ts +108 -0
- package/src/workspace/records-write.test.ts +32 -41
- package/src/workspace/records-write.ts +33 -15
- package/src/workspace/records.schema.json +36 -2
- package/src/workspace/records.test.ts +48 -3
- package/src/workspace/records.ts +109 -3
- package/src/workspace/status-contract.test.ts +13 -8
- package/src/workspace/status-gates.ts +9 -4
- package/src/workspace/status-stewards.ts +108 -4
- package/src/workspace/status.schema.json +111 -7
- package/src/workspace/status.ts +36 -1
- package/src/workspace/work-acceptance.test.ts +274 -0
- package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
- package/src/workspace/work-cli.ts +193 -10
- package/src/workspace/work-evidence.schema.json +212 -0
- package/src/workspace/work-evidence.ts +169 -0
- package/src/workspace/work-history.schema.json +349 -0
- package/src/workspace/work-history.test.ts +156 -0
- package/src/workspace/work.test.ts +3 -2
- package/src/workspace/work.ts +100 -1
- /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
- /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
- /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migrations chant ships (#2737).
|
|
3
|
+
*
|
|
4
|
+
* A template's own migrations (./lineage-migrations.ts) move a scope between
|
|
5
|
+
* two versions of that template. Some moves are chant's instead: a part a
|
|
6
|
+
* template used goes away, and chant knows where each piece of it went. The
|
|
7
|
+
* first is `chud-lexicon-exit`, which takes a repo made from chud's template
|
|
8
|
+
* (or the studio kit's copy of it) off `@intentius/chant-lexicon-chud` and
|
|
9
|
+
* `@intentius/chud-runtime` (ws-056).
|
|
10
|
+
*
|
|
11
|
+
* `chant workspace upgrade` asks each one for a plan after the template's own
|
|
12
|
+
* chain has run in the staging worktree, and before the per-file merge. A
|
|
13
|
+
* migration plans from what the scope holds, not from a version range, so it
|
|
14
|
+
* works whatever ref the scope is pinned at. Its plan lists every file it
|
|
15
|
+
* writes or deletes and why, what it cannot move and where that went, and any
|
|
16
|
+
* conflict. A plan with a conflict is not applied at all, and the upgrade's
|
|
17
|
+
* checks fail with it, so the dry run shows the whole plan and the tree is
|
|
18
|
+
* never left half moved.
|
|
19
|
+
*
|
|
20
|
+
* An applied migration's id is added to the lineage's `migrations`, so the
|
|
21
|
+
* lock records it and a second upgrade does not plan it again. A migration's
|
|
22
|
+
* plan is also empty once nothing it moves is left, so running it twice
|
|
23
|
+
* changes nothing either way.
|
|
24
|
+
*
|
|
25
|
+
* Files the migration rewrites keep their lineage entry: the lock's hash is
|
|
26
|
+
* still the template's, so to a later upgrade they are the project's own
|
|
27
|
+
* edits, merged per file like any other (ws-005). A deleted file keeps its
|
|
28
|
+
* entry too, so a template version that still has it leaves it deleted.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { dirname, join } from "node:path";
|
|
33
|
+
import type { Lineage } from "./lineage-lock";
|
|
34
|
+
import { chudLexiconExit } from "./chant-migrations/chud-lexicon-exit";
|
|
35
|
+
import { chudLexiconExitRollback } from "./chant-migrations/chud-lexicon-exit-rollback";
|
|
36
|
+
import { chudLexiconExitShipInputs } from "./chant-migrations/chud-lexicon-exit-ship-inputs";
|
|
37
|
+
import { chudLexiconExitShipPolicy } from "./chant-migrations/chud-lexicon-exit-ship-policy";
|
|
38
|
+
import { chudLexiconExitFlySite } from "./chant-migrations/chud-lexicon-exit-fly-site";
|
|
39
|
+
import { chudLexiconExitLiveNames } from "./chant-migrations/chud-lexicon-exit-live-names";
|
|
40
|
+
|
|
41
|
+
/** One file a plan writes or deletes, relative to the scope. */
|
|
42
|
+
export interface PlannedChange {
|
|
43
|
+
path: string;
|
|
44
|
+
action: "write" | "delete";
|
|
45
|
+
/** Why, in a few words. */
|
|
46
|
+
why: string;
|
|
47
|
+
/** The new content, for a write. */
|
|
48
|
+
data?: Buffer;
|
|
49
|
+
/** True when the file had been edited since the template wrote it; the edit stays in git history. */
|
|
50
|
+
edited?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A part the migration does not move, and where it went instead. */
|
|
54
|
+
export interface NotMoved {
|
|
55
|
+
what: string;
|
|
56
|
+
where: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Why a plan cannot be applied. */
|
|
60
|
+
export interface PlanConflict {
|
|
61
|
+
path: string;
|
|
62
|
+
reason: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ChantMigrationPlan {
|
|
66
|
+
id: string;
|
|
67
|
+
description: string;
|
|
68
|
+
changes: PlannedChange[];
|
|
69
|
+
notMoved: NotMoved[];
|
|
70
|
+
conflicts: PlanConflict[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ChantMigrationContext {
|
|
74
|
+
/** The scope directory (in the staging worktree). */
|
|
75
|
+
dir: string;
|
|
76
|
+
lineage: Lineage;
|
|
77
|
+
/** The chant version doing the upgrade, for dependency ranges. */
|
|
78
|
+
chantVersion: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ChantMigration {
|
|
82
|
+
id: string;
|
|
83
|
+
description: string;
|
|
84
|
+
/** The plan for this scope, or null when there is nothing here for this migration to move. */
|
|
85
|
+
plan(ctx: ChantMigrationContext): ChantMigrationPlan | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Every migration chant ships, in the order they are planned. */
|
|
89
|
+
export const CHANT_MIGRATIONS: readonly ChantMigration[] = [
|
|
90
|
+
chudLexiconExit,
|
|
91
|
+
chudLexiconExitRollback,
|
|
92
|
+
chudLexiconExitShipInputs,
|
|
93
|
+
chudLexiconExitShipPolicy,
|
|
94
|
+
chudLexiconExitFlySite,
|
|
95
|
+
chudLexiconExitLiveNames,
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
/** The plans for a scope: each migration not yet in the lineage that finds something to move. */
|
|
99
|
+
export function planChantMigrations(ctx: ChantMigrationContext, migrations: readonly ChantMigration[] = CHANT_MIGRATIONS): ChantMigrationPlan[] {
|
|
100
|
+
const applied = new Set(ctx.lineage.migrations);
|
|
101
|
+
const plans: ChantMigrationPlan[] = [];
|
|
102
|
+
for (const m of migrations) {
|
|
103
|
+
if (applied.has(m.id)) continue;
|
|
104
|
+
const plan = m.plan(ctx);
|
|
105
|
+
if (plan && (plan.changes.length > 0 || plan.conflicts.length > 0)) plans.push(plan);
|
|
106
|
+
}
|
|
107
|
+
return plans;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Plan each migration in order and apply each plan with no conflict before
|
|
112
|
+
* the next is planned, so a migration planned after another sees the tree
|
|
113
|
+
* the earlier one left (#2800: `chud-lexicon-exit-rollback` after
|
|
114
|
+
* `chud-lexicon-exit`). A plan with a conflict is not applied, and the ones
|
|
115
|
+
* after it plan from the tree as it is.
|
|
116
|
+
*/
|
|
117
|
+
export function runChantMigrations(ctx: ChantMigrationContext, migrations: readonly ChantMigration[] = CHANT_MIGRATIONS): Array<{ plan: ChantMigrationPlan; applied: boolean }> {
|
|
118
|
+
const out: Array<{ plan: ChantMigrationPlan; applied: boolean }> = [];
|
|
119
|
+
for (const m of migrations) {
|
|
120
|
+
if (ctx.lineage.migrations.includes(m.id)) continue;
|
|
121
|
+
const plan = m.plan(ctx);
|
|
122
|
+
if (!plan || (plan.changes.length === 0 && plan.conflicts.length === 0)) continue;
|
|
123
|
+
const applied = plan.conflicts.length === 0;
|
|
124
|
+
if (applied) applyChantMigration(plan, ctx.dir, ctx.lineage);
|
|
125
|
+
out.push({ plan, applied });
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Apply a plan with no conflicts, and record its id in the lineage. */
|
|
131
|
+
export function applyChantMigration(plan: ChantMigrationPlan, dir: string, lineage: Lineage): void {
|
|
132
|
+
if (plan.conflicts.length > 0) throw new Error(`${plan.id} has conflicts and cannot be applied`);
|
|
133
|
+
for (const c of plan.changes) {
|
|
134
|
+
const abs = join(dir, c.path);
|
|
135
|
+
if (c.action === "delete") {
|
|
136
|
+
rmSync(abs, { force: true });
|
|
137
|
+
} else {
|
|
138
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
139
|
+
writeFileSync(abs, c.data!);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!lineage.migrations.includes(plan.id)) lineage.migrations.push(plan.id);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** A plan as the upgrade reports it: no file contents. */
|
|
146
|
+
export interface ChantMigrationReport {
|
|
147
|
+
id: string;
|
|
148
|
+
description: string;
|
|
149
|
+
applied: boolean;
|
|
150
|
+
changes: Array<Omit<PlannedChange, "data">>;
|
|
151
|
+
notMoved: NotMoved[];
|
|
152
|
+
conflicts: PlanConflict[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function reportPlan(plan: ChantMigrationPlan, applied: boolean): ChantMigrationReport {
|
|
156
|
+
return {
|
|
157
|
+
id: plan.id,
|
|
158
|
+
description: plan.description,
|
|
159
|
+
applied,
|
|
160
|
+
changes: plan.changes.map(({ data: _d, ...rest }) => rest),
|
|
161
|
+
notMoved: plan.notMoved,
|
|
162
|
+
conflicts: plan.conflicts,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -11,6 +11,8 @@ import { afterAll, describe, expect, test } from "vitest";
|
|
|
11
11
|
import { cleanScratch, commitAll, contract, declaration, git, REPO, repo, scratchDir, validSchema } from "./__fixtures__/contract-repo";
|
|
12
12
|
import schema from "./check.schema.json";
|
|
13
13
|
import { BOX_FINDING_CODES } from "./checks/boxes";
|
|
14
|
+
import { RECORD_FINDING_CODES } from "./checks/records";
|
|
15
|
+
import { DIAGRAM_FINDING_CODES } from "./checks/diagrams";
|
|
14
16
|
import { WORKSPACE_ERROR_CODES } from "./declaration";
|
|
15
17
|
import { CHECK_CODES, CHECK_CONTRACT_VERSION, CHECK_ERROR_CODES, CHECK_OUTPUT_SCHEMA_ID, runChecks, type CheckDocument } from "./lineage-check";
|
|
16
18
|
|
|
@@ -36,8 +38,8 @@ describe("check output schema", () => {
|
|
|
36
38
|
test("lists exactly the codes the code can return", () => {
|
|
37
39
|
expect(schema.$defs.lockFinding.properties.code.enum).toEqual([...CHECK_CODES]);
|
|
38
40
|
expect(schema.$defs.failure.properties.error.properties.code.enum).toEqual([...CHECK_ERROR_CODES]);
|
|
39
|
-
expect(schema.$defs.diagnostic.properties.code.enum).toEqual([...WORKSPACE_ERROR_CODES, ...BOX_FINDING_CODES]);
|
|
40
|
-
expect(schema.$defs.suppressed.properties.code.enum).toEqual([...WORKSPACE_ERROR_CODES, ...BOX_FINDING_CODES]);
|
|
41
|
+
expect(schema.$defs.diagnostic.properties.code.enum).toEqual([...WORKSPACE_ERROR_CODES, ...BOX_FINDING_CODES, ...RECORD_FINDING_CODES, ...DIAGRAM_FINDING_CODES]);
|
|
42
|
+
expect(schema.$defs.suppressed.properties.code.enum).toEqual([...WORKSPACE_ERROR_CODES, ...BOX_FINDING_CODES, ...RECORD_FINDING_CODES, ...DIAGRAM_FINDING_CODES]);
|
|
41
43
|
});
|
|
42
44
|
});
|
|
43
45
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://intentius.io/chant/schemas/workspace/check/v1/check.schema.json",
|
|
4
4
|
"title": "chant workspace check output",
|
|
5
|
-
"description": "What `chant workspace check --format json` prints, version 1 of the read contract for checking a workspace (#2524 D9, D15, D16, #2535, #2536). The lineage lock findings carry reason codes; the declaration findings carry WSP ids from the check catalog, WSP001, a declaration that can't be read, also carries the declaration's reason code, and the box checks WSP121 to
|
|
5
|
+
"description": "What `chant workspace check --format json` prints, version 1 of the read contract for checking a workspace (#2524 D9, D15, D16, #2535, #2536). The lineage lock findings carry reason codes; the declaration findings carry WSP ids from the check catalog, WSP001, a declaration that can't be read, also carries the declaration's reason code, and the box checks WSP121 to WSP125 carry theirs. Readers ignore fields they do not know; a field is only ever added within a version. The reason and error codes are closed lists: a new code is a new contract version. Contract version 1 is written by chant 0.81.0 and newer; a reader that needs it refuses output whose `contract` it doesn't know. Every code is in the one closed list of `reason-codes.ts`.",
|
|
6
6
|
"oneOf": [
|
|
7
7
|
{
|
|
8
8
|
"$ref": "#/$defs/result"
|
|
@@ -226,9 +226,14 @@
|
|
|
226
226
|
"box-credential-declared",
|
|
227
227
|
"box-capability-unbrokered",
|
|
228
228
|
"box-isolation-collision",
|
|
229
|
-
"box-isolation-literal"
|
|
229
|
+
"box-isolation-literal",
|
|
230
|
+
"box-fountain-callback-undeclared",
|
|
231
|
+
"work-acceptance-unmet",
|
|
232
|
+
"diagram-source-missing",
|
|
233
|
+
"diagram-render-missing",
|
|
234
|
+
"diagram-render-drift"
|
|
230
235
|
],
|
|
231
|
-
"description": "For WSP001: why the declaration can't be read. For WSP121 to
|
|
236
|
+
"description": "For WSP001: why the declaration can't be read. For WSP121 to WSP125, the box finding (#2726, #2727, #2780). For WSP117, work-acceptance-unmet (#2772). For WSP131 to WSP133, the diagram finding (#2764)."
|
|
232
237
|
}
|
|
233
238
|
}
|
|
234
239
|
},
|
|
@@ -297,9 +302,14 @@
|
|
|
297
302
|
"box-credential-declared",
|
|
298
303
|
"box-capability-unbrokered",
|
|
299
304
|
"box-isolation-collision",
|
|
300
|
-
"box-isolation-literal"
|
|
305
|
+
"box-isolation-literal",
|
|
306
|
+
"box-fountain-callback-undeclared",
|
|
307
|
+
"work-acceptance-unmet",
|
|
308
|
+
"diagram-source-missing",
|
|
309
|
+
"diagram-render-missing",
|
|
310
|
+
"diagram-render-drift"
|
|
301
311
|
],
|
|
302
|
-
"description": "For WSP001: why the declaration can't be read. For WSP121 to
|
|
312
|
+
"description": "For WSP001: why the declaration can't be read. For WSP121 to WSP125, the box finding (#2726, #2727, #2780). For WSP117, work-acceptance-unmet (#2772). For WSP131 to WSP133, the diagram finding (#2764)."
|
|
303
313
|
},
|
|
304
314
|
"reason": {
|
|
305
315
|
"type": "string"
|
|
@@ -7,7 +7,7 @@ import { afterAll, describe, expect, test } from "vitest";
|
|
|
7
7
|
import { runDeclarationChecks } from "../checks";
|
|
8
8
|
import { parseDeclaration, WorkspaceReadError } from "../declaration";
|
|
9
9
|
import { gitTree } from "../tree";
|
|
10
|
-
import { isCredentialKey, isSecretReference, literalSecretsInCode, literalSecretsInText } from "./boxes";
|
|
10
|
+
import { fountainBoxCall, isCredentialKey, isSecretReference, literalSecretsInCode, literalSecretsInText } from "./boxes";
|
|
11
11
|
|
|
12
12
|
const scratch: string[] = [];
|
|
13
13
|
afterAll(() => {
|
|
@@ -195,3 +195,82 @@ describe("what counts as a literal secret", () => {
|
|
|
195
195
|
expect(literalSecretsInText(`# nothing\nkey: ${GITHUB_TOKEN}\n`).map((s) => s.line)).toEqual([2]);
|
|
196
196
|
});
|
|
197
197
|
});
|
|
198
|
+
|
|
199
|
+
describe("box-fountain-callback-undeclared (WSP125, #2780)", () => {
|
|
200
|
+
/** A studio-style fountain Box declaration. */
|
|
201
|
+
const fountainBox = `import { readFileSync } from "node:fs";
|
|
202
|
+
import { Box } from "@intentius/chant-lexicon-fountain";
|
|
203
|
+
|
|
204
|
+
export const { environment, agent } = Box({
|
|
205
|
+
name: "studio-box",
|
|
206
|
+
setupScript: readFileSync("box/setup.sh", "utf8"),
|
|
207
|
+
model: "anthropic/claude-sonnet-4-6",
|
|
208
|
+
permissionPolicy: { default: "auto_allow" },
|
|
209
|
+
});
|
|
210
|
+
`;
|
|
211
|
+
const CALLBACK = { name: "fountain-callback", broker: "fountain", scope: ["owner"] };
|
|
212
|
+
|
|
213
|
+
test("a box member that builds a fountain Box and does not declare the callback token fails, at the Box call", async () => {
|
|
214
|
+
const root = repo({ "chant.workspace.json": declaration(BROKERED), "spec/box.ts": fountainBox });
|
|
215
|
+
const d = await found(root);
|
|
216
|
+
expect(d.map((x) => [x.ruleId, x.code, x.entity, x.file, x.line])).toEqual([
|
|
217
|
+
["WSP125", "box-fountain-callback-undeclared", "spec", "spec/box.ts", 4],
|
|
218
|
+
]);
|
|
219
|
+
expect(d[0].message).toContain("callback token scoped to its owner (FOUNTAIN_TOKEN); the member's box block does not declare it");
|
|
220
|
+
expect(d[0].message).toContain('{ "name": "fountain-callback", "broker": "fountain", "scope": ["owner"] }');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("declaring fountain-callback, brokered by fountain with scope owner, passes, beside a fountain capability a lobby brokers", async () => {
|
|
224
|
+
const root = repo({
|
|
225
|
+
"chant.workspace.json": declaration({
|
|
226
|
+
capabilities: [...BROKERED.capabilities, { name: "fountain", broker: "lobby", scope: ["agent", "conversations"] }, CALLBACK],
|
|
227
|
+
}),
|
|
228
|
+
"spec/box.ts": fountainBox,
|
|
229
|
+
});
|
|
230
|
+
expect(await found(root)).toEqual([]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("fountain-callback behind another broker, or without owner in its scope, fails", async () => {
|
|
234
|
+
const lobby = repo({
|
|
235
|
+
"chant.workspace.json": declaration({ capabilities: [{ name: "fountain-callback", broker: "lobby", scope: ["conversations"] }] }),
|
|
236
|
+
"spec/box.ts": fountainBox,
|
|
237
|
+
});
|
|
238
|
+
const d = await found(lobby);
|
|
239
|
+
expect(d.map((x) => [x.ruleId, x.file])).toEqual([["WSP125", "spec/box.ts"]]);
|
|
240
|
+
expect(d[0].message).toContain("names the broker lobby, but fountain hands this token to the sandbox itself");
|
|
241
|
+
const narrow = repo({
|
|
242
|
+
"chant.workspace.json": declaration({ capabilities: [{ name: "fountain-callback", broker: "fountain", scope: ["conversations"] }] }),
|
|
243
|
+
"spec/box.ts": fountainBox,
|
|
244
|
+
});
|
|
245
|
+
expect((await found(narrow))[0].message).toContain("has scope [conversations], not owner");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("a box member that builds no fountain Box, and a member with no box block, are not flagged", async () => {
|
|
249
|
+
const other = repo({ "chant.workspace.json": declaration(BROKERED), "spec/box.ts": spec("${ANTHROPIC_API_KEY}") });
|
|
250
|
+
expect(await found(other)).toEqual([]);
|
|
251
|
+
const plain = repo({
|
|
252
|
+
"chant.workspace.json": JSON.stringify({ name: "acme", schema: 1, members: [{ name: "spec", dir: "spec", kind: "other", because: "x" }] }),
|
|
253
|
+
"spec/box.ts": fountainBox,
|
|
254
|
+
});
|
|
255
|
+
expect(await found(plain)).toEqual([]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("it can be turned down while fountain has no way to drop the token (managoat/fountain#2497)", async () => {
|
|
259
|
+
const root = repo({
|
|
260
|
+
"chant.workspace.json": declaration(BROKERED, { suppress: [{ check: "WSP125", because: "waiting on fountain" }] }),
|
|
261
|
+
"spec/box.ts": fountainBox,
|
|
262
|
+
});
|
|
263
|
+
expect((await found(root)).filter((x) => x.ruleId === "WSP125")).toEqual([]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("fountainBoxCall finds the composite imported by name, aliased or through a namespace, and nothing else", () => {
|
|
267
|
+
const at = (text: string) => fountainBoxCall(text, "box.ts", ts.ScriptKind.TS);
|
|
268
|
+
expect(at(fountainBox)).toEqual({ line: 4, column: 39 });
|
|
269
|
+
expect(at(`import { Box as FBox } from "@intentius/chant-lexicon-fountain";\nFBox({});\n`)).toEqual({ line: 2, column: 1 });
|
|
270
|
+
expect(at(`import * as fountain from "@intentius/chant-lexicon-fountain";\nfountain.Box({});\n`)).toEqual({ line: 2, column: 1 });
|
|
271
|
+
// Imported but never called, another package's Box, or a Box of one's own.
|
|
272
|
+
expect(at(`import { Box } from "@intentius/chant-lexicon-fountain";\nexport { Box };\n`)).toBeUndefined();
|
|
273
|
+
expect(at(`import { Box } from "@intentius/chant-lexicon-chaff";\nBox({});\n`)).toBeUndefined();
|
|
274
|
+
expect(at(`const Box = (o: object) => o;\nBox({}); // @intentius/chant-lexicon-fountain\n`)).toBeUndefined();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -26,6 +26,17 @@
|
|
|
26
26
|
*
|
|
27
27
|
* WSP123 and WSP124, box isolation (#2727), are in `./box-isolation.ts` and
|
|
28
28
|
* listed here after these two.
|
|
29
|
+
*
|
|
30
|
+
* WSP125 (`box-fountain-callback-undeclared`, #2780) is the one credential a
|
|
31
|
+
* box gets without asking. fountain v0.21.0 gives every persistent sandbox a
|
|
32
|
+
* callback token scoped to its owner (`FOUNTAIN_TOKEN`, `sandbox_api_access:
|
|
33
|
+
* owner`), and a fountain `Box` is always persistent, so its member's block
|
|
34
|
+
* has to say so: the capability `fountain-callback`, brokered by `fountain`,
|
|
35
|
+
* with scope `owner`. It is named apart from `fountain`, the API access a
|
|
36
|
+
* lobby may broker for the same box. A member builds a Box when one of its TypeScript or JavaScript files
|
|
37
|
+
* imports `Box` from `@intentius/chant-lexicon-fountain` and calls it, read as
|
|
38
|
+
* syntax and never run. Upstream, managoat/fountain#2497 asks for a way to run
|
|
39
|
+
* a persistent agent with no callback token.
|
|
29
40
|
*/
|
|
30
41
|
|
|
31
42
|
import * as ts from "typescript";
|
|
@@ -36,10 +47,77 @@ import { joinPath, skippedDir, type WorkspaceTree } from "../tree";
|
|
|
36
47
|
import { BOX_ISOLATION_CHECKS } from "./box-isolation";
|
|
37
48
|
|
|
38
49
|
/** The read contract's codes for the box findings, carried as `code` on each. */
|
|
39
|
-
export const BOX_FINDING_CODES = [
|
|
50
|
+
export const BOX_FINDING_CODES = [
|
|
51
|
+
"box-credential-declared",
|
|
52
|
+
"box-capability-unbrokered",
|
|
53
|
+
"box-isolation-collision",
|
|
54
|
+
"box-isolation-literal",
|
|
55
|
+
"box-fountain-callback-undeclared",
|
|
56
|
+
] as const satisfies readonly ReasonCode[];
|
|
40
57
|
|
|
41
58
|
export const WSP_BOX_CREDENTIAL = "WSP121";
|
|
42
59
|
export const WSP_BOX_UNBROKERED = "WSP122";
|
|
60
|
+
export const WSP_BOX_FOUNTAIN_CALLBACK = "WSP125";
|
|
61
|
+
|
|
62
|
+
/** The fountain lexicon's package, whose `Box` composite runs a persistent sandbox. */
|
|
63
|
+
const FOUNTAIN_LEXICON = "@intentius/chant-lexicon-fountain";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The capability a box block declares for fountain's callback token (#2780).
|
|
67
|
+
* The fountain lexicon exports the same value as `BOX_FOUNTAIN_CALLBACK_CAPABILITY`.
|
|
68
|
+
*/
|
|
69
|
+
export const FOUNTAIN_CALLBACK_CAPABILITY = { name: "fountain-callback", broker: "fountain", scope: ["owner"] } as const;
|
|
70
|
+
|
|
71
|
+
/** Where a file calls the fountain lexicon's `Box`, 1-based. */
|
|
72
|
+
export interface FountainBoxCall {
|
|
73
|
+
line: number;
|
|
74
|
+
column: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The first call of the fountain lexicon's `Box` in a TypeScript or
|
|
79
|
+
* JavaScript file, read as syntax and never run: `Box` imported by name
|
|
80
|
+
* (aliased or not) and called, or called through a namespace import. Any
|
|
81
|
+
* other `Box` is not the composite.
|
|
82
|
+
*/
|
|
83
|
+
export function fountainBoxCall(text: string, file: string, kind: ts.ScriptKind): FountainBoxCall | undefined {
|
|
84
|
+
if (!text.includes(FOUNTAIN_LEXICON)) return undefined;
|
|
85
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, kind);
|
|
86
|
+
const names = new Set<string>();
|
|
87
|
+
const namespaces = new Set<string>();
|
|
88
|
+
for (const stmt of source.statements) {
|
|
89
|
+
if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
90
|
+
const from = stmt.moduleSpecifier.text;
|
|
91
|
+
if (from !== FOUNTAIN_LEXICON && !from.startsWith(`${FOUNTAIN_LEXICON}/`)) continue;
|
|
92
|
+
const bindings = stmt.importClause?.namedBindings;
|
|
93
|
+
if (!bindings) continue;
|
|
94
|
+
if (ts.isNamespaceImport(bindings)) namespaces.add(bindings.name.text);
|
|
95
|
+
else for (const el of bindings.elements) if ((el.propertyName ?? el.name).text === "Box") names.add(el.name.text);
|
|
96
|
+
}
|
|
97
|
+
if (names.size === 0 && namespaces.size === 0) return undefined;
|
|
98
|
+
let found: ts.Node | undefined;
|
|
99
|
+
const visit = (node: ts.Node) => {
|
|
100
|
+
if (found) return;
|
|
101
|
+
if (ts.isCallExpression(node)) {
|
|
102
|
+
const callee = node.expression;
|
|
103
|
+
if (
|
|
104
|
+
(ts.isIdentifier(callee) && names.has(callee.text)) ||
|
|
105
|
+
(ts.isPropertyAccessExpression(callee) &&
|
|
106
|
+
callee.name.text === "Box" &&
|
|
107
|
+
ts.isIdentifier(callee.expression) &&
|
|
108
|
+
namespaces.has(callee.expression.text))
|
|
109
|
+
) {
|
|
110
|
+
found = node;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
ts.forEachChild(node, visit);
|
|
115
|
+
};
|
|
116
|
+
visit(source);
|
|
117
|
+
if (!found) return undefined;
|
|
118
|
+
const { line, character } = source.getLineAndCharacterOfPosition(found.getStart(source));
|
|
119
|
+
return { line: line + 1, column: character + 1 };
|
|
120
|
+
}
|
|
43
121
|
|
|
44
122
|
/** Values whose shape alone says they are a credential. The list FTN001 uses, plus Anthropic keys. */
|
|
45
123
|
export const CREDENTIAL_SHAPES: readonly { pattern: RegExp; label: string }[] = [
|
|
@@ -304,4 +382,61 @@ export const BOX_CHECKS: readonly WorkspaceCheck[] = [
|
|
|
304
382
|
},
|
|
305
383
|
// Box isolation (#2727), WSP123 and WSP124.
|
|
306
384
|
...BOX_ISOLATION_CHECKS,
|
|
385
|
+
{
|
|
386
|
+
id: WSP_BOX_FOUNTAIN_CALLBACK,
|
|
387
|
+
name: "box-fountain-callback-undeclared",
|
|
388
|
+
description:
|
|
389
|
+
"A box member that builds a fountain Box declares the callback token fountain gives its persistent sandbox: the fountain-callback capability, brokered by fountain, with scope owner.",
|
|
390
|
+
severity: "error",
|
|
391
|
+
configurable: true,
|
|
392
|
+
check(ctx) {
|
|
393
|
+
const out: WorkspaceDiagnostic[] = [];
|
|
394
|
+
const want = FOUNTAIN_CALLBACK_CAPABILITY;
|
|
395
|
+
const expected = `{ "name": "${want.name}", "broker": "${want.broker}", "scope": ["${want.scope[0]}"] }`;
|
|
396
|
+
for (const m of boxMembers(ctx)) {
|
|
397
|
+
const dir = m.dir === "." ? "" : m.dir;
|
|
398
|
+
if (ctx.tree.stat(dir) !== "dir") continue;
|
|
399
|
+
let at: { path: string; call: FountainBoxCall } | undefined;
|
|
400
|
+
for (const path of filesUnder(ctx.tree, dir)) {
|
|
401
|
+
const kind = CODE_EXTENSIONS[extension(path)];
|
|
402
|
+
if (kind === undefined || kind === ts.ScriptKind.JSON) continue;
|
|
403
|
+
let text: string;
|
|
404
|
+
try {
|
|
405
|
+
text = ctx.tree.read(path);
|
|
406
|
+
} catch {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (text.length > MAX_FILE_BYTES) continue;
|
|
410
|
+
const call = fountainBoxCall(text, path, kind);
|
|
411
|
+
if (call) {
|
|
412
|
+
at = { path, call };
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!at) continue;
|
|
417
|
+
const declared = m.box!.capabilities.find((c) => c.name === want.name);
|
|
418
|
+
if (declared && declared.broker === want.broker && declared.scope.includes(want.scope[0])) continue;
|
|
419
|
+
const why = !declared
|
|
420
|
+
? "the member's box block does not declare it"
|
|
421
|
+
: declared.broker !== want.broker
|
|
422
|
+
? `the box block's ${want.name} capability names the broker ${declared.broker ?? "(none)"}, but fountain hands this token to the sandbox itself`
|
|
423
|
+
: `the box block's ${want.name} capability has scope [${declared.scope.join(", ")}], not ${want.scope[0]}`;
|
|
424
|
+
out.push({
|
|
425
|
+
checkId: this.id,
|
|
426
|
+
severity: this.severity,
|
|
427
|
+
code: "box-fountain-callback-undeclared",
|
|
428
|
+
message:
|
|
429
|
+
`box-fountain-callback-undeclared: member ${m.name} builds a fountain Box (${at.path}), and fountain v0.21.0 gives a persistent box's sandbox ` +
|
|
430
|
+
`a callback token scoped to its owner (FOUNTAIN_TOKEN); ${why}. Declare ${expected} in the box block ` +
|
|
431
|
+
`(managoat/fountain#2497 tracks running without it)`,
|
|
432
|
+
entity: m.name,
|
|
433
|
+
pointer: declared?.pointer ?? m.box!.pointer,
|
|
434
|
+
treeFile: at.path,
|
|
435
|
+
line: at.call.line,
|
|
436
|
+
column: at.call.column,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
},
|
|
441
|
+
},
|
|
307
442
|
];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The diagram checks, WSP131 to WSP133 (#2764).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
10
|
+
import { runDeclarationChecks } from "../checks";
|
|
11
|
+
import { sha256Hex } from "../../content-digest";
|
|
12
|
+
|
|
13
|
+
const scratch: string[] = [];
|
|
14
|
+
afterAll(() => {
|
|
15
|
+
for (const d of scratch) rmSync(d, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
function repo(files: Record<string, string>): string {
|
|
18
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-diagrams-")));
|
|
19
|
+
scratch.push(root);
|
|
20
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
21
|
+
for (const [path, text] of Object.entries(files)) {
|
|
22
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
23
|
+
writeFileSync(join(root, path), text);
|
|
24
|
+
}
|
|
25
|
+
return root;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const renderer = { tool: "d2", version: "0.9.0", args: ["--layout=elk", "--theme=0", "--pad=40", "--omit-version"] };
|
|
29
|
+
const diagram = (extra: Record<string, unknown> = {}) => ({
|
|
30
|
+
name: "architecture",
|
|
31
|
+
title: "Studio architecture",
|
|
32
|
+
source: "docs/diagrams/architecture.d2",
|
|
33
|
+
render: "docs/diagrams/architecture.svg",
|
|
34
|
+
renderer,
|
|
35
|
+
...extra,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const declaration = (diagrams: unknown[], extra: Record<string, unknown> = {}) =>
|
|
39
|
+
JSON.stringify({ name: "acme", schema: 1, members: [{ name: "docs", dir: "docs", kind: "other", because: "the docs site", diagrams }], ...extra }, null, 2);
|
|
40
|
+
|
|
41
|
+
/** The findings, less WSP009, which every member of kind other gets. */
|
|
42
|
+
const found = async (root: string) => (await runDeclarationChecks(root, (f) => f, { gather: false })).diagnostics.filter((d) => d.ruleId !== "WSP009");
|
|
43
|
+
|
|
44
|
+
describe("diagram-source-missing (WSP131) and diagram-render-missing (WSP132)", () => {
|
|
45
|
+
test("both files present passes", async () => {
|
|
46
|
+
const root = repo({
|
|
47
|
+
"chant.workspace.json": declaration([diagram()]),
|
|
48
|
+
"docs/diagrams/architecture.d2": "x -> y",
|
|
49
|
+
"docs/diagrams/architecture.svg": "<svg></svg>",
|
|
50
|
+
});
|
|
51
|
+
expect(await found(root)).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("a missing source fails WSP131, and a missing render fails WSP132, each once", async () => {
|
|
55
|
+
const root = repo({
|
|
56
|
+
"chant.workspace.json": declaration([diagram()]),
|
|
57
|
+
"docs/diagrams/architecture.svg": "<svg></svg>",
|
|
58
|
+
});
|
|
59
|
+
const d = await found(root);
|
|
60
|
+
expect(d.map((x) => [x.ruleId, x.code, x.entity])).toEqual([["WSP131", "diagram-source-missing", "docs"]]);
|
|
61
|
+
expect(d[0].message).toContain("member docs's diagram architecture names the source docs/diagrams/architecture.d2, which does not exist");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("no source (an SVG with none) is never checked for a source", async () => {
|
|
65
|
+
const root = repo({
|
|
66
|
+
"chant.workspace.json": declaration([diagram({ source: null })]),
|
|
67
|
+
"docs/diagrams/architecture.svg": "<svg></svg>",
|
|
68
|
+
});
|
|
69
|
+
expect(await found(root)).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a missing render fails WSP132", async () => {
|
|
73
|
+
const root = repo({
|
|
74
|
+
"chant.workspace.json": declaration([diagram()]),
|
|
75
|
+
"docs/diagrams/architecture.d2": "x -> y",
|
|
76
|
+
});
|
|
77
|
+
const d = await found(root);
|
|
78
|
+
expect(d.map((x) => [x.ruleId, x.code])).toEqual([["WSP132", "diagram-render-missing"]]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("the top-level (workspace's own) diagrams are checked too, reported with no entity", async () => {
|
|
82
|
+
const root = repo({ "chant.workspace.json": declaration([], { diagrams: [diagram({ name: "boundary" })] }), "docs/README.md": "" });
|
|
83
|
+
const d = await found(root);
|
|
84
|
+
expect(d.map((x) => [x.ruleId, x.entity])).toEqual([
|
|
85
|
+
["WSP131", undefined],
|
|
86
|
+
["WSP132", undefined],
|
|
87
|
+
]);
|
|
88
|
+
expect(d[0].message).toContain("the workspace's own diagram boundary names the source");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("diagram-render-drift (WSP133)", () => {
|
|
93
|
+
const SOURCE = "x -> y\n";
|
|
94
|
+
const HASH = sha256Hex(Buffer.from(SOURCE, "utf-8"));
|
|
95
|
+
|
|
96
|
+
test("no recorded sourceHash: never checked for drift, even when the source changed", async () => {
|
|
97
|
+
const root = repo({
|
|
98
|
+
"chant.workspace.json": declaration([diagram()]),
|
|
99
|
+
"docs/diagrams/architecture.d2": "something else entirely",
|
|
100
|
+
"docs/diagrams/architecture.svg": "<svg></svg>",
|
|
101
|
+
});
|
|
102
|
+
expect(await found(root)).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("a recorded sourceHash that matches the source passes", async () => {
|
|
106
|
+
const root = repo({
|
|
107
|
+
"chant.workspace.json": declaration([diagram({ sourceHash: HASH })]),
|
|
108
|
+
"docs/diagrams/architecture.d2": SOURCE,
|
|
109
|
+
"docs/diagrams/architecture.svg": "<svg></svg>",
|
|
110
|
+
});
|
|
111
|
+
expect(await found(root)).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a source that changed since the recorded hash fails WSP133, without running any renderer", async () => {
|
|
115
|
+
const root = repo({
|
|
116
|
+
"chant.workspace.json": declaration([diagram({ sourceHash: HASH })]),
|
|
117
|
+
"docs/diagrams/architecture.d2": "x -> y -> z\n",
|
|
118
|
+
"docs/diagrams/architecture.svg": "<svg></svg>",
|
|
119
|
+
});
|
|
120
|
+
const d = await found(root);
|
|
121
|
+
expect(d.map((x) => [x.ruleId, x.code])).toEqual([["WSP133", "diagram-render-drift"]]);
|
|
122
|
+
expect(d[0].message).toContain("source docs/diagrams/architecture.d2 changed since docs/diagrams/architecture.svg was rendered from it");
|
|
123
|
+
expect(d[0].message).toContain(`recorded sourceHash ${HASH.slice(0, 12)}`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a missing source is left to WSP131; WSP133 finds nothing to compare", async () => {
|
|
127
|
+
const root = repo({
|
|
128
|
+
"chant.workspace.json": declaration([diagram({ sourceHash: HASH })]),
|
|
129
|
+
"docs/diagrams/architecture.svg": "<svg></svg>",
|
|
130
|
+
});
|
|
131
|
+
const d = await found(root);
|
|
132
|
+
expect(d.map((x) => x.ruleId)).toEqual(["WSP131"]);
|
|
133
|
+
});
|
|
134
|
+
});
|