@intentius/chant 0.44.14 → 0.46.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/discover.d.ts +26 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +10 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cli/build-params-cli.d.ts +16 -7
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +20 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +9 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
- package/dist/cli/commands/check-lexicon-examples.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/init-lexicon/templates/codegen.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-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/commands/onboard.d.ts +28 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/graph.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/run.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +27 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-pages.d.ts +39 -0
- package/dist/codegen/docs-pages.d.ts.map +1 -0
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-sidebar.d.ts +13 -2
- package/dist/codegen/docs-sidebar.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +30 -10
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +14 -2
- package/dist/codegen/docs.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/surface-snapshot.d.ts +27 -3
- package/dist/codegen/surface-snapshot.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +21 -3
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +13 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/config.d.ts +85 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +100 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +12 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ops.d.ts +17 -0
- package/dist/graph-ops.d.ts.map +1 -0
- package/dist/lexicon.d.ts +182 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +23 -2
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +8 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +130 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lifecycle/types.d.ts +7 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/policy.d.ts.map +1 -1
- 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/index.d.ts +2 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/managed-fields.d.ts +33 -21
- package/dist/managed-fields.d.ts.map +1 -1
- package/dist/observation.d.ts +23 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/op/builders.d.ts +36 -7
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.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 +0 -8
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__snapshots__/okf.test.ts.snap +20 -0
- package/src/audit/discover.test.ts +55 -1
- package/src/audit/discover.ts +70 -2
- package/src/audit/edge-init-safety.test.ts +149 -18
- package/src/audit/report-model.ts +12 -1
- package/src/audit/rules-doc.ts +1 -0
- package/src/build-params.test.ts +52 -0
- package/src/cli/build-params-cli.test.ts +25 -8
- package/src/cli/build-params-cli.ts +37 -23
- package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
- package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
- package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
- package/src/cli/commands/audit.test.ts +83 -1
- package/src/cli/commands/audit.ts +121 -28
- package/src/cli/commands/build.test.ts +275 -5
- package/src/cli/commands/build.ts +86 -14
- package/src/cli/commands/check-lexicon-docs.ts +30 -0
- package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
- package/src/cli/commands/check-lexicon-examples.ts +66 -17
- package/src/cli/commands/check-lexicon.test.ts +46 -2
- package/src/cli/commands/check-lexicon.ts +64 -4
- package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
- package/src/cli/commands/init-lexicon.test.ts +42 -1
- package/src/cli/commands/init-lexicon.ts +3 -1
- package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.ts +10 -1
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +19 -6
- package/src/cli/commands/onboard.test.ts +319 -225
- package/src/cli/commands/onboard.ts +144 -58
- package/src/cli/handlers/build.test.ts +3 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.ts +2 -2
- package/src/cli/handlers/graph.test.ts +44 -0
- package/src/cli/handlers/graph.ts +24 -4
- package/src/cli/handlers/lifecycle.test.ts +348 -1
- package/src/cli/handlers/lifecycle.ts +235 -3
- package/src/cli/handlers/run.test.ts +5 -5
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/handlers/search-live.test.ts +217 -0
- package/src/cli/handlers/search.test.ts +41 -1
- package/src/cli/handlers/search.ts +60 -8
- package/src/cli/main.ts +17 -4
- package/src/cli/registry.ts +7 -0
- package/src/cli/security-boundary.test.ts +2 -1
- package/src/codegen/docs-pages.test.ts +194 -0
- package/src/codegen/docs-pages.ts +138 -0
- package/src/codegen/docs-sections.ts +0 -8
- package/src/codegen/docs-sidebar.ts +50 -37
- package/src/codegen/docs-types.ts +32 -4
- package/src/codegen/docs.ts +78 -40
- package/src/codegen/lexicon-regen.ts +22 -2
- package/src/codegen/rolling-upgrade.test.ts +1 -1
- package/src/codegen/rolling-upgrade.ts +1 -1
- package/src/codegen/surface-snapshot.test.ts +139 -0
- package/src/codegen/surface-snapshot.ts +297 -122
- package/src/codegen/validate.test.ts +49 -3
- package/src/codegen/validate.ts +36 -7
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability-plugin.ts +23 -2
- package/src/components/driver.test.ts +45 -0
- package/src/components/driver.ts +64 -27
- package/src/components/index.ts +2 -0
- package/src/components/starter-plugin.ts +5 -2
- package/src/config.test.ts +40 -0
- package/src/config.ts +159 -8
- package/src/discovery/fold-import.test.ts +427 -2
- package/src/discovery/fold-import.ts +163 -7
- package/src/discovery/sandbox/driver.test.ts +11 -0
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/fold/fold.test.ts +41 -0
- package/src/fold/fold.ts +372 -2
- package/src/graph-ir-live.test.ts +42 -0
- package/src/graph-ir.ts +42 -2
- package/src/graph-ops.test.ts +69 -0
- package/src/graph-ops.ts +42 -0
- package/src/lexicon.ts +182 -2
- package/src/lifecycle/change-set.test.ts +41 -0
- package/src/lifecycle/change-set.ts +30 -2
- package/src/lifecycle/deep-observe.ts +4 -0
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/observe.ts +29 -2
- package/src/lifecycle/replay.ts +3 -0
- package/src/lifecycle/snapshot.ts +3 -0
- package/src/lifecycle/teardown.test.ts +537 -0
- package/src/lifecycle/teardown.ts +357 -0
- package/src/lifecycle/types.ts +7 -0
- package/src/lint/engine.ts +7 -1
- package/src/lint/policy.ts +14 -3
- 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/index.ts +4 -1
- package/src/managed-fields.test.ts +15 -42
- package/src/managed-fields.ts +43 -26
- package/src/observation.test.ts +24 -8
- package/src/observation.ts +48 -4
- package/src/op/builders.ts +40 -7
- package/src/op/index.ts +1 -1
- package/src/ownership.test.ts +63 -1
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
- package/src/yaml.test.ts +15 -0
- package/src/yaml.ts +19 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { build } from "../../build";
|
|
3
|
-
import { buildGraphIr, buildLiveGraphIr, sourceOverlayGraphs, type GraphIR, type IRNode, type IREdge } from "../../graph-ir";
|
|
3
|
+
import { buildGraphIr, buildLiveGraphIr, collectUnobserved, sourceOverlayGraphs, type GraphIR, type IRNode, type IREdge } from "../../graph-ir";
|
|
4
4
|
import { buildDeclaredPerStack } from "../../graph-declared";
|
|
5
5
|
import { enrichEffectiveTopology } from "../../graph-effective";
|
|
6
6
|
import { reconstructEdges, mergeCatalogs, type ReferenceCatalog } from "../../graph-refs";
|
|
@@ -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";
|
|
@@ -61,6 +61,13 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
|
|
|
61
61
|
|
|
62
62
|
let ir: GraphIR;
|
|
63
63
|
let source: AnswerSource = { kind: "declared" };
|
|
64
|
+
// Lexicons whose live read threw (#1263). "Nothing observed" and "could not
|
|
65
|
+
// observe" are different claims; this is what carries the second one.
|
|
66
|
+
const liveFailures: string[] = [];
|
|
67
|
+
// Run-level notes from the read (#1265) — "ownership could not be filtered
|
|
68
|
+
// on this path" — printed with the provenance footer, after the rows, so the
|
|
69
|
+
// answer is not preceded by what qualifies it.
|
|
70
|
+
let liveNotes: string[] = [];
|
|
64
71
|
// Kinds that can exist in the account without being declared (#1278). Known
|
|
65
72
|
// without a scan, so it costs nothing to mention.
|
|
66
73
|
let ambientKinds: string[] = [];
|
|
@@ -82,7 +89,10 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
|
|
|
82
89
|
}));
|
|
83
90
|
return 1;
|
|
84
91
|
}
|
|
85
|
-
|
|
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)) {
|
|
86
96
|
console.error(formatError({ message: `Unknown environment "${environment}"` }));
|
|
87
97
|
return 1;
|
|
88
98
|
}
|
|
@@ -122,8 +132,15 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
|
|
|
122
132
|
stacks,
|
|
123
133
|
ambient: args.ambient === true,
|
|
124
134
|
});
|
|
125
|
-
|
|
135
|
+
// A thrown read is not a warning. Printed as one, it sat between the
|
|
136
|
+
// ownership-filter notices that also print on a working run and carried
|
|
137
|
+
// no signal (#1263). Name the lexicon and the cause, as an error.
|
|
138
|
+
for (const e of observed.errors) {
|
|
139
|
+
liveFailures.push(e);
|
|
140
|
+
console.error(formatError({ message: `live read failed — ${e}` }));
|
|
141
|
+
}
|
|
126
142
|
observations = observed.observations;
|
|
143
|
+
liveNotes = observed.notes ?? [];
|
|
127
144
|
source = { kind: "live" };
|
|
128
145
|
}
|
|
129
146
|
let live = buildLiveGraphIr(observations);
|
|
@@ -189,7 +206,10 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
|
|
|
189
206
|
stacks.length > 0
|
|
190
207
|
? await buildDeclaredPerStack(stacks, projectPath)
|
|
191
208
|
: buildGraphIr((await discover(resolve(args.src ?? config.sourceDir ?? "."))).entities, projectPath);
|
|
192
|
-
|
|
209
|
+
// Carry the NOT-OBSERVED half of the tri-state (#1089) onto the rows, so a
|
|
210
|
+
// declared entity nobody could read is painted `_unobserved` and a row can
|
|
211
|
+
// say so instead of printing blank where a physical id would go (#1263).
|
|
212
|
+
ir = sourceOverlayGraphs(declared, live, { unobserved: collectUnobserved(observations) });
|
|
193
213
|
// Containment goes on AFTER the overlay, not through it.
|
|
194
214
|
//
|
|
195
215
|
// `sourceOverlayGraphs` admits a live edge only when one end is foreign,
|
|
@@ -242,6 +262,7 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
|
|
|
242
262
|
console.log("(no matches)");
|
|
243
263
|
availableAttrs(terms, ir);
|
|
244
264
|
if (args.explain) explain(terms, matches, ir, nodeById, query);
|
|
265
|
+
for (const n of liveNotes) console.error(formatWarning({ message: n }));
|
|
245
266
|
return 0;
|
|
246
267
|
}
|
|
247
268
|
for (const n of matches) {
|
|
@@ -254,12 +275,28 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
|
|
|
254
275
|
source.kind === "live" && !matches.some((n) => n.physicalId) && args.env
|
|
255
276
|
? (await hasSnapshot(String(args.env))) ? "yes" : undefined
|
|
256
277
|
: undefined;
|
|
257
|
-
provenance(matches, source, recorded);
|
|
278
|
+
provenance(matches, source, recorded, liveFailures);
|
|
279
|
+
// Qualifies the provenance line, so it sits with it: one line per distinct
|
|
280
|
+
// note for the whole run, not one per stack, and after the rows (#1265).
|
|
281
|
+
for (const n of liveNotes) console.error(formatWarning({ message: n }));
|
|
258
282
|
ambientHint(matches, ambientKinds, args.ambient === true, replayAmbient);
|
|
259
283
|
showMiss(matches, show);
|
|
260
284
|
regionSpread(terms, matches, show);
|
|
261
285
|
derivedSurface(terms, matches, ir, backed);
|
|
262
286
|
if (args.explain) explain(terms, matches, ir, nodeById, query);
|
|
287
|
+
// The caller asked for a live answer and at least one lexicon could not be
|
|
288
|
+
// read. The rows above are still printed — they are what the source declares,
|
|
289
|
+
// labelled as such — but the command did not do what it was asked, and a
|
|
290
|
+
// script or agent needs to see that without parsing the footer (#1263).
|
|
291
|
+
if (liveFailures.length > 0) {
|
|
292
|
+
console.error(formatError({
|
|
293
|
+
message: `live read failed for ${liveFailures.length} lexicon${liveFailures.length === 1 ? "" : "s"} — answer is declared-only`,
|
|
294
|
+
hint: recorded
|
|
295
|
+
? "answer from the recorded snapshot with --at latest, or drop --live for a declared-only query"
|
|
296
|
+
: "drop --live for a declared-only query, or record a snapshot with chant lifecycle snapshot",
|
|
297
|
+
}));
|
|
298
|
+
return 1;
|
|
299
|
+
}
|
|
263
300
|
return 0;
|
|
264
301
|
}
|
|
265
302
|
|
|
@@ -379,13 +416,24 @@ function ambientHint(
|
|
|
379
416
|
* already done. That is a fact about the query, printed for every query, and it
|
|
380
417
|
* encodes no expected answer.
|
|
381
418
|
*/
|
|
382
|
-
function provenance(matches: IRNode[], source: AnswerSource, recorded?: string): void {
|
|
419
|
+
function provenance(matches: IRNode[], source: AnswerSource, recorded?: string, liveFailures: string[] = []): void {
|
|
383
420
|
if (source.kind === "declared") {
|
|
384
421
|
console.log("— declared only · no observation · physical ids unavailable");
|
|
385
422
|
return;
|
|
386
423
|
}
|
|
387
424
|
const bound = matches.filter((n) => n.physicalId).length;
|
|
388
425
|
const what = source.kind === "live" ? "live read" : "snapshot";
|
|
426
|
+
if (liveFailures.length > 0) {
|
|
427
|
+
// Could not observe, as distinct from observed nothing. The rows carry the
|
|
428
|
+
// per-entity verdict; this names the lexicons that failed in one line.
|
|
429
|
+
const lexicons = liveFailures.map((e) => e.split(":")[0]).join(", ");
|
|
430
|
+
const unobserved = matches.filter((n) => (n.attrs as Record<string, unknown> | undefined)?._unobserved).length;
|
|
431
|
+
const rest = recorded
|
|
432
|
+
? "a snapshot of this environment is recorded — answer from it with --at latest"
|
|
433
|
+
: "answered from the declared graph · physical ids unavailable";
|
|
434
|
+
console.log(`— live read failed (${lexicons}) · ${unobserved}/${matches.length} rows unobserved · ${rest}`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
389
437
|
if (bound === 0) {
|
|
390
438
|
// The estate was asked for and nothing came back bound. Naming it is the
|
|
391
439
|
// difference between "these do not exist" and "nobody could see them".
|
|
@@ -742,6 +790,10 @@ function formatRow(n: IRNode, show: string[]): string {
|
|
|
742
790
|
// skip source-mode AttrRef placeholders (objects).
|
|
743
791
|
const physical = (n as { physicalId?: unknown }).physicalId ?? attrs["physicalId"] ?? attrs["InstanceId"] ?? attrs["Id"];
|
|
744
792
|
if (physical != null && typeof physical !== "object") parts.push(String(physical));
|
|
793
|
+
// A row nobody could read says so where its physical id would go (#1263). A
|
|
794
|
+
// blank there reads as "declared, not provisioned", which the read never
|
|
795
|
+
// established; the reason is the tri-state's (#1089).
|
|
796
|
+
else if (typeof attrs["_unobserved"] === "string") parts.push(`(unobserved: ${attrs["_unobserved"]})`);
|
|
745
797
|
for (const key of show) {
|
|
746
798
|
// Match the name case-insensitively, and report what was actually found.
|
|
747
799
|
// AWS attribute names are PascalCase and chant's derived ones are not, so a
|
|
@@ -763,4 +815,4 @@ function formatRow(n: IRNode, show: string[]): string {
|
|
|
763
815
|
}
|
|
764
816
|
|
|
765
817
|
/** Internals exposed for unit tests. */
|
|
766
|
-
export const __searchInternals = { parseQuery, matchTerm, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss };
|
|
818
|
+
export const __searchInternals = { parseQuery, matchTerm, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss, provenance };
|
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:
|
|
@@ -542,7 +552,9 @@ Options:
|
|
|
542
552
|
--fix Auto-fix fixable issues (lint command)
|
|
543
553
|
--force Force overwrite existing files (import command)
|
|
544
554
|
-w, --watch Watch for changes and rebuild/re-lint (build, lint)
|
|
545
|
-
-v, --verbose Show stack traces on errors
|
|
555
|
+
-v, --verbose Show stack traces on errors; (build) list every
|
|
556
|
+
resolved build parameter and per-file fold decision
|
|
557
|
+
instead of the one-line summaries
|
|
546
558
|
-h, --help Show this help message
|
|
547
559
|
-p, --profile <name> Temporal worker profile to use (run command)
|
|
548
560
|
--local Run an Op with the local in-process executor (default)
|
|
@@ -572,8 +584,8 @@ Options:
|
|
|
572
584
|
composite factory calls (#1022/#1023), falling back
|
|
573
585
|
to run per-file for anything else outside the fold
|
|
574
586
|
subset (a cross-file-only reference, a re-export,
|
|
575
|
-
\`export default\`, ...). Logs
|
|
576
|
-
|
|
587
|
+
\`export default\`, ...). Logs a fold/run count
|
|
588
|
+
(per-file lines under --verbose). DEFAULT since #1134 — this flag forces it on
|
|
577
589
|
over a chant.config.ts \`build.fold: false\`.
|
|
578
590
|
--no-fold (build) Opt out of folding for this invocation: every
|
|
579
591
|
source module is imported and run, the pre-#1134
|
|
@@ -778,6 +790,7 @@ const registry: CommandDef[] = [
|
|
|
778
790
|
{ name: "lifecycle rollback", handler: runLifecycleRollback },
|
|
779
791
|
{ name: "lifecycle plan", requiresPlugins: true, handler: runLifecyclePlan },
|
|
780
792
|
{ name: "lifecycle affected", requiresPlugins: true, handler: runLifecycleAffected },
|
|
793
|
+
{ name: "lifecycle teardown", requiresPlugins: true, handler: runLifecycleTeardown },
|
|
781
794
|
{ name: "lifecycle log", handler: runLifecycleLog },
|
|
782
795
|
|
|
783
796
|
// Component release ledger + status surface (#568, epic #551)
|
package/src/cli/registry.ts
CHANGED
|
@@ -85,6 +85,13 @@ export interface ParsedArgs {
|
|
|
85
85
|
namespace?: string;
|
|
86
86
|
/** `chant lifecycle rollback --dry-run` — compute the rollback delta and print it; open no PR, push nothing, leave no branch. */
|
|
87
87
|
dryRun?: boolean;
|
|
88
|
+
/** `chant lifecycle teardown <env> --yes` — execute the planned deletion
|
|
89
|
+
* (#1222). Without it the command plans and stops. */
|
|
90
|
+
yes?: boolean;
|
|
91
|
+
/** `chant lifecycle teardown <env> --yes --confirm-prod` — the non-interactive
|
|
92
|
+
* form of the extra confirmation a production-like environment name demands
|
|
93
|
+
* (#1222). Meaningless without `--yes`. */
|
|
94
|
+
confirmProd?: boolean;
|
|
88
95
|
/** `chant import --verbatim` — keep server-defaulted fields in live import */
|
|
89
96
|
verbatim?: boolean;
|
|
90
97
|
/** `chant lifecycle … --src <dir>` — build root override for lifecycle commands */
|
|
@@ -99,7 +99,8 @@ describe("CLI end-to-end — resolveProjectLexicons must not execute project sou
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
test("`chant build --fold` on a project with no `lexicons` config does not execute the hostile file", () => {
|
|
102
|
-
|
|
102
|
+
// --verbose: the per-file fold decision lines are behind it (#1424).
|
|
103
|
+
const { status, stdout, stderr } = runCli(["build", testDir, "--fold", "--verbose"]);
|
|
103
104
|
|
|
104
105
|
expect(stderr).not.toMatch(/EXFIL/);
|
|
105
106
|
expect(stdout).not.toMatch(/EXFIL/);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "fs";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { readAuthoredPages } from "./docs-pages";
|
|
7
|
+
import { docsPipeline, writeDocsSite, GENERATED_MARKER_TAG } from "./docs";
|
|
8
|
+
import { buildSidebar, quadrantItems } from "./docs-sidebar";
|
|
9
|
+
import type { DocsConfig, DocsResult, SidebarPage } from "./docs-types";
|
|
10
|
+
|
|
11
|
+
let root: string;
|
|
12
|
+
|
|
13
|
+
function config(overrides: Partial<DocsConfig> = {}): DocsConfig {
|
|
14
|
+
return {
|
|
15
|
+
name: "fixture",
|
|
16
|
+
displayName: "Fixture",
|
|
17
|
+
description: "fixture lexicon",
|
|
18
|
+
distDir: join(root, "dist"),
|
|
19
|
+
outDir: join(root, "docs"),
|
|
20
|
+
srcDir: join(root, "src"),
|
|
21
|
+
examplesDir: join(root, "examples"),
|
|
22
|
+
basePath: "/chant/lexicons/fixture/",
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function page(name: string, frontmatter: string, body = "Body.\n"): void {
|
|
28
|
+
mkdirSync(join(root, "docs", "pages"), { recursive: true });
|
|
29
|
+
writeFileSync(join(root, "docs", "pages", name), `---\n${frontmatter}\n---\n\n${body}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
root = mkdtempSync(join(tmpdir(), "chant-docs-pages-"));
|
|
34
|
+
mkdirSync(join(root, "dist"), { recursive: true });
|
|
35
|
+
mkdirSync(join(root, "src"), { recursive: true });
|
|
36
|
+
mkdirSync(join(root, "examples"), { recursive: true });
|
|
37
|
+
writeFileSync(
|
|
38
|
+
join(root, "dist", "manifest.json"),
|
|
39
|
+
JSON.stringify({ name: "fixture", version: "0.0.0", intrinsics: [{ name: "ref", description: "a ref" }] }),
|
|
40
|
+
);
|
|
41
|
+
writeFileSync(join(root, "dist", "meta.json"), JSON.stringify({}));
|
|
42
|
+
writeFileSync(join(root, "examples", "snippet.ts"), "export const x = 1;\n");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
rmSync(root, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("readAuthoredPages", () => {
|
|
50
|
+
test("returns nothing when docs/pages/ does not exist", () => {
|
|
51
|
+
expect(readAuthoredPages(config())).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("reads frontmatter, expands file markers, strips sidebar-only keys", () => {
|
|
55
|
+
page(
|
|
56
|
+
"getting-started.mdx",
|
|
57
|
+
'title: "Getting Started"\ndescription: "Start here"\ndiataxis: tutorial\norder: 1\ngroup: "Intro"\nlabel: "Start"\nhidden: false',
|
|
58
|
+
"Before\n\n{{file:snippet.ts}}\n\nAfter\n",
|
|
59
|
+
);
|
|
60
|
+
const [p] = readAuthoredPages(config());
|
|
61
|
+
expect(p.slug).toBe("getting-started");
|
|
62
|
+
expect(p.quadrant).toBe("tutorial");
|
|
63
|
+
expect(p.order).toBe(1);
|
|
64
|
+
expect(p.group).toBe("Intro");
|
|
65
|
+
expect(p.label).toBe("Start");
|
|
66
|
+
expect(p.hidden).toBe(false);
|
|
67
|
+
expect(p.content).toContain('```typescript title="snippet.ts"\nexport const x = 1;\n```');
|
|
68
|
+
expect(p.content).toContain("diataxis: tutorial");
|
|
69
|
+
expect(p.content).not.toMatch(/^(order|group|label|hidden):/m);
|
|
70
|
+
expect(p.content).toContain('title: "Getting Started"');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("label defaults to title", () => {
|
|
74
|
+
page("x.mdx", "title: Plain Title\ndiataxis: reference");
|
|
75
|
+
expect(readAuthoredPages(config())[0].label).toBe("Plain Title");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("missing diataxis throws with the file path", () => {
|
|
79
|
+
page("untagged.mdx", "title: Untagged");
|
|
80
|
+
expect(() => readAuthoredPages(config())).toThrow(/untagged\.mdx.*diataxis/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("unknown diataxis value throws", () => {
|
|
84
|
+
page("bad.mdx", "title: Bad\ndiataxis: guide");
|
|
85
|
+
expect(() => readAuthoredPages(config())).toThrow(/"guide" is not one of/);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("docsPipeline with authored pages", () => {
|
|
90
|
+
test("authored page wins a slug collision with a generated page", () => {
|
|
91
|
+
page("intrinsics.mdx", "title: My Intrinsics\ndiataxis: reference", "Hand-written.\n");
|
|
92
|
+
const result = docsPipeline(config());
|
|
93
|
+
expect(result.pages.get("intrinsics.mdx")).toContain("Hand-written.");
|
|
94
|
+
expect(result.sidebarPages.filter((p) => p.slug === "intrinsics")).toHaveLength(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("generated reference pages land in the reference quadrant after authored ones", () => {
|
|
98
|
+
page("howto.mdx", "title: Do It\ndiataxis: how-to");
|
|
99
|
+
const result = docsPipeline(config());
|
|
100
|
+
const ref = result.sidebarPages.filter((p) => p.quadrant === "reference").map((p) => p.slug);
|
|
101
|
+
expect(ref).toEqual(["intrinsics", "serialization"]);
|
|
102
|
+
expect(result.sidebarPages.find((p) => p.slug === "howto")?.quadrant).toBe("how-to");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("hidden pages are written but kept out of the sidebar", () => {
|
|
106
|
+
page("secret.mdx", "title: Secret\ndiataxis: reference\nhidden: true");
|
|
107
|
+
const result = docsPipeline(config());
|
|
108
|
+
expect(result.pages.has("secret.mdx")).toBe(true);
|
|
109
|
+
expect(result.sidebarPages.some((p) => p.slug === "secret")).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("authored pages carry a provenance marker naming the source file", () => {
|
|
113
|
+
page("howto.mdx", "title: Do It\ndiataxis: how-to");
|
|
114
|
+
const result = docsPipeline(config());
|
|
115
|
+
const out = result.pages.get("howto.mdx") as string;
|
|
116
|
+
expect(out).toContain(GENERATED_MARKER_TAG);
|
|
117
|
+
expect(out).toContain("docs/pages/howto.mdx");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("writeDocsSite reaps the written copy once the source is removed", () => {
|
|
121
|
+
page("howto.mdx", "title: Do It\ndiataxis: how-to");
|
|
122
|
+
const cfg = config();
|
|
123
|
+
writeDocsSite(cfg, docsPipeline(cfg));
|
|
124
|
+
const written = join(root, "docs", "src", "content", "docs", "howto.mdx");
|
|
125
|
+
expect(existsSync(written)).toBe(true);
|
|
126
|
+
rmSync(join(root, "docs", "pages", "howto.mdx"));
|
|
127
|
+
writeDocsSite(cfg, docsPipeline(cfg));
|
|
128
|
+
expect(existsSync(written)).toBe(false);
|
|
129
|
+
const astro = readFileSync(join(root, "docs", "astro.config.mjs"), "utf-8");
|
|
130
|
+
expect(astro).not.toContain("howto");
|
|
131
|
+
const schema = readFileSync(join(root, "docs", "src", "content.config.ts"), "utf-8");
|
|
132
|
+
expect(schema).toContain("diataxis");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("buildSidebar", () => {
|
|
137
|
+
const sb = (pages: SidebarPage[]) =>
|
|
138
|
+
buildSidebar(config(), { pages: new Map(), sidebarPages: pages, stats: { resources: 0, properties: 0, services: 0, rules: 0, intrinsics: 0 } } satisfies DocsResult);
|
|
139
|
+
|
|
140
|
+
test("groups in fixed quadrant order and omits empty quadrants", () => {
|
|
141
|
+
const items = sb([
|
|
142
|
+
{ slug: "why", label: "Why", quadrant: "explanation" },
|
|
143
|
+
{ slug: "start", label: "Start", quadrant: "tutorial" },
|
|
144
|
+
{ slug: "rules", label: "All Rules", quadrant: "reference", order: 1000 },
|
|
145
|
+
]);
|
|
146
|
+
expect(items.map((i) => i.label)).toEqual(["← chant docs", "Overview", "Tutorials", "Reference", "Explanation"]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("orders by order then label and nests groups after loose pages", () => {
|
|
150
|
+
const items = quadrantItems([
|
|
151
|
+
{ slug: "b", label: "Bravo", quadrant: "reference" },
|
|
152
|
+
{ slug: "a", label: "Alpha", quadrant: "reference" },
|
|
153
|
+
{ slug: "z", label: "Zulu", quadrant: "reference", order: 0 },
|
|
154
|
+
{ slug: "eks", label: "EKS", quadrant: "reference", group: "Vendor" },
|
|
155
|
+
{ slug: "aks", label: "AKS", quadrant: "reference", group: "Vendor" },
|
|
156
|
+
]);
|
|
157
|
+
expect(items).toEqual([
|
|
158
|
+
{ label: "Zulu", slug: "z" },
|
|
159
|
+
{ label: "Alpha", slug: "a" },
|
|
160
|
+
{ label: "Bravo", slug: "b" },
|
|
161
|
+
{ label: "Vendor", items: [{ label: "AKS", slug: "aks" }, { label: "EKS", slug: "eks" }] },
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* chant #1377 — `dist/manifest.json` is written by `npm run bundle` (prepack
|
|
168
|
+
* only), so its version is whatever was last bundled on the machine; on one
|
|
169
|
+
* checkout it put the fountain docs two minor versions behind what was
|
|
170
|
+
* already committed. `package.json` is the source of truth, so the rendered
|
|
171
|
+
* version comes from there and the manifest is only a fallback.
|
|
172
|
+
*/
|
|
173
|
+
describe("lexicon version in generated docs comes from package.json (#1377)", () => {
|
|
174
|
+
test("package.json beside dist/ wins over a stale manifest", () => {
|
|
175
|
+
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "fixture", version: "0.37.2" }));
|
|
176
|
+
const index = docsPipeline(config()).pages.get("index.mdx");
|
|
177
|
+
expect(index).toContain("**Lexicon version:** 0.37.2");
|
|
178
|
+
expect(index).not.toContain("0.0.0");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("packageJsonPath overrides the default location", () => {
|
|
182
|
+
mkdirSync(join(root, "elsewhere"), { recursive: true });
|
|
183
|
+
writeFileSync(join(root, "elsewhere", "package.json"), JSON.stringify({ version: "1.2.3" }));
|
|
184
|
+
const index = docsPipeline(config({ packageJsonPath: join(root, "elsewhere", "package.json") })).pages.get(
|
|
185
|
+
"index.mdx",
|
|
186
|
+
);
|
|
187
|
+
expect(index).toContain("**Lexicon version:** 1.2.3");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("falls back to the manifest when no package.json is present", () => {
|
|
191
|
+
const index = docsPipeline(config()).pages.get("index.mdx");
|
|
192
|
+
expect(index).toContain("**Lexicon version:** 0.0.0");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authored lexicon doc pages (chant #1731 / #1733).
|
|
3
|
+
*
|
|
4
|
+
* A lexicon's prose lives as `.mdx` files under `lexicons/<name>/docs/pages/`.
|
|
5
|
+
* Each carries a `diataxis` frontmatter field naming its Diátaxis quadrant
|
|
6
|
+
* (https://diataxis.fr). The pipeline expands `{{file:...}}` markers, stamps a
|
|
7
|
+
* provenance marker and writes the result into `src/content/docs/`, and the
|
|
8
|
+
* sidebar is grouped from the field. This replaced two older modes — prose
|
|
9
|
+
* inside `docs.ts` template literals (`extraPages`) and hand-written content
|
|
10
|
+
* pages wired through `sidebarExtra` — removed in chant #1757.
|
|
11
|
+
*
|
|
12
|
+
* Frontmatter recognised here, on top of Starlight's `title` / `description`:
|
|
13
|
+
*
|
|
14
|
+
* - `diataxis`: `tutorial | how-to | reference | explanation`. Required.
|
|
15
|
+
* - `label`: sidebar label; defaults to `title`.
|
|
16
|
+
* - `group`: a nested subgroup label inside the quadrant (e.g. "Vendor Composites").
|
|
17
|
+
* - `order`: number; lower sorts first within its group. Unordered pages follow, by label.
|
|
18
|
+
* - `hidden`: `true` keeps the page out of the sidebar (reachable by URL only).
|
|
19
|
+
*
|
|
20
|
+
* `group` / `order` / `hidden` / `label` are stripped from the written copy so
|
|
21
|
+
* Starlight's schema never sees them; `diataxis` stays, and the generated
|
|
22
|
+
* `content.config.ts` declares it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
26
|
+
import { join } from "path";
|
|
27
|
+
|
|
28
|
+
import { expandFileMarkers } from "./docs-file-markers";
|
|
29
|
+
import type { DocsConfig, Quadrant, SidebarPage } from "./docs-types";
|
|
30
|
+
|
|
31
|
+
export const QUADRANTS: readonly Quadrant[] = ["tutorial", "how-to", "reference", "explanation"];
|
|
32
|
+
|
|
33
|
+
/** Sidebar group label for each quadrant, in display order. */
|
|
34
|
+
export const QUADRANT_LABELS: Record<Quadrant, string> = {
|
|
35
|
+
tutorial: "Tutorials",
|
|
36
|
+
"how-to": "How-to guides",
|
|
37
|
+
reference: "Reference",
|
|
38
|
+
explanation: "Explanation",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface AuthoredPage extends SidebarPage {
|
|
42
|
+
/** Source file name under `pagesDir`, e.g. `getting-started.mdx`. */
|
|
43
|
+
file: string;
|
|
44
|
+
/** Page body with the rewritten frontmatter, markers expanded. */
|
|
45
|
+
content: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Frontmatter {
|
|
49
|
+
fields: Map<string, string>;
|
|
50
|
+
/** Raw frontmatter lines, in order, so unknown keys survive the rewrite. */
|
|
51
|
+
lines: string[];
|
|
52
|
+
body: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseFrontmatter(text: string, file: string): Frontmatter {
|
|
56
|
+
if (!text.startsWith("---\n")) {
|
|
57
|
+
throw new Error(`${file}: authored doc page must start with a frontmatter block`);
|
|
58
|
+
}
|
|
59
|
+
const end = text.indexOf("\n---", 4);
|
|
60
|
+
if (end === -1) throw new Error(`${file}: unterminated frontmatter`);
|
|
61
|
+
const lines = text.slice(4, end).split("\n");
|
|
62
|
+
const fields = new Map<string, string>();
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const m = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
|
|
65
|
+
if (m) fields.set(m[1], m[2].trim());
|
|
66
|
+
}
|
|
67
|
+
// Skip the closing `---` and the newline after it.
|
|
68
|
+
const afterClose = text.indexOf("\n", end + 1);
|
|
69
|
+
const body = afterClose === -1 ? "" : text.slice(afterClose + 1);
|
|
70
|
+
return { fields, lines, body };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function unquote(v: string): string {
|
|
74
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
75
|
+
try {
|
|
76
|
+
return v.startsWith('"') ? (JSON.parse(v) as string) : v.slice(1, -1);
|
|
77
|
+
} catch {
|
|
78
|
+
return v.slice(1, -1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return v;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Keys this module consumes and removes from the written page. */
|
|
85
|
+
const SIDEBAR_KEYS = new Set(["label", "group", "order", "hidden"]);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read every authored page under `pagesDir`. Returns an empty list when the
|
|
89
|
+
* directory does not exist, which is every lexicon before it migrates.
|
|
90
|
+
*/
|
|
91
|
+
export function readAuthoredPages(config: DocsConfig): AuthoredPage[] {
|
|
92
|
+
const pagesDir = config.pagesDir ?? join(config.outDir, "pages");
|
|
93
|
+
if (!existsSync(pagesDir)) return [];
|
|
94
|
+
|
|
95
|
+
const pages: AuthoredPage[] = [];
|
|
96
|
+
for (const file of readdirSync(pagesDir).sort()) {
|
|
97
|
+
if (!file.endsWith(".mdx") && !file.endsWith(".md")) continue;
|
|
98
|
+
const path = join(pagesDir, file);
|
|
99
|
+
const { fields, lines, body } = parseFrontmatter(readFileSync(path, "utf-8"), path);
|
|
100
|
+
|
|
101
|
+
const title = fields.get("title");
|
|
102
|
+
if (!title) throw new Error(`${path}: frontmatter needs a title`);
|
|
103
|
+
const quadrant = fields.get("diataxis");
|
|
104
|
+
if (!quadrant) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`${path}: frontmatter needs \`diataxis: ${QUADRANTS.join(" | ")}\` (https://diataxis.fr)`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (!QUADRANTS.includes(quadrant as Quadrant)) {
|
|
110
|
+
throw new Error(`${path}: diataxis "${quadrant}" is not one of ${QUADRANTS.join(", ")}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const orderRaw = fields.get("order");
|
|
114
|
+
const order = orderRaw === undefined ? undefined : Number(orderRaw);
|
|
115
|
+
if (order !== undefined && Number.isNaN(order)) {
|
|
116
|
+
throw new Error(`${path}: order must be a number, got "${orderRaw}"`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const kept = lines.filter((line) => {
|
|
120
|
+
const key = line.match(/^([A-Za-z_][\w-]*):/)?.[1];
|
|
121
|
+
return !(key && SIDEBAR_KEYS.has(key));
|
|
122
|
+
});
|
|
123
|
+
let content = body;
|
|
124
|
+
if (config.examplesDir) content = expandFileMarkers(content, config.examplesDir);
|
|
125
|
+
|
|
126
|
+
pages.push({
|
|
127
|
+
file,
|
|
128
|
+
slug: file.replace(/\.mdx?$/, ""),
|
|
129
|
+
label: unquote(fields.get("label") ?? title),
|
|
130
|
+
quadrant: quadrant as Quadrant,
|
|
131
|
+
group: fields.has("group") ? unquote(fields.get("group") as string) : undefined,
|
|
132
|
+
order,
|
|
133
|
+
hidden: fields.get("hidden") === "true",
|
|
134
|
+
content: ["---", ...kept, "---", "", content].join("\n"),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return pages;
|
|
138
|
+
}
|
|
@@ -42,14 +42,6 @@ export function generateOverview(
|
|
|
42
42
|
|
|
43
43
|
const suppress = new Set(config.suppressPages ?? []);
|
|
44
44
|
|
|
45
|
-
// Extra pages listed first in reference links
|
|
46
|
-
if (config.extraPages && config.extraPages.length > 0) {
|
|
47
|
-
for (const page of config.extraPages) {
|
|
48
|
-
if (page.sidebar === false) continue;
|
|
49
|
-
lines.push(`- [${page.title}](./${page.slug})`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
45
|
if (!suppress.has("intrinsics") && manifest.intrinsics && manifest.intrinsics.length > 0) {
|
|
54
46
|
lines.push(
|
|
55
47
|
`- [Intrinsic Functions](./intrinsics) — ${manifest.intrinsics.length} built-in functions`,
|