@shapeshift-labs/frontier-lang-compiler 0.2.121 → 0.2.123

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
@@ -286,6 +286,23 @@ re-export, export-star, local export, `export default`, and TypeScript
286
286
  `importedName`, `exportedName`, `isTypeOnly`, and public-contract metadata into
287
287
  the semantic index and project symbol graph.
288
288
 
289
+ `safeMergeJsTsProject` stays synchronous. When a caller already has parser-backed
290
+ native import results for merged output files, pass them as `outputProjectImports`
291
+ with `includeOutputProjectSymbolGraph`. The graph builder matches supplied
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
+
289
306
  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.
290
307
 
291
308
  ```js
@@ -7,6 +7,7 @@ import type {
7
7
  JsTsSafeMergeSummary
8
8
  } from './js-ts-safe-merge.js';
9
9
  import type { JsTsSafeMemberMergePolicy, JsTsSafeMemberMergePolicyRegion } from './js-ts-safe-member-merge.js';
10
+ import type { NativeSourceImportResult } from './import-adapter-core.js';
10
11
  import type { NativeProjectImportResult, NativeProjectSymbolGraphSummary } from './native-project.js';
11
12
  import type { NativeProjectModuleResolutionOptions } from './native-project-module-resolution.js';
12
13
 
@@ -45,6 +46,20 @@ export type JsTsProjectSafeMergeFileMap =
45
46
  | ReadonlyMap<string, string>
46
47
  | readonly { readonly sourcePath?: string; readonly path?: string; readonly sourceText?: string; readonly text?: string }[];
47
48
 
49
+ export type JsTsProjectSafeMergeOutputProjectImports =
50
+ | readonly NativeSourceImportResult[]
51
+ | ReadonlyMap<string, NativeSourceImportResult>
52
+ | Readonly<Record<string, NativeSourceImportResult>>;
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
+
48
63
  export interface JsTsProjectSafeMergeInput {
49
64
  readonly id?: string;
50
65
  readonly language?: FrontierSourceLanguage | string;
@@ -56,6 +71,12 @@ export interface JsTsProjectSafeMergeInput {
56
71
  readonly allowFileAdditions?: boolean;
57
72
  readonly allowFileDeletes?: boolean;
58
73
  readonly includeOutputProjectSymbolGraph?: boolean;
74
+ readonly includeProjectGraphDelta?: boolean;
75
+ readonly outputProjectImports?: JsTsProjectSafeMergeOutputProjectImports;
76
+ readonly baseProjectImports?: JsTsProjectSafeMergeOutputProjectImports;
77
+ readonly workerProjectImports?: JsTsProjectSafeMergeOutputProjectImports;
78
+ readonly headProjectImports?: JsTsProjectSafeMergeOutputProjectImports;
79
+ readonly projectGraphImports?: JsTsProjectSafeMergeProjectGraphImportsByStage;
59
80
  readonly moduleResolution?: NativeProjectModuleResolutionOptions;
60
81
  readonly tsconfig?: NativeProjectModuleResolutionOptions;
61
82
  readonly workerChangeSetId?: string;
@@ -108,6 +129,56 @@ export interface JsTsProjectSafeMergeAdmission {
108
129
  readonly conflictKeys: readonly string[];
109
130
  }
110
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
+
111
182
  export interface JsTsProjectSafeMergeResult {
112
183
  readonly kind: 'frontier.lang.jsTsProjectSafeMerge';
113
184
  readonly version: 1;
@@ -119,6 +190,7 @@ export interface JsTsProjectSafeMergeResult {
119
190
  readonly outputFiles: readonly JsTsProjectSafeMergeOutputFile[];
120
191
  readonly outputProjectImport?: NativeProjectImportResult;
121
192
  readonly outputProjectSymbolGraph?: NativeProjectSymbolGraphSummary;
193
+ readonly projectGraphDelta?: JsTsProjectGraphDelta;
122
194
  readonly conflicts: readonly JsTsSafeMergeConflict[];
123
195
  readonly admission: JsTsProjectSafeMergeAdmission;
124
196
  readonly summary: {
@@ -126,6 +198,12 @@ export interface JsTsProjectSafeMergeResult {
126
198
  readonly mergedFiles: number;
127
199
  readonly blockedFiles: number;
128
200
  readonly outputFiles: number;
201
+ readonly projectGraphConflicts: number;
202
+ readonly outputProjectGraphConflicts: number;
203
+ readonly projectGraphDeltaConflicts: number;
204
+ readonly projectGraphPublicContractConflicts: number;
205
+ readonly projectGraphReExportIdentityConflicts: number;
206
+ readonly projectGraphImportTargetConflicts: number;
129
207
  readonly semanticArtifactFiles: number;
130
208
  readonly operations: Readonly<Record<string, number>>;
131
209
  };
@@ -182,13 +182,16 @@ export interface NativeProjectSymbolGraphSummary {
182
182
  readonly remainingFields: readonly NativeProjectSymbolGraphRemainingField[];
183
183
  }
184
184
 
185
+ export interface NativeProjectSourcePreservationSummary { readonly total: number; readonly exactSourceAvailable: number; readonly sourceBytes: number; readonly tokens: number; readonly trivia: number; readonly directives: number; readonly truncated: boolean; readonly ids: readonly string[]; }
186
+
185
187
  export interface NativeProjectImportResultMetadata extends Record<string, unknown> {
186
- readonly importResultContract?: NativeImportResultContract;
187
- readonly projectAdmission?: NativeProjectImportAdmission;
188
- readonly semanticMergeAdmission?: NativeProjectImportAdmission;
189
- readonly nativeImportLossSummary?: NativeImportLossSummary;
190
- readonly semanticMergeReadiness?: SemanticMergeReadiness;
188
+ readonly sourceCount?: number; readonly sourcePaths?: readonly string[];
189
+ readonly importResultContract?: NativeImportResultContract; readonly projectAdmission?: NativeProjectImportAdmission;
190
+ readonly semanticMergeAdmission?: NativeProjectImportAdmission; readonly nativeImportLossSummary?: NativeImportLossSummary;
191
+ readonly sourcePreservationSummary?: NativeProjectSourcePreservationSummary; readonly semanticMergeReadiness?: SemanticMergeReadiness;
191
192
  readonly readinessReasons?: readonly string[];
193
+ readonly regionSummary?: NativeImportRegionSummary; readonly sourceMapSummary?: NativeImportSourceMapSummary;
194
+ readonly adapterCoverageSummary?: NativeImportAdapterCoverageContract;
192
195
  readonly projectSymbolGraph?: NativeProjectSymbolGraphSummary;
193
196
  }
194
197
 
@@ -244,24 +244,17 @@ function publicContractRegionForDeclaration(declaration, ownershipRegion, module
244
244
 
245
245
  function reExportIdentityForDeclaration(declaration, input, documentId, relationId, ownershipRegion, moduleEdge) {
246
246
  if (!moduleEdge?.isReExport && !declaration.reExport) return undefined;
247
+ const bindingCount = Number(declaration.nativeNode?.metadata?.bindingCount ?? declaration.metadata?.bindingCount ?? 0);
248
+ if (declaration.symbolKind === 'module' && !declaration.exportStar && !declaration.metadata?.exportStar && bindingCount > 1) return undefined;
249
+ const identityId = hashSemanticValue([relationId, declaration.symbolId, declaration.exportedName ?? declaration.metadata?.exportedName, declaration.importedName ?? declaration.metadata?.importedName, declaration.localName ?? declaration.metadata?.localName]);
250
+ const stableId = bindingCount > 1 ? idFragment(identityId) : idFragment(relationId);
247
251
  return compactRecord({
248
- kind: 'frontier.lang.reExportIdentity',
249
- version: 1,
250
- id: `reexport_${idFragment(relationId)}`,
251
- sourceDocumentId: documentId,
252
- sourcePath: input.sourcePath,
253
- sourceHash: input.sourceHash,
254
- moduleSpecifier: moduleEdge?.moduleSpecifier,
255
- exportedName: declaration.exportedName ?? declaration.metadata?.exportedName,
256
- importedName: declaration.importedName ?? declaration.metadata?.importedName,
257
- localName: declaration.localName ?? declaration.metadata?.localName,
258
- namespace: declaration.namespace ?? declaration.metadata?.namespace,
259
- isTypeOnly: declaration.isTypeOnly ?? declaration.metadata?.isTypeOnly ?? declaration.metadata?.typeOnly,
260
- exportStar: declaration.exportStar ?? declaration.metadata?.exportStar,
261
- symbolId: declaration.symbolId,
262
- relationId,
263
- ownershipRegionId: ownershipRegion.id,
264
- ownershipRegionKey: ownershipRegion.key,
252
+ kind: 'frontier.lang.reExportIdentity', version: 1, id: `reexport_${stableId}`, sourceDocumentId: documentId,
253
+ sourcePath: input.sourcePath, sourceHash: input.sourceHash, moduleSpecifier: moduleEdge?.moduleSpecifier,
254
+ exportedName: declaration.exportedName ?? declaration.metadata?.exportedName, importedName: declaration.importedName ?? declaration.metadata?.importedName,
255
+ localName: declaration.localName ?? declaration.metadata?.localName, namespace: declaration.namespace ?? declaration.metadata?.namespace,
256
+ isTypeOnly: declaration.isTypeOnly ?? declaration.metadata?.isTypeOnly ?? declaration.metadata?.typeOnly, exportStar: declaration.exportStar ?? declaration.metadata?.exportStar,
257
+ symbolId: declaration.symbolId, relationId, ownershipRegionId: ownershipRegion.id, ownershipRegionKey: ownershipRegion.key,
265
258
  publicContract: true
266
259
  });
267
260
  }
@@ -59,6 +59,7 @@ function createJsTsSafeMergeSemanticArtifacts(input = {}, merge = {}) {
59
59
  id: `js_ts_safe_merge_replay_${idFragment(id)}`,
60
60
  projection,
61
61
  currentSourceText: headSourceText,
62
+ currentSourceHash: headReplaySourceHash(input),
62
63
  currentSourcePath: sourcePath,
63
64
  language
64
65
  });
@@ -66,6 +67,7 @@ function createJsTsSafeMergeSemanticArtifacts(input = {}, merge = {}) {
66
67
  id: `js_ts_safe_merge_replay_already_applied_${idFragment(id)}`,
67
68
  projection,
68
69
  currentSourceText: mergedSourceText,
70
+ currentSourceHash: projection.projectedHash,
69
71
  currentSourcePath: sourcePath,
70
72
  language
71
73
  });
@@ -74,6 +76,8 @@ function createJsTsSafeMergeSemanticArtifacts(input = {}, merge = {}) {
74
76
  const status = !blocked && replayReady && alreadyAppliedReady ? 'verified' : 'blocked';
75
77
  const finalReasonCodes = uniqueStrings([
76
78
  ...reasonCodes,
79
+ ...(replayReady ? [] : replay.admission?.reasonCodes ?? replay.summary?.reasonCodes ?? []),
80
+ ...(alreadyAppliedReady ? [] : alreadyAppliedReplay.admission?.reasonCodes ?? alreadyAppliedReplay.summary?.reasonCodes ?? []),
77
81
  replayReady ? undefined : `semantic-replay-${replay.status}`,
78
82
  alreadyAppliedReady ? undefined : `semantic-replay-already-applied-${alreadyAppliedReplay.status}`
79
83
  ]);
@@ -263,4 +267,12 @@ function countBy(values, keyFor) {
263
267
  return result;
264
268
  }
265
269
 
270
+ function headReplaySourceHash(input) {
271
+ return nativeSemanticHash(input.currentSourceHash) ?? nativeSemanticHash(input.headHash);
272
+ }
273
+
274
+ function nativeSemanticHash(value) {
275
+ return typeof value === 'string' && /^fnv1a32:[0-9a-f]+$/i.test(value) ? value : undefined;
276
+ }
277
+
266
278
  export { createJsTsSafeMergeSemanticArtifacts };
@@ -0,0 +1,98 @@
1
+ import { compactRecord } from './js-ts-safe-merge-context.js';
2
+ import { projectGraphDeltaConflicts } from './js-ts-safe-project-merge-graph-delta-conflicts.js';
3
+
4
+ function outputProjectGraphConflicts(projectSymbolGraph) {
5
+ const importEdges = Array.isArray(projectSymbolGraph?.importEdges) ? projectSymbolGraph.importEdges : [];
6
+ const missingModuleGroups = new Map();
7
+ const missingSymbolGroups = new Map();
8
+ for (const edge of importEdges) {
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
+ }
22
+ }
23
+ return [
24
+ ...[...missingModuleGroups.values()].map(projectGraphMissingImportConflict),
25
+ ...[...missingSymbolGroups.values()].map(projectGraphMissingTargetConflict)
26
+ ];
27
+ }
28
+
29
+ function projectGraphMissingImportConflict(group) {
30
+ const edge = group[0] ?? {};
31
+ return {
32
+ code: 'project-output-module-unresolved',
33
+ gateId: 'project-symbol-graph',
34
+ message: `Output project graph contains unresolved ${edge.resolutionKind ?? 'missing'} import ${JSON.stringify(edge.moduleSpecifier ?? edge.resolvedModulePath ?? 'unknown')}.`,
35
+ sourcePath: edge.sourcePath,
36
+ details: compactRecord({
37
+ reasonCode: 'project-output-module-unresolved',
38
+ conflictKey: `project-module#${edge.sourcePath ?? 'unknown'}#${edge.moduleSpecifier ?? edge.resolvedModulePath ?? 'unknown'}`,
39
+ sourcePath: edge.sourcePath,
40
+ moduleSpecifier: edge.moduleSpecifier,
41
+ resolutionKind: edge.resolutionKind,
42
+ resolvedModulePath: edge.resolvedModulePath,
43
+ edgeIds: uniqueStrings(group.map((record) => record.id)),
44
+ importKinds: uniqueStrings(group.map((record) => record.importKind)),
45
+ importedNames: uniqueStrings(group.map((record) => record.importedName))
46
+ })
47
+ };
48
+ }
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
+
75
+ function isMissingProjectImportEdge(edge) {
76
+ return typeof edge?.resolutionKind === 'string' && edge.resolutionKind.endsWith('-missing');
77
+ }
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
+
94
+ function uniqueStrings(values) {
95
+ return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
96
+ }
97
+
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,36 +1,213 @@
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
- metadata: { semanticImportExpected: true, projectSafeMergeOutput: true }
46
+ sourceHash: file.sourceHash,
47
+ metadata: { semanticImportExpected: true, projectSafeMergeStage: stageName, projectSafeMergeOutput: stageName === 'output' }
12
48
  }));
