@intentius/chant 0.124.23 → 0.124.25
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/ci-jobs.d.ts +41 -1
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +48 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +14 -0
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +9 -0
- package/dist/cli/handlers/emulator.d.ts +13 -2
- package/dist/cli/handlers/emulator.d.ts.map +1 -1
- package/dist/cli/handlers/emulator.js +63 -3
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +7 -1
- package/dist/cli/handlers/run-wave.d.ts.map +1 -1
- package/dist/cli/handlers/run-wave.js +5 -4
- package/dist/cli/handlers/run.d.ts +4 -3
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +73 -9
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +38 -11
- package/dist/cli/mcp/workspace-tools.d.ts +3 -3
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.js +27 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +31 -0
- package/dist/cli/registry.d.ts +12 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +45 -6
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +97 -22
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +25 -10
- package/dist/effect-receipt.d.ts +13 -0
- package/dist/effect-receipt.d.ts.map +1 -1
- package/dist/effect-receipt.js +19 -0
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +24 -2
- package/dist/lexicon-module.d.ts +7 -1
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +7 -2
- package/dist/lifecycle/git.d.ts +11 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +62 -6
- package/dist/lifecycle/no-record.d.ts +59 -0
- package/dist/lifecycle/no-record.d.ts.map +1 -0
- package/dist/lifecycle/no-record.js +88 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.js +4 -2
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +30 -9
- package/dist/op/emulator-lifecycle.d.ts +23 -0
- package/dist/op/emulator-lifecycle.d.ts.map +1 -1
- package/dist/op/emulator-lifecycle.js +39 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +1 -1
- package/dist/op/local-executor.d.ts +10 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +71 -7
- package/dist/op/op-waves-notify.d.ts +2 -0
- package/dist/op/op-waves-notify.d.ts.map +1 -1
- package/dist/op/op-waves-notify.js +9 -3
- package/dist/op/op-waves-run.d.ts +8 -2
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +36 -11
- package/dist/op/op-waves.d.ts +85 -1
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +153 -1
- package/dist/op/receipt-store.d.ts +5 -0
- package/dist/op/receipt-store.d.ts.map +1 -1
- package/dist/op/receipt-store.js +4 -3
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +6 -0
- package/dist/op/step-console.d.ts +65 -0
- package/dist/op/step-console.d.ts.map +1 -0
- package/dist/op/step-console.js +140 -0
- package/dist/op/step-output-ref.d.ts +26 -18
- package/dist/op/step-output-ref.d.ts.map +1 -1
- package/dist/op/step-output-ref.js +86 -63
- package/dist/point-decision.d.ts.map +1 -1
- package/dist/point-decision.js +3 -0
- package/dist/wave-runtime.mjs +239 -239
- package/dist/workspace/agent-cli.d.ts.map +1 -1
- package/dist/workspace/agent-cli.js +3 -0
- package/dist/workspace/compose-graph.d.ts +22 -3
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/compose-graph.js +18 -5
- package/dist/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
- package/dist/workspace/conformance/writer.js +1 -1
- package/dist/workspace/decide.d.ts +51 -9
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/decide.js +125 -14
- package/dist/workspace/graph.schema.json +68 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +20 -11
- package/dist/workspace/nested-graph.d.ts +1 -1
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +5 -1
- package/dist/workspace/point-answer.schema.json +16 -1
- package/dist/workspace/points-cli.d.ts +5 -3
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points-cli.js +16 -7
- package/dist/workspace/points-write.schema.json +39 -4
- package/dist/workspace/points.schema.json +42 -3
- package/dist/workspace/reason-codes.d.ts +2 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +2 -1
- package/dist/workspace/records-cli.d.ts +12 -2
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-cli.js +27 -7
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records-write.js +2 -3
- package/dist/workspace/records.d.ts +10 -4
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/records.js +16 -3
- package/dist/workspace/records.schema.json +2 -1
- package/dist/workspace/runs.d.ts.map +1 -1
- package/dist/workspace/runs.js +4 -2
- package/dist/workspace/trust/verify-cli.d.ts.map +1 -1
- package/dist/workspace/trust/verify-cli.js +2 -1
- package/dist/workspace/trust/verify.d.ts +22 -0
- package/dist/workspace/trust/verify.d.ts.map +1 -1
- package/dist/workspace/trust/verify.js +94 -14
- package/dist/workspace/write-scope.d.ts +3 -1
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -4
- package/package.json +1 -1
- package/src/ci-jobs.ts +58 -1
- package/src/cli/commands/build.ts +18 -0
- package/src/cli/compiled-cli.e2e.test.ts +45 -1
- package/src/cli/handlers/approve.ts +9 -0
- package/src/cli/handlers/emulator.test.ts +61 -1
- package/src/cli/handlers/emulator.ts +65 -4
- package/src/cli/handlers/promote.test.ts +7 -0
- package/src/cli/handlers/promote.ts +7 -1
- package/src/cli/handlers/run-no-record.e2e.test.ts +87 -0
- package/src/cli/handlers/run-wave.ts +5 -4
- package/src/cli/handlers/run.test.ts +126 -0
- package/src/cli/handlers/run.ts +69 -9
- package/src/cli/main.test.ts +34 -0
- package/src/cli/main.ts +38 -11
- package/src/cli/mcp/workspace-tools.e2e.test.ts +1 -1
- package/src/cli/mcp/workspace-tools.ts +27 -4
- package/src/cli/parse-args.ts +21 -0
- package/src/cli/registry.ts +12 -0
- package/src/components/cli-support.test.ts +75 -0
- package/src/components/cli-support.ts +130 -25
- package/src/components/driver.test.ts +28 -0
- package/src/components/driver.ts +23 -10
- package/src/components/generate-disabled.test.ts +56 -0
- package/src/effect-receipt.ts +22 -0
- package/src/graph-ci-edges.test.ts +50 -0
- package/src/graph-detail.ts +21 -2
- package/src/lexicon-module.ts +9 -2
- package/src/lifecycle/git.ts +61 -6
- package/src/lifecycle/no-record.test.ts +175 -0
- package/src/lifecycle/no-record.ts +108 -0
- package/src/lifecycle/receipt-plan.test.ts +9 -0
- package/src/lifecycle/receipt-plan.ts +4 -1
- package/src/op/activities/shell.ts +28 -6
- package/src/op/effect-step.test.ts +95 -1
- package/src/op/emulator-lifecycle.test.ts +24 -1
- package/src/op/emulator-lifecycle.ts +58 -2
- package/src/op/gate.ts +3 -0
- package/src/op/index.ts +1 -1
- package/src/op/local-executor.test.ts +65 -0
- package/src/op/local-executor.ts +88 -8
- package/src/op/op-waves-jobs.test.ts +213 -0
- package/src/op/op-waves-notify.ts +11 -5
- package/src/op/op-waves-run.ts +42 -11
- package/src/op/op-waves.ts +202 -2
- package/src/op/receipt-store.ts +13 -3
- package/src/op/runtimes/local.ts +6 -0
- package/src/op/step-console.test.ts +150 -0
- package/src/op/step-console.ts +155 -0
- package/src/op/step-output-ref.test.ts +68 -24
- package/src/op/step-output-ref.ts +85 -65
- package/src/point-decision-bundle.test.ts +1 -0
- package/src/point-decision.ts +3 -0
- package/src/workspace/agent-cli.ts +2 -0
- package/src/workspace/compose-graph.test.ts +55 -0
- package/src/workspace/compose-graph.ts +28 -6
- package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
- package/src/workspace/conformance/writer.ts +1 -1
- package/src/workspace/decide.test.ts +87 -3
- package/src/workspace/decide.ts +142 -17
- package/src/workspace/graph-contract.test.ts +26 -1
- package/src/workspace/graph.schema.json +68 -1
- package/src/workspace/lineage-upgrade.test.ts +21 -0
- package/src/workspace/lineage-upgrade.ts +19 -11
- package/src/workspace/nested-graph.test.ts +14 -0
- package/src/workspace/nested-graph.ts +7 -2
- package/src/workspace/point-answer.schema.json +16 -1
- package/src/workspace/points-cli.ts +16 -8
- package/src/workspace/points-write.schema.json +39 -4
- package/src/workspace/points.schema.json +42 -3
- package/src/workspace/reason-codes.ts +2 -1
- package/src/workspace/record-assets.test.ts +14 -0
- package/src/workspace/records-cli.ts +28 -7
- package/src/workspace/records-write-commands.e2e.test.ts +32 -0
- package/src/workspace/records-write.ts +2 -3
- package/src/workspace/records.schema.json +2 -1
- package/src/workspace/records.ts +17 -4
- package/src/workspace/runs.ts +4 -2
- package/src/workspace/trust/trust.test.ts +105 -0
- package/src/workspace/trust/verify-cli.ts +2 -1
- package/src/workspace/trust/verify.ts +106 -13
- package/src/workspace/write-scope.test.ts +18 -0
- package/src/workspace/write-scope.ts +16 -5
|
@@ -10,7 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
import { activity, phase, effect } from "./builders";
|
|
11
11
|
import type { OpConfig } from "./types";
|
|
12
12
|
import { activityContract, type ActivityContract } from "./activity-contract";
|
|
13
|
-
import { stepOutput, isStepOutputRef, collectStepOutputRefs, validateStepOutputRefs } from "./step-output-ref";
|
|
13
|
+
import { stepOutput, isStepOutputRef, collectStepOutputRefs, validateStepOutputRefs, validateStepOutputRefScope } from "./step-output-ref";
|
|
14
14
|
import { EffectReceipt } from "../effect-receipt";
|
|
15
15
|
|
|
16
16
|
const lifecycleDiffContract = activityContract(
|
|
@@ -248,39 +248,83 @@ describe("validateStepOutputRefs() — structural coverage", () => {
|
|
|
248
248
|
expect(issues.some((i) => i.message.includes("declares no return schema"))).toBe(true);
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
it("
|
|
251
|
+
it("reports every issue across multiple refs, not just the first", () => {
|
|
252
|
+
const apply1 = activity("applyStacks", { stacks: stepOutput("nope1", "x") });
|
|
253
|
+
const apply2 = activity("applyStacks", { stacks: stepOutput("nope2", "x") });
|
|
254
|
+
const config = opWith({ phases: [phase("P", [apply1, apply2])] });
|
|
255
|
+
const issues = validateStepOutputRefs(config, new Map([["applyStacks", applyStacksContract]]));
|
|
256
|
+
expect(issues.some((i) => i.message.includes("nope1"))).toBe(true);
|
|
257
|
+
expect(issues.some((i) => i.message.includes("nope2"))).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ── onFailure and effect steps (#3948) ───────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
describe("validateStepOutputRefs() — onFailure and effect steps (#3948)", () => {
|
|
264
|
+
const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "hash", inputs: { file: "seed.sql", version: 3 } });
|
|
265
|
+
const contracts = new Map<string, ActivityContract>([
|
|
266
|
+
["lifecycleDiff", lifecycleDiffContract],
|
|
267
|
+
["applyStacks", applyStacksContract],
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
it("an onFailure step may reference any main-phase step", () => {
|
|
252
271
|
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
272
|
+
const apply = activity("applyStacks", { stacks: ["a"] });
|
|
253
273
|
const rollback = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
|
|
274
|
+
const config = opWith({ phases: [phase("Diff", [diff]), phase("Apply", [apply])], onFailure: [phase("Rollback", [rollback])] });
|
|
275
|
+
expect(validateStepOutputRefs(config, contracts)).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("an onFailure step's reference is still checked against the producer's contract", () => {
|
|
279
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
280
|
+
const rollback = activity("applyStacks", { stacks: stepOutput("diff", "nope") });
|
|
254
281
|
const config = opWith({ phases: [phase("P", [diff])], onFailure: [phase("Rollback", [rollback])] });
|
|
255
|
-
const contracts = new Map<string, ActivityContract>([
|
|
256
|
-
["lifecycleDiff", lifecycleDiffContract],
|
|
257
|
-
["applyStacks", applyStacksContract],
|
|
258
|
-
]);
|
|
259
282
|
const issues = validateStepOutputRefs(config, contracts);
|
|
260
|
-
expect(issues
|
|
283
|
+
expect(issues).toHaveLength(1);
|
|
284
|
+
expect(issues[0].phase).toBe("Rollback");
|
|
285
|
+
expect(issues[0].message).toMatch(/path "nope", which does not exist/);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("an onFailure step cannot reference another onFailure step", () => {
|
|
289
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "undoDiff" });
|
|
290
|
+
const rollback = activity("applyStacks", { stacks: stepOutput("undoDiff", "driftedStacks") });
|
|
291
|
+
const config = opWith({ phases: [phase("P", [activity("applyStacks", { stacks: [] })])], onFailure: [phase("Rollback", [diff, rollback])] });
|
|
292
|
+
expect(validateStepOutputRefScope(config)[0].message).toMatch(/unknown step id "undoDiff"/);
|
|
261
293
|
});
|
|
262
294
|
|
|
263
|
-
it("
|
|
264
|
-
const receipt = EffectReceipt("seeded", { effect: "db-seed", flavor: "hash", inputs: { file: "seed.sql", version: 3 } });
|
|
295
|
+
it("a step inside an effect may reference a step that runs before the effect", () => {
|
|
265
296
|
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
266
297
|
const nested = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
const contracts = new Map<string, ActivityContract>([
|
|
270
|
-
["lifecycleDiff", lifecycleDiffContract],
|
|
271
|
-
["applyStacks", applyStacksContract],
|
|
272
|
-
]);
|
|
273
|
-
const issues = validateStepOutputRefs(config, contracts);
|
|
274
|
-
expect(issues.some((i) => i.message.includes("nested inside an effect step"))).toBe(true);
|
|
298
|
+
const config = opWith({ phases: [phase("P", [diff, effect(seeded, [nested])])] });
|
|
299
|
+
expect(validateStepOutputRefs(config, contracts)).toEqual([]);
|
|
275
300
|
});
|
|
276
301
|
|
|
277
|
-
it("
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
const config = opWith({ phases: [phase("P", [
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
302
|
+
it("a step inside an effect in onFailure may reference a main-phase step", () => {
|
|
303
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
304
|
+
const nested = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
|
|
305
|
+
const config = opWith({ phases: [phase("P", [diff])], onFailure: [phase("Undo", [effect(seeded, [nested])])] });
|
|
306
|
+
expect(validateStepOutputRefs(config, contracts)).toEqual([]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("a step inside an effect cannot reference a step after the effect", () => {
|
|
310
|
+
const nested = activity("applyStacks", { stacks: stepOutput("diff", "driftedStacks") });
|
|
311
|
+
const diff = activity("lifecycleDiff", { env: "prod" }, { id: "diff" });
|
|
312
|
+
const config = opWith({ phases: [phase("P", [effect(seeded, [nested]), diff])] });
|
|
313
|
+
expect(validateStepOutputRefScope(config)[0].message).toMatch(/step "diff" does not run before this step/);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("nothing can reference a step nested inside an effect (decision A7)", () => {
|
|
317
|
+
const inner = activity("lifecycleDiff", { env: "prod" }, { id: "inner" });
|
|
318
|
+
const sibling = activity("applyStacks", { stacks: stepOutput("inner", "driftedStacks") });
|
|
319
|
+
const later = activity("applyStacks", { stacks: stepOutput("inner", "driftedStacks") });
|
|
320
|
+
const rollback = activity("applyStacks", { stacks: stepOutput("inner", "driftedStacks") });
|
|
321
|
+
const config = opWith({
|
|
322
|
+
phases: [phase("P", [effect(seeded, [inner, sibling]), later])],
|
|
323
|
+
onFailure: [phase("Rollback", [rollback])],
|
|
324
|
+
});
|
|
325
|
+
const issues = validateStepOutputRefScope(config);
|
|
326
|
+
expect(issues).toHaveLength(3);
|
|
327
|
+
for (const issue of issues) expect(issue.message).toMatch(/"inner", which is nested inside an effect step/);
|
|
284
328
|
});
|
|
285
329
|
});
|
|
286
330
|
|
|
@@ -23,15 +23,20 @@
|
|
|
23
23
|
* Scope, kept deliberately tight (see the issue's "Open questions"):
|
|
24
24
|
* - same-Op only — no cross-Op plumbing (that needs a durable place to put
|
|
25
25
|
* the value, which #1290 explicitly defers).
|
|
26
|
-
* -
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
26
|
+
* - producers are the top-level activity steps of the main `phases`. A step
|
|
27
|
+
* in `onFailure` or nested inside an `EffectStep` produces nothing another
|
|
28
|
+
* step may reference: an effect's nested steps run only when the receipt
|
|
29
|
+
* comparison mismatches, so a value one of them returns isn't reliably
|
|
30
|
+
* there for anything else (#3948 decision A7, "for now").
|
|
31
|
+
* - consumers are those producers, plus (#3948) every activity step in
|
|
32
|
+
* `onFailure` and every activity step nested inside an effect. An
|
|
33
|
+
* `onFailure` step may reference any main-phase producer. Compensation
|
|
34
|
+
* runs after a failure, so its producer may never have completed; the
|
|
35
|
+
* executor then skips the compensation step and records why (decision
|
|
36
|
+
* A6), rather than calling it with an `undefined` in place of the value.
|
|
37
|
+
* A step nested inside an effect may reference a producer that runs
|
|
38
|
+
* before the effect, and the executor folds the resolved values into the
|
|
39
|
+
* effect's expectation, so a changed value re-fires the effect.
|
|
35
40
|
* - producer must run to completion before the consumer starts: an earlier
|
|
36
41
|
* phase, or an earlier step within the same non-parallel phase. Two
|
|
37
42
|
* steps in the same parallel phase run concurrently (`Promise.all`) with
|
|
@@ -243,26 +248,63 @@ function locateStepOutputRefPath(value: unknown, target: StepOutputRef, path: st
|
|
|
243
248
|
// ── Validation ──────────────────────────────────────────────────────────────
|
|
244
249
|
|
|
245
250
|
interface StepLocation {
|
|
251
|
+
/** Index into `phases`; every `onFailure` phase sits after all of them. */
|
|
246
252
|
phaseIndex: number;
|
|
253
|
+
/** Index into the phase's `steps`. A step nested in an effect takes the effect's. */
|
|
247
254
|
stepIndex: number;
|
|
248
255
|
parallel: boolean;
|
|
249
256
|
step: ActivityStep;
|
|
257
|
+
phaseName: string;
|
|
250
258
|
}
|
|
251
259
|
|
|
252
|
-
/** Top-level `ActivityStep`s of `phases`, in authored (phase, then step) order
|
|
260
|
+
/** Top-level `ActivityStep`s of `phases`, in authored (phase, then step) order: the producers (see the module doc's scope note). */
|
|
253
261
|
function locateActivitySteps(phases: PhaseDefinition[]): StepLocation[] {
|
|
254
262
|
const locations: StepLocation[] = [];
|
|
255
263
|
phases.forEach((phase, phaseIndex) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
stepIndex++;
|
|
261
|
-
}
|
|
264
|
+
phase.steps.forEach((step, stepIndex) => {
|
|
265
|
+
if (step.kind !== "activity") return;
|
|
266
|
+
locations.push({ phaseIndex, stepIndex, parallel: phase.parallel === true, step, phaseName: phase.name });
|
|
267
|
+
});
|
|
262
268
|
});
|
|
263
269
|
return locations;
|
|
264
270
|
}
|
|
265
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Every activity step that may hold a reference (#3948): the producers, each
|
|
274
|
+
* step nested inside an effect of a main phase, and every activity step of
|
|
275
|
+
* `onFailure`, effect-nested ones included. An `onFailure` step is placed after
|
|
276
|
+
* every main phase, so any main-phase producer precedes it.
|
|
277
|
+
*/
|
|
278
|
+
function locateConsumers(config: Pick<OpConfig, "phases" | "onFailure">): StepLocation[] {
|
|
279
|
+
const consumers: StepLocation[] = [];
|
|
280
|
+
const visit = (phases: PhaseDefinition[], offset: number) => {
|
|
281
|
+
phases.forEach((phase, i) => {
|
|
282
|
+
const at = { phaseIndex: offset + i, parallel: phase.parallel === true, phaseName: phase.name };
|
|
283
|
+
phase.steps.forEach((step, stepIndex) => {
|
|
284
|
+
if (step.kind === "activity") consumers.push({ ...at, stepIndex, step });
|
|
285
|
+
if (step.kind === "effect") {
|
|
286
|
+
for (const nested of step.steps) if (nested.kind === "activity") consumers.push({ ...at, stepIndex, step: nested });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
visit(config.phases, 0);
|
|
292
|
+
visit(config.onFailure ?? [], config.phases.length);
|
|
293
|
+
return consumers;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Ids of the activity steps nested inside an effect, in any phase. Nothing may reference them (#3948 decision A7). */
|
|
297
|
+
function effectNestedIds(config: Pick<OpConfig, "phases" | "onFailure">): Set<string> {
|
|
298
|
+
const ids = new Set<string>();
|
|
299
|
+
for (const phase of [...config.phases, ...(config.onFailure ?? [])]) {
|
|
300
|
+
for (const step of phase.steps) {
|
|
301
|
+
if (step.kind !== "effect") continue;
|
|
302
|
+
for (const nested of step.steps) if (nested.kind === "activity" && nested.id) ids.add(nested.id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return ids;
|
|
306
|
+
}
|
|
307
|
+
|
|
266
308
|
/** True when `producer` is guaranteed to have completed before `consumer` starts. */
|
|
267
309
|
function producerPrecedes(producer: StepLocation, consumer: StepLocation): boolean {
|
|
268
310
|
if (producer.phaseIndex !== consumer.phaseIndex) return producer.phaseIndex < consumer.phaseIndex;
|
|
@@ -270,10 +312,6 @@ function producerPrecedes(producer: StepLocation, consumer: StepLocation): boole
|
|
|
270
312
|
return producer.stepIndex < consumer.stepIndex;
|
|
271
313
|
}
|
|
272
314
|
|
|
273
|
-
function effectNestedActivitySteps(phase: PhaseDefinition): ActivityStep[] {
|
|
274
|
-
return phase.steps.flatMap((s) => (s.kind === "effect" ? s.steps.filter((n) => n.kind === "activity") : []));
|
|
275
|
-
}
|
|
276
|
-
|
|
277
315
|
/** `byId`/`duplicateIds` over an Op's top-level main-phase activity steps — shared by {@link validateStepOutputRefScope} and {@link validateStepOutputRefs}. */
|
|
278
316
|
function indexById(locations: StepLocation[]): { byId: Map<string, StepLocation>; duplicateIds: Set<string> } {
|
|
279
317
|
const byId = new Map<string, StepLocation>();
|
|
@@ -298,46 +336,28 @@ function indexById(locations: StepLocation[]): { byId: Map<string, StepLocation>
|
|
|
298
336
|
* a public export a caller can invoke directly, bypassing that lint pass —
|
|
299
337
|
* this is its own defense-in-depth.
|
|
300
338
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
* explicitly here rather than left to fall
|
|
339
|
+
* Consumers are the main-phase activity steps, the steps nested inside an
|
|
340
|
+
* effect, and every `onFailure` activity step (#3948). Flags:
|
|
341
|
+
* - a reference to a step nested inside an effect step, from anywhere
|
|
342
|
+
* (#3948 decision A7) — flagged explicitly here rather than left to fall
|
|
343
|
+
* through as "unknown producer",
|
|
305
344
|
* - a reference to an unregistered step id (no step in scope declares it),
|
|
306
345
|
* - a duplicate step id (ambiguous producer),
|
|
307
346
|
* - a reference to a step that does not precede the referencing step
|
|
308
347
|
* (a later phase, a later step in the same phase, itself, or a step in
|
|
309
|
-
* the same parallel phase).
|
|
348
|
+
* the same parallel phase). A step nested inside an effect takes the
|
|
349
|
+
* effect's position, so it may reference only a step that runs before
|
|
350
|
+
* the effect. An `onFailure` step comes after every main phase.
|
|
310
351
|
*/
|
|
311
352
|
export function validateStepOutputRefScope(
|
|
312
353
|
config: Pick<OpConfig, "name" | "phases" | "onFailure"> & Partial<Pick<OpConfig, "workLease">>,
|
|
313
354
|
): ActivityContractIssue[] {
|
|
314
355
|
const issues: ActivityContractIssue[] = [];
|
|
315
356
|
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
for (const ref of collectStepOutputRefs(step.args)) {
|
|
319
|
-
issues.push({
|
|
320
|
-
opName: config.name,
|
|
321
|
-
phase: phaseName,
|
|
322
|
-
fn: step.fn,
|
|
323
|
-
message: `references step "${ref.step}"'s output, but ${reason}`,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
for (const phase of config.onFailure ?? []) {
|
|
329
|
-
const steps = phase.steps.filter((s): s is ActivityStep => s.kind === "activity").concat(effectNestedActivitySteps(phase));
|
|
330
|
-
flagOutOfScope(steps, phase.name, "step-output references are not supported in onFailure compensation phases");
|
|
331
|
-
}
|
|
332
|
-
for (const phase of config.phases) {
|
|
333
|
-
flagOutOfScope(effectNestedActivitySteps(phase), phase.name, "step-output references are not supported for a step nested inside an effect step");
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const locations = locateActivitySteps(config.phases);
|
|
337
|
-
const { byId, duplicateIds } = indexById(locations);
|
|
338
|
-
const phaseNameOf = (loc: StepLocation): string => config.phases[loc.phaseIndex]!.name;
|
|
357
|
+
const { byId, duplicateIds } = indexById(locateActivitySteps(config.phases));
|
|
358
|
+
const nestedIds = effectNestedIds(config);
|
|
339
359
|
|
|
340
|
-
for (const consumer of
|
|
360
|
+
for (const consumer of locateConsumers(config)) {
|
|
341
361
|
const refs = collectStepOutputRefs(consumer.step.args);
|
|
342
362
|
for (const ref of refs) {
|
|
343
363
|
// The run's work lease (#2748) is published under a reserved id once it
|
|
@@ -346,7 +366,7 @@ export function validateStepOutputRefScope(
|
|
|
346
366
|
if (duplicateIds.has(ref.step)) {
|
|
347
367
|
issues.push({
|
|
348
368
|
opName: config.name,
|
|
349
|
-
phase:
|
|
369
|
+
phase: consumer.phaseName,
|
|
350
370
|
fn: consumer.step.fn,
|
|
351
371
|
message: `references step id "${ref.step}", but that id is used by more than one step in this Op — ids must be unique`,
|
|
352
372
|
});
|
|
@@ -357,9 +377,11 @@ export function validateStepOutputRefScope(
|
|
|
357
377
|
if (!producer) {
|
|
358
378
|
issues.push({
|
|
359
379
|
opName: config.name,
|
|
360
|
-
phase:
|
|
380
|
+
phase: consumer.phaseName,
|
|
361
381
|
fn: consumer.step.fn,
|
|
362
|
-
message:
|
|
382
|
+
message: nestedIds.has(ref.step)
|
|
383
|
+
? `references step "${ref.step}", which is nested inside an effect step — a step inside an effect runs only when the effect fires, so no other step can reference its output`
|
|
384
|
+
: `references unknown step id "${ref.step}" — no step in this Op's main phases declares that id`,
|
|
363
385
|
});
|
|
364
386
|
continue;
|
|
365
387
|
}
|
|
@@ -373,7 +395,7 @@ export function validateStepOutputRefScope(
|
|
|
373
395
|
: `step "${ref.step}" does not run before this step`;
|
|
374
396
|
issues.push({
|
|
375
397
|
opName: config.name,
|
|
376
|
-
phase:
|
|
398
|
+
phase: consumer.phaseName,
|
|
377
399
|
fn: consumer.step.fn,
|
|
378
400
|
message: `references ${reason} — a step can only reference an earlier step's output`,
|
|
379
401
|
});
|
|
@@ -385,10 +407,10 @@ export function validateStepOutputRefScope(
|
|
|
385
407
|
}
|
|
386
408
|
|
|
387
409
|
/**
|
|
388
|
-
* Validate every step-output reference in an Op
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
410
|
+
* Validate every step-output reference in an Op against a contract map and
|
|
411
|
+
* the Op's own step ordering, over the consumers {@link
|
|
412
|
+
* validateStepOutputRefScope} covers (see the module doc). Layers
|
|
413
|
+
* contract-based checks on top of {@link
|
|
392
414
|
* validateStepOutputRefScope}'s scope/ordering checks.
|
|
393
415
|
*
|
|
394
416
|
* Flags everything {@link validateStepOutputRefScope} does, plus:
|
|
@@ -404,11 +426,9 @@ export function validateStepOutputRefs(
|
|
|
404
426
|
): ActivityContractIssue[] {
|
|
405
427
|
const issues = validateStepOutputRefScope(config);
|
|
406
428
|
|
|
407
|
-
const
|
|
408
|
-
const { byId, duplicateIds } = indexById(locations);
|
|
409
|
-
const phaseNameOf = (loc: StepLocation): string => config.phases[loc.phaseIndex]!.name;
|
|
429
|
+
const { byId, duplicateIds } = indexById(locateActivitySteps(config.phases));
|
|
410
430
|
|
|
411
|
-
for (const consumer of
|
|
431
|
+
for (const consumer of locateConsumers(config)) {
|
|
412
432
|
const refs = collectStepOutputRefs(consumer.step.args);
|
|
413
433
|
for (const ref of refs) {
|
|
414
434
|
// Already flagged by validateStepOutputRefScope — nothing to validate
|
|
@@ -421,7 +441,7 @@ export function validateStepOutputRefs(
|
|
|
421
441
|
if (!producerContract) {
|
|
422
442
|
issues.push({
|
|
423
443
|
opName: config.name,
|
|
424
|
-
phase:
|
|
444
|
+
phase: consumer.phaseName,
|
|
425
445
|
fn: consumer.step.fn,
|
|
426
446
|
message: `references step "${ref.step}"'s output, but "${producer.step.fn}" has no registered activity contract to validate the reference against`,
|
|
427
447
|
});
|
|
@@ -430,7 +450,7 @@ export function validateStepOutputRefs(
|
|
|
430
450
|
if (!producerContract.returns) {
|
|
431
451
|
issues.push({
|
|
432
452
|
opName: config.name,
|
|
433
|
-
phase:
|
|
453
|
+
phase: consumer.phaseName,
|
|
434
454
|
fn: consumer.step.fn,
|
|
435
455
|
message: `references step "${ref.step}"'s output, but "${producer.step.fn}"'s contract declares no return schema`,
|
|
436
456
|
});
|
|
@@ -439,7 +459,7 @@ export function validateStepOutputRefs(
|
|
|
439
459
|
if (ref.path && !pathExistsInSchema(producerContract.returns as z.ZodTypeAny, ref.path)) {
|
|
440
460
|
issues.push({
|
|
441
461
|
opName: config.name,
|
|
442
|
-
phase:
|
|
462
|
+
phase: consumer.phaseName,
|
|
443
463
|
fn: consumer.step.fn,
|
|
444
464
|
message: `references step "${ref.step}"'s output path "${ref.path}", which does not exist on "${producer.step.fn}"'s declared return type`,
|
|
445
465
|
});
|
|
@@ -466,7 +486,7 @@ export function validateStepOutputRefs(
|
|
|
466
486
|
if (consumerKind && producerKind && consumerKind !== producerKind) {
|
|
467
487
|
issues.push({
|
|
468
488
|
opName: config.name,
|
|
469
|
-
phase:
|
|
489
|
+
phase: consumer.phaseName,
|
|
470
490
|
fn: consumer.step.fn,
|
|
471
491
|
message: `references step "${ref.step}"'s output path "${ref.path}" (${producerKind}), but arg "${argPath.join(".")}" on "${consumer.step.fn}" declares ${consumerKind} — type mismatch`,
|
|
472
492
|
});
|
|
@@ -51,6 +51,7 @@ describe("the point-decision bundle", () => {
|
|
|
51
51
|
"packages/core/src/intrinsic.ts",
|
|
52
52
|
"packages/core/src/lifecycle/gate-ledger-select.ts",
|
|
53
53
|
"packages/core/src/lifecycle/gate-pending.ts",
|
|
54
|
+
"packages/core/src/lifecycle/no-record.ts",
|
|
54
55
|
"packages/core/src/lifecycle/plan-digest.ts",
|
|
55
56
|
"packages/core/src/op/duration.ts",
|
|
56
57
|
"packages/core/src/op/gate-point.ts",
|
package/src/point-decision.ts
CHANGED
|
@@ -37,6 +37,7 @@ import type { GateLedgerPort } from "./op/gate";
|
|
|
37
37
|
import { DEFAULT_GATE_POINT_PASS, defaultGatePointStore, gatePointInputs, gateSubject, type GatePoint, type GatePointFacts, type GatePointStore } from "./op/gate-point";
|
|
38
38
|
import type { WaitingPoint } from "./op/steward-points";
|
|
39
39
|
import { resolveGateRunLocator, type GateRunLocator } from "./op/gate-run-locator";
|
|
40
|
+
import { refuseGateWithoutRecording } from "./lifecycle/no-record";
|
|
40
41
|
|
|
41
42
|
export {
|
|
42
43
|
DEFAULT_GATE_POINT_PASS,
|
|
@@ -170,6 +171,8 @@ function resolvedByOf(q: GatePointQuestion): string {
|
|
|
170
171
|
|
|
171
172
|
/** Decide a gate that asks a point: ask it, then record the pending fact or the resolution that cites the answer. */
|
|
172
173
|
export async function evaluatePointGate(port: PointGateLedgerPort, asker: GatePointAsker, input: PointGateInput): Promise<PointGateCheck> {
|
|
174
|
+
// #3955: refused before the point is asked, which would itself write to the ledger.
|
|
175
|
+
refuseGateWithoutRecording(input.op, input.gate);
|
|
173
176
|
const now = input.now ?? new Date().toISOString();
|
|
174
177
|
const facts: GatePointFacts = { component: input.op, name: input.gate, env: input.env ?? null, planDigest: input.planDigest ?? null };
|
|
175
178
|
const q = await asker.ask({ point: input.point.name, inputs: input.inputs ?? {}, facts, subject: gateSubject(input.op, input.gate, input.env), ...(input.point.store ? { store: input.point.store } : {}) });
|
|
@@ -91,6 +91,8 @@ export async function agentSession(q: { cwd: string; name: string }): Promise<Ag
|
|
|
91
91
|
try {
|
|
92
92
|
const source = scopeSource(q.cwd);
|
|
93
93
|
if (source.declaration === null || source.from === null) {
|
|
94
|
+
// A declaration that is there but fails validation says so (#3433).
|
|
95
|
+
if (source.failure && source.failure.code !== "declaration-missing") throw source.failure;
|
|
94
96
|
throw new WorkspaceReadError("declaration-missing", "no chant.workspace.json or .jsonc between this directory and the git root, so no agent session is declared");
|
|
95
97
|
}
|
|
96
98
|
const decl = source.declaration;
|
|
@@ -104,6 +104,61 @@ describe("composeWorkspaceGraph", () => {
|
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
describe("CI edges, pipelines and dangling references (#3922)", () => {
|
|
108
|
+
/** A github member as `chant graph` reports it after #3910. */
|
|
109
|
+
const ci = (workflow: string, job: string): GraphIR => ({
|
|
110
|
+
version: 1,
|
|
111
|
+
nodes: [
|
|
112
|
+
{ id: workflow, kind: "Workflow", lexicon: "github", attrs: {} },
|
|
113
|
+
{ id: job, kind: "Job", lexicon: "github", attrs: {} },
|
|
114
|
+
{ id: "deploy", kind: "Job", lexicon: "github", attrs: {} },
|
|
115
|
+
],
|
|
116
|
+
edges: [
|
|
117
|
+
{ from: "deploy", to: job, kind: "needs", viaAttr: "needs" },
|
|
118
|
+
{ from: job, to: workflow, kind: "calls", viaAttr: "uses", crossPipeline: true },
|
|
119
|
+
],
|
|
120
|
+
groups: { byLexicon: { github: [job, "deploy", workflow] }, byPipeline: { ci: [job, "deploy", workflow] } },
|
|
121
|
+
dangling: [
|
|
122
|
+
{ from: "deploy", kind: "needs", path: "needs", value: "lint" },
|
|
123
|
+
{ from: job, kind: "calls", path: "uses", value: "other/repo/.github/workflows/x.yml@main", crossPipeline: true },
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
const doc = composeWorkspaceGraph({ name: "acme", root: "." }, [
|
|
127
|
+
{ member: member("svc", "svc"), ir: ci("ciWorkflow", "build") },
|
|
128
|
+
{ member: member("app", "app"), ir: ci("ciWorkflow", "test") },
|
|
129
|
+
{ member: member("db", "db"), ir: db },
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
test("edges keep their kind and crossPipeline", () => {
|
|
133
|
+
expect(doc.edges.filter((e) => e.member === "svc")).toEqual([
|
|
134
|
+
{ from: "svc/build", to: "svc/ciWorkflow", kind: "calls", viaAttr: "uses", crossPipeline: true, member: "svc" },
|
|
135
|
+
{ from: "svc/deploy", to: "svc/build", kind: "needs", viaAttr: "needs", member: "svc" },
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("byPipeline keys and ids are prefixed with the member", () => {
|
|
140
|
+
expect(doc.groups.byPipeline).toEqual({
|
|
141
|
+
"app/ci": ["app/ciWorkflow", "app/deploy", "app/test"],
|
|
142
|
+
"svc/ci": ["svc/build", "svc/ciWorkflow", "svc/deploy"],
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("dangling references keep their fields, with from prefixed and the member named, sorted", () => {
|
|
147
|
+
expect(doc.dangling).toEqual([
|
|
148
|
+
{ from: "app/deploy", kind: "needs", path: "needs", value: "lint", member: "app" },
|
|
149
|
+
{ from: "app/test", kind: "calls", path: "uses", value: "other/repo/.github/workflows/x.yml@main", crossPipeline: true, member: "app" },
|
|
150
|
+
{ from: "svc/build", kind: "calls", path: "uses", value: "other/repo/.github/workflows/x.yml@main", crossPipeline: true, member: "svc" },
|
|
151
|
+
{ from: "svc/deploy", kind: "needs", path: "needs", value: "lint", member: "svc" },
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("no dangling section and no byPipeline when no member reports either", () => {
|
|
156
|
+
const plain = composeWorkspaceGraph({ name: "acme", root: "." }, [{ member: member("db", "db"), ir: db }]);
|
|
157
|
+
expect("dangling" in plain).toBe(false);
|
|
158
|
+
expect(plain.groups.byPipeline).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
107
162
|
describe("the links section (#2539)", () => {
|
|
108
163
|
test("inferred joins carry their label, and a declared link suppresses the inferred edge it covers", () => {
|
|
109
164
|
const consumer: GraphIR = {
|
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
* `runtimeOwner` and export and import nodes are rewritten the same way.
|
|
12
12
|
* - `groups.byMember` maps each member to its node ids. `byLexicon` and
|
|
13
13
|
* `byComposite` keep their keys (a lexicon or composite type is the same
|
|
14
|
-
* thing in every member) and merge the ids. `byStack`, `byContainer
|
|
15
|
-
* `byWave` name things inside one member, so their keys
|
|
14
|
+
* thing in every member) and merge the ids. `byStack`, `byContainer`,
|
|
15
|
+
* `byWave` and `byPipeline` name things inside one member, so their keys
|
|
16
|
+
* are prefixed too.
|
|
17
|
+
* - Edges keep their `kind` and `crossPipeline` (#3910). A member's dangling
|
|
18
|
+
* references (`ir.dangling`) land on the top-level `dangling` list with
|
|
19
|
+
* `from` prefixed and a `member` field, like edges (#3922).
|
|
16
20
|
* - `links` holds the member links (#2524 D6, #2539): the declared links and
|
|
17
21
|
* the joins inferred from members' `imports` and `exports` with the core
|
|
18
22
|
* `joinKey()`, labelled `exact` or `folded` (`./links.ts`). A declared link
|
|
@@ -36,7 +40,7 @@ import { graphLinks, type LinkTableRow } from "./links";
|
|
|
36
40
|
import type { KindLinkRead } from "./kind-link-readers";
|
|
37
41
|
import type { RecordLinkRow } from "./record-assets";
|
|
38
42
|
import type { ReasonCode } from "./reason-codes";
|
|
39
|
-
import { GRAPH_IR_VERSION, type GraphIR, type IRExport, type IRGroups, type IRImport, type IREdge, type IRNode } from "../graph-ir";
|
|
43
|
+
import { GRAPH_IR_VERSION, type GraphIR, type IRExport, type IRDanglingRef, type IRGroups, type IRImport, type IREdge, type IRNode } from "../graph-ir";
|
|
40
44
|
|
|
41
45
|
/** The version of the composed document `chant workspace graph` writes. */
|
|
42
46
|
export const WORKSPACE_GRAPH_VERSION = 1;
|
|
@@ -165,6 +169,8 @@ export const COLLECTOR_META_KEY = "collector";
|
|
|
165
169
|
/** `nested` names the nested workspace's member a node was read through (#2551); absent for the outer workspace's own nodes. */
|
|
166
170
|
export type ComposedNode = IRNode & { member: string; nested?: string };
|
|
167
171
|
export type ComposedEdge = IREdge & { member: string };
|
|
172
|
+
/** A member's dangling reference (#3910, #3922): `from` is prefixed like a node id. `nested` as on {@link ComposedNode}. */
|
|
173
|
+
export type ComposedDangling = IRDanglingRef & { member: string; nested?: string };
|
|
168
174
|
|
|
169
175
|
export interface WorkspaceGraph {
|
|
170
176
|
version: number;
|
|
@@ -176,6 +182,12 @@ export interface WorkspaceGraph {
|
|
|
176
182
|
exports: (IRExport & { member: string })[];
|
|
177
183
|
imports: (IRImport & { member: string })[];
|
|
178
184
|
derivedAttrs?: Record<string, string[]>;
|
|
185
|
+
/**
|
|
186
|
+
* The members' dangling CI references (#3910, #3922): each `ir.dangling`
|
|
187
|
+
* entry with `from` prefixed by its member and a `member` field, sorted by
|
|
188
|
+
* `from`, `path` and `value`. Absent when no member has one.
|
|
189
|
+
*/
|
|
190
|
+
dangling?: ComposedDangling[];
|
|
179
191
|
/**
|
|
180
192
|
* Member links (#2524 D6, #2539), declared and inferred, then the links of
|
|
181
193
|
* the records read with `--kind` (#2549). Empty when no declaration is given.
|
|
@@ -289,6 +301,13 @@ function prefixRefs(value: unknown, member: string): unknown {
|
|
|
289
301
|
return out;
|
|
290
302
|
}
|
|
291
303
|
|
|
304
|
+
/** The groups whose keys name something inside one member, so are prefixed with it. */
|
|
305
|
+
export const SCOPED_GROUPS = ["byStack", "byContainer", "byWave", "byPipeline"] as const;
|
|
306
|
+
type ScopedGroup = (typeof SCOPED_GROUPS)[number];
|
|
307
|
+
|
|
308
|
+
/** The order the composed `dangling` list is sorted in. */
|
|
309
|
+
export const danglingKey = (d: IRDanglingRef): string => `${d.from}\u0000${d.path}\u0000${d.value}`;
|
|
310
|
+
|
|
292
311
|
function mergeInto(target: Record<string, string[]>, key: string, ids: string[]): void {
|
|
293
312
|
(target[key] ??= []).push(...ids);
|
|
294
313
|
}
|
|
@@ -317,7 +336,8 @@ export function composeWorkspaceGraph(
|
|
|
317
336
|
const imports: WorkspaceGraph["imports"] = [];
|
|
318
337
|
const byMember: Record<string, string[]> = {};
|
|
319
338
|
const shared: Record<"byLexicon" | "byComposite", Record<string, string[]>> = { byLexicon: {}, byComposite: {} };
|
|
320
|
-
const scoped: Record<
|
|
339
|
+
const scoped: Record<ScopedGroup, Record<string, string[]>> = { byStack: {}, byContainer: {}, byWave: {}, byPipeline: {} };
|
|
340
|
+
const dangling: ComposedDangling[] = [];
|
|
321
341
|
const derivedAttrs: Record<string, Set<string>> = {};
|
|
322
342
|
const collectors: MemberCollector[] = [];
|
|
323
343
|
|
|
@@ -337,9 +357,10 @@ export function composeWorkspaceGraph(
|
|
|
337
357
|
for (const key of ["byLexicon", "byComposite"] as const) {
|
|
338
358
|
for (const [k, v] of Object.entries(ir.groups[key] ?? {})) mergeInto(shared[key], k, v.map((id) => prefix(m, id)));
|
|
339
359
|
}
|
|
340
|
-
for (const key of
|
|
360
|
+
for (const key of SCOPED_GROUPS) {
|
|
341
361
|
for (const [k, v] of Object.entries(ir.groups[key] ?? {})) mergeInto(scoped[key], prefix(m, k), v.map((id) => prefix(m, id)));
|
|
342
362
|
}
|
|
363
|
+
for (const d of ir.dangling ?? []) dangling.push({ ...d, from: prefix(m, d.from), member: m });
|
|
343
364
|
for (const e of ir.exports ?? []) exports.push({ ...e, ...(e.node ? { node: prefix(m, e.node) } : {}), member: m });
|
|
344
365
|
for (const i of ir.imports ?? []) imports.push({ ...i, node: prefix(m, i.node), member: m });
|
|
345
366
|
for (const [kind, attrs] of Object.entries(ir.derivedAttrs ?? {})) for (const a of attrs) (derivedAttrs[kind] ??= new Set()).add(a);
|
|
@@ -360,7 +381,7 @@ export function composeWorkspaceGraph(
|
|
|
360
381
|
|
|
361
382
|
const groups: WorkspaceGraph["groups"] = { byMember };
|
|
362
383
|
for (const key of ["byLexicon", "byComposite"] as const) if (Object.keys(shared[key]).length) groups[key] = sorted(shared[key]);
|
|
363
|
-
for (const key of
|
|
384
|
+
for (const key of SCOPED_GROUPS) if (Object.keys(scoped[key]).length) groups[key] = sorted(scoped[key]);
|
|
364
385
|
|
|
365
386
|
const doc: WorkspaceGraph = {
|
|
366
387
|
version: WORKSPACE_GRAPH_VERSION,
|
|
@@ -377,6 +398,7 @@ export function composeWorkspaceGraph(
|
|
|
377
398
|
collectors,
|
|
378
399
|
records: [],
|
|
379
400
|
};
|
|
401
|
+
if (dangling.length) doc.dangling = dangling.sort((a, b) => danglingKey(a).localeCompare(danglingKey(b)));
|
|
380
402
|
if (Object.keys(derivedAttrs).length) {
|
|
381
403
|
doc.derivedAttrs = Object.fromEntries(Object.keys(derivedAttrs).sort().map((k) => [k, [...derivedAttrs[k]].sort()]));
|
|
382
404
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "urn:intentius:chant:point-answer:1",
|
|
4
4
|
"title": "Decision point answer front matter",
|
|
5
|
-
"description": "The front matter of one answer to a decision point (ws-058, #2739), as `chant workspace points ask` and `points answer` write it into the records directory of a record kind with an `answers` block. One record per point, declaration and inputs: its id is the point's name and the first 12 hex digits of the inputs hash, so the same question is answered once. A table's answer is `answered`; a model's answer at or above its threshold is `proposed` until a person confirms it; a question no decider before the quorum answered is `escalated`, open for people. An answer never changes in place: `points retract` escalates the question to people again and moves the answer into `retractions` (#3351, ws-084). An ad-hoc point's answer keeps the question and candidates its ask gave as `asked`, and they are part of the inputs hash (#3403). A workspace copies this file next to its answer kind, as reference-workspace/answers/ does. Values stay within what YAML and JSON share. Fields other than these must start with x-.",
|
|
5
|
+
"description": "The front matter of one answer to a decision point (ws-058, #2739), as `chant workspace points ask` and `points answer` write it into the records directory of a record kind with an `answers` block. One record per point, declaration and inputs: its id is the point's name and the first 12 hex digits of the inputs hash, so the same question is answered once. A table's answer is `answered`; a model's answer at or above its threshold is `proposed` until a person confirms it; a question no decider before the quorum answered is `escalated`, open for people. An answer never changes in place: `points retract` escalates the question to people again and moves the answer into `retractions` (#3351, ws-084). An open question that no longer applies is `withdrawn` (`points withdraw`, #2888), and asking it again opens it again. An ad-hoc point's answer keeps the question and candidates its ask gave as `asked`, and they are part of the inputs hash (#3403). A workspace copies this file next to its answer kind, as reference-workspace/answers/ does. Values stay within what YAML and JSON share. Fields other than these must start with x-.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"patternProperties": { "^x-": {} },
|
|
@@ -65,6 +65,17 @@
|
|
|
65
65
|
"description": "Answers people took back, oldest first (`points retract`, #3351): each answer as it was, and who retracted it, when and why. A retracted question is escalated to people again.",
|
|
66
66
|
"items": { "$ref": "#/definitions/retraction" }
|
|
67
67
|
},
|
|
68
|
+
"withdrawn": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"description": "An open question that no longer applies, such as one about a work item its author changed, which a run asks about anew (`points withdraw`, #2888): who withdrew it, when and why. A withdrawn question is not open, and nobody answers it; asking it again opens it again.",
|
|
71
|
+
"required": ["by", "on"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"by": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "description": "Who withdrew it." },
|
|
74
|
+
"on": { "$ref": "#/definitions/date" },
|
|
75
|
+
"note": { "type": "string", "minLength": 1, "description": "Why it was withdrawn (`points withdraw --note`)." }
|
|
76
|
+
},
|
|
77
|
+
"additionalProperties": false
|
|
78
|
+
},
|
|
68
79
|
"asked_on": { "$ref": "#/definitions/date" },
|
|
69
80
|
"answered_on": { "$ref": "#/definitions/date" },
|
|
70
81
|
"source": {
|
|
@@ -85,6 +96,10 @@
|
|
|
85
96
|
"if": { "properties": { "state": { "const": "escalated" } }, "required": ["state"] },
|
|
86
97
|
"then": { "not": { "required": ["answer"] }, "properties": { "decider": { "properties": { "kind": { "const": "quorum" } } } } }
|
|
87
98
|
},
|
|
99
|
+
{
|
|
100
|
+
"if": { "required": ["withdrawn"] },
|
|
101
|
+
"then": { "properties": { "state": { "enum": ["escalated", "proposed"] } } }
|
|
102
|
+
},
|
|
88
103
|
{
|
|
89
104
|
"if": { "properties": { "decider": { "properties": { "kind": { "const": "model" } }, "required": ["kind"] } }, "required": ["decider"] },
|
|
90
105
|
"then": { "required": ["probabilities", "confidence", "threshold"], "properties": { "decider": { "required": ["backend", "model"] } } }
|
|
@@ -614,7 +614,7 @@ export function isReadCall(argv: readonly string[]): boolean {
|
|
|
614
614
|
case "runs":
|
|
615
615
|
return !["start", "end", "record"].includes(sub ?? "");
|
|
616
616
|
case "points":
|
|
617
|
-
return !["ask", "answer", "retract"].includes(sub ?? "");
|
|
617
|
+
return !["ask", "answer", "retract", "withdraw"].includes(sub ?? "");
|
|
618
618
|
case "work":
|
|
619
619
|
return sub === "history";
|
|
620
620
|
case "wip":
|