@shapeshift-labs/frontier-lang-compiler 0.2.79 → 0.2.81

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
@@ -353,6 +353,27 @@ console.log(changeSet.mergeCandidate.readiness); // merge-admission classificati
353
353
 
354
354
  Use `diffNativeSourceImports` when the worker or runner already produced `importNativeSource` results. Changed regions include a `metadata.changedRegionProjection` envelope with before/after source hashes, source-map links, ownership keys, readiness, and `autoMergeClaim: false` so swarm admission tools can score or port patches without treating semantic metadata as proof. Body-only edits that the lightweight scanner cannot anchor to a symbol are still reported as file-level changed regions instead of being silently treated as safe.
355
355
 
356
+ Build a three-way semantic edit script when a coordinator has base, worker, and current-head source text. This is the first deterministic bridge from semantic sidecars to automatic merge admission:
357
+
358
+ ```js
359
+ import { createSemanticEditScript } from '@shapeshift-labs/frontier-lang-compiler';
360
+
361
+ const script = createSemanticEditScript({
362
+ language: 'typescript',
363
+ sourcePath: 'src/runtime.ts',
364
+ baseSourceText: 'export function step(v) { return v + 1; }\n',
365
+ workerSourceText: 'export function step(v) { return v + 2; }\n',
366
+ headSourceText: 'export function step(v) { return v + 1; }\n'
367
+ });
368
+
369
+ console.log(script.operations[0].kind); // "replaceBody"
370
+ console.log(script.operations[0].status); // "portable"
371
+ console.log(script.admission.status); // "auto-merge-candidate"
372
+ console.log(script.admission.autoMergeClaim); // false
373
+ ```
374
+
375
+ If the current head already changed the same semantic anchor, the script reports `conflict`; if the anchor moved or was renamed, it reports `needs-port` with a `reanchor` target; if no current head is supplied, it reports a review-only candidate. The script is intentionally an edit/admission record, not a proof of semantic equivalence.
376
+
356
377
  Bundle a native diff into a compact semantic patch record when a coordinator needs hashes, changed semantic regions, source-map links, proof/history/evidence IDs, and merge-admission status without loading whole source files:
357
378
 
358
379
  ```js
@@ -912,6 +933,7 @@ The published Frontier package family is generated from one shared package catal
912
933
  - [`@shapeshift-labs/frontier-realtime-server`](https://www.npmjs.com/package/@shapeshift-labs/frontier-realtime-server): Authoritative realtime room, tick, command validation, rate-limit, session, and snapshot-history runtime.
913
934
  - [`@shapeshift-labs/frontier-realtime-websocket`](https://www.npmjs.com/package/@shapeshift-labs/frontier-realtime-websocket): WebSocket client, wire, and Node room-server transport for Frontier realtime.
914
935
  - [`@shapeshift-labs/frontier-game`](https://www.npmjs.com/package/@shapeshift-labs/frontier-game): Game-facing entity, component, player, room, ownership, spatial interest, rollback, physics, and replication helpers above realtime.
936
+ - [`@shapeshift-labs/loom`](https://www.npmjs.com/package/@shapeshift-labs/loom): Repo-level semantic collaboration CLI for .loom workspaces, including init, scan, status, graph snapshots, projection plans, Frontier Lang delegation, Frontier Swarm delegation, and Frontier Framework delegation.
915
937
 
916
938
  Package source repositories:
917
939
 
@@ -1005,3 +1027,4 @@ Package source repositories:
1005
1027
  - [`siliconjungle/-shapeshift-labs-frontier-realtime-server`](https://github.com/siliconjungle/-shapeshift-labs-frontier-realtime-server)
1006
1028
  - [`siliconjungle/-shapeshift-labs-frontier-realtime-websocket`](https://github.com/siliconjungle/-shapeshift-labs-frontier-realtime-websocket)
1007
1029
  - [`siliconjungle/-shapeshift-labs-frontier-game`](https://github.com/siliconjungle/-shapeshift-labs-frontier-game)
1030
+ - [`siliconjungle/-shapeshift-labs-loom`](https://github.com/siliconjungle/-shapeshift-labs-loom)
@@ -0,0 +1,183 @@
1
+ import type {
2
+ EvidenceRecord,
3
+ FrontierSourceLanguage,
4
+ SemanticMergeReadiness,
5
+ SourceSpan
6
+ } from '@shapeshift-labs/frontier-lang-kernel';
7
+ import type { ImportNativeSourceOptions, NativeSourceImportResult } from './import-adapter-core.js';
8
+
9
+ export type SemanticEditScriptOperationStatus =
10
+ | 'candidate'
11
+ | 'portable'
12
+ | 'already-applied'
13
+ | 'needs-port'
14
+ | 'conflict'
15
+ | 'stale'
16
+ | 'blocked';
17
+
18
+ export type SemanticEditScriptAdmissionStatus =
19
+ | 'auto-merge-candidate'
20
+ | 'needs-port'
21
+ | 'conflict'
22
+ | 'stale'
23
+ | 'blocked'
24
+ | 'evidence-only';
25
+
26
+ export interface SemanticEditScriptOperation {
27
+ readonly id: string;
28
+ readonly kind: string;
29
+ readonly changeKind?: string;
30
+ readonly anchor: {
31
+ readonly key?: string;
32
+ readonly conflictKey?: string;
33
+ readonly regionId?: string;
34
+ readonly regionKind?: string;
35
+ readonly granularity?: string;
36
+ readonly language?: FrontierSourceLanguage | string;
37
+ readonly sourcePath?: string;
38
+ readonly symbolId?: string;
39
+ readonly symbolName?: string;
40
+ readonly symbolKind?: string;
41
+ readonly sourceSpan?: SourceSpan;
42
+ };
43
+ readonly spans?: {
44
+ readonly base?: SourceSpan;
45
+ readonly worker?: SourceSpan;
46
+ readonly head?: SourceSpan;
47
+ };
48
+ readonly hashes?: {
49
+ readonly baseSourceHash?: string;
50
+ readonly workerSourceHash?: string;
51
+ readonly headSourceHash?: string;
52
+ readonly baseSpanHash?: string;
53
+ readonly workerSpanHash?: string;
54
+ readonly headSpanHash?: string;
55
+ readonly baseTextHash?: string;
56
+ readonly workerTextHash?: string;
57
+ readonly headTextHash?: string;
58
+ readonly beforeSignatureHash?: string;
59
+ readonly afterSignatureHash?: string;
60
+ };
61
+ readonly status: SemanticEditScriptOperationStatus;
62
+ readonly reanchor?: {
63
+ readonly fromAnchorKey?: string;
64
+ readonly toAnchorKey?: string;
65
+ readonly lineageStatus?: string;
66
+ readonly traversedEventIds?: readonly string[];
67
+ };
68
+ readonly readiness: SemanticMergeReadiness | string;
69
+ readonly confidence: number;
70
+ readonly reasonCodes: readonly string[];
71
+ readonly evidenceIds?: readonly string[];
72
+ readonly metadata?: Record<string, unknown>;
73
+ }
74
+
75
+ export interface SemanticEditScriptSummary {
76
+ readonly operations: number;
77
+ readonly byStatus: Readonly<Record<string, number>>;
78
+ readonly byKind: Readonly<Record<string, number>>;
79
+ readonly portable: number;
80
+ readonly alreadyApplied: number;
81
+ readonly needsPort: number;
82
+ readonly conflicts: number;
83
+ readonly stale: number;
84
+ readonly blocked: number;
85
+ readonly candidates: number;
86
+ readonly autoMergeCandidates: number;
87
+ }
88
+
89
+ export interface SemanticEditScriptAdmission {
90
+ readonly status: SemanticEditScriptAdmissionStatus;
91
+ readonly action: 'run-gates-and-apply' | 'reanchor-or-human-port' | 'record-evidence' | 'block' | string;
92
+ readonly reviewRequired: boolean;
93
+ readonly autoApplyCandidate: boolean;
94
+ readonly autoMergeClaim: false;
95
+ readonly semanticEquivalenceClaim: false;
96
+ readonly reasonCodes: readonly string[];
97
+ readonly conflictKeys: readonly string[];
98
+ readonly evidenceIds: readonly string[];
99
+ }
100
+
101
+ export interface SemanticEditScript {
102
+ readonly kind: 'frontier.lang.semanticEditScript';
103
+ readonly version: 1;
104
+ readonly schema: 'frontier.lang.semanticEditScript.v1';
105
+ readonly id: string;
106
+ readonly stableId: string;
107
+ readonly hash: string;
108
+ readonly language?: FrontierSourceLanguage | string;
109
+ readonly sourcePath?: string;
110
+ readonly baseHash?: string;
111
+ readonly workerHash?: string;
112
+ readonly headHash?: string;
113
+ readonly workerChangeSetId: string;
114
+ readonly headChangeSetId?: string;
115
+ readonly lineageInferenceId?: string;
116
+ readonly operations: readonly SemanticEditScriptOperation[];
117
+ readonly summary: SemanticEditScriptSummary;
118
+ readonly admission: SemanticEditScriptAdmission;
119
+ readonly evidence: readonly EvidenceRecord[];
120
+ readonly metadata?: Record<string, unknown>;
121
+ }
122
+
123
+ export interface SemanticEditProjection {
124
+ readonly kind: 'frontier.lang.semanticEditProjection';
125
+ readonly version: 1;
126
+ readonly id: string;
127
+ readonly hash: string;
128
+ readonly scriptId?: string;
129
+ readonly status: 'projected' | 'blocked';
130
+ readonly sourcePath?: string;
131
+ readonly language?: FrontierSourceLanguage | string;
132
+ readonly baseHash?: string;
133
+ readonly workerHash?: string;
134
+ readonly headHash?: string;
135
+ readonly projectedHash?: string;
136
+ readonly appliedOperations: readonly string[];
137
+ readonly skippedOperations: readonly string[];
138
+ readonly sourceText?: string;
139
+ readonly admission: {
140
+ readonly status: 'auto-merge-candidate' | 'blocked';
141
+ readonly autoMergeClaim: false;
142
+ readonly semanticEquivalenceClaim: false;
143
+ readonly reasonCodes: readonly string[];
144
+ };
145
+ readonly metadata?: Record<string, unknown>;
146
+ }
147
+
148
+ export interface ProjectSemanticEditScriptToSourceOptions {
149
+ readonly id?: string;
150
+ readonly script: SemanticEditScript;
151
+ readonly workerSourceText: string;
152
+ readonly headSourceText: string;
153
+ readonly metadata?: Record<string, unknown>;
154
+ }
155
+
156
+ export interface CreateSemanticEditScriptOptions {
157
+ readonly id?: string;
158
+ readonly language?: FrontierSourceLanguage | string;
159
+ readonly sourcePath?: string;
160
+ readonly parser?: string;
161
+ readonly base?: NativeSourceImportResult | ImportNativeSourceOptions;
162
+ readonly before?: NativeSourceImportResult | ImportNativeSourceOptions;
163
+ readonly worker?: NativeSourceImportResult | ImportNativeSourceOptions;
164
+ readonly after?: NativeSourceImportResult | ImportNativeSourceOptions;
165
+ readonly head?: NativeSourceImportResult | ImportNativeSourceOptions;
166
+ readonly current?: NativeSourceImportResult | ImportNativeSourceOptions;
167
+ readonly baseSourceText?: string;
168
+ readonly beforeSourceText?: string;
169
+ readonly workerSourceText?: string;
170
+ readonly afterSourceText?: string;
171
+ readonly headSourceText?: string;
172
+ readonly currentSourceText?: string;
173
+ readonly baseSourceHash?: string;
174
+ readonly workerSourceHash?: string;
175
+ readonly headSourceHash?: string;
176
+ readonly generatedAt?: number;
177
+ readonly evidenceId?: string;
178
+ readonly metadata?: Record<string, unknown>;
179
+ }
180
+
181
+ export declare const SemanticEditScriptAdmissionStatuses: readonly SemanticEditScriptAdmissionStatus[];
182
+ export declare function createSemanticEditScript(input?: CreateSemanticEditScriptOptions): SemanticEditScript;
183
+ export declare function projectSemanticEditScriptToSource(input: ProjectSemanticEditScriptToSourceOptions): SemanticEditProjection;
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export * from './declarations/source-preservation.js';
16
16
  export * from './declarations/semantic-sidecar-admission.js';
17
17
  export * from './declarations/semantic-merge-candidates.js';
18
18
  export * from './declarations/semantic-merge-conflicts.js';
19
+ export * from './declarations/semantic-edit-script.js';
19
20
  export * from './declarations/semantic-lineage.js';
20
21
  export * from './declarations/semantic-history.js';
21
22
  export * from './declarations/semantic-patch-bundle.js';
package/dist/index.js CHANGED
@@ -69,6 +69,8 @@ export { queryProjectionReadinessMatrix } from './internal/index-impl/queryProje
69
69
  export { createSemanticPatchBundleRecord, querySemanticPatchBundleRecords, SemanticPatchBundleAdmissionStatuses } from './internal/index-impl/semanticPatchBundleRecords.js';
70
70
  export { createSemanticMergeCandidateAdmissionRecord, decorateSemanticMergeCandidateForAdmission, querySemanticMergeCandidateAdmissionOverlaps, SemanticMergeCandidateProjectionRisks, semanticMergeCandidateReadinessSortKey, sortSemanticMergeCandidateAdmissionRecords } from './internal/index-impl/semanticMergeCandidateRecords.js';
71
71
  export { querySemanticMergeConflictClasses, SemanticMergeConflictClasses, semanticMergeConflictRiskScore, sortSemanticMergeCandidatesByConflictRisk, summarizeSemanticMergeConflicts } from './internal/index-impl/semanticMergeConflicts.js';
72
+ export { createSemanticEditScript, SemanticEditScriptAdmissionStatuses } from './internal/index-impl/semanticEditScripts.js';
73
+ export { projectSemanticEditScriptToSource } from './internal/index-impl/projectSemanticEditScriptToSource.js';
72
74
  export { queryUniversalConversionPlan } from './internal/index-impl/queryUniversalConversionPlan.js';
73
75
  export { createSemanticAnchor, createSemanticLineageEvent, createSemanticLineageMap, querySemanticLineageEvents, SemanticLineageEventKinds } from './internal/index-impl/semanticLineageRecords.js';
74
76
  export { resolveSemanticLineage, resolveSemanticLineageBatch, SemanticLineageResolutionStatuses } from './internal/index-impl/semanticLineageResolutionRecords.js';
@@ -0,0 +1,101 @@
1
+ import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import { idFragment, uniqueStrings } from '../../native-import-utils.js';
3
+
4
+ export function projectSemanticEditScriptToSource(input = {}) {
5
+ const script = input.script;
6
+ const workerSourceText = input.workerSourceText;
7
+ const headSourceText = input.headSourceText;
8
+ const reasonCodes = [];
9
+ if (!script) throw new Error('projectSemanticEditScriptToSource requires a script');
10
+ if (script.admission?.status !== 'auto-merge-candidate') reasonCodes.push('script-not-auto-merge-candidate');
11
+ if (typeof workerSourceText !== 'string') reasonCodes.push('missing-worker-source-text');
12
+ if (typeof headSourceText !== 'string') reasonCodes.push('missing-head-source-text');
13
+ const edits = [];
14
+ for (const operation of script.operations ?? []) {
15
+ const edit = sourceEditForOperation(operation, workerSourceText, headSourceText);
16
+ if (edit.ok) edits.push(edit.value);
17
+ else reasonCodes.push(...edit.reasonCodes);
18
+ }
19
+ const blocked = reasonCodes.length > 0;
20
+ const sourceText = blocked ? undefined : applySourceEdits(headSourceText, edits);
21
+ const core = {
22
+ kind: 'frontier.lang.semanticEditProjection',
23
+ version: 1,
24
+ id: input.id ?? `semantic_edit_projection_${idFragment(script.id ?? script.hash ?? 'script')}`,
25
+ scriptId: script.id,
26
+ status: blocked ? 'blocked' : 'projected',
27
+ sourcePath: script.sourcePath,
28
+ language: script.language,
29
+ baseHash: script.baseHash,
30
+ workerHash: script.workerHash,
31
+ headHash: script.headHash,
32
+ projectedHash: sourceText === undefined ? undefined : hashSemanticValue(sourceText),
33
+ appliedOperations: blocked ? [] : edits.map((edit) => edit.operationId),
34
+ skippedOperations: blocked ? (script.operations ?? []).map((operation) => operation.id) : [],
35
+ sourceText,
36
+ admission: {
37
+ status: blocked ? 'blocked' : 'auto-merge-candidate',
38
+ autoMergeClaim: false,
39
+ semanticEquivalenceClaim: false,
40
+ reasonCodes: uniqueStrings(reasonCodes)
41
+ },
42
+ metadata: compactRecord({
43
+ autoMergeClaim: false,
44
+ semanticEquivalenceClaim: false,
45
+ editCount: edits.length,
46
+ ...input.metadata
47
+ })
48
+ };
49
+ return { ...core, hash: hashSemanticValue(core) };
50
+ }
51
+
52
+ function sourceEditForOperation(operation, workerSourceText, headSourceText) {
53
+ if (operation.status === 'already-applied') {
54
+ return { ok: true, value: { operationId: operation.id, start: 0, end: 0, replacement: '', alreadyApplied: true } };
55
+ }
56
+ if (operation.status !== 'portable') return { ok: false, reasonCodes: [`operation-not-portable:${operation.id}`] };
57
+ const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
58
+ const headOffsets = spanOffsets(headSourceText, operation.spans?.head ?? operation.spans?.base ?? operation.anchor?.sourceSpan);
59
+ const reasons = [];
60
+ if (!workerOffsets) reasons.push(`worker-span-not-resolvable:${operation.id}`);
61
+ if (!headOffsets) reasons.push(`head-span-not-resolvable:${operation.id}`);
62
+ if (reasons.length) return { ok: false, reasonCodes: reasons };
63
+ const replacement = workerSourceText.slice(workerOffsets.start, workerOffsets.end);
64
+ const current = headSourceText.slice(headOffsets.start, headOffsets.end);
65
+ if (operation.hashes?.workerTextHash && hashSemanticValue(replacement) !== operation.hashes.workerTextHash) {
66
+ reasons.push(`worker-span-hash-mismatch:${operation.id}`);
67
+ }
68
+ const expectedHeadHash = operation.hashes?.headTextHash ?? operation.hashes?.baseTextHash;
69
+ if (expectedHeadHash && hashSemanticValue(current) !== expectedHeadHash) {
70
+ reasons.push(`head-span-hash-mismatch:${operation.id}`);
71
+ }
72
+ if (reasons.length) return { ok: false, reasonCodes: reasons };
73
+ return { ok: true, value: { operationId: operation.id, start: headOffsets.start, end: headOffsets.end, replacement } };
74
+ }
75
+
76
+ function applySourceEdits(sourceText, edits) {
77
+ return edits.filter((edit) => !edit.alreadyApplied)
78
+ .sort((left, right) => right.start - left.start)
79
+ .reduce((text, edit) => text.slice(0, edit.start) + edit.replacement + text.slice(edit.end), sourceText);
80
+ }
81
+
82
+ function spanOffsets(sourceText, span) {
83
+ if (typeof sourceText !== 'string' || !span) return undefined;
84
+ if (typeof span.start === 'number' && typeof span.end === 'number' && span.end >= span.start) return { start: span.start, end: span.end };
85
+ if (typeof span.startLine !== 'number') return undefined;
86
+ const lineStarts = [0];
87
+ for (let index = 0; index < sourceText.length; index += 1) if (sourceText[index] === '\n') lineStarts.push(index + 1);
88
+ const startLine = Math.max(1, span.startLine);
89
+ const endLine = Math.max(startLine, typeof span.endLine === 'number' ? span.endLine : startLine);
90
+ const start = lineStarts[startLine - 1];
91
+ const endLineStart = lineStarts[endLine - 1];
92
+ if (start === undefined || endLineStart === undefined) return undefined;
93
+ const startColumn = Math.max(1, span.startColumn ?? 1) - 1;
94
+ const lineEnd = lineStarts[endLine] === undefined ? sourceText.length : lineStarts[endLine] - 1;
95
+ const endColumn = span.endColumn === undefined ? lineEnd - endLineStart : Math.max(1, span.endColumn) - 1;
96
+ return { start: start + startColumn, end: endLineStart + endColumn };
97
+ }
98
+
99
+ function compactRecord(value) {
100
+ return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0)));
101
+ }
@@ -0,0 +1,135 @@
1
+ import { uniqueStrings } from '../../native-import-utils.js';
2
+ import { resolveSemanticLineage } from './semanticLineageResolutionRecords.js';
3
+
4
+ export const SemanticEditScriptAdmissionStatuses = Object.freeze([
5
+ 'auto-merge-candidate',
6
+ 'needs-port',
7
+ 'conflict',
8
+ 'stale',
9
+ 'blocked',
10
+ 'evidence-only'
11
+ ]);
12
+
13
+ export function classifySemanticEdit(input) {
14
+ if (!input.context.head) return editStatus('candidate', 'needs-review', 0.66, ['head-source-not-provided']);
15
+ if (!input.anchorKey) return editStatus('blocked', 'blocked', 0, ['missing-semantic-anchor']);
16
+ if (input.context.workerChangeSet.beforeHash &&
17
+ input.context.headChangeSet?.afterHash === input.context.workerChangeSet.beforeHash &&
18
+ sameSourcePath(input.context.base, input.context.head)) {
19
+ return editStatus('portable', 'ready', 0.95, ['head-source-matches-base']);
20
+ }
21
+ if (input.region.changeKind === 'added') return classifyAddedRegion(input);
22
+ if (!input.baseSymbol) return classifyMissingBaseAnchor(input);
23
+ if (!input.headSymbol) return classifyMissingHeadAnchor(input);
24
+ if (input.headSymbol.spanHash && input.baseSymbol.spanHash && input.headSymbol.spanHash === input.baseSymbol.spanHash) {
25
+ return editStatus('portable', 'ready', 0.9, ['head-anchor-matches-base']);
26
+ }
27
+ if (input.headSymbol.spanHash && input.workerSymbol?.spanHash && input.headSymbol.spanHash === input.workerSymbol.spanHash) {
28
+ return editStatus('already-applied', 'ready', 0.92, ['head-anchor-matches-worker']);
29
+ }
30
+ return editStatus('conflict', 'blocked', 0.2, ['head-anchor-changed-since-base']);
31
+ }
32
+
33
+ export function summarizeSemanticEditOperations(operations) {
34
+ const byStatus = countBy(operations.map((operation) => operation.status));
35
+ const byKind = countBy(operations.map((operation) => operation.kind));
36
+ return {
37
+ operations: operations.length,
38
+ byStatus,
39
+ byKind,
40
+ portable: byStatus.portable ?? 0,
41
+ alreadyApplied: byStatus['already-applied'] ?? 0,
42
+ needsPort: byStatus['needs-port'] ?? 0,
43
+ conflicts: byStatus.conflict ?? 0,
44
+ stale: byStatus.stale ?? 0,
45
+ blocked: byStatus.blocked ?? 0,
46
+ candidates: byStatus.candidate ?? 0,
47
+ autoMergeCandidates: (byStatus.portable ?? 0) + (byStatus['already-applied'] ?? 0)
48
+ };
49
+ }
50
+
51
+ export function semanticEditAdmission(input) {
52
+ const status = admissionStatus(input.summary);
53
+ return {
54
+ status,
55
+ action: admissionAction(status),
56
+ reviewRequired: status !== 'auto-merge-candidate',
57
+ autoApplyCandidate: status === 'auto-merge-candidate',
58
+ autoMergeClaim: false,
59
+ semanticEquivalenceClaim: false,
60
+ reasonCodes: uniqueStrings([
61
+ ...input.workerChangeSet.reasons,
62
+ ...(input.headChangeSet?.reasons ?? []),
63
+ ...input.operations.flatMap((operation) => operation.reasonCodes ?? [])
64
+ ]),
65
+ conflictKeys: uniqueStrings(input.operations.map((operation) => operation.anchor?.conflictKey).filter(Boolean)),
66
+ evidenceIds: uniqueStrings(input.operations.flatMap((operation) => operation.evidenceIds ?? []))
67
+ };
68
+ }
69
+
70
+ function classifyAddedRegion(input) {
71
+ if (!input.headSymbol) return editStatus('portable', 'ready', 0.86, ['added-anchor-absent-from-head']);
72
+ if (input.headSymbol.spanHash && input.workerSymbol?.spanHash && input.headSymbol.spanHash === input.workerSymbol.spanHash) {
73
+ return editStatus('already-applied', 'ready', 0.88, ['added-anchor-already-present-in-head']);
74
+ }
75
+ return editStatus('conflict', 'blocked', 0.25, ['added-anchor-already-exists-in-head']);
76
+ }
77
+
78
+ function classifyMissingBaseAnchor(input) {
79
+ const conflictKey = input.region.conflictKey ?? `region:${input.anchorKey}`;
80
+ if (input.context.headChangedConflictKeys.has(conflictKey)) {
81
+ return editStatus('conflict', 'blocked', 0.18, ['base-anchor-missing-and-head-changed-same-region']);
82
+ }
83
+ return editStatus('stale', 'needs-review', 0.25, ['base-anchor-missing']);
84
+ }
85
+
86
+ function classifyMissingHeadAnchor(input) {
87
+ const resolved = input.context.headLineage
88
+ ? resolveSemanticLineage(input.context.headLineage.lineageMap, { anchorKey: input.anchorKey })
89
+ : undefined;
90
+ if (resolved?.status === 'resolved' && resolved.currentAnchors.length === 1) {
91
+ return editStatus('needs-port', 'needs-review', 0.72, ['anchor-moved-or-renamed'], {
92
+ fromAnchorKey: input.anchorKey,
93
+ toAnchorKey: resolved.currentAnchors[0].key,
94
+ lineageStatus: resolved.status,
95
+ traversedEventIds: resolved.traversedEventIds
96
+ }, resolved.evidenceIds);
97
+ }
98
+ if (resolved?.status === 'ambiguous') {
99
+ return editStatus('blocked', 'blocked', 0.1, ['anchor-lineage-ambiguous'], { fromAnchorKey: input.anchorKey, lineageStatus: resolved.status }, resolved.evidenceIds);
100
+ }
101
+ if (resolved?.status === 'deleted') {
102
+ return editStatus('blocked', 'blocked', 0.1, ['anchor-deleted-in-head'], { fromAnchorKey: input.anchorKey, lineageStatus: resolved.status }, resolved.evidenceIds);
103
+ }
104
+ return editStatus('stale', 'needs-review', 0.25, ['head-anchor-missing']);
105
+ }
106
+
107
+ function editStatus(status, readiness, confidence, reasonCodes, reanchor, evidenceIds = []) {
108
+ return { status, readiness, confidence, reasonCodes, reanchor, evidenceIds };
109
+ }
110
+
111
+ function admissionStatus(summary) {
112
+ if (summary.operations === 0) return 'evidence-only';
113
+ if (summary.blocked > 0) return 'blocked';
114
+ if (summary.conflicts > 0) return 'conflict';
115
+ if (summary.stale > 0) return 'stale';
116
+ if (summary.needsPort > 0 || summary.candidates > 0) return 'needs-port';
117
+ return 'auto-merge-candidate';
118
+ }
119
+
120
+ function admissionAction(status) {
121
+ if (status === 'auto-merge-candidate') return 'run-gates-and-apply';
122
+ if (status === 'needs-port') return 'reanchor-or-human-port';
123
+ if (status === 'evidence-only') return 'record-evidence';
124
+ return 'block';
125
+ }
126
+
127
+ function countBy(values) {
128
+ const result = {};
129
+ for (const value of values.filter(Boolean)) result[value] = (result[value] ?? 0) + 1;
130
+ return result;
131
+ }
132
+
133
+ function sameSourcePath(left, right) {
134
+ return (left?.sourcePath ?? left?.nativeSource?.sourcePath) === (right?.sourcePath ?? right?.nativeSource?.sourcePath);
135
+ }
@@ -0,0 +1,227 @@
1
+ import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import { idFragment, uniqueStrings } from '../../native-import-utils.js';
3
+ import { createSemanticImportSidecar } from './createSemanticImportSidecar.js';
4
+ import { diffNativeSourceImports } from './diffNativeSourceImports.js';
5
+ import { inferSemanticLineageEvents } from './inferSemanticLineageEvents.js';
6
+ import { mapDiffSymbols } from './mapDiffSymbols.js';
7
+ import { nativeImportSourceText } from './nativeImportSourceText.js';
8
+ import { normalizeNativeDiffImport } from './normalizeNativeDiffImport.js';
9
+ import {
10
+ classifySemanticEdit,
11
+ SemanticEditScriptAdmissionStatuses,
12
+ semanticEditAdmission,
13
+ summarizeSemanticEditOperations
14
+ } from './semanticEditScriptClassification.js';
15
+ import { sourceTextForSpan } from './sourceTextForSpan.js';
16
+
17
+ export { SemanticEditScriptAdmissionStatuses };
18
+
19
+ export function createSemanticEditScript(input = {}, options = {}) {
20
+ const language = input.language ?? input.worker?.language ?? input.after?.language ?? input.base?.language ?? input.before?.language;
21
+ if (!language) throw new Error('createSemanticEditScript requires a language');
22
+ const sourcePath = input.sourcePath ?? input.worker?.sourcePath ?? input.after?.sourcePath ?? input.base?.sourcePath ?? input.before?.sourcePath;
23
+ const base = normalizeNativeDiffImport(sourceInput(input, 'base'), { ...input, language, sourcePath }, 'base');
24
+ const worker = normalizeNativeDiffImport(sourceInput(input, 'worker'), { ...input, language, sourcePath }, 'worker');
25
+ if (!base || !worker) throw new Error('createSemanticEditScript requires base and worker source inputs');
26
+ const head = normalizeNativeDiffImport(sourceInput(input, 'head'), { ...input, language, sourcePath }, 'head');
27
+ const workerChangeSet = diffNativeSourceImports({
28
+ ...input,
29
+ language,
30
+ sourcePath,
31
+ before: base,
32
+ after: worker,
33
+ id: input.workerChangeSetId ?? `${input.id ?? 'semantic_edit'}_worker`
34
+ });
35
+ const headChangeSet = head ? diffNativeSourceImports({
36
+ ...input,
37
+ language,
38
+ sourcePath,
39
+ before: base,
40
+ after: head,
41
+ id: input.headChangeSetId ?? `${input.id ?? 'semantic_edit'}_head`
42
+ }) : undefined;
43
+ const headLineage = head ? inferSemanticLineageEvents({
44
+ before: base,
45
+ after: head,
46
+ id: input.lineageInferenceId ?? `${input.id ?? 'semantic_edit'}_head_lineage`,
47
+ language,
48
+ sourcePath,
49
+ generatedAt: input.generatedAt,
50
+ metadata: { source: 'createSemanticEditScript' }
51
+ }) : undefined;
52
+ const context = createEditContext({ base, worker, head, workerChangeSet, headChangeSet, headLineage });
53
+ const operations = workerChangeSet.changedRegions.map((region, index) => semanticEditOperation(region, index, context, input));
54
+ const summary = summarizeSemanticEditOperations(operations);
55
+ const admission = semanticEditAdmission({ operations, summary, head, workerChangeSet, headChangeSet, input });
56
+ const evidence = semanticEditEvidence({ input, language, sourcePath, workerChangeSet, headChangeSet, headLineage, summary, admission });
57
+ const core = {
58
+ kind: 'frontier.lang.semanticEditScript',
59
+ version: 1,
60
+ schema: 'frontier.lang.semanticEditScript.v1',
61
+ language,
62
+ sourcePath,
63
+ baseHash: workerChangeSet.beforeHash,
64
+ workerHash: workerChangeSet.afterHash,
65
+ headHash: headChangeSet?.afterHash,
66
+ workerChangeSetId: workerChangeSet.id,
67
+ headChangeSetId: headChangeSet?.id,
68
+ lineageInferenceId: headLineage?.id,
69
+ operations,
70
+ summary,
71
+ admission,
72
+ evidence,
73
+ metadata: compactRecord({
74
+ autoMergeClaim: false,
75
+ semanticEquivalenceClaim: false,
76
+ workerReadiness: workerChangeSet.readiness,
77
+ headReadiness: headChangeSet?.readiness,
78
+ workerReasons: workerChangeSet.reasons,
79
+ headReasons: headChangeSet?.reasons,
80
+ ...input.metadata,
81
+ ...options.metadata
82
+ })
83
+ };
84
+ const hash = hashSemanticValue(core);
85
+ const id = input.id ?? `semantic_edit_script_${idFragment(firstString(sourcePath, language, hash))}`;
86
+ return { ...core, id, stableId: `semantic_edit_script_${idFragment(hash)}`, hash };
87
+ }
88
+
89
+ function sourceInput(input, side) {
90
+ if (side === 'base') return input.base ?? input.before ?? sourceTextInput(input, 'base') ?? sourceTextInput(input, 'before');
91
+ if (side === 'worker') return input.worker ?? input.after ?? sourceTextInput(input, 'worker') ?? sourceTextInput(input, 'after');
92
+ return input.head ?? input.current ?? sourceTextInput(input, 'head') ?? sourceTextInput(input, 'current');
93
+ }
94
+
95
+ function sourceTextInput(input, side) {
96
+ const sourceText = input[`${side}SourceText`];
97
+ if (sourceText === undefined) return undefined;
98
+ return {
99
+ language: input.language,
100
+ sourcePath: input[`${side}SourcePath`] ?? input.sourcePath,
101
+ sourceText,
102
+ sourceHash: input[`${side}SourceHash`],
103
+ parser: input.parser,
104
+ metadata: input[`${side}Metadata`]
105
+ };
106
+ }
107
+
108
+ function createEditContext(input) {
109
+ return {
110
+ ...input,
111
+ baseSymbols: symbolsByAnchor(input.base),
112
+ workerSymbols: symbolsByAnchor(input.worker),
113
+ headSymbols: symbolsByAnchor(input.head),
114
+ headChangedConflictKeys: new Set(input.headChangeSet?.conflictKeys ?? [])
115
+ };
116
+ }
117
+
118
+ function symbolsByAnchor(imported) {
119
+ if (!imported) return new Map();
120
+ const sidecar = createSemanticImportSidecar(imported, { id: `semantic_edit_sidecar_${idFragment(imported.id ?? imported.sourcePath ?? 'source')}` });
121
+ const result = new Map();
122
+ for (const symbol of mapDiffSymbols(imported, sidecar).values()) {
123
+ for (const key of uniqueStrings([symbol.ownershipKey, symbol.key, symbol.id])) {
124
+ result.set(key, symbol);
125
+ }
126
+ }
127
+ return result;
128
+ }
129
+
130
+ function semanticEditOperation(region, index, context, input) {
131
+ const anchorKey = region.key ?? region.conflictKey ?? region.id;
132
+ const baseSymbol = context.baseSymbols.get(anchorKey);
133
+ const workerSymbol = context.workerSymbols.get(anchorKey);
134
+ const headSymbol = context.headSymbols.get(anchorKey);
135
+ const classification = classifySemanticEdit({ region, anchorKey, baseSymbol, workerSymbol, headSymbol, context });
136
+ const kind = semanticEditOperationKind(region);
137
+ const baseText = spanText(context.base, baseSymbol?.sourceSpan ?? region.metadata?.changedRegionProjection?.before?.sourceSpan ?? region.sourceSpan);
138
+ const workerText = spanText(context.worker, workerSymbol?.sourceSpan ?? region.metadata?.changedRegionProjection?.after?.sourceSpan ?? region.sourceSpan);
139
+ const headText = spanText(context.head, headSymbol?.sourceSpan ?? region.metadata?.changedRegionProjection?.head?.sourceSpan ?? baseSymbol?.sourceSpan);
140
+ return compactRecord({
141
+ id: `semantic_edit_op_${idFragment(firstString(input.id, anchorKey, index))}`,
142
+ kind,
143
+ changeKind: region.changeKind,
144
+ anchor: compactRecord({
145
+ key: anchorKey,
146
+ conflictKey: region.conflictKey ?? `region:${anchorKey}`,
147
+ regionId: region.id,
148
+ regionKind: region.regionKind,
149
+ granularity: region.granularity,
150
+ language: region.language ?? context.workerChangeSet.language,
151
+ sourcePath: region.sourcePath ?? context.workerChangeSet.sourcePath,
152
+ symbolId: region.symbolId ?? workerSymbol?.id ?? baseSymbol?.id,
153
+ symbolName: region.symbolName ?? workerSymbol?.name ?? baseSymbol?.name,
154
+ symbolKind: region.symbolKind ?? workerSymbol?.kind ?? baseSymbol?.kind,
155
+ sourceSpan: workerSymbol?.sourceSpan ?? region.sourceSpan
156
+ }),
157
+ spans: compactRecord({
158
+ base: baseSymbol?.sourceSpan ?? region.metadata?.changedRegionProjection?.before?.sourceSpan,
159
+ worker: workerSymbol?.sourceSpan ?? region.metadata?.changedRegionProjection?.after?.sourceSpan ?? region.sourceSpan,
160
+ head: headSymbol?.sourceSpan
161
+ }),
162
+ hashes: compactRecord({
163
+ baseSourceHash: context.workerChangeSet.beforeHash,
164
+ workerSourceHash: context.workerChangeSet.afterHash,
165
+ headSourceHash: context.headChangeSet?.afterHash,
166
+ baseSpanHash: baseSymbol?.spanHash,
167
+ workerSpanHash: workerSymbol?.spanHash,
168
+ headSpanHash: headSymbol?.spanHash,
169
+ baseTextHash: baseText === undefined ? undefined : hashSemanticValue(baseText),
170
+ workerTextHash: workerText === undefined ? undefined : hashSemanticValue(workerText),
171
+ headTextHash: headText === undefined ? undefined : hashSemanticValue(headText),
172
+ beforeSignatureHash: workerSymbol?.beforeSignatureHash ?? baseSymbol?.signatureHash,
173
+ afterSignatureHash: workerSymbol?.afterSignatureHash ?? workerSymbol?.signatureHash
174
+ }),
175
+ status: classification.status,
176
+ reanchor: classification.reanchor,
177
+ readiness: classification.readiness,
178
+ confidence: classification.confidence,
179
+ reasonCodes: classification.reasonCodes,
180
+ evidenceIds: classification.evidenceIds,
181
+ metadata: {
182
+ autoMergeClaim: false,
183
+ semanticEquivalenceClaim: false
184
+ }
185
+ });
186
+ }
187
+
188
+ function semanticEditOperationKind(region) {
189
+ const prefix = region.changeKind === 'added' ? 'add' : region.changeKind === 'removed' ? 'remove' : 'replace';
190
+ const kind = String(region.regionKind ?? region.granularity ?? 'region');
191
+ if (kind === 'body') return `${prefix}Body`;
192
+ if (kind === 'import') return `${prefix}Import`;
193
+ if (kind === 'type') return `${prefix}TypeDeclaration`;
194
+ if (kind === 'property') return `${prefix}Property`;
195
+ return `${prefix}Region`;
196
+ }
197
+
198
+ function spanText(imported, span) {
199
+ return sourceTextForSpan(nativeImportSourceText(imported), span);
200
+ }
201
+
202
+ function semanticEditEvidence(input) {
203
+ return [{
204
+ id: input.input.evidenceId ?? `evidence_${idFragment(input.input.id ?? input.sourcePath ?? 'semantic_edit')}_semantic_edit_script`,
205
+ kind: 'semantic-edit-script',
206
+ status: input.admission.status === 'blocked' || input.admission.status === 'conflict' ? 'needs-review' : 'passed',
207
+ path: input.sourcePath,
208
+ summary: `Created semantic edit script with ${input.summary.operations} operation(s): ${input.admission.status}.`,
209
+ metadata: {
210
+ workerChangeSetId: input.workerChangeSet.id,
211
+ headChangeSetId: input.headChangeSet?.id,
212
+ lineageInferenceId: input.headLineage?.id,
213
+ summary: input.summary,
214
+ admissionStatus: input.admission.status,
215
+ autoMergeClaim: false,
216
+ semanticEquivalenceClaim: false
217
+ }
218
+ }];
219
+ }
220
+
221
+ function firstString(...values) {
222
+ return values.map((value) => value === undefined || value === null ? '' : String(value)).find(Boolean);
223
+ }
224
+
225
+ function compactRecord(value) {
226
+ return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0)));
227
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-compiler",
3
- "version": "0.2.79",
3
+ "version": "0.2.81",
4
4
  "description": "Compiler facade for Frontier Lang source documents and language projection adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",