@intentius/chant 0.79.0 → 0.80.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.
Files changed (162) hide show
  1. package/dist/audit/core.d.ts.map +1 -1
  2. package/dist/audit/discover.d.ts +38 -0
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/audit/terraform-state.d.ts +14 -0
  5. package/dist/audit/terraform-state.d.ts.map +1 -1
  6. package/dist/cli/command-group.d.ts +6 -0
  7. package/dist/cli/command-group.d.ts.map +1 -1
  8. package/dist/cli/commands/audit.d.ts +27 -2
  9. package/dist/cli/commands/audit.d.ts.map +1 -1
  10. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  11. package/dist/cli/commands/doctor.d.ts.map +1 -1
  12. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  13. package/dist/cli/commands/init.d.ts +6 -0
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/lint.d.ts.map +1 -1
  16. package/dist/cli/commands/onboard.d.ts.map +1 -1
  17. package/dist/cli/commands/update.d.ts.map +1 -1
  18. package/dist/cli/conflict-check.d.ts +6 -0
  19. package/dist/cli/conflict-check.d.ts.map +1 -1
  20. package/dist/cli/handlers/dev.d.ts.map +1 -1
  21. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  22. package/dist/cli/handlers/misc.d.ts.map +1 -1
  23. package/dist/cli/handlers/operator.d.ts.map +1 -1
  24. package/dist/cli/handlers/promote.d.ts +17 -0
  25. package/dist/cli/handlers/promote.d.ts.map +1 -0
  26. package/dist/cli/handlers/run-generate.d.ts +39 -0
  27. package/dist/cli/handlers/run-generate.d.ts.map +1 -0
  28. package/dist/cli/handlers/run.d.ts.map +1 -1
  29. package/dist/cli/main.d.ts.map +1 -1
  30. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  31. package/dist/cli/plugins.d.ts +9 -0
  32. package/dist/cli/plugins.d.ts.map +1 -1
  33. package/dist/cli/registry.d.ts +14 -1
  34. package/dist/cli/registry.d.ts.map +1 -1
  35. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  36. package/dist/components/cli-support.d.ts.map +1 -1
  37. package/dist/components/discover.d.ts.map +1 -1
  38. package/dist/components/driver.d.ts.map +1 -1
  39. package/dist/components/fan-out-support.d.ts.map +1 -1
  40. package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
  41. package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
  42. package/dist/components/presets/ecs-fargate.d.ts +10 -4
  43. package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
  44. package/dist/components/promote.d.ts +211 -0
  45. package/dist/components/promote.d.ts.map +1 -0
  46. package/dist/components/unbound-gate-approval.d.ts +24 -0
  47. package/dist/components/unbound-gate-approval.d.ts.map +1 -0
  48. package/dist/config.d.ts +82 -5
  49. package/dist/config.d.ts.map +1 -1
  50. package/dist/discovery/convergence.d.ts +97 -0
  51. package/dist/discovery/convergence.d.ts.map +1 -0
  52. package/dist/discovery/files.d.ts +41 -2
  53. package/dist/discovery/files.d.ts.map +1 -1
  54. package/dist/discovery/fold-import.d.ts +15 -0
  55. package/dist/discovery/fold-import.d.ts.map +1 -1
  56. package/dist/graph-ir.d.ts +13 -0
  57. package/dist/graph-ir.d.ts.map +1 -1
  58. package/dist/lexicon-module.d.ts +65 -0
  59. package/dist/lexicon-module.d.ts.map +1 -0
  60. package/dist/lexicon.d.ts +24 -0
  61. package/dist/lexicon.d.ts.map +1 -1
  62. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  63. package/dist/lifecycle/legacy-digest.d.ts +26 -0
  64. package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
  65. package/dist/lifecycle/release-ledger.d.ts +25 -0
  66. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  67. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
  68. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
  69. package/dist/op/activity-contract-registry.d.ts.map +1 -1
  70. package/dist/op/activity-registry.d.ts.map +1 -1
  71. package/dist/op/discover.d.ts.map +1 -1
  72. package/dist/op/gate-approval.d.ts.map +1 -1
  73. package/dist/op/generate-pipeline.d.ts.map +1 -1
  74. package/dist/op/runtimes/local.d.ts.map +1 -1
  75. package/dist/workspace/record-source.d.ts +22 -0
  76. package/dist/workspace/record-source.d.ts.map +1 -0
  77. package/dist/workspace/records-cli.d.ts +57 -0
  78. package/dist/workspace/records-cli.d.ts.map +1 -0
  79. package/dist/workspace/records.d.ts +118 -0
  80. package/dist/workspace/records.d.ts.map +1 -0
  81. package/package.json +3 -3
  82. package/src/audit/core.ts +13 -2
  83. package/src/audit/discover.ts +90 -12
  84. package/src/audit/terraform-state.test.ts +86 -1
  85. package/src/audit/terraform-state.ts +26 -0
  86. package/src/cli/command-group.ts +7 -0
  87. package/src/cli/commands/audit-walk-warnings.test.ts +156 -0
  88. package/src/cli/commands/audit.ts +93 -19
  89. package/src/cli/commands/check-lexicon-examples.ts +2 -1
  90. package/src/cli/commands/doctor.ts +3 -0
  91. package/src/cli/commands/import-agents.ts +11 -4
  92. package/src/cli/commands/init.ts +57 -25
  93. package/src/cli/commands/lint.test.ts +58 -1
  94. package/src/cli/commands/lint.ts +48 -18
  95. package/src/cli/commands/onboard.ts +15 -0
  96. package/src/cli/commands/update.ts +5 -1
  97. package/src/cli/conflict-check.test.ts +18 -1
  98. package/src/cli/conflict-check.ts +13 -0
  99. package/src/cli/discovery-skip.test.ts +168 -0
  100. package/src/cli/handlers/build.test.ts +11 -0
  101. package/src/cli/handlers/build.ts +1 -1
  102. package/src/cli/handlers/dev.ts +3 -0
  103. package/src/cli/handlers/graph.test.ts +20 -0
  104. package/src/cli/handlers/graph.ts +4 -2
  105. package/src/cli/handlers/lifecycle.ts +2 -1
  106. package/src/cli/handlers/misc.ts +16 -0
  107. package/src/cli/handlers/operator.ts +2 -1
  108. package/src/cli/handlers/promote.test.ts +261 -0
  109. package/src/cli/handlers/promote.ts +325 -0
  110. package/src/cli/handlers/run-generate.test.ts +160 -0
  111. package/src/cli/handlers/run-generate.ts +133 -0
  112. package/src/cli/handlers/run.ts +13 -1
  113. package/src/cli/main.test.ts +33 -1
  114. package/src/cli/main.ts +69 -16
  115. package/src/cli/mcp/op-tools.ts +2 -1
  116. package/src/cli/path-lexicon-messages.test.ts +152 -0
  117. package/src/cli/plugins.ts +44 -6
  118. package/src/cli/registry.ts +14 -1
  119. package/src/components/__fixtures__/alb-ecs-service.json +0 -8
  120. package/src/components/capability-plugin-loader.ts +3 -1
  121. package/src/components/cli-support.ts +3 -2
  122. package/src/components/discover.ts +26 -2
  123. package/src/components/driver.test.ts +12 -11
  124. package/src/components/driver.ts +2 -0
  125. package/src/components/fan-out-support.ts +2 -1
  126. package/src/components/pilots/README.md +1 -1
  127. package/src/components/pilots/alb-ecs.pilot.ts +7 -7
  128. package/src/components/presets/ecs-fargate.ts +10 -5
  129. package/src/components/promote.test.ts +371 -0
  130. package/src/components/promote.ts +537 -0
  131. package/src/components/unbound-gate-approval.test.ts +82 -0
  132. package/src/components/unbound-gate-approval.ts +51 -0
  133. package/src/config.test.ts +40 -0
  134. package/src/config.ts +128 -11
  135. package/src/discovery/convergence.test.ts +231 -0
  136. package/src/discovery/convergence.ts +399 -0
  137. package/src/discovery/files.test.ts +127 -1
  138. package/src/discovery/files.ts +97 -9
  139. package/src/discovery/fold-import.ts +83 -9
  140. package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
  141. package/src/graph-ir.ts +14 -0
  142. package/src/lexicon-module.test.ts +203 -0
  143. package/src/lexicon-module.ts +117 -0
  144. package/src/lexicon.ts +26 -0
  145. package/src/lifecycle/build-ledger-store.ts +11 -1
  146. package/src/lifecycle/legacy-digest.test.ts +114 -0
  147. package/src/lifecycle/legacy-digest.ts +62 -0
  148. package/src/lifecycle/release-ledger.ts +34 -0
  149. package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
  150. package/src/op/activity-contract-registry.ts +18 -4
  151. package/src/op/activity-registry.ts +26 -0
  152. package/src/op/discover-convergence.test.ts +70 -0
  153. package/src/op/discover.ts +4 -0
  154. package/src/op/gate-approval.ts +3 -1
  155. package/src/op/generate-pipeline.ts +2 -1
  156. package/src/op/runtimes/local.ts +2 -1
  157. package/src/workspace/record-source.ts +117 -0
  158. package/src/workspace/records-cli.ts +126 -0
  159. package/src/workspace/records-contract.test.ts +88 -0
  160. package/src/workspace/records.schema.json +120 -0
  161. package/src/workspace/records.test.ts +243 -0
  162. package/src/workspace/records.ts +383 -0
