@intentius/chant 0.27.0 → 0.29.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 (106) hide show
  1. package/dist/cli/commands/build.d.ts.map +1 -1
  2. package/dist/cli/commands/lint.d.ts.map +1 -1
  3. package/dist/cli/handlers/components.d.ts.map +1 -1
  4. package/dist/cli/handlers/graph.d.ts.map +1 -1
  5. package/dist/cli/handlers/lifecycle.d.ts +5 -3
  6. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  7. package/dist/config.d.ts +46 -4
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/discovery/fold-import.d.ts +153 -17
  10. package/dist/discovery/fold-import.d.ts.map +1 -1
  11. package/dist/discovery/sandbox/config-run.d.ts.map +1 -1
  12. package/dist/discovery/sandbox/config-wire.d.ts +3 -2
  13. package/dist/discovery/sandbox/config-wire.d.ts.map +1 -1
  14. package/dist/discovery/sandbox/fork.d.ts +25 -0
  15. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  16. package/dist/discovery/sandbox/policy-run.d.ts.map +1 -1
  17. package/dist/discovery/sandbox/run.d.ts.map +1 -1
  18. package/dist/env.d.ts +5 -2
  19. package/dist/env.d.ts.map +1 -1
  20. package/dist/fold/fold.d.ts +12 -0
  21. package/dist/fold/fold.d.ts.map +1 -1
  22. package/dist/graph-ir.d.ts +29 -4
  23. package/dist/graph-ir.d.ts.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/kubectl-context.d.ts +100 -0
  27. package/dist/kubectl-context.d.ts.map +1 -0
  28. package/dist/lexicon.d.ts +31 -6
  29. package/dist/lexicon.d.ts.map +1 -1
  30. package/dist/lifecycle/change-set.d.ts +26 -5
  31. package/dist/lifecycle/change-set.d.ts.map +1 -1
  32. package/dist/lifecycle/live-diff.d.ts +25 -1
  33. package/dist/lifecycle/live-diff.d.ts.map +1 -1
  34. package/dist/lifecycle/observe.d.ts +4 -2
  35. package/dist/lifecycle/observe.d.ts.map +1 -1
  36. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  37. package/dist/lifecycle/status.d.ts +26 -1
  38. package/dist/lifecycle/status.d.ts.map +1 -1
  39. package/dist/lifecycle/types.d.ts +8 -0
  40. package/dist/lifecycle/types.d.ts.map +1 -1
  41. package/dist/lint/config.d.ts +80 -0
  42. package/dist/lint/config.d.ts.map +1 -1
  43. package/dist/lint/policy.d.ts +8 -2
  44. package/dist/lint/policy.d.ts.map +1 -1
  45. package/dist/lint/post-synth.d.ts +18 -1
  46. package/dist/lint/post-synth.d.ts.map +1 -1
  47. package/dist/live-endpoint.d.ts +92 -0
  48. package/dist/live-endpoint.d.ts.map +1 -0
  49. package/dist/observation.d.ts +123 -0
  50. package/dist/observation.d.ts.map +1 -0
  51. package/dist/stack-output.d.ts +9 -4
  52. package/dist/stack-output.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/src/cli/commands/build.test.ts +190 -0
  55. package/src/cli/commands/build.ts +34 -2
  56. package/src/cli/commands/lint.ts +17 -25
  57. package/src/cli/handlers/components.test.ts +63 -4
  58. package/src/cli/handlers/components.ts +78 -35
  59. package/src/cli/handlers/graph.test.ts +69 -6
  60. package/src/cli/handlers/graph.ts +61 -27
  61. package/src/cli/handlers/lifecycle.test.ts +285 -6
  62. package/src/cli/handlers/lifecycle.ts +297 -185
  63. package/src/config.test.ts +75 -0
  64. package/src/config.ts +61 -3
  65. package/src/discovery/fold-composite.test.ts +594 -0
  66. package/src/discovery/fold-import.ts +987 -43
  67. package/src/discovery/sandbox/config-boundary.test.ts +55 -1
  68. package/src/discovery/sandbox/config-run.ts +3 -0
  69. package/src/discovery/sandbox/config-wire.ts +3 -2
  70. package/src/discovery/sandbox/fork.ts +75 -1
  71. package/src/discovery/sandbox/policy-boundary.test.ts +56 -1
  72. package/src/discovery/sandbox/policy-run.ts +15 -1
  73. package/src/discovery/sandbox/run.test.ts +85 -1
  74. package/src/discovery/sandbox/run.ts +3 -0
  75. package/src/env.test.ts +12 -0
  76. package/src/env.ts +12 -4
  77. package/src/fold/fold.ts +12 -2
  78. package/src/graph-ir-live.test.ts +28 -1
  79. package/src/graph-ir.ts +68 -12
  80. package/src/index.ts +1 -0
  81. package/src/kubectl-context.test.ts +94 -0
  82. package/src/kubectl-context.ts +207 -0
  83. package/src/lexicon.ts +41 -6
  84. package/src/lifecycle/change-set.test.ts +93 -1
  85. package/src/lifecycle/change-set.ts +65 -13
  86. package/src/lifecycle/live-diff.test.ts +39 -0
  87. package/src/lifecycle/live-diff.ts +51 -5
  88. package/src/lifecycle/observe.test.ts +74 -3
  89. package/src/lifecycle/observe.ts +82 -22
  90. package/src/lifecycle/snapshot.test.ts +39 -1
  91. package/src/lifecycle/snapshot.ts +34 -9
  92. package/src/lifecycle/status.test.ts +89 -8
  93. package/src/lifecycle/status.ts +53 -3
  94. package/src/lifecycle/types.ts +8 -0
  95. package/src/lint/config.test.ts +93 -1
  96. package/src/lint/config.ts +108 -0
  97. package/src/lint/policy.test.ts +90 -0
  98. package/src/lint/policy.ts +17 -5
  99. package/src/lint/post-synth.test.ts +4 -0
  100. package/src/lint/post-synth.ts +30 -1
  101. package/src/live-endpoint.test.ts +115 -0
  102. package/src/live-endpoint.ts +148 -0
  103. package/src/observation.test.ts +96 -0
  104. package/src/observation.ts +213 -0
  105. package/src/stack-output.test.ts +76 -3
  106. package/src/stack-output.ts +59 -26
