@shapeshift-labs/frontier-lang-parser 0.3.17 → 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 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.
@@ -239,6 +262,27 @@ conversion TodoJavascriptToRust @id("conversion_todo_js_rust") {
239
262
 
240
263
  `sourceRuntime` and `targetRuntime` become runtime maps. `runtimeRequirement` rows become proof obligations for host/runtime capabilities, including authored `requiredSignals` denominators such as source hashes, target hashes, probe ids, runtime commands, telemetry hashes, and capability-specific trace hashes. `proofEvidence` and `evidence` attach evidence ids, but the compiler still requires bound evidence records before a proof obligation is satisfied. `dialect` and `extern` rows preserve dialect-specific constructs, projection readiness, loss/evidence ids, and binding metadata without requiring the authored Frontier file to drop down to raw JSON.
241
264
 
265
+ ## Authored possibility spaces
266
+
267
+ `.frontier` files can describe a governed space of valid implementations with `constraintSpace` or `possibilitySpace` blocks. These blocks are metadata-only: they do not add kernel nodes, but they preserve the variables, hard constraints, preferences, collapse strategies, and admission rules that let tools reason about semantic merge, translation, refactoring, and runtime projection as constraint satisfaction.
268
+
269
+ ```frontier
270
+ possibilitySpace CheckoutSurface @id("space_checkout_surface") {
271
+ subject action_checkout
272
+ scope mod_checkout
273
+ target react
274
+ target swiftui
275
+ variable surface @id("space_variable_surface") kind projection domain react|swiftui|html-css-js default react preserve identity|event-flow
276
+ hard identity @id("space_constraint_identity") kind semantic-identity family identity subject action_checkout requires action|state|effect failClosed
277
+ soft bundleSize @id("space_constraint_bundle_size") kind bundle-budget family runtime target react predicate "bundle < 50kb"
278
+ preference nativeControls @id("space_preference_native_controls") kind platform-idiom target swiftui weight 0.8 reason "prefer native controls on iOS"
279
+ collapse mobileCheckout @id("space_collapse_mobile_checkout") strategy evidence-first target swiftui requires identity|runtime-proof produces view_checkout_mobile
280
+ admission mergeSafe @id("space_admission_merge_safe") kind semantic-merge status open requires hardConstraints|runtimeProof decision review failClosed
281
+ }
282
+ ```
283
+
284
+ The parser projects these blocks into `metadata.constraintSpaces`. Hard and soft constraints remain separate from preferences: hard constraints define validity, while preferences help choose among several valid shapes. `collapse` rows describe how a tool may choose a concrete projection, and `admission` rows describe what proof is required before a generated or merged shape should be accepted.
285
+
242
286
  ## Authored native source evidence
243
287
 
244
288
  `nativeSource` blocks can also carry source-bound merge evidence. This keeps parser/source-map/merge-candidate facts in `.frontier` text instead of requiring a raw JSON sidecar for the first authored program slice.
@@ -0,0 +1,152 @@
1
+ export function parseConstraintSpaceBlock(block) {
2
+ const name = nameFrom(block.header);
3
+ const space = {
4
+ id: idFrom(block.header, `constraint_space_${name}`),
5
+ name,
6
+ targets: [],
7
+ variables: [],
8
+ constraints: [],
9
+ preferences: [],
10
+ collapseStrategies: [],
11
+ admissions: [],
12
+ metadata: { name }
13
+ };
14
+ for (const rawLine of block.body.split('\n')) {
15
+ const line = rawLine.trim();
16
+ if (!line || line.startsWith('#')) continue;
17
+ const target = /^target\s+([^\s,]+)/.exec(line)?.[1];
18
+ const subject = /^subject\s+([^\s,]+)/.exec(line)?.[1];
19
+ const scope = /^scope\s+([^\s,]+)/.exec(line)?.[1];
20
+ const record = /^(variable|var|constraint|hard|soft|preference|prefer|collapse|admission)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
21
+ if (target) space.targets.push(target);
22
+ else if (subject) space.subjectId = subject;
23
+ else if (scope) space.scopeId = scope;
24
+ else if (record) addConstraintSpaceRecord(space, record[1], record[2], record[3]);
25
+ }
26
+ return cleanRecord({
27
+ ...space,
28
+ targets: unique(space.targets),
29
+ summary: {
30
+ variableCount: space.variables.length,
31
+ constraintCount: space.constraints.length,
32
+ preferenceCount: space.preferences.length,
33
+ collapseStrategyCount: space.collapseStrategies.length,
34
+ admissionCount: space.admissions.length
35
+ }
36
+ });
37
+ }
38
+
39
+ function addConstraintSpaceRecord(space, section, name, text) {
40
+ if (section === 'variable' || section === 'var') space.variables.push(parseVariable(name, text));
41
+ else if (section === 'constraint' || section === 'hard' || section === 'soft') space.constraints.push(parseConstraint(name, text, section));
42
+ else if (section === 'preference' || section === 'prefer') space.preferences.push(parsePreference(name, text));
43
+ else if (section === 'collapse') space.collapseStrategies.push(parseCollapseStrategy(name, text));
44
+ else if (section === 'admission') space.admissions.push(parseAdmission(name, text));
45
+ }
46
+
47
+ function parseVariable(name, text) {
48
+ return cleanRecord({
49
+ id: idFrom(text, `constraint_space_variable_${name}`),
50
+ name,
51
+ kind: readInlineWord('kind', text),
52
+ domain: readInlineList(text, 'domain', 'choices', 'values'),
53
+ default: readInlineWord('default', text),
54
+ subjectId: readInlineWord('subject', text) ?? readInlineWord('subjectId', text),
55
+ sourceId: readInlineWord('source', text) ?? readInlineWord('sourceId', text),
56
+ target: readInlineWord('target', text),
57
+ preserve: readInlineList(text, 'preserve', 'preserves'),
58
+ evidenceIds: readInlineList(text, 'evidence', 'evidenceIds'),
59
+ metadata: { name }
60
+ });
61
+ }
62
+
63
+ function parseConstraint(name, text, section) {
64
+ return cleanRecord({
65
+ id: idFrom(text, `constraint_space_constraint_${name}`),
66
+ name,
67
+ kind: readInlineWord('kind', text),
68
+ strength: readInlineWord('strength', text) ?? (section === 'hard' ? 'hard' : section === 'soft' ? 'soft' : undefined),
69
+ family: readInlineWord('family', text),
70
+ subjectId: readInlineWord('subject', text) ?? readInlineWord('subjectId', text),
71
+ variableIds: readInlineList(text, 'variable', 'variables', 'variableIds'),
72
+ target: readInlineWord('target', text),
73
+ predicate: readInlineQuoted('predicate', text) ?? readInlineWord('predicate', text),
74
+ requires: readInlineList(text, 'requires', 'required', 'require'),
75
+ evidenceIds: readInlineList(text, 'evidence', 'evidenceIds'),
76
+ proofObligationIds: readInlineList(text, 'proofObligation', 'proofObligations', 'proofObligationIds'),
77
+ conflictKeys: readInlineList(text, 'conflictKey', 'conflictKeys'),
78
+ failClosed: readInlineFlag('failClosed', text),
79
+ metadata: { name }
80
+ });
81
+ }
82
+
83
+ function parsePreference(name, text) {
84
+ return cleanRecord({
85
+ id: idFrom(text, `constraint_space_preference_${name}`),
86
+ name,
87
+ kind: readInlineWord('kind', text),
88
+ weight: readNumber(readInlineWord('weight', text)),
89
+ subjectId: readInlineWord('subject', text) ?? readInlineWord('subjectId', text),
90
+ variableIds: readInlineList(text, 'variable', 'variables', 'variableIds'),
91
+ target: readInlineWord('target', text),
92
+ prefer: readInlineList(text, 'prefer', 'prefers'),
93
+ reason: readInlineQuoted('reason', text) ?? readInlineWord('reason', text),
94
+ evidenceIds: readInlineList(text, 'evidence', 'evidenceIds'),
95
+ metadata: { name }
96
+ });
97
+ }
98
+
99
+ function parseCollapseStrategy(name, text) {
100
+ return cleanRecord({
101
+ id: idFrom(text, `constraint_space_collapse_${name}`),
102
+ name,
103
+ strategy: readInlineWord('strategy', text) ?? readInlineWord('kind', text),
104
+ target: readInlineWord('target', text),
105
+ variableIds: readInlineList(text, 'variable', 'variables', 'variableIds'),
106
+ requires: readInlineList(text, 'requires', 'required', 'require'),
107
+ produces: readInlineList(text, 'produces', 'produce', 'outputs', 'output'),
108
+ evidenceIds: readInlineList(text, 'evidence', 'evidenceIds'),
109
+ admissionIds: readInlineList(text, 'admission', 'admissions', 'admissionIds'),
110
+ status: readInlineWord('status', text),
111
+ metadata: { name }
112
+ });
113
+ }
114
+
115
+ function parseAdmission(name, text) {
116
+ return cleanRecord({
117
+ id: idFrom(text, `constraint_space_admission_${name}`),
118
+ name,
119
+ kind: readInlineWord('kind', text),
120
+ status: readInlineWord('status', text),
121
+ subjectId: readInlineWord('subject', text) ?? readInlineWord('subjectId', text),
122
+ target: readInlineWord('target', text),
123
+ requires: readInlineList(text, 'requires', 'required', 'require'),
124
+ evidenceIds: readInlineList(text, 'evidence', 'evidenceIds'),
125
+ decision: readInlineWord('decision', text),
126
+ reason: readInlineQuoted('reason', text) ?? readInlineWord('reason', text),
127
+ failClosed: readInlineFlag('failClosed', text),
128
+ metadata: { name }
129
+ });
130
+ }
131
+
132
+ function idFrom(text, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(text)?.[1] ?? fallback; }
133
+ function nameFrom(header) { return /^([A-Za-z_$][\w$]*)/.exec(header)?.[1] ?? 'ConstraintSpace'; }
134
+ function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
135
+ function readInlineQuoted(label, text) { return new RegExp("(?:^|\\s)" + label + "\\s+[\"']([^\"']+)[\"']").exec(text)?.[1]?.trim(); }
136
+ function readInlineFlag(label, text) { return new RegExp('(?:^|\\s)' + label + '(?:\\s|$)').test(text) || undefined; }
137
+ function readInlineList(text, ...labels) {
138
+ for (const label of labels) {
139
+ const value = new RegExp('(?:^|\\s)' + label + '\\s+([^\\s]+)').exec(text)?.[1]?.trim();
140
+ if (value) return value.split(/[|,]/).map((item) => item.trim()).filter(Boolean);
141
+ }
142
+ return undefined;
143
+ }
144
+ function readNumber(value) {
145
+ if (value === undefined) return undefined;
146
+ const number = Number(value);
147
+ return Number.isFinite(number) ? number : undefined;
148
+ }
149
+ function cleanRecord(record) {
150
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0)));
151
+ }
152
+ function unique(values) { return [...new Set(values.filter(Boolean))]; }
@@ -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,5 +1,7 @@
1
1
  import { actionNode, capabilityNode, createDocument, effectNode, entityNode, externNode, latticeNode, migrationNode, stateNode, targetNode, typeNode } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import { parseConstraintSpaceBlock } from './constraint-space.js';
