@shapeshift-labs/frontier-lang-parser 0.3.27 → 0.3.28

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
@@ -260,6 +260,55 @@ and events stay attached to the child render node instead of leaking onto the
260
260
  parent. `kind component` records `component` instead of a literal HTML tag, so
261
261
  target adapters can decide how to project it.
262
262
 
263
+ ## Authored package and canvas syntax
264
+
265
+ `.frontier` files can describe package-management and canvas semantic surfaces
266
+ directly, without requiring the source of truth to be a generated JSON tree.
267
+ These blocks make dependency, script, export, draw-command, command-trace, and
268
+ proof-gap records queryable as source-level evidence. They intentionally do not
269
+ claim package install equivalence, canvas runtime equivalence, or visual
270
+ equivalence.
271
+
272
+ ```frontier
273
+ packageManifest AppPackage @id("pkg_manifest_app") {
274
+ sourcePath package.json
275
+ sourceHash sha256:package
276
+ packageManager npm@11.0.0
277
+ evidence packageProbe @id("evidence_package_probe") kind test status passed path reports/package.json
278
+ metadata name @id("pkg_meta_name") value "@example/app" evidence evidence_package_probe
279
+ dependency react @id("pkg_dep_react") section dependencies range ^19.0.0 evidence evidence_package_probe
280
+ dependency typescript @id("pkg_dep_typescript") section peerDependencies range ^5.9.0 proofGap package-peer-compatibility-boundary evidence evidence_package_probe
281
+ script test @id("pkg_script_test") command "vitest --run" proofGap package-script-runtime-boundary evidence evidence_package_probe
282
+ export root @id("pkg_export_root") section exports name . target ./dist/index.js proofGap package-conditional-resolution-boundary evidence evidence_package_probe
283
+ gap workspace @id("pkg_gap_workspace") code package-workspace-graph-boundary summary "Workspace expansion requires repository graph evidence."
284
+ }
285
+
286
+ canvasSurface PreviewCanvas @id("canvas_surface_preview") {
287
+ sourcePath src/draw.js
288
+ sourceHash sha256:draw
289
+ evidence canvasProbe @id("evidence_canvas_probe") kind browser-probe status passed path reports/canvas.json
290
+ element preview @id("canvas_element_preview") name canvas category html-canvas order 1 identity canvas:preview attributes data-frontier-key=preview|width=100 evidence evidence_canvas_probe
291
+ command context @id("canvas_command_context") name getContext category context context 2d order 2 proofGap canvas-context-runtime-boundary evidence evidence_canvas_probe
292
+ state fillStyle @id("canvas_state_fill_style") name fillStyle category state order 3 proofGap canvas-stateful-render-order-boundary evidence evidence_canvas_probe
293
+ command fill @id("canvas_command_fill") name fillRect category draw context 2d order 4 proofGap canvas-stateful-render-order-boundary evidence evidence_canvas_probe
294
+ command offscreen @id("canvas_command_offscreen") name transferControlToOffscreen category offscreen order 5 proofGap canvas-offscreen-worker-boundary evidence evidence_canvas_probe
295
+ trace drawFrame @id("canvas_trace_draw_frame") commands getContext|fillStyle|fillRect|transferControlToOffscreen evidence evidence_canvas_probe
296
+ gap image @id("canvas_gap_image") code canvas-image-resource-boundary summary "Image drawing needs bitmap/resource evidence."
297
+ }
298
+ ```
299
+
300
+ The parser stores package blocks in `metadata.packageManifests` and canvas
301
+ blocks in `metadata.canvasSurfaces`, and mirrors both under
302
+ `metadata.universalAst` so compiler and merge tooling can see them next to
303
+ native-source source maps and merge candidates. Package records keep
304
+ `packageInstallEquivalenceClaim`, `installEquivalenceClaim`, and
305
+ `runtimeEquivalenceClaim` false. Canvas records keep
306
+ `browserRuntimeEquivalenceClaim`, `canvasRuntimeEquivalenceClaim`, and
307
+ `canvasVisualEquivalenceClaim` false.
308
+ Runtime probes, package-manager solver output, lockfile proof, browser pixels,
309
+ worker transfer evidence, and GPU validation must still be supplied by higher
310
+ layers before admission.
311
+
263
312
  ## Authored target projection syntax
264
313
 
265
314
  `.frontier` target blocks can carry projection contracts next to their emit settings. These rows describe what a target lowering claims to represent, what it still needs proof for, and which losses or missing evidence must stay visible to merge and translation tooling.
