@intentius/chant 0.49.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +9 -0
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +6 -0
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +11 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/verbs/index.d.ts +6 -1
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +21 -14
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +93 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +4 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +29 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +42 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +14 -0
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +18 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/serializer.d.ts +18 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.ts +9 -0
- package/src/audit/discover.ts +29 -2
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +2 -0
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.ts +8 -3
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +215 -1
- package/src/cli/commands/audit.ts +86 -17
- package/src/cli/commands/build.test.ts +167 -2
- package/src/cli/commands/build.ts +114 -23
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +10 -1
- package/src/cli/handlers/lifecycle.ts +12 -4
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- package/src/cli/mcp/server.test.ts +265 -2
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/registry.ts +14 -2
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/verbs/index.ts +6 -1
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/composite.ts +31 -2
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +82 -19
- package/src/fold/subset.ts +79 -41
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +103 -2
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +4 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +45 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/evl001-non-literal-expression.test.ts +8 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +6 -6
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +59 -5
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +30 -0
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +45 -9
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +18 -1
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/serializer.ts +18 -0
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { parseOutputDocs, pick, get } from "./output-docs";
|
|
3
|
+
import type { SerializerResult } from "../serializer";
|
|
4
|
+
|
|
5
|
+
describe("parseOutputDocs", () => {
|
|
6
|
+
test("parses a single JSON output as one document", () => {
|
|
7
|
+
const outputs = new Map<string, string | SerializerResult>([
|
|
8
|
+
["aws", JSON.stringify({ AWSTemplateFormatVersion: "2010-09-09", Resources: {} })],
|
|
9
|
+
]);
|
|
10
|
+
const docs = parseOutputDocs(outputs);
|
|
11
|
+
expect(docs).toHaveLength(1);
|
|
12
|
+
expect(docs[0]).toMatchObject({
|
|
13
|
+
lexicon: "aws",
|
|
14
|
+
index: 0,
|
|
15
|
+
format: "json",
|
|
16
|
+
});
|
|
17
|
+
expect(docs[0].error).toBeUndefined();
|
|
18
|
+
expect((docs[0].value as { Resources: unknown }).Resources).toEqual({});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("splits multi-document YAML on `---` into one OutputDoc per document", () => {
|
|
22
|
+
const yaml = [
|
|
23
|
+
"apiVersion: v1",
|
|
24
|
+
"kind: Namespace",
|
|
25
|
+
"metadata:",
|
|
26
|
+
" name: ns-a",
|
|
27
|
+
"---",
|
|
28
|
+
"apiVersion: apps/v1",
|
|
29
|
+
"kind: Deployment",
|
|
30
|
+
"metadata:",
|
|
31
|
+
" name: web",
|
|
32
|
+
].join("\n");
|
|
33
|
+
const outputs = new Map<string, string | SerializerResult>([["k8s", yaml]]);
|
|
34
|
+
const docs = parseOutputDocs(outputs);
|
|
35
|
+
expect(docs).toHaveLength(2);
|
|
36
|
+
expect(docs[0]).toMatchObject({ lexicon: "k8s", index: 0, format: "yaml" });
|
|
37
|
+
expect(docs[1]).toMatchObject({ lexicon: "k8s", index: 1, format: "yaml" });
|
|
38
|
+
expect((docs[0].value as { kind: string }).kind).toBe("Namespace");
|
|
39
|
+
expect((docs[1].value as { kind: string }).kind).toBe("Deployment");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("handles a leading `---` document separator before any content", () => {
|
|
43
|
+
const yaml = ["---", "kind: Namespace", "---", "kind: Deployment"].join("\n");
|
|
44
|
+
const outputs = new Map<string, string | SerializerResult>([["k8s", yaml]]);
|
|
45
|
+
const docs = parseOutputDocs(outputs);
|
|
46
|
+
expect(docs).toHaveLength(2);
|
|
47
|
+
expect((docs[0].value as { kind: string }).kind).toBe("Namespace");
|
|
48
|
+
expect((docs[1].value as { kind: string }).kind).toBe("Deployment");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("drops empty documents between/around separators without erroring", () => {
|
|
52
|
+
const yaml = ["---", "", "---", "kind: Deployment", "---", ""].join("\n");
|
|
53
|
+
const outputs = new Map<string, string | SerializerResult>([["k8s", yaml]]);
|
|
54
|
+
const docs = parseOutputDocs(outputs);
|
|
55
|
+
expect(docs).toHaveLength(1);
|
|
56
|
+
expect((docs[0].value as { kind: string }).kind).toBe("Deployment");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("parses SerializerResult.files as additional, separately-indexed documents", () => {
|
|
60
|
+
const output: SerializerResult = {
|
|
61
|
+
primary: JSON.stringify({ kind: "root" }),
|
|
62
|
+
files: {
|
|
63
|
+
"nested.template.json": JSON.stringify({ kind: "nested-a" }),
|
|
64
|
+
"sidecar.yaml": "kind: nested-b\n---\nkind: nested-c",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const outputs = new Map<string, string | SerializerResult>([["aws", output]]);
|
|
68
|
+
const docs = parseOutputDocs(outputs);
|
|
69
|
+
|
|
70
|
+
const primaryDocs = docs.filter((d) => d.file === undefined);
|
|
71
|
+
expect(primaryDocs).toHaveLength(1);
|
|
72
|
+
expect((primaryDocs[0].value as { kind: string }).kind).toBe("root");
|
|
73
|
+
|
|
74
|
+
const nestedJson = docs.filter((d) => d.file === "nested.template.json");
|
|
75
|
+
expect(nestedJson).toHaveLength(1);
|
|
76
|
+
expect((nestedJson[0].value as { kind: string }).kind).toBe("nested-a");
|
|
77
|
+
|
|
78
|
+
// The sidecar YAML file is itself multi-document — index resets to 0
|
|
79
|
+
// within that file, since index is scoped to its own source.
|
|
80
|
+
const sidecarDocs = docs.filter((d) => d.file === "sidecar.yaml");
|
|
81
|
+
expect(sidecarDocs).toHaveLength(2);
|
|
82
|
+
expect(sidecarDocs[0].index).toBe(0);
|
|
83
|
+
expect(sidecarDocs[1].index).toBe(1);
|
|
84
|
+
expect((sidecarDocs[0].value as { kind: string }).kind).toBe("nested-b");
|
|
85
|
+
expect((sidecarDocs[1].value as { kind: string }).kind).toBe("nested-c");
|
|
86
|
+
|
|
87
|
+
expect(docs.every((d) => d.lexicon === "aws")).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("skips a source with no files entries and no primary content", () => {
|
|
91
|
+
const outputs = new Map<string, string | SerializerResult>([["empty", ""]]);
|
|
92
|
+
expect(parseOutputDocs(outputs)).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("parses every ctx.outputs entry, tagging each with its own lexicon key", () => {
|
|
96
|
+
const outputs = new Map<string, string | SerializerResult>([
|
|
97
|
+
["aws", JSON.stringify({ kind: "aws-thing" })],
|
|
98
|
+
["k8s", "kind: k8s-thing"],
|
|
99
|
+
]);
|
|
100
|
+
const docs = parseOutputDocs(outputs);
|
|
101
|
+
expect(docs.map((d) => d.lexicon).sort()).toEqual(["aws", "k8s"]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("malformed documents — marker, not throw", () => {
|
|
105
|
+
test("a bare scalar document is marked with an error instead of being treated as a manifest", () => {
|
|
106
|
+
// "42" is syntactically fine (valid YAML/JSON) but is a scalar, not an
|
|
107
|
+
// object/array — not something a manifest-shaped check can walk.
|
|
108
|
+
const yaml = ["kind: Deployment", "---", "42", "---", "kind: Service"].join("\n");
|
|
109
|
+
const outputs = new Map<string, string | SerializerResult>([["k8s", yaml]]);
|
|
110
|
+
const docs = parseOutputDocs(outputs);
|
|
111
|
+
|
|
112
|
+
expect(docs).toHaveLength(3);
|
|
113
|
+
expect(docs[0].error).toBeUndefined();
|
|
114
|
+
expect(docs[2].error).toBeUndefined();
|
|
115
|
+
|
|
116
|
+
// The malformed middle document is a marker: present, flagged, no value.
|
|
117
|
+
expect(docs[1].error).toBeDefined();
|
|
118
|
+
expect(docs[1].value).toBeUndefined();
|
|
119
|
+
expect(docs[1].format).toBe("yaml");
|
|
120
|
+
|
|
121
|
+
// Filtering it out is one line, and leaves the good documents intact.
|
|
122
|
+
const usable = docs.filter((d) => !d.error);
|
|
123
|
+
expect(usable).toHaveLength(2);
|
|
124
|
+
expect((usable[0].value as { kind: string }).kind).toBe("Deployment");
|
|
125
|
+
expect((usable[1].value as { kind: string }).kind).toBe("Service");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a lone scalar as the entire output is marked with an error, not thrown", () => {
|
|
129
|
+
// Valid JSON ("true" parses fine) but not an object/array — the
|
|
130
|
+
// whole-content JSON path hits the same isUsableDoc guard.
|
|
131
|
+
const outputs = new Map<string, string | SerializerResult>([["weird", "true"]]);
|
|
132
|
+
expect(() => parseOutputDocs(outputs)).not.toThrow();
|
|
133
|
+
const docs = parseOutputDocs(outputs);
|
|
134
|
+
expect(docs).toHaveLength(1);
|
|
135
|
+
expect(docs[0].format).toBe("json");
|
|
136
|
+
expect(docs[0].error).toBeDefined();
|
|
137
|
+
expect(docs[0].value).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("a malformed document in one files entry does not affect the others", () => {
|
|
141
|
+
const output: SerializerResult = {
|
|
142
|
+
primary: "kind: root",
|
|
143
|
+
files: {
|
|
144
|
+
"good.yaml": "kind: good",
|
|
145
|
+
"bad.yaml": "null",
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
const outputs = new Map<string, string | SerializerResult>([["k8s", output]]);
|
|
149
|
+
const docs = parseOutputDocs(outputs);
|
|
150
|
+
|
|
151
|
+
const good = docs.find((d) => d.file === "good.yaml")!;
|
|
152
|
+
const bad = docs.find((d) => d.file === "bad.yaml")!;
|
|
153
|
+
expect(good.error).toBeUndefined();
|
|
154
|
+
expect((good.value as { kind: string }).kind).toBe("good");
|
|
155
|
+
expect(bad.error).toBeDefined();
|
|
156
|
+
expect(bad.value).toBeUndefined();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("pick", () => {
|
|
162
|
+
interface Widget {
|
|
163
|
+
kind: string;
|
|
164
|
+
name: string;
|
|
165
|
+
extra: unknown;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
test("returns only the named keys that are present", () => {
|
|
169
|
+
const value = { kind: "Deployment", name: "web", other: "ignored" };
|
|
170
|
+
expect(pick<Widget>(value, ["kind", "name"])).toEqual({ kind: "Deployment", name: "web" });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("omits a named key that is absent — no invented default", () => {
|
|
174
|
+
const value = { kind: "Deployment" };
|
|
175
|
+
expect(pick<Widget>(value, ["kind", "name"])).toEqual({ kind: "Deployment" });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("returns an empty object for a non-object value", () => {
|
|
179
|
+
expect(pick<Widget>(null, ["kind"])).toEqual({});
|
|
180
|
+
expect(pick<Widget>("a string", ["kind"])).toEqual({});
|
|
181
|
+
expect(pick<Widget>(undefined, ["kind"])).toEqual({});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("copies values through unvalidated, whatever their actual shape", () => {
|
|
185
|
+
const value = { kind: 42 }; // wrong runtime type for `kind: string`
|
|
186
|
+
expect(pick<Widget>(value, ["kind"])).toEqual({ kind: 42 });
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("get", () => {
|
|
191
|
+
test("walks a dotted path through nested objects", () => {
|
|
192
|
+
const value = { spec: { template: { spec: { containers: [] } } } };
|
|
193
|
+
expect(get(value, "spec.template.spec")).toEqual({ containers: [] });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("returns undefined as soon as a segment is missing", () => {
|
|
197
|
+
const value = { spec: {} };
|
|
198
|
+
expect(get(value, "spec.template.spec")).toBeUndefined();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("returns undefined when an intermediate value is not indexable", () => {
|
|
202
|
+
const value = { spec: "not-an-object" };
|
|
203
|
+
expect(get(value, "spec.template")).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("indexes into arrays with a numeric path segment", () => {
|
|
207
|
+
const value = { items: [{ name: "first" }, { name: "second" }] };
|
|
208
|
+
expect(get(value, "items.1.name")).toBe("second");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("returns the value itself for an empty path", () => {
|
|
212
|
+
const value = { a: 1 };
|
|
213
|
+
expect(get(value, "")).toBe(value);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("does not throw on null/undefined input", () => {
|
|
217
|
+
expect(get(null, "a.b")).toBeUndefined();
|
|
218
|
+
expect(get(undefined, "a.b")).toBeUndefined();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsed-output view for post-synth checks (chant #975).
|
|
3
|
+
*
|
|
4
|
+
* `PostSynthContext.outputs` carries raw serialized strings — a `chant build`
|
|
5
|
+
* consumer has to parse YAML/JSON itself before it can reason about
|
|
6
|
+
* structure. Every lexicon that needs this today rolls its own splitter
|
|
7
|
+
* (`parseK8sManifests` in k8s, hand-rolled `JSON.parse` in aws's
|
|
8
|
+
* `cf-refs.ts`), and each of the ~30 k8s post-synth checks reparses the same
|
|
9
|
+
* output independently. This module is the shared, parse-once primitive:
|
|
10
|
+
* `parseOutputDocs` turns `ctx.outputs` into a flat list of `OutputDoc`s,
|
|
11
|
+
* and `PostSynthContext.docs` (see `./post-synth.ts`) memoizes one call to it
|
|
12
|
+
* per build so every check — lexicon-shipped or project-authored policy —
|
|
13
|
+
* shares the same parse.
|
|
14
|
+
*
|
|
15
|
+
* `pick`/`get` are the "look at only the fields you care about" ergonomic
|
|
16
|
+
* from the issue's `,remain`-style framing — a partial, unvalidated view of
|
|
17
|
+
* an already-parsed tree, not a query DSL. Selector languages over the
|
|
18
|
+
* *source* AST stay in `./declarative.ts`; this module is output-side only.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { SerializerResult } from "../serializer";
|
|
22
|
+
import { parseYAML } from "../yaml";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One parsed document from a build's serialized output.
|
|
26
|
+
*
|
|
27
|
+
* A single `ctx.outputs` entry can yield more than one `OutputDoc`: a
|
|
28
|
+
* multi-document YAML stream (`---`-separated) yields one per document, and
|
|
29
|
+
* a `SerializerResult` with `files` yields further docs for each nested
|
|
30
|
+
* file (a CloudFormation nested stack template, a sidecar manifest).
|
|
31
|
+
*/
|
|
32
|
+
export interface OutputDoc {
|
|
33
|
+
/** The `ctx.outputs` map key this document came from. */
|
|
34
|
+
lexicon: string;
|
|
35
|
+
/** Position of this document within its source (primary output or one
|
|
36
|
+
* `files` entry) — 0 for a single-document source, 0..n-1 for a
|
|
37
|
+
* multi-document YAML stream. */
|
|
38
|
+
index: number;
|
|
39
|
+
/** Whether this document was decoded as YAML or as whole-content JSON. */
|
|
40
|
+
format: "yaml" | "json";
|
|
41
|
+
/** The parsed tree. `undefined` when parsing failed — see `error`. */
|
|
42
|
+
value: unknown;
|
|
43
|
+
/**
|
|
44
|
+
* Set when this document came from a `SerializerResult.files` entry rather
|
|
45
|
+
* than the primary output — the file's key in that map (e.g. a nested
|
|
46
|
+
* stack template's filename).
|
|
47
|
+
*/
|
|
48
|
+
file?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Set when this document could not be parsed into a usable tree. The
|
|
51
|
+
* document is still returned (a marker, not a throw) so a caller can see
|
|
52
|
+
* that something in the build output failed to parse, rather than the
|
|
53
|
+
* failure disappearing silently the way `parseK8sManifests` has always
|
|
54
|
+
* swallowed it. `value` is `undefined` on an errored document. Filter with
|
|
55
|
+
* `ctx.docs.filter((d) => !d.error)` to get only usable documents.
|
|
56
|
+
*/
|
|
57
|
+
error?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse every output in `outputs` into a flat list of `OutputDoc`s: one call,
|
|
62
|
+
* shared by every post-synth check via `PostSynthContext.docs`.
|
|
63
|
+
*
|
|
64
|
+
* Format detection is whole-source, not per-document: if the entire source
|
|
65
|
+
* parses as JSON, it is one `"json"` document (CloudFormation, most non-k8s
|
|
66
|
+
* serializers). Otherwise the source is treated as a YAML stream — split on
|
|
67
|
+
* `---` document separators — and each resulting document is parsed as YAML
|
|
68
|
+
* (`format: "yaml"`), even one that happens to also be valid JSON, since
|
|
69
|
+
* JSON is a YAML subset. `SerializerResult.files` entries (nested templates,
|
|
70
|
+
* sidecar manifests) are parsed the same way, tagged with `file`.
|
|
71
|
+
*
|
|
72
|
+
* A document that fails to parse into a non-null object/array — malformed
|
|
73
|
+
* input, or a bare scalar where a manifest was expected — is not thrown:
|
|
74
|
+
* it is included with `error` set and `value: undefined`. See `OutputDoc`.
|
|
75
|
+
*/
|
|
76
|
+
export function parseOutputDocs(outputs: Map<string, string | SerializerResult>): OutputDoc[] {
|
|
77
|
+
const docs: OutputDoc[] = [];
|
|
78
|
+
for (const [lexicon, output] of outputs) {
|
|
79
|
+
const primary = typeof output === "string" ? output : output.primary;
|
|
80
|
+
docs.push(...parseSource(primary, lexicon));
|
|
81
|
+
|
|
82
|
+
const files = typeof output === "string" ? undefined : output.files;
|
|
83
|
+
for (const [file, content] of Object.entries(files ?? {})) {
|
|
84
|
+
docs.push(...parseSource(content, lexicon, file));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return docs;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Split a YAML stream on `---` document-separator lines (own line, optional
|
|
91
|
+
* trailing whitespace) — including a leading separator before any content,
|
|
92
|
+
* which `"\n---\n"`-style splitting misses. Empty documents (two separators
|
|
93
|
+
* back to back, or a leading/trailing one) are dropped. */
|
|
94
|
+
function splitYamlDocuments(source: string): string[] {
|
|
95
|
+
return source
|
|
96
|
+
.split(/^---[ \t]*$/m)
|
|
97
|
+
.map((doc) => doc.trim())
|
|
98
|
+
.filter((doc) => doc.length > 0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** True for a value a document is usable as — a non-null object or array.
|
|
102
|
+
* A bare scalar ("just a string") parses without error but is not a
|
|
103
|
+
* document a check can walk, so it is treated as malformed. */
|
|
104
|
+
function isUsableDoc(value: unknown): boolean {
|
|
105
|
+
return typeof value === "object" && value !== null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse one source string (a primary output, or one `files` entry) into its
|
|
110
|
+
* `OutputDoc`s, trying whole-content JSON first and falling back to a
|
|
111
|
+
* (possibly multi-document) YAML stream. Never throws — a parse failure or
|
|
112
|
+
* an unusable result becomes a marker document with `error` set.
|
|
113
|
+
*/
|
|
114
|
+
function parseSource(source: string, lexicon: string, file?: string): OutputDoc[] {
|
|
115
|
+
const trimmed = source.trim();
|
|
116
|
+
if (trimmed === "") return [];
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const value = JSON.parse(trimmed);
|
|
120
|
+
if (isUsableDoc(value)) {
|
|
121
|
+
return [{ lexicon, index: 0, format: "json", value, ...(file && { file }) }];
|
|
122
|
+
}
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
lexicon,
|
|
126
|
+
index: 0,
|
|
127
|
+
format: "json",
|
|
128
|
+
value: undefined,
|
|
129
|
+
error: "parsed JSON is not an object or array",
|
|
130
|
+
...(file && { file }),
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
} catch {
|
|
134
|
+
// Not whole-content JSON — fall through to the YAML stream path.
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return splitYamlDocuments(trimmed).map((segment, index) => {
|
|
138
|
+
try {
|
|
139
|
+
const value = parseYAML(segment);
|
|
140
|
+
if (isUsableDoc(value)) {
|
|
141
|
+
return { lexicon, index, format: "yaml" as const, value, ...(file && { file }) };
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
lexicon,
|
|
145
|
+
index,
|
|
146
|
+
format: "yaml" as const,
|
|
147
|
+
value: undefined,
|
|
148
|
+
error: "parsed YAML document is not an object or array",
|
|
149
|
+
...(file && { file }),
|
|
150
|
+
};
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return {
|
|
153
|
+
lexicon,
|
|
154
|
+
index,
|
|
155
|
+
format: "yaml" as const,
|
|
156
|
+
value: undefined,
|
|
157
|
+
error: err instanceof Error ? err.message : String(err),
|
|
158
|
+
...(file && { file }),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Partial typed decode — the `,remain`-style ergonomic from `ma91n/tfpolicy`:
|
|
166
|
+
* decode the fields you care about, leave everything else in place,
|
|
167
|
+
* unvalidated. Returns a shallow, top-level view of `value` narrowed to the
|
|
168
|
+
* keys named in `shape`. A key absent from `value` (or `value` not being a
|
|
169
|
+
* plain object) is simply absent from the result — no default is invented.
|
|
170
|
+
*
|
|
171
|
+
* This is NOT runtime validation: a key that IS present is copied as-is,
|
|
172
|
+
* whatever its actual shape, and cast to `T`'s declared type for that key.
|
|
173
|
+
* Use `get` for a deeper walk into one of the picked values.
|
|
174
|
+
*/
|
|
175
|
+
export function pick<T extends object>(value: unknown, shape: (keyof T)[]): Partial<T> {
|
|
176
|
+
const result: Partial<T> = {};
|
|
177
|
+
if (typeof value !== "object" || value === null) return result;
|
|
178
|
+
const source = value as Record<string, unknown>;
|
|
179
|
+
for (const key of shape) {
|
|
180
|
+
const propertyName = key as string;
|
|
181
|
+
if (propertyName in source) {
|
|
182
|
+
(result as Record<string, unknown>)[propertyName] = source[propertyName];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Dotted-path getter for the common walk over a parsed doc, e.g.
|
|
190
|
+
* `get(doc.value, "spec.template.spec")`. Not a query DSL — no wildcards,
|
|
191
|
+
* no predicates, no array-flattening. An array index is just a numeric path
|
|
192
|
+
* segment (`get(value, "items.0.name")`), since JS indexes arrays by string
|
|
193
|
+
* key underneath. Returns `undefined` as soon as any segment is missing or
|
|
194
|
+
* the value at that point isn't indexable — it never throws on a bad path.
|
|
195
|
+
*/
|
|
196
|
+
export function get(value: unknown, path: string): unknown {
|
|
197
|
+
if (path === "") return value;
|
|
198
|
+
let current: unknown = value;
|
|
199
|
+
for (const segment of path.split(".")) {
|
|
200
|
+
if (typeof current !== "object" || current === null) return undefined;
|
|
201
|
+
current = (current as Record<string, unknown>)[segment];
|
|
202
|
+
}
|
|
203
|
+
return current;
|
|
204
|
+
}
|
|
@@ -145,3 +145,100 @@ describe("isPostSynthCheck", () => {
|
|
|
145
145
|
// chant #1138 — `applyConfiguredSeverity` (the `lint.rules` severity-override
|
|
146
146
|
// pass over `PostSynthDiagnostic`s) is tested in `./config.test.ts`, where the
|
|
147
147
|
// function itself now lives — see `./config.ts`'s doc comment for why.
|
|
148
|
+
|
|
149
|
+
// ── chant #975 — ctx.docs: lazy, memoized, shared across every check ───────
|
|
150
|
+
describe("ctx.docs (chant #975)", () => {
|
|
151
|
+
/** A Map subclass that counts how many times it was iterated — the only
|
|
152
|
+
* way `parseOutputDocs` reads its outputs — so the tests below can prove
|
|
153
|
+
* "parsed once" without reaching into module internals. */
|
|
154
|
+
class CountingOutputs extends Map<string, string> {
|
|
155
|
+
iterations = 0;
|
|
156
|
+
[Symbol.iterator](): IterableIterator<[string, string]> {
|
|
157
|
+
this.iterations++;
|
|
158
|
+
return super[Symbol.iterator]();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
test("is not computed until first accessed", () => {
|
|
163
|
+
const outputs = new CountingOutputs([["k8s", "kind: Namespace"]]);
|
|
164
|
+
const check: PostSynthCheck = {
|
|
165
|
+
id: "PS-DOCS-1",
|
|
166
|
+
description: "never touches ctx.docs",
|
|
167
|
+
check() {
|
|
168
|
+
return [];
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
runPostSynthChecks([check], createBuildResult({ outputs: outputs as never }));
|
|
172
|
+
expect(outputs.iterations).toBe(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("is parsed exactly once even when read by multiple checks", () => {
|
|
176
|
+
const outputs = new CountingOutputs([
|
|
177
|
+
["k8s", "apiVersion: v1\nkind: Namespace\nmetadata:\n name: ns-a"],
|
|
178
|
+
]);
|
|
179
|
+
let seenByFirst: unknown;
|
|
180
|
+
let seenBySecond: unknown;
|
|
181
|
+
const checks: PostSynthCheck[] = [
|
|
182
|
+
{
|
|
183
|
+
id: "PS-DOCS-2A",
|
|
184
|
+
description: "reads ctx.docs once",
|
|
185
|
+
check(ctx) {
|
|
186
|
+
seenByFirst = ctx.docs;
|
|
187
|
+
return [];
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "PS-DOCS-2B",
|
|
192
|
+
description: "reads ctx.docs again, and a second time in the same check",
|
|
193
|
+
check(ctx) {
|
|
194
|
+
seenBySecond = ctx.docs;
|
|
195
|
+
void ctx.docs; // a second read within the same check — still no reparse
|
|
196
|
+
return [];
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
runPostSynthChecks(checks, createBuildResult({ outputs: outputs as never }));
|
|
201
|
+
expect(outputs.iterations).toBe(1);
|
|
202
|
+
// Every reader gets the exact same array instance, not an equal copy.
|
|
203
|
+
expect(seenByFirst).toBe(seenBySecond);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("parses ctx.outputs into the expected OutputDoc shape", () => {
|
|
207
|
+
const outputs = new Map<string, string>([
|
|
208
|
+
["k8s", "apiVersion: v1\nkind: Namespace\nmetadata:\n name: ns-a"],
|
|
209
|
+
]);
|
|
210
|
+
const check: PostSynthCheck = {
|
|
211
|
+
id: "PS-DOCS-3",
|
|
212
|
+
description: "reads a manifest field off ctx.docs",
|
|
213
|
+
check(ctx) {
|
|
214
|
+
const doc = (ctx.docs ?? [])[0];
|
|
215
|
+
return [
|
|
216
|
+
{
|
|
217
|
+
checkId: "PS-DOCS-3",
|
|
218
|
+
severity: "info",
|
|
219
|
+
message: `kind=${(doc.value as { kind?: string }).kind}`,
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
const diags = runPostSynthChecks([check], createBuildResult({ outputs: outputs as never }));
|
|
225
|
+
expect(diags[0].message).toBe("kind=Namespace");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("two independent runPostSynthChecks calls each get their own cache", () => {
|
|
229
|
+
const outputsA = new CountingOutputs([["k8s", "kind: Namespace"]]);
|
|
230
|
+
const outputsB = new CountingOutputs([["k8s", "kind: Deployment"]]);
|
|
231
|
+
const reader: PostSynthCheck = {
|
|
232
|
+
id: "PS-DOCS-4",
|
|
233
|
+
description: "reads ctx.docs",
|
|
234
|
+
check(ctx) {
|
|
235
|
+
void ctx.docs;
|
|
236
|
+
return [];
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
runPostSynthChecks([reader], createBuildResult({ outputs: outputsA as never }));
|
|
240
|
+
runPostSynthChecks([reader], createBuildResult({ outputs: outputsB as never }));
|
|
241
|
+
expect(outputsA.iterations).toBe(1);
|
|
242
|
+
expect(outputsB.iterations).toBe(1);
|
|
243
|
+
});
|
|
244
|
+
});
|
package/src/lint/post-synth.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Declarable } from "../declarable";
|
|
2
2
|
import type { SerializerResult } from "../serializer";
|
|
3
3
|
import type { Severity } from "./rule";
|
|
4
|
+
import { parseOutputDocs, type OutputDoc } from "./output-docs";
|
|
5
|
+
|
|
6
|
+
export { parseOutputDocs, pick, get, type OutputDoc } from "./output-docs";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Context provided to post-synthesis checks.
|
|
@@ -16,6 +19,25 @@ export interface PostSynthContext {
|
|
|
16
19
|
* e.g. "no public buckets in prod". Undefined when no environment is set.
|
|
17
20
|
*/
|
|
18
21
|
env?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Parsed output documents (chant #975) — `ctx.outputs` run through
|
|
24
|
+
* `parseOutputDocs` once and cached. A lazy `readonly` getter, not a plain
|
|
25
|
+
* field: computed on first access and shared across every check in the
|
|
26
|
+
* run, so a check that only reads `entities` pays nothing, and no two
|
|
27
|
+
* checks re-parse the same YAML/JSON. See `./output-docs.ts`.
|
|
28
|
+
*
|
|
29
|
+
* Optional at the type level — NOT because it can be absent from a real
|
|
30
|
+
* build. Every context chant itself constructs (`runPostSynthChecks`
|
|
31
|
+
* below, `@intentius/chant-test-utils`'s `createPostSynthContext` and
|
|
32
|
+
* `makePostSynthCtx*`) wires it up via `createDocsAccessor` and it is
|
|
33
|
+
* always present there. It is typed optional only so the many lexicon
|
|
34
|
+
* tests that build a `PostSynthContext` object literal by hand (predating
|
|
35
|
+
* this field) keep compiling unchanged, per this issue's own "existing
|
|
36
|
+
* checks compile unchanged" constraint — a new check that wants `ctx.docs`
|
|
37
|
+
* should still get a real array from every context chant builds; guard
|
|
38
|
+
* with `ctx.docs ?? []` only when a context's provenance is unknown.
|
|
39
|
+
*/
|
|
40
|
+
readonly docs?: OutputDoc[];
|
|
19
41
|
/** Raw build result object */
|
|
20
42
|
buildResult: {
|
|
21
43
|
outputs: Map<string, string | SerializerResult>;
|
|
@@ -26,6 +48,25 @@ export interface PostSynthContext {
|
|
|
26
48
|
};
|
|
27
49
|
}
|
|
28
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Build the lazy, memoized `docs` accessor shared by `runPostSynthChecks`
|
|
53
|
+
* (below) and `@intentius/chant-test-utils`'s `createPostSynthContext` — the
|
|
54
|
+
* two places a `PostSynthContext` gets constructed. Returns a zero-arg
|
|
55
|
+
* function suitable for a `get docs()` object-literal accessor; the first
|
|
56
|
+
* call parses, every later call returns the same cached array.
|
|
57
|
+
*/
|
|
58
|
+
export function createDocsAccessor(
|
|
59
|
+
outputs: Map<string, string | SerializerResult>,
|
|
60
|
+
): () => OutputDoc[] {
|
|
61
|
+
let cached: OutputDoc[] | undefined;
|
|
62
|
+
return () => {
|
|
63
|
+
if (cached === undefined) {
|
|
64
|
+
cached = parseOutputDocs(outputs);
|
|
65
|
+
}
|
|
66
|
+
return cached;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
29
70
|
/**
|
|
30
71
|
* Extract the primary content string from a serializer output.
|
|
31
72
|
*/
|
|
@@ -115,11 +156,15 @@ export function runPostSynthChecks(
|
|
|
115
156
|
buildResult: PostSynthContext["buildResult"],
|
|
116
157
|
env?: string,
|
|
117
158
|
): PostSynthDiagnostic[] {
|
|
159
|
+
const getDocs = createDocsAccessor(buildResult.outputs);
|
|
118
160
|
const ctx: PostSynthContext = {
|
|
119
161
|
outputs: buildResult.outputs,
|
|
120
162
|
entities: buildResult.entities,
|
|
121
163
|
env,
|
|
122
164
|
buildResult,
|
|
165
|
+
get docs(): OutputDoc[] {
|
|
166
|
+
return getDocs();
|
|
167
|
+
},
|
|
123
168
|
};
|
|
124
169
|
|
|
125
170
|
const diagnostics: PostSynthDiagnostic[] = [];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Component } from "../../../../../../components/component";
|
|
2
|
+
import { phase } from "../../../../../../components/component";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* COMP003 pass case (#1944): a bare "run-agent" step with no "noRollback"
|
|
6
|
+
* opt-out and no component-level "rollback" — must pass COMP003 because
|
|
7
|
+
* "run-agent"'s rollbackPolicy is "native" (#1941), not "needs-opt-out". This
|
|
8
|
+
* is the regression test proving the registry's declared rollbackPolicy for
|
|
9
|
+
* "run-agent" is wired correctly into ctx.rollbackPolicies, the same seam
|
|
10
|
+
* COMP005 uses for ctx.knownKinds — see comp.test.ts's FIXTURE_ROLLBACK_POLICIES.
|
|
11
|
+
*/
|
|
12
|
+
export const agentTurn: Component = {
|
|
13
|
+
name: "agent-turn",
|
|
14
|
+
archetype: "infra",
|
|
15
|
+
dependsOn: [],
|
|
16
|
+
deploy: [
|
|
17
|
+
phase("Run", [
|
|
18
|
+
{
|
|
19
|
+
kind: "run-agent",
|
|
20
|
+
agent: "claude",
|
|
21
|
+
task: { prompt: "run the migration script and report the result" },
|
|
22
|
+
workspace: {},
|
|
23
|
+
},
|
|
24
|
+
]),
|
|
25
|
+
],
|
|
26
|
+
};
|