@shapeshift-labs/frontier-lang-parser 0.3.18 → 0.3.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -220,6 +220,57 @@ 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
+
246
+ ## Authored resource graph syntax
247
+
248
+ `.frontier` files can carry semantic resource graph evidence directly in `resourceGraph` or `semanticResourceGraph` blocks. These blocks make ownership, aliases, loans, moves, drops, lifetimes, unsafe boundaries, conflicts, and proof obligations explicit for translation and semantic merge admission.
249
+
250
+ ```frontier
251
+ resourceGraph TodoResources @id("resource_graph_todo") {
252
+ sourceLanguage javascript
253
+ sourcePath src/todo.ts
254
+ sourceHash sha256:example
255
+ evidence artifact_todo_title_probe
256
+ resource todos @id("resource_todos") kind collection owner owner_todo_store
257
+ owner todoStore @id("owner_todo_store") kind store
258
+ lifetime request @id("life_request") kind lexical startLine 1 endLine 80
259
+ loan readTodos @id("loan_read_todos") resource resource_todos owner owner_todo_store lifetime life_request mode shared access read
260
+ alias todosAlias @id("alias_todos") resource resource_todos owner owner_todo_store alias alias:todos kind local
261
+ move todoMove @id("move_todos") resource resource_todos fromOwner owner_todo_store toOwner owner_worker kind transfer
262
+ drop todoDrop @id("drop_todos") resource resource_todos owner owner_worker lifetime life_request kind lexical-drop order 1
263
+ escape todoEscape @id("escape_todos") resource resource_todos loan loan_read_todos lifetime life_request kind returned-borrow status needs-proof
264
+ outlives requestModule @id("life_request_outlives_module") from life_module to life_request kind contains
265
+ borrow readScope @id("borrow_scope_todos") resource resource_todos lifetime life_request kind shared-borrow constraint shared|read-only
266
+ unsafe ffiBoundary @id("unsafe_todos_ffi") resource resource_todos kind ffi proofStatus missing
267
+ conflict aliasConflict @id("conflict_todos_alias") resource resource_todos loan loan_read_todos alias alias_todos reasonCode exclusive-resource-alias-overlap-requires-proof status open severity error
268
+ proof aliasProof @id("proof_obligation_alias") resource resource_todos conflict conflict_todos_alias kind alias-safety status open statement "Prove the alias cannot mutate during the shared loan."
269
+ }
270
+ ```
271
+
272
+ The parser projects these rows into `metadata.semanticResourceGraphs`. Resource graphs are evidence, not proof: generated claims for borrow-checker soundness, alias safety, lifetime soundness, semantic equivalence, and auto-merge stay false. Compiler conversion routes can use these authored graphs as source-side resource, ownership, lifetime, and borrow-checker evidence while still requiring target proof before admission.
273
+
223
274
  ## Authored conversion syntax
224
275
 