@@ -0,0 +1,206 @@
1
+ import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+
3
+ export function parseCanvasSurfaceBlock(block) {
4
+ const name = nameFrom(block.header);
5
+ const sourcePath = readLine('sourcePath', block.body) ?? readLine('path', block.body);
6
+ const sourceHash = readLine('sourceHash', block.body);
7
+ const evidence = parseEvidenceRows(block.body);
8
+ const records = [];
9
+ const commandTraces = [];
10
+ const proofGaps = [];
11
+ for (const rawLine of block.body.split('\n')) {
12
+ const line = rawLine.trim();
13
+ if (!line || line.startsWith('#') || isPropertyLine(line)) continue;
14
+ const match = /^(element|command|state|stateWrite|trace|gap|proofGap)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
15
+ if (!match) continue;
16
+ const [, rowKind, rowName, rest] = match;
17
+ if (rowKind === 'gap' || rowKind === 'proofGap') {
18
+ proofGaps.push(canvasProofGap(rowName, rest));
19
+ continue;
20
+ }
21
+ if (rowKind === 'trace') {
22
+ commandTraces.push(canvasTrace(rowName, rest, { sourcePath }));
23
+ continue;
24
+ }
25
+ records.push(canvasRecord(rowKind === 'stateWrite' ? 'state-write' : rowKind, rowName, rest, { sourcePath, sourceHash }));
26
+ }
27
+ const allGaps = [...records.flatMap((record) => record.proofGaps ?? []), ...proofGaps];
28
+ const tree = {
29
+ kind: 'frontier.lang.canvasSemanticTree',
30
+ version: 1,
31
+ id: idFrom(block.header, `canvas_surface_${name}`),
32
+ name,
33
+ sourcePath,
34
+ sourceHash,
35
+ records,
36
+ commandTraces,
37
+ proofGaps: allGaps,
38
+ evidence,
39
+ parser: { status: 'authored', errors: [] },
40
+ claims: {
41
+ autoMergeClaim: false,
42
+ semanticEquivalenceClaim: false,
43
+ browserRuntimeEquivalenceClaim: false,
44
+ canvasRuntimeEquivalenceClaim: false,
45
+ canvasVisualEquivalenceClaim: false
46
+ },
47
+ metadata: { authoredName: name }
48
+ };
49
+ tree.summary = summarizeCanvasSurface(tree);
50
+ tree.treeHash = hashSemanticValue({ kind: 'frontier.lang.canvas.authoredTree.v1', records: records.map(hashableRecord), proofGaps: allGaps.map((gap) => gap.code), commandTraces: commandTraces.map((trace) => trace.traceHash) });
51
+ return tree;
52
+ }
53
+
54
+ function canvasRecord(kind, name, text, context) {
55
+ const category = readInlineWord('category', text) ?? defaultCategory(kind);
56
+ const recordName = readInlineWord('name', text) ?? name;
57
+ const order = readInlineNumber('order', text) ?? readInlineNumber('renderOrder', text) ?? 0;
58
+ const proofGaps = readInlineList(text, 'proofGap', 'proofGaps', 'gap', 'gaps')?.map((code) => canvasProofGap(code, '')) ?? [];
59
+ const textValue = readInlineQuoted('text', text) ?? readInlineWord('text', text) ?? `${kind}:${category}:${recordName}:${order}`;
60
+ return cleanRecord({
61
+ kind,
62
+ id: idFrom(text, `canvas_${kind.replace(/-/g, '_')}_${safeId(recordName)}`),
63
+ name: recordName,
64
+ category,
65
+ contextKind: readInlineWord('context', text) ?? readInlineWord('contextKind', text),
66
+ renderOrder: order,
67
+ identityKey: readInlineWord('identity', text) ?? readInlineWord('identityKey', text) ?? `canvas:${kind}:${category}:${recordName}:${order || safeId(recordName)}`,
68
+ sourcePath: readInlineWord('sourcePath', text) ?? readInlineWord('path', text) ?? context.sourcePath,
69
+ sourceHash: readInlineWord('sourceHash', text) ?? context.sourceHash,
70
+ sourceSpan: parseSpan(readInlineWord('sourceSpan', text)),
71
+ textHash: hashSemanticValue({ kind: 'frontier.lang.canvas.authoredRecordText.v1', text: textValue }),
72
+ attributes: parsePairs(readInlineWord('attributes', text) ?? readInlineWord('attrs', text) ?? readInlineWord('attr', text)),
73
+ evidenceIds: readInlineList(text, 'evidence', 'evidenceIds'),
74
+ proofGaps
75
+ });
76
+ }
77
+
78
+ function canvasTrace(name, text, context) {
79
+ const commands = readInlineList(text, 'command', 'commands') ?? [];
80
+ const records = commands.map((command, index) => ({
81
+ kind: 'trace-command',
82
+ name: command,
83
+ category: readInlineWord('category', text) ?? 'custom',
84
+ ordinal: index + 1,
85
+ commandHash: hashSemanticValue({ kind: 'frontier.lang.canvas.authoredTraceCommand.v1', command, index }),
86
+ argsHash: hashSemanticValue({ kind: 'frontier.lang.canvas.authoredTraceArgs.v1', command, index }),
87
+ proofGaps: []
88
+ }));
89
+ return cleanRecord({
90
+ kind: 'frontier.lang.canvasCommandTrace',
91
+ version: 1,
92
+ id: idFrom(text, `canvas_trace_${name}`),
93
+ name,
94
+ sourcePath: readInlineWord('sourcePath', text) ?? readInlineWord('path', text) ?? context.sourcePath,
95
+ traceHash: hashSemanticValue({ kind: 'frontier.lang.canvas.authoredTrace.v1', commands }),
96
+ records,
97
+ evidenceIds: readInlineList(text, 'evidence', 'evidenceIds'),
98
+ runtimeEquivalenceClaim: false,
99
+ visualEquivalenceClaim: false,
100
+ summary: {
101
+ commands: records.length,
102
+ drawCommands: records.filter((record) => ['draw', 'image', 'text', 'path'].includes(record.category)).length,
103
+ proofGaps: 0
104
+ }
105
+ });
106
+ }
107
+
108
+ function canvasProofGap(name, text) {
109
+ const code = readInlineWord('code', text) ?? name;
110
+ return cleanRecord({
111
+ id: idFrom(text, `canvas_gap_${safeId(code)}`),
112
+ code,
113
+ status: readInlineWord('status', text) ?? 'not-claimed',
114
+ summary: readInlineQuoted('summary', text) ?? readInlineQuoted('message', text),
115
+ failClosed: true,
116
+ semanticEquivalenceClaim: false,
117
+ browserRuntimeEquivalenceClaim: false,
118
+ canvasRuntimeEquivalenceClaim: false,
119
+ canvasVisualEquivalenceClaim: false,
120
+ sourceSpan: parseSpan(readInlineWord('sourceSpan', text))
121
+ });
122
+ }
123
+
124
+ function parseEvidenceRows(body) {
125
+ const records = [];
126
+ for (const rawLine of body.split('\n')) {
127
+ const line = rawLine.trim();
128
+ const match = /^(?:evidence|proofEvidence)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
129
+ if (!match) continue;
130
+ records.push(cleanRecord({
131
+ id: idFrom(match[2], `evidence_${match[1]}`),
132
+ kind: readInlineWord('kind', match[2]) ?? 'note',
133
+ status: readInlineWord('status', match[2]) ?? 'unknown',
134
+ path: readInlineWord('path', match[2]),
135
+ summary: readInlineQuoted('summary', match[2]),
136
+ metadata: { name: match[1] }
137
+ }));
138
+ }
139
+ return records;
140
+ }
141
+
142
+ function summarizeCanvasSurface(tree) {
143
+ return {
144
+ elements: tree.records.filter((record) => record.kind === 'element').length,
145
+ commands: tree.records.filter((record) => record.kind === 'command').length,
146
+ stateWrites: tree.records.filter((record) => record.kind === 'state-write').length,
147
+ drawCommands: tree.records.filter((record) => ['draw', 'image', 'text', 'path'].includes(record.category)).length,
148
+ offscreenCommands: tree.records.filter((record) => record.category === 'offscreen').length,
149
+ gpuCommands: tree.records.filter((record) => record.contextKind === 'webgl' || record.contextKind === 'webgl2' || record.contextKind === 'webgpu').length,
150
+ commandTraces: tree.commandTraces.length,
151
+ proofGaps: tree.proofGaps.length,
152
+ parseErrors: tree.parser.errors.length
153
+ };
154
+ }
155
+
156
+ function defaultCategory(kind) {
157
+ if (kind === 'element') return 'html-canvas';
158
+ if (kind === 'state-write') return 'state';
159
+ return 'draw';
160
+ }
161
+
162
+ function parsePairs(value) {
163
+ if (!value) return undefined;
164
+ const pairs = Object.fromEntries(value.split(/[|,]/).map((item) => {
165
+ const [key, rawValue] = item.split('=');
166
+ return [key, rawValue ?? true];
167
+ }).filter(([key]) => key));
168
+ return Object.keys(pairs).length ? pairs : undefined;
169
+ }
170
+
171
+ function hashableRecord(record) {
172
+ return { kind: record.kind, name: record.name, category: record.category, contextKind: record.contextKind, renderOrder: record.renderOrder, proofGaps: record.proofGaps?.map((gap) => gap.code) };
173
+ }
174
+
175
+ function isPropertyLine(line) {
176
+ return /^(sourcePath|path|sourceHash|evidence|proofEvidence)\s+/.test(line);
177
+ }
178
+
179
+ function idFrom(text, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(text)?.[1] ?? fallback; }
180
+ function nameFrom(header) { return /^([A-Za-z_$][\w$-]*)/.exec(header)?.[1] ?? 'CanvasSurface'; }
181
+ function readLine(label, body) { return new RegExp('^\\s*' + label + '\\s+([^\\n]+)', 'm').exec(body)?.[1]?.trim(); }
182
+ function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
183
+ function readInlineQuoted(label, text) { return new RegExp("(?:^|\\s)" + label + "\\s+[\"']([^\"']+)[\"']").exec(text)?.[1]?.trim(); }
184
+ function readInlineNumber(label, text) {
185
+ const value = readInlineWord(label, text);
186
+ return value === undefined ? undefined : Number(value);
187
+ }
188
+ function readInlineList(text, ...labels) {
189
+ for (const label of labels) {
190
+ const value = new RegExp('(?:^|\\s)' + label + '\\s+([^\\s]+)').exec(text)?.[1]?.trim();
191
+ if (value) return value.split(/[|,]/).map((item) => item.trim()).filter(Boolean);
192
+ }
193
+ return undefined;
194
+ }
195
+ function parseSpan(value) {
196
+ if (!value) return undefined;
197
+ const match = /^(.+?):(\d+):(\d+)-(\d+):(\d+)$/.exec(value);
198
+ if (!match) return { path: value };
199
+ return { path: match[1], startLine: Number(match[2]), startColumn: Number(match[3]), endLine: Number(match[4]), endColumn: Number(match[5]) };
200
+ }
201
+ function cleanRecord(record) {
202
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0) && (!value || typeof value !== 'object' || Object.keys(value).length > 0)));
203
+ }
204
+ function safeId(value) {
205
+ return String(value ?? 'unknown').replace(/[^A-Za-z0-9_$-]+/g, '_');
206
+ }
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
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 { parseCanvasSurfaceBlock } from './canvas-surface.js';
4
5
  import { parseDecisionGraphBlock } from './decision-graph.js';
