@intentius/chant 0.41.20 → 0.44.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/build.d.ts +15 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- 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/plugins.d.ts +8 -0
- package/dist/cli/plugins.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/deploy-units.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 +47 -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/aws-resources.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/tier-map.d.ts +4 -3
- package/dist/terraform/tier-map.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/audit/rules-doc.ts +1 -1
- package/src/build.test.ts +95 -0
- package/src/build.ts +46 -1
- package/src/cli/commands/build.ts +9 -1
- 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 +16 -9
- 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/plugins.ts +17 -0
- 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/deploy-units.test.ts +13 -0
- package/src/components/deploy-units.ts +5 -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 +50 -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/aws-resources.test.ts +52 -2
- package/src/terraform/aws-resources.ts +95 -4
- 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 +73 -10
- package/src/terraform/graph.ts +124 -47
- 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/tier-map.ts +60 -8
- package/src/terraform/types.ts +7 -0
- package/src/terraform/unified-diff.test.ts +77 -0
- package/src/terraform/unified-diff.ts +146 -0
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/plugins.ts
CHANGED
|
@@ -95,6 +95,23 @@ export function resolveLexiconVersions(lexiconNames: readonly string[]): Record<
|
|
|
95
95
|
return versions;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Bind each loaded plugin's `buildRoots` hook (#1548 piece 3) to this
|
|
100
|
+
* invocation's config and project root, producing the closures
|
|
101
|
+
* `BuildOptions.buildRoots` takes — the same extract-then-thread shape the
|
|
102
|
+
* CLI uses for `intrinsics`. Plugins without the hook contribute nothing;
|
|
103
|
+
* an empty array is the common case and `build()` treats it as absent.
|
|
104
|
+
*/
|
|
105
|
+
export function collectBuildRootContributors(
|
|
106
|
+
plugins: readonly LexiconPlugin[] | undefined,
|
|
107
|
+
config: Record<string, unknown>,
|
|
108
|
+
projectRoot: string,
|
|
109
|
+
): Array<() => Promise<import("../lexicon").BuildRootContribution>> {
|
|
110
|
+
return (plugins ?? [])
|
|
111
|
+
.filter((plugin) => typeof plugin.buildRoots === "function")
|
|
112
|
+
.map((plugin) => () => plugin.buildRoots!({ projectRoot, config }));
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
/**
|
|
99
116
|
* Load plugins for all detected lexicon names.
|
|
100
117
|
* Calls `init()` on each plugin if present.
|
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
|
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { buildLexiconOkfBundle, type LexiconOkfInput } from "./okf-lexicon";
|
|
3
|
+
import { okfConformanceProblems, splitFrontmatter, OKF_VERSION, type OkfFile } from "../okf";
|
|
4
|
+
import { parseYAML } from "../yaml";
|
|
5
|
+
|
|
6
|
+
function fileMap(bundle: OkfFile[]): Map<string, string> {
|
|
7
|
+
return new Map(bundle.map((f) => [f.path, f.content]));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A docker-style registry: descriptions carried in the registry itself. */
|
|
11
|
+
const registryStyleInput: LexiconOkfInput = {
|
|
12
|
+
name: "mockdocker",
|
|
13
|
+
registry: JSON.stringify({
|
|
14
|
+
Service: {
|
|
15
|
+
resourceType: "Mock::Compose::Service",
|
|
16
|
+
kind: "resource",
|
|
17
|
+
description: "A containerized service definition",
|
|
18
|
+
properties: {
|
|
19
|
+
image: { type: "string", description: "Container image to use" },
|
|
20
|
+
ports: { type: "string[]", description: "Published ports" },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
Volume: {
|
|
24
|
+
resourceType: "Mock::Compose::Volume",
|
|
25
|
+
kind: "resource",
|
|
26
|
+
description: "A named volume",
|
|
27
|
+
properties: { driver: { type: "string", description: "Volume driver" } },
|
|
28
|
+
},
|
|
29
|
+
Service_Healthcheck: {
|
|
30
|
+
resourceType: "Mock::Compose::Service.Healthcheck",
|
|
31
|
+
kind: "property",
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
typesDTS: "",
|
|
35
|
+
rules: [
|
|
36
|
+
{
|
|
37
|
+
meta: {
|
|
38
|
+
id: "MCK001",
|
|
39
|
+
severity: "warning",
|
|
40
|
+
category: "correctness",
|
|
41
|
+
description: "Avoid :latest image references on a Service",
|
|
42
|
+
type: "lint",
|
|
43
|
+
},
|
|
44
|
+
source: `// Checks each Service's image prop\nexport const rule = { id: "MCK001" };`,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
meta: {
|
|
48
|
+
id: "MCK010",
|
|
49
|
+
severity: "error",
|
|
50
|
+
category: "post-synth",
|
|
51
|
+
description: "A declared Volume must be mounted somewhere",
|
|
52
|
+
type: "post-synth",
|
|
53
|
+
},
|
|
54
|
+
source: `export const check = { id: "MCK010", description: "..." }; // walks Volume mounts`,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** A CFN-style registry: no descriptions, JSDoc lives in the declarations. */
|
|
60
|
+
const dtsStyleInput: LexiconOkfInput = {
|
|
61
|
+
name: "mockaws",
|
|
62
|
+
registry: JSON.stringify({
|
|
63
|
+
Bucket: {
|
|
64
|
+
resourceType: "Mock::S3::Bucket",
|
|
65
|
+
kind: "resource",
|
|
66
|
+
lexicon: "mockaws",
|
|
67
|
+
attrs: { Arn: "Arn", DomainName: "DomainName" },
|
|
68
|
+
},
|
|
69
|
+
// An alias entry for the same resource type must not mint a second concept.
|
|
70
|
+
S3Bucket: {
|
|
71
|
+
resourceType: "Mock::S3::Bucket",
|
|
72
|
+
kind: "resource",
|
|
73
|
+
lexicon: "mockaws",
|
|
74
|
+
attrs: { Arn: "Arn", DomainName: "DomainName" },
|
|
75
|
+
},
|
|
76
|
+
Bucket_VersioningConfiguration: {
|
|
77
|
+
resourceType: "Mock::S3::Bucket.VersioningConfiguration",
|
|
78
|
+
kind: "property",
|
|
79
|
+
lexicon: "mockaws",
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
typesDTS: [
|
|
83
|
+
"export declare class Bucket {",
|
|
84
|
+
" constructor(props: {",
|
|
85
|
+
" /** A name for the bucket. */",
|
|
86
|
+
" BucketName?: string;",
|
|
87
|
+
" /** The versioning state. */",
|
|
88
|
+
" VersioningConfiguration?: Bucket_VersioningConfiguration;",
|
|
89
|
+
" }, attributes?: Record<string, unknown>);",
|
|
90
|
+
" readonly Arn: string;",
|
|
91
|
+
"}",
|
|
92
|
+
].join("\n"),
|
|
93
|
+
rules: [
|
|
94
|
+
{
|
|
95
|
+
meta: {
|
|
96
|
+
id: "MAW006",
|
|
97
|
+
severity: "warning",
|
|
98
|
+
category: "security",
|
|
99
|
+
description: "Detects Bucket creation without encryption",
|
|
100
|
+
type: "lint",
|
|
101
|
+
},
|
|
102
|
+
source: `if (expression.text === "Bucket") { /* Mock::S3::Bucket */ }`,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
describe("buildLexiconOkfBundle", () => {
|
|
108
|
+
test("emits a conformant bundle from a registry-described lexicon", () => {
|
|
109
|
+
const bundle = buildLexiconOkfBundle(registryStyleInput);
|
|
110
|
+
expect(okfConformanceProblems(bundle)).toEqual([]);
|
|
111
|
+
expect(bundle.map((f) => f.path)).toEqual([
|
|
112
|
+
"index.md",
|
|
113
|
+
"rules/MCK001.md",
|
|
114
|
+
"rules/MCK010.md",
|
|
115
|
+
"types/Service.md",
|
|
116
|
+
"types/Volume.md",
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("concept type is the category, the resource type rides in resource_type", () => {
|
|
121
|
+
const files = fileMap(buildLexiconOkfBundle(registryStyleInput));
|
|
122
|
+
const front = parseYAML(splitFrontmatter(files.get("types/Service.md")!)!.frontmatter);
|
|
123
|
+
expect(front.type).toBe("resource-type");
|
|
124
|
+
expect(front.resource_type).toBe("Mock::Compose::Service");
|
|
125
|
+
expect(front.title).toBe("Service");
|
|
126
|
+
expect(front.lexicon).toBe("mockdocker");
|
|
127
|
+
expect(front.description).toBe("A containerized service definition");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("registry-carried property descriptions land in the concept body", () => {
|
|
131
|
+
const files = fileMap(buildLexiconOkfBundle(registryStyleInput));
|
|
132
|
+
const body = splitFrontmatter(files.get("types/Service.md")!)!.body;
|
|
133
|
+
expect(body).toContain("## Properties");
|
|
134
|
+
expect(body).toContain("- `image` (`string`): Container image to use");
|
|
135
|
+
expect(body).toContain("- `ports` (`string[]`): Published ports");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("declaration JSDoc fills in property descriptions when the registry has none", () => {
|
|
139
|
+
const files = fileMap(buildLexiconOkfBundle(dtsStyleInput));
|
|
140
|
+
const body = splitFrontmatter(files.get("types/Bucket.md")!)!.body;
|
|
141
|
+
expect(body).toContain("- `BucketName` (`string`, optional): A name for the bucket.");
|
|
142
|
+
expect(body).toContain("- `VersioningConfiguration` (`Bucket_VersioningConfiguration`, optional): The versioning state.");
|
|
143
|
+
expect(body).toContain("## Attributes");
|
|
144
|
+
expect(body).toContain("- `Arn`");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("rules and resource types cross-link in both directions", () => {
|
|
148
|
+
const files = fileMap(buildLexiconOkfBundle(registryStyleInput));
|
|
149
|
+
expect(splitFrontmatter(files.get("types/Service.md")!)!.body).toContain(
|
|
150
|
+
"- [MCK001](/rules/MCK001.md): Avoid :latest image references on a Service",
|
|
151
|
+
);
|
|
152
|
+
expect(splitFrontmatter(files.get("rules/MCK001.md")!)!.body).toContain("- [Service](/types/Service.md)");
|
|
153
|
+
// MCK010 mentions Volume, not Service.
|
|
154
|
+
expect(splitFrontmatter(files.get("rules/MCK010.md")!)!.body).toContain("- [Volume](/types/Volume.md)");
|
|
155
|
+
expect(splitFrontmatter(files.get("rules/MCK010.md")!)!.body).not.toContain("types/Service.md");
|
|
156
|
+
expect(splitFrontmatter(files.get("types/Volume.md")!)!.body).toContain("- [MCK010](/rules/MCK010.md)");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("rule concepts carry id, severity, category, and a docs page", () => {
|
|
160
|
+
const files = fileMap(buildLexiconOkfBundle(registryStyleInput));
|
|
161
|
+
const lint = parseYAML(splitFrontmatter(files.get("rules/MCK001.md")!)!.frontmatter);
|
|
162
|
+
expect(lint.type).toBe("lint-rule");
|
|
163
|
+
expect(lint.id).toBe("MCK001");
|
|
164
|
+
expect(lint.severity).toBe("warning");
|
|
165
|
+
expect(lint.category).toBe("correctness");
|
|
166
|
+
expect(lint.docs).toBe("https://intentius.io/chant/lexicons/mockdocker/rules/");
|
|
167
|
+
|
|
168
|
+
const postSynth = parseYAML(splitFrontmatter(files.get("rules/MCK010.md")!)!.frontmatter);
|
|
169
|
+
expect(postSynth.type).toBe("post-synth-check");
|
|
170
|
+
expect(postSynth.severity).toBe("error");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("alias registry entries collapse into one concept per resource type", () => {
|
|
174
|
+
const bundle = buildLexiconOkfBundle(dtsStyleInput);
|
|
175
|
+
const typePaths = bundle.map((f) => f.path).filter((p) => p.startsWith("types/"));
|
|
176
|
+
expect(typePaths).toEqual(["types/Bucket.md"]);
|
|
177
|
+
expect(okfConformanceProblems(bundle)).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("property-kind registry entries do not become concepts", () => {
|
|
181
|
+
const files = fileMap(buildLexiconOkfBundle(registryStyleInput));
|
|
182
|
+
expect([...files.keys()].some((p) => p.includes("Healthcheck"))).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("index.md sections resource types and rules with okf_version its only frontmatter", () => {
|
|
186
|
+
const index = fileMap(buildLexiconOkfBundle(registryStyleInput)).get("index.md")!;
|
|
187
|
+
expect(index).toContain(`okf_version: '${OKF_VERSION}'`);
|
|
188
|
+
expect(index).toContain("# Resource types");
|
|
189
|
+
expect(index).toContain("* [Service](/types/Service.md) - Mock::Compose::Service");
|
|
190
|
+
expect(index).toContain("# Rules");
|
|
191
|
+
expect(index).toContain("* [MCK001](/rules/MCK001.md) - Avoid :latest image references on a Service");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("an empty lexicon still emits a conformant bundle", () => {
|
|
195
|
+
const bundle = buildLexiconOkfBundle({ name: "bare", registry: "{}", typesDTS: "", rules: [] });
|
|
196
|
+
expect(bundle.map((f) => f.path)).toEqual(["index.md"]);
|
|
197
|
+
expect(okfConformanceProblems(bundle)).toEqual([]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("deterministic: the same input yields byte-identical files", () => {
|
|
201
|
+
expect(buildLexiconOkfBundle(registryStyleInput)).toEqual(buildLexiconOkfBundle(registryStyleInput));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("multi-line spec descriptions collapse to a single frontmatter line", () => {
|
|
205
|
+
const input: LexiconOkfInput = {
|
|
206
|
+
name: "mock",
|
|
207
|
+
registry: JSON.stringify({
|
|
208
|
+
Thing: {
|
|
209
|
+
resourceType: "Mock::A::Thing",
|
|
210
|
+
kind: "resource",
|
|
211
|
+
description: "First line.\nSecond line with detail.",
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
typesDTS: "",
|
|
215
|
+
rules: [],
|
|
216
|
+
};
|
|
217
|
+
const bundle = buildLexiconOkfBundle(input);
|
|
218
|
+
expect(okfConformanceProblems(bundle)).toEqual([]);
|
|
219
|
+
const front = parseYAML(splitFrontmatter(fileMap(bundle).get("types/Thing.md")!)!.frontmatter);
|
|
220
|
+
expect(front.description).toBe("First line. Second line with detail.");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("registry-style and dts-style bundles snapshot stably", () => {
|
|
224
|
+
expect(buildLexiconOkfBundle(registryStyleInput)).toMatchSnapshot();
|
|
225
|
+
expect(buildLexiconOkfBundle(dtsStyleInput)).toMatchSnapshot();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("okfConformanceProblems", () => {
|
|
230
|
+
test("flags a concept without frontmatter and a bundle without an index", () => {
|
|
231
|
+
const problems = okfConformanceProblems([{ path: "types/Broken.md", content: "no frontmatter here" }]);
|
|
232
|
+
expect(problems).toContain("types/Broken.md: no frontmatter block");
|
|
233
|
+
expect(problems).toContain("bundle has no root index.md");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("flags an empty type and a relative cross-link", () => {
|
|
237
|
+
const bundle: OkfFile[] = [
|
|
238
|
+
{
|
|
239
|
+
path: "types/Bad.md",
|
|
240
|
+
content: "---\ntype: ''\n---\n\n- [x](x.md)\n",
|
|
241
|
+
},
|
|
242
|
+
{ path: "index.md", content: `---\nokf_version: '${OKF_VERSION}'\n---\n` },
|
|
243
|
+
];
|
|
244
|
+
const problems = okfConformanceProblems(bundle);
|
|
245
|
+
expect(problems.some((p) => p.includes("no non-empty type"))).toBe(true);
|
|
246
|
+
expect(problems.some((p) => p.includes('link target "x.md"'))).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
});
|