@intentius/chant 0.20.0 → 0.21.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 (45) hide show
  1. package/dist/cli/commands/check-lexicon-intrinsics.d.ts +17 -0
  2. package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -1
  3. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  4. package/dist/codegen/docs-types.d.ts +2 -0
  5. package/dist/codegen/docs-types.d.ts.map +1 -1
  6. package/dist/declarable.d.ts +16 -0
  7. package/dist/declarable.d.ts.map +1 -1
  8. package/dist/discovery/entity-wire-codec.d.ts.map +1 -1
  9. package/dist/discovery/fold-import.d.ts +30 -1
  10. package/dist/discovery/fold-import.d.ts.map +1 -1
  11. package/dist/discovery/index.d.ts.map +1 -1
  12. package/dist/fold/fold.d.ts +89 -16
  13. package/dist/fold/fold.d.ts.map +1 -1
  14. package/dist/fold/foldable-helpers.d.ts +121 -0
  15. package/dist/fold/foldable-helpers.d.ts.map +1 -0
  16. package/dist/fold/subset.d.ts +35 -3
  17. package/dist/fold/subset.d.ts.map +1 -1
  18. package/dist/lexicon-schema.d.ts +2 -0
  19. package/dist/lexicon-schema.d.ts.map +1 -1
  20. package/dist/lexicon.d.ts +73 -23
  21. package/dist/lexicon.d.ts.map +1 -1
  22. package/dist/runtime.d.ts +10 -1
  23. package/dist/runtime.d.ts.map +1 -1
  24. package/package.json +1 -1
  25. package/src/cli/commands/check-lexicon-intrinsics.test.ts +35 -1
  26. package/src/cli/commands/check-lexicon-intrinsics.ts +38 -2
  27. package/src/cli/commands/check-lexicon.ts +18 -0
  28. package/src/codegen/docs-sections.test.ts +7 -1
  29. package/src/codegen/docs-sections.ts +1 -1
  30. package/src/codegen/docs-types.ts +2 -0
  31. package/src/declarable.ts +20 -0
  32. package/src/discovery/entity-wire-codec.ts +9 -7
  33. package/src/discovery/fold-import.test.ts +572 -0
  34. package/src/discovery/fold-import.ts +229 -36
  35. package/src/discovery/index.ts +9 -0
  36. package/src/fold/fold.test.ts +277 -0
  37. package/src/fold/fold.ts +213 -56
  38. package/src/fold/foldable-helpers.ts +171 -0
  39. package/src/fold/subset-doc-parity.test.ts +27 -0
  40. package/src/fold/subset.test.ts +111 -0
  41. package/src/fold/subset.ts +109 -28
  42. package/src/lexicon-schema.test.ts +43 -0
  43. package/src/lexicon-schema.ts +5 -0
  44. package/src/lexicon.ts +74 -24
  45. package/src/runtime.ts +11 -2
@@ -1024,3 +1024,575 @@ describe("tryFoldFile — build-time parameters (chant #1064)", () => {
1024
1024
  setBuildParams({});
1025
1025
  });
1026
1026
  });