5
6
  import { parseDialectRegistryBlock } from './dialect-registry.js';
6
7
  import { parseInterlinguaBlock } from './interlingua.js';
7
8
  import { createParsedMetadata } from './metadata.js';
8
9
  import { parseSemanticOperationsBlock } from './operations.js';
10
+ import { parsePackageManifestBlock } from './package-manifest.js';
9
11
  import { parseParadigmBlock } from './paradigm.js';
10
12
  import { parseProofBlock } from './proof.js';
11
13
  import { parseResourceGraphBlock } from './resource-graph.js';
@@ -25,6 +27,8 @@ export function parseFrontierSource(source, options = {}) {
25
27
  const interlinguaBlocks = [];
26
28
  const resourceGraphBlocks = [];
27
29
  const nativeSourceBlocks = [];
30
+ const packageManifestBlocks = [];
31
+ const canvasSurfaceBlocks = [];
28
32
  const documentId = options.id ?? readId(source) ?? 'mod_frontier';
29
33
  const documentName = options.name ?? readName(source) ?? 'FrontierModule';
30
34
  for (const block of readBlocks(source)) {
@@ -53,8 +57,10 @@ export function parseFrontierSource(source, options = {}) {
53
57
  if (block.kind === 'dialectRegistry' || block.kind === 'universalDialectRegistry') dialectRegistryBlocks.push(parseDialectRegistryBlock(block));
54
58
  if (block.kind === 'interlingua' || block.kind === 'universalInterlingua') interlinguaBlocks.push(parseInterlinguaBlock(block));
55
59
  if (block.kind === 'resourceGraph' || block.kind === 'semanticResourceGraph') resourceGraphBlocks.push(parseResourceGraphBlock(block));
60
+ if (block.kind === 'packageManifest' || block.kind === 'packageGraph' || block.kind === 'packageSurface') packageManifestBlocks.push(parsePackageManifestBlock(block));
61
+ if (block.kind === 'canvasSurface' || block.kind === 'canvasGraph') canvasSurfaceBlocks.push(parseCanvasSurfaceBlock(block));
56
62
  }
57
- const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, constraintSpaceBlocks, decisionGraphBlocks, dialectRegistryBlocks, interlinguaBlocks, resourceGraphBlocks, nativeSourceBlocks });
63
+ const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, constraintSpaceBlocks, decisionGraphBlocks, dialectRegistryBlocks, interlinguaBlocks, resourceGraphBlocks, nativeSourceBlocks, packageManifestBlocks, canvasSurfaceBlocks });
58
64
  return createDocument({ id: documentId, name: documentName, nodes, ...(metadata ? { metadata } : {}) });
