@shapeshift-labs/frontier-lang-compiler 0.2.104 → 0.2.106

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
@@ -168,13 +168,25 @@ The JS/TS semantic merge smoke corpus lives at
168
168
  `test/smoke/js-ts-semantic-merge-oracles.mjs`. The fixtures are deliberately
169
169
  small and dependency-free. They cover accepted projection/replay cases, exact
170
170
  source preservation, generated/source-map boundaries, safe import/declaration
171
- merges, safe unordered member merges, and rejected unsafe cases such as stale
171
+ merges, safe unordered member merges, composed top-level/member safe merges,
172
+ and rejected unsafe cases such as stale
172
173
  ledger spans, import specifier reordering, computed keys, duplicate exported
173
174
  names, duplicate object members, decorators, overload anchors, and same-anchor
174
175
  edit conflicts. Fixture failures include the fixture id and the actual
175
176
  reason-code or gate values so distributed swarm evidence can point at a stable
176
177
  case instead of an agent transcript.
177
178
 
179
+ Successful `safeMergeJsTsImportsAndDeclarations` and `safeMergeJsTsSource`
180
+ results also include `semanticArtifacts`. These artifacts convert the
181
+ JS/TS ledger-approved head-to-merged source edits into a semantic edit script,
182
+ projection, replay, and already-applied replay. This is intentionally different
183
+ from asking the generic three-way edit classifier to bless every JS/TS case:
184
+ simultaneous import specifier additions are safe only because the JS/TS ledger
185
+ gates proved independent additions, stable anchors, and source replay. The
186
+ artifacts keep `autoMergeClaim: false` and `semanticEquivalenceClaim: false`,
187
+ but give coordinators machine-readable proof that the projected source matches
188
+ the merge output and that applying the same projection again is a no-op.
189
+
178
190
  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.
179
191
 
