@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
|
@@ -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,
|
|
@@ -30,7 +32,9 @@ import { discoverComponents } from "../../components/discover";
|
|
|
30
32
|
import { cfnDeployStacks } from "./components";
|
|
31
33
|
import { affectedStacks } from "../../lifecycle/affected";
|
|
32
34
|
import { rollbackToRevision } from "../../lifecycle/rollback";
|
|
33
|
-
import { loadChantConfig, environmentNames } from "../../config";
|
|
35
|
+
import { loadChantConfig, environmentNames, matchesDeclaredEnvironment, resolveOwnershipStack } from "../../config";
|
|
36
|
+
import { unknownEnvError, isProdLikeEnvironment } from "../../env";
|
|
37
|
+
import { planTeardown, executeTeardown, type TeardownPlan, type TeardownReport } from "../../lifecycle/teardown";
|
|
34
38
|
import { collectBuildRootContributors } from "../plugins";
|
|
35
39
|
import { applyLiveEndpoint } from "../../live-endpoint";
|
|
36
40
|
import { isResourceDeclarable } from "../../declarable";
|
|
@@ -109,7 +113,7 @@ export async function runLifecycleSnapshot(ctx: CommandContext): Promise<number>
|
|
|
109
113
|
const declaredParams = await commandBuildParams(config.buildParams, args);
|
|
110
114
|
if (!declaredParams) return 1;
|
|
111
115
|
const declaredEnvNames = environmentNames(config.environments);
|
|
112
|
-
if (declaredEnvNames && !
|
|
116
|
+
if (declaredEnvNames && !matchesDeclaredEnvironment(config.environments, environment)) {
|
|
113
117
|
console.error(formatError({
|
|
114
118
|
message: `Unknown environment "${environment}"`,
|
|
115
119
|
hint: `Defined environments: ${declaredEnvNames.join(", ")}`,
|
|
@@ -356,6 +360,10 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
356
360
|
// committed set. Absent is the normal state (nothing accepted yet).
|
|
357
361
|
const baseline = args.live ? await readObservationBaseline(environment) : null;
|
|
358
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>();
|
|
359
367
|
|
|
360
368
|
// #1166 — an environment can declare its own endpoint (a local emulator like
|
|
361
369
|
// Floci), so `--live` is self-sufficient even when the ambient shell never
|
|
@@ -428,6 +436,9 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
428
436
|
for (const [lexicon, deviations] of Object.entries(r.toAccept)) {
|
|
429
437
|
(accepted[lexicon] ??= []).push(...deviations);
|
|
430
438
|
}
|
|
439
|
+
if (args.updateBaseline) {
|
|
440
|
+
for (const name of collectEffectReceipts(buildResult.entities).keys()) receiptEntities.add(name);
|
|
441
|
+
}
|
|
431
442
|
if (json) {
|
|
432
443
|
if (target.stack) perStackJson[target.stack] = r.byLexicon;
|
|
433
444
|
else combinedLexiconsJson = r.byLexicon;
|
|
@@ -441,7 +452,7 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
441
452
|
// accepted, so it stops re-alerting. Runs before the summary lines so the
|
|
442
453
|
// "no drift" verdict below still describes the run that produced it.
|
|
443
454
|
if (args.live && args.updateBaseline) {
|
|
444
|
-
await recordAcceptedBaseline(environment, baseline, accepted, json);
|
|
455
|
+
await recordAcceptedBaseline(environment, baseline, accepted, json, receiptEntities);
|
|
445
456
|
}
|
|
446
457
|
|
|
447
458
|
if (args.live) {
|
|
@@ -493,6 +504,7 @@ async function recordAcceptedBaseline(
|
|
|
493
504
|
existing: ObservationBaseline | null,
|
|
494
505
|
accepted: Record<string, DeviationToAccept[]>,
|
|
495
506
|
json: boolean,
|
|
507
|
+
receipts?: ReadonlySet<string>,
|
|
496
508
|
): Promise<void> {
|
|
497
509
|
const total = Object.values(accepted).reduce((n, d) => n + d.length, 0);
|
|
498
510
|
if (total === 0) {
|
|
@@ -503,11 +515,14 @@ async function recordAcceptedBaseline(
|
|
|
503
515
|
}
|
|
504
516
|
return;
|
|
505
517
|
}
|
|
506
|
-
let next = existing ?? emptyBaseline(environment);
|
|
507
|
-
for (const [lexicon, deviations] of Object.entries(accepted)) {
|
|
508
|
-
next = acceptDeviations(next, lexicon, deviations);
|
|
509
|
-
}
|
|
510
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
|
+
}
|
|
511
526
|
await writeObservationBaseline(next);
|
|
512
527
|
const pushed = await pushLifecycle();
|
|
513
528
|
if (!json) {
|
|
@@ -518,7 +533,7 @@ async function recordAcceptedBaseline(
|
|
|
518
533
|
}
|
|
519
534
|
} catch (err) {
|
|
520
535
|
console.error(formatError({
|
|
521
|
-
message: `--update-baseline:
|
|
536
|
+
message: `--update-baseline: baseline not updated — ${err instanceof Error ? err.message : String(err)}`,
|
|
522
537
|
}));
|
|
523
538
|
}
|
|
524
539
|
}
|
|
@@ -1124,6 +1139,15 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1124
1139
|
const merged: ChangeSet = { env: environment, entries: [] };
|
|
1125
1140
|
let checked = 0;
|
|
1126
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
|
+
|
|
1127
1151
|
// #1166 — same self-sufficiency as `chant graph --live`: an environment can
|
|
1128
1152
|
// declare its own endpoint, applied here unless the ambient shell already
|
|
1129
1153
|
// set it. `chant lifecycle plan` is always a live read (no `--live` flag of
|
|
@@ -1154,11 +1178,14 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1154
1178
|
const declared = new Set<string>();
|
|
1155
1179
|
const entities = new Map<string, { entityType: string; props: Record<string, unknown> }>();
|
|
1156
1180
|
for (const [name, entity] of buildResult.entities) {
|
|
1157
|
-
|
|
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))) {
|
|
1158
1185
|
declared.add(name);
|
|
1159
1186
|
entities.set(name, {
|
|
1160
1187
|
entityType: entity.entityType,
|
|
1161
|
-
props: (entity.props != null ? entity.props : {}) as Record<string, unknown>,
|
|
1188
|
+
props: (isResourceDeclarable(entity) && entity.props != null ? entity.props : {}) as Record<string, unknown>,
|
|
1162
1189
|
});
|
|
1163
1190
|
}
|
|
1164
1191
|
}
|
|
@@ -1183,6 +1210,37 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1183
1210
|
...(args.namespace ? { namespace: args.namespace } : {}),
|
|
1184
1211
|
});
|
|
1185
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
|
+
|
|
1186
1244
|
const content = await readSnapshot(environment, lexiconName);
|
|
1187
1245
|
const observedThen = content ? (JSON.parse(content) as LifecycleSnapshot).resources : undefined;
|
|
1188
1246
|
|
|
@@ -1212,6 +1270,16 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1212
1270
|
return 1;
|
|
1213
1271
|
}
|
|
1214
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
|
+
|
|
1215
1283
|
merged.entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1216
1284
|
|
|
1217
1285
|
// Say it on stderr too, so `--json` and `--report gitlab-mr` consumers (whose
|
|
@@ -1240,6 +1308,227 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1240
1308
|
return 0;
|
|
1241
1309
|
}
|
|
1242
1310
|
|
|
1311
|
+
/**
|
|
1312
|
+
* chant lifecycle teardown <environment> (#1222).
|
|
1313
|
+
*
|
|
1314
|
+
* Enumerates what deleting the environment would remove: every live resource
|
|
1315
|
+
* carrying this project's ownership marker (managed-by + `ownership.stack`)
|
|
1316
|
+
* with the requested env identity. Stateless — live markers only, no build, no
|
|
1317
|
+
* snapshot. Without `--yes` nothing is deleted; with it the planned set is
|
|
1318
|
+
* executed per lexicon with one bounded retry pass over failures, and every
|
|
1319
|
+
* candidate's outcome is reported (deleted / failed / not-prunable / retained / skipped —
|
|
1320
|
+
* never silence). A production-like environment name additionally requires an
|
|
1321
|
+
* interactive confirmation, or `--confirm-prod` non-interactively.
|
|
1322
|
+
*/
|
|
1323
|
+
export async function runLifecycleTeardown(ctx: CommandContext): Promise<number> {
|
|
1324
|
+
const { args, plugins } = ctx;
|
|
1325
|
+
const environment = args.extraPositional;
|
|
1326
|
+
|
|
1327
|
+
if (!environment) {
|
|
1328
|
+
console.error(formatError({ message: "Environment is required: chant lifecycle teardown <environment>" }));
|
|
1329
|
+
return 1;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const { config } = await loadChantConfig(resolve("."));
|
|
1333
|
+
|
|
1334
|
+
// Refuse an env the project does not declare — a typo here is the difference
|
|
1335
|
+
// between tearing down `dev` and tearing down `prod`. Literal `environments`
|
|
1336
|
+
// entries only (#1221's pattern entries would extend this same check).
|
|
1337
|
+
const envErr = unknownEnvError(environment, config.environments);
|
|
1338
|
+
if (envErr) {
|
|
1339
|
+
console.error(formatError({ message: envErr }));
|
|
1340
|
+
return 1;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Teardown selects on the ownership marker; a project that stamps none has
|
|
1344
|
+
// nothing to key on, and "delete what looks like mine" is not a fallback.
|
|
1345
|
+
const stack = resolveOwnershipStack(config);
|
|
1346
|
+
if (stack === undefined) {
|
|
1347
|
+
console.error(formatError({
|
|
1348
|
+
message: "This project declares no ownership.stack — teardown is marker-scoped and has nothing to select on",
|
|
1349
|
+
hint: 'Set `ownership: { stack: "<name>" }` in chant.config.ts and deploy, so resources carry the marker teardown keys on.',
|
|
1350
|
+
}));
|
|
1351
|
+
return 1;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// The prod guard (#1222): a production-like name never falls to `--yes`
|
|
1355
|
+
// alone. Interactive runs re-type the environment name; non-interactive
|
|
1356
|
+
// runs say `--confirm-prod` explicitly. Checked before any live read so a
|
|
1357
|
+
// refused teardown touches nothing at all.
|
|
1358
|
+
if (args.yes && isProdLikeEnvironment(environment) && !args.confirmProd) {
|
|
1359
|
+
if (!process.stdin.isTTY) {
|
|
1360
|
+
console.error(formatError({
|
|
1361
|
+
message: `"${environment}" looks like a production environment — --yes alone is not enough`,
|
|
1362
|
+
hint: "Re-run with --yes --confirm-prod to tear it down non-interactively.",
|
|
1363
|
+
}));
|
|
1364
|
+
return 1;
|
|
1365
|
+
}
|
|
1366
|
+
const confirmed = await promptProdTeardown(environment);
|
|
1367
|
+
if (!confirmed) {
|
|
1368
|
+
console.error(formatError({ message: "Confirmation did not match — nothing was deleted." }));
|
|
1369
|
+
return 1;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// #1166 — teardown is always a live read (and with --yes, a live write), so
|
|
1374
|
+
// an environment's declared endpoint applies here too, unless the ambient
|
|
1375
|
+
// shell already set it.
|
|
1376
|
+
const readingPlugins = plugins.filter((p) => p.teardownOwned || p.describeResources || p.executeTeardown);
|
|
1377
|
+
const endpointResult = applyLiveEndpoint(config.environments, environment, readingPlugins);
|
|
1378
|
+
if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
|
|
1379
|
+
|
|
1380
|
+
let plan: TeardownPlan;
|
|
1381
|
+
let report: TeardownReport | undefined;
|
|
1382
|
+
try {
|
|
1383
|
+
// A multi-stack project's declared stacks, for stack-shaped teardowns
|
|
1384
|
+
// (aws enumerates and deletes whole CloudFormation stacks). A single-stack
|
|
1385
|
+
// project passes nothing and the env-named default convention applies.
|
|
1386
|
+
const deployedStacks = (config.stacks ?? []).map((s) => ({
|
|
1387
|
+
name: s.name,
|
|
1388
|
+
...(s.region ? { region: s.region } : {}),
|
|
1389
|
+
}));
|
|
1390
|
+
plan = await planTeardown({
|
|
1391
|
+
environment,
|
|
1392
|
+
stack,
|
|
1393
|
+
plugins,
|
|
1394
|
+
...(deployedStacks.length > 0 ? { deployedStacks } : {}),
|
|
1395
|
+
});
|
|
1396
|
+
if (args.yes) {
|
|
1397
|
+
report = await executeTeardown({
|
|
1398
|
+
environment,
|
|
1399
|
+
stack,
|
|
1400
|
+
plugins,
|
|
1401
|
+
plan,
|
|
1402
|
+
...(deployedStacks.length > 0 ? { deployedStacks } : {}),
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
} finally {
|
|
1406
|
+
endpointResult.restore();
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (args.json) {
|
|
1410
|
+
console.log(JSON.stringify(report ?? plan, null, 2));
|
|
1411
|
+
return report && report.outcomes.some((o) => o.outcome === "failed") ? 1 : 0;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
console.log(formatBold(
|
|
1415
|
+
args.yes
|
|
1416
|
+
? `Teardown — environment: ${environment}, stack: ${stack}`
|
|
1417
|
+
: `Teardown plan — environment: ${environment}, stack: ${stack} (plan only — nothing is deleted)`,
|
|
1418
|
+
));
|
|
1419
|
+
|
|
1420
|
+
if (plan.entries.length === 0) {
|
|
1421
|
+
console.error(formatWarning({
|
|
1422
|
+
message: `No live resources carry the marker stack "${stack}" + env "${environment}" — nothing would be deleted.` +
|
|
1423
|
+
(plan.holes.length > 0
|
|
1424
|
+
? " But this plan has holes (below) — parts of the estate could not be read, so \"nothing\" is a claim about what was readable, not about the environment."
|
|
1425
|
+
: " If this environment is deployed, check that its resources were stamped (ownership marking on, and the env identity resolved at build time)."),
|
|
1426
|
+
}));
|
|
1427
|
+
} else {
|
|
1428
|
+
console.log(`\n${plan.entries.length} resource(s) would be deleted:`);
|
|
1429
|
+
console.log("RESOURCE".padEnd(28) + "TYPE".padEnd(32) + "MARKER".padEnd(24) + "LEXICON");
|
|
1430
|
+
console.log("-".repeat(96));
|
|
1431
|
+
for (const entry of plan.entries) {
|
|
1432
|
+
console.log(
|
|
1433
|
+
entry.name.padEnd(28) +
|
|
1434
|
+
entry.type.padEnd(32) +
|
|
1435
|
+
`${entry.marker.stack}/${entry.marker.env}`.padEnd(24) +
|
|
1436
|
+
entry.lexicon,
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Holes are loud (#1089): an unreadable kind is unknown, not absent, and the
|
|
1442
|
+
// execution half must not treat this plan as the whole delete set.
|
|
1443
|
+
if (plan.holes.length > 0) {
|
|
1444
|
+
console.error(formatWarning({
|
|
1445
|
+
message: `${plan.holes.length} hole(s) — resources chant may own but could not read. This plan is incomplete, not clean.`,
|
|
1446
|
+
}));
|
|
1447
|
+
console.log(formatBold("\nHOLES (stamped kinds the read could not cover):"));
|
|
1448
|
+
for (const hole of plan.holes) {
|
|
1449
|
+
console.log(` ? ${hole.lexicon}: ${hole.name}${hole.type ? ` (${hole.type})` : ""} — ${hole.reason}${hole.detail ? `: ${hole.detail}` : ""}`);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
if (plan.skipped.length > 0) {
|
|
1454
|
+
console.error(formatWarning({
|
|
1455
|
+
message: `Skipped (no teardown enumeration and no describeResources): ${plan.skipped.join(", ")} — those lexicons' resources are not in this plan.`,
|
|
1456
|
+
}));
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (!report) {
|
|
1460
|
+
console.error(formatWarning({ message: "Plan only — re-run with --yes to execute." }));
|
|
1461
|
+
return 0;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
if (report.outcomes.length > 0) {
|
|
1465
|
+
console.log(formatBold("\nOutcomes:"));
|
|
1466
|
+
console.log("RESOURCE".padEnd(28) + "OUTCOME".padEnd(14) + "LEXICON".padEnd(12) + "DETAIL");
|
|
1467
|
+
console.log("-".repeat(96));
|
|
1468
|
+
for (const o of report.outcomes) {
|
|
1469
|
+
console.log(
|
|
1470
|
+
o.name.padEnd(28) +
|
|
1471
|
+
o.outcome.padEnd(14) +
|
|
1472
|
+
o.lexicon.padEnd(12) +
|
|
1473
|
+
(o.detail ?? "") +
|
|
1474
|
+
(o.retried ? " (after retry)" : ""),
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
const counts = { deleted: 0, failed: 0, "not-prunable": 0, retained: 0, skipped: 0 };
|
|
1480
|
+
for (const o of report.outcomes) counts[o.outcome]++;
|
|
1481
|
+
console.log(
|
|
1482
|
+
`\n${counts.deleted} deleted, ${counts.failed} failed, ${counts["not-prunable"]} not prunable, ` +
|
|
1483
|
+
`${counts.retained} retained, ${counts.skipped} skipped`,
|
|
1484
|
+
);
|
|
1485
|
+
|
|
1486
|
+
if (report.unimplemented.length > 0) {
|
|
1487
|
+
console.error(formatWarning({
|
|
1488
|
+
message: `Not executed (no teardown execution in these lexicons yet): ${report.unimplemented.join(", ")} — their candidates are reported as skipped, not deleted.`,
|
|
1489
|
+
}));
|
|
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
|
+
}
|
|
1497
|
+
if (plan.holes.length > 0) {
|
|
1498
|
+
console.error(formatWarning({
|
|
1499
|
+
message: "This teardown ran over an incomplete plan (holes above) — the environment cannot be called clean.",
|
|
1500
|
+
}));
|
|
1501
|
+
}
|
|
1502
|
+
if (counts.failed > 0) {
|
|
1503
|
+
console.error(formatError({
|
|
1504
|
+
message: `${counts.failed} candidate(s) failed to delete after the retry pass — see the outcomes above.`,
|
|
1505
|
+
}));
|
|
1506
|
+
return 1;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return 0;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/**
|
|
1513
|
+
* The interactive half of the prod guard: the operator re-types the
|
|
1514
|
+
* environment name. Anything else — including EOF — refuses.
|
|
1515
|
+
*/
|
|
1516
|
+
async function promptProdTeardown(environment: string): Promise<boolean> {
|
|
1517
|
+
const { createInterface } = await import("node:readline");
|
|
1518
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1519
|
+
return new Promise((resolvePrompt) => {
|
|
1520
|
+
rl.question(
|
|
1521
|
+
`"${environment}" looks like a production environment. Type the environment name to confirm teardown: `,
|
|
1522
|
+
(answer) => {
|
|
1523
|
+
resolvePrompt(answer.trim() === environment);
|
|
1524
|
+
rl.close();
|
|
1525
|
+
},
|
|
1526
|
+
);
|
|
1527
|
+
// EOF (Ctrl-D) closes the interface without answering — that is a refusal.
|
|
1528
|
+
rl.on("close", () => resolvePrompt(false));
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1243
1532
|
/**
|
|
1244
1533
|
* chant lifecycle log [environment]
|
|
1245
1534
|
*/
|
|
@@ -1268,7 +1557,7 @@ export async function runLifecycleLog(ctx: CommandContext): Promise<number> {
|
|
|
1268
1557
|
export async function runLifecycleUnknown(ctx: CommandContext): Promise<number> {
|
|
1269
1558
|
console.error(formatError({
|
|
1270
1559
|
message: `Unknown state subcommand: ${ctx.args.extraPositional ?? ctx.args.path}`,
|
|
1271
|
-
hint: "Available: chant lifecycle snapshot, chant lifecycle show, chant lifecycle diff, chant lifecycle plan, chant lifecycle log",
|
|
1560
|
+
hint: "Available: chant lifecycle snapshot, chant lifecycle show, chant lifecycle diff, chant lifecycle plan, chant lifecycle teardown, chant lifecycle log",
|
|
1272
1561
|
}));
|
|
1273
1562
|
return 1;
|
|
1274
1563
|
}
|
|
@@ -9,7 +9,7 @@ import { discover } from "../../discovery/index";
|
|
|
9
9
|
import { observeResources } from "../../lifecycle/observe";
|
|
10
10
|
import { replaySnapshots, hasSnapshot } from "../../lifecycle/replay";
|
|
11
11
|
import type { LiveObservation } from "../../graph-ir";
|
|
12
|
-
import { loadChantConfig } from "../../config";
|
|
12
|
+
import { loadChantConfig, matchesDeclaredEnvironment } from "../../config";
|
|
13
13
|
import { loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
14
14
|
import { formatError, formatWarning } from "../format";
|
|
15
15
|
import type { CommandContext } from "../registry";
|
|
@@ -89,7 +89,10 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
|
|
|
89
89
|
}));
|
|
90
90
|
return 1;
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
// Membership via matchesDeclaredEnvironment (#1221): handles `{ name,
|
|
93
|
+
// endpoint }` entries (#1166) — a plain `.includes` never matched those —
|
|
94
|
+
// and glob-pattern entries like `"pr-*"`.
|
|
95
|
+
if (config.environments && config.environments.length > 0 && !matchesDeclaredEnvironment(config.environments, environment)) {
|
|
93
96
|
console.error(formatError({ message: `Unknown environment "${environment}"` }));
|
|
94
97
|
return 1;
|
|
95
98
|
}
|
package/src/cli/main.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { runCarveAdvise, runCarveUnknown } from "./handlers/carve";
|
|
|
23
23
|
import { runCarveEmit } from "./handlers/carve-emit";
|
|
24
24
|
import { runCarveBridge } from "./handlers/carve-bridge";
|
|
25
25
|
import { runCarveApply } from "./handlers/carve-apply";
|
|
26
|
-
import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecycleRollback, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleUnknown } from "./handlers/lifecycle";
|
|
26
|
+
import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecycleRollback, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleTeardown, runLifecycleUnknown } from "./handlers/lifecycle";
|
|
27
27
|
import { runComponentsStatus, runComponentsReleaseRecord, runComponentsUnknown } from "./handlers/components";
|
|
28
28
|
import { runGraph } from "./handlers/graph";
|
|
29
29
|
import { runExplain } from "./handlers/explain";
|
|
@@ -78,6 +78,8 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
78
78
|
"--fold",
|
|
79
79
|
"--no-fold",
|
|
80
80
|
"--sandbox",
|
|
81
|
+
"--yes",
|
|
82
|
+
"--confirm-prod",
|
|
81
83
|
]);
|
|
82
84
|
|
|
83
85
|
/**
|
|
@@ -228,6 +230,10 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
228
230
|
result.emit = args[++i];
|
|
229
231
|
} else if (arg === "--dry-run") {
|
|
230
232
|
result.dryRun = true;
|
|
233
|
+
} else if (arg === "--yes") {
|
|
234
|
+
result.yes = true;
|
|
235
|
+
} else if (arg === "--confirm-prod") {
|
|
236
|
+
result.confirmProd = true;
|
|
231
237
|
} else if (arg === "--strict") {
|
|
232
238
|
result.strict = true;
|
|
233
239
|
} else if (arg === "--validate") {
|
|
@@ -484,6 +490,10 @@ Lifecycle (alias: lc):
|
|
|
484
490
|
lifecycle plan <env> Typed change set (create/update/delete/adopt) vs live
|
|
485
491
|
lifecycle affected Stacks a change affects (--base <ref> [--include-dependents])
|
|
486
492
|
--json: emit the ChangeSet as JSON
|
|
493
|
+
lifecycle teardown <env> Plan what deleting the environment would remove —
|
|
494
|
+
marker-scoped (this project's stack + env); --yes
|
|
495
|
+
executes the plan (production-like names also need
|
|
496
|
+
--confirm-prod, or an interactive confirmation)
|
|
487
497
|
lifecycle log [env] History of lifecycle snapshots
|
|
488
498
|
|
|
489
499
|
Component release ledger + status:
|
|
@@ -780,6 +790,7 @@ const registry: CommandDef[] = [
|
|
|
780
790
|
{ name: "lifecycle rollback", handler: runLifecycleRollback },
|
|
781
791
|
{ name: "lifecycle plan", requiresPlugins: true, handler: runLifecyclePlan },
|
|
782
792
|
{ name: "lifecycle affected", requiresPlugins: true, handler: runLifecycleAffected },
|
|
793
|
+
{ name: "lifecycle teardown", requiresPlugins: true, handler: runLifecycleTeardown },
|
|
783
794
|
{ name: "lifecycle log", handler: runLifecycleLog },
|
|
784
795
|
|
|
785
796
|
// Component release ledger + status surface (#568, epic #551)
|
|
@@ -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 () => {
|