@intentius/chant 0.45.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/core.d.ts +21 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +3 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +14 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
- package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +13 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +10 -0
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +8 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/config.d.ts +54 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +15 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +223 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +132 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +12 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rule.d.ts +31 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
- package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +4 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/builders.d.ts +134 -8
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/types.d.ts +31 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +11 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +11 -2
- package/src/audit/rules-doc.ts +11 -1
- package/src/build.test.ts +41 -0
- package/src/build.ts +34 -6
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/audit.test.ts +53 -0
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.test.ts +211 -0
- package/src/cli/commands/build.ts +126 -8
- package/src/cli/commands/check-lexicon.test.ts +45 -1
- package/src/cli/commands/check-lexicon.ts +45 -0
- package/src/cli/commands/lexicon-surface-diff.ts +9 -0
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +32 -7
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.ts +4 -4
- package/src/cli/handlers/lifecycle.test.ts +345 -1
- package/src/cli/handlers/lifecycle.ts +300 -11
- package/src/cli/handlers/search.ts +5 -2
- package/src/cli/main.ts +12 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +58 -1
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +7 -0
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/lexicon-regen.ts +19 -1
- package/src/codegen/validate.test.ts +33 -0
- package/src/codegen/validate.ts +21 -2
- package/src/components/auto-release.ts +6 -0
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +8 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/config.test.ts +55 -0
- package/src/config.ts +88 -1
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/fold/subset.test.ts +26 -0
- package/src/fold/subset.ts +45 -19
- package/src/index.ts +4 -0
- package/src/lexicon.ts +227 -2
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/index.ts +3 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/teardown.test.ts +568 -0
- package/src/lifecycle/teardown.ts +359 -0
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/engine.ts +7 -1
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.ts +15 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rule.ts +23 -0
- package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
- package/src/lint/rules/cor021-env-literal-name.ts +114 -0
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +27 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +8 -1
- package/src/lint/rules/index.ts +10 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/builders.ts +179 -8
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/index.ts +10 -3
- package/src/op/local-executor.ts +172 -25
- package/src/op/op.test.ts +25 -2
- package/src/op/receipt-store.ts +211 -0
- package/src/op/types.ts +33 -1
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +12 -0
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
import { resolve } from "path";
|
|
1
|
+
import { resolve, dirname } from "path";
|
|
2
2
|
import { discover } from "../../../discovery/index";
|
|
3
3
|
import { buildOkfBundle, OKF_VERSION } from "../../../okf";
|
|
4
|
+
import { loadOkfBundle, bindConcepts, type OkfConcept } from "../../../okf-read";
|
|
5
|
+
import { loadChantConfigUpward, resolveKnowledgeDir } from "../../../config";
|
|
6
|
+
|
|
7
|
+
/** One bound concept as surfaced in an explain knowledge section — the trio the #1867 design calls for, not the full {@link OkfConcept}. */
|
|
8
|
+
export interface ExplainKnowledgeEntry {
|
|
9
|
+
type: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
path: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toKnowledgeEntry(concept: OkfConcept): ExplainKnowledgeEntry {
|
|
15
|
+
return { type: concept.type, title: concept.title, path: concept.path };
|
|
16
|
+
}
|
|
4
17
|
|
|
5
18
|
/**
|
|
6
19
|
* Explain tool definition for MCP
|
|
@@ -46,6 +59,25 @@ export async function handleExplain(params: Record<string, unknown>): Promise<un
|
|
|
46
59
|
};
|
|
47
60
|
}
|
|
48
61
|
|
|
62
|
+
// Authored knowledge (#1867, design #1059): bind the project's OKF bundle
|
|
63
|
+
// (if any) to the discovered entities and carry each bound concept's
|
|
64
|
+
// type/title/path into the summary. A project with no `knowledge/` yet
|
|
65
|
+
// binds nothing — `bound` is empty — so the section is omitted below
|
|
66
|
+
// rather than rendered empty; this is the CLI and MCP tool's one shared
|
|
67
|
+
// code path, so both surfaces match by construction.
|
|
68
|
+
// `loadChantConfigUpward` walks up from `infraPath` (chant #1117) — a
|
|
69
|
+
// `chant explain src/<stack>` two levels below `chant.config.ts` must find
|
|
70
|
+
// the same `knowledge/` a `chant explain .` at the root would, matching
|
|
71
|
+
// `build.ts`'s `configDir` derivation for the same reason.
|
|
72
|
+
const loaded = await loadChantConfigUpward(infraPath);
|
|
73
|
+
const projectRoot = loaded.configPath ? dirname(loaded.configPath) : infraPath;
|
|
74
|
+
const knowledgeDir = resolveKnowledgeDir(loaded.config, projectRoot);
|
|
75
|
+
const bundle = await loadOkfBundle(knowledgeDir);
|
|
76
|
+
const { bound } = bindConcepts(bundle, result.entities);
|
|
77
|
+
const knowledge = new Map<string, ExplainKnowledgeEntry[]>(
|
|
78
|
+
Array.from(bound.entries()).map(([name, concepts]) => [name, concepts.map(toKnowledgeEntry)]),
|
|
79
|
+
);
|
|
80
|
+
|
|
49
81
|
// Group entities by lexicon and kind
|
|
50
82
|
const byLexicon = new Map<string, { resources: string[]; properties: string[] }>();
|
|
51
83
|
|
|
@@ -89,7 +121,11 @@ export async function handleExplain(params: Record<string, unknown>): Promise<un
|
|
|
89
121
|
};
|
|
90
122
|
|
|
91
123
|
if (format === "json") {
|
|
92
|
-
|
|
124
|
+
// Omitted entirely when no entity bound a concept — "cleanly absent" per
|
|
125
|
+
// #1867's AC, not an empty `{}`.
|
|
126
|
+
return knowledge.size > 0
|
|
127
|
+
? { ...summary, knowledge: Object.fromEntries(knowledge) }
|
|
128
|
+
: summary;
|
|
93
129
|
}
|
|
94
130
|
|
|
95
131
|
// Markdown format
|
|
@@ -133,6 +169,19 @@ export async function handleExplain(params: Record<string, unknown>): Promise<un
|
|
|
133
169
|
lines.push("");
|
|
134
170
|
}
|
|
135
171
|
|
|
172
|
+
if (knowledge.size > 0) {
|
|
173
|
+
lines.push("## Knowledge");
|
|
174
|
+
lines.push("");
|
|
175
|
+
for (const [name, entries] of knowledge) {
|
|
176
|
+
lines.push(`### \`${name}\``);
|
|
177
|
+
for (const entry of entries) {
|
|
178
|
+
const title = entry.title ? `${entry.title} ` : "";
|
|
179
|
+
lines.push(`- ${entry.type || "concept"}: ${title}(\`${entry.path}\`)`);
|
|
180
|
+
}
|
|
181
|
+
lines.push("");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
136
185
|
if (result.errors.length > 0) {
|
|
137
186
|
lines.push("## Errors");
|
|
138
187
|
lines.push("");
|
package/src/cli/plugins.ts
CHANGED
|
@@ -106,10 +106,12 @@ export function collectBuildRootContributors(
|
|
|
106
106
|
plugins: readonly LexiconPlugin[] | undefined,
|
|
107
107
|
config: Record<string, unknown>,
|
|
108
108
|
projectRoot: string,
|
|
109
|
-
): Array<
|
|
109
|
+
): Array<import("../lexicon").BuildRootContributor> {
|
|
110
110
|
return (plugins ?? [])
|
|
111
111
|
.filter((plugin) => typeof plugin.buildRoots === "function")
|
|
112
|
-
|
|
112
|
+
// `entities` is not bindable here — discovery has not run — so the merge
|
|
113
|
+
// hands it in when it calls the closure (#1828 / SOPS provenance).
|
|
114
|
+
.map((plugin) => (ctx) => plugin.buildRoots!({ projectRoot, config, entities: ctx?.entities }));
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/**
|
package/src/cli/registry.ts
CHANGED
|
@@ -85,6 +85,13 @@ export interface ParsedArgs {
|
|
|
85
85
|
namespace?: string;
|
|
86
86
|
/** `chant lifecycle rollback --dry-run` — compute the rollback delta and print it; open no PR, push nothing, leave no branch. */
|
|
87
87
|
dryRun?: boolean;
|
|
88
|
+
/** `chant lifecycle teardown <env> --yes` — execute the planned deletion
|
|
89
|
+
* (#1222). Without it the command plans and stops. */
|
|
90
|
+
yes?: boolean;
|
|
91
|
+
/** `chant lifecycle teardown <env> --yes --confirm-prod` — the non-interactive
|
|
92
|
+
* form of the extra confirmation a production-like environment name demands
|
|
93
|
+
* (#1222). Meaningless without `--yes`. */
|
|
94
|
+
confirmProd?: boolean;
|
|
88
95
|
/** `chant import --verbatim` — keep server-defaulted fields in live import */
|
|
89
96
|
verbatim?: boolean;
|
|
90
97
|
/** `chant lifecycle … --src <dir>` — build root override for lifecycle commands */
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { formatStylish, formatSummary, formatJson, formatSarif } from "./stylish";
|
|
3
3
|
import type { LintDiagnostic, LintRule } from "../../lint/rule";
|
|
4
|
+
import type { OkfBundle, OkfConcept } from "../../okf-read";
|
|
5
|
+
|
|
6
|
+
function concept(overrides: Partial<OkfConcept> & { path: string }): OkfConcept {
|
|
7
|
+
return {
|
|
8
|
+
type: "decision",
|
|
9
|
+
binds: [],
|
|
10
|
+
frontmatter: {},
|
|
11
|
+
body: "",
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
4
15
|
|
|
5
16
|
describe("formatStylish", () => {
|
|
6
17
|
const originalNoColor = process.env.NO_COLOR;
|
|
@@ -123,6 +134,149 @@ describe("formatStylish", () => {
|
|
|
123
134
|
});
|
|
124
135
|
});
|
|
125
136
|
|
|
137
|
+
describe("formatStylish suppressed section (#1866, design #1059)", () => {
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
process.env.NO_COLOR = "1";
|
|
140
|
+
});
|
|
141
|
+
afterEach(() => {
|
|
142
|
+
delete process.env.NO_COLOR;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const suppressedDiag: LintDiagnostic & { reason?: string } = {
|
|
146
|
+
file: "src/storage.ts",
|
|
147
|
+
line: 12,
|
|
148
|
+
column: 1,
|
|
149
|
+
ruleId: "AWS021",
|
|
150
|
+
severity: "warning",
|
|
151
|
+
message: "Bucket ACL is public-read",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
test("no section when there are no suppressed diagnostics", () => {
|
|
155
|
+
const result = formatStylish([], []);
|
|
156
|
+
expect(result).not.toContain("Suppressed");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("renders a Suppressed section when suppressed diagnostics exist", () => {
|
|
160
|
+
const result = formatStylish([], [{ ...suppressedDiag, reason: "backwards compat" }]);
|
|
161
|
+
|
|
162
|
+
expect(result).toContain("Suppressed");
|
|
163
|
+
expect(result).toContain("src/storage.ts");
|
|
164
|
+
expect(result).toContain("AWS021");
|
|
165
|
+
expect(result).toContain("backwards compat");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("renders the section even when there are no active diagnostics", () => {
|
|
169
|
+
const result = formatStylish([], [suppressedDiag]);
|
|
170
|
+
expect(result).toContain("Suppressed");
|
|
171
|
+
// No active errors/warnings were counted from a suppressed diagnostic
|
|
172
|
+
expect(result).toContain("No problems found");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("an okf: citation resolves to the concept's title and bundle-relative path", () => {
|
|
176
|
+
const bundle: OkfBundle = {
|
|
177
|
+
concepts: [
|
|
178
|
+
concept({
|
|
179
|
+
path: "decisions/public-assets.md",
|
|
180
|
+
title: "Public asset bucket stays world-readable",
|
|
181
|
+
}),
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const result = formatStylish(
|
|
186
|
+
[],
|
|
187
|
+
[{ ...suppressedDiag, reason: "okf:/decisions/public-assets.md" }],
|
|
188
|
+
bundle,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(result).toContain("Public asset bucket stays world-readable");
|
|
192
|
+
expect(result).toContain("decisions/public-assets.md");
|
|
193
|
+
expect(result).not.toContain("okf:/decisions/public-assets.md");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("also resolves a citation without a leading slash", () => {
|
|
197
|
+
const bundle: OkfBundle = {
|
|
198
|
+
concepts: [concept({ path: "decisions/public-assets.md", title: "Public assets" })],
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const result = formatStylish(
|
|
202
|
+
[],
|
|
203
|
+
[{ ...suppressedDiag, reason: "okf:decisions/public-assets.md" }],
|
|
204
|
+
bundle,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(result).toContain("Public assets");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("an unresolvable citation prints the raw reason with a warning", () => {
|
|
211
|
+
const bundle: OkfBundle = { concepts: [] };
|
|
212
|
+
|
|
213
|
+
const result = formatStylish(
|
|
214
|
+
[],
|
|
215
|
+
[{ ...suppressedDiag, reason: "okf:/decisions/does-not-exist.md" }],
|
|
216
|
+
bundle,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(result).toContain("okf:/decisions/does-not-exist.md");
|
|
220
|
+
expect(result).toContain("unresolved okf citation");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("an okf: citation with no bundle loaded prints the raw reason with a warning", () => {
|
|
224
|
+
const result = formatStylish([], [{ ...suppressedDiag, reason: "okf:/decisions/public-assets.md" }]);
|
|
225
|
+
|
|
226
|
+
expect(result).toContain("okf:/decisions/public-assets.md");
|
|
227
|
+
expect(result).toContain("unresolved okf citation");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("a plain (non-okf) reason renders as-is, unaffected by a loaded bundle", () => {
|
|
231
|
+
const bundle: OkfBundle = {
|
|
232
|
+
concepts: [concept({ path: "decisions/public-assets.md", title: "Public assets" })],
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const result = formatStylish([], [{ ...suppressedDiag, reason: "grandfathered, see #412" }], bundle);
|
|
236
|
+
|
|
237
|
+
expect(result).toContain("grandfathered, see #412");
|
|
238
|
+
expect(result).not.toContain("Public assets");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("a suppressed diagnostic with no reason still renders", () => {
|
|
242
|
+
const result = formatStylish([], [suppressedDiag]);
|
|
243
|
+
expect(result).toContain("AWS021");
|
|
244
|
+
expect(result).toContain("suppressed");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("active diagnostics and the suppressed section coexist", () => {
|
|
248
|
+
const active: LintDiagnostic = {
|
|
249
|
+
file: "src/other.ts",
|
|
250
|
+
line: 1,
|
|
251
|
+
column: 1,
|
|
252
|
+
ruleId: "COR001",
|
|
253
|
+
severity: "error",
|
|
254
|
+
message: "Active problem",
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const result = formatStylish([active], [{ ...suppressedDiag, reason: "okf:/decisions/public-assets.md" }], {
|
|
258
|
+
concepts: [concept({ path: "decisions/public-assets.md", title: "Public assets" })],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(result).toContain("src/other.ts");
|
|
262
|
+
expect(result).toContain("Active problem");
|
|
263
|
+
expect(result).toContain("1 error");
|
|
264
|
+
expect(result).toContain("Suppressed");
|
|
265
|
+
expect(result).toContain("Public assets");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("a title-less resolved concept falls back to its path as the label", () => {
|
|
269
|
+
const bundle: OkfBundle = {
|
|
270
|
+
concepts: [concept({ path: "decisions/public-assets.md" })],
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const result = formatStylish([], [{ ...suppressedDiag, reason: "okf:/decisions/public-assets.md" }], bundle);
|
|
274
|
+
|
|
275
|
+
expect(result).toContain("decisions/public-assets.md");
|
|
276
|
+
expect(result).not.toContain("unresolved");
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
126
280
|
describe("formatSummary", () => {
|
|
127
281
|
beforeEach(() => {
|
|
128
282
|
process.env.NO_COLOR = "1";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LintDiagnostic, LintRule, Severity } from "../../lint/rule";
|
|
2
|
+
import type { OkfBundle } from "../../okf-read";
|
|
2
3
|
import { pathToFileURL } from "node:url";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -33,64 +34,184 @@ function color(text: string, colorCode: string): string {
|
|
|
33
34
|
/**
|
|
34
35
|
* Format diagnostics in stylish format (similar to ESLint)
|
|
35
36
|
* Groups by file, shows severity, message, and rule ID
|
|
37
|
+
*
|
|
38
|
+
* @param diagnostics - Active (non-suppressed) diagnostics
|
|
39
|
+
* @param suppressed - Suppressed diagnostics with optional reason (optional). When
|
|
40
|
+
* present and non-empty, a "Suppressed" section is appended (#1866, design #1059)
|
|
41
|
+
* below the active diagnostics, mirroring SARIF's `suppressed` param on
|
|
42
|
+
* {@link formatSarif} rather than changing what "diagnostics" means here.
|
|
43
|
+
* @param bundle - The project's loaded OKF knowledge bundle (`../../okf-read`'s
|
|
44
|
+
* `loadOkfBundle`), used to resolve `okf:` citations in suppression reasons.
|
|
45
|
+
* Omitted (or a bundle with no matching concept) falls back to printing the
|
|
46
|
+
* raw reason with a warning — this never affects suppression itself, only
|
|
47
|
+
* how a cited reason renders.
|
|
36
48
|
*/
|
|
37
|
-
export function formatStylish(
|
|
38
|
-
|
|
49
|
+
export function formatStylish(
|
|
50
|
+
diagnostics: LintDiagnostic[],
|
|
51
|
+
suppressed?: Array<LintDiagnostic & { reason?: string }>,
|
|
52
|
+
bundle?: OkfBundle,
|
|
53
|
+
): string {
|
|
54
|
+
const hasSuppressed = suppressed !== undefined && suppressed.length > 0;
|
|
55
|
+
|
|
56
|
+
if (diagnostics.length === 0 && !hasSuppressed) {
|
|
39
57
|
return formatSummary(0, 0);
|
|
40
58
|
}
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
const lines: string[] = [];
|
|
61
|
+
let errorCount = 0;
|
|
62
|
+
let warningCount = 0;
|
|
63
|
+
|
|
64
|
+
if (diagnostics.length > 0) {
|
|
65
|
+
// Group by file
|
|
66
|
+
const byFile = new Map<string, LintDiagnostic[]>();
|
|
67
|
+
for (const diag of diagnostics) {
|
|
68
|
+
const existing = byFile.get(diag.file) ?? [];
|
|
69
|
+
existing.push(diag);
|
|
70
|
+
byFile.set(diag.file, existing);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const [file, fileDiags] of byFile) {
|
|
74
|
+
// File header with underline
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push(color(file, colors.underline));
|
|
77
|
+
|
|
78
|
+
// Sort by line, then column
|
|
79
|
+
fileDiags.sort((a, b) => {
|
|
80
|
+
if (a.line !== b.line) return a.line - b.line;
|
|
81
|
+
return a.column - b.column;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
for (const diag of fileDiags) {
|
|
85
|
+
// Count errors and warnings
|
|
86
|
+
if (diag.severity === "error") {
|
|
87
|
+
errorCount++;
|
|
88
|
+
} else if (diag.severity === "warning") {
|
|
89
|
+
warningCount++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Format: " line:col severity message ruleId"
|
|
93
|
+
const location = color(
|
|
94
|
+
`${String(diag.line).padStart(4)}:${String(diag.column).padEnd(3)}`,
|
|
95
|
+
colors.gray
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const severityColor = diag.severity === "error" ? colors.red : colors.yellow;
|
|
99
|
+
const severity = color(diag.severity.padEnd(7), severityColor);
|
|
100
|
+
|
|
101
|
+
const ruleId = color(diag.ruleId, colors.gray);
|
|
102
|
+
|
|
103
|
+
lines.push(` ${location} ${severity} ${diag.message} ${ruleId}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (hasSuppressed) {
|
|
109
|
+
lines.push(...formatSuppressedSection(suppressed!, bundle));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Summary line
|
|
113
|
+
lines.push("");
|
|
114
|
+
const summary = formatSummary(errorCount, warningCount);
|
|
115
|
+
lines.push(summary);
|
|
116
|
+
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A suppression reason's `okf:` citation, resolved against a loaded bundle.
|
|
122
|
+
*/
|
|
123
|
+
interface ResolvedCitation {
|
|
124
|
+
/** What to print beside the rule id: the concept title when resolved, the raw reason otherwise. */
|
|
125
|
+
label: string;
|
|
126
|
+
/** The concept's bundle-relative path, printed on the line below — only set when resolved. */
|
|
127
|
+
path?: string;
|
|
128
|
+
/** True when the reason carried an `okf:` token that named no concept in `bundle`. */
|
|
129
|
+
unresolved: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resolve a suppression reason's `okf:<path>` citation (#1866, design #1059)
|
|
134
|
+
* against a loaded bundle's concepts, matched on `OkfConcept.path`. Not an
|
|
135
|
+
* `okf:` citation at all: returns `null`, and the reason renders as plain
|
|
136
|
+
* text, same as before this section existed. A leading `/` on the cited path
|
|
137
|
+
* is accepted and stripped — `okf-read.ts`'s bundle-relative paths never
|
|
138
|
+
* carry one, but the design doc's worked example writes citations with one
|
|
139
|
+
* (mirroring an absolute-from-bundle-root reading), so both forms resolve.
|
|
140
|
+
*/
|
|
141
|
+
function resolveOkfCitation(reason: string, bundle: OkfBundle | undefined): ResolvedCitation | null {
|
|
142
|
+
const prefix = "okf:";
|
|
143
|
+
if (!reason.startsWith(prefix)) return null;
|
|
144
|
+
|
|
145
|
+
const token = reason.slice(prefix.length).trim().replace(/^\/+/, "");
|
|
146
|
+
const concept = bundle?.concepts.find((c) => c.path === token);
|
|
147
|
+
|
|
148
|
+
if (concept) {
|
|
149
|
+
return { label: concept.title ?? concept.path, path: concept.path, unresolved: false };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { label: reason, unresolved: true };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Render the "Suppressed" section: every suppressed diagnostic, grouped by
|
|
157
|
+
* file like the active section above, with an `okf:` reason resolved to its
|
|
158
|
+
* concept's title (and bundle-relative path on the line below) when
|
|
159
|
+
* `bundle` has a match. A citation naming no concept — including when no
|
|
160
|
+
* `bundle` was loaded at all — prints the raw reason plus a warning; a
|
|
161
|
+
* plain (non-`okf:`) reason prints as-is. Never affects which diagnostics
|
|
162
|
+
* are suppressed, only how the reason renders (design #1059: "a cited
|
|
163
|
+
* suppression suppresses exactly as an uncited one does").
|
|
164
|
+
*/
|
|
165
|
+
function formatSuppressedSection(
|
|
166
|
+
suppressed: Array<LintDiagnostic & { reason?: string }>,
|
|
167
|
+
bundle: OkfBundle | undefined,
|
|
168
|
+
): string[] {
|
|
169
|
+
const lines: string[] = [];
|
|
170
|
+
|
|
171
|
+
const byFile = new Map<string, Array<LintDiagnostic & { reason?: string }>>();
|
|
172
|
+
for (const diag of suppressed) {
|
|
45
173
|
const existing = byFile.get(diag.file) ?? [];
|
|
46
174
|
existing.push(diag);
|
|
47
175
|
byFile.set(diag.file, existing);
|
|
48
176
|
}
|
|
49
177
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
let warningCount = 0;
|
|
178
|
+
lines.push("");
|
|
179
|
+
lines.push(color("Suppressed", colors.underline));
|
|
53
180
|
|
|
54
181
|
for (const [file, fileDiags] of byFile) {
|
|
55
|
-
// File header with underline
|
|
56
182
|
lines.push("");
|
|
57
183
|
lines.push(color(file, colors.underline));
|
|
58
184
|
|
|
59
|
-
// Sort by line, then column
|
|
60
185
|
fileDiags.sort((a, b) => {
|
|
61
186
|
if (a.line !== b.line) return a.line - b.line;
|
|
62
187
|
return a.column - b.column;
|
|
63
188
|
});
|
|
64
189
|
|
|
65
190
|
for (const diag of fileDiags) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} else if (diag.severity === "warning") {
|
|
70
|
-
warningCount++;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Format: " line:col severity message ruleId"
|
|
74
|
-
const location = color(
|
|
75
|
-
`${String(diag.line).padStart(4)}:${String(diag.column).padEnd(3)}`,
|
|
76
|
-
colors.gray
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const severityColor = diag.severity === "error" ? colors.red : colors.yellow;
|
|
80
|
-
const severity = color(diag.severity.padEnd(7), severityColor);
|
|
81
|
-
|
|
191
|
+
const locationPlain = `${String(diag.line).padStart(4)}:${String(diag.column).padEnd(3)}`;
|
|
192
|
+
const location = color(locationPlain, colors.gray);
|
|
193
|
+
const severity = color("suppressed", colors.cyan);
|
|
82
194
|
const ruleId = color(diag.ruleId, colors.gray);
|
|
83
195
|
|
|
84
|
-
|
|
196
|
+
const citation = diag.reason ? resolveOkfCitation(diag.reason, bundle) : null;
|
|
197
|
+
const indent = " ".repeat(2 + locationPlain.length + 2 + "suppressed".length + 2 + diag.ruleId.length + 2);
|
|
198
|
+
|
|
199
|
+
if (citation) {
|
|
200
|
+
lines.push(` ${location} ${severity} ${ruleId} ${citation.label}`);
|
|
201
|
+
if (!citation.unresolved) {
|
|
202
|
+
lines.push(`${indent}${color(citation.path!, colors.gray)}`);
|
|
203
|
+
} else {
|
|
204
|
+
lines.push(`${indent}${color("⚠ unresolved okf citation", colors.yellow)}`);
|
|
205
|
+
}
|
|
206
|
+
} else if (diag.reason) {
|
|
207
|
+
lines.push(` ${location} ${severity} ${ruleId} ${diag.reason}`);
|
|
208
|
+
} else {
|
|
209
|
+
lines.push(` ${location} ${severity} ${ruleId}`);
|
|
210
|
+
}
|
|
85
211
|
}
|
|
86
212
|
}
|
|
87
213
|
|
|
88
|
-
|
|
89
|
-
lines.push("");
|
|
90
|
-
const summary = formatSummary(errorCount, warningCount);
|
|
91
|
-
lines.push(summary);
|
|
92
|
-
|
|
93
|
-
return lines.join("\n");
|
|
214
|
+
return lines;
|
|
94
215
|
}
|
|
95
216
|
|
|
96
217
|
/**
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { existsSync, readFileSync, writeFileSync, readdirSync, type Dirent } from "fs";
|
|
26
26
|
import { join } from "path";
|
|
27
27
|
import { execSync } from "child_process";
|
|
28
|
+
import { SNAPSHOT_UPDATE_ENV } from "./validate";
|
|
28
29
|
import {
|
|
29
30
|
extractSurface,
|
|
30
31
|
diffSurface,
|
|
@@ -114,6 +115,17 @@ export interface RegenOptions {
|
|
|
114
115
|
* sha256:<64-hex-chars> <relative-path-to-spec-file>
|
|
115
116
|
*/
|
|
116
117
|
pinnedDigestPath?: string;
|
|
118
|
+
/**
|
|
119
|
+
* This run is about to rewrite the baseline snapshot (#1825).
|
|
120
|
+
*
|
|
121
|
+
* The `validate` step runs with {@link SNAPSHOT_UPDATE_ENV} set so its
|
|
122
|
+
* `surface-matches-snapshot` check is skipped. A lexicon whose snapshot gate
|
|
123
|
+
* runs in `"always"` mode (#1475) fails validate on a stale baseline, and a
|
|
124
|
+
* stale baseline is exactly what a re-baseline starts from — without this
|
|
125
|
+
* exemption the documented update flow deadlocks. Every other validate check
|
|
126
|
+
* still gates the run, so a broken generate cannot be baselined.
|
|
127
|
+
*/
|
|
128
|
+
updatingSnapshot?: boolean;
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
// ── Pipeline ──────────────────────────────────────────────────────────
|
|
@@ -132,6 +144,7 @@ export async function regenLexicon(opts: RegenOptions): Promise<RegenResult> {
|
|
|
132
144
|
skipBuild = false,
|
|
133
145
|
skipLint = false,
|
|
134
146
|
skipExamples = true,
|
|
147
|
+
updatingSnapshot = false,
|
|
135
148
|
} = opts;
|
|
136
149
|
|
|
137
150
|
const failures: RegenFailure[] = [];
|
|
@@ -177,7 +190,12 @@ export async function regenLexicon(opts: RegenOptions): Promise<RegenResult> {
|
|
|
177
190
|
}
|
|
178
191
|
|
|
179
192
|
// Step 4: validate
|
|
180
|
-
|
|
193
|
+
// A re-baseline run is exempt from the surface-matches-snapshot check only
|
|
194
|
+
// (#1825) — see RegenOptions.updatingSnapshot.
|
|
195
|
+
const validateEnv: NodeJS.ProcessEnv = updatingSnapshot
|
|
196
|
+
? { ...process.env, [SNAPSHOT_UPDATE_ENV]: "1" }
|
|
197
|
+
: process.env;
|
|
198
|
+
const validateFail = runScript(lexiconDir, "validate", validateEnv, verbose);
|
|
181
199
|
if (validateFail) {
|
|
182
200
|
failures.push(validateFail);
|
|
183
201
|
}
|
|
@@ -182,4 +182,37 @@ describe("surface snapshot gate (#1473)", () => {
|
|
|
182
182
|
const check = result.checks.find((c) => c.name === "surface-matches-snapshot");
|
|
183
183
|
expect(check?.ok).toBe(false);
|
|
184
184
|
});
|
|
185
|
+
|
|
186
|
+
test("an update run skips the staleness check it is about to fix (#1825)", async () => {
|
|
187
|
+
// `--update-snapshot` sets CHANT_SNAPSHOT_UPDATE for its validate run.
|
|
188
|
+
// Without this, an "always" gate deadlocks the documented re-baseline flow:
|
|
189
|
+
// validate fails on the stale snapshot, and the update writes only after a
|
|
190
|
+
// green validate.
|
|
191
|
+
const stale = JSON.stringify({ schemaVersion: 1, generatedAt: "2026-01-01T00:00:00.000Z", entries: {} });
|
|
192
|
+
const result = await validateLexiconArtifacts({
|
|
193
|
+
lexiconJsonFilename: "lexicon-test.json",
|
|
194
|
+
requiredNames: [],
|
|
195
|
+
basePath: fixture({ snapshot: stale }),
|
|
196
|
+
checkSurfaceSnapshot: "always",
|
|
197
|
+
env: { CHANT_SNAPSHOT_UPDATE: "1" },
|
|
198
|
+
});
|
|
199
|
+
expect(result.checks.find((c) => c.name === "surface-matches-snapshot")).toBeUndefined();
|
|
200
|
+
expect(result.success).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("an update run is exempt from the staleness check and nothing else", async () => {
|
|
204
|
+
// A failing check other than surface-matches-snapshot must still refuse
|
|
205
|
+
// the run, so a broken generate cannot be baselined.
|
|
206
|
+
const stale = JSON.stringify({ schemaVersion: 1, generatedAt: "2026-01-01T00:00:00.000Z", entries: {} });
|
|
207
|
+
const result = await validateLexiconArtifacts({
|
|
208
|
+
lexiconJsonFilename: "lexicon-test.json",
|
|
209
|
+
requiredNames: ["NotThere"],
|
|
210
|
+
basePath: fixture({ snapshot: stale }),
|
|
211
|
+
checkSurfaceSnapshot: "always",
|
|
212
|
+
env: { CHANT_SNAPSHOT_UPDATE: "1" },
|
|
213
|
+
});
|
|
214
|
+
expect(result.checks.find((c) => c.name === "surface-matches-snapshot")).toBeUndefined();
|
|
215
|
+
expect(result.checks.find((c) => c.name === "required-names")?.ok).toBe(false);
|
|
216
|
+
expect(result.success).toBe(false);
|
|
217
|
+
});
|
|
185
218
|
});
|
package/src/codegen/validate.ts
CHANGED
|
@@ -27,6 +27,17 @@ export interface ValidateResult {
|
|
|
27
27
|
*/
|
|
28
28
|
export const RELEASE_GATE_ENV = "CHANT_RELEASE_GATE";
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Set by `chant dev surface-diff --update-snapshot` for the validate run it
|
|
32
|
+
* performs before rewriting the baseline (chant #1825). When set to "1", the
|
|
33
|
+
* `surface-matches-snapshot` check is skipped. That check fails on exactly the
|
|
34
|
+
* staleness the update run exists to fix, and with an `"always"` gate (#1475)
|
|
35
|
+
* the two would deadlock: validate cannot pass until the snapshot is updated,
|
|
36
|
+
* and the snapshot cannot be updated until validate passes. Every other check
|
|
37
|
+
* still runs, so a broken generate cannot be baselined.
|
|
38
|
+
*/
|
|
39
|
+
export const SNAPSHOT_UPDATE_ENV = "CHANT_SNAPSHOT_UPDATE";
|
|
40
|
+
|
|
30
41
|
export interface LexiconValidationConfig {
|
|
31
42
|
/** Filename of the lexicon JSON (e.g. "lexicon-mydom.json") */
|
|
32
43
|
lexiconJsonFilename: string;
|
|
@@ -201,10 +212,18 @@ export async function validateLexiconArtifacts(config: LexiconValidationConfig):
|
|
|
201
212
|
// Runs on the artifacts already on disk — no second generation — and is
|
|
202
213
|
// skipped for a lexicon with no committed snapshot, which is the case for a
|
|
203
214
|
// new lexicon before its first baseline.
|
|
215
|
+
//
|
|
216
|
+
// A re-baseline run (`chant dev surface-diff --update-snapshot`) sets
|
|
217
|
+
// {@link SNAPSHOT_UPDATE_ENV} and is exempt: the check would fail on the
|
|
218
|
+
// stale snapshot that run is about to rewrite, deadlocking the documented
|
|
219
|
+
// update flow against an `"always"` gate (#1825). Only this check is exempt —
|
|
220
|
+
// every other check above still gates the run.
|
|
204
221
|
const snapshotPath = join(config.basePath, "surface.snapshot.json");
|
|
222
|
+
const env = config.env ?? process.env;
|
|
205
223
|
const surfaceGate =
|
|
206
|
-
config.checkSurfaceSnapshot === "always" ||
|
|
207
|
-
|
|
224
|
+
(config.checkSurfaceSnapshot === "always" ||
|
|
225
|
+
(config.checkSurfaceSnapshot === true && env[RELEASE_GATE_ENV] === "1")) &&
|
|
226
|
+
env[SNAPSHOT_UPDATE_ENV] !== "1";
|
|
208
227
|
if (surfaceGate && lexiconData && existsSync(snapshotPath) && existsSync(dtsPath)) {
|
|
209
228
|
try {
|
|
210
229
|
const fresh = extractSurface(readFileSync(lexiconPath, "utf-8"), readFileSync(dtsPath, "utf-8"));
|
|
@@ -117,6 +117,10 @@ export interface AutoReleaseRunInfo {
|
|
|
117
117
|
digest?: string;
|
|
118
118
|
/** Orchestrator run identifier — a Temporal `runId`, or a locally generated id for the local executor (mirrors `runComponentsReleaseRecord`'s `--run-id` default). */
|
|
119
119
|
runId: string;
|
|
120
|
+
/** The bypassed capability-profile divergences, when the caller deliberately overrode a deploy-time profile assertion (chant #1244) — recorded verbatim as the release record's `profileOverride`. */
|
|
121
|
+
profileOverride?: string;
|
|
122
|
+
/** The deploy's input-side digest, when `digest` is a rendered-content identity (a pinned helm deploy, chant #1242) — recorded as the release record's `inputDigest` so ledger queries can still join on inputs across clusters whose bytes legitimately differ. */
|
|
123
|
+
inputDigest?: string;
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
/** Opt-out + field-override knobs for auto-release recording, threaded from CLI flags/config (#597: "opt-out-able (flag/config), documented default"). */
|
|
@@ -202,6 +206,8 @@ export async function maybeRecordAutoRelease(
|
|
|
202
206
|
runId: run.runId,
|
|
203
207
|
timestamp,
|
|
204
208
|
actor,
|
|
209
|
+
...(run.profileOverride ? { profileOverride: run.profileOverride } : {}),
|
|
210
|
+
...(run.inputDigest ? { inputDigest: run.inputDigest } : {}),
|
|
205
211
|
},
|
|
206
212
|
{ cwd: options.cwd },
|
|
207
213
|
);
|
|
@@ -92,9 +92,14 @@ describe("createCapabilityRegistry", () => {
|
|
|
92
92
|
}
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
// Every AWS/k8s cloud leaf moved out to its lexicon — but the three
|
|
96
|
+
// Cloudflare Workers apply verbs (#1293, epic #1296) stay in core by
|
|
97
|
+
// deliberate decision (see ./verbs/wrangler.ts's module doc): the epic
|
|
98
|
+
// cedes the Workers plane to `wrangler` and there is no cloudflare lexicon
|
|
99
|
+
// for them to live in instead.
|
|
100
|
+
test("family table lists exactly the agnostic starter families plus the Cloudflare apply leaves (#1293)", () => {
|
|
96
101
|
expect(Object.keys(STARTER_VERB_FAMILIES).sort()).toEqual(
|
|
97
|
-
["build", "escapeHatch", "sbom", "supplyChainPolicy", "supplyChainSecurity", "waitVerify"].sort(),
|
|
102
|
+
["build", "cloudflare", "escapeHatch", "sbom", "secrets", "supplyChainPolicy", "supplyChainSecurity", "waitVerify"].sort(),
|
|
98
103
|
);
|
|
99
104
|
});
|
|
100
105
|
|