@shapeshift-labs/frontier-lang 0.1.0 → 0.2.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/README.md +46 -159
- package/dist/index.d.ts +4 -202
- package/dist/index.js +4 -608
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
# `@shapeshift-labs/frontier-lang`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Umbrella package for the Frontier Lang package family.
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
5
|
+
Frontier Lang is a patch-native semantic programming model: source is a replayable semantic graph, edits are structured patches, merge admission is evidence-aware, and generated code is a projection from that semantic state.
|
|
10
6
|
|
|
11
7
|
## Install
|
|
12
8
|
|
|
@@ -14,192 +10,83 @@ The package does not try to be a full programming language yet. It provides the
|
|
|
14
10
|
npm install @shapeshift-labs/frontier-lang
|
|
15
11
|
```
|
|
16
12
|
|
|
17
|
-
The package
|
|
13
|
+
The root package re-exports the browser-safe runtime packages:
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
- `@shapeshift-labs/frontier-lang-kernel`: semantic document graph, patches, replay, hashing, and merge admission.
|
|
16
|
+
- `@shapeshift-labs/frontier-lang-parser`: first `.frontier` syntax slice.
|
|
17
|
+
- `@shapeshift-labs/frontier-lang-checker`: diagnostics for documents and patch evidence.
|
|
18
|
+
- `@shapeshift-labs/frontier-lang-typescript`: TypeScript projection adapter.
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
The Node CLI is intentionally separate:
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
```sh
|
|
23
|
+
npm install -g @shapeshift-labs/frontier-lang-cli
|
|
24
|
+
frontier-lang check examples/todo.frontier
|
|
25
|
+
```
|
|
24
26
|
|
|
25
27
|
## Example
|
|
26
28
|
|
|
27
|
-
```
|
|
29
|
+
```js
|
|
28
30
|
import {
|
|
29
|
-
|
|
31
|
+
checkDocument,
|
|
30
32
|
classifyMerge,
|
|
31
|
-
createDocument,
|
|
32
33
|
createPatch,
|
|
33
34
|
emitTypeScript,
|
|
34
|
-
entityNode,
|
|
35
35
|
hashDocumentBase,
|
|
36
|
-
|
|
36
|
+
parseFrontierSource
|
|
37
37
|
} from "@shapeshift-labs/frontier-lang";
|
|
38
38
|
|
|
39
|
-
const
|
|
40
|
-
|
|
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
|
-
});
|
|
39
|
+
const document = parseFrontierSource(`
|
|
40
|
+
module TodoApp @id("mod_todo")
|
|
76
41
|
|
|
77
|
-
|
|
78
|
-
id:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
42
|
+
entity Todo @id("ent_todo") {
|
|
43
|
+
title @id("field_title"): Text {
|
|
44
|
+
merge conflict
|
|
45
|
+
}
|
|
46
|
+
tags @id("field_tags"): Set<Text> {
|
|
47
|
+
merge union law semilattice
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
`);
|
|
82
51
|
|
|
83
52
|
const baseHash = hashDocumentBase(document);
|
|
84
|
-
|
|
85
53
|
const left = createPatch({
|
|
86
|
-
id: "
|
|
54
|
+
id: "left",
|
|
87
55
|
baseHash,
|
|
88
56
|
operations: [
|
|
89
57
|
{
|
|
90
|
-
op: "
|
|
91
|
-
id: "
|
|
92
|
-
|
|
93
|
-
touches: [{ id: "field_todo_tags", access: "write" }]
|
|
58
|
+
op: "addEvidence",
|
|
59
|
+
evidence: { id: "left-test", kind: "test", status: "passed" },
|
|
60
|
+
touches: [{ id: "field_tags", access: "write" }]
|
|
94
61
|
}
|
|
95
|
-
]
|
|
96
|
-
evidence: [{ id: "test_tags", kind: "test", status: "passed" }]
|
|
62
|
+
]
|
|
97
63
|
});
|
|
98
|
-
|
|
99
64
|
const right = createPatch({
|
|
100
|
-
id: "
|
|
65
|
+
id: "right",
|
|
101
66
|
baseHash,
|
|
102
67
|
operations: [
|
|
103
68
|
{
|
|
104
|
-
op: "
|
|
105
|
-
id: "
|
|
106
|
-
|
|
107
|
-
touches: [{ id: "field_todo_tags", access: "write" }]
|
|
69
|
+
op: "addEvidence",
|
|
70
|
+
evidence: { id: "right-test", kind: "test", status: "passed" },
|
|
71
|
+
touches: [{ id: "field_tags", access: "write" }]
|
|
108
72
|
}
|
|
109
|
-
]
|
|
110
|
-
evidence: [{ id: "test_more_tags", kind: "test", status: "passed" }]
|
|
73
|
+
]
|
|
111
74
|
});
|
|
112
75
|
|
|
76
|
+
console.log(checkDocument(document).ok);
|
|
113
77
|
console.log(classifyMerge(document, left, right).status);
|
|
114
|
-
// safe-by-merge-law
|
|
115
|
-
|
|
116
78
|
console.log(emitTypeScript(document));
|
|
117
79
|
```
|
|
118
80
|
|
|
119
|
-
##
|
|
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
|
-
```
|
|
81
|
+
## Package Shape
|
|
200
82
|
|
|
201
|
-
The
|
|
83
|
+
The split packages keep package boundaries explicit:
|
|
202
84
|
|
|
203
|
-
|
|
85
|
+
- Kernel stays runtime-neutral and dependency-light.
|
|
86
|
+
- Parser depends on kernel.
|
|
87
|
+
- Checker depends on kernel.
|
|
88
|
+
- TypeScript projection depends on kernel.
|
|
89
|
+
- CLI depends on all of the above and owns Node filesystem/bin behavior.
|
|
90
|
+
- Umbrella depends on the browser-safe packages and re-exports them for convenience.
|
|
204
91
|
|
|
205
|
-
|
|
92
|
+
This keeps JavaScript/TypeScript useful as projection targets without making generated code the canonical source model.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,202 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
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;
|
|
1
|
+
export * from "@shapeshift-labs/frontier-lang-kernel";
|
|
2
|
+
export * from "@shapeshift-labs/frontier-lang-parser";
|
|
3
|
+
export * from "@shapeshift-labs/frontier-lang-checker";
|
|
4
|
+
export * from "@shapeshift-labs/frontier-lang-typescript";
|
package/dist/index.js
CHANGED
|
@@ -1,608 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
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
|
-
}
|
|
1
|
+
export * from "@shapeshift-labs/frontier-lang-kernel";
|
|
2
|
+
export * from "@shapeshift-labs/frontier-lang-parser";
|
|
3
|
+
export * from "@shapeshift-labs/frontier-lang-checker";
|
|
4
|
+
export * from "@shapeshift-labs/frontier-lang-typescript";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shapeshift-labs/frontier-lang",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Umbrella package for Frontier Lang kernel, parser, checker, and TypeScript projection packages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
"patch",
|
|
31
31
|
"replay",
|
|
32
32
|
"compiler",
|
|
33
|
-
"typescript"
|
|
33
|
+
"typescript",
|
|
34
|
+
"parser",
|
|
35
|
+
"checker"
|
|
34
36
|
],
|
|
35
37
|
"author": "ShapeShift Labs",
|
|
36
38
|
"license": "MIT",
|
|
@@ -44,5 +46,11 @@
|
|
|
44
46
|
"homepage": "https://github.com/siliconjungle/-shapeshift-labs-frontier-lang#readme",
|
|
45
47
|
"publishConfig": {
|
|
46
48
|
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@shapeshift-labs/frontier-lang-kernel": "^0.1.1",
|
|
52
|
+
"@shapeshift-labs/frontier-lang-parser": "^0.1.0",
|
|
53
|
+
"@shapeshift-labs/frontier-lang-checker": "^0.1.0",
|
|
54
|
+
"@shapeshift-labs/frontier-lang-typescript": "^0.1.0"
|
|
47
55
|
}
|
|
48
56
|
}
|