@shapeshift-labs/frontier-lang 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShapeShift Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # `@shapeshift-labs/frontier-lang`
2
+
3
+ Patch-native semantic source kernel for replayable programs, merge admission, and generated TypeScript projections.
4
+
5
+ This package is the first small slice of a Frontier-oriented language idea:
6
+
7
+ > Source is semantic state. Editing is patches. Compilation is projection. Merge is replay plus proof/evidence.
8
+
9
+ The package does not try to be a full programming language yet. It provides the runtime-neutral source graph and compiler contracts that a concrete `.frontier` parser can target later.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install @shapeshift-labs/frontier-lang
15
+ ```
16
+
17
+ The package is ESM-first and dependency-free at runtime. Local checkout examples import from `dist`, so run `npm run build` before running files under `examples/`.
18
+
19
+ ## Why
20
+
21
+ Git merges text. Large agent swarms make concurrent semantic changes: APIs, state cells, routes, effects, migrations, tests, generated artifacts, and ownership surfaces.
22
+
23
+ `frontier-lang` models those as typed graph nodes and semantic patch bundles so tooling can classify a merge before it becomes a pile of conflicting files.
24
+
25
+ ## Example
26
+
27
+ ```ts
28
+ import {
29
+ actionNode,
30
+ classifyMerge,
31
+ createDocument,
32
+ createPatch,
33
+ emitTypeScript,
34
+ entityNode,
35
+ hashDocumentBase,
36
+ stateNode
37
+ } from "@shapeshift-labs/frontier-lang";
38
+
39
+ const todo = entityNode({
40
+ id: "ent_todo",
41
+ name: "Todo",
42
+ fields: [
43
+ { id: "field_todo_id", name: "id", type: "TodoId", key: true },
44
+ { id: "field_todo_title", name: "title", type: "Text", merge: { kind: "conflict" } },
45
+ {
46
+ id: "field_todo_tags",
47
+ name: "tags",
48
+ type: "Set<Text>",
49
+ merge: { kind: "union", law: "semilattice" }
50
+ }
51
+ ]
52
+ });
53
+
54
+ const state = stateNode({
55
+ id: "state_todo",
56
+ name: "TodoDb",
57
+ collections: [
58
+ {
59
+ id: "collection_todos",
60
+ name: "todos",
61
+ type: "Map<TodoId, Todo>",
62
+ merge: { kind: "byKey", law: "commutative" }
63
+ }
64
+ ]
65
+ });
66
+
67
+ const addTodo = actionNode({
68
+ id: "action_add_todo",
69
+ name: "addTodo",
70
+ input: "{ title: Text }",
71
+ returns: "Patch",
72
+ reads: ["TodoDb.todos"],
73
+ writes: ["TodoDb.todos"],
74
+ uses: ["Clock"]
75
+ });
76
+
77
+ const document = createDocument({
78
+ id: "mod_todo",
79
+ name: "TodoApp",
80
+ nodes: [todo, state, addTodo]
81
+ });
82
+
83
+ const baseHash = hashDocumentBase(document);
84
+
85
+ const left = createPatch({
86
+ id: "patch_add_tags",
87
+ baseHash,
88
+ operations: [
89
+ {
90
+ op: "updateNode",
91
+ id: "ent_todo",
92
+ set: { metadata: { changedBy: "left" } },
93
+ touches: [{ id: "field_todo_tags", access: "write" }]
94
+ }
95
+ ],
96
+ evidence: [{ id: "test_tags", kind: "test", status: "passed" }]
97
+ });
98
+
99
+ const right = createPatch({
100
+ id: "patch_add_more_tags",
101
+ baseHash,
102
+ operations: [
103
+ {
104
+ op: "updateNode",
105
+ id: "ent_todo",
106
+ set: { metadata: { changedBy: "right" } },
107
+ touches: [{ id: "field_todo_tags", access: "write" }]
108
+ }
109
+ ],
110
+ evidence: [{ id: "test_more_tags", kind: "test", status: "passed" }]
111
+ });
112
+
113
+ console.log(classifyMerge(document, left, right).status);
114
+ // safe-by-merge-law
115
+
116
+ console.log(emitTypeScript(document));
117
+ ```
118
+
119
+ ## Concrete Language Shape
120
+
121
+ A later parser could project source that looks like this into the graph above. This is future syntax, not a shipped parser yet:
122
+
123
+ ```frontier
124
+ entity Todo @id("ent_todo") {
125
+ id @id("field_todo_id"): TodoId @key
126
+
127
+ title @id("field_todo_title"): Text {
128
+ merge conflict
129
+ }
130
+
131
+ tags @id("field_todo_tags"): Set<Text> {
132
+ merge union law semilattice
133
+ }
134
+ }
135
+
136
+ state TodoDb @id("state_todo") {
137
+ todos @id("collection_todos"): Map<TodoId, Todo> {
138
+ merge byKey law commutative
139
+ }
140
+ }
141
+
142
+ action addTodo(input: { title: Text })
143
+ reads TodoDb.todos
144
+ writes TodoDb.todos
145
+ uses Clock
146
+ returns Patch
147
+ {
148
+ patch {
149
+ TodoDb.todos[TodoId.new()] = Todo {
150
+ title: input.title
151
+ tags: Set.empty()
152
+ }
153
+ }
154
+ }
155
+ ```
156
+
157
+ ## Generated Outputs And Host Capabilities
158
+
159
+ There are two separate boundaries to keep distinct:
160
+
161
+ 1. **JS/TS as optional projection targets.** Frontier semantic source can do the graph, replay, and merge work first, then project generated targets. This package currently ships `emitTypeScript`; JavaScript can be a later projection target or a build output derived from generated TypeScript. In this mode, JS packages are not part of the canonical source model.
162
+ 2. **JS packages as host capabilities.** Some real systems still need React, Playwright, SQLite clients, storage SDKs, crypto libraries, or browser APIs. The language can use those through explicit capability adapters such as `Network`, `Storage<T>`, `ReactView`, or `SqlClient`.
163
+
164
+ The second mode should be opt-in. Importing arbitrary JS directly into semantic source would pull late-bound JavaScript behavior back into the merge problem. A better design is:
165
+
166
+ Future syntax sketch:
167
+
168
+ ```frontier
169
+ capability ReactView from npm("react") {
170
+ effects dom
171
+ boundary generated
172
+ }
173
+ ```
174
+
175
+ Today, the source-kernel shape for this is an effect/capability contract: actions list capabilities in `uses`, effect nodes name the capability and resources, and target nodes describe generated output. The host adapter resolves `npm:react` or browser APIs outside the canonical semantic graph.
176
+
177
+ That keeps JS useful without making JavaScript the canonical language semantics.
178
+
179
+ ## API Surface
180
+
181
+ - `createDocument`
182
+ - `entityNode`, `stateNode`, `actionNode`, `viewNode`, `migrationNode`, `effectNode`, `moduleNode`, `targetNode`
183
+ - `createPatch`
184
+ - `applySemanticPatch`
185
+ - `replayDocument`
186
+ - `classifyMerge`
187
+ - `emitTypeScript`
188
+ - `stableStringify`
189
+ - `hashSemanticValue`
190
+ - `hashDocumentBase`
191
+ - `validateDocument`
192
+
193
+ ## Status
194
+
195
+ Experimental. This is the source-kernel package for proving the core loop:
196
+
197
+ ```txt
198
+ semantic source graph -> patch/replay -> merge classification -> generated TypeScript
199
+ ```
200
+
201
+ The next layers are a concrete `.frontier` parser, richer compiler targets, verifier hooks, and adapters to the existing Frontier package family.
202
+
203
+ ## License
204
+
205
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,202 @@
1
+ export type JsonPrimitive = string | number | boolean | null;
2
+ export type JsonValue = JsonPrimitive | JsonValue[] | { readonly [key: string]: JsonValue };
3
+ export type JsonObject = { readonly [key: string]: JsonValue };
4
+ export type SemanticId = string;
5
+ export type NodeKind = "module" | "entity" | "state" | "action" | "view" | "migration" | "effect" | "target";
6
+ export type MergePolicyKind = "conflict" | "union" | "max" | "lastWriterWins" | "byKey" | "preserveMoves" | "manual" | "custom";
7
+
8
+ export interface MergePolicy {
9
+ readonly kind: MergePolicyKind;
10
+ readonly law?: "semilattice" | "commutative" | "associative" | "idempotent";
11
+ readonly condition?: string;
12
+ readonly customName?: string;
13
+ }
14
+
15
+ export interface SemanticRegion {
16
+ readonly id: string;
17
+ readonly access?: "read" | "write" | "readwrite" | "effect" | "schema" | "evidence";
18
+ }
19
+
20
+ export interface BaseNode {
21
+ readonly kind: NodeKind;
22
+ readonly id: SemanticId;
23
+ readonly name: string;
24
+ readonly parentId?: SemanticId;
25
+ readonly regions?: readonly SemanticRegion[];
26
+ readonly metadata?: JsonObject;
27
+ }
28
+
29
+ export interface CompileTarget {
30
+ readonly language: "typescript" | "javascript" | string;
31
+ readonly packageName?: string;
32
+ readonly emitPath?: string;
33
+ readonly moduleFormat?: "esm" | "commonjs";
34
+ }
35
+
36
+ export interface ModuleNode extends BaseNode {
37
+ readonly kind: "module";
38
+ readonly imports?: readonly string[];
39
+ readonly targets?: readonly CompileTarget[];
40
+ }
41
+
42
+ export interface FieldDeclaration {
43
+ readonly id: SemanticId;
44
+ readonly name: string;
45
+ readonly type: string;
46
+ readonly key?: boolean;
47
+ readonly merge?: MergePolicy;
48
+ readonly metadata?: JsonObject;
49
+ }
50
+
51
+ export interface EntityNode extends BaseNode {
52
+ readonly kind: "entity";
53
+ readonly fields: readonly FieldDeclaration[];
54
+ }
55
+
56
+ export interface StateCollection {
57
+ readonly id: SemanticId;
58
+ readonly name: string;
59
+ readonly type: string;
60
+ readonly merge?: MergePolicy;
61
+ }
62
+
63
+ export interface StateNode extends BaseNode {
64
+ readonly kind: "state";
65
+ readonly collections: readonly StateCollection[];
66
+ }
67
+
68
+ export interface ActionNode extends BaseNode {
69
+ readonly kind: "action";
70
+ readonly input?: string;
71
+ readonly returns?: string;
72
+ readonly reads?: readonly string[];
73
+ readonly writes?: readonly string[];
74
+ readonly uses?: readonly string[];
75
+ readonly throws?: readonly string[];
76
+ readonly body?: readonly JsonObject[];
77
+ }
78
+
79
+ export interface ViewNode extends BaseNode {
80
+ readonly kind: "view";
81
+ readonly reads?: readonly string[];
82
+ readonly dispatches?: readonly string[];
83
+ }
84
+
85
+ export interface MigrationNode extends BaseNode {
86
+ readonly kind: "migration";
87
+ readonly fromVersion: string;
88
+ readonly toVersion: string;
89
+ readonly changes: readonly JsonObject[];
90
+ readonly invariants?: readonly string[];
91
+ }
92
+
93
+ export interface EffectNode extends BaseNode {
94
+ readonly kind: "effect";
95
+ readonly capability: string;
96
+ readonly resources?: readonly string[];
97
+ }
98
+
99
+ export interface TargetNode extends BaseNode {
100
+ readonly kind: "target";
101
+ readonly target: CompileTarget;
102
+ }
103
+
104
+ export type SemanticNode = ModuleNode | EntityNode | StateNode | ActionNode | ViewNode | MigrationNode | EffectNode | TargetNode;
105
+
106
+ export interface FrontierLangDocument {
107
+ readonly kind: "frontier.lang.document";
108
+ readonly version: 1;
109
+ readonly id: SemanticId;
110
+ readonly name: string;
111
+ readonly rootIds: readonly SemanticId[];
112
+ readonly nodes: Readonly<Record<SemanticId, SemanticNode>>;
113
+ readonly history?: readonly ReplayEvent[];
114
+ readonly metadata?: JsonObject;
115
+ }
116
+
117
+ export type SemanticPatchOperation =
118
+ | { readonly op: "upsertNode"; readonly node: SemanticNode; readonly touches?: readonly SemanticRegion[] }
119
+ | { readonly op: "removeNode"; readonly id: SemanticId; readonly touches?: readonly SemanticRegion[] }
120
+ | { readonly op: "renameNode"; readonly id: SemanticId; readonly name: string; readonly touches?: readonly SemanticRegion[] }
121
+ | { readonly op: "moveNode"; readonly id: SemanticId; readonly parentId?: SemanticId; readonly touches?: readonly SemanticRegion[] }
122
+ | { readonly op: "updateNode"; readonly id: SemanticId; readonly set: JsonObject; readonly touches?: readonly SemanticRegion[] }
123
+ | { readonly op: "addEvidence"; readonly evidence: EvidenceRecord; readonly touches?: readonly SemanticRegion[] };
124
+
125
+ export interface EvidenceRecord {
126
+ readonly id: string;
127
+ readonly kind: "typecheck" | "test" | "replay" | "proof" | "trace" | "review" | "note";
128
+ readonly status: "passed" | "failed" | "unknown";
129
+ readonly path?: string;
130
+ readonly summary?: string;
131
+ readonly metadata?: JsonObject;
132
+ }
133
+
134
+ export interface SemanticPatchBundle {
135
+ readonly kind: "frontier.lang.patch";
136
+ readonly version: 1;
137
+ readonly id: string;
138
+ readonly baseHash?: string;
139
+ readonly targetHash?: string;
140
+ readonly author?: string;
141
+ readonly risk?: "low" | "medium" | "high" | "unknown";
142
+ readonly operations: readonly SemanticPatchOperation[];
143
+ readonly evidence?: readonly EvidenceRecord[];
144
+ readonly metadata?: JsonObject;
145
+ }
146
+
147
+ export interface ReplayEvent {
148
+ readonly id: string;
149
+ readonly at?: string;
150
+ readonly actor?: string;
151
+ readonly patch: SemanticPatchBundle;
152
+ }
153
+
154
+ export type MergeStatus =
155
+ | "safe-by-disjoint-region"
156
+ | "safe-by-same-change"
157
+ | "safe-by-merge-law"
158
+ | "conflict-by-overlap"
159
+ | "conflict-by-effect-overlap"
160
+ | "unknown-by-dynamic-effect"
161
+ | "unknown-needs-review";
162
+
163
+ export interface MergeAdmission {
164
+ readonly status: MergeStatus;
165
+ readonly autoMergeable: boolean;
166
+ readonly reasons: readonly string[];
167
+ readonly overlappingNodeIds: readonly SemanticId[];
168
+ readonly overlappingRegions: readonly string[];
169
+ readonly overlappingEffects: readonly string[];
170
+ readonly evidence: readonly EvidenceRecord[];
171
+ }
172
+
173
+ export interface EmitTypeScriptOptions {
174
+ readonly banner?: string;
175
+ readonly includeRuntimeTypes?: boolean;
176
+ }
177
+
178
+ export declare function moduleNode(input: Omit<ModuleNode, "kind">): ModuleNode;
179
+ export declare function entityNode(input: Omit<EntityNode, "kind">): EntityNode;
180
+ export declare function stateNode(input: Omit<StateNode, "kind">): StateNode;
181
+ export declare function actionNode(input: Omit<ActionNode, "kind">): ActionNode;
182
+ export declare function viewNode(input: Omit<ViewNode, "kind">): ViewNode;
183
+ export declare function migrationNode(input: Omit<MigrationNode, "kind">): MigrationNode;
184
+ export declare function effectNode(input: Omit<EffectNode, "kind">): EffectNode;
185
+ export declare function targetNode(input: Omit<TargetNode, "kind">): TargetNode;
186
+ export declare function createPatch(input: Omit<SemanticPatchBundle, "kind" | "version">): SemanticPatchBundle;
187
+ export declare function createDocument(input: {
188
+ readonly id: SemanticId;
189
+ readonly name: string;
190
+ readonly nodes: readonly SemanticNode[];
191
+ readonly rootIds?: readonly SemanticId[];
192
+ readonly history?: readonly ReplayEvent[];
193
+ readonly metadata?: JsonObject;
194
+ }): FrontierLangDocument;
195
+ export declare function stableStringify(value: unknown): string;
196
+ export declare function hashSemanticValue(value: unknown): string;
197
+ export declare function hashDocumentBase(document: FrontierLangDocument): string;
198
+ export declare function validateDocument(document: FrontierLangDocument): readonly string[];
199
+ export declare function applySemanticPatch(document: FrontierLangDocument, patch: SemanticPatchBundle, event?: ReplayEvent): FrontierLangDocument;
200
+ export declare function replayDocument(initial: FrontierLangDocument, events: readonly ReplayEvent[]): FrontierLangDocument;
201
+ export declare function classifyMerge(base: FrontierLangDocument, left: SemanticPatchBundle, right: SemanticPatchBundle): MergeAdmission;
202
+ export declare function emitTypeScript(document: FrontierLangDocument, options?: EmitTypeScriptOptions): string;
package/dist/index.js ADDED
@@ -0,0 +1,608 @@
1
+ export function moduleNode(input) {
2
+ return { ...input, kind: "module" };
3
+ }
4
+
5
+ export function entityNode(input) {
6
+ return { ...input, kind: "entity" };
7
+ }
8
+
9
+ export function stateNode(input) {
10
+ return { ...input, kind: "state" };
11
+ }
12
+
13
+ export function actionNode(input) {
14
+ return { ...input, kind: "action" };
15
+ }
16
+
17
+ export function viewNode(input) {
18
+ return { ...input, kind: "view" };
19
+ }
20
+
21
+ export function migrationNode(input) {
22
+ return { ...input, kind: "migration" };
23
+ }
24
+
25
+ export function effectNode(input) {
26
+ return { ...input, kind: "effect" };
27
+ }
28
+
29
+ export function targetNode(input) {
30
+ return { ...input, kind: "target" };
31
+ }
32
+
33
+ export function createPatch(input) {
34
+ return {
35
+ ...input,
36
+ kind: "frontier.lang.patch",
37
+ version: 1
38
+ };
39
+ }
40
+
41
+ export function createDocument(input) {
42
+ const nodes = {};
43
+ for (const node of input.nodes) {
44
+ if (nodes[node.id]) {
45
+ throw new Error(`Duplicate semantic node id: ${node.id}`);
46
+ }
47
+ nodes[node.id] = node;
48
+ }
49
+
50
+ return {
51
+ kind: "frontier.lang.document",
52
+ version: 1,
53
+ id: input.id,
54
+ name: input.name,
55
+ rootIds: input.rootIds ?? input.nodes.filter((node) => !node.parentId).map((node) => node.id),
56
+ nodes,
57
+ history: input.history,
58
+ metadata: input.metadata
59
+ };
60
+ }
61
+
62
+ export function stableStringify(value) {
63
+ if (value === null || typeof value !== "object") {
64
+ return JSON.stringify(value);
65
+ }
66
+
67
+ if (Array.isArray(value)) {
68
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
69
+ }
70
+
71
+ const entries = Object.entries(value)
72
+ .filter(([, item]) => item !== undefined)
73
+ .sort(([left], [right]) => ordinalCompare(left, right));
74
+
75
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`;
76
+ }
77
+
78
+ export function hashSemanticValue(value) {
79
+ const serialized = stableStringify(value);
80
+ let hash = 0x811c9dc5;
81
+ for (let index = 0; index < serialized.length; index += 1) {
82
+ hash ^= serialized.charCodeAt(index);
83
+ hash = Math.imul(hash, 0x01000193);
84
+ }
85
+ return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}`;
86
+ }
87
+
88
+ export function hashDocumentBase(document) {
89
+ return hashSemanticValue(stripHistory(document));
90
+ }
91
+
92
+ export function validateDocument(document) {
93
+ const issues = [];
94
+ const rootSet = new Set();
95
+
96
+ for (const rootId of document.rootIds) {
97
+ if (rootSet.has(rootId)) {
98
+ issues.push(`Duplicate root node: ${rootId}`);
99
+ }
100
+ rootSet.add(rootId);
101
+ if (!document.nodes[rootId]) {
102
+ issues.push(`Missing root node: ${rootId}`);
103
+ }
104
+ }
105
+
106
+ for (const [nodeId, node] of Object.entries(document.nodes)) {
107
+ if (node.id !== nodeId) {
108
+ issues.push(`Node record key ${nodeId} does not match node id ${node.id}`);
109
+ }
110
+
111
+ if (node.parentId && !document.nodes[node.parentId]) {
112
+ issues.push(`Node ${node.id} references missing parent ${node.parentId}`);
113
+ }
114
+
115
+ if (node.parentId === node.id) {
116
+ issues.push(`Node ${node.id} cannot be its own parent`);
117
+ }
118
+
119
+ if (!node.parentId && !rootSet.has(node.id)) {
120
+ issues.push(`Parentless node ${node.id} is missing from rootIds`);
121
+ }
122
+
123
+ if (node.parentId && rootSet.has(node.id)) {
124
+ issues.push(`Node ${node.id} has a parent and cannot be a root`);
125
+ }
126
+
127
+ if (hasAncestorCycle(document.nodes, node.id)) {
128
+ issues.push(`Node ${node.id} is part of a parent cycle`);
129
+ }
130
+
131
+ if (node.kind === "entity") {
132
+ const fieldIds = new Set();
133
+ for (const field of node.fields) {
134
+ if (fieldIds.has(field.id)) {
135
+ issues.push(`Entity ${node.id} has duplicate field id ${field.id}`);
136
+ }
137
+ fieldIds.add(field.id);
138
+ }
139
+ }
140
+ }
141
+
142
+ return issues;
143
+ }
144
+
145
+ export function applySemanticPatch(document, patch, event) {
146
+ if (patch.baseHash && patch.baseHash !== hashDocumentBase(document)) {
147
+ throw new Error(`Patch ${patch.id} base hash does not match document`);
148
+ }
149
+
150
+ let nodes = { ...document.nodes };
151
+ let rootIds = [...document.rootIds];
152
+
153
+ for (const operation of patch.operations) {
154
+ switch (operation.op) {
155
+ case "upsertNode": {
156
+ nodes = { ...nodes, [operation.node.id]: operation.node };
157
+ if (!operation.node.parentId && !rootIds.includes(operation.node.id)) {
158
+ rootIds = [...rootIds, operation.node.id];
159
+ }
160
+ break;
161
+ }
162
+ case "removeNode": {
163
+ const nextNodes = { ...nodes };
164
+ delete nextNodes[operation.id];
165
+ nodes = nextNodes;
166
+ rootIds = rootIds.filter((id) => id !== operation.id);
167
+ break;
168
+ }
169
+ case "renameNode": {
170
+ const node = requireNode(nodes, operation.id);
171
+ nodes = { ...nodes, [operation.id]: { ...node, name: operation.name } };
172
+ break;
173
+ }
174
+ case "moveNode": {
175
+ const node = requireNode(nodes, operation.id);
176
+ nodes = { ...nodes, [operation.id]: { ...node, parentId: operation.parentId } };
177
+ rootIds = operation.parentId
178
+ ? rootIds.filter((id) => id !== operation.id)
179
+ : unique([...rootIds, operation.id]);
180
+ break;
181
+ }
182
+ case "updateNode": {
183
+ if (
184
+ Object.hasOwn(operation.set, "id") ||
185
+ Object.hasOwn(operation.set, "kind") ||
186
+ Object.hasOwn(operation.set, "parentId")
187
+ ) {
188
+ throw new Error(`Patch ${patch.id} cannot update semantic node identity, kind, or parent`);
189
+ }
190
+ const node = requireNode(nodes, operation.id);
191
+ nodes = { ...nodes, [operation.id]: { ...node, ...operation.set } };
192
+ break;
193
+ }
194
+ case "addEvidence":
195
+ break;
196
+ default:
197
+ throw new Error(`Unexpected operation: ${operation.op}`);
198
+ }
199
+ }
200
+
201
+ const next = {
202
+ ...document,
203
+ rootIds,
204
+ nodes
205
+ };
206
+
207
+ const issues = validateDocument(next);
208
+ if (issues.length > 0) {
209
+ throw new Error(`Invalid document after patch ${patch.id}: ${issues.join("; ")}`);
210
+ }
211
+
212
+ if (patch.targetHash && patch.targetHash !== hashDocumentBase(next)) {
213
+ throw new Error(`Patch ${patch.id} target hash does not match result`);
214
+ }
215
+
216
+ return {
217
+ ...next,
218
+ history: [
219
+ ...(document.history ?? []),
220
+ event ?? {
221
+ id: `event:${patch.id}`,
222
+ patch
223
+ }
224
+ ]
225
+ };
226
+ }
227
+
228
+ export function replayDocument(initial, events) {
229
+ return events.reduce((document, event) => applySemanticPatch(document, event.patch, event), initial);
230
+ }
231
+
232
+ export function classifyMerge(base, left, right) {
233
+ const baseHash = hashDocumentBase(base);
234
+ const reasons = [];
235
+
236
+ if (left.baseHash && left.baseHash !== baseHash) {
237
+ reasons.push(`Left patch base hash ${left.baseHash} does not match current base ${baseHash}`);
238
+ }
239
+ if (right.baseHash && right.baseHash !== baseHash) {
240
+ reasons.push(`Right patch base hash ${right.baseHash} does not match current base ${baseHash}`);
241
+ }
242
+
243
+ const leftSummary = summarizePatch(left);
244
+ const rightSummary = summarizePatch(right);
245
+ const overlappingNodeIds = intersection(leftSummary.nodeIds, rightSummary.nodeIds);
246
+ const overlappingRegions = intersection(leftSummary.regions, rightSummary.regions);
247
+ const overlappingEffects = intersection(leftSummary.effects, rightSummary.effects);
248
+ const evidence = [...(left.evidence ?? []), ...(right.evidence ?? [])];
249
+ const failedEvidence = evidence.filter((record) => record.status === "failed");
250
+
251
+ if (reasons.length > 0) {
252
+ return {
253
+ status: "unknown-needs-review",
254
+ autoMergeable: false,
255
+ reasons,
256
+ overlappingNodeIds,
257
+ overlappingRegions,
258
+ overlappingEffects,
259
+ evidence
260
+ };
261
+ }
262
+
263
+ if (failedEvidence.length > 0) {
264
+ return {
265
+ status: "unknown-needs-review",
266
+ autoMergeable: false,
267
+ reasons: [`Failed evidence prevents auto-merge: ${failedEvidence.map((record) => record.id).join(", ")}`],
268
+ overlappingNodeIds,
269
+ overlappingRegions,
270
+ overlappingEffects,
271
+ evidence
272
+ };
273
+ }
274
+
275
+ if (stableStringify(left.operations) === stableStringify(right.operations)) {
276
+ return {
277
+ status: "safe-by-same-change",
278
+ autoMergeable: true,
279
+ reasons: ["Both patches contain the same semantic operations."],
280
+ overlappingNodeIds,
281
+ overlappingRegions,
282
+ overlappingEffects,
283
+ evidence
284
+ };
285
+ }
286
+
287
+ const dynamicEffects = new Set(["dynamic", "eval", "unsafeEval", "ffi", "reflection", "proxy"]);
288
+ const hasDynamic = [...leftSummary.effects, ...rightSummary.effects].some((effect) =>
289
+ dynamicEffects.has(effect)
290
+ );
291
+ if (hasDynamic) {
292
+ return {
293
+ status: "unknown-by-dynamic-effect",
294
+ autoMergeable: false,
295
+ reasons: ["At least one patch touches a dynamic or opaque effect boundary."],
296
+ overlappingNodeIds,
297
+ overlappingRegions,
298
+ overlappingEffects,
299
+ evidence
300
+ };
301
+ }
302
+
303
+ if (overlappingEffects.length > 0) {
304
+ return {
305
+ status: "conflict-by-effect-overlap",
306
+ autoMergeable: false,
307
+ reasons: [`Both patches touch effect(s): ${overlappingEffects.join(", ")}`],
308
+ overlappingNodeIds,
309
+ overlappingRegions,
310
+ overlappingEffects,
311
+ evidence
312
+ };
313
+ }
314
+
315
+ if (overlappingNodeIds.length > 0 || overlappingRegions.length > 0) {
316
+ const laws = collectMergeLaws(base, [...overlappingNodeIds, ...overlappingRegions]);
317
+ if (laws.length > 0 && laws.every((law) => law === "semilattice" || law === "commutative")) {
318
+ return withReplayGate(base, left, right, {
319
+ status: "safe-by-merge-law",
320
+ autoMergeable: true,
321
+ reasons: [`Overlaps are covered by merge law(s): ${unique(laws).join(", ")}`],
322
+ overlappingNodeIds,
323
+ overlappingRegions,
324
+ overlappingEffects,
325
+ evidence
326
+ });
327
+ }
328
+
329
+ return {
330
+ status: "conflict-by-overlap",
331
+ autoMergeable: false,
332
+ reasons: ["Patches touch the same semantic node or region without a known merge law."],
333
+ overlappingNodeIds,
334
+ overlappingRegions,
335
+ overlappingEffects,
336
+ evidence
337
+ };
338
+ }
339
+
340
+ return withReplayGate(base, left, right, {
341
+ status: "safe-by-disjoint-region",
342
+ autoMergeable: true,
343
+ reasons: ["Patches touch disjoint semantic nodes, regions, and effects."],
344
+ overlappingNodeIds,
345
+ overlappingRegions,
346
+ overlappingEffects,
347
+ evidence
348
+ });
349
+ }
350
+
351
+ export function emitTypeScript(document, options = {}) {
352
+ const lines = [];
353
+ const banner = options.banner ?? "Generated by @shapeshift-labs/frontier-lang.";
354
+ lines.push(`// ${banner}`);
355
+ lines.push("");
356
+
357
+ if (options.includeRuntimeTypes ?? true) {
358
+ lines.push("export type FrontierPatchOperation =");
359
+ lines.push(" | { op: 'set'; path: string; value: unknown }");
360
+ lines.push(" | { op: 'remove'; path: string }");
361
+ lines.push(" | { op: 'insert'; path: string; value: unknown }");
362
+ lines.push(" | { op: 'merge'; path: string; value: unknown };");
363
+ lines.push("");
364
+ }
365
+
366
+ for (const node of Object.values(document.nodes)) {
367
+ if (node.kind === "entity") {
368
+ lines.push(`export interface ${safeIdentifier(node.name)} {`);
369
+ for (const field of node.fields) {
370
+ lines.push(` ${safeIdentifier(field.name)}: ${toTypeScriptType(field.type)};`);
371
+ }
372
+ lines.push("}");
373
+ lines.push("");
374
+ }
375
+ }
376
+
377
+ for (const node of Object.values(document.nodes)) {
378
+ if (node.kind === "action") {
379
+ const inputType = node.input ? toTypeScriptType(node.input) : "unknown";
380
+ const returnType = node.returns ? toTypeScriptType(node.returns) : "FrontierPatchOperation[]";
381
+ lines.push(`export function ${safeIdentifier(node.name)}(`);
382
+ lines.push(" state: unknown,");
383
+ lines.push(` input: ${inputType},`);
384
+ lines.push(" env: Record<string, unknown> = {}");
385
+ lines.push(`): ${returnType} {`);
386
+ lines.push(" void state;");
387
+ lines.push(" void input;");
388
+ lines.push(" void env;");
389
+ lines.push(` return [] as ${returnType};`);
390
+ lines.push("}");
391
+ lines.push("");
392
+ }
393
+ }
394
+
395
+ return `${lines.join("\n").trimEnd()}\n`;
396
+ }
397
+
398
+ function stripHistory(document) {
399
+ const { history: _history, ...rest } = document;
400
+ return rest;
401
+ }
402
+
403
+ function requireNode(nodes, id) {
404
+ const node = nodes[id];
405
+ if (!node) {
406
+ throw new Error(`Unknown semantic node id: ${id}`);
407
+ }
408
+ return node;
409
+ }
410
+
411
+ function hasAncestorCycle(nodes, startId) {
412
+ const seen = new Set();
413
+ let current = nodes[startId];
414
+ while (current?.parentId) {
415
+ if (seen.has(current.parentId)) {
416
+ return true;
417
+ }
418
+ seen.add(current.parentId);
419
+ current = nodes[current.parentId];
420
+ }
421
+ return false;
422
+ }
423
+
424
+ function summarizePatch(patch) {
425
+ const nodeIds = [];
426
+ const regions = [];
427
+ const effects = [];
428
+
429
+ for (const operation of patch.operations) {
430
+ if ("id" in operation && typeof operation.id === "string") {
431
+ nodeIds.push(operation.id);
432
+ }
433
+ if (operation.op === "upsertNode") {
434
+ nodeIds.push(operation.node.id);
435
+ for (const region of operation.node.regions ?? []) {
436
+ regions.push(region.id);
437
+ }
438
+ if (operation.node.kind === "action") {
439
+ effects.push(...(operation.node.uses ?? []));
440
+ }
441
+ if (operation.node.kind === "effect") {
442
+ effects.push(operation.node.capability);
443
+ }
444
+ }
445
+ if (operation.op === "updateNode") {
446
+ if (Array.isArray(operation.set.uses)) {
447
+ effects.push(...operation.set.uses.filter((value) => typeof value === "string"));
448
+ }
449
+ if (typeof operation.set.capability === "string") {
450
+ effects.push(operation.set.capability);
451
+ }
452
+ if (Array.isArray(operation.set.regions)) {
453
+ for (const region of operation.set.regions) {
454
+ if (region && typeof region === "object" && region.access === "effect" && typeof region.id === "string") {
455
+ effects.push(region.id);
456
+ }
457
+ }
458
+ }
459
+ }
460
+
461
+ for (const region of operation.touches ?? []) {
462
+ regions.push(region.id);
463
+ if (region.access === "effect") {
464
+ effects.push(region.id);
465
+ }
466
+ }
467
+ }
468
+
469
+ return {
470
+ nodeIds: unique(nodeIds),
471
+ regions: unique(regions),
472
+ effects: unique(effects)
473
+ };
474
+ }
475
+
476
+ function withReplayGate(base, left, right, admission) {
477
+ if (!admission.autoMergeable) {
478
+ return admission;
479
+ }
480
+
481
+ try {
482
+ const leftForReplay = stripPatchAdmissionHashes(left);
483
+ const rightForReplay = stripPatchAdmissionHashes(right);
484
+ const leftThenRight = applySemanticPatch(applySemanticPatch(base, leftForReplay), rightForReplay);
485
+ const rightThenLeft = applySemanticPatch(applySemanticPatch(base, rightForReplay), leftForReplay);
486
+ const leftHash = hashDocumentBase(leftThenRight);
487
+ const rightHash = hashDocumentBase(rightThenLeft);
488
+ if (leftHash !== rightHash) {
489
+ return {
490
+ ...admission,
491
+ status: "unknown-needs-review",
492
+ autoMergeable: false,
493
+ reasons: [
494
+ ...admission.reasons,
495
+ `Replay order is not commutative: left-right ${leftHash}, right-left ${rightHash}`
496
+ ]
497
+ };
498
+ }
499
+ return admission;
500
+ } catch (error) {
501
+ return {
502
+ ...admission,
503
+ status: "unknown-needs-review",
504
+ autoMergeable: false,
505
+ reasons: [...admission.reasons, `Replay gate failed: ${error instanceof Error ? error.message : String(error)}`]
506
+ };
507
+ }
508
+ }
509
+
510
+ function stripPatchAdmissionHashes(patch) {
511
+ const { baseHash: _baseHash, targetHash: _targetHash, ...rest } = patch;
512
+ return rest;
513
+ }
514
+
515
+ function collectMergeLaws(document, overlaps) {
516
+ const overlapSet = new Set(overlaps);
517
+ const laws = [];
518
+
519
+ for (const node of Object.values(document.nodes)) {
520
+ if (node.kind === "entity") {
521
+ for (const field of node.fields) {
522
+ if (overlapSet.has(field.id) || overlapSet.has(`${node.name}.${field.name}`)) {
523
+ if (field.merge?.law) {
524
+ laws.push(field.merge.law);
525
+ }
526
+ }
527
+ }
528
+ }
529
+ if (node.kind === "state") {
530
+ for (const collection of node.collections) {
531
+ if (overlapSet.has(collection.id) || overlapSet.has(`${node.name}.${collection.name}`)) {
532
+ if (collection.merge?.law) {
533
+ laws.push(collection.merge.law);
534
+ }
535
+ }
536
+ }
537
+ }
538
+ }
539
+
540
+ return laws;
541
+ }
542
+
543
+ function intersection(left, right) {
544
+ const rightSet = new Set(right);
545
+ return unique(left.filter((item) => rightSet.has(item)));
546
+ }
547
+
548
+ function unique(items) {
549
+ return [...new Set(items)];
550
+ }
551
+
552
+ function ordinalCompare(left, right) {
553
+ if (left < right) {
554
+ return -1;
555
+ }
556
+ if (left > right) {
557
+ return 1;
558
+ }
559
+ return 0;
560
+ }
561
+
562
+ function safeIdentifier(name) {
563
+ const identifier = name.replace(/[^A-Za-z0-9_$]/g, "_");
564
+ if (/^[A-Za-z_$]/.test(identifier)) {
565
+ return identifier;
566
+ }
567
+ return `_${identifier}`;
568
+ }
569
+
570
+ function toTypeScriptType(type) {
571
+ const primitive = {
572
+ Text: "string",
573
+ String: "string",
574
+ Bool: "boolean",
575
+ Boolean: "boolean",
576
+ Int: "number",
577
+ Float: "number",
578
+ Number: "number",
579
+ Instant: "string",
580
+ Json: "unknown",
581
+ Patch: "FrontierPatchOperation[]"
582
+ };
583
+
584
+ if (primitive[type]) {
585
+ return primitive[type];
586
+ }
587
+
588
+ const setMatch = /^Set<(.+)>$/.exec(type);
589
+ if (setMatch) {
590
+ return `ReadonlySet<${toTypeScriptType(setMatch[1].trim())}>`;
591
+ }
592
+
593
+ const listMatch = /^(List|MoveList)<(.+)>$/.exec(type);
594
+ if (listMatch) {
595
+ return `readonly ${toTypeScriptType(listMatch[2].trim())}[]`;
596
+ }
597
+
598
+ const mapMatch = /^Map<(.+),(.+)>$/.exec(type);
599
+ if (mapMatch) {
600
+ return `ReadonlyMap<${toTypeScriptType(mapMatch[1].trim())}, ${toTypeScriptType(mapMatch[2].trim())}>`;
601
+ }
602
+
603
+ if (/^\{.*\}$/.test(type)) {
604
+ return "Record<string, unknown>";
605
+ }
606
+
607
+ return safeIdentifier(type);
608
+ }
@@ -0,0 +1,83 @@
1
+ import {
2
+ actionNode,
3
+ classifyMerge,
4
+ createDocument,
5
+ createPatch,
6
+ emitTypeScript,
7
+ entityNode,
8
+ hashDocumentBase,
9
+ stateNode
10
+ } from "../dist/index.js";
11
+
12
+ const todo = entityNode({
13
+ id: "ent_todo",
14
+ name: "Todo",
15
+ fields: [
16
+ { id: "field_todo_id", name: "id", type: "TodoId", key: true },
17
+ { id: "field_todo_title", name: "title", type: "Text", merge: { kind: "conflict" } },
18
+ {
19
+ id: "field_todo_tags",
20
+ name: "tags",
21
+ type: "Set<Text>",
22
+ merge: { kind: "union", law: "semilattice" }
23
+ }
24
+ ]
25
+ });
26
+
27
+ const state = stateNode({
28
+ id: "state_todo",
29
+ name: "TodoDb",
30
+ collections: [
31
+ {
32
+ id: "collection_todos",
33
+ name: "todos",
34
+ type: "Map<TodoId, Todo>",
35
+ merge: { kind: "byKey", law: "commutative" }
36
+ }
37
+ ]
38
+ });
39
+
40
+ const addTodo = actionNode({
41
+ id: "action_add_todo",
42
+ name: "addTodo",
43
+ input: "{ title: Text }",
44
+ returns: "Patch",
45
+ reads: ["TodoDb.todos"],
46
+ writes: ["TodoDb.todos"],
47
+ uses: ["Clock"]
48
+ });
49
+
50
+ const document = createDocument({
51
+ id: "mod_todo",
52
+ name: "TodoApp",
53
+ nodes: [todo, state, addTodo]
54
+ });
55
+
56
+ const baseHash = hashDocumentBase(document);
57
+ const left = createPatch({
58
+ id: "patch_left_tags",
59
+ baseHash,
60
+ operations: [
61
+ {
62
+ op: "updateNode",
63
+ id: "ent_todo",
64
+ set: { metadata: { left: true } },
65
+ touches: [{ id: "field_todo_tags", access: "write" }]
66
+ }
67
+ ]
68
+ });
69
+ const right = createPatch({
70
+ id: "patch_right_tags",
71
+ baseHash,
72
+ operations: [
73
+ {
74
+ op: "updateNode",
75
+ id: "ent_todo",
76
+ set: { metadata: { right: true } },
77
+ touches: [{ id: "field_todo_tags", access: "write" }]
78
+ }
79
+ ]
80
+ });
81
+
82
+ console.log(classifyMerge(document, left, right));
83
+ console.log(emitTypeScript(document));
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@shapeshift-labs/frontier-lang",
3
+ "version": "0.1.0",
4
+ "description": "Patch-native semantic source kernel for replayable programs, merge admission, and generated TypeScript projections.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "examples",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "node scripts/build.mjs",
23
+ "test": "npm run build && node test/smoke.mjs",
24
+ "prepare": "npm run build",
25
+ "prepack": "npm test"
26
+ },
27
+ "keywords": [
28
+ "frontier",
29
+ "semantic-merge",
30
+ "patch",
31
+ "replay",
32
+ "compiler",
33
+ "typescript"
34
+ ],
35
+ "author": "ShapeShift Labs",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/siliconjungle/-shapeshift-labs-frontier-lang.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/siliconjungle/-shapeshift-labs-frontier-lang/issues"
43
+ },
44
+ "homepage": "https://github.com/siliconjungle/-shapeshift-labs-frontier-lang#readme",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }