@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.
Files changed (46) hide show
  1. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  2. package/dist/composite.d.ts +5 -0
  3. package/dist/composite.d.ts.map +1 -1
  4. package/dist/discovery/collect.d.ts.map +1 -1
  5. package/dist/discovery/fold-import.d.ts +15 -0
  6. package/dist/discovery/fold-import.d.ts.map +1 -1
  7. package/dist/discovery/param-deps.d.ts +59 -4
  8. package/dist/discovery/param-deps.d.ts.map +1 -1
  9. package/dist/fold/subset.d.ts +17 -2
  10. package/dist/fold/subset.d.ts.map +1 -1
  11. package/dist/fold-provenance.d.ts +49 -4
  12. package/dist/fold-provenance.d.ts.map +1 -1
  13. package/dist/import/ir-utils.d.ts.map +1 -1
  14. package/dist/import/parser.d.ts +8 -0
  15. package/dist/import/parser.d.ts.map +1 -1
  16. package/dist/provenance.d.ts +49 -0
  17. package/dist/provenance.d.ts.map +1 -1
  18. package/dist/serializer-walker.d.ts.map +1 -1
  19. package/dist/stack-output.d.ts +5 -0
  20. package/dist/stack-output.d.ts.map +1 -1
  21. package/dist/yaml.d.ts.map +1 -1
  22. package/package.json +1 -1
  23. package/src/cli/commands/import.test.ts +5 -4
  24. package/src/cli/handlers/lifecycle.test.ts +106 -1
  25. package/src/cli/handlers/lifecycle.ts +26 -5
  26. package/src/composite.ts +37 -0
  27. package/src/discovery/collect.ts +13 -3
  28. package/src/discovery/fold-composite.test.ts +22 -0
  29. package/src/discovery/fold-host-composite.test.ts +23 -0
  30. package/src/discovery/fold-import.ts +324 -30
  31. package/src/discovery/fold-package-composite.test.ts +14 -1
  32. package/src/discovery/fold-project-provenance.test.ts +584 -0
  33. package/src/discovery/param-deps.test.ts +163 -3
  34. package/src/discovery/param-deps.ts +173 -4
  35. package/src/fold/subset-public-export.test.ts +38 -0
  36. package/src/fold/subset.ts +17 -2
  37. package/src/fold-provenance.test.ts +190 -16
  38. package/src/fold-provenance.ts +158 -13
  39. package/src/import/ir-utils.ts +6 -1
  40. package/src/import/parser.ts +8 -0
  41. package/src/provenance.ts +59 -2
  42. package/src/serializer-walker.test.ts +10 -0
  43. package/src/serializer-walker.ts +17 -1
  44. package/src/stack-output.ts +6 -1
  45. package/src/yaml.test.ts +6 -0
  46. package/src/yaml.ts +4 -0
@@ -1,14 +1,21 @@
1
1
  import * as ts from "typescript";
2
2
  import { describe, expect, test } from "vitest";
3
3
  import { collectConsts } from "../fold/fold";
4
- import { collectCompositeOrigins, collectParamDependencies, collectTagOrigins } from "./param-deps";
4
+ import {
5
+ argumentLocation,
6
+ collectCompositeOrigins,
7
+ collectParamDependencies,
8
+ collectTagOrigins,
9
+ type CompositeCallSite,
10
+ type HostBindings,
11
+ } from "./param-deps";
5
12
  import type { PathOrigin } from "../provenance";
6
13
 
7
14
  /**
8
15
  * Collect dependencies for the props of the file's single
9
16
  * `export const x = new Type({...})`, which is the shape every case here uses.
10
17
  */
11
- function depsOf(source: string, paramLocals = ["params"]): Record<string, PathOrigin> {
18
+ function depsOf(source: string, paramLocals = ["params"], host?: HostBindings): Record<string, PathOrigin> {
12
19
  const file = ts.createSourceFile("fixture.ts", source, ts.ScriptTarget.Latest, true);
13
20
  const consts = collectConsts(file);
14
21
  let props: ts.ObjectLiteralExpression | undefined;
@@ -26,7 +33,7 @@ function depsOf(source: string, paramLocals = ["params"]): Record<string, PathOr
26
33
  }
27
34
  }
