@shapeshift-labs/frontier-lang-parser 0.3.63 → 0.3.65

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.
@@ -1,4 +1,5 @@
1
1
  import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import { createRowIdentityTracker } from './row-identity.js';
2
3
 
3
4
  export function parseApplicationSurfaceBlock(block) {
4
5
  const name = nameFrom(block.header);
@@ -6,26 +7,32 @@ export function parseApplicationSurfaceBlock(block) {
6
7
  const role = readLine('role', block.body) ?? defaultRole(surfaceKind);
7
8
  const sourcePath = readLine('sourcePath', block.body) ?? readLine('path', block.body);
8
9
  const sourceHash = readLine('sourceHash', block.body);
9
- const evidence = parseEvidenceRows(block.body);
10
+ const rowIdentity = createRowIdentityTracker();
11
+ const evidence = [];
10
12
  const records = [];
11
13
  const proofGaps = [];
12
14
  for (const rawLine of block.body.split('\n')) {
13
15
  const line = rawLine.trim();
14
16
  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);
17
+ const match = /^(mount|provide|provides|required|requires|require|route|event|asset|gate|evidence|proofEvidence|gap|proofGap)\s+([A-Za-z_$@/.*][\w$./@:*+-]*)(.*)$/.exec(line);
16
18
  if (!match) continue;
17
19
  const [, rowKind, rowName, rest] = match;
20
+ if (rowKind === 'evidence' || rowKind === 'proofEvidence') {
21
+ rowIdentity.push(evidence, applicationEvidence(rowName, rest), { rowKind, normalizedRowKind: 'evidence', name: rowName });
22
+ continue;
23
+ }
18
24
  if (rowKind === 'gap' || rowKind === 'proofGap') {
19
- proofGaps.push(applicationProofGap(rowName, rest));
25
+ rowIdentity.push(proofGaps, applicationProofGap(rowName, rest), { rowKind, normalizedRowKind: 'proofGap', name: rowName });
20
26
  continue;
21
27
  }
22
- records.push(applicationRecord(normalizeRowKind(rowKind), rowName, rest, {
28
+ const normalizedRowKind = normalizeRowKind(rowKind);
29
+ rowIdentity.push(records, applicationRecord(normalizedRowKind, rowName, rest, {
23
30
  surfaceId: idFrom(block.header, `application_surface_${safeId(name)}`),
24
31
  surfaceName: name,
25
32
  role,
26
33
  sourcePath,
27
34
  sourceHash
28
- }));
35
+ }), { rowKind, normalizedRowKind, name: rowName });
29
36
  }
30
37
  const allGaps = [...records.flatMap((record) => record.proofGaps ?? []), ...proofGaps];
31
38
  const tree = {
@@ -41,7 +48,7 @@ export function parseApplicationSurfaceBlock(block) {
41
48
  records,
42
49
  proofGaps: allGaps,
43
50
  evidence,
44
- parser: { status: 'authored', errors: [] },
51
+ parser: { status: 'authored', errors: rowIdentity.errors },
45
52
  claims: applicationFalseClaims(),
46
53
  metadata: { authoredName: name, authoredBlockKind: block.kind }
47
54
  };
@@ -200,22 +207,15 @@ function applicationProofGap(name, text) {
200
207
  });
201
208
  }
202
209
 