@@ -13,12 +13,18 @@ import type { PostSynthCheck, PostSynthDiagnostic } from "./post-synth.js";
13
13
  */
14
14
  export declare function loadPolicyChecks(paths: string[], configDir: string): Promise<PostSynthCheck[]>;
15
15
  export interface PolicyEvaluation {
16
- /** All policy diagnostics (errors + warnings). */
16
+ /** All policy diagnostics (errors + warnings), after `lint.rules` severity overrides. */
17
17
  diagnostics: PostSynthDiagnostic[];
18
- /** The error-severity subset — these are policy *violations* that gate. */
18
+ /** The error-severity subset (post-override) — these are policy *violations* that gate. */
19
19
  violations: PostSynthDiagnostic[];
20
20
  /** The environment policies were evaluated against (if any). */
21
21
  env?: string;
22
+ /**
23
+ * chant #1138 — diagnostics `lint.rules` turned `"off"`, present here rather
24
+ * than dropped so a caller (`policyGate`, below) can report a count instead
25
+ * of the finding just vanishing.
26
+ */
27
+ suppressed: PostSynthDiagnostic[];
22
28
  }
23
29
  /**
24
30
  * Build a project and run its `lint.policies` over the resolved resources,
@@ -1 +1 @@
1
- {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/lint/policy.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAGxE;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAuBpG;AAED,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,2EAA2E;IAC3E,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAClC,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA8B5B"}
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/lint/policy.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAIxE;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAuBpG;AAED,MAAM,WAAW,gBAAgB;IAC/B,yFAAyF;IACzF,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,2FAA2F;IAC3F,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAClC,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,UAAU,EAAE,mBAAmB,EAAE,CAAC;CACnC;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmC5B"}
@@ -33,6 +33,19 @@ export interface PostSynthContext {
33
33
  export declare function getPrimaryOutput(output: string | SerializerResult): string;
34
34
  /**
35
35
  * A diagnostic from a post-synthesis check.
36
+ *
37
+ * chant #1138 — deliberately carries no `file`/`line` the way `LintDiagnostic`
38
+ * (`./rule.ts`) does. A post-synth check runs over `ctx.outputs` — the
39
+ * SYNTHESIZED output text (a CloudFormation template, a Kubernetes manifest) —
40
+ * not a `ts.SourceFile`, so there is no AST position to report in the first
41
+ * place. `entity` (below) is the closest thing to a locator and is NOT a
42
+ * substitute: it names a resource in that synthesized output (a CFN logical
43
+ * id, a k8s `metadata.name`), which several checks in this repo never even
44
+ * set (a cross-cutting check with no single implicated resource), and which
45
+ * is not guaranteed to match a `ctx.entities` map key. This is why source-
46
+ * comment (`chant-disable`) suppression is out of scope for post-synth
47
+ * findings — see `./config.ts`'s `applyConfiguredSeverity` doc for the full
48
+ * reasoning and what suppression surface post-synth findings get instead.
36
49
  */