225
276
  `.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,10 +1,12 @@
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';
7
8
  import { parseProofBlock } from './proof.js';
9
+ import { parseResourceGraphBlock } from './resource-graph.js';
8
10
  import { parseNativeSourceBlock } from './source-evidence.js';
9
11
  import { parseViewBlock } from './view.js';
10
12
 
@@ -15,6 +17,8 @@ export function parseFrontierSource(source, options = {}) {
15
17
  const operationBlocks = [];
16
18
  const conversionBlocks = [];
17
19
  const constraintSpaceBlocks = [];
20
+ const decisionGraphBlocks = [];
21
+ const resourceGraphBlocks = [];
18
22
  const nativeSourceBlocks = [];
19
23
  const documentId = options.id ?? readId(source) ?? 'mod_frontier';
20
24
  const documentName = options.name ?? readName(source) ?? 'FrontierModule';
@@ -40,8 +44,10 @@ export function parseFrontierSource(source, options = {}) {
40
44
  if (block.kind === 'operations' || block.kind === 'semanticOperations') operationBlocks.push(parseSemanticOperationsBlock(block));
41
45
  if (block.kind === 'conversion' || block.kind === 'universalConversionPlan') conversionBlocks.push(parseConversionBlock(block));
42
46
  if (block.kind === 'constraintSpace' || block.kind === 'possibilitySpace') constraintSpaceBlocks.push(parseConstraintSpaceBlock(block));
47
+ if (block.kind === 'decisionGraph' || block.kind === 'admissionGraph') decisionGraphBlocks.push(parseDecisionGraphBlock(block));
48
+ if (block.kind === 'resourceGraph' || block.kind === 'semanticResourceGraph') resourceGraphBlocks.push(parseResourceGraphBlock(block));
43
49
  }
44
- const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, constraintSpaceBlocks, nativeSourceBlocks });
50
+ const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, constraintSpaceBlocks, decisionGraphBlocks, resourceGraphBlocks, nativeSourceBlocks });
45
51
  return createDocument({ id: documentId, name: documentName, nodes, ...(metadata ? { metadata } : {}) });
46
52
  }
47
53
 
@@ -51,7 +57,7 @@ function readName(source) { return /module\s+([A-Za-z_$][\w$]*)/.exec(source)?.[
51
57
  function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']+)["']\s*\)/.exec(source)?.[1]; }
52
58
  function readBlocks(source) {
53
59
  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;
60
+ 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|resourceGraph|semanticResourceGraph)\s+([^{}]+)\{/g;
55
61
  let match;
56
62
  while ((match = header.exec(source))) {
57
63
  let depth = 1; let index = header.lastIndex;
package/dist/metadata.js CHANGED
@@ -21,13 +21,15 @@ 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 = [], resourceGraphBlocks = [], 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);
32
+ if (resourceGraphBlocks.length) metadata.semanticResourceGraphs = mergeResourceGraphBlocks(resourceGraphBlocks);
31
33
  if (nativeSourceBlocks.some((block) => block.sourceMaps.length || block.mergeCandidates.length || block.evidence.length)) {
32
34
  metadata.universalAst = mergeNativeSourceBlocks(nativeSourceBlocks);
33
35
  }
@@ -112,10 +114,98 @@ function mergeNativeSourceBlocks(blocks) {
112
114
  };
113
115
  }
114
116
 
117
+ function mergeDecisionGraphBlocks(blocks) {
118
+ const records = blocks.flatMap((block) => block.records ?? []);
119
+ const graphs = blocks.map((block) => block.graph).filter(Boolean);
120
+ return {
121
+ id: blocks.length === 1 ? blocks[0].id : 'decisionGraph:source',
122
+ graphs,
123
+ records,
124
+ nodes: blocks.flatMap((block) => block.nodes ?? []),
125
+ edges: blocks.flatMap((block) => block.edges ?? []),
126
+ graphIds: ids(graphs),
127
+ recordIds: ids(records),
128
+ gateIds: idsByKind(records, 'frontier.lang.decisionGraph.gate'),
129
+ evidenceIds: idsByKind(records, 'frontier.lang.decisionGraph.evidence'),
130
+ semanticChangeIds: idsByKind(records, 'frontier.lang.decisionGraph.semanticChange'),
131
+ patchEventIds: idsByKind(records, 'frontier.lang.decisionGraph.patchEvent'),
132
+ admissionDecisionIds: idsByKind(records, 'frontier.lang.decisionGraph.admissionDecision'),
133
+ decisionIds: idsByKind(records, 'frontier.lang.decisionGraph.candidateDecision').concat(idsByKind(records, 'frontier.lang.decisionGraph.mergeDecision')),
134
+ replayRecordIds: idsByKind(records, 'frontier.lang.decisionGraph.replay'),
135
+ tournamentRecordIds: idsByKind(records, 'frontier.lang.decisionGraph.tournament').concat(idsByKind(records, 'frontier.lang.decisionGraph.tournamentCandidate')),
136
+ rsiLoopIds: idsByKind(records, 'frontier.lang.decisionGraph.rsiLoop'),
137
+ summary: {
138
+ graphCount: blocks.length,
139
+ recordCount: records.length,
140
+ nodeCount: sum(blocks, 'nodeCount'),
141
+ edgeCount: sum(blocks, 'edgeCount'),
142
+ semanticChangeCount: sum(blocks, 'semanticChangeCount'),
143
+ gateCount: sum(blocks, 'gateCount'),
144
+ evidenceCount: sum(blocks, 'evidenceCount'),
145
+ admissionDecisionCount: sum(blocks, 'admissionDecisionCount'),
146
+ decisionCount: sum(blocks, 'decisionCount'),
147
+ patchEventCount: sum(blocks, 'patchEventCount'),
148
+ replayCount: sum(blocks, 'replayCount'),
149
+ tournamentCount: sum(blocks, 'tournamentCount'),
150
+ rsiLoopCount: sum(blocks, 'rsiLoopCount')
151
+ },
152
+ metadata: { authoredDecisionGraphBlockIds: blocks.map((block) => block.id) }
153
+ };
154
+ }
155
+
156
+ function mergeResourceGraphBlocks(blocks) {
157
+ const graphs = blocks.map((block) => block.graph).filter(Boolean);
158
+ const records = blocks.flatMap((block) => block.records ?? []);
159
+ return {
160
+ id: blocks.length === 1 ? blocks[0].id : 'semanticResourceGraphs:source',
161
+ graphs,
162
+ resourceGraphs: graphs,
163
+ records,
164
+ graphIds: ids(graphs),
165
+ recordIds: ids(records),
166
+ resourceIds: blocks.flatMap((block) => ids(block.graph?.resources)),
167
+ ownerIds: blocks.flatMap((block) => ids(block.graph?.owners)),
168
+ loanIds: blocks.flatMap((block) => ids(block.graph?.loans)),
169
+ aliasIds: blocks.flatMap((block) => ids(block.graph?.aliases)),
170
+ moveIds: blocks.flatMap((block) => ids(block.graph?.moves)),
171
+ dropIds: blocks.flatMap((block) => ids(block.graph?.drops)),
172
+ escapeIds: blocks.flatMap((block) => ids(block.graph?.escapes)),
173
+ lifetimeRegionIds: blocks.flatMap((block) => ids(block.graph?.lifetimeRegions)),
174
+ lifetimeRelationIds: blocks.flatMap((block) => ids(block.graph?.lifetimeRelations)),
175
+ borrowScopeIds: blocks.flatMap((block) => ids(block.graph?.borrowScopes)),
176
+ unsafeBoundaryIds: blocks.flatMap((block) => ids(block.graph?.unsafeBoundaries)),
177
+ conflictIds: blocks.flatMap((block) => ids(block.graph?.conflicts)),
178
+ proofObligationIds: blocks.flatMap((block) => ids(block.graph?.proofObligations)),
179
+ summary: {
180
+ graphCount: blocks.length,
181
+ recordCount: records.length,
182
+ resourceCount: sum(blocks, 'resources'),
183
+ ownerCount: sum(blocks, 'owners'),
184
+ loanCount: sum(blocks, 'loans'),
185
+ aliasCount: sum(blocks, 'aliases'),
186
+ moveCount: sum(blocks, 'moves'),
187
+ dropCount: sum(blocks, 'drops'),
188
+ escapeCount: sum(blocks, 'escapes'),
189
+ lifetimeRegionCount: sum(blocks, 'lifetimeRegions'),
190
+ lifetimeRelationCount: sum(blocks, 'lifetimeRelations'),
191
+ borrowScopeCount: sum(blocks, 'borrowScopes'),
192
+ unsafeBoundaryCount: sum(blocks, 'unsafeBoundaries'),
193
+ conflictCount: sum(blocks, 'conflicts'),
194
+ proofObligationCount: sum(blocks, 'proofObligations'),
195
+ unsafeBoundariesWithoutProof: sum(blocks, 'unsafeBoundariesWithoutProof')
196
+ },
197
+ metadata: { authoredResourceGraphBlockIds: blocks.map((block) => block.id) }
198
+ };
199
+ }
200
+
115
201
  function ids(records = []) {
116
202
  return records.map((record) => record?.id).filter(Boolean);
117
203
  }
118
204
 
205
+ function idsByKind(records = [], kind) {
206
+ return ids(records.filter((record) => record?.recordKind === kind || record?.kind === kind));
207
+ }
208
+
119
209
  function sum(blocks, key) {
120
210
  return blocks.reduce((total, block) => total + (block.summary?.[key] ?? 0), 0);
121
211
  }
@@ -0,0 +1,208 @@
1
+ const GROUPS = {
2
+ resource: 'resources',
3
+ owner: 'owners',
4
+ loan: 'loans',
5
+ alias: 'aliases',
6
+ move: 'moves',
7
+ drop: 'drops',
8
+ escape: 'escapes',
9
+ lifetimeRegion: 'lifetimeRegions',
10
+ lifetimeRelation: 'lifetimeRelations',
11
+ borrowScope: 'borrowScopes',
12
+ unsafeBoundary: 'unsafeBoundaries',
13
+ conflict: 'conflicts',
14
+ proofObligation: 'proofObligations'
15
+ };
16
+
17
+ export function parseResourceGraphBlock(block) {
18
+ const name = nameFrom(block.header);
19
+ const graph = {
20
+ kind: 'frontier.lang.semanticResourceGraph',
21
+ version: 1,
22
+ id: idFrom(block.header, `resource_graph_${name}`),
23
+ sourceLanguage: readLine('sourceLanguage', block.body) ?? readLine('language', block.body),
24
+ sourcePath: readLine('sourcePath', block.body) ?? readLine('path', block.body),
25
+ sourceHash: readLine('sourceHash', block.body),
26
+ status: readLine('status', block.body) ?? 'partial',
27
+ evidenceIds: readListLine('evidence', block.body) ?? readListLine('evidenceIds', block.body) ?? [],
28
+ resources: [],
29
+ owners: [],
30
+ loans: [],
31
+ aliases: [],
32
+ moves: [],
33
+ drops: [],
34
+ escapes: [],
35
+ lifetimeRegions: [],
36
+ lifetimeRelations: [],
37
+ outlives: [],
38
+ borrowScopes: [],
39
+ borrowScopeRegions: [],
40
+ unsafeBoundaries: [],
41
+ conflicts: [],
42
+ proofObligations: [],
43
+ claims: {
44
+ borrowCheckerClaim: false,
45
+ aliasSafetyClaim: false,
46
+ lifetimeSoundnessClaim: false,
47
+ semanticEquivalenceClaim: false,
48
+ autoMergeClaim: false
49
+ },
50
+ metadata: { name }
51
+ };
52
+
53
+ for (const rawLine of block.body.split('\n')) {
54
+ const line = rawLine.trim();
55
+ if (!line || line.startsWith('#') || isGraphPropertyLine(line)) continue;
56
+ const match = /^([A-Za-z_$][\w$-]*)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
57
+ if (!match) continue;
58
+ const [, rowKind, rowName, rest] = match;
59
+ const normalized = normalizeRowKind(rowKind);
60
+ const record = parseResourceRecord(normalized, rowName, rest, graph);
61
+ const group = GROUPS[normalized];
62
+ if (record && group) graph[group].push(record);
63
+ }
64
+
65
+ graph.outlives = graph.lifetimeRelations;
66
+ graph.borrowScopeRegions = graph.borrowScopes;
67
+ graph.summary = summarize(graph);
68
+ graph.query = {
69
+ resourceIds: ids(graph.resources),
70
+ ownerIds: ids(graph.owners),
71
+ lifetimeRegionIds: ids(graph.lifetimeRegions),
72
+ sourcePaths: unique(allRecords(graph).map((record) => record.sourcePath)),
73
+ evidenceIds: unique([...graph.evidenceIds, ...allRecords(graph).flatMap((record) => record.evidenceIds ?? [])]),
74
+ blockerReasonCodes: unique(graph.conflicts.map((record) => record.reasonCode))
75
+ };
76
+
77
+ return {
78
+ id: graph.id,
79
+ graph,
80
+ records: allRecords(graph),
81
+ summary: {
82
+ graphCount: 1,
83
+ ...graph.summary
84
+ },
85
+ metadata: { name }
86
+ };
87
+ }
88
+
89
+ function parseResourceRecord(kind, name, text, graph) {
90
+ const common = commonRecord(kind, name, text, graph);
91
+ if (kind === 'resource') return cleanRecord({ ...common, resourceKind: readInlineWord('resourceKind', text) ?? readInlineWord('kind', text) ?? name, ownerId: readInlineWord('owner', text) ?? readInlineWord('ownerId', text), ownerName: readInlineWord('ownerName', text) });
92
+ if (kind === 'owner') return cleanRecord({ ...common, ownerKind: readInlineWord('ownerKind', text) ?? readInlineWord('kind', text) ?? 'owner' });
93
+ if (kind === 'loan') return cleanRecord({ ...common, resourceId: readInlineWord('resource', text) ?? readInlineWord('resourceId', text), ownerId: readInlineWord('owner', text) ?? readInlineWord('ownerId', text), lifetimeRegionId: readInlineWord('lifetime', text) ?? readInlineWord('lifetimeRegion', text) ?? readInlineWord('lifetimeRegionId', text), mode: readInlineWord('mode', text) ?? 'unknown', access: readInlineWord('access', text) });
94
+ if (kind === 'alias') return cleanRecord({ ...common, resourceId: readInlineWord('resource', text) ?? readInlineWord('resourceId', text), ownerId: readInlineWord('owner', text) ?? readInlineWord('ownerId', text), aliasId: readInlineWord('alias', text) ?? readInlineWord('aliasId', text), aliasKind: readInlineWord('aliasKind', text) ?? readInlineWord('kind', text) ?? 'alias' });
95
+ if (kind === 'move') return cleanRecord({ ...common, resourceId: readInlineWord('resource', text) ?? readInlineWord('resourceId', text), fromOwnerId: readInlineWord('fromOwner', text) ?? readInlineWord('fromOwnerId', text), toOwnerId: readInlineWord('toOwner', text) ?? readInlineWord('toOwnerId', text), moveKind: readInlineWord('moveKind', text) ?? readInlineWord('kind', text) });
96
+ if (kind === 'drop') return cleanRecord({ ...common, resourceId: readInlineWord('resource', text) ?? readInlineWord('resourceId', text), ownerId: readInlineWord('owner', text) ?? readInlineWord('ownerId', text), lifetimeRegionId: readInlineWord('lifetime', text) ?? readInlineWord('lifetimeRegion', text) ?? readInlineWord('lifetimeRegionId', text), dropKind: readInlineWord('dropKind', text) ?? readInlineWord('kind', text) ?? 'drop', line: readInlineNumber('line', text), order: readInlineNumber('order', text) });
97
+ if (kind === 'escape') return cleanRecord({ ...common, resourceId: readInlineWord('resource', text) ?? readInlineWord('resourceId', text), ownerId: readInlineWord('owner', text) ?? readInlineWord('ownerId', text), lifetimeRegionId: readInlineWord('lifetime', text) ?? readInlineWord('lifetimeRegion', text) ?? readInlineWord('lifetimeRegionId', text), loanId: readInlineWord('loan', text) ?? readInlineWord('loanId', text), escapeKind: readInlineWord('escapeKind', text) ?? readInlineWord('kind', text) ?? 'escape', status: readInlineWord('status', text) ?? 'needs-proof' });
98
+ if (kind === 'lifetimeRegion') return cleanRecord({ ...common, lifetimeKind: readInlineWord('lifetimeKind', text) ?? readInlineWord('kind', text) ?? 'lexical', startLine: readInlineNumber('startLine', text), endLine: readInlineNumber('endLine', text) });
99
+ if (kind === 'lifetimeRelation') return cleanRecord({ ...common, relationKind: readInlineWord('relationKind', text) ?? readInlineWord('kind', text) ?? 'outlives', fromLifetimeId: readInlineWord('fromLifetime', text) ?? readInlineWord('fromLifetimeId', text) ?? readInlineWord('from', text), toLifetimeId: readInlineWord('toLifetime', text) ?? readInlineWord('toLifetimeId', text) ?? readInlineWord('to', text), from: readInlineWord('from', text), to: readInlineWord('to', text) });
100
+ if (kind === 'borrowScope') return cleanRecord({ ...common, scopeKind: readInlineWord('scopeKind', text) ?? readInlineWord('kind', text) ?? 'borrow-scope', constraintKind: readInlineWord('constraintKind', text), constraintKinds: readInlineList(text, 'constraint', 'constraints', 'constraintKind', 'constraintKinds') ?? [], ownershipKind: readInlineWord('ownershipKind', text), lifetimeKind: readInlineWord('lifetimeKind', text), controlFlowKind: readInlineWord('controlFlowKind', text), sourceControlFlowId: readInlineWord('sourceControlFlow', text) ?? readInlineWord('sourceControlFlowId', text), lifetimeRegionId: readInlineWord('lifetime', text) ?? readInlineWord('lifetimeRegion', text) ?? readInlineWord('lifetimeRegionId', text), resourceId: readInlineWord('resource', text) ?? readInlineWord('resourceId', text) });
101
+ if (kind === 'unsafeBoundary') return cleanRecord({ ...common, resourceId: readInlineWord('resource', text) ?? readInlineWord('resourceId', text), unsafeBoundary: true, proofStatus: readInlineWord('proofStatus', text) ?? readInlineWord('status', text) ?? 'missing', kind: readInlineWord('kind', text) });
102
+ if (kind === 'conflict') return cleanRecord({ ...common, resourceId: readInlineWord('resource', text) ?? readInlineWord('resourceId', text), ownerId: readInlineWord('owner', text) ?? readInlineWord('ownerId', text), loanId: readInlineWord('loan', text) ?? readInlineWord('loanId', text), aliasId: readInlineWord('alias', text) ?? readInlineWord('aliasId', text), unsafeBoundaryId: readInlineWord('unsafeBoundary', text) ?? readInlineWord('unsafeBoundaryId', text), reasonCode: readInlineWord('reasonCode', text), message: readInlineQuoted('message', text), status: readInlineWord('status', text) ?? 'open', severity: readInlineWord('severity', text) ?? 'error' });
103
+ if (kind === 'proofObligation') return cleanRecord({ ...common, resourceId: readInlineWord('resource', text) ?? readInlineWord('resourceId', text), conflictId: readInlineWord('conflict', text) ?? readInlineWord('conflictId', text), kind: readInlineWord('kind', text), status: readInlineWord('status', text) ?? 'open', statement: readInlineQuoted('statement', text) });
104
+ return undefined;
105
+ }
106
+
107
+ function commonRecord(kind, name, text, graph) {
108
+ return cleanRecord({
109
+ recordKind: recordKind(kind),
110
+ id: idFrom(text, `${recordPrefix(kind)}_${name}`),
111
+ name,
112
+ sourcePath: readInlineWord('sourcePath', text) ?? readInlineWord('path', text) ?? graph.sourcePath,
113
+ sourceHash: readInlineWord('sourceHash', text) ?? graph.sourceHash,
114
+ evidenceIds: readInlineList(text, 'evidence', 'evidenceIds') ?? graph.evidenceIds,
115
+ metadata: { authoredName: name }
116
+ });
117
+ }
118
+
119
+ function summarize(graph) {
120
+ return {
121
+ records: allRecords(graph).length,
122
+ resources: graph.resources.length,
123
+ owners: graph.owners.length,
124
+ loans: graph.loans.length,
125
+ aliases: graph.aliases.length,
126
+ moves: graph.moves.length,
127
+ drops: graph.drops.length,
128
+ escapes: graph.escapes.length,
129
+ lifetimeRegions: graph.lifetimeRegions.length,
130
+ lifetimeRelations: graph.lifetimeRelations.length,
131
+ borrowScopes: graph.borrowScopes.length,
132
+ unsafeBoundaries: graph.unsafeBoundaries.length,
133
+ conflicts: graph.conflicts.length,
134
+ proofObligations: graph.proofObligations.length,
135
+ unsafeBoundariesWithoutProof: graph.unsafeBoundaries.filter((record) => record.proofStatus !== 'passed').length,
136
+ reasonCodes: unique(graph.conflicts.map((record) => record.reasonCode))
137
+ };
138
+ }
139
+
140
+ function allRecords(graph) {
141
+ return [
142
+ ...graph.resources,
143
+ ...graph.owners,
144
+ ...graph.loans,
145
+ ...graph.aliases,
146
+ ...graph.moves,
147
+ ...graph.drops,
148
+ ...graph.escapes,
149
+ ...graph.lifetimeRegions,
150
+ ...graph.lifetimeRelations,
151
+ ...graph.borrowScopes,
152
+ ...graph.unsafeBoundaries,
153
+ ...graph.conflicts,
154
+ ...graph.proofObligations
155
+ ];
156
+ }
157
+
158
+ function normalizeRowKind(kind) {
159
+ if (kind === 'lifetime' || kind === 'life') return 'lifetimeRegion';
160
+ if (kind === 'outlives' || kind === 'lifetimeRelation' || kind === 'lifeRelation') return 'lifetimeRelation';
161
+ if (kind === 'borrow' || kind === 'borrowScope' || kind === 'borrowRegion') return 'borrowScope';
162
+ if (kind === 'unsafe' || kind === 'unsafeBoundary') return 'unsafeBoundary';
163
+ if (kind === 'proof' || kind === 'obligation' || kind === 'proofObligation') return 'proofObligation';
164
+ return kind;
165
+ }
166
+
167
+ function recordKind(kind) {
168
+ if (kind === 'lifetimeRegion') return 'lifetime-region';
169
+ if (kind === 'lifetimeRelation') return 'lifetime-relation';
170
+ if (kind === 'borrowScope') return 'borrow-scope';
171
+ if (kind === 'unsafeBoundary') return 'unsafe-boundary';
172
+ if (kind === 'proofObligation') return 'proof-obligation';
173
+ return kind;
174
+ }
175
+
176
+ function recordPrefix(kind) {
177
+ return recordKind(kind).replace(/-/g, '_');
178
+ }
179
+
180
+ function isGraphPropertyLine(line) {
181
+ return /^(sourceLanguage|language|sourcePath|path|sourceHash|status|evidence|evidenceIds)\s+/.test(line);
182
+ }
183
+
184
+ function idFrom(text, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(text)?.[1] ?? fallback; }
185
+ function nameFrom(header) { return /^([A-Za-z_$][\w$]*)/.exec(header)?.[1] ?? 'ResourceGraph'; }
186
+ function readLine(label, body) { return new RegExp('^\\s*' + label + '\\s+([^\\n]+)', 'm').exec(body)?.[1]?.trim(); }
187
+ function readListLine(label, body) {
188
+ const line = readLine(label, body);
189
+ return line ? line.split(/[|,]/).map((item) => item.trim()).filter(Boolean) : undefined;
190
+ }
191
+ function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
192
+ function readInlineQuoted(label, text) { return new RegExp("(?:^|\\s)" + label + "\\s+[\"']([^\"']+)[\"']").exec(text)?.[1]?.trim(); }
193
+ function readInlineNumber(label, text) {
194
+ const value = readInlineWord(label, text);
195
+ return value === undefined ? undefined : Number(value);
196
+ }
197
+ function readInlineList(text, ...labels) {
198
+ for (const label of labels) {
199
+ const value = new RegExp('(?:^|\\s)' + label + '\\s+([^\\s]+)').exec(text)?.[1]?.trim();
200
+ if (value) return value.split(/[|,]/).map((item) => item.trim()).filter(Boolean);
201
+ }
202
+ return undefined;
203
+ }
204
+ function ids(records = []) { return records.map((record) => record?.id).filter(Boolean); }
205
+ function unique(values = []) { return [...new Set(values.filter(Boolean))]; }
206
+ function cleanRecord(record) {
207
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
208
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-parser",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
4
4
  "description": "Parser for the first Frontier Lang .frontier syntax slice.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "scripts": {
24
24
  "build": "node scripts/build.mjs",
25
- "test": "npm run build && node test/smoke.mjs",
25
+ "test": "npm run build && node test/smoke.mjs && node test/resource-graph-smoke.mjs",
26
26
  "typecheck": "node ./node_modules/typescript/bin/tsc --noEmit -p test/tsconfig.json",
27
27
  "fuzz": "npm run build && node fuzz/smoke.mjs",
28
28
  "bench": "npm run build && node bench/smoke.mjs",
@@ -54,7 +54,7 @@
54
54
  "access": "public"
55
55
  },
56
56
  "dependencies": {
57
- "@shapeshift-labs/frontier-lang-kernel": "0.3.12"
57
+ "@shapeshift-labs/frontier-lang-kernel": "0.3.13"
58
58
  },
59
59
  "devDependencies": {
60
60
  "typescript": "^5.9.3"