@intentius/chant 0.46.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/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +1 -1
- package/dist/cli/handlers/lifecycle.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/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.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 +26 -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/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 +44 -3
- 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 +2 -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 +6 -4
- package/dist/lifecycle/teardown.d.ts.map +1 -1
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- 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/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 +3 -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 +98 -1
- 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/yaml.d.ts.map +1 -1
- package/package.json +4 -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 +80 -0
- package/src/cli/commands/build.ts +106 -8
- package/src/cli/commands/lint.ts +15 -3
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.ts +2 -2
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +84 -11
- 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/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- 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 +15 -0
- package/src/config.ts +30 -0
- 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/fold/subset.test.ts +26 -0
- package/src/fold/subset.ts +45 -19
- package/src/index.ts +4 -0
- package/src/lexicon.ts +48 -3
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/index.ts +2 -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 +31 -0
- package/src/lifecycle/teardown.ts +6 -4
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- 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/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 +7 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/builders.ts +139 -1
- 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/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
import { computeBuildDigest, diffDigests } from "../../lifecycle/digest";
|
|
19
19
|
import { diffLive, diffLiveArtifacts, diffSnapshots, type LiveDiffResult, type LiveArtifactDiffResult, type SnapshotDiffResult } from "../../lifecycle/live-diff";
|
|
20
20
|
import { buildChangeSet, renderChangeSet, gitlabMrReport, summarize, type ChangeSet } from "../../lifecycle/change-set";
|
|
21
|
+
import { mergeReceiptEntries, observedValueResolver, planReceipts, readReceiptValue, type ReceiptReading } from "../../lifecycle/receipt-plan";
|
|
22
|
+
import { collectEffectReceipts, isEffectReceipt } from "../../effect-receipt";
|
|
21
23
|
import {
|
|
22
24
|
formatUnobserved,
|
|
23
25
|
mergeObservations,
|
|
@@ -358,6 +360,10 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
358
360
|
// committed set. Absent is the normal state (nothing accepted yet).
|
|
359
361
|
const baseline = args.live ? await readObservationBaseline(environment) : null;
|
|
360
362
|
const accepted: Record<string, DeviationToAccept[]> = {};
|
|
363
|
+
// Effect receipts across every target's build (#1833) — `--update-baseline`
|
|
364
|
+
// refuses to accept drift on them (the effect step is their only writer),
|
|
365
|
+
// and recognition is marker-based so materialized lexicon rows are caught.
|
|
366
|
+
const receiptEntities = new Set<string>();
|
|
361
367
|
|
|
362
368
|
// #1166 — an environment can declare its own endpoint (a local emulator like
|
|
363
369
|
// Floci), so `--live` is self-sufficient even when the ambient shell never
|
|
@@ -430,6 +436,9 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
430
436
|
for (const [lexicon, deviations] of Object.entries(r.toAccept)) {
|
|
431
437
|
(accepted[lexicon] ??= []).push(...deviations);
|
|
432
438
|
}
|
|
439
|
+
if (args.updateBaseline) {
|
|
440
|
+
for (const name of collectEffectReceipts(buildResult.entities).keys()) receiptEntities.add(name);
|
|
441
|
+
}
|
|
433
442
|
if (json) {
|
|
434
443
|
if (target.stack) perStackJson[target.stack] = r.byLexicon;
|
|
435
444
|
else combinedLexiconsJson = r.byLexicon;
|
|
@@ -443,7 +452,7 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
443
452
|
// accepted, so it stops re-alerting. Runs before the summary lines so the
|
|
444
453
|
// "no drift" verdict below still describes the run that produced it.
|
|
445
454
|
if (args.live && args.updateBaseline) {
|
|
446
|
-
await recordAcceptedBaseline(environment, baseline, accepted, json);
|
|
455
|
+
await recordAcceptedBaseline(environment, baseline, accepted, json, receiptEntities);
|
|
447
456
|
}
|
|
448
457
|
|
|
449
458
|
if (args.live) {
|
|
@@ -495,6 +504,7 @@ async function recordAcceptedBaseline(
|
|
|
495
504
|
existing: ObservationBaseline | null,
|
|
496
505
|
accepted: Record<string, DeviationToAccept[]>,
|
|
497
506
|
json: boolean,
|
|
507
|
+
receipts?: ReadonlySet<string>,
|
|
498
508
|
): Promise<void> {
|
|
499
509
|
const total = Object.values(accepted).reduce((n, d) => n + d.length, 0);
|
|
500
510
|
if (total === 0) {
|
|
@@ -505,11 +515,14 @@ async function recordAcceptedBaseline(
|
|
|
505
515
|
}
|
|
506
516
|
return;
|
|
507
517
|
}
|
|
508
|
-
let next = existing ?? emptyBaseline(environment);
|
|
509
|
-
for (const [lexicon, deviations] of Object.entries(accepted)) {
|
|
510
|
-
next = acceptDeviations(next, lexicon, deviations);
|
|
511
|
-
}
|
|
512
518
|
try {
|
|
519
|
+
// `acceptDeviations` throws on an effect-receipt deviation (#1833) —
|
|
520
|
+
// inside the try so the refusal reaches the operator as a formatted
|
|
521
|
+
// error, with no baseline written at all.
|
|
522
|
+
let next = existing ?? emptyBaseline(environment);
|
|
523
|
+
for (const [lexicon, deviations] of Object.entries(accepted)) {
|
|
524
|
+
next = acceptDeviations(next, lexicon, deviations, { receipts });
|
|
525
|
+
}
|
|
513
526
|
await writeObservationBaseline(next);
|
|
514
527
|
const pushed = await pushLifecycle();
|
|
515
528
|
if (!json) {
|
|
@@ -520,7 +533,7 @@ async function recordAcceptedBaseline(
|
|
|
520
533
|
}
|
|
521
534
|
} catch (err) {
|
|
522
535
|
console.error(formatError({
|
|
523
|
-
message: `--update-baseline:
|
|
536
|
+
message: `--update-baseline: baseline not updated — ${err instanceof Error ? err.message : String(err)}`,
|
|
524
537
|
}));
|
|
525
538
|
}
|
|
526
539
|
}
|
|
@@ -1126,6 +1139,15 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1126
1139
|
const merged: ChangeSet = { env: environment, entries: [] };
|
|
1127
1140
|
let checked = 0;
|
|
1128
1141
|
|
|
1142
|
+
// Effect receipts (#1832): declared, diffed, and observed like any resource,
|
|
1143
|
+
// but observe-only to the generic apply path — the plan compares live value
|
|
1144
|
+
// to resolved expectation and proposes the fire, never a create/update.
|
|
1145
|
+
const receipts = collectEffectReceipts(buildResult.entities);
|
|
1146
|
+
const receiptReadings = new Map<string, ReceiptReading>();
|
|
1147
|
+
// Every lexicon's observed resources merged, for resolving a receipt's
|
|
1148
|
+
// reference inputs against observed values / stack outputs at plan time.
|
|
1149
|
+
const allObservedResources: Record<string, ResourceMetadata> = {};
|
|
1150
|
+
|
|
1129
1151
|
// #1166 — same self-sufficiency as `chant graph --live`: an environment can
|
|
1130
1152
|
// declare its own endpoint, applied here unless the ambient shell already
|
|
1131
1153
|
// set it. `chant lifecycle plan` is always a live read (no `--live` flag of
|
|
@@ -1156,11 +1178,14 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1156
1178
|
const declared = new Set<string>();
|
|
1157
1179
|
const entities = new Map<string, { entityType: string; props: Record<string, unknown> }>();
|
|
1158
1180
|
for (const [name, entity] of buildResult.entities) {
|
|
1159
|
-
|
|
1181
|
+
// A receipt has no `props` payload of its own but is declared, diffed,
|
|
1182
|
+
// and observed like any resource (#1832) — it joins the declared axis
|
|
1183
|
+
// so its lexicon's observation can confirm presence or absence.
|
|
1184
|
+
if (entity.lexicon === lexiconName && (isResourceDeclarable(entity) || isEffectReceipt(entity))) {
|
|
1160
1185
|
declared.add(name);
|
|
1161
1186
|
entities.set(name, {
|
|
1162
1187
|
entityType: entity.entityType,
|
|
1163
|
-
props: (entity.props != null ? entity.props : {}) as Record<string, unknown>,
|
|
1188
|
+
props: (isResourceDeclarable(entity) && entity.props != null ? entity.props : {}) as Record<string, unknown>,
|
|
1164
1189
|
});
|
|
1165
1190
|
}
|
|
1166
1191
|
}
|
|
@@ -1185,6 +1210,37 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1185
1210
|
...(args.namespace ? { namespace: args.namespace } : {}),
|
|
1186
1211
|
});
|
|
1187
1212
|
|
|
1213
|
+
Object.assign(allObservedResources, observed.resources);
|
|
1214
|
+
for (const [receiptName, receipt] of receipts) {
|
|
1215
|
+
if (receipt.lexicon !== lexiconName) continue;
|
|
1216
|
+
const live = observed.resources[receiptName];
|
|
1217
|
+
const hole = observed.unobserved[receiptName];
|
|
1218
|
+
if (live) {
|
|
1219
|
+
receiptReadings.set(receiptName, {
|
|
1220
|
+
observed: true,
|
|
1221
|
+
present: true,
|
|
1222
|
+
value: readReceiptValue(live.attributes),
|
|
1223
|
+
type: live.type,
|
|
1224
|
+
...(live.physicalId ? { physicalId: live.physicalId } : {}),
|
|
1225
|
+
lexicon: lexiconName,
|
|
1226
|
+
});
|
|
1227
|
+
} else if (hole) {
|
|
1228
|
+
receiptReadings.set(receiptName, {
|
|
1229
|
+
observed: false,
|
|
1230
|
+
present: false,
|
|
1231
|
+
lexicon: lexiconName,
|
|
1232
|
+
...(hole.type ? { type: hole.type } : {}),
|
|
1233
|
+
unobservedReason: hole.reason,
|
|
1234
|
+
...(hole.detail ? { unobservedDetail: hole.detail } : {}),
|
|
1235
|
+
});
|
|
1236
|
+
} else {
|
|
1237
|
+
// Neither returned nor named unobserved: the lexicon looked and
|
|
1238
|
+
// confirmed the receipt absent — the same claim the change set
|
|
1239
|
+
// reads off a bare observation (#1089).
|
|
1240
|
+
receiptReadings.set(receiptName, { observed: true, present: false, lexicon: lexiconName });
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1188
1244
|
const content = await readSnapshot(environment, lexiconName);
|
|
1189
1245
|
const observedThen = content ? (JSON.parse(content) as LifecycleSnapshot).resources : undefined;
|
|
1190
1246
|
|
|
@@ -1214,6 +1270,16 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1214
1270
|
return 1;
|
|
1215
1271
|
}
|
|
1216
1272
|
|
|
1273
|
+
// Receipt classification (#1832): live value vs resolved expectation.
|
|
1274
|
+
// Absent/stale proposes the fire; unobservable is a loud hole; whatever the
|
|
1275
|
+
// generic classification proposed for a receipt (a create, a delete) is
|
|
1276
|
+
// replaced — the effect step is the sole writer, and a receipt is never a
|
|
1277
|
+
// prune candidate.
|
|
1278
|
+
if (receipts.size > 0) {
|
|
1279
|
+
const receiptEntries = planReceipts(receipts, receiptReadings, observedValueResolver(allObservedResources));
|
|
1280
|
+
mergeReceiptEntries(merged, receipts, receiptEntries);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1217
1283
|
merged.entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1218
1284
|
|
|
1219
1285
|
// Say it on stderr too, so `--json` and `--report gitlab-mr` consumers (whose
|
|
@@ -1250,7 +1316,7 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1250
1316
|
* with the requested env identity. Stateless — live markers only, no build, no
|
|
1251
1317
|
* snapshot. Without `--yes` nothing is deleted; with it the planned set is
|
|
1252
1318
|
* executed per lexicon with one bounded retry pass over failures, and every
|
|
1253
|
-
* candidate's outcome is reported (deleted / failed / not-prunable / skipped —
|
|
1319
|
+
* candidate's outcome is reported (deleted / failed / not-prunable / retained / skipped —
|
|
1254
1320
|
* never silence). A production-like environment name additionally requires an
|
|
1255
1321
|
* interactive confirmation, or `--confirm-prod` non-interactively.
|
|
1256
1322
|
*/
|
|
@@ -1410,10 +1476,11 @@ export async function runLifecycleTeardown(ctx: CommandContext): Promise<number>
|
|
|
1410
1476
|
}
|
|
1411
1477
|
}
|
|
1412
1478
|
|
|
1413
|
-
const counts = { deleted: 0, failed: 0, "not-prunable": 0, skipped: 0 };
|
|
1479
|
+
const counts = { deleted: 0, failed: 0, "not-prunable": 0, retained: 0, skipped: 0 };
|
|
1414
1480
|
for (const o of report.outcomes) counts[o.outcome]++;
|
|
1415
1481
|
console.log(
|
|
1416
|
-
`\n${counts.deleted} deleted, ${counts.failed} failed, ${counts["not-prunable"]} not prunable,
|
|
1482
|
+
`\n${counts.deleted} deleted, ${counts.failed} failed, ${counts["not-prunable"]} not prunable, ` +
|
|
1483
|
+
`${counts.retained} retained, ${counts.skipped} skipped`,
|
|
1417
1484
|
);
|
|
1418
1485
|
|
|
1419
1486
|
if (report.unimplemented.length > 0) {
|
|
@@ -1421,6 +1488,12 @@ export async function runLifecycleTeardown(ctx: CommandContext): Promise<number>
|
|
|
1421
1488
|
message: `Not executed (no teardown execution in these lexicons yet): ${report.unimplemented.join(", ")} — their candidates are reported as skipped, not deleted.`,
|
|
1422
1489
|
}));
|
|
1423
1490
|
}
|
|
1491
|
+
if (counts.retained > 0) {
|
|
1492
|
+
console.error(formatWarning({
|
|
1493
|
+
message: `${counts.retained} resource(s) retained — owned by this env but deliberately kept (generated-once secrets are never swept). ` +
|
|
1494
|
+
`The environment is NOT clean while they exist; delete them explicitly (e.g. kubectl delete) if you mean to.`,
|
|
1495
|
+
}));
|
|
1496
|
+
}
|
|
1424
1497
|
if (plan.holes.length > 0) {
|
|
1425
1498
|
console.error(formatWarning({
|
|
1426
1499
|
message: "This teardown ran over an incomplete plan (holes above) — the environment cannot be called clean.",
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { resolve, join, dirname } from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
2
3
|
import type { ResourceDefinition } from "./types";
|
|
3
4
|
import { getContext } from "./resources/context";
|
|
4
5
|
import { readSnapshot, readEnvironmentSnapshots } from "../../lifecycle/git";
|
|
5
6
|
import { discoverOps } from "../../op/discover";
|
|
6
7
|
import { makeTemporalClient } from "../handlers/run";
|
|
7
8
|
import { resolveWorkflowId } from "../handlers/run-client";
|
|
9
|
+
import { loadOkfBundle } from "../../okf-read";
|
|
10
|
+
import { loadChantConfigUpward, resolveKnowledgeDir } from "../../config";
|
|
8
11
|
|
|
9
12
|
type PluginResourceEntry = { definition: ResourceDefinition; handler: () => Promise<string> };
|
|
10
13
|
|
|
@@ -42,6 +45,12 @@ export const coreResourceDefinitions: ResourceDefinition[] = [
|
|
|
42
45
|
description: "Latest run state for a named Op",
|
|
43
46
|
mimeType: "application/json",
|
|
44
47
|
},
|
|
48
|
+
{
|
|
49
|
+
uri: "chant://knowledge",
|
|
50
|
+
name: "Knowledge bundle",
|
|
51
|
+
description: "The project's OKF knowledge bundle (#1864, #1059): index.md and every authored concept, empty when no bundle exists",
|
|
52
|
+
mimeType: "application/json",
|
|
53
|
+
},
|
|
45
54
|
{
|
|
46
55
|
uri: "chant://state/{environment}",
|
|
47
56
|
name: "State (all lexicons)",
|
|
@@ -122,6 +131,34 @@ export async function handleResourcesRead(
|
|
|
122
131
|
};
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
// Authored knowledge (#1867, #1864, design #1059): same bundle the CLI's
|
|
135
|
+
// `chant explain` reads, over MCP — the resource path has no local
|
|
136
|
+
// filesystem, so this is the only way an MCP client sees the `knowledge/`
|
|
137
|
+
// directory's contents at all. Resolved from cwd, matching every other
|
|
138
|
+
// path-less resource here (`chant://ops`, `chant://state/...`). A missing
|
|
139
|
+
// bundle is not an error — `loadOkfBundle` already treats it as empty, and
|
|
140
|
+
// a missing `index.md` reads as `index: null` rather than throwing.
|
|
141
|
+
if (uri === "chant://knowledge") {
|
|
142
|
+
const cwd = resolve(".");
|
|
143
|
+
const loaded = await loadChantConfigUpward(cwd);
|
|
144
|
+
const projectRoot = loaded.configPath ? dirname(loaded.configPath) : cwd;
|
|
145
|
+
const dir = resolveKnowledgeDir(loaded.config, projectRoot);
|
|
146
|
+
const bundle = await loadOkfBundle(dir);
|
|
147
|
+
let index: string | null = null;
|
|
148
|
+
try {
|
|
149
|
+
index = await readFile(join(dir, "index.md"), "utf8");
|
|
150
|
+
} catch {
|
|
151
|
+
index = null;
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
contents: [{
|
|
155
|
+
uri,
|
|
156
|
+
mimeType: "application/json",
|
|
157
|
+
text: JSON.stringify({ dir, index, concepts: bundle.concepts }, null, 2),
|
|
158
|
+
}],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
125
162
|
// Op resources
|
|
126
163
|
if (uri === "chant://ops") {
|
|
127
164
|
const { ops } = await discoverOps();
|
|
@@ -590,6 +590,7 @@ describe("McpServer", () => {
|
|
|
590
590
|
expect(uris).toContain("chant://ops");
|
|
591
591
|
expect(uris).toContain("chant://ops/{name}/runs");
|
|
592
592
|
expect(uris).toContain("chant://ops/{name}/runs/latest");
|
|
593
|
+
expect(uris).toContain("chant://knowledge");
|
|
593
594
|
|
|
594
595
|
// Each resource has required fields
|
|
595
596
|
for (const resource of result.resources) {
|
|
@@ -735,6 +736,62 @@ describe("McpServer", () => {
|
|
|
735
736
|
expect(data.error).toBeDefined();
|
|
736
737
|
});
|
|
737
738
|
});
|
|
739
|
+
|
|
740
|
+
describe("chant://knowledge", () => {
|
|
741
|
+
test("empty gracefully when no bundle exists", async () => {
|
|
742
|
+
const originalCwd = process.cwd();
|
|
743
|
+
process.chdir(testDir);
|
|
744
|
+
try {
|
|
745
|
+
const response = await server.handleRequest({
|
|
746
|
+
jsonrpc: "2.0",
|
|
747
|
+
id: 1,
|
|
748
|
+
method: "resources/read",
|
|
749
|
+
params: { uri: "chant://knowledge" },
|
|
750
|
+
});
|
|
751
|
+
expect(response.error).toBeUndefined();
|
|
752
|
+
const result = response.result as { contents: Array<{ text: string; mimeType: string }> };
|
|
753
|
+
expect(result.contents[0].mimeType).toBe("application/json");
|
|
754
|
+
const data = JSON.parse(result.contents[0].text);
|
|
755
|
+
expect(data.index).toBeNull();
|
|
756
|
+
expect(data.concepts).toEqual([]);
|
|
757
|
+
} finally {
|
|
758
|
+
process.chdir(originalCwd);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("serves the bundle index and concepts when present", async () => {
|
|
763
|
+
const originalCwd = process.cwd();
|
|
764
|
+
process.chdir(testDir);
|
|
765
|
+
try {
|
|
766
|
+
await mkdir(join(testDir, "knowledge", "decisions"), { recursive: true });
|
|
767
|
+
await writeFile(join(testDir, "knowledge", "index.md"), "# Knowledge index\n");
|
|
768
|
+
await writeFile(
|
|
769
|
+
join(testDir, "knowledge", "decisions", "public-assets.md"),
|
|
770
|
+
"---\ntype: decision\ntitle: Public assets\nbinds: bucket\n---\nBody text.\n",
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
const response = await server.handleRequest({
|
|
774
|
+
jsonrpc: "2.0",
|
|
775
|
+
id: 1,
|
|
776
|
+
method: "resources/read",
|
|
777
|
+
params: { uri: "chant://knowledge" },
|
|
778
|
+
});
|
|
779
|
+
expect(response.error).toBeUndefined();
|
|
780
|
+
const result = response.result as { contents: Array<{ text: string }> };
|
|
781
|
+
const data = JSON.parse(result.contents[0].text);
|
|
782
|
+
expect(data.index).toBe("# Knowledge index\n");
|
|
783
|
+
expect(data.concepts).toHaveLength(1);
|
|
784
|
+
expect(data.concepts[0]).toMatchObject({
|
|
785
|
+
path: "decisions/public-assets.md",
|
|
786
|
+
type: "decision",
|
|
787
|
+
title: "Public assets",
|
|
788
|
+
binds: ["bucket"],
|
|
789
|
+
});
|
|
790
|
+
} finally {
|
|
791
|
+
process.chdir(originalCwd);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
});
|
|
738
795
|
});
|
|
739
796
|
|
|
740
797
|
// -----------------------------------------------------------------------
|
|
@@ -1157,7 +1214,7 @@ describe("McpServer", () => {
|
|
|
1157
1214
|
|
|
1158
1215
|
const resourcesRes = await s.handleRequest({ jsonrpc: "2.0", id: 3, method: "resources/list" });
|
|
1159
1216
|
const resources = (resourcesRes.result as { resources: Array<{ uri: string }> }).resources;
|
|
1160
|
-
expect(resources).toHaveLength(
|
|
1217
|
+
expect(resources).toHaveLength(8);
|
|
1161
1218
|
});
|
|
1162
1219
|
|
|
1163
1220
|
test("server with empty plugins array works", async () => {
|
|
@@ -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
|
/**
|
|
@@ -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";
|