2
3
  import { parseConversionBlock } from './conversion.js';
4
+ import { parseDecisionGraphBlock } from './decision-graph.js';
3
5
  import { createParsedMetadata } from './metadata.js';
4
6
  import { parseSemanticOperationsBlock } from './operations.js';
5
7
  import { parseParadigmBlock } from './paradigm.js';
@@ -13,6 +15,8 @@ export function parseFrontierSource(source, options = {}) {
13
15
  const paradigmBlocks = [];
14
16
  const operationBlocks = [];
15
17
  const conversionBlocks = [];
18
+ const constraintSpaceBlocks = [];
19
+ const decisionGraphBlocks = [];
16
20
  const nativeSourceBlocks = [];
17
21
  const documentId = options.id ?? readId(source) ?? 'mod_frontier';
18
22
  const documentName = options.name ?? readName(source) ?? 'FrontierModule';
@@ -37,8 +41,10 @@ export function parseFrontierSource(source, options = {}) {
37
41
  if (block.kind === 'paradigm' || block.kind === 'paradigmSemantics') paradigmBlocks.push(parseParadigmBlock(block));
38
42
  if (block.kind === 'operations' || block.kind === 'semanticOperations') operationBlocks.push(parseSemanticOperationsBlock(block));
39
43
  if (block.kind === 'conversion' || block.kind === 'universalConversionPlan') conversionBlocks.push(parseConversionBlock(block));
44
+ if (block.kind === 'constraintSpace' || block.kind === 'possibilitySpace') constraintSpaceBlocks.push(parseConstraintSpaceBlock(block));
45
+ if (block.kind === 'decisionGraph' || block.kind === 'admissionGraph') decisionGraphBlocks.push(parseDecisionGraphBlock(block));
40
46
  }
41
- const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, nativeSourceBlocks });
47
+ const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, constraintSpaceBlocks, decisionGraphBlocks, nativeSourceBlocks });
42
48
  return createDocument({ id: documentId, name: documentName, nodes, ...(metadata ? { metadata } : {}) });
