@intentius/chant 0.59.0 → 0.61.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 (60) hide show
  1. package/dist/build-params.d.ts +2 -2
  2. package/dist/cli/commands/lint.d.ts.map +1 -1
  3. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  4. package/dist/cli/handlers/lint.d.ts.map +1 -1
  5. package/dist/codegen/json-schema.d.ts +5 -2
  6. package/dist/codegen/json-schema.d.ts.map +1 -1
  7. package/dist/components/pilots/alb-ecs.pilot.d.ts +2 -2
  8. package/dist/config.d.ts +4 -4
  9. package/dist/graph-ir.d.ts +70 -2
  10. package/dist/graph-ir.d.ts.map +1 -1
  11. package/dist/lexicon.d.ts +48 -5
  12. package/dist/lexicon.d.ts.map +1 -1
  13. package/dist/lifecycle/assert-live.d.ts.map +1 -1
  14. package/dist/lifecycle/observe.d.ts +4 -4
  15. package/dist/lifecycle/observe.d.ts.map +1 -1
  16. package/dist/observation.d.ts +21 -1
  17. package/dist/observation.d.ts.map +1 -1
  18. package/dist/op/activities/index.d.ts +1 -1
  19. package/dist/op/activities/index.d.ts.map +1 -1
  20. package/dist/op/activities/reconcile.d.ts +79 -7
  21. package/dist/op/activities/reconcile.d.ts.map +1 -1
  22. package/dist/op/gate-summary.d.ts +16 -4
  23. package/dist/op/gate-summary.d.ts.map +1 -1
  24. package/dist/params.d.ts +1 -1
  25. package/dist/project-root.d.ts +2 -2
  26. package/package.json +1 -1
  27. package/src/build-params.ts +2 -2
  28. package/src/cli/commands/build.ts +8 -8
  29. package/src/cli/commands/lint.test.ts +151 -0
  30. package/src/cli/commands/lint.ts +37 -4
  31. package/src/cli/handlers/components.ts +1 -1
  32. package/src/cli/handlers/graph.test.ts +4 -4
  33. package/src/cli/handlers/graph.ts +11 -11
  34. package/src/cli/handlers/lifecycle.ts +1 -0
  35. package/src/cli/handlers/lint.test.ts +107 -0
  36. package/src/cli/handlers/lint.ts +30 -0
  37. package/src/codegen/json-schema.test.ts +159 -0
  38. package/src/codegen/json-schema.ts +106 -8
  39. package/src/components/SPRAWL-VALIDATION.md +5 -5
  40. package/src/components/pilots/README.md +1 -1
  41. package/src/components/pilots/alb-ecs.pilot.ts +2 -2
  42. package/src/config.ts +4 -4
  43. package/src/discovery/fold-import.test.ts +1 -1
  44. package/src/discovery/fold-import.ts +3 -3
  45. package/src/graph-ir.test.ts +63 -0
  46. package/src/graph-ir.ts +125 -8
  47. package/src/lexicon.ts +49 -5
  48. package/src/lifecycle/assert-live.ts +1 -0
  49. package/src/lifecycle/observe.test.ts +2 -2
  50. package/src/lifecycle/observe.ts +11 -8
  51. package/src/lifecycle/release-ledger.test.ts +2 -2
  52. package/src/observation.test.ts +21 -9
  53. package/src/observation.ts +31 -2
  54. package/src/op/activities/index.ts +8 -1
  55. package/src/op/activities/reconcile.test.ts +238 -0
  56. package/src/op/activities/reconcile.ts +267 -13
  57. package/src/op/gate-summary.test.ts +62 -0
  58. package/src/op/gate-summary.ts +17 -5
  59. package/src/params.ts +1 -1
  60. package/src/project-root.ts +2 -2