180
192
  ```js
@@ -1,4 +1,4 @@
1
- export type JsTsSafeMemberMergeRegionKind = 'interface' | 'type' | 'object';
1
+ export type JsTsSafeMemberMergeRegionKind = 'interface' | 'type' | 'class' | 'object';
2
2
  export type JsTsSafeMemberMergeOrder = 'non-semantic' | string;
3
3
  export type JsTsSafeMemberMergeStatus = 'merged' | 'rejected';
4
4
 
@@ -26,6 +26,30 @@ export interface JsTsSafeMemberMergeInput {
26
26
  readonly policy?: JsTsSafeMemberMergePolicy | readonly JsTsSafeMemberMergePolicyRegion[];
27
27
  readonly mergePolicy?: JsTsSafeMemberMergePolicy | readonly JsTsSafeMemberMergePolicyRegion[];
28
28
  readonly unorderedRegions?: readonly JsTsSafeMemberMergePolicyRegion[];
29
+ readonly allowNonPolicySourceChanges?: boolean;
30
+ }
31
+
32
+ export interface JsTsSafeMemberMergeConflict {
33
+ readonly code: string;
34
+ readonly gateId: string;
35
+ readonly message: string;
36
+ readonly side?: 'base' | 'worker' | 'head' | string;
37
+ readonly sourcePath?: string;
38
+ readonly details?: Record<string, unknown>;
39
+ }
40
+
41
+ export interface JsTsSafeMemberMergeGate {
42
+ readonly id: string;
43
+ readonly status: 'passed' | 'blocked' | 'skipped' | string;
44
+ readonly reasonCodes: readonly string[];
45
+ }
46
+
47
+ export interface JsTsSafeMemberMergeAdmission {
48
+ readonly status: 'auto-merge-candidate' | 'blocked' | string;
49
+ readonly action: 'apply' | 'human-review' | string;
50
+ readonly reviewRequired: boolean;
51
+ readonly autoApplyCandidate: boolean;
52
+ readonly reasonCodes: readonly string[];
29
53
  }
30
54
 
31
55
  export interface JsTsSafeMemberMergedRegion {
@@ -42,12 +66,16 @@ export interface JsTsSafeMemberMergeResult {
42
66
  readonly status: JsTsSafeMemberMergeStatus;
43
67
  readonly sourceText?: string;
44
68
  readonly reasonCodes: readonly string[];
69
+ readonly conflicts: readonly JsTsSafeMemberMergeConflict[];
70
+ readonly gates: readonly JsTsSafeMemberMergeGate[];
71
+ readonly admission: JsTsSafeMemberMergeAdmission;
45
72
  readonly mergedRegions: readonly JsTsSafeMemberMergedRegion[];
46
73
  readonly summary: {
47
74
  readonly regions: number;
48
75
  readonly workerAdditions: number;
49
76
  readonly headAdditions: number;
50
77
  readonly appliedAdditions: number;
78
+ readonly conflicts: number;
51
79
  };
52
80
  readonly metadata: {
53
81
  readonly explicitPolicy: boolean;
@@ -1,4 +1,9 @@
1
1
  import type { FrontierSourceLanguage } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import type { SemanticEditProjection, SemanticEditReplay, SemanticEditScript } from './semantic-edit-script.js';
3
+ import type {
4
+ JsTsSafeMemberMergePolicy,
5
+ JsTsSafeMemberMergePolicyRegion
6
+ } from './js-ts-safe-member-merge.js';
2
7
 
3
8
  export type JsTsSafeMergeStatus = 'merged' | 'blocked';
4
9
  export type JsTsSafeMergeGateStatus = 'passed' | 'blocked' | 'skipped';
@@ -65,6 +70,9 @@ export interface JsTsSafeMergeInput {
65
70
  readonly baseSourceLedger?: unknown;
66
71
  readonly workerSourceLedger?: unknown;
67
72
  readonly headSourceLedger?: unknown;
73
+ readonly policy?: JsTsSafeMemberMergePolicy | readonly JsTsSafeMemberMergePolicyRegion[];
74
+ readonly mergePolicy?: JsTsSafeMemberMergePolicy | readonly JsTsSafeMemberMergePolicyRegion[];
75
+ readonly unorderedRegions?: readonly JsTsSafeMemberMergePolicyRegion[];
68
76
  }
69
77
 
70
78
  export interface JsTsSafeMergeConflict {
@@ -98,6 +106,43 @@ export interface JsTsSafeMergeSummary {
98
106
  readonly changedExistingDeclarations: number;
99
107
  readonly conflicts: number;
100
108
  readonly gatesPassed: number;
109
+ readonly memberRegions?: number;
110
+ readonly memberAdditions?: number;
111
+ readonly composedPhases?: number;
112
+ }
113
+
114
+ export interface JsTsSafeMergeSemanticArtifacts {
115
+ readonly kind: 'frontier.lang.jsTsSafeMergeSemanticArtifacts';
116
+ readonly version: 1;
117
+ readonly schema: 'frontier.lang.jsTsSafeMergeSemanticArtifacts.v1';
118
+ readonly id: string;
119
+ readonly hash: string;
120
+ readonly sourcePath?: string;
121
+ readonly language?: FrontierSourceLanguage | string;
122
+ readonly status: 'verified' | 'blocked' | string;
123
+ readonly script?: SemanticEditScript;
124
+ readonly projection?: SemanticEditProjection;
125
+ readonly replay?: SemanticEditReplay;
126
+ readonly alreadyAppliedReplay?: SemanticEditReplay;
127
+ readonly admission: {
128
+ readonly status: 'auto-merge-candidate' | 'blocked' | string;
129
+ readonly action: 'apply' | 'human-review' | string;
130
+ readonly reviewRequired: boolean;
131
+ readonly autoApplyCandidate: boolean;
132
+ readonly autoMergeClaim: false;
133
+ readonly semanticEquivalenceClaim: false;
134
+ readonly reasonCodes: readonly string[];
135
+ };
136
+ readonly summary: {
137
+ readonly operations: number;
138
+ readonly edits: number;
139
+ readonly replayStatus?: string;
140
+ readonly alreadyAppliedReplayStatus?: string;
141
+ readonly projectedSourceMatchesMerged: boolean;
142
+ readonly replayOutputMatchesMerged: boolean;
143
+ };
144
+ readonly evidence?: readonly unknown[];
145
+ readonly metadata?: Record<string, unknown>;
101
146
  }
102
147
 
103
148
  export interface JsTsSafeMergeResult {
@@ -114,7 +159,9 @@ export interface JsTsSafeMergeResult {
114
159
  readonly gates: readonly JsTsSafeMergeGate[];
115
160
  readonly admission: JsTsSafeMergeAdmission;
116
161
  readonly summary: JsTsSafeMergeSummary;
162
+ readonly semanticArtifacts?: JsTsSafeMergeSemanticArtifacts;
117
163
  readonly metadata?: Record<string, unknown>;
118
164
  }
119
165
 
120
166
  export declare function safeMergeJsTsImportsAndDeclarations(input: JsTsSafeMergeInput): JsTsSafeMergeResult;
167
+ export declare function safeMergeJsTsSource(input: JsTsSafeMergeInput): JsTsSafeMergeResult;
@@ -77,6 +77,114 @@ export interface ImportNativeProjectOptions {
77
77
  readonly sources: readonly NativeProjectSourceInput[];
78
78
  }
79
79
 
80
+ export type NativeProjectSymbolGraphRemainingField =
81
+ | 'moduleEdges[].resolvedModulePath'
82
+ | 'moduleEdges[].targetDocumentId'
83
+ | 'moduleEdges[].resolvedTargetSymbolId'
84
+ | 'moduleEdges[].resolutionKind'
85
+ | 'moduleEdges[].packageName'
86
+ | 'moduleEdges[].packageExportCondition'
87
+ | 'reExportIdentities[].originSymbolId'
88
+ | 'reExportIdentities[].exportedSymbolId'
89
+ | 'reExportIdentities[].localSymbolId'
90
+ | 'publicContractRegions[].apiSurfaceKind'
91
+ | 'publicContractRegions[].signatureHash'
92
+ | 'publicContractRegions[].contractHash'
93
+ | string;
94
+
95
+ export interface NativeProjectSymbolGraphFileHashRecord {
96
+ readonly id: string;
97
+ readonly documentId: string;
98
+ readonly sourcePath?: string;
99
+ readonly language?: FrontierSourceLanguage | string;
100
+ readonly sourceHash: string;
101
+ readonly algorithm?: string;
102
+ readonly value: string;
103
+ readonly factId?: string;
104
+ }
105
+
106
+ export interface NativeProjectSymbolGraphModuleEdgeRecord {
107
+ readonly id: string;
108
+ readonly sourceDocumentId: string;
109
+ readonly targetSymbolId: string;
110
+ readonly predicate: 'imports' | 'exports' | string;
111
+ readonly edgeKind?: 'import' | 'export' | 're-export' | string;
112
+ readonly sourcePath?: string;
113
+ readonly sourceHash?: string;
114
+ readonly moduleSpecifier?: string;
115
+ readonly importKind?: string;
116
+ readonly exportKind?: string;
117
+ readonly importedName?: string;
118
+ readonly exportedName?: string;
119
+ readonly localName?: string;
120
+ readonly namespace?: string;
121
+ readonly isTypeOnly?: boolean;
122
+ readonly isReExport?: boolean;
123
+ readonly publicContract?: boolean;
124
+ readonly evidenceIds?: readonly string[];
125
+ }
126
+
127
+ export interface NativeProjectSymbolGraphReExportIdentityRecord {
128
+ readonly kind?: 'frontier.lang.reExportIdentity' | string;
129
+ readonly version?: 1;
130
+ readonly id: string;
131
+ readonly sourceDocumentId?: string;
132
+ readonly sourcePath?: string;
133
+ readonly sourceHash?: string;
134
+ readonly moduleSpecifier?: string;
135
+ readonly exportedName?: string;
136
+ readonly importedName?: string;
137
+ readonly localName?: string;
138
+ readonly namespace?: string;
139
+ readonly isTypeOnly?: boolean;
140
+ readonly symbolId?: string;
141
+ readonly relationId?: string;
142
+ readonly ownershipRegionId?: string;
143
+ readonly ownershipRegionKey?: string;
144
+ readonly publicContract?: boolean;
145
+ readonly factId?: string;
146
+ }
147
+
148
+ export interface NativeProjectSymbolGraphPublicContractRegionRecord {
149
+ readonly id: string;
150
+ readonly key?: string;
151
+ readonly regionKind?: string;
152
+ readonly granularity?: string;
153
+ readonly language?: FrontierSourceLanguage | string;
154
+ readonly documentId?: string;
155
+ readonly sourcePath?: string;
156
+ readonly sourceHash?: string;
157
+ readonly symbolId?: string;
158
+ readonly symbolName?: string;
159
+ readonly symbolKind?: string;
160
+ readonly nativeAstNodeId?: string;
161
+ readonly sourceSpan?: SourceSpan;
162
+ readonly precision?: string;
163
+ readonly publicContract?: boolean;
164
+ readonly exportedName?: string;
165
+ readonly moduleSpecifier?: string;
166
+ readonly edgeKind?: string;
167
+ readonly factId?: string;
168
+ }
169
+
170
+ export interface NativeProjectSymbolGraphSummary {
171
+ readonly kind: 'frontier.lang.projectSymbolGraph';
172
+ readonly version: 1;
173
+ readonly projectRoot?: string;
174
+ readonly sourceCount: number;
175
+ readonly documentCount: number;
176
+ readonly symbolCount: number;
177
+ readonly occurrenceCount: number;
178
+ readonly relationCount: number;
179
+ readonly factCount: number;
180
+ readonly fileHashes: readonly NativeProjectSymbolGraphFileHashRecord[];
181
+ readonly importEdges: readonly NativeProjectSymbolGraphModuleEdgeRecord[];
182
+ readonly exportEdges: readonly NativeProjectSymbolGraphModuleEdgeRecord[];
183
+ readonly reExportIdentities: readonly NativeProjectSymbolGraphReExportIdentityRecord[];
184
+ readonly publicContractRegions: readonly NativeProjectSymbolGraphPublicContractRegionRecord[];
185
+ readonly remainingFields: readonly NativeProjectSymbolGraphRemainingField[];
186
+ }
187
+
80
188
  export interface NativeProjectImportResultMetadata extends Record<string, unknown> {
81
189
  readonly importResultContract?: NativeImportResultContract;
82
190
  readonly projectAdmission?: NativeProjectImportAdmission;
@@ -84,6 +192,7 @@ export interface NativeProjectImportResultMetadata extends Record<string, unknow
84
192
  readonly nativeImportLossSummary?: NativeImportLossSummary;
85
193
  readonly semanticMergeReadiness?: SemanticMergeReadiness;
86
194
  readonly readinessReasons?: readonly string[];
195
+ readonly projectSymbolGraph?: NativeProjectSymbolGraphSummary;
87
196
  }
88
197
 
89
198
  export interface NativeProjectImportResult {
@@ -102,6 +211,7 @@ export interface NativeProjectImportResult {
102
211
  readonly losses: readonly NativeAstLossRecord[];
103
212
  readonly evidence: readonly EvidenceRecord[];
104
213
  readonly mergeCandidates: readonly SemanticMergeCandidateRecord[];
214
+ readonly projectSymbolGraph?: NativeProjectSymbolGraphSummary;
105
215
  readonly metadata?: NativeProjectImportResultMetadata;
106
216
  }
107
217
 
@@ -4,7 +4,15 @@ export function createNativeProjectImportResult(input, imports) {
4
4
  const idPart = idFragment(input.id ?? input.projectRoot ?? 'native_project');
5
5
  const nodes = {};
6
6
  const rootIds = [];
7
- const semanticIndex = mergeSemanticIndexes(imports, input, idPart);
7
+ const mergedSemanticIndex = mergeSemanticIndexes(imports, input, idPart);
8
+ const projectSymbolGraph = createProjectSymbolGraphSummary(mergedSemanticIndex, imports, input);
9
+ const semanticIndex = mergedSemanticIndex ? {
10
+ ...mergedSemanticIndex,
11
+ metadata: {
12
+ ...mergedSemanticIndex.metadata,
13
+ projectSymbolGraph
14
+ }
15
+ } : mergedSemanticIndex;
8
16
  const nativeSources = [];
9
17
  const sourceMaps = [];
10
18
  const losses = [];
@@ -43,6 +51,7 @@ export function createNativeProjectImportResult(input, imports) {
43
51
  sourceCount: imports.length,
44
52
  nativeImportLossSummary,
45
53
  sourcePreservationSummary,
54
+ projectSymbolGraph,
46
55
  ...input.documentMetadata
47
56
  }
48
57
  });
@@ -60,6 +69,7 @@ export function createNativeProjectImportResult(input, imports) {
60
69
  sourceCount: imports.length,
61
70
  nativeImportLossSummary,
62
71
  sourcePreservationSummary,
72
+ projectSymbolGraph,
63
73
  ...input.universalAstMetadata
64
74
  }
65
75
  });
@@ -75,7 +85,8 @@ export function createNativeProjectImportResult(input, imports) {
75
85
  sourceMapIds: sourceMaps.map((sourceMap) => sourceMap.id),
76
86
  sourceCount: imports.length,
77
87
  nativeImportLossSummary,
78
- sourcePreservationSummary
88
+ sourcePreservationSummary,
89
+ projectSymbolGraph
79
90
  }
80
91
  });
81
92
  const projectResult = {
@@ -94,11 +105,13 @@ export function createNativeProjectImportResult(input, imports) {
94
105
  losses: uniqueLosses,
95
106
  evidence: uniqueEvidence,
96
107
  mergeCandidates,
108
+ projectSymbolGraph,
97
109
  metadata: {
98
110
  sourceCount: imports.length,
99
111
  sourcePaths: imports.map((result) => result.sourcePath).filter(Boolean),
100
112
  nativeImportLossSummary,
101
113
  sourcePreservationSummary,
114
+ projectSymbolGraph,
102
115
  ...input.metadata
103
116
  }
104
117
  };
@@ -120,7 +133,168 @@ export function createNativeProjectImportResult(input, imports) {
120
133
  readinessReasons: importResultContract.readiness.reasons,
121
134
  regionSummary: importResultContract.regions,
122
135
  sourceMapSummary: importResultContract.sourceMaps,
123
- adapterCoverageSummary: importResultContract.adapterCoverage
136
+ adapterCoverageSummary: importResultContract.adapterCoverage,
137
+ projectSymbolGraph
124
138
  }
125
139
  };
126
140
  }
141
+
142
+ const PROJECT_SYMBOL_GRAPH_REMAINING_FIELDS = Object.freeze([
143
+ 'moduleEdges[].resolvedModulePath',
144
+ 'moduleEdges[].targetDocumentId',
145
+ 'moduleEdges[].resolvedTargetSymbolId',
146
+ 'moduleEdges[].resolutionKind',
147
+ 'moduleEdges[].packageName',
148
+ 'moduleEdges[].packageExportCondition',
149
+ 'reExportIdentities[].originSymbolId',
150
+ 'reExportIdentities[].exportedSymbolId',
151
+ 'reExportIdentities[].localSymbolId',
152
+ 'publicContractRegions[].apiSurfaceKind',
153
+ 'publicContractRegions[].signatureHash',
154
+ 'publicContractRegions[].contractHash'
155
+ ]);
156
+
157
+ function createProjectSymbolGraphSummary(semanticIndex, imports, input) {
158
+ const documents = semanticIndex?.documents ?? [];
159
+ const symbolsById = new Map((semanticIndex?.symbols ?? []).map((symbol) => [symbol.id, symbol]));
160
+ const documentsById = new Map(documents.map((document) => [document.id, document]));
161
+ const facts = semanticIndex?.facts ?? [];
162
+ const moduleEdgeFacts = facts.filter((fact) => fact.predicate === 'moduleEdge');
163
+ const moduleEdgeByRelation = new Map(moduleEdgeFacts.map((fact) => [fact.subjectId, fact]));
164
+ const relations = semanticIndex?.relations ?? [];
165
+ const importEdges = relations
166
+ .filter((relation) => relation.predicate === 'imports')
167
+ .map((relation) => moduleEdgeRecord(relation, moduleEdgeByRelation, symbolsById, documentsById));
168
+ const exportEdges = relations
169
+ .filter((relation) => relation.predicate === 'exports')
170
+ .map((relation) => moduleEdgeRecord(relation, moduleEdgeByRelation, symbolsById, documentsById));
171
+ const reExportIdentities = uniqueRecords([
172
+ ...facts
173
+ .filter((fact) => fact.predicate === 'reExportIdentity' && fact.value)
174
+ .map((fact) => ({ ...objectValue(fact.value), factId: fact.id })),
175
+ ...exportEdges
176
+ .filter((edge) => edge.isReExport)
177
+ .map((edge) => compactRecord({
178
+ id: `reexport_${idFragment(edge.id)}`,
179
+ sourceDocumentId: edge.sourceDocumentId,
180
+ sourcePath: edge.sourcePath,
181
+ sourceHash: edge.sourceHash,
182
+ moduleSpecifier: edge.moduleSpecifier,
183
+ symbolId: edge.targetSymbolId,
184
+ relationId: edge.id,
185
+ publicContract: edge.publicContract
186
+ }))
187
+ ]);
188
+ const publicContractRegions = uniqueRecords(facts
189
+ .filter((fact) => fact.predicate === 'publicContractRegion' && fact.value)
190
+ .map((fact) => ({ ...objectValue(fact.value), factId: fact.id, symbolId: fact.subjectId })));
191
+ const fileHashes = uniqueRecords([
192
+ ...documents.map((document) => fileHashRecord(document)),
193
+ ...facts
194
+ .filter((fact) => fact.predicate === 'fileHash' && fact.value)
195
+ .map((fact) => ({ ...objectValue(fact.value), id: `file_hash_${idFragment(fact.subjectId)}`, factId: fact.id, documentId: fact.subjectId }))
196
+ ].filter(Boolean));
197
+ return {
198
+ kind: 'frontier.lang.projectSymbolGraph',
199
+ version: 1,
200
+ projectRoot: input.projectRoot,
201
+ sourceCount: imports.length,
202
+ documentCount: documents.length,
203
+ symbolCount: semanticIndex?.symbols?.length ?? 0,
204
+ occurrenceCount: semanticIndex?.occurrences?.length ?? 0,
205
+ relationCount: relations.length,
206
+ factCount: facts.length,
207
+ fileHashes,
208
+ importEdges,
209
+ exportEdges,
210
+ reExportIdentities,
211
+ publicContractRegions,
212
+ remainingFields: PROJECT_SYMBOL_GRAPH_REMAINING_FIELDS
213
+ };
214
+ }
215
+
216
+ function moduleEdgeRecord(relation, moduleEdgeByRelation, symbolsById, documentsById) {
217
+ const fact = moduleEdgeByRelation.get(relation.id);
218
+ const value = objectValue(fact?.value);
219
+ const metadata = objectValue(relation.metadata);
220
+ const moduleEdge = objectValue(metadata.moduleEdge);
221
+ const symbol = symbolsById.get(relation.targetId);
222
+ const symbolMetadata = objectValue(symbol?.metadata);
223
+ const document = documentsById.get(relation.sourceId);
224
+ const moduleSpecifier = firstString(
225
+ moduleEdge.moduleSpecifier,
226
+ value.moduleSpecifier,
227
+ metadata.moduleSpecifier,
228
+ symbolMetadata.moduleSpecifier,
229
+ symbol?.kind === 'module' ? symbol.name : undefined
230
+ );
231
+ return compactRecord({
232
+ id: relation.id,
233
+ sourceDocumentId: relation.sourceId,
234
+ targetSymbolId: relation.targetId,
235
+ predicate: relation.predicate,
236
+ edgeKind: firstString(moduleEdge.edgeKind, value.edgeKind, relation.predicate === 'imports' ? 'import' : moduleSpecifier ? 're-export' : 'export'),
237
+ sourcePath: document?.path,
238
+ sourceHash: document?.sourceHash,
239
+ moduleSpecifier,
240
+ importKind: firstString(moduleEdge.importKind, value.importKind),
241
+ exportKind: firstString(moduleEdge.exportKind, value.exportKind),
242
+ importedName: firstString(moduleEdge.importedName, value.importedName),
243
+ exportedName: firstString(moduleEdge.exportedName, value.exportedName),
244
+ localName: firstString(moduleEdge.localName, value.localName),
245
+ namespace: firstString(moduleEdge.namespace, value.namespace),
246
+ isTypeOnly: firstBoolean(moduleEdge.isTypeOnly, value.isTypeOnly),
247
+ isReExport: firstBoolean(moduleEdge.isReExport, value.isReExport) ?? (relation.predicate === 'exports' && Boolean(moduleSpecifier)),
248
+ publicContract: firstBoolean(moduleEdge.publicContract, value.publicContract, metadata.publicContract),
249
+ evidenceIds: uniqueStrings([...(relation.evidenceIds ?? []), ...(fact?.evidenceIds ?? [])])
250
+ });
251
+ }
252
+
253
+ function fileHashRecord(document) {
254
+ if (!document?.sourceHash) return undefined;
255
+ const sourceHash = String(document.sourceHash);
256
+ const separator = sourceHash.indexOf(':');
257
+ return compactRecord({
258
+ id: `file_hash_${idFragment(document.id)}`,
259
+ documentId: document.id,
260
+ sourcePath: document.path,
261
+ language: document.language,
262
+ sourceHash,
263
+ algorithm: separator > 0 ? sourceHash.slice(0, separator) : undefined,
264
+ value: separator > 0 ? sourceHash.slice(separator + 1) : sourceHash
265
+ });
266
+ }
267
+
268
+ function objectValue(value) {
269
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
270
+ }
271
+
272
+ function compactRecord(record) {
273
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
274
+ }
275
+
276
+ function uniqueRecords(records) {
277
+ const seen = new Set();
278
+ const result = [];
279
+ for (const record of records) {
280
+ const key = record.id ?? record.factId ?? JSON.stringify(record);
281
+ if (seen.has(key)) continue;
282
+ seen.add(key);
283
+ result.push(record);
284
+ }
285
+ return result;
286
+ }
287
+
288
+ function firstString(...values) {
289
+ for (const value of values) {
290
+ if (value !== undefined && value !== null && String(value)) return String(value);
291
+ }
292
+ return undefined;
293
+ }
294
+
295
+ function firstBoolean(...values) {
296
+ for (const value of values) {
297
+ if (typeof value === 'boolean') return value;
298
+ }
299
+ return undefined;
300
+ }
@@ -207,7 +207,7 @@ function currentSymbolRangeLabel(edit) {
207
207
  }
208
208
 
209
209
  function replayStatus(reasonCodes, edits, projection) {
210
- if (reasonCodes.some((reason) => reason !== 'current-source-hash-mismatch')) return 'blocked';
210
+ if (reasonCodes.length) return 'blocked';
211
211
  if (!edits.length && !(projection.edits ?? []).length) return 'evidence-only';
212
212
  if (edits.some((edit) => edit.status === 'blocked')) return 'blocked';
213
213
  if (edits.some((edit) => edit.status === 'conflict')) return 'conflict';