@shapeshift-labs/frontier-lang-compiler 0.2.65 → 0.2.67

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.
Files changed (67) hide show
  1. package/README.md +37 -8
  2. package/bench/smoke.mjs +15 -1
  3. package/bench/universal-fixture-suite.mjs +183 -0
  4. package/dist/declarations/import-adapter-core.d.ts +3 -0
  5. package/dist/declarations/native-project-admission.d.ts +133 -0
  6. package/dist/declarations/roundtrip-audit.d.ts +177 -0
  7. package/dist/declarations/roundtrip.d.ts +2 -53
  8. package/dist/declarations/semantic-history-records.d.ts +277 -0
  9. package/dist/declarations/semantic-history.d.ts +45 -92
  10. package/dist/declarations/semantic-merge-candidates.d.ts +200 -0
  11. package/dist/declarations/semantic-merge-conflicts.d.ts +12 -0
  12. package/dist/declarations/semantic-sidecar.d.ts +8 -3
  13. package/dist/declarations/semantic-slice-admission.d.ts +111 -0
  14. package/dist/declarations/semantic-slice.d.ts +36 -1
  15. package/dist/declarations/universal-conversion-plan.d.ts +59 -0
  16. package/dist/declarations/universal-runtime-capabilities.d.ts +171 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +2 -0
  19. package/dist/internal/index-impl/attachExternalOwnership.js +18 -10
  20. package/dist/internal/index-impl/createNativeRoundtripEvidence.js +54 -49
  21. package/dist/internal/index-impl/createSemanticImportSidecar.js +6 -0
  22. package/dist/internal/index-impl/createSemanticSlice.js +4 -3
  23. package/dist/internal/index-impl/createSemanticSliceAdmissionRecord.js +10 -1
  24. package/dist/internal/index-impl/diffNativeSourceImports.js +3 -2
  25. package/dist/internal/index-impl/expandSemanticSliceSelection.js +0 -1
  26. package/dist/internal/index-impl/externalSemanticBase.js +1 -0
  27. package/dist/internal/index-impl/importExternalSemanticIndex.js +4 -0
  28. package/dist/internal/index-impl/nativeRoundtripAudit.js +217 -0
  29. package/dist/internal/index-impl/projectImportAdmissionImportEvidence.js +160 -0
  30. package/dist/internal/index-impl/projectImportAdmissionLanguageSummaries.js +247 -0
  31. package/dist/internal/index-impl/projectImportAdmissionRanks.js +52 -0
  32. package/dist/internal/index-impl/projectImportAdmissionSummaries.js +77 -117
  33. package/dist/internal/index-impl/projectImportAdmissionTasks.js +239 -0
  34. package/dist/internal/index-impl/semanticHistoryRecordNormalizers.js +151 -0
  35. package/dist/internal/index-impl/semanticHistoryRecordOverlaps.js +113 -0
  36. package/dist/internal/index-impl/semanticHistoryRecords.js +210 -149
  37. package/dist/internal/index-impl/semanticMergeCandidateRecordInternals.js +314 -0
  38. package/dist/internal/index-impl/semanticMergeCandidateRecords.js +241 -0
  39. package/dist/internal/index-impl/semanticSliceAdmissionSurface.js +142 -0
  40. package/dist/internal/index-impl/semanticSliceExpectationAssertions.js +100 -0
  41. package/dist/internal/index-impl/semanticSliceExpectationRecords.js +75 -0
  42. package/dist/internal/index-impl/semanticSliceExpectedAssertions.js +5 -2
  43. package/dist/internal/index-impl/testSemanticSlice.js +4 -1
  44. package/dist/internal/index-impl/withExternalEmptyLoss.js +1 -0
  45. package/dist/language-adapter-package-contracts.js +12 -57
  46. package/dist/language-adapter-package-rows.js +116 -0
  47. package/dist/lightweight-dependency-language.js +8 -0
  48. package/dist/native-region-scanner-core.js +42 -10
  49. package/dist/native-region-scanner-js-helpers.js +2 -0
  50. package/dist/native-region-scanner-js-imports.js +111 -0
  51. package/dist/native-region-scanner-js.js +111 -28
  52. package/dist/universal-conversion-plan-summary.js +42 -0
  53. package/dist/universal-conversion-plan.js +46 -40
  54. package/dist/universal-runtime-capabilities.js +92 -0
  55. package/dist/universal-runtime-host-selectors.js +192 -0
  56. package/dist/universal-runtime-profiles.js +109 -0
  57. package/dist/universal-runtime-route-records.js +162 -0
  58. package/examples/js-frontier-rust-workbench-adapters.mjs +89 -0
  59. package/examples/js-frontier-rust-workbench-bounds.mjs +4 -3
  60. package/examples/js-frontier-rust-workbench-client.mjs +135 -59
  61. package/examples/js-frontier-rust-workbench-convert.mjs +161 -0
  62. package/examples/js-frontier-rust-workbench-html.mjs +20 -13
  63. package/examples/js-frontier-rust-workbench-route-styles.mjs +126 -0
  64. package/examples/js-frontier-rust-workbench-route.mjs +190 -0
  65. package/examples/js-frontier-rust-workbench-styles.mjs +12 -54
  66. package/examples/js-frontier-rust-workbench.mjs +54 -214
  67. package/package.json +1 -1
