@intentius/chant 0.23.0 → 0.24.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.
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AASnD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,2CAA2C;IAC3C,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB;IAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAoB/C;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,gCAAgC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,2JAA2J;IAC3J,WAAW,CAAC,EAAE,OAAO,kBAAkB,EAAE,oBAAoB,EAAE,CAAC;CACjE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAgV9E;AAiDD;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAIlD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAItD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,YAAY,EACrB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACxC,MAAM,IAAI,CAmCZ"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AASnD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,2CAA2C;IAC3C,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB;IAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAoB/C;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,gCAAgC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,2JAA2J;IAC3J,WAAW,CAAC,EAAE,OAAO,kBAAkB,EAAE,oBAAoB,EAAE,CAAC;CACjE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAuW9E;AAiDD;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAIlD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAItD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,YAAY,EACrB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACxC,MAAM,IAAI,CAmCZ"}
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/build.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA2ElD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6EnE"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/build.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAgFlD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6EnE"}
@@ -32,6 +32,14 @@ export declare function runOpCancel(ctx: CommandContext): Promise<number>;
32
32
  * (`../../components/driver.ts`) rather than a `*.op.ts` Op. Checked first,
33
33
  * mirroring `runGraph`'s `if (ctx.args.components) return
34
34
  * runComponentGraph(ctx)` branch (../handlers/graph.ts).
35
+ *
36
+ * chant #1116 — `--report` with `--components` is checked and hard-errored
37
+ * before that dispatch. There is no preview/dry-run mode for the component
38
+ * driver: unlike the Op path (where `--report` reads a past Temporal run),
39
+ * `runOpComponents` has never read `ctx.args.report` at all, so the flag was
40
+ * silently ignored and the command fell through to a real dispatch — observed
41
+ * live reaching an actual cloud shell-out. Erroring here is the safe minimum
42
+ * called out on the issue; a real preview is future work.
35
43
  */
36
44
  export declare function runOp(ctx: CommandContext): Promise<number>;
37
45
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/run.ts"],"names":[],"mappings":"AAIA,OAAO,EAA+C,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAO7F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA4ClD,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM;;;;GAO5F;AA8BD,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAoFD,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCtE;AA0CD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CnE;AA2DD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCtE;AAID,2LAA2L;AAC3L,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BtE;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAchE;AA6ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAsF1E;AA+OD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAiFrE;AAuLD,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAMjE"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/run.ts"],"names":[],"mappings":"AAIA,OAAO,EAA+C,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAO7F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA4ClD,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM;;;;GAO5F;AA8BD,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAoFD,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCtE;AA0CD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CnE;AA2DD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCtE;AAID,2LAA2L;AAC3L,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BtE;AAmCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBhE;AA6ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAsF1E;AA+OD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAiFrE;AAuLD,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAMjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAsB9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAqNpD;AAsZD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB5E"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAsB9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA+NpD;AA0ZD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB5E"}
@@ -32,6 +32,14 @@ export declare function loadPlugins(lexiconNames: string[]): Promise<LexiconPlug
32
32
  * that never needed live module exports to begin with. This is strictly
33
33
  * better than routing the detection through the sandbox: it removes the
34
34
  * execution entirely rather than containing it, at no bundling/spawn cost.
35
+ *
36
+ * chant #1117 — walks up from `projectPath` to the project root
37
+ * (`loadChantConfigUpward`) rather than reading `projectPath` alone: `chant
38
+ * build src/<stack>` calls this with the stack's own subdirectory, and a
39
+ * project that declares `lexicons` only in its root `chant.config.ts` (never
40
+ * detectable by `detectLexicons()`'s import scan alone, e.g. a lexicon that's
41
+ * loaded but never imported by name in that particular stack's files) would
42
+ * otherwise silently miss it.
35
43
  */
36
44
  export declare function resolveProjectLexicons(projectPath: string): Promise<string[]>;
37
45
  //# sourceMappingURL=plugins.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../../src/cli/plugins.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAKjE;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA6B5E;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA6BlF;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAUnF"}
1
+ {"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../../src/cli/plugins.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAKjE;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA6B5E;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA6BlF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAUnF"}
package/dist/config.d.ts CHANGED
@@ -280,6 +280,24 @@ export declare const DEFAULT_CHANT_CONFIG: ChantConfig;
280
280
  * Returns default config if neither exists.
281
281
  */
282
282
  export declare function loadChantConfig(dir: string): Promise<ResolvedConfig>;
283
+ /**
284
+ * Load project configuration by walking up from `startDir` to the project
285
+ * root (chant #1117), instead of trying only `startDir` itself.
286
+ *
287
+ * `chant build src/<stack>` (and anything else invoked with a subdirectory —
288
+ * `lint.policies`' `evaluateProjectPolicies`, `--components --generate`)
289
+ * builds scoped to that subdirectory, but `chant.config.ts` almost always
290
+ * lives at the project root, one or more levels up. Before this, callers
291
+ * either read `startDir` alone or bolted on a single `dirname()` fallback —
292
+ * fine for a one-level-deep stack, silently blind to anything deeper
293
+ * (loomster's `src/<stack>` layout is exactly one level too deep: `buildParams`'
294
+ * declared `env:` mappings never resolved, so `LOOM_TIER`/`LOOM_ENV` were inert
295
+ * under every `npm run synth:*` for two releases — loomster#162). Uses the
296
+ * same walk `chant lint`/`chant graph` already used ({@link findProjectConfig},
297
+ * shared with `./lint/config.ts`'s `findProjectRoot`) — one config-discovery
298
+ * contract for the whole CLI.
299
+ */
300
+ export declare function loadChantConfigUpward(startDir: string): Promise<ResolvedConfig>;
283
301
  /**
284
302
  * Resolve the ownership marker to stamp from project config, or undefined when
285
303
  * ownership marking is off (no `stack`, or `enabled: false`).
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import type { Severity, RuleConfig } from "./rule.js";
3
+ export { findProjectRoot } from "../project-root.js";
3
4
  export declare const LintConfigSchema: z.ZodObject<{
4
5
  rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<{
5
6
  error: "error";
@@ -75,18 +76,6 @@ export declare function parseRuleConfig(value: RuleConfig): ParsedRuleConfig;
75
76
  * Default configuration with all rules enabled at strict preset severities
76
77
  */
77
78
  export declare const DEFAULT_CONFIG: LintConfig;
78
- /**
79
- * Walk up from `startDir` to the nearest ancestor holding a chant project
80
- * config (`chant.config.ts` or `chant.config.json`). Returns that directory, or
81
- * `startDir` unchanged when none is found before the filesystem root.
82
- *
83
- * Linting a subpath (`chant graph src --format ir`, `chant lint src/lib`) must
84
- * still see the project-root config: its `lint.overrides` globs are written
85
- * project-root-relative (`src/lib/**`), and a rule set scoped only to the lint
86
- * arg would silently drop them. Config discovery therefore anchors on the
87
- * project root, not the path being linted.
88
- */
89
- export declare function findProjectRoot(startDir: string): string;
90
79
  /**
91
80
  * Load lint configuration from a directory.
92
81
  *
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lint/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAmBnD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAS3B,CAAC;AAkGH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,wCAAwC;IACxC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACnC,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,gDAAgD;IAChD,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,gBAAgB,CAsBnE;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,UAG5B,CAAC;AA0HF;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUxD;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAoClD;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAkBpG"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lint/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AASnD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAiBlD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAS3B,CAAC;AAkGH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,wCAAwC;IACxC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACnC,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,gDAAgD;IAChD,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,gBAAgB,CAsBnE;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,UAG5B,CAAC;AA0HF;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAoClD;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAkBpG"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Shared upward config-discovery walk (chant #1117).
3
+ *
4
+ * Before this, `chant build <subdir>` and `lint.policies`
5
+ * (`./lint/policy.ts`'s `evaluateProjectPolicies`) each searched only the
6
+ * build directory and its immediate parent for `chant.config.ts`/`.json`,
7
+ * while `chant lint`/`chant graph` (`./lint/config.ts`'s old, file-local
8
+ * `findProjectRoot`) already walked all the way up. A project with a deeper
9
+ * `src/<stack>` layout — `chant build src/<stack>` two or more levels below
10
+ * the project root — silently never found the root config: `buildParams`'
11
+ * declared `env:` mappings went inert, `ownership`/`lint.policies`/etc quietly
12
+ * fell back to defaults, and nothing warned (loomster#162: `LOOM_TIER`/
13
+ * `LOOM_ENV` inert under every `npm run synth:*` for two releases).
14
+ *
15
+ * `findProjectConfig` is the one walk every config-discovery call site now
16
+ * shares. It stops at the first of:
17
+ *
18
+ * 1. A directory holding `chant.config.ts` or `chant.config.json` — found.
19
+ * 2. A directory holding `.git` or `package.json` with no chant config of its
20
+ * own — the project boundary. Discovery must never wander past the actual
21
+ * project into an unrelated ancestor directory just because this project
22
+ * happens not to declare a config (a stray `chant.config.ts` two levels
23
+ * above an unrelated git repo must never be picked up).
24
+ * 3. `startDir` itself, unchanged, if the walk reaches the filesystem root
25
+ * without ever finding a config OR a boundary marker. This is not just a
26
+ * "give up gracefully" nicety — several callers (`resolveProjectLexicons`
27
+ * -> `findInfraFiles`) scope a real directory walk off this function's
28
+ * result; if a rootless/marker-less start dir (a bare tmpdir, as chant's
29
+ * own test suites use) resolved all the way to `/`, that downstream walk
30
+ * would scan the entire filesystem instead of failing fast. Falling back
31
+ * to `startDir` keeps every caller's blast radius local no matter how far
32
+ * up the walk had to look.
33
+ */
34
+ export interface ProjectConfigSearch {
35
+ /** The resolved project root: the config's directory, the boundary directory, or the (resolved) `startDir` when neither was found. */
36
+ dir: string;
37
+ /** Absolute path to the discovered `chant.config.ts`/`.json`, if any. */
38
+ configPath?: string;
39
+ }
40
+ /** Walk up from `startDir` (inclusive) to the nearest chant config or project boundary. See {@link ProjectConfigSearch}. */
41
+ export declare function findProjectConfig(startDir: string): ProjectConfigSearch;
42
+ /**
43
+ * Walk up from `startDir` to the nearest ancestor holding a chant project
44
+ * config (`chant.config.ts` or `chant.config.json`), the `.git`/`package.json`
45
+ * project boundary, or `startDir` itself when neither is found. Thin wrapper
46
+ * over {@link findProjectConfig} for callers that only need the directory
47
+ * (e.g. `chant lint`'s rule/plugin resolution, which just needs *a* stable
48
+ * root to resolve relative paths against).
49
+ */
50
+ export declare function findProjectRoot(startDir: string): string;
51
+ //# sourceMappingURL=project-root.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-root.d.ts","sourceRoot":"","sources":["../src/project-root.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,WAAW,mBAAmB;IAClC,sIAAsI;IACtI,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4HAA4H;AAC5H,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAqBvE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -1,5 +1,5 @@
1
1
  import { build } from "../../build";
