@shapeshift-labs/frontier-lang-compiler 0.2.161 → 0.2.163

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 (30) hide show
  1. package/dist/internal/index-impl/moduleHostResourceImportMetadata.js +47 -0
  2. package/dist/internal/index-impl/projectSymbolGraphCompilerAdvancedTypeMetadata.js +215 -1
  3. package/dist/internal/index-impl/projectSymbolGraphCompilerFacts.js +1 -1
  4. package/dist/internal/index-impl/projectSymbolGraphCssModuleUtils.js +56 -1
  5. package/dist/internal/index-impl/projectSymbolGraphCssModules.js +48 -4
  6. package/dist/internal/index-impl/projectSymbolGraphJsxPropRecordFields.js +91 -0
  7. package/dist/internal/index-impl/projectSymbolGraphJsxPropValues.js +35 -7
  8. package/dist/internal/index-impl/projectSymbolGraphJsxRecords.js +4 -31
  9. package/dist/internal/index-impl/projectSymbolGraphJsxRenderReturnCollectionHelpers.js +201 -0
  10. package/dist/internal/index-impl/projectSymbolGraphJsxRenderReturnCollections.js +210 -0
  11. package/dist/internal/index-impl/projectSymbolGraphJsxRenderReturns.js +12 -5
  12. package/dist/internal/index-impl/projectSymbolGraphJsxSpreadPropValues.js +196 -0
  13. package/dist/internal/index-impl/projectSymbolGraphJsxStaticLiterals.js +207 -0
  14. package/dist/internal/index-impl/projectSymbolGraphModuleResolution.js +12 -14
  15. package/dist/internal/index-impl/projectSymbolGraphPackageConditions.js +33 -31
  16. package/dist/internal/index-impl/projectSymbolGraphPackageRuntimeConditions.js +22 -0
  17. package/dist/internal/index-impl/projectSymbolGraphScopeUseDefLexical.js +11 -19
  18. package/dist/internal/index-impl/syntaxModuleDeclarationEntries.js +27 -1
  19. package/dist/js-ts-safe-project-merge-admission.js +10 -0
  20. package/dist/js-ts-safe-project-merge-evidence-routing.js +30 -2
  21. package/dist/js-ts-safe-project-merge-graph-delta-compiler-conflicts.js +7 -0
  22. package/dist/js-ts-safe-project-merge-html-css-matrix.js +7 -2
  23. package/dist/js-ts-safe-project-merge-html-css-summary.js +110 -2
  24. package/dist/js-ts-safe-project-merge-html-css.js +137 -3
  25. package/dist/js-ts-safe-project-merge-jsx-graph-conflict-details.js +9 -0
  26. package/dist/js-ts-safe-project-merge.js +3 -0
  27. package/dist/native-source-preservation-scanner.js +10 -0
  28. package/dist/semantic-import-runtime-dynamic-import-evidence.js +141 -0
  29. package/dist/semantic-import-runtime-order-evidence.js +5 -4
  30. package/package.json +1 -1
@@ -8,6 +8,7 @@ import { nearestPublicOwnerForOffset } from './projectSymbolGraphScopeUseDefOwne
8
8
  import { readStaticMemberLiteral } from './staticMemberLiteral.js';
9
9
 
10
10
  const jsTsKeywords = new Set('abstract as async await break case catch class const continue debugger declare default delete do else enum export extends false finally for from function if implements import in infer instanceof interface keyof let module namespace new null of package private protected public readonly return satisfies static super switch this throw true try type typeof undefined unique unknown var void while with yield'.split(' '));
11
+ const namespaceComputedMemberDynamicUnsupportedReason = 'lexical-scope-namespace-computed-member-dynamic-unsupported', namespaceComputedMemberUnresolvedReason = 'lexical-scope-namespace-computed-member-unresolved';
11
12
 