28
35
  if (!props) throw new Error("fixture has no `const x = new Type({...})`");
29
- return collectParamDependencies(props, consts, new Set(paramLocals));
36
+ return collectParamDependencies(props, consts, new Set(paramLocals), host);
30
37
  }
31
38
 
32
39
  const param = (...names: string[]): PathOrigin => ({ kind: "build-param", params: names });
@@ -292,3 +299,156 @@ describe("collectTagOrigins", () => {
292
299
  ).toEqual({ "engine.args": fromParam("version"), engine: fixed });
293
300
  });
294
301
  });
302
+
303
+ // ─────────────────────────────────────────────────────────────────────────
304
+ // chant #3597 — where the argument behind a parameter was written.
305
+ // ─────────────────────────────────────────────────────────────────────────
306
+
307
+ /** The first call expression in `source`, as a call site in `/p/src/app.ts`. */
308
+ function callSiteOf(source: string): CompositeCallSite {
309
+ const file = ts.createSourceFile("/p/src/app.ts", source, ts.ScriptTarget.Latest, true);
310
+ let node: ts.CallExpression | undefined;
311
+ const visit = (n: ts.Node): void => {
312
+ if (!node && ts.isCallExpression(n)) node = n;
313
+ ts.forEachChild(n, visit);
314
+ };
315
+ visit(file);
316
+ if (!node) throw new Error("fixture has no call");
317
+ return { file: "/p/src/app.ts", node };
318
+ }
319
+
320
+ describe("argumentLocation (#3597)", () => {
321
+ const source = [
322
+ 'import { WebApp } from "./web-app";',
323
+ "",
324
+ "export const web = WebApp({",
325
+ ' name: "web-app",',
326
+ " replicas: 3,",
327
+ ' iam: { path: "/service/" },',
328
+ " port,",
329
+ "});",
330
+ ].join("\n");
331
+
332
+ test("a top-level argument is the property as written, on its own line", () => {
333
+ expect(argumentLocation(callSiteOf(source), "replicas")).toEqual({
334
+ parameter: "replicas",
335
+ file: "/p/src/app.ts",
336
+ line: 5,
337
+ column: 3,
338
+ text: "replicas: 3",
339
+ });
340
+ });
341
+
342
+ test("a nested parameter path descends the argument's object literals", () => {
343
+ expect(argumentLocation(callSiteOf(source), "iam.path")).toEqual({
344
+ parameter: "iam.path",
345
+ file: "/p/src/app.ts",
346
+ line: 6,
347
+ column: 10,
348
+ text: 'path: "/service/"',
349
+ });
350
+ });
351
+
352
+ test("a shorthand property is written as its name", () => {
353
+ expect(argumentLocation(callSiteOf(source), "port")).toMatchObject({ line: 7, column: 3, text: "port" });
354
+ });
355
+
356
+ test("a parameter the call leaves out falls back to the call, and claims no text", () => {
357
+ expect(argumentLocation(callSiteOf(source), "image")).toEqual({
358
+ parameter: "image",
359
+ file: "/p/src/app.ts",
360
+ line: 3,
361
+ column: 20,
362
+ });
363
+ expect(argumentLocation(callSiteOf(source), "iam.role")).not.toHaveProperty("text");
364
+ });
365
+
366
+ test("an argument that is not an object literal falls back to the call", () => {
367
+ const location = argumentLocation(callSiteOf("export const web = WebApp(shared);"), "replicas");
368
+ expect(location).toEqual({ parameter: "replicas", file: "/p/src/app.ts", line: 1, column: 20 });
369
+ });
370
+
371
+ test("collectCompositeOrigins records the locations in the same pass, in parameter order", () => {
372
+ const call = callSiteOf(source);
373
+ const body = ts.createSourceFile(
374
+ "factory.ts",
375
+ "const f = (props) => new Thing({ spec: { replicas: props.replicas }, label: `${props.name}-${props.iam.path}`, kind: \"web\" });",
376
+ ts.ScriptTarget.Latest,
377
+ true,
378
+ );
379
+ let props: ts.ObjectLiteralExpression | undefined;
380
+ const visit = (n: ts.Node): void => {
381
+ if (!props && ts.isNewExpression(n)) props = n.arguments?.find(ts.isObjectLiteralExpression);
382
+ ts.forEachChild(n, visit);
383
+ };
384
+ visit(body);
385
+ const origins = collectCompositeOrigins(
386
+ props as ts.ObjectLiteralExpression,
387
+ new Map(),
388
+ { whole: new Set(["props"]), destructured: new Map() },
389
+ "WebApp",
390
+ call,
391
+ );
392
+ expect(origins["spec.replicas"]).toEqual({
393
+ kind: "composite-parameter",
394
+ composite: "WebApp",
395
+ parameters: ["replicas"],
396
+ arguments: [{ parameter: "replicas", file: "/p/src/app.ts", line: 5, column: 3, text: "replicas: 3" }],
397
+ });
398
+ expect(origins.label).toMatchObject({
399
+ parameters: ["iam.path", "name"],
400
+ arguments: [
401
+ { parameter: "iam.path", line: 6, text: 'path: "/service/"' },
402
+ { parameter: "name", line: 4, text: 'name: "web-app"' },
403
+ ],
404
+ });
405
+ // A literal carries no location: nothing at the call moves it.
406
+ expect(origins.kind).toEqual({ kind: "composite-literal", composite: "WebApp" });
407
+ });
408
+
409
+ test("without a call site the origins are what they were", () => {
410
+ expect(compositeOriginsOf("return new Thing({ replicas: props.replicas });")).toEqual({
411
+ replicas: { kind: "composite-parameter", composite: "WebService", parameters: ["replicas"] },
412
+ });
413
+ });
414
+ });
415
+
416
+ describe("collectParamDependencies with host bindings (typescript-as-data#248)", () => {
417
+ const host: HostBindings = { values: new Set(["AWS"]), eagerCalls: new Set(["matrix"]) };
418
+ const hostCall: PathOrigin = { kind: "host", reason: "host-call" };
419
+ const hostValue: PathOrigin = { kind: "host", reason: "host-value" };
420
+
421
+ test("a method call on a receiver is a host call, at its own path", () => {
422
+ const source = `export const x = new Thing({ name: "x".toUpperCase(), spec: { tag: [1, 2].join("-") }, fixed: "k" });`;
423
+ expect(depsOf(source, [], host)).toEqual({ name: hostCall, "spec.tag": hostCall });
424
+ });
425
+
426
+ test("an eager lexicon helper is a host call, and a project function is not", () => {
427
+ const source = `export const x = new Thing({ a: matrix("os"), b: helper("os") });`;
428
+ expect(depsOf(source, [], host)).toEqual({ a: hostCall });
429
+ });
430
+
431
+ test("a lexicon export is a host value, through a const and an operator", () => {
432
+ const source = [`const region = AWS.Region;`, "export const x = new Thing({ a: `${region}-x`, b: AWS });"].join("\n");
433
+ expect(depsOf(source, [], host)).toEqual({ a: hostValue, b: hostValue });
434
+ });
435
+
436
+ test("a host read outranks a build parameter the same expression reads", () => {
437
+ const source = `export const x = new Thing({ name: params.name.toUpperCase(), size: params.size });`;
438
+ expect(depsOf(source, ["params"], host)).toEqual({ name: hostCall, size: param("size") });
439
+ });
440
+
441
+ test("a sibling resource, a member name and a property key are not host reads", () => {
442
+ const source = [
443
+ `const logs = new Bucket({ name: "x".toUpperCase() });`,
444
+ `const AWS2 = 1;`,
445
+ `export const x = new Thing({ a: logs.Arn, b: { AWS: 1 }, c: thing.AWS });`,
446
+ ].join("\n");
447
+ expect(depsOf(source, [], host)).toEqual({});
448
+ });
449
+
450
+ test("a tag is not read, its interpolations are", () => {
451
+ const source = "export const x = new Thing({ a: Sub`${AWS.Region}`, b: Sub`plain` });";
452
+ expect(depsOf(source, [], { values: new Set(["AWS", "Sub"]), eagerCalls: new Set() })).toEqual({ a: hostValue });
453
+ });
454
+ });
@@ -1,5 +1,5 @@
1
1
  import * as ts from "typescript";