13
- const imports = sources.map((source) => importNativeSource(source));
49
+ const suppliedImports = normalizeProjectImports(stageImports);
50
+ const importSelections = sources.map((source) => {
51
+ const suppliedImport = matchingProjectImport(source, suppliedImports);
52
+ return {
53
+ importResult: suppliedImport ?? importNativeSource(source),
54
+ sourceKind: suppliedImport ? `supplied-${stageName}-project-import` : `lightweight-${stageName}-project-scan`
55
+ };
56
+ });
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
+ };
14
64
  const projectImport = createNativeProjectImportResult({
15
- id: `${mergeId}_output_project_import`,
65
+ id: `${mergeId}_${stageName}_project_import`,
16
66
  projectRoot: input.projectRoot,
17
67
  moduleResolution: input.moduleResolution ?? input.tsconfig,
18
68
  sources,
19
69
  metadata: {
20
70
  projectSafeMergeId: mergeId,
21
- outputProjectSymbolGraph: true
71
+ projectGraphStage: stageName,
72
+ outputProjectSymbolGraph: stageName === 'output',
73
+ projectGraphImportSource,
74
+ ...(stageName === 'output' ? { outputProjectImportSource: projectGraphImportSource } : {})
22
75
  }
23
76
  }, imports);
24
77
  return {
78
+ kind: 'frontier.lang.jsTsProjectGraphStage',
79
+ version: 1,
80
+ stage: stageName,
25
81
  projectImport,
26
- projectSymbolGraph: projectImport.projectSymbolGraph
82
+ projectSymbolGraph: projectImport.projectSymbolGraph,
83
+ summary: projectGraphStageSummary(stageName, projectImport.projectSymbolGraph, projectGraphImportSource)
27
84
  };
