@intentius/chant 0.46.0 → 0.49.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/core.d.ts +21 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +3 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +8 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +15 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +44 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +6 -4
- package/dist/lifecycle/teardown.d.ts.map +1 -1
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +12 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/builders.d.ts +98 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/types.d.ts +31 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +11 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +11 -2
- package/src/audit/rules-doc.ts +11 -1
- package/src/build.test.ts +41 -0
- package/src/build.ts +34 -6
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/audit.test.ts +53 -0
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.test.ts +80 -0
- package/src/cli/commands/build.ts +106 -8
- package/src/cli/commands/lint.ts +15 -3
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.ts +2 -2
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +84 -11
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +58 -1
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/plugins.ts +4 -2
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/components/auto-release.ts +6 -0
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +8 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/config.test.ts +15 -0
- package/src/config.ts +30 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/fold/subset.test.ts +26 -0
- package/src/fold/subset.ts +45 -19
- package/src/index.ts +4 -0
- package/src/lexicon.ts +48 -3
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/index.ts +2 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/teardown.test.ts +31 -0
- package/src/lifecycle/teardown.ts +6 -4
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.ts +15 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +27 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +8 -1
- package/src/lint/rules/index.ts +7 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/builders.ts +139 -1
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/index.ts +10 -3
- package/src/op/local-executor.ts +172 -25
- package/src/op/op.test.ts +25 -2
- package/src/op/receipt-store.ts +211 -0
- package/src/op/types.ts +33 -1
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +12 -0
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -104,6 +104,33 @@ describe("EVL001: non-literal-expression", () => {
|
|
|
104
104
|
expect(diags[0].ruleId).toBe("EVL001");
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
// chant #1544 — the single-action Composite() wrapper idiom
|
|
108
|
+
// (`Checkout({...}).step`) every lexicon's own docs/examples embed inline
|
|
109
|
+
// inside a `Job`'s `steps:` array must not trip EVL001.
|
|
110
|
+
test("allows a composite call's .step access embedded in an array", () => {
|
|
111
|
+
const ctx = createContext(`new Job({ "runs-on": "ubuntu-latest", steps: [Checkout({}).step] });`);
|
|
112
|
+
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
113
|
+
expect(diags).toHaveLength(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("allows a composite call's .step access as a direct property value", () => {
|
|
117
|
+
const ctx = createContext(`new Job({ step: Checkout({ ref: "main" }).step });`);
|
|
118
|
+
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
119
|
+
expect(diags).toHaveLength(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("still flags a call accessing a non-.step property", () => {
|
|
123
|
+
const ctx = createContext(`new Job({ steps: [Checkout({}).output] });`);
|
|
124
|
+
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
125
|
+
expect(diags).toHaveLength(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("still flags a bare call (no trailing .step access)", () => {
|
|
129
|
+
const ctx = createContext(`new Bucket({ name: getName() });`);
|
|
130
|
+
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
131
|
+
expect(diags).toHaveLength(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
107
134
|
test("flags multiple violations", () => {
|
|
108
135
|
const ctx = createContext(`new Bucket({ a: foo(), b: bar() });`);
|
|
109
136
|
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
@@ -31,7 +31,14 @@ function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnos
|
|
|
31
31
|
const firstArg = node.arguments[0];
|
|
32
32
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
33
33
|
for (const prop of firstArg.properties) {
|
|
34
|
-
|
|
34
|
+
// chant #1544 — `allowCompositeStepAccess: true` opts EVL001 (and
|
|
35
|
+
// only EVL001; `fold()` never sets this) into treating
|
|
36
|
+
// `Checkout({...}).step` — the single-action Composite()-wrapper
|
|
37
|
+
// idiom every lexicon's own docs/examples embed inline inside a
|
|
38
|
+
// Job's `steps:` array — as shape-valid. See findSubsetViolation's
|
|
39
|
+
// doc comment (../../fold/subset.ts) for why this is a documented,
|
|
40
|
+
// EVL-only, more-permissive divergence rather than a shared one.
|
|
41
|
+
const violation = checkObjectMember(prop, context.intrinsics, true);
|
|
35
42
|
if (violation) {
|
|
36
43
|
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
37
44
|
violation.node.getStart(context.sourceFile),
|
package/src/lint/rules/index.ts
CHANGED
|
@@ -26,6 +26,8 @@ export { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform"
|
|
|
26
26
|
export { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
|
|
27
27
|
export { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
|
|
28
28
|
export { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
|
|
29
|
+
export { cor022ReceiptLeafRule, collectReceiptVariables, receiptFactoryCall } from "./cor022-receipt-leaf";
|
|
30
|
+
export { cor024ReceiptSecretPointerRule } from "./cor024-receipt-secret-pointer";
|
|
29
31
|
export { isInsideCompositeFactory } from "./composite-scope";
|
|
30
32
|
|
|
31
33
|
import { flatDeclarationsRule } from "./flat-declarations";
|
|
@@ -50,9 +52,11 @@ import { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform"
|
|
|
50
52
|
import { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
|
|
51
53
|
import { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
|
|
52
54
|
import { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
|
|
55
|
+
import { cor022ReceiptLeafRule } from "./cor022-receipt-leaf";
|
|
56
|
+
import { cor024ReceiptSecretPointerRule } from "./cor024-receipt-secret-pointer";
|
|
53
57
|
|
|
54
58
|
/**
|
|
55
|
-
* Load all
|
|
59
|
+
* Load all 24 core lint rules (COR + EVL).
|
|
56
60
|
*/
|
|
57
61
|
export function loadCoreRules(): LintRule[] {
|
|
58
62
|
return [
|
|
@@ -78,5 +82,7 @@ export function loadCoreRules(): LintRule[] {
|
|
|
78
82
|
cor017CompositeNameMatchRule,
|
|
79
83
|
cor018CompositePreferLexiconTypeRule,
|
|
80
84
|
cor021EnvLiteralNameRule,
|
|
85
|
+
cor022ReceiptLeafRule,
|
|
86
|
+
cor024ReceiptSecretPointerRule,
|
|
81
87
|
];
|
|
82
88
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { loadOkfBundle, bindConcepts } from "./okf-read";
|
|
5
|
+
import { buildOkfBundle } from "./okf";
|
|
6
|
+
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
7
|
+
|
|
8
|
+
function decl<T extends object>(base: T): Declarable & T {
|
|
9
|
+
return { [DECLARABLE_MARKER]: true, ...base } as Declarable & T;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const TEST_DIR = join(import.meta.dirname, "__test_okf_read__");
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function write(relPath: string, content: string): void {
|
|
23
|
+
const full = join(TEST_DIR, relPath);
|
|
24
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
25
|
+
writeFileSync(full, content);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("loadOkfBundle", () => {
|
|
29
|
+
test("a missing directory yields an empty bundle, not an error", async () => {
|
|
30
|
+
const bundle = await loadOkfBundle(join(TEST_DIR, "does-not-exist"));
|
|
31
|
+
expect(bundle.concepts).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("skips index.md and log.md, at any depth", async () => {
|
|
35
|
+
write("index.md", "---\nokf_version: '0.2'\n---\n");
|
|
36
|
+
write("log.md", "---\ntype: log\n---\nsome log\n");
|
|
37
|
+
write("nested/log.md", "---\ntype: log\n---\nnested log\n");
|
|
38
|
+
write("decisions/keep.md", "---\ntype: decision\ntitle: Keep me\n---\nbody\n");
|
|
39
|
+
|
|
40
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
41
|
+
expect(bundle.concepts.map((c) => c.path)).toEqual(["decisions/keep.md"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("passes through unknown type values and extra frontmatter keys", async () => {
|
|
45
|
+
write("weird.md", "---\ntype: some-unknown-type\ncustom_field: hello\n---\nbody text\n");
|
|
46
|
+
|
|
47
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
48
|
+
expect(bundle.concepts).toHaveLength(1);
|
|
49
|
+
const [concept] = bundle.concepts;
|
|
50
|
+
expect(concept.type).toBe("some-unknown-type");
|
|
51
|
+
expect(concept.frontmatter.custom_field).toBe("hello");
|
|
52
|
+
expect(concept.body).toContain("body text");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("skips a file with unparseable frontmatter, warns, and does not affect other files", async () => {
|
|
56
|
+
write("broken.md", "not frontmatter at all\n");
|
|
57
|
+
write("fine.md", "---\ntype: decision\ntitle: Fine\n---\nbody\n");
|
|
58
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
59
|
+
|
|
60
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
61
|
+
|
|
62
|
+
expect(bundle.concepts.map((c) => c.path)).toEqual(["fine.md"]);
|
|
63
|
+
expect(warn).toHaveBeenCalled();
|
|
64
|
+
warn.mockRestore();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("never rejects the bundle for a malformed file — an empty frontmatter block loads with an empty type", async () => {
|
|
68
|
+
write("empty-front.md", "---\n---\nbody\n");
|
|
69
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
70
|
+
expect(bundle.concepts).toHaveLength(1);
|
|
71
|
+
expect(bundle.concepts[0].type).toBe("");
|
|
72
|
+
expect(bundle.concepts[0].binds).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("binds accepts a single name", async () => {
|
|
76
|
+
write("single.md", "---\ntype: decision\nbinds: myBucket\n---\nbody\n");
|
|
77
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
78
|
+
expect(bundle.concepts[0].binds).toEqual(["myBucket"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("binds accepts a list", async () => {
|
|
82
|
+
write("multi.md", "---\ntype: decision\nbinds:\n - myBucket\n - vpc\n---\nbody\n");
|
|
83
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
84
|
+
expect(bundle.concepts[0].binds).toEqual(["myBucket", "vpc"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a concept without binds loads as unbound and is legitimate", async () => {
|
|
88
|
+
write("orphan.md", "---\ntype: runbook\ntitle: Incident response\n---\nbody\n");
|
|
89
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
90
|
+
expect(bundle.concepts[0].binds).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("bindConcepts", () => {
|
|
95
|
+
test("returns bound concepts per entity name plus unresolved bindings", async () => {
|
|
96
|
+
write("bound.md", "---\ntype: decision\ntitle: Bound\nbinds: myBucket\n---\nbody\n");
|
|
97
|
+
write("multi.md", "---\ntype: decision\ntitle: Spans two\nbinds:\n - myBucket\n - vpc\n---\nbody\n");
|
|
98
|
+
write("stale.md", "---\ntype: decision\ntitle: Stale\nbinds: ghost\n---\nbody\n");
|
|
99
|
+
write("orphan.md", "---\ntype: runbook\ntitle: Orphan\n---\nbody\n");
|
|
100
|
+
|
|
101
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
102
|
+
const entities = new Map<string, Declarable>([
|
|
103
|
+
["myBucket", decl({ lexicon: "aws", entityType: "AWS::S3::Bucket" })],
|
|
104
|
+
["vpc", decl({ lexicon: "gcp", entityType: "Vpc" })],
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const { bound, unresolved } = bindConcepts(bundle, entities);
|
|
108
|
+
|
|
109
|
+
expect(bound.get("myBucket")?.map((c) => c.title)).toEqual(["Bound", "Spans two"]);
|
|
110
|
+
expect(bound.get("vpc")?.map((c) => c.title)).toEqual(["Spans two"]);
|
|
111
|
+
expect(bound.has("ghost")).toBe(false);
|
|
112
|
+
expect(unresolved).toHaveLength(1);
|
|
113
|
+
expect(unresolved[0].name).toBe("ghost");
|
|
114
|
+
expect(unresolved[0].concept.title).toBe("Stale");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("an entity with no bound concepts is absent from the map, never present with []", async () => {
|
|
118
|
+
const bundle = await loadOkfBundle(join(TEST_DIR, "empty"));
|
|
119
|
+
const entities = new Map<string, Declarable>([["vpc", decl({ lexicon: "gcp", entityType: "Vpc" })]]);
|
|
120
|
+
const { bound, unresolved } = bindConcepts(bundle, entities);
|
|
121
|
+
expect(bound.size).toBe(0);
|
|
122
|
+
expect(unresolved).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("round trip with buildOkfBundle (#1058) — the emitter never writes binds", () => {
|
|
127
|
+
test("reading back an emitted bundle yields zero bindings", async () => {
|
|
128
|
+
const bucket = decl({ lexicon: "aws", entityType: "AWS::S3::Bucket" });
|
|
129
|
+
const vpc = decl({ lexicon: "gcp", entityType: "Vpc" });
|
|
130
|
+
const entities = new Map<string, Declarable>([
|
|
131
|
+
["myBucket", bucket],
|
|
132
|
+
["vpc", vpc],
|
|
133
|
+
]);
|
|
134
|
+
const dependencies = new Map([["myBucket", new Set(["vpc"])]]);
|
|
135
|
+
|
|
136
|
+
for (const file of buildOkfBundle({ entities, dependencies })) {
|
|
137
|
+
write(file.path, file.content);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const bundle = await loadOkfBundle(TEST_DIR);
|
|
141
|
+
// index.md is reserved and skipped; the two entity concepts load.
|
|
142
|
+
expect(bundle.concepts.map((c) => c.path).sort()).toEqual(["aws/myBucket.md", "gcp/vpc.md"]);
|
|
143
|
+
expect(bundle.concepts.every((c) => c.binds.length === 0)).toBe(true);
|
|
144
|
+
|
|
145
|
+
const { bound, unresolved } = bindConcepts(bundle, entities);
|
|
146
|
+
expect(bound.size).toBe(0);
|
|
147
|
+
expect(unresolved).toEqual([]);
|
|
148
|
+
});
|
|
149
|
+
});
|
package/src/okf-read.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join, relative, sep } from "node:path";
|
|
4
|
+
import type { Declarable } from "./declarable";
|
|
5
|
+
import { splitFrontmatter } from "./okf";
|
|
6
|
+
import { parseYAML } from "./yaml";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OKF bundle reader (#1864, design #1059, epic #1057) — the input half of
|
|
10
|
+
* `./okf.ts`'s emitter: loads a project-authored knowledge bundle from disk
|
|
11
|
+
* and binds its concepts to discovered entities.
|
|
12
|
+
*
|
|
13
|
+
* Tolerant to the letter of the OKF v0.2 spec (§11), the same posture
|
|
14
|
+
* `okfConformanceProblems` enforces on the emit side: unknown `type` values
|
|
15
|
+
* and extra frontmatter keys pass through untouched, a file with unparseable
|
|
16
|
+
* or missing frontmatter is skipped with a warning, and nothing here ever
|
|
17
|
+
* rejects the bundle — a missing knowledge directory included.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const RESERVED_OKF_FILES = new Set(["index.md", "log.md"]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One authored concept document, parsed from a bundle. Deliberately thinner
|
|
24
|
+
* than the emit side's internal `Concept` (`./okf.ts`) — this is what a
|
|
25
|
+
* *reader* trusts, and per the #1059 design that is `type`/`title` plus the
|
|
26
|
+
* single frontmatter key chant interprets, `binds`. Everything else an
|
|
27
|
+
* author wrote rides along in {@link frontmatter} unexamined, so a consumer
|
|
28
|
+
* that needs a project-specific key (a future `category`, say) can still
|
|
29
|
+
* reach it without a reader change.
|
|
30
|
+
*/
|
|
31
|
+
export interface OkfConcept {
|
|
32
|
+
/** Bundle-relative path (forward-slash separated), e.g. "decisions/public-assets.md". */
|
|
33
|
+
path: string;
|
|
34
|
+
/** The frontmatter `type`. May be empty — the reader never rejects a concept for it, unlike the emitter's conformance check. */
|
|
35
|
+
type: string;
|
|
36
|
+
/** Frontmatter `title`, when the author set one. */
|
|
37
|
+
title?: string;
|
|
38
|
+
/**
|
|
39
|
+
* This concept's `binds` frontmatter key, normalized to a list — one name,
|
|
40
|
+
* several, or `[]` for "declared no binding, and that's legitimate."
|
|
41
|
+
* Structural-looking keys (`name`, `lexicon`, `kind`) are deliberately
|
|
42
|
+
* *not* surfaced here or anywhere else on this type: the reader interprets
|
|
43
|
+
* exactly `binds` (#1059's precedence-by-construction), so authored and
|
|
44
|
+
* inferred knowledge cannot conflict at the fact level.
|
|
45
|
+
*/
|
|
46
|
+
binds: string[];
|
|
47
|
+
/** Every frontmatter key this concept's document carries, `binds` included, verbatim. */
|
|
48
|
+
frontmatter: Record<string, unknown>;
|
|
49
|
+
/** Markdown body (everything after the closing `---`), unchanged. */
|
|
50
|
+
body: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A loaded OKF knowledge bundle: every authored concept found, reserved files already excluded. */
|
|
54
|
+
export interface OkfBundle {
|
|
55
|
+
concepts: OkfConcept[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `binds` accepts one name or a list (#1059); anything else is treated as "no binding" rather than rejecting the concept. */
|
|
59
|
+
function normalizeBinds(value: unknown): string[] {
|
|
60
|
+
if (typeof value === "string") return value.length > 0 ? [value] : [];
|
|
61
|
+
if (Array.isArray(value)) return value.filter((v): v is string => typeof v === "string" && v.length > 0);
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Recursively collect every `.md` file under `dir`, as bundle-relative (forward-slash) paths paired with their absolute path. */
|
|
66
|
+
async function findMarkdownFiles(dir: string): Promise<Array<{ relPath: string; fullPath: string }>> {
|
|
67
|
+
const found: Array<{ relPath: string; fullPath: string }> = [];
|
|
68
|
+
|
|
69
|
+
async function scan(current: string): Promise<void> {
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
73
|
+
} catch {
|
|
74
|
+
return; // unreadable directory: silently skip, same posture as findInfraFiles
|
|
75
|
+
}
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const fullPath = join(current, entry.name);
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
await scan(fullPath);
|
|
80
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
81
|
+
const relPath = relative(dir, fullPath).split(sep).join("/");
|
|
82
|
+
found.push({ relPath, fullPath });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await scan(dir);
|
|
88
|
+
return found;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load every authored concept from an OKF knowledge bundle directory.
|
|
93
|
+
* `index.md`/`log.md` are skipped (reserved, by basename, at any depth —
|
|
94
|
+
* matching `okfConformanceProblems`'s check on the emit side). A directory
|
|
95
|
+
* that does not exist yields an empty bundle, not an error: the #1059 design
|
|
96
|
+
* treats a missing `knowledge/` as "this project has none yet," the same
|
|
97
|
+
* posture as a project with no lexicons configured.
|
|
98
|
+
*
|
|
99
|
+
* A file that fails to parse — no frontmatter block, unparseable YAML, or
|
|
100
|
+
* frontmatter that isn't a mapping — is skipped with a `console.warn` and
|
|
101
|
+
* does not affect any other file. Nothing here throws.
|
|
102
|
+
*/
|
|
103
|
+
export async function loadOkfBundle(dir: string): Promise<OkfBundle> {
|
|
104
|
+
if (!existsSync(dir)) return { concepts: [] };
|
|
105
|
+
|
|
106
|
+
const concepts: OkfConcept[] = [];
|
|
107
|
+
for (const { relPath, fullPath } of await findMarkdownFiles(dir)) {
|
|
108
|
+
if (RESERVED_OKF_FILES.has(relPath.split("/").pop()!)) continue;
|
|
109
|
+
|
|
110
|
+
let content: string;
|
|
111
|
+
try {
|
|
112
|
+
content = await readFile(fullPath, "utf8");
|
|
113
|
+
} catch {
|
|
114
|
+
console.warn(`[chant] warning: knowledge bundle concept "${relPath}" could not be read, skipping`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const split = splitFrontmatter(content);
|
|
119
|
+
if (!split) {
|
|
120
|
+
console.warn(`[chant] warning: knowledge bundle concept "${relPath}" has no frontmatter block, skipping`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let parsed: unknown;
|
|
125
|
+
try {
|
|
126
|
+
parsed = parseYAML(split.frontmatter);
|
|
127
|
+
} catch {
|
|
128
|
+
console.warn(`[chant] warning: knowledge bundle concept "${relPath}" has unparseable frontmatter, skipping`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
132
|
+
console.warn(`[chant] warning: knowledge bundle concept "${relPath}" frontmatter is not a mapping, skipping`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const frontmatter = parsed as Record<string, unknown>;
|
|
137
|
+
concepts.push({
|
|
138
|
+
path: relPath,
|
|
139
|
+
type: typeof frontmatter.type === "string" ? frontmatter.type : "",
|
|
140
|
+
title: typeof frontmatter.title === "string" ? frontmatter.title : undefined,
|
|
141
|
+
binds: normalizeBinds(frontmatter.binds),
|
|
142
|
+
frontmatter,
|
|
143
|
+
body: split.body,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
concepts.sort((a, b) => a.path.localeCompare(b.path));
|
|
148
|
+
return { concepts };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** {@link bindConcepts}'s result: bound concepts per entity, plus every binding that resolved nothing. */
|
|
152
|
+
export interface BindConceptsResult {
|
|
153
|
+
/**
|
|
154
|
+
* Bound concepts keyed by entity logical name — the same names that key
|
|
155
|
+
* `DiscoveryResult.entities` (`./discovery/index.ts`). An entity with no
|
|
156
|
+
* bound concepts is absent from the map, never present with `[]`.
|
|
157
|
+
*/
|
|
158
|
+
bound: Map<string, OkfConcept[]>;
|
|
159
|
+
/** Every `binds` entry that named no entity in `entities` — chant's own broken-link posture (#1059): a warning for the caller to surface (COR022, #1865), never a load failure. */
|
|
160
|
+
unresolved: UnresolvedBinding[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** One `binds` entry that resolved to nothing. */
|
|
164
|
+
export interface UnresolvedBinding {
|
|
165
|
+
/** The concept whose `binds` named this entity. */
|
|
166
|
+
concept: OkfConcept;
|
|
167
|
+
/** The unresolved logical name. */
|
|
168
|
+
name: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolve every concept's `binds` against a project's discovered entities.
|
|
173
|
+
* `entities` is the same shape `./okf.ts`'s emitter takes, `DiscoveryResult`'s
|
|
174
|
+
* own `entities` map — a bare logical name is unambiguous within one
|
|
175
|
+
* discovery pass (#1059's "Rejected options" on binding ambiguity).
|
|
176
|
+
*
|
|
177
|
+
* A concept with no `binds` contributes to neither `bound` nor `unresolved`;
|
|
178
|
+
* an unbound concept is legitimate, not an omission (a runbook, a decision
|
|
179
|
+
* about the project as a whole).
|
|
180
|
+
*/
|
|
181
|
+
export function bindConcepts(bundle: OkfBundle, entities: Map<string, Declarable>): BindConceptsResult {
|
|
182
|
+
const bound = new Map<string, OkfConcept[]>();
|
|
183
|
+
const unresolved: UnresolvedBinding[] = [];
|
|
184
|
+
|
|
185
|
+
for (const concept of bundle.concepts) {
|
|
186
|
+
for (const name of concept.binds) {
|
|
187
|
+
if (entities.has(name)) {
|
|
188
|
+
if (!bound.has(name)) bound.set(name, []);
|
|
189
|
+
bound.get(name)!.push(concept);
|
|
190
|
+
} else {
|
|
191
|
+
unresolved.push({ concept, name });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { bound, unresolved };
|
|
197
|
+
}
|
package/src/op/builders.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { OpResource } from "./resource";
|
|
2
|
-
import type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep } from "./types";
|
|
2
|
+
import type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep, EffectStep } from "./types";
|
|
3
|
+
import { isEffectReceipt, type EffectReceiptDeclaration } from "../effect-receipt";
|
|
4
|
+
import { receiptCheckInput } from "./receipt-store";
|
|
3
5
|
|
|
4
6
|
// ── Core builders ─────────────────────────────────────────────────────────────
|
|
5
7
|
|
|
@@ -58,6 +60,62 @@ export function gate(
|
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Wrap nested steps in read-compare-run-write over an effect receipt (#1834,
|
|
65
|
+
* epic #1703). At run: read the live receipt through the receipt store,
|
|
66
|
+
* compare it against the resolved expectation, and on a match skip the nested
|
|
67
|
+
* steps ("effect already applied"). On a mismatch the nested steps run in
|
|
68
|
+
* authored order, and ONLY when every one of them succeeds is the receipt
|
|
69
|
+
* written — last, once. This step is the sole writer of a receipt (decision
|
|
70
|
+
* 3); a nested-step failure leaves the receipt untouched (stale), so the next
|
|
71
|
+
* run re-proposes the effect.
|
|
72
|
+
*
|
|
73
|
+
* `receipt` is the typed EffectReceipt declaration — import the const from
|
|
74
|
+
* your receipts module. There is no string form: a receipt named by string
|
|
75
|
+
* would sever the step from the declaration that lint, plan, and the lexicon
|
|
76
|
+
* row all key on.
|
|
77
|
+
*
|
|
78
|
+
* A gate authored inside `steps` pauses only when the effect will fire (the
|
|
79
|
+
* matched path never reaches it). The receipt-store activities (`receiptRead`,
|
|
80
|
+
* `receiptWrite`) are provided by the receipt row's lexicon (#1835, aws).
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* import { seededReceipt } from "./receipts";
|
|
85
|
+
* phase("Seed", [
|
|
86
|
+
* effect(seededReceipt, [
|
|
87
|
+
* shell("npm run db:seed"),
|
|
88
|
+
* ]),
|
|
89
|
+
* ]),
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function effect(
|
|
93
|
+
receipt: EffectReceiptDeclaration,
|
|
94
|
+
steps: Array<ActivityStep | GateStep>,
|
|
95
|
+
opts?: { description?: string },
|
|
96
|
+
): EffectStep {
|
|
97
|
+
if (!isEffectReceipt(receipt)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"effect(): `receipt` must be the EffectReceipt declaration itself — import the const; there is no string form",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
for (const step of steps) {
|
|
103
|
+
if ((step as { kind?: unknown }).kind === "effect") {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`effect("${receipt.name}"): effect steps do not nest — one receipt witnesses one effect`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const { receipt: ref, expectation } = receiptCheckInput(receipt);
|
|
110
|
+
return {
|
|
111
|
+
kind: "effect",
|
|
112
|
+
receipt: ref,
|
|
113
|
+
...(expectation !== undefined ? { expectation } : {}),
|
|
114
|
+
steps,
|
|
115
|
+
...(opts?.description ? { description: opts.description } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
61
119
|
// ── Pre-built activity shortcuts ──────────────────────────────────────────────
|
|
62
120
|
|
|
63
121
|
/**
|
|
@@ -100,6 +158,22 @@ export const helmInstall = (
|
|
|
100
158
|
return activity("helmInstall", { name, chart, ...args }, profile ?? "longInfra");
|
|
101
159
|
};
|
|
102
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Deploy a recorded pinned render by its `sha256:` content digest (chant
|
|
163
|
+
* #1242): the helm lexicon's `helmInstall` activity loads the render from
|
|
164
|
+
* the render store, verifies the digest, and installs those exact bytes as
|
|
165
|
+
* a structure-preserving wrapper chart — no deploy-time render. Defaults to
|
|
166
|
+
* the `longInfra` profile (override via `opts.profile`).
|
|
167
|
+
*/
|
|
168
|
+
export const helmInstallPinned = (
|
|
169
|
+
name: string,
|
|
170
|
+
contentDigest: string,
|
|
171
|
+
opts?: { namespace?: string; profile?: ActivityStep["profile"]; [k: string]: unknown },
|
|
172
|
+
): ActivityStep => {
|
|
173
|
+
const { args, profile } = takeProfile(opts);
|
|
174
|
+
return activity("helmInstall", { name, contentDigest, ...args }, profile ?? "longInfra");
|
|
175
|
+
};
|
|
176
|
+
|
|
103
177
|
/** Poll for stack readiness (kubectl rollout, CloudFormation complete, etc). Defaults to the `k8sWait` profile (override via `opts.profile`). */
|
|
104
178
|
export const waitForStack = (name: string, opts?: Record<string, unknown>): ActivityStep => {
|
|
105
179
|
const { args, profile } = takeProfile(opts);
|
|
@@ -133,6 +207,28 @@ export const shell = (
|
|
|
133
207
|
): ActivityStep =>
|
|
134
208
|
activity("shellCmd", { cmd, ...(opts?.env ? { env: opts.env } : {}) }, opts?.profile);
|
|
135
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Ensure a `generated-once` secret exists in the target store (#1829, epic
|
|
212
|
+
* #1365). The op-step surface over the same engine as the `ensure-secret`
|
|
213
|
+
* capability verb (`ensureSecretMaterialization`, core's
|
|
214
|
+
* secret-materialization module): read-then-write — if the secret exists, its
|
|
215
|
+
* declared key-set and any declared `metadata` are verified and the step
|
|
216
|
+
* stops (present means done); if absent, one value per key is minted at
|
|
217
|
+
* apply time and written straight to the store. Never mints over an existing
|
|
218
|
+
* value, never rotates implicitly; a mismatch fails the step loudly, naming
|
|
219
|
+
* key names and metadata keys — never values. No output of the activity
|
|
220
|
+
* carries secret material. The backing activity is provided by the store's
|
|
221
|
+
* lexicon (k8s, #1830).
|
|
222
|
+
*/
|
|
223
|
+
export const ensureSecret = (
|
|
224
|
+
name: string,
|
|
225
|
+
keys: string[],
|
|
226
|
+
opts?: { metadata?: Record<string, string>; profile?: ActivityStep["profile"]; [k: string]: unknown },
|
|
227
|
+
): ActivityStep => {
|
|
228
|
+
const { args, profile } = takeProfile(opts);
|
|
229
|
+
return activity("ensureSecret", { name, keys, ...args }, profile);
|
|
230
|
+
};
|
|
231
|
+
|
|
136
232
|
/** Run `chant teardown` in the given project directory. Uses `longInfra` profile. */
|
|
137
233
|
export const teardown = (path: string): ActivityStep =>
|
|
138
234
|
activity("chantTeardown", { path }, "longInfra");
|
|
@@ -200,6 +296,48 @@ export const k3dDown = (name: string, opts?: Record<string, unknown>): ActivityS
|
|
|
200
296
|
return activity("k3dDown", { name, ...args }, profile ?? "fastIdempotent");
|
|
201
297
|
};
|
|
202
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Run the pinned k3s installer against a reachable host (`role`: `"server"`
|
|
301
|
+
* or `"agent"`). Idempotent on an already-installed matching version.
|
|
302
|
+
* Defaults to the `longInfra` profile (the installer downloads and starts
|
|
303
|
+
* the binary); override via `opts.profile`.
|
|
304
|
+
*
|
|
305
|
+
* The implementation lives in the k3s lexicon (chant #1601) — the project's
|
|
306
|
+
* `chant.config.ts` must list `"k3s"` in `lexicons` for the activity to load.
|
|
307
|
+
* Bounded exactly as `k3dUp`/`k3dDown` (chant #1410): drives the reachable-host
|
|
308
|
+
* case only, no host provisioning, no SSH orchestration.
|
|
309
|
+
*
|
|
310
|
+
* `opts` accepts `configFile` (required — path to the chant-emitted
|
|
311
|
+
* config.yaml, passed as `--config`), `version` (overrides the lexicon's
|
|
312
|
+
* pinned `INSTALL_K3S_VERSION`), and `tokenFile` (path to a file holding the
|
|
313
|
+
* join token, passed to the installer as `K3S_TOKEN_FILE`). There is no
|
|
314
|
+
* `token` option — the join secret's value never travels through this step;
|
|
315
|
+
* only a file path does (the #1601 token boundary, same stance as #1365).
|
|
316
|
+
*/
|
|
317
|
+
export const k3sInstall = (
|
|
318
|
+
role: "server" | "agent",
|
|
319
|
+
opts: { configFile: string; version?: string; tokenFile?: string; profile?: ActivityStep["profile"]; [k: string]: unknown },
|
|
320
|
+
): ActivityStep => {
|
|
321
|
+
const { args, profile } = takeProfile(opts);
|
|
322
|
+
return activity("k3sInstall", { role, ...args }, profile ?? "longInfra");
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Uninstall k3s from a reachable host (`role`: `"server"` or `"agent"`).
|
|
327
|
+
* Defaults to the `fastIdempotent` profile (override via `opts.profile`).
|
|
328
|
+
* Implementation lives in the k3s lexicon (chant #1601) — requires `"k3s"`
|
|
329
|
+
* in the project's `lexicons`. A host where k3s was never installed is a
|
|
330
|
+
* no-op success, the same shape as `k3dDown` against an already-gone
|
|
331
|
+
* cluster.
|
|
332
|
+
*/
|
|
333
|
+
export const k3sUninstall = (
|
|
334
|
+
role: "server" | "agent",
|
|
335
|
+
opts?: Record<string, unknown>,
|
|
336
|
+
): ActivityStep => {
|
|
337
|
+
const { args, profile } = takeProfile(opts);
|
|
338
|
+
return activity("k3sUninstall", { role, ...args }, profile ?? "fastIdempotent");
|
|
339
|
+
};
|
|
340
|
+
|
|
203
341
|
/**
|
|
204
342
|
* Boot a local Floci AWS emulator in Docker and point subsequent steps at it —
|
|
205
343
|
* sets `AWS_ENDPOINT_URL` + test creds in the process env so a following
|