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

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,98 @@ 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
+
312
+ ## Authored application and plugin syntax
313
+
314
+ `.frontier` files can describe application composition boundaries directly.
315
+ These blocks make host/plugin contracts source-level objects: mount points,
316
+ provided surfaces, required capabilities, routes, events, assets, gates, and
317
+ proof gaps can be queried by projection and merge tooling without collapsing
318
+ them into a target-language implementation detail.
319
+
320
+ ```frontier
321
+ appHost WorkbenchHost @id("app_surface_workbench") {
322
+ role host
323
+ sourcePath app.frontier
324
+ sourceHash sha256:host
325
+ evidence previewProbe @id("evidence_preview_probe") kind browser-probe status passed path reports/preview.json
326
+ mount dashboard @id("app_mount_dashboard") kind region path /dashboard view view_dashboard target react evidence evidence_preview_probe
327
+ provides shell @id("app_provide_shell") surface view view view_dashboard mount app_mount_dashboard capability host.fetch|host.storage evidence evidence_preview_probe
328
+ requires fetch @id("app_require_fetch") capability host.fetch category network permission network proofGap app-host-capability-adapter-boundary evidence evidence_preview_probe
329
+ route dashboard @id("app_route_dashboard") path /dashboard view view_dashboard action action_refresh mount app_mount_dashboard evidence evidence_preview_probe
330
+ gate preview @id("app_gate_preview") kind browser-probe command "npm run preview:probe" required subject view_dashboard evidence evidence_preview_probe
331
+ gap pluginAbi @id("app_gap_plugin_abi") code plugin-abi-compatibility-boundary summary "Plugin ABI requires host/runtime compatibility proof."
332
+ }
333
+
334
+ plugin WeatherWidget @id("plugin_weather_widget") {
335
+ role plugin
336
+ host app_surface_workbench
337
+ sourcePath plugins/weather.frontier
338
+ sourceHash sha256:plugin
339
+ evidence sandboxProbe @id("evidence_sandbox_probe") kind sandbox-probe status passed path reports/sandbox.json
340
+ provides weatherPanel @id("plugin_provide_weather") surface view view view_weather_panel mount app_mount_dashboard capability host.fetch proofGap plugin-projection-runtime-boundary evidence evidence_sandbox_probe
341
+ requires fetch @id("plugin_require_fetch") capability host.fetch category network permission network adapter host_fetch_adapter proofGap plugin-capability-grant-boundary evidence evidence_sandbox_probe
342
+ gate sandbox @id("plugin_gate_sandbox") kind sandbox command "npm run sandbox:probe" required subject view_weather_panel evidence evidence_sandbox_probe
343
+ gap sandbox @id("plugin_gap_sandbox") code plugin-sandbox-safety-boundary summary "Sandbox safety requires runtime proof."
344
+ }
345
+ ```
346
+
347
+ The parser stores these blocks in `metadata.applicationSurfaces` and mirrors
348
+ them under `metadata.universalAst.applicationSurfaces`. They are composition
349
+ contracts, not compatibility proof: `runtimeEquivalenceClaim`,
350
+ `abiCompatibilityClaim`, `projectionEquivalenceClaim`,
351
+ `pluginCompatibilityClaim`, and `sandboxSafetyClaim` remain false until a
352
+ higher layer supplies source-bound runtime, ABI, projection, and sandbox
353
+ evidence.
354
+
263
355
  ## Authored target projection syntax
264
356
 