12
13
  function lexicalScopeRecordsForImport(sourceText, context) {
13
14
  const masked = maskNonCode(sourceText);
@@ -230,15 +231,17 @@ function namespacePropertyAccess(code, token, binding, context) {
230
231
 
231
232
  function namespaceComputedPropertyAccess(sourceText, code, tokenStart, open) {
232
233
  const close = findMatchingBracket(code, open);
233
- if (close === -1) return blockedNamespaceComputedPropertyAccess();
234
+ if (close === -1) return blockedNamespaceComputedPropertyAccess(undefined, [namespaceComputedMemberUnresolvedReason]);
234
235
  const writeOperation = namespaceMemberWriteOperation(code, tokenStart, close + 1);
235
236
  let index = open + 1;
236
237
  while (index < close && /\s/.test(sourceText[index])) index += 1;
238
+ if (index >= close) return blockedNamespaceComputedPropertyAccess(writeOperation, [namespaceComputedMemberUnresolvedReason]);
237
239
  const literal = readStaticMemberLiteral(sourceText, index, close);
238
- if (!literal) return blockedNamespaceComputedPropertyAccess(writeOperation);
240
+ if (!literal) return blockedNamespaceComputedPropertyAccess(writeOperation, [namespaceComputedMemberDynamicUnsupportedReason]);
239
241
  let afterLiteral = literal.end + 1;
240
242
  while (afterLiteral < close && /\s/.test(sourceText[afterLiteral])) afterLiteral += 1;
241
- if (afterLiteral !== close || !identifierRegExp.test(literal.value)) return blockedNamespaceComputedPropertyAccess(writeOperation);
243
+ if (afterLiteral !== close) return blockedNamespaceComputedPropertyAccess(writeOperation, [namespaceComputedMemberDynamicUnsupportedReason]);
244
+ if (!identifierRegExp.test(literal.value)) return blockedNamespaceComputedPropertyAccess(writeOperation);
242
245
  if (writeOperation) {
243
246
  return blockedNamespaceMemberWrite('namespace-computed-property-write', {
244
247
  memberName: literal.value,
@@ -252,31 +255,18 @@ function namespaceComputedPropertyAccess(sourceText, code, tokenStart, open) {
252
255
  return { referenceKind: 'namespace-computed-property-read', memberName: literal.value, memberStart: literal.start, memberEnd: literal.end, memberComputed: true, memberLiteralKind: literal.literalKind };
253
256
  }
254
257
 
255
- function blockedNamespaceComputedPropertyAccess(writeOperation) {
258
+ function blockedNamespaceComputedPropertyAccess(writeOperation, reasonCodes = []) {
256
259
  return {
257
260
  referenceKind: writeOperation ? 'namespace-computed-property-write' : 'namespace-computed-property-read',
258
261
  memberComputed: true,
259
262
  writeOperation,
260
263
  status: 'blocked',
261
- reasonCodes: [
262
- LexicalUseDefReasonCodes.namespaceComputedMemberUnsupported,
263
- ...(writeOperation ? [LexicalUseDefReasonCodes.namespaceMemberWriteUnsupported] : [])
264
- ]
264
+ reasonCodes: uniqueReasonCodes([LexicalUseDefReasonCodes.namespaceComputedMemberUnsupported, ...reasonCodes, ...(writeOperation ? [LexicalUseDefReasonCodes.namespaceMemberWriteUnsupported] : [])])
265
265
  };
266
266
  }
267
267
 
268
268
  function blockedNamespaceMemberWrite(referenceKind, fields = {}) {
269
- return {
270
- referenceKind,
271
- memberName: fields.memberName,
272
- memberStart: fields.memberStart,
273
- memberEnd: fields.memberEnd,
274
- memberComputed: fields.memberComputed,
275
- memberLiteralKind: fields.memberLiteralKind,
276
- status: 'blocked',
277
- reasonCodes: [LexicalUseDefReasonCodes.namespaceMemberWriteUnsupported],
278
- writeOperation: fields.writeOperation
279
- };
269
+ return { referenceKind, memberName: fields.memberName, memberStart: fields.memberStart, memberEnd: fields.memberEnd, memberComputed: fields.memberComputed, memberLiteralKind: fields.memberLiteralKind, status: 'blocked', reasonCodes: [LexicalUseDefReasonCodes.namespaceMemberWriteUnsupported], writeOperation: fields.writeOperation };
280
270
  }
281
271
 
282
272
  function namespaceMemberWriteOperation(code, tokenStart, accessEnd) {
@@ -317,4 +307,6 @@ function findMatchingBracket(code, open) {
317
307
  return -1;
318
308
  }
319
309
 
310
+ function uniqueReasonCodes(reasonCodes) { return [...new Set(reasonCodes.filter(Boolean))]; }
311
+
320
312
  export { lexicalScopeRecordsForImport };
@@ -5,6 +5,7 @@ import {
5
5
  } from './syntaxCommonJsModuleDeclarationEntries.js';
6
6
  import { dynamicImportExpressionMetadata } from './dynamicImportExpressionMetadata.js';
7
7
  import { hostModuleDependencyMetadata } from './importMetaUrlDependencyMetadata.js';
8
+ import { moduleHostResourceImportMetadata } from './moduleHostResourceImportMetadata.js';
8
9
  import { moduleImportAttributeMetadata } from './moduleImportAttributeMetadata.js';
9
10
  import {
10
11
  declarationName,
@@ -38,10 +39,15 @@ function importDeclarationEntries(node, nativeNodeId, input) {
38
39
  if (!moduleSpecifier) return [];
39
40
  const typeOnly = node.importKind === 'type';
40
41
  const bindings = (node.specifiers ?? []).map((specifier) => importSpecifierBinding(specifier, typeOnly)).filter(Boolean);
42
+ const importKind = bindings.length === 0 ? 'side-effect' : 'module';
41
43
  return importModuleEntries(input, nativeNodeId, moduleSpecifier, bindings, {
42
44
  typeOnly,
43
45
  sideEffectOnly: bindings.length === 0,
44
- importKind: bindings.length === 0 ? 'side-effect' : 'module',
46
+ importKind,
47
+ ...moduleHostResourceImportMetadata(moduleSpecifier, {
48
+ hostDependencyBase: 'module-import',
49
+ expressionText: `import ${JSON.stringify(moduleSpecifier)}`
50
+ }),
45
51
  ...moduleImportAttributeMetadata(node)
46
52
  });
47
53
  }
@@ -54,6 +60,10 @@ function dynamicImportExpressionEntries(node, nativeNodeId, input) {
54
60
  importKind: 'dynamic-import',
55
61
  dynamicImport: true,
56
62
  ...dynamicImportExpressionMetadata(importArgument, moduleSpecifier),
63
+ ...moduleHostResourceImportMetadata(moduleSpecifier, {
64
+ hostDependencyBase: 'dynamic-import',
65
+ expressionText: `import(${JSON.stringify(moduleSpecifier)})`
66
+ }),
57
67
  ...moduleImportAttributeMetadata(node)
58
68
  });
59
69
  }
@@ -75,12 +85,20 @@ function exportNamedDeclarationEntries(node, nativeNodeId, input) {
75
85
  typeOnly,
76
86
  reexport: true,
77
87
  importKind: 'reexport',
88
+ ...moduleHostResourceImportMetadata(moduleSpecifier, {
89
+ hostDependencyBase: 'module-reexport',
90
+ expressionText: `export from ${JSON.stringify(moduleSpecifier)}`
91
+ }),
78
92
  ...moduleImportAttributeMetadata(node)
79
93
  }) : []),
80
94
  ...exportModuleEntries(input, nativeNodeId, moduleSpecifier, bindings, {
81
95
  typeOnly,
82
96
  reExport: Boolean(moduleSpecifier),
83
97
  exportKind: typeOnly ? 'type-named' : 'named',
98
+ ...moduleHostResourceImportMetadata(moduleSpecifier, {
99
+ hostDependencyBase: 'module-reexport',
100
+ expressionText: `export from ${JSON.stringify(moduleSpecifier)}`
101
+ }),
84
102
  ...moduleImportAttributeMetadata(node)
85
103
  })
86
104
  ];
@@ -107,6 +125,10 @@ function exportAllDeclarationEntries(node, nativeNodeId, input) {
107
125
  reexport: true,
108
126
  exportStar: !exportedName,
109
127
  importKind: exportedName ? 'namespace-reexport' : 'reexport',
128
+ ...moduleHostResourceImportMetadata(moduleSpecifier, {
129
+ hostDependencyBase: 'module-reexport',
130
+ expressionText: `export from ${JSON.stringify(moduleSpecifier)}`
131
+ }),
110
132
  ...moduleImportAttributeMetadata(node)
111
133
  }),
112
134
  ...exportModuleEntries(input, nativeNodeId, moduleSpecifier, bindings, {
@@ -114,6 +136,10 @@ function exportAllDeclarationEntries(node, nativeNodeId, input) {
114
136
  reExport: true,
115
137
  exportStar: !exportedName,
116
138
  exportKind: exportedName ? 'namespace-reexport' : 'export-star',
139
+ ...moduleHostResourceImportMetadata(moduleSpecifier, {
140
+ hostDependencyBase: 'module-reexport',
141
+ expressionText: `export from ${JSON.stringify(moduleSpecifier)}`
142
+ }),
117
143
  ...moduleImportAttributeMetadata(node)
118
144
  })
119
145
  ];
@@ -80,10 +80,18 @@ function exactBranchProjectSemanticEditAdmission(options) {
80
80
  if (requireOtherBranchUnchanged && !otherProjectBranchUnchanged(file, classification.branch, { requireBase: requireBaseForOtherBranchUnchanged })) return undefined;
81
81
  }
82
82
  const outputHash = hashProjectSourceText(branchText);
83
+ const admissionOutcome = 'safe';
84
+ const admissionOutcomeReasonCode = exactMergedOutput && allowExistingExactOutput
85
+ ? 'existing-exact-branch-output'
86
+ : typeof branchText === 'string'
87
+ ? 'exact-branch-output-other-branch-unchanged'
88
+ : 'exact-branch-deletion-other-branch-unchanged';
83
89
  const admission = {
84
90
  id: safeProjectEvidenceId(`${classification.details?.conflictKey ?? classification.code}_${file.sourcePath}`),
85
91
  kind: admissionKind,
86
92
  status: 'passed',
93
+ admissionOutcome,
94
+ admissionOutcomeReasonCode,
87
95
  branch: classification.branch,
88
96
  ...(admissionFields(classification) ?? {}),
89
97
  sourcePath: file.sourcePath,
@@ -94,6 +102,8 @@ function exactBranchProjectSemanticEditAdmission(options) {
94
102
  ...(details(classification) ?? {}),
95
103
  sourcePath: file.sourcePath,
96
104
  outputHash,
105
+ admissionOutcome,
106
+ admissionOutcomeReasonCode,
97
107
  exactBranchOutput: true,
98
108
  deletedOutput: typeof branchText !== 'string' || undefined,
99
109
  autoMergeClaim: false,
@@ -21,7 +21,7 @@ function fileAdmissionEvidenceRecords(files = []) {
21
21
  return files.flatMap((file) => [
22
22
  ...recordArraysWithSuffix(file.summary, 'AdmissionEvidence'),
23
23
  ...recordArraysWithSuffix(file.metadata, 'Admissions')
24
- ]);
24
+ ].map(admissionEvidenceRecordWithOutcome));
25
25
  }
26
26
 
27
27
  function recordArraysWithSuffix(record, suffix) {
@@ -32,7 +32,35 @@ function recordArraysWithSuffix(record, suffix) {
32
32
  .filter((value) => value?.kind && value?.id);
33
33
  }
34
34
 
35
+ function admissionEvidenceRecordWithOutcome(record) {
36
+ const admissionOutcome = admissionEvidenceOutcome(record);
37
+ return compactRecord({
38
+ ...record,
39
+ admissionOutcome,
40
+ admissionOutcomeReasonCode: admissionEvidenceOutcomeReasonCode(record, admissionOutcome)
41
+ });
42
+ }
43
+
44
+ function admissionEvidenceOutcome(record) {
45
+ if (['safe', 'review', 'blocked'].includes(record?.admissionOutcome)) return record.admissionOutcome;
46
+ const routeStatus = record?.admissionRoute?.status;
47
+ if (record?.status === 'passed' && (!routeStatus || routeStatus === 'passed')) return 'safe';
48
+ if (record?.status === 'failed' || routeStatus === 'failed' || routeStatus === 'blocked') return 'blocked';
49
+ return 'review';
50
+ }
51
+
52
+ function admissionEvidenceOutcomeReasonCode(record, admissionOutcome) {
53
+ if (typeof record?.admissionOutcomeReasonCode === 'string' && record.admissionOutcomeReasonCode.length > 0) return record.admissionOutcomeReasonCode;
54
+ const routeReasonCode = record?.admissionRoute?.reasonCodes?.[0];
55
+ if (typeof record?.details?.reasonCode === 'string' && record.details.reasonCode.length > 0) return record.details.reasonCode;
56
+ if (typeof routeReasonCode === 'string' && routeReasonCode.length > 0) return routeReasonCode;
57
+ if (admissionOutcome === 'safe' && record?.details?.exactBranchOutput === true) return 'passed-exact-branch-output';
58
+ if (admissionOutcome === 'safe') return 'passed-admission-evidence';
59
+ if (admissionOutcome === 'blocked') return 'failed-admission-evidence';
60
+ return 'review-admission-evidence';
61
+ }
62
+
35
63
  function isPlainObject(value) { return Boolean(value && typeof value === 'object' && !Array.isArray(value)); }
36
64
  function compactRecord(record) { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)); }
37
65
 
38
- export { failedEvidenceMissingItems, fileAdmissionEvidenceRecords };
66
+ export { admissionEvidenceOutcome, failedEvidenceMissingItems, fileAdmissionEvidenceRecords };
@@ -117,6 +117,8 @@ function missingSharedCompilerTypeEquivalenceEvidence(workerRecord, headRecord)
117
117
  enumMemberCount: record.enumMemberCount,
118
118
  enumComputedMemberCount: record.enumComputedMemberCount,
119
119
  typeEquivalenceEnumRuntimeShapeHash: record.typeEquivalenceEnumRuntimeShapeHash,
120
+ advancedTypeProofRequirement: record.advancedTypeProofRequirement,
121
+ advancedTypeMissingProof: record.advancedTypeMissingProof,
120
122
  typeEquivalenceSignatureSetHash: record.typeEquivalenceSignatureSetHash,
121
123
  typeEquivalenceCallSignatureSetHash: record.typeEquivalenceCallSignatureSetHash, typeEquivalenceConstructSignatureSetHash: record.typeEquivalenceConstructSignatureSetHash,
122
124
  typeEquivalenceTypeParameterSetHash: record.typeEquivalenceTypeParameterSetHash,
@@ -169,9 +171,14 @@ function hasPassedCompilerTypeEquivalenceProof(record) {
169
171
  ];
170
172
  return Boolean(record?.typeEquivalenceStatus === 'compiler-backed-equivalent'
171
173
  && record?.typeEquivalenceProof?.status === 'passed'
174
+ && !hasMissingAdvancedTypeProof(record)
172
175
  && requiredHashes.every(([required, hash]) => !required || hash)
173
176
  && record?.apiSignatureHash);
174
177
  }
178
+ function hasMissingAdvancedTypeProof(record) {
179
+ const status = record?.advancedTypeMissingProof?.status ?? record?.advancedTypeProofRequirement?.status;
180
+ return status === 'missing-compiler-evidence' || status === 'requires-review';
181
+ }
175
182
  function recordsByIdentityKey(records, identityKey) { const result = new Map(); for (const record of records ?? []) { const key = identityKey(record); if (!key || result.has(key)) continue; result.set(key, record); } return result; }
176
183
  function optionalFingerprint(record, fingerprint) { return record ? fingerprint(record) : undefined; }
177
184
  function stableKey(parts) { const values = parts.map((part) => part === undefined || part === null ? '' : String(part)); return values.some(Boolean) ? values.join('#') : undefined; }
@@ -5,6 +5,7 @@ const HtmlCssProjectMergeMissingSignals = Object.freeze({
5
5
  cssSelectorTargetEvidence: 'css-selector-target-evidence-missing',
6
6
  htmlStructuralMerge: 'html-structural-merge-proof-blocked',
7
7
  cssCascadeMerge: 'css-cascade-merge-proof-blocked',
8
+ cssDependencyGraphEvidence: 'css-dependency-graph-evidence-missing',
8
9
  htmlCssBrowserRuntimeProof: 'html-css-browser-runtime-proof-not-available'
9
10
  });
10
11
 
@@ -16,6 +17,7 @@ function htmlCssProjectMergeMissingEvidenceRoutes(route, signals) {
16
17
  [signals.cssSelectorTargetEvidence]: route('prove-css-selector-target-evidence', 'layout-style-targets', 'supply-selector-target-graph-and-rebase-evidence'),
17
18
  [signals.htmlStructuralMerge]: route('admit-html-structural-merge', 'layout-markup-graph', 'supply-html-parser-reference-and-boundary-evidence'),
18
19
  [signals.cssCascadeMerge]: route('admit-css-cascade-merge', 'layout-style-graph', 'supply-css-parser-cascade-and-scope-evidence'),
20
+ [signals.cssDependencyGraphEvidence]: route('prove-css-dependency-graph', 'layout-style-graph', 'supply-css-custom-property-animation-font-and-asset-dependency-graph'),
19
21
  [signals.htmlCssBrowserRuntimeProof]: route('prove-html-css-browser-runtime', 'browser-proof', 'attach-browser-runtime-proof-bundle')
20
22
  };
21
23
  }