37
50
  export interface PostSynthDiagnostic {
38
51
  /** ID of the check that produced this diagnostic */
@@ -41,7 +54,11 @@ export interface PostSynthDiagnostic {
41
54
  severity: Severity;
42
55
  /** Human-readable message */
43
56
  message: string;
44
- /** Optional entity name related to this diagnostic */
57
+ /**
58
+ * Optional resource name related to this diagnostic — a name from the
59
+ * SYNTHESIZED OUTPUT (a CFN logical id, a k8s `metadata.name`), not a
60
+ * source file/line. See this interface's doc comment.
61
+ */
45
62
  entity?: string;
46
63
  /** Optional lexicon related to this diagnostic */
47
64
  lexicon?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"post-synth.d.ts","sourceRoot":"","sources":["../../src/lint/post-synth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAEvC;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAAC,CAAC;IAChD,8CAA8C;IAC9C,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAClC;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,WAAW,EAAE;QACX,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAAC,CAAC;QAChD,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAClC,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,MAAM,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjD,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,MAAM,CAE1E;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,qBAAqB;IACrB,QAAQ,EAAE,QAAQ,CAAC;IACnB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,KAAK,CAAC,GAAG,EAAE,gBAAgB,GAAG,mBAAmB,EAAE,CAAC;CACrD;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAQxE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,cAAc,EAAE,EACxB,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,EAC5C,GAAG,CAAC,EAAE,MAAM,GACX,mBAAmB,EAAE,CAavB"}
1
+ {"version":3,"file":"post-synth.d.ts","sourceRoot":"","sources":["../../src/lint/post-synth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAEvC;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAAC,CAAC;IAChD,8CAA8C;IAC9C,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAClC;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,WAAW,EAAE;QACX,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAAC,CAAC;QAChD,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAClC,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,MAAM,EAAE,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjD,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,MAAM,CAE1E;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,qBAAqB;IACrB,QAAQ,EAAE,QAAQ,CAAC;IACnB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,KAAK,CAAC,GAAG,EAAE,gBAAgB,GAAG,mBAAmB,EAAE,CAAC;CACrD;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAQxE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,cAAc,EAAE,EACxB,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,EAC5C,GAAG,CAAC,EAAE,MAAM,GACX,mBAAmB,EAAE,CAavB"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * chant #1166 — let a declared `environment` carry its own endpoint so
3
+ * `--live --env <name>` is self-sufficient.
4
+ *
5
+ * The bug this closes: `chant graph --live --env floci` (and `lifecycle
6
+ * diff`/`plan`) observe a stack by shelling out through each lexicon's
7
+ * `describeResources()`. For AWS that shell-out honors the ambient
8
+ * `AWS_ENDPOINT_URL` env var — when a project's `floci` environment is a local
9
+ * emulator (`http://localhost:4566`) but the invoking shell never exported
10
+ * that var, the AWS CLI silently targets real AWS instead. The stack named
11
+ * after the environment doesn't exist there, so `describeResources` hits its
12
+ * `stackDoesNotExist` branch and returns an empty, unremarkable "nothing is
13
+ * deployed" — indistinguishable from the truthful answer. This cost real
14
+ * debugging time validating #1162's live overlay: the observation code was
15
+ * right, the manual repro just never set the var.
16
+ *
17
+ * The fix: `environments` in `chant.config.ts` can name an endpoint per
18
+ * environment (`config.ts`'s `EnvironmentDeclaration`), and {@link
19
+ * applyLiveEndpoint} injects it into the ambient env var each observing
20
+ * lexicon's CLI shell-out actually reads — but only for a var that isn't
21
+ * already set. Ambient always wins: a shell that already exports
22
+ * `AWS_ENDPOINT_URL` sees no change in behavior.
23
+ *
24
+ * Audited (#1166) which lexicons have an ambient-env-var endpoint knob at
25
+ * all, since that's the specific footgun — a lexicon whose environment
26
+ * binding is resolved from `chant.config` itself (not an ambient var) has
27
+ * nothing to inject here:
28
+ *
29
+ * - **aws** — `AWS_ENDPOINT_URL`, read directly by
30
+ * `lexicons/aws/src/components/cloud-executor.ts` / `plugin.ts` before
31
+ * every `aws …` shell-out (`applyAwsEndpoint`/`applyAwsEndpointArgv`).
32
+ * - **fly** — `FLY_FLAPS_BASE_URL`, read by `resolveEndpoint()` in
33
+ * `lexicons/fly/src/op/activities/fly-apply.ts`, the same seam
34
+ * `describeResources` (`../describe-resources.ts`) calls through.
35
+ * - **gcp**, **k8s** — resolve their live target from `chant.config` itself
36
+ * (`k8s.profiles.<env>.context` via `resolveClusterTarget`,
37
+ * `packages/core/src/kubectl-context.ts`), not an ambient var. Nothing to
38
+ * inject: the config *is* the binding already.
39
+ * - **azure** — resolves via the `az` CLI's own logged-in
40
+ * subscription/session context; no ambient endpoint var exists to miss.
41
+ * - **temporal** — resolves its connection from `temporal.profiles.<env>`
42
+ * (`resolveProfile`, `lexicons/temporal/src/describe-resources.ts`), the
43
+ * same "config is the binding" shape as k8s/gcp.
44
+ */
45
+ import { type EnvironmentDeclaration } from "./config.js";
46
+ /**
47
+ * Per-lexicon ambient env var a `--live` read honors for its endpoint. Only
48
+ * lexicons with a genuine ambient-var footgun are listed — see the module doc
49
+ * for the full audit (gcp/k8s/azure/temporal resolve their target from
50
+ * `chant.config` instead, so they have nothing to inject).
51
+ */
52
+ export declare const LEXICON_ENDPOINT_ENV_VAR: Record<string, string>;
53
+ /** Result of {@link applyLiveEndpoint} — always call `restore()`, even when nothing was applied (it is then a no-op). */
54
+ export interface AppliedEndpoint {
55
+ /**
56
+ * One line describing what happened, or `undefined` when the environment
57
+ * declares no endpoint at all (nothing to say). Present whether the
58
+ * declared endpoint was applied OR an ambient var already won — #1166's
59
+ * "no silent anything" stance: an operator should never have to guess which
60
+ * target a `--live` read actually used.
61
+ */
62
+ notice?: string;
63
+ /** Undo whatever ambient env vars this call set. Always safe to call. */
64
+ restore: () => void;
65
+ }
66
+ /**
67
+ * Resolve `environment`'s declared endpoint (if any) from `config.environments`
68
+ * and apply it to the ambient env var of every lexicon in `lexicons` that has
69
+ * one ({@link LEXICON_ENDPOINT_ENV_VAR}) — but only when that var isn't
70
+ * already set. Ambient always wins (#1166): behavior for a shell that already
71
+ * exports `AWS_ENDPOINT_URL` is unchanged.
72
+ *
73
+ * Call before a `--live` describe/enrich pass; `restore()` in a `finally` so
74
+ * the injected value never leaks into a later invocation in the same process
75
+ * (tests, or a long-lived host like the MCP server).
76
+ */
77
+ export declare function applyLiveEndpoint(environments: EnvironmentDeclaration[] | undefined, environment: string, lexicons: readonly string[], env?: NodeJS.ProcessEnv): AppliedEndpoint;
78
+ /**
79
+ * #1166 acceptance: when a `--live` describe comes back with zero resources
80
+ * for a lexicon that had declared entities to look for, and nothing was
81
+ * already reported NOT-OBSERVED (#1089) either, that is either "genuinely
82
+ * nothing is deployed yet" or a misconfigured endpoint/credentials — the two
83
+ * are visually identical, so a caller must say so rather than stay quiet.
84
+ * Returns `undefined` when there is nothing to declare (no declared entities
85
+ * to have asked about, or the emptiness is already explained by #1089's
86
+ * `unobserved`).
87
+ */
88
+ export declare function zeroResourcesWarning(lexicon: string, environment: string, declaredCount: number, observed: {
89
+ resources: Record<string, unknown>;
90
+ unobserved: Record<string, unknown>;
91
+ }): string | undefined;
92
+ //# sourceMappingURL=live-endpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-endpoint.d.ts","sourceRoot":"","sources":["../src/live-endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,EAAuB,KAAK,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAE5E;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAG3D,CAAC;AAEF,yHAAyH;AACzH,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,sBAAsB,EAAE,GAAG,SAAS,EAClD,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,eAAe,CAqCjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GACpF,MAAM,GAAG,SAAS,CAKpB"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The observation contract (#1089) — what a lexicon's `describeResources()`
3
+ * is allowed to mean.
4
+ *
5
+ * Before this module there were two ways for an observation to return nothing
6
+ * for a declared entity and no way to tell them apart:
7
+ *
8
+ * 1. The provider was asked and said the resource does not exist.
9
+ * 2. The lexicon never looked — no reader for that kind, the read errored,
10
+ * no credentials, no cluster/subscription binding.
11
+ *
12
+ * Both arrived at the change set as "absent", and absent + declared classifies
13
+ * as `create`. So `chant lifecycle plan` proposed creating a Kubernetes CRD
14
+ * that already existed in the cluster, with nothing in the plan to say the tool
15
+ * had simply not looked (a warn on stderr is not a signal in a change set).
16
+ *
17
+ * The contract is a tri-state, per declared entity:
18
+ *
19
+ * - **OBSERVED-PRESENT** — a key in `resources`. The provider returned it.
20
+ * - **OBSERVED-ABSENT** — in neither map. The lexicon looked and the provider
21
+ * reported it missing. This is the only shape that may become a `create`.
22
+ * - **NOT-OBSERVED** — a key in `unobserved`, carrying a total
23
+ * {@link UnobservedReason}. Never a `create`, never a `delete`; consumers
24
+ * surface it and stop.
25
+ *
26
+ * Compatibility: `describeResources()` may still return the bare
27
+ * `name → ResourceMetadata` map it always did — that means "everything I was
28
+ * asked about, I looked at" and normalizes to an empty `unobserved`. Reporting
29
+ * NOT-OBSERVED requires the explicitly versioned {@link ObservationResult}
30
+ * envelope, which is discriminated by its literal `observation: "v1"` field (a
31
+ * bare map's value at any key is a `ResourceMetadata` object, never that
32
+ * string, so the two shapes can never be confused).
33
+ */
34
+ import type { ResourceMetadata } from "./lexicon.js";
35
+ /**
36
+ * Why a declared entity was not observed. Total: a lexicon that cannot observe
37
+ * an entity must pick one of these, and consumers may switch exhaustively.
38
+ *
39
+ * - `read-failed` — the provider was reached and the read errored (a non-zero
40
+ * CLI exit, a 5xx, an unparseable response).
41
+ * - `no-credentials` — no usable credentials/authorization for the target.
42
+ * - `no-binding` — the environment resolves to no concrete target (no kubectl
43
+ * context, no subscription, no stack, no endpoint).
44
+ * - `unsupported-kind` — the lexicon has no reader for this entity type (the
45
+ * K8s CRD case, Azure's nested ARM types). The resource may well exist.
46
+ * - `filtered` — the entity was reached but withheld by a caller-requested
47
+ * filter (`owned: true` against a resource carrying no chant marker). The
48
+ * result deliberately says nothing about it, which is still not absence: a
49
+ * declared resource that exists but is foreign must not classify as `create`.
50
+ */
51
+ export type UnobservedReason = "read-failed" | "no-credentials" | "no-binding" | "unsupported-kind" | "filtered";
52
+ /** Every legal {@link UnobservedReason}, for validation and conformance checks. */
53
+ export declare const UNOBSERVED_REASONS: readonly UnobservedReason[];
54
+ /** True when `value` is a legal {@link UnobservedReason}. */
55
+ export declare function isUnobservedReason(value: unknown): value is UnobservedReason;
56
+ /** One declared entity the lexicon could not observe, and why. */
57
+ export interface UnobservedEntity {
58
+ /** Declared entity type, when the lexicon knows it (it usually does — the entity is declared). */
59
+ type?: string;
60
+ /** Total verdict. */
61
+ reason: UnobservedReason;
62
+ /** Human-readable detail: the command that failed, the missing binding key, the unsupported kind. */
63
+ detail?: string;
64
+ }
65
+ /**
66
+ * The observation envelope — a `describeResources()` return value that can say
67
+ * "I did not look at this one". Explicitly versioned: `observation: "v1"`.
68
+ */
69
+ export interface ObservationResult {
70
+ /** Discriminant + wire version. Distinguishes the envelope from the bare `name → ResourceMetadata` map. */
71
+ readonly observation: "v1";
72
+ /** OBSERVED-PRESENT, keyed by chant entity name. */
73
+ resources: Record<string, ResourceMetadata>;
74
+ /** NOT-OBSERVED, keyed by chant entity name. Omit or leave empty when everything asked about was looked at. */
75
+ unobserved?: Record<string, UnobservedEntity>;
76
+ }
77
+ /**
78
+ * What `describeResources()` may return: the bare map (pre-#1089, still valid —
79
+ * "I looked at everything") or the {@link ObservationResult} envelope.
80
+ */
81
+ export type DescribeResourcesResult = Record<string, ResourceMetadata> | ObservationResult;
82
+ /** Normalized form every consumer works with. Both maps always present. */
83
+ export interface NormalizedObservation {
84
+ resources: Record<string, ResourceMetadata>;
85
+ unobserved: Record<string, UnobservedEntity>;
86
+ }
87
+ /** True when `value` is the versioned {@link ObservationResult} envelope. */
88
+ export declare function isObservationResult(value: unknown): value is ObservationResult;
89
+ /**
90
+ * Build an {@link ObservationResult}. Lexicons use this rather than writing the
91
+ * discriminant by hand.
92
+ */
93
+ export declare function observation(resources: Record<string, ResourceMetadata>, unobserved?: Record<string, UnobservedEntity>): ObservationResult;
94
+ /**
95
+ * Normalize either accepted return shape. `undefined` (a lexicon that returned
96
+ * nothing at all) normalizes to two empty maps — which reads as "everything was
97
+ * observed absent", so callers that mean "the read failed" must say so with
98
+ * {@link unobservedAll} rather than returning nothing.
99
+ */
100
+ export declare function normalizeObservation(value: DescribeResourcesResult | undefined): NormalizedObservation;
101
+ /**
102
+ * Mark every named entity NOT-OBSERVED with one reason — the whole-lexicon
103
+ * failure case (the provider CLI is missing, the cluster binding refused, the
104
+ * credentials are gone). Core applies this when `describeResources()` throws,
105
+ * so a thrown read degrades to an honest "did not look" for each declared
106
+ * entity instead of an empty map that classifies as N creates.
107
+ */
108
+ export declare function unobservedAll(names: Iterable<string>, reason: UnobservedReason, detail?: string, types?: Map<string, {
109
+ entityType: string;
110
+ }> | Record<string, string>): Record<string, UnobservedEntity>;
111
+ /**
112
+ * Union several observations of the same lexicon (the multi-stack read, where
113
+ * `describeResources` runs once per stack). Precedence is
114
+ * present > not-observed > absent: a resource found in any stack is present; an
115
+ * entity nobody could look at stays not-observed; an entity every reader looked
116
+ * for and did not find is absent.
117
+ */
118
+ export declare function mergeObservations(parts: Iterable<NormalizedObservation>): NormalizedObservation;
119
+ /** One-line human phrasing of a reason, for CLI output. */
120
+ export declare function unobservedReasonText(reason: UnobservedReason): string;
121
+ /** `name — reason (detail)`, the shared rendering for CLI and plan output. */
122
+ export declare function formatUnobserved(name: string, entry: UnobservedEntity): string;
123
+ //# sourceMappingURL=observation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,gBAAgB,GAChB,YAAY,GACZ,kBAAkB,GAClB,UAAU,CAAC;AAEf,mFAAmF;AACnF,eAAO,MAAM,kBAAkB,EAAE,SAAS,gBAAgB,EAMzD,CAAC;AAEF,6DAA6D;AAC7D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAE5E;AAED,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,kGAAkG;IAClG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,MAAM,EAAE,gBAAgB,CAAC;IACzB,qGAAqG;IACrG,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,2GAA2G;IAC3G,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C,+GAA+G;IAC/G,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC/C;AAED;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,iBAAiB,CAAC;AAE3F,2EAA2E;AAC3E,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC9C;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAM9E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC3C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAC5C,iBAAiB,CAMnB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,uBAAuB,GAAG,SAAS,GAAG,qBAAqB,CAMtG;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,gBAAgB,EACxB,MAAM,CAAC,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAYlC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,qBAAqB,CAW/F;AAED,2DAA2D;AAC3D,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAarE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAG9E"}
@@ -22,10 +22,13 @@ export interface StackOutput extends Declarable {
22
22
  readonly lexicon: string;
23
23
  readonly entityType: string;
24
24
  readonly kind: "output";
25
- /** The exported value: a bare attribute reference, or an intrinsic wrapping
26
- * one (e.g. `Join(",", zone.NameServers)`). */
27
- readonly sourceRef: AttrRef | Intrinsic;
25
+ /** The exported value: a bare attribute reference, an intrinsic wrapping
26
+ * one (e.g. `Join(",", zone.NameServers)`), or a literal string. */
27
+ readonly sourceRef: AttrRef | Intrinsic | string;
28
28
  readonly description?: string;
29
+ /** When set, the serializer emits a cross-stack export under this name
30
+ * (CloudFormation `Output.Export.Name`) in addition to the plain output. */
31
+ readonly exportName?: string;
29
32
  }
30
33
  /**
31
34
  * Type guard for StackOutput.
@@ -49,7 +52,9 @@ export declare function isStackOutput(value: unknown): value is StackOutput;
49
52
  * });
50
53
  * ```
51
54
  */
52
- export declare function stackOutput(ref: AttrRef | Intrinsic, options?: {
55
+ export declare function stackOutput(ref: AttrRef | Intrinsic | string, options?: {
53
56
  description?: string;
57
+ exportName?: string;
58
+ lexicon?: string;
54
59
  }): StackOutput;
55
60
  //# sourceMappingURL=stack-output.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"stack-output.d.ts","sourceRoot":"","sources":["../src/stack-output.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,iBAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAG1D;;GAEG;AACH,eAAO,MAAM,mBAAmB,eAAkC,CAAC;AAEnE;;;GAGG;AACH,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,QAAQ,CAAC,CAAC,mBAAmB,CAAC,EAAE,IAAI,CAAC;IACrC,QAAQ,CAAC,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB;mDAC+C;IAC/C,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAqBD;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAOlE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,OAAO,GAAG,SAAS,EACxB,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACjC,WAAW,CAiCb"}
1
+ {"version":3,"file":"stack-output.d.ts","sourceRoot":"","sources":["../src/stack-output.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,iBAAiB,EAAgB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAG1D;;GAEG;AACH,eAAO,MAAM,mBAAmB,eAAkC,CAAC;AAEnE;;;GAGG;AACH,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,QAAQ,CAAC,CAAC,mBAAmB,CAAC,EAAE,IAAI,CAAC;IACrC,QAAQ,CAAC,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB;wEACoE;IACpE,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;gFAC4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AA2CD;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAOlE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,EACjC,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACxE,WAAW,CAyCb"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.27.0",
3
+ "version": "0.29.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,12 +1,14 @@
1
1
  import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
2
2
  import { buildCommand, resolveBuildFormat, type BuildOptions } from "./build";
3
3
  import type { Serializer } from "../../serializer";
4
+ import type { LexiconPlugin } from "../../lexicon";
4
5
  import { parseYAML } from "../../yaml";
5
6
  import { mkdir, rm, writeFile } from "node:fs/promises";
6
7
  import { existsSync, readFileSync } from "node:fs";
7
8
  import { join, dirname, resolve as resolvePath } from "node:path";
8
9
  import { tmpdir } from "node:os";
9
10
  import { fileURLToPath } from "node:url";
11
+ import { resetSandboxPolicyExecutionForTests } from "../../lint/policy-sandbox";
10
12
 
11
13
  describe("buildCommand", () => {
12
14
  let testDir: string;
@@ -514,6 +516,194 @@ export const testEntity = {
514
516
  { Key: "env", Value: "prod" },
515
517
  ]);
516
518
  });
519
+
520
+ /**
521
+ * chant #1138 — post-synth findings (a lexicon-shipped check's, and a
522
+ * project's `lint.policies`') honor `lint.rules` severity overrides the
523
+ * same way a pre-synth COR/EVL/COMP diagnostic does. `runComponentChecks`
524
+ * proved the shape (`component-lint.test.ts`); this proves the post-synth
525
+ * side of the same fix.
526
+ */
527
+ describe("post-synth findings honor lint.rules (chant #1138)", () => {
528
+ // `armSandboxPolicyExecution` is a one-way, process-global latch — once a
529
+ // `--sandbox` build in this file arms it, EVERY later `buildCommand` call
530
+ // in this worker (sandboxed or not) would otherwise see it armed and
531
+ // refuse to load policies in-process. Reset it around each test here so
532
+ // the plain/sandboxed pairs below are actually independent.
533
+ beforeEach(() => resetSandboxPolicyExecutionForTests());
534
+ afterEach(() => resetSandboxPolicyExecutionForTests());
535
+
536
+ /**
537
+ * One trivial declarable using the `mockSerializer`'s "test" lexicon, so
538
+ * `result.outputs` is non-empty and the build's own "discovered source
539
+ * but produced no output" guard (`../commands/build.ts`) doesn't fire —
540
+ * these tests are about post-synth suppression, not that guard.
541
+ */
542
+ async function writeTrivialEntity(): Promise<void> {
543
+ await writeFile(
544
+ join(testDir, "main.ts"),
545
+ `export const e = { lexicon: "test", entityType: "TestEntity", [Symbol.for("chant.declarable")]: true };\n`,
546
+ );
547
+ }
548
+
549
+ /** A minimal `LexiconPlugin` whose one post-synth check always emits one fixed diagnostic — the fixed-input half of a severity-override test. */
550
+ function fakePostSynthPlugin(checkId: string, severity: "error" | "warning" | "info"): LexiconPlugin {
551
+ return {
552
+ name: "fake",
553
+ serializer: { name: "fake", rulePrefix: "FAKE", serialize: () => "{}" },
554
+ generate: async () => {},
555
+ validate: async () => {},
556
+ coverage: async () => {},
557
+ package: async () => {},
558
+ postSynthChecks: () => [
559
+ {
560
+ id: checkId,
561
+ description: "test check",
562
+ check: () => [{ checkId, severity, message: `${checkId} triggered` }],
563
+ },
564
+ ],
565
+ };
566
+ }
567
+
568
+ test("lint.rules off suppresses a lexicon-shipped post-synth finding and reports a suppressed count", async () => {
569
+ await writeTrivialEntity();
570
+ await writeFile(
571
+ join(testDir, "chant.config.ts"),
572
+ `export default { lint: { rules: { "FAKE001": "off" } } };\n`,
573
+ );
574
+
575
+ const result = await buildCommand({
576
+ path: testDir,
577
+ format: "json",
578
+ serializers: [mockSerializer],
579
+ plugins: [fakePostSynthPlugin("FAKE001", "error")],
580
+ });
581
+
582
+ expect(result.success).toBe(true);
583
+ expect(result.errors).toEqual([]);
584
+ expect(result.errors.join("\n")).not.toContain("FAKE001 triggered");
585
+ expect(result.warnings.some((w) => w.includes("1 post-synth finding(s) suppressed"))).toBe(true);
586
+ });
587
+
588
+ test("lint.rules downgrades an error-severity post-synth check to warning, so it no longer fails the build", async () => {
589
+ await writeTrivialEntity();
590
+ await writeFile(
591
+ join(testDir, "chant.config.ts"),
592
+ `export default { lint: { rules: { "FAKE002": "warning" } } };\n`,
593
+ );
594
+
595
+ const result = await buildCommand({
596
+ path: testDir,
597
+ format: "json",
598
+ serializers: [mockSerializer],
599
+ plugins: [fakePostSynthPlugin("FAKE002", "error")],
600
+ });
601
+
602
+ expect(result.success).toBe(true);
603
+ expect(result.errors).toEqual([]);
604
+ expect(result.warnings.some((w) => w.includes("FAKE002 triggered"))).toBe(true);
605
+ });
606
+
607
+ test("lint.rules upgrades a warning-severity post-synth check to error, so it now fails the build", async () => {
608
+ await writeTrivialEntity();
609
+ await writeFile(
610
+ join(testDir, "chant.config.ts"),
611
+ `export default { lint: { rules: { "FAKE003": "error" } } };\n`,
612
+ );
613
+
614
+ const result = await buildCommand({
615
+ path: testDir,
616
+ format: "json",
617
+ serializers: [mockSerializer],
618
+ plugins: [fakePostSynthPlugin("FAKE003", "warning")],
619
+ });
620
+
621
+ expect(result.success).toBe(false);
622
+ expect(result.errors.some((e) => e.includes("FAKE003 triggered"))).toBe(true);
623
+ });
624
+
625
+ test("an unconfigured post-synth check id is unaffected — no drift from this fix for the common case", async () => {
626
+ await writeTrivialEntity();
627
+ await writeFile(join(testDir, "chant.config.ts"), `export default {};\n`);
628
+
629
+ const result = await buildCommand({
630
+ path: testDir,
631
+ format: "json",
632
+ serializers: [mockSerializer],
633
+ plugins: [fakePostSynthPlugin("FAKE004", "error")],
634
+ });
635
+
636
+ expect(result.success).toBe(false);
637
+ expect(result.errors.some((e) => e.includes("FAKE004 triggered"))).toBe(true);
638
+ expect(result.warnings.some((w) => w.includes("suppressed"))).toBe(false);
639
+ });
640
+
641
+ /** Write a `lint.policies` project: one policy file whose check always emits one fixed diagnostic. */
642
+ async function writePolicyProject(checkId: string, severity: "error" | "warning", rules: Record<string, string>): Promise<void> {
643
+ await mkdir(join(testDir, "policies"), { recursive: true });
644
+ await writeTrivialEntity();
645
+ await writeFile(
646
+ join(testDir, "policies", "org.ts"),
647
+ `export const check = {\n` +
648
+ ` id: ${JSON.stringify(checkId)},\n` +
649
+ ` description: "test policy",\n` +
650
+ ` check: () => [{ checkId: ${JSON.stringify(checkId)}, severity: ${JSON.stringify(severity)}, message: ${JSON.stringify(`${checkId} triggered`)} }],\n` +
651
+ `};\n`,
652
+ );
653
+ await writeFile(
654
+ join(testDir, "chant.config.ts"),
655
+ `export default { lint: { policies: ["policies/org.ts"], rules: ${JSON.stringify(rules)} } };\n`,
656
+ );
657
+ }
658
+
659
+ test(
660
+ "lint.rules off suppresses a lint.policies finding identically under plain and --sandbox builds",
661
+ async () => {
662
+ await writePolicyProject("ORG-OFF", "error", { "ORG-OFF": "off" });
663
+
664
+ const plain = await buildCommand({ path: testDir, format: "json", serializers: [mockSerializer], plugins: [] });
665
+ const sandboxed = await buildCommand({
666
+ path: testDir,
667
+ format: "json",
668
+ serializers: [mockSerializer],
669
+ plugins: [],
670
+ fold: true,
671
+ sandbox: true,
672
+ });
673
+
674
+ for (const result of [plain, sandboxed]) {
675
+ expect(result.success).toBe(true);
676
+ expect(result.errors).toEqual([]);
677
+ expect(result.warnings.some((w) => w.includes("1 post-synth finding(s) suppressed"))).toBe(true);
678
+ }
679
+ },
680
+ 30_000,
681
+ );
682
+
683
+ test(
684
+ "lint.rules upgrades a lint.policies warning to error identically under plain and --sandbox builds",
685
+ async () => {
686
+ await writePolicyProject("ORG-UP", "warning", { "ORG-UP": "error" });
687
+
688
+ const plain = await buildCommand({ path: testDir, format: "json", serializers: [mockSerializer], plugins: [] });
689
+ const sandboxed = await buildCommand({
690
+ path: testDir,
691
+ format: "json",
692
+ serializers: [mockSerializer],
693
+ plugins: [],
694
+ fold: true,
695
+ sandbox: true,
696
+ });
697
+
698
+ for (const result of [plain, sandboxed]) {
699
+ expect(result.success).toBe(false);
700
+ expect(result.errors.some((e) => e.includes("[policy:ORG-UP]") && e.includes("ORG-UP triggered"))).toBe(true);
701
+ expect(result.warnings.some((w) => w.includes("suppressed"))).toBe(false);
702
+ }
703
+ },
704
+ 30_000,
705
+ );
706
+ });
517
707
  });
518
708
 
519
709
  // ── #284 bug 1: -o extension drives format when --format is absent ────────
@@ -4,6 +4,7 @@ import { resolveCliBuildParams } from "../build-params-cli";
4
4
  import type { Serializer, SerializerResult } from "../../serializer";
5
5
  import type { LexiconPlugin } from "../../lexicon";
6
6
  import { runPostSynthChecks } from "../../lint/post-synth";
7
+ import { applyConfiguredSeverity } from "../../lint/config";
7
8
  import { loadPolicyChecks } from "../../lint/policy";
8
9
  import { armSandboxPolicyExecution, runProjectPolicies } from "../../lint/policy-sandbox";
9
10
  import { sortedJsonReplacer } from "../../utils";
@@ -282,6 +283,16 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
282
283
  }
283
284
 
284
285
  // Run post-synth checks from plugins — each plugin only sees its own lexicon's output
286
+ //
287
+ // chant #1138 — every diagnostic collected below (a lexicon-shipped check's
288
+ // AND a project's `lint.policies`') is resolved against `lint.rules` before
289
+ // it becomes an error/warning line, through the exact same
290
+ // `applyConfiguredSeverity` (../../lint/post-synth.ts) that keys off
291
+ // `diag.checkId` the way `lintCommand` keys an AST/COMP* diagnostic off its
292
+ // rule id — so `lint.rules: { WAW019: "off" }` suppresses a post-synth
293
+ // finding just as it suppresses a pre-synth one. A finding it suppresses is
294
+ // counted, not dropped silently — see `suppressedPostSynthCount` below.
295
+ let suppressedPostSynthCount = 0;
285
296
  if (result.errors.length === 0 && options.plugins) {
286
297
  for (const plugin of options.plugins) {
287
298
  if (!plugin.postSynthChecks) continue;
@@ -301,7 +312,9 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
301
312
 
302
313
  const scopedResult = { ...result, outputs: scopedOutputs };
303
314
  const postDiags = runPostSynthChecks(checks, scopedResult, env);
304
- for (const diag of postDiags) {
315
+ const { diagnostics: activeDiags, suppressed } = applyConfiguredSeverity(postDiags, config.lint?.rules);
316
+ suppressedPostSynthCount += suppressed.length;
317
+ for (const diag of activeDiags) {
305
318
  const prefix = diag.entity ? `[${diag.entity}] ` : "";
306
319
  const lexiconSuffix = diag.lexicon ? ` (${diag.lexicon})` : "";
307
320
  if (diag.severity === "error") {
@@ -321,6 +334,15 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
321
334
  // result to a post-merge sandboxed child, which imports the policy modules
322
335
  // and runs their checks there, and only plain `PostSynthDiagnostic`s come
323
336
  // back. Unsandboxed, it is the same in-process load-and-run as before.
337
+ //
338
+ // #1138 — `applyConfiguredSeverity` runs HERE, in the parent, after the
339
+ // sandboxed child (when armed) has already returned — never inside it.
340
+ // The child only knows the policy paths and the encoded build result, not
341
+ // this project's resolved `lint.rules`, and by design nothing about the
342
+ // suppression surface needs to cross that boundary: both the plain and
343
+ // the `--sandbox` path funnel through this identical call with the
344
+ // identical `config.lint?.rules`, so a sandboxed and an unsandboxed build
345
+ // of the same project apply the same config to the same diagnostics.
324
346
  if (policies.length > 0) {
325
347
  const policyDiags = await runProjectPolicies({
326
348
  policies,
@@ -329,7 +351,9 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
329
351
  env,
330
352
  preloaded: preloadedPolicyChecks,
331
353
  });
332
- for (const diag of policyDiags) {
354
+ const { diagnostics: activePolicyDiags, suppressed } = applyConfiguredSeverity(policyDiags, config.lint?.rules);
355
+ suppressedPostSynthCount += suppressed.length;
356
+ for (const diag of activePolicyDiags) {
333
357
  const prefix = diag.entity ? `[${diag.entity}] ` : "";
334
358
  const where = diag.lexicon ? ` (${diag.lexicon})` : "";
335
359
  const msg = `[policy:${diag.checkId}] ${prefix}${diag.message}${where}`;
@@ -337,6 +361,14 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
337
361
  else warnings.push(formatWarning({ message: msg }));
338
362
  }
339
363
  }
364
+
365
+ if (suppressedPostSynthCount > 0) {
366
+ warnings.push(
367
+ formatWarning({
368
+ message: `${suppressedPostSynthCount} post-synth finding(s) suppressed via lint.rules (severity "off")`,
369
+ }),
370
+ );
371
+ }
340
372
  }
341
373
 
342
374
  // Empty-output guard: source files were discovered but no lexicon produced