@intentius/chant 0.108.1 → 0.110.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.
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -0
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +59 -4
- package/dist/discovery/param-deps.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +17 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold-provenance.d.ts +49 -4
- package/dist/fold-provenance.d.ts.map +1 -1
- package/dist/import/ir-utils.d.ts.map +1 -1
- package/dist/import/parser.d.ts +8 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/provenance.d.ts +49 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/serializer-walker.d.ts.map +1 -1
- package/dist/stack-output.d.ts +5 -0
- package/dist/stack-output.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/import.test.ts +5 -4
- package/src/cli/handlers/lifecycle.test.ts +106 -1
- package/src/cli/handlers/lifecycle.ts +26 -5
- package/src/composite.ts +37 -0
- package/src/discovery/collect.ts +13 -3
- package/src/discovery/fold-composite.test.ts +22 -0
- package/src/discovery/fold-host-composite.test.ts +23 -0
- package/src/discovery/fold-import.ts +324 -30
- package/src/discovery/fold-package-composite.test.ts +14 -1
- package/src/discovery/fold-project-provenance.test.ts +584 -0
- package/src/discovery/param-deps.test.ts +163 -3
- package/src/discovery/param-deps.ts +173 -4
- package/src/fold/subset-public-export.test.ts +38 -0
- package/src/fold/subset.ts +17 -2
- package/src/fold-provenance.test.ts +190 -16
- package/src/fold-provenance.ts +158 -13
- package/src/import/ir-utils.ts +6 -1
- package/src/import/parser.ts +8 -0
- package/src/provenance.ts +59 -2
- package/src/serializer-walker.test.ts +10 -0
- package/src/serializer-walker.ts +17 -1
- package/src/stack-output.ts +6 -1
- package/src/yaml.test.ts +6 -0
- package/src/yaml.ts +4 -0
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
classifyFieldOrigin,
|
|
19
19
|
emittedFieldPaths,
|
|
20
20
|
foldProvenanceOfEntity,
|
|
21
|
+
describeCompositeArguments,
|
|
21
22
|
describeFoldFieldOrigin,
|
|
22
23
|
resolveDeepDrift,
|
|
23
24
|
resolveDriftedField,
|
|
@@ -76,7 +77,7 @@ describe("classifyFieldOrigin", () => {
|
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
test("a recorded composite literal says which composite fixes it", () => {
|
|
79
|
-
expect(classifyFieldOrigin({ kind: "composite-literal", composite: "WebService" }, composite)).
|
|
80
|
+
expect(classifyFieldOrigin({ kind: "composite-literal", composite: "WebService" }, composite)).toMatchObject({
|
|
80
81
|
kind: "composite-literal",
|
|
81
82
|
composite: "WebService",
|
|
82
83
|
instance: "web",
|
|
@@ -102,6 +103,17 @@ describe("classifyFieldOrigin", () => {
|
|
|
102
103
|
});
|
|
103
104
|
});
|
|
104
105
|
|
|
106
|
+
test("a host origin is unknown with that reason, never a direct declaration (typescript-as-data#248)", () => {
|
|
107
|
+
expect(classifyFieldOrigin({ kind: "host", reason: "host-call" }, authored)).toEqual({
|
|
108
|
+
kind: "unknown",
|
|
109
|
+
reason: "host-call",
|
|
110
|
+
});
|
|
111
|
+
expect(classifyFieldOrigin({ kind: "host", reason: "host-value" }, authored)).toEqual({
|
|
112
|
+
kind: "unknown",
|
|
113
|
+
reason: "host-value",
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
105
117
|
test("no origin on an entity no composite expanded is a direct declaration", () => {
|
|
106
118
|
expect(classifyFieldOrigin(undefined, authored)).toEqual({ kind: "direct" });
|
|
107
119
|
});
|
|
@@ -244,6 +256,126 @@ describe("resolveDriftedField", () => {
|
|
|
244
256
|
});
|
|
245
257
|
});
|
|
246
258
|
|
|
259
|
+
describe("the drift report's origin line (#3597)", () => {
|
|
260
|
+
const call = { file: "/p/src/app.ts", line: 10, column: 20 };
|
|
261
|
+
const compositeProv: EntityProvenance = {
|
|
262
|
+
sourceFile: "/p/src/app.ts",
|
|
263
|
+
composite: "WebApp",
|
|
264
|
+
compositeInstance: "web",
|
|
265
|
+
compositeCall: call,
|
|
266
|
+
};
|
|
267
|
+
const base = { entity: "webDeployment", entityType: "Deployment", root: "/p", declared: 3, live: 5 };
|
|
268
|
+
|
|
269
|
+
test("a composite parameter names the call, the argument as written, and its file and line", () => {
|
|
270
|
+
const resolved = resolveDriftedField({
|
|
271
|
+
...base,
|
|
272
|
+
path: "spec.replicas",
|
|
273
|
+
origin: {
|
|
274
|
+
kind: "composite-parameter",
|
|
275
|
+
composite: "WebApp",
|
|
276
|
+
parameters: ["replicas"],
|
|
277
|
+
arguments: [{ parameter: "replicas", file: "/p/src/app.ts", line: 12, column: 3, text: "replicas: 3" }],
|
|
278
|
+
},
|
|
279
|
+
provenance: compositeProv,
|
|
280
|
+
});
|
|
281
|
+
expect(resolved.summary).toBe(
|
|
282
|
+
"spec.replicas on Deployment webDeployment comes from WebApp({ replicas: 3 }) at src/app.ts:12",
|
|
283
|
+
);
|
|
284
|
+
expect(resolved.resolution.description).toContain("the `web` call of composite WebApp at src/app.ts:12");
|
|
285
|
+
// The origin object carries the locations, for --json.
|
|
286
|
+
expect(resolved.origin).toEqual({
|
|
287
|
+
kind: "composite-parameter",
|
|
288
|
+
composite: "WebApp",
|
|
289
|
+
instance: "web",
|
|
290
|
+
parameters: ["replicas"],
|
|
291
|
+
call,
|
|
292
|
+
arguments: [{ parameter: "replicas", file: "/p/src/app.ts", line: 12, column: 3, text: "replicas: 3" }],
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("a nested parameter is shown nested, and a parameter the call does not write says so", () => {
|
|
297
|
+
expect(describeCompositeArguments("WebService", [
|
|
298
|
+
{ parameter: "iam.path", file: "/p/a.ts", line: 4, column: 9, text: 'path: "/x/"' },
|
|
299
|
+
])).toBe('WebService({ iam: { path: "/x/" } })');
|
|
300
|
+
|
|
301
|
+
const resolved = resolveDriftedField({
|
|
302
|
+
...base,
|
|
303
|
+
path: "spec.template.spec.containers[#web].image",
|
|
304
|
+
origin: {
|
|
305
|
+
kind: "composite-parameter",
|
|
306
|
+
composite: "WebApp",
|
|
307
|
+
parameters: ["image"],
|
|
308
|
+
arguments: [{ parameter: "image", ...call }],
|
|
309
|
+
},
|
|
310
|
+
provenance: compositeProv,
|
|
311
|
+
});
|
|
312
|
+
expect(resolved.summary).toBe(
|
|
313
|
+
"spec.template.spec.containers[#web].image on Deployment webDeployment comes from WebApp(...) " +
|
|
314
|
+
"at src/app.ts:10 (parameter image, not written at the call)",
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("a composite literal says the composite fixes it, at the call", () => {
|
|
319
|
+
const resolved = resolveDriftedField({
|
|
320
|
+
...base,
|
|
321
|
+
path: "spec.strategy.type",
|
|
322
|
+
origin: { kind: "composite-literal", composite: "WebApp" },
|
|
323
|
+
provenance: compositeProv,
|
|
324
|
+
});
|
|
325
|
+
expect(resolved.summary).toBe(
|
|
326
|
+
"spec.strategy.type on Deployment webDeployment is fixed inside composite WebApp; " +
|
|
327
|
+
"no argument to the call at src/app.ts:10 moves it",
|
|
328
|
+
);
|
|
329
|
+
expect(resolved.resolution.description).toContain("fixed by the `web` call of composite WebApp at src/app.ts:10");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("a direct declaration says direct, and the resolution is today's", () => {
|
|
333
|
+
const resolved = resolveDriftedField({
|
|
334
|
+
...base,
|
|
335
|
+
entity: "worker",
|
|
336
|
+
path: "spec.replicas",
|
|
337
|
+
provenance: { sourceFile: "/p/src/app.ts" },
|
|
338
|
+
});
|
|
339
|
+
expect(resolved.origin).toEqual({ kind: "direct" });
|
|
340
|
+
expect(resolved.summary).toBe("spec.replicas on Deployment worker is declared directly in src/app.ts");
|
|
341
|
+
expect(resolved.resolution).toEqual({
|
|
342
|
+
kind: "edit-declaration",
|
|
343
|
+
sourceFile: "/p/src/app.ts",
|
|
344
|
+
description: "change the declared value of `spec.replicas` in src/app.ts from 3 to 5.",
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("an unknown origin says unknown and why, and never reads as direct", () => {
|
|
349
|
+
for (const [provenance, why] of [
|
|
350
|
+
[{ sourceFile: "/p/src/app.ts", composite: "WebApp", compositeInstance: "web" }, "did not interpret"],
|
|
351
|
+
[undefined, "recorded no provenance"],
|
|
352
|
+
] as const) {
|
|
353
|
+
const resolved = resolveDriftedField({ ...base, path: "spec.replicas", ...(provenance ? { provenance } : {}) });
|
|
354
|
+
expect(resolved.origin.kind).toBe("unknown");
|
|
355
|
+
expect(resolved.summary).toMatch(/^spec\.replicas on Deployment webDeployment has an unknown origin: /);
|
|
356
|
+
expect(resolved.summary).toContain(why);
|
|
357
|
+
expect(resolved.summary).not.toContain("direct");
|
|
358
|
+
expect(resolved.resolution.kind).toBe("fall-back");
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("a file outside the project root is printed as recorded", () => {
|
|
363
|
+
const resolved = resolveDriftedField({
|
|
364
|
+
...base,
|
|
365
|
+
root: "/elsewhere",
|
|
366
|
+
path: "spec.replicas",
|
|
367
|
+
origin: {
|
|
368
|
+
kind: "composite-parameter",
|
|
369
|
+
composite: "WebApp",
|
|
370
|
+
parameters: ["replicas"],
|
|
371
|
+
arguments: [{ parameter: "replicas", file: "/p/src/app.ts", line: 12, column: 3, text: "replicas: 3" }],
|
|
372
|
+
},
|
|
373
|
+
provenance: compositeProv,
|
|
374
|
+
});
|
|
375
|
+
expect(resolved.summary).toContain("at /p/src/app.ts:12");
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
247
379
|
describe("resolveDeepDrift", () => {
|
|
248
380
|
test("reads the drifted list and nothing else", () => {
|
|
249
381
|
const verdicts = resolveDeepDrift(
|
|
@@ -399,25 +531,25 @@ describe("fold provenance over a real composite build (#2161)", () => {
|
|
|
399
531
|
// 1. A field the composite PARAMETERIZES, including a nested parameter
|
|
400
532
|
// path, one read through a body `const`, and one read from inside an
|
|
401
533
|
// array literal.
|
|
402
|
-
expect(result.foldProvenance.webBucket.fields.BucketName).
|
|
534
|
+
expect(result.foldProvenance.webBucket.fields.BucketName).toMatchObject({
|
|
403
535
|
kind: "composite-parameter",
|
|
404
536
|
composite: "WebService",
|
|
405
537
|
instance: "web",
|
|
406
538
|
parameters: ["name"],
|
|
407
539
|
});
|
|
408
|
-
expect(result.foldProvenance.webBucket.fields.Tags).
|
|
540
|
+
expect(result.foldProvenance.webBucket.fields.Tags).toMatchObject({
|
|
409
541
|
kind: "composite-parameter",
|
|
410
542
|
composite: "WebService",
|
|
411
543
|
instance: "web",
|
|
412
544
|
parameters: ["tier"],
|
|
413
545
|
});
|
|
414
|
-
expect(result.foldProvenance.webRole.fields.RoleName).
|
|
546
|
+
expect(result.foldProvenance.webRole.fields.RoleName).toMatchObject({
|
|
415
547
|
kind: "composite-parameter",
|
|
416
548
|
composite: "WebService",
|
|
417
549
|
instance: "web",
|
|
418
550
|
parameters: ["name"],
|
|
419
551
|
});
|
|
420
|
-
expect(result.foldProvenance.webRole.fields.Path).
|
|
552
|
+
expect(result.foldProvenance.webRole.fields.Path).toMatchObject({
|
|
421
553
|
kind: "composite-parameter",
|
|
422
554
|
composite: "WebService",
|
|
423
555
|
instance: "web",
|
|
@@ -425,12 +557,12 @@ describe("fold provenance over a real composite build (#2161)", () => {
|
|
|
425
557
|
});
|
|
426
558
|
|
|
427
559
|
// 2. A field the composite FIXES.
|
|
428
|
-
expect(result.foldProvenance.webBucket.fields["VersioningConfiguration.Status"]).
|
|
560
|
+
expect(result.foldProvenance.webBucket.fields["VersioningConfiguration.Status"]).toMatchObject({
|
|
429
561
|
kind: "composite-literal",
|
|
430
562
|
composite: "WebService",
|
|
431
563
|
instance: "web",
|
|
432
564
|
});
|
|
433
|
-
expect(result.foldProvenance.webRole.fields.Description).
|
|
565
|
+
expect(result.foldProvenance.webRole.fields.Description).toMatchObject({
|
|
434
566
|
kind: "composite-literal",
|
|
435
567
|
composite: "WebService",
|
|
436
568
|
instance: "web",
|
|
@@ -507,6 +639,43 @@ describe("fold provenance over a real composite build (#2161)", () => {
|
|
|
507
639
|
// A direct declaration: today's behaviour, unchanged and unremarked.
|
|
508
640
|
expect(verdicts[3].resolution.kind).toBe("edit-declaration");
|
|
509
641
|
});
|
|
642
|
+
|
|
643
|
+
test("the build records where the composite call and each argument were written (#3597)", async () => {
|
|
644
|
+
const result = await build(srcDir, [namesSerializer], undefined, {
|
|
645
|
+
fold: true,
|
|
646
|
+
lexicons: [LEXICON_NAME],
|
|
647
|
+
});
|
|
648
|
+
expect(result.errors).toEqual([]);
|
|
649
|
+
const main = join(srcDir, "main.ts");
|
|
650
|
+
const lines = MAIN.split("\n");
|
|
651
|
+
const lineOf = (needle: string) => lines.findIndex((l) => l.includes(needle)) + 1;
|
|
652
|
+
const columnOf = (needle: string) => (lines.find((l) => l.includes(needle)) as string).indexOf(needle) + 1;
|
|
653
|
+
const call = { file: main, line: lineOf("WebService({"), column: columnOf("WebService({") };
|
|
654
|
+
|
|
655
|
+
expect(result.foldProvenance.webBucket.compositeCall).toEqual(call);
|
|
656
|
+
expect(result.foldProvenance.webRole.compositeCall).toEqual(call);
|
|
657
|
+
expect(result.foldProvenance.webBucket.fields.BucketName).toEqual({
|
|
658
|
+
kind: "composite-parameter",
|
|
659
|
+
composite: "WebService",
|
|
660
|
+
instance: "web",
|
|
661
|
+
parameters: ["name"],
|
|
662
|
+
call,
|
|
663
|
+
arguments: [{ parameter: "name", file: main, line: call.line, column: columnOf('name: "data"'), text: 'name: "data"' }],
|
|
664
|
+
});
|
|
665
|
+
expect(result.foldProvenance.webRole.fields.Path).toMatchObject({
|
|
666
|
+
arguments: [{ parameter: "iam.path", line: call.line, column: columnOf('path: "/service/"'), text: 'path: "/service/"' }],
|
|
667
|
+
});
|
|
668
|
+
expect(result.foldProvenance.webBucket.fields["VersioningConfiguration.Status"]).toEqual({
|
|
669
|
+
kind: "composite-literal",
|
|
670
|
+
composite: "WebService",
|
|
671
|
+
instance: "web",
|
|
672
|
+
call,
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// A direct declaration and an uninterpreted composite record no call.
|
|
676
|
+
expect(result.foldProvenance.logs.compositeCall).toBeUndefined();
|
|
677
|
+
expect(result.foldProvenance.legacyBucket.compositeCall).toBeUndefined();
|
|
678
|
+
});
|
|
510
679
|
});
|
|
511
680
|
|
|
512
681
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -604,18 +773,23 @@ describe("fold provenance through a tagged template in a composite factory (#321
|
|
|
604
773
|
const at = (parameters: string[]) => ({ kind: "composite-parameter", composite: "EventsTable", instance: "clicks", parameters });
|
|
605
774
|
const fixed = { kind: "composite-literal", composite: "EventsTable", instance: "clicks" };
|
|
606
775
|
|
|
607
|
-
expect(events.fields.name).
|
|
608
|
-
expect(events.fields.ttl).
|
|
609
|
-
expect(events.fields.ddl).
|
|
610
|
-
expect(events.fields.engine).
|
|
611
|
-
expect(events.fields.orderBy).
|
|
776
|
+
expect(events.fields.name).toMatchObject(at(["name"]));
|
|
777
|
+
expect(events.fields.ttl).toMatchObject(at(["ttlDays"]));
|
|
778
|
+
expect(events.fields.ddl).toMatchObject(at(["name", "ttlDays"]));
|
|
779
|
+
expect(events.fields.engine).toMatchObject(fixed);
|
|
780
|
+
expect(events.fields.orderBy).toMatchObject(fixed);
|
|
781
|
+
// #3597 — the argument behind a tag-fed field, where the call wrote it.
|
|
782
|
+
expect(events.fields.ttl).toMatchObject({ arguments: [{ parameter: "ttlDays", line: 6, text: "ttlDays: 30" }] });
|
|
783
|
+
expect(events.fields.ddl).toMatchObject({
|
|
784
|
+
arguments: [{ parameter: "name", text: 'name: "clicks"' }, { parameter: "ttlDays", text: "ttlDays: 30" }],
|
|
785
|
+
});
|
|
612
786
|
|
|
613
787
|
// Through a body `const`; an interpolated literal string is fixed; an
|
|
614
788
|
// interpolated sibling entity is wiring, so it governs nothing.
|
|
615
|
-
expect(rollup.fields.name).
|
|
616
|
-
expect(rollup.fields.engine).
|
|
617
|
-
expect(rollup.fields.comment).
|
|
618
|
-
expect(rollup.fields.ddl).
|
|
789
|
+
expect(rollup.fields.name).toMatchObject(at(["name"]));
|
|
790
|
+
expect(rollup.fields.engine).toMatchObject(fixed);
|
|
791
|
+
expect(rollup.fields.comment).toMatchObject(fixed);
|
|
792
|
+
expect(rollup.fields.ddl).toMatchObject(at(["name"]));
|
|
619
793
|
|
|
620
794
|
expect(result.foldProvenance.audit.fields.ttl).toBeUndefined();
|
|
621
795
|
expect(result.foldProvenance.audit.fields.name).toEqual({ kind: "direct" });
|
package/src/fold-provenance.ts
CHANGED
|
@@ -70,10 +70,21 @@
|
|
|
70
70
|
*/
|
|
71
71
|
|
|
72
72
|
import { isResourceDeclarable, type Declarable } from "./declarable";
|
|
73
|
-
import {
|
|
73
|
+
import { relative, isAbsolute } from "node:path";
|
|
74
|
+
import {
|
|
75
|
+
originOfPath,
|
|
76
|
+
type ArgumentLocation,
|
|
77
|
+
type EntityProvenance,
|
|
78
|
+
type PathOrigin,
|
|
79
|
+
type SourceLocation,
|
|
80
|
+
} from "./provenance";
|
|
74
81
|
import type { DeepEntityDrift, PropertyDrift } from "./lifecycle/deep-diff";
|
|
75
82
|
|
|
76
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Why a field's origin could not be determined. The first two are facts about
|
|
85
|
+
* the BUILD; the host reasons are facts about the expression, and match the
|
|
86
|
+
* strings the specification's reference implementation reports.
|
|
87
|
+
*/
|
|
77
88
|
export type UnknownOriginReason =
|
|
78
89
|
/**
|
|
79
90
|
* A composite expanded the entity and this build did not interpret its body,
|
|
@@ -87,7 +98,15 @@ export type UnknownOriginReason =
|
|
|
87
98
|
* composite make it" is answerable. A sandboxed child's entities arrive this
|
|
88
99
|
* way: `discovery/entity-wire-codec.ts` drops build metadata over the wire.
|
|
89
100
|
*/
|
|
90
|
-
| "no-provenance"
|
|
101
|
+
| "no-provenance"
|
|
102
|
+
/**
|
|
103
|
+
* INTENTIUS/typescript-as-data#248: host code computed the value, by a method
|
|
104
|
+
* called on a real receiver (`"x".toUpperCase()`, `[1, 2].join("-")`) or an
|
|
105
|
+
* eager lexicon helper. The file wrote the call, not the value.
|
|
106
|
+
*/
|
|
107
|
+
| "host-call"
|
|
108
|
+
/** A value a lexicon package exports, bound by an import (`AWS.Region`, `S3Actions.GetObject`). */
|
|
109
|
+
| "host-value";
|
|
91
110
|
|
|
92
111
|
/**
|
|
93
112
|
* What produced one emitted property path.
|
|
@@ -97,8 +116,17 @@ export type UnknownOriginReason =
|
|
|
97
116
|
* address and "the `api` call of `AutoscaledService`" is.
|
|
98
117
|
*/
|
|
99
118
|
export type FoldFieldOrigin =
|
|
100
|
-
| {
|
|
101
|
-
|
|
119
|
+
| {
|
|
120
|
+
kind: "composite-parameter";
|
|
121
|
+
composite: string;
|
|
122
|
+
instance?: string;
|
|
123
|
+
parameters: string[];
|
|
124
|
+
/** Where the composite call was written (chant #3597), when the build saw it. */
|
|
125
|
+
call?: SourceLocation;
|
|
126
|
+
/** Where each parameter's argument was written at that call, in `parameters` order. */
|
|
127
|
+
arguments?: ArgumentLocation[];
|
|
128
|
+
}
|
|
129
|
+
| { kind: "composite-literal"; composite: string; instance?: string; call?: SourceLocation }
|
|
102
130
|
| { kind: "direct" }
|
|
103
131
|
| { kind: "unknown"; reason: UnknownOriginReason };
|
|
104
132
|
|
|
@@ -110,6 +138,11 @@ export interface EntityFoldProvenance {
|
|
|
110
138
|
composite?: string;
|
|
111
139
|
/** The composite call (export name) it belongs to, when one expanded it. */
|
|
112
140
|
instance?: string;
|
|
141
|
+
/**
|
|
142
|
+
* Where that composite call was written (chant #3597), when the fold
|
|
143
|
+
* interpreted the factory at a call it saw as written.
|
|
144
|
+
*/
|
|
145
|
+
compositeCall?: SourceLocation;
|
|
113
146
|
/**
|
|
114
147
|
* Emitted property path to origin, in sorted key order. Every path the entity
|
|
115
148
|
* emits is present: a path with no attribution carries an `unknown` origin
|
|
@@ -178,6 +211,7 @@ export function classifyFieldOrigin(
|
|
|
178
211
|
): FoldFieldOrigin {
|
|
179
212
|
if (!provenance) return { kind: "unknown", reason: "no-provenance" };
|
|
180
213
|
const instance = provenance.compositeInstance;
|
|
214
|
+
const call = provenance.compositeCall;
|
|
181
215
|
|
|
182
216
|
switch (origin?.kind) {
|
|
183
217
|
case "composite-parameter":
|
|
@@ -186,15 +220,33 @@ export function classifyFieldOrigin(
|
|
|
186
220
|
composite: origin.composite,
|
|
187
221
|
...(instance ? { instance } : {}),
|
|
188
222
|
parameters: [...origin.parameters],
|
|
223
|
+
...(call ? { call: { ...call } } : {}),
|
|
224
|
+
...(origin.arguments ? { arguments: origin.arguments.map((a) => ({ ...a })) } : {}),
|
|
189
225
|
};
|
|
190
226
|
case "composite-literal":
|
|
191
|
-
return {
|
|
227
|
+
return {
|
|
228
|
+
kind: "composite-literal",
|
|
229
|
+
composite: origin.composite,
|
|
230
|
+
...(instance ? { instance } : {}),
|
|
231
|
+
...(call ? { call: { ...call } } : {}),
|
|
232
|
+
};
|
|
192
233
|
case "composite":
|
|
193
234
|
// The coarse kind: a composite expanded this, and nothing said which
|
|
194
235
|
// parameter. Not a parameter, not a fixed literal, and above all not a
|
|
195
236
|
// direct declaration.
|
|
196
237
|
return { kind: "unknown", reason: "composite-not-interpreted" };
|
|
197
238
|
case "authored":
|
|
239
|
+
// chant#3608 — `authored` is the file's own root record, and it reaches
|
|
240
|
+
// a path only when nothing finer did. On an entity a composite wrote (a
|
|
241
|
+
// member destructured from a composite the fold invoked) that means the
|
|
242
|
+
// fold could not attribute the path, which is `unknown`, never `direct`.
|
|
243
|
+
return provenance.composite
|
|
244
|
+
? { kind: "unknown", reason: "composite-not-interpreted" }
|
|
245
|
+
: { kind: "direct" };
|
|
246
|
+
case "host":
|
|
247
|
+
// INTENTIUS/typescript-as-data#248 — host code produced the value, so
|
|
248
|
+
// editing the declaration edits a call, not the value it returned.
|
|
249
|
+
return { kind: "unknown", reason: origin.reason };
|
|
198
250
|
case "build-param":
|
|
199
251
|
// A build parameter governs the expression the AUTHOR wrote in their own
|
|
200
252
|
// source, so the declaration is still theirs to edit.
|
|
@@ -224,6 +276,7 @@ export function foldProvenanceOfEntity(
|
|
|
224
276
|
...(provenance?.sourceFile ? { sourceFile: provenance.sourceFile } : {}),
|
|
225
277
|
...(provenance?.composite ? { composite: provenance.composite } : {}),
|
|
226
278
|
...(provenance?.compositeInstance ? { instance: provenance.compositeInstance } : {}),
|
|
279
|
+
...(provenance?.compositeCall ? { compositeCall: { ...provenance.compositeCall } } : {}),
|
|
227
280
|
fields,
|
|
228
281
|
};
|
|
229
282
|
}
|
|
@@ -270,6 +323,10 @@ export function unknownOriginText(reason: UnknownOriginReason): string {
|
|
|
270
323
|
return "expanded by a composite whose factory this build did not interpret";
|
|
271
324
|
case "no-provenance":
|
|
272
325
|
return "this build recorded no provenance for the entity";
|
|
326
|
+
case "host-call":
|
|
327
|
+
return "the value is what a host call returned";
|
|
328
|
+
case "host-value":
|
|
329
|
+
return "the value is one a host package exports";
|
|
273
330
|
}
|
|
274
331
|
}
|
|
275
332
|
|
|
@@ -278,9 +335,57 @@ function compositeCall(composite: string, instance: string | undefined): string
|
|
|
278
335
|
return instance ? `the \`${instance}\` call of composite ${composite}` : `composite ${composite}`;
|
|
279
336
|
}
|
|
280
337
|
|
|
281
|
-
/**
|
|
282
|
-
function
|
|
283
|
-
|
|
338
|
+
/** A file as a report prints it: relative to `root` when it is inside it, otherwise as recorded. */
|
|
339
|
+
function displayFile(file: string, root: string | undefined): string {
|
|
340
|
+
if (!root || !isAbsolute(file)) return file;
|
|
341
|
+
const rel = relative(root, file);
|
|
342
|
+
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : file;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** `src/app.ts:12`, relative to `root` when it is inside it. */
|
|
346
|
+
export function formatSourceLocation(location: SourceLocation, root?: string): string {
|
|
347
|
+
return `${displayFile(location.file, root)}:${location.line}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** ` in src/app.infra.ts`, or nothing when the build recorded no file. */
|
|
351
|
+
function inFile(sourceFile: string | undefined, root: string | undefined): string {
|
|
352
|
+
return sourceFile ? ` in ${displayFile(sourceFile, root)}` : "";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The composite call as the author wrote the arguments that govern a field:
|
|
357
|
+
* `WebApp({ replicas: 3 })`, or `WebApp({ iam: { path: "/service/" } })` for a
|
|
358
|
+
* nested parameter. Only arguments actually written at the call appear; when
|
|
359
|
+
* none is, the call is shown as `WebApp(...)`.
|
|
360
|
+
*/
|
|
361
|
+
export function describeCompositeArguments(composite: string, locations: readonly ArgumentLocation[]): string {
|
|
362
|
+
const written = locations.filter((a) => a.text !== undefined);
|
|
363
|
+
if (written.length === 0) return `${composite}(...)`;
|
|
364
|
+
const parts = written.map((a) => {
|
|
365
|
+
const outer = a.parameter.split(".").slice(0, -1);
|
|
366
|
+
return outer.reduceRight((inner, key) => `${key}: { ${inner} }`, a.text as string);
|
|
367
|
+
});
|
|
368
|
+
return `${composite}({ ${parts.join(", ")} })`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Where a composite-parameter field was set, as `at src/app.ts:12`: the first
|
|
373
|
+
* argument written at the call, else the call, else the declaring file.
|
|
374
|
+
*/
|
|
375
|
+
function parameterSite(
|
|
376
|
+
origin: Extract<FoldFieldOrigin, { kind: "composite-parameter" }>,
|
|
377
|
+
sourceFile: string | undefined,
|
|
378
|
+
root: string | undefined,
|
|
379
|
+
): string {
|
|
380
|
+
const written = origin.arguments?.find((a) => a.text !== undefined);
|
|
381
|
+
const location = written ?? origin.call ?? origin.arguments?.[0];
|
|
382
|
+
if (location) return ` at ${formatSourceLocation(location, root)}`;
|
|
383
|
+
return inFile(sourceFile, root);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** ` at src/app.ts:10` for the composite call, else the declaring file. */
|
|
387
|
+
function callSite(call: SourceLocation | undefined, sourceFile: string | undefined, root: string | undefined): string {
|
|
388
|
+
return call ? ` at ${formatSourceLocation(call, root)}` : inFile(sourceFile, root);
|
|
284
389
|
}
|
|
285
390
|
|
|
286
391
|
/** A value as a report prints it. */
|
|
@@ -327,6 +432,12 @@ export interface FieldReconcile {
|
|
|
327
432
|
declared?: unknown;
|
|
328
433
|
live?: unknown;
|
|
329
434
|
origin: FoldFieldOrigin;
|
|
435
|
+
/**
|
|
436
|
+
* One line naming where the field came from (chant #3597):
|
|
437
|
+
* `spec.replicas on Deployment webDeployment comes from WebApp({ replicas: 3 }) at src/app.ts:12`.
|
|
438
|
+
* An `unknown` origin says so and why; it never reads as a direct declaration.
|
|
439
|
+
*/
|
|
440
|
+
summary: string;
|
|
330
441
|
resolution: DriftResolution;
|
|
331
442
|
}
|
|
332
443
|
|
|
@@ -337,18 +448,48 @@ export interface FieldReconcile {
|
|
|
337
448
|
* source declared, whose live value moved. Undeclared fields are held
|
|
338
449
|
* elsewhere and never reach here, so the only question left is which of the
|
|
339
450
|
* four origins produced the field.
|
|
451
|
+
*
|
|
452
|
+
* `entityType` names the resource in the summary line, and `root` (the
|
|
453
|
+
* project directory) makes the file names in it relative. Both are optional;
|
|
454
|
+
* without them the summary uses the entity name alone and absolute files.
|
|
340
455
|
*/
|
|
341
456
|
export function resolveDriftedField(input: {
|
|
342
457
|
entity: string;
|
|
458
|
+
entityType?: string;
|
|
343
459
|
path: string;
|
|
344
460
|
declared?: unknown;
|
|
345
461
|
live?: unknown;
|
|
346
462
|
origin?: PathOrigin;
|
|
347
463
|
provenance?: EntityProvenance;
|
|
464
|
+
root?: string;
|
|
348
465
|
}): FieldReconcile {
|
|
349
466
|
const origin = classifyFieldOrigin(input.origin, input.provenance);
|
|
350
467
|
const sourceFile = input.provenance?.sourceFile;
|
|
468
|
+
const root = input.root;
|
|
351
469
|
const move = `${renderValue(input.declared)} to ${renderValue(input.live)}`;
|
|
470
|
+
const subject = `${input.path} on ${input.entityType ? `${input.entityType} ` : ""}${input.entity}`;
|
|
471
|
+
|
|
472
|
+
const summary = ((): string => {
|
|
473
|
+
switch (origin.kind) {
|
|
474
|
+
case "composite-parameter":
|
|
475
|
+
return (
|
|
476
|
+
`${subject} comes from ${describeCompositeArguments(origin.composite, origin.arguments ?? [])}` +
|
|
477
|
+
`${parameterSite(origin, sourceFile, root)}` +
|
|
478
|
+
(origin.arguments?.some((a) => a.text !== undefined)
|
|
479
|
+
? ""
|
|
480
|
+
: ` (parameter ${origin.parameters.join(", ")}, not written at the call)`)
|
|
481
|
+
);
|
|
482
|
+
case "composite-literal":
|
|
483
|
+
return (
|
|
484
|
+
`${subject} is fixed inside composite ${origin.composite}; ` +
|
|
485
|
+
`no argument to the call${callSite(origin.call, sourceFile, root)} moves it`
|
|
486
|
+
);
|
|
487
|
+
case "direct":
|
|
488
|
+
return `${subject} is declared directly${inFile(sourceFile, root)}`;
|
|
489
|
+
case "unknown":
|
|
490
|
+
return `${subject} has an unknown origin: ${unknownOriginText(origin.reason)}`;
|
|
491
|
+
}
|
|
492
|
+
})();
|
|
352
493
|
|
|
353
494
|
const resolution: DriftResolution = ((): DriftResolution => {
|
|
354
495
|
switch (origin.kind) {
|
|
@@ -363,7 +504,7 @@ export function resolveDriftedField(input: {
|
|
|
363
504
|
description:
|
|
364
505
|
`change ${parameters.length === 1 ? "parameter" : "parameters"} ` +
|
|
365
506
|
`${parameters.map((p) => `\`${p}\``).join(", ")} of ` +
|
|
366
|
-
`${compositeCall(origin.composite, origin.instance)}${
|
|
507
|
+
`${compositeCall(origin.composite, origin.instance)}${parameterSite(origin, sourceFile, root)} ` +
|
|
367
508
|
`so \`${input.path}\` moves from ${move}. The composite stays.`,
|
|
368
509
|
};
|
|
369
510
|
}
|
|
@@ -375,14 +516,14 @@ export function resolveDriftedField(input: {
|
|
|
375
516
|
...(sourceFile ? { sourceFile } : {}),
|
|
376
517
|
description:
|
|
377
518
|
`refused: \`${input.path}\` is fixed by ${compositeCall(origin.composite, origin.instance)}` +
|
|
378
|
-
`${
|
|
519
|
+
`${callSite(origin.call, sourceFile, root)}, so no argument at the call site moves it. ` +
|
|
379
520
|
`Parameterize the field in composite ${origin.composite}, or stop using the composite here.`,
|
|
380
521
|
};
|
|
381
522
|
case "direct":
|
|
382
523
|
return {
|
|
383
524
|
kind: "edit-declaration",
|
|
384
525
|
...(sourceFile ? { sourceFile } : {}),
|
|
385
|
-
description: `change the declared value of \`${input.path}\`${inFile(sourceFile)} from ${move}.`,
|
|
526
|
+
description: `change the declared value of \`${input.path}\`${inFile(sourceFile, root)} from ${move}.`,
|
|
386
527
|
};
|
|
387
528
|
case "unknown":
|
|
388
529
|
return {
|
|
@@ -391,7 +532,7 @@ export function resolveDriftedField(input: {
|
|
|
391
532
|
...(sourceFile ? { sourceFile } : {}),
|
|
392
533
|
description:
|
|
393
534
|
`origin unknown (${unknownOriginText(origin.reason)}), so falling back to changing the ` +
|
|
394
|
-
`declared value of \`${input.path}\`${inFile(sourceFile)} from ${move}.`,
|
|
535
|
+
`declared value of \`${input.path}\`${inFile(sourceFile, root)} from ${move}.`,
|
|
395
536
|
};
|
|
396
537
|
}
|
|
397
538
|
})();
|
|
@@ -402,6 +543,7 @@ export function resolveDriftedField(input: {
|
|
|
402
543
|
...("declared" in input ? { declared: input.declared } : {}),
|
|
403
544
|
...("live" in input ? { live: input.live } : {}),
|
|
404
545
|
origin,
|
|
546
|
+
summary,
|
|
405
547
|
resolution,
|
|
406
548
|
};
|
|
407
549
|
}
|
|
@@ -420,6 +562,7 @@ export function resolveDriftedField(input: {
|
|
|
420
562
|
export function resolveDeepDrift(
|
|
421
563
|
drifted: readonly DeepEntityDrift[],
|
|
422
564
|
provenanceOf: (entity: string) => EntityProvenance | undefined,
|
|
565
|
+
options: { root?: string } = {},
|
|
423
566
|
): FieldReconcile[] {
|
|
424
567
|
const out: FieldReconcile[] = [];
|
|
425
568
|
for (const entity of drifted) {
|
|
@@ -428,11 +571,13 @@ export function resolveDeepDrift(
|
|
|
428
571
|
out.push(
|
|
429
572
|
resolveDriftedField({
|
|
430
573
|
entity: entity.name,
|
|
574
|
+
entityType: entity.type,
|
|
431
575
|
path: change.path,
|
|
432
576
|
...("declared" in change ? { declared: change.declared } : {}),
|
|
433
577
|
...("live" in change ? { live: change.live } : {}),
|
|
434
578
|
...(change.origin ? { origin: change.origin } : {}),
|
|
435
579
|
...(provenance ? { provenance } : {}),
|
|
580
|
+
...(options.root ? { root: options.root } : {}),
|
|
436
581
|
}),
|
|
437
582
|
);
|
|
438
583
|
}
|
package/src/import/ir-utils.ts
CHANGED
|
@@ -36,10 +36,15 @@ export function hasIntrinsicInValue(value: unknown, name: string): boolean {
|
|
|
36
36
|
*/
|
|
37
37
|
export function irUsesIntrinsic(ir: TemplateIR, name: string): boolean {
|
|
38
38
|
for (const resource of ir.resources) {
|
|
39
|
-
if (
|
|
39
|
+
if (
|
|
40
|
+
hasIntrinsicInValue(resource.properties, name) ||
|
|
41
|
+
hasIntrinsicInValue(resource.metadata, name) ||
|
|
42
|
+
hasIntrinsicInValue(resource.attributes, name)
|
|
43
|
+
) {
|
|
40
44
|
return true;
|
|
41
45
|
}
|
|
42
46
|
}
|
|
47
|
+
if (hasIntrinsicInValue(ir.metadata, name)) return true;
|
|
43
48
|
for (const condition of ir.conditions ?? []) {
|
|
44
49
|
if (hasIntrinsicInValue(condition.expression, name)) {
|
|
45
50
|
return true;
|
package/src/import/parser.ts
CHANGED
|
@@ -9,6 +9,9 @@ export interface ParameterIR {
|
|
|
9
9
|
readonly description?: string;
|
|
10
10
|
readonly defaultValue?: unknown;
|
|
11
11
|
readonly required?: boolean;
|
|
12
|
+
/** What the value is checked against, under the source format's own keys
|
|
13
|
+
* and as written (CloudFormation `AllowedValues`, `MinLength`, `NoEcho`). */
|
|
14
|
+
readonly constraints?: Readonly<Record<string, unknown>>;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -22,6 +25,11 @@ export interface ResourceIR {
|
|
|
22
25
|
/** Name of the condition gating this resource (CloudFormation `Condition`
|
|
23
26
|
* key, or equivalent), when the source template declares one (#2069). */
|
|
24
27
|
readonly condition?: string;
|
|
28
|
+
/** Resource-level attributes other than the condition, under the source
|
|
29
|
+
* format's own keys (CloudFormation `DependsOn`, `DeletionPolicy`,
|
|
30
|
+
* `UpdateReplacePolicy`, `UpdatePolicy`, `CreationPolicy`). Values are
|
|
31
|
+
* parsed value trees, like {@link properties}. */
|
|
32
|
+
readonly attributes?: Readonly<Record<string, unknown>>;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
/**
|