@shapeshift-labs/frontier-lang-compiler 0.2.9 → 0.2.11

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/dist/index.js CHANGED
@@ -67,33 +67,57 @@ const semanticMergeReadinessRank = Object.freeze({
67
67
  blocked: 3
68
68
  });
69
69
 
70
+ export const NativeImportRoundtripReadinessStatuses = Object.freeze([
71
+ 'exact',
72
+ 'preserved-source',
73
+ 'stub-only',
74
+ 'blocked',
75
+ 'needs-review'
76
+ ]);
77
+
70
78
  export const NativeImportTaxonomyKinds = Object.freeze([
71
79
  'exactAstImport',
72
80
  'declarationsOnly',
73
81
  'opaqueBodies',
74
82
  'macroExpansion',
75
83
  'preprocessor',
84
+ 'conditionalCompilation',
76
85
  'metaprogramming',
86
+ 'reflection',
77
87
  'generatedCode',
88
+ 'overloadTypeInference',
78
89
  'sourcePreservation',
90
+ 'commentsTrivia',
79
91
  'parserDiagnostics',
80
92
  'unsupportedSyntax',
81
93
  'partialSemanticIndex',
82
- 'sourceMapApproximation'
94
+ 'sourceMapApproximation',
95
+ 'targetProjectionLoss'
83
96
  ]);
84
97
 
85
98
  export const NativeImportLossKinds = Object.freeze([
86
99
  'declarationOnlyCoverage',
87
100
  'opaqueNative',
88
101
  'macroExpansion',
102
+ 'macroHygiene',
89
103
  'preprocessor',
104
+ 'conditionalCompilation',
90
105
  'metaprogramming',
106
+ 'reflection',
107
+ 'dynamicRuntime',
108
+ 'dynamicDispatch',
91
109
  'generatedCode',
110
+ 'overloadResolution',
111
+ 'typeInference',
92
112
  'sourcePreservation',
113
+ 'commentsTrivia',
93
114
  'parserDiagnostic',
94
115
  'unsupportedSyntax',
116
+ 'unsupportedSemantic',
117
+ 'unverifiedNativeAst',
95
118
  'partialSemanticIndex',
96
- 'sourceMapApproximation'
119
+ 'sourceMapApproximation',
120
+ 'targetProjectionLoss'
97
121
  ]);
98
122
 
