@intentius/chant-lexicon-aws 0.56.0 → 0.58.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 (61) hide show
  1. package/dist/agentcore/trace-fetch.d.ts +5 -4
  2. package/dist/agentcore/trace-fetch.d.ts.map +1 -1
  3. package/dist/condition.d.ts +27 -0
  4. package/dist/condition.d.ts.map +1 -0
  5. package/dist/generated/index.d.ts +1 -1
  6. package/dist/generated/index.d.ts.map +1 -1
  7. package/dist/import/generator.d.ts +19 -0
  8. package/dist/import/generator.d.ts.map +1 -1
  9. package/dist/import/parser.d.ts +19 -0
  10. package/dist/import/parser.d.ts.map +1 -1
  11. package/dist/index.d.ts +3 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/integrity.json +4 -3
  14. package/dist/intrinsics.d.ts +77 -3
  15. package/dist/intrinsics.d.ts.map +1 -1
  16. package/dist/lint/audit-catalog.d.ts.map +1 -1
  17. package/dist/lint/audit-lineage.d.ts +10 -0
  18. package/dist/lint/audit-lineage.d.ts.map +1 -0
  19. package/dist/lint/post-synth/index.d.ts.map +1 -1
  20. package/dist/lint/post-synth/waw069.d.ts +15 -0
  21. package/dist/lint/post-synth/waw069.d.ts.map +1 -0
  22. package/dist/manifest.json +29 -1
  23. package/dist/okf/index.md +1 -0
  24. package/dist/okf/rules/WAW069.md +11 -0
  25. package/dist/op/activities/aws-apply.d.ts.map +1 -1
  26. package/dist/op/activities/floci.d.ts +3 -2
  27. package/dist/op/activities/floci.d.ts.map +1 -1
  28. package/dist/op/builders.d.ts +4 -4
  29. package/dist/plugin.d.ts.map +1 -1
  30. package/dist/rules/waw069.ts +0 -0
  31. package/dist/serializer.d.ts.map +1 -1
  32. package/dist/types/index.d.ts +3 -3
  33. package/package.json +2 -2
  34. package/src/agentcore/trace-fetch.ts +5 -4
  35. package/src/codegen/docs.ts +3 -3
  36. package/src/codegen/generate-typescript.ts +3 -3
  37. package/src/codegen/generate.ts +1 -1
  38. package/src/condition.test.ts +129 -0
  39. package/src/condition.ts +40 -0
  40. package/src/deep-observe.test.ts +23 -15
  41. package/src/deep-topology.test.ts +7 -3
  42. package/src/generated/index.d.ts +3 -3
  43. package/src/generated/index.ts +1 -1
  44. package/src/import/generator.ts +131 -50
  45. package/src/import/parser.test.ts +96 -0
  46. package/src/import/parser.ts +86 -1
  47. package/src/import/roundtrip-fixtures.test.ts +50 -0
  48. package/src/index.ts +15 -3
  49. package/src/intrinsics.ts +130 -5
  50. package/src/lint/audit-catalog.ts +6 -1
  51. package/src/lint/audit-lineage.ts +159 -0
  52. package/src/lint/post-synth/index.ts +2 -0
  53. package/src/lint/post-synth/waw069.test.ts +94 -0
  54. package/src/lint/post-synth/waw069.ts +0 -0
  55. package/src/op/activities/aws-apply.ts +1 -3
  56. package/src/op/activities/floci.ts +3 -2
  57. package/src/op/builders.ts +4 -4
  58. package/src/plugin.test.ts +5 -1
  59. package/src/plugin.ts +4 -0
  60. package/src/serializer.ts +24 -3
  61. package/src/testdata/roundtrip/with-conditions.json +16 -0