2
- import type { PathOrigin } from "../provenance";
2
+ import type { ArgumentLocation, HostOriginReason, PathOrigin, SourceLocation } from "../provenance";
3
3
 
4
4
  /**
5
5
  * Which build parameters an authored props expression READS, per path (chant
@@ -117,6 +117,94 @@ function readParams(
117
117
  visit(expr);
118
118
  }
119
119
 
120
+ /**
121
+ * What a file binds to host code, for {@link hostReadOf} (INTENTIUS/typescript-as-data#248).
122
+ */
123
+ export interface HostBindings {
124
+ /** Local names bound to a value a lexicon package exports. */
125
+ values: ReadonlySet<string>;
126
+ /** Local names of eager lexicon helpers: a call to one is evaluated by host code at fold time. */
127
+ eagerCalls: ReadonlySet<string>;
128
+ }
129
+
130
+ /**
131
+ * The first host read in `expr`, following this file's own top-level consts,
132
+ * or `undefined` when it reads none (INTENTIUS/typescript-as-data#248).
133
+ *
134
+ * A method called on a receiver is `host-call` whatever the receiver is, since
135
+ * fold calls the real method on it; so is a call to an eager lexicon helper. An
136
+ * identifier bound to a lexicon export is `host-value`. A call fold reduces to
137
+ * an envelope (an authoring helper, a call-form intrinsic) or evaluates itself
138
+ * (a project function) is not host code, and only its arguments are read.
139
+ *
140
+ * Syntactic, like {@link readParams}, so it over-reports in one place: both
141
+ * arms of a conditional are read although only one is taken. That errs toward
142
+ * `unknown`, which never licenses an edit `direct` would not.
143
+ */
144
+ export function hostReadOf(
145
+ expr: ts.Expression,
146
+ consts: ReadonlyMap<string, ts.Expression>,
147
+ paramLocals: ReadonlySet<string>,
148
+ host: HostBindings,
149
+ ): HostOriginReason | undefined {
150
+ const followed = new Set<string>();
151
+ let found: HostOriginReason | undefined;
152
+
153
+ const visit = (node: ts.Node): void => {
154
+ if (found) return;
155
+ if (ts.isCallExpression(node)) {
156
+ const callee = unwrap(node.expression);
157
+ if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) {
158
+ found = "host-call";
159
+ return;
160
+ }
161
+ if (ts.isIdentifier(callee) && !consts.has(callee.text) && host.eagerCalls.has(callee.text)) {
162
+ found = "host-call";
163
+ return;
164
+ }
165
+ for (const argument of node.arguments) visit(argument);
166
+ return;
167
+ }
168
+
169
+ if (ts.isPropertyAccessExpression(node)) {
170
+ // The member name is not a reference.
171
+ visit(node.expression);
172
+ return;
173
+ }
174
+
175
+ if (ts.isIdentifier(node)) {
176
+ if (paramLocals.has(node.text)) return;
177
+ const initializer = consts.get(node.text);
178
+ if (initializer) {
179
+ // A sibling resource is a reference to it, not a read of its props.
180
+ if (ts.isNewExpression(unwrap(initializer)) || followed.has(node.text)) return;
181
+ followed.add(node.text);
182
+ visit(initializer);
183
+ return;
184
+ }
185
+ if (host.values.has(node.text)) found = "host-value";
186
+ return;
187
+ }
188
+
189
+ if (ts.isPropertyAssignment(node)) {
190
+ visit(node.initializer);
191
+ return;
192
+ }
193
+
194
+ // A tag is an intrinsic the lexicon registered, and only its
195
+ // interpolations are read.
196
+ if (ts.isTaggedTemplateExpression(node)) {
197
+ visit(node.template);
198
+ return;
199
+ }
200
+
201
+ ts.forEachChild(node, visit);
202
+ };
203
+
204
+ visit(expr);
205
+ return found;
206
+ }
207
+
120
208
  /**
121
209
  * Path → build-parameter origin for one resource's props object literal.
122
210
  *
@@ -129,16 +217,27 @@ function readParams(
129
217
  * A spread is attributed to the object it spreads INTO, at that object's own
130
218
  * path (the entity root for a top-level spread), since which keys it
131
219
  * contributes is not knowable here.
220
+ *
221
+ * With `host`, a path whose expression reads host code ({@link hostReadOf})
222
+ * is recorded `host` instead, and that outranks a build parameter it also
223
+ * reads: the value is what host code returned, whatever the parameter was.
132
224
  */