203
- function parseEvidenceRows(body) {
204
- const records = [];
205
- for (const rawLine of body.split('\n')) {
206
- const line = rawLine.trim();
207
- const match = /^(?:evidence|proofEvidence)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
208
- if (!match) continue;
209
- records.push(cleanRecord({
210
- id: idFrom(match[2], `evidence_${match[1]}`),
211
- kind: readInlineWord('kind', match[2]) ?? 'note',
212
- status: readInlineWord('status', match[2]) ?? 'unknown',
213
- path: readInlineWord('path', match[2]),
214
- summary: readInlineQuoted('summary', match[2]),
215
- metadata: { name: match[1] }
216
- }));
217
- }
218
- return records;
210
+ function applicationEvidence(name, text) {
211
+ return cleanRecord({
212
+ id: idFrom(text, `evidence_${name}`),
213
+ kind: readInlineWord('kind', text) ?? 'note',
214
+ status: readInlineWord('status', text) ?? 'unknown',
215
+ path: readInlineWord('path', text),
216
+ summary: readInlineQuoted('summary', text),
217
+ metadata: { name }
218
+ });
219
219
  }
220
220
 
221
221
  function summarizeApplicationSurface(tree) {
@@ -280,7 +280,7 @@ function defaultRole(surfaceKind) {
280
280
  function count(records, kind) { return records.filter((record) => record.recordKind === kind).length; }
281
281
  function ids(records = []) { return records.map((record) => record?.id).filter(Boolean); }
282
282
  function idsByRecordKind(records = [], kind) { return ids(records.filter((record) => record?.recordKind === kind)); }
283
- function isPropertyLine(line) { return /^(role|host|hostId|sourcePath|path|sourceHash|evidence|proofEvidence)\s+/.test(line); }
283
+ function isPropertyLine(line) { return /^(role|host|hostId|sourcePath|path|sourceHash)\s+/.test(line); }
284
284
  function idFrom(text, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(text)?.[1] ?? fallback; }
285
285
  function nameFrom(header) { return /^([A-Za-z_$][\w$-]*)/.exec(header)?.[1] ?? 'ApplicationSurface'; }
286
286
  function readLine(label, body) { return new RegExp('^\\s*' + label + '\\s+([^\\n]+)', 'm').exec(body)?.[1]?.trim(); }
@@ -1,28 +1,35 @@
1
1
  import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import { createRowIdentityTracker } from './row-identity.js';
2
3
 
3
4
  export function parseCanvasSurfaceBlock(block) {
4
5
  const name = nameFrom(block.header);
5
6
  const sourcePath = readLine('sourcePath', block.body) ?? readLine('path', block.body);
6
7
  const sourceHash = readLine('sourceHash', block.body);
7
- const evidence = parseEvidenceRows(block.body);
8
+ const rowIdentity = createRowIdentityTracker();
9
+ const evidence = [];
8
10
  const records = [];
9
11
  const commandTraces = [];
10
12
  const proofGaps = [];
11
13
  for (const rawLine of block.body.split('\n')) {
12
14
  const line = rawLine.trim();
13
15
  if (!line || line.startsWith('#') || isPropertyLine(line)) continue;
14
- const match = /^(element|command|state|stateWrite|trace|gap|proofGap)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
16
+ const match = /^(element|command|state|stateWrite|trace|evidence|proofEvidence|gap|proofGap)\s+([A-Za-z_$][\w$-]*)(.*)$/.exec(line);
15
17
  if (!match) continue;
16
18
  const [, rowKind, rowName, rest] = match;
19
+ if (rowKind === 'evidence' || rowKind === 'proofEvidence') {
20
+ rowIdentity.push(evidence, canvasEvidence(rowName, rest), { rowKind, normalizedRowKind: 'evidence', name: rowName });
21
+ continue;
22
+ }
17
23
  if (rowKind === 'gap' || rowKind === 'proofGap') {
18
- proofGaps.push(canvasProofGap(rowName, rest));
24
+ rowIdentity.push(proofGaps, canvasProofGap(rowName, rest), { rowKind, normalizedRowKind: 'proofGap', name: rowName });
19
25
  continue;
20
26
  }
21
27
  if (rowKind === 'trace') {
22
- commandTraces.push(canvasTrace(rowName, rest, { sourcePath }));
28
+ rowIdentity.push(commandTraces, canvasTrace(rowName, rest, { sourcePath }), { rowKind, normalizedRowKind: 'trace', name: rowName });
23
29
  continue;
24
30
  }
25
- records.push(canvasRecord(rowKind === 'stateWrite' ? 'state-write' : rowKind, rowName, rest, { sourcePath, sourceHash }));
31
+ const normalizedRowKind = rowKind === 'stateWrite' ? 'state-write' : rowKind;
32
+ rowIdentity.push(records, canvasRecord(normalizedRowKind, rowName, rest, { sourcePath, sourceHash }), { rowKind, normalizedRowKind, name: rowName });
26
33
  }
27
34
  const allGaps = [...records.flatMap((record) => record.proofGaps ?? []), ...proofGaps];
28
35
  const tree = {
@@ -36,7 +43,7 @@ export function parseCanvasSurfaceBlock(block) {
36
43
  commandTraces,
37
44
  proofGaps: allGaps,
38
45
  evidence,
39
- parser: { status: 'authored', errors: [] },
46
+ parser: { status: 'authored', errors: rowIdentity.errors },
40
47
  claims: {
41
48
  autoMergeClaim: false,
42
49
  semanticEquivalenceClaim: false,
@@ -121,22 +128,15 @@ function canvasProofGap(name, text) {
121
128
  });
122
129
  }
123
130
 
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;
131
+ function canvasEvidence(name, text) {
132
+ return cleanRecord({
133
+ id: idFrom(text, `evidence_${name}`),
134
+ kind: readInlineWord('kind', text) ?? 'note',
135
+ status: readInlineWord('status', text) ?? 'unknown',
136
+ path: readInlineWord('path', text),
137
+ summary: readInlineQuoted('summary', text),
138
+ metadata: { name }
139
+ });
140
140
  }
141
141
 
142
142
  function summarizeCanvasSurface(tree) {
@@ -173,7 +173,7 @@ function hashableRecord(record) {
173
173
  }
174
174
 
175
175
  function isPropertyLine(line) {
176
- return /^(sourcePath|path|sourceHash|evidence|proofEvidence)\s+/.test(line);
176
+ return /^(sourcePath|path|sourceHash)\s+/.test(line);
177
177
  }
178
178
 
179
179
  function idFrom(text, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(text)?.[1] ?? fallback; }
@@ -1,24 +1,30 @@
1
1
  import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import { createRowIdentityTracker } from './row-identity.js';
2
3
 
3
4
  export function parsePackageManifestBlock(block) {
4
5
  const name = nameFrom(block.header);
5
6
  const sourcePath = readLine('sourcePath', block.body) ?? readLine('path', block.body) ?? 'package.json';
6
7
  const sourceHash = readLine('sourceHash', block.body);
7
- const evidence = parseEvidenceRows(block.body);
8
+ const rowIdentity = createRowIdentityTracker();
9
+ const evidence = [];
8
10
  const records = [];
9
11
  const proofGaps = [];
10
12
  for (const rawLine of block.body.split('\n')) {
11
13
  const line = rawLine.trim();
12
14
  if (!line || line.startsWith('#') || isPropertyLine(line)) continue;
13
- const match = /^(metadata|dependency|script|export|gap|proofGap)\s+([A-Za-z_$.*@/][\w$./@*-]*)(.*)$/.exec(line);
15
+ const match = /^(metadata|dependency|script|export|evidence|proofEvidence|gap|proofGap)\s+([A-Za-z_$.*@/][\w$./@*-]*)(.*)$/.exec(line);
14
16
  if (!match) continue;
15
17
  const [, rowKind, rowName, rest] = match;
18
+ if (rowKind === 'evidence' || rowKind === 'proofEvidence') {
19
+ rowIdentity.push(evidence, packageEvidence(rowName, rest), { rowKind, normalizedRowKind: 'evidence', name: rowName });
20
+ continue;
21
+ }
16
22
  if (rowKind === 'gap' || rowKind === 'proofGap') {
17
- proofGaps.push(packageProofGap(rowName, rest));
23
+ rowIdentity.push(proofGaps, packageProofGap(rowName, rest), { rowKind, normalizedRowKind: 'proofGap', name: rowName });
18
24
  continue;
19
25
  }
20
26
  const record = packageRecord(rowKind, rowName, rest, { sourcePath, sourceHash, evidence });
21
- if (record) records.push(record);
27
+ rowIdentity.push(records, record, { rowKind, normalizedRowKind: rowKind, name: rowName });
22
28
  }
23
29
  const recordGaps = records.flatMap((record) => record.proofGaps ?? []);
24
30
  const allGaps = [...recordGaps, ...proofGaps];
@@ -33,7 +39,7 @@ export function parsePackageManifestBlock(block) {
33
39
  records,
34
40
  proofGaps: allGaps,
35
41
  evidence,
36
- parser: { status: 'authored', errors: [] },
42
+ parser: { status: 'authored', errors: rowIdentity.errors },
37
43
  claims: {
38
44
  autoMergeClaim: false,
39
45
  semanticEquivalenceClaim: false,
@@ -99,22 +105,15 @@ function packageProofGap(name, text) {
99
105
  });
100
106
  }
101
107
 
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;
108
+ function packageEvidence(name, text) {
109
+ return cleanRecord({
110
+ id: idFrom(text, `evidence_${name}`),
111
+ kind: readInlineWord('kind', text) ?? 'note',
112
+ status: readInlineWord('status', text) ?? 'unknown',
113
+ path: readInlineWord('path', text),
114
+ summary: readInlineQuoted('summary', text),
115
+ metadata: { name }
116
+ });
118
117
  }
119
118
 
120
119
  function summarizePackageManifest(tree) {
@@ -133,7 +132,7 @@ function hashableRecord(record) {
133
132
  }
134
133
 
135
134
  function isPropertyLine(line) {
136
- return /^(sourcePath|path|sourceHash|packageManager|evidence|proofEvidence)\s+/.test(line);
135
+ return /^(sourcePath|path|sourceHash|packageManager)\s+/.test(line);
137
136
  }
138
137
 
139
138
  function idFrom(text, fallback) { return /@id\(\s*["']([^"']+)["']\s*\)/.exec(text)?.[1] ?? fallback; }
@@ -1,3 +1,5 @@
1
+ import { createRowIdentityTracker } from './row-identity.js';
2
+
1
3
  const GROUPS = {
2
4
  resource: 'resources',
3
5
  owner: 'owners',
@@ -16,6 +18,7 @@ const GROUPS = {
16
18
 
17
19
  export function parseResourceGraphBlock(block) {
18
20
  const name = nameFrom(block.header);
21
+ const rowIdentity = createRowIdentityTracker();
19
22
  const graph = {
20
23
  kind: 'frontier.lang.semanticResourceGraph',
21
24
  version: 1,
@@ -40,6 +43,7 @@ export function parseResourceGraphBlock(block) {
40
43
  unsafeBoundaries: [],
41
44
  conflicts: [],
42
45
  proofObligations: [],
46
+ parser: { status: 'authored', errors: rowIdentity.errors },
43
47
  claims: {
44
48
  borrowCheckerClaim: false,
45
49
  aliasSafetyClaim: false,
@@ -59,7 +63,7 @@ export function parseResourceGraphBlock(block) {
59
63
  const normalized = normalizeRowKind(rowKind);
60
64
  const record = parseResourceRecord(normalized, rowName, rest, graph);
61
65
  const group = GROUPS[normalized];
62
- if (record && group) graph[group].push(record);
66
+ if (record && group) rowIdentity.push(graph[group], record, { rowKind, normalizedRowKind: normalized, name: rowName });
63
67
  }
64
68
 
65
69
  graph.outlives = graph.lifetimeRelations;
@@ -133,6 +137,7 @@ function summarize(graph) {
133
137
  conflicts: graph.conflicts.length,
134
138
  proofObligations: graph.proofObligations.length,
135
139
  unsafeBoundariesWithoutProof: graph.unsafeBoundaries.filter((record) => record.proofStatus !== 'passed').length,
140
+ parseErrors: graph.parser?.errors?.length ?? 0,
136
141
  reasonCodes: unique(graph.conflicts.map((record) => record.reasonCode))
137
142
  };
138
143
  }
@@ -0,0 +1,81 @@
1
+ export function createRowIdentityTracker(options = {}) {
2
+ const seenIds = new Map();
3
+ const errors = [];
4
+ const code = options.code ?? 'duplicate-generic-row-id';
5
+
6
+ function accept(record, context = {}) {
7
+ if (!record?.id) return true;
8
+ const first = seenIds.get(record.id);
9
+ if (first) {
10
+ errors.push(duplicateRowIdError(record, context, first, code, errors.length, true));
11
+ return false;
12
+ }
13
+ seenIds.set(record.id, rowIdentity(record, context));
14
+ return true;
15
+ }
16
+
17
+ function preserve(record, context = {}) {
18
+ if (!record?.id) return true;
19
+ const first = seenIds.get(record.id);
20
+ if (first) {
21
+ errors.push(duplicateRowIdError(record, context, first, code, errors.length, false));
22
+ return true;
23
+ }
24
+ seenIds.set(record.id, rowIdentity(record, context));
25
+ return true;
26
+ }
27
+
28
+ return {
29
+ errors,
30
+ accept,
31
+ preserve,
32
+ push(target, record, context = {}) {
33
+ if (record && accept(record, context)) target.push(record);
34
+ return record;
35
+ }
36
+ };
37
+ }
38
+
39
+ function duplicateRowIdError(record, context, first, code, index, suppressed) {
40
+ const current = rowIdentity(record, context);
41
+ return cleanRecord({
42
+ id: `parser_error_${safeId(code)}_${safeId(record.id)}_${index}`,
43
+ code,
44
+ reason: code,
45
+ severity: 'error',
46
+ failClosed: true,
47
+ action: suppressed ? 'ignored-duplicate-row' : 'preserved-duplicate-row',
48
+ disposition: context.disposition,
49
+ suppressed,
50
+ rowId: record.id,
51
+ nodeId: record.id,
52
+ rowKind: current.rowKind,
53
+ normalizedRowKind: current.normalizedRowKind,
54
+ name: current.name,
55
+ sourceSpan: current.sourceSpan,
56
+ firstRowKind: first.rowKind,
57
+ firstNormalizedRowKind: first.normalizedRowKind,
58
+ firstName: first.name,
59
+ firstSourceSpan: first.sourceSpan,
60
+ message: suppressed
61
+ ? `Duplicate authored row id "${record.id}" was ignored.`
62
+ : `Duplicate authored row id "${record.id}" was preserved.`
63
+ });
64
+ }
65
+
66
+ function rowIdentity(record, context = {}) {
67
+ return cleanRecord({
68
+ rowKind: context.rowKind ?? record.rowKind ?? record.recordKind ?? record.kind,
69
+ normalizedRowKind: context.normalizedRowKind ?? record.normalizedRowKind ?? record.recordKind ?? record.kind,
70
+ name: context.name ?? record.name,
71
+ sourceSpan: context.sourceSpan ?? record.sourceSpan
72
+ });
73
+ }
74
+
75
+ function cleanRecord(record) {
76
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
77
+ }
78
+
79
+ function safeId(value) {
80
+ return String(value ?? 'unknown').replace(/[^A-Za-z0-9_$-]+/g, '_');
81
+ }
@@ -9,7 +9,8 @@ export function summarizeRuntimeCapabilities(matrix) {
9
9
  capabilityCount: matrix.hostProfiles.reduce((total, profile) => total + Object.keys(profile.capabilities ?? {}).length, 0),
10
10
  runtimeRequirementCount: matrix.runtimeRequirements.length,
11
11
  evidenceCount: matrix.evidence.length,
12
- proofGapCount: matrix.proofGaps.length
12
+ proofGapCount: matrix.proofGaps.length,
13
+ parseErrors: matrix.parser?.errors?.length ?? 0
13
14
  };
14
15
  }
15
16
 
@@ -1,4 +1,5 @@
1
1
  import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
+ import { createRowIdentityTracker } from './row-identity.js';
2
3
  import {
3
4
  cleanRecord,
4
5
  hostIds,
@@ -21,6 +22,7 @@ import {
21
22
 
22
23
  export function parseRuntimeCapabilityBlock(block) {
23
24
  const name = nameFrom(block.header);
25
+ const rowIdentity = createRowIdentityTracker();
24
26
  const matrix = {
25
27
  kind: 'frontier.lang.authoredRuntimeCapabilityMatrixInput',
26
28
  version: 1,
@@ -34,6 +36,7 @@ export function parseRuntimeCapabilityBlock(block) {
34
36
  runtimeRequirements: [],
35
37
  evidence: [],
36
38
  proofGaps: [],
39
+ parser: { status: 'authored', errors: rowIdentity.errors },
37
40
  claims: runtimeFalseClaims(),
38
41
  metadata: { authoredName: name, authoredBlockKind: block.kind }
39
42
  };
@@ -46,26 +49,35 @@ export function parseRuntimeCapabilityBlock(block) {
46
49
  if (!match) continue;
47
50
  const [, rowKind, rowName, rest] = match;
48
51
  if (rowKind === 'gap' || rowKind === 'proofGap') {
49
- matrix.proofGaps.push(runtimeProofGap(rowName, rest));
52
+ rowIdentity.push(matrix.proofGaps, runtimeProofGap(rowName, rest), { rowKind, normalizedRowKind: 'proofGap', name: rowName });
50
53
  continue;
51
54
  }
52
55
  if (rowKind === 'evidence' || rowKind === 'proofEvidence') {
53
- matrix.evidence.push(runtimeEvidence(rowName, rest));
56
+ rowIdentity.push(matrix.evidence, runtimeEvidence(rowName, rest), { rowKind, normalizedRowKind: 'evidence', name: rowName });
54
57
  continue;
55
58
  }
56
59
  if (rowKind === 'requirement' || rowKind === 'runtimeRequirement' || rowKind === 'requiredRuntime') {
57
- matrix.runtimeRequirements.push(runtimeRequirement(rowName, rest));
60
+ rowIdentity.push(matrix.runtimeRequirements, runtimeRequirement(rowName, rest), { rowKind, normalizedRowKind: 'runtimeRequirement', name: rowName });
58
61
  continue;
59
62
  }
60
63
  if (rowKind === 'capability' || rowKind === 'hostCapability') {
61
- attachCapability(hostMap, matrix, rowName, rest);
64
+ const capabilityRecord = runtimeHostCapability(rowName, rest);
65
+ if (capabilityRecord && rowIdentity.accept(capabilityRecord, { rowKind, normalizedRowKind: 'hostCapability', name: rowName })) {
66
+ attachCapability(hostMap, matrix, capabilityRecord, rest);
67
+ }
62
68
  continue;
63
69
  }
64
70
  if (rowKind === 'hostBinding' || rowKind === 'binding') {
65
- matrix.hostBindings.push(runtimeHostBinding(rowName, rest));
71
+ rowIdentity.push(matrix.hostBindings, runtimeHostBinding(rowName, rest), { rowKind, normalizedRowKind: 'hostBinding', name: rowName });
66
72
  continue;
67
73
  }
68
74
  const profile = runtimeHostProfile(rowKind, rowName, rest);
75
+ rowIdentity.preserve(profile, {
76
+ rowKind,
77
+ normalizedRowKind: 'hostProfile',
78
+ name: rowName,
79
+ disposition: 'preserved-runtime-host-profile-upsert'
80
+ });
69
81
  upsertHost(hostMap, matrix, profile, rowKind);
70
82
  }
71
83
 
@@ -115,6 +127,7 @@ export function mergeRuntimeCapabilityBlocks(blocks) {
115
127
  const runtimeRequirements = uniqueRecords(blocks.flatMap((block) => block.runtimeRequirements ?? []));
116
128
  const evidence = uniqueRecords(blocks.flatMap((block) => block.evidence ?? []));
117
129
  const proofGaps = uniqueRecords(blocks.flatMap((block) => block.proofGaps ?? []));
130
+ const parser = { status: 'authored', errors: blocks.flatMap((block) => block.parser?.errors ?? []) };
118
131
  return {
119
132
  kind: 'frontier.lang.authoredRuntimeCapabilityMatrixInput',
120
133
  version: 1,
@@ -129,6 +142,7 @@ export function mergeRuntimeCapabilityBlocks(blocks) {
129
142
  runtimeRequirements,
130
143
  evidence,
131
144
  proofGaps,
145
+ parser,
132
146
  hostProfileIds: ids(hostProfiles),
133
147
  sourceHostIds: hostIds(sourceHosts),
134
148
  targetHostIds: hostIds(targetHosts),
@@ -137,7 +151,7 @@ export function mergeRuntimeCapabilityBlocks(blocks) {
137
151
  runtimeRequirementIds: ids(runtimeRequirements),
138
152
  evidenceIds: ids(evidence),
139
153
  proofGapCodes: [...new Set(proofGaps.map((gap) => gap.code).filter(Boolean))],
140
- summary: summarizeRuntimeCapabilities({ hostProfiles, sourceHosts, targetHosts, hostCapabilities, hostBindings, runtimeRequirements, evidence, proofGaps }),
154
+ summary: summarizeRuntimeCapabilities({ hostProfiles, sourceHosts, targetHosts, hostCapabilities, hostBindings, runtimeRequirements, evidence, proofGaps, parser }),
141
155
  claims: runtimeFalseClaims(),
142
156
  metadata: { authoredRuntimeCapabilityBlockIds: ids(blocks) }
143
157
  };
@@ -177,13 +191,32 @@ function upsertHost(hostMap, matrix, profile, rowKind) {
177
191
  if (rowKind === 'targetHost' || merged.role === 'target') matrix.targetHosts.push(merged.id);
178
192
  }
179
193
 
180
- function attachCapability(hostMap, matrix, name, text) {
181
- const hostId = readInlineWord('host', text) ?? readInlineWord('hostId', text) ?? readInlineWord('sourceHost', text) ?? readInlineWord('targetHost', text);
182
- if (!hostId) return;
194
+ function attachCapability(hostMap, matrix, capabilityRecord, text) {
195
+ const hostId = capabilityRecord.hostId;
183
196
  const profile = hostMap.get(hostId) ?? runtimeHostProfile('host', hostId, '');
197
+ const capability = capabilityRecord.capability;
198
+ const normalized = cleanRecord({
199
+ kind: capability,
200
+ support: capabilityRecord.support,
201
+ binding: capabilityRecord.binding ?? `${hostId}.${capability}`,
202
+ notes: readInlineList(text, 'note', 'notes'),
203
+ evidenceIds: capabilityRecord.evidenceIds,
204
+ sourcePath: capabilityRecord.sourcePath,
205
+ sourceHash: capabilityRecord.sourceHash
206
+ });
207
+ profile.capabilities = { ...(profile.capabilities ?? {}), [capability]: normalized };
208
+ hostMap.set(hostId, profile);
209
+ matrix.hostCapabilities.push(capabilityRecord);
210
+ if (readInlineFlag('source', text) || readInlineWord('role', text) === 'source') matrix.sourceHosts.push(hostId);
211
+ if (readInlineFlag('target', text) || readInlineWord('role', text) === 'target') matrix.targetHosts.push(hostId);
212
+ }
213
+
214
+ function runtimeHostCapability(name, text) {
215
+ const hostId = readInlineWord('host', text) ?? readInlineWord('hostId', text) ?? readInlineWord('sourceHost', text) ?? readInlineWord('targetHost', text);
216
+ if (!hostId) return undefined;
184
217
  const capability = readInlineWord('capability', text) ?? readInlineWord('kind', text) ?? name;
185
218
  const bindingId = readInlineWord('bindingId', text) ?? readInlineWord('binding', text);
186
- const capabilityRecord = cleanRecord({
219
+ return cleanRecord({
187
220
  kind: 'frontier.lang.runtimeCapability.hostCapability',
188
221
  id: idFrom(text, `runtime_capability_${safeId(hostId)}_${safeId(capability)}`),
189
222
  name,
@@ -204,20 +237,6 @@ function attachCapability(hostMap, matrix, name, text) {
204
237
  failClosed: true,
205
238
  claims: runtimeFalseClaims()
206
239
  });
207
- const normalized = cleanRecord({
208
- kind: capability,
209
- support: capabilityRecord.support,
210
- binding: capabilityRecord.binding ?? `${hostId}.${capability}`,
211
- notes: readInlineList(text, 'note', 'notes'),
212
- evidenceIds: capabilityRecord.evidenceIds,
213
- sourcePath: capabilityRecord.sourcePath,
214
- sourceHash: capabilityRecord.sourceHash
215
- });
216
- profile.capabilities = { ...(profile.capabilities ?? {}), [capability]: normalized };
217
- hostMap.set(hostId, profile);
218
- matrix.hostCapabilities.push(capabilityRecord);
219
- if (readInlineFlag('source', text) || readInlineWord('role', text) === 'source') matrix.sourceHosts.push(hostId);
220
- if (readInlineFlag('target', text) || readInlineWord('role', text) === 'target') matrix.targetHosts.push(hostId);
221
240
  }
222
241
 
223
242
  function runtimeHostBinding(name, text) {
@@ -222,6 +222,7 @@ function conversionChild(source, block, options, line, child) {
222
222
 
223
223
  function readGenericRowSyntaxChildren(source, block, options, config) {
224
224
  const children = [];
225
+ const seenIds = new Set();
225
226
  const rowPattern = new RegExp('^([A-Za-z_$][\\w$-]*)\\s+' + ROW_NAME_PATTERN + '(.*)$');
226
227
  for (const line of readBodyLines(source, block)) {
227
228
  if (!line.text || line.text.startsWith('#')) continue;
@@ -230,12 +231,22 @@ function readGenericRowSyntaxChildren(source, block, options, config) {
230
231
  const [, rowKind, name, rest] = row;
231
232
  if (!config.rowKinds.has(rowKind)) continue;
232
233
  const normalizedRowKind = config.normalize?.(rowKind) ?? rowKind;
234
+ const id = idFrom(rest, `${config.idPrefix}_${safeId(normalizedRowKind)}_${safeId(name)}`);
235
+ let recognized = true;
236
+ let reason;
237
+ if (seenIds.has(id)) {
238
+ recognized = false;
239
+ reason = 'duplicate-generic-row-id';
240
+ }
241
+ if (recognized) {
242
+ seenIds.add(id);
243
+ }
233
244
  children.push(cleanRecord({
234
245
  kind: config.childKind,
235
246
  rowKind,
236
247
  normalizedRowKind,
237
248
  name,
238
- id: idFrom(rest, `${config.idPrefix}_${safeId(normalizedRowKind)}_${safeId(name)}`),
249
+ id,
239
250
  header: line.text,
240
251
  startOffset: line.startOffset,
241
252
  endOffset: line.endOffset,
@@ -246,7 +257,8 @@ function readGenericRowSyntaxChildren(source, block, options, config) {
246
257
  moduleId: block.moduleId,
247
258
  moduleName: block.moduleName,
248
259
  sourceSpan: sourceSpan(source, block, line.startOffset, line.endOffset, options),
249
- recognized: true
260
+ recognized,
261
+ reason
250
262
  }));
251
263
  }
252
264
  return children;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-parser",
3
- "version": "0.3.63",
3
+ "version": "0.3.65",
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/member-identity-smoke.mjs && node test/type-variant-payload-smoke.mjs && node test/type-generic-ref-smoke.mjs && node test/type-parameter-constraints-smoke.mjs && node test/type-structural-expression-smoke.mjs && node test/action-body-smoke.mjs && node test/action-structured-literals-smoke.mjs && node test/action-return-smoke.mjs && node test/action-else-smoke.mjs && node test/action-match-smoke.mjs && node test/action-for-in-smoke.mjs && node test/action-repeat-smoke.mjs && node test/action-call-smoke.mjs && node test/target-projection-aggregate-smoke.mjs && node test/conversion-constraint-fields-smoke.mjs && node test/conversion-evidence-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",
25
+ "test": "npm run build && node test/smoke.mjs && node test/member-identity-smoke.mjs && node test/generic-row-parse-identity-smoke.mjs && node test/type-variant-payload-smoke.mjs && node test/type-generic-ref-smoke.mjs && node test/type-parameter-constraints-smoke.mjs && node test/type-structural-expression-smoke.mjs && node test/action-body-smoke.mjs && node test/action-structured-literals-smoke.mjs && node test/action-return-smoke.mjs && node test/action-else-smoke.mjs && node test/action-match-smoke.mjs && node test/action-for-in-smoke.mjs && node test/action-repeat-smoke.mjs && node test/action-call-smoke.mjs && node test/target-projection-aggregate-smoke.mjs && node test/conversion-constraint-fields-smoke.mjs && node test/conversion-evidence-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",