59
65
  }
60
66
 
@@ -64,7 +70,7 @@ function readName(source) { return /module\s+([A-Za-z_$][\w$]*)/.exec(source)?.[
64
70
  function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']+)["']\s*\)/.exec(source)?.[1]; }
65
71
  function readBlocks(source) {
66
72
  const blocks = [];
67
- 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|dialectRegistry|universalDialectRegistry|interlingua|universalInterlingua|resourceGraph|semanticResourceGraph)\s+([^{}]+)\{/g;
73
+ 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|dialectRegistry|universalDialectRegistry|interlingua|universalInterlingua|resourceGraph|semanticResourceGraph|packageManifest|packageGraph|packageSurface|canvasSurface|canvasGraph)\s+([^{}]+)\{/g;
68
74
  let match;
69
75
  while ((match = header.exec(source))) {
70
76
  let depth = 1; let index = header.lastIndex;
package/dist/metadata.js CHANGED
@@ -23,7 +23,7 @@ const PARADIGM_GROUPS = [
23
23
  'loweringRecords'
24
24
  ];
25
25
 
26
- export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], operationBlocks = [], conversionBlocks = [], constraintSpaceBlocks = [], decisionGraphBlocks = [], dialectRegistryBlocks = [], interlinguaBlocks = [], resourceGraphBlocks = [], nativeSourceBlocks = [] } = {}) {
26
+ export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], operationBlocks = [], conversionBlocks = [], constraintSpaceBlocks = [], decisionGraphBlocks = [], dialectRegistryBlocks = [], interlinguaBlocks = [], resourceGraphBlocks = [], nativeSourceBlocks = [], packageManifestBlocks = [], canvasSurfaceBlocks = [] } = {}) {
27
27
  const metadata = {};
28
28
  if (proofBlocks.length) metadata.proof = mergeProofBlocks(proofBlocks);
29
29
  if (paradigmBlocks.length) metadata.paradigmSemantics = mergeParadigmBlocks(paradigmBlocks);
@@ -34,8 +34,10 @@ export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], op
34
34
  if (dialectRegistryBlocks.length) metadata.dialects = mergeDialectRegistryBlocks(dialectRegistryBlocks);
