@intentius/chant 0.41.20 → 0.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/carve-apply.d.ts +9 -0
- package/dist/cli/commands/carve-apply.d.ts.map +1 -1
- package/dist/cli/commands/carve-bridge.d.ts +7 -0
- package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/carve-emit.d.ts +7 -0
- package/dist/cli/commands/carve-emit.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/handlers/carve-apply.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/explain.d.ts +11 -0
- package/dist/cli/handlers/explain.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +2 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts +11 -0
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/codegen/okf-lexicon.d.ts +36 -0
- package/dist/codegen/okf-lexicon.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +2 -1
- package/dist/codegen/package.d.ts.map +1 -1
- package/dist/components/verbs/sbom.d.ts +1 -1
- package/dist/components/verbs/sbom.d.ts.map +1 -1
- package/dist/components/verbs/vuln-gate.d.ts +15 -4
- package/dist/components/verbs/vuln-gate.d.ts.map +1 -1
- package/dist/components/verbs/vuln-scan.d.ts +18 -6
- package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +13 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/lexicon.d.ts +3 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/okf.d.ts +57 -0
- package/dist/okf.d.ts.map +1 -0
- package/dist/terraform/__fixtures__/build-graph.d.ts +13 -0
- package/dist/terraform/__fixtures__/build-graph.d.ts.map +1 -0
- package/dist/terraform/adopt-state.d.ts +33 -1
- package/dist/terraform/adopt-state.d.ts.map +1 -1
- package/dist/terraform/bridge.d.ts +7 -1
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve.d.ts +12 -0
- package/dist/terraform/carve.d.ts.map +1 -1
- package/dist/terraform/excise.d.ts +31 -0
- package/dist/terraform/excise.d.ts.map +1 -0
- package/dist/terraform/graduate.d.ts +12 -0
- package/dist/terraform/graduate.d.ts.map +1 -1
- package/dist/terraform/graph.d.ts +35 -5
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/manifest.d.ts +89 -0
- package/dist/terraform/manifest.d.ts.map +1 -0
- package/dist/terraform/parse.d.ts +9 -4
- package/dist/terraform/parse.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +7 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/dist/terraform/unified-diff.d.ts +11 -0
- package/dist/terraform/unified-diff.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/__snapshots__/okf.test.ts.snap +381 -0
- package/src/cli/commands/carve-apply.test.ts +94 -2
- package/src/cli/commands/carve-apply.ts +62 -11
- package/src/cli/commands/carve-bridge.test.ts +82 -4
- package/src/cli/commands/carve-bridge.ts +79 -11
- package/src/cli/commands/carve-emit-state.test.ts +110 -1
- package/src/cli/commands/carve-emit.ts +184 -8
- package/src/cli/commands/init.ts +1 -1
- package/src/cli/handlers/carve-apply.ts +1 -0
- package/src/cli/handlers/components.ts +4 -2
- package/src/cli/handlers/explain.test.ts +93 -0
- package/src/cli/handlers/explain.ts +60 -0
- package/src/cli/handlers/lifecycle.ts +9 -7
- package/src/cli/main.ts +21 -5
- package/src/cli/mcp/server.test.ts +17 -0
- package/src/cli/mcp/tools/explain.ts +15 -3
- package/src/cli/registry.ts +2 -0
- package/src/codegen/__snapshots__/okf-lexicon.test.ts.snap +170 -0
- package/src/codegen/docs-rule-scanning.ts +35 -14
- package/src/codegen/okf-lexicon.test.ts +248 -0
- package/src/codegen/okf-lexicon.ts +303 -0
- package/src/codegen/package.ts +29 -4
- package/src/components/config-defaults.test.ts +30 -0
- package/src/components/verbs/__fixtures__/grype-with-kev-epss.json +1 -0
- package/src/components/verbs/__fixtures__/trivy-with-kev-epss.json +631 -0
- package/src/components/verbs/exploitability-roundtrip.test.ts +190 -0
- package/src/components/verbs/sbom.ts +1 -1
- package/src/components/verbs/vuln-gate.test.ts +144 -1
- package/src/components/verbs/vuln-gate.ts +61 -10
- package/src/components/verbs/vuln-scan.test.ts +94 -0
- package/src/components/verbs/vuln-scan.ts +44 -9
- package/src/config.test.ts +21 -0
- package/src/config.ts +18 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +32 -13
- package/src/lexicon.ts +3 -0
- package/src/lifecycle/observe.test.ts +18 -0
- package/src/lifecycle/observe.ts +10 -3
- package/src/lifecycle/snapshot.ts +5 -4
- package/src/okf.test.ts +169 -0
- package/src/okf.ts +308 -0
- package/src/terraform/__fixtures__/build-graph.ts +25 -0
- package/src/terraform/adopt-state.test.ts +34 -1
- package/src/terraform/adopt-state.ts +69 -2
- package/src/terraform/bridge.test.ts +38 -14
- package/src/terraform/bridge.ts +48 -14
- package/src/terraform/carve.test.ts +9 -7
- package/src/terraform/carve.ts +0 -0
- package/src/terraform/excise.test.ts +92 -0
- package/src/terraform/excise.ts +139 -0
- package/src/terraform/graduate.test.ts +73 -6
- package/src/terraform/graduate.ts +49 -0
- package/src/terraform/graph.test.ts +56 -10
- package/src/terraform/graph.ts +112 -45
- package/src/terraform/manifest.test.ts +105 -0
- package/src/terraform/manifest.ts +162 -0
- package/src/terraform/parse.test.ts +39 -1
- package/src/terraform/parse.ts +36 -9
- package/src/terraform/score.test.ts +8 -8
- package/src/terraform/state.test.ts +4 -4
- package/src/terraform/types.ts +7 -0
- package/src/terraform/unified-diff.test.ts +77 -0
- package/src/terraform/unified-diff.ts +146 -0
|
@@ -42,6 +42,7 @@ import { findBuildManifestByArtifactDigest } from "../../lifecycle/build-ledger-
|
|
|
42
42
|
import type { ComponentBomSummary } from "../../lifecycle/build-ledger";
|
|
43
43
|
import { loadChantConfig } from "../../config";
|
|
44
44
|
import { applyLiveEndpoint } from "../../live-endpoint";
|
|
45
|
+
import { isResourceDeclarable } from "../../declarable";
|
|
45
46
|
import { build } from "../../build";
|
|
46
47
|
import { discoverComponents } from "../../components/discover";
|
|
47
48
|
import { formatError, formatWarning, formatSuccess, formatBold } from "../format";
|
|
@@ -397,11 +398,12 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
|
|
|
397
398
|
const declared = new Set<string>();
|
|
398
399
|
const entities = new Map<string, { entityType: string; props: Record<string, unknown> }>();
|
|
399
400
|
for (const [name, entity] of buildResult.entities) {
|
|
400
|
-
|
|
401
|
+
// Resource declarables only (see lifecycle/observe.ts).
|
|
402
|
+
if (entity.lexicon === plugin.name && isResourceDeclarable(entity)) {
|
|
401
403
|
declared.add(name);
|
|
402
404
|
entities.set(name, {
|
|
403
405
|
entityType: entity.entityType,
|
|
404
|
-
props: (
|
|
406
|
+
props: (entity.props != null ? entity.props : {}) as Record<string, unknown>,
|
|
405
407
|
});
|
|
406
408
|
}
|
|
407
409
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
|
|
6
|
+
import type { ParsedArgs } from "../registry";
|
|
7
|
+
|
|
8
|
+
const discoverMock = vi.fn();
|
|
9
|
+
vi.mock("../../discovery/index", () => ({
|
|
10
|
+
discover: (...a: unknown[]) => discoverMock(...a),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { runExplain } from "./explain";
|
|
14
|
+
|
|
15
|
+
function decl<T extends object>(base: T): Declarable & T {
|
|
16
|
+
return { [DECLARABLE_MARKER]: true, ...base } as Declarable & T;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ctx(overrides: Partial<ParsedArgs>): Parameters<typeof runExplain>[0] {
|
|
20
|
+
return {
|
|
21
|
+
args: { command: "explain", path: ".", format: "", fix: false, watch: false, verbose: false, help: false, live: false, ...overrides } as ParsedArgs,
|
|
22
|
+
plugins: [],
|
|
23
|
+
serializers: [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function discoveryResult() {
|
|
28
|
+
const vpc = decl({ lexicon: "gcp", entityType: "Vpc" });
|
|
29
|
+
const subnet = decl({ lexicon: "gcp", entityType: "Subnet" });
|
|
30
|
+
return {
|
|
31
|
+
entities: new Map<string, Declarable>([
|
|
32
|
+
["vpc", vpc],
|
|
33
|
+
["subnet", subnet],
|
|
34
|
+
]),
|
|
35
|
+
dependencies: new Map([["subnet", new Set(["vpc"])]]),
|
|
36
|
+
sourceFiles: ["net.ts"],
|
|
37
|
+
errors: [],
|
|
38
|
+
foldDecisions: [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("runExplain", () => {
|
|
43
|
+
let outDir: string;
|
|
44
|
+
let logSpy: ReturnType<typeof vi.spyOn>;
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
outDir = await mkdtemp(join(tmpdir(), "chant-explain-okf-"));
|
|
48
|
+
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
49
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
50
|
+
discoverMock.mockResolvedValue(discoveryResult());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(async () => {
|
|
54
|
+
vi.restoreAllMocks();
|
|
55
|
+
await rm(outDir, { recursive: true, force: true });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("--format okf -o <dir> writes the bundle as a directory tree", async () => {
|
|
59
|
+
const code = await runExplain(ctx({ format: "okf", output: outDir }));
|
|
60
|
+
expect(code).toBe(0);
|
|
61
|
+
|
|
62
|
+
expect((await stat(join(outDir, "index.md"))).isFile()).toBe(true);
|
|
63
|
+
const subnet = await readFile(join(outDir, "gcp", "subnet.md"), "utf-8");
|
|
64
|
+
expect(subnet).toContain("type: Subnet");
|
|
65
|
+
expect(subnet).toContain("- [vpc](/gcp/vpc.md)");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("--format okf without -o prints the bundle as JSON path -> content", async () => {
|
|
69
|
+
const code = await runExplain(ctx({ format: "okf" }));
|
|
70
|
+
expect(code).toBe(0);
|
|
71
|
+
|
|
72
|
+
const parsed = JSON.parse(logSpy.mock.calls[0][0] as string);
|
|
73
|
+
expect(parsed.okf_version).toBe("0.2");
|
|
74
|
+
expect(Object.keys(parsed.files).sort()).toEqual(["gcp/subnet.md", "gcp/vpc.md", "index.md"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("default format renders the markdown summary", async () => {
|
|
78
|
+
const code = await runExplain(ctx({}));
|
|
79
|
+
expect(code).toBe(0);
|
|
80
|
+
expect(logSpy.mock.calls[0][0]).toContain("# Project Summary");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("an unknown format is rejected", async () => {
|
|
84
|
+
const code = await runExplain(ctx({ format: "xml" }));
|
|
85
|
+
expect(code).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("discovery errors surface as a non-zero exit for okf", async () => {
|
|
89
|
+
discoverMock.mockResolvedValue({ ...discoveryResult(), errors: [new Error("boom")] });
|
|
90
|
+
const code = await runExplain(ctx({ format: "okf" }));
|
|
91
|
+
expect(code).toBe(1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { resolve, join, dirname } from "node:path";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { discover } from "../../discovery/index";
|
|
4
|
+
import { buildOkfBundle, OKF_VERSION, type OkfFile } from "../../okf";
|
|
5
|
+
import { handleExplain } from "../mcp/tools/explain";
|
|
6
|
+
import { formatError, formatSuccess } from "../format";
|
|
7
|
+
import type { CommandContext } from "../registry";
|
|
8
|
+
|
|
9
|
+
const EXPLAIN_FORMATS = ["markdown", "json", "okf"] as const;
|
|
10
|
+
type ExplainFormat = (typeof EXPLAIN_FORMATS)[number];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `chant explain [path] [--format markdown|json|okf] [-o <dir>]` — the CLI
|
|
14
|
+
* path of the MCP `explain` tool (#1058): a structured summary of every
|
|
15
|
+
* discovered entity. `--format okf` emits an OKF v0.2 knowledge bundle
|
|
16
|
+
* instead — one markdown concept per entity plus an `index.md` — written as a
|
|
17
|
+
* directory tree under `-o <dir>`, or printed as JSON (path → content) when
|
|
18
|
+
* no output directory is given.
|
|
19
|
+
*/
|
|
20
|
+
export async function runExplain(ctx: CommandContext): Promise<number> {
|
|
21
|
+
const { args } = ctx;
|
|
22
|
+
const format = (args.format || "markdown") as ExplainFormat;
|
|
23
|
+
if (!EXPLAIN_FORMATS.includes(format)) {
|
|
24
|
+
console.error(formatError({
|
|
25
|
+
message: `Invalid --format: ${format}. Expected one of ${EXPLAIN_FORMATS.join(", ")}.`,
|
|
26
|
+
}));
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const projectPath = resolve(args.path === "." ? "." : args.path);
|
|
31
|
+
|
|
32
|
+
if (format === "okf") {
|
|
33
|
+
const result = await discover(projectPath);
|
|
34
|
+
for (const err of result.errors) console.error(formatError({ message: err.message }));
|
|
35
|
+
const bundle = buildOkfBundle(result, projectPath);
|
|
36
|
+
if (args.output) {
|
|
37
|
+
await writeBundle(bundle, resolve(args.output));
|
|
38
|
+
console.log(formatSuccess(`Wrote OKF v${OKF_VERSION} bundle: ${bundle.length} file(s) under ${args.output}`));
|
|
39
|
+
} else {
|
|
40
|
+
console.log(JSON.stringify({
|
|
41
|
+
okf_version: OKF_VERSION,
|
|
42
|
+
files: Object.fromEntries(bundle.map((f) => [f.path, f.content])),
|
|
43
|
+
}, null, 2));
|
|
44
|
+
}
|
|
45
|
+
return result.errors.length > 0 ? 1 : 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const summary = await handleExplain({ path: projectPath, format });
|
|
49
|
+
console.log(typeof summary === "string" ? summary : JSON.stringify(summary, null, 2));
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Write the bundle's files under `outDir`, creating subdirectories as needed. */
|
|
54
|
+
async function writeBundle(bundle: OkfFile[], outDir: string): Promise<void> {
|
|
55
|
+
for (const file of bundle) {
|
|
56
|
+
const target = join(outDir, file.path);
|
|
57
|
+
await mkdir(dirname(target), { recursive: true });
|
|
58
|
+
await writeFile(target, file.content, "utf-8");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -32,6 +32,7 @@ import { affectedStacks } from "../../lifecycle/affected";
|
|
|
32
32
|
import { rollbackToRevision } from "../../lifecycle/rollback";
|
|
33
33
|
import { loadChantConfig, environmentNames } from "../../config";
|
|
34
34
|
import { applyLiveEndpoint } from "../../live-endpoint";
|
|
35
|
+
import { isResourceDeclarable } from "../../declarable";
|
|
35
36
|
import { formatError, formatWarning, formatSuccess, formatBold } from "../format";
|
|
36
37
|
import type { CommandContext } from "../registry";
|
|
37
38
|
import type { LifecycleSnapshot } from "../../lifecycle/types";
|
|
@@ -758,17 +759,18 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<LiveDiffOutcome
|
|
|
758
759
|
continue;
|
|
759
760
|
}
|
|
760
761
|
|
|
761
|
-
// Build per-lexicon entity index
|
|
762
|
+
// Build per-lexicon entity index. Resource declarables only — outputs,
|
|
763
|
+
// parameters and serializer directives have no live counterpart, and a
|
|
764
|
+
// declared name the reader can never resolve reads as missing (see
|
|
765
|
+
// lifecycle/observe.ts).
|
|
762
766
|
const declared = new Set<string>();
|
|
763
767
|
const entities = new Map<string, { entityType: string; props: Record<string, unknown> }>();
|
|
764
768
|
for (const [name, entity] of args.buildResult.entities) {
|
|
765
|
-
if (entity.lexicon === lexiconName) {
|
|
769
|
+
if (entity.lexicon === lexiconName && isResourceDeclarable(entity)) {
|
|
766
770
|
declared.add(name);
|
|
767
771
|
entities.set(name, {
|
|
768
772
|
entityType: entity.entityType,
|
|
769
|
-
props: (
|
|
770
|
-
? entity.props
|
|
771
|
-
: {}) as Record<string, unknown>,
|
|
773
|
+
props: (entity.props != null ? entity.props : {}) as Record<string, unknown>,
|
|
772
774
|
});
|
|
773
775
|
}
|
|
774
776
|
}
|
|
@@ -1119,11 +1121,11 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1119
1121
|
const declared = new Set<string>();
|
|
1120
1122
|
const entities = new Map<string, { entityType: string; props: Record<string, unknown> }>();
|
|
1121
1123
|
for (const [name, entity] of buildResult.entities) {
|
|
1122
|
-
if (entity.lexicon === lexiconName) {
|
|
1124
|
+
if (entity.lexicon === lexiconName && isResourceDeclarable(entity)) {
|
|
1123
1125
|
declared.add(name);
|
|
1124
1126
|
entities.set(name, {
|
|
1125
1127
|
entityType: entity.entityType,
|
|
1126
|
-
props: (
|
|
1128
|
+
props: (entity.props != null ? entity.props : {}) as Record<string, unknown>,
|
|
1127
1129
|
});
|
|
1128
1130
|
}
|
|
1129
1131
|
}
|
package/src/cli/main.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { runCarveApply } from "./handlers/carve-apply";
|
|
|
26
26
|
import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecycleRollback, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleUnknown } from "./handlers/lifecycle";
|
|
27
27
|
import { runComponentsStatus, runComponentsReleaseRecord, runComponentsUnknown } from "./handlers/components";
|
|
28
28
|
import { runGraph } from "./handlers/graph";
|
|
29
|
+
import { runExplain } from "./handlers/explain";
|
|
29
30
|
import { runSearch } from "./handlers/search";
|
|
30
31
|
import { runOp, runOpList, runOpStatus, runOpSignal, runOpCancel, runOpLog } from "./handlers/run";
|
|
31
32
|
import { runEmulator } from "./handlers/emulator";
|
|
@@ -212,6 +213,8 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
212
213
|
result.carveStack = args[++i];
|
|
213
214
|
} else if (arg === "--write") {
|
|
214
215
|
result.write = true;
|
|
216
|
+
} else if (arg === "--write-source") {
|
|
217
|
+
result.writeSource = true;
|
|
215
218
|
} else if (arg === "--to") {
|
|
216
219
|
result.migrateTo = args[++i];
|
|
217
220
|
} else if (arg === "--emit") {
|
|
@@ -385,6 +388,10 @@ Commands:
|
|
|
385
388
|
lint Check specifications for issues
|
|
386
389
|
list List discovered entities
|
|
387
390
|
describe Show the effective config for one component
|
|
391
|
+
explain Summarize discovered entities (--format markdown|json|okf;
|
|
392
|
+
okf emits an OKF v0.2 knowledge bundle — one markdown
|
|
393
|
+
concept per entity + index.md; -o <dir> writes the
|
|
394
|
+
bundle tree, otherwise JSON path→content on stdout)
|
|
388
395
|
vendor Pull pinned, checksummed patterns into your repo
|
|
389
396
|
import Import external template into TypeScript
|
|
390
397
|
audit [path|url] Audit a repo's CI YAML for security issues
|
|
@@ -402,14 +409,22 @@ Commands:
|
|
|
402
409
|
--select <addr> (recommended for TF-managed resources); --env
|
|
403
410
|
--state|--env <env> adopts via live cloud import (--live-name
|
|
404
411
|
<logical-id> narrows a multi-resource stack).
|
|
412
|
+
Persists a carve manifest bridge/apply compose
|
|
413
|
+
with; scaffolds the output dir into a buildable
|
|
414
|
+
chant project (src/ + config + package.json).
|
|
405
415
|
carve bridge Generate the surviving-TF patch (data sources + rewired
|
|
406
|
-
--from <tf-dir> refs) + deferred inputs + reversible runbook
|
|
407
|
-
--select <addr>
|
|
408
|
-
|
|
416
|
+
--from <tf-dir> refs) + deferred inputs + reversible runbook,
|
|
417
|
+
[--select <addr>] plus one git-applyable .patch for the whole
|
|
418
|
+
edit. Writes proposals for review;
|
|
419
|
+
--apply-rewrites edits the .tf in place.
|
|
420
|
+
--select is optional when the output dir
|
|
421
|
+
holds one carve manifest.
|
|
409
422
|
carve apply Apply graduation: ownership marker + finalized apply
|
|
410
423
|
--from <tf-dir> runbook (dial-turn observe→apply). BYOL —
|
|
411
|
-
--select <addr>
|
|
412
|
-
--env <env>
|
|
424
|
+
[--select <addr>] no cloud call; --write saves the doc. --select
|
|
425
|
+
--env <env> is optional with a carve manifest present.
|
|
426
|
+
--write-source stamps the ownership marker
|
|
427
|
+
into the emitted chant source.
|
|
413
428
|
|
|
414
429
|
Ops:
|
|
415
430
|
run <name> Start an Op workflow (spawns worker + submits to Temporal)
|
|
@@ -705,6 +720,7 @@ const registry: CommandDef[] = [
|
|
|
705
720
|
{ name: "lint", handler: runLint },
|
|
706
721
|
{ name: "list", handler: runList },
|
|
707
722
|
{ name: "describe", handler: runDescribe },
|
|
723
|
+
{ name: "explain", handler: runExplain },
|
|
708
724
|
{ name: "search", handler: runSearch },
|
|
709
725
|
{ name: "import", handler: runImport },
|
|
710
726
|
{ name: "audit", handler: runAudit },
|
|
@@ -327,6 +327,23 @@ describe("McpServer", () => {
|
|
|
327
327
|
expect(parsed.sourceFiles).toBeDefined();
|
|
328
328
|
});
|
|
329
329
|
|
|
330
|
+
test("calls explain tool with okf format (#1058)", async () => {
|
|
331
|
+
const response = await server.handleRequest({
|
|
332
|
+
jsonrpc: "2.0",
|
|
333
|
+
id: 1,
|
|
334
|
+
method: "tools/call",
|
|
335
|
+
params: { name: "explain", arguments: { path: testDir, format: "okf" } },
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(response.error).toBeUndefined();
|
|
339
|
+
const result = response.result as { content: Array<{ text: string }> };
|
|
340
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
341
|
+
expect(parsed.okf_version).toBe("0.2");
|
|
342
|
+
// Even an empty project yields a bundle with a root index.md.
|
|
343
|
+
expect(Object.keys(parsed.files)).toEqual(["index.md"]);
|
|
344
|
+
expect(parsed.files["index.md"]).toContain("okf_version: '0.2'");
|
|
345
|
+
});
|
|
346
|
+
|
|
330
347
|
test("calls scaffold tool with generic fallback", async () => {
|
|
331
348
|
const response = await server.handleRequest({
|
|
332
349
|
jsonrpc: "2.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve } from "path";
|
|
2
2
|
import { discover } from "../../../discovery/index";
|
|
3
|
+
import { buildOkfBundle, OKF_VERSION } from "../../../okf";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Explain tool definition for MCP
|
|
@@ -16,8 +17,8 @@ export const explainTool = {
|
|
|
16
17
|
},
|
|
17
18
|
format: {
|
|
18
19
|
type: "string",
|
|
19
|
-
enum: ["markdown", "json"],
|
|
20
|
-
description: "Output format (default: markdown)",
|
|
20
|
+
enum: ["markdown", "json", "okf"],
|
|
21
|
+
description: "Output format (default: markdown). okf: an OKF v0.2 knowledge bundle — one markdown concept per entity plus an index.md (#1058)",
|
|
21
22
|
},
|
|
22
23
|
},
|
|
23
24
|
required: ["path"],
|
|
@@ -29,11 +30,22 @@ export const explainTool = {
|
|
|
29
30
|
*/
|
|
30
31
|
export async function handleExplain(params: Record<string, unknown>): Promise<unknown> {
|
|
31
32
|
const path = params.path as string;
|
|
32
|
-
const format = (params.format as "markdown" | "json") ?? "markdown";
|
|
33
|
+
const format = (params.format as "markdown" | "json" | "okf") ?? "markdown";
|
|
33
34
|
|
|
34
35
|
const infraPath = resolve(path);
|
|
35
36
|
const result = await discover(infraPath);
|
|
36
37
|
|
|
38
|
+
// OKF knowledge bundle (#1058): one concept document per entity plus a root
|
|
39
|
+
// index.md, returned as bundle-relative path → content. The CLI path writes
|
|
40
|
+
// the same files to a directory.
|
|
41
|
+
if (format === "okf") {
|
|
42
|
+
return {
|
|
43
|
+
okf_version: OKF_VERSION,
|
|
44
|
+
files: Object.fromEntries(buildOkfBundle(result, infraPath).map((f) => [f.path, f.content])),
|
|
45
|
+
errors: result.errors.map((e) => e.message),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
// Group entities by lexicon and kind
|
|
38
50
|
const byLexicon = new Map<string, { resources: string[]; properties: string[] }>();
|
|
39
51
|
|
package/src/cli/registry.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface ParsedArgs {
|
|
|
45
45
|
carveStack?: string;
|
|
46
46
|
/** `chant carve apply --write` — save the graduation doc */
|
|
47
47
|
write?: boolean;
|
|
48
|
+
/** `chant carve apply --write-source` — stamp the ownership marker into the emitted source */
|
|
49
|
+
writeSource?: boolean;
|
|
48
50
|
/** `chant migrate --to <name>` (default "gitlab") */
|
|
49
51
|
migrateTo?: string;
|
|
50
52
|
/** `chant migrate --emit yaml|ts` */
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`buildLexiconOkfBundle > registry-style and dts-style bundles snapshot stably 1`] = `
|
|
4
|
+
[
|
|
5
|
+
{
|
|
6
|
+
"content": "---
|
|
7
|
+
okf_version: '0.2'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Resource types
|
|
11
|
+
|
|
12
|
+
* [Service](/types/Service.md) - Mock::Compose::Service
|
|
13
|
+
* [Volume](/types/Volume.md) - Mock::Compose::Volume
|
|
14
|
+
|
|
15
|
+
# Rules
|
|
16
|
+
|
|
17
|
+
* [MCK001](/rules/MCK001.md) - Avoid :latest image references on a Service
|
|
18
|
+
* [MCK010](/rules/MCK010.md) - A declared Volume must be mounted somewhere
|
|
19
|
+
",
|
|
20
|
+
"path": "index.md",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"content": "---
|
|
24
|
+
type: lint-rule
|
|
25
|
+
title: MCK001
|
|
26
|
+
description: Avoid :latest image references on a Service
|
|
27
|
+
id: MCK001
|
|
28
|
+
severity: warning
|
|
29
|
+
category: correctness
|
|
30
|
+
lexicon: mockdocker
|
|
31
|
+
docs: https://intentius.io/chant/lexicons/mockdocker/rules/
|
|
32
|
+
---
|
|
33
|
+
Avoid :latest image references on a Service
|
|
34
|
+
|
|
35
|
+
## Applies to
|
|
36
|
+
|
|
37
|
+
- [Service](/types/Service.md)
|
|
38
|
+
",
|
|
39
|
+
"path": "rules/MCK001.md",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"content": "---
|
|
43
|
+
type: post-synth-check
|
|
44
|
+
title: MCK010
|
|
45
|
+
description: A declared Volume must be mounted somewhere
|
|
46
|
+
id: MCK010
|
|
47
|
+
severity: error
|
|
48
|
+
category: post-synth
|
|
49
|
+
lexicon: mockdocker
|
|
50
|
+
docs: https://intentius.io/chant/lexicons/mockdocker/rules/
|
|
51
|
+
---
|
|
52
|
+
A declared Volume must be mounted somewhere
|
|
53
|
+
|
|
54
|
+
## Applies to
|
|
55
|
+
|
|
56
|
+
- [Volume](/types/Volume.md)
|
|
57
|
+
",
|
|
58
|
+
"path": "rules/MCK010.md",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"content": "---
|
|
62
|
+
type: resource-type
|
|
63
|
+
title: Service
|
|
64
|
+
description: A containerized service definition
|
|
65
|
+
name: Service
|
|
66
|
+
lexicon: mockdocker
|
|
67
|
+
resource_type: Mock::Compose::Service
|
|
68
|
+
---
|
|
69
|
+
A containerized service definition
|
|
70
|
+
|
|
71
|
+
## Properties
|
|
72
|
+
|
|
73
|
+
- \`image\` (\`string\`): Container image to use
|
|
74
|
+
- \`ports\` (\`string[]\`): Published ports
|
|
75
|
+
|
|
76
|
+
## Governed by
|
|
77
|
+
|
|
78
|
+
- [MCK001](/rules/MCK001.md): Avoid :latest image references on a Service
|
|
79
|
+
",
|
|
80
|
+
"path": "types/Service.md",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"content": "---
|
|
84
|
+
type: resource-type
|
|
85
|
+
title: Volume
|
|
86
|
+
description: A named volume
|
|
87
|
+
name: Volume
|
|
88
|
+
lexicon: mockdocker
|
|
89
|
+
resource_type: Mock::Compose::Volume
|
|
90
|
+
---
|
|
91
|
+
A named volume
|
|
92
|
+
|
|
93
|
+
## Properties
|
|
94
|
+
|
|
95
|
+
- \`driver\` (\`string\`): Volume driver
|
|
96
|
+
|
|
97
|
+
## Governed by
|
|
98
|
+
|
|
99
|
+
- [MCK010](/rules/MCK010.md): A declared Volume must be mounted somewhere
|
|
100
|
+
",
|
|
101
|
+
"path": "types/Volume.md",
|
|
102
|
+
},
|
|
103
|
+
]
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
exports[`buildLexiconOkfBundle > registry-style and dts-style bundles snapshot stably 2`] = `
|
|
107
|
+
[
|
|
108
|
+
{
|
|
109
|
+
"content": "---
|
|
110
|
+
okf_version: '0.2'
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
# Resource types
|
|
114
|
+
|
|
115
|
+
* [Bucket](/types/Bucket.md) - Mock::S3::Bucket
|
|
116
|
+
|
|
117
|
+
# Rules
|
|
118
|
+
|
|
119
|
+
* [MAW006](/rules/MAW006.md) - Detects Bucket creation without encryption
|
|
120
|
+
",
|
|
121
|
+
"path": "index.md",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"content": "---
|
|
125
|
+
type: lint-rule
|
|
126
|
+
title: MAW006
|
|
127
|
+
description: Detects Bucket creation without encryption
|
|
128
|
+
id: MAW006
|
|
129
|
+
severity: warning
|
|
130
|
+
category: security
|
|
131
|
+
lexicon: mockaws
|
|
132
|
+
docs: https://intentius.io/chant/lexicons/mockaws/rules/
|
|
133
|
+
---
|
|
134
|
+
Detects Bucket creation without encryption
|
|
135
|
+
|
|
136
|
+
## Applies to
|
|
137
|
+
|
|
138
|
+
- [Bucket](/types/Bucket.md)
|
|
139
|
+
",
|
|
140
|
+
"path": "rules/MAW006.md",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"content": "---
|
|
144
|
+
type: resource-type
|
|
145
|
+
title: Bucket
|
|
146
|
+
description: mockaws resource type Mock::S3::Bucket
|
|
147
|
+
name: Bucket
|
|
148
|
+
lexicon: mockaws
|
|
149
|
+
resource_type: Mock::S3::Bucket
|
|
150
|
+
---
|
|
151
|
+
\`Mock::S3::Bucket\`, a resource type of the mockaws lexicon.
|
|
152
|
+
|
|
153
|
+
## Properties
|
|
154
|
+
|
|
155
|
+
- \`BucketName\` (\`string\`, optional): A name for the bucket.
|
|
156
|
+
- \`VersioningConfiguration\` (\`Bucket_VersioningConfiguration\`, optional): The versioning state.
|
|
157
|
+
|
|
158
|
+
## Attributes
|
|
159
|
+
|
|
160
|
+
- \`Arn\`
|
|
161
|
+
- \`DomainName\`
|
|
162
|
+
|
|
163
|
+
## Governed by
|
|
164
|
+
|
|
165
|
+
- [MAW006](/rules/MAW006.md): Detects Bucket creation without encryption
|
|
166
|
+
",
|
|
167
|
+
"path": "types/Bucket.md",
|
|
168
|
+
},
|
|
169
|
+
]
|
|
170
|
+
`;
|
|
@@ -11,12 +11,27 @@ import { join } from "path";
|
|
|
11
11
|
import { escapeMdx } from "./docs-file-markers";
|
|
12
12
|
import type { DocsConfig, RuleMeta } from "./docs-types";
|
|
13
13
|
|
|
14
|
+
/** A scanned rule: its extracted metadata plus the source it came from. */
|
|
15
|
+
export interface ScannedRule {
|
|
16
|
+
meta: RuleMeta;
|
|
17
|
+
source: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
/**
|
|
15
21
|
* Scan lint rule and post-synth check source files to extract metadata.
|
|
16
22
|
* Uses regex to find id, severity, category, and description from source.
|
|
17
23
|
*/
|
|
18
24
|
export function scanRules(srcDir: string): RuleMeta[] {
|
|
19
|
-
|
|
25
|
+
return scanRulesWithSources(srcDir).map((r) => r.meta);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Like {@link scanRules}, but keeps each rule's source text alongside its
|
|
30
|
+
* metadata — the OKF bundle builder (#1060) reads the source to associate a
|
|
31
|
+
* rule with the resource types it mentions.
|
|
32
|
+
*/
|
|
33
|
+
export function scanRulesWithSources(srcDir: string): ScannedRule[] {
|
|
34
|
+
const rules: ScannedRule[] = [];
|
|
20
35
|
|
|
21
36
|
// Scan lint rules
|
|
22
37
|
scanDir(join(srcDir, "lint", "rules"), "lint", rules);
|
|
@@ -27,7 +42,7 @@ export function scanRules(srcDir: string): RuleMeta[] {
|
|
|
27
42
|
return rules;
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
function scanDir(dir: string, type: "lint" | "post-synth", out:
|
|
45
|
+
function scanDir(dir: string, type: "lint" | "post-synth", out: ScannedRule[]): void {
|
|
31
46
|
let entries: string[];
|
|
32
47
|
try {
|
|
33
48
|
entries = readdirSync(dir);
|
|
@@ -62,13 +77,16 @@ function scanDir(dir: string, type: "lint" | "post-synth", out: RuleMeta[]): voi
|
|
|
62
77
|
);
|
|
63
78
|
|
|
64
79
|
out.push({
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
meta: {
|
|
81
|
+
id: idMatch[1],
|
|
82
|
+
severity: severityMatch?.[1] ?? "warning",
|
|
83
|
+
category: categoryMatch?.[1] ?? "general",
|
|
84
|
+
description:
|
|
85
|
+
descMatch?.[1]?.trim() ??
|
|
86
|
+
extractDescriptionFromComment(content, idMatch[1]),
|
|
87
|
+
type: "lint",
|
|
88
|
+
},
|
|
89
|
+
source: content,
|
|
72
90
|
});
|
|
73
91
|
}
|
|
74
92
|
} else {
|
|
@@ -78,11 +96,14 @@ function scanDir(dir: string, type: "lint" | "post-synth", out: RuleMeta[]): voi
|
|
|
78
96
|
|
|
79
97
|
if (idMatch) {
|
|
80
98
|
out.push({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
99
|
+
meta: {
|
|
100
|
+
id: idMatch[1],
|
|
101
|
+
severity: "error",
|
|
102
|
+
category: "post-synth",
|
|
103
|
+
description: descMatch?.[1] ?? idMatch[1],
|
|
104
|
+
type: "post-synth",
|
|
105
|
+
},
|
|
106
|
+
source: content,
|
|
86
107
|
});
|
|
87
108
|
}
|
|
88
109
|
}
|