133
225
  export function collectParamDependencies(
134
226
  props: ts.ObjectLiteralExpression,
135
227
  consts: ReadonlyMap<string, ts.Expression>,
136
228
  paramLocals: ReadonlySet<string>,
229
+ host?: HostBindings,
137
230
  ): Record<string, PathOrigin> {
138
231
  const out: Record<string, PathOrigin> = {};
139
- if (paramLocals.size === 0) return out;
232
+ if (paramLocals.size === 0 && !host) return out;
140
233
 
141
234
  const record = (path: string, expr: ts.Expression): void => {
235
+ if (out[path]?.kind === "host") return;
236
+ const reason = host ? hostReadOf(expr, consts, paramLocals, host) : undefined;
237
+ if (reason) {
238
+ out[path] = { kind: "host", reason };
239
+ return;
240
+ }
142
241
  const found = new Set<string>();
143
242
  readParams(expr, consts, paramLocals, found);
144
243
  if (found.size === 0) return;
@@ -288,6 +387,61 @@ function readCompositeParameters(
288
387
  visit(expr);
289
388
  }
290
389
 
390
+ /**
391
+ * The composite call an interpreted factory body was reached from (chant
392
+ * #3597): the `WebApp({ replicas: 3 })` expression in the author's source and
393
+ * the absolute path of the file it is written in. The node must come from a
394
+ * source file parsed with parent pointers, which is how `fold-import.ts`
395
+ * parses every file it interprets.
396
+ */
397
+ export interface CompositeCallSite {
398
+ file: string;
399
+ node: ts.CallExpression;
400
+ }
401
+
402
+ /** A node's start as a 1-based {@link SourceLocation}. */
403
+ export function sourceLocationOf(file: string, node: ts.Node): SourceLocation {
404
+ const sourceFile = node.getSourceFile();
405
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
406
+ return { file, line: line + 1, column: character + 1 };
407
+ }
408
+
409
+ /**
410
+ * Where `parameter` was written at `call`: the property at that path in the
411
+ * call's object-literal argument, descending nested object literals segment by
412
+ * segment, or the call itself when there is no such property.
413
+ *
414
+ * The fallback covers a parameter the call leaves out, one that arrives through
415
+ * a spread, and an argument that is not an object literal. In each of those the
416
+ * call is the nearest place that governs the value, and no `text` is given,
417
+ * because no property at the call says what it is.
418
+ */
419
+ export function argumentLocation(call: CompositeCallSite, parameter: string): ArgumentLocation {
420
+ const fallback: ArgumentLocation = { parameter, ...sourceLocationOf(call.file, call.node) };
421
+ const first = call.node.arguments[0];
422
+ if (!first) return fallback;
423
+ let object: ts.Expression = unwrap(first);
424
+ let found: ts.ObjectLiteralElementLike | undefined;
425
+ for (const segment of parameter.split(".")) {
426
+ if (!ts.isObjectLiteralExpression(object)) return fallback;
427
+ found = undefined;
428
+ // Last write wins, as it does when the object literal is evaluated.
429
+ for (const member of object.properties) {
430
+ if (ts.isShorthandPropertyAssignment(member) && member.name.text === segment) found = member;
431
+ else if (ts.isPropertyAssignment(member) && literalName(member.name) === segment) found = member;
432
+ }
433
+ if (!found) return fallback;
434
+ object = ts.isPropertyAssignment(found) ? unwrap(found.initializer) : found.name;
435
+ }
436
+ if (!found) return fallback;
437
+ return { parameter, ...sourceLocationOf(call.file, found), text: found.getText() };
438
+ }
439
+
440
+ /** {@link argumentLocation} for each of `parameters`, in order, or nothing when no call was seen. */
441
+ function argumentLocations(call: CompositeCallSite | undefined, parameters: readonly string[]): ArgumentLocation[] | undefined {
442
+ return call ? parameters.map((parameter) => argumentLocation(call, parameter)) : undefined;
443
+ }
444
+
291
445
  /**
292
446
  * Path → composite origin for one `new Type({...})` inside an interpreted
293
447
  * composite factory body (chant #2161).
@@ -304,12 +458,16 @@ function readCompositeParameters(
304
458
  * something under here", while `composite-literal` would be a claim that every
305
459
  * key it brings is fixed. That claim is left unmade, and those paths come back
306
460
  * `unknown` from ../fold-provenance.ts instead.
461
+ *
462
+ * `call` (chant #3597), when the fold saw the composite call, adds where each
463
+ * parameter's argument was written there, in the same pass.
307
464
  */
308
465
  export function collectCompositeOrigins(
309
466
  props: ts.ObjectLiteralExpression,
310
467
  consts: ReadonlyMap<string, ts.Expression>,
311
468
  scope: CompositeParamScope,
312
469
  composite: string,
470
+ call?: CompositeCallSite,
313
471
  ): Record<string, PathOrigin> {
314
472
  const out: Record<string, PathOrigin> = {};
315
473
 
@@ -327,7 +485,7 @@ export function collectCompositeOrigins(
327
485
  existing && existing.kind === "composite-parameter"
328
486
  ? [...new Set([...existing.parameters, ...parameters])].sort()
329
487
  : parameters;
330
- out[path] = { kind: "composite-parameter", composite, parameters: merged };
488
+ out[path] = compositeParameterOrigin(composite, merged, call);
331
489
  return;
332
490
  }
333
491
  if (spread) return;
@@ -384,6 +542,7 @@ export function collectTagOrigins(
384
542
  consts: ReadonlyMap<string, ts.Expression>,
385
543
  scope: CompositeParamScope,
386
544
  composite: string,
545
+ call?: CompositeCallSite,
387
546
  ): Record<string, PathOrigin> {
388
547
  const out: Record<string, PathOrigin> = {};
389
548
 
@@ -395,7 +554,7 @@ export function collectTagOrigins(
395
554
  const existing = out[path];
396
555
  const merged = new Set(existing?.kind === "composite-parameter" ? existing.parameters : []);
397
556
  for (const parameter of found) merged.add(parameter);
398
- out[path] = { kind: "composite-parameter", composite, parameters: [...merged].sort() };
557
+ out[path] = compositeParameterOrigin(composite, [...merged].sort(), call);
399
558
  }
400
559
  });
401
560
 
@@ -406,3 +565,13 @@ export function collectTagOrigins(
406
565
  }
407
566
  return out;
408
567
  }
568
+
569
+ /** A `composite-parameter` origin, with its argument locations when the call was seen. */
570
+ function compositeParameterOrigin(
571
+ composite: string,
572
+ parameters: string[],
573
+ call: CompositeCallSite | undefined,
574
+ ): PathOrigin {
575
+ const locations = argumentLocations(call, parameters);
576
+ return { kind: "composite-parameter", composite, parameters, ...(locations ? { arguments: locations } : {}) };
577
+ }
@@ -56,3 +56,41 @@ describe("SPEC_VERSION is declared on the package entry (chant#2424)", () => {
56
56
  expect(chant.SPEC_VERSION).toBe(SPEC_VERSION);
57
57
  });
58
58
  });