35
35
  if (interlinguaBlocks.length) metadata.universalInterlingua = mergeInterlinguaBlocks(interlinguaBlocks);
36
36
  if (resourceGraphBlocks.length) metadata.semanticResourceGraphs = mergeResourceGraphBlocks(resourceGraphBlocks);
37
- if (nativeSourceBlocks.some((block) => block.sourceMaps.length || block.mergeCandidates.length || block.evidence.length)) {
38
- metadata.universalAst = mergeNativeSourceBlocks(nativeSourceBlocks);
37
+ if (packageManifestBlocks.length) metadata.packageManifests = mergePackageManifestBlocks(packageManifestBlocks);
38
+ if (canvasSurfaceBlocks.length) metadata.canvasSurfaces = mergeCanvasSurfaceBlocks(canvasSurfaceBlocks);
39
+ if (nativeSourceBlocks.some((block) => block.sourceMaps.length || block.mergeCandidates.length || block.evidence.length) || packageManifestBlocks.length || canvasSurfaceBlocks.length) {
40
+ metadata.universalAst = mergeUniversalAstBlocks(nativeSourceBlocks, packageManifestBlocks, canvasSurfaceBlocks);
39
41
  }
40
42
  return Object.keys(metadata).length ? metadata : undefined;
41
43
  }
@@ -106,7 +108,7 @@ function mergeConstraintSpaceBlocks(blocks) {
106
108
  };
107
109
  }
108
110
 