@@ -28,6 +30,7 @@ function htmlCssProjectMergeAdmissionMatrixRows(matrixRow, signals) {
28
30
  matrixRow('css-selector-target-evidence', 'bounded-evidence', ['css-selector-target-evidence'], [signals.cssSelectorTargetEvidence]),
29
31
  matrixRow('html-structural-merge-admission', 'partial', ['html-structural-merge'], [signals.htmlStructuralMerge]),
30
32
  matrixRow('css-cascade-merge-admission', 'partial', ['css-cascade-merge'], [signals.cssCascadeMerge]),
33
+ matrixRow('css-dependency-graph-evidence', 'bounded-evidence', ['css-dependency-graph'], [signals.cssDependencyGraphEvidence]),
31
34
  matrixRow('html-css-browser-runtime-proof', 'bounded-evidence', ['browser-runtime-proof'], [signals.htmlCssBrowserRuntimeProof])
32
35
  ];
33
36
  }
@@ -36,10 +39,11 @@ function htmlCssProjectMergeMissingEvidenceItems(summary, signals, missingEviden
36
39
  const items = [];
37
40
  if (summary.htmlFiles && summary.htmlParserEvidenceFiles !== summary.htmlFiles) items.push(missingEvidenceItem({ code: signals.htmlParserEvidence, scope: 'layout-markup-parser', kind: 'html-parser-source-evidence', proofLevel: 'html-parser-source-evidence', action: 'review', summary: `HTML project merge has parser/source evidence for ${summary.htmlParserEvidenceFiles}/${summary.htmlFiles} file(s); require parse5 source spans, zero parse errors, and attribute/trivia spans when those surfaces are present before parser evidence admission.` }));
38
41
  if (summary.cssFiles && summary.cssParserEvidenceFiles !== summary.cssFiles) items.push(missingEvidenceItem({ code: signals.cssParserEvidence, scope: 'layout-style-parser', kind: 'css-parser-source-evidence', proofLevel: 'css-parser-source-evidence', action: 'review', summary: `CSS project merge has parser/source evidence for ${summary.cssParserEvidenceFiles}/${summary.cssFiles} file(s); require PostCSS rule/declaration spans, raw trivia hashes, and zero parse errors before parser evidence admission.` }));
39
- if (summary.htmlFiles && summary.htmlIdentityEvidenceFiles !== summary.htmlFiles) items.push(missingEvidenceItem({ code: signals.htmlIdentityEvidence, scope: 'layout-markup-identity', kind: 'html-identity-evidence', proofLevel: 'html-identity-evidence', action: 'review', summary: `HTML project merge has structural identity evidence for ${summary.htmlIdentityEvidenceFiles}/${summary.htmlFiles} file(s); require parser-backed structural spans and stable explicit/path identity accounting before structural admission.` }));
42
+ if (summary.htmlFiles && summary.htmlIdentityEvidenceFiles !== summary.htmlFiles) items.push(missingEvidenceItem({ code: signals.htmlIdentityEvidence, scope: 'layout-markup-identity', kind: 'html-identity-evidence', proofLevel: 'html-identity-evidence', action: 'review', summary: `HTML project merge has structural identity evidence for ${summary.htmlIdentityEvidenceFiles}/${summary.htmlFiles} file(s) with ${summary.htmlDuplicateIdentityEvidenceFiles ?? 0} duplicate identity evidence file(s); require parser-backed structural spans, unique explicit identity keys, and stable explicit/path identity accounting before structural admission.` }));
40
43
  if (summary.cssFiles && (summary.cssSelectorTargetEvidenceFiles !== summary.cssFiles || summary.cssSelectorTargetConflictFiles)) items.push(missingEvidenceItem({ code: signals.cssSelectorTargetEvidence, scope: 'layout-style-targets', kind: 'css-selector-target-evidence', proofLevel: 'css-selector-target-evidence', action: 'review', summary: `CSS project merge has selector-target evidence for ${summary.cssSelectorTargetEvidenceFiles}/${summary.cssFiles} file(s) with ${summary.cssSelectorTargetConflictFiles} selector target conflict(s); require selector target graph and proven rebase before target-moving merges.` }));
41
44
  if (summary.htmlBlockedFiles) items.push(missingEvidenceItem({ code: signals.htmlStructuralMerge, scope: 'layout-markup-graph', kind: 'html-structural-merge-proof', proofLevel: 'html-structural-merge', action: 'review', summary: `HTML project merge has ${summary.htmlBlockedFiles} blocked file(s); supply parser/source-span evidence, stable element identity, and runtime-boundary proof before admission.` }));
42
- if (summary.cssBlockedFiles) items.push(missingEvidenceItem({ code: signals.cssCascadeMerge, scope: 'layout-style-graph', kind: 'css-cascade-merge-proof', proofLevel: 'css-cascade-merge', action: 'review', summary: `CSS project merge has ${summary.cssBlockedFiles} blocked file(s); supply parser/cascade/scope evidence and keep browser claims false until runtime proof passes.` }));
45
+ if (summary.cssBlockedFiles) items.push(missingEvidenceItem({ code: signals.cssCascadeMerge, scope: 'layout-style-graph', kind: 'css-cascade-merge-proof', proofLevel: 'css-cascade-merge', action: 'review', summary: `CSS project merge has ${summary.cssBlockedFiles} blocked file(s), including ${summary.cssScopedCascadeBlockedFiles} scoped cascade proof block(s); supply parser/cascade/scope evidence, require scopedCascadeGraphHash for changed declarations under @media/@supports/@container/@layer/@scope or nested scopes, and keep browser claims false until runtime proof passes.` }));
46
+ if (summary.cssDependencySurfaceFiles && (summary.cssDependencyGraphEvidenceFiles !== summary.cssDependencySurfaceFiles || summary.cssDependencyGraphMissingProofFiles || summary.cssDependencyGraphBlockedFiles)) items.push(missingEvidenceItem({ code: signals.cssDependencyGraphEvidence, scope: 'layout-style-graph', kind: 'css-dependency-graph-evidence', proofLevel: 'css-dependency-graph', action: 'review', summary: `CSS project merge has dependency graph evidence for ${summary.cssDependencyGraphEvidenceFiles}/${summary.cssDependencySurfaceFiles} dependency-surface file(s), ${summary.cssDependencyGraphMissingProofFiles} missing dependency graph proof(s), and ${summary.cssDependencyGraphBlockedFiles} dependency blocker(s); require custom property, var() fallback, @keyframes/animation-name, @font-face, and url() asset graph evidence before cascade/browser equivalence claims.`, suggestedInput: { cssDependencyGraphEvidence: true } }));
43
47
  if (summary.htmlCssMergedFiles && !summary.htmlCssBrowserRuntimeProofs) items.push(missingEvidenceItem({ code: signals.htmlCssBrowserRuntimeProof, scope: 'browser-proof', kind: 'browser-runtime-proof', proofLevel: 'browser-runtime-proof', action: 'review', summary: 'HTML/CSS structural source merge was available, but browser DOM/cascade/layout/runtime proof was not attached; keep browser equivalence claims false.', suggestedInput: { browserRuntimeProof: true } }));
44
48
  return items;
45
49
  }