1027
+
1028
+ /**
1029
+ * chant #1082 — registered authoring helpers, and constructor argument
1030
+ * positions.
1031
+ *
1032
+ * Fixtures import chant-core's REAL helpers by absolute path (the convention
1033
+ * every other suite in this file uses), which is also the second arm of the
1034
+ * provenance check: a specifier that resolves inside chant-core's own tree
1035
+ * counts as chant's own, exactly like the `@intentius/chant*` package
1036
+ * specifier a real project writes.
1037
+ */
1038
+ describe("tryFoldFile — registered authoring helpers (#1082)", () => {
1039
+ let testDir: string;
1040
+
1041
+ beforeEach(async () => {
1042
+ testDir = join(tmpdir(), `chant-fold-import-helpers-test-${Date.now()}-${Math.random()}`);
1043
+ await mkdir(testDir, { recursive: true });
1044
+ });
1045
+
1046
+ afterEach(async () => {
1047
+ await rm(testDir, { recursive: true, force: true });
1048
+ });
1049
+
1050
+ /** Absolute path to the real component-authoring helpers (`phase`, `gate`, `stackOutput`). */
1051
+ const componentPath = resolve(thisDir, "../components/component");
1052
+ /** Absolute path to the real `output()` / `LexiconOutput`. */
1053
+ const lexiconOutputPath = resolve(thisDir, "../lexicon-output");
1054
+
1055
+ test("a component authored with phase()/gate()/stackOutput() folds to the real plain data — zero module execution", async () => {
1056
+ const file = join(testDir, "web.component.ts");
1057
+ await writeFile(
1058
+ file,
1059
+ `
1060
+ import { phase, gate, stackOutput } from ${JSON.stringify(componentPath)};
1061
+ throw new Error("must never execute — sentinel for #1082 fold verification");
1062
+ export const web = {
1063
+ name: "web",
1064
+ dependsOn: ["shared-foundation"],
1065
+ deploy: [
1066
+ phase("Apply", [
1067
+ { kind: "cfn-deploy", stack: "web", inputs: { pVpcId: stackOutput("shared-foundation", "oVpcId") } },
1068
+ gate("approve", { timeout: "24h" }),
1069
+ ], { parallel: true }),
1070
+ ],
1071
+ };
1072
+ `,
1073
+ );
1074
+
1075
+ const result = await tryFoldFile(file);
1076
+
1077
+ expect(result.ok).toBe(true);
1078
+ if (!result.ok) return;
1079
+ // The revived value is what the real helpers return, not an envelope:
1080
+ // `phase()`'s `{ phase, steps, parallel }`, `gate()`'s `{ kind: "gate", … }`,
1081
+ // `stackOutput()`'s `{ stackOutput: { stack, name } }`.
1082
+ expect(result.exportedValues.get("web")).toEqual({
1083
+ name: "web",
1084
+ dependsOn: ["shared-foundation"],
1085
+ deploy: [
1086
+ {
1087
+ phase: "Apply",
1088
+ parallel: true,
1089
+ steps: [
1090
+ {
1091
+ kind: "cfn-deploy",
1092
+ stack: "web",
1093
+ inputs: { pVpcId: { stackOutput: { stack: "shared-foundation", name: "oVpcId" } } },
1094
+ },
1095
+ { kind: "gate", signalName: "approve", timeout: "24h" },
1096
+ ],
1097
+ },
1098
+ ],
1099
+ });
1100
+ });
1101
+
1102
+ test("a nested (fan-out) phase folds — helper envelopes revive bottom-up", async () => {
1103
+ const file = join(testDir, "fanout.component.ts");
1104
+ await writeFile(
1105
+ file,
1106
+ `
1107
+ import { phase } from ${JSON.stringify(componentPath)};
1108
+ export const fanout = { deploy: [phase("Outer", [phase("Inner", [{ kind: "noop" }])])] };
1109
+ `,
1110
+ );
1111
+
1112
+ const result = await tryFoldFile(file);
1113
+
1114
+ expect(result.ok).toBe(true);
1115
+ if (!result.ok) return;
1116
+ expect(result.exportedValues.get("fanout")).toEqual({
1117
+ deploy: [{ phase: "Outer", steps: [{ phase: "Inner", steps: [{ kind: "noop" }] }] }],
1118
+ });
1119
+ });
1120
+
1121
+ test("a registered NAME imported from the project's own code does NOT fold — the allowlist is chant's, not the name's", async () => {
1122
+ await writeFile(
1123
+ join(testDir, "helpers.ts"),
1124
+ `export function phase(name, steps) { return { phase: name, steps, mine: true }; }`,
1125
+ );
1126
+ const file = join(testDir, "web.component.ts");
1127
+ await writeFile(
1128
+ file,
1129
+ `
1130
+ import { phase } from "./helpers";
1131
+ export const web = { deploy: [phase("Apply", [])] };
1132
+ `,
1133
+ );
1134
+
1135
+ const result = await tryFoldFile(file);
1136
+
1137
+ expect(result.ok).toBe(false);
1138
+ if (result.ok) return;
1139
+ expect(result.reason).toContain("./helpers");
1140
+ expect(result.reason).toContain("not chant's own");
1141
+ });
1142
+
1143
+ test("a registered name declared in the file itself does NOT fold — it is not an import at all", async () => {
1144
+ const file = join(testDir, "local.component.ts");
1145
+ await writeFile(
1146
+ file,
1147
+ `
1148
+ function phase(name, steps) { return { phase: name, steps }; }
1149
+ export const web = { deploy: [phase("Apply", [])] };
1150
+ `,
1151
+ );
1152
+
1153
+ const result = await tryFoldFile(file);
1154
+
1155
+ expect(result.ok).toBe(false);
1156
+ if (result.ok) return;
1157
+ expect(result.reason).toContain("is not a resolvable import");
1158
+ });
1159
+
1160
+ test("an UNREGISTERED chant import called as a value still falls back — registration is per-helper, not per-module", async () => {
1161
+ const file = join(testDir, "unregistered.component.ts");
1162
+ await writeFile(
1163
+ file,
1164
+ `
1165
+ import { inferArchetype } from ${JSON.stringify(componentPath)};
1166
+ export const web = { archetype: inferArchetype({ deploy: [] }) };
1167
+ `,
1168
+ );
1169
+
1170
+ const result = await tryFoldFile(file);
1171
+
1172
+ expect(result.ok).toBe(false);
1173
+ if (result.ok) return;
1174
+ expect(result.reason).toContain("inferArchetype(...)");
1175
+ });
1176
+
1177
+ test("output() folds against a REAL cross-file AttrRef, producing a genuine LexiconOutput", async () => {
1178
+ await writeFile(
1179
+ join(testDir, "defs.ts"),
1180
+ `
1181
+ import { createResource } from ${JSON.stringify(runtimePath)};
1182
+ export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
1183
+ `,
1184
+ );
1185
+ await writeFile(
1186
+ join(testDir, "resources.ts"),
1187
+ `
1188
+ import { Bucket } from "./defs";
1189
+ export const bucket = new Bucket({ name: "my-bucket" });
1190
+ `,
1191
+ );
1192
+ const file = join(testDir, "outputs.ts");
1193
+ await writeFile(
1194
+ file,
1195
+ `
1196
+ import { output } from ${JSON.stringify(lexiconOutputPath)};
1197
+ import { bucket } from "./resources";
1198
+ export const oArn = bucket ? output(bucket.arn, "oArn") : undefined;
1199
+ `,
1200
+ );
1201
+
1202
+ const result = await tryFoldFile(file);
1203
+
1204
+ expect(result.ok).toBe(true);
1205
+ if (!result.ok) return;
1206
+ const oArn = result.exportedValues.get("oArn") as { outputName: string; sourceLexicon: string };
1207
+ // A genuine LexiconOutput built from the real AttrRef — it read the ref's
1208
+ // parent through its WeakRef to learn the lexicon, which is only possible
1209
+ // with a live reference, never with a `{ __attrRef }` envelope.
1210
+ expect(oArn.outputName).toBe("oArn");
1211
+ expect(oArn.sourceLexicon).toBe("aws");
1212
+ });
1213
+
1214
+ test("output() over a SAME-FILE resource reference falls back rather than wrapping a symbolic envelope", async () => {
1215
+ await writeFile(
1216
+ join(testDir, "defs.ts"),
1217
+ `
1218
+ import { createResource } from ${JSON.stringify(runtimePath)};
1219
+ export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
1220
+ `,
1221
+ );
1222
+ const file = join(testDir, "same-file-outputs.ts");
1223
+ await writeFile(
1224
+ file,
1225
+ `
1226
+ import { Bucket } from "./defs";
1227
+ import { output } from ${JSON.stringify(lexiconOutputPath)};
1228
+ const enabled = true;
1229
+ export const bucket = new Bucket({ name: "my-bucket" });
1230
+ export const oArn = enabled ? output(bucket.arn, "oArn") : undefined;
1231
+ `,
1232
+ );
1233
+
1234
+ const result = await tryFoldFile(file);
1235
+
1236
+ // `bucket.arn` folds to a `{ __attrRef }` envelope, and `LexiconOutput`'s
1237
+ // constructor needs a real `AttrRef` (it derefs the parent to learn the
1238
+ // lexicon). Constructing it from the envelope would silently produce a
1239
+ // wrong output, so the whole file falls back to run instead.
1240
+ expect(result.ok).toBe(false);
1241
+ if (result.ok) return;
1242
+ expect(result.reason).toContain("same-file resource reference");
1243
+ });
1244
+ });
1245
+
1246
+ describe("tryFoldFile — constructor argument positions (#1082)", () => {
1247
+ let testDir: string;
1248
+
1249
+ beforeEach(async () => {
1250
+ testDir = join(tmpdir(), `chant-fold-import-ctorargs-test-${Date.now()}-${Math.random()}`);
1251
+ await mkdir(testDir, { recursive: true });
1252
+ });
1253
+
1254
+ afterEach(async () => {
1255
+ await rm(testDir, { recursive: true, force: true });
1256
+ });
1257
+
1258
+ /** A `(type, props)` constructor, structurally identical to the real AWS deploy-time `Parameter` (lexicons/aws/src/parameter.ts). */
1259
+ async function writeParameterDef(): Promise<void> {
1260
+ await writeFile(
1261
+ join(testDir, "parameter.ts"),
1262
+ `
1263
+ import { DECLARABLE_MARKER } from ${JSON.stringify(resolve(thisDir, "../declarable"))};
1264
+
1265
+ export class Parameter {
1266
+ constructor(type, options) {
1267
+ this[DECLARABLE_MARKER] = true;
1268
+ this.lexicon = "aws";
1269
+ this.entityType = "AWS::CloudFormation::Parameter";
1270
+ this.parameterType = type;
1271
+ this.description = options?.description;
1272
+ this.defaultValue = options?.defaultValue;
1273
+ }
1274
+ }
1275
+ `,
1276
+ );
1277
+ }
1278
+
1279
+ test("`new Parameter(\"String\", {...})` folds — the props object need not be the first argument", async () => {
1280
+ await writeParameterDef();
1281
+ const file = join(testDir, "params.ts");
1282
+ await writeFile(
1283
+ file,
1284
+ `
1285
+ import { Parameter } from "./parameter";
1286
+ throw new Error("must never execute — sentinel for #1082 fold verification");
1287
+ export const pVpcId = new Parameter("AWS::EC2::VPC::Id", { description: "vpc id" });
1288
+ `,
1289
+ );
1290
+
1291
+ const result = await tryFoldFile(file);
1292
+
1293
+ expect(result.ok).toBe(true);
1294
+ if (!result.ok) return;
1295
+ expect(result.entities).toHaveLength(1);
1296
+ const [name, entity] = result.entities[0];
1297
+ expect(name).toBe("pVpcId");
1298
+ if (!isDeclarable(entity)) throw new Error("expected a Declarable");
1299
+ // Both arguments reached the real constructor, in the right positions.
1300
+ expect((entity as unknown as { parameterType: string }).parameterType).toBe("AWS::EC2::VPC::Id");
1301
+ expect((entity as unknown as { description: string }).description).toBe("vpc id");
1302
+ });
1303
+
1304
+ test("a constructor called with only a non-object argument folds too — no props object is invented", async () => {
1305
+ await writeParameterDef();
1306
+ const file = join(testDir, "params.ts");
1307
+ await writeFile(
1308
+ file,
1309
+ `
1310
+ import { Parameter } from "./parameter";
1311
+ export const pName = new Parameter("String");
1312
+ `,
1313
+ );
1314
+
1315
+ const result = await tryFoldFile(file);
1316
+
1317
+ expect(result.ok).toBe(true);
1318
+ if (!result.ok) return;
1319
+ const [, entity] = result.entities[0];
1320
+ expect((entity as unknown as { parameterType: string }).parameterType).toBe("String");
1321
+ expect((entity as unknown as { description?: string }).description).toBeUndefined();
1322
+ });
1323
+
1324
+ test("a non-foldable argument in any position still falls the file back to run", async () => {
1325
+ await writeParameterDef();
1326
+ const file = join(testDir, "params.ts");
1327
+ await writeFile(
1328
+ file,
1329
+ `
1330
+ import { Parameter } from "./parameter";
1331
+ export const pName = new Parameter(computeType(), { description: "x" });
1332
+ `,
1333
+ );
1334
+
1335
+ const result = await tryFoldFile(file);
1336
+
1337
+ expect(result.ok).toBe(false);
1338
+ if (result.ok) return;
1339
+ expect(result.reason).toContain("computeType(...)");
1340
+ });
1341
+ });
1342
+
1343
+ /**
1344
+ * chant #1044 — registered lexicon intrinsics in PLAIN-CALL form, end to end.
1345
+ *
1346
+ * The unit tests in ../fold/fold.test.ts cover the reducer's half (a call
1347
+ * reduces to a `{__intrinsic, args}` envelope, executing nothing). These
1348
+ * cover the other half: the envelope is revived by resolving the name
1349
+ * through THIS FILE'S OWN imports and invoking the real function, so what
1350
+ * lands in a resource's props is a genuine live intrinsic instance — the
1351
+ * same object the run path would have built.
1352
+ */
1353
+ describe("tryFoldFile — registered call-form intrinsics (#1044)", () => {
1354
+ let testDir: string;
1355
+
1356
+ beforeEach(async () => {
1357
+ testDir = join(tmpdir(), `chant-fold-import-callintrinsic-test-${Date.now()}-${Math.random()}`);
1358
+ await mkdir(testDir, { recursive: true });
1359
+ });
1360
+
1361
+ afterEach(async () => {
1362
+ await rm(testDir, { recursive: true, force: true });
1363
+ });
1364
+
1365
+ const REF: IntrinsicDef = { name: "Ref", isTag: false, foldsAsCall: true, outputKey: "Test::Ref" };
1366
+ const NOT_OPTED_IN: IntrinsicDef = { name: "Ref", isTag: false };
1367
+
1368
+ /** A plain-call intrinsic shaped exactly like aws's real `Ref`: a factory returning a class that implements the `Intrinsic` contract and resolves its target at `toJSON()` time. */
1369
+ async function writeCallIntrinsicDefs(): Promise<void> {
1370
+ await writeFile(
1371
+ join(testDir, "intrinsics.ts"),
1372
+ `
1373
+ import { INTRINSIC_MARKER } from ${JSON.stringify(intrinsicPath)};
1374
+
1375
+ export class RefIntrinsic {
1376
+ constructor(target) {
1377
+ this[INTRINSIC_MARKER] = true;
1378
+ this.target = target;
1379
+ }
1380
+ toJSON() {
1381
+ return { "Test::Ref": typeof this.target === "string" ? this.target : this.target.logicalHint };
1382
+ }
1383
+ }
1384
+
1385
+ export function Ref(target) {
1386
+ return new RefIntrinsic(target);
1387
+ }
1388
+
1389
+ export const NS = { Region: "NS::Region" };
1390
+ `,
1391
+ );
1392
+ }
1393
+
1394
+ test("an opted-in call folds end-to-end to the real intrinsic instance — zero module execution", async () => {
1395
+ await writeCallIntrinsicDefs();
1396
+ await writeFile(
1397
+ join(testDir, "resources.ts"),
1398
+ `
1399
+ import { createResource } from ${JSON.stringify(runtimePath)};
1400
+ export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
1401
+ `,
1402
+ );
1403
+ const file = join(testDir, "main.ts");
1404
+ await writeFile(
1405
+ file,
1406
+ `
1407
+ import { Bucket } from "./resources";
1408
+ import { Ref } from "./intrinsics";
1409
+ throw new Error("must never execute — sentinel for #1044 fold verification");
1410
+ export const bucket = new Bucket({ name: Ref("environment") });
1411
+ `,
1412
+ );
1413
+
1414
+ const result = await tryFoldFile(file, [REF]);
1415
+
1416
+ expect(result.ok).toBe(true);
1417
+ if (!result.ok) return;
1418
+ const [, entity] = result.entities[0];
1419
+ if (!isDeclarable(entity)) throw new Error("expected a Declarable");
1420
+ const name = (entity as unknown as { props: { name: unknown } }).props.name;
1421
+ // A genuine live instance built by the file's own `Ref`, not the
1422
+ // `{ __intrinsic, args }` envelope the reducer produced internally.
1423
+ expect((name as { toJSON(): unknown }).toJSON()).toEqual({ "Test::Ref": "environment" });
1424
+ });
1425
+
1426
+ test("a call-form intrinsic folds inside a registered tag's interpolation, and a symbolic argument resolves through the file's own import", async () => {
1427
+ await writeFile(
1428
+ join(testDir, "intrinsics.ts"),
1429
+ `
1430
+ import { INTRINSIC_MARKER } from ${JSON.stringify(intrinsicPath)};
1431
+ export class RefIntrinsic {
1432
+ constructor(target) { this[INTRINSIC_MARKER] = true; this.target = target; }
1433
+ toJSON() { return { "Test::Ref": this.target }; }
1434
+ }
1435
+ export function Ref(target) { return new RefIntrinsic(target); }
1436
+ export class SubIntrinsic {
1437
+ constructor(strings, values) { this[INTRINSIC_MARKER] = true; this.strings = strings; this.values = values; }
1438
+ toJSON() {
1439
+ let out = "";
1440
+ for (let i = 0; i < this.strings.length; i++) {
1441
+ out += this.strings[i];
1442
+ if (i < this.values.length) out += JSON.stringify(this.values[i].toJSON ? this.values[i].toJSON() : this.values[i]);
1443
+ }
1444
+ return { "Test::Sub": out };
1445
+ }
1446
+ }
1447
+ export function Sub(strings, ...values) { return new SubIntrinsic(strings, values); }
1448
+ export const NS = { Region: "NS::Region" };
1449
+ `,
1450
+ );
1451
+ await writeFile(
1452
+ join(testDir, "resources.ts"),
1453
+ `
1454
+ import { createResource } from ${JSON.stringify(runtimePath)};
1455
+ export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
1456
+ `,
1457
+ );
1458
+ const file = join(testDir, "main.ts");
1459
+ await writeFile(
1460
+ file,
1461
+ `
1462
+ import { Bucket } from "./resources";
1463
+ import { Sub, Ref, NS } from "./intrinsics";
1464
+ export const bucket = new Bucket({ name: Sub\`\${Ref(NS.Region)}-fn\` });
1465
+ `,
1466
+ );
1467
+
1468
+ const result = await tryFoldFile(file, [REF, { name: "Sub", isTag: true }]);
1469
+
1470
+ expect(result.ok).toBe(true);
1471
+ if (!result.ok) return;
1472
+ const [, entity] = result.entities[0];
1473
+ if (!isDeclarable(entity)) throw new Error("expected a Declarable");
1474
+ const name = (entity as unknown as { props: { name: unknown } }).props.name;
1475
+ expect((name as { toJSON(): unknown }).toJSON()).toEqual({
1476
+ "Test::Sub": `{"Test::Ref":"NS::Region"}-fn`,
1477
+ });
1478
+ });
1479
+
1480
+ test("a cross-file resource passed to an intrinsic call arrives as the SHARED live instance, not a re-imported copy", async () => {
1481
+ await writeCallIntrinsicDefs();
1482
+ await writeFile(
1483
+ join(testDir, "resources.ts"),
1484
+ `
1485
+ import { createResource } from ${JSON.stringify(runtimePath)};
1486
+ export const Param = createResource("Test::Param", "aws", {});
1487
+ `,
1488
+ );
1489
+ await writeFile(
1490
+ join(testDir, "params.ts"),
1491
+ `
1492
+ import { Param } from "./resources";
1493
+ export const environment = new Param({ logicalHint: "EnvParam" });
1494
+ `,
1495
+ );
1496
+ const file = join(testDir, "main.ts");
1497
+ await writeFile(
1498
+ file,
1499
+ `
1500
+ import { Param } from "./resources";
1501
+ import { environment } from "./params";
1502
+ import { Ref } from "./intrinsics";
1503
+ export const other = new Param({ name: Ref(environment) });
1504
+ `,
1505
+ );
1506
+
1507
+ const session = createFoldSession([REF]);
1508
+ const paramsResult = await tryFoldFile(join(testDir, "params.ts"), [REF], session);
1509
+ const result = await tryFoldFile(file, [REF], session);
1510
+
1511
+ expect(result.ok).toBe(true);
1512
+ if (!result.ok || !paramsResult.ok) return;
1513
+ const [, entity] = result.entities[0];
1514
+ if (!isDeclarable(entity)) throw new Error("expected a Declarable");
1515
+ const ref = (entity as unknown as { props: { name: { target: unknown } } }).props.name;
1516
+ // Identity, not equality: the SAME object params.ts's own fold produced
1517
+ // and discovery will collect. A second instance would serialize with no
1518
+ // logical name at all (see planFoldTaint's doc).
1519
+ expect(ref.target).toBe(paramsResult.exportedValues.get("environment"));
1520
+ });
1521
+
1522
+ test("a REGISTERED intrinsic with no opt-in still falls the file back to run", async () => {
1523
+ await writeCallIntrinsicDefs();
1524
+ await writeFile(
1525
+ join(testDir, "resources.ts"),
1526
+ `
1527
+ import { createResource } from ${JSON.stringify(runtimePath)};
1528
+ export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
1529
+ `,
1530
+ );
1531
+ const file = join(testDir, "main.ts");
1532
+ await writeFile(
1533
+ file,
1534
+ `
1535
+ import { Bucket } from "./resources";
1536
+ import { Ref } from "./intrinsics";
1537
+ export const bucket = new Bucket({ name: Ref("environment") });
1538
+ `,
1539
+ );
1540
+
1541
+ const result = await tryFoldFile(file, [NOT_OPTED_IN]);
1542
+
1543
+ expect(result.ok).toBe(false);
1544
+ if (result.ok) return;
1545
+ expect(result.reason).toContain("function call as a value is not foldable: Ref(...)");
1546
+ });
1547
+
1548
+ test("an opted-in NAME that this file never imported falls back — the registry is not permission to invoke a name", async () => {
1549
+ await writeFile(
1550
+ join(testDir, "resources.ts"),
1551
+ `
1552
+ import { createResource } from ${JSON.stringify(runtimePath)};
1553
+ export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
1554
+ `,
1555
+ );
1556
+ const file = join(testDir, "main.ts");
1557
+ await writeFile(
1558
+ file,
1559
+ `
1560
+ import { Bucket } from "./resources";
1561
+ export const bucket = new Bucket({ name: Ref("environment") });
1562
+ `,
1563
+ );
1564
+
1565
+ const result = await tryFoldFile(file, [REF]);
1566
+
1567
+ expect(result.ok).toBe(false);
1568
+ if (result.ok) return;
1569
+ expect(result.reason).toContain(`"Ref" is not a resolvable import`);
1570
+ });
1571
+
1572
+ test("a same-file resource reference passed to an intrinsic call falls back rather than folding to the wrong value", async () => {
1573
+ await writeCallIntrinsicDefs();
1574
+ await writeFile(
1575
+ join(testDir, "resources.ts"),
1576
+ `
1577
+ import { createResource } from ${JSON.stringify(runtimePath)};
1578
+ export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
1579
+ `,
1580
+ );
1581
+ const file = join(testDir, "main.ts");
1582
+ await writeFile(
1583
+ file,
1584
+ `
1585
+ import { Bucket } from "./resources";
1586
+ import { Ref } from "./intrinsics";
1587
+ export const source = new Bucket({ name: "src" });
1588
+ export const bucket = new Bucket({ name: Ref(source.arn) });
1589
+ `,
1590
+ );
1591
+
1592
+ const result = await tryFoldFile(file, [REF]);
1593
+
1594
+ expect(result.ok).toBe(false);
1595
+ if (result.ok) return;
1596
+ expect(result.reason).toContain("same-file resource reference");
1597
+ });
1598
+ });