@shapeshift-labs/frontier-lang-compiler 0.2.54 → 0.2.56

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
@@ -19,6 +19,31 @@ const result = compileFrontierSource(source, { target: 'typescript' });
19
19
  if (result.ok) console.log(result.output);
20
20
  ```
21
21
 
22
+ Run a small end-to-end demo after installing or building the package:
23
+
24
+ ```sh
25
+ npm run build
26
+ node examples/native-js-to-rust-demo.mjs
27
+ ```
28
+
29
+ Run the interactive Frontier-style workbench with editable TypeScript and Rust panes:
30
+
31
+ ```sh
32
+ npm run demo:ts-rust -- --port 4177
33
+ ```
34
+
35
+ The workbench converts only when `Run` is pressed. TypeScript edits project through
36
+ the Frontier semantic graph into Rust scaffolding; Rust edits project back through
37
+ the graph into TypeScript scaffolding. The middle pane shows symbols, relations,
38
+ source maps, readiness, losses, patch hints, and the explicit supported/review-only/
39
+ unsupported bounds for the current direction.
40
+
41
+ The demo prints JavaScript source, the Frontier universal AST/semantic-index summary,
42
+ Rust declaration stubs, a host-adapter Rust projection, and a direct Frontier-source
43
+ to Rust projection. Native JavaScript projection remains loss-aware: without a
44
+ target adapter the compiler emits review-required stubs rather than claiming a
45
+ lossless JS-to-Rust transpilation.
46
+
22
47
  Emit code with declaration-level source-map sidecars for semantic review and merge admission:
23
48
 
24
49
  ```js