@@ -6,6 +6,13 @@
6
6
  * re-run (chant #2231). It therefore needs a pull-request trigger, and
7
7
  * {@link resolvePullRequestContext} fails the step by name when the run has
8
8
  * none.
9
+ *
10
+ * On GitLab the same mode writes a merge-request note (chant #2256): the same
11
+ * marker, the same edit-in-place, a different API. Which forge a run is on is
12
+ * read off the run's own CI variables rather than configured — a
13
+ * `merge_request_event` pipeline sets `CI_MERGE_REQUEST_IID`, a GitHub
14
+ * `pull_request` event sets `GITHUB_REPOSITORY`, and no run sets both. See
15
+ * {@link mergeRequestContextFrom}.
9
16
  */
10
17
  export type ReconcileMode = "pull-request" | "issue" | "report" | "comment";
11
18
  /** A change-set entry that triggered reconciliation. */
@@ -69,10 +76,12 @@ export interface ReconcileResult {
69
76
  prUrl?: string;
70
77
  /** Opened issue URL (issue mode). */
71
78
  issueUrl?: string;
72
- /** The posted or updated PR comment's URL (comment mode). */
79
+ /** The posted or updated PR comment / MR note URL (comment mode). */
73
80
  commentUrl?: string;
74
- /** The pull request the comment landed on, `owner/repo#number` (comment mode). */
81
+ /** The pull request the comment landed on, `owner/repo#number` (comment mode, GitHub). */
75
82
  pullRequest?: string;
83
+ /** The merge request the note landed on, `group/project!iid` (comment mode, GitLab — #2256). */
84
+ mergeRequest?: string;
76
85
  /** The markdown summary used as the PR/issue body. */
77
86
  summary: string;
78
87
  /** The entries that triggered the reconcile. */
@@ -101,7 +110,7 @@ export interface PullRequestContext {
101
110
  * environments own two comments and each updates in place.
102
111
  */
103
112
  export declare function commentMarker(env: string): string;
104
- /** What a `comment`-mode step says when the run it is in has no pull request. */
113
+ /** What a `comment`-mode step says when the run it is in has no pull request and no merge request. */
105
114
  export declare function noPullRequestContextMessage(): string;
106
115
  /**
107
116
  * Derive the triggering pull request from CI environment variables plus the
@@ -119,6 +128,66 @@ export declare function pullRequestContextFrom(env: Record<string, string | unde
119
128
  * for it.
120
129
  */
121
130
  export declare function resolvePullRequestContext(env?: Record<string, string | undefined>): Promise<PullRequestContext>;
131
+ /**
132
+ * The merge request a `comment`-mode run posts its note onto (#2256), as a
133
+ * GitLab CI job knows it. The GitLab counterpart of {@link
134
+ * PullRequestContext}.
135
+ */
136
+ export interface MergeRequestContext {
137
+ /** REST v4 base, from `CI_API_V4_URL` or derived from `CI_SERVER_URL`. */
138
+ api: string;
139
+ /** The project holding the merge request — its numeric id, or a `group/project` path. */
140
+ project: string;
141
+ /** The merge request's `iid` (its per-project number, which is what the API path takes). */
142
+ iid: number;
143
+ /** `group/project`, for the human-readable `group/project!iid` on the result. */
144
+ path?: string;
145
+ /** The project's web URL, used to build the note's own URL. */
146
+ webUrl?: string;
147
+ }
148
+ /**
149
+ * Derive the triggering merge request from a GitLab job's CI variables. Pure
150
+ * — exported for testing, and the whole forge detection: nothing but a
151
+ * `merge_request_event` pipeline sets `CI_MERGE_REQUEST_IID`, so a run that
152
+ * has it is on GitLab and has a merge request, and a run that does not is
153
+ * neither.
154
+ *
155
+ * The project is the merge request's own (`CI_MERGE_REQUEST_PROJECT_ID`) in
156
+ * preference to the pipeline's (`CI_PROJECT_ID`): a merge request opened from
157
+ * a fork runs its pipeline in the fork, and the note belongs on the target
158
+ * project's merge request rather than on an iid that means something else in
159
+ * the fork.
160
+ *
161
+ * Returns undefined rather than throwing, so the caller owns the message.
162
+ */
163
+ export declare function mergeRequestContextFrom(env: Record<string, string | undefined>): MergeRequestContext | undefined;
164
+ /** The credential a merge-request note is written with, and the header GitLab reads it from. */
165
+ export interface GitlabNoteToken {
166
+ /** `PRIVATE-TOKEN` for a personal/project/group access token, `JOB-TOKEN` for `CI_JOB_TOKEN`. */
167
+ header: "PRIVATE-TOKEN" | "JOB-TOKEN";
168
+ value: string;
169
+ /** The variable it came from, so a refusal or a log line can name it. */
170
+ source: string;
171
+ }
172
+ /**
173
+ * Resolve the token a merge-request note is written with, most specific
174
+ * first. Pure — exported for testing.
175
+ *
176
+ * Two headers, not one, because GitLab reads two different credentials from
177
+ * two different headers: an access token goes in `PRIVATE-TOKEN`, and the
178
+ * pipeline's own ephemeral `CI_JOB_TOKEN` goes in `JOB-TOKEN`. Sending one in
179
+ * the other's header is a 401, not a fallback.
180
+ *
181
+ * The access token is preferred because it is the one that reliably works:
182
+ * `CI_JOB_TOKEN` reaches only the endpoints GitLab's job-token allowlist
183
+ * names, and the notes API is not among them on current GitLab, so a project
184
+ * that has not widened that allowlist needs a token with `api` scope. It is
185
+ * still accepted last rather than refused, so a project on an instance whose
186
+ * allowlist does cover notes needs no long-lived credential at all.
187
+ */
188
+ export declare function gitlabNoteTokenFrom(env: Record<string, string | undefined>): GitlabNoteToken | undefined;
189
+ /** What a `comment`-mode step says on a merge request it has no credential for. */
190
+ export declare function noGitlabNoteTokenMessage(iid: number): string;
122
191
  /**
123
192
  * Map a `chant lifecycle plan --json` ChangeSet to reconcile entries, dropping
124
193
  * `noop` entries (nothing to reconcile). Pure — exported for testing.
@@ -131,10 +200,13 @@ export declare function entriesFromPlan(planJson: string): ReconcileEntry[];
131
200
  * - `issue` — open a GitHub issue describing the drift (no code change).
132
201
  * - `comment` — post the body as one comment on the pull request that
133
202
  * triggered the run, editing that same comment on every re-run rather than
134
- * stacking a new one (#2231). Needs a pull-request-triggered run; fails by
135
- * name when there is none. No code change, and the `pull-requests: write`
136
- * the generated workflow already grants on that trigger is the whole scope
137
- * it spends.
203
+ * stacking a new one (#2231), or, on a GitLab `merge_request_event`
204
+ * pipeline, as one note on that merge request by the same recipe (#2256).
205
+ * Needs a pull-request- or merge-request-triggered run; fails by name when
206
+ * there is none. No code change, and the `pull-requests: write` the
207
+ * generated workflow already grants on that trigger is the whole scope it
208
+ * spends on GitHub; on GitLab the scope is whatever the token it is given
209
+ * carries.
138
210
  * - `pull-request` — create a branch, regenerate source via
139
211
  * `chant import --from <env>`, commit, push, and open a PR whose diff is the
140
212
  * regenerated TypeScript. Never commits to the main branch.
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../../src/op/activities/reconcile.ts"],"names":[],"mappings":"AAMA;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE5E,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC7B,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,8CAA8C;IAC9C,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;OAcG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,aAAa,CAAC;IACpB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED,4EAA4E;AAC5E,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,MAAM,CAkB/E;AAMD,wDAAwD;AACxD,MAAM,WAAW,kBAAkB;IACjC,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,iFAAiF;AACjF,wBAAgB,2BAA2B,IAAI,MAAM,CAQpD;AAeD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EACvC,YAAY,CAAC,EAAE,OAAO,GACrB,kBAAkB,GAAG,SAAS,CAOhC;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,OAAO,CAAC,kBAAkB,CAAC,CAe7B;AA2CD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,EAAE,CAOlE;AAgBD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAmDvG"}
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../../src/op/activities/reconcile.ts"],"names":[],"mappings":"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"}
@@ -8,9 +8,14 @@
8
8
  * markdown scratchpad through `GITHUB_STEP_SUMMARY`, rendered at the top of
9
9
  * the run page; anything else that sets the variable gets the same block.
10
10
  *
11
- * That is the whole forge coupling: one environment variable, appended to
12
- * when it is set, and nothing at all when it is not. No API call, no token,
13
- * no flag to turn it on.
11
+ * GitLab CI sets no such variable and has no step summary at all: a job's
12
+ * surfaces are its log and its artifacts (#2256). So `CHANT_GATE_SUMMARY`
13
+ * names a second path, which the generated GitLab job sets to a file it also
14
+ * declares under `artifacts:`. Same block, same rule, one more variable.
15
+ *
16
+ * That is the whole forge coupling: two environment variables, appended to
17
+ * whichever is set, and nothing at all when neither is. No API call, no
18
+ * token, no flag to turn it on.
14
19
  */
15
20
  /** What a gated run knows about the gate it stopped on. */
16
21
  export interface GatedRunSummary {
@@ -33,7 +38,14 @@ export interface GatedRunSummary {
33
38
  export declare function gatedRunSummaryMarkdown(summary: GatedRunSummary): string;
34
39
  /**
35
40
  * Append {@link gatedRunSummaryMarkdown} to the file `GITHUB_STEP_SUMMARY`
36
- * names, when the variable is set and the file can be written.
41
+ * names, or, where the forge sets no such variable, the one
42
+ * `CHANT_GATE_SUMMARY` names (#2256) — when it is set and the file can be
43
+ * written.
44
+ *
45
+ * The forge's own variable wins where both are set: a step summary is
46
+ * rendered on the run page, an artifact has to be downloaded, and writing
47
+ * both would put the same block in two places on a forge that already shows
48
+ * one of them.
37
49
  *
38
50
  * Returns the path written, or `undefined` when there was nothing to write to.
39
51
  * A write that fails is swallowed: a run that already decided its outcome must
@@ -1 +1 @@
1
- {"version":3,"file":"gate-summary.d.ts","sourceRoot":"","sources":["../../src/op/gate-summary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,iDAAiD;IACjD,EAAE,EAAE,MAAM,CAAC;IACX,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAuBxE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,eAAe,EACxB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,GAAG,SAAS,CASpB"}
1
+ {"version":3,"file":"gate-summary.d.ts","sourceRoot":"","sources":["../../src/op/gate-summary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,iDAAiD;IACjD,EAAE,EAAE,MAAM,CAAC;IACX,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAuBxE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,eAAe,EACxB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,GAAG,SAAS,CASpB"}
package/dist/params.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * is a deploy-time CloudFormation parameter: it emits a `Parameters:` block and
7
7
  * resolves when the STACK deploys. A build-time parameter resolves before the
8
8
  * template is even synthesized — its value can change WHICH resources are
9
- * produced at all (loomster's `LOOM_TIER` selecting `light` vs `production` vs
9
+ * produced at all (a `tier` parameter selecting `light` vs `production` vs
10
10
  * `production-ha`), which a deploy-time `Parameter` structurally cannot do.
11
11
  *
12
12
  * Project source never reads `process.env` directly to vary a build — that
@@ -9,8 +9,8 @@
9
9
  * `src/<stack>` layout — `chant build src/<stack>` two or more levels below
10
10
  * the project root — silently never found the root config: `buildParams`'
11
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).
12
+ * fell back to defaults, and nothing warned. That failure ran under every
13
+ * `npm run synth:*` of one project for two releases before anyone noticed it.
14
14
  *
15
15
  * `findProjectConfig` is the one walk every config-discovery call site now
16
16
  * shares. It stops at the first of:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.59.0",
3
+ "version": "0.61.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",
@@ -33,8 +33,8 @@ export interface BuildParamDef {
33
33
  /**
34
34
  * Allowed values — a resolved value outside this list is a build error
35
35
  * naming the parameter (never a thrown error from user source). Replaces
36
- * the hand-written `if (!VALID.includes(raw)) throw ...` pattern loomster's
37
- * `params.ts` files used before migrating to this mechanism.
36
+ * the hand-written `if (!VALID.includes(raw)) throw ...` pattern a project's
37
+ * own `params.ts` writes before migrating to this mechanism.
38
38
  */
39
39
  enum?: readonly BuildParamValue[];
40
40
  /**
@@ -174,8 +174,8 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
174
174
  // Resolve opt-in ownership marking from project config. chant #1117 — walks
175
175
  // up from the infra dir to the project root (`loadChantConfigUpward`), not
176
176
  // just the infra dir's immediate parent: a project whose stacks live two or
177
- // more levels below `chant.config.ts` (loomster's `src/<stack>` layout)
178
- // otherwise never finds the root config at all, and every declared
177
+ // more levels below `chant.config.ts` (a `src/<stack>` layout) otherwise
178
+ // never finds the root config at all, and every declared
179
179
  // `buildParams`/`ownership`/`lint.policies` setting silently falls back to
180
180
  // its default.
181
181
  const loaded = await loadChantConfigUpward(infraPath);
@@ -233,10 +233,10 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
233
233
  // BEFORE calling build() — a resolution failure (an unknown name, a
234
234
  // missing required value, a type/enum mismatch) is reported as a chant
235
235
  // build error naming the parameter, never a thrown error from inside user
236
- // source (which is what loomster's hand-rolled `tierFromEnv()`-style
237
- // validators did before migrating to this mechanism). Also logs a one-line
238
- // count on success, or every resolved value (`[param] name = value
239
- // (source)`) under --verbose (#1424).
236
+ // source (which is what a hand-rolled `tierFromEnv()`-style validator in a
237
+ // project's own `params.ts` does before migrating to this mechanism). Also
238
+ // logs a one-line count on success, or every resolved value (`[param] name =
239
+ // value (source)`) under --verbose (#1424).
240
240
  const paramsResolution = resolveCliBuildParams(config.buildParams, {
241
241
  cli: options.params,
242
242
  paramsFile: options.paramsFile,
@@ -248,8 +248,8 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
248
248
  }
249
249
 
250
250
  // chant #1117 — a project that declares buildParams but resolves NONE of
251
- // them for this build is the exact shape that let loomster#162 live for two
252
- // releases: the discovered config wasn't the one the project author
251
+ // them for this build is the exact shape that let a silent misresolution run
252
+ // for two releases: the discovered config wasn't the one the project author
253
253
  // expected (a stale --path, a workspace boundary that stopped the walk
254
254
  // short), or every declared parameter's `env:` var went unset, and either
255
255
  // way every `params.<name>` read silently falls back to `undefined` — with
@@ -5,6 +5,7 @@ import { mkdir, rm, writeFile } from "node:fs/promises";
5
5
  import { join, resolve } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
  import { execFileSync } from "node:child_process";
8
+ import { params, setBuildParams } from "../../params";
8
9
 
9
10
  describe("lintCommand", () => {
10
11
  let testDir: string;
@@ -118,6 +119,69 @@ export default {
118
119
  ).toBe(true);
119
120
  });
120
121
 
122
+ // #2251 — `chant lint` imports every `*.op.ts` file to read the Op it
123
+ // declares, and an Op that takes a step argument from `params.<name>`
124
+ // (`@intentius/chant/params`) evaluates that read at module load. Before
125
+ // this, nothing populated the shared parameters object first, so every such
126
+ // argument was `undefined` and OPS012 reported the activity contract
127
+ // violated on source that builds and runs. The fixture imports the real
128
+ // params module by absolute path — `@intentius/chant/params` maps to the
129
+ // same file (packages/core/package.json's `exports`), so it is the one
130
+ // module record `lintCommand` mutates.
131
+ describe("build parameters reach an Op's step arguments (#2251)", () => {
132
+ const paramsModule = resolve(import.meta.dirname, "../../params.ts");
133
+
134
+ async function writeParamReadingOp(): Promise<void> {
135
+ await writeFile(
136
+ join(testDir, "mini.op.ts"),
137
+ `
138
+ import { params } from ${JSON.stringify(paramsModule)};
139
+
140
+ export default {
141
+ [Symbol.for("chant.declarable")]: true,
142
+ entityType: "Chant::Op",
143
+ lexicon: "chant",
144
+ kind: "resource",
145
+ props: {
146
+ name: "mini",
147
+ overview: "test",
148
+ phases: [
149
+ { name: "Greet", steps: [
150
+ { kind: "activity", fn: "shellCmd", args: { cmd: params.greeting } },
151
+ ] },
152
+ ],
153
+ },
154
+ };
155
+ `,
156
+ );
157
+ }
158
+
159
+ test("OPS012 does not fire when the invocation's parameters are supplied", async () => {
160
+ await writeParamReadingOp();
161
+
162
+ const result = await lintCommand({
163
+ path: testDir,
164
+ format: "stylish",
165
+ buildParams: [{ name: "greeting", value: "echo hi", source: "default" }],
166
+ });
167
+
168
+ expect(result.diagnostics.filter((d) => d.ruleId === "OPS012")).toEqual([]);
169
+ expect(result.success).toBe(true);
170
+ });
171
+
172
+ test("OPS012 fires on the same source when no parameters are supplied", async () => {
173
+ await writeParamReadingOp();
174
+
175
+ const result = await lintCommand({ path: testDir, format: "stylish" });
176
+
177
+ expect(
178
+ result.diagnostics.some(
179
+ (d) => d.ruleId === "OPS012" && d.message.includes("args.cmd"),
180
+ ),
181
+ ).toBe(true);
182
+ });
183
+ });
184
+
121
185
  test("formats output as JSON", async () => {
122
186
  await writeFile(
123
187
  join(testDir, "nested.ts"),
@@ -686,3 +750,90 @@ describe("lintCommand — a declared lexicon that cannot be resolved (#2222)", (
686
750
  expect(result.diagnostics.some((d) => d.ruleId === LEXICON_RESOLUTION_RULE_ID)).toBe(false);
687
751
  });
688
752
  });
753
+
754
+ /**
755
+ * chant #2249 — `lintCommand` resolves the project's own declared
756
+ * `buildParams` when its caller supplied none, so an in-process lint agrees
757
+ * with `chant lint` on the command line.
758
+ *
759
+ * #2251 taught the CLI handler (../handlers/lint.ts) to resolve
760
+ * `--param`/`--params-file`/declared defaults and pass them down, because
761
+ * the OPS* checks import every `*.op.ts` file and an Op reading
762
+ * `params.<name>` evaluates that read at module load. Every other caller of
763
+ * `lintCommand` has no flags to read and passed nothing, so `params` stayed
764
+ * empty and OPS012 reported the activity contract violated for source that
765
+ * lints clean from a shell. That is how the root-examples gate
766
+ * (examples/root-examples-gate.test.ts) went red on github-pr-preview while
767
+ * `chant lint .` in the same directory exited 0, and it applied equally to
768
+ * `handleLint` over MCP and to test-utils' example harness.
769
+ *
770
+ * These drive the real `lintCommand` against a temp project holding one
771
+ * `*.op.ts` file (enough for the OPS* pass to run its parameter binding) and
772
+ * read the shared `params` object the binding populates.
773
+ */
774
+ describe("lintCommand build-time parameters (#2249)", () => {
775
+ let testDir: string;
776
+
777
+ beforeEach(async () => {
778
+ testDir = join(tmpdir(), `chant-lint-params-${Date.now()}-${Math.random()}`);
779
+ await mkdir(testDir, { recursive: true });
780
+ // The OPS* pass binds parameters only when there is an Op file to import.
781
+ // A plain default export is enough: it is skipped as not-an-Op after the
782
+ // binding has already happened.
783
+ await writeFile(join(testDir, "noop.op.ts"), `export default { props: {} };\n`);
784
+ });
785
+
786
+ afterEach(async () => {
787
+ await rm(testDir, { recursive: true, force: true });
788
+ delete process.env.CHANT_TEST_ENV_2249;
789
+ setBuildParams({});
790
+ });
791
+
792
+ test("a declared default reaches params with no caller-supplied provenance", async () => {
793
+ await writeFile(
794
+ join(testDir, "chant.config.json"),
795
+ JSON.stringify({ buildParams: { env: { type: "string", default: "local" } } }),
796
+ );
797
+
798
+ await lintCommand({ path: testDir, format: "stylish" });
799
+
800
+ expect(params.env).toBe("local");
801
+ });
802
+
803
+ test("a declared env mapping reaches params the same way chant lint resolves it", async () => {
804
+ await writeFile(
805
+ join(testDir, "chant.config.json"),
806
+ JSON.stringify({
807
+ buildParams: { env: { type: "string", default: "local", env: "CHANT_TEST_ENV_2249" } },
808
+ }),
809
+ );
810
+ process.env.CHANT_TEST_ENV_2249 = "pr-42";
811
+
812
+ await lintCommand({ path: testDir, format: "stylish" });
813
+
814
+ expect(params.env).toBe("pr-42");
815
+ });
816
+
817
+ test("caller-supplied parameters win over the declared defaults", async () => {
818
+ await writeFile(
819
+ join(testDir, "chant.config.json"),
820
+ JSON.stringify({ buildParams: { env: { type: "string", default: "local" } } }),
821
+ );
822
+
823
+ await lintCommand({
824
+ path: testDir,
825
+ format: "stylish",
826
+ buildParams: [{ name: "env", value: "pr-42", source: "cli" }],
827
+ });
828
+
829
+ expect(params.env).toBe("pr-42");
830
+ });
831
+
832
+ test("a project declaring none binds an empty parameter set", async () => {
833
+ await writeFile(join(testDir, "chant.config.json"), JSON.stringify({}));
834
+
835
+ await lintCommand({ path: testDir, format: "stylish" });
836
+
837
+ expect(params).toEqual({});
838
+ });
839
+ });
@@ -20,6 +20,8 @@ import { rule } from "../../lint/declarative";
20
20
  import { watchDirectory, formatTimestamp, formatChangedFiles } from "../watch";
21
21
  import { formatError, formatInfo } from "../format";
22
22
  import { GENERATED_MARKER } from "../../discovery/files";
23
+ import { buildParamValues, resolveBuildParams } from "../../build-params";
24
+ import { setBuildParams } from "../../params";
23
25
  import { isNoLexiconDetected } from "../../detectLexicon";
24
26
 
25
27
  // Import config loader
@@ -535,11 +537,21 @@ async function runComponentCheckDiagnostics(
535
537
  async function runOpCheckDiagnostics(
536
538
  infraPath: string,
537
539
  files: string[],
540
+ buildParams?: BuildParamProvenance[],
538
541
  ): Promise<{ diagnostics: LintDiagnostic[]; suppressed: Array<LintDiagnostic & { reason?: string }> }> {
539
542
  const config = loadConfig(findProjectRoot(infraPath));
540
543
  const opFiles = files.filter((f) => f.endsWith(".op.ts"));
541
544
  if (opFiles.length === 0) return { diagnostics: [], suppressed: [] };
542
545
 
546
+ // chant #2251 — the same step `discover()` runs before it imports or folds a
547
+ // project file (../../discovery/index.ts): populate the shared build-time
548
+ // parameters object BEFORE the imports below, so an Op that takes a step
549
+ // argument from `params.<name>` reads the value this invocation resolved
550
+ // rather than `undefined`. Unconditional, so a stale value from a prior
551
+ // lint in the same process (a test, `--watch`) never leaks into one that
552
+ // resolved none.
553
+ setBuildParams(buildParamValues(buildParams ?? []));
554
+
543
555
  const entities = new Map<string, unknown>();
544
556
  const fileByOpName = new Map<string, string>();
545
557
  for (const filePath of opFiles) {
@@ -640,10 +652,31 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
640
652
  // than a failure — `loadOkfBundle` already treats a missing directory as
641
653
  // an empty bundle.
642
654
  let knowledgeBundle: OkfBundle | undefined;
655
+ /**
656
+ * chant #2249 — this invocation's build-time parameters, falling back to
657
+ * the project's own declared `buildParams` when the caller passed none.
658
+ *
659
+ * `chant lint` resolves them in ./handlers/lint.ts (#2251) because only
660
+ * the CLI knows about `--param`/`--params-file`, and passes them here. An
661
+ * in-process caller has no such flags to read, and before this fell
662
+ * through to the empty parameter object: the OPS* checks import each
663
+ * `*.op.ts`, an Op taking a step argument from `params.<name>` read
664
+ * `undefined` at module load, and OPS012 reported the activity contract
665
+ * violated for source `chant lint` on the command line passes. That made
666
+ * `lintCommand` disagree with its own CLI (the root-examples gate,
667
+ * `handleLint` over MCP, test-utils' example harness). Resolving the
668
+ * declared defaults + `env` mappings here is what the command line
669
+ * already gets; anything a flag would override still arrives in
670
+ * `options.buildParams` and wins. Best-effort like `projectConfig`:
671
+ * `resolveBuildParams` collects failures per parameter without throwing,
672
+ * and the ones that did resolve are still better than none.
673
+ */
674
+ let buildParams = options.buildParams;
643
675
  try {
644
676
  const chantConfig = (await loadChantConfig(projectRoot)).config;
645
677
  projectConfig = chantConfig as LintProjectConfig;
646
678
  knowledgeBundle = await loadOkfBundle(resolveKnowledgeDir(chantConfig, projectRoot));
679
+ buildParams ??= resolveBuildParams(chantConfig.buildParams, { env: process.env }).provenance;
647
680
  } catch {
648
681
  projectConfig = undefined;
649
682
  knowledgeBundle = undefined;
@@ -696,14 +729,14 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
696
729
  // structurally distinct check family (whole-project, post-discovery,
697
730
  // see ../../lint/component-checks.ts) but the same `chant lint` output and
698
731
  // the same error-severity gating as every COR/EVL diagnostic.
699
- const componentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox, options.buildParams);
732
+ const componentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox, buildParams);
700
733
  diagnostics.push(...componentResult.diagnostics);
701
734
  suppressed.push(...componentResult.suppressed);
702
735
 
703
736
  // Run the OPS* Op-model post-synth checks (#2122) over every `*.op.ts`
704
737
  // file under the lint target — see runOpCheckDiagnostics's doc for why
705
738
  // this needs no lexicon or build to fire.
706
- const opResult = await runOpCheckDiagnostics(infraPath, files);
739
+ const opResult = await runOpCheckDiagnostics(infraPath, files, buildParams);
707
740
  diagnostics.push(...opResult.diagnostics);
708
741
  suppressed.push(...opResult.suppressed);
709
742
 
@@ -751,13 +784,13 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
751
784
  // `*.component.ts` file on their behalf), but a fix applied to another
752
785
  // rule could still be in the same file a component was discovered from —
753
786
  // re-run for consistency with the AST re-lint above.
754
- const postComponentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox, options.buildParams);
787
+ const postComponentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox, buildParams);
755
788
  diagnostics.push(...postComponentResult.diagnostics);
756
789
  suppressed.push(...postComponentResult.suppressed);
757
790
 
758
791
  // OPS* checks have no `.fix` either; re-run for the same consistency
759
792
  // reason as the COMP* re-run just above.
760
- const postOpResult = await runOpCheckDiagnostics(infraPath, files);
793
+ const postOpResult = await runOpCheckDiagnostics(infraPath, files, buildParams);
761
794
  diagnostics.push(...postOpResult.diagnostics);
762
795
  suppressed.push(...postOpResult.suppressed);
763
796
  }
@@ -591,7 +591,7 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
591
591
  // rather than "stale" (recorded, and nothing live).
592
592
  const message = err instanceof Error ? err.message : String(err);
593
593
  console.error(formatWarning({ message: `${plugin.name}: describeResources failed — ${message} (components in this lexicon report unknown, not stale)` }));
594
- observed = { resources: {}, unobserved: unobservedAll(declared, "read-failed", message, entities), queried: {}, notes: [] };
594
+ observed = { resources: {}, unobserved: unobservedAll(declared, "read-failed", message, entities), queried: {}, sources: {}, notes: [] };
595
595
  }
596
596
  const cs = buildChangeSet(environment, {
597
597
  declared,
@@ -710,10 +710,10 @@ describe("runGraph", () => {
710
710
  });
711
711
  });
712
712
 
713
- // The bug this branch fixes (#57): a multi-stack, per-component project
714
- // (loomster/Floci) has no stack literally named after the environment, so
715
- // the live graph must resolve each component's own `cfn-deploy` stack(s)
716
- // and pass them through to `observeResources` for the per-stack union.
713
+ // The bug this branch fixes (#57): a multi-stack, per-component project has
714
+ // no stack literally named after the environment, so the live graph must
715
+ // resolve each component's own `cfn-deploy` stack(s) and pass them through
716
+ // to `observeResources` for the per-stack union.
717
717
  test("multi-stack component project: resolves each component's cfn-deploy stack(s) and passes them to observeResources", async () => {
718
718
  resolveLexMock.mockResolvedValue(["aws"]);
719
719
  loadPluginsMock.mockResolvedValue([
@@ -253,17 +253,17 @@ async function runGraphLive(
253
253
  return 1;
254
254
  }
255
255
 
256
- // Multi-stack, per-component projects (loomster/Floci, #57): AWS's
257
- // single-stack convention (`describeResources` defaults to a stack named
258
- // after the environment, lexicons/aws/src/plugin.ts) never matches a
259
- // per-component layout (e.g. `loom-local-a-<component>`), so the plain
260
- // single call always observes zero nodes. Resolve every discovered
261
- // component's `cfn-deploy` stack(s) — the same walk `chant components
262
- // status --live` uses (`cfnDeployStacks`, ./components.ts) — and hand them
263
- // to `observeResources`, which queries `describeResources` once per stack
264
- // and unions the results. A project with no components (or whose discovery
265
- // errors) yields no stacks, so `observeResources` falls back to its
266
- // original single-stack call — unchanged.
256
+ // Multi-stack, per-component projects (#57): AWS's single-stack convention
257
+ // (`describeResources` defaults to a stack named after the environment,
258
+ // lexicons/aws/src/plugin.ts) never matches a per-component layout (e.g.
259
+ // `loom-local-a-<component>`), so the plain single call always observes zero
260
+ // nodes. Resolve every discovered component's `cfn-deploy` stack(s) — the
261
+ // same walk `chant components status --live` uses (`cfnDeployStacks`,
262
+ // ./components.ts) — and hand them to `observeResources`, which queries
263
+ // `describeResources` once per stack and unions the results. A project with
264
+ // no components (or whose discovery errors) yields no stacks, so
265
+ // `observeResources` falls back to its original single-stack call —
266
+ // unchanged.
267
267
  const componentsDiscovery = await discoverComponents(resolve(args.src ?? config.sourceDir ?? "."), {
268
268
  sandbox: args.sandbox,
269
269
  });
@@ -782,6 +782,7 @@ async function observeLexicon(
782
782
  resources: {},
783
783
  unobserved: unobservedAll(entityNames, "read-failed", message, opts.entities),
784
784
  queried: {},
785
+ sources: {},
785
786
  notes: [],
786
787
  };
787
788
  }