@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,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect receipts (chant #1831, epic #1703).
|
|
3
|
+
*
|
|
4
|
+
* A RECEIPT is the declared witness that an out-of-band effect (a migration,
|
|
5
|
+
* a seed job, a one-shot bootstrap) has run — declared, diffed, and observed
|
|
6
|
+
* like any resource, but observe-only to the generic apply path: the
|
|
7
|
+
* `effect()` step (#1834) is the sole writer, on success, last. Anything
|
|
8
|
+
* else silently converts at-least-once into never (epic decision log, item
|
|
9
|
+
* 3). That write-exclusion is #1832's enforcement; what THIS module provides
|
|
10
|
+
* is the recognition marker that makes it possible: core code (lint, plan,
|
|
11
|
+
* apply) identifies receipts via {@link EFFECT_RECEIPT_MARKER} without any
|
|
12
|
+
* lexicon knowledge, while per-lexicon rows (#1835's `AWS::SSM::Parameter`)
|
|
13
|
+
* materialize them as real resources. Unlike a secret provenance declaration
|
|
14
|
+
* (./secret-provenance.ts), a receipt DOES serialize — the marker identifies,
|
|
15
|
+
* it does not exclude.
|
|
16
|
+
*
|
|
17
|
+
* The resolution split (epic decision log, item 5):
|
|
18
|
+
*
|
|
19
|
+
* - STATIC inputs hash at synthesis, in the serializer — a fully static
|
|
20
|
+
* receipt's expectation is already known when the template is written
|
|
21
|
+
* ({@link receiptExpectation}).
|
|
22
|
+
* - REFERENCE inputs (attr-refs and other intrinsics — deploy-time values)
|
|
23
|
+
* are recorded in placeholder form at synthesis and resolve in the plan
|
|
24
|
+
* engine (#1832) and again in the effect step (#1834), via
|
|
25
|
+
* {@link resolveReceiptExpectation}. Synthesis resolves NOTHING:
|
|
26
|
+
* {@link receiptExpectation} refuses a hash-flavor receipt that still
|
|
27
|
+
* carries references rather than hashing a placeholder.
|
|
28
|
+
* - Build-time `params.*` are not references here: they fold to literals
|
|
29
|
+
* before discovery ever runs (../params.ts), so by the time this factory
|
|
30
|
+
* sees them they are static.
|
|
31
|
+
*
|
|
32
|
+
* Nothing in this module reads live state. Every function is pure over the
|
|
33
|
+
* declaration and, for resolution, the caller-supplied resolver.
|
|
34
|
+
*
|
|
35
|
+
* Hashing is JCS-style canonical JSON (RFC 8785 shape: sorted keys, standard
|
|
36
|
+
* ECMAScript number/string encoding — implemented minimally here, no
|
|
37
|
+
* dependency) digested with sha256 ({@link canonicalJson},
|
|
38
|
+
* `sha256:<hex>` like the build-ledger digests in ./lifecycle/build-ledger.ts).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { createHash } from "node:crypto";
|
|
42
|
+
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
43
|
+
import { isIntrinsic, type Intrinsic } from "./intrinsic";
|
|
44
|
+
|
|
45
|
+
/** The closed union of receipt flavors. */
|
|
46
|
+
export type EffectReceiptFlavor = "existence" | "hash";
|
|
47
|
+
|
|
48
|
+
/** Every receipt flavor, for exhaustiveness checks. */
|
|
49
|
+
export const EFFECT_RECEIPT_FLAVORS: readonly EffectReceiptFlavor[] = ["existence", "hash"];
|
|
50
|
+
|
|
51
|
+
/** Marker symbol identifying an effect receipt. `Symbol.for` so it survives
|
|
52
|
+
* the entity-wire codec (./discovery/entity-wire-codec.ts) and so a lexicon
|
|
53
|
+
* row can stamp the SAME symbol on its materialized resource — core's lint
|
|
54
|
+
* (#1833), plan (#1832), and apply write-exclusion recognize receipts through
|
|
55
|
+
* this marker alone, lexicon-independently. */
|
|
56
|
+
export const EFFECT_RECEIPT_MARKER = Symbol.for("chant.effect-receipt");
|
|
57
|
+
|
|
58
|
+
/** The `entityType` the core factory stamps. Lexicon materialization rows
|
|
59
|
+
* (#1835) use their own entityType and carry the marker instead. */
|
|
60
|
+
export const EFFECT_RECEIPT_ENTITY_TYPE = "Chant::EffectReceipt";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The expected stored value of an `existence`-flavor receipt: a fixed marker
|
|
64
|
+
* constant, the same for every existence receipt. Present-and-equal means the
|
|
65
|
+
* effect has run; anything else renders an effect-will-fire row (#1832).
|
|
66
|
+
*/
|
|
67
|
+
export const EXISTENCE_EXPECTATION = "chant.effect-receipt:exists";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* An effect receipt declaration — a Declarable, so discovery collects it,
|
|
71
|
+
* `chant list` shows it, and (unlike a secret declaration) serialization
|
|
72
|
+
* keeps it: a lexicon row turns it into a real, observable resource.
|
|
73
|
+
*/
|
|
74
|
+
export interface EffectReceiptDeclaration extends Declarable {
|
|
75
|
+
readonly [EFFECT_RECEIPT_MARKER]: true;
|
|
76
|
+
/** `"chant"` for the plain factory; a lexicon row (#1835) declares its own. */
|
|
77
|
+
readonly lexicon: string;
|
|
78
|
+
/** The core factory stamps EFFECT_RECEIPT_ENTITY_TYPE; a lexicon row uses its materialized kind. */
|
|
79
|
+
readonly entityType: string;
|
|
80
|
+
/** The receipt's own name (the export-level identity of the witness). */
|
|
81
|
+
readonly name: string;
|
|
82
|
+
/** The effect this receipt witnesses — the identity the `effect()` step
|
|
83
|
+
* (#1834) and the receipt path (#1835) key on. */
|
|
84
|
+
readonly effect: string;
|
|
85
|
+
/** How the receipt is compared: mere presence, or a digest of the inputs. */
|
|
86
|
+
readonly flavor: EffectReceiptFlavor;
|
|
87
|
+
/**
|
|
88
|
+
* The effect's inputs as recorded at synthesis: static values verbatim,
|
|
89
|
+
* references (intrinsics) kept in placeholder form — never resolved here.
|
|
90
|
+
*/
|
|
91
|
+
readonly inputs: Readonly<Record<string, unknown>>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Factory options for {@link EffectReceipt}. */
|
|
95
|
+
export interface EffectReceiptOptions {
|
|
96
|
+
/** The effect this receipt witnesses. Non-empty. */
|
|
97
|
+
readonly effect: string;
|
|
98
|
+
/** `existence` — presence is the witness; `hash` — a digest of the inputs
|
|
99
|
+
* is, so changed inputs re-propose the fire. */
|
|
100
|
+
readonly flavor: EffectReceiptFlavor;
|
|
101
|
+
/** The effect's inputs. Static values hash at synthesis; intrinsic values
|
|
102
|
+
* (attr-refs, deploy-time references) resolve at plan and at run. */
|
|
103
|
+
readonly inputs?: Record<string, unknown>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Declare an effect receipt. The returned object is a locked Declarable:
|
|
108
|
+
* declared fields are immutable, plain-data input structures are frozen
|
|
109
|
+
* (intrinsics are left live — discovery still stamps logical names onto
|
|
110
|
+
* attr-refs), and the top-level object stays extensible for discovery's own
|
|
111
|
+
* symbol-keyed metadata.
|
|
112
|
+
*/
|
|
113
|
+
export function EffectReceipt(name: string, options: EffectReceiptOptions): EffectReceiptDeclaration {
|
|
114
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
115
|
+
throw new Error("EffectReceipt: `name` must be a non-empty string");
|
|
116
|
+
}
|
|
117
|
+
if (typeof options?.effect !== "string" || options.effect.length === 0) {
|
|
118
|
+
throw new Error(`EffectReceipt("${name}"): \`effect\` must be a non-empty string`);
|
|
119
|
+
}
|
|
120
|
+
if (!EFFECT_RECEIPT_FLAVORS.includes(options.flavor)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`EffectReceipt("${name}"): unknown flavor "${String(options.flavor)}" — ` +
|
|
123
|
+
`expected one of ${EFFECT_RECEIPT_FLAVORS.join(", ")}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (options.inputs !== undefined && (typeof options.inputs !== "object" || options.inputs === null || Array.isArray(options.inputs))) {
|
|
127
|
+
throw new Error(`EffectReceipt("${name}"): \`inputs\` must be a plain object when present`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const inputs = deepFreezeStatic({ ...(options.inputs ?? {}) }) as Readonly<Record<string, unknown>>;
|
|
131
|
+
|
|
132
|
+
const decl: EffectReceiptDeclaration = {
|
|
133
|
+
[DECLARABLE_MARKER]: true,
|
|
134
|
+
[EFFECT_RECEIPT_MARKER]: true,
|
|
135
|
+
lexicon: "chant",
|
|
136
|
+
entityType: EFFECT_RECEIPT_ENTITY_TYPE,
|
|
137
|
+
name,
|
|
138
|
+
effect: options.effect,
|
|
139
|
+
flavor: options.flavor,
|
|
140
|
+
inputs,
|
|
141
|
+
};
|
|
142
|
+
// Declared fields immutable, object extensible — same shape secret
|
|
143
|
+
// declarations use (./secret-provenance.ts's lockDeclaredFields).
|
|
144
|
+
for (const key of Object.keys(decl)) {
|
|
145
|
+
Object.defineProperty(decl, key, { writable: false, configurable: false });
|
|
146
|
+
}
|
|
147
|
+
return decl;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Freeze plain objects and arrays in place, leaving intrinsics (and any
|
|
151
|
+
* other class instance) untouched — discovery mutates AttrRefs when it
|
|
152
|
+
* assigns logical names. */
|
|
153
|
+
function deepFreezeStatic(value: unknown, seen: Set<object> = new Set()): unknown {
|
|
154
|
+
if (typeof value !== "object" || value === null || isIntrinsic(value)) return value;
|
|
155
|
+
if (seen.has(value)) return value;
|
|
156
|
+
seen.add(value);
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
for (const el of value) deepFreezeStatic(el, seen);
|
|
159
|
+
return Object.freeze(value);
|
|
160
|
+
}
|
|
161
|
+
if (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) {
|
|
162
|
+
for (const el of Object.values(value)) deepFreezeStatic(el, seen);
|
|
163
|
+
return Object.freeze(value);
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Type guard for an effect receipt — the recognition read core's guards use.
|
|
169
|
+
* True for the core declaration AND for any lexicon-materialized resource
|
|
170
|
+
* that carries the marker. */
|
|
171
|
+
export function isEffectReceipt(value: unknown): value is EffectReceiptDeclaration {
|
|
172
|
+
return (
|
|
173
|
+
typeof value === "object" &&
|
|
174
|
+
value !== null &&
|
|
175
|
+
EFFECT_RECEIPT_MARKER in value &&
|
|
176
|
+
(value as Record<symbol, unknown>)[EFFECT_RECEIPT_MARKER] === true
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Extract the effect receipts from a discovered entity map — the read surface
|
|
182
|
+
* for lint (#1833), the plan engine (#1832), and the apply write-exclusion.
|
|
183
|
+
* Keyed by entity name (export name), the same key `DiscoveryResult.entities`
|
|
184
|
+
* uses.
|
|
185
|
+
*/
|
|
186
|
+
export function collectEffectReceipts(
|
|
187
|
+
entities: ReadonlyMap<string, Declarable>,
|
|
188
|
+
): Map<string, EffectReceiptDeclaration> {
|
|
189
|
+
const out = new Map<string, EffectReceiptDeclaration>();
|
|
190
|
+
for (const [name, entity] of entities) {
|
|
191
|
+
if (isEffectReceipt(entity)) out.set(name, entity);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Split an entity map into the apply-bound set and the receipts (#1832).
|
|
198
|
+
*
|
|
199
|
+
* The write-exclusion seam: receipts are observe-only to the generic apply
|
|
200
|
+
* path — the `effect()` step is the sole writer (epic #1703, decision 3), and
|
|
201
|
+
* a receipt the generic apply stamped would silently convert at-least-once
|
|
202
|
+
* into never. The build calls this at serializer-input assembly, the one core
|
|
203
|
+
* choke point every applier's input flows through (appliers consume serialized
|
|
204
|
+
* build outputs), so no lexicon's serialized apply document ever contains a
|
|
205
|
+
* receipt and no applier's desired or prune set can. Receipts still reach the
|
|
206
|
+
* serializer — for visibility rendering outside the apply-bound document
|
|
207
|
+
* (#1835) — via `SerializeContext.receipts`, never in the entity map.
|
|
208
|
+
*/
|
|
209
|
+
export function splitReceiptEntities(entities: ReadonlyMap<string, Declarable>): {
|
|
210
|
+
applyBound: Map<string, Declarable>;
|
|
211
|
+
receipts: Map<string, EffectReceiptDeclaration>;
|
|
212
|
+
} {
|
|
213
|
+
const applyBound = new Map<string, Declarable>();
|
|
214
|
+
const receipts = new Map<string, EffectReceiptDeclaration>();
|
|
215
|
+
for (const [name, entity] of entities) {
|
|
216
|
+
if (isEffectReceipt(entity)) receipts.set(name, entity);
|
|
217
|
+
else applyBound.set(name, entity);
|
|
218
|
+
}
|
|
219
|
+
return { applyBound, receipts };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
223
|
+
// Canonical hashing — JCS-style canonical JSON + sha256.
|
|
224
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* JCS-style canonical JSON (the RFC 8785 shape, implemented minimally):
|
|
228
|
+
* object keys sorted by UTF-16 code units, numbers and strings in standard
|
|
229
|
+
* ECMAScript `JSON.stringify` encoding, no insignificant whitespace.
|
|
230
|
+
* `toJSON()` is honored the way `JSON.stringify` honors it — which is what
|
|
231
|
+
* puts an intrinsic's PLACEHOLDER envelope (e.g. an attr-ref's
|
|
232
|
+
* `{"__attrRef":{...}}`) into the canonical form rather than a resolved
|
|
233
|
+
* value. Non-representable values (undefined outside an object property,
|
|
234
|
+
* functions, symbols, bigints, non-finite numbers, cycles) throw — a hash
|
|
235
|
+
* input silently coerced is a wrong expectation.
|
|
236
|
+
*/
|
|
237
|
+
export function canonicalJson(value: unknown): string {
|
|
238
|
+
const out = encodeCanonical(value, "$", new Set());
|
|
239
|
+
if (out === undefined) {
|
|
240
|
+
throw new Error(`canonicalJson: value at $ is not representable in JSON`);
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Returns undefined only for values JSON.stringify would drop as an object
|
|
246
|
+
* property (undefined); throws for everything else non-representable. */
|
|
247
|
+
function encodeCanonical(value: unknown, path: string, seen: Set<object>): string | undefined {
|
|
248
|
+
// toJSON first, like JSON.stringify — once per node.
|
|
249
|
+
if (value !== null && (typeof value === "object" || typeof value === "function")) {
|
|
250
|
+
const toJSON = (value as { toJSON?: unknown }).toJSON;
|
|
251
|
+
if (typeof toJSON === "function") {
|
|
252
|
+
value = (toJSON as () => unknown).call(value);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (value === undefined) return undefined;
|
|
256
|
+
if (value === null) return "null";
|
|
257
|
+
switch (typeof value) {
|
|
258
|
+
case "boolean":
|
|
259
|
+
return value ? "true" : "false";
|
|
260
|
+
case "number":
|
|
261
|
+
if (!Number.isFinite(value)) {
|
|
262
|
+
throw new Error(`canonicalJson: non-finite number at ${path}`);
|
|
263
|
+
}
|
|
264
|
+
return String(value); // ECMAScript ToString — what JCS specifies.
|
|
265
|
+
case "string":
|
|
266
|
+
return JSON.stringify(value);
|
|
267
|
+
case "bigint":
|
|
268
|
+
throw new Error(`canonicalJson: bigint at ${path} is not representable in JSON`);
|
|
269
|
+
case "function":
|
|
270
|
+
case "symbol":
|
|
271
|
+
throw new Error(`canonicalJson: ${typeof value} at ${path} is not representable in JSON`);
|
|
272
|
+
}
|
|
273
|
+
const obj = value as object;
|
|
274
|
+
if (seen.has(obj)) {
|
|
275
|
+
throw new Error(`canonicalJson: circular structure at ${path}`);
|
|
276
|
+
}
|
|
277
|
+
seen.add(obj);
|
|
278
|
+
try {
|
|
279
|
+
if (Array.isArray(obj)) {
|
|
280
|
+
const parts = obj.map((el, i) => encodeCanonical(el, `${path}[${i}]`, seen) ?? "null");
|
|
281
|
+
return `[${parts.join(",")}]`;
|
|
282
|
+
}
|
|
283
|
+
const keys = Object.keys(obj).sort();
|
|
284
|
+
const parts: string[] = [];
|
|
285
|
+
for (const key of keys) {
|
|
286
|
+
const encoded = encodeCanonical((obj as Record<string, unknown>)[key], `${path}.${key}`, seen);
|
|
287
|
+
if (encoded !== undefined) parts.push(`${JSON.stringify(key)}:${encoded}`);
|
|
288
|
+
}
|
|
289
|
+
return `{${parts.join(",")}}`;
|
|
290
|
+
} finally {
|
|
291
|
+
seen.delete(obj);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function sha256Digest(canonical: string): string {
|
|
296
|
+
return `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
300
|
+
// The resolution split.
|
|
301
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
/** The paths (dot/bracket, rooted at `inputs`) of every reference (intrinsic)
|
|
304
|
+
* input still unresolved on the receipt. Empty means the receipt is fully
|
|
305
|
+
* static and {@link receiptExpectation} can stamp its digest at synthesis. */
|
|
306
|
+
export function referenceInputPaths(receipt: EffectReceiptDeclaration): string[] {
|
|
307
|
+
const paths: string[] = [];
|
|
308
|
+
collectReferencePaths(receipt.inputs, "inputs", paths, new Set());
|
|
309
|
+
return paths;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function collectReferencePaths(value: unknown, path: string, out: string[], seen: Set<object>): void {
|
|
313
|
+
if (typeof value !== "object" || value === null) return;
|
|
314
|
+
if (isIntrinsic(value)) {
|
|
315
|
+
out.push(path);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (seen.has(value)) return;
|
|
319
|
+
seen.add(value);
|
|
320
|
+
if (Array.isArray(value)) {
|
|
321
|
+
value.forEach((el, i) => collectReferencePaths(el, `${path}[${i}]`, out, seen));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
for (const [key, el] of Object.entries(value)) {
|
|
325
|
+
collectReferencePaths(el, `${path}.${key}`, out, seen);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The receipt's expected stored value, computable at synthesis:
|
|
331
|
+
*
|
|
332
|
+
* - `existence` → {@link EXISTENCE_EXPECTATION}, always.
|
|
333
|
+
* - `hash` → `sha256:<hex>` over the canonical JSON of
|
|
334
|
+
* `{ effect, inputs }` — the effect name is bound into the digest so the
|
|
335
|
+
* same inputs under a different effect never collide.
|
|
336
|
+
*
|
|
337
|
+
* A hash-flavor receipt that still carries reference inputs THROWS instead
|
|
338
|
+
* of hashing placeholders: synthesis resolves nothing (epic decision log,
|
|
339
|
+
* item 5). The plan engine and the effect step get the digest through
|
|
340
|
+
* {@link resolveReceiptExpectation}.
|
|
341
|
+
*/
|
|
342
|
+
export function receiptExpectation(receipt: EffectReceiptDeclaration): string {
|
|
343
|
+
if (receipt.flavor === "existence") return EXISTENCE_EXPECTATION;
|
|
344
|
+
const refs = referenceInputPaths(receipt);
|
|
345
|
+
if (refs.length > 0) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`receiptExpectation("${receipt.name}"): unresolved reference inputs at ${refs.join(", ")} — ` +
|
|
348
|
+
`references resolve at plan and at run, never at synthesis; ` +
|
|
349
|
+
`use resolveReceiptExpectation with a resolver`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return sha256Digest(canonicalJson({ effect: receipt.effect, inputs: receipt.inputs }));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Resolves one reference input to its live value. `path` is the reference's
|
|
357
|
+
* location (as {@link referenceInputPaths} renders it). Must return a
|
|
358
|
+
* JSON-representable value — returning `undefined` or another intrinsic is
|
|
359
|
+
* an error, reported with the path.
|
|
360
|
+
*/
|
|
361
|
+
export type ReceiptInputResolver = (ref: Intrinsic, path: string) => unknown;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* The receipt's expected stored value with references resolved — the form
|
|
365
|
+
* the plan engine (#1832) compares against the live receipt and the effect
|
|
366
|
+
* step (#1834) writes on success. Deterministic over the receipt and the
|
|
367
|
+
* resolver's answers; resolves nothing itself and reads no live state (the
|
|
368
|
+
* resolver is the caller's seam to deploy-time values).
|
|
369
|
+
*/
|
|
370
|
+
export function resolveReceiptExpectation(
|
|
371
|
+
receipt: EffectReceiptDeclaration,
|
|
372
|
+
resolver: ReceiptInputResolver,
|
|
373
|
+
): string {
|
|
374
|
+
if (receipt.flavor === "existence") return EXISTENCE_EXPECTATION;
|
|
375
|
+
const resolved = resolveValue(receipt.inputs, "inputs", resolver, new Set());
|
|
376
|
+
return sha256Digest(canonicalJson({ effect: receipt.effect, inputs: resolved }));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function resolveValue(
|
|
380
|
+
value: unknown,
|
|
381
|
+
path: string,
|
|
382
|
+
resolver: ReceiptInputResolver,
|
|
383
|
+
seen: Set<object>,
|
|
384
|
+
): unknown {
|
|
385
|
+
if (typeof value !== "object" || value === null) return value;
|
|
386
|
+
if (isIntrinsic(value)) {
|
|
387
|
+
const resolved = resolver(value, path);
|
|
388
|
+
if (resolved === undefined) {
|
|
389
|
+
throw new Error(`resolveReceiptExpectation: resolver returned undefined for reference at ${path}`);
|
|
390
|
+
}
|
|
391
|
+
if (isIntrinsic(resolved)) {
|
|
392
|
+
throw new Error(`resolveReceiptExpectation: resolver returned another reference for ${path}`);
|
|
393
|
+
}
|
|
394
|
+
return resolved;
|
|
395
|
+
}
|
|
396
|
+
if (seen.has(value)) {
|
|
397
|
+
throw new Error(`resolveReceiptExpectation: circular structure at ${path}`);
|
|
398
|
+
}
|
|
399
|
+
seen.add(value);
|
|
400
|
+
try {
|
|
401
|
+
if (Array.isArray(value)) {
|
|
402
|
+
return value.map((el, i) => resolveValue(el, `${path}[${i}]`, resolver, seen));
|
|
403
|
+
}
|
|
404
|
+
const out: Record<string, unknown> = {};
|
|
405
|
+
for (const [key, el] of Object.entries(value)) {
|
|
406
|
+
out[key] = resolveValue(el, `${path}.${key}`, resolver, seen);
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
} finally {
|
|
410
|
+
seen.delete(value);
|
|
411
|
+
}
|
|
412
|
+
}
|
package/src/fold/fold.test.ts
CHANGED
|
@@ -435,7 +435,11 @@ describe("fold — registered call-form intrinsics (#1044)", () => {
|
|
|
435
435
|
expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
|
|
436
436
|
});
|
|
437
437
|
|
|
438
|
-
test("an array method taking a closure is rejected — .map is
|
|
438
|
+
test("an array method taking a closure is rejected — the closure argument, not .map itself, is what fails (chant #1966)", () => {
|
|
439
|
+
// chant #1966 — `.map` is now attempted (a real method on a real folded
|
|
440
|
+
// array), so the rejection moves from "no case for this call" to the
|
|
441
|
+
// closure argument itself: a function used as a value is never foldable,
|
|
442
|
+
// with or without a registered intrinsic inside it.
|
|
439
443
|
const consts = parseConsts(`const cidrs = ["10.0.0.0/24"]; const x = cidrs.map((c) => Ref(c));`);
|
|
440
444
|
const expr = consts.get("x");
|
|
441
445
|
if (!expr) throw new Error("fixture error");
|
|
@@ -446,7 +450,7 @@ describe("fold — registered call-form intrinsics (#1044)", () => {
|
|
|
446
450
|
error = e;
|
|
447
451
|
}
|
|
448
452
|
expect(error).toBeInstanceOf(FoldError);
|
|
449
|
-
expect((error as FoldError).message).toContain("
|
|
453
|
+
expect((error as FoldError).message).toContain("a function used as a value is not foldable");
|
|
450
454
|
});
|
|
451
455
|
|
|
452
456
|
test("a user-defined function is rejected however it's named", () => {
|
package/src/fold/fold.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import { relative } from "node:path";
|
|
3
|
-
import { intrinsicCallFolds, intrinsicTagFolds, type IntrinsicDef } from "../lexicon";
|
|
3
|
+
import { intrinsicCallFolds, intrinsicCallFoldsEagerly, intrinsicTagFolds, type IntrinsicDef } from "../lexicon";
|
|
4
4
|
import {
|
|
5
5
|
SUPPORTED_BINARY_OPERATORS,
|
|
6
6
|
SUPPORTED_UNARY_OPERATORS,
|
|
@@ -67,7 +67,27 @@ import { isFoldableHelperName } from "./foldable-helpers";
|
|
|
67
67
|
* folds its body against the defining module's scope with the folded
|
|
68
68
|
* arguments bound. Still nothing is imported or run.
|
|
69
69
|
*
|
|
70
|
-
*
|
|
70
|
+
* A bare composite factory call (`Checkout({...})` on its own) is still out
|
|
71
|
+
* of scope (epic Phase 5, #1023 covers only interpreting a factory's OWN
|
|
72
|
+
* body, not consuming its result as a value elsewhere). chant #1174 adds one
|
|
73
|
+
* narrow exception at the PROPERTY-ACCESS level rather than here: the
|
|
74
|
+
* `<Identifier>(...).step` idiom — see {@link FoldedCompositeStepCall} on the
|
|
75
|
+
* property-access branch below. A call with no `.step` narrowing, or any
|
|
76
|
+
* other member, still throws from this branch exactly as before.
|
|
77
|
+
*
|
|
78
|
+
* chant #1966 adds a fourth call shape, and a method call on top of any of
|
|
79
|
+
* the four: a lexicon-package function its lexicon registered with
|
|
80
|
+
* {@link intrinsicCallFoldsEagerly} (../lexicon.ts) evaluates eagerly —
|
|
81
|
+
* unlike the other three, which envelope for later revival — because its
|
|
82
|
+
* usual use (`` `${matrix("os")}` ``) coerces the result via `String()` at
|
|
83
|
+
* fold time, before any revival would run. A `CallExpression` whose callee is
|
|
84
|
+
* a property access — `github.actor.toString()`, `[...].join(",")`,
|
|
85
|
+
* `matrix("os").toString()` — folds its receiver and calls the named method
|
|
86
|
+
* on it directly, PROVIDED the receiver is a real value and not one of
|
|
87
|
+
* fold's own symbolic envelopes (see {@link isFoldSymbolicEnvelope}); nothing
|
|
88
|
+
* about the method name is otherwise restricted.
|
|
89
|
+
*
|
|
90
|
+
* Everything else — an ordinary package function, an array `.map`, a
|
|
71
91
|
* registered name shadowed by a local binding — still throws.
|
|
72
92
|
*
|
|
73
93
|
* Cross-file identifier resolution (chant #1020): `consts` alone is always
|
|
@@ -105,7 +125,8 @@ export type FoldedValue =
|
|
|
105
125
|
| FoldedIntrinsic
|
|
106
126
|
| FoldedHelperCall
|
|
107
127
|
| SymbolicValue
|
|
108
|
-
| FoldedResource
|
|
128
|
+
| FoldedResource
|
|
129
|
+
| FoldedCompositeStepCall;
|
|
109
130
|
|
|
110
131
|
/**
|
|
111
132
|
* Symbolic reference produced when a property/element access resolves to an
|
|
@@ -225,6 +246,40 @@ export interface FoldedResource {
|
|
|
225
246
|
args?: FoldedValue[];
|
|
226
247
|
}
|
|
227
248
|
|
|
249
|
+
/**
|
|
250
|
+
* The result of folding the `<Identifier>(...).step` composite-consumer
|
|
251
|
+
* idiom (chant #1174) — `Checkout({...}).step`, `SetupNode({...}).step`,
|
|
252
|
+
* every lexicon's single-action `Composite()` wrapper embedded inline in a
|
|
253
|
+
* `Job`'s `steps` array, exactly as composites.mdx documents it. This is the
|
|
254
|
+
* SAME shape chant #1544 already carved out of EVL001 as a documented,
|
|
255
|
+
* correct fallback rather than a lint error
|
|
256
|
+
* ({@link "./subset"}'s `allowCompositeStepAccess`) — `fold()` never set
|
|
257
|
+
* that flag, so before this it fell back to run every time, as designed.
|
|
258
|
+
* This is the fold-side counterpart that actually reduces it instead.
|
|
259
|
+
*
|
|
260
|
+
* Symbolic, exactly like {@link FoldedIntrinsic}/{@link FoldedHelperCall}:
|
|
261
|
+
* `fold()` executes nothing here. It records which composite factory the
|
|
262
|
+
* source named, its folded arguments (in source order), and that the result
|
|
263
|
+
* was immediately narrowed to `.step`. ../discovery/fold-import.ts's bridge
|
|
264
|
+
* resolves the callee through the folding file's own imports — a
|
|
265
|
+
* project-file registered `Composite` is interpreted (chant #1023's existing
|
|
266
|
+
* machinery), anything else (every lexicon-package composite, which is what
|
|
267
|
+
* `Checkout`/`SetupNode` are) is imported and invoked for real, exactly as a
|
|
268
|
+
* top-level `export const x = Checkout({...})` already does via
|
|
269
|
+
* `resolveCallExpression` — and then reads `.step` off the REAL result.
|
|
270
|
+
*
|
|
271
|
+
* Deliberately narrower than "any call, any member access": the member name
|
|
272
|
+
* is fixed to `"step"`, matching the one idiom ../fold/subset.ts's EVL
|
|
273
|
+
* carve-out already permits. `fold()` may never accept a shape EVL doesn't
|
|
274
|
+
* (see that module's doc, point 2c, for the direction it is not allowed to
|
|
275
|
+
* be wrong in) — widening past `.step` here without widening the shared
|
|
276
|
+
* predicate in lockstep would open exactly that gap.
|
|
277
|
+
*/
|
|
278
|
+
export interface FoldedCompositeStepCall {
|
|
279
|
+
__compositeStep: string;
|
|
280
|
+
args: FoldedValue[];
|
|
281
|
+
}
|
|
282
|
+
|
|
228
283
|
/**
|
|
229
284
|
* One entry per exported `const` resource declaration in {@link foldModule}'s
|
|
230
285
|
* result. The `ok: false` case surfaces the same located, rule-id-tagged
|
|
@@ -648,11 +703,57 @@ function resolvesToResource(consts: Map<string, ts.Expression>, ident: ts.Identi
|
|
|
648
703
|
return init !== undefined && ts.isNewExpression(init);
|
|
649
704
|
}
|
|
650
705
|
|
|
706
|
+
/**
|
|
707
|
+
* True when `node` is a call through a bare identifier that isn't ALREADY
|
|
708
|
+
* one of `fold()`'s other three call shapes — a registered authoring helper,
|
|
709
|
+
* a registered call-form intrinsic, or a project-local {@link FoldableFunction}
|
|
710
|
+
* — i.e. exactly the callee the `CallExpression` branch below would
|
|
711
|
+
* otherwise throw {@link callExpressionMessage} for. Used only by the
|
|
712
|
+
* `.step` narrowing in the property-access branch (chant #1174,
|
|
713
|
+
* {@link FoldedCompositeStepCall}): checked from the OUTSIDE, at the
|
|
714
|
+
* property-access node, so `Checkout({...})` alone (no `.step`) still falls
|
|
715
|
+
* through to the ordinary `CallExpression` throw, unchanged.
|
|
716
|
+
*/
|
|
717
|
+
function isUnclaimedBareCall(
|
|
718
|
+
node: ts.Expression,
|
|
719
|
+
consts: Map<string, ts.Expression>,
|
|
720
|
+
intrinsics: readonly IntrinsicDef[],
|
|
721
|
+
externals?: ReadonlyMap<string, unknown>,
|
|
722
|
+
): node is ts.CallExpression {
|
|
723
|
+
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) return false;
|
|
724
|
+
const name = node.expression.text;
|
|
725
|
+
if (consts.has(name)) return false;
|
|
726
|
+
if (isFoldableHelperName(name)) return false;
|
|
727
|
+
if (intrinsics.some((i) => i.name === name && intrinsicCallFolds(i))) return false;
|
|
728
|
+
if (isFoldableFunction(externals?.get(name))) return false;
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
|
|
651
732
|
/** True when a folded value is a {@link FoldedResource} envelope (a `new Type(...)` that nothing constructed yet). */
|
|
652
733
|
function isFoldedResource(value: FoldedValue): value is FoldedResource {
|
|
653
734
|
return typeof value === "object" && value !== null && !Array.isArray(value) && "__resource" in value;
|
|
654
735
|
}
|
|
655
736
|
|
|
737
|
+
/**
|
|
738
|
+
* True when `value` is one of `fold()`'s own symbolic envelope shapes — a
|
|
739
|
+
* stand-in for a value nothing has constructed or revived yet, not the value
|
|
740
|
+
* itself. A method call (see the `CallExpression` branch's property-access
|
|
741
|
+
* case below) must refuse one rather than silently falling through to
|
|
742
|
+
* `Object.prototype`'s own inherited methods — `toString` chief among them —
|
|
743
|
+
* which would answer with the placeholder's shape instead of what the real,
|
|
744
|
+
* eventually-revived value would produce.
|
|
745
|
+
*/
|
|
746
|
+
function isFoldSymbolicEnvelope(value: FoldedValue): boolean {
|
|
747
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
748
|
+
return (
|
|
749
|
+
"__attrRef" in value ||
|
|
750
|
+
"__intrinsic" in value ||
|
|
751
|
+
"__helper" in value ||
|
|
752
|
+
"__resource" in value ||
|
|
753
|
+
"__compositeStep" in value
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
656
757
|
/**
|
|
657
758
|
* chant #1535 — an attribute read whose object folded to a resource ENVELOPE
|
|
658
759
|
* rather than resolving through {@link resolvesToResource}. That happens when
|
|
@@ -892,6 +993,15 @@ export function fold(
|
|
|
892
993
|
if (isFoldableFunction(external)) {
|
|
893
994
|
throw foldError(node, `function "${node.text}" used as a value is not foldable`);
|
|
894
995
|
}
|
|
996
|
+
// chant #1966 — a registered eager-fold lexicon helper (see the
|
|
997
|
+
// CallExpression branch below) is callable, never a bare value:
|
|
998
|
+
// nothing downstream can serialize a function.
|
|
999
|
+
if (
|
|
1000
|
+
typeof external === "function" &&
|
|
1001
|
+
intrinsics.some((i) => i.name === node.text && intrinsicCallFoldsEagerly(i))
|
|
1002
|
+
) {
|
|
1003
|
+
throw foldError(node, `function "${node.text}" used as a value is not foldable — call it instead`);
|
|
1004
|
+
}
|
|
895
1005
|
return external as FoldedValue;
|
|
896
1006
|
}
|
|
897
1007
|
// chant #1064 — a bare `process` reference is ALWAYS an ambient
|
|
@@ -948,6 +1058,21 @@ export function fold(
|
|
|
948
1058
|
if (ts.isIdentifier(node.expression) && resolvesToResource(consts, node.expression)) {
|
|
949
1059
|
return { __attrRef: { entity: node.expression.text, attribute: node.name.text } };
|
|
950
1060
|
}
|
|
1061
|
+
// chant #1174 — `<Identifier>(...).step`, the composite-consumer idiom
|
|
1062
|
+
// (`Checkout({...}).step`) — see FoldedCompositeStepCall's doc. Checked
|
|
1063
|
+
// here, at the property-access node, rather than inside the
|
|
1064
|
+
// CallExpression branch: a bare `Checkout({...})` with no `.step` still
|
|
1065
|
+
// has no case there and throws exactly as before.
|
|
1066
|
+
if (node.name.text === "step" && isUnclaimedBareCall(node.expression, consts, intrinsics, externals)) {
|
|
1067
|
+
const call = node.expression;
|
|
1068
|
+
const calleeName = (call.expression as ts.Identifier).text;
|
|
1069
|
+
const inside = insideFunctionBody(node, `composite call \`${calleeName}(...).step\``);
|
|
1070
|
+
if (inside) throw inside;
|
|
1071
|
+
return {
|
|
1072
|
+
__compositeStep: calleeName,
|
|
1073
|
+
args: call.arguments.map((arg) => fold(arg, consts, intrinsics, externals)),
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
951
1076
|
const obj = fold(node.expression, consts, intrinsics, externals);
|
|
952
1077
|
if (obj === null || obj === undefined) return undefined;
|
|
953
1078
|
if (isFoldedResource(obj)) return attrRefOnFoldedResource(node, node.name.text);
|
|
@@ -1146,6 +1271,62 @@ export function fold(
|
|
|
1146
1271
|
}
|
|
1147
1272
|
}
|
|
1148
1273
|
|
|
1274
|
+
// chant #1966 — the fourth call shape: a lexicon-package function its
|
|
1275
|
+
// lexicon registered with {@link intrinsicCallFoldsEagerly} (../lexicon.ts),
|
|
1276
|
+
// resolved into `externals` by ../discovery/fold-import.ts's
|
|
1277
|
+
// `resolveActiveLexiconExport` exactly like a plain data export
|
|
1278
|
+
// (`Azure.ResourceGroupLocation`, chant #1063), except callable. Unlike
|
|
1279
|
+
// the intrinsic-call shape above, this one is EVALUATED right here rather
|
|
1280
|
+
// than enveloped: a lexicon's own string-building helper (`matrix("os")`,
|
|
1281
|
+
// github lexicon) is typically embedded directly in a template literal
|
|
1282
|
+
// (`` `${matrix("os")}` ``), which coerces its result via native
|
|
1283
|
+
// `String()` at fold time — an envelope deferred to later revival would
|
|
1284
|
+
// stringify as "[object Object]" there. Evaluating eagerly, with the
|
|
1285
|
+
// folded arguments, produces the real, already-live return value instead
|
|
1286
|
+
// — the same guarantee a live external's own getter execution already
|
|
1287
|
+
// gives {@link fold}'s property-access branch.
|
|
1288
|
+
if (
|
|
1289
|
+
ts.isIdentifier(node.expression) &&
|
|
1290
|
+
!consts.has(node.expression.text) &&
|
|
1291
|
+
intrinsics.some((i) => i.name === (node.expression as ts.Identifier).text && intrinsicCallFoldsEagerly(i))
|
|
1292
|
+
) {
|
|
1293
|
+
const callee = externals?.get(node.expression.text);
|
|
1294
|
+
if (typeof callee !== "function") {
|
|
1295
|
+
throw foldError(node, `"${node.expression.text}" did not resolve to a function — falls back to run`);
|
|
1296
|
+
}
|
|
1297
|
+
const args = node.arguments.map((arg) => fold(arg, consts, intrinsics, externals));
|
|
1298
|
+
return (callee as (...callArgs: unknown[]) => unknown)(...args) as FoldedValue;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// chant #1966 — a method call whose RECEIVER is itself foldable: property
|
|
1302
|
+
// access on a live external (`github.actor.toString()`), a call folded by
|
|
1303
|
+
// one of the shapes above (`matrix("os").toString()`), or fold's own
|
|
1304
|
+
// array/object literal (`[...].join(",")`). The method is never checked
|
|
1305
|
+
// by name — only that the receiver is a REAL value (not one of fold's own
|
|
1306
|
+
// symbolic envelopes, see {@link isFoldSymbolicEnvelope}) and that the
|
|
1307
|
+
// named property on it is actually a function. Calling it with the folded
|
|
1308
|
+
// arguments is then no different from what running the file would do:
|
|
1309
|
+
// the receiver is the same real object either way.
|
|
1310
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
1311
|
+
const methodName = node.expression.name.text;
|
|
1312
|
+
const receiver = fold(node.expression.expression, consts, intrinsics, externals);
|
|
1313
|
+
if (receiver === null || receiver === undefined) {
|
|
1314
|
+
throw foldError(node, `cannot call ".${methodName}(...)" on ${String(receiver)}`);
|
|
1315
|
+
}
|
|
1316
|
+
if (isFoldSymbolicEnvelope(receiver)) {
|
|
1317
|
+
throw foldError(
|
|
1318
|
+
node,
|
|
1319
|
+
`method call \`.${methodName}(...)\` on an unresolved value is not foldable — falls back to run`,
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
const method = (receiver as Record<string, unknown>)[methodName];
|
|
1323
|
+
if (typeof method !== "function") {
|
|
1324
|
+
throw foldError(node, `"${methodName}" is not a callable method on the folded value — falls back to run`);
|
|
1325
|
+
}
|
|
1326
|
+
const args = node.arguments.map((arg) => fold(arg, consts, intrinsics, externals));
|
|
1327
|
+
return (method as (...methodArgs: unknown[]) => unknown).apply(receiver, args) as FoldedValue;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1149
1330
|
throw foldError(node, callExpressionMessage(node));
|
|
1150
1331
|
}
|
|
1151
1332
|
|