@@ -18,9 +18,13 @@ export function createSemanticImportSidecar(importResult, options = {}) {
18
18
  const proofSpec = summarizeSemanticImportSidecarProofSpec(importEntries);
19
19
  const paradigmSemantics = summarizeSemanticImportSidecarParadigmSemantics(importEntries);
20
20
  const dependencies = summarizeSemanticImportDependencies(imports);
21
+ const entryReadiness = importEntries.reduce(
22
+ (current, entry) => maxSemanticMergeReadiness(current, entry.readiness),
23
+ lossSummary.semanticMergeReadiness
24
+ );
21
25
  const readiness = mergeCandidates.reduce(
22
26
  (current, candidate) => maxSemanticMergeReadiness(current, candidate.readiness),
23
- lossSummary.semanticMergeReadiness
27
+ entryReadiness
24
28
  );
25
29
  const patchHints = ownershipRegions.map((region) => semanticPatchHintForRegion(region, readiness, options));
26
30
  const quality = createSemanticImportSidecarQuality({
@@ -1,24 +1,32 @@
1
- import{declarationRecord}from'./declarationRecord.js';import{nativeNodeId}from'./nativeNodeId.js';import{shortNodeText}from'./shortNodeText.js';import{treeSitterFieldText}from'./treeSitterFieldText.js';
1
+ import{declarationRecord}from'./declarationRecord.js';import{shortNodeText}from'./shortNodeText.js';import{treeSitterChildText,treeSitterFieldText}from'./treeSitterNodeAccess.js';
2
2
  export function treeSitterDeclaration(node, kind, nativeNodeId, input) {
3
3
  if (/import|include|use/.test(kind)) {
4
- const name = treeSitterFieldText(node, 'path') ?? treeSitterFieldText(node, 'source') ?? shortNodeText(node);
4
+ const name = treeSitterNamedText(node, ['string', 'string_fragment', 'identifier']) ?? shortNodeText(node);
5
5
  if (name) return declarationRecord(input, nativeNodeId, name, 'module', 'import');
6
6
  }
7
7
  if (/function|method|fn_item|function_declaration/.test(kind)) {
8
- const name = treeSitterFieldText(node, 'name');
8
+ const name = treeSitterNamedText(node, ['identifier', 'property_identifier']);
9
9
  if (name) return declarationRecord(input, nativeNodeId, name, 'function', 'definition');
10
10
  }
11
11
  if (/class/.test(kind)) {
12
- const name = treeSitterFieldText(node, 'name');
12
+ const name = treeSitterNamedText(node, ['type_identifier', 'identifier']);
13
13
  if (name) return declarationRecord(input, nativeNodeId, name, 'class', 'definition');
14
14
  }
15
15
  if (/interface/.test(kind)) {
16
- const name = treeSitterFieldText(node, 'name');
16
+ const name = treeSitterNamedText(node, ['type_identifier', 'identifier']);
17
17
  if (name) return declarationRecord(input, nativeNodeId, name, 'interface', 'definition');
18
18
  }
19
19
  if (/struct|enum|type/.test(kind)) {
20
- const name = treeSitterFieldText(node, 'name');
20
+ const name = treeSitterNamedText(node, ['type_identifier', 'identifier']);
21
21
  if (name) return declarationRecord(input, nativeNodeId, name, 'type', 'definition');
22
22
  }
23
+ if (/variable_declarator|property_declaration|field_declaration/.test(kind)) {
24
+ const name = treeSitterNamedText(node, ['identifier', 'property_identifier', 'field_identifier']);
25
+ if (name) return declarationRecord(input, nativeNodeId, name, 'variable', 'definition');
26
+ }
23
27
  return undefined;
24
28
  }
29
+
30
+ function treeSitterNamedText(node, childKinds) {
31
+ return treeSitterFieldText(node, 'name') ?? treeSitterFieldText(node, 'declarator') ?? treeSitterChildText(node, childKinds);
32
+ }
@@ -1,5 +1 @@
1
- import{shortNodeText}from'./shortNodeText.js';
2
- export function treeSitterFieldText(node, field) {
3
- if (typeof node.childForFieldName !== 'function') return undefined;
4
- return shortNodeText(node.childForFieldName(field));
5
- }
1
+ export{treeSitterFieldText}from'./treeSitterNodeAccess.js';
@@ -0,0 +1,75 @@
1
+ import{shortNodeText}from'./shortNodeText.js';
2
+ export function treeSitterNodeKind(node) {
3
+ return String(node?.type ?? node?.kind ?? 'node');
4
+ }
5
+
6
+ export function treeSitterChildren(node) {
7
+ const children = treeSitterArrayChildren(node, 'children');
8
+ if (children.length) return children;
9
+ const childCountChildren = treeSitterIndexedChildren(node, 'childCount', 'child');
10
+ if (childCountChildren.length) return childCountChildren;
11
+ const namedChildren = treeSitterArrayChildren(node, 'namedChildren');
12
+ if (namedChildren.length) return namedChildren;
13
+ return treeSitterIndexedChildren(node, 'namedChildCount', 'namedChild');
14
+ }
15
+
16
+ export function treeSitterChildText(node, kinds) {
17
+ const wanted = new Set(kinds);
18
+ const stack = [...treeSitterChildren(node)];
19
+ while (stack.length) {
20
+ const child = stack.shift();
21
+ if (!child || typeof child !== 'object') continue;
22
+ if (wanted.has(treeSitterNodeKind(child))) {
23
+ const text = shortNodeText(child);
24
+ if (text) return text;
25
+ }
26
+ stack.unshift(...treeSitterChildren(child));
27
+ }
28
+ return undefined;
29
+ }
30
+
31
+ export function treeSitterFieldText(node, field) {
32
+ const exact = treeSitterFieldNode(node, field);
33
+ if (exact) return shortNodeText(exact);
34
+ return treeSitterNamedFieldText(node, field);
35
+ }
36
+
37
+ function treeSitterFieldNode(node, field) {
38
+ if (typeof node?.childForFieldName === 'function') return node.childForFieldName(field);
39
+ if (typeof node?.child_by_field_name === 'function') return node.child_by_field_name(field);
40
+ return undefined;
41
+ }
42
+
43
+ function treeSitterNamedFieldText(node, field) {
44
+ for (const child of treeSitterChildren(node)) {
45
+ if (treeSitterFieldName(child) === field) {
46
+ const text = shortNodeText(child);
47
+ if (text) return text;
48
+ }
49
+ }
50
+ return undefined;
51
+ }
52
+
53
+ function treeSitterFieldName(node) {
54
+ if (typeof node?.fieldName === 'string') return node.fieldName;
55
+ if (typeof node?.field_name === 'string') return node.field_name;
56
+ if (typeof node?.fieldName === 'function') return node.fieldName();
57
+ if (typeof node?.field_name === 'function') return node.field_name();
58
+ return undefined;
59
+ }
60
+
61
+ function treeSitterArrayChildren(node, field) {
62
+ const value = node?.[field];
63
+ return Array.isArray(value) ? value.filter((child) => child && typeof child === 'object') : [];
64
+ }
65
+
66
+ function treeSitterIndexedChildren(node, countField, childMethod) {
67
+ const count = Number(node?.[countField]);
68
+ if (!Number.isFinite(count) || count <= 0 || typeof node?.[childMethod] !== 'function') return [];
69
+ const children = [];
70
+ for (let index = 0; index < count; index += 1) {
71
+ const child = node[childMethod](index);
72
+ if (child && typeof child === 'object') children.push(child);
73
+ }
74
+ return children;
75
+ }
@@ -1,5 +1,5 @@
1
1
  import{idFragment}from'../../native-import-utils.js';
2
- import{nativeNodeId}from'./nativeNodeId.js';import{numberOrUndefined}from'./numberOrUndefined.js';import{shortNodeText}from'./shortNodeText.js';import{spanFromTreeSitterNode}from'./spanFromTreeSitterNode.js';import{treeSitterDeclaration}from'./treeSitterDeclaration.js';
2
+ import{nativeNodeId}from'./nativeNodeId.js';import{numberOrUndefined}from'./numberOrUndefined.js';import{shortNodeText}from'./shortNodeText.js';import{spanFromTreeSitterNode}from'./spanFromTreeSitterNode.js';import{treeSitterDeclaration}from'./treeSitterDeclaration.js';import{treeSitterChildren,treeSitterNodeKind}from'./treeSitterNodeAccess.js';
3
3
  export function visitTreeSitterNode(node, context, propertyPath, depth = 0) {
4
4
  if (!node || typeof node !== 'object' || context.truncated) return undefined;
5
5
  if (context.objectIds.has(node)) return context.objectIds.get(node);
@@ -8,7 +8,7 @@ export function visitTreeSitterNode(node, context, propertyPath, depth = 0) {
8
8
  return undefined;
9
9
  }
10
10
  context.counter += 1;
11
- const kind = String(node.type ?? node.kind ?? 'node');
11
+ const kind = treeSitterNodeKind(node);
12
12
  const span = spanFromTreeSitterNode(node, context.input);
13
13
  const id = nativeNodeId(context, kind, { start: { line: span?.startLine, column: span?.startColumn } }, propertyPath);
14
14
  context.objectIds.set(node, id);
@@ -82,32 +82,6 @@ export function visitTreeSitterNode(node, context, propertyPath, depth = 0) {
82
82
  return id;
83
83
  }
84
84
 
85
- function treeSitterChildren(node) {
86
- const children = treeSitterArrayChildren(node, 'children');
87
- if (children.length) return children;
88
- const childCountChildren = treeSitterIndexedChildren(node, 'childCount', 'child');
89
- if (childCountChildren.length) return childCountChildren;
90
- const namedChildren = treeSitterArrayChildren(node, 'namedChildren');
91
- if (namedChildren.length) return namedChildren;
92
- return treeSitterIndexedChildren(node, 'namedChildCount', 'namedChild');
93
- }
94
-
95
- function treeSitterArrayChildren(node, field) {
96
- const value = node[field];
97
- return Array.isArray(value) ? value.filter((child) => child && typeof child === 'object') : [];
98
- }
99
-
100
- function treeSitterIndexedChildren(node, countField, childMethod) {
101
- const count = Number(node[countField]);
102
- if (!Number.isFinite(count) || count <= 0 || typeof node[childMethod] !== 'function') return [];
103
- const children = [];
104
- for (let index = 0; index < count; index += 1) {
105
- const child = node[childMethod](index);
106
- if (child && typeof child === 'object') children.push(child);
107
- }
108
- return children;
109
- }
110
-
111
85
  function treeSitterBoolean(node, ...fields) {
112
86
  for (const field of fields) {
113
87
  const value = node[field];
@@ -18,6 +18,7 @@ function semanticImportSidecarEntry(imported, index, options) {
18
18
  const proofSpec = summarizeProofSpecLayer(imported?.universalAst?.proof ?? imported?.proof);
19
19
  const paradigmSemantics = summarizeParadigmSemanticsLayer(imported?.universalAst?.paradigmSemantics ?? imported?.paradigmSemantics);
20
20
  const dependencies = summarizeSemanticImportDependencyRelations(semanticIndex?.relations ?? []);
21
+ const readiness = semanticImportEntryReadiness(imported);
21
22
  const mappingsBySymbolId = new Map();
22
23
  for (const mapping of sourceMapMappings) {
23
24
  if (mapping.semanticSymbolId && !mappingsBySymbolId.has(mapping.semanticSymbolId)) {
@@ -44,7 +45,7 @@ function semanticImportSidecarEntry(imported, index, options) {
44
45
  ownershipRegionId: region.id,
45
46
  ownershipKey: region.key,
46
47
  ownershipRegionKind: region.regionKind,
47
- readiness: imported?.metadata?.semanticMergeReadiness ?? imported?.mergeCandidates?.[0]?.readiness ?? 'needs-review'
48
+ readiness
48
49
  });
49
50
  }
50
51
  const ownershipRegions = uniqueRecordsById(regions);
@@ -71,7 +72,7 @@ function semanticImportSidecarEntry(imported, index, options) {
71
72
  paradigmSemantics,
72
73
  dependencyRelationCount: dependencies.total,
73
74
  dependencyPredicates: dependencies.predicates,
74
- readiness: imported?.metadata?.semanticMergeReadiness ?? imported?.mergeCandidates?.[0]?.readiness ?? 'needs-review',
75
+ readiness,
75
76
  emptySemanticIndex: symbols.length === 0,
76
77
  regionTaxonomy,
77
78
  symbols,
@@ -79,4 +80,14 @@ function semanticImportSidecarEntry(imported, index, options) {
79
80
  };
80
81
  }
81
82
 
83
+ function semanticImportEntryReadiness(imported) {
84
+ const readiness = imported?.metadata?.semanticMergeReadiness
85
+ ?? imported?.metadata?.nativeImportLossSummary?.semanticMergeReadiness
86
+ ?? imported?.readiness?.semanticMergeReadiness
87
+ ?? imported?.readiness?.readiness
88
+ ?? (typeof imported?.readiness === 'string' ? imported.readiness : undefined)
89
+ ?? imported?.mergeCandidates?.[0]?.readiness;
90
+ return readiness ?? 'needs-review';
91
+ }
92
+
82
93
  export { semanticImportSidecarEntry };
@@ -0,0 +1,24 @@
1
+ export function conversionBounds(sourceLanguage, targetLanguage) {
2
+ return {
3
+ sourceLanguage,
4
+ targetLanguage,
5
+ supportedNow: [
6
+ 'Top-level imports, functions, classes, types, constants, and simple ownership regions.',
7
+ 'Frontier universal AST envelopes, semantic indexes, source maps, losses, and sidecar patch hints.',
8
+ 'Deterministic scaffold projection through explicit demo target adapters.',
9
+ 'Manual run submission so every conversion has a source hash and evidence record.'
10
+ ],
11
+ reviewRequired: [
12
+ 'Function bodies are preserved as source evidence and emitted as TODO scaffolds.',
13
+ 'Types are declaration evidence, not a full Rust trait/lifetime/ownership proof.',
14
+ 'Runtime APIs such as fetch, timers, storage, DOM, Node, and npm crates need adapters.',
15
+ 'Round trips are useful for review, but not automatic semantic equivalence proofs.'
16
+ ],
17
+ unsupportedToday: [
18
+ 'Behavior-preserving transpilation of arbitrary TypeScript or Rust.',
19
+ 'Full generic constraints, overload resolution, conditional types, macros, and borrow checking.',
20
+ 'Async control-flow lowering, exception/error model conversion, and memory ownership synthesis.',
21
+ 'Perfect comments/trivia formatting in projected target code without richer parser evidence.'
22
+ ]
23
+ };
24
+ }
@@ -0,0 +1,243 @@
1
+ export function workbenchClientScript() {
2
+ return `
3
+ const initial = JSON.parse(document.getElementById('initial-state').textContent);
4
+ const state = {
5
+ sources: { ...initial.sources },
6
+ sourceLanguage: initial.sourceLanguage || 'typescript',
7
+ result: initial.result,
8
+ activeTab: 'graph',
9
+ pending: false
10
+ };
11
+
12
+ window.frontierLangWorkbench = {
13
+ kind: 'frontier.framework.demo.langWorkbench',
14
+ version: 1,
15
+ state,
16
+ feature: {
17
+ id: 'frontier-lang-ts-rust-workbench',
18
+ package: '@shapeshift-labs/frontier-lang-compiler',
19
+ route: '/examples/js-frontier-rust-workbench',
20
+ panels: ['source', 'frontier', 'rust']
21
+ },
22
+ evidence: []
23
+ };
24
+
25
+ const typescriptInput = document.getElementById('typescriptInput');
26
+ const rustInput = document.getElementById('rustInput');
27
+ const frontierJson = document.getElementById('frontierJson');
28
+ const graphView = document.getElementById('graphView');
29
+ const readinessValue = document.getElementById('readinessValue');
30
+ const symbolValue = document.getElementById('symbolValue');
31
+ const mappingValue = document.getElementById('mappingValue');
32
+ const modeValue = document.getElementById('modeValue');
33
+ const typescriptStatus = document.getElementById('typescriptStatus');
34
+ const rustStatus = document.getElementById('rustStatus');
35
+
36
+ let updatingEditors = false;
37
+ typescriptInput.value = state.sources.typescript;
38
+ rustInput.value = state.sources.rust;
39
+ render();
40
+
41
+ typescriptInput.addEventListener('input', () => {
42
+ if (updatingEditors) return;
43
+ state.sources.typescript = typescriptInput.value;
44
+ state.sourceLanguage = 'typescript';
45
+ renderEditorModes();
46
+ });
47
+
48
+ rustInput.addEventListener('input', () => {
49
+ if (updatingEditors) return;
50
+ state.sources.rust = rustInput.value;
51
+ state.sourceLanguage = 'rust';
52
+ renderEditorModes();
53
+ });
54
+
55
+ document.getElementById('runButton').addEventListener('click', async () => {
56
+ await convertFrom(state.sourceLanguage);
57
+ });
58
+
59
+ document.getElementById('resetButton').addEventListener('click', async () => {
60
+ state.sources = { ...initial.sources };
61
+ state.sourceLanguage = 'typescript';
62
+ state.result = initial.result;
63
+ render();
64
+ });
65
+
66
+ document.getElementById('copyButton').addEventListener('click', async () => {
67
+ await navigator.clipboard.writeText(projectedSourceText());
68
+ });
69
+
70
+ for (const button of document.querySelectorAll('[data-source-mode]')) {
71
+ button.addEventListener('click', () => {
72
+ state.sourceLanguage = button.dataset.sourceMode;
73
+ renderEditorModes();
74
+ });
75
+ }
76
+
77
+ for (const button of document.querySelectorAll('[data-frontier-tab]')) {
78
+ button.addEventListener('click', () => {
79
+ state.activeTab = button.dataset.frontierTab;
80
+ for (const peer of document.querySelectorAll('[data-frontier-tab]')) {
81
+ peer.setAttribute('aria-pressed', String(peer === button));
82
+ }
83
+ render();
84
+ });
85
+ }
86
+
87
+ async function convertFrom(sourceLanguage) {
88
+ state.sourceLanguage = sourceLanguage;
89
+ state.sources[sourceLanguage] = sourceLanguage === 'rust' ? rustInput.value : typescriptInput.value;
90
+ setPending(true);
91
+ try {
92
+ const response = await fetch('/api/convert', {
93
+ method: 'POST',
94
+ headers: { 'content-type': 'application/json' },
95
+ body: JSON.stringify({ source: state.sources[sourceLanguage], sourceLanguage })
96
+ });
97
+ const payload = await response.json();
98
+ if (!response.ok || !payload.ok) throw new Error(payload.error || 'conversion failed');
99
+ state.result = payload.result;
100
+ state.sources[payload.result.projection.targetLanguage] = payload.result.projection.output;
101
+ window.frontierLangWorkbench.evidence.push({
102
+ at: Date.now(),
103
+ kind: 'conversion',
104
+ sourceHash: payload.result.sourceHash,
105
+ symbolCount: payload.result.summary.symbols,
106
+ readiness: payload.result.summary.readiness
107
+ });
108
+ } catch (error) {
109
+ state.result = errorResult(error);
110
+ } finally {
111
+ setPending(false);
112
+ render();
113
+ }
114
+ }
115
+
116
+ function render() {
117
+ const result = state.result || {};
118
+ const summary = result.summary || {};
119
+ readinessValue.textContent = summary.readiness || 'error';
120
+ readinessValue.style.color = readinessColor(summary.readiness);
121
+ symbolValue.textContent = String(summary.symbols ?? 0);
122
+ mappingValue.textContent = String(summary.sourceMapMappings ?? 0);
123
+ modeValue.textContent = result.projection?.mode || '-';
124
+ typescriptStatus.textContent = languageStatus('typescript', result);
125
+ rustStatus.textContent = languageStatus('rust', result);
126
+ renderEditorModes();
127
+ frontierJson.textContent = JSON.stringify(result.frontier, null, 2);
128
+ frontierJson.hidden = state.activeTab !== 'json';
129
+ graphView.hidden = state.activeTab !== 'graph';
130
+ if (!graphView.hidden) graphView.innerHTML = graphHtml(result);
131
+ }
132
+
133
+ function renderEditorModes() {
134
+ updatingEditors = true;
135
+ typescriptInput.value = state.sources.typescript || '';
136
+ rustInput.value = state.sources.rust || '';
137
+ updatingEditors = false;
138
+ document.querySelector('[data-view-panel="source"]').classList.toggle('isSource', state.sourceLanguage === 'typescript');
139
+ document.querySelector('[data-view-panel="rust"]').classList.toggle('isSource', state.sourceLanguage === 'rust');
140
+ for (const button of document.querySelectorAll('[data-source-mode]')) {
141
+ button.setAttribute('aria-pressed', String(button.dataset.sourceMode === state.sourceLanguage));
142
+ }
143
+ }
144
+
145
+ function languageStatus(language, result) {
146
+ if (state.sourceLanguage === language) return result.sourceHash || 'source';
147
+ return (result.projection?.targetLanguage === language ? result.projection.readiness : 'projection') || 'projection';
148
+ }
149
+
150
+ function graphHtml(result) {
151
+ const summary = result.summary || {};
152
+ const frontier = result.frontier || {};
153
+ const symbols = frontier.symbols || [];
154
+ const relations = frontier.relations || [];
155
+ return [
156
+ '<div class="chipRow">',
157
+ chip(summary.readiness, readinessClass(summary.readiness)),
158
+ chip(String(summary.losses || 0) + ' losses', 'review'),
159
+ chip(String(summary.patchHints || 0) + ' patch hints', 'ready'),
160
+ chip((result.sourceLanguage || '-') + ' -> ' + (result.projection?.targetLanguage || '-'), 'review'),
161
+ '</div>',
162
+ '<div class="sectionTitle">Symbols</div>',
163
+ '<div class="graphGrid">',
164
+ ...symbols.map((symbol) => nodeCard(symbol)),
165
+ '</div>',
166
+ '<div class="sectionTitle">Relations</div>',
167
+ '<ul class="edgeList">',
168
+ ...relations.slice(0, 18).map((edge) => '<li>' + escapeHtml(edge.label) + '</li>'),
169
+ relations.length > 18 ? '<li>' + (relations.length - 18) + ' more</li>' : '',
170
+ '</ul>',
171
+ boundsHtml(result.bounds || {})
172
+ ].join('');
173
+ }
174
+
175
+ function nodeCard(symbol) {
176
+ return '<article class="nodeCard" data-graph-node="' + escapeHtml(symbol.id || symbol.name) + '">' +
177
+ '<strong>' + escapeHtml(symbol.name) + '</strong>' +
178
+ '<span>' + escapeHtml(symbol.kind || 'symbol') + ' · ' + escapeHtml(symbol.region || 'region') + '</span>' +
179
+ '</article>';
180
+ }
181
+
182
+ function chip(text, className) {
183
+ return '<span class="chip ' + className + '">' + escapeHtml(text || '-') + '</span>';
184
+ }
185
+
186
+ function readinessClass(readiness) {
187
+ if (readiness === 'ready' || readiness === 'ready-with-losses') return 'ready';
188
+ if (readiness === 'blocked') return 'blocked';
189
+ return 'review';
190
+ }
191
+
192
+ function readinessColor(readiness) {
193
+ if (readiness === 'ready' || readiness === 'ready-with-losses') return 'var(--green)';
194
+ if (readiness === 'blocked') return 'var(--red)';
195
+ return 'var(--amber)';
196
+ }
197
+
198
+ function errorResult(error) {
199
+ return {
200
+ summary: { readiness: 'blocked', symbols: 0, sourceMapMappings: 0, losses: 1, patchHints: 0 },
201
+ frontier: { symbols: [], relations: [], error: String(error.message || error) },
202
+ projection: { output: String(error.stack || error), readiness: 'blocked', mode: 'error' }
203
+ };
204
+ }
205
+
206
+ function projectedSourceText() {
207
+ const target = state.result?.projection?.targetLanguage;
208
+ return target ? state.sources[target] || '' : '';
209
+ }
210
+
211
+ function boundsHtml(bounds) {
212
+ return [
213
+ '<div class="sectionTitle">Bounds</div>',
214
+ '<div class="boundsGrid">',
215
+ boundCard('Supported now', bounds.supportedNow || []),
216
+ boundCard('Review required', bounds.reviewRequired || []),
217
+ boundCard('Unsupported today', bounds.unsupportedToday || []),
218
+ '</div>'
219
+ ].join('');
220
+ }
221
+
222
+ function boundCard(title, rows) {
223
+ return '<section class="boundCard"><strong>' + escapeHtml(title) + '</strong><ul>' +
224
+ rows.map((row) => '<li>' + escapeHtml(row) + '</li>').join('') +
225
+ '</ul></section>';
226
+ }
227
+
228
+ function setPending(pending) {
229
+ state.pending = pending;
230
+ document.body.toggleAttribute('data-pending', pending);
231
+ }
232
+
233
+ function escapeHtml(value) {
234
+ return String(value ?? '').replace(/[&<>"']/g, (char) => ({
235
+ '&': '&amp;',
236
+ '<': '&lt;',
237
+ '>': '&gt;',
238
+ '"': '&quot;',
239
+ "'": '&#39;'
240
+ })[char]);
241
+ }
242
+ `;
243
+ }
@@ -0,0 +1,76 @@
1
+ export function renderWorkbenchHtml(initialState) {
2
+ return `<!doctype html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>Frontier Lang TypeScript to Rust Workbench</title>
8
+ <link rel="stylesheet" href="/styles.css">
9
+ </head>
10
+ <body>
11
+ <main class="appShell" data-frontier-app="lang-workbench">
12
+ <header class="topbar">
13
+ <div class="brand">
14
+ <span class="brandMark">F</span>
15
+ <div>
16
+ <h1>Frontier Lang Workbench</h1>
17
+ <p>TypeScript -> Frontier graph -> Rust</p>
18
+ </div>
19
+ </div>
20
+ <div class="topActions">
21
+ <div class="modeGroup" role="group" aria-label="Source language">
22
+ <button data-source-mode="typescript" aria-pressed="true">TS source</button>
23
+ <button data-source-mode="rust" aria-pressed="false">Rust source</button>
24
+ </div>
25
+ <button class="runButton" id="runButton">Run</button>
26
+ <button class="iconButton" id="resetButton" title="Reset source" aria-label="Reset source">↺</button>
27
+ <button class="iconButton" id="copyButton" title="Copy projection" aria-label="Copy projection">⧉</button>
28
+ </div>
29
+ </header>
30
+
31
+ <section class="statusStrip" data-view-panel="status">
32
+ <div><span class="label">Readiness</span><strong id="readinessValue">-</strong></div>
33
+ <div><span class="label">Symbols</span><strong id="symbolValue">-</strong></div>
34
+ <div><span class="label">Mappings</span><strong id="mappingValue">-</strong></div>
35
+ <div><span class="label">Mode</span><strong id="modeValue">-</strong></div>
36
+ </section>
37
+
38
+ <section class="workspace">
39
+ <section class="pane sourcePane" data-view-panel="source">
40
+ <div class="paneHeader">
41
+ <h2>TypeScript</h2>
42
+ <span id="typescriptStatus">source</span>
43
+ </div>
44
+ <textarea id="typescriptInput" spellcheck="false" aria-label="TypeScript source"></textarea>
45
+ </section>
46
+
47
+ <section class="pane graphPane" data-view-panel="frontier">
48
+ <div class="paneHeader">
49
+ <h2>Frontier</h2>
50
+ <div class="segmented" role="tablist" aria-label="Frontier view">
51
+ <button data-frontier-tab="graph" aria-pressed="true">Graph</button>
52
+ <button data-frontier-tab="json" aria-pressed="false">JSON</button>
53
+ </div>
54
+ </div>
55
+ <div id="graphView" class="graphView"></div>
56
+ <pre id="frontierJson" class="codeBlock" hidden></pre>
57
+ </section>
58
+
59
+ <section class="pane outputPane" data-view-panel="rust">
60
+ <div class="paneHeader">
61
+ <h2>Rust</h2>
62
+ <span id="rustStatus">target</span>
63
+ </div>
64
+ <textarea id="rustInput" spellcheck="false" aria-label="Rust source"></textarea>
65
+ </section>
66
+ </section>
67
+ </main>
68
+ <script type="application/json" id="initial-state">${escapeScriptJson(initialState)}</script>
69
+ <script src="/client.js" type="module"></script>
70
+ </body>
71
+ </html>`;
72
+ }
73
+
74
+ function escapeScriptJson(value) {
75
+ return JSON.stringify(value).replace(/</g, '\\u003c');
76
+ }
@@ -0,0 +1,243 @@
1
+ export function workbenchStyles() {
2
+ return `
3
+ :root {
4
+ color-scheme: dark;
5
+ --bg: #08090a;
6
+ --panel: #111417;
7
+ --panel-2: #171b20;
8
+ --panel-3: #20262c;
9
+ --line: #303941;
10
+ --text: #f4f7f2;
11
+ --muted: #aab3aa;
12
+ --faint: #758075;
13
+ --green: #61d394;
14
+ --amber: #e7b75f;
15
+ --red: #e46c69;
16
+ --blue: #6db4e8;
17
+ --violet: #a78bfa;
18
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
19
+ --ui: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
20
+ }
21
+
22
+ * { box-sizing: border-box; }
23
+ html, body { height: 100%; margin: 0; }
24
+ body {
25
+ background: var(--bg);
26
+ color: var(--text);
27
+ font-family: var(--ui);
28
+ overflow: hidden;
29
+ }
30
+
31
+ .appShell {
32
+ min-height: 100vh;
33
+ display: grid;
34
+ grid-template-rows: 58px 46px minmax(0, 1fr);
35
+ }
36
+
37
+ .topbar, .statusStrip, .paneHeader {
38
+ display: flex;
39
+ align-items: center;
40
+ border-bottom: 1px solid var(--line);
41
+ }
42
+
43
+ .topbar {
44
+ justify-content: space-between;
45
+ padding: 0 14px;
46
+ background: #0c0e10;
47
+ }
48
+
49
+ .brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
50
+ .brandMark {
51
+ width: 34px;
52
+ height: 34px;
53
+ display: grid;
54
+ place-items: center;
55
+ border: 1px solid var(--line);
56
+ border-radius: 8px;
57
+ background: #151b17;
58
+ color: var(--green);
59
+ font-weight: 800;
60
+ }
61
+ .brand h1 { margin: 0; font-size: 15px; line-height: 1.1; letter-spacing: 0; }
62
+ .brand p { margin: 4px 0 0; color: var(--muted); font-size: 12px; }
63
+
64
+ .topActions { display: flex; align-items: center; gap: 8px; }
65
+ .iconButton, .segmented button, .modeGroup button, .runButton {
66
+ height: 32px;
67
+ border: 1px solid var(--line);
68
+ background: var(--panel-2);
69
+ color: var(--text);
70
+ border-radius: 8px;
71
+ }
72
+ .iconButton { width: 34px; font-size: 16px; cursor: pointer; }
73
+ .runButton { min-width: 64px; padding: 0 14px; cursor: pointer; color: var(--green); }
74
+ .modeGroup { display: flex; }
75
+ .modeGroup button {
76
+ width: 84px;
77
+ border-radius: 0;
78
+ font-size: 12px;
79
+ cursor: pointer;
80
+ }
81
+ .modeGroup button:first-child { border-radius: 7px 0 0 7px; }
82
+ .modeGroup button:last-child { border-radius: 0 7px 7px 0; border-left: 0; }
83
+ .modeGroup button[aria-pressed="true"] { background: #213127; color: var(--green); }
84
+ .iconButton:hover, .segmented button:hover, .modeGroup button:hover, .runButton:hover { background: var(--panel-3); }
85
+
86
+ .statusStrip {
87
+ display: grid;
88
+ grid-template-columns: repeat(4, minmax(0, 1fr));
89
+ background: #0f1214;
90
+ }
91
+ .statusStrip div {
92
+ min-width: 0;
93
+ padding: 8px 14px;
94
+ border-right: 1px solid var(--line);
95
+ }
96
+ .label {
97
+ display: block;
98
+ color: var(--faint);
99
+ font-size: 10px;
100
+ text-transform: uppercase;
101
+ }
102
+ .statusStrip strong { font-size: 13px; font-weight: 650; }
103
+
104
+ .workspace {
105
+ min-height: 0;
106
+ display: grid;
107
+ grid-template-columns: minmax(270px, 0.95fr) minmax(330px, 1.1fr) minmax(300px, 1fr);
108
+ }
109
+ .pane {
110
+ min-width: 0;
111
+ min-height: 0;
112
+ display: grid;
113
+ grid-template-rows: 44px minmax(0, 1fr);
114
+ border-right: 1px solid var(--line);
115
+ background: var(--panel);
116
+ }
117
+ .pane:last-child { border-right: 0; }
118
+ .pane.isSource .paneHeader { box-shadow: inset 0 -2px 0 var(--green); }
119
+ .paneHeader {
120
+ justify-content: space-between;
121
+ padding: 0 12px;
122
+ background: var(--panel-2);
123
+ }
124
+ .paneHeader h2 { margin: 0; font-size: 13px; letter-spacing: 0; }
125
+ .paneHeader span { color: var(--muted); font-size: 12px; font-family: var(--mono); }
126
+
127
+ textarea, .codeBlock {
128
+ width: 100%;
129
+ height: 100%;
130
+ min-height: 0;
131
+ margin: 0;
132
+ border: 0;
133
+ resize: none;
134
+ outline: none;
135
+ background: #0d1012;
136
+ color: #edf2ea;
137
+ font-family: var(--mono);
138
+ font-size: 13px;
139
+ line-height: 1.48;
140
+ padding: 14px;
141
+ overflow: auto;
142
+ white-space: pre;
143
+ }
144
+
145
+ .graphView {
146
+ min-height: 0;
147
+ overflow: auto;
148
+ padding: 12px;
149
+ }
150
+ .graphGrid {
151
+ display: grid;
152
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
153
+ gap: 8px;
154
+ }
155
+ .nodeCard {
156
+ border: 1px solid var(--line);
157
+ border-radius: 8px;
158
+ background: #14191d;
159
+ padding: 10px;
160
+ }
161
+ .nodeCard strong {
162
+ display: block;
163
+ overflow-wrap: anywhere;
164
+ font-size: 13px;
165
+ }
166
+ .nodeCard span { color: var(--muted); font-size: 12px; }
167
+ .chipRow { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
168
+ .chip {
169
+ border: 1px solid var(--line);
170
+ border-radius: 999px;
171
+ padding: 4px 8px;
172
+ color: var(--muted);
173
+ font-size: 11px;
174
+ }
175
+ .chip.ready { color: var(--green); border-color: rgba(97, 211, 148, .45); }
176
+ .chip.review { color: var(--amber); border-color: rgba(231, 183, 95, .45); }
177
+ .chip.blocked { color: var(--red); border-color: rgba(228, 108, 105, .45); }
178
+ .sectionTitle {
179
+ margin: 12px 0 8px;
180
+ color: var(--muted);
181
+ font-size: 11px;
182
+ text-transform: uppercase;
183
+ }
184
+ .edgeList {
185
+ display: grid;
186
+ gap: 6px;
187
+ margin: 0;
188
+ padding: 0;
189
+ list-style: none;
190
+ }
191
+ .edgeList li {
192
+ border-left: 2px solid var(--blue);
193
+ background: rgba(109, 180, 232, .08);
194
+ padding: 6px 8px;
195
+ color: var(--muted);
196
+ font-size: 12px;
197
+ overflow-wrap: anywhere;
198
+ }
199
+ .boundsGrid {
200
+ display: grid;
201
+ grid-template-columns: repeat(3, minmax(0, 1fr));
202
+ gap: 8px;
203
+ }
204
+ .boundCard {
205
+ border: 1px solid var(--line);
206
+ border-radius: 8px;
207
+ background: #12171a;
208
+ padding: 10px;
209
+ }
210
+ .boundCard strong {
211
+ display: block;
212
+ margin-bottom: 8px;
213
+ font-size: 12px;
214
+ }
215
+ .boundCard ul {
216
+ margin: 0;
217
+ padding-left: 16px;
218
+ color: var(--muted);
219
+ font-size: 11px;
220
+ line-height: 1.45;
221
+ }
222
+ .segmented { display: flex; gap: 0; }
223
+ .segmented button {
224
+ width: 58px;
225
+ border-radius: 0;
226
+ font-size: 12px;
227
+ }
228
+ .segmented button:first-child { border-radius: 7px 0 0 7px; }
229
+ .segmented button:last-child { border-radius: 0 7px 7px 0; border-left: 0; }
230
+ .segmented button[aria-pressed="true"] { background: #213127; color: var(--green); }
231
+
232
+ @media (max-width: 980px) {
233
+ body { overflow: auto; }
234
+ .appShell { min-height: 100vh; grid-template-rows: auto auto auto; }
235
+ .workspace { grid-template-columns: 1fr; }
236
+ .pane { min-height: 360px; border-right: 0; border-bottom: 1px solid var(--line); }
237
+ .statusStrip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
238
+ .topbar { align-items: flex-start; gap: 10px; padding: 10px 14px; }
239
+ .topActions { flex-wrap: wrap; justify-content: flex-end; }
240
+ .boundsGrid { grid-template-columns: 1fr; }
241
+ }
242
+ `;
243
+ }
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+ import http from 'node:http';
3
+ import { compileNativeSource, createSemanticImportSidecar, importNativeSource, writeUniversalAstJson } from '../dist/index.js';
4
+ import { conversionBounds } from './js-frontier-rust-workbench-bounds.mjs';
5
+ import { renderWorkbenchHtml } from './js-frontier-rust-workbench-html.mjs';
6
+ import { workbenchClientScript } from './js-frontier-rust-workbench-client.mjs';
7
+ import { workbenchStyles } from './js-frontier-rust-workbench-styles.mjs';
8
+
9
+ const args = readArgs(process.argv.slice(2));
10
+ const port = Number(args.port ?? 4177);
11
+ const sampleSource = `import { nanoid } from "nanoid";
12
+
13
+ export type TodoInput = {
14
+ title: string;
15
+ };
16
+
17
+ export function addTodo(input: TodoInput) {
18
+ return { id: nanoid(), title: input.title };
19
+ }
20
+
21
+ export class TodoStore {
22
+ save(title: string) {
23
+ return addTodo({ title });
24
+ }
25
+ }
26
+ `;
27
+ const sampleRustSource = `// Generated from Frontier semantic graph evidence.
28
+ pub fn add_todo() {
29
+ // port body from preserved TypeScript source
30
+ }
31
+
32
+ pub struct TodoStore;
33
+ `;
34
+
35
+ if (args.smoke) {
36
+ const result = convertSource(sampleSource, { sourceLanguage: 'typescript' });
37
+ assert(result.summary.symbols >= 2, 'expected semantic symbols');
38
+ assert(result.projection.output.includes('pub'), 'expected Rust projection');
39
+ const reverse = convertSource(result.projection.output, { sourceLanguage: 'rust' });
40
+ assert(reverse.projection.output.includes('export'), 'expected TypeScript projection');
41
+ console.log(JSON.stringify({ ok: true, summary: result.summary }, null, 2));
42
+ } else {
43
+ const server = http.createServer(routeRequest);
44
+ server.listen(port, '127.0.0.1', () => {
45
+ console.log(`Frontier Lang workbench: http://127.0.0.1:${port}/`);
46
+ });
47
+ }
48
+
49
+ async function routeRequest(request, response) {
50
+ try {
51
+ if (request.method === 'GET' && request.url === '/') {
52
+ return send(response, 200, renderWorkbenchHtml({
53
+ sourceLanguage: 'typescript',
54
+ sources: { typescript: sampleSource, rust: sampleRustSource },
55
+ result: convertSource(sampleSource, { sourceLanguage: 'typescript' })
56
+ }), 'text/html; charset=utf-8');
57
+ }
58
+ if (request.method === 'GET' && request.url === '/styles.css') {
59
+ return send(response, 200, workbenchStyles(), 'text/css; charset=utf-8');
60
+ }
61
+ if (request.method === 'GET' && request.url === '/client.js') {
62
+ return send(response, 200, workbenchClientScript(), 'text/javascript; charset=utf-8');
63
+ }
64
+ if (request.method === 'POST' && request.url === '/api/convert') {
65
+ const body = await readJsonBody(request);
66
+ return sendJson(response, 200, {
67
+ ok: true,
68
+ result: convertSource(String(body.source ?? ''), {
69
+ sourceLanguage: body.sourceLanguage ?? 'typescript'
70
+ })
71
+ });
72
+ }
73
+ return send(response, 404, 'not found', 'text/plain; charset=utf-8');
74
+ } catch (error) {
75
+ return sendJson(response, 500, { ok: false, error: String(error.message || error) });
76
+ }
77
+ }
78
+
79
+ function convertSource(source, options = {}) {
80
+ const sourceLanguage = normalizeSourceLanguage(options.sourceLanguage);
81
+ const target = sourceLanguage === 'rust' ? 'typescript' : 'rust';
82
+ const imported = importNativeSource({
83
+ language: sourceLanguage,
84
+ sourcePath: `workbench/input.${sourceExtension(sourceLanguage)}`,
85
+ sourceText: source
86
+ });
87
+ const projection = compileNativeSource(imported, {
88
+ target,
89
+ targetPath: `workbench/output.${targetExtension(target)}`,
90
+ targetAdapters: [createTsToRustWorkbenchAdapter(), createRustToTsWorkbenchAdapter()],
91
+ emitOnBlocked: true
92
+ });
93
+ const sidecar = createSemanticImportSidecar(imported, { generatedAt: 0, targetPath: projection.sourceMap?.targetPath });
94
+ return {
95
+ sourceHash: imported.nativeSource.sourceHash,
96
+ sourceLanguage,
97
+ summary: {
98
+ readiness: sidecar.summary.readiness,
99
+ symbols: imported.semanticIndex.symbols.length,
100
+ sourceMapMappings: imported.sourceMaps[0]?.mappings.length ?? 0,
101
+ losses: imported.losses.length,
102
+ patchHints: sidecar.patchHints.length
103
+ },
104
+ frontier: {
105
+ universalAst: universalAstSummary(imported),
106
+ semanticIndexId: imported.semanticIndex.id,
107
+ sidecarId: sidecar.id,
108
+ symbols: imported.semanticIndex.symbols.map(symbolSummary),
109
+ relations: relationSummaries(imported),
110
+ losses: imported.losses.map(lossSummary),
111
+ patchHints: sidecar.patchHints.map((hint) => ({
112
+ id: hint.id,
113
+ readiness: hint.readiness,
114
+ operations: hint.supportedOperations,
115
+ ownershipKey: hint.ownershipKey
116
+ }))
117
+ },
118
+ bounds: conversionBounds(sourceLanguage, target),
119
+ projection: {
120
+ target,
121
+ targetLanguage: target,
122
+ sourceLanguage,
123
+ mode: projection.outputMode,
124
+ readiness: projection.readiness.readiness,
125
+ ok: projection.ok,
126
+ output: projection.output,
127
+ sourceMapMappings: projection.sourceMap?.mappings.length ?? 0
128
+ }
129
+ };
130
+ }
131
+
132
+ function createTsToRustWorkbenchAdapter() {
133
+ return {
134
+ id: 'frontier-lang-workbench-ts-to-rust',
135
+ sourceLanguage: 'typescript',
136
+ target: 'rust',
137
+ version: '0.0.0-demo',
138
+ capabilities: ['semantic-symbol-scaffold'],
139
+ coverage: {
140
+ readiness: 'ready',
141
+ handledLossKinds: ['opaqueNative', 'declarationOnlyCoverage', 'partialSemanticIndex', 'sourceMapApproximation', 'sourcePreservation'],
142
+ notes: ['Workbench adapter lowers TypeScript semantic symbols to Rust scaffolding and leaves bodies explicit.']
143
+ },
144
+ project(input) {
145
+ return {
146
+ output: rustFromSymbols(input.importResult.semanticIndex?.symbols ?? []),
147
+ readiness: 'ready',
148
+ evidence: [{
149
+ id: 'evidence_workbench_js_to_rust',
150
+ kind: 'projection',
151
+ status: 'passed',
152
+ summary: 'Workbench adapter projected TypeScript semantic symbols to Rust scaffolding.'
153
+ }]
154
+ };
155
+ }
156
+ };
157
+ }
158
+
159
+ function createRustToTsWorkbenchAdapter() {
160
+ return {
161
+ id: 'frontier-lang-workbench-rust-to-ts',
162
+ sourceLanguage: 'rust',
163
+ target: 'typescript',
164
+ version: '0.0.0-demo',
165
+ capabilities: ['semantic-symbol-scaffold'],
166
+ coverage: {
167
+ readiness: 'ready',
168
+ handledLossKinds: ['opaqueNative', 'macroExpansion', 'declarationOnlyCoverage', 'partialSemanticIndex', 'sourceMapApproximation', 'sourcePreservation'],
169
+ notes: ['Workbench adapter lowers Rust semantic symbols to TypeScript scaffolding and leaves bodies explicit.']
170
+ },
171
+ project(input) {
172
+ return {
173
+ output: tsFromSymbols(input.importResult.semanticIndex?.symbols ?? []),
174
+ readiness: 'ready',
175
+ evidence: [{
176
+ id: 'evidence_workbench_rust_to_ts',
177
+ kind: 'projection',
178
+ status: 'passed',
179
+ summary: 'Workbench adapter projected Rust semantic symbols to TypeScript scaffolding.'
180
+ }]
181
+ };
182
+ }
183
+ };
184
+ }
185
+
186
+ function rustFromSymbols(symbols) {
187
+ const lines = ['// Generated from Frontier semantic graph evidence.'];
188
+ for (const symbol of symbols) {
189
+ if (symbol.kind === 'class') lines.push('', `pub struct ${safeTypeName(symbol.name)};`);
190
+ if (symbol.kind === 'function' || symbol.kind === 'method') {
191
+ lines.push('', `pub fn ${safeRustName(symbol.name)}() {`, ' // port body from preserved TypeScript source', '}');
192
+ }
193
+ }
194
+ return `${lines.join('\n').trim()}\n`;
195
+ }
196
+
197
+ function tsFromSymbols(symbols) {
198
+ const lines = ['// Generated from Frontier semantic graph evidence.'];
199
+ for (const symbol of symbols) {
200
+ if (symbol.kind === 'type' || symbol.kind === 'class') lines.push('', `export class ${safeTypeName(symbol.name)} {}`);
201
+ if (symbol.kind === 'function' || symbol.kind === 'method') {
202
+ lines.push('', `export function ${safeJsName(symbol.name)}(...args: unknown[]) {`, " throw new Error('port body from preserved Rust source');", '}');
203
+ }
204
+ }
205
+ return `${lines.join('\n').trim()}\n`;
206
+ }
207
+
208
+ function universalAstSummary(imported) {
209
+ const summary = { valid: true, validationError: undefined };
210
+ let parsed = imported.universalAst;
211
+ try {
212
+ parsed = JSON.parse(writeUniversalAstJson(imported.universalAst));
213
+ } catch (error) {
214
+ summary.valid = false;
215
+ summary.validationError = String(error.message || error);
216
+ }
217
+ return {
218
+ ...summary,
219
+ kind: parsed.kind,
220
+ id: parsed.id,
221
+ nativeSources: parsed.nativeSources?.length ?? 0,
222
+ semanticSymbols: parsed.semanticIndex?.symbols?.length ?? 0,
223
+ sourceMaps: parsed.sourceMaps?.length ?? 0,
224
+ layers: Object.keys(parsed.layers ?? {})
225
+ };
226
+ }
227
+
228
+ function symbolSummary(symbol) {
229
+ return {
230
+ id: symbol.id,
231
+ name: symbol.name,
232
+ kind: symbol.kind,
233
+ language: symbol.language,
234
+ region: symbol.metadata?.ownershipRegionKind,
235
+ ownershipKey: symbol.metadata?.ownershipRegionKey
236
+ };
237
+ }
238
+
239
+ function relationSummaries(imported) {
240
+ const names = new Map(imported.semanticIndex.symbols.map((symbol) => [symbol.id, symbol.name]));
241
+ return imported.semanticIndex.relations.map((relation) => ({
242
+ id: relation.id,
243
+ predicate: relation.predicate,
244
+ label: `${names.get(relation.sourceId) ?? relation.sourceId} ${relation.predicate} ${names.get(relation.targetId) ?? relation.targetId}`
245
+ }));
246
+ }
247
+
248
+ function lossSummary(loss) {
249
+ return { id: loss.id, kind: loss.kind, severity: loss.severity, message: loss.message };
250
+ }
251
+
252
+ function normalizeSourceLanguage(language) {
253
+ return String(language || 'typescript').toLowerCase() === 'rust' ? 'rust' : 'typescript';
254
+ }
255
+
256
+ function targetExtension(target) {
257
+ return target === 'rust' ? 'rs' : 'ts';
258
+ }
259
+
260
+ function sourceExtension(language) {
261
+ return language === 'rust' ? 'rs' : 'ts';
262
+ }
263
+
264
+ function safeTypeName(name) {
265
+ const clean = String(name).replace(/[^A-Za-z0-9_]/g, '');
266
+ return clean && /^[A-Z]/.test(clean) ? clean : `Frontier${clean || 'Type'}`;
267
+ }
268
+
269
+ function safeRustName(name) {
270
+ return String(name).replace(/\./g, '_').replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/[^A-Za-z0-9_]/g, '_').toLowerCase();
271
+ }
272
+
273
+ function safeJsName(name) {
274
+ const clean = String(name).replace(/[^A-Za-z0-9_$]/g, '_');
275
+ return /^[A-Za-z_$]/.test(clean) ? clean : `frontier_${clean}`;
276
+ }
277
+
278
+ function sendJson(response, status, value) {
279
+ return send(response, status, JSON.stringify(value), 'application/json; charset=utf-8');
280
+ }
281
+
282
+ function send(response, status, body, type) {
283
+ response.writeHead(status, { 'content-type': type, 'cache-control': 'no-store' });
284
+ response.end(body);
285
+ }
286
+
287
+ function readJsonBody(request) {
288
+ return new Promise((resolve, reject) => {
289
+ let text = '';
290
+ request.setEncoding('utf8');
291
+ request.on('data', (chunk) => {
292
+ text += chunk;
293
+ if (text.length > 200000) reject(new Error('request body too large'));
294
+ });
295
+ request.on('end', () => resolve(text ? JSON.parse(text) : {}));
296
+ request.on('error', reject);
297
+ });
298
+ }
299
+
300
+ function readArgs(entries) {
301
+ const parsed = {};
302
+ for (let index = 0; index < entries.length; index += 1) {
303
+ const entry = entries[index];
304
+ if (entry === '--serve') parsed.serve = true;
305
+ else if (entry === '--smoke') parsed.smoke = true;
306
+ else if (entry === '--port') parsed.port = entries[++index];
307
+ else if (entry.startsWith('--port=')) parsed.port = entry.slice('--port='.length);
308
+ }
309
+ return parsed;
310
+ }
311
+
312
+ function assert(condition, message) {
313
+ if (!condition) throw new Error(message);
314
+ }
@@ -0,0 +1,148 @@
1
+ import {
2
+ compileFrontierSource,
3
+ compileNativeSource,
4
+ importNativeSource,
5
+ writeUniversalAstJson
6
+ } from '../dist/index.js';
7
+
8
+ const javascriptSource = `import { nanoid } from "nanoid";
9
+
10
+ export function addTodo(title) {
11
+ return { id: nanoid(), title };
12
+ }
13
+
14
+ export class TodoStore {
15
+ save(title) {
16
+ return addTodo(title);
17
+ }
18
+ }
19
+ `;
20
+
21
+ const imported = importNativeSource({
22
+ language: 'javascript',
23
+ sourcePath: 'src/todo.js',
24
+ sourceText: javascriptSource
25
+ });
26
+
27
+ const graphSummary = {
28
+ universalAst: imported.universalAst.kind,
29
+ semanticIndexId: imported.semanticIndex.id,
30
+ symbols: imported.semanticIndex.symbols.map((symbol) => ({
31
+ name: symbol.name,
32
+ kind: symbol.kind,
33
+ region: symbol.metadata?.ownershipRegionKind
34
+ })),
35
+ sourceMapMappings: imported.sourceMaps[0]?.mappings.length ?? 0,
36
+ mergeReadiness: imported.metadata.nativeImportLossSummary.semanticMergeReadiness,
37
+ losses: imported.losses.map((loss) => loss.kind)
38
+ };
39
+
40
+ console.log('--- JavaScript source ---');
41
+ console.log(javascriptSource.trim());
42
+ console.log('\n--- Frontier graph summary ---');
43
+ console.log(JSON.stringify(graphSummary, null, 2));
44
+ console.log('\n--- Universal AST envelope excerpt ---');
45
+ console.log(JSON.stringify(universalAstExcerpt(imported), null, 2));
46
+
47
+ const stubProjection = compileNativeSource(imported, {
48
+ target: 'rust',
49
+ targetPath: 'src/todo.rs'
50
+ });
51
+
52
+ console.log('\n--- Rust declaration stubs without a target adapter ---');
53
+ console.log(`ok=${stubProjection.ok} readiness=${stubProjection.readiness.readiness} mode=${stubProjection.outputMode}`);
54
+ console.log(stubProjection.output.trim());
55
+
56
+ const adapterProjection = compileNativeSource(imported, {
57
+ target: 'rust',
58
+ targetPath: 'src/todo.rs',
59
+ targetAdapters: [createDemoJsToRustAdapter()]
60
+ });
61
+
62
+ console.log('\n--- Rust output with a host-owned target adapter ---');
63
+ console.log(`ok=${adapterProjection.ok} readiness=${adapterProjection.readiness.readiness} mode=${adapterProjection.outputMode}`);
64
+ console.log(adapterProjection.output.trim());
65
+
66
+ const frontierSource = `module TodoApp @id("mod_todo")
67
+
68
+ type TodoInput @id("type_todo_input") {
69
+ title: Text
70
+ }
71
+
72
+ entity Todo @id("ent_todo") {
73
+ title @id("field_title"): Text
74
+ }
75
+
76
+ action addTodo @id("action_add") {
77
+ input TodoInput
78
+ writes field_title
79
+ returns Patch
80
+ }
81
+ `;
82
+
83
+ const canonicalRust = compileFrontierSource(frontierSource, { target: 'rust' });
84
+
85
+ console.log('\n--- Frontier source projected directly to Rust ---');
86
+ console.log(`ok=${canonicalRust.ok} target=${canonicalRust.target}`);
87
+ console.log(canonicalRust.output.trim());
88
+
89
+ function universalAstExcerpt(importResult) {
90
+ const parsed = JSON.parse(writeUniversalAstJson(importResult.universalAst));
91
+ return {
92
+ kind: parsed.kind,
93
+ id: parsed.id,
94
+ documentCount: parsed.documents?.length ?? 0,
95
+ symbolCount: parsed.semanticIndex?.symbols?.length ?? 0,
96
+ sourceMapCount: parsed.sourceMaps?.length ?? 0
97
+ };
98
+ }
99
+
100
+ function createDemoJsToRustAdapter() {
101
+ return {
102
+ id: 'demo-js-to-rust-target-adapter',
103
+ sourceLanguage: 'javascript',
104
+ target: 'rust',
105
+ version: '0.0.0-demo',
106
+ capabilities: ['declaration-stubs'],
107
+ coverage: {
108
+ readiness: 'ready',
109
+ handledLossKinds: ['opaqueNative', 'declarationOnlyCoverage', 'partialSemanticIndex', 'sourceMapApproximation', 'sourcePreservation'],
110
+ notes: ['Demo adapter turns Frontier semantic symbols into deterministic Rust scaffolding.']
111
+ },
112
+ project(input) {
113
+ const symbols = input.importResult.semanticIndex?.symbols ?? [];
114
+ return {
115
+ output: renderRustScaffold(symbols),
116
+ readiness: 'ready',
117
+ evidence: [{
118
+ id: 'evidence_demo_js_to_rust_adapter',
119
+ kind: 'projection',
120
+ status: 'passed',
121
+ summary: 'Demo adapter projected JavaScript semantic symbols to Rust scaffolding.'
122
+ }]
123
+ };
124
+ }
125
+ };
126
+ }
127
+
128
+ function renderRustScaffold(symbols) {
129
+ const lines = ['// Generated from Frontier semantic graph evidence.'];
130
+ for (const symbol of symbols) {
131
+ if (symbol.kind === 'class') {
132
+ lines.push(`pub struct ${symbol.name};`, '');
133
+ } else if (symbol.kind === 'function' || symbol.kind === 'method') {
134
+ lines.push(`pub fn ${rustName(symbol.name)}() {`);
135
+ lines.push(' todo!("port body from native source evidence");');
136
+ lines.push('}', '');
137
+ }
138
+ }
139
+ return `${lines.join('\n').trim()}\n`;
140
+ }
141
+
142
+ function rustName(name) {
143
+ return String(name)
144
+ .replace(/\./g, '_')
145
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
146
+ .replace(/[^A-Za-z0-9_]/g, '_')
147
+ .toLowerCase();
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-compiler",
3
- "version": "0.2.54",
3
+ "version": "0.2.56",
4
4
  "description": "Compiler facade for Frontier Lang source documents and language projection adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -26,6 +26,10 @@
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",
29
+ "demo:ts-rust": "npm run build && node examples/js-frontier-rust-workbench.mjs --serve",
30
+ "demo:ts-rust:smoke": "npm run build && node examples/js-frontier-rust-workbench.mjs --smoke",
31
+ "demo:js-rust": "npm run demo:ts-rust",
32
+ "demo:js-rust:smoke": "npm run demo:ts-rust:smoke",
29
33
  "prepare": "npm run build",
30
34
  "prepack": "npm run lint && npm test",
31
35
  "pack:dry": "npm pack --dry-run",