109
- function mergeNativeSourceBlocks(blocks) {
111
+ function mergeUniversalAstBlocks(blocks, packageManifestBlocks = [], canvasSurfaceBlocks = []) {
110
112
  return {
111
113
  id: blocks.length === 1 ? `universalAst:${blocks[0].node.id}` : 'universalAst:source',
112
114
  nativeSourceIds: blocks.map((block) => block.node.id),
@@ -114,7 +116,71 @@ function mergeNativeSourceBlocks(blocks) {
114
116
  mergeCandidates: blocks.flatMap((block) => block.mergeCandidates ?? []),
115
117
  evidence: blocks.flatMap((block) => block.evidence ?? []),
116
118
  losses: blocks.flatMap((block) => block.losses ?? []),
117
- metadata: { authoredNativeSourceIds: blocks.map((block) => block.node.id) }
119
+ packageManifests: packageManifestBlocks,
120
+ canvasSurfaces: canvasSurfaceBlocks,
121
+ packageManifestIds: ids(packageManifestBlocks),
122
+ canvasSurfaceIds: ids(canvasSurfaceBlocks),
123
+ metadata: {
124
+ authoredNativeSourceIds: blocks.map((block) => block.node.id),
125
+ authoredPackageManifestIds: ids(packageManifestBlocks),
126
+ authoredCanvasSurfaceIds: ids(canvasSurfaceBlocks)
127
+ }
128
+ };
129
+ }
130
+
131
+ function mergePackageManifestBlocks(blocks) {
132
+ return {
133
+ id: blocks.length === 1 ? blocks[0].id : 'packageManifests:source',
134
+ manifests: blocks,
135
+ manifestIds: ids(blocks),
136
+ recordIds: blocks.flatMap((block) => ids(block.records)),
137
+ evidenceIds: blocks.flatMap((block) => ids(block.evidence)),
138
+ proofGapCodes: [...new Set(blocks.flatMap((block) => (block.proofGaps ?? []).map((gap) => gap.code).filter(Boolean)))],
139
+ summary: {
140
+ manifestCount: blocks.length,
141
+ recordCount: blocks.reduce((sum, block) => sum + (block.records?.length ?? 0), 0),
142
+ dependencyCount: blocks.reduce((sum, block) => sum + (block.summary?.dependencies ?? 0), 0),
143
+ scriptCount: blocks.reduce((sum, block) => sum + (block.summary?.scripts ?? 0), 0),
144
+ exportCount: blocks.reduce((sum, block) => sum + (block.summary?.exports ?? 0), 0),
145
+ proofGapCount: blocks.reduce((sum, block) => sum + (block.proofGaps?.length ?? 0), 0)
146
+ },
147
+ claims: {
148
+ autoMergeClaim: false,
149
+ semanticEquivalenceClaim: false,
150
+ packageInstallEquivalenceClaim: false,
151
+ installEquivalenceClaim: false,
152
+ runtimeEquivalenceClaim: false
153
+ },
154
+ metadata: { authoredPackageManifestIds: ids(blocks) }
155
+ };
156
+ }
157
+
158
+ function mergeCanvasSurfaceBlocks(blocks) {
159
+ return {
160
+ id: blocks.length === 1 ? blocks[0].id : 'canvasSurfaces:source',
161
+ surfaces: blocks,
162
+ surfaceIds: ids(blocks),
163
+ recordIds: blocks.flatMap((block) => ids(block.records)),
164
+ commandTraceIds: blocks.flatMap((block) => ids(block.commandTraces)),
165
+ evidenceIds: blocks.flatMap((block) => ids(block.evidence)),
166
+ proofGapCodes: [...new Set(blocks.flatMap((block) => (block.proofGaps ?? []).map((gap) => gap.code).filter(Boolean)))],
167
+ summary: {
168
+ surfaceCount: blocks.length,
169
+ recordCount: blocks.reduce((sum, block) => sum + (block.records?.length ?? 0), 0),
170
+ commandTraceCount: blocks.reduce((sum, block) => sum + (block.commandTraces?.length ?? 0), 0),
171
+ drawCommandCount: blocks.reduce((sum, block) => sum + (block.summary?.drawCommands ?? 0), 0),
172
+ offscreenCommandCount: blocks.reduce((sum, block) => sum + (block.summary?.offscreenCommands ?? 0), 0),
173
+ gpuCommandCount: blocks.reduce((sum, block) => sum + (block.summary?.gpuCommands ?? 0), 0),
174
+ proofGapCount: blocks.reduce((sum, block) => sum + (block.proofGaps?.length ?? 0), 0)
175
+ },
176
+ claims: {
177
+ autoMergeClaim: false,
178
+ semanticEquivalenceClaim: false,
179
+ browserRuntimeEquivalenceClaim: false,
180
+ canvasRuntimeEquivalenceClaim: false,
181
+ canvasVisualEquivalenceClaim: false
182
+ },
183
+ metadata: { authoredCanvasSurfaceIds: ids(blocks) }
118
184
  };
119
185
  }
120
186
 
@@ -0,0 +1,162 @@
1
+ import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+
3
+ export function parsePackageManifestBlock(block) {
4
+ const name = nameFrom(block.header);
5
+ const sourcePath = readLine('sourcePath', block.body) ?? readLine('path', block.body) ?? 'package.json';
6
+ const sourceHash = readLine('sourceHash', block.body);
7
+ const evidence = parseEvidenceRows(block.body);
8
+ const records = [];
9
+ const proofGaps = [];
10
+ for (const rawLine of block.body.split('\n')) {
11
+ const line = rawLine.trim();
12
+ if (!line || line.startsWith('#') || isPropertyLine(line)) continue;
13
+ const match = /^(metadata|dependency|script|export|gap|proofGap)\s+([A-Za-z_$.*@/][\w$./@*-]*)(.*)$/.exec(line);
14
+ if (!match) continue;
15
+ const [, rowKind, rowName, rest] = match;
16
+ if (rowKind === 'gap' || rowKind === 'proofGap') {
17
+ proofGaps.push(packageProofGap(rowName, rest));
18
+ continue;
19
+ }
20
+ const record = packageRecord(rowKind, rowName, rest, { sourcePath, sourceHash, evidence });
21
+ if (record) records.push(record);
22
+ }
23
+ const recordGaps = records.flatMap((record) => record.proofGaps ?? []);
24
+ const allGaps = [...recordGaps, ...proofGaps];
25
+ const tree = {
26
+ kind: 'frontier.lang.packageManifestSemanticTree',
27
+ version: 1,
28
+ id: idFrom(block.header, `package_manifest_${name}`),
29
+ name,
30
+ sourcePath,
31
+ sourceHash,
32
+ packageManager: readLine('packageManager', block.body),
33
+ records,
34
+ proofGaps: allGaps,
35
+ evidence,
36
+ parser: { status: 'authored', errors: [] },
37
+ claims: {
38
+ autoMergeClaim: false,
39
+ semanticEquivalenceClaim: false,
40
+ packageInstallEquivalenceClaim: false,
41
+ installEquivalenceClaim: false,
42
+ runtimeEquivalenceClaim: false
43
+ },
44
+ metadata: { authoredName: name }
45
+ };
46
+ tree.summary = summarizePackageManifest(tree);
47
+ tree.treeHash = hashSemanticValue({ kind: 'frontier.lang.package.authoredTree.v1', records: records.map(hashableRecord), proofGaps: allGaps.map((gap) => gap.code) });
48
+ return tree;
49
+ }
50
+
51
+ function packageRecord(kind, name, text, context) {
52
+ const section = readInlineWord('section', text) ?? defaultSection(kind);
53
+ const recordName = readInlineWord('name', text) ?? name;
54
+ const value = packageValue(kind, text);
55
+ const proofGaps = readInlineList(text, 'proofGap', 'proofGaps', 'gap', 'gaps')?.map((code) => packageProofGap(code, '')) ?? [];
56
+ return cleanRecord({
57
+ kind,
58
+ id: idFrom(text, `package_${kind}_${safeId(section)}_${safeId(recordName)}`),
59
+ section,
60
+ name: recordName,
61
+ value,
62
+ valueHash: hashSemanticValue({ kind: 'frontier.lang.package.authoredRecordValue.v1', value }),
63
+ identityKey: readInlineWord('identity', text) ?? readInlineWord('identityKey', text) ?? `${kind}:${section}:${recordName}`,
64
+ sourcePath: readInlineWord('sourcePath', text) ?? readInlineWord('path', text) ?? context.sourcePath,
65
+ sourceHash: readInlineWord('sourceHash', text) ?? context.sourceHash,
66
+ sourceSpan: parseSpan(readInlineWord('sourceSpan', text)),
67
+ evidenceIds: readInlineList(text, 'evidence', 'evidenceIds'),
68
+ proofGaps
69
+ });
70
+ }
71
+
72
+ function packageValue(kind, text) {
73
+ if (kind === 'dependency') return readInlineQuoted('value', text) ?? readInlineWord('range', text) ?? readInlineWord('value', text) ?? '*';
74
+ if (kind === 'script') return readInlineQuoted('command', text) ?? readInlineQuoted('value', text) ?? readInlineWord('command', text) ?? readInlineWord('value', text);
75
+ if (kind === 'export') return readInlineQuoted('target', text) ?? readInlineQuoted('value', text) ?? readInlineWord('target', text) ?? readInlineWord('value', text);
76
+ return readInlineQuoted('value', text) ?? readInlineWord('value', text);
77
+ }
78
+
79
+ function defaultSection(kind) {
80
+ if (kind === 'dependency') return 'dependencies';
81
+ if (kind === 'script') return 'scripts';
82
+ if (kind === 'export') return 'exports';
83
+ return 'metadata';
84
+ }
85
+
86
+ function packageProofGap(name, text) {
87
+ const code = readInlineWord('code', text) ?? name;
88
+ return cleanRecord({
89
+ id: idFrom(text, `package_gap_${safeId(code)}`),
90
+ code,
91
+ status: readInlineWord('status', text) ?? 'not-claimed',
92
+ summary: readInlineQuoted('summary', text) ?? readInlineQuoted('message', text),
93
+ failClosed: true,
94
+ semanticEquivalenceClaim: false,
95
+ packageInstallEquivalenceClaim: false,
96
+ installEquivalenceClaim: false,
97
+ runtimeEquivalenceClaim: false,
98
+ sourceSpan: parseSpan(readInlineWord('sourceSpan', text))
99
+ });
100
+ }
101
+
102
+ function parseEvidenceRows(body) {
103
+ const records = [];
104
+ for (const rawLine of body.split('\n')) {
105
+ const line = rawLine.trim();
106
+ const match = /^(?:evidence|proofEvidence)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
107
+ if (!match) continue;
108
+ records.push(cleanRecord({
109
+ id: idFrom(match[2], `evidence_${match[1]}`),
110
+ kind: readInlineWord('kind', match[2]) ?? 'note',
111
+ status: readInlineWord('status', match[2]) ?? 'unknown',
112
+ path: readInlineWord('path', match[2]),
113
+ summary: readInlineQuoted('summary', match[2]),
114
+ metadata: { name: match[1] }
115
+ }));
116
+ }
117
+ return records;
118
+ }
119
+
120
+ function summarizePackageManifest(tree) {
121
+ return {
122
+ metadata: tree.records.filter((record) => record.kind === 'metadata').length,
123
+ dependencies: tree.records.filter((record) => record.kind === 'dependency').length,
124
+ scripts: tree.records.filter((record) => record.kind === 'script').length,
125
+ exports: tree.records.filter((record) => record.kind === 'export').length,
126
+ proofGaps: tree.proofGaps.length,
127
+ parseErrors: tree.parser.errors.length
128
+ };
129
+ }
130
+
131
+ function hashableRecord(record) {
132
+ return { kind: record.kind, section: record.section, name: record.name, value: record.value, proofGaps: record.proofGaps?.map((gap) => gap.code) };
133
+ }
134
+
135
+ function isPropertyLine(line) {
136
+ return /^(sourcePath|path|sourceHash|packageManager|evidence|proofEvidence)\s+/.test(line);
137
+ }
138
+
139
+ function idFrom(text, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(text)?.[1] ?? fallback; }
140
+ function nameFrom(header) { return /^([A-Za-z_$][\w$-]*)/.exec(header)?.[1] ?? 'PackageManifest'; }
141
+ function readLine(label, body) { return new RegExp('^\\s*' + label + '\\s+([^\\n]+)', 'm').exec(body)?.[1]?.trim(); }
142
+ function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
143
+ function readInlineQuoted(label, text) { return new RegExp("(?:^|\\s)" + label + "\\s+[\"']([^\"']+)[\"']").exec(text)?.[1]?.trim(); }
144
+ function readInlineList(text, ...labels) {
145
+ for (const label of labels) {
146
+ const value = new RegExp('(?:^|\\s)' + label + '\\s+([^\\s]+)').exec(text)?.[1]?.trim();
147
+ if (value) return value.split(/[|,]/).map((item) => item.trim()).filter(Boolean);
148
+ }
149
+ return undefined;
150
+ }
151
+ function parseSpan(value) {
152
+ if (!value) return undefined;
153
+ const match = /^(.+?):(\d+):(\d+)-(\d+):(\d+)$/.exec(value);
154
+ if (!match) return { path: value };
155
+ return { path: match[1], startLine: Number(match[2]), startColumn: Number(match[3]), endLine: Number(match[4]), endColumn: Number(match[5]) };
156
+ }
157
+ function cleanRecord(record) {
158
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && (!Array.isArray(value) || value.length > 0) && (!value || typeof value !== 'object' || Object.keys(value).length > 0)));
159
+ }
160
+ function safeId(value) {
161
+ return String(value ?? 'unknown').replace(/[^A-Za-z0-9_$-]+/g, '_');
162
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-parser",
3
- "version": "0.3.27",
3
+ "version": "0.3.28",
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 && node test/conversion-constraint-fields-smoke.mjs && node test/view-render-graph-smoke.mjs && node test/resource-graph-smoke.mjs && node test/interlingua-smoke.mjs && node test/dialect-registry-smoke.mjs",
25
+ "test": "npm run build && node test/smoke.mjs && node test/conversion-constraint-fields-smoke.mjs && node test/package-canvas-surface-smoke.mjs && node test/view-render-graph-smoke.mjs && node test/resource-graph-smoke.mjs && node test/interlingua-smoke.mjs && node test/dialect-registry-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",