@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
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "../../post-synth";
|
|
54
54
|
import { classifyOpVerbClass, isWellFormedPredicate, predicateReferencesField } from "../../../op";
|
|
55
55
|
import type { ConvergeRule, OpConfig } from "../../../op";
|
|
56
|
-
import { CONVERGE_SYMPTOM_FIELDS, type ConvergeSymptom } from "../../../lifecycle/symptoms";
|
|
56
|
+
import { CONVERGE_SYMPTOM_FIELDS, RESOURCE_SYMPTOM_FIELDS, type ConvergeSymptom } from "../../../lifecycle/symptoms";
|
|
57
57
|
import { isOpEntity } from "./support";
|
|
58
58
|
|
|
59
59
|
function looksLikeOpProps(props: Record<string, unknown>): boolean {
|
|
@@ -61,12 +61,15 @@ function looksLikeOpProps(props: Record<string, unknown>): boolean {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** Read a ConvergeOp's rule table back off its `convergeTick` step's `args.rules`. `undefined` if this doesn't look like a ConvergeOp-shaped Op (defensive — never crashes the build over an unexpected shape). */
|
|
64
|
-
function findConvergeRules(props: OpConfig): ConvergeRule<ConvergeSymptom>[] | undefined {
|
|
64
|
+
function findConvergeRules(props: OpConfig): { rules: ConvergeRule<ConvergeSymptom>[]; fields: ReadonlySet<string> } | undefined {
|
|
65
65
|
for (const phase of props.phases) {
|
|
66
66
|
for (const step of phase.steps) {
|
|
67
67
|
if (step.kind === "activity" && step.fn === "convergeTick") {
|
|
68
68
|
const rules = step.args?.rules;
|
|
69
|
-
|
|
69
|
+
// #2778: a ConvergeOp with an observer step passes its result as
|
|
70
|
+
// `observed`, and its rules read a ResourceSymptom.
|
|
71
|
+
const fields = step.args?.observed !== undefined ? RESOURCE_SYMPTOM_FIELDS : CONVERGE_SYMPTOM_FIELDS;
|
|
72
|
+
return Array.isArray(rules) ? { rules: rules as ConvergeRule<ConvergeSymptom>[], fields } : undefined;
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
}
|
|
@@ -100,8 +103,9 @@ export const ops014: PostSynthCheck = {
|
|
|
100
103
|
if (props.labels?.Converge !== "true") continue;
|
|
101
104
|
|
|
102
105
|
const dial = props.labels?.Dial ?? "observe";
|
|
103
|
-
const
|
|
104
|
-
if (!
|
|
106
|
+
const found = findConvergeRules(props);
|
|
107
|
+
if (!found) continue;
|
|
108
|
+
const { rules, fields } = found;
|
|
105
109
|
|
|
106
110
|
for (const rule of rules) {
|
|
107
111
|
const label = typeof rule?.id === "string" ? rule.id : "(unnamed rule)";
|
|
@@ -119,7 +123,7 @@ export const ops014: PostSynthCheck = {
|
|
|
119
123
|
push("every rule must carry its why — refused at build");
|
|
120
124
|
continue;
|
|
121
125
|
}
|
|
122
|
-
if (!isWellFormedPredicate(rule.when,
|
|
126
|
+
if (!isWellFormedPredicate(rule.when, fields)) {
|
|
123
127
|
push("predicate is outside the evaluable subset (build it from eq/neq/gt/gte/lt/lte/truthy/falsy/allOf/anyOf over a known symptom field)");
|
|
124
128
|
continue;
|
|
125
129
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { Op, phase, decide } from "../../../op";
|
|
3
|
+
import type { PostSynthContext } from "../../post-synth";
|
|
4
|
+
import { plainRemoteHost, sys010, sys010Check } from "./sys010-key-over-plain-http";
|
|
5
|
+
import { coreOpChecks } from "./index";
|
|
6
|
+
|
|
7
|
+
const ctx = (op?: unknown): PostSynthContext => ({ outputs: new Map(), entities: new Map(op ? [["op", op as never]] : []), buildResult: {} as never }) as unknown as PostSynthContext;
|
|
8
|
+
|
|
9
|
+
describe("SYS010", () => {
|
|
10
|
+
it("fires when a decide step sends a key over plain http to a remote host", () => {
|
|
11
|
+
const op = Op({ name: "ask", overview: "ask", phases: [phase("Ask", [decide("triage", { backends: { s: { url: "http://jev.example.com", key: { env: "K" } } } })])] });
|
|
12
|
+
const d = sys010.check(ctx(op));
|
|
13
|
+
expect(d).toHaveLength(1);
|
|
14
|
+
expect(d[0]).toMatchObject({ checkId: "SYS010", severity: "error" });
|
|
15
|
+
expect(d[0].message).toContain("jev.example.com");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("is quiet for https, for loopback, and for a backend with no key", () => {
|
|
19
|
+
const op = Op({
|
|
20
|
+
name: "ask",
|
|
21
|
+
overview: "ask",
|
|
22
|
+
phases: [
|
|
23
|
+
phase("Ask", [
|
|
24
|
+
decide("a", { backends: { s: { url: "https://api.typesafe.ai", key: { env: "K" } } } }),
|
|
25
|
+
decide("b", { backends: { s: { url: "http://127.0.0.1:8080", key: { env: "K" } } } }),
|
|
26
|
+
decide("c", { backends: { s: { url: "http://jev.example.com" } } }),
|
|
27
|
+
decide("d"),
|
|
28
|
+
]),
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
expect(sys010.check(ctx(op))).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("fires on a backend configured in decide.backends, and is quiet for https", () => {
|
|
35
|
+
const check = sys010Check({ remote: { url: "http://jev.internal:8080", key: { env: "K" } }, ok: { url: "https://api.typesafe.ai", key: { env: "K" } } });
|
|
36
|
+
const d = check.check(ctx());
|
|
37
|
+
expect(d.map((x) => x.entity)).toEqual(["decide.backends.remote"]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("runs with core's Op checks, with the configured backends when given", () => {
|
|
41
|
+
expect(coreOpChecks().map((c) => c.id)).toContain("SYS010");
|
|
42
|
+
const configured = coreOpChecks({ decideBackends: { remote: { url: "http://10.0.0.2", key: { env: "K" } } } }).find((c) => c.id === "SYS010")!;
|
|
43
|
+
expect(configured.check(ctx())).toHaveLength(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("reads hosts", () => {
|
|
47
|
+
expect(plainRemoteHost("http://localhost:1")).toBeUndefined();
|
|
48
|
+
expect(plainRemoteHost("http://[::1]:1")).toBeUndefined();
|
|
49
|
+
expect(plainRemoteHost("http://10.0.0.2")).toBe("10.0.0.2");
|
|
50
|
+
expect(plainRemoteHost("not a url")).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SYS010: a `decide` backend sends its key over plain HTTP (#2740, core's
|
|
3
|
+
* since #2828).
|
|
4
|
+
*
|
|
5
|
+
* A backend in `decide.backends` in chant.config, or in a `decide` step's own
|
|
6
|
+
* `backends`, may have a key and an `http://` URL. The bearer key then crosses
|
|
7
|
+
* the network in clear text. A loopback address (127.0.0.1, ::1, localhost)
|
|
8
|
+
* is exempt, since that is where a local server such as `von serve` or the
|
|
9
|
+
* stub runs, and where a broker that is itself the endpoint listens.
|
|
10
|
+
*
|
|
11
|
+
* The steps are read off the Op entities, which covers a step written with
|
|
12
|
+
* the `decide` builder and one written with `activity("decide", ...)`. The
|
|
13
|
+
* configured backends are not entities: `chant build` and `chant lint` read
|
|
14
|
+
* them from the project's config and hand them to {@link sys010Check}. The id
|
|
15
|
+
* keeps the SYS prefix it had in the systemone lexicon.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "../../post-synth";
|
|
19
|
+
import type { OpConfig, PhaseDefinition, ActivityStep } from "../../../op/types";
|
|
20
|
+
import type { DecideBackend } from "../../../op/decide-config";
|
|
21
|
+
import { isOpEntity } from "./support";
|
|
22
|
+
|
|
23
|
+
const LOOPBACK = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
24
|
+
|
|
25
|
+
function* decideSteps(phases: PhaseDefinition[] | undefined): Generator<ActivityStep> {
|
|
26
|
+
for (const phase of phases ?? []) {
|
|
27
|
+
for (const step of phase.steps ?? []) {
|
|
28
|
+
if (step.kind === "activity" && step.fn === "decide") yield step;
|
|
29
|
+
else if (step.kind === "effect") {
|
|
30
|
+
for (const nested of step.steps ?? []) if (nested.kind === "activity" && nested.fn === "decide") yield nested;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The host of an http:// URL that is not loopback, or undefined. */
|
|
37
|
+
export function plainRemoteHost(url: unknown): string | undefined {
|
|
38
|
+
if (typeof url !== "string") return undefined;
|
|
39
|
+
let parsed: URL;
|
|
40
|
+
try {
|
|
41
|
+
parsed = new URL(url);
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (parsed.protocol !== "http:") return undefined;
|
|
46
|
+
return LOOPBACK.has(parsed.hostname) ? undefined : parsed.hostname;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const DESCRIPTION = "A decide backend sends its key over plain HTTP to a host that is not loopback";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* SYS010 over the Op entities and, when given, the backends configured in
|
|
53
|
+
* `decide.backends`. A configured backend's diagnostic names it as the entity
|
|
54
|
+
* `decide.backends.<name>`.
|
|
55
|
+
*/
|
|
56
|
+
export function sys010Check(configured?: Record<string, Pick<DecideBackend, "url" | "key">>): PostSynthCheck {
|
|
57
|
+
return {
|
|
58
|
+
id: "SYS010",
|
|
59
|
+
description: DESCRIPTION,
|
|
60
|
+
|
|
61
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
62
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
63
|
+
for (const [name, b] of Object.entries(configured ?? {})) {
|
|
64
|
+
const host = b?.key !== undefined ? plainRemoteHost(b.url) : undefined;
|
|
65
|
+
if (!host) continue;
|
|
66
|
+
diagnostics.push({
|
|
67
|
+
checkId: "SYS010",
|
|
68
|
+
severity: "error",
|
|
69
|
+
message: `decide.backends.${name} sends its key over http:// to ${host}; use https://, or a loopback broker`,
|
|
70
|
+
entity: `decide.backends.${name}`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (const [entityKey, entity] of ctx.entities) {
|
|
74
|
+
if (!isOpEntity(entity)) continue;
|
|
75
|
+
const rec = entity as unknown as Record<string, unknown>;
|
|
76
|
+
const props = ((entity as { props?: Record<string, unknown> }).props ?? {}) as unknown as OpConfig;
|
|
77
|
+
for (const step of [...decideSteps(props.phases), ...decideSteps(props.onFailure)]) {
|
|
78
|
+
const backends = (step.args as { backends?: unknown } | undefined)?.backends;
|
|
79
|
+
if (backends === null || typeof backends !== "object") continue;
|
|
80
|
+
for (const [name, b] of Object.entries(backends as Record<string, { url?: unknown; key?: unknown }>)) {
|
|
81
|
+
if (b?.key === undefined) continue;
|
|
82
|
+
const host = plainRemoteHost(b.url);
|
|
83
|
+
if (!host) continue;
|
|
84
|
+
diagnostics.push({
|
|
85
|
+
checkId: "SYS010",
|
|
86
|
+
severity: "error",
|
|
87
|
+
message: `Op "${props.name}", decide ${JSON.stringify((step.args as { point?: unknown }).point)}: backend ${name} sends its key over http:// to ${host}; use https://, or a loopback broker`,
|
|
88
|
+
entity: entityKey,
|
|
89
|
+
lexicon: typeof rec.lexicon === "string" ? rec.lexicon : undefined,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return diagnostics;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** SYS010 over the Op entities alone, with no configured backends. */
|
|
100
|
+
export const sys010: PostSynthCheck = sys010Check();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { noLiteralKeyRule } from "./sys001-no-literal-key";
|
|
4
|
+
import { loadCoreRules } from "./index";
|
|
5
|
+
|
|
6
|
+
const run = (code: string) =>
|
|
7
|
+
noLiteralKeyRule.check({ sourceFile: ts.createSourceFile("x.ts", code, ts.ScriptTarget.Latest, true), filePath: "x.ts", entities: [] } as never);
|
|
8
|
+
|
|
9
|
+
describe("SYS001", () => {
|
|
10
|
+
it("is one of core's rules", () => {
|
|
11
|
+
expect(loadCoreRules().map((r) => r.id)).toContain("SYS001");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("fires on a literal key in the config's decide block", () => {
|
|
15
|
+
const d = run(`export default { decide: { backends: { s: { url: "https://x", key: "sk-abc" } } } };`);
|
|
16
|
+
expect(d.map((x) => x.ruleId)).toEqual(["SYS001"]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("fires on a literal key in a decide step's backends", () => {
|
|
20
|
+
expect(run('decide("p", { backends: { s: { url: "https://x", key: `k` } } });')).toHaveLength(1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("is quiet for { env } and a brokered capability", () => {
|
|
24
|
+
expect(run(`export default { decide: { backends: { s: { url: "https://x", key: { env: "K" } }, b: { url: "http://127.0.0.1", key: { capability: "inference" } } } } };`)).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("is quiet for a key property outside decide, and for another tool's backends", () => {
|
|
28
|
+
expect(run(`const x = { key: "not a backend" };`)).toEqual([]);
|
|
29
|
+
expect(run(`const tf = { backends: { s3: { bucket: "b", key: "state/terraform.tfstate" } } };`)).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { LintRule, LintDiagnostic, LintContext } from "../rule";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SYS001: a `decide` backend's key is never a literal (#2740, core's since
|
|
6
|
+
* #2828).
|
|
7
|
+
*
|
|
8
|
+
* A key is `{ env: "VARIABLE" }` or a brokered capability
|
|
9
|
+
* (`{ capability: "inference", member: "box" }`, #2726). A string written
|
|
10
|
+
* where a key goes is a credential in git history, and a box that holds one
|
|
11
|
+
* fails `workspace check` with WSP121 as well.
|
|
12
|
+
*
|
|
13
|
+
* Fires on a `key` property whose value is a string or template literal,
|
|
14
|
+
* inside a backend of a `backends` object that belongs to `decide`: the
|
|
15
|
+
* `decide` block of a config object (`decide: { backends: ... }`) or the
|
|
16
|
+
* options of a `decide(...)` step. A `backends` object anywhere else, such as
|
|
17
|
+
* a Terraform backend's `key`, is not a decide backend and is left alone.
|
|
18
|
+
* The id keeps the SYS prefix it had in the systemone lexicon.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function keyText(name: ts.PropertyName): string | undefined {
|
|
22
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const isLiteral = (node: ts.Expression): boolean => ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateExpression(node);
|
|
27
|
+
|
|
28
|
+
const isDecideCall = (node: ts.Node): node is ts.CallExpression =>
|
|
29
|
+
ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "decide";
|
|
30
|
+
|
|
31
|
+
/** Where the walk is: outside decide, inside decide's options or config block, or inside its `backends`. */
|
|
32
|
+
type Scope = "outside" | "decide" | "backends";
|
|
33
|
+
|
|
34
|
+
export const noLiteralKeyRule: LintRule = {
|
|
35
|
+
id: "SYS001",
|
|
36
|
+
severity: "error",
|
|
37
|
+
category: "security",
|
|
38
|
+
description: "A decide backend key is { env } or a brokered capability, never a literal string",
|
|
39
|
+
|
|
40
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
41
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
42
|
+
const source = context.sourceFile;
|
|
43
|
+
const visit = (node: ts.Node, scope: Scope) => {
|
|
44
|
+
let within = scope;
|
|
45
|
+
if (isDecideCall(node)) within = "decide";
|
|
46
|
+
else if (ts.isPropertyAssignment(node)) {
|
|
47
|
+
const key = keyText(node.name);
|
|
48
|
+
if (key === "decide") within = "decide";
|
|
49
|
+
else if (key === "backends" && scope === "decide") within = "backends";
|
|
50
|
+
if (scope === "backends" && key === "key" && isLiteral(node.initializer)) {
|
|
51
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.initializer.getStart(source));
|
|
52
|
+
diagnostics.push({
|
|
53
|
+
ruleId: "SYS001",
|
|
54
|
+
severity: "error",
|
|
55
|
+
message: 'Literal backend key: use { env: "VARIABLE" } or a brokered capability ({ capability, member }) instead',
|
|
56
|
+
file: context.filePath,
|
|
57
|
+
line: line + 1,
|
|
58
|
+
column: character + 1,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
ts.forEachChild(node, (child) => visit(child, within));
|
|
63
|
+
};
|
|
64
|
+
visit(source, "outside");
|
|
65
|
+
return diagnostics;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stub `POST /v1/systemone` server for tests of the `decide` activity
|
|
3
|
+
* (`@intentius/chant/op/__fixtures__/decide-stub-backend`): a real HTTP server on
|
|
4
|
+
* 127.0.0.1 that checks a request the way a Jev-compatible server would and
|
|
5
|
+
* answers with what the test scripts. No model runs.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const stub = await startStubBackend({ key: "k", answers: { slice_tier: { type: "choice", choice: "large", probabilities: {...}, confidence: 0.9 } } });
|
|
9
|
+
* // decide.backends, or a decide step's backends: { systemone: { url: stub.url, key: { env: "STUB_KEY" } } }
|
|
10
|
+
* await stub.close();
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* It answers 401 when a key is set and the request's bearer differs, 422 when
|
|
14
|
+
* the body is not a request in the wire format, and `status` when the test
|
|
15
|
+
* forces one (429 or 529, say). Every request it read is kept in `requests`.
|
|
16
|
+
*
|
|
17
|
+
* It lives under `__fixtures__` because it listens on a port, and no module
|
|
18
|
+
* chant runs does (#2657, `test/no-listener.test.ts`): only tests start it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
22
|
+
import type { AddressInfo } from "node:net";
|
|
23
|
+
import type { WireAnswer } from "../../workspace/points";
|
|
24
|
+
import { SYSTEMONE_PATH, type BackendRequest } from "../decide-backend";
|
|
25
|
+
|
|
26
|
+
export interface StubQuestion {
|
|
27
|
+
name: string;
|
|
28
|
+
type: string;
|
|
29
|
+
instructions: string;
|
|
30
|
+
criteria: Record<string, string> | string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface StubBackendOptions {
|
|
34
|
+
/** The bearer key the stub requires. Without it, any request is taken. */
|
|
35
|
+
key?: string;
|
|
36
|
+
/** The model id the response reports. Defaults to the id the request sent. */
|
|
37
|
+
model?: string;
|
|
38
|
+
/** The answer to a question, by question name, or computed from the question. Defaults to {@link defaultAnswer}. */
|
|
39
|
+
answers?: Record<string, WireAnswer> | ((question: StubQuestion, request: BackendRequest) => WireAnswer);
|
|
40
|
+
/** Answer every request with this status and no answers. */
|
|
41
|
+
status?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface StubBackend {
|
|
45
|
+
/** The base URL, without `/v1/systemone`. */
|
|
46
|
+
url: string;
|
|
47
|
+
/** Each request body the stub read, in order. */
|
|
48
|
+
requests: BackendRequest[];
|
|
49
|
+
/** The Authorization header of each request, or null. */
|
|
50
|
+
authorizations: (string | null)[];
|
|
51
|
+
close(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A confident answer of the question's own type: true, the first option, or the last level. */
|
|
55
|
+
export function defaultAnswer(question: StubQuestion): WireAnswer {
|
|
56
|
+
if (question.type === "noul") return { type: "noul", noul: 0.95 };
|
|
57
|
+
if (question.type === "choice") {
|
|
58
|
+
const options = Object.keys(question.criteria);
|
|
59
|
+
const rest = options.length > 1 ? 0.05 / (options.length - 1) : 0;
|
|
60
|
+
const probabilities = Object.fromEntries(options.map((o, i) => [o, i === 0 ? 0.95 : rest]));
|
|
61
|
+
return { type: "choice", choice: options[0], probabilities, confidence: 0.925 };
|
|
62
|
+
}
|
|
63
|
+
if (question.type === "score") {
|
|
64
|
+
const levels = question.criteria as string[];
|
|
65
|
+
const probabilities = Object.fromEntries(levels.map((l, i) => [l, i === levels.length - 1 ? 0.95 : 0.05 / Math.max(1, levels.length - 1)]));
|
|
66
|
+
return { type: "score", score: levels.length - 0.05, probabilities, confidence: 0.925 };
|
|
67
|
+
}
|
|
68
|
+
return { type: "unsupported" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Why a body is not a request in the wire format, or null when it is one. */
|
|
72
|
+
export function requestProblem(body: unknown): string | null {
|
|
73
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) return "the body is not a JSON object";
|
|
74
|
+
const b = body as Record<string, unknown>;
|
|
75
|
+
if (typeof b.model !== "string" || b.model === "") return "model is required";
|
|
76
|
+
if (b.state === undefined) return "state is required";
|
|
77
|
+
if (b.questions === null || typeof b.questions !== "object" || Array.isArray(b.questions) || Object.keys(b.questions).length === 0) return "questions must name at least one question";
|
|
78
|
+
for (const [name, q] of Object.entries(b.questions as Record<string, unknown>)) {
|
|
79
|
+
const question = q as Record<string, unknown> | null;
|
|
80
|
+
if (question === null || typeof question !== "object") return `questions.${name} is not an object`;
|
|
81
|
+
if (typeof question.instructions !== "string") return `questions.${name}.instructions is required`;
|
|
82
|
+
if (question.type === "noul" || question.type === "choice") {
|
|
83
|
+
if (question.criteria === null || typeof question.criteria !== "object" || Array.isArray(question.criteria)) return `questions.${name}.criteria must be an object`;
|
|
84
|
+
if (question.type === "noul" && !("true" in question.criteria && "false" in question.criteria)) return `questions.${name}.criteria must describe true and false`;
|
|
85
|
+
} else if (question.type === "score") {
|
|
86
|
+
if (!Array.isArray(question.criteria) || question.criteria.length < 2 || question.criteria.length > 10) return `questions.${name}.criteria must be 2 to 10 levels`;
|
|
87
|
+
} else {
|
|
88
|
+
return `questions.${name}.type must be noul, choice or score`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function send(res: ServerResponse, status: number, body: unknown): void {
|
|
95
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
96
|
+
res.end(JSON.stringify(body));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readBody(req: IncomingMessage): Promise<string> {
|
|
100
|
+
const chunks: Buffer[] = [];
|
|
101
|
+
for await (const chunk of req) chunks.push(chunk as Buffer);
|
|
102
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Start the stub on a free port on 127.0.0.1. */
|
|
106
|
+
export async function startStubBackend(opts: StubBackendOptions = {}): Promise<StubBackend> {
|
|
107
|
+
const requests: BackendRequest[] = [];
|
|
108
|
+
const authorizations: (string | null)[] = [];
|
|
109
|
+
const server = createServer((req, res) => {
|
|
110
|
+
void (async () => {
|
|
111
|
+
if (req.method !== "POST" || req.url !== SYSTEMONE_PATH) return send(res, 404, { error: { type: "not_found", message: `${req.method} ${req.url}` } });
|
|
112
|
+
const auth = req.headers.authorization ?? null;
|
|
113
|
+
authorizations.push(auth);
|
|
114
|
+
if (opts.key !== undefined && auth !== `Bearer ${opts.key}`) return send(res, 401, { error: { type: "authentication_error", message: "invalid key" } });
|
|
115
|
+
let body: unknown;
|
|
116
|
+
try {
|
|
117
|
+
body = JSON.parse(await readBody(req));
|
|
118
|
+
} catch {
|
|
119
|
+
return send(res, 422, { error: { type: "invalid_request", message: "the body is not JSON" } });
|
|
120
|
+
}
|
|
121
|
+
const problem = requestProblem(body);
|
|
122
|
+
if (problem) return send(res, 422, { error: { type: "invalid_request", message: problem } });
|
|
123
|
+
const request = body as BackendRequest;
|
|
124
|
+
requests.push(request);
|
|
125
|
+
if (opts.status !== undefined) return send(res, opts.status, { error: { type: "stub", message: `the stub answers ${opts.status}` } });
|
|
126
|
+
const answers: Record<string, WireAnswer> = {};
|
|
127
|
+
for (const [name, q] of Object.entries(request.questions)) {
|
|
128
|
+
const question: StubQuestion = { name, ...q };
|
|
129
|
+
const scripted = typeof opts.answers === "function" ? opts.answers(question, request) : opts.answers?.[name];
|
|
130
|
+
answers[name] = scripted ?? defaultAnswer(question);
|
|
131
|
+
}
|
|
132
|
+
send(res, 200, { model: opts.model ?? request.model, answers, usage: { input_tokens: JSON.stringify(request).length, output_tokens: 0 } });
|
|
133
|
+
})().catch((err: unknown) => send(res, 500, { error: { type: "stub", message: err instanceof Error ? err.message : String(err) } }));
|
|
134
|
+
});
|
|
135
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
136
|
+
const { port } = server.address() as AddressInfo;
|
|
137
|
+
return {
|
|
138
|
+
url: `http://127.0.0.1:${port}`,
|
|
139
|
+
requests,
|
|
140
|
+
authorizations,
|
|
141
|
+
close: () => new Promise<void>((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import { z } from "zod";
|
|
27
27
|
import { activityContract } from "../activity-contract";
|
|
28
|
+
import { backendSchema } from "../decide-config";
|
|
28
29
|
|
|
29
30
|
export const lifecycleSnapshotContract = activityContract(
|
|
30
31
|
"lifecycleSnapshot",
|
|
@@ -54,8 +55,14 @@ export const shellCmdContract = activityContract(
|
|
|
54
55
|
env: z.record(z.string(), z.string()).optional(),
|
|
55
56
|
cwd: z.string().optional(),
|
|
56
57
|
okExit: z.array(z.number()).optional(),
|
|
58
|
+
gatedExit: z.number().optional(),
|
|
59
|
+
gate: z.strictObject({ op: z.string(), gate: z.string() }).optional(),
|
|
60
|
+
json: z.boolean().optional(),
|
|
57
61
|
}),
|
|
58
|
-
|
|
62
|
+
// `json` is stdout parsed, when the step sets `json` (#2787): whatever the
|
|
63
|
+
// command printed, so its shape is the author's and only the whole value is
|
|
64
|
+
// a reference target (`sh.out.json`).
|
|
65
|
+
z.object({ stdout: z.string(), stderr: z.string(), exitCode: z.number(), json: z.unknown().optional() }),
|
|
59
66
|
);
|
|
60
67
|
|
|
61
68
|
export const httpCheckContract = activityContract(
|
|
@@ -166,3 +173,185 @@ export const proposeWorkspaceUpgradeContract = activityContract(
|
|
|
166
173
|
summary: z.string(),
|
|
167
174
|
}),
|
|
168
175
|
);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The forward coverage check over an Op's own diff (#2773). `args` mirrors
|
|
179
|
+
* `ChangeCoverageArgs`. `returns` names what a step reads: whether the check
|
|
180
|
+
* passed, the range and work item checked, and each finding with the
|
|
181
|
+
* `triage` a work item seeded from it takes as its `source`.
|
|
182
|
+
*/
|
|
183
|
+
export const changeCoverageContract = activityContract(
|
|
184
|
+
"changeCoverage",
|
|
185
|
+
z.strictObject({
|
|
186
|
+
cwd: z.string().optional(),
|
|
187
|
+
range: z.string().optional(),
|
|
188
|
+
work: z.string().optional(),
|
|
189
|
+
severity: z.enum(["off", "warn", "fail"]).optional(),
|
|
190
|
+
}),
|
|
191
|
+
z.object({
|
|
192
|
+
ok: z.boolean(),
|
|
193
|
+
range: z.object({ spec: z.string(), base: z.string(), head: z.string() }),
|
|
194
|
+
work: z.string().nullable(),
|
|
195
|
+
severity: z.enum(["off", "warn", "fail"]),
|
|
196
|
+
findings: z.array(
|
|
197
|
+
z.object({
|
|
198
|
+
code: z.enum(["change-uncovered", "change-out-of-scope"]),
|
|
199
|
+
path: z.string(),
|
|
200
|
+
message: z.string(),
|
|
201
|
+
severity: z.enum(["warn", "fail"]),
|
|
202
|
+
records: z.array(z.string()),
|
|
203
|
+
triage: z.object({ finding: z.enum(["change-uncovered", "change-out-of-scope"]), region: z.string() }),
|
|
204
|
+
}),
|
|
205
|
+
),
|
|
206
|
+
summary: z.object({
|
|
207
|
+
paths: z.number(),
|
|
208
|
+
covered: z.number(),
|
|
209
|
+
uncovered: z.number(),
|
|
210
|
+
outOfScope: z.number(),
|
|
211
|
+
ignored: z.number(),
|
|
212
|
+
records: z.number(),
|
|
213
|
+
}),
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Evidence for an acceptance criterion, under the run's work lease (#2772).
|
|
219
|
+
* `lease` is the run's lease, a reference the builder fills in, so it is not
|
|
220
|
+
* checked here. `returns` names what a step reads: the item, the record's
|
|
221
|
+
* path, the entry appended and the criteria counted with it.
|
|
222
|
+
*/
|
|
223
|
+
export const workEvidenceContract = activityContract(
|
|
224
|
+
"workEvidence",
|
|
225
|
+
z.strictObject({
|
|
226
|
+
lease: z.unknown(),
|
|
227
|
+
criterion: z.string(),
|
|
228
|
+
result: z.enum(["pass", "fail"]),
|
|
229
|
+
title: z.string(),
|
|
230
|
+
url: z.string().optional(),
|
|
231
|
+
path: z.string().optional(),
|
|
232
|
+
kind: z.string().optional(),
|
|
233
|
+
cwd: z.string().optional(),
|
|
234
|
+
}),
|
|
235
|
+
z.object({
|
|
236
|
+
item: z.string(),
|
|
237
|
+
path: z.string(),
|
|
238
|
+
evidence: z.record(z.string(), z.unknown()),
|
|
239
|
+
acceptance: z.object({ met: z.number(), total: z.number(), criteria: z.array(z.object({ id: z.string(), verification: z.string(), met: z.boolean() })) }),
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The source-release activities (#2782, ./source-release.ts). A release Op
|
|
245
|
+
* reads `archive.out.digest` into its plan, `plan.out.digest` into its gate
|
|
246
|
+
* and its ship step, and `plan.out.file` into its record step.
|
|
247
|
+
*/
|
|
248
|
+
export const sourceArchiveContract = activityContract(
|
|
249
|
+
"sourceArchive",
|
|
250
|
+
z.strictObject({ path: z.string(), ref: z.string().optional(), out: z.string().optional(), cwd: z.string().optional() }),
|
|
251
|
+
z.object({ digest: z.string(), archive: z.string(), commit: z.string(), dir: z.string(), files: z.number(), bytes: z.number() }),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
export const releasePlanContract = activityContract(
|
|
255
|
+
"releasePlan",
|
|
256
|
+
z.strictObject({
|
|
257
|
+
component: z.string(),
|
|
258
|
+
env: z.string(),
|
|
259
|
+
gitSha: z.string(),
|
|
260
|
+
content: z.record(z.string(), z.unknown()),
|
|
261
|
+
dir: z.string().optional(),
|
|
262
|
+
cwd: z.string().optional(),
|
|
263
|
+
}),
|
|
264
|
+
z.object({ digest: z.string(), file: z.string(), gitSha: z.string() }),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
export const releaseRecordContract = activityContract(
|
|
268
|
+
"releaseRecord",
|
|
269
|
+
z.strictObject({
|
|
270
|
+
plan: z.string(),
|
|
271
|
+
digest: z.string().optional(),
|
|
272
|
+
approval: z.strictObject({ op: z.string(), gate: z.string() }).optional(),
|
|
273
|
+
actor: z.string().optional(),
|
|
274
|
+
runId: z.string().optional(),
|
|
275
|
+
cwd: z.string().optional(),
|
|
276
|
+
}),
|
|
277
|
+
z.object({ recorded: z.boolean(), digest: z.string(), env: z.string(), component: z.string(), approver: z.string().nullable() }),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* `decide` (#2740, core's since #2828). OPS012 checks a step's args against
|
|
282
|
+
* it and OPS013 a later step's reference into its result, so a misspelled
|
|
283
|
+
* `point` or a key written as a literal string fails `chant build` rather
|
|
284
|
+
* than a run.
|
|
285
|
+
*/
|
|
286
|
+
const decideEscalation = z.object({ kind: z.enum(["table", "model"]), reason: z.string() }).loose();
|
|
287
|
+
|
|
288
|
+
export const decideContract = activityContract(
|
|
289
|
+
"decide",
|
|
290
|
+
z.strictObject({
|
|
291
|
+
point: z.string().min(1),
|
|
292
|
+
inputs: z.record(z.string(), z.unknown()).optional(),
|
|
293
|
+
read: z.record(z.string(), z.string()).optional(),
|
|
294
|
+
subject: z.string().optional(),
|
|
295
|
+
kind: z.string().optional(),
|
|
296
|
+
cwd: z.string().optional(),
|
|
297
|
+
backends: z.record(z.string(), backendSchema).optional(),
|
|
298
|
+
dryRun: z.boolean().optional(),
|
|
299
|
+
}),
|
|
300
|
+
z.strictObject({
|
|
301
|
+
id: z.string(),
|
|
302
|
+
path: z.string(),
|
|
303
|
+
state: z.enum(["escalated", "proposed", "answered"]),
|
|
304
|
+
open: z.boolean(),
|
|
305
|
+
answer: z.union([z.string(), z.boolean(), z.null()]),
|
|
306
|
+
decider: z.string(),
|
|
307
|
+
model: z.string().nullable(),
|
|
308
|
+
backend: z.string().nullable(),
|
|
309
|
+
confidence: z.number().nullable(),
|
|
310
|
+
threshold: z.number().nullable(),
|
|
311
|
+
answeredBy: z.array(z.string()),
|
|
312
|
+
escalations: z.array(decideEscalation),
|
|
313
|
+
missing: z.array(z.string()),
|
|
314
|
+
}),
|
|
315
|
+
{ entities: ["point"] },
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The rollback activities for a source release (#2800, ./source-rollback.ts).
|
|
320
|
+
* A rollback Op reads `plan.out.digest` into its gate, `plan.out.to` and the
|
|
321
|
+
* archive fields into the step that restores the Machine, and
|
|
322
|
+
* `plan.out.file` into its record step.
|
|
323
|
+
*/
|
|
324
|
+
export const releaseRollbackPlanContract = activityContract(
|
|
325
|
+
"releaseRollbackPlan",
|
|
326
|
+
z.strictObject({
|
|
327
|
+
component: z.string(),
|
|
328
|
+
env: z.string(),
|
|
329
|
+
to: z.string().optional(),
|
|
330
|
+
path: z.string().optional(),
|
|
331
|
+
dir: z.string().optional(),
|
|
332
|
+
cwd: z.string().optional(),
|
|
333
|
+
}),
|
|
334
|
+
z.object({
|
|
335
|
+
digest: z.string(),
|
|
336
|
+
file: z.string(),
|
|
337
|
+
to: z.string(),
|
|
338
|
+
from: z.string(),
|
|
339
|
+
gitSha: z.string(),
|
|
340
|
+
archive: z.string(),
|
|
341
|
+
archiveDigest: z.string(),
|
|
342
|
+
dir: z.string(),
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
export const releaseRollbackRecordContract = activityContract(
|
|
347
|
+
"releaseRollbackRecord",
|
|
348
|
+
z.strictObject({
|
|
349
|
+
plan: z.string(),
|
|
350
|
+
digest: z.string().optional(),
|
|
351
|
+
approval: z.strictObject({ op: z.string(), gate: z.string() }).optional(),
|
|
352
|
+
actor: z.string().optional(),
|
|
353
|
+
runId: z.string().optional(),
|
|
354
|
+
cwd: z.string().optional(),
|
|
355
|
+
}),
|
|
356
|
+
z.object({ recorded: z.boolean(), digest: z.string(), env: z.string(), component: z.string(), approver: z.string().nullable() }),
|
|
357
|
+
);
|