@@ -51,6 +55,7 @@ function htmlCssProjectMergeMatrixProofStatus(level, summary) {
51
55
  if (level === 'css-selector-target-evidence') return summary.cssFiles ? (summary.cssSelectorTargetConflictFiles ? 'failed' : summary.cssSelectorTargetEvidenceFiles === summary.cssFiles ? 'passed' : 'missing') : 'absent';
52
56
  if (level === 'html-structural-merge') return summary.htmlFiles ? (summary.htmlBlockedFiles ? 'failed' : summary.htmlMergedFiles ? 'passed' : 'missing') : 'absent';
53
57
  if (level === 'css-cascade-merge') return summary.cssFiles ? (summary.cssBlockedFiles ? 'failed' : summary.cssMergedFiles ? 'passed' : 'missing') : 'absent';
58
+ if (level === 'css-dependency-graph') return summary.cssDependencySurfaceFiles ? (summary.cssDependencyGraphBlockedFiles ? 'failed' : summary.cssDependencyGraphEvidenceFiles === summary.cssDependencySurfaceFiles ? 'passed' : 'missing') : 'absent';
54
59
  if (level === 'browser-runtime-proof') return summary.htmlCssFiles ? (summary.htmlCssBrowserRuntimeProofs ? 'passed' : 'missing') : 'absent';
55
60
  return undefined;
56
61
  }
