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

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
@@ -239,6 +239,27 @@ conversion TodoJavascriptToRust @id("conversion_todo_js_rust") {
239
239
 
240
240
  `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
241
 
242
+ ## Authored possibility spaces
243
+
244
+ `.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.
245
+
246
+ ```frontier
247
+ possibilitySpace CheckoutSurface @id("space_checkout_surface") {
248
+ subject action_checkout
249
+ scope mod_checkout
250
+ target react
251
+ target swiftui
252
+ variable surface @id("space_variable_surface") kind projection domain react|swiftui|html-css-js default react preserve identity|event-flow
253
+ hard identity @id("space_constraint_identity") kind semantic-identity family identity subject action_checkout requires action|state|effect failClosed
254
+ soft bundleSize @id("space_constraint_bundle_size") kind bundle-budget family runtime target react predicate "bundle < 50kb"
255
+ preference nativeControls @id("space_preference_native_controls") kind platform-idiom target swiftui weight 0.8 reason "prefer native controls on iOS"
256
+ collapse mobileCheckout @id("space_collapse_mobile_checkout") strategy evidence-first target swiftui requires identity|runtime-proof produces view_checkout_mobile
257
+ admission mergeSafe @id("space_admission_merge_safe") kind semantic-merge status open requires hardConstraints|runtimeProof decision review failClosed
258
+ }
259
+ ```
260
+
261
+ 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.
262
+
242
263
  ## Authored native source evidence
243
264
 
244
265
  `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))]; }
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
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';
3
4
  import { createParsedMetadata } from './metadata.js';
4
5
  import { parseSemanticOperationsBlock } from './operations.js';
@@ -13,6 +14,7 @@ export function parseFrontierSource(source, options = {}) {
13
14
  const paradigmBlocks = [];
14
15
  const operationBlocks = [];
15
16
  const conversionBlocks = [];
17
+ const constraintSpaceBlocks = [];
16
18
  const nativeSourceBlocks = [];
17
19
  const documentId = options.id ?? readId(source) ?? 'mod_frontier';
18
20
  const documentName = options.name ?? readName(source) ?? 'FrontierModule';
@@ -37,8 +39,9 @@ export function parseFrontierSource(source, options = {}) {
37
39
  if (block.kind === 'paradigm' || block.kind === 'paradigmSemantics') paradigmBlocks.push(parseParadigmBlock(block));
38
40
  if (block.kind === 'operations' || block.kind === 'semanticOperations') operationBlocks.push(parseSemanticOperationsBlock(block));
39
41
  if (block.kind === 'conversion' || block.kind === 'universalConversionPlan') conversionBlocks.push(parseConversionBlock(block));
42
+ if (block.kind === 'constraintSpace' || block.kind === 'possibilitySpace') constraintSpaceBlocks.push(parseConstraintSpaceBlock(block));
40
43
  }
41
- const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, nativeSourceBlocks });
44
+ const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, constraintSpaceBlocks, nativeSourceBlocks });
42
45
  return createDocument({ id: documentId, name: documentName, nodes, ...(metadata ? { metadata } : {}) });
43
46
  }
44
47
 
@@ -48,7 +51,7 @@ function readName(source) { return /module\s+([A-Za-z_$][\w$]*)/.exec(source)?.[
48
51
  function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']+)["']\s*\)/.exec(source)?.[1]; }
49
52
  function readBlocks(source) {
50
53
  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;
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;
52
55
  let match;
53
56
  while ((match = header.exec(source))) {
54
57
  let depth = 1; let index = header.lastIndex;
package/dist/metadata.js CHANGED
@@ -21,12 +21,13 @@ 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 = [], 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);
30
31
  if (nativeSourceBlocks.some((block) => block.sourceMaps.length || block.mergeCandidates.length || block.evidence.length)) {
31
32
  metadata.universalAst = mergeNativeSourceBlocks(nativeSourceBlocks);
32
33
  }
@@ -77,6 +78,28 @@ function mergeConversionBlocks(blocks) {
77
78
  return plan;
78
79
  }
79
80
 
81
+ function mergeConstraintSpaceBlocks(blocks) {
82
+ return {
83
+ id: blocks.length === 1 ? blocks[0].id : 'constraintSpaces:source',
84
+ spaces: blocks,
85
+ targets: [...new Set(blocks.flatMap((block) => block.targets ?? []))],
86
+ variableIds: blocks.flatMap((block) => ids(block.variables)),
87
+ constraintIds: blocks.flatMap((block) => ids(block.constraints)),
88
+ preferenceIds: blocks.flatMap((block) => ids(block.preferences)),
89
+ collapseStrategyIds: blocks.flatMap((block) => ids(block.collapseStrategies)),
90
+ admissionIds: blocks.flatMap((block) => ids(block.admissions)),
91
+ summary: {
92
+ spaceCount: blocks.length,
93
+ variableCount: sum(blocks, 'variableCount'),
94
+ constraintCount: sum(blocks, 'constraintCount'),
95
+ preferenceCount: sum(blocks, 'preferenceCount'),
96
+ collapseStrategyCount: sum(blocks, 'collapseStrategyCount'),
97
+ admissionCount: sum(blocks, 'admissionCount')
98
+ },
99
+ metadata: { authoredConstraintSpaceBlockIds: blocks.map((block) => block.id) }
100
+ };
101
+ }
102
+
80
103
  function mergeNativeSourceBlocks(blocks) {
81
104
  return {
82
105
  id: blocks.length === 1 ? `universalAst:${blocks[0].node.id}` : 'universalAst:source',
@@ -88,3 +111,11 @@ function mergeNativeSourceBlocks(blocks) {
88
111
  metadata: { authoredNativeSourceIds: blocks.map((block) => block.node.id) }
89
112
  };
90
113
  }
114
+
115
+ function ids(records = []) {
116
+ return records.map((record) => record?.id).filter(Boolean);
117
+ }
118
+
119
+ function sum(blocks, key) {
120
+ return blocks.reduce((total, block) => total + (block.summary?.[key] ?? 0), 0);
121
+ }
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.18",
4
4
  "description": "Parser for the first Frontier Lang .frontier syntax slice.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",