@intentius/chant 0.46.0 → 0.50.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/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +30 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +9 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- 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 +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +12 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +13 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +20 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +137 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +6 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lifecycle/teardown.d.ts +6 -4
- package/dist/lifecycle/teardown.d.ts.map +1 -1
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +41 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +140 -3
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +18 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +49 -2
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +29 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +40 -4
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +13 -1
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.test.ts +41 -0
- package/src/build.ts +39 -6
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +268 -1
- package/src/cli/commands/audit.ts +87 -18
- package/src/cli/commands/build.test.ts +245 -0
- package/src/cli/commands/build.ts +210 -21
- package/src/cli/commands/lint.ts +15 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +12 -3
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +96 -15
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +323 -3
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +14 -2
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/auto-release.ts +6 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +13 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/composite.ts +31 -2
- package/src/config.test.ts +15 -0
- package/src/config.ts +30 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +95 -6
- package/src/fold/subset.ts +66 -2
- package/src/index.ts +4 -0
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +151 -5
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +6 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lifecycle/teardown.test.ts +31 -0
- package/src/lifecycle/teardown.ts +6 -4
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +60 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +35 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +7 -0
- package/src/lint/rules/index.ts +7 -1
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +198 -6
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +40 -3
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +212 -29
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/op.test.ts +25 -2
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/receipt-store.ts +211 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +51 -2
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +30 -0
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
import { buildProjectImportEdges } from "./fold-import";
|
|
3
|
+
import type { FoldDecision } from "./index";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* chant #1083 — rank fold blockers by dominator retained-count over the
|
|
7
|
+
* import graph discovery already builds.
|
|
8
|
+
*
|
|
9
|
+
* ## Why dominators, not a flat "files downstream of X" count
|
|
10
|
+
*
|
|
11
|
+
* The import graph is a DAG (occasionally cyclic, when project source
|
|
12
|
+
* itself has an import cycle), not a tree: a file can be reachable through
|
|
13
|
+
* several paths, so naively summing "files that import X, transitively"
|
|
14
|
+
* double-counts every diamond and the totals stop meaning anything. The
|
|
15
|
+
* fix is the same one heap snapshot tools use for object retained sizes
|
|
16
|
+
* (Cooper–Harvey–Kennedy immediate dominators, prior art: spicypath's
|
|
17
|
+
* `src/heap-dominators.js`, cited on chant #1083): file `d` dominates file
|
|
18
|
+
* `n` when every causal path to `n`'s failure passes through `d`. A file's
|
|
19
|
+
* retained count is then the size of its dominator subtree — the number of
|
|
20
|
+
* files that would fold if `d` folded — and every node lands in exactly one
|
|
21
|
+
* dominator subtree, so retained counts are conserved: they sum to the
|
|
22
|
+
* total across the tree's top-level blockers, with nothing counted twice
|
|
23
|
+
* through a diamond, and a file with two INDEPENDENT blockers (no common
|
|
24
|
+
* ancestor besides the synthetic root) inflates neither.
|
|
25
|
+
*
|
|
26
|
+
* ## One direction only
|
|
27
|
+
*
|
|
28
|
+
* This models FORWARD contagion only: file `n` fails to fold because one of
|
|
29
|
+
* its own imports, `d`, also fails — an unresolved cross-file reference.
|
|
30
|
+
* The edge direction for dominance purposes is therefore `d -> n` (the
|
|
31
|
+
* blocker points at what it blocks), which is the REVERSE of the literal
|
|
32
|
+
* import edge `n -> d` (n imports d).
|
|
33
|
+
*
|
|
34
|
+
* Fold has a second, opposite-direction rule (chant #1044, implemented in
|
|
35
|
+
* {@link import("./fold-import").planFoldTaint}): a file that WOULD fold
|
|
36
|
+
* fine in isolation is forced back to run anyway because some file that
|
|
37
|
+
* imports it (directly or transitively) itself runs — folding it
|
|
38
|
+
* independently would create a second, non-identical instance. That is
|
|
39
|
+
* contagion from importER to importEE, the opposite of the direction this
|
|
40
|
+
* tree models, and a dominator tree over import edges cannot express it: a
|
|
41
|
+
* fix to the "blocker" here wouldn't actually unblock the reverse-tainted
|
|
42
|
+
* file (it was never broken on its own terms), so crediting it would
|
|
43
|
+
* overstate the fix's blast radius. Every file discovery marked
|
|
44
|
+
* {@link FoldDecision.reverseTainted} is therefore excluded from the tree
|
|
45
|
+
* entirely and reported in {@link FoldRankResult.reverseTainted} instead —
|
|
46
|
+
* see chant #1083's re-scope comment.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/** Sentinel "file path" for the synthetic dominator-tree root. Never a real file — file paths never contain NUL. */
|
|
50
|
+
const ROOT = "\0chant-fold-rank-root\0";
|
|
51
|
+
|
|
52
|
+
/** One node in the forward-contagion dominator tree — a `"run"`, non-reverse-tainted file. */
|
|
53
|
+
export interface FoldBlocker {
|
|
54
|
+
/** Absolute source file path (matches {@link FoldDecision.file}). */
|
|
55
|
+
file: string;
|
|
56
|
+
/** Why THIS file itself falls back to run (its own {@link FoldDecision.reason}). */
|
|
57
|
+
reason?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Number of files — including this one — that would fold if this file's
|
|
60
|
+
* own fold problem were fixed: this node's dominator-subtree size.
|
|
61
|
+
* Conserved: summing this field over every blocker with no closer
|
|
62
|
+
* dominator ({@link topLevel}) equals {@link FoldRankResult.totalBlocked}.
|
|
63
|
+
*/
|
|
64
|
+
retained: number;
|
|
65
|
+
/** True when nothing else in the tree dominates this file more closely than the synthetic root — i.e. this file's own fold failure isn't explained by importing another blocker already in the tree. */
|
|
66
|
+
topLevel: boolean;
|
|
67
|
+
/** This file's immediate dominator's file path, or `undefined` for a {@link topLevel} blocker. */
|
|
68
|
+
dominatedBy?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A `"run"` file held back only by the chant #1044 reverse rule — never part of the forward dominator tree (see this module's doc). */
|
|
72
|
+
export interface FoldReverseTaintedFile {
|
|
73
|
+
file: string;
|
|
74
|
+
reason?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface FoldRankResult {
|
|
78
|
+
/**
|
|
79
|
+
* Every forward-contagion blocker (a `"run"`, non-reverse-tainted file),
|
|
80
|
+
* sorted by {@link FoldBlocker.retained} descending, ties broken by file
|
|
81
|
+
* path for determinism. Includes every node in the tree, not just
|
|
82
|
+
* top-level ones — an intermediate hub file (e.g. a `params.ts` many
|
|
83
|
+
* others funnel through) is itself a meaningful blocker to rank.
|
|
84
|
+
*/
|
|
85
|
+
blockers: FoldBlocker[];
|
|
86
|
+
/** `"run"` files excluded from the tree by the reverse rule (chant #1044) — see this module's doc. */
|
|
87
|
+
reverseTainted: FoldReverseTaintedFile[];
|
|
88
|
+
/** Total forward-blocked files considered ({@link blockers}.length). Top-level blockers' retained counts sum to exactly this. */
|
|
89
|
+
totalBlocked: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Compute immediate dominators for `nodes` over `edges` (blocker -> blocked,
|
|
94
|
+
* i.e. flowing in the CAUSAL direction, not the import direction), rooted at
|
|
95
|
+
* a synthetic {@link ROOT}. Handles a cyclic/irreducible graph correctly —
|
|
96
|
+
* Cooper–Harvey–Kennedy's iterative algorithm converges on any graph, not
|
|
97
|
+
* just reducible ones (this is exactly why it's the right tool here: an
|
|
98
|
+
* import graph can have cycles).
|
|
99
|
+
*
|
|
100
|
+
* `rootChildren` must already guarantee every node in `nodes` is reachable
|
|
101
|
+
* from {@link ROOT} via `rootChildren` + `edges` — see `rankFoldBlockers`'s
|
|
102
|
+
* two-phase root-selection below.
|
|
103
|
+
*/
|
|
104
|
+
function computeImmediateDominators(
|
|
105
|
+
nodes: readonly string[],
|
|
106
|
+
edges: ReadonlyMap<string, ReadonlySet<string>>,
|
|
107
|
+
rootChildren: ReadonlySet<string>,
|
|
108
|
+
): Map<string, string> {
|
|
109
|
+
const succ = (n: string): ReadonlySet<string> => (n === ROOT ? rootChildren : edges.get(n) ?? new Set());
|
|
110
|
+
|
|
111
|
+
// Predecessors, including the synthetic ROOT edge into every root child.
|
|
112
|
+
const preds = new Map<string, Set<string>>();
|
|
113
|
+
for (const n of nodes) preds.set(n, new Set());
|
|
114
|
+
for (const [from, targets] of edges) {
|
|
115
|
+
for (const to of targets) {
|
|
116
|
+
if (!preds.has(to)) preds.set(to, new Set());
|
|
117
|
+
preds.get(to)!.add(from);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const rc of rootChildren) preds.get(rc)?.add(ROOT);
|
|
121
|
+
|
|
122
|
+
// Reverse-postorder numbering via one DFS from ROOT — every node in
|
|
123
|
+
// `nodes` must be reached (guaranteed by the caller's root selection).
|
|
124
|
+
const postorderNumber = new Map<string, number>();
|
|
125
|
+
const visited = new Set<string>([ROOT]);
|
|
126
|
+
let counter = 0;
|
|
127
|
+
const visitStack: Array<{ node: string; iter: Iterator<string> }> = [
|
|
128
|
+
{ node: ROOT, iter: succ(ROOT).values() },
|
|
129
|
+
];
|
|
130
|
+
while (visitStack.length > 0) {
|
|
131
|
+
const top = visitStack[visitStack.length - 1];
|
|
132
|
+
const next = top.iter.next();
|
|
133
|
+
if (next.done) {
|
|
134
|
+
postorderNumber.set(top.node, counter++);
|
|
135
|
+
visitStack.pop();
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const child = next.value;
|
|
139
|
+
if (!visited.has(child)) {
|
|
140
|
+
visited.add(child);
|
|
141
|
+
visitStack.push({ node: child, iter: succ(child).values() });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const rpo = [...nodes, ROOT].filter((n) => postorderNumber.has(n));
|
|
146
|
+
rpo.sort((a, b) => postorderNumber.get(b)! - postorderNumber.get(a)!); // reverse postorder: highest number first
|
|
147
|
+
|
|
148
|
+
const idom = new Map<string, string>();
|
|
149
|
+
idom.set(ROOT, ROOT);
|
|
150
|
+
|
|
151
|
+
const intersect = (a: string, b: string): string => {
|
|
152
|
+
let finger1 = a;
|
|
153
|
+
let finger2 = b;
|
|
154
|
+
while (finger1 !== finger2) {
|
|
155
|
+
while (postorderNumber.get(finger1)! < postorderNumber.get(finger2)!) finger1 = idom.get(finger1)!;
|
|
156
|
+
while (postorderNumber.get(finger2)! < postorderNumber.get(finger1)!) finger2 = idom.get(finger2)!;
|
|
157
|
+
}
|
|
158
|
+
return finger1;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
let changed = true;
|
|
162
|
+
while (changed) {
|
|
163
|
+
changed = false;
|
|
164
|
+
for (const node of rpo) {
|
|
165
|
+
if (node === ROOT) continue;
|
|
166
|
+
let newIdom: string | undefined;
|
|
167
|
+
for (const p of preds.get(node) ?? []) {
|
|
168
|
+
if (!idom.has(p)) continue; // not yet processed this pass
|
|
169
|
+
newIdom = newIdom === undefined ? p : intersect(newIdom, p);
|
|
170
|
+
}
|
|
171
|
+
if (newIdom !== undefined && idom.get(node) !== newIdom) {
|
|
172
|
+
idom.set(node, newIdom);
|
|
173
|
+
changed = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
idom.delete(ROOT);
|
|
179
|
+
return idom;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Rank fold blockers by dominator retained-count over the (forward-only)
|
|
184
|
+
* import graph among this build's `"run"`-mode files. See this module's doc
|
|
185
|
+
* for the model. `decisions` is expected to be a `--fold` build's complete
|
|
186
|
+
* {@link FoldDecision}[] — every discovered file, fold or run.
|
|
187
|
+
*/
|
|
188
|
+
export async function rankFoldBlockers(decisions: readonly FoldDecision[]): Promise<FoldRankResult> {
|
|
189
|
+
const allFiles = decisions.map((d) => d.file);
|
|
190
|
+
const reverseTainted = decisions
|
|
191
|
+
.filter((d) => d.mode === "run" && d.reverseTainted === true)
|
|
192
|
+
.map((d) => ({ file: d.file, reason: d.reason }))
|
|
193
|
+
.sort((a, b) => a.file.localeCompare(b.file));
|
|
194
|
+
|
|
195
|
+
const nodeDecisions = decisions.filter((d) => d.mode === "run" && d.reverseTainted !== true);
|
|
196
|
+
const reasonByFile = new Map(nodeDecisions.map((d) => [d.file, d.reason] as const));
|
|
197
|
+
const nodes = nodeDecisions.map((d) => d.file).sort((a, b) => a.localeCompare(b));
|
|
198
|
+
const nodeSet = new Set(nodes);
|
|
199
|
+
|
|
200
|
+
// file -> the OTHER discovered files it imports (real import direction).
|
|
201
|
+
const importEdges = await buildProjectImportEdges(allFiles);
|
|
202
|
+
|
|
203
|
+
// Cause graph, blocker -> blocked: importee -> importer, restricted to
|
|
204
|
+
// both endpoints being forward-tree nodes (a fold-mode or reverse-tainted
|
|
205
|
+
// file is neither a blocker nor blocked in this tree).
|
|
206
|
+
const causeEdges = new Map<string, Set<string>>();
|
|
207
|
+
for (const n of nodes) causeEdges.set(n, new Set());
|
|
208
|
+
for (const [importer, targets] of importEdges) {
|
|
209
|
+
if (!nodeSet.has(importer)) continue;
|
|
210
|
+
for (const target of targets) {
|
|
211
|
+
if (!nodeSet.has(target)) continue;
|
|
212
|
+
causeEdges.get(target)!.add(importer);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Predecessor count, to find true root causes (a blocked file that is
|
|
217
|
+
// itself never blocked by another tree node).
|
|
218
|
+
const predCount = new Map<string, number>();
|
|
219
|
+
for (const n of nodes) predCount.set(n, 0);
|
|
220
|
+
for (const targets of causeEdges.values()) {
|
|
221
|
+
for (const t of targets) predCount.set(t, (predCount.get(t) ?? 0) + 1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Two-phase root selection, so every node is reachable from ROOT even
|
|
225
|
+
// across an import cycle with no external entry point (chant #1083's doc
|
|
226
|
+
// cites spicypath's dominators working over a graph that is "cyclic and
|
|
227
|
+
// irreducible" as the precedent this needs to match).
|
|
228
|
+
const rootChildren = new Set<string>();
|
|
229
|
+
const reached = new Set<string>();
|
|
230
|
+
const expandFrom = (start: string): void => {
|
|
231
|
+
if (reached.has(start)) return;
|
|
232
|
+
const stack = [start];
|
|
233
|
+
reached.add(start);
|
|
234
|
+
while (stack.length > 0) {
|
|
235
|
+
const cur = stack.pop()!;
|
|
236
|
+
for (const next of causeEdges.get(cur) ?? []) {
|
|
237
|
+
if (!reached.has(next)) {
|
|
238
|
+
reached.add(next);
|
|
239
|
+
stack.push(next);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
for (const n of nodes) {
|
|
245
|
+
if (predCount.get(n) === 0) {
|
|
246
|
+
rootChildren.add(n);
|
|
247
|
+
expandFrom(n);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const n of nodes) {
|
|
251
|
+
if (!reached.has(n)) {
|
|
252
|
+
rootChildren.add(n);
|
|
253
|
+
expandFrom(n);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const idom = computeImmediateDominators(nodes, causeEdges, rootChildren);
|
|
258
|
+
|
|
259
|
+
// Dominator-tree children, to compute retained (subtree size) bottom-up.
|
|
260
|
+
const domChildren = new Map<string, string[]>();
|
|
261
|
+
for (const n of nodes) domChildren.set(n, []);
|
|
262
|
+
for (const [n, parent] of idom) {
|
|
263
|
+
if (parent !== ROOT) domChildren.get(parent)!.push(n);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const retained = new Map<string, number>();
|
|
267
|
+
const computeRetained = (n: string): number => {
|
|
268
|
+
const cached = retained.get(n);
|
|
269
|
+
if (cached !== undefined) return cached;
|
|
270
|
+
let size = 1;
|
|
271
|
+
for (const child of domChildren.get(n) ?? []) size += computeRetained(child);
|
|
272
|
+
retained.set(n, size);
|
|
273
|
+
return size;
|
|
274
|
+
};
|
|
275
|
+
for (const n of nodes) computeRetained(n);
|
|
276
|
+
|
|
277
|
+
const blockers: FoldBlocker[] = nodes.map((file) => {
|
|
278
|
+
const parent = idom.get(file);
|
|
279
|
+
const topLevel = parent === undefined || parent === ROOT;
|
|
280
|
+
return {
|
|
281
|
+
file,
|
|
282
|
+
reason: reasonByFile.get(file),
|
|
283
|
+
retained: retained.get(file)!,
|
|
284
|
+
topLevel,
|
|
285
|
+
dominatedBy: topLevel ? undefined : parent,
|
|
286
|
+
};
|
|
287
|
+
});
|
|
288
|
+
blockers.sort((a, b) => b.retained - a.retained || a.file.localeCompare(b.file));
|
|
289
|
+
|
|
290
|
+
return { blockers, reverseTainted, totalBlocked: nodes.length };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** `file -> dominatedBy` lookup built from a {@link FoldRankResult}, for reconstructing a blocker's dominator-chain (used by {@link toCollapsedFormat}). */
|
|
294
|
+
function dominatorChain(result: FoldRankResult, file: string): string[] {
|
|
295
|
+
const dominatedByFile = new Map(result.blockers.map((b) => [b.file, b.dominatedBy] as const));
|
|
296
|
+
const chain: string[] = [file];
|
|
297
|
+
let current: string | undefined = file;
|
|
298
|
+
const guard = new Set<string>([file]);
|
|
299
|
+
for (;;) {
|
|
300
|
+
const parent = dominatedByFile.get(current!);
|
|
301
|
+
if (parent === undefined) break;
|
|
302
|
+
if (guard.has(parent)) break; // defensive: never trust a cycle in the tree itself
|
|
303
|
+
chain.push(parent);
|
|
304
|
+
guard.add(parent);
|
|
305
|
+
current = parent;
|
|
306
|
+
}
|
|
307
|
+
return chain.reverse(); // top-level ancestor first, `file` last
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** A collapsed-format frame may not contain `;` (the frame separator) or a newline; sanitize rather than silently mis-render. */
|
|
311
|
+
function sanitizeFrame(label: string): string {
|
|
312
|
+
return label.replace(/[\n\r]/g, " ").replace(/;/g, ",");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Export the dominator tree in Brendan Gregg collapsed stack format
|
|
317
|
+
* (`frame;frame;...;frame count`), weighted by retained count, so it opens
|
|
318
|
+
* in any flame or icicle graph viewer with no chant-specific tooling (the
|
|
319
|
+
* seam is the file format, per chant #1083's original scope).
|
|
320
|
+
*
|
|
321
|
+
* One line per blocker, `count` always `1`: each file's stack is the chain
|
|
322
|
+
* of dominators from the top-level blocker down to itself. Folding
|
|
323
|
+
* identical prefixes — what every collapsed-format consumer does — then
|
|
324
|
+
* reproduces each blocker's retained count as that prefix's total weight,
|
|
325
|
+
* with nothing double-counted through a diamond. `rootLabel` is a single
|
|
326
|
+
* synthetic top frame (default `"fold"`) so every top-level blocker renders
|
|
327
|
+
* under one shared root instead of the viewer showing several disconnected
|
|
328
|
+
* trees; pass `relativeTo` (typically the build's infra path) to shorten
|
|
329
|
+
* frame labels to project-relative paths.
|
|
330
|
+
*/
|
|
331
|
+
export function toCollapsedFormat(
|
|
332
|
+
result: FoldRankResult,
|
|
333
|
+
options?: { rootLabel?: string; relativeTo?: string },
|
|
334
|
+
): string[] {
|
|
335
|
+
const rootLabel = options?.rootLabel ?? "fold";
|
|
336
|
+
const toLabel = (file: string): string =>
|
|
337
|
+
sanitizeFrame(options?.relativeTo ? relative(options.relativeTo, file) || file : file);
|
|
338
|
+
|
|
339
|
+
return result.blockers
|
|
340
|
+
.slice()
|
|
341
|
+
.sort((a, b) => a.file.localeCompare(b.file))
|
|
342
|
+
.map((blocker) => {
|
|
343
|
+
const chain = dominatorChain(result, blocker.file).map(toLabel);
|
|
344
|
+
return `${sanitizeFrame(rootLabel)};${chain.join(";")} 1`;
|
|
345
|
+
});
|
|
346
|
+
}
|
package/src/discovery/index.ts
CHANGED
|
@@ -85,6 +85,21 @@ export interface FoldDecision {
|
|
|
85
85
|
reason?: string;
|
|
86
86
|
/** Number of entities the fold produced. Present only when `mode === "fold"`. */
|
|
87
87
|
resourceCount?: number;
|
|
88
|
+
/**
|
|
89
|
+
* chant #1083 — true when this file's OWN fold attempt succeeded (it would
|
|
90
|
+
* fold cleanly in isolation) but it was forced to `"run"` anyway by
|
|
91
|
+
* {@link planFoldTaint}'s REVERSE rule (chant #1044): some other file that
|
|
92
|
+
* imports it, directly or transitively, itself falls back to run, so
|
|
93
|
+
* folding this one independently would create a duplicate, non-identical
|
|
94
|
+
* instance. Present only when `mode === "run"`.
|
|
95
|
+
*
|
|
96
|
+
* The fold-blocker dominator ranking (`./fold-rank.ts`) keys on this: a
|
|
97
|
+
* reverse-tainted file is not a forward-contagion blocker (fixing it
|
|
98
|
+
* unblocks nothing — it was never the cause), and the tree can't express
|
|
99
|
+
* the edge that DOES hold it back (an importer, not something it imports).
|
|
100
|
+
* It's reported in a separate bucket instead of a dominator subtree.
|
|
101
|
+
*/
|
|
102
|
+
reverseTainted?: boolean;
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
/**
|
|
@@ -295,7 +310,7 @@ export async function discover(path: string, options?: DiscoveryOptions): Promis
|
|
|
295
310
|
const reason = !folded.ok
|
|
296
311
|
? folded.reason
|
|
297
312
|
: `would fold in isolation, but a file that imports it (directly or transitively) falls back to run — folding independently would create a duplicate, non-identical instance`;
|
|
298
|
-
foldDecisions.push({ file, mode: "run", reason });
|
|
313
|
+
foldDecisions.push({ file, mode: "run", reason, reverseTainted: folded.ok });
|
|
299
314
|
}
|
|
300
315
|
|
|
301
316
|
if (options?.sandbox) {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { collectConsts } from "../fold/fold";
|
|
4
|
+
import { collectParamDependencies } from "./param-deps";
|
|
5
|
+
import type { PathOrigin } from "../provenance";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Collect dependencies for the props of the file's single
|
|
9
|
+
* `export const x = new Type({...})`, which is the shape every case here uses.
|
|
10
|
+
*/
|
|
11
|
+
function depsOf(source: string, paramLocals = ["params"]): Record<string, PathOrigin> {
|
|
12
|
+
const file = ts.createSourceFile("fixture.ts", source, ts.ScriptTarget.Latest, true);
|
|
13
|
+
const consts = collectConsts(file);
|
|
14
|
+
let props: ts.ObjectLiteralExpression | undefined;
|
|
15
|
+
for (const statement of file.statements) {
|
|
16
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
17
|
+
for (const decl of statement.declarationList.declarations) {
|
|
18
|
+
if (decl.name.getText() !== "x" || !decl.initializer) continue;
|
|
19
|
+
if (!ts.isNewExpression(decl.initializer)) continue;
|
|
20
|
+
for (const argument of decl.initializer.arguments ?? []) {
|
|
21
|
+
if (ts.isObjectLiteralExpression(argument)) {
|
|
22
|
+
props = argument;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!props) throw new Error("fixture has no `const x = new Type({...})`");
|
|
29
|
+
return collectParamDependencies(props, consts, new Set(paramLocals));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const param = (...names: string[]): PathOrigin => ({ kind: "build-param", params: names });
|
|
33
|
+
|
|
34
|
+
describe("collectParamDependencies", () => {
|
|
35
|
+
test("a direct read is attributed to its own path", () => {
|
|
36
|
+
expect(depsOf(`export const x = new Thing({ replicas: params.replicas, name: "fixed" });`)).toEqual({
|
|
37
|
+
replicas: param("replicas"),
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("nested object literals are descended into, dotted", () => {
|
|
42
|
+
const source = `export const x = new Thing({ spec: { template: { image: params.image } } });`;
|
|
43
|
+
expect(depsOf(source)).toEqual({ "spec.template.image": param("image") });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("an expression is attributed to every parameter it can read, not to its value", () => {
|
|
47
|
+
const source = `export const x = new Thing({ replicas: params.tier === "prod" ? params.big : 1 });`;
|
|
48
|
+
expect(depsOf(source)).toEqual({ replicas: param("big", "tier") });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a parameter hoisted into a const is followed to the field that uses it", () => {
|
|
52
|
+
const source = [
|
|
53
|
+
`const replicas = params.tier === "prod" ? 5 : 1;`,
|
|
54
|
+
`export const x = new Thing({ replicas });`,
|
|
55
|
+
].join("\n");
|
|
56
|
+
expect(depsOf(source)).toEqual({ replicas: param("tier") });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("const chains are followed transitively, and a cycle terminates", () => {
|
|
60
|
+
const source = [
|
|
61
|
+
`const a = b;`,
|
|
62
|
+
`const b = \`\${params.region}-\${a}\`;`,
|
|
63
|
+
`export const x = new Thing({ zone: a });`,
|
|
64
|
+
].join("\n");
|
|
65
|
+
expect(depsOf(source)).toEqual({ zone: param("region") });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("template literals and calls are walked", () => {
|
|
69
|
+
const source = `export const x = new Thing({ bucket: \`\${params.env}-assets\`.toLowerCase() });`;
|
|
70
|
+
expect(depsOf(source)).toEqual({ bucket: param("env") });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("an array is attributed whole, never per index", () => {
|
|
74
|
+
const source = `export const x = new Thing({ containers: [{ image: params.image }, { image: "sidecar" }] });`;
|
|
75
|
+
expect(depsOf(source)).toEqual({ containers: param("image") });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("a spread is attributed to the object it spreads into", () => {
|
|
79
|
+
const source = [
|
|
80
|
+
`const base = { region: params.region };`,
|
|
81
|
+
`export const x = new Thing({ ...base, spec: { ...base, replicas: 1 } });`,
|
|
82
|
+
].join("\n");
|
|
83
|
+
expect(depsOf(source)).toEqual({ "": param("region"), spec: param("region") });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("bracket access with a literal key names the parameter", () => {
|
|
87
|
+
expect(depsOf(`export const x = new Thing({ zone: params["region"] });`)).toEqual({ zone: param("region") });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a property KEY that happens to match a const is not a reference", () => {
|
|
91
|
+
const source = [`const tier = params.tier;`, `export const x = new Thing({ spec: { tier: "fixed" } });`].join("\n");
|
|
92
|
+
expect(depsOf(source)).toEqual({});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a bare reference to the whole params object records nothing", () => {
|
|
96
|
+
// It names no single parameter; under-reporting is the safe direction.
|
|
97
|
+
expect(depsOf(`export const x = new Thing({ all: params });`)).toEqual({});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a file that never imported params records nothing", () => {
|
|
101
|
+
expect(depsOf(`export const x = new Thing({ replicas: params.replicas });`, [])).toEqual({});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("the local name the import bound is what counts, not the word 'params'", () => {
|
|
105
|
+
const source = `export const x = new Thing({ replicas: p.replicas, other: params.replicas });`;
|
|
106
|
+
expect(depsOf(source, ["p"])).toEqual({ replicas: param("replicas") });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("parameter names are sorted and de-duplicated", () => {
|
|
110
|
+
const source = `export const x = new Thing({ n: params.z + params.a + params.z });`;
|
|
111
|
+
expect(depsOf(source)).toEqual({ n: param("a", "z") });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a computed key is skipped rather than guessed at", () => {
|
|
115
|
+
const source = `export const x = new Thing({ [params.key]: 1, name: params.name });`;
|
|
116
|
+
expect(depsOf(source)).toEqual({ name: param("name") });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { PathOrigin } from "../provenance";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which build parameters an authored props expression READS, per path (chant
|
|
6
|
+
* #1443) — the declared-side counterpart of `managed-fields.ts`'s per-path live
|
|
7
|
+
* `owners`.
|
|
8
|
+
*
|
|
9
|
+
* This is a syntactic dependency, not a value taint. Fold substitutes
|
|
10
|
+
* `params.<name>` before anything is emitted, so by output time the value is a
|
|
11
|
+
* literal and indistinguishable from one typed by hand; tainting the value
|
|
12
|
+
* through fold's evaluator would mean propagating a tag through every operator
|
|
13
|
+
* it supports, and one operator that forgot would produce silently wrong
|
|
14
|
+
* provenance. Recording which parameters the *expression* mentions cannot
|
|
15
|
+
* degrade that way: a shape this walk does not follow drops a dependency, it
|
|
16
|
+
* never invents one.
|
|
17
|
+
*
|
|
18
|
+
* It also answers the more useful question. For
|
|
19
|
+
* `replicas: params.tier === "prod" ? 5 : 1`, a taint reports that the value
|
|
20
|
+
* came from the literal `5` — true, and no help to anyone about to edit the
|
|
21
|
+
* field. The dependency reports that the field is governed by `tier`.
|
|
22
|
+
*
|
|
23
|
+
* Known under-reporting, all in the safe direction: destructured parameters
|
|
24
|
+
* (`const { tier } = params`) are not tracked, because `collectConsts` only
|
|
25
|
+
* records identifier bindings; a bare reference to the whole `params` object
|
|
26
|
+
* names no single parameter and records nothing; and the run path has no
|
|
27
|
+
* expression to walk at all.
|
|
28
|
+
*
|
|
29
|
+
* The one direction it can over-report is a local binding that shadows a
|
|
30
|
+
* top-level `const` of the same name, since names resolve against the file's
|
|
31
|
+
* consts without scope tracking. That reports a parameter the field could
|
|
32
|
+
* plausibly be governed by rather than one it definitely is, which is the
|
|
33
|
+
* failure the "could affect" reading is written to absorb.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** The property name a member declares, when it is a literal one. */
|
|
37
|
+
function literalName(name: ts.PropertyName | undefined): string | undefined {
|
|
38
|
+
if (!name) return undefined;
|
|
39
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Strip the wrappers that do not change what an initializer reads. */
|
|
44
|
+
function unwrap(expr: ts.Expression): ts.Expression {
|
|
45
|
+
let current = expr;
|
|
46
|
+
for (;;) {
|
|
47
|
+
if (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isNonNullExpression(current)) {
|
|
48
|
+
current = current.expression;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
return current;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Every parameter name `expr` reads, following this file's own top-level
|
|
57
|
+
* `const` bindings so a value hoisted into a local is still attributed
|
|
58
|
+
* (`const replicas = params.tier === "prod" ? 5 : 1` used as `replicas`).
|
|
59
|
+
*/
|
|
60
|
+
function readParams(
|
|
61
|
+
expr: ts.Expression,
|
|
62
|
+
consts: ReadonlyMap<string, ts.Expression>,
|
|
63
|
+
paramLocals: ReadonlySet<string>,
|
|
64
|
+
out: Set<string>,
|
|
65
|
+
): void {
|
|
66
|
+
const followed = new Set<string>();
|
|
67
|
+
|
|
68
|
+
const visit = (node: ts.Node): void => {
|
|
69
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
70
|
+
if (ts.isIdentifier(node.expression) && paramLocals.has(node.expression.text)) {
|
|
71
|
+
out.add(node.name.text);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
visit(node.expression);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (ts.isElementAccessExpression(node)) {
|
|
79
|
+
if (ts.isIdentifier(node.expression) && paramLocals.has(node.expression.text)) {
|
|
80
|
+
if (ts.isStringLiteralLike(node.argumentExpression)) out.add(node.argumentExpression.text);
|
|
81
|
+
} else {
|
|
82
|
+
visit(node.expression);
|
|
83
|
+
}
|
|
84
|
+
visit(node.argumentExpression);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (ts.isIdentifier(node)) {
|
|
89
|
+
// A bare `params` names no single parameter — see the under-reporting
|
|
90
|
+
// note above.
|
|
91
|
+
if (paramLocals.has(node.text)) return;
|
|
92
|
+
const initializer = consts.get(node.text);
|
|
93
|
+
if (initializer && !followed.has(node.text)) {
|
|
94
|
+
followed.add(node.text);
|
|
95
|
+
visit(initializer);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A property KEY is not a reference; without this a nested `{ tier: 1 }`
|
|
101
|
+
// would resolve `tier` against `consts` and manufacture a dependency.
|
|
102
|
+
if (ts.isPropertyAssignment(node)) {
|
|
103
|
+
visit(node.initializer);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
ts.forEachChild(node, visit);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
visit(expr);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Path → build-parameter origin for one resource's props object literal.
|
|
115
|
+
*
|
|
116
|
+
* Paths are dotted property names only, per `EntityProvenance.paths`: an object
|
|
117
|
+
* literal is descended into, and anything else — an array literal included — is
|
|
118
|
+
* attributed whole at its own path. An array's elements are deliberately not
|
|
119
|
+
* indexed, because an index-shaped key would not survive an element moving and
|
|
120
|
+
* would not match the `[#key]` addressing a diff uses.
|
|
121
|
+
*
|
|
122
|
+
* A spread is attributed to the object it spreads INTO, at that object's own
|
|
123
|
+
* path (the entity root for a top-level spread), since which keys it
|
|
124
|
+
* contributes is not knowable here.
|
|
125
|
+
*/
|
|
126
|
+
export function collectParamDependencies(
|
|
127
|
+
props: ts.ObjectLiteralExpression,
|
|
128
|
+
consts: ReadonlyMap<string, ts.Expression>,
|
|
129
|
+
paramLocals: ReadonlySet<string>,
|
|
130
|
+
): Record<string, PathOrigin> {
|
|
131
|
+
const out: Record<string, PathOrigin> = {};
|
|
132
|
+
if (paramLocals.size === 0) return out;
|
|
133
|
+
|
|
134
|
+
const record = (path: string, expr: ts.Expression): void => {
|
|
135
|
+
const found = new Set<string>();
|
|
136
|
+
readParams(expr, consts, paramLocals, found);
|
|
137
|
+
if (found.size === 0) return;
|
|
138
|
+
const existing = out[path];
|
|
139
|
+
const merged =
|
|
140
|
+
existing && existing.kind === "build-param" ? new Set([...existing.params, ...found]) : found;
|
|
141
|
+
out[path] = { kind: "build-param", params: [...merged].sort() };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const walk = (object: ts.ObjectLiteralExpression, prefix: string): void => {
|
|
145
|
+
for (const member of object.properties) {
|
|
146
|
+
if (ts.isSpreadAssignment(member)) {
|
|
147
|
+
record(prefix, member.expression);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (ts.isShorthandPropertyAssignment(member)) {
|
|
151
|
+
const key = member.name.text;
|
|
152
|
+
record(prefix ? `${prefix}.${key}` : key, member.name);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!ts.isPropertyAssignment(member)) continue;
|
|
156
|
+
const key = literalName(member.name);
|
|
157
|
+
if (key === undefined) continue;
|
|
158
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
159
|
+
const initializer = unwrap(member.initializer);
|
|
160
|
+
if (ts.isObjectLiteralExpression(initializer)) {
|
|
161
|
+
walk(initializer, path);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
record(path, member.initializer);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
walk(props, "");
|
|
169
|
+
return out;
|
|
170
|
+
}
|