@@ -7,8 +7,13 @@ function htmlCssProjectSummary(files) {
7
7
  htmlParserEvidenceFiles: htmlFiles.filter(hasHtmlParserEvidence).length, cssParserEvidenceFiles: cssFiles.filter(hasCssParserEvidence).length, htmlCssParserEvidenceFiles: htmlCssFiles.filter((file) => hasHtmlParserEvidence(file) || hasCssParserEvidence(file)).length,
8
8
  htmlParserEvidenceFailedFiles: htmlFiles.filter(hasParserEvidenceFailure).length, cssParserEvidenceFailedFiles: cssFiles.filter(hasParserEvidenceFailure).length, htmlCssParserEvidenceFailedFiles: htmlCssFiles.filter(hasParserEvidenceFailure).length,
9
9
  htmlIdentityEvidenceFiles: htmlFiles.filter(hasHtmlIdentityEvidence).length, cssSelectorTargetEvidenceFiles: cssFiles.filter(hasCssSelectorTargetEvidence).length, htmlCssStructuralTargetEvidenceFiles: htmlCssFiles.filter((file) => hasHtmlIdentityEvidence(file) || hasCssSelectorTargetEvidence(file)).length,
10
+ cssSelectorTargetGraphEvidenceFiles: cssFiles.filter(hasCssSelectorTargetGraphEvidence).length, cssSelectorSpecificityEvidenceFiles: cssFiles.filter(hasCssSelectorSpecificityEvidence).length, cssSelectorTargetMoveFiles: cssFiles.filter(hasCssSelectorTargetMove).length,
11
+ htmlExplicitIdentityEvidenceFiles: htmlFiles.filter(hasHtmlExplicitIdentityEvidence).length, htmlPathOnlyIdentityResidualFiles: htmlFiles.filter(hasHtmlPathOnlyIdentityResidual).length, htmlDuplicateIdentityEvidenceFiles: htmlFiles.filter(hasHtmlDuplicateIdentityEvidence).length, htmlDuplicateIdentityKeys: htmlFiles.reduce((sum, file) => sum + htmlDuplicateIdentityKeyCount(file), 0),
12
+ htmlRuntimeBoundaryEvidenceFiles: htmlFiles.filter(hasHtmlRuntimeBoundaryEvidence).length, htmlFrameworkBoundaryEvidenceFiles: htmlFiles.filter(hasHtmlFrameworkBoundaryEvidence).length, htmlProofGapBlockedFiles: htmlFiles.filter(hasHtmlProofGapBlockedConflict).length,
10
13
  htmlIdentityEvidenceFailedFiles: htmlFiles.filter(hasHtmlIdentityEvidenceFailure).length, cssSelectorTargetConflictFiles: cssFiles.filter(hasCssSelectorTargetConflict).length, htmlCssStructuralTargetEvidenceFailedFiles: htmlCssFiles.filter((file) => hasHtmlIdentityEvidenceFailure(file) || hasCssSelectorTargetConflict(file)).length,
11
14
  cssSelectorTargetRebasedFiles: cssFiles.filter(hasCssSelectorTargetRebase).length,
15
+ cssScopedCascadeFiles: cssFiles.filter(hasCssScopedCascadeScope).length, cssScopedCascadeEvidenceFiles: cssFiles.filter(hasCssScopedCascadeEvidence).length, cssScopedCascadeBlockedFiles: cssFiles.filter(hasCssScopedCascadeMissingProof).length,
16
+ cssDependencySurfaceFiles: cssFiles.filter(hasCssDependencySurface).length, cssDependencyGraphEvidenceFiles: cssFiles.filter((file) => hasCssDependencySurface(file) && hasCssDependencyGraphEvidence(file)).length, cssDependencyGraphMissingProofFiles: cssFiles.filter(hasCssDependencyGraphMissingProof).length, cssDependencyGraphBlockedFiles: cssFiles.filter(hasCssDependencyGraphBlockedConflict).length,
12
17
  htmlCssBrowserRuntimeProofs: htmlCssFiles.filter(hasBrowserRuntimeProof).length
13
18
  };
14
19
  }
