@intentius/chant 0.51.0 → 0.52.1

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 (39) hide show
  1. package/dist/cli/build-options.d.ts +68 -0
  2. package/dist/cli/build-options.d.ts.map +1 -0
  3. package/dist/cli/commands/build.d.ts.map +1 -1
  4. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  5. package/dist/cli/handlers/run.d.ts +0 -21
  6. package/dist/cli/handlers/run.d.ts.map +1 -1
  7. package/dist/cli/main.d.ts.map +1 -1
  8. package/dist/discovery/fold-import.d.ts.map +1 -1
  9. package/dist/lexicon.d.ts +10 -9
  10. package/dist/lexicon.d.ts.map +1 -1
  11. package/dist/lifecycle/assert-live.d.ts +2 -3
  12. package/dist/lifecycle/assert-live.d.ts.map +1 -1
  13. package/dist/lifecycle/change-set.d.ts +23 -0
  14. package/dist/lifecycle/change-set.d.ts.map +1 -1
  15. package/dist/lint/policy.d.ts +16 -0
  16. package/dist/lint/policy.d.ts.map +1 -1
  17. package/dist/op/local-executor.d.ts +15 -1
  18. package/dist/op/local-executor.d.ts.map +1 -1
  19. package/dist/ownership.d.ts +5 -4
  20. package/dist/ownership.d.ts.map +1 -1
  21. package/package.json +1 -1
  22. package/src/cli/build-options.test.ts +101 -0
  23. package/src/cli/build-options.ts +109 -0
  24. package/src/cli/commands/build.ts +24 -53
  25. package/src/cli/handlers/lifecycle.test.ts +19 -0
  26. package/src/cli/handlers/lifecycle.ts +14 -7
  27. package/src/cli/handlers/run.test.ts +96 -0
  28. package/src/cli/handlers/run.ts +37 -1
  29. package/src/cli/main.ts +2 -0
  30. package/src/discovery/fold-import.test.ts +102 -0
  31. package/src/discovery/fold-import.ts +54 -4
  32. package/src/lexicon.ts +10 -9
  33. package/src/lifecycle/assert-live.ts +2 -3
  34. package/src/lifecycle/change-set.test.ts +144 -1
  35. package/src/lifecycle/change-set.ts +133 -11
  36. package/src/lint/policy-build-parity.test.ts +232 -0
  37. package/src/lint/policy.ts +51 -6
  38. package/src/op/local-executor.ts +28 -0
  39. package/src/ownership.ts +5 -4
@@ -7,6 +7,8 @@ import { resolve, dirname } from "node:path";
7
7
  import { loadChantConfigUpward, resolveOwnershipEnv, resolveOwnershipMarker } from "../config";
8
8
  import { resolveBuildParams } from "../build-params";
9
9
  import { resolveProjectLexicons, loadPlugins } from "../cli/plugins";
10
+ import { resolveBuildModes, resolveProjectBuildOptions } from "../cli/build-options";
11
+ import { formatWarning } from "../cli/format";
10
12
  import { build } from "../build";
11
13
  import { runPostSynthChecks, isPostSynthCheck } from "./post-synth";
12
14
  import type { PostSynthCheck, PostSynthDiagnostic } from "./post-synth";
@@ -63,6 +65,15 @@ export interface PolicyEvaluation {
63
65
  * of the finding just vanishing.
64
66
  */
65
67
  suppressed: PostSynthDiagnostic[];
68
+ /**
69
+ * chant #2002 — divergences between this evaluation and what `chant build`
70
+ * would do, named rather than left silent. Today that is a project whose
71
+ * resolved `build.sandbox` is `true`: the gate builds unsandboxed, because
72
+ * running it inside the boundary needs the per-execution arming decision
73
+ * tracked on #1157. Also written to stderr — the gate's caller is an
74
+ * activity wrapper that surfaces output, not a structured result.
75
+ */
76
+ warnings: string[];
66
77
  }
67
78
 
68
79
  /**
@@ -70,6 +81,13 @@ export interface PolicyEvaluation {
70
81
  * standalone — used by the `policyGate` Op step to gate an apply on the same
71
82
  * organizational policy `chant build` enforces. Loads the project's lexicons,
72
83
  * builds, then runs the policy pack with `env` (explicit, else `ownership.env`).
84
+ *
85
+ * chant #2002 — "the same organizational policy `chant build` enforces" only
86
+ * holds if it is the same build. The build options are assembled by
87
+ * `../cli/build-options.ts`'s `resolveProjectBuildOptions`, the single function
88
+ * `chant build` also calls, rather than by a second list here; a build option
89
+ * added for one caller and not the other is what let a gate pass a build
90
+ * `chant build` fails.
73
91
  */