43
49
  }
44
50
 
@@ -48,7 +54,7 @@ function readName(source) { return /module\s+([A-Za-z_$][\w$]*)/.exec(source)?.[
48
54
  function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']+)["']\s*\)/.exec(source)?.[1]; }
49
55
  function readBlocks(source) {
50
56
  const blocks = [];
51
- const header = /\b(entity|state|action|view|migration|capability|effect|type|extern|lattice|nativeSource|target|proof|paradigm|paradigmSemantics|operations|semanticOperations|conversion|universalConversionPlan)\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;
52
58
  let match;
53
59
  while ((match = header.exec(source))) {
54
60
  let depth = 1; let index = header.lastIndex;
package/dist/metadata.js CHANGED
@@ -21,12 +21,14 @@ const PARADIGM_GROUPS = [
21
21
  'loweringRecords'
22
22
  ];
23
23
 
24
- export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], operationBlocks = [], conversionBlocks = [], 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
+ if (constraintSpaceBlocks.length) metadata.constraintSpaces = mergeConstraintSpaceBlocks(constraintSpaceBlocks);
31
+ if (decisionGraphBlocks.length) metadata.decisionGraph = mergeDecisionGraphBlocks(decisionGraphBlocks);
30
32
  if (nativeSourceBlocks.some((block) => block.sourceMaps.length || block.mergeCandidates.length || block.evidence.length)) {
31
33
  metadata.universalAst = mergeNativeSourceBlocks(nativeSourceBlocks);
32
34
  }
@@ -77,6 +79,28 @@ function mergeConversionBlocks(blocks) {
77
79
  return plan;
78
80
  }
79
81
 
82
+ function mergeConstraintSpaceBlocks(blocks) {
83
+ return {
84
+ id: blocks.length === 1 ? blocks[0].id : 'constraintSpaces:source',
85
+ spaces: blocks,
86
+ targets: [...new Set(blocks.flatMap((block) => block.targets ?? []))],
87
+ variableIds: blocks.flatMap((block) => ids(block.variables)),
88
+ constraintIds: blocks.flatMap((block) => ids(block.constraints)),
89
+ preferenceIds: blocks.flatMap((block) => ids(block.preferences)),
90
+ collapseStrategyIds: blocks.flatMap((block) => ids(block.collapseStrategies)),
91
+ admissionIds: blocks.flatMap((block) => ids(block.admissions)),
92
+ summary: {
93
+ spaceCount: blocks.length,
94
+ variableCount: sum(blocks, 'variableCount'),
95
+ constraintCount: sum(blocks, 'constraintCount'),
96
+ preferenceCount: sum(blocks, 'preferenceCount'),
97
+ collapseStrategyCount: sum(blocks, 'collapseStrategyCount'),
98
+ admissionCount: sum(blocks, 'admissionCount')
99
+ },
100
+ metadata: { authoredConstraintSpaceBlockIds: blocks.map((block) => block.id) }
101
+ };
102
+ }
103
+
80
104
  function mergeNativeSourceBlocks(blocks) {
81
105
  return {
82
106
  id: blocks.length === 1 ? `universalAst:${blocks[0].node.id}` : 'universalAst:source',
@@ -88,3 +112,54 @@ function mergeNativeSourceBlocks(blocks) {
88
112
  metadata: { authoredNativeSourceIds: blocks.map((block) => block.node.id) }
89
113
  };
90
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
+
155
+ function ids(records = []) {
156
+ return records.map((record) => record?.id).filter(Boolean);
157
+ }
158
+
159
+ function idsByKind(records = [], kind) {
160
+ return ids(records.filter((record) => record?.recordKind === kind || record?.kind === kind));
161
+ }
162
+
163
+ function sum(blocks, key) {
164
+ return blocks.reduce((total, block) => total + (block.summary?.[key] ?? 0), 0);
165
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-parser",
3
- "version": "0.3.17",
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.12"
57
+ "@shapeshift-labs/frontier-lang-kernel": "0.3.13"
58
58
  },
59
59
  "devDependencies": {
60
60
  "typescript": "^5.9.3"