@shapeshift-labs/frontier-lang-compiler 0.2.122 → 0.2.124

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/README.md CHANGED
@@ -289,8 +289,28 @@ the semantic index and project symbol graph.
289
289
  `safeMergeJsTsProject` stays synchronous. When a caller already has parser-backed
290
290
  native import results for merged output files, pass them as `outputProjectImports`
291
291
  with `includeOutputProjectSymbolGraph`. The graph builder matches supplied
292
- imports by `sourcePath` and `sourceHash`, uses them for output graph artifacts,
293
- and falls back to the lightweight scanner for missing or stale files.
292
+ imports by `sourcePath` and `sourceHash`, requires hash-verified matches when
293
+ the merged source has a hash, uses them for output graph artifacts, and falls
294
+ back to the lightweight scanner for missing or stale files.
295
+
296
+ For admission queues that need bounded cross-branch API checks, enable
297
+ `includeProjectGraphDelta`. This additionally builds base, worker, head, and
298
+ output project graph stages and blocks the merge when worker and head both
299
+ change the same public contract, re-export identity, or import target in
300
+ incompatible ways. Parser-backed stage imports can be supplied with
301
+ `baseProjectImports`, `workerProjectImports`, `headProjectImports`, and
302
+ `outputProjectImports`; missing or hash-stale stages fall back to the synchronous
303
+ lightweight scanner. This is a conservative admission gate only: results still
304
+ keep `autoMergeClaim: false` and `semanticEquivalenceClaim: false`.
305
+
306
+ Artifact-size and runtime note: these graph options are deliberately opt-in.
307
+ On a local Node v26.1.0 smoke fixture with 10 small JS/TS files and 36 scanned
308
+ stage files for the delta case, baseline project merge JSON was 115 KB at a
309
+ 21.6 ms median. `includeOutputProjectSymbolGraph` raised the returned JSON to
310
+ 17.8 MB at a 303.1 ms median, and `includeProjectGraphDelta` raised it to
311
+ 83.0 MB at a 1,466.8 ms median. Keep these paths behind admission-queue caps
312
+ for file count, total source bytes, graph edge count, and serialized artifact
313
+ bytes, or expose summary/lazy graph materialization before using them broadly.
294
314
 
295
315
  High-risk native features also have explicit evidence policies. These policies are advisory in this package: they tell a swarm or admission queue what evidence is missing without silently changing the existing readiness classification.
296
316
 
@@ -51,6 +51,15 @@ export type JsTsProjectSafeMergeOutputProjectImports =
51
51
  | ReadonlyMap<string, NativeSourceImportResult>
52
52
  | Readonly<Record<string, NativeSourceImportResult>>;
53
53
 