2
- import { loadChantConfig, resolveOwnershipMarker, resolveFoldEnabled, resolveSandboxEnabled } from "../../config";
2
+ import { loadChantConfigUpward, resolveOwnershipMarker, resolveFoldEnabled, resolveSandboxEnabled } from "../../config";
3
3
  import { resolveCliBuildParams } from "../build-params-cli";
4
4
  import type { Serializer, SerializerResult } from "../../serializer";
5
5
  import type { LexiconPlugin } from "../../lexicon";
@@ -128,11 +128,14 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
128
128
  // Resolve the path
129
129
  const infraPath = resolve(options.path);
130
130
 
131
- // Resolve opt-in ownership marking from project config (search the infra dir
132
- // and its parent, where chant.config.* usually lives).
133
- const loaded = await loadChantConfig(infraPath).then((r) =>
134
- r.configPath ? r : loadChantConfig(dirname(infraPath)),
135
- );
131
+ // Resolve opt-in ownership marking from project config. chant #1117 walks
132
+ // up from the infra dir to the project root (`loadChantConfigUpward`), not
133
+ // just the infra dir's immediate parent: a project whose stacks live two or
134
+ // more levels below `chant.config.ts` (loomster's `src/<stack>` layout)
135
+ // otherwise never finds the root config at all, and every declared
136
+ // `buildParams`/`ownership`/`lint.policies` setting silently falls back to
137
+ // its default.
138
+ const loaded = await loadChantConfigUpward(infraPath);
136
139
  const config = loaded.config;
