@shapeshift-labs/frontier-lang-parser 0.3.18 → 0.3.19
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 +23 -0
- package/dist/decision-graph.js +239 -0
- package/dist/index.js +5 -2
- package/dist/metadata.js +45 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -220,6 +220,29 @@ npm install @shapeshift-labs/frontier-lang-parser
|
|
|
220
220
|
|
|
221
221
|
The parser projects text into `@shapeshift-labs/frontier-lang-kernel` documents. The syntax is intentionally small and experimental.
|
|
222
222
|
|
|
223
|
+
## Authored decision graph syntax
|
|
224
|
+
|
|
225
|
+
`.frontier` files can carry semantic merge admission evidence directly in `decisionGraph` or `admissionGraph` blocks. These blocks preserve the causal review shape around a candidate edit: semantic changes, gates, evidence records, patch events, admission decisions, merge decisions, graph nodes, and graph edges.
|
|
226
|
+
|
|
227
|
+
```frontier
|
|
228
|
+
decisionGraph TodoAdmission @id("decision_graph_todo") {
|
|
229
|
+
graphKind semantic-merge-admission
|
|
230
|
+
scope mod_todo
|
|
231
|
+
root merge_decision_title
|
|
232
|
+
subject action_add,field_title
|
|
233
|
+
node change @id("decision_node_change") kind semantic-change record semantic_change_title label "Title change" status passed
|
|
234
|
+
edge changeToGate @id("decision_edge_change_gate") from semantic_change_title to gate_typecheck kind gates status passed
|
|
235
|
+
change title @id("semantic_change_title") kind source-edit language typescript sourcePath src/todo.ts semanticNode field_title semanticSymbol symbol:Todo.title evidence evidence_typecheck
|
|
236
|
+
gate typecheck @id("gate_typecheck") kind typecheck status passed required command "npm run typecheck" semanticChange semantic_change_title evidence evidence_typecheck
|
|
237
|
+
evidence typecheck @id("evidence_typecheck") kind test status passed path reports/typecheck.json gate gate_typecheck semanticChange semantic_change_title
|
|
238
|
+
patchEvent workerPatch @id("patch_event_worker") patch patch_worker status passed baseHash h_base targetHash h_worker semanticChange semantic_change_title gate gate_typecheck evidence evidence_typecheck deterministic
|
|
239
|
+
admission titleSafe @id("admission_title_safe") candidate candidate_todo_title semanticChange semantic_change_title classification safe decision merge autoMergeable gate gate_typecheck evidence evidence_typecheck
|
|
240
|
+
merge titleMerge @id("merge_decision_title") candidate candidate_todo_title semanticChange semantic_change_title admissionDecision admission_title_safe decision merge autoMergeable gate gate_typecheck evidence evidence_typecheck
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The parser projects these rows into `metadata.decisionGraph`. Decision graph records are normalized by the kernel helpers, so authored files can describe why a merge is admissible without embedding raw JSON. This is intentionally evidence-first: a safe merge can be represented as a graph of changes, gates, evidence, and decisions, while missing proof can remain explicit as blocked or review-required records.
|
|
245
|
+
|
|
223
246
|
## Authored conversion syntax
|
|
224
247
|
|
|
225
248
|
`.frontier` files can carry universal conversion evidence directly in `conversion` or `universalConversionPlan` blocks. The parser projects these records into `metadata.universalConversionPlan` for the compiler facade and downstream semantic merge tooling.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDecisionGraphAdmissionDecisionRecord,
|
|
3
|
+
createDecisionGraphCandidateDecisionRecord,
|
|
4
|
+
createDecisionGraphChunkRecord,
|
|
5
|
+
createDecisionGraphEvidenceRecord,
|
|
6
|
+
createDecisionGraphGateRecord,
|
|
7
|
+
createDecisionGraphGraphRecord,
|
|
8
|
+
createDecisionGraphImprovementFeedbackRecord,
|
|
9
|
+
createDecisionGraphMergeDecisionRecord,
|
|
10
|
+
createDecisionGraphPanelProjectionRecord,
|
|
11
|
+
createDecisionGraphPatchEventRecord,
|
|
12
|
+
createDecisionGraphReplayRecord,
|
|
13
|
+
createDecisionGraphRsiLoopRecord,
|
|
14
|
+
createDecisionGraphSemanticChangeRecord,
|
|
15
|
+
createDecisionGraphTournamentCandidateRecord,
|
|
16
|
+
createDecisionGraphTournamentRecord
|
|
17
|
+
} from '@shapeshift-labs/frontier-lang-kernel';
|
|
18
|
+
|
|
19
|
+
const RECORD_FIELDS = Object.freeze({
|
|
20
|
+
gate: 'gateIds',
|
|
21
|
+
evidence: 'evidenceIds',
|
|
22
|
+
semanticChange: 'semanticChangeIds',
|
|
23
|
+
patchEvent: 'patchEventIds',
|
|
24
|
+
admissionDecision: 'admissionDecisionIds',
|
|
25
|
+
candidateDecision: 'decisionIds',
|
|
26
|
+
mergeDecision: 'decisionIds',
|
|
27
|
+
replay: 'replayRecordIds',
|
|
28
|
+
tournament: 'tournamentRecordIds',
|
|
29
|
+
tournamentCandidate: 'tournamentRecordIds',
|
|
30
|
+
rsiLoop: 'rsiLoopIds',
|
|
31
|
+
improvementFeedback: 'feedbackIds'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export function parseDecisionGraphBlock(block) {
|
|
35
|
+
const name = nameFrom(block.header);
|
|
36
|
+
const graph = {
|
|
37
|
+
id: idFrom(block.header, `decisionGraph_${name}`),
|
|
38
|
+
graphKind: readLine('graphKind', block.body) ?? readLine('kind', block.body) ?? 'semantic-merge-admission',
|
|
39
|
+
scopeId: readLine('scope', block.body) ?? readLine('scopeId', block.body),
|
|
40
|
+
rootId: readLine('root', block.body) ?? readLine('rootId', block.body),
|
|
41
|
+
status: readLine('status', block.body) ?? 'open',
|
|
42
|
+
subjectIds: readListLine('subject', block.body) ?? readListLine('subjects', block.body) ?? [],
|
|
43
|
+
nodes: [],
|
|
44
|
+
edges: [],
|
|
45
|
+
records: [],
|
|
46
|
+
metadata: { name }
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (const rawLine of block.body.split('\n')) {
|
|
50
|
+
const line = rawLine.trim();
|
|
51
|
+
if (!line || line.startsWith('#') || isGraphPropertyLine(line)) continue;
|
|
52
|
+
const match = /^([A-Za-z_$][\w$-]*)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
|
|
53
|
+
if (!match) continue;
|
|
54
|
+
const [, rowKind, rowName, rest] = match;
|
|
55
|
+
const normalized = normalizeRowKind(rowKind);
|
|
56
|
+
if (normalized === 'node') graph.nodes.push(parseNode(rowName, rest));
|
|
57
|
+
else if (normalized === 'edge') graph.edges.push(parseEdge(rowName, rest));
|
|
58
|
+
else {
|
|
59
|
+
const record = parseDecisionRecord(normalized, rowName, rest, graph.id);
|
|
60
|
+
if (record) graph.records.push(record);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const grouped = groupRecords(graph.records);
|
|
65
|
+
const graphRecord = createDecisionGraphGraphRecord({
|
|
66
|
+
id: graph.id,
|
|
67
|
+
graphKind: graph.graphKind,
|
|
68
|
+
scopeId: graph.scopeId,
|
|
69
|
+
rootId: graph.rootId,
|
|
70
|
+
status: graph.status,
|
|
71
|
+
subjectIds: graph.subjectIds,
|
|
72
|
+
recordIds: graph.records.map((record) => record.id),
|
|
73
|
+
nodeIds: graph.nodes.map((node) => node.id),
|
|
74
|
+
edgeIds: graph.edges.map((edge) => edge.id),
|
|
75
|
+
semanticChangeIds: grouped.semanticChangeIds,
|
|
76
|
+
admissionDecisionIds: grouped.admissionDecisionIds,
|
|
77
|
+
decisionIds: grouped.decisionIds,
|
|
78
|
+
gateIds: grouped.gateIds,
|
|
79
|
+
evidenceIds: grouped.evidenceIds,
|
|
80
|
+
replayRecordIds: grouped.replayRecordIds,
|
|
81
|
+
patchEventIds: grouped.patchEventIds,
|
|
82
|
+
tournamentRecordIds: grouped.tournamentRecordIds,
|
|
83
|
+
rsiLoopIds: grouped.rsiLoopIds,
|
|
84
|
+
nodes: graph.nodes,
|
|
85
|
+
edges: graph.edges,
|
|
86
|
+
metadata: graph.metadata
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
id: graph.id,
|
|
91
|
+
graph: graphRecord,
|
|
92
|
+
records: graph.records,
|
|
93
|
+
nodes: graph.nodes,
|
|
94
|
+
edges: graph.edges,
|
|
95
|
+
summary: {
|
|
96
|
+
graphCount: 1,
|
|
97
|
+
recordCount: graph.records.length,
|
|
98
|
+
nodeCount: graph.nodes.length,
|
|
99
|
+
edgeCount: graph.edges.length,
|
|
100
|
+
semanticChangeCount: grouped.semanticChangeIds.length,
|
|
101
|
+
gateCount: grouped.gateIds.length,
|
|
102
|
+
evidenceCount: grouped.evidenceIds.length,
|
|
103
|
+
admissionDecisionCount: grouped.admissionDecisionIds.length,
|
|
104
|
+
decisionCount: grouped.decisionIds.length,
|
|
105
|
+
patchEventCount: grouped.patchEventIds.length,
|
|
106
|
+
replayCount: grouped.replayRecordIds.length,
|
|
107
|
+
tournamentCount: grouped.tournamentRecordIds.length,
|
|
108
|
+
rsiLoopCount: grouped.rsiLoopIds.length
|
|
109
|
+
},
|
|
110
|
+
metadata: { name }
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseNode(name, text) {
|
|
115
|
+
return cleanRecord({
|
|
116
|
+
id: idFrom(text, `decision_node_${name}`),
|
|
117
|
+
nodeKind: readInlineWord('nodeKind', text) ?? readInlineWord('kind', text) ?? 'record',
|
|
118
|
+
recordId: readInlineWord('record', text) ?? readInlineWord('recordId', text),
|
|
119
|
+
label: readInlineQuoted('label', text) ?? readInlineWord('label', text) ?? name,
|
|
120
|
+
status: readInlineWord('status', text)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseEdge(name, text) {
|
|
125
|
+
return cleanRecord({
|
|
126
|
+
id: idFrom(text, `decision_edge_${name}`),
|
|
127
|
+
edgeKind: readInlineWord('edgeKind', text) ?? readInlineWord('kind', text) ?? 'relates-to',
|
|
128
|
+
fromId: readInlineWord('from', text) ?? readInlineWord('fromId', text) ?? readInlineWord('source', text) ?? readInlineWord('sourceId', text),
|
|
129
|
+
toId: readInlineWord('to', text) ?? readInlineWord('toId', text) ?? readInlineWord('target', text) ?? readInlineWord('targetId', text),
|
|
130
|
+
status: readInlineWord('status', text)
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseDecisionRecord(kind, name, text, graphId) {
|
|
135
|
+
const common = commonRecord(name, text, graphId);
|
|
136
|
+
if (kind === 'chunk') return createDecisionGraphChunkRecord({ ...common, chunkKind: readInlineWord('chunkKind', text) ?? readInlineWord('kind', text), sequence: readInlineNumber('sequence', text), recordIds: readInlineList(text, 'record', 'records', 'recordIds') });
|
|
137
|
+
if (kind === 'gate') return createDecisionGraphGateRecord({ ...common, gateKind: readInlineWord('gateKind', text) ?? readInlineWord('kind', text) ?? name, command: readInlineQuoted('command', text) ?? readInlineWord('command', text), required: readInlineFlag('required', text) });
|
|
138
|
+
if (kind === 'evidence') return createDecisionGraphEvidenceRecord({ ...common, evidenceKind: readInlineWord('evidenceKind', text) ?? readInlineWord('kind', text) ?? name, path: readInlineWord('path', text) });
|
|
139
|
+
if (kind === 'semanticChange') return createDecisionGraphSemanticChangeRecord({ ...common, changeKind: readInlineWord('changeKind', text) ?? readInlineWord('kind', text) ?? 'semantic', language: readInlineWord('language', text), sourcePath: readInlineWord('sourcePath', text) ?? readInlineWord('path', text), baseHash: readInlineWord('baseHash', text), targetHash: readInlineWord('targetHash', text), patchIds: readInlineList(text, 'patch', 'patches', 'patchId', 'patchIds'), operationIds: readInlineList(text, 'operation', 'operations', 'operationId', 'operationIds'), semanticNodeIds: readInlineList(text, 'semanticNode', 'semanticNodes', 'semanticNodeId', 'semanticNodeIds'), semanticSymbolIds: readInlineList(text, 'semanticSymbol', 'semanticSymbols', 'symbol', 'symbols', 'semanticSymbolId', 'semanticSymbolIds'), effectIds: readInlineList(text, 'effect', 'effects', 'effectId', 'effectIds'), regions: readInlineList(text, 'region', 'regions'), risk: readInlineWord('risk', text) });
|
|
140
|
+
if (kind === 'patchEvent') return createDecisionGraphPatchEventRecord({ ...common, eventId: readInlineWord('event', text) ?? readInlineWord('eventId', text), patchId: readInlineWord('patch', text) ?? readInlineWord('patchId', text), patchIds: readInlineList(text, 'patch', 'patches', 'patchId', 'patchIds'), actor: readInlineWord('actor', text), at: readInlineWord('at', text), baseHash: readInlineWord('baseHash', text), targetHash: readInlineWord('targetHash', text), operationIds: readInlineList(text, 'operation', 'operations', 'operationId', 'operationIds'), deterministic: readInlineFlag('deterministic', text) });
|
|
141
|
+
if (kind === 'admissionDecision') return createDecisionGraphAdmissionDecisionRecord({ ...common, admissionId: readInlineWord('admission', text) ?? readInlineWord('admissionId', text), candidateId: readInlineWord('candidate', text) ?? readInlineWord('candidateId', text), classification: readInlineWord('classification', text), decision: readInlineWord('decision', text), autoMergeable: readInlineFlag('autoMergeable', text) || readInlineFlag('autoMerge', text), conflictKeys: readInlineList(text, 'conflictKey', 'conflictKeys'), conflictKeyKinds: readInlineList(text, 'conflictKeyKind', 'conflictKeyKinds'), reasons: readInlineList(text, 'reason', 'reasons') });
|
|
142
|
+
if (kind === 'candidateDecision') return createDecisionGraphCandidateDecisionRecord({ ...common, candidateId: readInlineWord('candidate', text) ?? readInlineWord('candidateId', text), decision: readInlineWord('decision', text), score: readInlineNumber('score', text), rank: readInlineNumber('rank', text), reviewerIds: readInlineList(text, 'reviewer', 'reviewers', 'reviewerId', 'reviewerIds'), reasons: readInlineList(text, 'reason', 'reasons') });
|
|
143
|
+
if (kind === 'mergeDecision') return createDecisionGraphMergeDecisionRecord({ ...common, decision: readInlineWord('decision', text), autoMergeable: readInlineFlag('autoMergeable', text) || readInlineFlag('autoMerge', text), baseHash: readInlineWord('baseHash', text), targetHash: readInlineWord('targetHash', text), conflictKeys: readInlineList(text, 'conflictKey', 'conflictKeys'), reasons: readInlineList(text, 'reason', 'reasons'), candidateDecisionIds: readInlineList(text, 'candidateDecision', 'candidateDecisionId', 'candidateDecisionIds') });
|
|
144
|
+
if (kind === 'replay') return createDecisionGraphReplayRecord({ ...common, eventIds: readInlineList(text, 'event', 'events', 'eventId', 'eventIds'), patchIds: readInlineList(text, 'patch', 'patches', 'patchId', 'patchIds'), baseHash: readInlineWord('baseHash', text), finalHash: readInlineWord('finalHash', text), issues: readInlineList(text, 'issue', 'issues'), replayComplete: readInlineFlag('replayComplete', text), deterministic: readInlineFlag('deterministic', text) });
|
|
145
|
+
if (kind === 'tournament') return createDecisionGraphTournamentRecord({ ...common, tournamentId: readInlineWord('tournament', text) ?? readInlineWord('tournamentId', text), tournamentKind: readInlineWord('tournamentKind', text) ?? readInlineWord('kind', text), winnerCandidateId: readInlineWord('winner', text) ?? readInlineWord('winnerCandidateId', text) });
|
|
146
|
+
if (kind === 'tournamentCandidate') return createDecisionGraphTournamentCandidateRecord({ ...common, tournamentId: readInlineWord('tournament', text) ?? readInlineWord('tournamentId', text), candidateId: readInlineWord('candidate', text) ?? readInlineWord('candidateId', text), lane: readInlineWord('lane', text), taskId: readInlineWord('task', text) ?? readInlineWord('taskId', text), agentId: readInlineWord('agent', text) ?? readInlineWord('agentId', text), score: readInlineNumber('score', text), rank: readInlineNumber('rank', text) });
|
|
147
|
+
if (kind === 'panelProjection') return createDecisionGraphPanelProjectionRecord({ ...common, panelId: readInlineWord('panel', text) ?? readInlineWord('panelId', text), projectionKind: readInlineWord('projectionKind', text) ?? readInlineWord('kind', text), mergeDecisionIds: readInlineList(text, 'mergeDecision', 'mergeDecisionId', 'mergeDecisionIds'), fields: readInlineList(text, 'field', 'fields') });
|
|
148
|
+
if (kind === 'rsiLoop') return createDecisionGraphRsiLoopRecord({ ...common, loopId: readInlineWord('loop', text) ?? readInlineWord('loopId', text), loopKind: readInlineWord('loopKind', text) ?? readInlineWord('kind', text), iteration: readInlineNumber('iteration', text), objective: readInlineQuoted('objective', text) ?? readInlineWord('objective', text), action: readInlineWord('action', text), feedbackIds: readInlineList(text, 'feedback', 'feedbackId', 'feedbackIds') });
|
|
149
|
+
if (kind === 'improvementFeedback') return createDecisionGraphImprovementFeedbackRecord({ ...common, loopId: readInlineWord('loop', text) ?? readInlineWord('loopId', text), loopKind: readInlineWord('loopKind', text), feedbackKind: readInlineWord('feedbackKind', text) ?? readInlineWord('kind', text), subjectId: readInlineWord('subject', text) ?? readInlineWord('subjectId', text), severity: readInlineWord('severity', text), action: readInlineWord('action', text), feedback: readInlineQuoted('feedback', text) });
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function commonRecord(name, text, graphId) {
|
|
154
|
+
return cleanRecord({
|
|
155
|
+
id: idFrom(text, undefined),
|
|
156
|
+
name,
|
|
157
|
+
graphIds: [graphId],
|
|
158
|
+
status: readInlineWord('status', text),
|
|
159
|
+
subjectIds: readInlineList(text, 'subject', 'subjects', 'subjectId', 'subjectIds'),
|
|
160
|
+
candidateIds: readInlineList(text, 'candidate', 'candidates', 'candidateId', 'candidateIds'),
|
|
161
|
+
semanticMergeCandidateIds: readInlineList(text, 'semanticMergeCandidate', 'semanticMergeCandidates', 'semanticMergeCandidateId', 'semanticMergeCandidateIds'),
|
|
162
|
+
semanticChangeIds: readInlineList(text, 'semanticChange', 'semanticChanges', 'semanticChangeId', 'semanticChangeIds'),
|
|
163
|
+
admissionDecisionIds: readInlineList(text, 'admissionDecision', 'admissionDecisionId', 'admissionDecisionIds'),
|
|
164
|
+
decisionIds: readInlineList(text, 'decisionId', 'decisionIds'),
|
|
165
|
+
gateIds: readInlineList(text, 'gate', 'gates', 'gateId', 'gateIds'),
|
|
166
|
+
evidenceIds: readInlineList(text, 'evidence', 'evidenceId', 'evidenceIds'),
|
|
167
|
+
replayRecordIds: readInlineList(text, 'replay', 'replayId', 'replayRecord', 'replayRecordId', 'replayRecordIds'),
|
|
168
|
+
patchEventIds: readInlineList(text, 'patchEvent', 'patchEventId', 'patchEventIds'),
|
|
169
|
+
tournamentRecordIds: readInlineList(text, 'tournamentRecord', 'tournamentRecordId', 'tournamentRecordIds'),
|
|
170
|
+
rsiLoopIds: readInlineList(text, 'rsiLoop', 'rsiLoopId', 'rsiLoopIds'),
|
|
171
|
+
summary: readInlineQuoted('summary', text),
|
|
172
|
+
metadata: { authoredName: name }
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function groupRecords(records) {
|
|
177
|
+
const grouped = {
|
|
178
|
+
gateIds: [],
|
|
179
|
+
evidenceIds: [],
|
|
180
|
+
semanticChangeIds: [],
|
|
181
|
+
patchEventIds: [],
|
|
182
|
+
admissionDecisionIds: [],
|
|
183
|
+
decisionIds: [],
|
|
184
|
+
replayRecordIds: [],
|
|
185
|
+
tournamentRecordIds: [],
|
|
186
|
+
rsiLoopIds: [],
|
|
187
|
+
feedbackIds: []
|
|
188
|
+
};
|
|
189
|
+
for (const record of records) {
|
|
190
|
+
const key = RECORD_FIELDS[recordName(record)];
|
|
191
|
+
if (key && record.id) grouped[key].push(record.id);
|
|
192
|
+
}
|
|
193
|
+
return Object.fromEntries(Object.entries(grouped).map(([key, value]) => [key, unique(value)]));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function recordName(record) {
|
|
197
|
+
return String(record?.recordKind ?? record?.kind ?? '').replace('frontier.lang.decisionGraph.', '');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeRowKind(kind) {
|
|
201
|
+
if (kind === 'change') return 'semanticChange';
|
|
202
|
+
if (kind === 'patch') return 'patchEvent';
|
|
203
|
+
if (kind === 'admission') return 'admissionDecision';
|
|
204
|
+
if (kind === 'candidate') return 'candidateDecision';
|
|
205
|
+
if (kind === 'merge') return 'mergeDecision';
|
|
206
|
+
if (kind === 'panel') return 'panelProjection';
|
|
207
|
+
if (kind === 'feedback') return 'improvementFeedback';
|
|
208
|
+
return kind;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isGraphPropertyLine(line) {
|
|
212
|
+
return /^(graphKind|kind|scope|scopeId|root|rootId|status|subject|subjects)\s+/.test(line);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function idFrom(text, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(text)?.[1] ?? fallback; }
|
|
216
|
+
function nameFrom(header) { return /^([A-Za-z_$][\w$]*)/.exec(header)?.[1] ?? 'DecisionGraph'; }
|
|
217
|
+
function readLine(label, body) { return new RegExp('^\\s*' + label + '\\s+([^\\n]+)', 'm').exec(body)?.[1]?.trim(); }
|
|
218
|
+
function readListLine(label, body) {
|
|
219
|
+
const line = readLine(label, body);
|
|
220
|
+
return line ? line.split(/[|,]/).map((item) => item.trim()).filter(Boolean) : undefined;
|
|
221
|
+
}
|
|
222
|
+
function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
|
|
223
|
+
function readInlineQuoted(label, text) { return new RegExp("(?:^|\\s)" + label + "\\s+[\"']([^\"']+)[\"']").exec(text)?.[1]?.trim(); }
|
|
224
|
+
function readInlineFlag(label, text) { return new RegExp('(?:^|\\s)' + label + '(?:\\s|$)').test(text) || undefined; }
|
|
225
|
+
function readInlineNumber(label, text) {
|
|
226
|
+
const value = readInlineWord(label, text);
|
|
227
|
+
return value === undefined ? undefined : Number(value);
|
|
228
|
+
}
|
|
229
|
+
function readInlineList(text, ...labels) {
|
|
230
|
+
for (const label of labels) {
|
|
231
|
+
const value = new RegExp('(?:^|\\s)' + label + '\\s+([^\\s]+)').exec(text)?.[1]?.trim();
|
|
232
|
+
if (value) return value.split(/[|,]/).map((item) => item.trim()).filter(Boolean);
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
function unique(values) { return [...new Set(values.filter(Boolean))]; }
|
|
237
|
+
function cleanRecord(record) {
|
|
238
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
|
|
239
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { actionNode, capabilityNode, createDocument, effectNode, entityNode, externNode, latticeNode, migrationNode, stateNode, targetNode, typeNode } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
2
|
import { parseConstraintSpaceBlock } from './constraint-space.js';
|
|
3
3
|
import { parseConversionBlock } from './conversion.js';
|
|
4
|
+
import { parseDecisionGraphBlock } from './decision-graph.js';
|
|
4
5
|
import { createParsedMetadata } from './metadata.js';
|
|
5
6
|
import { parseSemanticOperationsBlock } from './operations.js';
|
|
6
7
|
import { parseParadigmBlock } from './paradigm.js';
|
|
@@ -15,6 +16,7 @@ export function parseFrontierSource(source, options = {}) {
|
|
|
15
16
|
const operationBlocks = [];
|
|
16
17
|
const conversionBlocks = [];
|
|
17
18
|
const constraintSpaceBlocks = [];
|
|
19
|
+
const decisionGraphBlocks = [];
|
|
18
20
|
const nativeSourceBlocks = [];
|
|
19
21
|
const documentId = options.id ?? readId(source) ?? 'mod_frontier';
|
|
20
22
|
const documentName = options.name ?? readName(source) ?? 'FrontierModule';
|
|
@@ -40,8 +42,9 @@ export function parseFrontierSource(source, options = {}) {
|
|
|
40
42
|
if (block.kind === 'operations' || block.kind === 'semanticOperations') operationBlocks.push(parseSemanticOperationsBlock(block));
|
|
41
43
|
if (block.kind === 'conversion' || block.kind === 'universalConversionPlan') conversionBlocks.push(parseConversionBlock(block));
|
|
42
44
|
if (block.kind === 'constraintSpace' || block.kind === 'possibilitySpace') constraintSpaceBlocks.push(parseConstraintSpaceBlock(block));
|
|
45
|
+
if (block.kind === 'decisionGraph' || block.kind === 'admissionGraph') decisionGraphBlocks.push(parseDecisionGraphBlock(block));
|
|
43
46
|
}
|
|
44
|
-
const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, constraintSpaceBlocks, nativeSourceBlocks });
|
|
47
|
+
const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, constraintSpaceBlocks, decisionGraphBlocks, nativeSourceBlocks });
|
|
45
48
|
return createDocument({ id: documentId, name: documentName, nodes, ...(metadata ? { metadata } : {}) });
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -51,7 +54,7 @@ function readName(source) { return /module\s+([A-Za-z_$][\w$]*)/.exec(source)?.[
|
|
|
51
54
|
function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']+)["']\s*\)/.exec(source)?.[1]; }
|
|
52
55
|
function readBlocks(source) {
|
|
53
56
|
const blocks = [];
|
|
54
|
-
const header = /\b(entity|state|action|view|migration|capability|effect|type|extern|lattice|nativeSource|target|proof|paradigm|paradigmSemantics|operations|semanticOperations|conversion|universalConversionPlan|constraintSpace|possibilitySpace)\s+([^{}]+)\{/g;
|
|
57
|
+
const header = /\b(entity|state|action|view|migration|capability|effect|type|extern|lattice|nativeSource|target|proof|paradigm|paradigmSemantics|operations|semanticOperations|conversion|universalConversionPlan|constraintSpace|possibilitySpace|decisionGraph|admissionGraph)\s+([^{}]+)\{/g;
|
|
55
58
|
let match;
|
|
56
59
|
while ((match = header.exec(source))) {
|
|
57
60
|
let depth = 1; let index = header.lastIndex;
|
package/dist/metadata.js
CHANGED
|
@@ -21,13 +21,14 @@ const PARADIGM_GROUPS = [
|
|
|
21
21
|
'loweringRecords'
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
-
export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], operationBlocks = [], conversionBlocks = [], constraintSpaceBlocks = [], nativeSourceBlocks = [] } = {}) {
|
|
24
|
+
export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], operationBlocks = [], conversionBlocks = [], constraintSpaceBlocks = [], decisionGraphBlocks = [], nativeSourceBlocks = [] } = {}) {
|
|
25
25
|
const metadata = {};
|
|
26
26
|
if (proofBlocks.length) metadata.proof = mergeProofBlocks(proofBlocks);
|
|
27
27
|
if (paradigmBlocks.length) metadata.paradigmSemantics = mergeParadigmBlocks(paradigmBlocks);
|
|
28
28
|
if (operationBlocks.length) metadata.semanticOperations = mergeOperationBlocks(operationBlocks);
|
|
29
29
|
if (conversionBlocks.length) metadata.universalConversionPlan = mergeConversionBlocks(conversionBlocks);
|
|
30
30
|
if (constraintSpaceBlocks.length) metadata.constraintSpaces = mergeConstraintSpaceBlocks(constraintSpaceBlocks);
|
|
31
|
+
if (decisionGraphBlocks.length) metadata.decisionGraph = mergeDecisionGraphBlocks(decisionGraphBlocks);
|
|
31
32
|
if (nativeSourceBlocks.some((block) => block.sourceMaps.length || block.mergeCandidates.length || block.evidence.length)) {
|
|
32
33
|
metadata.universalAst = mergeNativeSourceBlocks(nativeSourceBlocks);
|
|
33
34
|
}
|
|
@@ -112,10 +113,53 @@ function mergeNativeSourceBlocks(blocks) {
|
|
|
112
113
|
};
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
function mergeDecisionGraphBlocks(blocks) {
|
|
117
|
+
const records = blocks.flatMap((block) => block.records ?? []);
|
|
118
|
+
const graphs = blocks.map((block) => block.graph).filter(Boolean);
|
|
119
|
+
return {
|
|
120
|
+
id: blocks.length === 1 ? blocks[0].id : 'decisionGraph:source',
|
|
121
|
+
graphs,
|
|
122
|
+
records,
|
|
123
|
+
nodes: blocks.flatMap((block) => block.nodes ?? []),
|
|
124
|
+
edges: blocks.flatMap((block) => block.edges ?? []),
|
|
125
|
+
graphIds: ids(graphs),
|
|
126
|
+
recordIds: ids(records),
|
|
127
|
+
gateIds: idsByKind(records, 'frontier.lang.decisionGraph.gate'),
|
|
128
|
+
evidenceIds: idsByKind(records, 'frontier.lang.decisionGraph.evidence'),
|
|
129
|
+
semanticChangeIds: idsByKind(records, 'frontier.lang.decisionGraph.semanticChange'),
|
|
130
|
+
patchEventIds: idsByKind(records, 'frontier.lang.decisionGraph.patchEvent'),
|
|
131
|
+
admissionDecisionIds: idsByKind(records, 'frontier.lang.decisionGraph.admissionDecision'),
|
|
132
|
+
decisionIds: idsByKind(records, 'frontier.lang.decisionGraph.candidateDecision').concat(idsByKind(records, 'frontier.lang.decisionGraph.mergeDecision')),
|
|
133
|
+
replayRecordIds: idsByKind(records, 'frontier.lang.decisionGraph.replay'),
|
|
134
|
+
tournamentRecordIds: idsByKind(records, 'frontier.lang.decisionGraph.tournament').concat(idsByKind(records, 'frontier.lang.decisionGraph.tournamentCandidate')),
|
|
135
|
+
rsiLoopIds: idsByKind(records, 'frontier.lang.decisionGraph.rsiLoop'),
|
|
136
|
+
summary: {
|
|
137
|
+
graphCount: blocks.length,
|
|
138
|
+
recordCount: records.length,
|
|
139
|
+
nodeCount: sum(blocks, 'nodeCount'),
|
|
140
|
+
edgeCount: sum(blocks, 'edgeCount'),
|
|
141
|
+
semanticChangeCount: sum(blocks, 'semanticChangeCount'),
|
|
142
|
+
gateCount: sum(blocks, 'gateCount'),
|
|
143
|
+
evidenceCount: sum(blocks, 'evidenceCount'),
|
|
144
|
+
admissionDecisionCount: sum(blocks, 'admissionDecisionCount'),
|
|
145
|
+
decisionCount: sum(blocks, 'decisionCount'),
|
|
146
|
+
patchEventCount: sum(blocks, 'patchEventCount'),
|
|
147
|
+
replayCount: sum(blocks, 'replayCount'),
|
|
148
|
+
tournamentCount: sum(blocks, 'tournamentCount'),
|
|
149
|
+
rsiLoopCount: sum(blocks, 'rsiLoopCount')
|
|
150
|
+
},
|
|
151
|
+
metadata: { authoredDecisionGraphBlockIds: blocks.map((block) => block.id) }
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
115
155
|
function ids(records = []) {
|
|
116
156
|
return records.map((record) => record?.id).filter(Boolean);
|
|
117
157
|
}
|
|
118
158
|
|
|
159
|
+
function idsByKind(records = [], kind) {
|
|
160
|
+
return ids(records.filter((record) => record?.recordKind === kind || record?.kind === kind));
|
|
161
|
+
}
|
|
162
|
+
|
|
119
163
|
function sum(blocks, key) {
|
|
120
164
|
return blocks.reduce((total, block) => total + (block.summary?.[key] ?? 0), 0);
|
|
121
165
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shapeshift-labs/frontier-lang-parser",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
4
4
|
"description": "Parser for the first Frontier Lang .frontier syntax slice.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@shapeshift-labs/frontier-lang-kernel": "0.3.
|
|
57
|
+
"@shapeshift-labs/frontier-lang-kernel": "0.3.13"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"typescript": "^5.9.3"
|