@shapeshift-labs/frontier-lang-compiler 0.2.108 → 0.2.110
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 +21 -0
- package/dist/declarations/js-ts-safe-project-merge.d.ts +128 -0
- package/dist/declarations/js-ts-semantic-merge.d.ts +1 -0
- package/dist/declarations/native-project.d.ts +1 -1
- package/dist/internal/index-impl/createNativeProjectImportResult.js +17 -13
- package/dist/internal/index-impl/projectSymbolGraphModuleResolution.js +27 -0
- package/dist/js-ts-safe-project-merge.js +288 -0
- package/dist/js-ts-semantic-merge.js +3 -0
- package/dist/native-region-scanner-core.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -187,6 +187,27 @@ artifacts keep `autoMergeClaim: false` and `semanticEquivalenceClaim: false`,
|
|
|
187
187
|
but give coordinators machine-readable proof that the projected source matches
|
|
188
188
|
the merge output and that applying the same projection again is a no-op.
|
|
189
189
|
|
|
190
|
+
Project-level JS/TS safe merges compose the same file-level gates across a
|
|
191
|
+
base/worker/head file set. They preserve head-only files, admit worker-only
|
|
192
|
+
file additions when file additions are enabled, block conflicting same-path
|
|
193
|
+
additions, and attach per-file semantic artifacts for files merged through the
|
|
194
|
+
JS/TS source merger:
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
import { safeMergeJsTsProject } from '@shapeshift-labs/frontier-lang-compiler';
|
|
198
|
+
|
|
199
|
+
const project = safeMergeJsTsProject({
|
|
200
|
+
language: 'typescript',
|
|
201
|
+
baseFiles: { 'src/index.ts': 'export const stable = 1;\n' },
|
|
202
|
+
workerFiles: { 'src/index.ts': 'export const stable = 1;\nexport const workerOnly = 1;\n' },
|
|
203
|
+
headFiles: { 'src/index.ts': 'export const stable = 1;\n' }
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
console.log(project.status); // "merged"
|
|
207
|
+
console.log(project.outputFiles[0].sourcePath); // "src/index.ts"
|
|
208
|
+
console.log(project.files[0].semanticArtifacts.status); // "verified"
|
|
209
|
+
```
|
|
210
|
+
|
|
190
211
|
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.
|
|
191
212
|
|
|
192
213
|
```js
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { FrontierSourceLanguage } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import type {
|
|
3
|
+
JsTsSafeMergeAdmission,
|
|
4
|
+
JsTsSafeMergeConflict,
|
|
5
|
+
JsTsSafeMergeResult,
|
|
6
|
+
JsTsSafeMergeSemanticArtifacts,
|
|
7
|
+
JsTsSafeMergeSummary
|
|
8
|
+
} from './js-ts-safe-merge.js';
|
|
9
|
+
import type { JsTsSafeMemberMergePolicy, JsTsSafeMemberMergePolicyRegion } from './js-ts-safe-member-merge.js';
|
|
10
|
+
|
|
11
|
+
export type JsTsProjectSafeMergeStatus = 'merged' | 'blocked';
|
|
12
|
+
export type JsTsProjectSafeMergeFileStatus = 'merged' | 'blocked';
|
|
13
|
+
export type JsTsProjectSafeMergeFileOperation =
|
|
14
|
+
| 'merged-source'
|
|
15
|
+
| 'merged-source-and-members'
|
|
16
|
+
| 'worker-added'
|
|
17
|
+
| 'head-only'
|
|
18
|
+
| 'both-added-identical'
|
|
19
|
+
| 'worker-deleted'
|
|
20
|
+
| 'head-deleted-worker-unchanged'
|
|
21
|
+
| 'blocked-merge'
|
|
22
|
+
| 'blocked-file-presence'
|
|
23
|
+
| string;
|
|
24
|
+
|
|
25
|
+
export interface JsTsProjectSafeMergeFileInput {
|
|
26
|
+
readonly sourcePath?: string;
|
|
27
|
+
readonly path?: string;
|
|
28
|
+
readonly language?: FrontierSourceLanguage | string;
|
|
29
|
+
readonly baseSourceText?: string;
|
|
30
|
+
readonly baseText?: string;
|
|
31
|
+
readonly workerSourceText?: string;
|
|
32
|
+
readonly workerText?: string;
|
|
33
|
+
readonly headSourceText?: string;
|
|
34
|
+
readonly headText?: string;
|
|
35
|
+
readonly workerDeleted?: boolean;
|
|
36
|
+
readonly headDeleted?: boolean;
|
|
37
|
+
readonly policy?: JsTsSafeMemberMergePolicy | readonly JsTsSafeMemberMergePolicyRegion[];
|
|
38
|
+
readonly mergePolicy?: JsTsSafeMemberMergePolicy | readonly JsTsSafeMemberMergePolicyRegion[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type JsTsProjectSafeMergeFileMap =
|
|
42
|
+
| Readonly<Record<string, string>>
|
|
43
|
+
| ReadonlyMap<string, string>
|
|
44
|
+
| readonly { readonly sourcePath?: string; readonly path?: string; readonly sourceText?: string; readonly text?: string }[];
|
|
45
|
+
|
|
46
|
+
export interface JsTsProjectSafeMergeInput {
|
|
47
|
+
readonly id?: string;
|
|
48
|
+
readonly language?: FrontierSourceLanguage | string;
|
|
49
|
+
readonly projectRoot?: string;
|
|
50
|
+
readonly files?: readonly JsTsProjectSafeMergeFileInput[];
|
|
51
|
+
readonly baseFiles?: JsTsProjectSafeMergeFileMap;
|
|
52
|
+
readonly workerFiles?: JsTsProjectSafeMergeFileMap;
|
|
53
|
+
readonly headFiles?: JsTsProjectSafeMergeFileMap;
|
|
54
|
+
readonly allowFileAdditions?: boolean;
|
|
55
|
+
readonly allowFileDeletes?: boolean;
|
|
56
|
+
readonly workerChangeSetId?: string;
|
|
57
|
+
readonly headChangeSetId?: string;
|
|
58
|
+
readonly policy?: JsTsSafeMemberMergePolicy | readonly JsTsSafeMemberMergePolicyRegion[];
|
|
59
|
+
readonly mergePolicy?: JsTsSafeMemberMergePolicy | readonly JsTsSafeMemberMergePolicyRegion[];
|
|
60
|
+
readonly policyByPath?: Readonly<Record<string, JsTsSafeMemberMergePolicy | readonly JsTsSafeMemberMergePolicyRegion[]>>;
|
|
61
|
+
readonly mergePolicyByPath?: Readonly<Record<string, JsTsSafeMemberMergePolicy | readonly JsTsSafeMemberMergePolicyRegion[]>>;
|
|
62
|
+
readonly requireSourceLedgerSpans?: boolean;
|
|
63
|
+
readonly sourceLedgers?: Record<string, unknown>;
|
|
64
|
+
readonly sourceLedgersByPath?: Record<string, Record<string, unknown>>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface JsTsProjectSafeMergeOutputFile {
|
|
68
|
+
readonly sourcePath: string;
|
|
69
|
+
readonly language?: FrontierSourceLanguage | string;
|
|
70
|
+
readonly sourceText: string;
|
|
71
|
+
readonly sourceHash?: string;
|
|
72
|
+
readonly operation: JsTsProjectSafeMergeFileOperation;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface JsTsProjectSafeMergeFileResult {
|
|
76
|
+
readonly kind: 'frontier.lang.jsTsProjectSafeMergeFile';
|
|
77
|
+
readonly version: 1;
|
|
78
|
+
readonly sourcePath?: string;
|
|
79
|
+
readonly language?: FrontierSourceLanguage | string;
|
|
80
|
+
readonly status: JsTsProjectSafeMergeFileStatus;
|
|
81
|
+
readonly operation: JsTsProjectSafeMergeFileOperation;
|
|
82
|
+
readonly outputSourceText?: string;
|
|
83
|
+
readonly outputHash?: string;
|
|
84
|
+
readonly baseHash?: string;
|
|
85
|
+
readonly workerHash?: string;
|
|
86
|
+
readonly headHash?: string;
|
|
87
|
+
readonly result?: JsTsSafeMergeResult;
|
|
88
|
+
readonly semanticArtifacts?: JsTsSafeMergeSemanticArtifacts;
|
|
89
|
+
readonly conflicts: readonly JsTsSafeMergeConflict[];
|
|
90
|
+
readonly admission: JsTsSafeMergeAdmission;
|
|
91
|
+
readonly summary?: JsTsSafeMergeSummary | Record<string, unknown>;
|
|
92
|
+
readonly conflictKeys: readonly string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface JsTsProjectSafeMergeAdmission {
|
|
96
|
+
readonly status: 'auto-merge-candidate' | 'blocked' | string;
|
|
97
|
+
readonly action: 'apply-project' | 'human-review' | string;
|
|
98
|
+
readonly reviewRequired: boolean;
|
|
99
|
+
readonly autoApplyCandidate: boolean;
|
|
100
|
+
readonly autoMergeClaim: false;
|
|
101
|
+
readonly semanticEquivalenceClaim: false;
|
|
102
|
+
readonly reasonCodes: readonly string[];
|
|
103
|
+
readonly conflictKeys: readonly string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface JsTsProjectSafeMergeResult {
|
|
107
|
+
readonly kind: 'frontier.lang.jsTsProjectSafeMerge';
|
|
108
|
+
readonly version: 1;
|
|
109
|
+
readonly schema: 'frontier.lang.jsTsProjectSafeMerge.v1';
|
|
110
|
+
readonly id: string;
|
|
111
|
+
readonly hash: string;
|
|
112
|
+
readonly status: JsTsProjectSafeMergeStatus;
|
|
113
|
+
readonly files: readonly JsTsProjectSafeMergeFileResult[];
|
|
114
|
+
readonly outputFiles: readonly JsTsProjectSafeMergeOutputFile[];
|
|
115
|
+
readonly conflicts: readonly JsTsSafeMergeConflict[];
|
|
116
|
+
readonly admission: JsTsProjectSafeMergeAdmission;
|
|
117
|
+
readonly summary: {
|
|
118
|
+
readonly files: number;
|
|
119
|
+
readonly mergedFiles: number;
|
|
120
|
+
readonly blockedFiles: number;
|
|
121
|
+
readonly outputFiles: number;
|
|
122
|
+
readonly semanticArtifactFiles: number;
|
|
123
|
+
readonly operations: Readonly<Record<string, number>>;
|
|
124
|
+
};
|
|
125
|
+
readonly metadata?: Record<string, unknown>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export declare function safeMergeJsTsProject(input?: JsTsProjectSafeMergeInput): JsTsProjectSafeMergeResult;
|
|
@@ -78,7 +78,6 @@ export interface ImportNativeProjectOptions {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export type NativeProjectSymbolGraphRemainingField =
|
|
81
|
-
| 'moduleEdges[].resolvedTargetSymbolId'
|
|
82
81
|
| 'moduleEdges[].resolutionKind'
|
|
83
82
|
| 'moduleEdges[].packageName'
|
|
84
83
|
| 'moduleEdges[].packageExportCondition'
|
|
@@ -112,6 +111,7 @@ export interface NativeProjectSymbolGraphModuleEdgeRecord {
|
|
|
112
111
|
readonly moduleSpecifier?: string;
|
|
113
112
|
readonly resolvedModulePath?: string;
|
|
114
113
|
readonly targetDocumentId?: string;
|
|
114
|
+
readonly resolvedTargetSymbolId?: string;
|
|
115
115
|
readonly resolutionKind?: 'relative-source' | 'relative-missing' | string;
|
|
116
116
|
readonly importKind?: string;
|
|
117
117
|
readonly exportKind?: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import{idFragment,uniqueByEvidenceId,uniqueByLossId,uniqueStrings}from'../../native-import-utils.js';import{createDocument,createPatch,createUniversalAstEnvelope}from'@shapeshift-labs/frontier-lang-kernel';
|
|
2
2
|
import{createNativeImportResultContract}from'./createNativeImportResultContract.js';import{createProjectImportAdmissionRecord}from'./createProjectImportAdmissionRecord.js';import{mergeSemanticIndexes}from'./mergeSemanticIndexes.js';import{summarizeNativeImportLosses}from'./summarizeNativeImportLosses.js';import{summarizeProjectSourcePreservation}from'./summarizeProjectSourcePreservation.js';
|
|
3
|
-
import{resolveRelativeProjectModule}from'./projectSymbolGraphModuleResolution.js';
|
|
3
|
+
import{createProjectModuleSymbolResolver,resolveRelativeProjectModule}from'./projectSymbolGraphModuleResolution.js';
|
|
4
4
|
export function createNativeProjectImportResult(input, imports) {
|
|
5
5
|
const idPart = idFragment(input.id ?? input.projectRoot ?? 'native_project');
|
|
6
6
|
const nodes = {};
|
|
@@ -141,7 +141,6 @@ export function createNativeProjectImportResult(input, imports) {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
const PROJECT_SYMBOL_GRAPH_REMAINING_FIELDS = Object.freeze([
|
|
144
|
-
'moduleEdges[].resolvedTargetSymbolId',
|
|
145
144
|
'moduleEdges[].resolutionKind',
|
|
146
145
|
'moduleEdges[].packageName',
|
|
147
146
|
'moduleEdges[].packageExportCondition',
|
|
@@ -158,16 +157,17 @@ function createProjectSymbolGraphSummary(semanticIndex, imports, input) {
|
|
|
158
157
|
const symbolsById = new Map((semanticIndex?.symbols ?? []).map((symbol) => [symbol.id, symbol]));
|
|
159
158
|
const documentsById = new Map(documents.map((document) => [document.id, document]));
|
|
160
159
|
const documentsByPath = new Map(documents.filter((document) => document.path).map((document) => [document.path, document]));
|
|
160
|
+
const resolveTargetSymbolId = createProjectModuleSymbolResolver(semanticIndex?.symbols ?? [], documents);
|
|
161
161
|
const facts = semanticIndex?.facts ?? [];
|
|
162
162
|
const moduleEdgeFacts = facts.filter((fact) => fact.predicate === 'moduleEdge');
|
|
163
163
|
const moduleEdgeByRelation = new Map(moduleEdgeFacts.map((fact) => [fact.subjectId, fact]));
|
|
164
164
|
const relations = semanticIndex?.relations ?? [];
|
|
165
165
|
const importEdges = relations
|
|
166
166
|
.filter((relation) => relation.predicate === 'imports')
|
|
167
|
-
.map((relation) => moduleEdgeRecord(relation, moduleEdgeByRelation, symbolsById, documentsById, documentsByPath));
|
|
167
|
+
.map((relation) => moduleEdgeRecord(relation, moduleEdgeByRelation, symbolsById, documentsById, documentsByPath, resolveTargetSymbolId));
|
|
168
168
|
const exportEdges = relations
|
|
169
169
|
.filter((relation) => relation.predicate === 'exports')
|
|
170
|
-
.map((relation) => moduleEdgeRecord(relation, moduleEdgeByRelation, symbolsById, documentsById, documentsByPath));
|
|
170
|
+
.map((relation) => moduleEdgeRecord(relation, moduleEdgeByRelation, symbolsById, documentsById, documentsByPath, resolveTargetSymbolId));
|
|
171
171
|
const reExportIdentities = uniqueRecords([
|
|
172
172
|
...facts
|
|
173
173
|
.filter((fact) => fact.predicate === 'reExportIdentity' && fact.value)
|
|
@@ -213,7 +213,7 @@ function createProjectSymbolGraphSummary(semanticIndex, imports, input) {
|
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
function moduleEdgeRecord(relation, moduleEdgeByRelation, symbolsById, documentsById, documentsByPath) {
|
|
216
|
+
function moduleEdgeRecord(relation, moduleEdgeByRelation, symbolsById, documentsById, documentsByPath, resolveTargetSymbolId) {
|
|
217
217
|
const fact = moduleEdgeByRelation.get(relation.id);
|
|
218
218
|
const value = objectValue(fact?.value);
|
|
219
219
|
const metadata = objectValue(relation.metadata);
|
|
@@ -226,10 +226,13 @@ function moduleEdgeRecord(relation, moduleEdgeByRelation, symbolsById, documents
|
|
|
226
226
|
value.moduleSpecifier,
|
|
227
227
|
metadata.moduleSpecifier,
|
|
228
228
|
symbolMetadata.moduleSpecifier,
|
|
229
|
+
symbolMetadata.importPath,
|
|
230
|
+
symbolMetadata.exportPath,
|
|
231
|
+
symbolMetadata.source,
|
|
229
232
|
symbol?.kind === 'module' ? symbol.name : undefined
|
|
230
233
|
);
|
|
231
234
|
const resolution = resolveRelativeProjectModule(document?.path, moduleSpecifier, documentsByPath);
|
|
232
|
-
|
|
235
|
+
const record = {
|
|
233
236
|
id: relation.id,
|
|
234
237
|
sourceDocumentId: relation.sourceId,
|
|
235
238
|
targetSymbolId: relation.targetId,
|
|
@@ -241,17 +244,18 @@ function moduleEdgeRecord(relation, moduleEdgeByRelation, symbolsById, documents
|
|
|
241
244
|
resolvedModulePath: resolution?.path,
|
|
242
245
|
targetDocumentId: resolution?.documentId,
|
|
243
246
|
resolutionKind: resolution?.kind,
|
|
244
|
-
importKind: firstString(moduleEdge.importKind, value.importKind),
|
|
245
|
-
exportKind: firstString(moduleEdge.exportKind, value.exportKind),
|
|
246
|
-
importedName: firstString(moduleEdge.importedName, value.importedName),
|
|
247
|
-
exportedName: firstString(moduleEdge.exportedName, value.exportedName),
|
|
248
|
-
localName: firstString(moduleEdge.localName, value.localName),
|
|
249
|
-
namespace: firstString(moduleEdge.namespace, value.namespace),
|
|
247
|
+
importKind: firstString(moduleEdge.importKind, value.importKind, symbolMetadata.importKind),
|
|
248
|
+
exportKind: firstString(moduleEdge.exportKind, value.exportKind, symbolMetadata.exportKind),
|
|
249
|
+
importedName: firstString(moduleEdge.importedName, value.importedName, symbolMetadata.importedName),
|
|
250
|
+
exportedName: firstString(moduleEdge.exportedName, value.exportedName, symbolMetadata.exportedName),
|
|
251
|
+
localName: firstString(moduleEdge.localName, value.localName, symbolMetadata.localName),
|
|
252
|
+
namespace: firstString(moduleEdge.namespace, value.namespace, symbolMetadata.namespace),
|
|
250
253
|
isTypeOnly: firstBoolean(moduleEdge.isTypeOnly, value.isTypeOnly),
|
|
251
254
|
isReExport: firstBoolean(moduleEdge.isReExport, value.isReExport) ?? (relation.predicate === 'exports' && Boolean(moduleSpecifier)),
|
|
252
255
|
publicContract: firstBoolean(moduleEdge.publicContract, value.publicContract, metadata.publicContract),
|
|
253
256
|
evidenceIds: uniqueStrings([...(relation.evidenceIds ?? []), ...(fact?.evidenceIds ?? [])])
|
|
254
|
-
}
|
|
257
|
+
};
|
|
258
|
+
return compactRecord({ ...record, resolvedTargetSymbolId: resolveTargetSymbolId(record) });
|
|
255
259
|
}
|
|
256
260
|
|
|
257
261
|
function fileHashRecord(document) {
|
|
@@ -10,6 +10,23 @@ export function resolveRelativeProjectModule(sourcePath, moduleSpecifier, docume
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export function createProjectModuleSymbolResolver(symbols, documents) {
|
|
14
|
+
const documentsByPath = new Map(documents.filter((document) => document.path).map((document) => [document.path, document]));
|
|
15
|
+
const exportedByDocumentAndName = new Map();
|
|
16
|
+
for (const symbol of symbols ?? []) {
|
|
17
|
+
if (symbol?.kind !== 'export' || !symbol.name) continue;
|
|
18
|
+
const document = documentsByPath.get(symbol.definitionSpan?.path);
|
|
19
|
+
if (!document) continue;
|
|
20
|
+
exportedByDocumentAndName.set(symbolKey(document.id, symbol.name), symbol);
|
|
21
|
+
}
|
|
22
|
+
return function resolveProjectModuleSymbol(edge) {
|
|
23
|
+
if (!edge?.targetDocumentId) return undefined;
|
|
24
|
+
const targetName = targetExportName(edge);
|
|
25
|
+
if (!targetName) return undefined;
|
|
26
|
+
return exportedByDocumentAndName.get(symbolKey(edge.targetDocumentId, targetName))?.id;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
function moduleTargetDocument(path, documentsByPath) {
|
|
14
31
|
for (const candidate of modulePathCandidates(path)) {
|
|
15
32
|
const document = documentsByPath.get(candidate);
|
|
@@ -22,6 +39,16 @@ function modulePathCandidates(path) {
|
|
|
22
39
|
return [path, `${path}.js`, `${path}.ts`, `${path}.tsx`, `${path}.jsx`, `${path}/index.js`, `${path}/index.ts`];
|
|
23
40
|
}
|
|
24
41
|
|
|
42
|
+
function targetExportName(edge) {
|
|
43
|
+
const name = edge.importedName ?? edge.localName ?? edge.exportedName;
|
|
44
|
+
if (!name || name === '*') return undefined;
|
|
45
|
+
return String(name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function symbolKey(documentId, name) {
|
|
49
|
+
return `${documentId}\u0000${name}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
function normalizeProjectPath(path) {
|
|
26
53
|
const parts = [];
|
|
27
54
|
for (const part of String(path).split('/')) {
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { safeMergeJsTsSource } from './js-ts-safe-merge-composed.js';
|
|
3
|
+
import { compactRecord } from './js-ts-safe-merge-context.js';
|
|
4
|
+
|
|
5
|
+
function safeMergeJsTsProject(input = {}) {
|
|
6
|
+
const id = String(input.id ?? 'js_ts_project_safe_merge');
|
|
7
|
+
const files = normalizeProjectFiles(input);
|
|
8
|
+
const fileResults = files.map((file) => mergeProjectFile(file, input, id));
|
|
9
|
+
const blockedFiles = fileResults.filter((file) => file.status === 'blocked');
|
|
10
|
+
const status = blockedFiles.length ? 'blocked' : 'merged';
|
|
11
|
+
const outputFiles = fileResults
|
|
12
|
+
.filter((file) => typeof file.outputSourceText === 'string')
|
|
13
|
+
.map((file) => compactRecord({
|
|
14
|
+
sourcePath: file.sourcePath,
|
|
15
|
+
language: file.language,
|
|
16
|
+
sourceText: file.outputSourceText,
|
|
17
|
+
sourceHash: file.outputHash,
|
|
18
|
+
operation: file.operation
|
|
19
|
+
}));
|
|
20
|
+
const reasonCodes = uniqueStrings(blockedFiles.flatMap((file) => file.admission.reasonCodes));
|
|
21
|
+
const core = {
|
|
22
|
+
kind: 'frontier.lang.jsTsProjectSafeMerge',
|
|
23
|
+
version: 1,
|
|
24
|
+
schema: 'frontier.lang.jsTsProjectSafeMerge.v1',
|
|
25
|
+
id,
|
|
26
|
+
status,
|
|
27
|
+
files: fileResults,
|
|
28
|
+
outputFiles,
|
|
29
|
+
conflicts: fileResults.flatMap((file) => file.conflicts),
|
|
30
|
+
admission: {
|
|
31
|
+
status: status === 'merged' ? 'auto-merge-candidate' : 'blocked',
|
|
32
|
+
action: status === 'merged' ? 'apply-project' : 'human-review',
|
|
33
|
+
reviewRequired: status !== 'merged',
|
|
34
|
+
autoApplyCandidate: status === 'merged',
|
|
35
|
+
autoMergeClaim: false,
|
|
36
|
+
semanticEquivalenceClaim: false,
|
|
37
|
+
reasonCodes,
|
|
38
|
+
conflictKeys: uniqueStrings(fileResults.flatMap((file) => file.conflictKeys))
|
|
39
|
+
},
|
|
40
|
+
summary: projectSummary(fileResults),
|
|
41
|
+
metadata: compactRecord({
|
|
42
|
+
workerChangeSetId: input.workerChangeSetId,
|
|
43
|
+
headChangeSetId: input.headChangeSetId,
|
|
44
|
+
projectRoot: input.projectRoot,
|
|
45
|
+
filesInput: Array.isArray(input.files) ? 'records' : 'maps',
|
|
46
|
+
autoMergeClaim: false,
|
|
47
|
+
semanticEquivalenceClaim: false
|
|
48
|
+
})
|
|
49
|
+
};
|
|
50
|
+
return { ...core, hash: hashSemanticValue(core) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mergeProjectFile(file, input, projectId) {
|
|
54
|
+
const base = file.baseSourceText;
|
|
55
|
+
const worker = file.workerDeleted ? undefined : file.workerSourceText ?? base;
|
|
56
|
+
const head = file.headDeleted ? undefined : file.headSourceText ?? base;
|
|
57
|
+
const context = { sourcePath: file.sourcePath, language: file.language ?? input.language ?? 'typescript' };
|
|
58
|
+
if (!file.sourcePath) return blockedFile(file, context, 'missing-source-path');
|
|
59
|
+
if (base === undefined && worker === undefined && head !== undefined) {
|
|
60
|
+
return syntheticFile(file, context, head, 'head-only');
|
|
61
|
+
}
|
|
62
|
+
if (base === undefined && worker !== undefined && head === undefined) {
|
|
63
|
+
return input.allowFileAdditions === false
|
|
64
|
+
? blockedFile(file, context, 'worker-file-addition-disabled')
|
|
65
|
+
: syntheticFile(file, context, worker, 'worker-added');
|
|
66
|
+
}
|
|
67
|
+
if (base === undefined && worker !== undefined && head !== undefined) {
|
|
68
|
+
return worker === head
|
|
69
|
+
? syntheticFile(file, context, worker, 'both-added-identical')
|
|
70
|
+
: blockedFile(file, context, 'file-add-conflict');
|
|
71
|
+
}
|
|
72
|
+
if (base !== undefined && worker === undefined) {
|
|
73
|
+
return input.allowFileDeletes
|
|
74
|
+
? syntheticFile(file, context, undefined, 'worker-deleted')
|
|
75
|
+
: blockedFile(file, context, 'worker-file-delete-blocked');
|
|
76
|
+
}
|
|
77
|
+
if (base !== undefined && head === undefined) {
|
|
78
|
+
return worker === base
|
|
79
|
+
? syntheticFile(file, context, undefined, 'head-deleted-worker-unchanged')
|
|
80
|
+
: blockedFile(file, context, 'head-file-delete-conflict');
|
|
81
|
+
}
|
|
82
|
+
const result = safeMergeJsTsSource({
|
|
83
|
+
...input,
|
|
84
|
+
...context,
|
|
85
|
+
id: `${projectId}_${safeId(file.sourcePath)}`,
|
|
86
|
+
baseSourceText: base,
|
|
87
|
+
workerSourceText: worker,
|
|
88
|
+
headSourceText: head,
|
|
89
|
+
sourceLedgers: sourceLedgersForFile(input, file.sourcePath),
|
|
90
|
+
policy: file.policy ?? file.mergePolicy ?? policyForFile(input, file.sourcePath)
|
|
91
|
+
});
|
|
92
|
+
if (result.status !== 'merged') return mergeBlockedFile(file, context, result);
|
|
93
|
+
return compactRecord({
|
|
94
|
+
kind: 'frontier.lang.jsTsProjectSafeMergeFile',
|
|
95
|
+
version: 1,
|
|
96
|
+
sourcePath: file.sourcePath,
|
|
97
|
+
language: context.language,
|
|
98
|
+
status: 'merged',
|
|
99
|
+
operation: result.summary.memberAdditions ? 'merged-source-and-members' : 'merged-source',
|
|
100
|
+
outputSourceText: result.mergedSourceText,
|
|
101
|
+
outputHash: hashText(result.mergedSourceText),
|
|
102
|
+
baseHash: hashText(base),
|
|
103
|
+
workerHash: hashText(worker),
|
|
104
|
+
headHash: hashText(head),
|
|
105
|
+
result,
|
|
106
|
+
semanticArtifacts: result.semanticArtifacts,
|
|
107
|
+
conflicts: [],
|
|
108
|
+
admission: result.admission,
|
|
109
|
+
summary: result.summary,
|
|
110
|
+
conflictKeys: [`source#${file.sourcePath}`]
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function mergeBlockedFile(file, context, result) {
|
|
115
|
+
return compactRecord({
|
|
116
|
+
kind: 'frontier.lang.jsTsProjectSafeMergeFile',
|
|
117
|
+
version: 1,
|
|
118
|
+
sourcePath: file.sourcePath,
|
|
119
|
+
language: context.language,
|
|
120
|
+
status: 'blocked',
|
|
121
|
+
operation: 'blocked-merge',
|
|
122
|
+
result,
|
|
123
|
+
conflicts: result.conflicts ?? [],
|
|
124
|
+
admission: result.admission,
|
|
125
|
+
summary: result.summary,
|
|
126
|
+
conflictKeys: [`source#${file.sourcePath}`]
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function syntheticFile(file, context, sourceText, operation) {
|
|
131
|
+
return compactRecord({
|
|
132
|
+
kind: 'frontier.lang.jsTsProjectSafeMergeFile',
|
|
133
|
+
version: 1,
|
|
134
|
+
sourcePath: file.sourcePath,
|
|
135
|
+
language: context.language,
|
|
136
|
+
status: 'merged',
|
|
137
|
+
operation,
|
|
138
|
+
outputSourceText: sourceText,
|
|
139
|
+
outputHash: hashText(sourceText),
|
|
140
|
+
baseHash: hashText(file.baseSourceText),
|
|
141
|
+
workerHash: hashText(file.workerSourceText),
|
|
142
|
+
headHash: hashText(file.headSourceText),
|
|
143
|
+
conflicts: [],
|
|
144
|
+
admission: admittedSyntheticAdmission(operation),
|
|
145
|
+
summary: { conflicts: 0, synthetic: true },
|
|
146
|
+
conflictKeys: [`source#${file.sourcePath}`]
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function blockedFile(file, context, reasonCode) {
|
|
151
|
+
const conflict = {
|
|
152
|
+
code: reasonCode,
|
|
153
|
+
gateId: 'project-file-presence',
|
|
154
|
+
message: `Project file cannot be safely merged: ${reasonCode}.`,
|
|
155
|
+
sourcePath: file.sourcePath,
|
|
156
|
+
details: { sourcePath: file.sourcePath }
|
|
157
|
+
};
|
|
158
|
+
return compactRecord({
|
|
159
|
+
kind: 'frontier.lang.jsTsProjectSafeMergeFile',
|
|
160
|
+
version: 1,
|
|
161
|
+
sourcePath: file.sourcePath,
|
|
162
|
+
language: context.language,
|
|
163
|
+
status: 'blocked',
|
|
164
|
+
operation: 'blocked-file-presence',
|
|
165
|
+
conflicts: [conflict],
|
|
166
|
+
admission: blockedAdmission(reasonCode),
|
|
167
|
+
summary: { conflicts: 1, synthetic: true },
|
|
168
|
+
conflictKeys: [`source#${file.sourcePath ?? 'unknown'}`]
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeProjectFiles(input) {
|
|
173
|
+
if (Array.isArray(input.files)) return input.files.map(normalizeFileRecord).sort(bySourcePath);
|
|
174
|
+
const base = normalizeFileMap(input.baseFiles);
|
|
175
|
+
const worker = normalizeFileMap(input.workerFiles);
|
|
176
|
+
const head = normalizeFileMap(input.headFiles);
|
|
177
|
+
const paths = [...new Set([...base.keys(), ...worker.keys(), ...head.keys()])].sort();
|
|
178
|
+
return paths.map((sourcePath) => normalizeFileRecord({
|
|
179
|
+
sourcePath,
|
|
180
|
+
baseSourceText: base.get(sourcePath),
|
|
181
|
+
workerSourceText: worker.get(sourcePath),
|
|
182
|
+
headSourceText: head.get(sourcePath)
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalizeFileRecord(record = {}) {
|
|
187
|
+
return {
|
|
188
|
+
sourcePath: record.sourcePath ?? record.path,
|
|
189
|
+
language: record.language,
|
|
190
|
+
baseSourceText: stringOrUndefined(record.baseSourceText ?? record.baseText),
|
|
191
|
+
workerSourceText: stringOrUndefined(record.workerSourceText ?? record.workerText),
|
|
192
|
+
headSourceText: stringOrUndefined(record.headSourceText ?? record.headText),
|
|
193
|
+
workerDeleted: record.workerDeleted === true,
|
|
194
|
+
headDeleted: record.headDeleted === true,
|
|
195
|
+
policy: record.policy,
|
|
196
|
+
mergePolicy: record.mergePolicy
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeFileMap(value) {
|
|
201
|
+
const map = new Map();
|
|
202
|
+
if (!value) return map;
|
|
203
|
+
if (value instanceof Map) {
|
|
204
|
+
for (const [sourcePath, sourceText] of value) map.set(String(sourcePath), String(sourceText));
|
|
205
|
+
return map;
|
|
206
|
+
}
|
|
207
|
+
if (Array.isArray(value)) {
|
|
208
|
+
for (const entry of value) {
|
|
209
|
+
if (!entry?.sourcePath && !entry?.path) continue;
|
|
210
|
+
map.set(String(entry.sourcePath ?? entry.path), String(entry.sourceText ?? entry.text ?? ''));
|
|
211
|
+
}
|
|
212
|
+
return map;
|
|
213
|
+
}
|
|
214
|
+
for (const [sourcePath, sourceText] of Object.entries(value)) map.set(sourcePath, String(sourceText));
|
|
215
|
+
return map;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function policyForFile(input, sourcePath) {
|
|
219
|
+
if (input.policyByPath?.[sourcePath]) return input.policyByPath[sourcePath];
|
|
220
|
+
if (input.mergePolicyByPath?.[sourcePath]) return input.mergePolicyByPath[sourcePath];
|
|
221
|
+
return input.policy ?? input.mergePolicy;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function sourceLedgersForFile(input, sourcePath) {
|
|
225
|
+
const byPath = input.sourceLedgersByPath?.[sourcePath] ?? input.sourceLedgers?.[sourcePath];
|
|
226
|
+
if (byPath) return byPath;
|
|
227
|
+
if (input.sourceLedgers?.base || input.sourceLedgers?.worker || input.sourceLedgers?.head) return input.sourceLedgers;
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function projectSummary(files) {
|
|
232
|
+
const byOperation = {};
|
|
233
|
+
for (const file of files) byOperation[file.operation] = (byOperation[file.operation] ?? 0) + 1;
|
|
234
|
+
return {
|
|
235
|
+
files: files.length,
|
|
236
|
+
mergedFiles: files.filter((file) => file.status === 'merged').length,
|
|
237
|
+
blockedFiles: files.filter((file) => file.status === 'blocked').length,
|
|
238
|
+
outputFiles: files.filter((file) => typeof file.outputSourceText === 'string').length,
|
|
239
|
+
semanticArtifactFiles: files.filter((file) => file.semanticArtifacts).length,
|
|
240
|
+
operations: byOperation
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function admittedSyntheticAdmission(operation) {
|
|
245
|
+
return {
|
|
246
|
+
status: 'auto-merge-candidate',
|
|
247
|
+
action: operation === 'head-only' ? 'preserve-head' : 'apply',
|
|
248
|
+
reviewRequired: false,
|
|
249
|
+
autoApplyCandidate: true,
|
|
250
|
+
autoMergeClaim: false,
|
|
251
|
+
semanticEquivalenceClaim: false,
|
|
252
|
+
reasonCodes: []
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function blockedAdmission(reasonCode) {
|
|
257
|
+
return {
|
|
258
|
+
status: 'blocked',
|
|
259
|
+
action: 'human-review',
|
|
260
|
+
reviewRequired: true,
|
|
261
|
+
autoApplyCandidate: false,
|
|
262
|
+
autoMergeClaim: false,
|
|
263
|
+
semanticEquivalenceClaim: false,
|
|
264
|
+
reasonCodes: [reasonCode]
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function hashText(text) {
|
|
269
|
+
return typeof text === 'string' ? hashSemanticValue(text) : undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function stringOrUndefined(value) {
|
|
273
|
+
return typeof value === 'string' ? value : undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function safeId(value) {
|
|
277
|
+
return String(value ?? 'unknown').replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'file';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function bySourcePath(left, right) {
|
|
281
|
+
return String(left.sourcePath ?? '').localeCompare(String(right.sourcePath ?? ''));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function uniqueStrings(values) {
|
|
285
|
+
return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export { safeMergeJsTsProject };
|
|
@@ -71,6 +71,8 @@ function nativeImportBindingDeclaration(input, lineNumber, importPath, binding,
|
|
|
71
71
|
},
|
|
72
72
|
metadata: {
|
|
73
73
|
scan: 'lightweight-import-binding',
|
|
74
|
+
importPath: String(importPath),
|
|
75
|
+
moduleSpecifier: String(importPath),
|
|
74
76
|
localName,
|
|
75
77
|
importedName,
|
|
76
78
|
importKind,
|
package/package.json
CHANGED