137
140
  const ownership = resolveOwnershipMarker(config);
138
141
 
@@ -174,6 +177,26 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
174
177
  return { success: false, resourceCount: 0, fileCount: 0, errors, warnings };
175
178
  }
176
179
 
180
+ // chant #1117 — a project that declares buildParams but resolves NONE of
181
+ // them for this build is the exact shape that let loomster#162 live for two
182
+ // releases: the discovered config wasn't the one the project author
183
+ // expected (a stale --path, a workspace boundary that stopped the walk
184
+ // short), or every declared parameter's `env:` var went unset, and either
185
+ // way every `params.<name>` read silently falls back to `undefined` — with
186
+ // no error (an all-`required: false` declaration resolves successfully to
187
+ // an empty set). Warn, naming the config path this build actually
188
+ // discovered, so a mismatch is visible instead of silent.
189
+ if (
190
+ Object.keys(config.buildParams ?? {}).length > 0 &&
191
+ paramsResolution.provenance.length === 0
192
+ ) {
193
+ warnings.push(
194
+ formatWarning({
195
+ message: `chant.config.ts declares buildParams${loaded.configPath ? ` (${loaded.configPath})` : ""}, but none resolved for this build — every params.<name> read will be undefined. Pass --param/--params-file, or check that this is the config you expect.`,
196
+ }),
197
+ );
198
+ }
199
+
177
200
  // #1039 — thread each loaded plugin's registered intrinsics (e.g. AWS's
178
201
  // `Sub`) through to the fold path, so a file using a registered intrinsic
179
202
  // tagged template folds instead of unconditionally falling back to run.
@@ -8,8 +8,10 @@
8
8
  * run --components` (see ../handlers/run.test.ts's "build-time parameters"
9
9
  * describe block for the equivalent local/`--temporal` coverage).
10
10
  *
11
- * Mocks `generateComponentsPipeline` and `loadChantConfig` and exercises the
12
- * public `runBuild` dispatcher (`runGenerateComponents` itself isn't
11
+ * Mocks `generateComponentsPipeline` and `loadChantConfigUpward` (chant
12
+ * #1117 `runGenerateComponents` walks up to the project root now, same as
13
+ * `chant build` proper, instead of reading `args.path` alone) and exercises
14
+ * the public `runBuild` dispatcher (`runGenerateComponents` itself isn't
13
15
  * exported), mirroring `run.test.ts`'s style of driving the handler through
14
16
  * its `CommandContext` entrypoint rather than reaching into private helpers.
15
17
  */
@@ -17,14 +19,14 @@ import { describe, test, expect, vi, beforeEach } from "vitest";
17
19
  import type { ParsedArgs } from "../registry";
18
20
 
19
21
  const generateComponentsPipelineMock = vi.fn();
20
- const loadChantConfigMock = vi.fn();
22
+ const loadChantConfigUpwardMock = vi.fn();
21
23
 
22
24
  vi.mock("../../components/cli-support", () => ({
23
25
  generateComponentsPipeline: (...args: unknown[]) => generateComponentsPipelineMock(...args),
24
26
  }));
25
27
  vi.mock("../../config", async () => {
26
28
  const actual = await vi.importActual<typeof import("../../config")>("../../config");
27
- return { ...actual, loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args) };
29
+ return { ...actual, loadChantConfigUpward: (...args: unknown[]) => loadChantConfigUpwardMock(...args) };
28
30
  });
29
31
 
30
32
  const { runBuild } = await import("./build");
@@ -54,7 +56,7 @@ function makeStderrSpy() {
54
56
  describe("runBuild --components --generate (chant #1108 build-time parameters)", () => {
55
57
  beforeEach(() => {
56
58
  generateComponentsPipelineMock.mockReset();
57
- loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
59
+ loadChantConfigUpwardMock.mockReset().mockResolvedValue({ config: {} });
58
60
  });
59
61
 
60
62
  test("no declared buildParams → generateComponentsPipeline is called with an empty provenance array", async () => {
@@ -69,7 +71,7 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
69
71
  });
70
72
 
71
73
  test("chant.config.ts's declared buildParams resolve, log, and are forwarded to generateComponentsPipeline", async () => {
72
- loadChantConfigMock.mockResolvedValue({
74
+ loadChantConfigUpwardMock.mockResolvedValue({
73
75
  config: { buildParams: { tier: { type: "string", default: "light" } } },
74
76
  });
75
77
  generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
@@ -91,7 +93,7 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
91
93
  });
92
94
 
93
95
  test("--param overrides a declared default", async () => {
94
- loadChantConfigMock.mockResolvedValue({
96
+ loadChantConfigUpwardMock.mockResolvedValue({
95
97
  config: { buildParams: { tier: { type: "string", default: "light" } } },
96
98
  });
97
99
  generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
@@ -115,7 +117,7 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
115
117
  });
116
118
 
117
119
  test("an unresolved required build-time parameter → exit 1, never reaches generateComponentsPipeline", async () => {
118
- loadChantConfigMock.mockResolvedValue({
120
+ loadChantConfigUpwardMock.mockResolvedValue({
119
121
  config: { buildParams: { tier: { type: "string" } } },
120
122
  });
121
123
  const stderr = makeStderrSpy();
@@ -128,7 +130,7 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
128
130
  });
129
131
 
130
132
  test("an enum violation on --param → exit 1, never reaches generateComponentsPipeline", async () => {
131
- loadChantConfigMock.mockResolvedValue({
133
+ loadChantConfigUpwardMock.mockResolvedValue({
132
134
  config: { buildParams: { tier: { type: "string", enum: ["light", "production"] } } },
133
135
  });
134
136
  const stderr = makeStderrSpy();
@@ -4,7 +4,7 @@ import { buildCommand, buildCommandWatch, printErrors, printWarnings, resolveBui
4
4
  import { formatError, formatInfo, formatSuccess, formatBold } from "../format";
5
5
  import type { CommandContext } from "../registry";
6
6
  import { generateComponentsPipeline } from "../../components/cli-support";
7
- import { loadChantConfig, type ChantConfig } from "../../config";
7
+ import { loadChantConfigUpward, type ChantConfig } from "../../config";
8
8
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
9
9
 
10
10
  /**
@@ -23,12 +23,17 @@ import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
23
23
  * Before this, `params.*` (`@intentius/chant/params`) was always `{}` under
24
24
  * this command too — generate mode shares `discoverComponents` with `chant