99
123
  export const NativeImportReadinessBySeverity = Object.freeze({
@@ -312,6 +336,134 @@ export function classifyNativeImportReadiness(losses = [], options = {}) {
312
336
  };
313
337
  }
314
338
 
339
+ export function classifyNativeImportRoundtripReadiness(importResult, options = {}) {
340
+ if (!importResult || typeof importResult !== 'object') {
341
+ throw new Error('classifyNativeImportRoundtripReadiness requires a native import result');
342
+ }
343
+ const imports = nativeImportEntries(importResult);
344
+ const importLosses = uniqueByLossId([
345
+ ...(importResult.losses ?? []),
346
+ ...imports.flatMap((imported) => imported?.losses ?? [])
347
+ ]);
348
+ const importEvidence = uniqueByEvidenceId([
349
+ ...(importResult.evidence ?? []),
350
+ ...imports.flatMap((imported) => imported?.evidence ?? [])
351
+ ]);
352
+ const exactAst = imports.length > 0 && imports.every((imported) => nativeImportHasExactAstCoverage(imported));
353
+ const importReadiness = classifyNativeImportReadiness(importLosses, {
354
+ exactAst,
355
+ evidence: importEvidence,
356
+ parser: nativeImportRoundtripParser(importResult, imports),
357
+ scanKind: importResult.metadata?.nativeImportLossSummary?.scanKind,
358
+ semanticStatus: importResult.metadata?.semanticStatus ?? importResult.universalAst?.metadata?.semanticStatus
359
+ });
360
+ const projection = options.projection ?? projectNativeImportToSource(importResult, options);
361
+ const projectionReadiness = projection.readiness ?? classifyNativeImportReadiness(projection.losses ?? [], {
362
+ evidence: projection.evidence,
363
+ parser: projection.metadata?.nativeImportLossSummary?.parser,
364
+ scanKind: 'native-source-projection'
365
+ });
366
+ const universalAst = importResult.universalAst;
367
+ const universalAstIssues = universalAst
368
+ ? validateUniversalAstEnvelope(universalAst)
369
+ : ['missing-universal-ast'];
370
+ const universalAstNativeSources = universalAst?.nativeSources?.length ?? importResult.nativeSources?.length ?? (importResult.nativeSource ? 1 : 0);
371
+ const semanticIndex = importResult.semanticIndex ?? universalAst?.semanticIndex;
372
+ const semanticSymbols = semanticIndex?.symbols?.length ?? 0;
373
+ const sourceMaps = importResult.sourceMaps ?? universalAst?.sourceMaps ?? [];
374
+ const sourceMapMappings = sourceMaps.reduce((sum, sourceMap) => sum + (sourceMap?.mappings?.length ?? 0), 0);
375
+ const projectionMatchesSourceHash = Boolean(projection.sourceHash && projection.outputHash === projection.sourceHash);
376
+ const preservedSource = projection.mode === 'preserved-source';
377
+ const failedEvidenceIds = uniqueStrings([
378
+ ...importEvidence.filter((record) => record?.status === 'failed').map((record) => record.id),
379
+ ...(projection.evidence ?? []).filter((record) => record?.status === 'failed').map((record) => record.id)
380
+ ]);
381
+ const blockingReasons = [
382
+ ...(importReadiness.readiness === 'blocked' ? importReadiness.reasons : []),
383
+ ...(projectionReadiness.readiness === 'blocked' ? projectionReadiness.reasons : []),
384
+ ...(failedEvidenceIds.length ? [`Failed evidence prevents native roundtrip readiness: ${failedEvidenceIds.join(', ')}`] : []),
385
+ ...(universalAstIssues.length ? [`Universal AST validation failed: ${universalAstIssues.join('; ')}`] : [])
386
+ ];
387
+ const reviewReasons = [
388
+ ...(semanticSymbols === 0 ? ['Universal AST semantic index has no symbols for source projection review.'] : []),
389
+ ...(sourceMapMappings === 0 ? ['Universal AST has no native source-map mappings for roundtrip review.'] : []),
390
+ ...(preservedSource && !projectionMatchesSourceHash ? ['Projected source was preserved without a verified import source hash match.'] : []),
391
+ ...importReadiness.reasons.filter((reason) => importReadiness.readiness !== 'ready' || !exactAst),
392
+ ...projectionReadiness.reasons.filter((reason) => projectionReadiness.readiness !== 'ready')
393
+ ];
394
+ let status;
395
+ if (blockingReasons.length) {
396
+ status = 'blocked';
397
+ } else if (projection.mode === 'native-source-stubs') {
398
+ status = 'stub-only';
399
+ } else if (reviewReasons.some((reason) => reason.startsWith('Universal AST')) || (preservedSource && !projectionMatchesSourceHash)) {
400
+ status = 'needs-review';
401
+ } else if (exactAst && preservedSource && projectionMatchesSourceHash && projectionReadiness.readiness === 'ready') {
402
+ status = 'exact';
403
+ } else if (preservedSource && projectionMatchesSourceHash) {
404
+ status = 'preserved-source';
405
+ } else {
406
+ status = 'needs-review';
407
+ }
408
+ const reasons = nativeImportRoundtripReasons(status, {
409
+ blockingReasons,
410
+ reviewReasons,
411
+ projection,
412
+ importReadiness,
413
+ projectionReadiness
414
+ });
415
+ return {
416
+ kind: 'frontier.lang.nativeImportRoundtripReadiness',
417
+ version: 1,
418
+ status,
419
+ semanticMergeReadiness: maxSemanticMergeReadiness(importReadiness.readiness, projectionReadiness.readiness),
420
+ reasons,
421
+ importReadiness,
422
+ projectionReadiness,
423
+ projectionMode: projection.mode,
424
+ checks: {
425
+ nativeImport: {
426
+ imports: imports.length,
427
+ exactAst,
428
+ losses: importReadiness.summary.total,
429
+ readiness: importReadiness.readiness
430
+ },
431
+ universalAst: {
432
+ present: Boolean(universalAst),
433
+ valid: universalAstIssues.length === 0,
434
+ issues: universalAstIssues,
435
+ nativeSources: universalAstNativeSources,
436
+ semanticSymbols,
437
+ sourceMaps: sourceMaps.length,
438
+ sourceMapMappings
439
+ },
440
+ projectedSource: {
441
+ mode: projection.mode,
442
+ outputHash: projection.outputHash,
443
+ expectedSourceHash: projection.sourceHash,
444
+ sourceHashVerified: projectionMatchesSourceHash,
445
+ declarations: projection.declarations?.length ?? 0,
446
+ losses: projection.lossSummary?.total ?? projection.losses?.length ?? 0,
447
+ readiness: projectionReadiness.readiness
448
+ }
449
+ },
450
+ evidence: {
451
+ importEvidenceIds: importEvidence.map((record) => record.id).filter(Boolean),
452
+ projectionEvidenceIds: (projection.evidence ?? []).map((record) => record.id).filter(Boolean),
453
+ failedEvidenceIds
454
+ },
455
+ metadata: {
456
+ nativeImportId: importResult.id,
457
+ universalAstId: universalAst?.id,
458
+ projectionId: projection.id,
459
+ sourcePath: projection.sourcePath ?? importResult.sourcePath,
460
+ language: projection.language ?? importResult.language,
461
+ sourcePreservationId: projection.metadata?.sourcePreservationId,
462
+ ...options.metadata
463
+ }
464
+ };
465
+ }
466
+
315
467
  export function createNativeImportCoverageMatrix(input = {}) {
316
468
  const imports = input.imports ?? [];
317
469
  const adapters = input.adapters ?? [];
@@ -356,6 +508,70 @@ export function createNativeImportCoverageMatrix(input = {}) {
356
508
  };
357
509
  }
358
510
 
511
+ export function createNativeSourcePreservation(options) {
512
+ if (!options || typeof options.sourceText !== 'string') {
513
+ throw new Error('createNativeSourcePreservation requires sourceText');
514
+ }
515
+ const language = options.language ?? 'source';
516
+ const sourceText = options.sourceText;
517
+ const computedSourceHash = hashSemanticValue(sourceText);
518
+ const declaredSourceHash = options.sourceHash;
519
+ const sourceHash = computedSourceHash;
520
+ const tokensAndTrivia = scanPreservedSourceTokens(sourceText, {
521
+ language,
522
+ sourcePath: options.sourcePath,
523
+ sourceHash,
524
+ includeTokens: options.includeTokens !== false,
525
+ includeTrivia: options.includeTrivia !== false,
526
+ maxTokens: options.maxTokens,
527
+ maxTrivia: options.maxTrivia
528
+ });
529
+ const directiveScan = options.includeDirectives === false
530
+ ? { directives: [], truncated: false }
531
+ : scanPreservedSourceDirectives(sourceText, {
532
+ language,
533
+ sourcePath: options.sourcePath,
534
+ sourceHash,
535
+ maxDirectives: options.maxDirectives
536
+ });
537
+ const directives = directiveScan.directives;
538
+ const newline = detectNewlineStyle(sourceText);
539
+ return {
540
+ kind: 'frontier.lang.nativeSourcePreservation',
541
+ version: 1,
542
+ id: options.id ?? `native_source_preservation_${idFragment(options.sourcePath ?? language)}_${idFragment(sourceHash)}`,
543
+ language,
544
+ sourcePath: options.sourcePath,
545
+ sourceHash,
546
+ sourceBytes: Buffer.byteLength(sourceText, options.encoding ?? 'utf8'),
547
+ lineCount: sourceText.length ? sourceText.split(/\r\n|\r|\n/).length : 0,
548
+ newline,
549
+ encoding: options.encoding ?? 'utf8',
550
+ ...(options.includeSourceText === false ? {} : { sourceText }),
551
+ tokens: tokensAndTrivia.tokens,
552
+ trivia: tokensAndTrivia.trivia,
553
+ directives,
554
+ summary: {
555
+ tokens: tokensAndTrivia.tokens.length,
556
+ trivia: tokensAndTrivia.trivia.length,
557
+ directives: directives.length,
558
+ comments: tokensAndTrivia.trivia.filter((entry) => entry.kind === 'comment').length,
559
+ whitespace: tokensAndTrivia.trivia.filter((entry) => entry.kind === 'whitespace' || entry.kind === 'newline').length,
560
+ exactSourceAvailable: options.includeSourceText !== false,
561
+ truncated: tokensAndTrivia.truncated || directiveScan.truncated
562
+ },
563
+ metadata: {
564
+ preservation: 'source-text-token-trivia-directive-evidence',
565
+ tokenization: 'frontier-lightweight-lexical-scan',
566
+ ...(declaredSourceHash ? {
567
+ declaredSourceHash,
568
+ sourceHashVerified: declaredSourceHash === computedSourceHash
569
+ } : {}),
570
+ ...options.metadata
571
+ }
572
+ };
573
+ }
574
+
359
575
  export function createSemanticImportSidecar(importResult, options = {}) {
360
576
  const imports = Array.isArray(importResult?.imports) ? importResult.imports : [importResult].filter(Boolean);
361
577
  const importEntries = imports.map((imported, index) => semanticImportSidecarEntry(imported, index, options));
@@ -458,6 +674,20 @@ export function createTypeScriptCompilerNativeImporterAdapter(options = {}) {
458
674
  parser: options.parser ?? 'typescript-compiler-api',
459
675
  version: options.version,
460
676
  capabilities: uniqueStrings(['nativeAst', 'semanticIndex', 'sourceMaps', 'diagnostics', ...(options.capabilities ?? [])]),
677
+ coverage: nativeImporterAdapterCoverage({
678
+ exactness: 'exact-parser-ast',
679
+ exactAst: true,
680
+ tokens: false,
681
+ trivia: false,
682
+ diagnostics: true,
683
+ sourceRanges: true,
684
+ generatedRanges: false,
685
+ semanticCoverage: declarationSemanticCoverage(),
686
+ notes: [
687
+ 'Normalizes a caller-owned TypeScript SourceFile into native AST nodes and declaration-level semantic index records.',
688
+ 'Type resolution, reference resolution, control flow, generated ranges, and parser token/trivia streams require host-supplied adapter evidence.'
689
+ ]
690
+ }, options.coverage),
461
691
  supportedExtensions: options.supportedExtensions ?? ['.ts', '.tsx', '.js', '.jsx'],
462
692
  diagnostics: options.diagnostics,
463
693
  parse(input) {
@@ -488,6 +718,20 @@ export function createTreeSitterNativeImporterAdapter(options = {}) {
488
718
  parser: options.parserName ?? options.parser ?? 'tree-sitter',
489
719
  version: options.version,
490
720
  capabilities: uniqueStrings(['nativeAst', 'semanticIndex', 'sourceMaps', 'diagnostics', ...(options.capabilities ?? [])]),
721
+ coverage: nativeImporterAdapterCoverage({
722
+ exactness: 'parser-tree',
723
+ exactAst: true,
724
+ tokens: false,
725
+ trivia: false,
726
+ diagnostics: true,
727
+ sourceRanges: true,
728
+ generatedRanges: false,
729
+ semanticCoverage: declarationSemanticCoverage(),
730
+ notes: [
731
+ 'Normalizes a caller-owned tree-sitter tree into native AST nodes and declaration-level semantic index records.',
732
+ 'The built-in wrapper walks named syntax nodes; exact token/trivia streams and generated ranges require adapter-specific evidence.'
733
+ ]
734
+ }, options.coverage),
491
735
  supportedExtensions: options.supportedExtensions ?? [],
492
736
  diagnostics: options.diagnostics,
493
737
  parse(input) {
@@ -592,7 +836,14 @@ export async function runNativeImporterAdapter(adapter, input = {}) {
592
836
  parseResult.losses,
593
837
  diagnostics.map((diagnostic, index) => adapterDiagnosticToLoss(diagnostic, index, summary, parseInput))
594
838
  );
595
- const sourceEvidence = adapterDiagnosticsEvidence(summary, diagnostics, {
839
+ const adapterSummary = {
840
+ ...summary,
841
+ coverage: observeNativeImporterAdapterCoverage(summary.coverage, parseResult, {
842
+ diagnostics,
843
+ losses
844
+ })
845
+ };
846
+ const sourceEvidence = adapterDiagnosticsEvidence(adapterSummary, diagnostics, {
596
847
  language,
597
848
  parser,
598
849
  parserVersion,
@@ -614,8 +865,9 @@ export async function runNativeImporterAdapter(adapter, input = {}) {
614
865
  metadata: {
615
866
  adapterId: summary.id,
616
867
  adapterVersion: summary.version,
617
- adapterCapabilities: summary.capabilities,
618
- supportedExtensions: summary.supportedExtensions,
868
+ adapterCapabilities: adapterSummary.capabilities,
869
+ adapterCoverage: adapterSummary.coverage,
870
+ supportedExtensions: adapterSummary.supportedExtensions,
619
871
  diagnostics: diagnostics.map(serializableDiagnostic),
620
872
  ...input.metadata,
621
873
  ...parseResult.metadata
@@ -624,6 +876,7 @@ export async function runNativeImporterAdapter(adapter, input = {}) {
624
876
  adapterId: summary.id,
625
877
  adapterVersion: summary.version,
626
878
  parser,
879
+ adapterCoverage: adapterSummary.coverage,
627
880
  ...input.nativeAstMetadata,
628
881
  ...parseResult.nativeAstMetadata
629
882
  },
@@ -631,6 +884,7 @@ export async function runNativeImporterAdapter(adapter, input = {}) {
631
884
  adapterId: summary.id,
632
885
  adapterVersion: summary.version,
633
886
  parser,
887
+ adapterCoverage: adapterSummary.coverage,
634
888
  ...input.nativeSourceMetadata,
635
889
  ...parseResult.nativeSourceMetadata
636
890
  },
@@ -649,7 +903,7 @@ export async function runNativeImporterAdapter(adapter, input = {}) {
649
903
  };
650
904
  return {
651
905
  ...importNativeSource(importInput),
652
- adapter: summary,
906
+ adapter: adapterSummary,
653
907
  diagnostics
654
908
  };
655
909
  }
@@ -681,6 +935,7 @@ export function projectNativeImportToSource(importResult, options = {}) {
681
935
  sourcePath: context.sourcePath,
682
936
  expectedSourceHash: context.sourceHash,
683
937
  providedSourceHash: candidateSource?.sourceHash,
938
+ sourcePreservationId: candidateSource?.sourcePreservationId,
684
939
  sourceHashVerified: candidateSource?.hashVerified ?? false,
685
940
  declarationCount: declarations.length
686
941
  }
@@ -725,6 +980,7 @@ export function projectNativeImportToSource(importResult, options = {}) {
725
980
  universalAstId: importResult.universalAst?.id,
726
981
  exactSourceAvailable: candidateSource?.exact === true,
727
982
  sourceTextAvailable: typeof candidateSource?.sourceText === 'string',
983
+ sourcePreservationId: candidateSource?.sourcePreservationId,
728
984
  sourceHashVerified: candidateSource?.hashVerified ?? false,
729
985
  nativeImportLossSummary,
730
986
  ...options.metadata
@@ -736,17 +992,30 @@ export function importNativeSource(input) {
736
992
  const language = input.language ?? input.nativeAst?.language;
737
993
  if (!language) throw new Error('importNativeSource requires a language or nativeAst.language');
738
994
  const sourcePath = input.sourcePath ?? input.nativeAst?.sourcePath;
739
- const sourceHash = input.sourceHash ?? input.nativeAst?.sourceHash ?? (input.sourceText ? hashSemanticValue(input.sourceText) : hashSemanticValue(input.nativeAst?.nodes ?? input.nativeAst ?? {}));
995
+ const declaredSourceHash = input.sourceHash ?? input.nativeAst?.sourceHash;
996
+ const sourceHash = typeof input.sourceText === 'string'
997
+ ? hashSemanticValue(input.sourceText)
998
+ : declaredSourceHash ?? hashSemanticValue(input.nativeAst?.nodes ?? input.nativeAst ?? {});
740
999
  const targetPath = input.targetPath ?? input.target?.emitPath;
741
1000
  const targetHash = input.targetHash;
742
1001
  const importIdPart = idFragment(input.id ?? input.nativeSourceId ?? sourcePath ?? language);
1002
+ const sourcePreservation = input.sourcePreservation ?? (typeof input.sourceText === 'string'
1003
+ ? createNativeSourcePreservation({
1004
+ language,
1005
+ sourcePath,
1006
+ sourceHash: declaredSourceHash,
1007
+ sourceText: input.sourceText,
1008
+ metadata: { importIdPart }
1009
+ })
1010
+ : undefined);
743
1011
  const lightweight = !input.nativeAst && !input.nodes && input.sourceText
744
1012
  ? createLightweightNativeImport({
745
1013
  language,
746
1014
  sourceText: input.sourceText,
747
1015
  sourcePath,
748
1016
  sourceHash,
749
- parser: input.parser
1017
+ parser: input.parser,
1018
+ sourcePreservation
750
1019
  })
751
1020
  : undefined;
752
1021
  const nativeAst = input.nativeAst ?? createNativeAstRecord({
@@ -769,6 +1038,15 @@ export function importNativeSource(input) {
769
1038
  losses: input.losses ?? lightweight?.losses,
770
1039
  metadata: {
771
1040
  ...(input.sourceText ? { sourceBytes: input.sourceText.length } : {}),
1041
+ ...(sourcePreservation ? {
1042
+ sourcePreservationId: sourcePreservation.id,
1043
+ sourcePreservationSummary: sourcePreservation.summary,
1044
+ sourcePreservation
1045
+ } : {}),
1046
+ ...(declaredSourceHash && declaredSourceHash !== sourceHash ? {
1047
+ declaredSourceHash,
1048
+ sourceHashVerified: false
1049
+ } : {}),
772
1050
  ...lightweight?.metadata,
773
1051
  ...input.nativeAstMetadata
774
1052
  }
@@ -776,7 +1054,17 @@ export function importNativeSource(input) {
776
1054
  const frontierNodeIds = input.frontierNodeIds ?? input.semanticNodes?.map((node) => node.id) ?? [];
777
1055
  const semanticNodes = input.semanticNodes ?? [];
778
1056
  const semanticStatus = input.semanticStatus ?? (semanticNodes.length ? 'mapped' : 'native-only');
779
- const losses = normalizeNativeLossRecords(input.losses ?? nativeAst.losses ?? lightweight?.losses ?? []);
1057
+ const nativeAstExact = hasNativeExactAstEvidence(input, nativeAst, lightweight);
1058
+ const baseLosses = normalizeNativeLossRecords(input.losses ?? nativeAst.losses ?? lightweight?.losses ?? []);
1059
+ const losses = normalizeNativeLossRecords([
1060
+ ...baseLosses,
1061
+ ...unverifiedNativeAstLosses(input, nativeAst, {
1062
+ importIdPart,
1063
+ exactAst: nativeAstExact,
1064
+ hasLosses: baseLosses.length > 0,
1065
+ lightweight
1066
+ })
1067
+ ]);
780
1068
  const nativeSource = nativeSourceNode({
781
1069
  id: input.nativeSourceId ?? `native_source_${importIdPart}`,
782
1070
  name: input.name ?? sourcePath?.split(/[\\/]/).filter(Boolean).at(-1) ?? `${language}NativeSource`,
@@ -793,6 +1081,14 @@ export function importNativeSource(input) {
793
1081
  metadata: {
794
1082
  semanticStatus,
795
1083
  mappings: input.mappings ?? [],
1084
+ ...(sourcePreservation ? {
1085
+ sourcePreservationId: sourcePreservation.id,
1086
+ sourcePreservation
1087
+ } : {}),
1088
+ ...(declaredSourceHash && declaredSourceHash !== sourceHash ? {
1089
+ declaredSourceHash,
1090
+ sourceHashVerified: false
1091
+ } : {}),
796
1092
  ...input.nativeSourceMetadata
797
1093
  }
798
1094
  });
@@ -816,11 +1112,20 @@ export function importNativeSource(input) {
816
1112
  metadata: {
817
1113
  parser: nativeAst.parser,
818
1114
  sourcePath,
819
- semanticStatus
1115
+ semanticStatus,
1116
+ ...(sourcePreservation ? {
1117
+ sourcePreservationId: sourcePreservation.id,
1118
+ sourcePreservationSummary: sourcePreservation.summary
1119
+ } : {})
1120
+ ,
1121
+ ...(declaredSourceHash && declaredSourceHash !== sourceHash ? {
1122
+ declaredSourceHash,
1123
+ sourceHashVerified: false
1124
+ } : {})
820
1125
  }
821
1126
  }];
822
1127
  const lossSummary = summarizeNativeImportLosses(losses, {
823
- exactAst: Boolean(input.nativeAst || input.nodes),
1128
+ exactAst: nativeAstExact,
824
1129
  evidence: baseEvidence,
825
1130
  parser: nativeAst.parser,
826
1131
  scanKind: lightweight?.metadata?.scanKind,
@@ -896,6 +1201,14 @@ export function importNativeSource(input) {
896
1201
  sourcePath,
897
1202
  semanticStatus,
898
1203
  nativeImportLossSummary: lossSummary,
1204
+ ...(sourcePreservation ? {
1205
+ sourcePreservationId: sourcePreservation.id,
1206
+ sourcePreservation
1207
+ } : {}),
1208
+ ...(declaredSourceHash && declaredSourceHash !== sourceHash ? {
1209
+ declaredSourceHash,
1210
+ sourceHashVerified: false
1211
+ } : {}),
899
1212
  ...input.universalAstMetadata
900
1213
  }
901
1214
  });
@@ -915,6 +1228,14 @@ export function importNativeSource(input) {
915
1228
  semanticIndexId: semanticIndex?.id,
916
1229
  universalAstId: universalAst.id,
917
1230
  sourceMapIds: sourceMaps.map((sourceMap) => sourceMap.id),
1231
+ ...(sourcePreservation ? {
1232
+ sourcePreservationId: sourcePreservation.id,
1233
+ sourcePreservationSummary: sourcePreservation.summary
1234
+ } : {}),
1235
+ ...(declaredSourceHash && declaredSourceHash !== sourceHash ? {
1236
+ declaredSourceHash,
1237
+ sourceHashVerified: false
1238
+ } : {}),
918
1239
  nativeImportLossSummary: lossSummary
919
1240
  }
920
1241
  });
@@ -937,6 +1258,14 @@ export function importNativeSource(input) {
937
1258
  sourceMapIds: sourceMaps.map((sourceMap) => sourceMap.id),
938
1259
  semanticStatus,
939
1260
  mappings: resultSourceMapMappings,
1261
+ ...(sourcePreservation ? {
1262
+ sourcePreservationId: sourcePreservation.id,
1263
+ sourcePreservation
1264
+ } : {}),
1265
+ ...(declaredSourceHash && declaredSourceHash !== sourceHash ? {
1266
+ declaredSourceHash,
1267
+ sourceHashVerified: false
1268
+ } : {}),
940
1269
  nativeImportLossSummary: lossSummary,
941
1270
  ...input.metadata
942
1271
  }
@@ -1040,7 +1369,7 @@ function createLightweightNativeImport(input) {
1040
1369
  }
1041
1370
  if (declaration.loss) losses.push(declaration.loss);
1042
1371
  }
1043
- losses.push(...lightweightCoverageLosses(input, declarations));
1372
+ losses.push(...lightweightCoverageLosses(input, declarations, input.sourcePreservation));
1044
1373
 
1045
1374
  const semanticIndex = createSemanticIndexRecord({
1046
1375
  id: `index_${idFragment(input.sourcePath ?? input.language)}`,
@@ -1076,7 +1405,15 @@ function createLightweightNativeImport(input) {
1076
1405
  losses,
1077
1406
  semanticIndex,
1078
1407
  mappings,
1079
- metadata: { parser, scanKind: 'lightweight-declaration-scan', declarationCount: declarations.length }
1408
+ metadata: {
1409
+ parser,
1410
+ scanKind: 'lightweight-declaration-scan',
1411
+ declarationCount: declarations.length,
1412
+ ...(input.sourcePreservation ? {
1413
+ sourcePreservationId: input.sourcePreservation.id,
1414
+ sourcePreservationSummary: input.sourcePreservation.summary
1415
+ } : {})
1416
+ }
1080
1417
  };
1081
1418
  }
1082
1419
 
@@ -1121,20 +1458,32 @@ function nativeImportProjectionContext(importResult, options) {
1121
1458
  }
1122
1459
 
1123
1460
  function nativeProjectionSourceCandidate(context, options) {
1124
- const sourceText = options.sourceText ?? options.preservedSourceText ?? options.exactSourceText;
1461
+ const preservation = sourcePreservationFromProjectionContext(context);
1462
+ const explicitSourceText = options.sourceText ?? options.preservedSourceText ?? options.exactSourceText;
1463
+ const sourceText = explicitSourceText ?? preservation?.sourceText;
1125
1464
  if (typeof sourceText !== 'string') return undefined;
1126
- const sourceHash = options.sourceHash ?? hashSemanticValue(sourceText);
1465
+ const computedSourceHash = hashSemanticValue(sourceText);
1466
+ const declaredSourceHash = options.sourceHash ?? (explicitSourceText === undefined ? preservation?.sourceHash : undefined);
1467
+ const sourceHash = computedSourceHash;
1127
1468
  const hashVerified = Boolean(context.sourceHash);
1128
1469
  const exact = !context.sourceHash || sourceHash === context.sourceHash || options.verifySourceHash === false;
1129
1470
  return {
1130
1471
  sourceText,
1131
1472
  sourceHash,
1473
+ declaredSourceHash,
1132
1474
  hashVerified,
1133
1475
  exact,
1134
- mismatch: hashVerified && sourceHash !== context.sourceHash && options.verifySourceHash !== false
1476
+ mismatch: hashVerified && sourceHash !== context.sourceHash && options.verifySourceHash !== false,
1477
+ sourcePreservationId: preservation?.id
1135
1478
  };
1136
1479
  }
1137
1480
 
1481
+ function sourcePreservationFromProjectionContext(context) {
1482
+ return context.nativeSource?.metadata?.sourcePreservation
1483
+ ?? context.nativeAst?.metadata?.sourcePreservation
1484
+ ?? context.nativeSource?.ast?.metadata?.sourcePreservation;
1485
+ }
1486
+
1138
1487
  function nativeProjectionDeclarations(importResult, context) {
1139
1488
  const semanticIndex = context.semanticIndex;
1140
1489
  const occurrencesBySymbol = new Map();
@@ -1238,14 +1587,15 @@ function nativeProjectionStubLosses(context, candidateSource, declarations, opti
1238
1587
  : 'Exact native source text was not provided; emitted declaration stubs reconstructed from import metadata.';
1239
1588
  const losses = [nativeProjectionLoss(context, {
1240
1589
  id: `loss_${context.idPart}_native_source_stub`,
1241
- kind: 'sourcePreservation',
1590
+ kind: candidateSource?.mismatch ? 'sourcePreservation' : 'targetProjectionLoss',
1242
1591
  severity: 'warning',
1243
1592
  message,
1244
1593
  metadata: {
1245
1594
  reason,
1246
1595
  projectionMode: 'native-source-stubs',
1247
1596
  expectedSourceHash: context.sourceHash,
1248
- providedSourceHash: candidateSource?.sourceHash
1597
+ providedSourceHash: candidateSource?.sourceHash,
1598
+ declaredSourceHash: candidateSource?.declaredSourceHash
1249
1599
  }
1250
1600
  })];
1251
1601
  if (!declarations.length) {
@@ -2249,7 +2599,7 @@ function opaqueBodyLoss(input, lineNumber, nodeId, name) {
2249
2599
  };
2250
2600
  }
2251
2601
 
2252
- function lightweightCoverageLosses(input, declarations) {
2602
+ function lightweightCoverageLosses(input, declarations, sourcePreservation) {
2253
2603
  const id = idFragment(input.sourcePath ?? input.language);
2254
2604
  const span = declarations[0]?.span ?? {
2255
2605
  sourceId: input.sourceHash,
@@ -2291,12 +2641,275 @@ function lightweightCoverageLosses(input, declarations) {
2291
2641
  phase: 'read',
2292
2642
  sourceFormat: input.language,
2293
2643
  kind: 'sourcePreservation',
2294
- message: 'Comments, whitespace, token order, directives, and formatting are not preserved by the lightweight importer.',
2295
- span
2644
+ message: sourcePreservation
2645
+ ? 'Comments, whitespace, token order, directives, and formatting are preserved as opaque native source evidence; exact structural edits still require a parser adapter.'
2646
+ : 'Comments, whitespace, token order, directives, and formatting are not preserved by the lightweight importer.',
2647
+ span,
2648
+ metadata: sourcePreservation ? {
2649
+ sourcePreservationId: sourcePreservation.id,
2650
+ sourcePreservationSummary: sourcePreservation.summary
2651
+ } : undefined
2296
2652
  }
2297
2653
  ];
2298
2654
  }
2299
2655
 
2656
+ function scanPreservedSourceTokens(sourceText, input) {
2657
+ const tokens = [];
2658
+ const trivia = [];
2659
+ const includeTokens = input.includeTokens !== false;
2660
+ const includeTrivia = input.includeTrivia !== false;
2661
+ const maxTokens = Number.isFinite(input.maxTokens) ? Math.max(0, input.maxTokens) : 20000;
2662
+ const maxTrivia = Number.isFinite(input.maxTrivia) ? Math.max(0, input.maxTrivia) : 20000;
2663
+ let offset = 0;
2664
+ let line = 1;
2665
+ let column = 1;
2666
+ let truncated = false;
2667
+ const push = (target, kind, text, start) => {
2668
+ if ((target === tokens && !includeTokens) || (target === trivia && !includeTrivia)) return;
2669
+ const max = target === tokens ? maxTokens : maxTrivia;
2670
+ if (target.length >= max) {
2671
+ truncated = true;
2672
+ return;
2673
+ }
2674
+ target.push(preservedSourceSegment({
2675
+ index: target.length,
2676
+ kind,
2677
+ text,
2678
+ start,
2679
+ end: { offset, line, column },
2680
+ sourceHash: input.sourceHash,
2681
+ sourcePath: input.sourcePath
2682
+ }));
2683
+ };
2684
+ while (offset < sourceText.length) {
2685
+ const start = { offset, line, column };
2686
+ const char = sourceText[offset];
2687
+ const next = sourceText[offset + 1];
2688
+ if (char === '\r' || char === '\n') {
2689
+ const text = char === '\r' && next === '\n' ? '\r\n' : char;
2690
+ offset += text.length;
2691
+ line += 1;
2692
+ column = 1;
2693
+ push(trivia, 'newline', text, start);
2694
+ continue;
2695
+ }
2696
+ if (char === ' ' || char === '\t' || char === '\v' || char === '\f') {
2697
+ let text = '';
2698
+ while (offset < sourceText.length && /[ \t\v\f]/.test(sourceText[offset])) {
2699
+ text += sourceText[offset];
2700
+ offset += 1;
2701
+ column += 1;
2702
+ }
2703
+ push(trivia, 'whitespace', text, start);
2704
+ continue;
2705
+ }
2706
+ if (char === '/' && next === '/') {
2707
+ let text = '';
2708
+ while (offset < sourceText.length && sourceText[offset] !== '\n' && sourceText[offset] !== '\r') {
2709
+ text += sourceText[offset];
2710
+ offset += 1;
2711
+ column += 1;
2712
+ }
2713
+ push(trivia, 'comment', text, start);
2714
+ continue;
2715
+ }
2716
+ if (char === '/' && next === '*') {
2717
+ let text = '';
2718
+ while (offset < sourceText.length) {
2719
+ const current = sourceText[offset];
2720
+ text += current;
2721
+ offset += 1;
2722
+ if (current === '\n') {
2723
+ line += 1;
2724
+ column = 1;
2725
+ } else {
2726
+ column += 1;
2727
+ }
2728
+ if (current === '*' && sourceText[offset] === '/') {
2729
+ text += '/';
2730
+ offset += 1;
2731
+ column += 1;
2732
+ break;
2733
+ }
2734
+ }
2735
+ push(trivia, 'comment', text, start);
2736
+ continue;
2737
+ }
2738
+ if (char === '#' && isHashCommentLanguage(input.language)) {
2739
+ let text = '';
2740
+ while (offset < sourceText.length && sourceText[offset] !== '\n' && sourceText[offset] !== '\r') {
2741
+ text += sourceText[offset];
2742
+ offset += 1;
2743
+ column += 1;
2744
+ }
2745
+ push(trivia, preservedHashLineKind(text), text, start);
2746
+ continue;
2747
+ }
2748
+ if (char === '"' || char === '\'' || char === '`') {
2749
+ const quote = char;
2750
+ let text = char;
2751
+ offset += 1;
2752
+ column += 1;
2753
+ let escaped = false;
2754
+ while (offset < sourceText.length) {
2755
+ const current = sourceText[offset];
2756
+ text += current;
2757
+ offset += 1;
2758
+ if (current === '\n') {
2759
+ line += 1;
2760
+ column = 1;
2761
+ } else {
2762
+ column += 1;
2763
+ }
2764
+ if (escaped) {
2765
+ escaped = false;
2766
+ } else if (current === '\\') {
2767
+ escaped = true;
2768
+ } else if (current === quote) {
2769
+ break;
2770
+ }
2771
+ }
2772
+ push(tokens, 'string', text, start);
2773
+ continue;
2774
+ }
2775
+ if (/[0-9]/.test(char)) {
2776
+ let text = '';
2777
+ while (offset < sourceText.length && /[0-9a-fA-F_xXoObBeE.+-]/.test(sourceText[offset])) {
2778
+ text += sourceText[offset];
2779
+ offset += 1;
2780
+ column += 1;
2781
+ }
2782
+ push(tokens, 'number', text, start);
2783
+ continue;
2784
+ }
2785
+ if (isIdentifierStart(char)) {
2786
+ let text = '';
2787
+ while (offset < sourceText.length && isIdentifierPart(sourceText[offset])) {
2788
+ text += sourceText[offset];
2789
+ offset += 1;
2790
+ column += 1;
2791
+ }
2792
+ push(tokens, preservedKeywordSet.has(text) ? 'keyword' : 'identifier', text, start);
2793
+ continue;
2794
+ }
2795
+ let text = char;
2796
+ if (/[=+\-*/%&|^!<>?:.]/.test(char)) {
2797
+ while (offset + text.length < sourceText.length && /[=+\-*/%&|^!<>?:.]/.test(sourceText[offset + text.length])) text += sourceText[offset + text.length];
2798
+ offset += text.length;
2799
+ column += text.length;
2800
+ push(tokens, 'operator', text, start);
2801
+ } else {
2802
+ offset += 1;
2803
+ column += 1;
2804
+ push(tokens, /[()[\]{};,]/.test(char) ? 'punctuation' : 'unknown', text, start);
2805
+ }
2806
+ }
2807
+ return { tokens, trivia, truncated };
2808
+ }
2809
+
2810
+ function scanPreservedSourceDirectives(sourceText, input) {
2811
+ const directives = [];
2812
+ const maxDirectives = Number.isFinite(input.maxDirectives) ? Math.max(0, input.maxDirectives) : 20000;
2813
+ let truncated = false;
2814
+ let offset = 0;
2815
+ for (const { line, number } of sourceLines(sourceText)) {
2816
+ const trimmed = line.trim();
2817
+ const directiveKind = preservedDirectiveKind(trimmed, input.language);
2818
+ if (directiveKind) {
2819
+ if (directives.length >= maxDirectives) {
2820
+ truncated = true;
2821
+ offset += line.length + 1;
2822
+ continue;
2823
+ }
2824
+ const startColumn = Math.max(1, line.indexOf(trimmed) + 1);
2825
+ directives.push({
2826
+ id: `directive_${idFragment(input.sourcePath ?? input.language)}_${directives.length + 1}`,
2827
+ kind: directiveKind,
2828
+ text: trimmed,
2829
+ textHash: hashSemanticValue(trimmed),
2830
+ span: {
2831
+ sourceId: input.sourceHash,
2832
+ path: input.sourcePath,
2833
+ start: offset + startColumn - 1,
2834
+ end: offset + startColumn - 1 + trimmed.length,
2835
+ startLine: number,
2836
+ startColumn,
2837
+ endLine: number,
2838
+ endColumn: startColumn + trimmed.length
2839
+ },
2840
+ metadata: { language: input.language }
2841
+ });
2842
+ }
2843
+ offset += line.length + 1;
2844
+ }
2845
+ return { directives, truncated };
2846
+ }
2847
+
2848
+ function preservedSourceSegment(input) {
2849
+ const id = `${input.kind}_${input.index + 1}_${idFragment(input.start.offset)}`;
2850
+ return {
2851
+ id,
2852
+ kind: input.kind,
2853
+ text: input.text,
2854
+ textHash: hashSemanticValue(input.text),
2855
+ span: {
2856
+ sourceId: input.sourceHash,
2857
+ path: input.sourcePath,
2858
+ start: input.start.offset,
2859
+ end: input.end.offset,
2860
+ startLine: input.start.line,
2861
+ startColumn: input.start.column,
2862
+ endLine: input.end.line,
2863
+ endColumn: input.end.column
2864
+ }
2865
+ };
2866
+ }
2867
+
2868
+ function preservedDirectiveKind(trimmed, language) {
2869
+ if (!trimmed) return undefined;
2870
+ if (/^#\s*(include|define|if|ifdef|ifndef|elif|else|endif|pragma)\b/.test(trimmed)) return 'preprocessor';
2871
+ if (/^#!\s*/.test(trimmed)) return 'shebang';
2872
+ if (/^['"]use strict['"];?$/.test(trimmed)) return 'runtime-directive';
2873
+ if (/^(import|export|package|module|namespace|use|using|from|require)\b/.test(trimmed)) return 'module-directive';
2874
+ if (normalizeNativeLanguageId(language) === 'python' && /^from\s+\S+\s+import\b/.test(trimmed)) return 'module-directive';
2875
+ return undefined;
2876
+ }
2877
+
2878
+ function preservedHashLineKind(text) {
2879
+ return preservedDirectiveKind(String(text).trim(), 'c') ? 'directive' : 'comment';
2880
+ }
2881
+
2882
+ function isHashCommentLanguage(language) {
2883
+ return ['python', 'ruby', 'shell', 'bash', 'zsh', 'r', 'perl', 'yaml', 'toml'].includes(normalizeNativeLanguageId(language));
2884
+ }
2885
+
2886
+ function detectNewlineStyle(sourceText) {
2887
+ const crlf = (sourceText.match(/\r\n/g) ?? []).length;
2888
+ const normalized = sourceText.replace(/\r\n/g, '');
2889
+ const lf = (normalized.match(/\n/g) ?? []).length;
2890
+ const cr = (normalized.match(/\r/g) ?? []).length;
2891
+ const kinds = [crlf ? 'crlf' : undefined, lf ? 'lf' : undefined, cr ? 'cr' : undefined].filter(Boolean);
2892
+ if (!kinds.length) return 'none';
2893
+ if (kinds.length > 1 || cr) return 'mixed';
2894
+ return kinds[0];
2895
+ }
2896
+
2897
+ const preservedKeywordSet = new Set([
2898
+ 'abstract', 'as', 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'def', 'defer',
2899
+ 'do', 'else', 'enum', 'export', 'extends', 'extern', 'false', 'final', 'fn', 'for', 'from', 'func', 'function',
2900
+ 'if', 'impl', 'import', 'in', 'interface', 'let', 'match', 'mod', 'module', 'mut', 'namespace', 'new', 'nil',
2901
+ 'none', 'null', 'package', 'private', 'protected', 'pub', 'public', 'return', 'self', 'static', 'struct',
2902
+ 'switch', 'this', 'throw', 'trait', 'true', 'try', 'type', 'use', 'using', 'var', 'while', 'yield'
2903
+ ]);
2904
+
2905
+ function isIdentifierStart(char) {
2906
+ return /[A-Za-z_$]/.test(char ?? '');
2907
+ }
2908
+
2909
+ function isIdentifierPart(char) {
2910
+ return /[A-Za-z0-9_$]/.test(char ?? '');
2911
+ }
2912
+
2300
2913
  function sourceLines(sourceText) {
2301
2914
  return String(sourceText ?? '').split(/\r?\n/).map((line, index) => ({ line, number: index + 1 }));
2302
2915
  }
@@ -2636,11 +3249,13 @@ function nativeImportCategoryForLossKind(kind) {
2636
3249
  if (kind === 'preprocessor' || kind === 'conditionalCompilation' || kind === 'macroHygiene') return 'preprocessor';
2637
3250
  if (kind === 'metaprogramming' || kind === 'reflection' || kind === 'dynamicDispatch' || kind === 'dynamicRuntime') return 'metaprogramming';
2638
3251
  if (kind === 'generatedCode') return 'generatedCode';
2639
- if (kind === 'sourcePreservation' || kind === 'nonRoundTrippable') return 'sourcePreservation';
3252
+ if (kind === 'overloadResolution' || kind === 'typeInference') return 'overloadTypeInference';
3253
+ if (kind === 'sourcePreservation' || kind === 'commentsTrivia' || kind === 'nonRoundTrippable') return 'sourcePreservation';
2640
3254
  if (kind === 'parserDiagnostic') return 'parserDiagnostics';
2641
3255
  if (kind === 'unsupportedSyntax' || kind === 'unsupportedSemantic') return 'unsupportedSyntax';
2642
- if (kind === 'partialSemanticIndex') return 'partialSemanticIndex';
3256
+ if (kind === 'partialSemanticIndex' || kind === 'unverifiedNativeAst') return 'partialSemanticIndex';
2643
3257
  if (kind === 'sourceMapApproximation') return 'sourceMapApproximation';
3258
+ if (kind === 'targetProjectionLoss') return 'targetProjectionLoss';
2644
3259
  return String(kind ?? 'opaqueNative');
2645
3260
  }
2646
3261
 
@@ -2924,6 +3539,36 @@ function attachNativeImportLossSummary(evidence, lossSummary) {
2924
3539
  }));
2925
3540
  }
2926
3541
 
3542
+ function hasNativeExactAstEvidence(input, nativeAst, lightweight) {
3543
+ if (lightweight) return false;
3544
+ if (!(input?.nativeAst || input?.nodes)) return false;
3545
+ if (input.exactAst === true || input.metadata?.exactAst === true || input.nativeAstMetadata?.exactAst === true) return true;
3546
+ const coverage = input.metadata?.adapterCoverage
3547
+ ?? input.nativeAstMetadata?.adapterCoverage
3548
+ ?? input.nativeSourceMetadata?.adapterCoverage
3549
+ ?? nativeAst?.metadata?.adapterCoverage;
3550
+ if (coverage?.exactAst !== true) return false;
3551
+ const observedNodes = coverage.observed?.nativeAstNodes;
3552
+ return observedNodes === undefined || observedNodes > 0;
3553
+ }
3554
+
3555
+ function unverifiedNativeAstLosses(input, nativeAst, context) {
3556
+ if (context.lightweight || context.exactAst || context.hasLosses) return [];
3557
+ if (!(input?.nativeAst || input?.nodes)) return [];
3558
+ return [{
3559
+ id: `loss_${context.importIdPart}_unverified_native_ast`,
3560
+ severity: 'warning',
3561
+ kind: 'unverifiedNativeAst',
3562
+ nodeId: nativeAst?.rootId,
3563
+ message: 'Caller supplied native AST nodes without explicit exactAst or adapter coverage evidence.',
3564
+ metadata: {
3565
+ reason: 'missing-exact-ast-evidence',
3566
+ nativeAstId: nativeAst?.id,
3567
+ parser: nativeAst?.parser
3568
+ }
3569
+ }];
3570
+ }
3571
+
2927
3572
  function withNativeImportReadiness(importResult, lossSummary) {
2928
3573
  const mergeCandidates = (importResult.mergeCandidates ?? []).map((candidate) => {
2929
3574
  const readiness = maxSemanticMergeReadiness(candidate.readiness, lossSummary.semanticMergeReadiness);
@@ -2959,6 +3604,49 @@ function withNativeImportReadiness(importResult, lossSummary) {
2959
3604
  };
2960
3605
  }
2961
3606
 
3607
+ function nativeImportEntries(importResult) {
3608
+ if (Array.isArray(importResult?.imports)) return importResult.imports.filter(Boolean);
3609
+ return [importResult].filter(Boolean);
3610
+ }
3611
+
3612
+ function nativeImportHasExactAstCoverage(imported) {
3613
+ if (imported?.metadata?.nativeImportLossSummary?.exactAst === true) return true;
3614
+ if (imported?.adapter?.coverage?.exactAst === true && !(imported?.losses?.length)) return true;
3615
+ return false;
3616
+ }
3617
+
3618
+ function nativeImportRoundtripParser(importResult, imports) {
3619
+ const parsers = uniqueStrings([
3620
+ importResult.nativeAst?.parser,
3621
+ importResult.nativeSource?.parser,
3622
+ importResult.metadata?.parser,
3623
+ ...imports.map((imported) => imported?.nativeAst?.parser ?? imported?.nativeSource?.parser ?? imported?.metadata?.parser)
3624
+ ].filter(Boolean));
3625
+ return parsers.length === 1 ? parsers[0] : parsers.length ? parsers.join(',') : undefined;
3626
+ }
3627
+
3628
+ function nativeImportRoundtripReasons(status, input) {
3629
+ if (status === 'blocked') return uniqueStrings(input.blockingReasons);
3630
+ if (status === 'stub-only') {
3631
+ return uniqueStrings([
3632
+ `Native source projection emitted declaration stubs in ${input.projection.mode} mode.`,
3633
+ ...input.projectionReadiness.reasons,
3634
+ ...input.importReadiness.reasons.filter((reason) => input.importReadiness.readiness !== 'ready')
3635
+ ]);
3636
+ }
3637
+ if (status === 'needs-review') return uniqueStrings(input.reviewReasons);
3638
+ if (status === 'exact') {
3639
+ return ['Exact native AST import and verified preserved source projection are available.'];
3640
+ }
3641
+ if (status === 'preserved-source') {
3642
+ return uniqueStrings([
3643
+ 'Verified native source text is preserved for projection; semantic import evidence may still require review.',
3644
+ ...input.importReadiness.reasons.filter((reason) => input.importReadiness.readiness !== 'ready')
3645
+ ]);
3646
+ }
3647
+ return ['Native import roundtrip readiness requires review.'];
3648
+ }
3649
+
2962
3650
  function maxSemanticMergeReadiness(left, right) {
2963
3651
  const leftRank = semanticMergeReadinessRank[left] ?? semanticMergeReadinessRank['needs-review'];
2964
3652
  const rightRank = semanticMergeReadinessRank[right] ?? semanticMergeReadinessRank['needs-review'];
@@ -3004,6 +3692,20 @@ function createJavaScriptSyntaxImporterAdapter(options) {
3004
3692
  parser: options.parser,
3005
3693
  version: options.version,
3006
3694
  capabilities: uniqueStrings(['nativeAst', 'semanticIndex', 'sourceMaps', 'diagnostics', ...(options.capabilities ?? [])]),
3695
+ coverage: nativeImporterAdapterCoverage({
3696
+ exactness: 'exact-parser-ast',
3697
+ exactAst: true,
3698
+ tokens: false,
3699
+ trivia: false,
3700
+ diagnostics: true,
3701
+ sourceRanges: true,
3702
+ generatedRanges: false,
3703
+ semanticCoverage: declarationSemanticCoverage(),
3704
+ notes: [
3705
+ 'Normalizes a caller-owned ESTree/Babel-compatible AST into native AST nodes and declaration-level semantic index records.',
3706
+ 'The wrapper ignores parser token/trivia/comment arrays unless a host adapter explicitly maps them into preservation evidence.'
3707
+ ]
3708
+ }, options.coverage),
3007
3709
  supportedExtensions: options.supportedExtensions,
3008
3710
  diagnostics: options.diagnostics,
3009
3711
  parse(input) {
@@ -3454,6 +4156,14 @@ function createNativeProjectImportResult(input, imports) {
3454
4156
  mergeCandidates.push(...(result.mergeCandidates ?? []));
3455
4157
  operations.push(...(result.patch?.operations ?? []));
3456
4158
  }
4159
+ const uniqueLosses = uniqueByLossId(losses);
4160
+ const uniqueEvidence = uniqueByEvidenceId(evidence);
4161
+ const nativeImportLossSummary = summarizeNativeImportLosses(uniqueLosses, {
4162
+ evidence: uniqueEvidence,
4163
+ scanKind: 'native-project-import',
4164
+ semanticStatus: uniqueLosses.some((loss) => loss.severity === 'error') ? 'partial' : 'mapped'
4165
+ });
4166
+ const sourcePreservationSummary = summarizeProjectSourcePreservation(imports);
3457
4167
  const document = createDocument({
3458
4168
  id: input.documentId ?? `document_${idPart}`,
3459
4169
  name: input.documentName ?? input.name ?? 'NativeProject',
@@ -3464,6 +4174,8 @@ function createNativeProjectImportResult(input, imports) {
3464
4174
  semanticStatus: losses.some((loss) => loss.severity === 'error') ? 'partial' : 'mapped',
3465
4175
  projectRoot: input.projectRoot,
3466
4176
  sourceCount: imports.length,
4177
+ nativeImportLossSummary,
4178
+ sourcePreservationSummary,
3467
4179
  ...input.documentMetadata
3468
4180
  }
3469
4181
  });
@@ -3473,12 +4185,14 @@ function createNativeProjectImportResult(input, imports) {
3473
4185
  nativeSources,
3474
4186
  semanticIndex,
3475
4187
  sourceMaps,
3476
- losses: uniqueByLossId(losses),
3477
- evidence: uniqueByEvidenceId(evidence),
4188
+ losses: uniqueLosses,
4189
+ evidence: uniqueEvidence,
3478
4190
  metadata: {
3479
4191
  sourceLanguage: input.language ?? 'mixed',
3480
4192
  projectRoot: input.projectRoot,
3481
4193
  sourceCount: imports.length,
4194
+ nativeImportLossSummary,
4195
+ sourcePreservationSummary,
3482
4196
  ...input.universalAstMetadata
3483
4197
  }
3484
4198
  });
@@ -3487,12 +4201,14 @@ function createNativeProjectImportResult(input, imports) {
3487
4201
  author: input.author ?? '@shapeshift-labs/frontier-lang-compiler/importNativeProject',
3488
4202
  risk: losses.some((loss) => loss.severity === 'error') ? 'high' : losses.some((loss) => loss.severity === 'warning') ? 'medium' : 'low',
3489
4203
  operations,
3490
- evidence: uniqueByEvidenceId(evidence),
4204
+ evidence: uniqueEvidence,
3491
4205
  metadata: {
3492
4206
  semanticIndexId: semanticIndex?.id,
3493
4207
  universalAstId: universalAst.id,
3494
4208
  sourceMapIds: sourceMaps.map((sourceMap) => sourceMap.id),
3495
- sourceCount: imports.length
4209
+ sourceCount: imports.length,
4210
+ nativeImportLossSummary,
4211
+ sourcePreservationSummary
3496
4212
  }
3497
4213
  });
3498
4214
  return {
@@ -3508,17 +4224,35 @@ function createNativeProjectImportResult(input, imports) {
3508
4224
  semanticIndex,
3509
4225
  universalAst,
3510
4226
  sourceMaps,
3511
- losses: uniqueByLossId(losses),
3512
- evidence: uniqueByEvidenceId(evidence),
4227
+ losses: uniqueLosses,
4228
+ evidence: uniqueEvidence,
3513
4229
  mergeCandidates,
3514
4230
  metadata: {
3515
4231
  sourceCount: imports.length,
3516
4232
  sourcePaths: imports.map((result) => result.sourcePath).filter(Boolean),
4233
+ nativeImportLossSummary,
4234
+ sourcePreservationSummary,
3517
4235
  ...input.metadata
3518
4236
  }
3519
4237
  };
3520
4238
  }
3521
4239
 
4240
+ function summarizeProjectSourcePreservation(imports) {
4241
+ const records = imports
4242
+ .map((result) => result.metadata?.sourcePreservation ?? result.nativeSource?.metadata?.sourcePreservation ?? result.nativeAst?.metadata?.sourcePreservation)
4243
+ .filter(Boolean);
4244
+ return {
4245
+ total: records.length,
4246
+ exactSourceAvailable: records.filter((record) => record.summary?.exactSourceAvailable).length,
4247
+ sourceBytes: records.reduce((sum, record) => sum + (record.sourceBytes ?? 0), 0),
4248
+ tokens: records.reduce((sum, record) => sum + (record.summary?.tokens ?? record.tokens?.length ?? 0), 0),
4249
+ trivia: records.reduce((sum, record) => sum + (record.summary?.trivia ?? record.trivia?.length ?? 0), 0),
4250
+ directives: records.reduce((sum, record) => sum + (record.summary?.directives ?? record.directives?.length ?? 0), 0),
4251
+ truncated: records.some((record) => record.summary?.truncated === true),
4252
+ ids: records.map((record) => record.id).filter(Boolean)
4253
+ };
4254
+ }
4255
+
3522
4256
  function mergeSemanticIndexes(imports, input, idPart) {
3523
4257
  const indexes = imports.map((result) => result.semanticIndex ?? result.universalAst?.semanticIndex).filter(Boolean);
3524
4258
  if (!indexes.length) return undefined;
@@ -3564,9 +4298,15 @@ function normalizeNativeImporterAdapter(adapter) {
3564
4298
  parser: String(adapter.parser),
3565
4299
  version: adapter.version === undefined ? undefined : String(adapter.version)
3566
4300
  };
4301
+ const capabilities = normalizeStringList(adapter.capabilities);
3567
4302
  return Object.freeze({
3568
4303
  ...summaryInput,
3569
- capabilities: normalizeStringList(adapter.capabilities),
4304
+ capabilities,
4305
+ coverage: normalizeNativeImporterAdapterCoverage(adapter.coverage, {
4306
+ capabilities,
4307
+ language: adapter.language,
4308
+ parser: String(adapter.parser)
4309
+ }),
3570
4310
  supportedExtensions: normalizeStringList(adapter.supportedExtensions).map((extension) => extension.startsWith('.') ? extension.toLowerCase() : `.${extension.toLowerCase()}`),
3571
4311
  diagnostics: normalizeAdapterDiagnostics(adapter.diagnostics, summaryInput, {
3572
4312
  language: adapter.language,
@@ -3576,6 +4316,138 @@ function normalizeNativeImporterAdapter(adapter) {
3576
4316
  });
3577
4317
  }
3578
4318
 
4319
+ function nativeImporterAdapterCoverage(defaults = {}, overrides = {}) {
4320
+ return {
4321
+ ...defaults,
4322
+ ...overrides,
4323
+ semanticCoverage: {
4324
+ ...(defaults.semanticCoverage ?? {}),
4325
+ ...(overrides.semanticCoverage ?? {})
4326
+ },
4327
+ notes: uniqueStrings([...(defaults.notes ?? []), ...(overrides.notes ?? [])])
4328
+ };
4329
+ }
4330
+
4331
+ function normalizeNativeImporterAdapterCoverage(value = {}, context = {}) {
4332
+ const capabilities = new Set(normalizeStringList(context.capabilities).map((capability) => capability.toLowerCase()));
4333
+ const hasCapability = (...names) => names.some((name) => capabilities.has(String(name).toLowerCase()));
4334
+ const exactAst = Boolean(value.exactAst ?? hasCapability('exactAst', 'exactAstImport'));
4335
+ const sourceRanges = Boolean(value.sourceRanges ?? hasCapability('sourceRanges', 'sourceRange', 'ranges', 'sourceMaps'));
4336
+ const generatedRanges = Boolean(value.generatedRanges ?? hasCapability('generatedRanges', 'generatedRange', 'generatedSourceMaps'));
4337
+ const diagnostics = Boolean(value.diagnostics ?? hasCapability('diagnostics', 'parserDiagnostics'));
4338
+ return Object.freeze({
4339
+ exactness: String(value.exactness ?? inferredAdapterExactness(exactAst, capabilities)),
4340
+ exactAst,
4341
+ tokens: Boolean(value.tokens ?? hasCapability('tokens', 'tokenStream')),
4342
+ trivia: Boolean(value.trivia ?? hasCapability('trivia', 'comments', 'formatting')),
4343
+ diagnostics,
4344
+ sourceRanges,
4345
+ generatedRanges,
4346
+ semanticCoverage: normalizeNativeImporterSemanticCoverage(value.semanticCoverage, {
4347
+ capabilities,
4348
+ sourceRanges,
4349
+ generatedRanges
4350
+ }),
4351
+ notes: uniqueStrings(value.notes ?? inferredAdapterCoverageNotes(context, {
4352
+ exactAst,
4353
+ sourceRanges,
4354
+ generatedRanges,
4355
+ diagnostics
4356
+ }))
4357
+ });
4358
+ }
4359
+
4360
+ function observeNativeImporterAdapterCoverage(coverage, parseResult = {}, context = {}) {
4361
+ const nodes = parseResult.nativeAst?.nodes ?? parseResult.nodes ?? {};
4362
+ const nodeList = Object.values(nodes);
4363
+ const sourceMapMappings = parseResult.sourceMaps?.flatMap((sourceMap) => sourceMap.mappings ?? []) ?? parseResult.mappings ?? [];
4364
+ const semanticIndex = parseResult.semanticIndex;
4365
+ const semanticSymbols = semanticIndex?.symbols?.length ?? 0;
4366
+ const observedSourceRanges = nodeList.some((node) => Boolean(node?.span)) || sourceMapMappings.some((mapping) => Boolean(mapping?.sourceSpan));
4367
+ const observedGeneratedRanges = sourceMapMappings.some((mapping) => Boolean(mapping?.generatedSpan));
4368
+ const observedSemanticCoverage = normalizeNativeImporterSemanticCoverage({
4369
+ ...coverage.semanticCoverage,
4370
+ level: semanticSymbols
4371
+ ? maxSemanticCoverageLevel(coverage.semanticCoverage?.level, 'declaration-index')
4372
+ : coverage.semanticCoverage?.level,
4373
+ declarations: coverage.semanticCoverage?.declarations || semanticSymbols > 0,
4374
+ symbols: coverage.semanticCoverage?.symbols || semanticSymbols > 0
4375
+ }, {});
4376
+ return Object.freeze({
4377
+ ...coverage,
4378
+ diagnostics: coverage.diagnostics || (context.diagnostics?.length ?? 0) > 0,
4379
+ sourceRanges: coverage.sourceRanges || observedSourceRanges,
4380
+ generatedRanges: coverage.generatedRanges || observedGeneratedRanges,
4381
+ semanticCoverage: observedSemanticCoverage,
4382
+ observed: {
4383
+ diagnostics: context.diagnostics?.length ?? 0,
4384
+ losses: context.losses?.length ?? 0,
4385
+ nativeAstNodes: nodeList.length,
4386
+ semanticSymbols,
4387
+ sourceMapMappings: sourceMapMappings.length,
4388
+ sourceRanges: observedSourceRanges,
4389
+ generatedRanges: observedGeneratedRanges
4390
+ }
4391
+ });
4392
+ }
4393
+
4394
+ function declarationSemanticCoverage() {
4395
+ return {
4396
+ level: 'declaration-index',
4397
+ declarations: true,
4398
+ symbols: true,
4399
+ references: false,
4400
+ types: false,
4401
+ controlFlow: false
4402
+ };
4403
+ }
4404
+
4405
+ function normalizeNativeImporterSemanticCoverage(value = {}, context = {}) {
4406
+ const capabilities = context.capabilities ?? new Set();
4407
+ const hasCapability = (...names) => names.some((name) => capabilities.has(String(name).toLowerCase()));
4408
+ const declarations = Boolean(value.declarations ?? hasCapability('semanticIndex', 'declarations'));
4409
+ const symbols = Boolean(value.symbols ?? declarations);
4410
+ const references = Boolean(value.references ?? hasCapability('references', 'referenceIndex'));
4411
+ const types = Boolean(value.types ?? hasCapability('types', 'typeResolution', 'typeChecking'));
4412
+ const controlFlow = Boolean(value.controlFlow ?? hasCapability('controlFlow', 'cfg'));
4413
+ return Object.freeze({
4414
+ level: String(value.level ?? inferredSemanticCoverageLevel({ declarations, symbols, references, types, controlFlow })),
4415
+ declarations,
4416
+ symbols,
4417
+ references,
4418
+ types,
4419
+ controlFlow
4420
+ });
4421
+ }
4422
+
4423
+ function inferredAdapterExactness(exactAst, capabilities) {
4424
+ if (exactAst) return 'exact-parser-ast';
4425
+ if (capabilities.has('nativeast')) return 'adapter-reported-native-ast';
4426
+ return 'loss-aware-native-ast';
4427
+ }
4428
+
4429
+ function inferredSemanticCoverageLevel(input) {
4430
+ if (input.references || input.types || input.controlFlow) return 'semantic-index';
4431
+ if (input.declarations || input.symbols) return 'declaration-index';
4432
+ return 'native-ast';
4433
+ }
4434
+
4435
+ function maxSemanticCoverageLevel(left, right) {
4436
+ const ranks = { 'native-ast': 0, 'declaration-index': 1, 'semantic-index': 2 };
4437
+ const leftRank = ranks[left] ?? 0;
4438
+ const rightRank = ranks[right] ?? 0;
4439
+ return rightRank > leftRank ? right : left;
4440
+ }
4441
+
4442
+ function inferredAdapterCoverageNotes(context, coverage) {
4443
+ const notes = [];
4444
+ if (!coverage.exactAst) notes.push('Adapter did not declare exact parser AST/CST coverage; import readiness depends on losses and evidence.');
4445
+ if (!coverage.generatedRanges) notes.push('Adapter does not declare generated-range coverage unless parse output includes generated spans.');
4446
+ if (!coverage.diagnostics) notes.push('Adapter did not declare parser diagnostics support.');
4447
+ if (context.language && context.parser) notes.push(`Coverage summary applies to ${context.language} via ${context.parser}.`);
4448
+ return notes;
4449
+ }
4450
+
3579
4451
  function normalizeStringList(value) {
3580
4452
  if (value === undefined || value === null) return [];
3581
4453
  if (Array.isArray(value)) return value.map((item) => String(item)).filter(Boolean);
@@ -3669,6 +4541,7 @@ function adapterDiagnosticsEvidence(adapter, diagnostics, input) {
3669
4541
  parserVersion: input.parserVersion,
3670
4542
  sourceHash: input.sourceHash,
3671
4543
  capabilities: adapter.capabilities,
4544
+ coverage: adapter.coverage,
3672
4545
  supportedExtensions: adapter.supportedExtensions,
3673
4546
  diagnostics: diagnostics.map(serializableDiagnostic),
3674
4547
  errors,