74
92
  export async function evaluateProjectPolicies(opts: {
75
93
  path: string;
@@ -96,10 +114,37 @@ export async function evaluateProjectPolicies(opts: {
96
114
  }
97
115
  const env = opts.env ?? resolveOwnershipEnv(config, params.provenance);
98
116
 
99
- const result = await build(buildPath, serializers, undefined, {
100
- ownership: resolveOwnershipMarker(config, params.provenance),
101
- buildParams: params.provenance,
102
- });
117
+ // chant #2002 — the build options come from the shared assembler
118
+ // (`../cli/build-options.ts`), the same one `chant build` uses, so the gate
119
+ // decides on the project `chant build` produces: folded by default (#1134),
120
+ // with the project's config reaching the serializers, and with
121
+ // config-declared build roots contributing their entities. Assembling a
122
+ // second, shorter list here is what let the two drift apart.
123
+ const modes = resolveBuildModes(config);
124
+ const warnings: string[] = [];
125
+ if (modes.sandbox) {
126
+ // Resolved, reported, and not yet honoured. Building inside the boundary
127
+ // needs per-execution arming (#1157) — until that lands, saying so is the
128
+ // difference between a known gap and a silent one.
129
+ warnings.push(
130
+ `build.sandbox is enabled for this project, but the policy gate builds it in this process — the project's source files and its lint.policies modules execute here, unsandboxed (chant #1157). Run \`chant build --sandbox\` for a sandboxed build of the same project.`,
131
+ );
132
+ }
133
+ for (const warning of warnings) console.error(formatWarning({ message: warning }));
134
+
135
+ const result = await build(
136
+ buildPath,
137
+ serializers,
138
+ undefined,
139
+ resolveProjectBuildOptions({
140
+ config,
141
+ configDir,
142
+ plugins,
143
+ modes: { fold: modes.fold, sandbox: false },
144
+ ownership: resolveOwnershipMarker(config, params.provenance),
145
+ buildParams: params.provenance,
146
+ }),
147
+ );
103
148
  if (result.errors.length > 0) {
104
149
  throw new Error("Build failed — cannot evaluate policy on a broken build");
105
150
  }
@@ -108,7 +153,7 @@ export async function evaluateProjectPolicies(opts: {
108
153
  ? await loadPolicyChecks(config.lint.policies, configDir)
109
154
  : [];
110
155
  if (checks.length === 0) {
111
- return { diagnostics: [], violations: [], env, suppressed: [] };
156
+ return { diagnostics: [], violations: [], env, suppressed: [], warnings };
112
157
  }
113
158
 
114
159
  const raw = runPostSynthChecks(checks, result, env);
@@ -118,5 +163,5 @@ export async function evaluateProjectPolicies(opts: {
118
163
  // fails on it either, and vice versa for a check turned UP to "error".
119
164
  const { diagnostics, suppressed } = applyConfiguredSeverity(raw, config.lint?.rules);
120
165
  const violations = diagnostics.filter((d) => d.severity === "error");
121
- return { diagnostics, violations, env, suppressed };
166
+ return { diagnostics, violations, env, suppressed, warnings };
122
167
  }
@@ -136,6 +136,34 @@ export function findGate(config: OpConfig): GateStep | undefined {
136
136
  return undefined;
137
137
  }
138
138
 
139
+ /**
140
+ * Find the first `policyGate` step anywhere in the Op (phases + `onFailure`,
141
+ * including steps nested inside effect steps), if any.
142
+ *
143
+ * chant #2003 — `--sandbox` is a GLOBAL flag (`../cli/registry.ts`), and
144
+ * `../cli/main.ts` arms the process-wide policy latch off it for every command,
145
+ * `chant run` included. A `policyGate` step then reaches `loadPolicyChecks`,
146
+ * which refuses while armed with a message written for a chant maintainer —
147
+ * shown to a user who passed a documented flag. The combination cannot be
148
+ * honoured until the gate can build and load policies inside the boundary
149
+ * (#1157), so both `chant run` paths pre-flight it with this and refuse before
150
+ * any phase executes, the same shape as {@link findGate}'s pre-flight.
151
+ */
152
+ export function findPolicyGateStep(config: OpConfig): ActivityStep | undefined {
153
+ const all = [...config.phases, ...(config.onFailure ?? [])];
154
+ const isPolicyGate = (s: StepDefinition): s is ActivityStep => isActivity(s) && s.fn === "policyGate";
155
+ for (const phase of all) {
156
+ for (const step of phase.steps) {
157
+ if (isPolicyGate(step)) return step;
158
+ if (isEffect(step)) {
159
+ const nested = step.steps.find(isPolicyGate);
160
+ if (nested) return nested;
161
+ }
162
+ }
163
+ }
164
+ return undefined;
165
+ }
166
+
139
167
  const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
140
168
 
141
169
  /**
package/src/ownership.ts CHANGED
@@ -133,10 +133,11 @@ export function readOwnership(
133
133
  * A read path that can resolve an ownership verdict from the marker (#1348).
134
134
  *
135
135
  * Per-path rather than per-lexicon because the answer genuinely differs by
136
- * path: aws stamps tags at synthesis and reads them on the deep observation and
137
- * on live export, but `describeResources` is sourced from
138
- * `describe-stack-resources`, which returns no tags at all — so an `owned: true`
139
- * thin read against aws can only answer `unknown`.
136
+ * path — in granularity as much as in availability. aws stamps tags at
137
+ * synthesis and reads them per resource on the deep observation and on live
138
+ * export; its `describeResources` is sourced from `describe-stack-resources`,
139
+ * which returns no per-resource tags, so it resolves the verdict from the
140
+ * STACK's own tags instead (#1998).
140
141
  */
141
142
  export type OwnershipReadPath = "describeResources" | "observeResourcesDeep" | "exportResources";
142
143