@@ -0,0 +1,152 @@
1
+ /**
2
+ * chant#2578 — init, onboard, audit and import-agents name a lexicon declared
3
+ * by path (#2520) by that path, and print no install line for it. A lexicon
4
+ * named by package keeps its package name and `npm i` line.
5
+ */
6
+ import { describe, test, expect, beforeEach, afterEach } from "vitest";
7
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import {
11
+ lexiconPackagesToInstall,
12
+ lexiconSourceLabel,
13
+ registerLexiconDeclarations,
14
+ resetLexiconModules,
15
+ } from "../lexicon-module";
16
+ import { initCommand } from "./commands/init";
17
+ import { onboardCommand } from "./commands/onboard";
18
+ import { installLine } from "./commands/audit";
19
+ import { importAgentsCommand } from "./commands/import-agents";
20
+ import { auditFiles, MissingLexiconError, type AuditInput } from "../audit/core";
21
+
22
+ let dir: string;
23
+
24
+ beforeEach(() => {
25
+ dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-2578-")));
26
+ resetLexiconModules();
27
+ });
28
+
29
+ afterEach(() => {
30
+ resetLexiconModules();
31
+ rmSync(dir, { recursive: true, force: true });
32
+ });
33
+
34
+ describe("the shared helpers", () => {
35
+ test("a path lexicon is labelled by its path and has no package to install", () => {
36
+ registerLexiconDeclarations(["aws", { name: "site", module: "./lexicon/index.ts" }], dir);
37
+ expect(lexiconSourceLabel("site", dir)).toBe("./lexicon/index.ts");
38
+ expect(lexiconSourceLabel("aws", dir)).toBe("@intentius/chant-lexicon-aws");
39
+ expect(lexiconPackagesToInstall(["aws", "site"])).toEqual(["@intentius/chant-lexicon-aws"]);
40
+ });
41
+
42
+ test("a path outside the directory is shown absolute", () => {
43
+ registerLexiconDeclarations([{ name: "site", module: "/elsewhere/site.ts" }], dir);
44
+ expect(lexiconSourceLabel("site", dir)).toBe("/elsewhere/site.ts");
45
+ });
46
+ });
47
+
48
+ describe("init", () => {
49
+ function writeConfig(entry: string): void {
50
+ writeFileSync(join(dir, "chant.config.ts"), `export default { lexicons: [${entry}] };\n`);
51
+ }
52
+
53
+ test("a lexicon the existing config declares by path gets no dependency and no type stub", async () => {
54
+ writeConfig(`{ name: "site", module: "./lexicon/index.ts" }`);
55
+ const result = await initCommand({ path: dir, lexicon: "site", force: true, skipMcp: true, skipInstall: true });
56
+
57
+ expect(result.success).toBe(true);
58
+ expect(result.lexiconModule).toBe("./lexicon/index.ts");
59
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
60
+ expect(Object.keys(pkg.dependencies)).toEqual(["@intentius/chant"]);
61
+ expect(existsSync(join(dir, ".chant", "types", "lexicon-site"))).toBe(false);
62
+ });
63
+
64
+ test("a package lexicon is unchanged", async () => {
65
+ writeConfig(`"aws"`);
66
+ const result = await initCommand({ path: dir, lexicon: "aws", force: true, skipMcp: true, skipInstall: true });
67
+
68
+ expect(result.success).toBe(true);
69
+ expect(result.lexiconModule).toBeUndefined();
70
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
71
+ expect(Object.keys(pkg.dependencies)).toEqual(["@intentius/chant", "@intentius/chant-lexicon-aws"]);
72
+ expect(existsSync(join(dir, ".chant", "types", "lexicon-aws", "package.json"))).toBe(true);
73
+ });
74
+ });
75
+
76
+ describe("onboard", () => {
77
+ test("a lexicon declared by path is named by its path, with nothing patched", () => {
78
+ registerLexiconDeclarations([{ name: "site", module: "./lexicon/index.ts" }], process.cwd());
79
+ const result = onboardCommand({ name: "site", root: dir });
80
+
81
+ expect(result.success).toBe(false);
82
+ expect(result.error).toContain("./lexicon/index.ts");
83
+ expect(result.error).not.toMatch(/npm (i|install)\b/);
84
+ expect(result.patched).toEqual([]);
85
+ });
86
+
87
+ test("a package lexicon still patches", () => {
88
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "root", dependencies: {} }));
89
+ const result = onboardCommand({ name: "site", root: dir });
90
+
91
+ expect(result.success).toBe(true);
92
+ expect(result.patched.some((p) => p.startsWith("package.json"))).toBe(true);
93
+ });
94
+ });
95
+
96
+ describe("audit", () => {
97
+ test("the npx line leaves out a lexicon declared by path", () => {
98
+ registerLexiconDeclarations([{ name: "github", module: "./gh/index.ts" }], dir);
99
+ expect(installLine(["github", "gitlab"], ".")).toBe(
100
+ "npx -p @intentius/chant -p @intentius/chant-lexicon-gitlab chant audit .",
101
+ );
102
+ });
103
+
104
+ test("a path lexicon that fails to load is named by its path, with no npm line", async () => {
105
+ registerLexiconDeclarations([{ name: "helm", module: join(dir, "missing.ts") }], dir);
106
+ const input = { lexicon: "helm", path: "Chart.yaml", content: "" } as AuditInput;
107
+
108
+ const error = await auditFiles([input]).then(
109
+ () => undefined,
110
+ (err: unknown) => err,
111
+ );
112
+ expect(error).toBeInstanceOf(MissingLexiconError);
113
+ expect((error as Error).message).toContain(join(dir, "missing.ts"));
114
+ expect((error as Error).message).not.toContain("npm i");
115
+ });
116
+ });
117
+
118
+ describe("import-agents", () => {
119
+ function seedAgentConfig(home: string): void {
120
+ mkdirSync(join(home, ".claude"), { recursive: true });
121
+ writeFileSync(join(home, ".claude", "mcp.json"), JSON.stringify({ mcpServers: { a: { command: "x" } } }));
122
+ }
123
+
124
+ test("a path lexicon that fails to load is named by its path, with no npm line", async () => {
125
+ const home = join(dir, "home");
126
+ seedAgentConfig(home);
127
+ registerLexiconDeclarations([{ name: "fountain", module: "./agents/missing.ts" }], dir);
128
+
129
+ const result = await importAgentsCommand({ home, platform: "linux", projectRoots: [], output: join(dir, "out") });
130
+
131
+ expect(result.success).toBe(false);
132
+ expect(result.error).toContain("declared by path");
133
+ expect(result.error).toContain("missing.ts");
134
+ expect(result.error).not.toContain("npm i");
135
+ });
136
+
137
+ test("a package lexicon still gets its npm line", async () => {
138
+ const home = join(dir, "home");
139
+ seedAgentConfig(home);
140
+
141
+ const result = await importAgentsCommand({
142
+ home,
143
+ platform: "linux",
144
+ projectRoots: [],
145
+ output: join(dir, "out"),
146
+ lexicon: "no-such-lexicon",
147
+ });
148
+
149
+ expect(result.success).toBe(false);
150
+ expect(result.error).toContain("npm i @intentius/chant-lexicon-no-such-lexicon");
151
+ });
152
+ });
@@ -1,10 +1,30 @@
1
+ import { importLexiconModule, lexiconModulePath, lexiconNames } from "../lexicon-module";
1
2
  import { createRequire } from "node:module";