54
+ export type JsTsProjectGraphStageName = 'base' | 'worker' | 'head' | 'output' | string;
55
+
56
+ export type JsTsProjectSafeMergeProjectGraphImportsByStage = Readonly<{
57
+ base?: JsTsProjectSafeMergeOutputProjectImports;
58
+ worker?: JsTsProjectSafeMergeOutputProjectImports;
59
+ head?: JsTsProjectSafeMergeOutputProjectImports;
60
+ output?: JsTsProjectSafeMergeOutputProjectImports;
61
+ } & Record<string, JsTsProjectSafeMergeOutputProjectImports | undefined>>;
62
+
54
63
  export interface JsTsProjectSafeMergeInput {
55
64
  readonly id?: string;
56
65
  readonly language?: FrontierSourceLanguage | string;
@@ -62,7 +71,12 @@ export interface JsTsProjectSafeMergeInput {
62
71
  readonly allowFileAdditions?: boolean;
63
72
  readonly allowFileDeletes?: boolean;
64
73
  readonly includeOutputProjectSymbolGraph?: boolean;
74
+ readonly includeProjectGraphDelta?: boolean;
65
75
  readonly outputProjectImports?: JsTsProjectSafeMergeOutputProjectImports;
76
+ readonly baseProjectImports?: JsTsProjectSafeMergeOutputProjectImports;
77
+ readonly workerProjectImports?: JsTsProjectSafeMergeOutputProjectImports;
78
+ readonly headProjectImports?: JsTsProjectSafeMergeOutputProjectImports;
79
+ readonly projectGraphImports?: JsTsProjectSafeMergeProjectGraphImportsByStage;
66
80
  readonly moduleResolution?: NativeProjectModuleResolutionOptions;
67
81
  readonly tsconfig?: NativeProjectModuleResolutionOptions;
68
82
  readonly workerChangeSetId?: string;
@@ -115,6 +129,56 @@ export interface JsTsProjectSafeMergeAdmission {
115
129
  readonly conflictKeys: readonly string[];
116
130
  }
117
131
 
132
+ export interface JsTsProjectGraphDeltaStageSummary {
133
+ readonly stage: JsTsProjectGraphStageName;
134
+ readonly sourceFiles: number;
135
+ readonly documents: number;
136
+ readonly symbols: number;
137
+ readonly fileHashes: number;
138
+ readonly importEdges: number;
139
+ readonly exportEdges: number;
140
+ readonly publicContractRegions: number;
141
+ readonly reExportIdentities: number;
142
+ readonly unresolvedImportEdges: number;
143
+ readonly suppliedImports: number;
144
+ readonly matchedSuppliedImports: number;
145
+ readonly scannerFallbackImports: number;
146
+ }
147
+
148
+ export interface JsTsProjectGraphDeltaStage {
149
+ readonly kind: 'frontier.lang.jsTsProjectGraphStage';
150
+ readonly version: 1;
151
+ readonly stage: JsTsProjectGraphStageName;
152
+ readonly projectImport?: NativeProjectImportResult;
153
+ readonly projectSymbolGraph?: NativeProjectSymbolGraphSummary;
154
+ readonly summary: JsTsProjectGraphDeltaStageSummary;
155
+ }
156
+
157
+ export interface JsTsProjectGraphDeltaSummary {
158
+ readonly stages: number;
159
+ readonly sourceFiles: number;
160
+ readonly publicContractRegions: number;
161
+ readonly reExportIdentities: number;
162
+ readonly importEdges: number;
163
+ readonly exportEdges: number;
164
+ readonly unresolvedImportEdges: number;
165
+ readonly suppliedImports: number;
166
+ readonly matchedSuppliedImports: number;
167
+ readonly scannerFallbackImports: number;
168
+ readonly conflicts: number;
169
+ readonly publicContractConflicts: number;
170
+ readonly reExportIdentityConflicts: number;
171
+ readonly importTargetConflicts: number;
172
+ readonly stageSummaries: Readonly<Record<string, JsTsProjectGraphDeltaStageSummary>>;
173
+ }
174
+
175
+ export interface JsTsProjectGraphDelta {
176
+ readonly kind: 'frontier.lang.jsTsProjectGraphDelta';
177
+ readonly version: 1;
178
+ readonly stages: Readonly<Record<string, JsTsProjectGraphDeltaStage>>;
179
+ readonly summary: JsTsProjectGraphDeltaSummary;
180
+ }
181
+
118
182
  export interface JsTsProjectSafeMergeResult {
119
183
  readonly kind: 'frontier.lang.jsTsProjectSafeMerge';
120
184
  readonly version: 1;
@@ -126,6 +190,7 @@ export interface JsTsProjectSafeMergeResult {
126
190
  readonly outputFiles: readonly JsTsProjectSafeMergeOutputFile[];
127
191
  readonly outputProjectImport?: NativeProjectImportResult;
128
192
  readonly outputProjectSymbolGraph?: NativeProjectSymbolGraphSummary;
193
+ readonly projectGraphDelta?: JsTsProjectGraphDelta;
129
194
  readonly conflicts: readonly JsTsSafeMergeConflict[];
130
195
  readonly admission: JsTsProjectSafeMergeAdmission;
131
196
  readonly summary: {
@@ -134,6 +199,11 @@ export interface JsTsProjectSafeMergeResult {
134
199
  readonly blockedFiles: number;
135
200
  readonly outputFiles: number;
136
201
  readonly projectGraphConflicts: number;
202
+ readonly outputProjectGraphConflicts: number;
203
+ readonly projectGraphDeltaConflicts: number;
204
+ readonly projectGraphPublicContractConflicts: number;
205
+ readonly projectGraphReExportIdentityConflicts: number;
206
+ readonly projectGraphImportTargetConflicts: number;
137
207
  readonly semanticArtifactFiles: number;
138
208
  readonly operations: Readonly<Record<string, number>>;
139
209
  };
@@ -1,16 +1,29 @@
1
1
  import { compactRecord } from './js-ts-safe-merge-context.js';
2
+ import { projectGraphDeltaConflicts } from './js-ts-safe-project-merge-graph-delta-conflicts.js';
2
3
 
3
4
  function outputProjectGraphConflicts(projectSymbolGraph) {
4
5
  const importEdges = Array.isArray(projectSymbolGraph?.importEdges) ? projectSymbolGraph.importEdges : [];
5
- const groups = new Map();
6
+ const missingModuleGroups = new Map();
7
+ const missingSymbolGroups = new Map();
6
8
  for (const edge of importEdges) {
7
- if (!isMissingProjectImportEdge(edge)) continue;
8
- const key = [edge.sourcePath, edge.moduleSpecifier, edge.resolutionKind, edge.resolvedModulePath].join('\u0000');
9
- const group = groups.get(key) ?? [];
10
- group.push(edge);
11
- groups.set(key, group);
9
+ if (isMissingProjectImportEdge(edge)) {
10
+ const key = [edge.sourcePath, edge.moduleSpecifier, edge.resolutionKind, edge.resolvedModulePath].join('\u0000');
11
+ const group = missingModuleGroups.get(key) ?? [];
12
+ group.push(edge);
13
+ missingModuleGroups.set(key, group);
14
+ continue;
15
+ }
16
+ if (isMissingProjectImportTargetEdge(edge)) {
17
+ const key = [edge.sourcePath, edge.moduleSpecifier, projectImportTargetName(edge), edge.resolvedModulePath].join('\u0000');
18
+ const group = missingSymbolGroups.get(key) ?? [];
19
+ group.push(edge);
20
+ missingSymbolGroups.set(key, group);
21
+ }
12
22
  }
13
- return [...groups.values()].map(projectGraphMissingImportConflict);
23
+ return [
24
+ ...[...missingModuleGroups.values()].map(projectGraphMissingImportConflict),
25
+ ...[...missingSymbolGroups.values()].map(projectGraphMissingTargetConflict)
26
+ ];
14
27
  }
15
28
 
16
29
  function projectGraphMissingImportConflict(group) {
@@ -34,12 +47,52 @@ function projectGraphMissingImportConflict(group) {
34
47
  };
35
48
  }
36
49
 
50
+ function projectGraphMissingTargetConflict(group) {
51
+ const edge = group[0] ?? {};
52
+ const targetName = projectImportTargetName(edge);
53
+ return {
54
+ code: 'project-output-symbol-unresolved',
55
+ gateId: 'project-symbol-graph',
56
+ message: `Output project graph import ${JSON.stringify(edge.moduleSpecifier ?? 'unknown')} resolves to a module without exported symbol ${JSON.stringify(targetName ?? 'unknown')}.`,
57
+ sourcePath: edge.sourcePath,
58
+ details: compactRecord({
59
+ reasonCode: 'project-output-symbol-unresolved',
60
+ conflictKey: `project-symbol#${edge.sourcePath ?? 'unknown'}#${edge.moduleSpecifier ?? edge.resolvedModulePath ?? 'unknown'}#${targetName ?? 'unknown'}`,
61
+ sourcePath: edge.sourcePath,
62
+ moduleSpecifier: edge.moduleSpecifier,
63
+ resolutionKind: edge.resolutionKind,
64
+ resolvedModulePath: edge.resolvedModulePath,
65
+ targetDocumentId: edge.targetDocumentId,
66
+ targetExportName: targetName,
67
+ edgeIds: uniqueStrings(group.map((record) => record.id)),
68
+ importKinds: uniqueStrings(group.map((record) => record.importKind)),
69
+ importedNames: uniqueStrings(group.map((record) => record.importedName)),
70
+ localNames: uniqueStrings(group.map((record) => record.localName))
71
+ })
72
+ };
73
+ }
74
+
37
75
  function isMissingProjectImportEdge(edge) {
38
76
  return typeof edge?.resolutionKind === 'string' && edge.resolutionKind.endsWith('-missing');
39
77
  }
40
78
 
79
+ function isMissingProjectImportTargetEdge(edge) {
80
+ return hasResolvedProjectModule(edge) && Boolean(projectImportTargetName(edge)) && !edge.resolvedTargetSymbolId;
81
+ }
82
+
83
+ function hasResolvedProjectModule(edge) {
84
+ return Boolean(edge?.targetDocumentId) && typeof edge?.resolutionKind === 'string' && edge.resolutionKind.endsWith('-source');
85
+ }
86
+
87
+ function projectImportTargetName(edge) {
88
+ if (edge?.importKind === 'side-effect' || edge?.importKind === 'namespace') return undefined;
89
+ const name = edge?.importedName ?? edge?.localName ?? edge?.exportedName;
90
+ if (!name || name === '*') return undefined;
91
+ return String(name);
92
+ }
93
+
41
94
  function uniqueStrings(values) {
42
95
  return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
43
96
  }
44
97
 
45
- export { outputProjectGraphConflicts };
98
+ export { outputProjectGraphConflicts, projectGraphDeltaConflicts };
@@ -0,0 +1,279 @@
1
+ import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import { compactRecord } from './js-ts-safe-merge-context.js';
3
+
4
+ function projectGraphDeltaConflicts(projectGraphDelta) {
5
+ const baseGraph = projectGraphDelta?.stages?.base?.projectSymbolGraph;
6
+ const workerGraph = projectGraphDelta?.stages?.worker?.projectSymbolGraph;
7
+ const headGraph = projectGraphDelta?.stages?.head?.projectSymbolGraph;
8
+ const outputGraph = projectGraphDelta?.stages?.output?.projectSymbolGraph;
9
+ if (!baseGraph || !workerGraph || !headGraph) return [];
10
+ return [
11
+ ...changedIdentityConflicts({
12
+ code: 'project-public-contract-delta-conflict',
13
+ label: 'public contract',
14
+ baseRecords: baseGraph.publicContractRegions,
15
+ workerRecords: workerGraph.publicContractRegions,
16
+ headRecords: headGraph.publicContractRegions,
17
+ outputRecords: outputGraph?.publicContractRegions,
18
+ identityKey: publicContractIdentityKey,
19
+ fingerprint: publicContractFingerprint,
20
+ details: publicContractDetails
21
+ }),
22
+ ...changedIdentityConflicts({
23
+ code: 'project-re-export-identity-delta-conflict',
24
+ label: 're-export identity',
25
+ baseRecords: baseGraph.reExportIdentities,
26
+ workerRecords: workerGraph.reExportIdentities,
27
+ headRecords: headGraph.reExportIdentities,
28
+ outputRecords: outputGraph?.reExportIdentities,
29
+ identityKey: reExportIdentityKey,
30
+ fingerprint: reExportIdentityFingerprint,
31
+ details: reExportIdentityDetails
32
+ }),
33
+ ...projectImportTargetDeltaConflicts(projectGraphDelta)
34
+ ];
35
+ }
36
+
37
+ function addProjectGraphDeltaConflictSummary(projectGraphDelta, conflicts) {
38
+ if (!projectGraphDelta) return undefined;
39
+ return {
40
+ ...projectGraphDelta,
41
+ summary: {
42
+ ...projectGraphDelta.summary,
43
+ conflicts: conflicts.length,
44
+ publicContractConflicts: conflicts.filter((conflict) => conflict.code === 'project-public-contract-delta-conflict').length,
45
+ reExportIdentityConflicts: conflicts.filter((conflict) => conflict.code === 'project-re-export-identity-delta-conflict').length,
46
+ importTargetConflicts: conflicts.filter((conflict) => conflict.code === 'project-import-target-delta-conflict').length
47
+ }
48
+ };
49
+ }
50
+
51
+ function changedIdentityConflicts(input) {
52
+ const base = recordsByIdentityKey(input.baseRecords, input.identityKey);
53
+ const worker = recordsByIdentityKey(input.workerRecords, input.identityKey);
54
+ const head = recordsByIdentityKey(input.headRecords, input.identityKey);
55
+ const output = recordsByIdentityKey(input.outputRecords, input.identityKey);
56
+ const keys = uniqueStrings([...base.keys(), ...worker.keys(), ...head.keys()]);
57
+ return keys.flatMap((identityKey) => {
58
+ const baseRecord = base.get(identityKey);
59
+ const workerRecord = worker.get(identityKey);
60
+ const headRecord = head.get(identityKey);
61
+ const baseFingerprint = optionalFingerprint(baseRecord, input.fingerprint);
62
+ const workerFingerprint = optionalFingerprint(workerRecord, input.fingerprint);
63
+ const headFingerprint = optionalFingerprint(headRecord, input.fingerprint);
64
+ if (baseFingerprint === workerFingerprint || baseFingerprint === headFingerprint || workerFingerprint === headFingerprint) return [];
65
+ return [projectGraphDeltaConflict({
66
+ code: input.code,
67
+ label: input.label,
68
+ identityKey,
69
+ baseRecord,
70
+ workerRecord,
71
+ headRecord,
72
+ outputRecord: output.get(identityKey),
73
+ details: input.details
74
+ })];
75
+ });
76
+ }
77
+
78
+ function projectImportTargetDeltaConflicts(projectGraphDelta) {
79
+ const baseStage = projectGraphDelta?.stages?.base;
80
+ const workerStage = projectGraphDelta?.stages?.worker;
81
+ const headStage = projectGraphDelta?.stages?.head;
82
+ const outputStage = projectGraphDelta?.stages?.output;
83
+ const workerEdges = importEdgesByIdentityKey(workerStage?.projectSymbolGraph?.importEdges);
84
+ const baseSymbolIds = semanticSymbolIds(baseStage);
85
+ const headSymbolIds = semanticSymbolIds(headStage);
86
+ const conflicts = [];
87
+ for (const [identityKey, edge] of importEdgesByIdentityKey(outputStage?.projectSymbolGraph?.importEdges)) {
88
+ if (!edge.resolvedTargetSymbolId) continue;
89
+ const workerEdge = workerEdges.get(identityKey);
90
+ if (!workerEdge || workerEdge.resolvedTargetSymbolId === edge.resolvedTargetSymbolId) continue;
91
+ if (baseSymbolIds.has(edge.resolvedTargetSymbolId) || !headSymbolIds.has(edge.resolvedTargetSymbolId)) continue;
92
+ conflicts.push(projectImportTargetDeltaConflict(identityKey, edge, workerEdge));
93
+ }
94
+ return conflicts;
95
+ }
96
+
97
+ function projectGraphDeltaConflict(input) {
98
+ const sourcePath = input.workerRecord?.sourcePath ?? input.headRecord?.sourcePath ?? input.baseRecord?.sourcePath;
99
+ const conflictKey = `project-graph-delta#${input.label.replace(/\s+/g, '-')}#${input.identityKey}`;
100
+ return {
101
+ code: input.code,
102
+ gateId: 'project-graph-delta',
103
+ message: `Worker and head both changed ${input.label} ${JSON.stringify(input.identityKey)} in incompatible ways.`,
104
+ sourcePath,
105
+ details: compactRecord({
106
+ reasonCode: input.code,
107
+ conflictKey,
108
+ identityKey: input.identityKey,
109
+ sourcePath,
110
+ base: input.details(input.baseRecord),
111
+ worker: input.details(input.workerRecord),
112
+ head: input.details(input.headRecord),
113
+ output: input.details(input.outputRecord)
114
+ })
115
+ };
116
+ }
117
+
118
+ function projectImportTargetDeltaConflict(identityKey, edge, workerEdge) {
119
+ return {
120
+ code: 'project-import-target-delta-conflict',
121
+ gateId: 'project-graph-delta',
122
+ message: `Output import ${JSON.stringify(projectImportTargetName(edge) ?? edge.moduleSpecifier ?? 'unknown')} resolves to a head-branch exported symbol that the worker import did not resolve against.`,
123
+ sourcePath: edge.sourcePath,
124
+ details: compactRecord({
125
+ reasonCode: 'project-import-target-delta-conflict',
126
+ conflictKey: `project-graph-delta#import-target#${identityKey}`,
127
+ identityKey,
128
+ sourcePath: edge.sourcePath,
129
+ moduleSpecifier: edge.moduleSpecifier,
130
+ importedName: edge.importedName,
131
+ localName: edge.localName,
132
+ importKind: edge.importKind,
133
+ resolvedModulePath: edge.resolvedModulePath,
134
+ outputTargetSymbolId: edge.resolvedTargetSymbolId,
135
+ workerTargetSymbolId: workerEdge.resolvedTargetSymbolId,
136
+ workerResolutionKind: workerEdge.resolutionKind,
137
+ outputResolutionKind: edge.resolutionKind
138
+ })
139
+ };
140
+ }
141
+
142
+ function recordsByIdentityKey(records, identityKey) {
143
+ const result = new Map();
144
+ for (const record of records ?? []) {
145
+ const key = identityKey(record);
146
+ if (!key || result.has(key)) continue;
147
+ result.set(key, record);
148
+ }
149
+ return result;
150
+ }
151
+
152
+ function publicContractIdentityKey(record) {
153
+ return firstString(
154
+ record?.key,
155
+ stableKey(['public-contract', record?.sourcePath, record?.apiSurfaceKind, record?.edgeKind, record?.moduleSpecifier, record?.exportedName ?? record?.symbolName ?? record?.symbolId])
156
+ );
157
+ }
158
+
159
+ function publicContractFingerprint(record) {
160
+ return hashSemanticValue({
161
+ kind: 'frontier.lang.projectGraphDelta.publicContractFingerprint',
162
+ contractHash: record.contractHash,
163
+ signatureHash: record.signatureHash,
164
+ symbolName: record.symbolName,
165
+ symbolKind: record.symbolKind,
166
+ apiSurfaceKind: record.apiSurfaceKind,
167
+ exportedName: record.exportedName,
168
+ moduleSpecifier: record.moduleSpecifier,
169
+ edgeKind: record.edgeKind
170
+ });
171
+ }
172
+
173
+ function publicContractDetails(record) {
174
+ if (!record) return undefined;
175
+ return compactRecord({
176
+ sourcePath: record.sourcePath,
177
+ key: publicContractIdentityKey(record),
178
+ symbolName: record.symbolName,
179
+ symbolKind: record.symbolKind,
180
+ apiSurfaceKind: record.apiSurfaceKind,
181
+ exportedName: record.exportedName,
182
+ moduleSpecifier: record.moduleSpecifier,
183
+ edgeKind: record.edgeKind,
184
+ signatureHash: record.signatureHash,
185
+ contractHash: record.contractHash,
186
+ sourceHash: record.sourceHash
187
+ });
188
+ }
189
+
190
+ function reExportIdentityKey(record) {
191
+ return stableKey(['re-export-identity', record?.sourcePath, record?.exportedName ?? record?.localName ?? record?.namespace ?? (record?.exportStar || record?.isExportStar ? '*' : undefined)]);
192
+ }
193
+
194
+ function reExportIdentityFingerprint(record) {
195
+ return hashSemanticValue({
196
+ kind: 'frontier.lang.projectGraphDelta.reExportIdentityFingerprint',
197
+ sourcePath: record.sourcePath,
198
+ moduleSpecifier: record.moduleSpecifier,
199
+ exportedName: record.exportedName,
200
+ importedName: record.importedName,
201
+ localName: record.localName,
202
+ namespace: record.namespace,
203
+ isTypeOnly: record.isTypeOnly,
204
+ exportStar: record.exportStar,
205
+ isExportStar: record.isExportStar,
206
+ originSymbolId: record.originSymbolId,
207
+ exportedSymbolId: record.exportedSymbolId,
208
+ localSymbolId: record.localSymbolId
209
+ });
210
+ }
211
+
212
+ function reExportIdentityDetails(record) {
213
+ if (!record) return undefined;
214
+ return compactRecord({
215
+ sourcePath: record.sourcePath,
216
+ key: reExportIdentityKey(record),
217
+ moduleSpecifier: record.moduleSpecifier,
218
+ exportedName: record.exportedName,
219
+ importedName: record.importedName,
220
+ localName: record.localName,
221
+ namespace: record.namespace,
222
+ isTypeOnly: record.isTypeOnly,
223
+ exportStar: record.exportStar,
224
+ isExportStar: record.isExportStar,
225
+ originSymbolId: record.originSymbolId,
226
+ exportedSymbolId: record.exportedSymbolId,
227
+ localSymbolId: record.localSymbolId,
228
+ sourceHash: record.sourceHash
229
+ });
230
+ }
231
+
232
+ function importEdgesByIdentityKey(records) {
233
+ const result = new Map();
234
+ for (const record of records ?? []) {
235
+ const key = importEdgeIdentityKey(record);
236
+ if (!key || result.has(key)) continue;
237
+ result.set(key, record);
238
+ }
239
+ return result;
240
+ }
241
+
242
+ function importEdgeIdentityKey(edge) {
243
+ const targetName = projectImportTargetName(edge);
244
+ if (!edge?.sourcePath || !edge.moduleSpecifier || !targetName) return undefined;
245
+ return stableKey(['import-target', edge.sourcePath, edge.moduleSpecifier, targetName, edge.importKind, edge.isTypeOnly]);
246
+ }
247
+
248
+ function projectImportTargetName(edge) {
249
+ if (edge?.importKind === 'side-effect' || edge?.importKind === 'namespace') return undefined;
250
+ const name = edge?.importedName ?? edge?.localName ?? edge?.exportedName;
251
+ if (!name || name === '*') return undefined;
252
+ return String(name);
253
+ }
254
+
255
+ function optionalFingerprint(record, fingerprint) {
256
+ return record ? fingerprint(record) : undefined;
257
+ }
258
+
259
+ function semanticSymbolIds(stage) {
260
+ return new Set((stage?.projectImport?.semanticIndex?.symbols ?? []).map((symbol) => symbol.id).filter(Boolean));
261
+ }
262
+
263
+ function stableKey(parts) {
264
+ const values = parts.map((part) => part === undefined || part === null ? '' : String(part));
265
+ return values.some(Boolean) ? values.join('#') : undefined;
266
+ }
267
+
268
+ function firstString(...values) {
269
+ for (const value of values) {
270
+ if (value !== undefined && value !== null && String(value)) return String(value);
271
+ }
272
+ return undefined;
273
+ }
274
+
275
+ function uniqueStrings(values) {
276
+ return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
277
+ }
278
+
279
+ export { addProjectGraphDeltaConflictSummary, projectGraphDeltaConflicts };
@@ -1,47 +1,90 @@
1
1
  import { idFragment } from './native-import-utils.js';
2
+ import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
3
+ import { compactRecord } from './js-ts-safe-merge-context.js';
2
4
  import { createNativeProjectImportResult } from './internal/index-impl/createNativeProjectImportResult.js';
3
5
  import { importNativeSource } from './internal/index-impl/importNativeSource.js';
4
6
 
5
7
  function createJsTsProjectSafeMergeGraphArtifacts(input, outputFiles, mergeId) {
6
- const sources = outputFiles.map((file) => ({
7
- id: `${mergeId}_output_${idFragment(file.sourcePath)}`,
8
+ return createProjectGraphStageArtifacts(input, outputFiles, mergeId, 'output', projectImportsForStage(input, 'output'));
9
+ }
10
+
11
+ function createJsTsProjectSafeMergeGraphDelta(input, files, outputFiles, mergeId) {
12
+ const stageFiles = projectGraphDeltaStageFiles(files, outputFiles, input);
13
+ const stages = {};
14
+ for (const stageName of ['base', 'worker', 'head', 'output']) {
15
+ const files = stageFiles?.[stageName];
16
+ if (!Array.isArray(files)) continue;
17
+ stages[stageName] = createProjectGraphStageArtifacts(input, files, mergeId, stageName, projectImportsForStage(input, stageName));
18
+ }
19
+ const stageSummaries = Object.fromEntries(Object.entries(stages).map(([stageName, artifacts]) => [stageName, artifacts.summary]));
20
+ return {
21
+ kind: 'frontier.lang.jsTsProjectGraphDelta',
22
+ version: 1,
23
+ stages,
24
+ summary: {
25
+ stages: Object.keys(stages).length,
26
+ sourceFiles: sumStageSummary(stageSummaries, 'sourceFiles'),
27
+ publicContractRegions: sumStageSummary(stageSummaries, 'publicContractRegions'),
28
+ reExportIdentities: sumStageSummary(stageSummaries, 'reExportIdentities'),
29
+ importEdges: sumStageSummary(stageSummaries, 'importEdges'),
30
+ exportEdges: sumStageSummary(stageSummaries, 'exportEdges'),
31
+ unresolvedImportEdges: sumStageSummary(stageSummaries, 'unresolvedImportEdges'),
32
+ suppliedImports: sumStageSummary(stageSummaries, 'suppliedImports'),
33
+ matchedSuppliedImports: sumStageSummary(stageSummaries, 'matchedSuppliedImports'),
34
+ scannerFallbackImports: sumStageSummary(stageSummaries, 'scannerFallbackImports'),
35
+ stageSummaries
36
+ }
37
+ };
38
+ }
39
+
40
+ function createProjectGraphStageArtifacts(input, files, mergeId, stageName, stageImports) {
41
+ const sources = files.map((file) => ({
42
+ id: `${mergeId}_${stageName}_${idFragment(file.sourcePath)}`,
8
43
  language: file.language ?? input.language ?? languageForPath(file.sourcePath),
9
44
  sourcePath: file.sourcePath,
10
45
  sourceText: file.sourceText,
11
46
  sourceHash: file.sourceHash,
12
- metadata: { semanticImportExpected: true, projectSafeMergeOutput: true }
47
+ metadata: { semanticImportExpected: true, projectSafeMergeStage: stageName, projectSafeMergeOutput: stageName === 'output' }
13
48
  }));
14
- const suppliedImports = normalizeOutputProjectImports(input.outputProjectImports);
49
+ const suppliedImports = normalizeProjectImports(stageImports);
15
50
  const importSelections = sources.map((source) => {
16
- const suppliedImport = matchingOutputProjectImport(source, suppliedImports);
51
+ const suppliedImport = matchingProjectImport(source, suppliedImports);
17
52
  return {
18
53
  importResult: suppliedImport ?? importNativeSource(source),
19
- sourceKind: suppliedImport ? 'supplied-output-project-import' : 'lightweight-output-project-scan'
54
+ sourceKind: suppliedImport ? `supplied-${stageName}-project-import` : `lightweight-${stageName}-project-scan`
20
55
  };
21
56
  });
22
57
  const imports = importSelections.map((selection) => selection.importResult);
58
+ const projectGraphImportSource = {
59
+ stage: stageName,
60
+ suppliedImports: suppliedImports.length,
61
+ matchedSuppliedImports: importSelections.filter((selection) => selection.sourceKind === `supplied-${stageName}-project-import`).length,
62
+ scannerFallbackImports: importSelections.filter((selection) => selection.sourceKind === `lightweight-${stageName}-project-scan`).length
63
+ };
23
64
  const projectImport = createNativeProjectImportResult({
24
- id: `${mergeId}_output_project_import`,
65
+ id: `${mergeId}_${stageName}_project_import`,
25
66
  projectRoot: input.projectRoot,
26
67
  moduleResolution: input.moduleResolution ?? input.tsconfig,
27
68
  sources,
28
69
  metadata: {
29
70
  projectSafeMergeId: mergeId,
30
- outputProjectSymbolGraph: true,
31
- outputProjectImportSource: {
32
- suppliedImports: suppliedImports.length,
33
- matchedSuppliedImports: importSelections.filter((selection) => selection.sourceKind === 'supplied-output-project-import').length,
34
- scannerFallbackImports: importSelections.filter((selection) => selection.sourceKind === 'lightweight-output-project-scan').length
35
- }
71
+ projectGraphStage: stageName,
72
+ outputProjectSymbolGraph: stageName === 'output',
73
+ projectGraphImportSource,
74
+ ...(stageName === 'output' ? { outputProjectImportSource: projectGraphImportSource } : {})
36
75
  }
37
76
  }, imports);
38
77
  return {
78
+ kind: 'frontier.lang.jsTsProjectGraphStage',
79
+ version: 1,
80
+ stage: stageName,
39
81
  projectImport,
40
- projectSymbolGraph: projectImport.projectSymbolGraph
82
+ projectSymbolGraph: projectImport.projectSymbolGraph,
83
+ summary: projectGraphStageSummary(stageName, projectImport.projectSymbolGraph, projectGraphImportSource)
41
84
  };
42
85
  }
43
86
 
44
- function normalizeOutputProjectImports(value) {
87
+ function normalizeProjectImports(value) {
45
88
  if (!value) return [];
46
89
  if (Array.isArray(value)) return value.filter(Boolean);
47
90
  if (value instanceof Map) return [...value.values()].filter(Boolean);
@@ -49,18 +92,93 @@ function normalizeOutputProjectImports(value) {
49
92
  return [];
50
93
  }
51
94
 
52
- function matchingOutputProjectImport(source, imports) {
95
+ function projectImportsForStage(input, stageName) {
96
+ if (stageName === 'output') return input.outputProjectImports ?? input.projectGraphImports?.output;
97
+ if (stageName === 'base') return input.baseProjectImports ?? input.projectGraphImports?.base;
98
+ if (stageName === 'worker') return input.workerProjectImports ?? input.projectGraphImports?.worker;
99
+ if (stageName === 'head') return input.headProjectImports ?? input.projectGraphImports?.head;
100
+ return input.projectGraphImports?.[stageName];
101
+ }
102
+
103
+ function matchingProjectImport(source, imports) {
53
104
  const sourcePath = String(source.sourcePath ?? '');
54
105
  const sourceHash = String(source.sourceHash ?? '');
55
106
  return imports.find((importResult) => {
56
107
  const importSourcePath = sourcePathForImport(importResult);
57
108
  if (importSourcePath && sourcePath && importSourcePath !== sourcePath) return false;
58
109
  const importSourceHash = sourceHashForImport(importResult);
110
+ if (sourceHash && importSourceHash !== sourceHash) return false;
59
111
  if (importSourceHash && sourceHash && importSourceHash !== sourceHash) return false;
60
112
  return importSourcePath === sourcePath || importSourceHash === sourceHash;
61
113
  });
62
114
  }
63
115
 
116
+ function projectGraphStageSummary(stageName, projectSymbolGraph, importSource) {
117
+ const importEdges = Array.isArray(projectSymbolGraph?.importEdges) ? projectSymbolGraph.importEdges : [];
118
+ return {
119
+ stage: stageName,
120
+ sourceFiles: projectSymbolGraph?.sourceCount ?? 0,
121
+ documents: projectSymbolGraph?.documentCount ?? 0,
122
+ symbols: projectSymbolGraph?.symbolCount ?? 0,
123
+ fileHashes: projectSymbolGraph?.fileHashes?.length ?? 0,
124
+ importEdges: importEdges.length,
125
+ exportEdges: projectSymbolGraph?.exportEdges?.length ?? 0,
126
+ publicContractRegions: projectSymbolGraph?.publicContractRegions?.length ?? 0,
127
+ reExportIdentities: projectSymbolGraph?.reExportIdentities?.length ?? 0,
128
+ unresolvedImportEdges: importEdges.filter(isMissingProjectImportEdge).length,
129
+ suppliedImports: importSource.suppliedImports,
130
+ matchedSuppliedImports: importSource.matchedSuppliedImports,
131
+ scannerFallbackImports: importSource.scannerFallbackImports
132
+ };
133
+ }
134
+
135
+ function isMissingProjectImportEdge(edge) {
136
+ return typeof edge?.resolutionKind === 'string' && edge.resolutionKind.endsWith('-missing');
137
+ }
138
+
139
+ function sumStageSummary(stageSummaries, field) {
140
+ return Object.values(stageSummaries).reduce((total, summary) => total + Number(summary?.[field] ?? 0), 0);
141
+ }
142
+
143
+ function projectGraphDeltaStageFiles(files, outputFiles, input) {
144
+ return {
145
+ base: files
146
+ .map((file) => stageFile(file, file.baseSourceText, input))
147
+ .filter(Boolean),
148
+ worker: files
149
+ .map((file) => stageFile(file, workerStageSourceText(file), input))
150
+ .filter(Boolean),
151
+ head: files
152
+ .map((file) => stageFile(file, headStageSourceText(file), input))
153
+ .filter(Boolean),
154
+ output: outputFiles
155
+ };
156
+ }
157
+
158
+ function workerStageSourceText(file) {
159
+ if (file.workerDeleted) return undefined;
160
+ return file.workerSourceText ?? file.baseSourceText;
161
+ }
162
+
163
+ function headStageSourceText(file) {
164
+ if (file.headDeleted) return undefined;
165
+ return file.headSourceText ?? file.baseSourceText;
166
+ }
167
+
168
+ function stageFile(file, sourceText, input) {
169
+ if (typeof sourceText !== 'string' || !file.sourcePath) return undefined;
170
+ return compactRecord({
171
+ sourcePath: file.sourcePath,
172
+ language: file.language ?? input.language,
173
+ sourceText,
174
+ sourceHash: hashText(sourceText)
175
+ });
176
+ }
177
+
178
+ function hashText(sourceText) {
179
+ return hashSemanticValue(sourceText);
180
+ }
181
+
64
182
  function sourcePathForImport(importResult) {
65
183
  return firstString(
66
184
  importResult?.sourcePath,
@@ -92,4 +210,4 @@ function firstString(...values) {
92
210
  return undefined;
93
211
  }
94
212
 
95
- export { createJsTsProjectSafeMergeGraphArtifacts };
213
+ export { createJsTsProjectSafeMergeGraphArtifacts, createJsTsProjectSafeMergeGraphDelta };
@@ -1,8 +1,9 @@
1
1
  import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
2
  import { safeMergeJsTsSource } from './js-ts-safe-merge-composed.js';
3
3
  import { compactRecord } from './js-ts-safe-merge-context.js';
4
- import { createJsTsProjectSafeMergeGraphArtifacts } from './js-ts-safe-project-merge-graph.js';
5
- import { outputProjectGraphConflicts } from './js-ts-safe-project-merge-graph-conflicts.js';
4
+ import { createJsTsProjectSafeMergeGraphArtifacts, createJsTsProjectSafeMergeGraphDelta } from './js-ts-safe-project-merge-graph.js';
5
+ import { addProjectGraphDeltaConflictSummary } from './js-ts-safe-project-merge-graph-delta-conflicts.js';
6
+ import { outputProjectGraphConflicts, projectGraphDeltaConflicts } from './js-ts-safe-project-merge-graph-conflicts.js';
6
7
 
7
8
  function safeMergeJsTsProject(input = {}) {
8
9
  const id = String(input.id ?? 'js_ts_project_safe_merge');
@@ -18,10 +19,16 @@ function safeMergeJsTsProject(input = {}) {
18
19
  sourceHash: file.outputHash,
19
20
  operation: file.operation
20
21
  }));
21
- const graphArtifacts = blockedFiles.length === 0 && input.includeOutputProjectSymbolGraph
22
- ? createJsTsProjectSafeMergeGraphArtifacts(input, outputFiles, id)
22
+ const projectGraphDelta = blockedFiles.length === 0 && input.includeProjectGraphDelta
23
+ ? createJsTsProjectSafeMergeGraphDelta(input, files, outputFiles, id)
23
24
  : undefined;
24
- const graphConflicts = outputProjectGraphConflicts(graphArtifacts?.projectSymbolGraph);
25
+ const graphArtifacts = projectGraphDelta?.stages?.output ?? (blockedFiles.length === 0 && input.includeOutputProjectSymbolGraph
26
+ ? createJsTsProjectSafeMergeGraphArtifacts(input, outputFiles, id)
27
+ : undefined);
28
+ const outputGraphConflicts = outputProjectGraphConflicts(graphArtifacts?.projectSymbolGraph);
29
+ const deltaGraphConflicts = projectGraphDeltaConflicts(projectGraphDelta);
30
+ const projectGraphDeltaWithConflicts = addProjectGraphDeltaConflictSummary(projectGraphDelta, deltaGraphConflicts);
31
+ const graphConflicts = [...outputGraphConflicts, ...deltaGraphConflicts];
25
32
  const status = blockedFiles.length || graphConflicts.length ? 'blocked' : 'merged';
26
33
  const reasonCodes = uniqueStrings([
27
34
  ...blockedFiles.flatMap((file) => file.admission.reasonCodes),
@@ -41,6 +48,7 @@ function safeMergeJsTsProject(input = {}) {
41
48
  outputFiles,
42
49
  outputProjectImport: graphArtifacts?.projectImport,
43
50
  outputProjectSymbolGraph: graphArtifacts?.projectSymbolGraph,
51
+ projectGraphDelta: projectGraphDeltaWithConflicts,
44
52
  conflicts: [...fileResults.flatMap((file) => file.conflicts), ...graphConflicts],
45
53
  admission: {
46
54
  status: status === 'merged' ? 'auto-merge-candidate' : 'blocked',
@@ -59,7 +67,10 @@ function safeMergeJsTsProject(input = {}) {
59
67
  projectRoot: input.projectRoot,
60
68
  filesInput: Array.isArray(input.files) ? 'records' : 'maps',
61
69
  outputProjectSymbolGraph: Boolean(graphArtifacts?.projectSymbolGraph),
70
+ projectGraphDelta: Boolean(projectGraphDeltaWithConflicts),
62
71
  projectGraphConflicts: graphConflicts.length || undefined,
72
+ outputProjectGraphConflicts: outputGraphConflicts.length || undefined,
73
+ projectGraphDeltaConflicts: deltaGraphConflicts.length || undefined,
63
74
  autoMergeClaim: false,
64
75
  semanticEquivalenceClaim: false
65
76
  })
@@ -240,20 +251,24 @@ function policyForFile(input, sourcePath) {
240
251
 
241
252
  function sourceLedgersForFile(input, sourcePath) {
242
253
  const byPath = input.sourceLedgersByPath?.[sourcePath] ?? input.sourceLedgers?.[sourcePath];
243
- if (byPath) return byPath;
244
- if (input.sourceLedgers?.base || input.sourceLedgers?.worker || input.sourceLedgers?.head) return input.sourceLedgers;
245
- return undefined;
254
+ return byPath ?? (input.sourceLedgers?.base || input.sourceLedgers?.worker || input.sourceLedgers?.head ? input.sourceLedgers : undefined);
246
255
  }
247
256
 
248
257
  function projectSummary(files, graphConflicts = []) {
249
258
  const byOperation = {};
250
259
  for (const file of files) byOperation[file.operation] = (byOperation[file.operation] ?? 0) + 1;
260
+ const deltaConflicts = graphConflicts.filter((conflict) => conflict.gateId === 'project-graph-delta');
251
261
  return {
252
262
  files: files.length,
253
263
  mergedFiles: files.filter((file) => file.status === 'merged').length,
254
264
  blockedFiles: files.filter((file) => file.status === 'blocked').length,
255
265
  outputFiles: files.filter((file) => typeof file.outputSourceText === 'string').length,
256
266
  projectGraphConflicts: graphConflicts.length,
267
+ outputProjectGraphConflicts: graphConflicts.length - deltaConflicts.length,
268
+ projectGraphDeltaConflicts: deltaConflicts.length,
269
+ projectGraphPublicContractConflicts: deltaConflicts.filter((conflict) => conflict.code === 'project-public-contract-delta-conflict').length,
270
+ projectGraphReExportIdentityConflicts: deltaConflicts.filter((conflict) => conflict.code === 'project-re-export-identity-delta-conflict').length,
271
+ projectGraphImportTargetConflicts: deltaConflicts.filter((conflict) => conflict.code === 'project-import-target-delta-conflict').length,
257
272
  semanticArtifactFiles: files.filter((file) => file.semanticArtifacts).length,
258
273
  operations: byOperation
259
274
  };
@@ -283,21 +298,15 @@ function blockedAdmission(reasonCode) {
283
298
  };
284
299
  }
285
300
 
286
- function hashText(text) {
287
- return typeof text === 'string' ? hashSemanticValue(text) : undefined;
288
- }
301
+ function hashText(text) { return typeof text === 'string' ? hashSemanticValue(text) : undefined; }
289
302
 
290
- function stringOrUndefined(value) {
291
- return typeof value === 'string' ? value : undefined;
292
- }
303
+ function stringOrUndefined(value) { return typeof value === 'string' ? value : undefined; }
293
304
 
294
305
  function safeId(value) {
295
306
  return String(value ?? 'unknown').replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'file';
296
307
  }
297
308
 
298
- function bySourcePath(left, right) {
299
- return String(left.sourcePath ?? '').localeCompare(String(right.sourcePath ?? ''));
300
- }
309
+ function bySourcePath(left, right) { return String(left.sourcePath ?? '').localeCompare(String(right.sourcePath ?? '')); }
301
310
 
302
311
  function uniqueStrings(values) {
303
312
  return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-compiler",
3
- "version": "0.2.122",
3
+ "version": "0.2.124",
4
4
  "description": "Compiler facade for Frontier Lang source documents and language projection adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",