@@ -0,0 +1,94 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw069, checkUndeclaredConditions } from "./waw069";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW069: Template references an undeclared condition", () => {
10
+ test("check metadata", () => {
11
+ expect(waw069.id).toBe("WAW069");
12
+ expect(waw069.description).toContain("condition");
13
+ });
14
+
15
+ test("resource Condition key referencing an undeclared condition → error", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ Rule: { Type: "AWS::Logs::LogGroup", Condition: "DoCutover", Properties: { RetentionInDays: 7 } },
19
+ },
20
+ });
21
+ const diags = checkUndeclaredConditions(ctx);
22
+ expect(diags).toHaveLength(1);
23
+ expect(diags[0].checkId).toBe("WAW069");
24
+ expect(diags[0].severity).toBe("error");
25
+ expect(diags[0].message).toContain("DoCutover");
26
+ expect(diags[0].entity).toBe("Rule");
27
+ });
28
+
29
+ test("Fn::If nested in properties referencing an undeclared condition → error", () => {
30
+ const ctx = makeCtx({
31
+ Resources: {
32
+ Rule: {
33
+ Type: "AWS::Logs::LogGroup",
34
+ Properties: { LogGroupName: { "Fn::If": ["DoCutover", "/on", "/off"] } },
35
+ },
36
+ },
37
+ });
38
+ const diags = checkUndeclaredConditions(ctx);
39
+ expect(diags).toHaveLength(1);
40
+ expect(diags[0].message).toContain("Fn::If");
41
+ });
42
+
43
+ test("output Condition key and Condition reference inside Conditions → error", () => {
44
+ const ctx = makeCtx({
45
+ Conditions: { NoCutover: { "Fn::Not": [{ Condition: "DoCutover" }] } },
46
+ Resources: { Rule: { Type: "AWS::Logs::LogGroup", Properties: {} } },
47
+ Outputs: { RuleName: { Condition: "AlsoMissing", Value: { Ref: "Rule" } } },
48
+ });
49
+ const diags = checkUndeclaredConditions(ctx);
50
+ expect(diags).toHaveLength(2);
51
+ const names = diags.map((d) => d.message);
52
+ expect(names.some((m) => m.includes('"DoCutover"'))).toBe(true);
53
+ expect(names.some((m) => m.includes('"AlsoMissing"'))).toBe(true);
54
+ });
55
+
56
+ test("declared conditions are quiet", () => {
57
+ const ctx = makeCtx({
58
+ Conditions: {
59
+ DoCutover: { "Fn::Equals": [{ Ref: "Cutover" }, "true"] },
60
+ NoCutover: { "Fn::Not": [{ Condition: "DoCutover" }] },
61
+ },
62
+ Resources: {
63
+ Rule: {
64
+ Type: "AWS::Logs::LogGroup",
65
+ Condition: "DoCutover",
66
+ Properties: { LogGroupName: { "Fn::If": ["DoCutover", "/on", "/off"] } },
67
+ },
68
+ },
69
+ Outputs: { RuleName: { Condition: "DoCutover", Value: { Ref: "Rule" } } },
70
+ });
71
+ expect(checkUndeclaredConditions(ctx)).toHaveLength(0);
72
+ });
73
+
74
+ test("an IAM policy Condition block is not a condition reference", () => {
75
+ const ctx = makeCtx({
76
+ Resources: {
77
+ Role: {
78
+ Type: "AWS::IAM::Role",
79
+ Properties: {
80
+ AssumeRolePolicyDocument: {
81
+ Statement: [
82
+ {
83
+ Effect: "Allow",
84
+ Condition: { StringEquals: { "aws:SourceAccount": "123456789012" } },
85
+ },
86
+ ],
87
+ },
88
+ },
89
+ },
90
+ },
91
+ });
92
+ expect(checkUndeclaredConditions(ctx)).toHaveLength(0);
93
+ });
94
+ });
Binary file
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { safeHeartbeat, sleep } from "@intentius/chant/op";
2
+ import { sleep } from "@intentius/chant/op";
3
3
  import { awsDeployCapabilitiesForBody } from "../../components/cloud-executor.js";
4
4
  import { resolveEndpointOverride } from "../../api/read-client.js";
5
5
  import { ownershipStackTagsForBody } from "../../ownership.js";