25
25
  * run --components`, so it had the identical gap.
26
+ *
27
+ * chant #1117 — loads config by walking up from `args.path` to the project
28
+ * root (`loadChantConfigUpward`), same as `chant build` proper, instead of
29
+ * `args.path` alone: a components-only project built from a subdirectory
30
+ * otherwise never sees the root `chant.config.ts`'s `buildParams` either.
26
31
  */
27
32
  async function runGenerateComponents(ctx: CommandContext): Promise<number> {
28
33
  const { args } = ctx;
29
34
  const lexicon = args.generate as string;
30
35
 
31
- const { config } = await loadChantConfig(resolve(args.path)).catch(() => ({ config: {} as ChantConfig }));
36
+ const { config } = await loadChantConfigUpward(resolve(args.path)).catch(() => ({ config: {} as ChantConfig }));
32
37
  const paramsResolution = resolveCliBuildParams(config.buildParams, {
33
38
  cli: parseParamFlags(args.param),
34
39
  paramsFile: args.paramsFile,
@@ -882,6 +882,37 @@ describe("runOp dispatcher: --components routes to runOpComponents", () => {
882
882
  expect(discoverOpsMock).not.toHaveBeenCalled();
883
883
  vi.restoreAllMocks();
884
884
  });
885
+
886
+ // chant #1116 — --report is Op/Temporal-only (reads a past workflow run);
887
+ // the component driver never checked it, so it was silently ignored and the
888
+ // command fell through to a real dispatch. Hard-error instead, before
889
+ // runComponents is ever reached.
890
+ test("--report combined with --components → exit 1 before any dispatch, no fall-through (#1116)", async () => {
891
+ discoverOpsMock.mockReset();
892
+ const stderr = makeStderrSpy();
893
+
894
+ const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: true, temporal: false }), plugins: [], serializers: [] });
895
+
896
+ expect(exit).toBe(1);
897
+ expect(stderr.join("\n")).toContain("not supported with --components");
898
+ expect(stderr.join("\n")).toContain("#1116");
899
+ expect(discoverOpsMock).not.toHaveBeenCalled();
900
+ expect(runComponentsMock).not.toHaveBeenCalled();
901
+ });
902
+
903
+ // Plain --components (no --report) must be unaffected: it still reaches a
904
+ // real dispatch through runComponents — mocked here, never a real cloud call.
905
+ test("plain --components (no --report) still dispatches to runComponents (#1116 regression guard)", async () => {
906
+ discoverOpsMock.mockReset();
907
+ runComponentsMock.mockResolvedValue({ success: true, selected: ["svc"], run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true } });
908
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
909
+
910
+ const exit = await runOp({ args: makeArgs({ path: "svc", components: true, report: false, temporal: false }), plugins: [], serializers: [] });
911
+
912
+ expect(exit).toBe(0);
913
+ expect(runComponentsMock).toHaveBeenCalled();
914
+ vi.restoreAllMocks();
915
+ });
885
916
  });
886
917
 
887
918
  describe("runOpComponents", () => {
@@ -548,8 +548,23 @@ function renderProgress(opName: string, history: WorkflowHistoryRaw): void {
548
548
  * (`../../components/driver.ts`) rather than a `*.op.ts` Op. Checked first,
549
549
  * mirroring `runGraph`'s `if (ctx.args.components) return