28
85
  }
29
86
 
87
+ function normalizeProjectImports(value) {
88
+ if (!value) return [];
89
+ if (Array.isArray(value)) return value.filter(Boolean);
90
+ if (value instanceof Map) return [...value.values()].filter(Boolean);
91
+ if (typeof value === 'object') return Object.values(value).filter(Boolean);
92
+ return [];
93
+ }
94
+
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) {
104
+ const sourcePath = String(source.sourcePath ?? '');
105
+ const sourceHash = String(source.sourceHash ?? '');
106
+ return imports.find((importResult) => {
107
+ const importSourcePath = sourcePathForImport(importResult);
108
+ if (importSourcePath && sourcePath && importSourcePath !== sourcePath) return false;
109
+ const importSourceHash = sourceHashForImport(importResult);
110
+ if (sourceHash && importSourceHash !== sourceHash) return false;
111
+ if (importSourceHash && sourceHash && importSourceHash !== sourceHash) return false;
112
+ return importSourcePath === sourcePath || importSourceHash === sourceHash;
113
+ });
114
+ }
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
+
182
+ function sourcePathForImport(importResult) {
183
+ return firstString(
184
+ importResult?.sourcePath,
185
+ importResult?.nativeSource?.sourcePath,
186
+ importResult?.nativeAst?.sourcePath,
187
+ importResult?.semanticIndex?.documents?.[0]?.path
188
+ );
189
+ }
190
+
191
+ function sourceHashForImport(importResult) {
192
+ return firstString(
193
+ importResult?.sourceHash,
194
+ importResult?.nativeSource?.sourceHash,
195
+ importResult?.nativeAst?.sourceHash,
196
+ importResult?.semanticIndex?.documents?.[0]?.sourceHash
197
+ );
198
+ }
199
+
30
200
  function languageForPath(sourcePath) {
31
201
  const path = String(sourcePath ?? '').toLowerCase();
32
202
  if (path.endsWith('.js') || path.endsWith('.jsx') || path.endsWith('.mjs') || path.endsWith('.cjs')) return 'javascript';
33
203
  return 'typescript';
34
204
  }
