@intentius/chant 0.108.1 → 0.109.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 +30 -3
- package/dist/discovery/param-deps.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +10 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold-provenance.d.ts +35 -2
- package/dist/fold-provenance.d.ts.map +1 -1
- package/dist/provenance.d.ts +38 -0
- package/dist/provenance.d.ts.map +1 -1
- package/package.json +1 -1
- 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-import.ts +251 -9
- package/src/discovery/fold-package-composite.test.ts +14 -1
- package/src/discovery/fold-project-provenance.test.ts +532 -0
- package/src/discovery/param-deps.test.ts +120 -1
- package/src/discovery/param-deps.ts +73 -3
- package/src/fold/subset-public-export.test.ts +38 -0
- package/src/fold/subset.ts +10 -2
- package/src/fold-provenance.test.ts +179 -16
- package/src/fold-provenance.ts +136 -11
- package/src/provenance.ts +46 -1
package/src/composite.ts
CHANGED
|
@@ -134,6 +134,16 @@ export function Composite<P, M extends CompositeFactoryMembers = CompositeMember
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// chant#3608 — every member records the composite that built it, here,
|
|
138
|
+
// when the instance is made. A member destructured from the call
|
|
139
|
+
// (`export const { vpc } = VpcDefault({})`) is never expanded, so this is
|
|
140
|
+
// the only trace that a composite wrote it; `collectEntities` reads it
|
|
141
|
+
// through {@link compositeOfMember}. A nested instance recorded its own
|
|
142
|
+
// members first and the first record is kept, so a member keeps its
|
|
143
|
+
// innermost composite. Kept off the provenance record so the composite
|
|
144
|
+
// `expandComposite` records for an instance exported whole is unchanged.
|
|
145
|
+
markCompositeMembers(members, compositeName);
|
|
146
|
+
|
|
137
147
|
// Define `members` and `_definition` as non-enumerable so spreading a
|
|
138
148
|
// composite instance (`...someComposite`) only exposes the actual member
|
|
139
149
|
// resources, not the framework's bookkeeping properties. Without this, a
|
|
@@ -156,6 +166,33 @@ export function Composite<P, M extends CompositeFactoryMembers = CompositeMember
|
|
|
156
166
|
return definition;
|
|
157
167
|
}
|
|
158
168
|
|
|
169
|
+
/** Non-enumerable, symbol-keyed: invisible to serializers, spreads and comparisons, like provenance. */
|
|
170
|
+
const COMPOSITE_MEMBER_OF = Symbol.for("chant.composite.memberOf");
|
|
171
|
+
|
|
172
|
+
/** Record `compositeName` on each declarable member, descending into nested instances. First record wins. */
|
|
173
|
+
function markCompositeMembers(members: object, compositeName: string): void {
|
|
174
|
+
for (const member of Object.values(members)) {
|
|
175
|
+
if (isCompositeInstance(member)) {
|
|
176
|
+
markCompositeMembers(member.members, compositeName);
|
|
177
|
+
} else if (
|
|
178
|
+
isDeclarable(member) &&
|
|
179
|
+
Object.isExtensible(member) &&
|
|
180
|
+
!Object.prototype.hasOwnProperty.call(member, COMPOSITE_MEMBER_OF)
|
|
181
|
+
) {
|
|
182
|
+
Object.defineProperty(member, COMPOSITE_MEMBER_OF, { value: compositeName, enumerable: false, configurable: true });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* chant#3608 — the composite whose call built `entity`, or `undefined` for an
|
|
189
|
+
* entity no chant composite built.
|
|
190
|
+
*/
|
|
191
|
+
export function compositeOfMember(entity: object): string | undefined {
|
|
192
|
+
const name = (entity as Record<symbol, unknown>)[COMPOSITE_MEMBER_OF];
|
|
193
|
+
return typeof name === "string" ? name : undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
159
196
|
/**
|
|
160
197
|
* Expands a CompositeInstance into a flat Map of prefixed entity names to Declarables.
|
|
161
198
|
* Handles nested composites recursively.
|
package/src/discovery/collect.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename, dirname, relative, resolve } from "node:path";
|
|
2
2
|
import { isDeclarable, type Declarable } from "../declarable";
|
|
3
|
-
import { isCompositeInstance, expandComposite } from "../composite";
|
|
3
|
+
import { isCompositeInstance, expandComposite, compositeOfMember } from "../composite";
|
|
4
4
|
import { isLexiconOutput } from "../lexicon-output";
|
|
5
5
|
import { DiscoveryError } from "../errors";
|
|
6
6
|
import { setProvenance, type EntityProvenance } from "../provenance";
|
|
@@ -43,6 +43,16 @@ function stackPrefix(file: string, buildRoot: string | undefined): string {
|
|
|
43
43
|
*/
|
|
44
44
|
const AUTHORED_ROOT: EntityProvenance["paths"] = { "": { kind: "authored" } };
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The record for an entity exported on its own. chant#3608: one destructured
|
|
48
|
+
* from a composite call also names that composite, so fold provenance reads
|
|
49
|
+
* its unattributed paths as `unknown` rather than as the file's own `direct`.
|
|
50
|
+
*/
|
|
51
|
+
function authoredProvenance(entity: Declarable, file: string): EntityProvenance {
|
|
52
|
+
const composite = compositeOfMember(entity);
|
|
53
|
+
return { sourceFile: file, ...(composite ? { composite } : {}), paths: AUTHORED_ROOT };
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
/** One entity to be placed into the map, produced by {@link enumerateEntries}. */
|
|
47
57
|
interface PendingEntry {
|
|
48
58
|
/** The un-disambiguated map key this entity would take (export name, indexed
|
|
@@ -82,12 +92,12 @@ function enumerateEntries(
|
|
|
82
92
|
for (const [rawName, value] of sortedExports) {
|
|
83
93
|
const name = exportKey(rawName, file);
|
|
84
94
|
if (isDeclarable(value)) {
|
|
85
|
-
entries.push({ bareKey: name, value, file, provenance:
|
|
95
|
+
entries.push({ bareKey: name, value, file, provenance: authoredProvenance(value, file) });
|
|
86
96
|
} else if (Array.isArray(value)) {
|
|
87
97
|
for (let i = 0; i < value.length; i++) {
|
|
88
98
|
const item = value[i];
|
|
89
99
|
if (isDeclarable(item)) {
|
|
90
|
-
entries.push({ bareKey: `${name}_${i}`, value: item, file, provenance:
|
|
100
|
+
entries.push({ bareKey: `${name}_${i}`, value: item, file, provenance: authoredProvenance(item, file) });
|
|
91
101
|
} else if (isCompositeInstance(item)) {
|
|
92
102
|
const indexedName = `${name}_${i}`;
|
|
93
103
|
for (const [expandedName, entity] of expandComposite(indexedName, item)) {
|
|
@@ -52,9 +52,13 @@ import {
|
|
|
52
52
|
collectCompositeOrigins,
|
|
53
53
|
collectParamDependencies,
|
|
54
54
|
collectTagOrigins,
|
|
55
|
+
sourceLocationOf,
|
|
56
|
+
type CompositeCallSite,
|
|
55
57
|
type CompositeParamScope,
|
|
56
58
|
} from "./param-deps";
|
|
57
|
-
import { getInterpolationFields, setPathProvenance } from "../provenance";
|
|
59
|
+
import { getInterpolationFields, getProvenance, setPathProvenance, setProvenance } from "../provenance";
|
|
60
|
+
import { foldProvenanceOfEntities, type FoldProvenance } from "../fold-provenance";
|
|
61
|
+
import { collectEntities } from "./collect";
|
|
58
62
|
import { intrinsicCallFoldsEagerly, intrinsicTagFolds, type IntrinsicDef } from "../lexicon";
|
|
59
63
|
import type { BuildParamValue } from "../build-params";
|
|
60
64
|
|
|
@@ -1479,6 +1483,13 @@ interface CompositeParamRecorder {
|
|
|
1479
1483
|
* `fold()` sees them.
|
|
1480
1484
|
*/
|
|
1481
1485
|
consts: Map<string, ts.Expression>;
|
|
1486
|
+
/**
|
|
1487
|
+
* chant #3597 — the composite call this body was reached from, when the
|
|
1488
|
+
* fold saw it as written. A call made from already-folded values (the
|
|
1489
|
+
* nested-value path, {@link resolveCompositeCall}) has no node, and its
|
|
1490
|
+
* members record origins without locations.
|
|
1491
|
+
*/
|
|
1492
|
+
call?: CompositeCallSite;
|
|
1482
1493
|
}
|
|
1483
1494
|
|
|
1484
1495
|
/** `{ value }` when `node`'s shape was recognized and resolved (value may itself be `undefined`/`null` — e.g. an optional composite member that wasn't created); `undefined` when the shape isn't one the live resolver understands (a plain literal, etc.) — callers fall back to the original, unchanged handling for that shape. */
|
|
@@ -1698,7 +1709,7 @@ async function resolveImportedCall(
|
|
|
1698
1709
|
|
|
1699
1710
|
if (factory) {
|
|
1700
1711
|
const args = await resolveCallArguments(node, calleeName, ctx);
|
|
1701
|
-
const interpreted = await interpretCompositeFactory(factory, args, ctx);
|
|
1712
|
+
const interpreted = await interpretCompositeFactory(factory, args, ctx, { file: ctx.file, node });
|
|
1702
1713
|
if (interpreted) return interpreted.value;
|
|
1703
1714
|
// Declined — the body's shape passed but something in it did not resolve
|
|
1704
1715
|
// (an identifier the module's own imports don't reach, a constructor
|
|
@@ -1808,7 +1819,17 @@ async function invokeResolvedCallee(
|
|
|
1808
1819
|
const packageInternal = ctx.packageRoot !== undefined && isUnderDirectory(modulePath, ctx.packageRoot);
|
|
1809
1820
|
if (isProjectImport(binding.specifier, ctx.file) && !packageInternal) executionCounts.projectFactoryInvocations += 1;
|
|
1810
1821
|
|
|
1811
|
-
|
|
1822
|
+
const result = (Fn as (...fnArgs: unknown[]) => unknown)(...args);
|
|
1823
|
+
// chant#3608 — a host composite invoked rather than interpreted (one the
|
|
1824
|
+
// host publishes, or one whose body interpretation declined) returns the
|
|
1825
|
+
// host's own instance. Mark it as the interpreted form is marked, so an
|
|
1826
|
+
// instance exported whole is expanded and its members, whole or
|
|
1827
|
+
// destructured, are known to come from a composite.
|
|
1828
|
+
const compositeName = (Fn as { compositeName?: unknown }).compositeName;
|
|
1829
|
+
if (typeof compositeName === "string" && isIndexableObject(result) && !isCompositeInstance(result)) {
|
|
1830
|
+
markHostCompositeInstance(result, compositeName);
|
|
1831
|
+
}
|
|
1832
|
+
return result;
|
|
1812
1833
|
}
|
|
1813
1834
|
|
|
1814
1835
|
/**
|
|
@@ -2798,6 +2819,7 @@ function isLiteralPropertyNameNode(node: ts.PropertyName): boolean {
|
|
|
2798
2819
|
function compositeParamRecorder(
|
|
2799
2820
|
factory: InterpretableFactory,
|
|
2800
2821
|
moduleConsts: ReadonlyMap<string, ts.Expression>,
|
|
2822
|
+
call?: CompositeCallSite,
|
|
2801
2823
|
): CompositeParamRecorder {
|
|
2802
2824
|
const whole = new Set<string>();
|
|
2803
2825
|
const destructured = new Map<string, string>();
|
|
@@ -2828,7 +2850,7 @@ function compositeParamRecorder(
|
|
|
2828
2850
|
}
|
|
2829
2851
|
}
|
|
2830
2852
|
|
|
2831
|
-
return { composite: factory.compositeName, scope: { whole, destructured }, consts };
|
|
2853
|
+
return { composite: factory.compositeName, scope: { whole, destructured }, consts, ...(call ? { call } : {}) };
|
|
2832
2854
|
}
|
|
2833
2855
|
|
|
2834
2856
|
/**
|
|
@@ -2852,10 +2874,21 @@ function stampCompositeOrigins(entity: unknown, node: ts.NewExpression, ctx: Res
|
|
|
2852
2874
|
}
|
|
2853
2875
|
}
|
|
2854
2876
|
if (!propsArg) return;
|
|
2855
|
-
|
|
2877
|
+
stampCompositeCall(entity, recorder);
|
|
2878
|
+
const origins = collectCompositeOrigins(propsArg, recorder.consts, recorder.scope, recorder.composite, recorder.call);
|
|
2856
2879
|
for (const [path, origin] of Object.entries(origins)) setPathProvenance(entity, path, origin);
|
|
2857
2880
|
}
|
|
2858
2881
|
|
|
2882
|
+
/**
|
|
2883
|
+
* chant #3597 — record where the composite call that expanded `entity` was
|
|
2884
|
+
* written, beside the per-path origins and in the same pass. First writer
|
|
2885
|
+
* wins, as for every provenance field, so the innermost call is kept.
|
|
2886
|
+
*/
|
|
2887
|
+
function stampCompositeCall(entity: object, recorder: CompositeParamRecorder): void {
|
|
2888
|
+
if (!recorder.call) return;
|
|
2889
|
+
setProvenance(entity, { compositeCall: sourceLocationOf(recorder.call.file, recorder.call.node) });
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2859
2892
|
/**
|
|
2860
2893
|
* chant #3212 — the same record for an entity a registered tag built inside an
|
|
2861
2894
|
* interpreted factory body (`` table`CREATE TABLE ${props.name} ...` ``).
|
|
@@ -2873,7 +2906,16 @@ function stampTagOrigins(entity: unknown, node: ts.TaggedTemplateExpression, ctx
|
|
|
2873
2906
|
const interpolations = ts.isTemplateExpression(node.template)
|
|
2874
2907
|
? node.template.templateSpans.map((span) => span.expression)
|
|
2875
2908
|
: [];
|
|
2876
|
-
|
|
2909
|
+
stampCompositeCall(entity, recorder);
|
|
2910
|
+
const origins = collectTagOrigins(
|
|
2911
|
+
interpolations,
|
|
2912
|
+
fields,
|
|
2913
|
+
entity.props,
|
|
2914
|
+
recorder.consts,
|
|
2915
|
+
recorder.scope,
|
|
2916
|
+
recorder.composite,
|
|
2917
|
+
recorder.call,
|
|
2918
|
+
);
|
|
2877
2919
|
for (const [path, origin] of Object.entries(origins)) setPathProvenance(entity, path, origin);
|
|
2878
2920
|
}
|
|
2879
2921
|
|
|
@@ -2892,6 +2934,7 @@ async function interpretCompositeFactory(
|
|
|
2892
2934
|
factory: InterpretableFactory,
|
|
2893
2935
|
args: readonly unknown[],
|
|
2894
2936
|
ctx: ResolveCtx,
|
|
2937
|
+
call?: CompositeCallSite,
|
|
2895
2938
|
): Promise<{ value: unknown } | undefined> {
|
|
2896
2939
|
// A `Composite()` factory takes exactly one props argument. Anything else at
|
|
2897
2940
|
// the call site means the callee is being used as something this doesn't
|
|
@@ -2932,7 +2975,7 @@ async function interpretCompositeFactory(
|
|
|
2932
2975
|
interpretDepth: ctx.interpretDepth + 1,
|
|
2933
2976
|
// chant #2161 — set unconditionally, and NOT inherited from `ctx`: a nested
|
|
2934
2977
|
// composite records its own parameters, never the enclosing one's.
|
|
2935
|
-
compositeParams: compositeParamRecorder(factory, scope.consts),
|
|
2978
|
+
compositeParams: compositeParamRecorder(factory, scope.consts, call),
|
|
2936
2979
|
};
|
|
2937
2980
|
|
|
2938
2981
|
const bind = (name: string, value: unknown): void => {
|
|
@@ -2973,7 +3016,7 @@ async function interpretCompositeFactory(
|
|
|
2973
3016
|
// rejecting it for not being chant's rather than for being malformed.
|
|
2974
3017
|
// Chant's own form still goes through `Composite`, unchanged.
|
|
2975
3018
|
const value = factory.hostForm
|
|
2976
|
-
? members
|
|
3019
|
+
? markHostCompositeInstance(members, factory.compositeName)
|
|
2977
3020
|
: (Composite<void, CompositeMembers>(() => members as CompositeMembers, factory.compositeName))();
|
|
2978
3021
|
executionCounts.factoryInterpretations += 1;
|
|
2979
3022
|
return { value };
|
|
@@ -2987,6 +3030,53 @@ async function interpretCompositeFactory(
|
|
|
2987
3030
|
}
|
|
2988
3031
|
}
|
|
2989
3032
|
|
|
3033
|
+
/**
|
|
3034
|
+
* chant#3598 — a host composite instance's own marker. Non-enumerable and
|
|
3035
|
+
* symbol-keyed, like provenance, so nothing that serializes or compares the
|
|
3036
|
+
* exports sees it. It carries the composite's name, which the members object
|
|
3037
|
+
* otherwise has no record of once it is returned.
|
|
3038
|
+
*/
|
|
3039
|
+
const HOST_COMPOSITE_INSTANCE = Symbol.for("chant.fold.hostCompositeInstance");
|
|
3040
|
+
|
|
3041
|
+
/**
|
|
3042
|
+
* Mark `members` as the instance a host's composite produced, and return it
|
|
3043
|
+
* unchanged.
|
|
3044
|
+
*
|
|
3045
|
+
* chant#3608 — every declarable member, and every member of a nested host
|
|
3046
|
+
* instance, is also stamped with the composite, the way `expandComposite`
|
|
3047
|
+
* stamps a chant instance's members. A destructured member is never expanded,
|
|
3048
|
+
* so this is the only record it has that a composite wrote it, and without
|
|
3049
|
+
* it a path the fold did not attribute reads as authored and so `direct`.
|
|
3050
|
+
* `setProvenance` keeps the first writer, and a nested instance was marked
|
|
3051
|
+
* when its own call returned, so a member keeps its innermost composite.
|
|
3052
|
+
*/
|
|
3053
|
+
function markHostCompositeInstance(members: Record<string, unknown>, compositeName: string): Record<string, unknown> {
|
|
3054
|
+
if (Object.isExtensible(members) && !Object.prototype.hasOwnProperty.call(members, HOST_COMPOSITE_INSTANCE)) {
|
|
3055
|
+
Object.defineProperty(members, HOST_COMPOSITE_INSTANCE, {
|
|
3056
|
+
value: compositeName,
|
|
3057
|
+
enumerable: false,
|
|
3058
|
+
configurable: true,
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
stampHostCompositeMembers(members, compositeName);
|
|
3062
|
+
return members;
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
/** Stamp `composite` on each declarable member of a host instance, descending into nested host instances. */
|
|
3066
|
+
function stampHostCompositeMembers(members: Record<string, unknown>, compositeName: string): void {
|
|
3067
|
+
for (const member of Object.values(members)) {
|
|
3068
|
+
if (isDeclarable(member)) setProvenance(member, { composite: compositeName });
|
|
3069
|
+
else if (hostCompositeName(member) !== undefined) stampHostCompositeMembers(member as Record<string, unknown>, compositeName);
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
/** The composite name a host composite instance was marked with, or `undefined` for any other value. */
|
|
3074
|
+
function hostCompositeName(value: unknown): string | undefined {
|
|
3075
|
+
if (!isIndexableObject(value)) return undefined;
|
|
3076
|
+
const name = (value as Record<symbol, unknown>)[HOST_COMPOSITE_INSTANCE];
|
|
3077
|
+
return typeof name === "string" ? name : undefined;
|
|
3078
|
+
}
|
|
3079
|
+
|
|
2990
3080
|
/** Evaluate an admissible factory body's statements in order, binding each `const`, and return the `return` expression's value. */
|
|
2991
3081
|
async function interpretFactoryBody(
|
|
2992
3082
|
fn: ts.ArrowFunction | ts.FunctionExpression,
|
|
@@ -4850,6 +4940,20 @@ export interface FoldProjectVerdict {
|
|
|
4850
4940
|
readonly taintedBy?: { from: string; kind: TaintEdgeKind };
|
|
4851
4941
|
/** The file's complete export namespace, present only when `verdict` is "fold". */
|
|
4852
4942
|
readonly exports?: ReadonlyMap<string, unknown>;
|
|
4943
|
+
/**
|
|
4944
|
+
* chant#3598 — fold provenance for the entities this file contributes,
|
|
4945
|
+
* keyed by entity name, in the shape `build()` reports as
|
|
4946
|
+
* `BuildResult.foldProvenance`. Present only when `verdict` is "fold" and
|
|
4947
|
+
* the folded files' entities could be collected (a duplicate entity name
|
|
4948
|
+
* fails collection for the whole set, as it fails a build, and then no
|
|
4949
|
+
* verdict carries it). A file that folds and declares nothing with
|
|
4950
|
+
* properties carries `{}`.
|
|
4951
|
+
*
|
|
4952
|
+
* A "run" file has no entry: foldProject never executes one, so it has no
|
|
4953
|
+
* entities to attribute. That holds under `sandbox` too, where nothing
|
|
4954
|
+
* crosses the child-process wire because no child is started.
|
|
4955
|
+
*/
|
|
4956
|
+
readonly foldProvenance?: FoldProvenance;
|
|
4853
4957
|
}
|
|
4854
4958
|
|
|
4855
4959
|
/**
|
|
@@ -4910,12 +5014,29 @@ export async function foldProject(
|
|
|
4910
5014
|
})),
|
|
4911
5015
|
);
|
|
4912
5016
|
|
|
5017
|
+
const folded = files.filter((file) => attempts.get(file)?.ok === true && !plan.tainted.has(file));
|
|
5018
|
+
const provenanceByFile = foldProvenanceByFile(
|
|
5019
|
+
folded.map((file) => {
|
|
5020
|
+
const attempt = attempts.get(file);
|
|
5021
|
+
return {
|
|
5022
|
+
file,
|
|
5023
|
+
exports: attempt?.ok === true ? expandHostCompositeExports(Object.fromEntries(attempt.exportedValues)) : {},
|
|
5024
|
+
};
|
|
5025
|
+
}),
|
|
5026
|
+
);
|
|
5027
|
+
|
|
4913
5028
|
const out = new Map<string, FoldProjectVerdict>();
|
|
4914
5029
|
for (const file of files) {
|
|
4915
5030
|
const attempt = attempts.get(file)!;
|
|
4916
5031
|
const tentative = attempt.ok ? "fold" : "run";
|
|
4917
5032
|
if (attempt.ok && !plan.tainted.has(file)) {
|
|
4918
|
-
|
|
5033
|
+
const foldProvenance = provenanceByFile?.get(file);
|
|
5034
|
+
out.set(file, {
|
|
5035
|
+
verdict: "fold",
|
|
5036
|
+
tentative,
|
|
5037
|
+
exports: attempt.exportedValues,
|
|
5038
|
+
...(foldProvenance ? { foldProvenance } : {}),
|
|
5039
|
+
});
|
|
4919
5040
|
continue;
|
|
4920
5041
|
}
|
|
4921
5042
|
out.set(file, {
|
|
@@ -4927,3 +5048,124 @@ export async function foldProject(
|
|
|
4927
5048
|
}
|
|
4928
5049
|
return out;
|
|
4929
5050
|
}
|
|
5051
|
+
|
|
5052
|
+
/**
|
|
5053
|
+
* chant#3598 — the folded files' fold provenance, split by declaring file.
|
|
5054
|
+
*
|
|
5055
|
+
* This is `build()`'s computation, not a second one: the same
|
|
5056
|
+
* `collectEntities` over the same folded export namespaces `discover()` hands
|
|
5057
|
+
* it, then `foldProvenanceOfEntities` with `getProvenance`. One call over the
|
|
5058
|
+
* whole folded set rather than one per file, because composite expansion and
|
|
5059
|
+
* cross-directory name disambiguation (#932) are decided over the set and an
|
|
5060
|
+
* entity's name is part of what is being reported. The build root is the
|
|
5061
|
+
* files' common directory, which is what `chant build <dir>` passes when every
|
|
5062
|
+
* file sits under `<dir>`.
|
|
5063
|
+
*
|
|
5064
|
+
* Returns `undefined` when collection throws (a duplicate name), the case
|
|
5065
|
+
* where a build has no entities either.
|
|
5066
|
+
*/
|
|
5067
|
+
function foldProvenanceByFile(
|
|
5068
|
+
modules: Array<{ file: string; exports: Record<string, unknown> }>,
|
|
5069
|
+
): Map<string, FoldProvenance> | undefined {
|
|
5070
|
+
if (modules.length === 0) return new Map();
|
|
5071
|
+
let entities: Map<string, Declarable>;
|
|
5072
|
+
try {
|
|
5073
|
+
entities = collectEntities(modules, commonDirectory(modules.map((m) => m.file)));
|
|
5074
|
+
} catch {
|
|
5075
|
+
return undefined;
|
|
5076
|
+
}
|
|
5077
|
+
|
|
5078
|
+
const byResolved = new Map<string, FoldProvenance>();
|
|
5079
|
+
const out = new Map<string, FoldProvenance>();
|
|
5080
|
+
for (const { file } of modules) {
|
|
5081
|
+
const record: FoldProvenance = {};
|
|
5082
|
+
byResolved.set(resolvePath(file), record);
|
|
5083
|
+
out.set(file, record);
|
|
5084
|
+
}
|
|
5085
|
+
|
|
5086
|
+
const all = foldProvenanceOfEntities(entities, getProvenance);
|
|
5087
|
+
for (const [name, record] of Object.entries(all)) {
|
|
5088
|
+
const sourceFile = getProvenance(entities.get(name) as Declarable)?.sourceFile;
|
|
5089
|
+
const target = sourceFile === undefined ? undefined : byResolved.get(resolvePath(sourceFile));
|
|
5090
|
+
if (target) target[name] = record;
|
|
5091
|
+
}
|
|
5092
|
+
return out;
|
|
5093
|
+
}
|
|
5094
|
+
|
|
5095
|
+
/**
|
|
5096
|
+
* chant#3598 — a file's exports with every host composite instance replaced by
|
|
5097
|
+
* its members, for {@link foldProvenanceByFile} only.
|
|
5098
|
+
*
|
|
5099
|
+
* A host's registration form (chant#2442) returns the members object the
|
|
5100
|
+
* factory built, not a chant `CompositeInstance`, because wrapping it would run
|
|
5101
|
+
* chant's member validation against the host's own entities. `collectEntities`
|
|
5102
|
+
* expands only chant's instances, so an instance exported whole
|
|
5103
|
+
* (`export const store = Store({...})`) contributed no entity and therefore no
|
|
5104
|
+
* record, while a destructured member, a plain declarable export, did.
|
|
5105
|
+
*
|
|
5106
|
+
* The members are named and stamped the way `expandComposite` names and stamps
|
|
5107
|
+
* a chant instance: `${exportName}${Member}`, with a whole-entity `composite`
|
|
5108
|
+
* origin under the paths the fold already attributed. That origin is written
|
|
5109
|
+
* before `collectEntities` adds its `authored` one, and the first writer wins,
|
|
5110
|
+
* so a path the fold did not attribute is `unknown` rather than `direct`.
|
|
5111
|
+
*
|
|
5112
|
+
* Only a top-level export is expanded. A host instance nested in another
|
|
5113
|
+
* composite's body or placed in an array does not fold today, so its file is
|
|
5114
|
+
* `run` and has no record to give.
|
|
5115
|
+
*
|
|
5116
|
+
* The exports a caller receives in the verdict are the originals.
|
|
5117
|
+
*/
|
|
5118
|
+
function expandHostCompositeExports(exports: Record<string, unknown>): Record<string, unknown> {
|
|
5119
|
+
const out: Record<string, unknown> = {};
|
|
5120
|
+
for (const [name, value] of Object.entries(exports)) {
|
|
5121
|
+
if (hostCompositeName(value) === undefined) {
|
|
5122
|
+
out[name] = value;
|
|
5123
|
+
continue;
|
|
5124
|
+
}
|
|
5125
|
+
expandHostCompositeInstance(name, value as Record<string, unknown>, name, out);
|
|
5126
|
+
}
|
|
5127
|
+
return out;
|
|
5128
|
+
}
|
|
5129
|
+
|
|
5130
|
+
/**
|
|
5131
|
+
* chant#3608 — one host instance's members into `out`, a nested host instance
|
|
5132
|
+
* expanded in place under its member's name (`siteMainBucket`), as
|
|
5133
|
+
* `expandComposite` expands a nested chant instance. Every member belongs to
|
|
5134
|
+
* the top-level `instance`; its composite is the innermost one, which
|
|
5135
|
+
* {@link markHostCompositeInstance} already stamped, so the whole-entity
|
|
5136
|
+
* origin names that composite too.
|
|
5137
|
+
*/
|
|
5138
|
+
function expandHostCompositeInstance(
|
|
5139
|
+
prefix: string,
|
|
5140
|
+
members: Record<string, unknown>,
|
|
5141
|
+
instance: string,
|
|
5142
|
+
out: Record<string, unknown>,
|
|
5143
|
+
): void {
|
|
5144
|
+
const compositeName = hostCompositeName(members) as string;
|
|
5145
|
+
for (const [memberName, member] of Object.entries(members)) {
|
|
5146
|
+
if (memberName.length === 0) continue;
|
|
5147
|
+
const fullName = `${prefix}${memberName[0].toUpperCase()}${memberName.slice(1)}`;
|
|
5148
|
+
if (hostCompositeName(member) !== undefined) {
|
|
5149
|
+
expandHostCompositeInstance(fullName, member as Record<string, unknown>, instance, out);
|
|
5150
|
+
continue;
|
|
5151
|
+
}
|
|
5152
|
+
if (!isDeclarable(member)) continue;
|
|
5153
|
+
setProvenance(member, { composite: compositeName, compositeInstance: instance });
|
|
5154
|
+
const composite = getProvenance(member)?.composite ?? compositeName;
|
|
5155
|
+
setPathProvenance(member, "", { kind: "composite", composite, instance });
|
|
5156
|
+
out[fullName] = member;
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5160
|
+
/** The deepest directory every one of `files` sits under. */
|
|
5161
|
+
function commonDirectory(files: readonly string[]): string {
|
|
5162
|
+
const split = files.map((file) => dirname(resolvePath(file)).split(sep));
|
|
5163
|
+
const first = split[0];
|
|
5164
|
+
let depth = first.length;
|
|
5165
|
+
for (const parts of split.slice(1)) {
|
|
5166
|
+
let i = 0;
|
|
5167
|
+
while (i < depth && i < parts.length && parts[i] === first[i]) i++;
|
|
5168
|
+
depth = i;
|
|
5169
|
+
}
|
|
5170
|
+
return first.slice(0, depth).join(sep) || sep;
|
|
5171
|
+
}
|
|
@@ -96,15 +96,28 @@ describe("lexicon-package and re-exported composites are interpreted (chant#3247
|
|
|
96
96
|
expect(folded.errors).toEqual([]);
|
|
97
97
|
expect(foldExecutionCounts()).toMatchObject({ factoryInterpretations: 1, factoryInvocations: 0 });
|
|
98
98
|
const fields = folded.foldProvenance.sinkAssignmentActivityLogSink!.fields;
|
|
99
|
+
// chant#3597 — where the call and each argument were written in main.ts.
|
|
100
|
+
const file = join(srcDir, "main.ts");
|
|
101
|
+
const lines = CALL_SINK.split("\n");
|
|
102
|
+
const line = lines.findIndex((l) => l.includes("ActivityLogSink({")) + 1;
|
|
103
|
+
const at = (needle: string) => ({ file, line, column: lines[line - 1].indexOf(needle) + 1 });
|
|
104
|
+
const call = at("ActivityLogSink({");
|
|
105
|
+
const written: Record<string, string> = {
|
|
106
|
+
location: 'location: "eastus"',
|
|
107
|
+
workspaceId: 'workspaceId: "/subscriptions/s/workspaces/logs"',
|
|
108
|
+
};
|
|
99
109
|
const param = (...parameters: string[]) => ({
|
|
100
110
|
kind: "composite-parameter",
|
|
101
111
|
composite: "ActivityLogSink",
|
|
102
112
|
instance: "sink",
|
|
103
113
|
parameters,
|
|
114
|
+
call,
|
|
115
|
+
arguments: parameters.map((parameter) => ({ parameter, ...at(written[parameter]), text: written[parameter] })),
|
|
104
116
|
});
|
|
105
117
|
expect(fields.location).toEqual(param("location"));
|
|
106
118
|
expect(fields["parameters.logAnalytics.value"]).toEqual(param("workspaceId"));
|
|
107
|
-
expect(fields.name).toEqual({ kind: "composite-literal", composite: "ActivityLogSink", instance: "sink" });
|
|
119
|
+
expect(fields.name).toEqual({ kind: "composite-literal", composite: "ActivityLogSink", instance: "sink", call });
|
|
120
|
+
expect(folded.foldProvenance.sinkAssignmentActivityLogSink!.compositeCall).toEqual(call);
|
|
108
121
|
|
|
109
122
|
// Interpreting is not a second evaluation: the run path writes the same bytes.
|
|
110
123
|
const run = await build(srcDir, [propsSerializer("azure")], undefined, { fold: false, lexicons: ["azure"] });
|