@@ -29,23 +34,126 @@ function hasCssParserEvidence(file) {
29
34
  function hasParserEvidenceFailure(file) { return (file?.result?.parserEvidence?.parseErrors ?? 0) > 0; }
30
35
  function hasHtmlIdentityEvidence(file) {
31
36
  const evidence = file?.result?.identityEvidence;
32
- return evidence?.parserBackedStructuralSpans === true && evidence.structuralAddressability === true;
37
+ return evidence?.parserBackedStructuralSpans === true && evidence.structuralAddressability === true && !hasDuplicateExplicitIdentityKeys(evidence);
38
+ }
39
+ function hasHtmlExplicitIdentityEvidence(file) {
40
+ const evidence = file?.result?.identityEvidence;
41
+ return hasHtmlIdentityEvidence(file) && evidence.explicitIdentityAvailable === true;
42
+ }
43
+ function hasHtmlPathOnlyIdentityResidual(file) { return (file?.result?.identityEvidence?.pathOnlyIdentityElements ?? 0) > 0; }
44
+ function hasHtmlRuntimeBoundaryEvidence(file) {
45
+ return (file?.result?.identityEvidence?.runtimeBoundaryElements ?? 0) > 0 || hasRuntimeBoundaryConflict(file);
46
+ }
47
+ function hasHtmlFrameworkBoundaryEvidence(file) {
48
+ return (file?.result?.identityEvidence?.frameworkBoundaryElements ?? 0) > 0 || hasFrameworkBoundaryConflict(file);
33
49
  }
34
50
  function hasHtmlIdentityEvidenceFailure(file) {
35
51
  const evidence = file?.result?.identityEvidence;
36
- return Boolean(evidence) && (evidence.parserBackedStructuralSpans !== true || evidence.structuralAddressability !== true);
52
+ return Boolean(evidence) && (evidence.parserBackedStructuralSpans !== true || evidence.structuralAddressability !== true || hasDuplicateExplicitIdentityKeys(evidence));
37
53
  }
38
54
  function hasCssSelectorTargetEvidence(file) {
39
55
  const evidence = file?.result?.selectorTargetEvidence;
40
56
  return evidence?.parserBackedRuleSpans === true;
41
57
  }
58
+ function hasCssSelectorTargetGraphEvidence(file) {
59
+ return file?.result?.selectorTargetEvidence?.selectorTargetGraphHashPresent === true;
60
+ }
61
+ function hasCssSelectorSpecificityEvidence(file) {
62
+ const sides = Object.values(file?.result?.selectorTargetEvidence?.sides ?? {});
63
+ return sides.length > 0 && sides.every((side) => (side?.ruleCount ?? 0) === 0 || (side?.selectorSpecificityRecords ?? 0) >= side.ruleCount);
64
+ }
65
+ function hasCssSelectorTargetMove(file) {
66
+ return (file?.result?.selectorTargetEvidence?.selectorMoveCount ?? 0) > 0;
67
+ }
42
68
  function hasCssSelectorTargetConflict(file) {
43
69
  return (file?.result?.conflicts ?? file?.conflicts ?? []).some((conflict) => conflict.code === 'css-selector-target-conflict');
44
70
  }
71
+ function hasHtmlProofGapBlockedConflict(file) {
72
+ return (file?.result?.conflicts ?? file?.conflicts ?? []).some((conflict) => conflict.code === 'html-proof-gap-blocked');
73
+ }
74
+ function hasRuntimeBoundaryConflict(file) {
75
+ return (file?.result?.conflicts ?? file?.conflicts ?? []).some((conflict) => HtmlRuntimeBoundaryReasonCodes.has(conflict?.details?.reasonCode));
76
+ }
77
+ function hasFrameworkBoundaryConflict(file) {
78
+ return (file?.result?.conflicts ?? file?.conflicts ?? []).some((conflict) => HtmlFrameworkBoundaryReasonCodes.has(conflict?.details?.reasonCode));
79
+ }
45
80
  function hasCssSelectorTargetRebase(file) { return (file?.result?.selectorTargetEvidence?.rebasedChangeCount ?? 0) > 0; }
81
+ function hasCssScopedCascadeScope(file) {
82
+ return cssScopedRuleCount(file) > 0 || hasCssScopedCascadeProofReason(file) || file?.result?.parserEvidence?.scopedCascadeGraphHashPresent === false;
83
+ }
84
+ function hasCssScopedCascadeEvidence(file) {
85
+ return hasCssScopedCascadeScope(file) && !hasCssScopedCascadeMissingProof(file) && file?.result?.parserEvidence?.scopedCascadeGraphHashPresent === true;
86
+ }
87
+ function hasCssScopedCascadeMissingProof(file) {
88
+ return hasCssScopedCascadeProofReason(file);
89
+ }
90
+ function hasCssScopedCascadeProofReason(file) {
91
+ return (file?.result?.conflicts ?? file?.conflicts ?? []).some((conflict) => ScopedCascadeMissingProofReasonCodes.has(conflict?.details?.reasonCode));
92
+ }
93
+ function cssScopedRuleCount(file) {
94
+ const sides = Object.values(file?.result?.selectorTargetEvidence?.sides ?? {});
95
+ return sides.reduce((count, side) => Math.max(count, side?.scopedRuleCount ?? 0), 0);
96
+ }
97
+ function hasCssDependencySurface(file) {
98
+ return hasCssDependencyEvidenceSurface(file) || hasCssDependencyTextSurface(file) || hasCssDependencyConflictSurface(file);
99
+ }
100
+ function hasCssDependencyGraphEvidence(file) {
101
+ return cssDependencyEvidenceRecords(file).some((evidence) => evidence.dependencyGraphHashPresent === true || evidence.cssDependencyGraphHashPresent === true || typeof evidence.dependencyGraphHash === 'string' || typeof evidence.cssDependencyGraphHash === 'string' || typeof evidence.graphHash === 'string' || typeof evidence.customPropertyGraphHash === 'string' || typeof evidence.animationGraphHash === 'string' || typeof evidence.fontFaceGraphHash === 'string' || typeof evidence.assetGraphHash === 'string');
102
+ }
103
+ function hasCssDependencyGraphMissingProof(file) { return hasCssDependencySurface(file) && !hasCssDependencyGraphEvidence(file); }
104
+ function hasCssDependencyEvidenceSurface(file) {
105
+ return cssDependencyEvidenceRecords(file).some((evidence) => evidence.hasDependencySurface === true || (evidence.dependencySurfaceCount ?? 0) > 0 || (evidence.customPropertyDefinitions ?? 0) > 0 || (evidence.customPropertyReferences ?? 0) > 0 || (evidence.varReferences ?? 0) > 0 || (evidence.varFallbackReferences ?? 0) > 0 || (evidence.animationNameLinks ?? 0) > 0 || (evidence.keyframeLinks ?? 0) > 0 || (evidence.fontFaceLinks ?? 0) > 0 || (evidence.urlAssetReferences ?? 0) > 0);
106
+ }
107
+ function hasCssDependencyTextSurface(file) {
108
+ const sourceText = cssDependencySourceText(file);
109
+ return sourceText.length > 0 && CssDependencySurfacePatterns.some((pattern) => pattern.test(sourceText));
110
+ }
111
+ function hasCssDependencyConflictSurface(file) {
112
+ return cssFileConflicts(file).some(isCssDependencyConflict);
113
+ }
114
+ function hasCssDependencyGraphBlockedConflict(file) {
115
+ return cssFileConflicts(file).some((conflict) => isCssDependencyConflict(conflict) || CssDependencyMissingProofReasonCodes.has(conflict?.details?.reasonCode));
116
+ }
117
+ function cssDependencyEvidenceRecords(file) {
118
+ const result = file?.result ?? {};
119
+ return [result.dependencyEvidence, result.cssDependencyEvidence, result.dependencyGraphEvidence, result.cssDependencyGraphEvidence, result.parserEvidence?.dependencyEvidence, result.parserEvidence?.cssDependencyEvidence, result.parserEvidence?.dependencyGraphEvidence, result.parserEvidence?.cssDependencyGraphEvidence].filter(isPlainObject);
120
+ }
121
+ function cssDependencySourceText(file) {
122
+ return [file?.outputSourceText, file?.sourceText, file?.result?.mergedSourceText].filter((value) => typeof value === 'string').join('\n');
123
+ }
124
+ function cssFileConflicts(file) { return file?.result?.conflicts ?? file?.conflicts ?? []; }
125
+ function isCssDependencyConflict(conflict) {
126
+ const details = conflict?.details ?? {};
127
+ const codes = [conflict?.code, details.reasonCode, details.proofGap?.code].map((value) => String(value ?? ''));
128
+ return codes.some((code) => CssDependencyCodeFragments.some((fragment) => code.includes(fragment))) || isCssDependencyAtRule(details.before) || isCssDependencyAtRule(details.after);
129
+ }
130
+ function isCssDependencyAtRule(shape) { return CssDependencyAtRuleNames.has(String(shape?.atRuleName ?? '').toLowerCase()); }
46
131
  function hasBrowserRuntimeProof(file) {
47
132
  const admission = file?.result?.admission ?? file?.admission ?? {};
48
133
  return admission.browserRuntimeEquivalenceClaim === true || admission.browserCascadeEquivalenceClaim === true || admission.browserRenderEquivalenceClaim === true;
49
134
  }
135
+ function hasHtmlDuplicateIdentityEvidence(file) { return hasDuplicateExplicitIdentityKeys(file?.result?.identityEvidence); }
136
+ function htmlDuplicateIdentityKeyCount(file) {
137
+ return duplicateExplicitIdentityKeys(file?.result?.identityEvidence).length;
138
+ }
139
+ function hasDuplicateExplicitIdentityKeys(evidence) {
140
+ return duplicateExplicitIdentityKeys(evidence).length > 0;
141
+ }
142
+ function duplicateExplicitIdentityKeys(evidence) {
143
+ return Object.values(evidence?.sides ?? {}).flatMap((side) => {
144
+ const counts = new Map();
145
+ for (const key of side?.explicitIdentityKeys ?? []) counts.set(key, (counts.get(key) ?? 0) + 1);
146
+ return [...counts.entries()].filter(([, count]) => count > 1).map(([key]) => key);
147
+ });
148
+ }
149
+
150
+ const ScopedCascadeMissingProofReasonCodes = new Set(['css-scoped-cascade-equivalence-unproved', 'css-media-cascade-scope-unproved', 'css-supports-cascade-scope-unproved', 'css-container-cascade-scope-unproved', 'css-layer-cascade-scope-unproved', 'css-scope-cascade-scope-unproved']);
151
+ const HtmlRuntimeBoundaryReasonCodes = new Set(['script-runtime-boundary', 'style-runtime-boundary', 'template-runtime-boundary', 'slot-runtime-boundary', 'custom-element-runtime-boundary', 'event-handler-runtime-boundary', 'inline-style-runtime-boundary', 'iframe-runtime-boundary', 'iframe-srcdoc-runtime-boundary']);
152
+ const HtmlFrameworkBoundaryReasonCodes = new Set(['framework-directive-boundary', 'custom-element-runtime-boundary']);
153
+ const CssDependencyMissingProofReasonCodes = new Set(['css-dependency-graph-evidence-missing', 'css-custom-property-dependency-graph-unproved', 'css-var-fallback-dependency-graph-unproved', 'css-animation-name-keyframes-graph-unproved', 'css-font-face-dependency-graph-unproved', 'css-url-asset-dependency-graph-unproved']);
154
+ const CssDependencyAtRuleNames = new Set(['keyframes', 'font-face']);
155
+ const CssDependencyCodeFragments = ['custom-property', 'var-fallback', 'variable-dependency', 'dependency-graph', 'keyframes', 'animation-name', 'font-face', 'url-asset', 'asset-dependency'];
156
+ const CssDependencySurfacePatterns = [/(^|[;{\s])--[-_A-Za-z][\w-]*\s*:/, /\bvar\s*\(/i, /@(?:-[\w]+-)?keyframes\b/i, /(^|[;{\s])animation(?:-name)?\s*:/i, /@font-face\b/i, /\burl\s*\(/i];
157
+ function isPlainObject(value) { return Boolean(value && typeof value === 'object' && !Array.isArray(value)); }
50
158
 
51
159
  export { htmlCssProjectSummary };
@@ -1,7 +1,7 @@
1
1
  import { safeMergeCssSource } from '@shapeshift-labs/frontier-lang-css';
2
2
  import { safeMergeHtmlSource } from '@shapeshift-labs/frontier-lang-html';
3
3
  import { compactRecord } from './js-ts-safe-merge-context.js';
4
- import { hashText, safeId } from './js-ts-safe-project-merge-core.js';
4
+ import { hashText, safeId, uniqueStrings } from './js-ts-safe-project-merge-core.js';
5
5
 
6
6
  function projectFileLanguage(file, input) {
7
7
  return file.language ?? inferLanguageFromPath(file.sourcePath) ?? input.language ?? 'typescript';
@@ -12,8 +12,12 @@ function maybeMergeHtmlCssProjectFile(options) {
12
12
  const language = String(context.language ?? '').toLowerCase();
13
13
  const merge = language === 'html' ? safeMergeHtmlSource : language === 'css' ? safeMergeCssSource : undefined;
14
14
  if (!merge) return undefined;
15
- const result = merge({ ...sourceInput, ...htmlCssMergeOptionsForProjectFile(input, file.sourcePath, language), ...context, id: `${projectId}_${safeId(file.sourcePath)}`, baseSourceText: base, workerSourceText: worker, headSourceText: head });
16
- return result.status === 'merged' ? mergedHtmlCssFile(file, context, result, language) : blockedHtmlCssFile(file, context, result);
15
+ const resultId = `${projectId}_${safeId(file.sourcePath)}`;
16
+ const result = merge({ ...sourceInput, ...htmlCssMergeOptionsForProjectFile(input, file.sourcePath, language), ...context, id: resultId, baseSourceText: base, workerSourceText: worker, headSourceText: head });
17
+ const admittedResult = language === 'html' && result.status === 'merged'
18
+ ? blockHtmlProofGapChanges({ result, id: resultId, sourcePath: file.sourcePath, base, worker, head }) ?? result
19
+ : result;
20
+ return admittedResult.status === 'merged' ? mergedHtmlCssFile(file, context, admittedResult, language) : blockedHtmlCssFile(file, context, admittedResult);
17
21
  }
18
22
 
19
23
  function inferLanguageFromPath(sourcePath) {
@@ -32,6 +36,136 @@ function htmlCssMergeOptionsForProjectFile(input, sourcePath, language) {
32
36
  return compactRecord({ ...(language === 'css' ? input.cssMergeOptions ?? input.styleMergeOptions : input.htmlMergeOptions ?? input.markupMergeOptions), ...(byPath?.[sourcePath] ?? {}) });
33
37
  }
34
38
 
39
+ function blockHtmlProofGapChanges({ result, id, sourcePath, base, worker, head }) {
40
+ const conflicts = [
41
+ htmlDuplicateIdentityConflict(result),
42
+ ...htmlRuntimeBoundaryChanges(base, worker, head).map((change) => htmlProofGapConflict(id, sourcePath, change.reasonCode, change))
43
+ ].filter(Boolean);
44
+ if (!conflicts.length) return undefined;
45
+ const allConflicts = [...(result.conflicts ?? []), ...conflicts];
46
+ const { mergedSourceText, mergedSourceHash, ...rest } = result;
47
+ return compactRecord({
48
+ ...rest,
49
+ status: 'blocked',
50
+ operation: 'blocked',
51
+ conflicts: allConflicts,
52
+ admission: blockedHtmlProofGapAdmission(result.admission, allConflicts),
53
+ autoMergeClaim: false,
54
+ semanticEquivalenceClaim: false,
55
+ browserRuntimeEquivalenceClaim: false
56
+ });
57
+ }
58
+
59
+ function htmlDuplicateIdentityConflict(result) {
60
+ const duplicates = duplicateHtmlExplicitIdentityKeys(result?.identityEvidence);
61
+ if (!duplicates.length) return undefined;
62
+ return htmlProofGapConflict(result.id, result.sourcePath, 'html-duplicate-explicit-identity', {
63
+ boundary: 'html-explicit-identity',
64
+ duplicateIdentityKeys: duplicates
65
+ }, 'html-duplicate-identity-blocked');
66
+ }
67
+
68
+ function duplicateHtmlExplicitIdentityKeys(identityEvidence) {
69
+ return Object.entries(identityEvidence?.sides ?? {}).flatMap(([side, evidence]) => {
70
+ const counts = new Map();
71
+ for (const key of evidence?.explicitIdentityKeys ?? []) counts.set(key, (counts.get(key) ?? 0) + 1);
72
+ return [...counts.entries()]
73
+ .filter(([, count]) => count > 1)
74
+ .map(([key, count]) => ({ side, key, count }));
75
+ });
76
+ }
77
+
78
+ function htmlRuntimeBoundaryChanges(base, worker, head) {
79
+ const baseEventHandlers = htmlEventHandlerBoundaryFingerprint(base);
80
+ return [
81
+ htmlRuntimeBoundaryChange('worker', base, worker, baseEventHandlers),
82
+ htmlRuntimeBoundaryChange('head', base, head, baseEventHandlers)
83
+ ].filter(Boolean);
84
+ }
85
+
86
+ function htmlRuntimeBoundaryChange(side, base, sourceText, baseEventHandlers) {
87
+ if (sourceText === base) return undefined;
88
+ const eventHandlers = htmlEventHandlerBoundaryFingerprint(sourceText);
89
+ if (eventHandlers === baseEventHandlers) return undefined;
90
+ return {
91
+ side,
92
+ reasonCode: 'event-handler-runtime-boundary',
93
+ boundary: 'html-event-handler-attribute',
94
+ boundaryAttributes: htmlEventHandlerBoundaryAttributeNames(sourceText)
95
+ };
96
+ }
97
+
98
+ function htmlEventHandlerBoundaryFingerprint(sourceText) {
99
+ return htmlEventHandlerBoundaryAttributes(sourceText)
100
+ .map((attribute) => `${attribute.tagName}:${attribute.name}=${String(attribute.value)}`)
101
+ .sort()
102
+ .join('\n');
103
+ }
104
+
105
+ function htmlEventHandlerBoundaryAttributeNames(sourceText) {
106
+ return uniqueStrings(htmlEventHandlerBoundaryAttributes(sourceText).map((attribute) => attribute.name));
107
+ }
108
+
109
+ function htmlEventHandlerBoundaryAttributes(sourceText) {
110
+ const attributes = [];
111
+ for (const tag of String(sourceText ?? '').matchAll(/<[A-Za-z][\w:-]*(?:\s+[^<>]*?)?\/?>/g)) {
112
+ const parsed = /^<([A-Za-z][\w:-]*)([\s\S]*?)\/?>$/.exec(tag[0]);
113
+ if (!parsed) continue;
114
+ const tagName = parsed[1].toLowerCase();
115
+ for (const attribute of parseHtmlAttributes(parsed[2] ?? '')) {
116
+ if (/^on[\w:.-]+$/i.test(attribute.name)) attributes.push({ ...attribute, name: attribute.name.toLowerCase(), tagName });
117
+ }
118
+ }
119
+ return attributes;
120
+ }
121
+
122
+ function parseHtmlAttributes(text) {
123
+ const attributes = [];
124
+ const pattern = /([:@A-Za-z_][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
125
+ for (const match of text.matchAll(pattern)) attributes.push({ name: match[1], value: match[2] ?? match[3] ?? match[4] ?? true });
126
+ return attributes;
127
+ }
128
+
129
+ function htmlProofGapConflict(id, sourcePath, reasonCode, details = {}, code = 'html-proof-gap-blocked') {
130
+ return {
131
+ code,
132
+ gateId: 'html-semantic-merge',
133
+ sourcePath,
134
+ details: compactRecord({
135
+ reasonCode,
136
+ conflictKey: `html#${id}#${reasonCode}#${details.side ?? details.duplicateIdentityKeys?.[0]?.key ?? sourcePath ?? 'source'}`,
137
+ proofGap: {
138
+ code: reasonCode,
139
+ status: 'not-claimed',
140
+ summary: htmlProofGapSummary(reasonCode),
141
+ failClosed: true,
142
+ semanticEquivalenceClaim: false
143
+ },
144
+ ...details
145
+ })
146
+ };
147
+ }
148
+
149
+ function htmlProofGapSummary(reasonCode) {
150
+ if (reasonCode === 'html-duplicate-explicit-identity') return 'Duplicate explicit HTML identity keys make structural target admission ambiguous.';
151
+ if (reasonCode === 'event-handler-runtime-boundary') return 'HTML event handler attributes execute in the browser runtime and require source-bound host evidence.';
152
+ return 'HTML proof gap requires source-bound evidence before structural merge admission.';
153
+ }
154
+
155
+ function blockedHtmlProofGapAdmission(admission = {}, conflicts = []) {
156
+ return {
157
+ ...admission,
158
+ status: 'blocked',
159
+ action: 'human-review',
160
+ reviewRequired: true,
161
+ autoApplyCandidate: false,
162
+ autoMergeClaim: false,
163
+ semanticEquivalenceClaim: false,
164
+ browserRuntimeEquivalenceClaim: false,
165
+ reasonCodes: uniqueStrings([...(admission.reasonCodes ?? []), ...conflicts.map((conflict) => conflict.details?.reasonCode ?? conflict.code)])
166
+ };
167
+ }
168
+
35
169
  function mergedHtmlCssFile(file, context, result, language) {
36
170
  return compactRecord({
37
171
  kind: 'frontier.lang.jsTsProjectSafeMergeFile', version: 1, sourcePath: file.sourcePath, language: context.language, status: 'merged', operation: `merged-${language}-source`,
@@ -98,6 +98,15 @@ function jsxPropDetails(record) {
98
98
  propValueReferenceRoot: record.propValueReferenceRoot, propValueReferencePath: record.propValueReferencePath,
99
99
  propValueOptionalReference: record.propValueOptionalReference, propValueOptionalReferenceSegments: record.propValueOptionalReferenceSegments,
100
100
  propValueOptionalReferenceSegmentIndexes: record.propValueOptionalReferenceSegmentIndexes, propValueOptionalNullishBoundaryCount: record.propValueOptionalNullishBoundaryCount,
101
+ propValueClaimScope: record.propValueClaimScope, propValueRenderEquivalenceClaim: record.propValueRenderEquivalenceClaim,
102
+ propValueStaticSpreadSourceKind: record.propValueStaticSpreadSourceKind, propValueStaticSpreadSourceName: record.propValueStaticSpreadSourceName,
103
+ propValueStaticSpreadPropEntries: record.propValueStaticSpreadPropEntries, propValueStaticSpreadPropNames: record.propValueStaticSpreadPropNames,
104
+ propValueStaticSpreadPropCount: record.propValueStaticSpreadPropCount,
105
+ propValueStaticSpreadEffectivePropEntries: record.propValueStaticSpreadEffectivePropEntries, propValueStaticSpreadEffectivePropNames: record.propValueStaticSpreadEffectivePropNames,
106
+ propValueStaticSpreadExplicitOverridePropNames: record.propValueStaticSpreadExplicitOverridePropNames,
107
+ propValueStaticSpreadOverridesExplicitPropNames: record.propValueStaticSpreadOverridesExplicitPropNames,
108
+ propValueStaticSpreadDuplicatePropNames: record.propValueStaticSpreadDuplicatePropNames,
109
+ propValueStaticSpreadPrecedenceStatus: record.propValueStaticSpreadPrecedenceStatus,
101
110
  propValueDynamicText: record.propValueDynamicText, propValueDynamicBlockerReasonCode: record.propValueDynamicBlockerReasonCode,
102
111
  propValueExpressionHash: record.propValueExpressionHash, propValueSignatureHash: record.propValueSignatureHash,
103
112
  componentPropRenderFlowStatus: record.componentPropRenderFlowStatus, componentPropRenderFlowReasonCode: record.componentPropRenderFlowReasonCode,
@@ -259,6 +259,9 @@ function mergeProjectFile(file, input, projectId, projectSymbolRenames) {
259
259
  policy: file.policy ?? file.mergePolicy ?? policyForFile(input, file.sourcePath)
260
260
  });
261
261
  if (result.status !== 'merged') {
262
+ if (base === worker && base === head) {
263
+ return syntheticFile(file, context, base, 'unchanged-identical');
264
+ }
262
265
  return maybeMergeImportSpecifierRemovalFile(file, context, result, input)
263
266
  ?? mergeBlockedFile(file, context, result);
264
267
  }