59
+
60
+ /**
61
+ * chant#3598 — the conformance adapter reads fold provenance (spec 2.2) off
62
+ * `foldProject`'s verdicts, and maps or recomputes it with the functions
63
+ * below. All of them, and the types the verdict field is written in, have to
64
+ * be reachable from this namespace.
65
+ */
66
+ describe("fold provenance is on the package entry (chant#3598)", () => {
67
+ test("the functions are exported", () => {
68
+ for (const name of [
69
+ "foldProject",
70
+ "foldProvenanceOfEntities",
71
+ "foldProvenanceOfEntity",
72
+ "classifyFieldOrigin",
73
+ "emittedFieldPaths",
74
+ "describeFoldFieldOrigin",
75
+ "getProvenance",
76
+ ] as const) {
77
+ expect(typeof chant[name]).toBe("function");
78
+ }
79
+ });
80
+
81
+ test("a verdict's foldProvenance is typed in the exported shapes", () => {
82
+ const origins: chant.FoldFieldOrigin[] = [
83
+ { kind: "composite-parameter", composite: "C", instance: "i", parameters: ["p"] },
84
+ { kind: "composite-literal", composite: "C" },
85
+ { kind: "direct" },
86
+ { kind: "unknown", reason: "no-provenance" satisfies chant.UnknownOriginReason },
87
+ ];
88
+ const record: chant.EntityFoldProvenance = { fields: Object.fromEntries(origins.map((o, i) => [`f${i}`, o])) };
89
+ const verdict: chant.FoldProjectVerdict = {
90
+ verdict: "fold",
91
+ tentative: "fold",
92
+ foldProvenance: { e: record } satisfies chant.FoldProvenance,
93
+ };
94
+ expect(Object.keys(verdict.foldProvenance!.e.fields)).toEqual(["f0", "f1", "f2", "f3"]);
95
+ });
96
+ });
@@ -179,7 +179,7 @@ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, type IntrinsicDef } from
179
179
  * `spec/VERSION` in the specification repository carries the same string, and