35
205
 
36
- export { createJsTsProjectSafeMergeGraphArtifacts };
206
+ function firstString(...values) {
207
+ for (const value of values) {
208
+ if (value !== undefined && value !== null && String(value)) return String(value);
209
+ }
210
+ return undefined;
211
+ }
212
+
213
+ export { createJsTsProjectSafeMergeGraphArtifacts, createJsTsProjectSafeMergeGraphDelta };
@@ -1,14 +1,15 @@
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';
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';
5
7
 
6
8
  function safeMergeJsTsProject(input = {}) {
7
9
  const id = String(input.id ?? 'js_ts_project_safe_merge');
8
10
  const files = normalizeProjectFiles(input);
9
11
  const fileResults = files.map((file) => mergeProjectFile(file, input, id));
10
12
  const blockedFiles = fileResults.filter((file) => file.status === 'blocked');
11
- const status = blockedFiles.length ? 'blocked' : 'merged';
12
13
  const outputFiles = fileResults
13
14
  .filter((file) => typeof file.outputSourceText === 'string')
14
15
  .map((file) => compactRecord({
@@ -18,10 +19,25 @@ function safeMergeJsTsProject(input = {}) {
18
19
  sourceHash: file.outputHash,
19
20
  operation: file.operation
20
21
  }));
21
- const reasonCodes = uniqueStrings(blockedFiles.flatMap((file) => file.admission.reasonCodes));
22
- const graphArtifacts = status === 'merged' && input.includeOutputProjectSymbolGraph
23
- ? createJsTsProjectSafeMergeGraphArtifacts(input, outputFiles, id)
22
+ const projectGraphDelta = blockedFiles.length === 0 && input.includeProjectGraphDelta
23
+ ? createJsTsProjectSafeMergeGraphDelta(input, files, outputFiles, id)
24
24
  : undefined;
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];
32
+ const status = blockedFiles.length || graphConflicts.length ? 'blocked' : 'merged';
33
+ const reasonCodes = uniqueStrings([
34
+ ...blockedFiles.flatMap((file) => file.admission.reasonCodes),
35
+ ...graphConflicts.map((conflict) => conflict.code)
36
+ ]);
37
+ const conflictKeys = uniqueStrings([
38
+ ...fileResults.flatMap((file) => file.conflictKeys),
39
+ ...graphConflicts.map((conflict) => conflict.details?.conflictKey)
40
+ ]);
25
41
  const core = {
26
42
  kind: 'frontier.lang.jsTsProjectSafeMerge',
27
43
  version: 1,
@@ -32,7 +48,8 @@ function safeMergeJsTsProject(input = {}) {
32
48
  outputFiles,
33
49
  outputProjectImport: graphArtifacts?.projectImport,
34
50
  outputProjectSymbolGraph: graphArtifacts?.projectSymbolGraph,
35
- conflicts: fileResults.flatMap((file) => file.conflicts),
51
+ projectGraphDelta: projectGraphDeltaWithConflicts,
52
+ conflicts: [...fileResults.flatMap((file) => file.conflicts), ...graphConflicts],
36
53
  admission: {
37
54
  status: status === 'merged' ? 'auto-merge-candidate' : 'blocked',
38
55
  action: status === 'merged' ? 'apply-project' : 'human-review',
@@ -41,15 +58,19 @@ function safeMergeJsTsProject(input = {}) {
41
58
  autoMergeClaim: false,
42
59
  semanticEquivalenceClaim: false,
43
60
  reasonCodes,
44
- conflictKeys: uniqueStrings(fileResults.flatMap((file) => file.conflictKeys))
61
+ conflictKeys
45
62
  },
46
- summary: projectSummary(fileResults),
63
+ summary: projectSummary(fileResults, graphConflicts),
47
64
  metadata: compactRecord({
48
65
  workerChangeSetId: input.workerChangeSetId,
49
66
  headChangeSetId: input.headChangeSetId,
50
67
  projectRoot: input.projectRoot,
51
68
  filesInput: Array.isArray(input.files) ? 'records' : 'maps',
52
69
  outputProjectSymbolGraph: Boolean(graphArtifacts?.projectSymbolGraph),
70
+ projectGraphDelta: Boolean(projectGraphDeltaWithConflicts),
71
+ projectGraphConflicts: graphConflicts.length || undefined,
72
+ outputProjectGraphConflicts: outputGraphConflicts.length || undefined,
73
+ projectGraphDeltaConflicts: deltaGraphConflicts.length || undefined,
53
74
  autoMergeClaim: false,
54
75
  semanticEquivalenceClaim: false
55
76
  })
@@ -230,19 +251,24 @@ function policyForFile(input, sourcePath) {
230
251
 
231
252
  function sourceLedgersForFile(input, sourcePath) {
232
253
  const byPath = input.sourceLedgersByPath?.[sourcePath] ?? input.sourceLedgers?.[sourcePath];
233
- if (byPath) return byPath;
234
- if (input.sourceLedgers?.base || input.sourceLedgers?.worker || input.sourceLedgers?.head) return input.sourceLedgers;
235
- return undefined;
254
+ return byPath ?? (input.sourceLedgers?.base || input.sourceLedgers?.worker || input.sourceLedgers?.head ? input.sourceLedgers : undefined);
236
255
  }
237
256
 
238
- function projectSummary(files) {
257
+ function projectSummary(files, graphConflicts = []) {
239
258
  const byOperation = {};
240
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');
241
261
  return {
242
262
  files: files.length,
243
263
  mergedFiles: files.filter((file) => file.status === 'merged').length,
244
264
  blockedFiles: files.filter((file) => file.status === 'blocked').length,
245
265
  outputFiles: files.filter((file) => typeof file.outputSourceText === 'string').length,
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,
246
272
  semanticArtifactFiles: files.filter((file) => file.semanticArtifacts).length,
247
273
  operations: byOperation
248
274
  };
@@ -272,21 +298,15 @@ function blockedAdmission(reasonCode) {
272
298
  };
273
299
  }
274
300
 
275
- function hashText(text) {
276
- return typeof text === 'string' ? hashSemanticValue(text) : undefined;
277
- }
301
+ function hashText(text) { return typeof text === 'string' ? hashSemanticValue(text) : undefined; }
278
302
 
279
- function stringOrUndefined(value) {
280
- return typeof value === 'string' ? value : undefined;
281
- }
303
+ function stringOrUndefined(value) { return typeof value === 'string' ? value : undefined; }
282
304
 
283
305
  function safeId(value) {
284
306
  return String(value ?? 'unknown').replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'file';
285
307
  }
286
308
 
287
- function bySourcePath(left, right) {
288
- return String(left.sourcePath ?? '').localeCompare(String(right.sourcePath ?? ''));
289
- }
309
+ function bySourcePath(left, right) { return String(left.sourcePath ?? '').localeCompare(String(right.sourcePath ?? '')); }
290
310
 
291
311
  function uniqueStrings(values) {
292
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.121",
3
+ "version": "0.2.123",
4
4
  "description": "Compiler facade for Frontier Lang source documents and language projection adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",