@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
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* digest) over re-scanning the image: deterministic (same SBOM -> same
|
|
10
10
|
* findings), fast, and reuses work already done. Real backends shell out to
|
|
11
11
|
* `grype`/`trivy` through the injectable `ProcessRunner` (./process-runner.ts),
|
|
12
|
-
* exactly like ./tool-sbom-generator.ts's deep-scan backend — tests inject
|
|
13
|
-
* `
|
|
14
|
-
* scanner, network, or vuln DB.
|
|
15
|
-
* job, not chant's.
|
|
12
|
+
* exactly like ./tool-sbom-generator.ts's deep-scan backend — tests inject an
|
|
13
|
+
* inline fake `VulnScanner` (an object literal with a canned `scan()`, see
|
|
14
|
+
* ./vuln-gate.test.ts) and never invoke a real scanner, network, or vuln DB.
|
|
15
|
+
* The scanner's vuln DB currency is the tool's job, not chant's.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { writeFileSync } from "node:fs";
|
|
@@ -57,6 +57,18 @@ export interface VulnFinding {
|
|
|
57
57
|
fixedVersion?: string;
|
|
58
58
|
/** True when a fix exists (upgradeable) — the beginner-safe default gate blocks only fixable findings, since an unfixable one can't be actioned by bumping. */
|
|
59
59
|
fixable: boolean;
|
|
60
|
+
/** EPSS score (0.0–1.0): probability of exploitation in the next 30 days. Absent when the scanner did not report one. */
|
|
61
|
+
epss?: number;
|
|
62
|
+
/** EPSS percentile (0.0–1.0) — rank against all scored CVEs. */
|
|
63
|
+
epssPercentile?: number;
|
|
64
|
+
/** Present in CISA's Known Exploited Vulnerabilities catalog. `undefined` means the scanner did not report KEV membership at all — NOT the same conclusion as a reported `false`. Never default this. */
|
|
65
|
+
inKev?: boolean;
|
|
66
|
+
/** When the CVE entered the KEV catalog (ISO date, as the source reports it). */
|
|
67
|
+
kevDateAdded?: string;
|
|
68
|
+
/** KEV remediation due date (ISO date). */
|
|
69
|
+
kevDueDate?: string;
|
|
70
|
+
/** Known use in a ransomware campaign, per KEV. */
|
|
71
|
+
kevRansomware?: boolean;
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
// ── injectable scanner boundary ──────────────────────────────────────────────
|
|
@@ -72,8 +84,8 @@ export interface ScanInput {
|
|
|
72
84
|
* Injectable vulnerability-scan boundary — the scan-side analogue of
|
|
73
85
|
* `SbomGenerator` (./sbom-generator.ts) and `CloudExecutor`
|
|
74
86
|
* (./cloud-executor.ts). A real implementation shells out to `grype`/`trivy`;
|
|
75
|
-
* tests substitute
|
|
76
|
-
* never touch a real tool, network, or vuln DB.
|
|
87
|
+
* tests substitute an inline fake (an object literal with a canned `scan()`)
|
|
88
|
+
* and never touch a real tool, network, or vuln DB.
|
|
77
89
|
*/
|
|
78
90
|
export interface VulnScanner {
|
|
79
91
|
/** Scan an SBOM, returning every known vulnerability it surfaces. */
|
|
@@ -83,15 +95,26 @@ export interface VulnScanner {
|
|
|
83
95
|
/** Which real CLI scanner a `ProcessRunner`-backed scanner shells out to. */
|
|
84
96
|
export type ScannerTool = "grype" | "trivy";
|
|
85
97
|
|
|
86
|
-
/** grype `-o json` output shape (the subset we read). */
|
|
98
|
+
/** grype `-o json` output shape (the subset we read). `epss` and `knownExploited` are omitted (not empty) for a vuln grype has no data on, so absence maps to `undefined`, never `false`/`0`. grype's composite `risk` score is deliberately not read — policy gates on the inputs (EPSS, KEV), not one tool's weighting of them. */
|
|
87
99
|
interface GrypeOutput {
|
|
88
100
|
matches?: Array<{
|
|
89
|
-
vulnerability?: {
|
|
101
|
+
vulnerability?: {
|
|
102
|
+
id?: string;
|
|
103
|
+
severity?: string;
|
|
104
|
+
fix?: { versions?: string[]; state?: string };
|
|
105
|
+
epss?: Array<{ cve?: string; epss?: number; percentile?: number; date?: string }>;
|
|
106
|
+
knownExploited?: Array<{
|
|
107
|
+
cve?: string;
|
|
108
|
+
dateAdded?: string;
|
|
109
|
+
dueDate?: string;
|
|
110
|
+
knownRansomwareCampaignUse?: string;
|
|
111
|
+
}>;
|
|
112
|
+
};
|
|
90
113
|
artifact?: { name?: string; version?: string };
|
|
91
114
|
}>;
|
|
92
115
|
}
|
|
93
116
|
|
|
94
|
-
/** trivy `--format json` output shape (the subset we read). */
|
|
117
|
+
/** trivy `--format json` output shape (the subset we read). Trivy (v0.73, __fixtures__/trivy-with-kev-epss.json) reports no KEV/EPSS data in its JSON output, so a trivy-backed finding carries every exploitability field as `undefined` — the honest "not reported" state, not `false`. */
|
|
95
118
|
interface TrivyOutput {
|
|
96
119
|
Results?: Array<{
|
|
97
120
|
Vulnerabilities?: Array<{
|
|
@@ -109,6 +132,8 @@ export function parseGrypeOutput(stdout: string): VulnFinding[] {
|
|
|
109
132
|
const doc = JSON.parse(stdout) as GrypeOutput;
|
|
110
133
|
return (doc.matches ?? []).map((m) => {
|
|
111
134
|
const fixVersions = m.vulnerability?.fix?.versions ?? [];
|
|
135
|
+
const epss = m.vulnerability?.epss?.[0];
|
|
136
|
+
const kev = m.vulnerability?.knownExploited?.[0];
|
|
112
137
|
return {
|
|
113
138
|
cveId: m.vulnerability?.id ?? "UNKNOWN",
|
|
114
139
|
severity: normalizeSeverity(m.vulnerability?.severity),
|
|
@@ -116,6 +141,16 @@ export function parseGrypeOutput(stdout: string): VulnFinding[] {
|
|
|
116
141
|
installedVersion: m.artifact?.version ?? "",
|
|
117
142
|
fixedVersion: fixVersions[0],
|
|
118
143
|
fixable: m.vulnerability?.fix?.state === "fixed" || fixVersions.length > 0,
|
|
144
|
+
epss: epss?.epss,
|
|
145
|
+
epssPercentile: epss?.percentile,
|
|
146
|
+
// grype omits `knownExploited` for a non-KEV vuln, so `undefined` here
|
|
147
|
+
// means "no KEV annotation reported" — never coerced to `false`.
|
|
148
|
+
inKev: kev ? true : undefined,
|
|
149
|
+
kevDateAdded: kev?.dateAdded,
|
|
150
|
+
kevDueDate: kev?.dueDate,
|
|
151
|
+
// KEV's ransomware field is "known"/"unknown" — "unknown" is not "no",
|
|
152
|
+
// so only an explicit "known" becomes `true`; everything else stays unset.
|
|
153
|
+
kevRansomware: kev?.knownRansomwareCampaignUse?.toLowerCase() === "known" ? true : undefined,
|
|
119
154
|
} satisfies VulnFinding;
|
|
120
155
|
});
|
|
121
156
|
}
|
package/src/config.test.ts
CHANGED
|
@@ -239,6 +239,27 @@ describe("environments (#1166 — string or { name, endpoint })", () => {
|
|
|
239
239
|
});
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
+
describe("vulnPolicy exploitability fields (#1466)", () => {
|
|
243
|
+
test("loads the exploitability fields, zero thresholds included", async () => {
|
|
244
|
+
writeFileSync(
|
|
245
|
+
join(TEST_DIR, "chant.config.json"),
|
|
246
|
+
JSON.stringify({ vulnPolicy: { failOnKev: true, failEpssAtOrAbove: 0, warnEpssAtOrAbove: 0.01, exploitabilityFixableOnly: false } }),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const result = await loadChantConfig(TEST_DIR);
|
|
250
|
+
expect(result.config.vulnPolicy).toEqual({ failOnKev: true, failEpssAtOrAbove: 0, warnEpssAtOrAbove: 0.01, exploitabilityFixableOnly: false });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("rejects an EPSS threshold outside 0.0–1.0", async () => {
|
|
254
|
+
writeFileSync(
|
|
255
|
+
join(TEST_DIR, "chant.config.json"),
|
|
256
|
+
JSON.stringify({ vulnPolicy: { failEpssAtOrAbove: 1.5 } }),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
await expect(loadChantConfig(TEST_DIR)).rejects.toThrow(/failEpssAtOrAbove/);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
242
263
|
describe("environmentName / environmentNames / environmentEndpoint (#1166)", () => {
|
|
243
264
|
test("environmentName reduces either form to its name", () => {
|
|
244
265
|
expect(environmentName("prod")).toBe("prod");
|
package/src/config.ts
CHANGED
|
@@ -106,6 +106,10 @@ export const ChantConfigSchema = z.object({
|
|
|
106
106
|
warnSeverity: z.enum(["critical", "high", "medium", "low", "negligible", "unknown"]).optional(),
|
|
107
107
|
failOnLicense: z.boolean().optional(),
|
|
108
108
|
failOnUnknownSeverity: z.boolean().optional(),
|
|
109
|
+
failOnKev: z.boolean().optional(),
|
|
110
|
+
failEpssAtOrAbove: z.number().min(0).max(1).optional(),
|
|
111
|
+
warnEpssAtOrAbove: z.number().min(0).max(1).optional(),
|
|
112
|
+
exploitabilityFixableOnly: z.boolean().optional(),
|
|
109
113
|
license: z.object({
|
|
110
114
|
allow: z.array(z.string()).optional(),
|
|
111
115
|
deny: z.array(z.string()).optional(),
|
|
@@ -308,6 +312,14 @@ export interface ChantConfig {
|
|
|
308
312
|
failOnLicense?: boolean;
|
|
309
313
|
/** Block on an `unknown`-severity finding (always warned regardless). Default `false`. */
|
|
310
314
|
failOnUnknownSeverity?: boolean;
|
|
315
|
+
/** Block any finding in the CISA KEV catalog, regardless of severity. Default `false`. */
|
|
316
|
+
failOnKev?: boolean;
|
|
317
|
+
/** Block when EPSS is at or above this (0.0–1.0). Omit to ignore EPSS. */
|
|
318
|
+
failEpssAtOrAbove?: number;
|
|
319
|
+
/** Warn (not block) at or above this EPSS. Omit to ignore. */
|
|
320
|
+
warnEpssAtOrAbove?: number;
|
|
321
|
+
/** Apply `fixableOnly` to exploitability blocks too. Default `true`. */
|
|
322
|
+
exploitabilityFixableOnly?: boolean;
|
|
311
323
|
/** License allow/deny lists evaluated against the SBOM's declared licenses. */
|
|
312
324
|
license?: { allow?: string[]; deny?: string[] };
|
|
313
325
|
/** Which real scanner a `ProcessRunner`-backed `vuln-gate`/`scan-vulnerabilities` shells out to. Default `"grype"`. Read where the capability/scanner is constructed. */
|
|
@@ -560,6 +572,12 @@ export function resolveVulnPolicy(config: ChantConfig): Partial<VulnPolicy> {
|
|
|
560
572
|
if (v.warnSeverity) out.warnSeverity = v.warnSeverity;
|
|
561
573
|
if (v.failOnLicense !== undefined) out.failOnLicense = v.failOnLicense;
|
|
562
574
|
if (v.failOnUnknownSeverity !== undefined) out.failOnUnknownSeverity = v.failOnUnknownSeverity;
|
|
575
|
+
// `!== undefined`, never truthiness: `failEpssAtOrAbove: 0` (match any scored
|
|
576
|
+
// finding) and an explicit `failOnKev: false` override must both survive.
|
|
577
|
+
if (v.failOnKev !== undefined) out.failOnKev = v.failOnKev;
|
|
578
|
+
if (v.failEpssAtOrAbove !== undefined) out.failEpssAtOrAbove = v.failEpssAtOrAbove;
|
|
579
|
+
if (v.warnEpssAtOrAbove !== undefined) out.warnEpssAtOrAbove = v.warnEpssAtOrAbove;
|
|
580
|
+
if (v.exploitabilityFixableOnly !== undefined) out.exploitabilityFixableOnly = v.exploitabilityFixableOnly;
|
|
563
581
|
if (v.license) out.license = v.license;
|
|
564
582
|
return out;
|
|
565
583
|
}
|
|
@@ -75,6 +75,25 @@ describe("normalizeDeepProperties", () => {
|
|
|
75
75
|
expect(out.List).toEqual(["x", "y"]);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
test("a value the unresolved hook claims collapses to UNRESOLVED, and only that value", () => {
|
|
79
|
+
const hooks: DeepNormalizationHooks = {
|
|
80
|
+
unresolved: (n) =>
|
|
81
|
+
n.side === "declared" && typeof n.value === "string" && n.value.startsWith("[") && n.value.endsWith("]"),
|
|
82
|
+
};
|
|
83
|
+
const declared = normalizeDeepProperties(
|
|
84
|
+
{ ref: { id: "[resourceId('T', 'x')]" }, name: "n" },
|
|
85
|
+
{ entityType: "T", side: "declared", hooks },
|
|
86
|
+
);
|
|
87
|
+
expect(declared).toEqual({ ref: { id: UNRESOLVED }, name: "n" });
|
|
88
|
+
// The hook sees the side, so the live tree — where the same shape is an
|
|
89
|
+
// evaluated value — is untouched.
|
|
90
|
+
const live = normalizeDeepProperties(
|
|
91
|
+
{ ref: { id: "[literal]" }, name: "n" },
|
|
92
|
+
{ entityType: "T", side: "live", hooks },
|
|
93
|
+
);
|
|
94
|
+
expect(live).toEqual({ ref: { id: "[literal]" }, name: "n" });
|
|
95
|
+
});
|
|
96
|
+
|
|
78
97
|
test("prunes by hook, and prunes the whole subtree", () => {
|
|
79
98
|
const hooks: DeepNormalizationHooks = { prune: (n) => n.pattern === "Status" };
|
|
80
99
|
const out = normalizeDeepProperties(
|
package/src/deep-observation.ts
CHANGED
|
@@ -253,6 +253,19 @@ export interface DeepNormalizationHooks {
|
|
|
253
253
|
* the order the provider returned it.
|
|
254
254
|
*/
|
|
255
255
|
orderKey?(element: DeepArrayElement): string | undefined;
|
|
256
|
+
/**
|
|
257
|
+
* Return true when this value cannot be known without deploying — it is
|
|
258
|
+
* collapsed to {@link UNRESOLVED}, and the diff skips any path whose declared
|
|
259
|
+
* value is UNRESOLVED, exactly as it already does for a non-JSON intrinsic.
|
|
260
|
+
*
|
|
261
|
+
* The pass collapses class-instance intrinsics (`Fn::Sub`, `Ref`) on its own;
|
|
262
|
+
* this hook exists for the lexicons whose unevaluated references are plain
|
|
263
|
+
* JSON data the pass cannot recognize — an ARM template's
|
|
264
|
+
* `"[resourceId(...)]"` expression string is the motivating case (#1213).
|
|
265
|
+
* Left alone, every such reference diffs against its evaluated live value and
|
|
266
|
+
* reads as permanent drift.
|
|
267
|
+
*/
|
|
268
|
+
unresolved?(node: DeepNode): boolean;
|
|
256
269
|
}
|
|
257
270
|
|
|
258
271
|
/** Everything the pass needs besides the tree itself. */
|
|
@@ -371,25 +384,31 @@ export function normalizeDeepProperties(
|
|
|
371
384
|
): Record<string, unknown> {
|
|
372
385
|
const { entityType, side, hooks, counterpartPaths } = options;
|
|
373
386
|
|
|
387
|
+
const nodeOf = (path: string, pattern: string, key: string, value: unknown): DeepNode => ({
|
|
388
|
+
entityType,
|
|
389
|
+
path,
|
|
390
|
+
pattern,
|
|
391
|
+
key,
|
|
392
|
+
value,
|
|
393
|
+
side,
|
|
394
|
+
counterpart: !counterpartPaths
|
|
395
|
+
? "unknown"
|
|
396
|
+
: counterpartPaths.has(path) || counterpartPaths.has(pattern)
|
|
397
|
+
? "present"
|
|
398
|
+
: "absent",
|
|
399
|
+
});
|
|
400
|
+
|
|
374
401
|
const prune = (path: string, pattern: string, key: string, value: unknown): boolean => {
|
|
375
402
|
if (!hooks?.prune) return false;
|
|
376
|
-
return hooks.prune(
|
|
377
|
-
entityType,
|
|
378
|
-
path,
|
|
379
|
-
pattern,
|
|
380
|
-
key,
|
|
381
|
-
value,
|
|
382
|
-
side,
|
|
383
|
-
counterpart: !counterpartPaths
|
|
384
|
-
? "unknown"
|
|
385
|
-
: counterpartPaths.has(path) || counterpartPaths.has(pattern)
|
|
386
|
-
? "present"
|
|
387
|
-
: "absent",
|
|
388
|
-
});
|
|
403
|
+
return hooks.prune(nodeOf(path, pattern, key, value));
|
|
389
404
|
};
|
|
390
405
|
|
|
391
406
|
const normalizeValue = (value: unknown, path: string, pattern: string, key: string): unknown => {
|
|
392
407
|
if (isSensitiveKey(key)) return MASKED;
|
|
408
|
+
// A value the lexicon says cannot be known without deploying — an
|
|
409
|
+
// expression-string reference — collapses exactly like a class-instance
|
|
410
|
+
// intrinsic does below.
|
|
411
|
+
if (hooks?.unresolved?.(nodeOf(path, pattern, key, value))) return UNRESOLVED;
|
|
393
412
|
if (isJsonPrimitive(value)) return value;
|
|
394
413
|
|
|
395
414
|
// A PROPERTY-kind Declarable is authored data wearing a class, not an
|
package/src/lexicon.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Serializer } from "./serializer";
|
|
2
|
+
import type { Declarable } from "./declarable";
|
|
2
3
|
import type { LintRule } from "./lint/rule";
|
|
3
4
|
import type { RuleSpec } from "./lint/declarative";
|
|
4
5
|
import type { PostSynthCheck } from "./lint/post-synth";
|
|
5
6
|
import type { TemplateParser, TemplateIR } from "./import/parser";
|
|
6
7
|
import type { TypeScriptGenerator } from "./import/generator";
|
|
7
8
|
import type { ArtifactIntegrity } from "./lexicon-integrity";
|
|
9
|
+
import type { OkfFile } from "./okf";
|
|
8
10
|
import type { CompletionContext, CompletionItem, HoverContext, HoverInfo, CodeActionContext, CodeAction } from "./lsp/types";
|
|
9
11
|
import type { McpToolContribution, McpResourceContribution } from "./mcp/types";
|
|
10
12
|
import type { DriverComponent } from "./components/driver";
|
|
@@ -122,6 +124,8 @@ export interface BundleSpec {
|
|
|
122
124
|
typesDTS: string;
|
|
123
125
|
rules: Map<string, string>;
|
|
124
126
|
skills: Map<string, string>;
|
|
127
|
+
/** OKF knowledge bundle over the registry and rules (#1060), written to dist/okf/. */
|
|
128
|
+
okf?: OkfFile[];
|
|
125
129
|
integrity?: ArtifactIntegrity;
|
|
126
130
|
metadata?: PackageMetadata;
|
|
127
131
|
}
|
|
@@ -421,6 +425,31 @@ export interface StackStatusObservation {
|
|
|
421
425
|
healthy?: boolean;
|
|
422
426
|
}
|
|
423
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Context for a lexicon's {@link LexiconPlugin.buildRoots} hook (#1548).
|
|
430
|
+
*/
|
|
431
|
+
export interface BuildRootContext {
|
|
432
|
+
/**
|
|
433
|
+
* The directory the project configuration was loaded from (where
|
|
434
|
+
* `chant.config.*` lives) — relative root paths resolve against it, NOT
|
|
435
|
+
* against `sourceDir`, because a non-chant-source root (a kustomize
|
|
436
|
+
* overlay tree) usually lives beside the typed source rather than inside
|
|
437
|
+
* it.
|
|
438
|
+
*/
|
|
439
|
+
projectRoot: string;
|
|
440
|
+
/** The resolved project configuration, for the lexicon's own namespace. */
|
|
441
|
+
config: Record<string, unknown>;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* What a build-root render contributes to the build: entities keyed like
|
|
446
|
+
* discovered ones, plus non-fatal diagnostics.
|
|
447
|
+
*/
|
|
448
|
+
export interface BuildRootContribution {
|
|
449
|
+
entities: Map<string, Declarable>;
|
|
450
|
+
warnings?: string[];
|
|
451
|
+
}
|
|
452
|
+
|
|
424
453
|
export interface LexiconPlugin {
|
|
425
454
|
// ── Required ──────────────────────────────────────────────
|
|
426
455
|
/** Human-readable name (e.g. "aws", "gcp") */
|
|
@@ -554,6 +583,27 @@ export interface LexiconPlugin {
|
|
|
554
583
|
options?: ComponentPipelineOptions,
|
|
555
584
|
): ComponentPipelineResult;
|
|
556
585
|
|
|
586
|
+
/**
|
|
587
|
+
* Render this lexicon's config-declared build roots into entities (#1548
|
|
588
|
+
* piece 3) — the seam for a build root that is NOT typed chant source. The
|
|
589
|
+
* k8s lexicon implements it for `k8s.kustomize.roots`: each named
|
|
590
|
+
* kustomization dir renders (`kustomize build`) at build time and the
|
|
591
|
+
* documents come back as entities, which the build merges into the
|
|
592
|
+
* discovered set BEFORE partitioning — so they are serialized by this
|
|
593
|
+
* lexicon's serializer (ownership stamping included), seen by post-synth
|
|
594
|
+
* checks, and observed by `describeResources` exactly like discovered
|
|
595
|
+
* entities.
|
|
596
|
+
*
|
|
597
|
+
* Called once per top-level build (never for nested child projects), only
|
|
598
|
+
* when the CLI wires it up (`collectBuildRootContributors` in
|
|
599
|
+
* ./cli/plugins.ts → {@link BuildOptions.buildRoots}). A hook that has
|
|
600
|
+
* nothing configured must return an empty map, not throw; a hook that
|
|
601
|
+
* cannot render (missing binary, missing dir) should throw with a message
|
|
602
|
+
* naming the problem — the build reports it as a build error, not a stack
|
|
603
|
+
* trace. Omit for lexicons with no non-source build-root concept.
|
|
604
|
+
*/
|
|
605
|
+
buildRoots?(ctx: BuildRootContext): Promise<BuildRootContribution>;
|
|
606
|
+
|
|
557
607
|
// LSP
|
|
558
608
|
/** Provide completions for LSP */
|
|
559
609
|
completionProvider?(ctx: CompletionContext): CompletionItem[];
|
|
@@ -64,6 +64,24 @@ describe("observeResources", () => {
|
|
|
64
64
|
expect(names).toEqual(["web-vpc"]);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
it("keeps non-resource declarables out of the observation universe", async () => {
|
|
68
|
+
// Outputs, parameters and serializer directives (gcp's defaultAnnotations)
|
|
69
|
+
// have no `props` and no live counterpart — a declared name the reader can
|
|
70
|
+
// never resolve would read as unobserved or missing forever.
|
|
71
|
+
const buildResult = {
|
|
72
|
+
outputs: new Map<string, string>([["aws", "{}"]]),
|
|
73
|
+
entities: new Map<string, unknown>([
|
|
74
|
+
["web-vpc", { lexicon: "aws", entityType: "AWS::EC2::VPC", props: {} }],
|
|
75
|
+
["annotations", { lexicon: "aws", entityType: "chant:aws:directive" }],
|
|
76
|
+
]),
|
|
77
|
+
errors: [],
|
|
78
|
+
} as unknown as BuildResult;
|
|
79
|
+
let names: string[] = [];
|
|
80
|
+
const plugins = [awsPlugin(({ entityNames }) => { names = entityNames; return {}; })];
|
|
81
|
+
await observeResources("prod", plugins, buildResult);
|
|
82
|
+
expect(names).toEqual(["web-vpc"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
67
85
|
it("collects a throwing plugin into errors instead of failing the whole graph, and reports its entities unobserved (#1089)", async () => {
|
|
68
86
|
const plugins = [
|
|
69
87
|
awsPlugin(() => { throw new Error("access denied"); }),
|
package/src/lifecycle/observe.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "../observation";
|
|
26
26
|
import { zeroResourcesWarning } from "../live-endpoint";
|
|
27
27
|
import { unqualifiedKey } from "./identity";
|
|
28
|
+
import { isResourceDeclarable } from "../declarable";
|
|
28
29
|
|
|
29
30
|
export interface ObserveResult {
|
|
30
31
|
observations: LiveObservation[];
|
|
@@ -123,10 +124,16 @@ export async function observeResources(
|
|
|
123
124
|
const entities = new Map<string, { entityType: string; props: Record<string, unknown> }>();
|
|
124
125
|
for (const [name, entity] of buildResult.entities) {
|
|
125
126
|
if (entity.lexicon !== plugin.name) continue;
|
|
127
|
+
// Only resource declarables have a live counterpart to observe. Outputs,
|
|
128
|
+
// parameters and serializer directives (gcp's `defaultAnnotations`) are
|
|
129
|
+
// build-time inputs — declared, but with nothing in any cloud to compare
|
|
130
|
+
// against, so keeping them in the universe makes every diff report a
|
|
131
|
+
// hole (or worse, a deletion) for a resource that cannot exist.
|
|
132
|
+
if (!isResourceDeclarable(entity)) continue;
|
|
126
133
|
entityNames.push(name);
|
|
127
134
|
entities.set(name, {
|
|
128
135
|
entityType: entity.entityType,
|
|
129
|
-
props: (
|
|
136
|
+
props: (entity.props != null ? entity.props : {}) as Record<string, unknown>,
|
|
130
137
|
});
|
|
131
138
|
}
|
|
132
139
|
|
|
@@ -145,11 +152,11 @@ export async function observeResources(
|
|
|
145
152
|
stackEntityNames = [];
|
|
146
153
|
stackEntities = new Map();
|
|
147
154
|
for (const [name, entity] of sb.entities) {
|
|
148
|
-
if (entity.lexicon !== plugin.name) continue;
|
|
155
|
+
if (entity.lexicon !== plugin.name || !isResourceDeclarable(entity)) continue;
|
|
149
156
|
stackEntityNames.push(name);
|
|
150
157
|
stackEntities.set(name, {
|
|
151
158
|
entityType: entity.entityType,
|
|
152
|
-
props: (
|
|
159
|
+
props: (entity.props != null ? entity.props : {}) as Record<string, unknown>,
|
|
153
160
|
});
|
|
154
161
|
}
|
|
155
162
|
const raw = sb.outputs.get(plugin.name);
|
|
@@ -18,6 +18,7 @@ import { formatUnobserved, normalizeObservation, unobservedAll, type UnobservedE
|
|
|
18
18
|
// property tree is ever rendered or committed. Two lists would eventually
|
|
19
19
|
// disagree about what counts as a secret.
|
|
20
20
|
import { isSensitiveKey } from "../deep-observation";
|
|
21
|
+
import { isResourceDeclarable } from "../declarable";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Check for potential sensitive data in resource attributes and return warnings.
|
|
@@ -125,13 +126,13 @@ export async function takeSnapshot(
|
|
|
125
126
|
const entityNames: string[] = [];
|
|
126
127
|
const entities = new Map<string, { entityType: string; props: Record<string, unknown> }>();
|
|
127
128
|
for (const [name, entity] of buildResult.entities) {
|
|
128
|
-
|
|
129
|
+
// Resource declarables only — outputs, parameters and serializer
|
|
130
|
+
// directives have no live counterpart (see lifecycle/observe.ts).
|
|
131
|
+
if (entity.lexicon === plugin.name && isResourceDeclarable(entity)) {
|
|
129
132
|
entityNames.push(name);
|
|
130
133
|
entities.set(name, {
|
|
131
134
|
entityType: entity.entityType,
|
|
132
|
-
props: (
|
|
133
|
-
? entity.props
|
|
134
|
-
: {}) as Record<string, unknown>,
|
|
135
|
+
props: (entity.props != null ? entity.props : {}) as Record<string, unknown>,
|
|
135
136
|
});
|
|
136
137
|
}
|
|
137
138
|
}
|
package/src/okf.test.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { buildOkfBundle, okfConformanceProblems, splitFrontmatter, OKF_VERSION, type OkfFile } from "./okf";
|
|
5
|
+
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
6
|
+
import { setProvenance } from "./provenance";
|
|
7
|
+
import { parseYAML } from "./yaml";
|
|
8
|
+
import { discover } from "./discovery/index";
|
|
9
|
+
|
|
10
|
+
function decl<T extends object>(base: T): Declarable & T {
|
|
11
|
+
return { [DECLARABLE_MARKER]: true, ...base } as Declarable & T;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function fileMap(bundle: OkfFile[]): Map<string, string> {
|
|
15
|
+
return new Map(bundle.map((f) => [f.path, f.content]));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Assert the four OKF v0.2 conformance criteria (spec §11) over a bundle via
|
|
20
|
+
* the shared checker (`okfConformanceProblems`) — the same one the lexicon
|
|
21
|
+
* bundle tests (#1060) run — plus the version pin our own emitter guarantees.
|
|
22
|
+
*/
|
|
23
|
+
function assertConformant(bundle: OkfFile[]): void {
|
|
24
|
+
expect(okfConformanceProblems(bundle)).toEqual([]);
|
|
25
|
+
const index = bundle.find((f) => f.path === "index.md");
|
|
26
|
+
expect(index).toBeDefined();
|
|
27
|
+
const split = splitFrontmatter(index!.content);
|
|
28
|
+
expect(split).toBeDefined();
|
|
29
|
+
expect(parseYAML(split!.frontmatter)).toEqual({ okf_version: OKF_VERSION });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("buildOkfBundle", () => {
|
|
33
|
+
test("emits one concept per entity with parseable frontmatter and a non-empty type", () => {
|
|
34
|
+
const bucket = decl({ lexicon: "aws", entityType: "AWS::S3::Bucket", kind: "resource" as const });
|
|
35
|
+
setProvenance(bucket, { sourceFile: "/proj/src/storage.ts" });
|
|
36
|
+
const vpc = decl({ lexicon: "gcp", entityType: "Vpc" });
|
|
37
|
+
const entities = new Map<string, Declarable>([
|
|
38
|
+
["myBucket", bucket],
|
|
39
|
+
["vpc", vpc],
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const bundle = buildOkfBundle({ entities, dependencies: new Map() }, "/proj");
|
|
43
|
+
const files = fileMap(bundle);
|
|
44
|
+
expect([...files.keys()]).toEqual(["aws/myBucket.md", "gcp/vpc.md", "index.md"]);
|
|
45
|
+
|
|
46
|
+
const concept = splitFrontmatter(files.get("aws/myBucket.md")!)!;
|
|
47
|
+
const front = parseYAML(concept.frontmatter);
|
|
48
|
+
expect(front.type).toBe("AWS::S3::Bucket");
|
|
49
|
+
expect(front.title).toBe("myBucket");
|
|
50
|
+
expect(front.lexicon).toBe("aws");
|
|
51
|
+
expect(front.kind).toBe("resource");
|
|
52
|
+
expect(front.source).toBe("src/storage.ts");
|
|
53
|
+
expect(concept.body).toContain("Declared in `src/storage.ts`.");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("dependency edges become bundle-relative markdown links, both directions", () => {
|
|
57
|
+
const vpc = decl({ lexicon: "gcp", entityType: "Vpc" });
|
|
58
|
+
const subnet = decl({ lexicon: "gcp", entityType: "Subnet" });
|
|
59
|
+
const entities = new Map<string, Declarable>([
|
|
60
|
+
["vpc", vpc],
|
|
61
|
+
["subnet", subnet],
|
|
62
|
+
]);
|
|
63
|
+
const dependencies = new Map([["subnet", new Set(["vpc"])]]);
|
|
64
|
+
|
|
65
|
+
const files = fileMap(buildOkfBundle({ entities, dependencies }));
|
|
66
|
+
expect(files.get("gcp/subnet.md")).toContain("## Depends on");
|
|
67
|
+
expect(files.get("gcp/subnet.md")).toContain("- [vpc](/gcp/vpc.md)");
|
|
68
|
+
expect(files.get("gcp/vpc.md")).toContain("## Referenced by");
|
|
69
|
+
expect(files.get("gcp/vpc.md")).toContain("- [subnet](/gcp/subnet.md)");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("an unresolved dependency emits a link rather than failing — broken links are permitted", () => {
|
|
73
|
+
const app = decl({ lexicon: "k8s", entityType: "Deployment" });
|
|
74
|
+
const entities = new Map<string, Declarable>([["app", app]]);
|
|
75
|
+
const dependencies = new Map([["app", new Set(["ghost"])]]);
|
|
76
|
+
|
|
77
|
+
const bundle = buildOkfBundle({ entities, dependencies });
|
|
78
|
+
expect(fileMap(bundle).get("k8s/app.md")).toContain("- [ghost](/ghost.md)");
|
|
79
|
+
assertConformant(bundle);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("index.md groups entities into per-lexicon sections with okf_version frontmatter", () => {
|
|
83
|
+
const bucket = decl({ lexicon: "aws", entityType: "AWS::S3::Bucket" });
|
|
84
|
+
const job = decl({ lexicon: "gitlab", entityType: "GitLab::Job" });
|
|
85
|
+
const entities = new Map<string, Declarable>([
|
|
86
|
+
["assets", bucket],
|
|
87
|
+
["buildJob", job],
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const index = fileMap(buildOkfBundle({ entities, dependencies: new Map() })).get("index.md")!;
|
|
91
|
+
expect(index).toContain(`okf_version: '${OKF_VERSION}'`);
|
|
92
|
+
expect(index).toContain("# Lexicon: aws");
|
|
93
|
+
expect(index).toContain("# Lexicon: gitlab");
|
|
94
|
+
expect(index).toContain("* [assets](/aws/assets.md) - aws resource of type AWS::S3::Bucket");
|
|
95
|
+
expect(index).toContain("* [buildJob](/gitlab/buildJob.md) - gitlab resource of type GitLab::Job");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("composite provenance rides along as extra frontmatter keys", () => {
|
|
99
|
+
const dep = decl({ lexicon: "k8s", entityType: "K8s::Apps::Deployment" });
|
|
100
|
+
setProvenance(dep, { sourceFile: "/proj/src/web.ts", composite: "WebApp", compositeInstance: "prodApp" });
|
|
101
|
+
const entities = new Map<string, Declarable>([["prodAppDeployment", dep]]);
|
|
102
|
+
|
|
103
|
+
const front = parseYAML(
|
|
104
|
+
splitFrontmatter(fileMap(buildOkfBundle({ entities, dependencies: new Map() }, "/proj")).get("k8s/prodAppDeployment.md")!)!.frontmatter,
|
|
105
|
+
);
|
|
106
|
+
expect(front.composite).toBe("WebApp");
|
|
107
|
+
expect(front.composite_instance).toBe("prodApp");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("entity names that slug to the same path are deduped, never overwritten", () => {
|
|
111
|
+
const a = decl({ lexicon: "aws", entityType: "AWS::S3::Bucket" });
|
|
112
|
+
const b = decl({ lexicon: "aws", entityType: "AWS::S3::Bucket" });
|
|
113
|
+
const entities = new Map<string, Declarable>([
|
|
114
|
+
["my/bucket", a],
|
|
115
|
+
["my-bucket", b],
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const paths = buildOkfBundle({ entities, dependencies: new Map() }).map((f) => f.path);
|
|
119
|
+
expect(new Set(paths).size).toBe(paths.length);
|
|
120
|
+
expect(paths).toContain("aws/my-bucket.md");
|
|
121
|
+
expect(paths).toContain("aws/my-bucket-2.md");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("an empty project still emits a conformant bundle (just the index)", () => {
|
|
125
|
+
const bundle = buildOkfBundle({ entities: new Map(), dependencies: new Map() });
|
|
126
|
+
expect(bundle.map((f) => f.path)).toEqual(["index.md"]);
|
|
127
|
+
assertConformant(bundle);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("deterministic: the same input yields byte-identical files", () => {
|
|
131
|
+
const vpc = decl({ lexicon: "gcp", entityType: "Vpc" });
|
|
132
|
+
const subnet = decl({ lexicon: "gcp", entityType: "Subnet" });
|
|
133
|
+
const entities = new Map<string, Declarable>([
|
|
134
|
+
["vpc", vpc],
|
|
135
|
+
["subnet", subnet],
|
|
136
|
+
]);
|
|
137
|
+
const dependencies = new Map([["subnet", new Set(["vpc"])]]);
|
|
138
|
+
|
|
139
|
+
expect(buildOkfBundle({ entities, dependencies })).toEqual(buildOkfBundle({ entities, dependencies }));
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Shipped examples, spanning lexicons (#1058 acceptance): each bundle is
|
|
145
|
+
// asserted conformant against the four v0.2 criteria and snapshot-tested.
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
const repoRoot = resolve(fileURLToPath(import.meta.url), "../../../..");
|
|
149
|
+
|
|
150
|
+
const exampleCases: Array<[string, string]> = [
|
|
151
|
+
["k8s layered-config", "lexicons/k8s/examples/layered-config/src"],
|
|
152
|
+
["aws lambda-s3", "lexicons/aws/examples/lambda-s3/src"],
|
|
153
|
+
["gitlab node-pipeline", "lexicons/gitlab/examples/node-pipeline/src"],
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
describe("buildOkfBundle over shipped examples", () => {
|
|
157
|
+
for (const [name, rel] of exampleCases) {
|
|
158
|
+
test(`${name} emits a conformant, snapshot-stable bundle`, async () => {
|
|
159
|
+
const projectPath = resolve(repoRoot, rel);
|
|
160
|
+
const result = await discover(projectPath);
|
|
161
|
+
expect(result.errors).toEqual([]);
|
|
162
|
+
expect(result.entities.size).toBeGreaterThan(0);
|
|
163
|
+
|
|
164
|
+
const bundle = buildOkfBundle(result, projectPath);
|
|
165
|
+
assertConformant(bundle);
|
|
166
|
+
expect(bundle).toMatchSnapshot();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|