180
180
  * the conformance adapter reads this one to fill its `specVersion` field.
181
181
  *
182
- * ## Why 1.6, and what each version since 1.0 asked for (chant#2445)
182
+ * ## Why 2.3, and what each version since 1.0 asked for (chant#2445)
183
183
  *
184
184
  * Raising this is the last step of adopting a rule set, so each version was
185
185
  * checked against what chant does rather than against whether the suite is
@@ -220,6 +220,21 @@ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, type IntrinsicDef } from
220
220
  * - **1.8** added `ι = executing`, and chant carries it as of `0.72.3`
221
221
  * (#2455) — `FoldProjectOptions.executing`, mutually exclusive with
222
222
  * `sandbox`. `open` stays the default and stays strict (#2453).
223
+ * - **2.2** added obligations to `F-Obs-Provenance` for an implementation that
224
+ * claims provenance: one origin per emitted path of four kinds, the
225
+ * innermost writer wins, and an origin that cannot be determined is reported
226
+ * as unknown, never as direct. chant claims provenance, so this is the
227
+ * version whose obligations it takes on. 2.2 also widens two rules to what
228
+ * chant already did: a call-bound local exported by name (`export { x }`)
229
+ * resolves by F-Call as the single declarator does, and a same-file call's
230
+ * result read inside an F-Call argument resolves once per file (#3329).
231
+ * - **2.3** says a value host code produced, written at an export or into a
232
+ * field, has an unknown origin and never a direct one. chant reports
233
+ * `host-call` and `host-value` for resource fields since
234
+ * typescript-as-data#248. 2.3 also lets `F-Host-Composite` interpret a
235
+ * composite an active package publishes as TypeScript source, which chant
236
+ * has done since #3247, and adds fixtures for a call result read inside an
237
+ * argument, which chant passes since #3610.
223
238
  *
224
239
  * `scripts/check-docs-citations.ts` refuses to run when this constant and the
225
240
  * rules it loads disagree, so this and the `@intentius/tsad-conformance`
@@ -231,7 +246,7 @@ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, type IntrinsicDef } from
231
246
  * Until chant#2470 the rules came from a commit `docs-check.yml` checked out;
232
247
  * now they come from the published package, so bumping it is what moves this.
233
248
  */
234
- export const SPEC_VERSION = "2.1";
249
+ export const SPEC_VERSION = "2.3";
235
250
 
236
251
  /** The two EVL rule ids a shape violation can be attributed to. */
237
252
  export type SubsetRuleId = "EVL001" | "EVL003";