265
357
  `.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,303 @@
1
+ import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+
3
+ export function parseApplicationSurfaceBlock(block) {
4
+ const name = nameFrom(block.header);
5
+ const surfaceKind = surfaceKindFrom(block.kind);
6
+ const role = readLine('role', block.body) ?? defaultRole(surfaceKind);
7
+ const sourcePath = readLine('sourcePath', block.body) ?? readLine('path', block.body);
8
+ const sourceHash = readLine('sourceHash', block.body);
9
+ const evidence = parseEvidenceRows(block.body);
10
+ const records = [];
11
+ const proofGaps = [];
12
+ for (const rawLine of block.body.split('\n')) {
13
+ const line = rawLine.trim();
14
+ if (!line || line.startsWith('#') || isPropertyLine(line)) continue;
15
+ const match = /^(mount|provide|provides|required|requires|require|route|event|asset|gate|gap|proofGap)\s+([A-Za-z_$@/.*][\w$./@:*+-]*)(.*)$/.exec(line);
16
+ if (!match) continue;
17
+ const [, rowKind, rowName, rest] = match;
18
+ if (rowKind === 'gap' || rowKind === 'proofGap') {
19
+ proofGaps.push(applicationProofGap(rowName, rest));
20
+ continue;
21
+ }
22
+ records.push(applicationRecord(normalizeRowKind(rowKind), rowName, rest, {
23
+ surfaceId: idFrom(block.header, `application_surface_${safeId(name)}`),
24
+ surfaceName: name,
25
+ role,
26
+ sourcePath,
27
+ sourceHash
28
+ }));
29
+ }
30
+ const allGaps = [...records.flatMap((record) => record.proofGaps ?? []), ...proofGaps];
31
+ const tree = {
32
+ kind: 'frontier.lang.applicationSurface',
33
+ version: 1,
34
+ id: idFrom(block.header, `application_surface_${safeId(name)}`),
35
+ name,
36
+ surfaceKind,
37
+ role,
38
+ hostId: readLine('host', block.body) ?? readLine('hostId', block.body),
39
+ sourcePath,
40
+ sourceHash,
41
+ records,
42
+ proofGaps: allGaps,
43
+ evidence,
44
+ parser: { status: 'authored', errors: [] },
45
+ claims: applicationFalseClaims(),
46
+ metadata: { authoredName: name, authoredBlockKind: block.kind }
47
+ };
48
+ tree.summary = summarizeApplicationSurface(tree);
49
+ tree.treeHash = hashSemanticValue({
50
+ kind: 'frontier.lang.applicationSurface.authoredTree.v1',
51
+ role,
52
+ records: records.map(hashableRecord),
53
+ proofGaps: allGaps.map((gap) => gap.code)
54
+ });
55
+ return tree;
56
+ }
57
+
58
+ export function mergeApplicationSurfaceBlocks(blocks) {
59
+ const records = blocks.flatMap((block) => block.records ?? []);
60
+ return {
61
+ id: blocks.length === 1 ? blocks[0].id : 'applicationSurfaces:source',
62
+ surfaces: blocks,
63
+ surfaceIds: ids(blocks),
64
+ recordIds: ids(records),
65
+ mountIds: idsByRecordKind(records, 'mount'),
66
+ providedSurfaceIds: idsByRecordKind(records, 'provided-surface'),
67
+ requiredCapabilityIds: idsByRecordKind(records, 'required-capability'),
68
+ routeIds: idsByRecordKind(records, 'route'),
69
+ eventIds: idsByRecordKind(records, 'event'),
70
+ assetIds: idsByRecordKind(records, 'asset'),
71
+ gateIds: idsByRecordKind(records, 'gate'),
72
+ evidenceIds: blocks.flatMap((block) => ids(block.evidence)),
73
+ proofGapCodes: [...new Set(blocks.flatMap((block) => (block.proofGaps ?? []).map((gap) => gap.code).filter(Boolean)))],
74
+ summary: {
75
+ surfaceCount: blocks.length,
76
+ recordCount: records.length,
77
+ mountCount: blocks.reduce((total, block) => total + (block.summary?.mountCount ?? 0), 0),
78
+ providedSurfaceCount: blocks.reduce((total, block) => total + (block.summary?.providedSurfaceCount ?? 0), 0),
79
+ requiredCapabilityCount: blocks.reduce((total, block) => total + (block.summary?.requiredCapabilityCount ?? 0), 0),
80
+ routeCount: blocks.reduce((total, block) => total + (block.summary?.routeCount ?? 0), 0),
81
+ eventCount: blocks.reduce((total, block) => total + (block.summary?.eventCount ?? 0), 0),
82
+ assetCount: blocks.reduce((total, block) => total + (block.summary?.assetCount ?? 0), 0),
83
+ gateCount: blocks.reduce((total, block) => total + (block.summary?.gateCount ?? 0), 0),
84
+ proofGapCount: blocks.reduce((total, block) => total + (block.proofGaps?.length ?? 0), 0)
85
+ },
86
+ claims: applicationFalseClaims(),
87
+ metadata: { authoredApplicationSurfaceIds: ids(blocks) }
88
+ };
89
+ }
90
+
91
+ function applicationRecord(kind, name, text, context) {
92
+ const recordName = readInlineWord('name', text) ?? name;
93
+ const proofGaps = readInlineList(text, 'proofGap', 'proofGaps', 'gap', 'gaps')?.map((code) => applicationProofGap(code, '')) ?? [];
94
+ const base = {
95
+ kind: 'frontier.lang.applicationSurface.record',
96
+ recordKind: kind,
97
+ id: idFrom(text, `application_${kind.replace(/-/g, '_')}_${safeId(recordName)}`),
98
+ name: recordName,
99
+ surfaceId: context.surfaceId,
100
+ surfaceName: context.surfaceName,
101
+ role: context.role,
102
+ identityKey: readInlineWord('identity', text) ?? readInlineWord('identityKey', text) ?? `${kind}:${context.role}:${recordName}`,
103
+ sourcePath: readInlineWord('sourcePath', text) ?? readInlineWord('path', text) ?? context.sourcePath,
104
+ sourceHash: readInlineWord('sourceHash', text) ?? context.sourceHash,
105
+ sourceSpan: parseSpan(readInlineWord('sourceSpan', text)),
106
+ evidenceIds: readInlineList(text, 'evidence', 'evidenceIds'),
107
+ proofEvidenceIds: readInlineList(text, 'proofEvidence', 'proofEvidenceIds'),
108
+ missingEvidence: readInlineList(text, 'missingEvidence'),
109
+ proofGaps,
110
+ claims: applicationFalseClaims()
111
+ };
112
+ if (kind === 'mount') return cleanRecord({
113
+ ...base,
114
+ mountKind: readInlineWord('kind', text) ?? readInlineWord('mountKind', text) ?? 'region',
115
+ path: readInlineQuoted('path', text) ?? readInlineWord('path', text),
116
+ slot: readInlineWord('slot', text),
117
+ parentId: readInlineWord('parent', text) ?? readInlineWord('parentId', text),
118
+ viewId: readInlineWord('view', text) ?? readInlineWord('viewId', text),
119
+ target: readInlineWord('target', text),
120
+ renderer: readInlineWord('renderer', text)
121
+ });
122
+ if (kind === 'provided-surface') return cleanRecord({
123
+ ...base,
124
+ surfaceKind: readInlineWord('surface', text) ?? readInlineWord('surfaceKind', text) ?? readInlineWord('kind', text) ?? 'view',
125
+ viewId: readInlineWord('view', text) ?? readInlineWord('viewId', text),
126
+ actionId: readInlineWord('action', text) ?? readInlineWord('actionId', text),
127
+ effectId: readInlineWord('effect', text) ?? readInlineWord('effectId', text),
128
+ component: readInlineWord('component', text),
129
+ mountId: readInlineWord('mount', text) ?? readInlineWord('mountId', text),
130
+ routeId: readInlineWord('route', text) ?? readInlineWord('routeId', text),
131
+ capabilityIds: readInlineList(text, 'capability', 'capabilities', 'capabilityIds'),
132
+ target: readInlineWord('target', text),
133
+ language: readInlineWord('language', text),
134
+ contractId: readInlineWord('contract', text) ?? readInlineWord('contractId', text)
135
+ });
136
+ if (kind === 'required-capability') return cleanRecord({
137
+ ...base,
138
+ capability: readInlineWord('capability', text) ?? readInlineWord('capabilityId', text) ?? recordName,
139
+ category: readInlineWord('category', text),
140
+ permission: readInlineWord('permission', text),
141
+ adapterId: readInlineWord('adapter', text) ?? readInlineWord('adapterId', text),
142
+ hostId: readInlineWord('host', text) ?? readInlineWord('hostId', text),
143
+ runtime: readInlineWord('runtime', text),
144
+ target: readInlineWord('target', text)
145
+ });
146
+ if (kind === 'route') return cleanRecord({
147
+ ...base,
148
+ path: readInlineQuoted('path', text) ?? readInlineWord('path', text) ?? recordName,
149
+ method: readInlineWord('method', text),
150
+ viewId: readInlineWord('view', text) ?? readInlineWord('viewId', text),
151
+ actionId: readInlineWord('action', text) ?? readInlineWord('actionId', text),
152
+ mountId: readInlineWord('mount', text) ?? readInlineWord('mountId', text),
153
+ paramIds: readInlineList(text, 'param', 'params', 'paramIds')
154
+ });
155
+ if (kind === 'event') return cleanRecord({
156
+ ...base,
157
+ eventKind: readInlineWord('kind', text) ?? readInlineWord('eventKind', text),
158
+ eventName: readInlineWord('event', text) ?? readInlineWord('eventName', text) ?? recordName,
159
+ actionId: readInlineWord('action', text) ?? readInlineWord('actionId', text),
160
+ input: readInlineWord('input', text),
161
+ sourceId: readInlineWord('source', text) ?? readInlineWord('sourceId', text),
162
+ targetId: readInlineWord('target', text) ?? readInlineWord('targetId', text)
163
+ });
164
+ if (kind === 'asset') return cleanRecord({
165
+ ...base,
166
+ assetKind: readInlineWord('kind', text) ?? readInlineWord('assetKind', text),
167
+ path: readInlineQuoted('path', text) ?? readInlineWord('path', text),
168
+ assetHash: readInlineWord('hash', text) ?? readInlineWord('assetHash', text),
169
+ integrity: readInlineWord('integrity', text),
170
+ runtime: readInlineWord('runtime', text),
171
+ mountId: readInlineWord('mount', text) ?? readInlineWord('mountId', text)
172
+ });
173
+ if (kind === 'gate') return cleanRecord({
174
+ ...base,
175
+ gateKind: readInlineWord('kind', text) ?? readInlineWord('gateKind', text) ?? 'test',
176
+ command: readInlineQuoted('command', text) ?? readInlineWord('command', text),
177
+ required: readInlineFlag('required', text) ?? undefined,
178
+ status: readInlineWord('status', text),
179
+ subjectIds: readInlineList(text, 'subject', 'subjects', 'subjectIds'),
180
+ runtime: readInlineWord('runtime', text)
181
+ });
182
+ return cleanRecord(base);
183
+ }
184
+
185
+ function applicationProofGap(name, text) {
186
+ const code = readInlineWord('code', text) ?? name;
187
+ return cleanRecord({
188
+ id: idFrom(text, `application_gap_${safeId(code)}`),
189
+ code,
190
+ status: readInlineWord('status', text) ?? 'not-claimed',
191
+ summary: readInlineQuoted('summary', text) ?? readInlineQuoted('message', text),
192
+ failClosed: true,
193
+ ...applicationFalseClaims(),
194
+ sourceSpan: parseSpan(readInlineWord('sourceSpan', text))
195
+ });
196
+ }
197
+
198
+ function parseEvidenceRows(body) {
199
+ const records = [];
200
+ for (const rawLine of body.split('\n')) {
201
+ const line = rawLine.trim();
202
+ const match = /^(?:evidence|proofEvidence)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
203
+ if (!match) continue;
204
+ records.push(cleanRecord({
205
+ id: idFrom(match[2], `evidence_${match[1]}`),
206
+ kind: readInlineWord('kind', match[2]) ?? 'note',
207
+ status: readInlineWord('status', match[2]) ?? 'unknown',
208
+ path: readInlineWord('path', match[2]),
209
+ summary: readInlineQuoted('summary', match[2]),
210
+ metadata: { name: match[1] }
211
+ }));
212
+ }
213
+ return records;
214
+ }
215
+
216
+ function summarizeApplicationSurface(tree) {
217
+ return {
218
+ recordCount: tree.records.length,
219
+ mountCount: count(tree.records, 'mount'),
220
+ providedSurfaceCount: count(tree.records, 'provided-surface'),
221
+ requiredCapabilityCount: count(tree.records, 'required-capability'),
222
+ routeCount: count(tree.records, 'route'),
223
+ eventCount: count(tree.records, 'event'),
224
+ assetCount: count(tree.records, 'asset'),
225
+ gateCount: count(tree.records, 'gate'),
226
+ evidenceCount: tree.evidence.length,
227
+ proofGapCount: tree.proofGaps.length,
228
+ parseErrors: tree.parser.errors.length
229
+ };
230
+ }
231
+
232
+ function applicationFalseClaims() {
233
+ return {
234
+ autoMergeClaim: false,
235
+ semanticEquivalenceClaim: false,
236
+ runtimeEquivalenceClaim: false,
237
+ abiCompatibilityClaim: false,
238
+ projectionEquivalenceClaim: false,
239
+ pluginCompatibilityClaim: false,
240
+ sandboxSafetyClaim: false
241
+ };
242
+ }
243
+
244
+ function hashableRecord(record) {
245
+ return {
246
+ recordKind: record.recordKind,
247
+ name: record.name,
248
+ identityKey: record.identityKey,
249
+ capability: record.capability,
250
+ path: record.path,
251
+ viewId: record.viewId,
252
+ actionId: record.actionId,
253
+ proofGaps: record.proofGaps?.map((gap) => gap.code)
254
+ };
255
+ }
256
+
257
+ function normalizeRowKind(kind) {
258
+ if (kind === 'provide' || kind === 'provides') return 'provided-surface';
259
+ if (kind === 'require' || kind === 'requires' || kind === 'required') return 'required-capability';
260
+ return kind;
261
+ }
262
+
263
+ function surfaceKindFrom(kind) {
264
+ if (kind === 'appHost') return 'host';
265
+ if (kind === 'plugin' || kind === 'pluginSurface' || kind === 'pluginContract') return 'plugin';
266
+ return 'application';
267
+ }
268
+
269
+ function defaultRole(surfaceKind) {
270
+ if (surfaceKind === 'host') return 'host';
271
+ if (surfaceKind === 'plugin') return 'plugin';
272
+ return 'application';
273
+ }
274
+
275
+ function count(records, kind) { return records.filter((record) => record.recordKind === kind).length; }
276
+ function ids(records = []) { return records.map((record) => record?.id).filter(Boolean); }
277
+ function idsByRecordKind(records = [], kind) { return ids(records.filter((record) => record?.recordKind === kind)); }
278
+ function isPropertyLine(line) { return /^(role|host|hostId|sourcePath|path|sourceHash|evidence|proofEvidence)\s+/.test(line); }
279
+ function idFrom(text, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(text)?.[1] ?? fallback; }
280
+ function nameFrom(header) { return /^([A-Za-z_$][\w$-]*)/.exec(header)?.[1] ?? 'ApplicationSurface'; }
281
+ function readLine(label, body) { return new RegExp('^\\s*' + label + '\\s+([^\\n]+)', 'm').exec(body)?.[1]?.trim(); }
282
+ function readInlineWord(label, text) { return new RegExp('(?:^|\\s)' + label + '\\s+([^\\s,]+)').exec(text)?.[1]?.trim(); }
283
+ function readInlineQuoted(label, text) { return new RegExp("(?:^|\\s)" + label + "\\s+[\"']([^\"']+)[\"']").exec(text)?.[1]?.trim(); }
284
+ function readInlineFlag(label, text) { return new RegExp('(?:^|\\s)' + label + '(?:\\s|$)').test(text) || undefined; }
285
+ function readInlineList(text, ...labels) {
286
+ for (const label of labels) {
287
+ const value = new RegExp('(?:^|\\s)' + label + '\\s+([^\\s]+)').exec(text)?.[1]?.trim();
288
+ if (value) return value.split(/[|,]/).map((item) => item.trim()).filter(Boolean);
289
+ }
290
+ return undefined;
291
+ }
292
+ function parseSpan(value) {
293
+ if (!value) return undefined;
294
+ const match = /^(.+?):(\d+):(\d+)-(\d+):(\d+)$/.exec(value);
295
+ if (!match) return { path: value };
296
+ return { path: match[1], startLine: Number(match[2]), startColumn: Number(match[3]), endLine: Number(match[4]), endColumn: Number(match[5]) };
297
+ }
298
+ function cleanRecord(record) {
299
+ 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)));
300
+ }
301
+ function safeId(value) {
302
+ return String(value ?? 'unknown').replace(/[^A-Za-z0-9_$-]+/g, '_');
303
+ }
@@ -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,14 @@
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 { parseApplicationSurfaceBlock } from './application-surface.js';
5
+ import { parseCanvasSurfaceBlock } from './canvas-surface.js';
4
6
  import { parseDecisionGraphBlock } from './decision-graph.js';
5
7
  import { parseDialectRegistryBlock } from './dialect-registry.js';
6
8
  import { parseInterlinguaBlock } from './interlingua.js';
7
9
  import { createParsedMetadata } from './metadata.js';
8
10
  import { parseSemanticOperationsBlock } from './operations.js';
11
+ import { parsePackageManifestBlock } from './package-manifest.js';
9
12
  import { parseParadigmBlock } from './paradigm.js';
10
13
  import { parseProofBlock } from './proof.js';
11
14
  import { parseResourceGraphBlock } from './resource-graph.js';
@@ -25,6 +28,9 @@ export function parseFrontierSource(source, options = {}) {
25
28
  const interlinguaBlocks = [];
26
29
  const resourceGraphBlocks = [];
27
30
  const nativeSourceBlocks = [];
31
+ const packageManifestBlocks = [];
32
+ const canvasSurfaceBlocks = [];
33
+ const applicationSurfaceBlocks = [];
28
34
  const documentId = options.id ?? readId(source) ?? 'mod_frontier';
29
35
  const documentName = options.name ?? readName(source) ?? 'FrontierModule';
30
36
  for (const block of readBlocks(source)) {
@@ -53,8 +59,11 @@ export function parseFrontierSource(source, options = {}) {
53
59
  if (block.kind === 'dialectRegistry' || block.kind === 'universalDialectRegistry') dialectRegistryBlocks.push(parseDialectRegistryBlock(block));
54
60
  if (block.kind === 'interlingua' || block.kind === 'universalInterlingua') interlinguaBlocks.push(parseInterlinguaBlock(block));
55
61
  if (block.kind === 'resourceGraph' || block.kind === 'semanticResourceGraph') resourceGraphBlocks.push(parseResourceGraphBlock(block));
62
+ if (block.kind === 'packageManifest' || block.kind === 'packageGraph' || block.kind === 'packageSurface') packageManifestBlocks.push(parsePackageManifestBlock(block));
63
+ if (block.kind === 'canvasSurface' || block.kind === 'canvasGraph') canvasSurfaceBlocks.push(parseCanvasSurfaceBlock(block));
64
+ if (block.kind === 'applicationSurface' || block.kind === 'appHost' || block.kind === 'plugin' || block.kind === 'pluginSurface' || block.kind === 'pluginContract') applicationSurfaceBlocks.push(parseApplicationSurfaceBlock(block));
56
65
  }
57
- const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, constraintSpaceBlocks, decisionGraphBlocks, dialectRegistryBlocks, interlinguaBlocks, resourceGraphBlocks, nativeSourceBlocks });
66
+ const metadata = createParsedMetadata({ proofBlocks, paradigmBlocks, operationBlocks, conversionBlocks, constraintSpaceBlocks, decisionGraphBlocks, dialectRegistryBlocks, interlinguaBlocks, resourceGraphBlocks, nativeSourceBlocks, packageManifestBlocks, canvasSurfaceBlocks, applicationSurfaceBlocks });
58
67
  return createDocument({ id: documentId, name: documentName, nodes, ...(metadata ? { metadata } : {}) });
59
68
  }
60
69
 
@@ -64,7 +73,7 @@ function readName(source) { return /module\s+([A-Za-z_$][\w$]*)/.exec(source)?.[
64
73
  function readId(source) { return /module\s+[A-Za-z_$][\w$]*\s+@id\(\s*["']([^"']+)["']\s*\)/.exec(source)?.[1]; }
65
74
  function readBlocks(source) {
66
75
  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;
76
+ 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|applicationSurface|appHost|plugin|pluginSurface|pluginContract)\s+([^{}]+)\{/g;
68
77
  let match;
69
78
  while ((match = header.exec(source))) {
70
79
  let depth = 1; let index = header.lastIndex;
package/dist/metadata.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { mergeDialectRegistryBlocks } from './dialect-registry.js';
2
+ import { mergeApplicationSurfaceBlocks } from './application-surface.js';
2
3
 
3
4
  const PROOF_GROUPS = ['contracts', 'refinements', 'invariants', 'termination', 'temporal', 'obligations', 'artifacts', 'assumptions'];
4
5
  const PARADIGM_GROUPS = [
@@ -23,7 +24,7 @@ const PARADIGM_GROUPS = [
23
24
  'loweringRecords'
24
25
  ];
25
26
 
26
- export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], operationBlocks = [], conversionBlocks = [], constraintSpaceBlocks = [], decisionGraphBlocks = [], dialectRegistryBlocks = [], interlinguaBlocks = [], resourceGraphBlocks = [], nativeSourceBlocks = [] } = {}) {
27
+ export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], operationBlocks = [], conversionBlocks = [], constraintSpaceBlocks = [], decisionGraphBlocks = [], dialectRegistryBlocks = [], interlinguaBlocks = [], resourceGraphBlocks = [], nativeSourceBlocks = [], packageManifestBlocks = [], canvasSurfaceBlocks = [], applicationSurfaceBlocks = [] } = {}) {
27
28
  const metadata = {};
28
29
  if (proofBlocks.length) metadata.proof = mergeProofBlocks(proofBlocks);
29
30
  if (paradigmBlocks.length) metadata.paradigmSemantics = mergeParadigmBlocks(paradigmBlocks);
@@ -34,8 +35,11 @@ export function createParsedMetadata({ proofBlocks = [], paradigmBlocks = [], op
34
35
  if (dialectRegistryBlocks.length) metadata.dialects = mergeDialectRegistryBlocks(dialectRegistryBlocks);
35
36
  if (interlinguaBlocks.length) metadata.universalInterlingua = mergeInterlinguaBlocks(interlinguaBlocks);
36
37
  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);
38
+ if (packageManifestBlocks.length) metadata.packageManifests = mergePackageManifestBlocks(packageManifestBlocks);
39
+ if (canvasSurfaceBlocks.length) metadata.canvasSurfaces = mergeCanvasSurfaceBlocks(canvasSurfaceBlocks);
40
+ if (applicationSurfaceBlocks.length) metadata.applicationSurfaces = mergeApplicationSurfaceBlocks(applicationSurfaceBlocks);
41
+ if (nativeSourceBlocks.some((block) => block.sourceMaps.length || block.mergeCandidates.length || block.evidence.length) || packageManifestBlocks.length || canvasSurfaceBlocks.length || applicationSurfaceBlocks.length) {
42
+ metadata.universalAst = mergeUniversalAstBlocks(nativeSourceBlocks, packageManifestBlocks, canvasSurfaceBlocks, applicationSurfaceBlocks);
39
43
  }
40
44
  return Object.keys(metadata).length ? metadata : undefined;
41
45
  }
@@ -106,7 +110,7 @@ function mergeConstraintSpaceBlocks(blocks) {
106
110
  };
107
111
  }
108
112
 
109
- function mergeNativeSourceBlocks(blocks) {
113
+ function mergeUniversalAstBlocks(blocks, packageManifestBlocks = [], canvasSurfaceBlocks = [], applicationSurfaceBlocks = []) {
110
114
  return {
111
115
  id: blocks.length === 1 ? `universalAst:${blocks[0].node.id}` : 'universalAst:source',
112
116
  nativeSourceIds: blocks.map((block) => block.node.id),
@@ -114,7 +118,74 @@ function mergeNativeSourceBlocks(blocks) {
114
118
  mergeCandidates: blocks.flatMap((block) => block.mergeCandidates ?? []),
115
119
  evidence: blocks.flatMap((block) => block.evidence ?? []),
116
120
  losses: blocks.flatMap((block) => block.losses ?? []),
117
- metadata: { authoredNativeSourceIds: blocks.map((block) => block.node.id) }
121
+ packageManifests: packageManifestBlocks,
122
+ canvasSurfaces: canvasSurfaceBlocks,
123
+ applicationSurfaces: applicationSurfaceBlocks,
124
+ packageManifestIds: ids(packageManifestBlocks),
125
+ canvasSurfaceIds: ids(canvasSurfaceBlocks),
126
+ applicationSurfaceIds: ids(applicationSurfaceBlocks),
127
+ metadata: {
128
+ authoredNativeSourceIds: blocks.map((block) => block.node.id),
129
+ authoredPackageManifestIds: ids(packageManifestBlocks),
130
+ authoredCanvasSurfaceIds: ids(canvasSurfaceBlocks),
131
+ authoredApplicationSurfaceIds: ids(applicationSurfaceBlocks)
132
+ }
133
+ };
134
+ }
135
+
136
+ function mergePackageManifestBlocks(blocks) {
137
+ return {
138
+ id: blocks.length === 1 ? blocks[0].id : 'packageManifests:source',
139
+ manifests: blocks,
140
+ manifestIds: ids(blocks),
141
+ recordIds: blocks.flatMap((block) => ids(block.records)),
142
+ evidenceIds: blocks.flatMap((block) => ids(block.evidence)),
143
+ proofGapCodes: [...new Set(blocks.flatMap((block) => (block.proofGaps ?? []).map((gap) => gap.code).filter(Boolean)))],
144
+ summary: {
145
+ manifestCount: blocks.length,
146
+ recordCount: blocks.reduce((sum, block) => sum + (block.records?.length ?? 0), 0),
147
+ dependencyCount: blocks.reduce((sum, block) => sum + (block.summary?.dependencies ?? 0), 0),
148
+ scriptCount: blocks.reduce((sum, block) => sum + (block.summary?.scripts ?? 0), 0),
149
+ exportCount: blocks.reduce((sum, block) => sum + (block.summary?.exports ?? 0), 0),
150
+ proofGapCount: blocks.reduce((sum, block) => sum + (block.proofGaps?.length ?? 0), 0)
151
+ },
152
+ claims: {
153
+ autoMergeClaim: false,
154
+ semanticEquivalenceClaim: false,
155
+ packageInstallEquivalenceClaim: false,
156
+ installEquivalenceClaim: false,
157
+ runtimeEquivalenceClaim: false
158
+ },
159
+ metadata: { authoredPackageManifestIds: ids(blocks) }
160
+ };
161
+ }
162
+
163
+ function mergeCanvasSurfaceBlocks(blocks) {
164
+ return {
165
+ id: blocks.length === 1 ? blocks[0].id : 'canvasSurfaces:source',
166
+ surfaces: blocks,
167
+ surfaceIds: ids(blocks),
168
+ recordIds: blocks.flatMap((block) => ids(block.records)),
169
+ commandTraceIds: blocks.flatMap((block) => ids(block.commandTraces)),
170
+ evidenceIds: blocks.flatMap((block) => ids(block.evidence)),
171
+ proofGapCodes: [...new Set(blocks.flatMap((block) => (block.proofGaps ?? []).map((gap) => gap.code).filter(Boolean)))],
172
+ summary: {
173
+ surfaceCount: blocks.length,
174
+ recordCount: blocks.reduce((sum, block) => sum + (block.records?.length ?? 0), 0),
175
+ commandTraceCount: blocks.reduce((sum, block) => sum + (block.commandTraces?.length ?? 0), 0),
176
+ drawCommandCount: blocks.reduce((sum, block) => sum + (block.summary?.drawCommands ?? 0), 0),
177
+ offscreenCommandCount: blocks.reduce((sum, block) => sum + (block.summary?.offscreenCommands ?? 0), 0),
178
+ gpuCommandCount: blocks.reduce((sum, block) => sum + (block.summary?.gpuCommands ?? 0), 0),
179
+ proofGapCount: blocks.reduce((sum, block) => sum + (block.proofGaps?.length ?? 0), 0)
180
+ },
181
+ claims: {
182
+ autoMergeClaim: false,
183
+ semanticEquivalenceClaim: false,
184
+ browserRuntimeEquivalenceClaim: false,
185
+ canvasRuntimeEquivalenceClaim: false,
186
+ canvasVisualEquivalenceClaim: false
187
+ },
188
+ metadata: { authoredCanvasSurfaceIds: ids(blocks) }
118
189
  };
119
190
  }
120
191
 
@@ -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.29",
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",