550
550
  * runComponentGraph(ctx)` branch (../handlers/graph.ts).
551
+ *
552
+ * chant #1116 — `--report` with `--components` is checked and hard-errored
553
+ * before that dispatch. There is no preview/dry-run mode for the component
554
+ * driver: unlike the Op path (where `--report` reads a past Temporal run),
555
+ * `runOpComponents` has never read `ctx.args.report` at all, so the flag was
556
+ * silently ignored and the command fell through to a real dispatch — observed
557
+ * live reaching an actual cloud shell-out. Erroring here is the safe minimum
558
+ * called out on the issue; a real preview is future work.
551
559
  */
552
560
  export async function runOp(ctx: CommandContext): Promise<number> {
561
+ if (ctx.args.components && ctx.args.report) {
562
+ console.error(formatError({
563
+ message: "--report is not supported with --components",
564
+ hint: "No preview/dry-run mode exists yet for the component driver (see chant#1116). Omit --report.",
565
+ }));
566
+ return 1;
567
+ }
553
568
  if (ctx.args.components) return runOpComponents(ctx);
554
569
  if (ctx.args.local && ctx.args.temporal) {
555
570
  console.error(formatError({
@@ -270,6 +270,29 @@ describe("parseArgs", () => {
270
270
  const result = parseArgs(["build", "src", "--params-file", "./params.json"]);
271
271
  expect(result.paramsFile).toBe("./params.json");
272
272
  });
273
+
274
+ // ── --param=name=value hard error (chant #1118) ──────────────────────────
275
+ // The joined `--flag=value` form is not supported anywhere in this parser
276
+ // (see "ignores unknown flags" above) — a dropped --param can silently
277
+ // change what a build measures/deploys, so this form is rejected loudly
278
+ // instead of silently accepted as a no-op.
279
+
280
+ test("--param=name=value throws instead of silently dropping", () => {
281
+ expect(() => parseArgs(["build", "src", "--param=tier=production"])).toThrow(/--param=tier=production/);
282
+ });
283
+
284
+ test("--param=name=value error names the working form", () => {
285
+ expect(() => parseArgs(["build", "src", "--param=tier=production"])).toThrow(/--param name=value/);
286
+ });
287
+
288
+ test("--param= (empty value) also throws", () => {
289
+ expect(() => parseArgs(["build", "src", "--param="])).toThrow(/--param=/);
290
+ });
291
+
292
+ test("plain --param name=value is unaffected", () => {
293
+ const result = parseArgs(["build", "src", "--param", "tier=production"]);
294
+ expect(result.param).toEqual(["tier=production"]);
295
+ });
273
296
  });
274
297
 
275
298
  // ── resolveCommand tests ──────────────────────────────────────────
package/src/cli/main.ts CHANGED
@@ -5,7 +5,7 @@ import { isEntryPoint } from "./is-entry-point";
5
5
  import { formatSuccess, formatError } from "./format";
6
6
  import { loadPlugins, resolveProjectLexicons } from "./plugins";
7
7
  import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
8
- import { loadChantConfig } from "../config";
8
+ import { loadChantConfigUpward } from "../config";
9
9
  import { ENV_VAR, unknownEnvError } from "../env";
10
10
  import { initRuntime } from "../runtime-adapter";
11
11
  import { runBuild } from "./handlers/build";
@@ -224,6 +224,16 @@ export function parseArgs(args: string[]): ParsedArgs {
224
224
  result.sandbox = true;
225
225
  } else if (arg === "--param") {
226
226
  (result.param ??= []).push(args[++i]);
227
+ } else if (arg.startsWith("--param=")) {
228
+ // chant #1118 — this parser never supports an `--flag=value` joined
229
+ // form for any value-taking flag (every branch above is an exact `===`
230
+ // match, so a joined token falls through unrecognized and is silently
231
+ // dropped — see the "ignores unknown flags" case below). `--param
232
+ // name=value` (space-separated) is the only accepted form. Rather than
233
+ // teach the parser joined forms generally, `--param=name=value` is
234
+ // called out as a hard error instead of a silent no-op: a dropped
235
+ // `--param` can silently change what a build measures/deploys.
236
+ throw new Error(`${arg} is not supported. Use --param name=value (space-separated) instead.`);
227
237
  } else if (arg === "--params-file") {
228
238
  result.paramsFile = args[++i];
229
239
  } else if (!arg.startsWith("-")) {
@@ -588,11 +598,15 @@ async function main(): Promise<void> {
588
598
  // chant.config itself may branch on the env. (#505)
589
599
  if (args.env) process.env[ENV_VAR] = args.env;
590
600
 
591
- // Initialize runtime adapter early — before plugins or commands run
601
+ // Initialize runtime adapter early — before plugins or commands run.
602
+ // chant #1117 — walks up from `args.path` to the project root: for a
603
+ // subdirectory build/command (`chant build src/<stack> --env prod`) the
604
+ // declared `environments` almost always live in the root `chant.config.ts`,
605
+ // not `args.path` itself.
592
606
  const projectPath0 = resolve(args.path === "." ? "." : args.path);
593
607
  let loadedConfig;
594
608
  try {
595
- loadedConfig = await loadChantConfig(projectPath0);
609
+ loadedConfig = await loadChantConfigUpward(projectPath0);
596
610
  initRuntime();
597
611
  } catch {
598
612
  // Config may not exist yet (e.g. `chant init`)
@@ -1,5 +1,5 @@
1
1
  import { isLexiconPlugin, type LexiconPlugin } from "../lexicon";
2
- import { loadChantConfig } from "../config";
2
+ import { loadChantConfigUpward } from "../config";
3
3
  import { findInfraFiles, detectLexicons } from "../index";
4
4
  import { checkConflicts } from "./conflict-check";
5
5
 
@@ -96,9 +96,17 @@ export async function loadPlugins(lexiconNames: string[]): Promise<LexiconPlugin
96
96
  * that never needed live module exports to begin with. This is strictly
97
97
  * better than routing the detection through the sandbox: it removes the
98
98
  * execution entirely rather than containing it, at no bundling/spawn cost.
99
+ *
100
+ * chant #1117 — walks up from `projectPath` to the project root
101
+ * (`loadChantConfigUpward`) rather than reading `projectPath` alone: `chant
102
+ * build src/<stack>` calls this with the stack's own subdirectory, and a
103
+ * project that declares `lexicons` only in its root `chant.config.ts` (never
104
+ * detectable by `detectLexicons()`'s import scan alone, e.g. a lexicon that's
105
+ * loaded but never imported by name in that particular stack's files) would
106
+ * otherwise silently miss it.
99
107
  */
100
108
  export async function resolveProjectLexicons(projectPath: string): Promise<string[]> {
101
- const { config } = await loadChantConfig(projectPath);
109
+ const { config } = await loadChantConfigUpward(projectPath);
102
110
 
103
111
  if (config.lexicons && config.lexicons.length > 0) {
104
112
  return config.lexicons;
package/src/config.ts CHANGED
@@ -7,6 +7,7 @@ import { DEFAULT_SBOM_FORMAT, type SbomFormat } from "./components/verbs/sbom-ge
7
7
  import type { Severity } from "./components/verbs/vuln-scan";
8
8
  import type { VulnPolicy } from "./components/verbs/vuln-gate";
9
9
  import type { BuildParamsConfig } from "./build-params";
10
+ import { findProjectConfig } from "./project-root";
10
11
 
11
12
  /**
12
13
  * Zod schema for ChantConfig validation.
@@ -297,6 +298,28 @@ export async function loadChantConfig(dir: string): Promise<ResolvedConfig> {
297
298
  return { config: DEFAULT_CHANT_CONFIG };
298
299
  }
299
300
 
301
+ /**
302
+ * Load project configuration by walking up from `startDir` to the project
303
+ * root (chant #1117), instead of trying only `startDir` itself.
304
+ *
305
+ * `chant build src/<stack>` (and anything else invoked with a subdirectory —
306
+ * `lint.policies`' `evaluateProjectPolicies`, `--components --generate`)
307
+ * builds scoped to that subdirectory, but `chant.config.ts` almost always
308
+ * lives at the project root, one or more levels up. Before this, callers
309
+ * either read `startDir` alone or bolted on a single `dirname()` fallback —
310
+ * fine for a one-level-deep stack, silently blind to anything deeper
311
+ * (loomster's `src/<stack>` layout is exactly one level too deep: `buildParams`'
312
+ * declared `env:` mappings never resolved, so `LOOM_TIER`/`LOOM_ENV` were inert
313
+ * under every `npm run synth:*` for two releases — loomster#162). Uses the
314
+ * same walk `chant lint`/`chant graph` already used ({@link findProjectConfig},
315
+ * shared with `./lint/config.ts`'s `findProjectRoot`) — one config-discovery
316
+ * contract for the whole CLI.
317
+ */
318
+ export async function loadChantConfigUpward(startDir: string): Promise<ResolvedConfig> {
319
+ const { dir } = findProjectConfig(startDir);
320
+ return loadChantConfig(dir);
321
+ }
322
+
300
323
  /**
301
324
  * Resolve the ownership marker to stamp from project config, or undefined when
302
325
  * ownership marking is off (no `stack`, or `enabled: false`).
@@ -1,7 +1,7 @@
1
1
  import { describe, test, expect, beforeEach, afterEach } from "vitest";
2
2
  import { loadConfig, DEFAULT_CONFIG, findProjectRoot } from "./config";
3
3
  import { writeFileSync, mkdirSync, rmSync } from "fs";
4
- import { join } from "path";
4
+ import { join, resolve } from "path";
5
5
 
6
6
  const TEST_DIR = join(import.meta.dirname, "__test_config__");
7
7
 
@@ -705,10 +705,15 @@ describe("findProjectRoot", () => {
705
705
  expect(findProjectRoot(sub)).toBe(TEST_DIR);
706
706
  });
707
707
 
708
- test("falls back to the start dir when no config is found", () => {
708
+ test("stops at the nearest .git/package.json boundary when no config is found (#1117)", () => {
709
709
  const sub = join(TEST_DIR, "nowhere");
710
710
  mkdirSync(sub, { recursive: true });
711
- // No chant.config anywhere under TEST_DIR — returns the (resolved) start dir.
712
- expect(findProjectRoot(sub)).toBe(sub);
711
+ // No chant.config anywhere under TEST_DIR — walking up from this real
712
+ // repo location reaches `packages/core`'s own package.json before the
713
+ // filesystem root, so that's the returned boundary, not `sub` itself
714
+ // (chant #1117 — discovery must never wander past the project just
715
+ // because it declares no config).
716
+ const packageRoot = resolve(import.meta.dirname, "..", "..");
717
+ expect(findProjectRoot(sub)).toBe(packageRoot);
713
718
  });
714
719
  });
@@ -6,6 +6,13 @@ import type { Severity, RuleConfig } from "./rule";
6
6
  import { moduleDir, getRuntime } from "../runtime-adapter";
7
7
  import strictPreset from "./presets/strict.json";
8
8
 
9
+ // chant #1117 — the upward config-discovery walk moved to a shared module
10
+ // (`../project-root`) so `chant build`/`lint.policies` use the identical walk
11
+ // `chant lint`/`chant graph` already did. Re-exported here since this is
12
+ // still where every existing call site (`./config.test.ts`, `../cli/commands/lint.ts`)
13
+ // imports it from.
14
+ export { findProjectRoot } from "../project-root";
15
+
9
16
  /** Mapping of built-in preset names to their file paths */
10
17
  const BUILTIN_PRESETS: Record<string, string> = {
11
18
  "@intentius/chant/lint/presets/strict": resolve(moduleDir(import.meta.url), "presets/strict.json"),
@@ -323,29 +330,6 @@ function loadConfigFile(configPath: string, visited: Set<string> = new Set()): L
323
330
  return mergedConfig;
324
331
  }
325
332
 
326
- /**
327
- * Walk up from `startDir` to the nearest ancestor holding a chant project
328
- * config (`chant.config.ts` or `chant.config.json`). Returns that directory, or
329
- * `startDir` unchanged when none is found before the filesystem root.
330
- *
331
- * Linting a subpath (`chant graph src --format ir`, `chant lint src/lib`) must
332
- * still see the project-root config: its `lint.overrides` globs are written
333
- * project-root-relative (`src/lib/**`), and a rule set scoped only to the lint
334
- * arg would silently drop them. Config discovery therefore anchors on the
335
- * project root, not the path being linted.
336
- */
337
- export function findProjectRoot(startDir: string): string {
338
- let dir = resolve(startDir);
339
- for (;;) {
340
- if (existsSync(join(dir, "chant.config.ts")) || existsSync(join(dir, "chant.config.json"))) {
341
- return dir;
342
- }
343
- const parent = dirname(dir);
344
- if (parent === dir) return resolve(startDir);
345
- dir = parent;
346
- }
347
- }
348
-
349
333
  /**
350
334
  * Load lint configuration from a directory.
351
335
  *
@@ -4,7 +4,7 @@
4
4
  * `policyGate` Op step runs this to gate an apply on the same checks.
5
5
  */
6
6
  import { resolve, dirname } from "node:path";
7
- import { loadChantConfig } from "../config";
7
+ import { loadChantConfigUpward } from "../config";
8
8
  import { resolveProjectLexicons, loadPlugins } from "../cli/plugins";
9
9
  import { build } from "../build";
10
10
  import { runPostSynthChecks, isPostSynthCheck } from "./post-synth";
@@ -55,10 +55,10 @@ export async function evaluateProjectPolicies(opts: {
55
55
  const plugins = await loadPlugins(lexiconNames);
56
56
  const serializers = plugins.map((p) => p.serializer);
57
57
 
58
- // Config can live in the build dir or its parent (the project root).
59
- const loaded = await loadChantConfig(buildPath).then((r) =>
60
- r.configPath ? r : loadChantConfig(dirname(buildPath)),
61
- );
58
+ // chant #1117 walks up from the build dir to the project root, same as
59
+ // `chant build` (`../cli/commands/build.ts`'s `loadChantConfigUpward`), not
60
+ // just the build dir's immediate parent.
61
+ const loaded = await loadChantConfigUpward(buildPath);
62
62
  const config = loaded.config;
63
63
  const configDir = loaded.configPath ? dirname(loaded.configPath) : buildPath;
64
64
  const env = opts.env ?? config.ownership?.env;
@@ -0,0 +1,105 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { findProjectConfig, findProjectRoot } from "./project-root";
6
+
7
+ // Every test builds its own isolated directory tree under a fresh tmpdir —
8
+ // never reused across tests, and never anywhere near the real repo's own
9
+ // .git/package.json — so the boundary-stop behavior is exercised
10
+ // deterministically instead of depending on where this file happens to sit
11
+ // in the real chant checkout.
12
+ let root: string;
13
+
14
+ beforeEach(() => {
15
+ root = mkdtempSync(join(tmpdir(), "chant-project-root-"));
16
+ });
17
+
18
+ afterEach(() => {
19
+ rmSync(root, { recursive: true, force: true });
20
+ });
21
+
22
+ describe("findProjectConfig / findProjectRoot (chant #1117)", () => {
23
+ test("found at root — chant.config.ts in the start dir itself", () => {
24
+ writeFileSync(join(root, "chant.config.ts"), "export default {};");
25
+
26
+ const result = findProjectConfig(root);
27
+
28
+ expect(result.dir).toBe(root);
29
+ expect(result.configPath).toBe(join(root, "chant.config.ts"));
30
+ expect(findProjectRoot(root)).toBe(root);
31
+ });
32
+
33
+ test("found at an intermediate ancestor — several levels below the config", () => {
34
+ writeFileSync(join(root, "chant.config.json"), "{}");
35
+ const stackDir = join(root, "src", "stacks", "shared-foundation");
36
+ mkdirSync(stackDir, { recursive: true });
37
+
38
+ const result = findProjectConfig(stackDir);
39
+
40
+ expect(result.dir).toBe(root);
41
+ expect(result.configPath).toBe(join(root, "chant.config.json"));
42
+ expect(findProjectRoot(stackDir)).toBe(root);
43
+ });
44
+
45
+ test("not found — no chant.config, no .git, no package.json anywhere above start dir: falls back to start dir, not the filesystem root", () => {
46
+ const deep = join(root, "a", "b", "c");
47
+ mkdirSync(deep, { recursive: true });
48
+
49
+ const result = findProjectConfig(deep);
50
+
51
+ // Nothing between `deep` and the real filesystem root declares a chant
52
+ // config or a boundary marker (this mkdtemp tree carries none). Rather
53
+ // than adopting "/" as the project root — which would make a downstream
54
+ // caller that scopes a directory walk off this result (e.g.
55
+ // `resolveProjectLexicons` -> `findInfraFiles`) scan the entire disk —
56
+ // the walk gives up and returns `deep` itself unchanged.
57
+ expect(result.configPath).toBeUndefined();
58
+ expect(result.dir).toBe(deep);
59
+ expect(findProjectRoot(deep)).toBe(deep);
60
+ });
61
+
62
+ test("boundary stop — a .git/package.json ancestor halts the walk before an outer chant.config.ts", () => {
63
+ // An unrelated chant.config.ts two levels above this project's own git
64
+ // root must never be picked up — the boundary marker (.git here) stops
65
+ // the walk at the project's own root first.
66
+ writeFileSync(join(root, "chant.config.ts"), "export default { unrelated: true };");
67
+ const projectRoot = join(root, "project");
68
+ mkdirSync(join(projectRoot, ".git"), { recursive: true });
69
+ const stackDir = join(projectRoot, "src", "stack");
70
+ mkdirSync(stackDir, { recursive: true });
71
+
72
+ const result = findProjectConfig(stackDir);
73
+
74
+ expect(result.dir).toBe(projectRoot);
75
+ expect(result.configPath).toBeUndefined();
76
+ expect(findProjectRoot(stackDir)).toBe(projectRoot);
77
+ });
78
+
79
+ test("boundary stop — package.json (no .git) halts the walk the same way", () => {
80
+ writeFileSync(join(root, "chant.config.json"), "{}");
81
+ const projectRoot = join(root, "project");
82
+ mkdirSync(projectRoot, { recursive: true });
83
+ writeFileSync(join(projectRoot, "package.json"), "{}");
84
+ const stackDir = join(projectRoot, "src", "stack");
85
+ mkdirSync(stackDir, { recursive: true });
86
+
87
+ const result = findProjectConfig(stackDir);
88
+
89
+ expect(result.dir).toBe(projectRoot);
90
+ expect(result.configPath).toBeUndefined();
91
+ });
92
+
93
+ test("a config living exactly at the boundary dir is still found — boundary check never wins over a real config", () => {
94
+ const projectRoot = join(root, "project");
95
+ mkdirSync(join(projectRoot, ".git"), { recursive: true });
96
+ writeFileSync(join(projectRoot, "chant.config.ts"), "export default {};");
97
+ const stackDir = join(projectRoot, "src", "stack");
98
+ mkdirSync(stackDir, { recursive: true });
99
+
100
+ const result = findProjectConfig(stackDir);
101
+
102
+ expect(result.dir).toBe(projectRoot);
103
+ expect(result.configPath).toBe(join(projectRoot, "chant.config.ts"));
104
+ });
105
+ });
@@ -0,0 +1,78 @@
1
+ import { existsSync } from "fs";
2
+ import { dirname, join, resolve } from "path";
3
+
4
+ /**
5
+ * Shared upward config-discovery walk (chant #1117).
6
+ *
7
+ * Before this, `chant build <subdir>` and `lint.policies`
8
+ * (`./lint/policy.ts`'s `evaluateProjectPolicies`) each searched only the
9
+ * build directory and its immediate parent for `chant.config.ts`/`.json`,
10
+ * while `chant lint`/`chant graph` (`./lint/config.ts`'s old, file-local
11
+ * `findProjectRoot`) already walked all the way up. A project with a deeper
12
+ * `src/<stack>` layout — `chant build src/<stack>` two or more levels below
13
+ * the project root — silently never found the root config: `buildParams`'
14
+ * declared `env:` mappings went inert, `ownership`/`lint.policies`/etc quietly
15
+ * fell back to defaults, and nothing warned (loomster#162: `LOOM_TIER`/
16
+ * `LOOM_ENV` inert under every `npm run synth:*` for two releases).
17
+ *
18
+ * `findProjectConfig` is the one walk every config-discovery call site now
19
+ * shares. It stops at the first of:
20
+ *
21
+ * 1. A directory holding `chant.config.ts` or `chant.config.json` — found.
22
+ * 2. A directory holding `.git` or `package.json` with no chant config of its
23
+ * own — the project boundary. Discovery must never wander past the actual
24
+ * project into an unrelated ancestor directory just because this project
25
+ * happens not to declare a config (a stray `chant.config.ts` two levels
26
+ * above an unrelated git repo must never be picked up).
27
+ * 3. `startDir` itself, unchanged, if the walk reaches the filesystem root
28
+ * without ever finding a config OR a boundary marker. This is not just a
29
+ * "give up gracefully" nicety — several callers (`resolveProjectLexicons`
30
+ * -> `findInfraFiles`) scope a real directory walk off this function's
31
+ * result; if a rootless/marker-less start dir (a bare tmpdir, as chant's
32
+ * own test suites use) resolved all the way to `/`, that downstream walk
33
+ * would scan the entire filesystem instead of failing fast. Falling back
34
+ * to `startDir` keeps every caller's blast radius local no matter how far
35
+ * up the walk had to look.
36
+ */
37
+ export interface ProjectConfigSearch {
38
+ /** The resolved project root: the config's directory, the boundary directory, or the (resolved) `startDir` when neither was found. */
39
+ dir: string;
40
+ /** Absolute path to the discovered `chant.config.ts`/`.json`, if any. */
41
+ configPath?: string;
42
+ }
43
+
44
+ /** Walk up from `startDir` (inclusive) to the nearest chant config or project boundary. See {@link ProjectConfigSearch}. */
45
+ export function findProjectConfig(startDir: string): ProjectConfigSearch {
46
+ const resolvedStart = resolve(startDir);
47
+ let dir = resolvedStart;
48
+ for (;;) {
49
+ const tsPath = join(dir, "chant.config.ts");
50
+ if (existsSync(tsPath)) return { dir, configPath: tsPath };
51
+ const jsonPath = join(dir, "chant.config.json");
52
+ if (existsSync(jsonPath)) return { dir, configPath: jsonPath };
53
+
54
+ if (existsSync(join(dir, ".git")) || existsSync(join(dir, "package.json"))) {
55
+ return { dir };
56
+ }
57
+
58
+ const parent = dirname(dir);
59
+ if (parent === dir) {
60
+ // Filesystem root, no config, no boundary ever seen — give up rather
61
+ // than adopting "/" as the project root (see the module doc above).
62
+ return { dir: resolvedStart };
63
+ }
64
+ dir = parent;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Walk up from `startDir` to the nearest ancestor holding a chant project
70
+ * config (`chant.config.ts` or `chant.config.json`), the `.git`/`package.json`
71
+ * project boundary, or `startDir` itself when neither is found. Thin wrapper
72
+ * over {@link findProjectConfig} for callers that only need the directory
73
+ * (e.g. `chant lint`'s rule/plugin resolution, which just needs *a* stable
74
+ * root to resolve relative paths against).
75
+ */
76
+ export function findProjectRoot(startDir: string): string {
77
+ return findProjectConfig(startDir).dir;
78
+ }