2
3
  import { dirname, join } from "node:path";
3
4
  import { existsSync, readFileSync } from "node:fs";
4
5
  import { isLexiconPlugin, type LexiconPlugin } from "../lexicon";
5
- import { loadChantConfigUpward } from "../config";
6
+ import { loadChantConfig, loadChantConfigUpward } from "../config";
7
+ import { findProjectConfig } from "../project-root";
6
8
  import { findInfraFiles, detectLexicons } from "../index";
7
- import { checkConflicts } from "./conflict-check";
9
+ import { checkConflicts, describeConflict } from "./conflict-check";
10
+
11
+ /**
12
+ * chant#2578 — load the chant.config nearest `dir` (walking up) so the
13
+ * lexicons it declares by path are recorded (../lexicon-module.ts), for a
14
+ * command that does not otherwise read the config. With no config there is
15
+ * nothing to record. A config that fails to load records nothing either, and
16
+ * the command keeps its package behaviour; the commands that need a valid
17
+ * config report its errors themselves.
18
+ */
19
+ export async function recordProjectLexicons(dir: string): Promise<void> {
20
+ const { dir: projectDir, configPath } = findProjectConfig(dir);
21
+ if (configPath === undefined) return;
22
+ try {
23
+ await loadChantConfig(projectDir);
24
+ } catch {
25
+ // See above: an unloadable config changes nothing here.
26
+ }
27
+ }
8
28
 