@@ -128,7 +128,6 @@ export async function waitForStackSettled(
128
128
  const deadline = Date.now() + opts.timeoutMs;
129
129
  while (Date.now() < deadline) {
130
130
  if (signal?.aborted) throw new Error("awsApply aborted");
131
- safeHeartbeat({ step: "awsApply", stack: stackName });
132
131
  const res = await http(url, cfnForm("DescribeStacks", { StackName: stackName }), signal);
133
132
  const status = stackStatus(res.text);
134
133
  if (status && isTerminalStatus(status)) return status;
@@ -231,7 +230,6 @@ export async function awsDelete(
231
230
  const deadline = Date.now() + timeoutMs;
232
231
  while (Date.now() < deadline) {
233
232
  if (signal?.aborted) throw new Error("awsDelete aborted");
234
- safeHeartbeat({ step: "awsDelete", stack: args.stackName });
235
233
  const d = await http(url, cfnForm("DescribeStacks", { StackName: args.stackName }), signal);
236
234
  if (d.status >= 300 && isStackMissing(d.text)) return { stackName: args.stackName, deleted: true };
237
235
  const status = stackStatus(d.text);
@@ -101,8 +101,9 @@ export function flociRunCommand(args: FlociUpArgs = {}): string {
101
101
  * Idempotent: reuses a running container of the same name. Waits for the health
102
102
  * endpoint to report `readyService`, then sets `AWS_ENDPOINT_URL` + test creds in
103
103
  * the process environment so a following `nativeApply`/`cfn-deploy` targets the
104
- * emulator. Env injection assumes the in-process local executor; under a
105
- * distributed Temporal worker, pass the endpoint explicitly instead.
104
+ * emulator. Env injection works because the local executor runs every step of a
105
+ * run in this process; a step that runs anywhere else needs the endpoint passed
106
+ * to it explicitly.
106
107
  */
107
108
  export async function flociUp(args: FlociUpArgs = {}, signal?: AbortSignal): Promise<{ endpoint: string }> {
108
109
  const region = args.region ?? DEFAULT_REGION;
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Typed step-builder wrappers for this lexicon's activities (chant #1288
3
3
  * Stage 2). See `lexicons/k8s/src/op/builders.ts`'s module doc for why these
4
- * live beside their `*Args` interfaces rather than in core or the temporal
5
- * barrel: core cannot import a lexicon's types (lexicons depend on core, not
6
- * the reverse), and wiring them into the cloud-agnostic temporal barrel
7
- * would make it depend on every cloud lexicon at runtime. `opts`'s type in
4
+ * live beside their `*Args` interfaces rather than in core: core cannot
5
+ * import a lexicon's types (lexicons depend on core, not the reverse), so a
6
+ * wrapper whose `opts` IS the activity's own `*Args` interface can only sit
7
+ * in the lexicon that declares that interface. `opts`'s type in
8
8
  * each wrapper below IS the activity's own `*Args` interface (via
9
9
  * `Omit`/`WithStepRefs`) — never restated. `core`'s own
10
10
  * `awsApply`/`awsDelete`/`flociUp`/`flociDown` are unchanged and produce
@@ -48,11 +48,15 @@ describe("awsPlugin", () => {
48
48
 
49
49
  test("returns intrinsics", () => {
50
50
  const intrinsics = awsPlugin.intrinsics!();
51
- expect(intrinsics.length).toBe(9);
51
+ expect(intrinsics.length).toBe(13);
52
52
  const names = intrinsics.map((i) => i.name);
53
53
  expect(names).toContain("Sub");
54
54
  expect(names).toContain("Ref");
55
55
  expect(names).toContain("GetAtt");
56
+ expect(names).toContain("Equals");
57
+ expect(names).toContain("And");
58
+ expect(names).toContain("Or");
59
+ expect(names).toContain("Not");
56
60
  });
57
61
 
58
62
  test("returns pseudo-parameters", () => {
package/src/plugin.ts CHANGED
@@ -111,6 +111,10 @@ export const awsPlugin: LexiconPlugin = {
111
111
  { name: "Split", description: "Fn::Split — split string by delimiter", isTag: false, foldsAsCall: true },
112
112
  { name: "Base64", description: "Fn::Base64 — encode to Base64", isTag: false, foldsAsCall: true },
113
113
  { name: "GetAZs", description: "Fn::GetAZs — list Availability Zones", isTag: false, foldsAsCall: true },
114
+ { name: "Equals", description: "Fn::Equals — condition comparing two values", isTag: false, foldsAsCall: true },
115
+ { name: "And", description: "Fn::And — condition conjunction", isTag: false, foldsAsCall: true },
116
+ { name: "Or", description: "Fn::Or — condition disjunction", isTag: false, foldsAsCall: true },
117
+ { name: "Not", description: "Fn::Not — condition negation", isTag: false, foldsAsCall: true },
114
118
  ];
115
119
  },
116
120
 
package/src/serializer.ts CHANGED
@@ -19,8 +19,9 @@ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
19
19
  import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
20
20
  import { isChildProject, type ChildProjectInstance } from "@intentius/chant/child-project";
21
21
  import { isStackOutput, type StackOutput } from "@intentius/chant/stack-output";
22
- import { isAttrRefLike } from "@intentius/chant/utils";
22
+ import { isAttrRefLike, getLogicalName } from "@intentius/chant/utils";
23
23
  import { resolveDependsOn } from "@intentius/chant/resource-attributes";
24
+ import { isCondition } from "./condition";
24
25
  import { isDefaultTags, type TagEntry } from "./default-tags";
25
26
  import { isTemplateTransform } from "./template-transform";
26
27
  import { loadTaggableResources } from "./taggable";
@@ -41,6 +42,7 @@ interface CFTemplate {
41
42
  Metadata?: Record<string, unknown>;
42
43
  Transform?: string | string[];
43
44
  Parameters?: Record<string, CFParameter>;
45
+ Conditions?: Record<string, unknown>;
44
46
  Resources: Record<string, CFResource>;
45
47
  Outputs?: Record<string, CFOutput>;
46
48
  }
@@ -347,6 +349,13 @@ function serializeToTemplate(
347
349
  }
348
350
 
349
351
  template.Parameters[name] = param;
352
+ } else if (isCondition(entity)) {
353
+ // Condition declarable → Conditions section (#2068), lifted the way
354
+ // Parameter is lifted into Parameters above.
355
+ if (!template.Conditions) {
356
+ template.Conditions = {};
357
+ }
358
+ template.Conditions[name] = toCFValue(entity.expression, entityNames);
350
359
  } else if (isChildProject(entity)) {
351
360
  // ChildProjectInstance → AWS::CloudFormation::Stack resource
352
361
  const childProject = entity as ChildProjectInstance;
@@ -396,8 +405,13 @@ function serializeToTemplate(
396
405
  resource.DependsOn = resolved.length === 1 ? resolved[0] : resolved;
397
406
  }
398
407
  }
399
- // Pass-through attributes
400
- if (attrs.Condition) resource.Condition = attrs.Condition as string;
408
+ // Pass-through attributes. Condition accepts the Condition declarable
409
+ // as well as a name string (#2068).
410
+ if (attrs.Condition) {
411
+ resource.Condition = isCondition(attrs.Condition)
412
+ ? entityNames.get(attrs.Condition) ?? getLogicalName(attrs.Condition)
413
+ : attrs.Condition as string;
414
+ }
401
415
  if (attrs.DeletionPolicy) resource.DeletionPolicy = attrs.DeletionPolicy as string;
402
416
  if (attrs.UpdateReplacePolicy) resource.UpdateReplacePolicy = attrs.UpdateReplacePolicy as string;
403
417
  if (attrs.UpdatePolicy) resource.UpdatePolicy = attrs.UpdatePolicy;
@@ -471,6 +485,13 @@ function serializeToTemplate(
471
485
  if (exportName) {
472
486
  output.Export = { Name: exportName };
473
487
  }
488
+ // Same defensive read: condition (#2068/#2069) postdates older cores.
489
+ const condition = (stackOutput as { condition?: unknown }).condition;
490
+ if (condition) {
491
+ output.Condition = isCondition(condition)
492
+ ? entityNames.get(condition) ?? getLogicalName(condition)
493
+ : condition as string;
494
+ }
474
495
  template.Outputs[name] = output;
475
496
  }
476
497
  }
@@ -0,0 +1,16 @@
1
+ {
2
+ "AWSTemplateFormatVersion": "2010-09-09",
3
+ "Parameters": { "Cutover": { "Type": "String", "Default": "false", "AllowedValues": ["true", "false"] } },
4
+ "Conditions": {
5
+ "DoCutover": { "Fn::Equals": [ { "Ref": "Cutover" }, "true" ] },
6
+ "NoCutover": { "Fn::Not": [ { "Condition": "DoCutover" } ] }
7
+ },
8
+ "Resources": {
9
+ "Rule": {
10
+ "Type": "AWS::Logs::LogGroup",
11
+ "Condition": "DoCutover",
12
+ "Properties": { "LogGroupName": { "Fn::If": [ "DoCutover", "/cut/on", "/cut/off" ] }, "RetentionInDays": 7 }
13
+ }
14
+ },
15
+ "Outputs": { "RuleName": { "Condition": "DoCutover", "Value": { "Ref": "Rule" } } }
16
+ }