@intentius/chant 0.27.0 → 0.29.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/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +5 -3
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/config.d.ts +46 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +153 -17
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/sandbox/config-run.d.ts.map +1 -1
- package/dist/discovery/sandbox/config-wire.d.ts +3 -2
- package/dist/discovery/sandbox/config-wire.d.ts.map +1 -1
- package/dist/discovery/sandbox/fork.d.ts +25 -0
- package/dist/discovery/sandbox/fork.d.ts.map +1 -1
- package/dist/discovery/sandbox/policy-run.d.ts.map +1 -1
- package/dist/discovery/sandbox/run.d.ts.map +1 -1
- package/dist/env.d.ts +5 -2
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +12 -0
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +29 -4
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts +100 -0
- package/dist/kubectl-context.d.ts.map +1 -0
- package/dist/lexicon.d.ts +31 -6
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +26 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/live-diff.d.ts +25 -1
- package/dist/lifecycle/live-diff.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +4 -2
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts +26 -1
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +8 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lint/config.d.ts +80 -0
- package/dist/lint/config.d.ts.map +1 -1
- package/dist/lint/policy.d.ts +8 -2
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/lint/post-synth.d.ts +18 -1
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/live-endpoint.d.ts +92 -0
- package/dist/live-endpoint.d.ts.map +1 -0
- package/dist/observation.d.ts +123 -0
- package/dist/observation.d.ts.map +1 -0
- package/dist/stack-output.d.ts +9 -4
- package/dist/stack-output.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/build.test.ts +190 -0
- package/src/cli/commands/build.ts +34 -2
- package/src/cli/commands/lint.ts +17 -25
- package/src/cli/handlers/components.test.ts +63 -4
- package/src/cli/handlers/components.ts +78 -35
- package/src/cli/handlers/graph.test.ts +69 -6
- package/src/cli/handlers/graph.ts +61 -27
- package/src/cli/handlers/lifecycle.test.ts +285 -6
- package/src/cli/handlers/lifecycle.ts +297 -185
- package/src/config.test.ts +75 -0
- package/src/config.ts +61 -3
- package/src/discovery/fold-composite.test.ts +594 -0
- package/src/discovery/fold-import.ts +987 -43
- package/src/discovery/sandbox/config-boundary.test.ts +55 -1
- package/src/discovery/sandbox/config-run.ts +3 -0
- package/src/discovery/sandbox/config-wire.ts +3 -2
- package/src/discovery/sandbox/fork.ts +75 -1
- package/src/discovery/sandbox/policy-boundary.test.ts +56 -1
- package/src/discovery/sandbox/policy-run.ts +15 -1
- package/src/discovery/sandbox/run.test.ts +85 -1
- package/src/discovery/sandbox/run.ts +3 -0
- package/src/env.test.ts +12 -0
- package/src/env.ts +12 -4
- package/src/fold/fold.ts +12 -2
- package/src/graph-ir-live.test.ts +28 -1
- package/src/graph-ir.ts +68 -12
- package/src/index.ts +1 -0
- package/src/kubectl-context.test.ts +94 -0
- package/src/kubectl-context.ts +207 -0
- package/src/lexicon.ts +41 -6
- package/src/lifecycle/change-set.test.ts +93 -1
- package/src/lifecycle/change-set.ts +65 -13
- package/src/lifecycle/live-diff.test.ts +39 -0
- package/src/lifecycle/live-diff.ts +51 -5
- package/src/lifecycle/observe.test.ts +74 -3
- package/src/lifecycle/observe.ts +82 -22
- package/src/lifecycle/snapshot.test.ts +39 -1
- package/src/lifecycle/snapshot.ts +34 -9
- package/src/lifecycle/status.test.ts +89 -8
- package/src/lifecycle/status.ts +53 -3
- package/src/lifecycle/types.ts +8 -0
- package/src/lint/config.test.ts +93 -1
- package/src/lint/config.ts +108 -0
- package/src/lint/policy.test.ts +90 -0
- package/src/lint/policy.ts +17 -5
- package/src/lint/post-synth.test.ts +4 -0
- package/src/lint/post-synth.ts +30 -1
- package/src/live-endpoint.test.ts +115 -0
- package/src/live-endpoint.ts +148 -0
- package/src/observation.test.ts +96 -0
- package/src/observation.ts +213 -0
- package/src/stack-output.test.ts +76 -3
- package/src/stack-output.ts +59 -26
|
@@ -32,8 +32,8 @@ describe("status", () => {
|
|
|
32
32
|
const cs: ChangeSet = {
|
|
33
33
|
env: "prod",
|
|
34
34
|
entries: [
|
|
35
|
-
{ name: "search-service", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
|
|
36
|
-
{ name: "orphan-thing", type: "T", action: "adopt", evidence: { declared: false, inSnapshot: false, live: true }, ownership: "foreign" },
|
|
35
|
+
{ name: "search-service", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true, observed: true }, ownership: "owned" },
|
|
36
|
+
{ name: "orphan-thing", type: "T", action: "adopt", evidence: { declared: false, inSnapshot: false, live: true, observed: true }, ownership: "foreign" },
|
|
37
37
|
],
|
|
38
38
|
};
|
|
39
39
|
const evidence = liveEvidenceFromChangeSet(cs);
|
|
@@ -49,7 +49,7 @@ describe("status", () => {
|
|
|
49
49
|
const cs: ChangeSet = {
|
|
50
50
|
env: "prod",
|
|
51
51
|
entries: [
|
|
52
|
-
{ name: "search-service-v2", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
|
|
52
|
+
{ name: "search-service-v2", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true, observed: true }, ownership: "owned" },
|
|
53
53
|
],
|
|
54
54
|
};
|
|
55
55
|
const mapping: LiveNameMapping = new Map([["search-svc", ["search-service-v2"]]]);
|
|
@@ -63,7 +63,7 @@ describe("status", () => {
|
|
|
63
63
|
const cs: ChangeSet = {
|
|
64
64
|
env: "prod",
|
|
65
65
|
entries: [
|
|
66
|
-
{ name: "search-service", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
|
|
66
|
+
{ name: "search-service", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true, observed: true }, ownership: "owned" },
|
|
67
67
|
],
|
|
68
68
|
};
|
|
69
69
|
const mapping: LiveNameMapping = new Map([["some-other-component", ["renamed-thing"]]]);
|
|
@@ -75,8 +75,8 @@ describe("status", () => {
|
|
|
75
75
|
const cs: ChangeSet = {
|
|
76
76
|
env: "prod",
|
|
77
77
|
entries: [
|
|
78
|
-
{ name: "cluster-node-1", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
|
|
79
|
-
{ name: "cluster-node-2", type: "T", action: "update", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
|
|
78
|
+
{ name: "cluster-node-1", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true, observed: true }, ownership: "owned" },
|
|
79
|
+
{ name: "cluster-node-2", type: "T", action: "update", evidence: { declared: true, inSnapshot: true, live: true, observed: true }, ownership: "owned" },
|
|
80
80
|
],
|
|
81
81
|
};
|
|
82
82
|
const mapping: LiveNameMapping = new Map([["neo4j-cluster", ["cluster-node-1", "cluster-node-2"]]]);
|
|
@@ -297,7 +297,7 @@ describe("status", () => {
|
|
|
297
297
|
const cs: ChangeSet = {
|
|
298
298
|
env: "prod",
|
|
299
299
|
entries: [
|
|
300
|
-
{ name: "search-service-v2", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
|
|
300
|
+
{ name: "search-service-v2", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true, observed: true }, ownership: "owned" },
|
|
301
301
|
],
|
|
302
302
|
};
|
|
303
303
|
const mapping: LiveNameMapping = new Map([["search-svc", ["search-service-v2"]]]);
|
|
@@ -311,7 +311,7 @@ describe("status", () => {
|
|
|
311
311
|
const cs: ChangeSet = {
|
|
312
312
|
env: "prod",
|
|
313
313
|
entries: [
|
|
314
|
-
{ name: "search-service", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
|
|
314
|
+
{ name: "search-service", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true, observed: true }, ownership: "owned" },
|
|
315
315
|
],
|
|
316
316
|
};
|
|
317
317
|
const liveEvidence = liveEvidenceFromChangeSet(cs);
|
|
@@ -381,5 +381,86 @@ describe("status", () => {
|
|
|
381
381
|
const merged = mergeLiveEvidence(undefined, supplement);
|
|
382
382
|
expect(merged.get("c")).toEqual({ live: true, ownership: "owned", action: undefined });
|
|
383
383
|
});
|
|
384
|
+
|
|
385
|
+
test("a direct stack observation clears an inherited hole; a failing one keeps it (#1089)", () => {
|
|
386
|
+
const base = new Map<string, LiveComponentEvidence>([
|
|
387
|
+
["a", { live: false, unobserved: { reason: "read-failed" } }],
|
|
388
|
+
["b", { live: false, unobserved: { reason: "read-failed" } }],
|
|
389
|
+
]);
|
|
390
|
+
const supplement = new Map<string, LiveComponentEvidence>([
|
|
391
|
+
["a", { live: true, ownership: "owned" }],
|
|
392
|
+
["b", { live: false, unobserved: { reason: "read-failed", detail: "no determinate status" } }],
|
|
393
|
+
]);
|
|
394
|
+
const merged = mergeLiveEvidence(base, supplement);
|
|
395
|
+
expect(merged.get("a")!.unobserved).toBeUndefined();
|
|
396
|
+
expect(merged.get("b")!.unobserved?.reason).toBe("read-failed");
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// ── The observation tri-state reaches the status join (#1089) ─────────────
|
|
401
|
+
|
|
402
|
+
describe("not-observed never becomes 'stale' (#1089)", () => {
|
|
403
|
+
const record = {
|
|
404
|
+
version: 1,
|
|
405
|
+
component: "search-svc",
|
|
406
|
+
env: "prod",
|
|
407
|
+
digest: "sha256:abc",
|
|
408
|
+
gitSha: "g",
|
|
409
|
+
runId: "r",
|
|
410
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
411
|
+
actor: "ci",
|
|
412
|
+
} as const;
|
|
413
|
+
|
|
414
|
+
test("a recorded component whose live state could not be read reports unknown", () => {
|
|
415
|
+
const rows = reconcileStatus("prod", [record], {
|
|
416
|
+
liveEvidence: new Map<string, LiveComponentEvidence>([
|
|
417
|
+
["search-svc", { live: false, unobserved: { reason: "no-binding", detail: "no kubectl context" } }],
|
|
418
|
+
]),
|
|
419
|
+
});
|
|
420
|
+
expect(rows[0].reconciliation).toBe("unknown");
|
|
421
|
+
expect(rows[0].detail).toContain("could not be observed");
|
|
422
|
+
expect(rows[0].detail).toContain("no kubectl context");
|
|
423
|
+
// `live` is omitted entirely — `false` would read as "not deployed".
|
|
424
|
+
expect(rows[0].live).toBeUndefined();
|
|
425
|
+
expect(rows[0].unobserved).toEqual({ reason: "no-binding", detail: "no kubectl context" });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("the same component, actually observed absent, still reports stale", () => {
|
|
429
|
+
const rows = reconcileStatus("prod", [record], {
|
|
430
|
+
liveEvidence: new Map<string, LiveComponentEvidence>([["search-svc", { live: false }]]),
|
|
431
|
+
});
|
|
432
|
+
expect(rows[0].reconciliation).toBe("stale");
|
|
433
|
+
expect(rows[0].live).toBe(false);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("an unrecorded component that could not be read is unknown, not unrecorded", () => {
|
|
437
|
+
const rows = reconcileStatus("prod", [], {
|
|
438
|
+
allComponents: ["search-svc"],
|
|
439
|
+
liveEvidence: new Map<string, LiveComponentEvidence>([
|
|
440
|
+
["search-svc", { live: false, unobserved: { reason: "read-failed" } }],
|
|
441
|
+
]),
|
|
442
|
+
});
|
|
443
|
+
expect(rows[0].reconciliation).toBe("unknown");
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("liveEvidenceFromChangeSet carries the plan's unobserved verdict", () => {
|
|
447
|
+
const evidence = liveEvidenceFromChangeSet({
|
|
448
|
+
env: "prod",
|
|
449
|
+
entries: [
|
|
450
|
+
{
|
|
451
|
+
name: "search-svc",
|
|
452
|
+
action: "unobserved",
|
|
453
|
+
evidence: { declared: true, inSnapshot: false, live: false, observed: false },
|
|
454
|
+
ownership: "unknown",
|
|
455
|
+
unobservedReason: "unsupported-kind",
|
|
456
|
+
unobservedDetail: "no reader",
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
});
|
|
460
|
+
expect(evidence.get("search-svc")!.unobserved).toEqual({
|
|
461
|
+
reason: "unsupported-kind",
|
|
462
|
+
detail: "no reader",
|
|
463
|
+
});
|
|
464
|
+
});
|
|
384
465
|
});
|
|
385
466
|
});
|
package/src/lifecycle/status.ts
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
* lower-confidence signal rather than silently treated as "reconciled".
|
|
22
22
|
* - **reconciled** — a release record exists and live evidence (via
|
|
23
23
|
* ownership) confirms the component is present and owned by chant.
|
|
24
|
+
* - **unknown** — live evidence was requested and could not be read (#1089),
|
|
25
|
+
* or was not requested at all. A component chant could not observe is never
|
|
26
|
+
* reported `stale`: "the read failed" and "it is gone" are different facts.
|
|
24
27
|
*
|
|
25
28
|
* This is deliberately a light-touch reconciliation: chant's lexicons report
|
|
26
29
|
* resource-level status (`ResourceMetadata`), not "the digest currently
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
import type { ChangeSet, ChangeAction } from "./change-set";
|
|
37
40
|
import { latestPerComponent, type ReleaseRecord } from "./release-ledger";
|
|
38
41
|
import type { BuildLedgerEntry, ComponentBomSummary } from "./build-ledger";
|
|
42
|
+
import { unobservedReasonText, type UnobservedReason } from "../observation";
|
|
39
43
|
|
|
40
44
|
/** One row of `chant components status [env]` — the per-component join of recorded vs live. */
|
|
41
45
|
export interface ComponentStatusRow {
|
|
@@ -63,9 +67,18 @@ export interface ComponentStatusRow {
|
|
|
63
67
|
/**
|
|
64
68
|
* Machine-readable "observed live", when live evidence was gathered (`--live`).
|
|
65
69
|
* A consumer joining this row onto a graph node should read this rather than
|
|
66
|
-
* string-matching `detail`. Absent when `--live` was not requested
|
|
70
|
+
* string-matching `detail`. Absent when `--live` was not requested — and, since
|
|
71
|
+
* #1089, also absent when live state could not be read at all: `false` means
|
|
72
|
+
* "looked, not there", never "did not look". Read {@link unobserved} for that
|
|
73
|
+
* case; a consumer that treats absent as "unknown" already handles it.
|
|
67
74
|
*/
|
|
68
75
|
live?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Set when `--live` was requested and the observation could not read this
|
|
78
|
+
* component (#1089). `live` is absent alongside it and `reconciliation` is
|
|
79
|
+
* `unknown` — the row reports a hole rather than a verdict.
|
|
80
|
+
*/
|
|
81
|
+
unobserved?: { reason: UnobservedReason; detail?: string };
|
|
69
82
|
/**
|
|
70
83
|
* The owning deploy unit's raw status, when a lexicon reported it (AWS: the
|
|
71
84
|
* component's own CFN stack via `describeStackStatus`). Lets a renderer paint a
|
|
@@ -103,6 +116,12 @@ export interface ComponentStatusResult {
|
|
|
103
116
|
export interface LiveComponentEvidence {
|
|
104
117
|
/** True if this component name was observed live at all (declared+live, or orphan+live). */
|
|
105
118
|
live: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Set when the observation could not read this component (#1089). `live` is
|
|
121
|
+
* `false` alongside it, but only because the boolean has nowhere else to go —
|
|
122
|
+
* every consumer must branch on this field before believing `live: false`.
|
|
123
|
+
*/
|
|
124
|
+
unobserved?: { reason: UnobservedReason; detail?: string };
|
|
106
125
|
/** The `ChangeSet` action chant's existing plan logic assigned, when the component maps to a tracked entity/resource name. */
|
|
107
126
|
action?: ChangeAction;
|
|
108
127
|
/** Ownership verdict, when known. */
|
|
@@ -132,6 +151,10 @@ export function mergeLiveEvidence(
|
|
|
132
151
|
const b = merged.get(component);
|
|
133
152
|
merged.set(component, {
|
|
134
153
|
live: sup.live,
|
|
154
|
+
// A direct stack observation that succeeded answers the question the
|
|
155
|
+
// change-set axis could not — so it clears an inherited "not observed".
|
|
156
|
+
// A supplement that itself could not read keeps the hole.
|
|
157
|
+
...(sup.unobserved ? { unobserved: sup.unobserved } : {}),
|
|
135
158
|
ownership: sup.ownership ?? b?.ownership,
|
|
136
159
|
action: b?.action,
|
|
137
160
|
stack: sup.stack ?? b?.stack,
|
|
@@ -178,8 +201,12 @@ function mergeEvidence(entries: LiveComponentEvidence[]): LiveComponentEvidence
|
|
|
178
201
|
const action = entries.some((e) => e.action === "update")
|
|
179
202
|
? "update"
|
|
180
203
|
: entries.find((e) => e.action !== undefined)?.action;
|
|
204
|
+
// A component whose entities were partly readable is still partly unknown, so
|
|
205
|
+
// any unobserved entity keeps the hole — unless something under it was
|
|
206
|
+
// actually seen live, which already answers "is this deployed".
|
|
207
|
+
const unobserved = live ? undefined : entries.find((e) => e.unobserved)?.unobserved;
|
|
181
208
|
|
|
182
|
-
return { live, ownership, action };
|
|
209
|
+
return { live, ownership, action, ...(unobserved ? { unobserved } : {}) };
|
|
183
210
|
}
|
|
184
211
|
|
|
185
212
|
/**
|
|
@@ -204,6 +231,16 @@ export function liveEvidenceFromChangeSet(
|
|
|
204
231
|
live: entry.evidence.live,
|
|
205
232
|
action: entry.action,
|
|
206
233
|
ownership: entry.ownership,
|
|
234
|
+
// Carry the plan's "could not look" verdict through so the status join
|
|
235
|
+
// reports a hole instead of reading `live: false` as "gone" (#1089).
|
|
236
|
+
...(entry.action === "unobserved" && entry.unobservedReason
|
|
237
|
+
? {
|
|
238
|
+
unobserved: {
|
|
239
|
+
reason: entry.unobservedReason,
|
|
240
|
+
...(entry.unobservedDetail ? { detail: entry.unobservedDetail } : {}),
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
: {}),
|
|
207
244
|
});
|
|
208
245
|
}
|
|
209
246
|
|
|
@@ -275,6 +312,15 @@ export function reconcileStatus(
|
|
|
275
312
|
detail = recorded
|
|
276
313
|
? "recorded; live status not queried (pass --live to reconcile)"
|
|
277
314
|
: "no release record found";
|
|
315
|
+
} else if (evidence?.unobserved) {
|
|
316
|
+
// Live evidence was requested and could not be read (#1089). Neither
|
|
317
|
+
// `stale` (which claims the component is gone) nor `reconciled` is
|
|
318
|
+
// supportable — the honest verdict is that nothing is known.
|
|
319
|
+
reconciliation = "unknown";
|
|
320
|
+
const why = `${unobservedReasonText(evidence.unobserved.reason)}${evidence.unobserved.detail ? `: ${evidence.unobserved.detail}` : ""}`;
|
|
321
|
+
detail = recorded
|
|
322
|
+
? `recorded ${recorded.timestamp} (digest ${recorded.digest}), but live state could not be observed — ${why}`
|
|
323
|
+
: `no release record, and live state could not be observed — ${why}`;
|
|
278
324
|
} else if (!recorded && evidence?.live) {
|
|
279
325
|
reconciliation = "unrecorded";
|
|
280
326
|
detail = `live${evidence.ownership === "owned" ? " and chant-owned" : ""}, but no release record exists — deployed outside the recorded path`;
|
|
@@ -300,7 +346,11 @@ export function reconcileStatus(
|
|
|
300
346
|
componentBom,
|
|
301
347
|
reconciliation,
|
|
302
348
|
detail,
|
|
303
|
-
|
|
349
|
+
// `live` is only emitted when it is a real answer: requested AND read.
|
|
350
|
+
// An unread component leaves it absent (= unknown to every consumer)
|
|
351
|
+
// rather than reporting `false`, which would read as "not deployed".
|
|
352
|
+
...(liveEvidence && !evidence?.unobserved ? { live: !!evidence?.live } : {}),
|
|
353
|
+
...(evidence?.unobserved ? { unobserved: evidence.unobserved } : {}),
|
|
304
354
|
...(evidence?.stack ? { stack: evidence.stack } : {}),
|
|
305
355
|
});
|
|
306
356
|
}
|
package/src/lifecycle/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResourceMetadata, ArtifactMetadata } from "../lexicon";
|
|
2
|
+
import type { UnobservedEntity } from "../observation";
|
|
2
3
|
|
|
3
4
|
export type { ResourceMetadata, ArtifactMetadata } from "../lexicon";
|
|
4
5
|
|
|
@@ -18,6 +19,13 @@ export interface LifecycleSnapshot {
|
|
|
18
19
|
timestamp: string;
|
|
19
20
|
/** Resource metadata keyed by logical name */
|
|
20
21
|
resources: Record<string, ResourceMetadata>;
|
|
22
|
+
/**
|
|
23
|
+
* Declared entities this observation could not read (#1089), keyed by logical
|
|
24
|
+
* name. Additive and optional: a snapshot without it observed everything it
|
|
25
|
+
* was asked about. Present so a later diff can tell "was not there when the
|
|
26
|
+
* snapshot was taken" from "was never looked at".
|
|
27
|
+
*/
|
|
28
|
+
unobserved?: Record<string, UnobservedEntity>;
|
|
21
29
|
/** Artifact metadata keyed by server-side identifier (lexicon-specific). */
|
|
22
30
|
artifacts?: Record<string, ArtifactMetadata>;
|
|
23
31
|
/** Build digest at snapshot time — what was declared when this snapshot was taken */
|
package/src/lint/config.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { loadConfig, DEFAULT_CONFIG, findProjectRoot } from "./config";
|
|
2
|
+
import { loadConfig, DEFAULT_CONFIG, findProjectRoot, resolveConfiguredSeverity, applyConfiguredSeverity } from "./config";
|
|
3
|
+
import type { PostSynthDiagnostic } from "./post-synth";
|
|
3
4
|
import { writeFileSync, mkdirSync, rmSync } from "fs";
|
|
4
5
|
import { join, resolve } from "path";
|
|
5
6
|
|
|
@@ -717,3 +718,94 @@ describe("findProjectRoot", () => {
|
|
|
717
718
|
expect(findProjectRoot(sub)).toBe(packageRoot);
|
|
718
719
|
});
|
|
719
720
|
});
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* chant #1138 — the one severity-resolution path AST lint rules
|
|
724
|
+
* (`../cli/commands/lint.ts`'s `getDefaultRules`), COMP* checks
|
|
725
|
+
* (`runComponentCheckDiagnostics`), and post-synth checks/policies
|
|
726
|
+
* (`applyConfiguredSeverity`, below) all now call, keyed by whichever id the
|
|
727
|
+
* caller has (a `LintRule.id`, a `ComponentCheck.id`, or a
|
|
728
|
+
* `PostSynthDiagnostic.checkId`) — a rule id behaves the same regardless of
|
|
729
|
+
* which phase produced it.
|
|
730
|
+
*/
|
|
731
|
+
describe("resolveConfiguredSeverity", () => {
|
|
732
|
+
test("an id with no config entry falls back to the caller's default severity, with no options", () => {
|
|
733
|
+
expect(resolveConfiguredSeverity(undefined, "COR001", "error")).toEqual({ severity: "error" });
|
|
734
|
+
expect(resolveConfiguredSeverity({}, "COR001", "warning")).toEqual({ severity: "warning" });
|
|
735
|
+
expect(resolveConfiguredSeverity({ OTHER: "off" }, "COR001", "error")).toEqual({ severity: "error" });
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test("a bare severity string overrides the default", () => {
|
|
739
|
+
expect(resolveConfiguredSeverity({ COR001: "warning" }, "COR001", "error")).toEqual({ severity: "warning" });
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test('"off" suppresses regardless of the default severity', () => {
|
|
743
|
+
expect(resolveConfiguredSeverity({ WAW019: "off" }, "WAW019", "error")).toEqual({ severity: "off" });
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test("a [severity, options] tuple carries options through", () => {
|
|
747
|
+
expect(resolveConfiguredSeverity({ COR009: ["warning", { max: 12 }] }, "COR009", "error")).toEqual({
|
|
748
|
+
severity: "warning",
|
|
749
|
+
options: { max: 12 },
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("an invalid severity in a [severity, options] tuple throws, naming the bad value", () => {
|
|
754
|
+
expect(() =>
|
|
755
|
+
resolveConfiguredSeverity({ COR009: ["fatal" as never, { max: 12 }] }, "COR009", "error"),
|
|
756
|
+
).toThrow(/severity "fatal"/);
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* chant #1138 — `lint.rules` severity overrides apply to a post-synth check
|
|
762
|
+
* id (`diag.checkId`) through the identical `resolveConfiguredSeverity` an
|
|
763
|
+
* AST rule id or a COMP* check id goes through, so
|
|
764
|
+
* `lint.rules: { WAW019: "off" }` suppresses a post-synth finding just like a
|
|
765
|
+
* pre-synth one — the bug this issue reports.
|
|
766
|
+
*/
|
|
767
|
+
describe("applyConfiguredSeverity", () => {
|
|
768
|
+
function diag(overrides: Partial<PostSynthDiagnostic> = {}): PostSynthDiagnostic {
|
|
769
|
+
return { checkId: "WAW019", severity: "error", message: "open ingress", ...overrides };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
test("an unconfigured check id passes through unchanged — no drift for the common case", () => {
|
|
773
|
+
const result = applyConfiguredSeverity([diag()], undefined);
|
|
774
|
+
expect(result.diagnostics).toEqual([diag()]);
|
|
775
|
+
expect(result.suppressed).toEqual([]);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test('"off" suppresses the finding — moved to `suppressed`, not dropped, so it stays countable', () => {
|
|
779
|
+
const result = applyConfiguredSeverity([diag()], { WAW019: "off" });
|
|
780
|
+
expect(result.diagnostics).toEqual([]);
|
|
781
|
+
expect(result.suppressed).toEqual([diag()]);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test('"warning" downgrades an error-severity finding', () => {
|
|
785
|
+
const result = applyConfiguredSeverity([diag({ severity: "error" })], { WAW019: "warning" });
|
|
786
|
+
expect(result.diagnostics).toEqual([diag({ severity: "warning" })]);
|
|
787
|
+
expect(result.suppressed).toEqual([]);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
test('"error" upgrades a warning-severity finding', () => {
|
|
791
|
+
const result = applyConfiguredSeverity([diag({ severity: "warning" })], { WAW019: "error" });
|
|
792
|
+
expect(result.diagnostics).toEqual([diag({ severity: "error" })]);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test("resolves each diagnostic by its own checkId — one config, independent ids", () => {
|
|
796
|
+
const diags = [
|
|
797
|
+
diag({ checkId: "WAW019" }),
|
|
798
|
+
diag({ checkId: "WAW049", message: "no logging" }),
|
|
799
|
+
diag({ checkId: "WAW099", message: "untouched" }),
|
|
800
|
+
];
|
|
801
|
+
const result = applyConfiguredSeverity(diags, { WAW019: "off", WAW049: "warning" });
|
|
802
|
+
expect(result.suppressed.map((d) => d.checkId)).toEqual(["WAW019"]);
|
|
803
|
+
expect(result.diagnostics.map((d) => d.checkId)).toEqual(["WAW049", "WAW099"]);
|
|
804
|
+
expect(result.diagnostics.find((d) => d.checkId === "WAW049")?.severity).toBe("warning");
|
|
805
|
+
expect(result.diagnostics.find((d) => d.checkId === "WAW099")?.severity).toBe("error");
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test("an empty diagnostics list is a no-op", () => {
|
|
809
|
+
expect(applyConfiguredSeverity([], { WAW019: "off" })).toEqual({ diagnostics: [], suppressed: [] });
|
|
810
|
+
});
|
|
811
|
+
});
|
package/src/lint/config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join, dirname, resolve } from "path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { evaluateProjectConfigSync } from "../config-sandbox";
|
|
5
5
|
import type { Severity, RuleConfig } from "./rule";
|
|
6
|
+
import type { PostSynthDiagnostic } from "./post-synth";
|
|
6
7
|
import { moduleDir, getRuntime } from "../runtime-adapter";
|
|
7
8
|
import strictPreset from "./presets/strict.json";
|
|
8
9
|
|
|
@@ -202,6 +203,40 @@ export function parseRuleConfig(value: RuleConfig): ParsedRuleConfig {
|
|
|
202
203
|
return { severity, options };
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Resolve one check/rule id's effective severity (and options) against an
|
|
208
|
+
* already-resolved `lint.rules` map — the ONE place `"off"`/severity-override
|
|
209
|
+
* resolution happens, so a rule id behaves identically regardless of which
|
|
210
|
+
* phase produced it.
|
|
211
|
+
*
|
|
212
|
+
* chant #1138 — before this, the same `lint.rules: { ID: "off" }` config was
|
|
213
|
+
* resolved by two independent call sites that had grown their own copy of
|
|
214
|
+
* this logic (`../cli/commands/lint.ts`'s `getDefaultRules` for AST COR/EVL
|
|
215
|
+
* rules, and its `runComponentCheckDiagnostics` for whole-component COMP*
|
|
216
|
+
* checks) — identical in effect, but a rule id's suppression having two
|
|
217
|
+
* places to (potentially, eventually) diverge is itself the bug class #1138
|
|
218
|
+
* is about. Both were converted to call this instead, and post-synth checks/
|
|
219
|
+
* policies (`./post-synth.ts`'s `applyConfiguredSeverity`) now go through it
|
|
220
|
+
* too, closing the gap the issue reports: a post-synth check id honors
|
|
221
|
+
* `lint.rules` exactly like an AST rule id does.
|
|
222
|
+
*
|
|
223
|
+
* `rules` takes the already-resolved map (`config.rules`, or
|
|
224
|
+
* `resolveRulesForFile`'s per-file merge) rather than a whole `LintConfig` —
|
|
225
|
+
* callers that need per-file `overrides` resolve that first; post-synth
|
|
226
|
+
* checks have no per-file scope to begin with (see {@link
|
|
227
|
+
* ./post-synth.ts!PostSynthDiagnostic}'s doc for why), so they always pass
|
|
228
|
+
* `config.rules` directly.
|
|
229
|
+
*/
|
|
230
|
+
export function resolveConfiguredSeverity(
|
|
231
|
+
rules: Record<string, RuleConfig> | undefined,
|
|
232
|
+
id: string,
|
|
233
|
+
defaultSeverity: Severity,
|
|
234
|
+
): ParsedRuleConfig {
|
|
235
|
+
const configValue = rules?.[id];
|
|
236
|
+
if (configValue === undefined) return { severity: defaultSeverity };
|
|
237
|
+
return parseRuleConfig(configValue);
|
|
238
|
+
}
|
|
239
|
+
|
|
205
240
|
/**
|
|
206
241
|
* Default configuration with all rules enabled at strict preset severities
|
|
207
242
|
*/
|
|
@@ -411,3 +446,76 @@ export function resolveRulesForFile(config: LintConfig, filePath: string): Recor
|
|
|
411
446
|
|
|
412
447
|
return rules;
|
|
413
448
|
}
|
|
449
|
+
|
|
450
|
+
/** Result of applying `lint.rules` to a set of post-synth diagnostics. */
|
|
451
|
+
export interface PostSynthSeverityResult {
|
|
452
|
+
/** Diagnostics after config resolution — `"off"`-suppressed ones removed, everything else at its resolved severity. */
|
|
453
|
+
diagnostics: PostSynthDiagnostic[];
|
|
454
|
+
/**
|
|
455
|
+
* Diagnostics `lint.rules` turned `"off"`, unaltered — present so a caller
|
|
456
|
+
* can report a count (chant #1138) rather than the finding simply
|
|
457
|
+
* vanishing, mirroring `../lint/engine.ts`'s `LintRunResult.suppressed` for
|
|
458
|
+
* AST rules.
|
|
459
|
+
*/
|
|
460
|
+
suppressed: PostSynthDiagnostic[];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Apply `lint.rules` severity overrides to already-produced post-synth
|
|
465
|
+
* diagnostics — one lexicon-shipped check's findings, or one project's
|
|
466
|
+
* `lint.policies` findings, it doesn't matter which: both are plain
|
|
467
|
+
* `PostSynthDiagnostic[]` by the time they reach this function, so both go
|
|
468
|
+
* through the identical resolution an AST rule id or a COMP* check id gets
|
|
469
|
+
* (`resolveConfiguredSeverity`, above), keyed by `diag.checkId` instead of
|
|
470
|
+
* `LintRule.id`/`ComponentCheck.id`. `"off"` suppresses (moved to
|
|
471
|
+
* `suppressed`); any other configured severity replaces `diag.severity`,
|
|
472
|
+
* exactly as a config override changes an AST diagnostic's reported level.
|
|
473
|
+
*
|
|
474
|
+
* Lives here rather than in `./post-synth.ts` (where `PostSynthDiagnostic` is
|
|
475
|
+
* declared) on purpose: `post-synth.ts` is a leaf every lexicon's checks
|
|
476
|
+
* import as a real runtime module, and this file resolves built-in preset
|
|
477
|
+
* paths via the runtime adapter at module scope — pulling that into every
|
|
478
|
+
* lexicon's check barrel merely to share one filter function would be the
|
|
479
|
+
* wrong trade. Only this file's TYPE (`PostSynthDiagnostic`) crosses back,
|
|
480
|
+
* which costs nothing at runtime.
|
|
481
|
+
*
|
|
482
|
+
* chant #1138 — deliberately does NOT also honor `chant-disable` source
|
|
483
|
+
* comments. `PostSynthDiagnostic` has no source anchor to disable AT — see
|
|
484
|
+
* its doc comment (`./post-synth.ts`) for why `entity` doesn't supply one —
|
|
485
|
+
* so there is no coherent site to check for a directive. Even in the one case
|
|
486
|
+
* a real anchor sometimes exists (a live, in-process `ctx.entities` value
|
|
487
|
+
* stamped with build provenance, `../provenance.ts`'s `getProvenance`), it
|
|
488
|
+
* would not generalize: not every check sets `entity`, `entity` isn't
|
|
489
|
+
* guaranteed to be an entities-map key (it's a name in the synthesized
|
|
490
|
+
* OUTPUT — a CFN logical id, a k8s `metadata.name` — which a serializer is
|
|
491
|
+
* free to have derived, prefixed, or renamed from the source-level entity
|
|
492
|
+
* name), and that provenance never crosses the `--sandbox` policy child's
|
|
493
|
+
* JSON wire (`../discovery/entity-wire-codec.ts` doesn't carry it, and
|
|
494
|
+
* re-deriving it on the far side would mean sending source file paths into a
|
|
495
|
+
* channel that's supposed to carry only the resolved build). Building
|
|
496
|
+
* directive suppression on a sometimes-present, sometimes-not anchor would
|
|
497
|
+
* make `chant build` and `chant build --sandbox` disagree about the
|
|
498
|
+
* identical finding depending on which path happened to still have the
|
|
499
|
+
* entity object around — precisely the kind of inconsistency #1138 exists to
|
|
500
|
+
* remove. Config severity (`"off"`) is the one suppression surface this can
|
|
501
|
+
* offer uniformly; a check that wants a per-instance escape hatch can read
|
|
502
|
+
* `ctx.env`/its own options to decide not to emit a diagnostic at all.
|
|
503
|
+
*/
|
|
504
|
+
export function applyConfiguredSeverity(
|
|
505
|
+
diagnostics: readonly PostSynthDiagnostic[],
|
|
506
|
+
rules: Record<string, RuleConfig> | undefined,
|
|
507
|
+
): PostSynthSeverityResult {
|
|
508
|
+
const kept: PostSynthDiagnostic[] = [];
|
|
509
|
+
const suppressed: PostSynthDiagnostic[] = [];
|
|
510
|
+
|
|
511
|
+
for (const diag of diagnostics) {
|
|
512
|
+
const resolved = resolveConfiguredSeverity(rules, diag.checkId, diag.severity);
|
|
513
|
+
if (resolved.severity === "off") {
|
|
514
|
+
suppressed.push(diag);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
kept.push(resolved.severity === diag.severity ? diag : { ...diag, severity: resolved.severity });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return { diagnostics: kept, suppressed };
|
|
521
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chant #1138 — `evaluateProjectPolicies` (the `policyGate` Op step's entry
|
|
3
|
+
* point, ../../lexicons/temporal/src/op/activities/policy.ts) applies
|
|
4
|
+
* `lint.rules` severity overrides to policy diagnostics the same way `chant
|
|
5
|
+
* build` does (`../cli/commands/build.ts`), so a check `lint.rules` turns
|
|
6
|
+
* "off" no longer gates an apply either — before this fix, only `chant
|
|
7
|
+
* build`'s own error list was affected (and, before #1138, not even that).
|
|
8
|
+
*
|
|
9
|
+
* No lexicon is declared: the fixture project has no source files, so
|
|
10
|
+
* `resolveProjectLexicons` detects none and `build()` succeeds trivially with
|
|
11
|
+
* zero entities — the policy pack is the only thing under test.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
14
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { evaluateProjectPolicies } from "./policy";
|
|
18
|
+
|
|
19
|
+
describe("evaluateProjectPolicies honors lint.rules (chant #1138)", () => {
|
|
20
|
+
let testDir: string;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
testDir = join(tmpdir(), `chant-policy-eval-test-${Date.now()}-${Math.random()}`);
|
|
24
|
+
await mkdir(join(testDir, "policies"), { recursive: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await rm(testDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function writePolicyProject(checkId: string, severity: "error" | "warning", rules: Record<string, string>): Promise<void> {
|
|
32
|
+
await writeFile(
|
|
33
|
+
join(testDir, "policies", "org.ts"),
|
|
34
|
+
`export const check = {\n` +
|
|
35
|
+
` id: ${JSON.stringify(checkId)},\n` +
|
|
36
|
+
` description: "test policy",\n` +
|
|
37
|
+
` check: () => [{ checkId: ${JSON.stringify(checkId)}, severity: ${JSON.stringify(severity)}, message: ${JSON.stringify(`${checkId} triggered`)} }],\n` +
|
|
38
|
+
`};\n`,
|
|
39
|
+
);
|
|
40
|
+
await writeFile(
|
|
41
|
+
join(testDir, "chant.config.ts"),
|
|
42
|
+
// `lexicons: ["k8s"]` avoids `resolveProjectLexicons`'s source-import
|
|
43
|
+
// auto-detection, which throws on a project with no lexicon-importing
|
|
44
|
+
// source file at all (this fixture has none — the policy pack is the
|
|
45
|
+
// only thing under test).
|
|
46
|
+
`export default { lexicons: ["k8s"], lint: { policies: ["policies/org.ts"], rules: ${JSON.stringify(rules)} } };\n`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('lint.rules "off" removes a check from violations and reports it as suppressed', async () => {
|
|
51
|
+
await writePolicyProject("ORG-OFF", "error", { "ORG-OFF": "off" });
|
|
52
|
+
|
|
53
|
+
const evaluation = await evaluateProjectPolicies({ path: testDir });
|
|
54
|
+
|
|
55
|
+
expect(evaluation.violations).toEqual([]);
|
|
56
|
+
expect(evaluation.diagnostics).toEqual([]);
|
|
57
|
+
expect(evaluation.suppressed).toHaveLength(1);
|
|
58
|
+
expect(evaluation.suppressed[0].checkId).toBe("ORG-OFF");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('lint.rules "warning" downgrades an error-severity check out of violations', async () => {
|
|
62
|
+
await writePolicyProject("ORG-DOWN", "error", { "ORG-DOWN": "warning" });
|
|
63
|
+
|
|
64
|
+
const evaluation = await evaluateProjectPolicies({ path: testDir });
|
|
65
|
+
|
|
66
|
+
expect(evaluation.violations).toEqual([]);
|
|
67
|
+
expect(evaluation.diagnostics).toHaveLength(1);
|
|
68
|
+
expect(evaluation.diagnostics[0].severity).toBe("warning");
|
|
69
|
+
expect(evaluation.suppressed).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('lint.rules "error" upgrades a warning-severity check INTO violations', async () => {
|
|
73
|
+
await writePolicyProject("ORG-UP", "warning", { "ORG-UP": "error" });
|
|
74
|
+
|
|
75
|
+
const evaluation = await evaluateProjectPolicies({ path: testDir });
|
|
76
|
+
|
|
77
|
+
expect(evaluation.violations).toHaveLength(1);
|
|
78
|
+
expect(evaluation.violations[0].checkId).toBe("ORG-UP");
|
|
79
|
+
expect(evaluation.violations[0].severity).toBe("error");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("an unconfigured check id is unaffected — no drift for the common case", async () => {
|
|
83
|
+
await writePolicyProject("ORG-PLAIN", "error", {});
|
|
84
|
+
|
|
85
|
+
const evaluation = await evaluateProjectPolicies({ path: testDir });
|
|
86
|
+
|
|
87
|
+
expect(evaluation.violations).toHaveLength(1);
|
|
88
|
+
expect(evaluation.suppressed).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/src/lint/policy.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { resolveProjectLexicons, loadPlugins } from "../cli/plugins";
|
|
|
9
9
|
import { build } from "../build";
|
|
10
10
|
import { runPostSynthChecks, isPostSynthCheck } from "./post-synth";
|
|
11
11
|
import type { PostSynthCheck, PostSynthDiagnostic } from "./post-synth";
|
|
12
|
+
import { applyConfiguredSeverity } from "./config";
|
|
12
13
|
import { importPolicyModule, isSandboxPolicyExecutionArmed } from "./policy-import";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -49,12 +50,18 @@ export async function loadPolicyChecks(paths: string[], configDir: string): Prom
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export interface PolicyEvaluation {
|
|
52
|
-
/** All policy diagnostics (errors + warnings). */
|
|
53
|
+
/** All policy diagnostics (errors + warnings), after `lint.rules` severity overrides. */
|
|
53
54
|
diagnostics: PostSynthDiagnostic[];
|
|
54
|
-
/** The error-severity subset — these are policy *violations* that gate. */
|
|
55
|
+
/** The error-severity subset (post-override) — these are policy *violations* that gate. */
|
|
55
56
|
violations: PostSynthDiagnostic[];
|
|
56
57
|
/** The environment policies were evaluated against (if any). */
|
|
57
58
|
env?: string;
|
|
59
|
+
/**
|
|
60
|
+
* chant #1138 — diagnostics `lint.rules` turned `"off"`, present here rather
|
|
61
|
+
* than dropped so a caller (`policyGate`, below) can report a count instead
|
|
62
|
+
* of the finding just vanishing.
|
|
63
|
+
*/
|
|
64
|
+
suppressed: PostSynthDiagnostic[];
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/**
|
|
@@ -90,10 +97,15 @@ export async function evaluateProjectPolicies(opts: {
|
|
|
90
97
|
? await loadPolicyChecks(config.lint.policies, configDir)
|
|
91
98
|
: [];
|
|
92
99
|
if (checks.length === 0) {
|
|
93
|
-
return { diagnostics: [], violations: [], env };
|
|
100
|
+
return { diagnostics: [], violations: [], env, suppressed: [] };
|
|
94
101
|
}
|
|
95
102
|
|
|
96
|
-
const
|
|
103
|
+
const raw = runPostSynthChecks(checks, result, env);
|
|
104
|
+
// chant #1138 — same `lint.rules` resolution `chant build` applies
|
|
105
|
+
// (`../cli/commands/build.ts`), so a check `lint.rules` turned "off"/
|
|
106
|
+
// "warning" doesn't gate an apply here even though `chant build` no longer
|
|
107
|
+
// fails on it either, and vice versa for a check turned UP to "error".
|
|
108
|
+
const { diagnostics, suppressed } = applyConfiguredSeverity(raw, config.lint?.rules);
|
|
97
109
|
const violations = diagnostics.filter((d) => d.severity === "error");
|
|
98
|
-
return { diagnostics, violations, env };
|
|
110
|
+
return { diagnostics, violations, env, suppressed };
|
|
99
111
|
}
|
|
@@ -141,3 +141,7 @@ describe("isPostSynthCheck", () => {
|
|
|
141
141
|
expect(isPostSynthCheck("nope")).toBe(false);
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
// chant #1138 — `applyConfiguredSeverity` (the `lint.rules` severity-override
|
|
146
|
+
// pass over `PostSynthDiagnostic`s) is tested in `./config.test.ts`, where the
|
|
147
|
+
// function itself now lives — see `./config.ts`'s doc comment for why.
|