@@ -2,8 +2,8 @@ export function workbenchClientScript() {
2
2
  return `
3
3
  const initial = JSON.parse(document.getElementById('initial-state').textContent);
4
4
  const state = {
5
- sources: { ...initial.sources },
6
- sourceLanguage: initial.sourceLanguage || 'typescript',
5
+ sources: { typescript: initial.sources?.typescript || '' },
6
+ sourceLanguage: 'typescript',
7
7
  result: initial.result,
8
8
  activeTab: 'graph',
9
9
  pending: false
@@ -11,69 +11,65 @@ const state = {
11
11
 
12
12
  window.frontierLangWorkbench = {
13
13
  kind: 'frontier.framework.demo.langWorkbench',
14
- version: 1,
14
+ version: 2,
15
15
  state,
16
16
  feature: {
17
- id: 'frontier-lang-ts-rust-workbench',
17
+ id: 'frontier-lang-ts-rust-python-workbench',
18
18
  package: '@shapeshift-labs/frontier-lang-compiler',
19
19
  route: '/examples/js-frontier-rust-workbench',
20
- panels: ['source', 'frontier', 'rust']
20
+ panels: ['typescript', 'frontier', 'rust', 'python'],
21
+ conversions: ['typescript->rust', 'typescript->python']
21
22
  },
22
23
  evidence: []
23
24
  };
24
25
 
25
26
  const typescriptInput = document.getElementById('typescriptInput');
26
- const rustInput = document.getElementById('rustInput');
27
+ const rustOutput = document.getElementById('rustOutput');
28
+ const pythonOutput = document.getElementById('pythonOutput');
27
29
  const frontierJson = document.getElementById('frontierJson');
28
30
  const graphView = document.getElementById('graphView');
29
31
  const readinessValue = document.getElementById('readinessValue');
30
32
  const symbolValue = document.getElementById('symbolValue');
31
33
  const mappingValue = document.getElementById('mappingValue');
32
- const modeValue = document.getElementById('modeValue');
34
+ const rustValue = document.getElementById('rustValue');
35
+ const pythonValue = document.getElementById('pythonValue');
33
36
  const typescriptStatus = document.getElementById('typescriptStatus');
34
37
  const rustStatus = document.getElementById('rustStatus');
38
+ const pythonStatus = document.getElementById('pythonStatus');
35
39
 
36
- let updatingEditors = false;
37
40
  typescriptInput.value = state.sources.typescript;
38
- rustInput.value = state.sources.rust;
39
41
  render();
40
42
 
41
43
  typescriptInput.addEventListener('input', () => {
42
- if (updatingEditors) return;
43
44
  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
45
  });
54
46
 
55
- document.getElementById('runButton').addEventListener('click', async () => {
56
- await convertFrom(state.sourceLanguage);
47
+ document.getElementById('convertForm').addEventListener('submit', async (event) => {
48
+ event.preventDefault();
49
+ await convertFromTypescript();
57
50
  });
58
51
 
59
- document.getElementById('resetButton').addEventListener('click', async () => {
60
- state.sources = { ...initial.sources };
52
+ document.getElementById('resetButton').addEventListener('click', () => {
53
+ state.sources = { typescript: initial.sources?.typescript || '' };
61
54
  state.sourceLanguage = 'typescript';
62
55
  state.result = initial.result;
63
56
  render();
64
57
  });
65
58
 
66
59
  document.getElementById('copyButton').addEventListener('click', async () => {
67
- await navigator.clipboard.writeText(projectedSourceText());
60
+ try {
61
+ if (!navigator.clipboard) throw new Error('clipboard unavailable');
62
+ await navigator.clipboard.writeText(projectedSourceText());
63
+ } catch (error) {
64
+ window.frontierLangWorkbench.evidence.push({
65
+ at: Date.now(),
66
+ kind: 'clipboard',
67
+ status: 'failed',
68
+ message: String(error.message || error)
69
+ });
70
+ }
68
71
  });
69
72
 
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
73
  for (const button of document.querySelectorAll('[data-frontier-tab]')) {
78
74
  button.addEventListener('click', () => {
79
75
  state.activeTab = button.dataset.frontierTab;
@@ -84,26 +80,37 @@ for (const button of document.querySelectorAll('[data-frontier-tab]')) {
84
80
  });
85
81
  }
86
82
 
87
- async function convertFrom(sourceLanguage) {
88
- state.sourceLanguage = sourceLanguage;
89
- state.sources[sourceLanguage] = sourceLanguage === 'rust' ? rustInput.value : typescriptInput.value;
83
+ async function convertFromTypescript() {
84
+ state.sourceLanguage = 'typescript';
85
+ state.sources.typescript = typescriptInput.value;
90
86
  setPending(true);
91
87
  try {
92
88
  const response = await fetch('/api/convert', {
93
89
  method: 'POST',
94
90
  headers: { 'content-type': 'application/json' },
95
- body: JSON.stringify({ source: state.sources[sourceLanguage], sourceLanguage })
91
+ body: JSON.stringify({ source: state.sources.typescript, sourceLanguage: 'typescript' })
96
92
  });
97
93
  const payload = await response.json();
98
94
  if (!response.ok || !payload.ok) throw new Error(payload.error || 'conversion failed');
99
95
  state.result = payload.result;
100
- state.sources[payload.result.projection.targetLanguage] = payload.result.projection.output;
96
+ const projections = projectionSet(payload.result);
101
97
  window.frontierLangWorkbench.evidence.push({
102
98
  at: Date.now(),
103
99
  kind: 'conversion',
104
100
  sourceHash: payload.result.sourceHash,
105
101
  symbolCount: payload.result.summary.symbols,
106
- readiness: payload.result.summary.readiness
102
+ readiness: payload.result.summary.readiness,
103
+ projections: Object.keys(projections),
104
+ semanticMergeReadiness: payload.result.routeExplanation?.semanticMerge?.readiness,
105
+ missingEvidence: payload.result.routeExplanation?.semanticMerge?.missingEvidence ?? [],
106
+ routeExplanation: (payload.result.routeExplanation?.routes ?? []).map((route) => ({
107
+ target: route.target,
108
+ mode: route.mode,
109
+ routeAction: route.routeAction,
110
+ readiness: route.readiness,
111
+ admissionAction: route.semanticMerge?.admissionAction,
112
+ missingEvidence: route.missingEvidence
113
+ }))
107
114
  });
108
115
  } catch (error) {
109
116
  state.result = errorResult(error);
@@ -116,49 +123,63 @@ async function convertFrom(sourceLanguage) {
116
123
  function render() {
117
124
  const result = state.result || {};
118
125
  const summary = result.summary || {};
126
+ const projections = projectionSet(result);
119
127
  readinessValue.textContent = summary.readiness || 'error';
120
128
  readinessValue.style.color = readinessColor(summary.readiness);
121
129
  symbolValue.textContent = String(summary.symbols ?? 0);
122
130
  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);
131
+ rustValue.textContent = projectionStatus(projections.rust);
132
+ pythonValue.textContent = projectionStatus(projections.python);
133
+ typescriptStatus.textContent = result.sourceHash || 'source';
134
+ rustStatus.textContent = projectionSummaryText(projections.rust);
135
+ pythonStatus.textContent = projectionSummaryText(projections.python);
136
+ typescriptInput.value = state.sources.typescript || '';
137
+ rustOutput.value = projections.rust?.output || '';
138
+ pythonOutput.value = projections.python?.output || '';
139
+ document.querySelector('[data-view-panel="source"]').classList.add('isSource');
140
+ frontierJson.textContent = JSON.stringify(result.frontier || {}, null, 2);
128
141
  frontierJson.hidden = state.activeTab !== 'json';
129
142
  graphView.hidden = state.activeTab !== 'graph';
130
143
  if (!graphView.hidden) graphView.innerHTML = graphHtml(result);
131
144
  }
132
145
 
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));
146
+ function projectionSet(result) {
147
+ if (result.projections) return result.projections;
148
+ const projections = {};
149
+ if (result.projection) {
150
+ projections[result.projection.targetLanguage || result.projection.target || 'rust'] = result.projection;
142
151
  }
152
+ return projections;
143
153
  }
144
154
 
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';
155
+ function projectionStatus(projection) {
156
+ if (!projection) return 'missing';
157
+ return projection.ok ? projection.readiness || 'ready' : 'blocked';
158
+ }
159
+
160
+ function projectionSummaryText(projection) {
161
+ if (!projection) return 'missing';
162
+ return (projection.mode || '-') + ' / ' + String(projection.sourceMapMappings ?? 0) + ' maps';
148
163
  }
149
164
 
150
165
  function graphHtml(result) {
151
166
  const summary = result.summary || {};
152
167
  const frontier = result.frontier || {};
168
+ const routeExplanation = result.routeExplanation || {};
169
+ const semanticMerge = routeExplanation.semanticMerge || {};
153
170
  const symbols = frontier.symbols || [];
154
171
  const relations = frontier.relations || [];
172
+ const missingEvidence = semanticMerge.missingEvidence || [];
155
173
  return [
156
174
  '<div class="chipRow">',
157
175
  chip(summary.readiness, readinessClass(summary.readiness)),
176
+ chip('merge ' + (semanticMerge.readiness || 'unknown'), readinessClass(semanticMerge.readiness)),
177
+ chip(String(missingEvidence.length) + ' missing evidence', missingEvidence.length ? 'review' : 'ready'),
158
178
  chip(String(summary.losses || 0) + ' losses', 'review'),
159
179
  chip(String(summary.patchHints || 0) + ' patch hints', 'ready'),
160
- chip((result.sourceLanguage || '-') + ' -> ' + (result.projection?.targetLanguage || '-'), 'review'),
180
+ chip(projectionTargets(result), 'review'),
161
181
  '</div>',
182
+ routeExplanationHtml(routeExplanation),
162
183
  '<div class="sectionTitle">Symbols</div>',
163
184
  '<div class="graphGrid">',
164
185
  ...symbols.map((symbol) => nodeCard(symbol)),
@@ -172,10 +193,56 @@ function graphHtml(result) {
172
193
  ].join('');
173
194
  }
174
195
 
196
+ function projectionTargets(result) {
197
+ const targets = Object.keys(projectionSet(result));
198
+ return (result.sourceLanguage || '-') + ' -> ' + (targets.length ? targets.join('/') : '-');
199
+ }
200
+
201
+ function routeExplanationHtml(explanation) {
202
+ const routes = explanation.routes || [];
203
+ if (!routes.length) return '';
204
+ const semanticMerge = explanation.semanticMerge || {};
205
+ return [
206
+ '<div class="sectionTitle">Route Explanation</div>',
207
+ '<section class="routeSummary">',
208
+ '<strong>' + escapeHtml('Semantic merge: ' + (semanticMerge.readiness || 'unknown')) + '</strong>',
209
+ '<span>' + escapeHtml((semanticMerge.admissionAction || 'prioritize') + ' / score ' + String(semanticMerge.score ?? 0)) + '</span>',
210
+ '</section>',
211
+ '<div class="routeGrid">',
212
+ ...routes.map((route) => routeCard(route)),
213
+ '</div>'
214
+ ].join('');
215
+ }
216
+
217
+ function routeCard(route) {
218
+ const semanticMerge = route.semanticMerge || {};
219
+ const missing = route.missingEvidenceDetails || [];
220
+ return '<article class="routeCard">' +
221
+ '<div class="routeCardHeader">' +
222
+ '<strong>' + escapeHtml((route.sourceLanguage || '-') + ' -> ' + (route.target || '-')) + '</strong>' +
223
+ chip(semanticMerge.readiness || route.readiness, readinessClass(semanticMerge.readiness || route.readiness)) +
224
+ '</div>' +
225
+ '<p>' + escapeHtml(route.explanation || '') + '</p>' +
226
+ '<dl class="routeFacts">' +
227
+ factHtml('Mode', route.mode) +
228
+ factHtml('Action', route.routeAction) +
229
+ factHtml('Adapter', route.adapter || 'none') +
230
+ factHtml('Admission', semanticMerge.admissionAction || 'prioritize') +
231
+ '</dl>' +
232
+ '<div class="missingEvidenceList">' +
233
+ missing.map((gap) => '<span title="' + escapeHtml(gap.summary) + '">' + escapeHtml(gap.label) + '</span>').join('') +
234
+ '</div>' +
235
+ '</article>';
236
+ }
237
+
238
+ function factHtml(label, value) {
239
+ return '<div><dt>' + escapeHtml(label) + '</dt><dd>' + escapeHtml(value || '-') + '</dd></div>';
240
+ }
241
+
175
242
  function nodeCard(symbol) {
176
243
  return '<article class="nodeCard" data-graph-node="' + escapeHtml(symbol.id || symbol.name) + '">' +
177
244
  '<strong>' + escapeHtml(symbol.name) + '</strong>' +
178
- '<span>' + escapeHtml(symbol.kind || 'symbol') + ' · ' + escapeHtml(symbol.region || 'region') + '</span>' +
245
+ '<span>' + escapeHtml(symbol.kind || 'symbol') + ' - ' + escapeHtml(symbol.region || 'region') + '</span>' +
179
246
  '</article>';
180
247
  }
181
248
 
@@ -199,13 +266,22 @@ function errorResult(error) {
199
266
  return {
200
267
  summary: { readiness: 'blocked', symbols: 0, sourceMapMappings: 0, losses: 1, patchHints: 0 },
201
268
  frontier: { symbols: [], relations: [], error: String(error.message || error) },
202
- projection: { output: String(error.stack || error), readiness: 'blocked', mode: 'error' }
269
+ projections: {
270
+ rust: { output: String(error.stack || error), readiness: 'blocked', mode: 'error', ok: false },
271
+ python: { output: String(error.stack || error), readiness: 'blocked', mode: 'error', ok: false }
272
+ }
203
273
  };
204
274
  }
205
275
 
206
276
  function projectedSourceText() {
207
- const target = state.result?.projection?.targetLanguage;
208
- return target ? state.sources[target] || '' : '';
277
+ const projections = projectionSet(state.result || {});
278
+ return [
279
+ '// Rust projection',
280
+ projections.rust?.output || '',
281
+ '',
282
+ '# Python projection',
283
+ projections.python?.output || ''
284
+ ].join('\\n');
209
285
  }
210
286
 
211
287
  function boundsHtml(bounds) {
@@ -0,0 +1,161 @@
1
+ import {
2
+ compileNativeSource,
3
+ createSemanticImportSidecar,
4
+ createUniversalConversionPlan,
5
+ importNativeSource,
6
+ queryUniversalConversionPlan,
7
+ writeUniversalAstJson
8
+ } from '../dist/index.js';
9
+ import {
10
+ createTsToPythonWorkbenchAdapter,
11
+ createTsToRustWorkbenchAdapter
12
+ } from './js-frontier-rust-workbench-adapters.mjs';
13
+ import { conversionBounds } from './js-frontier-rust-workbench-bounds.mjs';
14
+ import {
15
+ explainRoute,
16
+ routeExplanationSummary
17
+ } from './js-frontier-rust-workbench-route.mjs';
18
+
19
+ export function convertSource(source, options = {}) {
20
+ const sourceLanguage = normalizeSourceLanguage(options.sourceLanguage);
21
+ const targets = ['rust', 'python'];
22
+ const imported = importNativeSource({
23
+ language: sourceLanguage,
24
+ sourcePath: `workbench/input.${sourceExtension(sourceLanguage)}`,
25
+ sourceText: source
26
+ });
27
+ const targetAdapters = [createTsToRustWorkbenchAdapter(), createTsToPythonWorkbenchAdapter()];
28
+ const projections = Object.fromEntries(targets.map((target) => {
29
+ const projection = compileNativeSource(imported, {
30
+ target,
31
+ targetPath: `workbench/output.${targetExtension(target)}`,
32
+ targetAdapters,
33
+ emitOnBlocked: true
34
+ });
35
+ return [target, projectionSummary(projection, { sourceLanguage, target })];
36
+ }));
37
+ const conversionPlan = createUniversalConversionPlan({
38
+ generatedAt: 0,
39
+ imports: [imported],
40
+ targetAdapters,
41
+ targets
42
+ });
43
+ const routes = targets.map((target) => explainRoute(
44
+ queryUniversalConversionPlan(conversionPlan, { sourceLanguage, target }).bestRoute,
45
+ { projection: projections[target], sourceLanguage, target }
46
+ ));
47
+ const sidecar = createSemanticImportSidecar(imported, { generatedAt: 0, targetPath: projections.rust?.targetPath });
48
+ return {
49
+ sourceHash: imported.nativeSource.sourceHash,
50
+ sourceLanguage,
51
+ summary: {
52
+ readiness: sidecar.summary.readiness,
53
+ symbols: imported.semanticIndex.symbols.length,
54
+ sourceMapMappings: imported.sourceMaps[0]?.mappings.length ?? 0,
55
+ losses: imported.losses.length,
56
+ patchHints: sidecar.patchHints.length
57
+ },
58
+ frontier: {
59
+ universalAst: universalAstSummary(imported),
60
+ semanticIndexId: imported.semanticIndex.id,
61
+ sidecarId: sidecar.id,
62
+ symbols: imported.semanticIndex.symbols.map(symbolSummary),
63
+ relations: relationSummaries(imported),
64
+ losses: imported.losses.map(lossSummary),
65
+ patchHints: sidecar.patchHints.map((hint) => ({
66
+ id: hint.id,
67
+ readiness: hint.readiness,
68
+ operations: hint.supportedOperations,
69
+ ownershipKey: hint.ownershipKey
70
+ }))
71
+ },
72
+ bounds: conversionBounds(sourceLanguage, targets.join('/')),
73
+ routeExplanation: routeExplanationSummary(conversionPlan, routes),
74
+ projection: projections.rust,
75
+ projections
76
+ };
77
+ }
78
+
79
+ function projectionSummary(projection, { sourceLanguage, target }) {
80
+ return {
81
+ target,
82
+ targetLanguage: target,
83
+ sourceLanguage,
84
+ targetPath: projection.sourceMap?.targetPath,
85
+ mode: projection.outputMode,
86
+ readiness: projection.readiness.readiness,
87
+ ok: projection.ok,
88
+ output: projection.output,
89
+ sourceMapMappings: projection.sourceMap?.mappings.length ?? 0,
90
+ targetAdapter: projection.metadata?.targetProjectionAdapterId,
91
+ evidence: (projection.evidence ?? []).map(evidenceSummary)
92
+ };
93
+ }
94
+
95
+ function evidenceSummary(record) {
96
+ return {
97
+ id: record.id,
98
+ kind: record.kind,
99
+ status: record.status,
100
+ summary: record.summary,
101
+ adapterId: record.metadata?.adapterId
102
+ };
103
+ }
104
+
105
+ function universalAstSummary(imported) {
106
+ const summary = { valid: true, validationError: undefined };
107
+ let parsed = imported.universalAst;
108
+ try {
109
+ parsed = JSON.parse(writeUniversalAstJson(imported.universalAst));
110
+ } catch (error) {
111
+ summary.valid = false;
112
+ summary.validationError = String(error.message || error);
113
+ }
114
+ return {
115
+ ...summary,
116
+ kind: parsed.kind,
117
+ id: parsed.id,
118
+ nativeSources: parsed.nativeSources?.length ?? 0,
119
+ semanticSymbols: parsed.semanticIndex?.symbols?.length ?? 0,
120
+ sourceMaps: parsed.sourceMaps?.length ?? 0,
121
+ layers: Object.keys(parsed.layers ?? {})
122
+ };
123
+ }
124
+
125
+ function symbolSummary(symbol) {
126
+ return {
127
+ id: symbol.id,
128
+ name: symbol.name,
129
+ kind: symbol.kind,
130
+ language: symbol.language,
131
+ region: symbol.metadata?.ownershipRegionKind,
132
+ ownershipKey: symbol.metadata?.ownershipRegionKey
133
+ };
134
+ }
135
+
136
+ function relationSummaries(imported) {
137
+ const names = new Map(imported.semanticIndex.symbols.map((symbol) => [symbol.id, symbol.name]));
138
+ return imported.semanticIndex.relations.map((relation) => ({
139
+ id: relation.id,
140
+ predicate: relation.predicate,
141
+ label: `${names.get(relation.sourceId) ?? relation.sourceId} ${relation.predicate} ${names.get(relation.targetId) ?? relation.targetId}`
142
+ }));
143
+ }
144
+
145
+ function lossSummary(loss) {
146
+ return { id: loss.id, kind: loss.kind, severity: loss.severity, message: loss.message };
147
+ }
148
+
149
+ function normalizeSourceLanguage(language) {
150
+ return 'typescript';
151
+ }
152
+
153
+ function targetExtension(target) {
154
+ if (target === 'rust') return 'rs';
155
+ if (target === 'python') return 'py';
156
+ return 'txt';
157
+ }
158
+
159
+ function sourceExtension(language) {
160
+ return 'ts';
161
+ }
@@ -4,7 +4,7 @@ export function renderWorkbenchHtml(initialState) {
4
4
  <head>
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
- <title>Frontier Lang TypeScript to Rust Workbench</title>
7
+ <title>Frontier Lang TypeScript Projection Workbench</title>
8
8
  <link rel="stylesheet" href="/styles.css">
9
9
  </head>
10
10
  <body>
@@ -14,25 +14,22 @@ export function renderWorkbenchHtml(initialState) {
14
14
  <span class="brandMark">F</span>
15
15
  <div>
16
16
  <h1>Frontier Lang Workbench</h1>
17
- <p>TypeScript -> Frontier graph -> Rust</p>
17
+ <p>TypeScript -> Frontier graph -> Rust + Python</p>
18
18
  </div>
19
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>
20
+ <form class="topActions" id="convertForm">
21
+ <button class="runButton" id="runButton" type="submit">Run</button>
22
+ <button class="iconButton" id="resetButton" type="button" title="Reset source" aria-label="Reset source">↺</button>
23
+ <button class="iconButton" id="copyButton" type="button" title="Copy projections" aria-label="Copy projections">⧉</button>
24
+ </form>
29
25
  </header>
30
26
 
31
27
  <section class="statusStrip" data-view-panel="status">
32
28
  <div><span class="label">Readiness</span><strong id="readinessValue">-</strong></div>
33
29
  <div><span class="label">Symbols</span><strong id="symbolValue">-</strong></div>
34
30
  <div><span class="label">Mappings</span><strong id="mappingValue">-</strong></div>
35
- <div><span class="label">Mode</span><strong id="modeValue">-</strong></div>
31
+ <div><span class="label">Rust</span><strong id="rustValue">-</strong></div>
32
+ <div><span class="label">Python</span><strong id="pythonValue">-</strong></div>
36
33
  </section>
37
34
 
38
35
  <section class="workspace">
@@ -64,7 +61,17 @@ export function renderWorkbenchHtml(initialState) {
64
61
  <span id="rustStatus">target</span>
65
62
  </div>
66
63
  <div class="paneBody editorBody">
67
- <textarea id="rustInput" spellcheck="false" aria-label="Rust source"></textarea>
64
+ <textarea id="rustOutput" spellcheck="false" aria-label="Rust projection" readonly></textarea>
65
+ </div>
66
+ </section>
67
+
68
+ <section class="pane outputPane" data-view-panel="python">
69
+ <div class="paneHeader">
70
+ <h2>Python</h2>
71
+ <span id="pythonStatus">target</span>
72
+ </div>
73
+ <div class="paneBody editorBody">
74
+ <textarea id="pythonOutput" spellcheck="false" aria-label="Python projection" readonly></textarea>
68
75
  </div>
69
76
  </section>
70
77
  </section>
@@ -0,0 +1,126 @@
1
+ export function workbenchRouteStyles() {
2
+ return `
3
+ .routeSummary {
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: space-between;
7
+ gap: 10px;
8
+ border: 1px solid var(--line);
9
+ border-radius: 8px;
10
+ background: #12171a;
11
+ padding: 10px;
12
+ }
13
+ .routeSummary strong {
14
+ min-width: 0;
15
+ overflow-wrap: anywhere;
16
+ font-size: 13px;
17
+ }
18
+ .routeSummary span {
19
+ flex: 0 0 auto;
20
+ color: var(--muted);
21
+ font-family: var(--mono);
22
+ font-size: 11px;
23
+ }
24
+ .routeGrid {
25
+ display: grid;
26
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
27
+ gap: 8px;
28
+ margin-top: 8px;
29
+ }
30
+ .routeCard {
31
+ border: 1px solid var(--line);
32
+ border-radius: 8px;
33
+ background: #14191d;
34
+ padding: 10px;
35
+ }
36
+ .routeCardHeader {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: space-between;
40
+ gap: 8px;
41
+ }
42
+ .routeCardHeader strong {
43
+ min-width: 0;
44
+ overflow-wrap: anywhere;
45
+ font-size: 13px;
46
+ }
47
+ .routeCard p {
48
+ margin: 8px 0;
49
+ color: var(--muted);
50
+ font-size: 12px;
51
+ line-height: 1.45;
52
+ overflow-wrap: anywhere;
53
+ }
54
+ .routeFacts {
55
+ display: grid;
56
+ grid-template-columns: repeat(2, minmax(0, 1fr));
57
+ gap: 6px;
58
+ margin: 0 0 8px;
59
+ }
60
+ .routeFacts div {
61
+ min-width: 0;
62
+ }
63
+ .routeFacts dt {
64
+ color: var(--faint);
65
+ font-size: 10px;
66
+ text-transform: uppercase;
67
+ }
68
+ .routeFacts dd {
69
+ margin: 2px 0 0;
70
+ color: var(--text);
71
+ font-family: var(--mono);
72
+ font-size: 11px;
73
+ overflow-wrap: anywhere;
74
+ }
75
+ .missingEvidenceList {
76
+ display: flex;
77
+ flex-wrap: wrap;
78
+ gap: 5px;
79
+ }
80
+ .missingEvidenceList span {
81
+ border: 1px solid rgba(231, 183, 95, .45);
82
+ border-radius: 999px;
83
+ padding: 3px 6px;
84
+ color: var(--amber);
85
+ font-size: 10px;
86
+ }
87
+ .edgeList {
88
+ display: grid;
89
+ gap: 6px;
90
+ margin: 0;
91
+ padding: 0;
92
+ list-style: none;
93
+ }
94
+ .edgeList li {
95
+ border-left: 2px solid var(--blue);
96
+ background: rgba(109, 180, 232, .08);
97
+ padding: 6px 8px;
98
+ color: var(--muted);
99
+ font-size: 12px;
100
+ overflow-wrap: anywhere;
101
+ }
102
+ .boundsGrid {
103
+ display: grid;
104
+ grid-template-columns: repeat(3, minmax(0, 1fr));
105
+ gap: 8px;
106
+ }
107
+ .boundCard {
108
+ border: 1px solid var(--line);
109
+ border-radius: 8px;
110
+ background: #12171a;
111
+ padding: 10px;
112
+ }
113
+ .boundCard strong {
114
+ display: block;
115
+ margin-bottom: 8px;
116
+ font-size: 12px;
117
+ }
118
+ .boundCard ul {
119
+ margin: 0;
120
+ padding-left: 16px;
121
+ color: var(--muted);
122
+ font-size: 11px;
123
+ line-height: 1.45;
124
+ }
125
+ `;
126
+ }