@intentius/chant 0.91.0 → 0.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +6 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +2 -1
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +12 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/lexicon-module.d.ts +22 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +19 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.d.ts +2 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/symptoms.d.ts +51 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +4 -2
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.d.ts +14 -3
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
- package/dist/op/activities/activity-contracts.d.ts +201 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/apply.d.ts.map +1 -1
- package/dist/op/activities/change-coverage.d.ts +44 -0
- package/dist/op/activities/change-coverage.d.ts.map +1 -0
- package/dist/op/activities/converge.d.ts +24 -1
- package/dist/op/activities/converge.d.ts.map +1 -1
- package/dist/op/activities/decide.d.ts +107 -0
- package/dist/op/activities/decide.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +10 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/shell.d.ts +32 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/source-release.d.ts +86 -0
- package/dist/op/activities/source-release.d.ts.map +1 -0
- package/dist/op/activities/source-rollback.d.ts +100 -0
- package/dist/op/activities/source-rollback.d.ts.map +1 -0
- package/dist/op/activities/work-evidence.d.ts +42 -0
- package/dist/op/activities/work-evidence.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +1 -0
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/activity-profiles.d.ts +16 -0
- package/dist/op/activity-profiles.d.ts.map +1 -1
- package/dist/op/builders.d.ts +84 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/composites/converge-op.d.ts +31 -4
- package/dist/op/composites/converge-op.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts +116 -0
- package/dist/op/decide-backend.d.ts.map +1 -0
- package/dist/op/decide-config.d.ts +120 -0
- package/dist/op/decide-config.d.ts.map +1 -0
- package/dist/op/decide-read-inputs.d.ts +40 -0
- package/dist/op/decide-read-inputs.d.ts.map +1 -0
- package/dist/op/gate-wait.d.ts +43 -0
- package/dist/op/gate-wait.d.ts.map +1 -0
- package/dist/op/gate.d.ts +5 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +11 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +29 -4
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts +2 -0
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -0
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/operator.d.ts +97 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +37 -5
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/source-archive.d.ts +63 -0
- package/dist/op/source-archive.d.ts.map +1 -0
- package/dist/op/steward-points.d.ts +136 -0
- package/dist/op/steward-points.d.ts.map +1 -0
- package/dist/op/steward-turn.d.ts +53 -0
- package/dist/op/steward-turn.d.ts.map +1 -0
- package/dist/op/steward.d.ts +22 -1
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/types.d.ts +7 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts +1 -1
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
- package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
- package/dist/workspace/answers-ledger.d.ts +76 -0
- package/dist/workspace/answers-ledger.d.ts.map +1 -0
- package/dist/workspace/changes-cli.d.ts +16 -0
- package/dist/workspace/changes-cli.d.ts.map +1 -0
- package/dist/workspace/changes.d.ts +200 -0
- package/dist/workspace/changes.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
- package/dist/workspace/chant-migrations.d.ts +99 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -0
- package/dist/workspace/checks/boxes.d.ts +34 -1
- package/dist/workspace/checks/boxes.d.ts.map +1 -1
- package/dist/workspace/checks/diagrams.d.ts +31 -0
- package/dist/workspace/checks/diagrams.d.ts.map +1 -0
- package/dist/workspace/checks/records.d.ts +7 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +3 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +2 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +49 -4
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/declaration.d.ts +52 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +114 -1
- package/dist/workspace/declared-kinds.d.ts +20 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/generated-files.d.ts +7 -0
- package/dist/workspace/generated-files.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/lineage-check.d.ts +3 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +6 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +39 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/point-answer.schema.json +95 -0
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts +14 -2
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +11 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +1 -1
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +11 -5
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +34 -4
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/status-gates.d.ts +7 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +59 -1
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +16 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +113 -2
- package/dist/workspace/work-cli.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +74 -0
- package/dist/workspace/work-evidence.d.ts.map +1 -0
- package/dist/workspace/work.d.ts +43 -1
- package/dist/workspace/work.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/audit.test.ts +9 -1
- package/src/cli/commands/build.ts +3 -2
- package/src/cli/commands/lint.ts +7 -1
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +16 -2
- package/src/cli/handlers/operator.ts +34 -2
- package/src/cli/handlers/run.test.ts +154 -2
- package/src/cli/handlers/run.ts +121 -2
- package/src/cli/main.test.ts +10 -0
- package/src/cli/main.ts +19 -0
- package/src/cli/mcp/workspace-tools.test.ts +35 -19
- package/src/cli/mcp/workspace-tools.ts +62 -4
- package/src/cli/plugins.ts +11 -3
- package/src/cli/registry.ts +12 -2
- package/src/codegen/pinned-upgrade.ts +2 -1
- package/src/codegen/release-wiring.test.ts +49 -2
- package/src/components/capability-plugin-loader.ts +3 -3
- package/src/components/cli-support.ts +2 -2
- package/src/config.ts +11 -0
- package/src/lexicon-module.ts +56 -1
- package/src/lexicon-package.test.ts +96 -0
- package/src/lifecycle/converge-ledger.ts +20 -0
- package/src/lifecycle/run-ledger.ts +6 -0
- package/src/lifecycle/symptoms.ts +95 -0
- package/src/lint/rules/index.ts +6 -2
- package/src/lint/rules/op/index.ts +17 -4
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
- package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
- package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
- package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
- package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
- package/src/lint/rules/sys001-no-literal-key.ts +67 -0
- package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
- package/src/op/activities/activity-contracts.ts +190 -1
- package/src/op/activities/apply.ts +7 -6
- package/src/op/activities/change-coverage.ts +60 -0
- package/src/op/activities/converge.test.ts +55 -1
- package/src/op/activities/converge.ts +208 -29
- package/src/op/activities/decide.live.test.ts +78 -0
- package/src/op/activities/decide.test.ts +300 -0
- package/src/op/activities/decide.ts +267 -0
- package/src/op/activities/index.ts +21 -0
- package/src/op/activities/shell.test.ts +91 -1
- package/src/op/activities/shell.ts +91 -2
- package/src/op/activities/source-release.test.ts +118 -0
- package/src/op/activities/source-release.ts +168 -0
- package/src/op/activities/source-rollback.test.ts +131 -0
- package/src/op/activities/source-rollback.ts +239 -0
- package/src/op/activities/work-evidence.ts +62 -0
- package/src/op/activity-contract-registry.test.ts +2 -0
- package/src/op/activity-contract-registry.ts +2 -2
- package/src/op/activity-contract.test.ts +12 -0
- package/src/op/activity-contract.ts +6 -1
- package/src/op/activity-profiles.ts +29 -0
- package/src/op/activity-registry.test.ts +1 -1
- package/src/op/activity-registry.ts +2 -2
- package/src/op/builders.ts +119 -5
- package/src/op/command-gate.test.ts +192 -0
- package/src/op/composites/converge-op.test.ts +27 -1
- package/src/op/composites/converge-op.ts +69 -15
- package/src/op/decide-backend.test.ts +44 -0
- package/src/op/decide-backend.ts +212 -0
- package/src/op/decide-config.test.ts +52 -0
- package/src/op/decide-config.ts +80 -0
- package/src/op/decide-read-inputs.ts +117 -0
- package/src/op/gate-approval.ts +2 -2
- package/src/op/gate-wait.ts +57 -0
- package/src/op/gate.ts +8 -3
- package/src/op/generate-pipeline.ts +2 -2
- package/src/op/index.ts +18 -3
- package/src/op/local-executor.ts +151 -15
- package/src/op/local-output.test.ts +33 -0
- package/src/op/local-output.ts +20 -2
- package/src/op/op-ir.test.ts +10 -0
- package/src/op/op-ir.ts +4 -0
- package/src/op/op-verb-class.ts +1 -0
- package/src/op/operator.ts +290 -11
- package/src/op/runtime.ts +31 -8
- package/src/op/runtimes/local.ts +3 -1
- package/src/op/source-archive.ts +161 -0
- package/src/op/steward-points.test.ts +313 -0
- package/src/op/steward-points.ts +194 -0
- package/src/op/steward-turn.ts +79 -0
- package/src/op/steward.test.ts +87 -3
- package/src/op/steward.ts +25 -1
- package/src/op/types.ts +7 -0
- package/src/op/work-lease-run.test.ts +20 -1
- package/src/op/work-lease-run.ts +2 -2
- package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
- package/src/workspace/answers-ledger.ts +118 -0
- package/src/workspace/changes-cli.ts +56 -0
- package/src/workspace/changes.schema.json +472 -0
- package/src/workspace/changes.test.ts +315 -0
- package/src/workspace/changes.ts +512 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
- package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
- package/src/workspace/chant-migrations.ts +164 -0
- package/src/workspace/check-contract.test.ts +4 -2
- package/src/workspace/check.schema.json +15 -5
- package/src/workspace/checks/boxes.test.ts +80 -1
- package/src/workspace/checks/boxes.ts +136 -1
- package/src/workspace/checks/diagrams.test.ts +134 -0
- package/src/workspace/checks/diagrams.ts +108 -0
- package/src/workspace/checks/records.ts +47 -0
- package/src/workspace/checks.ts +8 -1
- package/src/workspace/compose-graph.ts +2 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
- package/src/workspace/conformance/index.ts +5 -0
- package/src/workspace/decide.test.ts +1 -1
- package/src/workspace/decide.ts +156 -23
- package/src/workspace/declaration.schema.json +114 -1
- package/src/workspace/declaration.test.ts +58 -0
- package/src/workspace/declaration.ts +104 -0
- package/src/workspace/declared-kinds.test.ts +5 -5
- package/src/workspace/declared-kinds.ts +46 -1
- package/src/workspace/generated-files.ts +14 -0
- package/src/workspace/graph-cli.ts +1 -1
- package/src/workspace/graph.schema.json +9 -1
- package/src/workspace/intent.schema.json +8 -2
- package/src/workspace/intent.ts +3 -10
- package/src/workspace/lineage-check.ts +16 -1
- package/src/workspace/lineage-upgrade-cli.ts +1 -1
- package/src/workspace/lineage-upgrade.test.ts +70 -0
- package/src/workspace/lineage-upgrade.ts +81 -10
- package/src/workspace/ls-contract.test.ts +57 -0
- package/src/workspace/ls.schema.json +46 -1
- package/src/workspace/ls.ts +62 -3
- package/src/workspace/points-cli.ts +14 -3
- package/src/workspace/points-write.schema.json +11 -1
- package/src/workspace/points.schema.json +33 -0
- package/src/workspace/points.test.ts +1 -1
- package/src/workspace/points.ts +1 -1
- package/src/workspace/read-contract.test.ts +1 -1
- package/src/workspace/reason-codes.test.ts +11 -1
- package/src/workspace/reason-codes.ts +18 -2
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-cli.ts +13 -3
- package/src/workspace/records-close.schema.json +2 -0
- package/src/workspace/records-new.schema.json +2 -0
- package/src/workspace/records-review.schema.json +2 -0
- package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
- package/src/workspace/records-sessions-write.test.ts +2 -34
- package/src/workspace/records-write.e2e.test.ts +108 -0
- package/src/workspace/records-write.test.ts +32 -41
- package/src/workspace/records-write.ts +33 -15
- package/src/workspace/records.schema.json +36 -2
- package/src/workspace/records.test.ts +48 -3
- package/src/workspace/records.ts +109 -3
- package/src/workspace/status-contract.test.ts +13 -8
- package/src/workspace/status-gates.ts +9 -4
- package/src/workspace/status-stewards.ts +108 -4
- package/src/workspace/status.schema.json +111 -7
- package/src/workspace/status.ts +36 -1
- package/src/workspace/work-acceptance.test.ts +274 -0
- package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
- package/src/workspace/work-cli.ts +193 -10
- package/src/workspace/work-evidence.schema.json +212 -0
- package/src/workspace/work-evidence.ts +169 -0
- package/src/workspace/work-history.schema.json +349 -0
- package/src/workspace/work-history.test.ts +156 -0
- package/src/workspace/work.test.ts +3 -2
- package/src/workspace/work.ts +100 -1
- /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
- /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
- /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import { exec } from "node:child_process";
|
|
36
36
|
import { promisify } from "node:util";
|
|
37
37
|
import { normalizeApply, type ApplyResult } from "../../apply";
|
|
38
|
+
import { importLexiconPackage } from "../../lexicon-module";
|
|
38
39
|
|
|
39
40
|
const execAsync = promisify(exec);
|
|
40
41
|
|
|
@@ -241,7 +242,7 @@ async function loadK8sApplier(): Promise<K8sApplier> {
|
|
|
241
242
|
const spec = "@intentius/chant-lexicon-k8s/op/activities";
|
|
242
243
|
let mod: { applyManifest?: K8sApplier };
|
|
243
244
|
try {
|
|
244
|
-
mod = (await
|
|
245
|
+
mod = (await importLexiconPackage(spec)) as { applyManifest?: K8sApplier };
|
|
245
246
|
} catch (err) {
|
|
246
247
|
throw new Error(
|
|
247
248
|
`apply target "kubectl" needs @intentius/chant-lexicon-k8s, which could not be loaded ` +
|
|
@@ -269,7 +270,7 @@ async function loadAzureApplier(): Promise<AzureApplier> {
|
|
|
269
270
|
};
|
|
270
271
|
let mod: AzureModule;
|
|
271
272
|
try {
|
|
272
|
-
mod = (await
|
|
273
|
+
mod = (await importLexiconPackage(spec)) as AzureModule;
|
|
273
274
|
} catch (err) {
|
|
274
275
|
throw new Error(
|
|
275
276
|
`apply target "arm" needs @intentius/chant-lexicon-azure, which could not be loaded ` +
|
|
@@ -304,7 +305,7 @@ async function loadGcpApplier(): Promise<GcpApplier> {
|
|
|
304
305
|
};
|
|
305
306
|
let mod: GcpModule;
|
|
306
307
|
try {
|
|
307
|
-
mod = (await
|
|
308
|
+
mod = (await importLexiconPackage(spec)) as GcpModule;
|
|
308
309
|
} catch (err) {
|
|
309
310
|
throw new Error(
|
|
310
311
|
`apply target "gcp" needs @intentius/chant-lexicon-gcp, which could not be loaded ` +
|
|
@@ -339,7 +340,7 @@ async function loadFlyApplier(): Promise<FlyApplier> {
|
|
|
339
340
|
};
|
|
340
341
|
let mod: FlyModule;
|
|
341
342
|
try {
|
|
342
|
-
mod = (await
|
|
343
|
+
mod = (await importLexiconPackage(spec)) as FlyModule;
|
|
343
344
|
} catch (err) {
|
|
344
345
|
throw new Error(
|
|
345
346
|
`apply target "fly" needs @intentius/chant-lexicon-fly, which could not be loaded ` +
|
|
@@ -370,7 +371,7 @@ async function loadAwsApplier(): Promise<AwsApplier> {
|
|
|
370
371
|
const spec = "@intentius/chant-lexicon-aws/op/activities";
|
|
371
372
|
let mod: { awsApply?: AwsApplier };
|
|
372
373
|
try {
|
|
373
|
-
mod = (await
|
|
374
|
+
mod = (await importLexiconPackage(spec)) as { awsApply?: AwsApplier };
|
|
374
375
|
} catch (err) {
|
|
375
376
|
throw new Error(
|
|
376
377
|
`apply target "cloudformation" needs @intentius/chant-lexicon-aws, which could not be loaded ` +
|
|
@@ -395,7 +396,7 @@ async function loadAwsRollback(): Promise<AwsRollback> {
|
|
|
395
396
|
const spec = "@intentius/chant-lexicon-aws/op/activities";
|
|
396
397
|
let mod: { rollbackStack?: AwsRollback };
|
|
397
398
|
try {
|
|
398
|
-
mod = (await
|
|
399
|
+
mod = (await importLexiconPackage(spec)) as { rollbackStack?: AwsRollback };
|
|
399
400
|
} catch (err) {
|
|
400
401
|
throw new Error(
|
|
401
402
|
`rollback for target "cloudformation" needs @intentius/chant-lexicon-aws, which could not be ` +
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* changeCoverage — the forward coverage check (#2773) as an Op activity, for
|
|
3
|
+
* an Op that changes the checkout (#2748) to run on its own diff.
|
|
4
|
+
*
|
|
5
|
+
* Such an Op's leased steps work in a worktree of their own on
|
|
6
|
+
* `chant/work/[<member>/]<item>`. Given that worktree as `cwd`
|
|
7
|
+
* (`workLeaseOutput("worktree")`) and no `range`, the activity checks the
|
|
8
|
+
* commits the branch holds beyond the main worktree's HEAD, with the branch's
|
|
9
|
+
* item as the work item in hand, exactly as `chant workspace check --changes
|
|
10
|
+
* <base>..HEAD --work <item>` would. A `range` or `work` given replaces what
|
|
11
|
+
* the branch says.
|
|
12
|
+
*
|
|
13
|
+
* The step fails when the check's severity is `fail` and it has a finding,
|
|
14
|
+
* or when the check can't run; otherwise it returns the document's findings
|
|
15
|
+
* and summary for later steps. The workspace code loads on first call, so
|
|
16
|
+
* importing core's activities loads nothing under `workspace/` (#2525 rule 5).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ChangeFinding, ChangesDocument } from "../../workspace/changes";
|
|
20
|
+
|
|
21
|
+
export interface ChangeCoverageArgs {
|
|
22
|
+
/** The checkout to check: the Op's worktree. Default: the working directory. */
|
|
23
|
+
cwd?: string;
|
|
24
|
+
/** `<base>..<head>`, `<base>...<head>` or `<base>`. Default: the work branch's own commits. */
|
|
25
|
+
range?: string;
|
|
26
|
+
/** The work item in hand. Default: the work branch's item. */
|
|
27
|
+
work?: string;
|
|
28
|
+
/** In place of the declaration's `changes.severity`. */
|
|
29
|
+
severity?: "off" | "warn" | "fail";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ChangeCoverageResult {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
range: { spec: string; base: string; head: string };
|
|
35
|
+
work: string | null;
|
|
36
|
+
severity: "off" | "warn" | "fail";
|
|
37
|
+
findings: Pick<ChangeFinding, "code" | "path" | "message" | "severity" | "records" | "triage">[];
|
|
38
|
+
summary: Exclude<ChangesDocument, { error: unknown }>["summary"];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function changeCoverage(args: ChangeCoverageArgs = {}): Promise<ChangeCoverageResult> {
|
|
42
|
+
const { checkChanges, workBranchChanges } = await import("../../workspace/changes");
|
|
43
|
+
const cwd = args.cwd ?? process.cwd();
|
|
44
|
+
const branch = args.range === undefined ? workBranchChanges(cwd) : undefined;
|
|
45
|
+
const range = args.range ?? branch?.range;
|
|
46
|
+
if (range === undefined) throw new Error(`changeCoverage: ${cwd} is not on a chant/work/ branch, so give the range to check`);
|
|
47
|
+
const work = args.work ?? branch?.work;
|
|
48
|
+
const { doc } = await checkChanges({ cwd, range, ...(work !== undefined ? { work } : {}), ...(args.severity ? { severity: args.severity } : {}) });
|
|
49
|
+
if ("error" in doc) throw new Error(`changeCoverage: ${doc.error.code}: ${doc.error.message}`);
|
|
50
|
+
const result: ChangeCoverageResult = {
|
|
51
|
+
ok: doc.ok,
|
|
52
|
+
range: doc.range,
|
|
53
|
+
work: doc.work?.record ?? null,
|
|
54
|
+
severity: doc.severity,
|
|
55
|
+
findings: doc.findings.map(({ code, path, message, severity, records, triage }) => ({ code, path, message, severity, records, triage })),
|
|
56
|
+
summary: doc.summary,
|
|
57
|
+
};
|
|
58
|
+
if (!doc.ok) throw new Error(`changeCoverage: ${doc.findings.length} change ${doc.findings.length === 1 ? "finding" : "findings"} at severity fail: ${doc.findings.map((f) => `${f.code} ${f.path}`).join(", ")}`);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { eq, gt, run, report, when } from "../converge-rule";
|
|
3
|
-
import type
|
|
3
|
+
import { parseResourceObservation, type ConvergeSymptom, type ObservedResource, type ResourceSymptom } from "../../lifecycle/symptoms";
|
|
4
4
|
import type { ConvergeTickRecord, ConvergeRuleOutcome } from "../../lifecycle/converge-ledger";
|
|
5
5
|
import {
|
|
6
6
|
planConvergeTick,
|
|
7
|
+
planResourceTick,
|
|
8
|
+
consecutiveResourceFires,
|
|
7
9
|
verbClassAllowedToDispatch,
|
|
8
10
|
enforceVerbClassAtDispatch,
|
|
9
11
|
sanitizeOneLine,
|
|
@@ -354,3 +356,55 @@ describe("classifyDispatchFailure", () => {
|
|
|
354
356
|
expect(classifyDispatchFailure("")).toBeUndefined();
|
|
355
357
|
});
|
|
356
358
|
});
|
|
359
|
+
|
|
360
|
+
describe("planResourceTick — rules evaluated once per observed resource (#2778)", () => {
|
|
361
|
+
const restart = when<ResourceSymptom>(eq("status", "drifted"), run("restart-service"), { id: "restart-drifted", why: "Restart a drifted service." });
|
|
362
|
+
const unknownR = when<ResourceSymptom>(eq("status", "unknown"), report("not observed"), { id: "report-unknown", why: "unknown never remediates." });
|
|
363
|
+
const resources: ObservedResource[] = [
|
|
364
|
+
{ name: "app", status: "drifted", detail: "stopped" },
|
|
365
|
+
{ name: "hud", status: "in-sync" },
|
|
366
|
+
{ name: "door", status: "drifted" },
|
|
367
|
+
{ name: "site", status: "unknown", detail: "no health" },
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
test("a fired run() is planned for each drifted resource, and names it", () => {
|
|
371
|
+
const plan = planResourceTick("box", resources, [restart, unknownR], [], "box-converge", "apply", 3);
|
|
372
|
+
expect(plan.firedRuleIds).toEqual(["restart-drifted", "report-unknown"]);
|
|
373
|
+
expect(plan.outcomes).toEqual([
|
|
374
|
+
{ ruleId: "restart-drifted", resource: "app", action: "ran", op: "restart-service" },
|
|
375
|
+
{ ruleId: "restart-drifted", resource: "door", action: "ran", op: "restart-service" },
|
|
376
|
+
{ ruleId: "report-unknown", resource: "site", action: "reported", reason: "not observed" },
|
|
377
|
+
]);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("unknown never remediates, the dial and the budget hold per tick", () => {
|
|
381
|
+
const anyStatus = when<ResourceSymptom>(eq("env", "box"), run("restart-service"), { id: "restart-all", why: "x" });
|
|
382
|
+
const plan = planResourceTick("box", [{ name: "site", status: "unknown" }], [anyStatus], [], "box-converge", "apply", 3);
|
|
383
|
+
expect(plan.outcomes[0]).toMatchObject({ action: "reported", resource: "site", reason: expect.stringMatching(/unknown never remediates/) });
|
|
384
|
+
expect(planResourceTick("box", resources, [restart], [], "box-converge", "observe", 3).outcomes.map((o) => o.action)).toEqual(["reported", "reported"]);
|
|
385
|
+
expect(planResourceTick("box", resources, [restart], [], "box-converge", "apply", 1).outcomes.map((o) => o.action)).toEqual(["ran", "skipped-budget"]);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("flap damping counts one rule's fires for one resource, on this op's ticks", () => {
|
|
389
|
+
const fired = (resource: string, op = "box-converge") =>
|
|
390
|
+
tick({ op, firedRuleIds: ["restart-drifted"], outcomes: [{ ruleId: "restart-drifted", action: "ran", op: "restart-service", resource }] });
|
|
391
|
+
const prior = [fired("app"), fired("app"), fired("hud", "other-converge"), fired("app")];
|
|
392
|
+
expect(consecutiveResourceFires(prior, "box-converge", "restart-drifted", "app")).toBe(3);
|
|
393
|
+
expect(consecutiveResourceFires(prior, "box-converge", "restart-drifted", "door")).toBe(0);
|
|
394
|
+
const plan = planResourceTick("box", resources, [restart], prior, "box-converge", "apply", 3);
|
|
395
|
+
expect(plan.outcomes.map((o) => [o.resource, o.action])).toEqual([["app", "skipped-flap"], ["door", "ran"]]);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe("parseResourceObservation (#2778)", () => {
|
|
400
|
+
test("reads an observation, or a shell result that prints one, and refuses anything else", () => {
|
|
401
|
+
const observation = { resources: [{ name: "app", status: "drifted", detail: "stopped" }] };
|
|
402
|
+
expect(parseResourceObservation(observation)).toEqual(observation);
|
|
403
|
+
expect(parseResourceObservation({ stdout: JSON.stringify(observation), stderr: "", exitCode: 0 })).toEqual(observation);
|
|
404
|
+
expect(() => parseResourceObservation({ stdout: "nope", stderr: "", exitCode: 0 })).toThrow(/not JSON/);
|
|
405
|
+
expect(() => parseResourceObservation({})).toThrow(/no `resources` array/);
|
|
406
|
+
expect(() => parseResourceObservation({ resources: [{ name: "app", status: "down" }] })).toThrow(/in-sync, drifted or unknown/);
|
|
407
|
+
expect(() => parseResourceObservation({ resources: [{ status: "drifted" }] })).toThrow(/has no name/);
|
|
408
|
+
expect(() => parseResourceObservation({ resources: [{ name: "a", status: "drifted" }, { name: "a", status: "in-sync" }] })).toThrow(/twice/);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
@@ -40,7 +40,14 @@ import {
|
|
|
40
40
|
} from "../converge-rule";
|
|
41
41
|
import { classifyOpVerbClass, type OpVerbClass } from "../op-verb-class";
|
|
42
42
|
import { discoverOps } from "../discover";
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
deriveSymptoms,
|
|
45
|
+
parseResourceObservation,
|
|
46
|
+
CONVERGE_RESOURCE_ENV,
|
|
47
|
+
type ConvergeSymptom,
|
|
48
|
+
type ObservedResource,
|
|
49
|
+
type ResourceSymptom,
|
|
50
|
+
} from "../../lifecycle/symptoms";
|
|
44
51
|
import {
|
|
45
52
|
appendConvergeRecord,
|
|
46
53
|
readConvergeLedger,
|
|
@@ -88,6 +95,14 @@ export interface ConvergeTickArgs {
|
|
|
88
95
|
* (this activity re-derives its own symptom independently).
|
|
89
96
|
*/
|
|
90
97
|
preflightDrift?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* The observer step's result, for a ConvergeOp declared with `observe`
|
|
100
|
+
* (#2778): a {@link ResourceObservation}, or a `shell()` result whose
|
|
101
|
+
* stdout is one. Present, the tick observes nothing itself: it evaluates
|
|
102
|
+
* `rules` once per reported resource, against a `ResourceSymptom`, and
|
|
103
|
+
* dispatches a fired `run()` for that resource.
|
|
104
|
+
*/
|
|
105
|
+
observed?: unknown;
|
|
91
106
|
}
|
|
92
107
|
|
|
93
108
|
export interface ConvergeTickResult {
|
|
@@ -209,9 +224,10 @@ export function classifyDispatchFailure(raw: string): { gateName: string } | und
|
|
|
209
224
|
async function dispatchOp(
|
|
210
225
|
opName: string,
|
|
211
226
|
signal?: AbortSignal,
|
|
227
|
+
env?: Record<string, string>,
|
|
212
228
|
): Promise<{ ok: true } | { ok: false; error: string; gateName?: string }> {
|
|
213
229
|
try {
|
|
214
|
-
await execAsync(`chant run ${shellQuote(opName)} --json`, { signal });
|
|
230
|
+
await execAsync(`chant run ${shellQuote(opName)} --json`, { signal, ...(env ? { env: { ...process.env, ...env } } : {}) });
|
|
215
231
|
return { ok: true };
|
|
216
232
|
} catch (err) {
|
|
217
233
|
const e = err as { code?: number; stdout?: string; stderr?: string; message?: string };
|
|
@@ -383,25 +399,106 @@ function renderLog(env: string, s: ConvergeSymptom, counts: ReturnType<typeof su
|
|
|
383
399
|
);
|
|
384
400
|
}
|
|
385
401
|
|
|
386
|
-
|
|
387
|
-
await fetchLifecycle().catch(() => undefined);
|
|
402
|
+
// ── Resources an observer step reports (#2778) ──────────────────────────────
|
|
388
403
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
404
|
+
/**
|
|
405
|
+
* How many consecutive most-recent ticks of `op` fired `ruleId` for
|
|
406
|
+
* `resource`. A rule of a ConvergeOp with an observer step fires once per
|
|
407
|
+
* resource, so its flap damping is counted per resource: a rule that
|
|
408
|
+
* restarts `app` on one tick and `hud` on the next is not flapping.
|
|
409
|
+
*/
|
|
410
|
+
export function consecutiveResourceFires(records: ConvergeTickRecord[], op: string, ruleId: string, resource: string): number {
|
|
411
|
+
let count = 0;
|
|
412
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
413
|
+
if (records[i].op !== op) continue;
|
|
414
|
+
if (!records[i].outcomes.some((o) => o.ruleId === ruleId && o.resource === resource)) break;
|
|
415
|
+
count++;
|
|
416
|
+
}
|
|
417
|
+
return count;
|
|
418
|
+
}
|
|
394
419
|
|
|
395
|
-
|
|
396
|
-
|
|
420
|
+
/**
|
|
421
|
+
* The pure planning core of a resource tick (#2778): every rule against every
|
|
422
|
+
* observed resource, in authored order within each resource. The rules of
|
|
423
|
+
* {@link planConvergeTick} hold per resource: `unknown` never remediates, a
|
|
424
|
+
* `report()` never dispatches, the dial decides whether a `run()` may, and
|
|
425
|
+
* `budget` bounds the dispatches across the whole tick.
|
|
426
|
+
*/
|
|
427
|
+
export function planResourceTick(
|
|
428
|
+
env: string,
|
|
429
|
+
resources: ObservedResource[],
|
|
430
|
+
rules: ConvergeRule<ResourceSymptom>[],
|
|
431
|
+
priorRecords: ConvergeTickRecord[],
|
|
432
|
+
op: string,
|
|
433
|
+
dial: ConvergeTickArgs["dial"],
|
|
434
|
+
budget: number,
|
|
435
|
+
): ConvergeTickPlan {
|
|
436
|
+
const fired = new Set<string>();
|
|
437
|
+
const outcomes: ConvergeRuleOutcome[] = [];
|
|
438
|
+
let dispatched = 0;
|
|
397
439
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
440
|
+
for (const resource of resources) {
|
|
441
|
+
const symptom: ResourceSymptom = { env, resource: resource.name, status: resource.status, detail: resource.detail ?? "" };
|
|
442
|
+
for (const rule of rules) {
|
|
443
|
+
if (!evaluatePredicate(rule.when, symptom)) continue;
|
|
444
|
+
fired.add(rule.id);
|
|
445
|
+
const base = { ruleId: rule.id, resource: resource.name };
|
|
446
|
+
|
|
447
|
+
const threshold = rule.flapThreshold ?? DEFAULT_FLAP_THRESHOLD;
|
|
448
|
+
const consecutive = consecutiveResourceFires(priorRecords, op, rule.id, resource.name);
|
|
449
|
+
if (consecutive >= threshold) {
|
|
450
|
+
outcomes.push({
|
|
451
|
+
...base,
|
|
452
|
+
action: "skipped-flap",
|
|
453
|
+
reason: `fired for ${resource.name} ${consecutive + 1} consecutive ticks without clearing (threshold ${threshold}) — escalated to report`,
|
|
454
|
+
});
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const action: RuleAction = rule.then;
|
|
459
|
+
if (resource.status === "unknown" && action.kind === "run") {
|
|
460
|
+
outcomes.push({
|
|
461
|
+
...base,
|
|
462
|
+
action: "reported",
|
|
463
|
+
reason: `${resource.name} is unknown (${resource.detail || "unobserved"}) — unknown never remediates`,
|
|
464
|
+
});
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (action.kind === "report") {
|
|
468
|
+
outcomes.push({ ...base, action: "reported", reason: action.reason });
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (!dialAllowsDispatch(dial)) {
|
|
472
|
+
outcomes.push({ ...base, action: "reported", reason: `dial "${dial}" does not permit dispatch — report only`, op: action.op });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (dispatched >= budget) {
|
|
476
|
+
outcomes.push({ ...base, action: "skipped-budget", op: action.op });
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
dispatched++;
|
|
480
|
+
outcomes.push({ ...base, action: "ran", op: action.op });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return { firedRuleIds: [...fired], outcomes };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function renderResourceLog(env: string, resources: ObservedResource[], counts: ReturnType<typeof summarizeOutcomes>): string {
|
|
487
|
+
const n = (status: ObservedResource["status"]) => resources.filter((r) => r.status === status).length;
|
|
488
|
+
return (
|
|
489
|
+
`converge(${env}): resources=${resources.length} in-sync=${n("in-sync")} drifted=${n("drifted")} unknown=${n("unknown")} ` +
|
|
490
|
+
`remediated=${counts.remediated} reported=${counts.reported} skipped-budget=${counts.skippedBudget} ` +
|
|
491
|
+
`skipped-flap=${counts.skippedFlap} gated=${counts.gated}`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Run the planned dispatches of a tick, in place: the verb-class backstop, then `chant run`, then what came of it. */
|
|
496
|
+
async function dispatchPlanned(
|
|
497
|
+
outcomes: ConvergeRuleOutcome[],
|
|
498
|
+
args: ConvergeTickArgs,
|
|
499
|
+
signal?: AbortSignal,
|
|
500
|
+
): Promise<void> {
|
|
501
|
+
for (const outcome of outcomes) {
|
|
405
502
|
if (outcome.action !== "ran" || !outcome.op) continue;
|
|
406
503
|
|
|
407
504
|
const verbClass = await classifyDispatchTarget(outcome.op);
|
|
@@ -410,7 +507,9 @@ export async function convergeTick(args: ConvergeTickArgs, signal?: AbortSignal)
|
|
|
410
507
|
outcome.reason = backstopped.reason;
|
|
411
508
|
if (outcome.action !== "ran") continue;
|
|
412
509
|
|
|
413
|
-
|
|
510
|
+
// A rule fired for one resource (#2778) hands the dispatched Op its name.
|
|
511
|
+
const env = outcome.resource !== undefined ? { [CONVERGE_RESOURCE_ENV]: outcome.resource } : undefined;
|
|
512
|
+
const result = await dispatchOp(outcome.op, signal, env);
|
|
414
513
|
if (!result.ok && result.gateName) {
|
|
415
514
|
// Gate-as-fact (#1485, closed by #2119): a terminal, durable,
|
|
416
515
|
// non-blocking fact — not an ordinary dispatch failure the operator
|
|
@@ -432,6 +531,95 @@ export async function convergeTick(args: ConvergeTickArgs, signal?: AbortSignal)
|
|
|
432
531
|
outcome.reason = `dispatch of "${outcome.op}" failed: ${result.error}`;
|
|
433
532
|
}
|
|
434
533
|
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Push the tick's ledger record, and say whether it reached the remote (chant#2337). */
|
|
537
|
+
async function pushTick(): Promise<{ pushed: boolean; pushWarning?: string }> {
|
|
538
|
+
try {
|
|
539
|
+
const pushed = await pushLifecycle();
|
|
540
|
+
return pushed ? { pushed } : { pushed, pushWarning: "no remote is configured for chant/lifecycle — the tick record was recorded locally only" };
|
|
541
|
+
} catch (err) {
|
|
542
|
+
return { pushed: false, pushWarning: err instanceof Error ? err.message : String(err) };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** A tick of a ConvergeOp with an observer step (#2778): the observation is already in `args.observed`. */
|
|
547
|
+
async function resourceTick(args: ConvergeTickArgs, signal?: AbortSignal): Promise<ConvergeTickResult> {
|
|
548
|
+
const { resources } = parseResourceObservation(args.observed);
|
|
549
|
+
const ledger = await readConvergeLedger(args.env);
|
|
550
|
+
const plan = planResourceTick(
|
|
551
|
+
args.env,
|
|
552
|
+
resources,
|
|
553
|
+
args.rules as unknown as ConvergeRule<ResourceSymptom>[],
|
|
554
|
+
ledger.records,
|
|
555
|
+
args.opName,
|
|
556
|
+
args.dial,
|
|
557
|
+
args.budget,
|
|
558
|
+
);
|
|
559
|
+
await dispatchPlanned(plan.outcomes, args, signal);
|
|
560
|
+
|
|
561
|
+
const counts = summarizeOutcomes(plan.outcomes);
|
|
562
|
+
const log = renderResourceLog(args.env, resources, counts);
|
|
563
|
+
const drifted = resources.filter((r) => r.status === "drifted").length;
|
|
564
|
+
const { record } = await appendConvergeRecord({
|
|
565
|
+
op: args.opName,
|
|
566
|
+
env: args.env,
|
|
567
|
+
timestamp: new Date().toISOString(),
|
|
568
|
+
firedRuleIds: plan.firedRuleIds,
|
|
569
|
+
outcomes: plan.outcomes,
|
|
570
|
+
resources: resources.map((r) => ({
|
|
571
|
+
name: r.name,
|
|
572
|
+
status: r.status,
|
|
573
|
+
...(r.detail ? { detail: sanitizeLedgerText(r.detail) } : {}),
|
|
574
|
+
})),
|
|
575
|
+
summary: {
|
|
576
|
+
drifted,
|
|
577
|
+
remediated: counts.remediated,
|
|
578
|
+
reported: counts.reported,
|
|
579
|
+
skippedBudget: counts.skippedBudget,
|
|
580
|
+
skippedFlap: counts.skippedFlap,
|
|
581
|
+
gated: counts.gated,
|
|
582
|
+
unobserved: resources.filter((r) => r.status === "unknown").length,
|
|
583
|
+
adopted: 0,
|
|
584
|
+
},
|
|
585
|
+
log,
|
|
586
|
+
});
|
|
587
|
+
const push = await pushTick();
|
|
588
|
+
return {
|
|
589
|
+
id: record.id,
|
|
590
|
+
drifted: drifted > 0,
|
|
591
|
+
remediated: record.summary.remediated,
|
|
592
|
+
reported: record.summary.reported,
|
|
593
|
+
skippedBudget: record.summary.skippedBudget,
|
|
594
|
+
skippedFlap: record.summary.skippedFlap,
|
|
595
|
+
gated: record.summary.gated ?? 0,
|
|
596
|
+
unobserved: record.summary.unobserved,
|
|
597
|
+
adopted: 0,
|
|
598
|
+
log: record.log,
|
|
599
|
+
...push,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export async function convergeTick(args: ConvergeTickArgs, signal?: AbortSignal): Promise<ConvergeTickResult> {
|
|
604
|
+
await fetchLifecycle().catch(() => undefined);
|
|
605
|
+
if (args.observed !== undefined) return resourceTick(args, signal);
|
|
606
|
+
|
|
607
|
+
const [cs, statusRows, ledger] = await Promise.all([
|
|
608
|
+
observeChangeSet(args.env, signal),
|
|
609
|
+
observeStatusRows(args.env, signal),
|
|
610
|
+
readConvergeLedger(args.env),
|
|
611
|
+
]);
|
|
612
|
+
|
|
613
|
+
const symptom = deriveSymptoms(args.env, cs, statusRows);
|
|
614
|
+
const plan = planConvergeTick(symptom, args.rules, ledger.records, args.dial, args.budget);
|
|
615
|
+
|
|
616
|
+
// Execute: only "ran" outcomes cause a subprocess dispatch. Before that
|
|
617
|
+
// dispatch, the runtime backstop (Finding A, #1954 pre-merge review)
|
|
618
|
+
// re-classifies the target and downgrades to "reported" if this dial/verb
|
|
619
|
+
// class pairing was never supposed to reach dispatch — OPS014 (build time)
|
|
620
|
+
// is the primary defense; this is what catches a rule table that reached
|
|
621
|
+
// `convergeTick` without going through it.
|
|
622
|
+
await dispatchPlanned(plan.outcomes, args, signal);
|
|
435
623
|
|
|
436
624
|
const counts = summarizeOutcomes(plan.outcomes);
|
|
437
625
|
const log = renderLog(args.env, symptom, counts);
|
|
@@ -461,16 +649,7 @@ export async function convergeTick(args: ConvergeTickArgs, signal?: AbortSignal)
|
|
|
461
649
|
// chant#2337 — reported rather than swallowed, the way #2336 does it for the
|
|
462
650
|
// gate. A push that did not land leaves a correct local record nobody else
|
|
463
651
|
// can read, and the caller is the only one positioned to say so.
|
|
464
|
-
|
|
465
|
-
let pushWarning: string | undefined;
|
|
466
|
-
try {
|
|
467
|
-
pushed = await pushLifecycle();
|
|
468
|
-
if (!pushed) {
|
|
469
|
-
pushWarning = "no remote is configured for chant/lifecycle — the tick record was recorded locally only";
|
|
470
|
-
}
|
|
471
|
-
} catch (err) {
|
|
472
|
-
pushWarning = err instanceof Error ? err.message : String(err);
|
|
473
|
-
}
|
|
652
|
+
const { pushed, pushWarning } = await pushTick();
|
|
474
653
|
|
|
475
654
|
return {
|
|
476
655
|
id: record.id,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The decide activity against a real Jev-compatible server (#2740). Gated:
|
|
3
|
+
* it runs only when TYPESAFE_API_KEY is set, and is skipped otherwise, so the
|
|
4
|
+
* suite never needs an account or the network.
|
|
5
|
+
*
|
|
6
|
+
* TYPESAFE_API_KEY=... npx vitest run packages/core/src/op/activities/decide.live.test.ts
|
|
7
|
+
*
|
|
8
|
+
* TYPESAFE_BASE_URL points it at another compatible server (the default is
|
|
9
|
+
* https://api.typesafe.ai), and TYPESAFE_MODEL at another pinned model id (the
|
|
10
|
+
* default is jev-1.13.0). A model id is always a versioned one, never an alias.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
14
|
+
import { isPointWait } from "../steward-points";
|
|
15
|
+
import { workspacePoints } from "../../workspace/points-cli";
|
|
16
|
+
import { postQuestion } from "../decide-backend";
|
|
17
|
+
import { cleanScratch, workspace } from "../../workspace/__fixtures__/decide-workspace";
|
|
18
|
+
import { runDecide } from "./decide";
|
|
19
|
+
|
|
20
|
+
const key = process.env.TYPESAFE_API_KEY;
|
|
21
|
+
const url = process.env.TYPESAFE_BASE_URL ?? "https://api.typesafe.ai";
|
|
22
|
+
const model = process.env.TYPESAFE_MODEL ?? "jev-1.13.0";
|
|
23
|
+
const backend = { url, key: { env: "TYPESAFE_API_KEY" }, timeoutMs: 60_000 };
|
|
24
|
+
|
|
25
|
+
afterAll(cleanScratch);
|
|
26
|
+
|
|
27
|
+
describe.skipIf(!key)("decide against a real Jev-compatible server (TYPESAFE_API_KEY is set)", () => {
|
|
28
|
+
test("the server answers each question type in the wire format", async () => {
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
const noul = await postQuestion(backend, "systemone", {
|
|
31
|
+
point: "live-noul",
|
|
32
|
+
backend: "systemone",
|
|
33
|
+
model,
|
|
34
|
+
question: { type: "noul", instructions: "Is the sky in the state blue?", criteria: { true: "It is blue.", false: "It is not blue." } },
|
|
35
|
+
state: { sky: "a clear blue sky at noon" },
|
|
36
|
+
}, { cwd });
|
|
37
|
+
expect(noul.model).toBe(model);
|
|
38
|
+
expect(noul.answer.type).toBe("noul");
|
|
39
|
+
expect((noul.answer as { noul: number }).noul).toBeGreaterThanOrEqual(0);
|
|
40
|
+
|
|
41
|
+
const choice = await postQuestion(backend, "systemone", {
|
|
42
|
+
point: "live-choice",
|
|
43
|
+
backend: "systemone",
|
|
44
|
+
model,
|
|
45
|
+
question: { type: "choice", instructions: "Which team takes the ticket in the state?", criteria: { billing: "Payments and invoices.", platform: "Servers and deploys." } },
|
|
46
|
+
state: { ticket: "The production deploy failed with a disk full error." },
|
|
47
|
+
}, { cwd });
|
|
48
|
+
expect(choice.answer).toMatchObject({ type: "choice" });
|
|
49
|
+
expect(["billing", "platform"]).toContain((choice.answer as { choice: string }).choice);
|
|
50
|
+
|
|
51
|
+
const score = await postQuestion(backend, "systemone", {
|
|
52
|
+
point: "live-score",
|
|
53
|
+
backend: "systemone",
|
|
54
|
+
model,
|
|
55
|
+
question: { type: "score", instructions: "How urgent is the ticket in the state?", criteria: ["not urgent", "somewhat urgent", "very urgent"] },
|
|
56
|
+
state: { ticket: "Every customer is locked out of their account." },
|
|
57
|
+
}, { cwd });
|
|
58
|
+
expect(score.answer).toMatchObject({ type: "score" });
|
|
59
|
+
}, 120_000);
|
|
60
|
+
|
|
61
|
+
test("the activity asks a point and records the model's answer or its escalation", async () => {
|
|
62
|
+
const root = workspace();
|
|
63
|
+
const outcome = await runDecide({ cwd: root, point: "triage", inputs: { "record.size": 5, "record.risky": true }, backends: { systemone: backend } }, {}).then(
|
|
64
|
+
(r) => r.id,
|
|
65
|
+
(e: unknown) => {
|
|
66
|
+
if (isPointWait(e)) return e.question.id;
|
|
67
|
+
throw e;
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
const doc = await workspacePoints({ cwd: root });
|
|
71
|
+
if ("error" in doc) throw new Error(doc.error.message);
|
|
72
|
+
const q = doc.questions.find((x) => x.id === outcome)!;
|
|
73
|
+
// The server answered: a proposal at the pin, or an escalation below the threshold with the model's lean. Never unreachable.
|
|
74
|
+
expect(["proposed", "escalated"]).toContain(q.state);
|
|
75
|
+
if (q.state === "proposed") expect(q.decider).toMatchObject({ kind: "model", model });
|
|
76
|
+
else expect(q.escalations.find((e) => e.kind === "model")?.reason).not.toContain("could not answer");
|
|
77
|
+
}, 120_000);
|
|
78
|
+
});
|