9
29
  /**
10
30
  * Load a single lexicon plugin by lexicon name.
@@ -13,6 +33,23 @@ import { checkConflicts } from "./conflict-check";
13
33
  * LexiconPlugin export. Falls back to wrapping a raw Serializer export.
14
34
  */
15
35
  export async function loadPlugin(lexiconName: string): Promise<LexiconPlugin> {
36
+ // chant #2520 — a lexicon declared by module path loads from that module.
37
+ const modulePath = lexiconModulePath(lexiconName);
38
+ if (modulePath !== undefined) {
39
+ const mod = (await importLexiconModule(lexiconName))!;
40
+ for (const value of Object.values(mod)) {
41
+ if (isLexiconPlugin(value)) {
42
+ if (value.name !== lexiconName) {
43
+ throw new Error(
44
+ `lexicon "${lexiconName}" is declared with module ${modulePath}, but the plugin it exports is named "${value.name}"`,
45
+ );
46
+ }
47
+ return value;
48
+ }
49
+ }
50
+ throw new Error(`Module ${modulePath} (lexicon "${lexiconName}") does not export a LexiconPlugin`);
51
+ }
52
+
16
53
  const packageName = `@intentius/chant-lexicon-${lexiconName}`;
17
54
  const mod = await import(packageName);
18
55
 
@@ -67,6 +104,9 @@ export function resolveLexiconVersions(lexiconNames: readonly string[]): Record<
67
104
  const versions: Record<string, string> = {};
68
105
 
69
106
  for (const name of lexiconNames) {
107
+ // chant #2520 — a lexicon declared by module path is not an installed
108
+ // package, so it has no package version to report.
109
+ if (lexiconModulePath(name) !== undefined) continue;
70
110
  const packageName = `@intentius/chant-lexicon-${name}`;
71
111
  try {
72
112
  let dir = dirname(require_.resolve(packageName));
@@ -177,9 +217,7 @@ export async function loadPlugins(lexiconNames: string[]): Promise<LexiconPlugin
177
217
  }
178
218
 
179
219
  if (report.conflicts.length > 0) {
180
- const details = report.conflicts
181
- .map((c) => ` ${c.type} "${c.key}" from: ${c.plugins.join(", ")}`)
182
- .join("\n");
220
+ const details = report.conflicts.map(describeConflict).join("\n");
183
221
  throw new Error(
184
222
  `Cross-lexicon conflicts detected:\n${details}`,
185
223
  );
@@ -222,7 +260,7 @@ export async function resolveProjectLexicons(projectPath: string): Promise<strin
222
260
  const { config } = await loadChantConfigUpward(projectPath);
223
261
 
224
262
  if (config.lexicons && config.lexicons.length > 0) {
225
- return config.lexicons;
263
+ return lexiconNames(config.lexicons);
226
264
  }
227
265
 
228
266
  // Fallback: detect from source imports — a pure text scan, no execution.
@@ -145,6 +145,8 @@ export interface ParsedArgs {
145
145
  /** `--env <name>` — active environment: sets CHANT_ENV so env-aware source
146
146
  * re-evaluates for that environment (`build` + `graph`), and drives policy. */
147
147
  env?: string;
148
+ /** `chant build --components --generate <lexicon> --promote-to <env>` (#2575) — add a promote job to the generated pipeline. */
149
+ promoteTo?: string;
148
150
  /** `chant graph --stacks` — render the cross-stack apply-ordering graph */
149
151
  stacks?: boolean;
150
152
  /** `chant graph --live --overlay` — classify the provisioned graph against
@@ -175,8 +177,13 @@ export interface ParsedArgs {
175
177
  components?: boolean;
176
178
  /** `chant build --components --generate <lexicon>` — generate mode (#563):
177
179
  * synthesize CI YAML from discovered components instead of running a normal
178
- * lexicon build. Only "gitlab" is implemented for v1. */
180
+ * lexicon build. Also `chant run --generate <provider>` (#2533): one CI
181
+ * pipeline file per scheduled Op, through `generateOpsPipeline`. */
179
182
  generate?: string;
183
+ /** `chant run --generate <provider> --spec <file>` (#2533): a JSON file of
184
+ * `ScheduledOpSpec`s (or `{ ops, options }`) to generate instead of every
185
+ * discovered Op that declares a `schedule`. */
186
+ opsSpec?: string;
180
187
  /** `chant graph --format ir --detail <0..3>` — graph IR detail tier */
181
188
  detail?: number;
182
189
  /** `chant graph --lens <kind>:<target>` — focus the graph IR on a slice */
@@ -242,6 +249,8 @@ export interface ParsedArgs {
242
249
  allProjects?: boolean;
243
250
  /** `chant audit --fail-on merge-worthy|warning|error|none` */
244
251
  failOn?: string;
252
+ /** `chant audit --max-files <n>` (#2528) — files the local walk takes before it stops (default 1000). */
253
+ maxFiles?: number;
245
254
  /** `chant audit --theme <file>` — JSON theme knobs for the HTML report */
246
255
  theme?: string;
247
256
  /** `chant dev surface-diff --update-snapshot` — write the fresh snapshot as the new baseline */
@@ -270,6 +279,10 @@ export interface ParsedArgs {
270
279
  * should win.
271
280
  */
272
281
  at?: string;
282
+ /** `chant workspace records --kind <path>` (#2546): the record kind file to read records through. */
283
+ kind?: string;
284
+ /** `chant workspace records --current` (#2546): leave out records a closed record supersedes. */
285
+ current?: boolean;
273
286
  /**
274
287
  * `chant search "<q>" --ambient --live --env <name>` (#1278) — also report
275
288
  * resources of a kind this estate manages that exist in the account without
@@ -39,13 +39,5 @@
39
39
  { "kind": "health-gate", "path": "/healthz" }
40
40
  ]
41
41
  }
42
- ],
43
- "rollback": [
44
- {
45
- "phase": "Rollback",
46
- "steps": [
47
- { "kind": "rollback-previous", "service": "search", "cluster": "$env.cluster" }
48
- ]
49
- }
50
42
  ]
51
43
  }
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import { CapabilityRegistry } from "./capability";
25
+ import { lexiconModulePath } from "../lexicon-module";
25
26
  import type { CapabilityPlugin } from "./capability-plugin";
26
27
  import { isCapabilityPlugin } from "./capability-plugin";
27
28
  import { starterCapabilityPlugin } from "./starter-plugin";
@@ -113,7 +114,8 @@ export async function loadCapabilityPlugins(names: string[]): Promise<Capability
113
114
  * where the package's whole reason to exist is the plugin.
114
115
  */
115
116
  export async function loadCapabilityPluginFromLexicon(name: string): Promise<CapabilityPlugin | null> {
116
- const packageName = `@intentius/chant-lexicon-${name}`;
117
+ // chant #2520 — a lexicon declared by module path is imported from that path.
118
+ const packageName = lexiconModulePath(name) ?? `@intentius/chant-lexicon-${name}`;
117
119
  let mod: Record<string, unknown>;
118
120
  try {
119
121
  mod = (await import(packageName)) as Record<string, unknown>;
@@ -22,6 +22,7 @@
22
22
  * executor, the CLI entrypoint the driver (#556) never had.
23
23
  */
24
24
 
25
+ import { lexiconModulePath, lexiconNames } from "../lexicon-module";
25
26
  import { discoverComponents } from "./discover";
26
27
  import type { BuildParamProvenance } from "../provenance";
27
28
  import { projectToJson, type Archetype } from "./component";
@@ -237,7 +238,7 @@ export interface GenerateComponentsResult {
237
238
  async function loadLexiconPlugin(name: string): Promise<LexiconPlugin | null> {
238
239
  let mod: Record<string, unknown>;
239
240
  try {
240
- mod = (await import(`@intentius/chant-lexicon-${name}`)) as Record<string, unknown>;
241
+ mod = (await import(lexiconModulePath(name) ?? `@intentius/chant-lexicon-${name}`)) as Record<string, unknown>;
241
242
  } catch {
242
243
  return null;
243
244
  }
@@ -550,7 +551,7 @@ export async function runComponents(
550
551
  options.registry ??
551
552
  (await buildCapabilityRegistry({
552
553
  plugins: options.capabilityPlugins ?? config.capabilities,
553
- lexicons: config.lexicons,
554
+ lexicons: lexiconNames(config.lexicons),
554
555
  }));
555
556
  const env = options.env ?? "local";
556
557
  const selected = targets.map((c) => c.name);
@@ -49,6 +49,9 @@ import { isComponent, type Component } from "./component";
49
49
  import type { BuildParamProvenance } from "../provenance";
50
50
  import { buildParamValues } from "../build-params";
51
51
  import { setBuildParams } from "../params";
52
+ import { compileDiscoveryFilter, hasDiscoveryMarker } from "../discovery/files";
53
+ import { resolveDiscoveryGlobs } from "../config";
54
+ import { warnDiscoveryChanges } from "../discovery/convergence";
52
55
 
53
56
  /** One discovered component, paired with the file it was exported from. */
54
57
  export interface DiscoveredComponent {
@@ -118,11 +121,15 @@ export interface ImportedComponentModule {
118
121
  * Recursively find every `*.component.ts` file under `path`, the same
119
122
  * child-project boundary rule `findInfraFiles` (`../discovery/files.ts`)
120
123
  * uses: a subdirectory with its own `chant.config.ts` is a separate scope
121
- * once the outer project's own source root has been established.
124
+ * once the outer project's own source root has been established. The
125
+ * project's `exclude`/`include` globs and the skip marker apply here as they
126
+ * do there (#2519).
122
127
  */
123
128
  async function findComponentFiles(path: string): Promise<string[]> {
124
129
  const files: string[] = [];
125
130
  let sourceRoot: string | null = null;
131
+ const skip = compileDiscoveryFilter(await resolveDiscoveryGlobs(path));
132
+ const skippedChildren: string[] = [];
126
133
 
127
134
  async function scanDirectory(dir: string): Promise<void> {
128
135
  let entries;
@@ -148,6 +155,7 @@ async function findComponentFiles(path: string): Promise<string[]> {
148
155
  if (sourceRoot === null) {
149
156
  sourceRoot = fullPath;
150
157
  } else {
158
+ skippedChildren.push(fullPath);
151
159
  continue;
152
160
  }
153
161
  }
@@ -156,7 +164,9 @@ async function findComponentFiles(path: string): Promise<string[]> {
156
164
  entry.isFile() &&
157
165
  entry.name.endsWith(".component.ts") &&
158
166
  !entry.name.endsWith(".test.component.ts") &&
159
- !entry.name.endsWith(".spec.component.ts")
167
+ !entry.name.endsWith(".spec.component.ts") &&
168
+ !skip?.(fullPath) &&
169
+ !(await hasDiscoveryMarker(fullPath))
160
170
  ) {
161
171
  files.push(fullPath);
162
172
  }
@@ -164,6 +174,20 @@ async function findComponentFiles(path: string): Promise<string[]> {
164
174
  }
165
175
 
166
176
  await scanDirectory(path);
177
+ // #2527's warning release: the list returned is today's, unchanged.
178
+ await warnDiscoveryChanges({
179
+ walker: "components",
180
+ root: path,
181
+ files,
182
+ sourceRoot,
183
+ skippedChildren,
184
+ fileOk: async (name, full) =>
185
+ name.endsWith(".component.ts") &&
186
+ !name.endsWith(".test.component.ts") &&
187
+ !name.endsWith(".spec.component.ts") &&
188
+ !skip?.(full) &&
189
+ !(await hasDiscoveryMarker(full)),
190
+ });
167
191
  return files;
168
192
  }
169
193
 
@@ -450,7 +450,7 @@ describe("runComponentDeploy — onFailure saga rollback", () => {
450
450
  );
451
451
  });
452
452
 
453
- it("runs a component-level rollback (schema `rollback` field) after step rollback, matching the ALB/ECS pilot's explicit compensation", async () => {
453
+ it("runs a component-level rollback (schema `rollback` field) after step rollback", async () => {
454
454
  const registry = new CapabilityRegistry();
455
455
  registry.register(fakeCapability("docker-build", { run: () => ({ digest: "sha256:abc" }) }).capability);
456
456
  registry.register(
@@ -458,26 +458,29 @@ describe("runComponentDeploy — onFailure saga rollback", () => {
458
458
  .capability,
459
459
  );
460
460
  registry.register(fakeCapability("cfn-deploy", { run: () => ({ stackStatus: "UPDATE_COMPLETE", outputs: {} }) }).capability);
461
- // ecs-update-service fails, and (per the pilot's comment) has no native
462
- // rollback — the component's own `rollback` phase is what should fire.
461
+ // ecs-update-service fails; the component's own `rollback` phase is what
462
+ // should fire after the executed steps unwind.
463
463
  registry.register(fakeCapability("ecs-update-service", { failRun: true }).capability);
464
- const { capability: rollbackPreviousCap, calls: rollbackPreviousCalls } = fakeCapability("rollback-previous", {
464
+ const { capability: restoreCap, calls: restoreCalls } = fakeCapability("restore-service", {
465
465
  run: () => ({ restored: true }),
466
466
  });
467
- registry.register(rollbackPreviousCap);
467
+ registry.register(restoreCap);
468
468
  // Verify/rollback phases never reached since Apply fails first, but register
469
469
  // stubs anyway so a hypothetical reordering wouldn't crash the test on an
470
470
  // unregistered kind.
471
471
  registry.register(fakeCapability("wait-steady-state", { run: () => ({ runningCount: 1 }) }).capability);
472
472
  registry.register(fakeCapability("health-gate", { run: () => ({ healthy: true }) }).capability);
473
473
 
474
- const json = projectToJson(searchService) as unknown as DriverComponent;
474
+ const json: DriverComponent = {
475
+ ...(projectToJson(searchService) as unknown as DriverComponent),
476
+ rollback: [{ phase: "Rollback", steps: [{ kind: "restore-service", service: "search" }] }],
477
+ };
475
478
  const result = await runComponentDeploy(json, { env: "dev", component: "search-service" }, registry, {});
476
479
 
477
480
  expect(result.ok).toBe(false);
478
- expect(rollbackPreviousCalls).toHaveLength(1);
479
- expect(rollbackPreviousCalls[0]!.input).toMatchObject({ service: "search" });
480
- const rollbackRecord = result.records.find((r) => r.phase === "Rollback" && r.kind === "rollback-previous");
481
+ expect(restoreCalls).toHaveLength(1);
482
+ expect(restoreCalls[0]!.input).toMatchObject({ service: "search" });
483
+ const rollbackRecord = result.records.find((r) => r.phase === "Rollback" && r.kind === "restore-service");
481
484
  expect(rollbackRecord?.status).toBe("ok");
482
485
  });
483
486
  });
@@ -630,7 +633,6 @@ describe("runInterpretDriver — end to end", () => {
630
633
  "ecs-update-service",
631
634
  "wait-steady-state",
632
635
  "health-gate",
633
- "rollback-previous",
634
636
  "wait-for-stack",
635
637
  "run-migration",
636
638
  ];
@@ -750,7 +752,6 @@ describe("runInterpretDriver — end to end", () => {
750
752
  "ecs-update-service",
751
753
  "wait-steady-state",
752
754
  "health-gate",
753
- "rollback-previous",
754
755
  "wait-for-stack",
755
756
  "run-migration",
756
757
  "lambda-deploy",
@@ -49,6 +49,7 @@
49
49
  import { topoSort } from "../codegen/topo-sort";
50
50
  import { evaluateGate, gitGateLedgerPort, type GateLedgerPort } from "../op/gate";
51
51
  import { gateName } from "../op/gate-name";
52
+ import { warnOnUnboundComponentApproval } from "./unbound-gate-approval";
52
53
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
53
54
  import type { CapabilityRegistry, DeployContext } from "./capability";
54
55
  import type { RunProgressEvent, RunProgressStatus } from "./run-progress";
@@ -479,6 +480,7 @@ async function runPhase(
479
480
  }
480
481
  throw new GateStop(gateRecords, [], check.pending, check.pushed, check.pushWarning);
481
482
  }
483
+ warnOnUnboundComponentApproval(ctx.component, gateName(gate), ctx.env, check.resolution);
482
484
  gateRecords.push({
483
485
  ...base,
484
486
  status: "ok" as const,
@@ -13,6 +13,7 @@
13
13
  * over.
14
14
  */
15
15
 
16
+ import { lexiconNames } from "../lexicon-module";
16
17
  import { resolveComponentTargets } from "./cli-support";
17
18
  import { applyConfigDefaults } from "./config-defaults";
18
19
  import { buildCapabilityRegistry } from "./capability-plugin-loader";
@@ -91,5 +92,5 @@ export async function deriveFanOut(options: DeriveFanOutOptions): Promise<Derive
91
92
  */
92
93
  export async function fanOutRegistry(path: string, config?: ChantConfig): Promise<CapabilityRegistry> {
93
94
  const resolved = config ?? (await loadChantConfig(path)).config;
94
- return buildCapabilityRegistry({ plugins: resolved.capabilities, lexicons: resolved.lexicons });
95
+ return buildCapabilityRegistry({ plugins: resolved.capabilities, lexicons: lexiconNames(resolved.lexicons) });
95
96
  }
@@ -13,7 +13,7 @@ The epic ([#551](https://github.com/INTENTIUS/chant/issues/551)) picked these th
13
13
  |---|---|---|---|---|---|---|
14
14
  | **Neo4j per-instance fan-out** | [`neo4j-fanout.pilot.ts`](./neo4j-fanout.pilot.ts) / [`neo4j-fanout.json`](../__fixtures__/neo4j-fanout.json) | no-build (`infra`, applies pre-built templates) | **fan-out** — one component composes 3 per-instance mini-compositions (`cfn-deploy` + `code-deploy` + `wait-cluster-healthy`), seed-first then rolling, gated at node 1 | simple apply (no `onReplace`/`stageGsi` — no sticky CFN concerns here) | none (self-contained cluster, no shared-stack imports) | **auto** — `code-deploy` (AWS CodeDeploy) rollback is native/automatic on failure, declared once inside the capability, never scripted per node |
15
15
  | **DynamoDB table** | [`dynamodb.pilot.ts`](./dynamodb.pilot.ts) / [`dynamodb-infra.json`](../__fixtures__/dynamodb-infra.json) | no-build (`infra`, applies an existing table template) | single (one `cfn-deploy`, no fan-out) | **sticky** — `onReplace: "block"` refuses a replacing changeset (data loss guard); `stageGsi: true` stages the GSI add→backfill→remove instead of an in-place replace | none | no rollback declared — a blocked replacement is not something to compensate, it is a stop |
16
- | **ALB/ECS target** | [`alb-ecs.pilot.ts`](./alb-ecs.pilot.ts) / [`alb-ecs-service.json`](../__fixtures__/alb-ecs-service.json) | **build** — `docker-build` → `publish-image` (promote by digest at deploy time) | single (one service, one `cfn-deploy` + `ecs-update-service`) | simple apply (no replacement-sensitive resource here) | **cross-stack** — imports `shared-alb`'s `ListenerArn`/`ClusterArn`/`Subnets` via `stackOutput()`, replacing the `describe-stacks \| jq` glue in [`examples/gitlab-aws-alb-services/src/pipeline.ts`](../../../../../examples/gitlab-aws-alb-services/src/pipeline.ts) | **no** (component-declared) — `ecs-update-service`/`cfn-deploy` have no native automatic rollback for an already-running service swap, so the component supplies an explicit `rollback` phase (`rollback-previous`) rather than relying on capability compensation |
16
+ | **ALB/ECS target** | [`alb-ecs.pilot.ts`](./alb-ecs.pilot.ts) / [`alb-ecs-service.json`](../__fixtures__/alb-ecs-service.json) | **build** — `docker-build` → `publish-image` (promote by digest at deploy time) | single (one service, one `cfn-deploy` + `ecs-update-service`) | simple apply (no replacement-sensitive resource here) | **cross-stack** — imports `shared-alb`'s `ListenerArn`/`ClusterArn`/`Subnets` via `stackOutput()`, replacing the `describe-stacks \| jq` glue in [`examples/gitlab-aws-alb-services/src/pipeline.ts`](../../../../../examples/gitlab-aws-alb-services/src/pipeline.ts) | **no** — the in-run unwind calls `ecs-update-service`'s best-effort re-apply and `cfn-deploy`'s native rollback. The component declares no `rollback` phase (#2576), and an earlier release is restored with `chant components rollback` |
17
17
 
18
18
  Read together, the three cover every cell at least once: build only shows up for ALB/ECS, fan-out only for Neo4j, sticky-apply only for DynamoDB, cross-stack only for ALB/ECS, and both rollback styles (capability-native vs component-declared) appear once each.
19
19
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Pilot: ALB/ECS target (#555, epic #551).
3
3
  *
4
- * Exercises the build + cross-stack + auto-rollback axes: `docker-build` →
4
+ * Exercises the build + cross-stack axes: `docker-build` →
5
5
  * `publish-image` (promote by digest, deploy-time) → `cfn-deploy` (importing
6
6
  * the shared ALB's listener/cluster/subnets as cross-stack outputs, resolved
7
7
  * by `chant graph --stacks` rather than a `describe-stacks | jq` pipeline
@@ -16,6 +16,12 @@
16
16
  * capabilities. `service` archetype: build → publish → apply → verify, the
17
17
  * fullest of the three archetypes.
18
18
  *
19
+ * It declares no component-level `rollback` phase (#2576). It used to declare
20
+ * one holding a `rollback-previous` step with only `service` and `cluster`,
21
+ * which the aws capability turned into an `aws ecs update-service` with no
22
+ * changes. Taking an environment back to an earlier recorded release is
23
+ * `chant components rollback` (#2531).
24
+ *
19
25
  * The JSON projection of this pilot is authoritative at
20
26
  * ../__fixtures__/alb-ecs-service.json (already schema-validated by
21
27
  * component-schema.test.ts) — this module is the real typed `Component`
@@ -54,10 +60,4 @@ export const searchService: Component = {
54
60
  { kind: "health-gate", path: "/healthz" },
55
61
  ]),
56
62
  ],
57
- // No native `rollback` on ecs-update-service/cfn-deploy for an already-running
58
- // service swap — the component declares an explicit compensation phase
59
- // (auto/no-rollback axis: this is the "no automatic capability rollback,
60
- // component supplies its own" side, contrasted with code-deploy's native
61
- // automatic rollback in the Neo4j pilot).
62
- rollback: [phase("Rollback", [{ kind: "rollback-previous", service: "search", cluster: "$env.cluster" }])],
63
63
  };
@@ -4,10 +4,16 @@
4
4
  * The named-composition builder for the ALB/ECS shape
5
5
  * (`../pilots/alb-ecs.pilot.ts`): `docker-build` -> `publish-image` ->
6
6
  * `cfn-deploy` (importing a shared ALB's cross-stack outputs) ->
7
- * `ecs-update-service` -> `wait-steady-state` + `health-gate`, with the same
8
- * component-declared `rollback-previous` compensation phase the pilot uses
9
- * (no native rollback for an already-running service swap — see
10
- * ../pilots/alb-ecs.pilot.ts's docstring).
7
+ * `ecs-update-service` -> `wait-steady-state` + `health-gate`.
8
+ *
9
+ * There is no component-level `rollback` phase (#2576). The preset used to
10
+ * declare one holding a `rollback-previous` step with only `service` and
11
+ * `cluster`. The aws capability behind that kind rolls a service back to the
12
+ * `taskDefinition` it is given, and with none given it ran `aws ecs
13
+ * update-service` with no changes, so the phase restored nothing. The
14
+ * in-run unwind still calls each executed step's own rollback, and taking an
15
+ * environment back to an earlier recorded release is `chant components
16
+ * rollback <env> --component <name>` (#2531).
11
17
  *
12
18
  * This is Level 2 reuse over Level 1 raw capabilities: a component that looks
13
19
  * like this composes `EcsFargateComponent({ ... })` directly; the odd case
@@ -98,6 +104,5 @@ export function EcsFargateComponent(config: EcsFargateComponentConfig): Componen
98
104
  { kind: "health-gate", path: config.healthPath },
99
105
  ]),
100
106
  ],
101
- rollback: [phase("Rollback", [{ kind: "rollback-previous", service, cluster }])],
102
107
  };
103
108
  }