@shapeshift-labs/frontier-lang-css 0.1.9 → 0.1.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/README.md CHANGED
@@ -232,11 +232,11 @@ const merge = safeMergeCssSource({
232
232
  });
233
233
  ```
234
234
 
235
- `sourceMap.mappings` links emitted rule blocks back to Frontier Lang semantic node ids. `createCssSemanticMergeEvidence` records selectors, specificity, declarations, custom properties, cascade keys, statement-form at-rules, CSS Modules exports, ICSS edges, scoped cascade graph proof hashes, source spans, stable hashes, and fail-closed proof gaps for cascade/render-sensitive CSS surfaces. `safeMergeCssSource` admits independent declaration edits by cascade key, including existing scoped `@media` / `@supports` / `@container` / `@layer` declarations when a scoped cascade graph proof hash is supplied, preserves unchanged statement-form at-rules such as `@layer reset, components;`, and for `.module.css` files it classifies exported local classes, `composes`, and ICSS import/export records as explicit merge contracts.
235
+ `sourceMap.mappings` links emitted rule blocks back to Frontier Lang semantic node ids. `createCssSemanticMergeEvidence` records selectors, specificity, declarations, custom properties, `var()` fallback references, animation/keyframe links, font-face links, URL asset references, cascade keys, statement-form at-rules, CSS Modules exports, ICSS edges, scoped cascade graph proof hashes, source spans, stable hashes, and fail-closed proof gaps for cascade/render-sensitive CSS surfaces. `safeMergeCssSource` admits independent declaration edits by cascade key, including existing scoped `@media` / `@supports` / `@container` / `@layer` declarations when a scoped cascade graph proof hash is supplied, carries source-bound dependency graph evidence into the merge result, preserves unchanged statement-form at-rules such as `@layer reset, components;`, and for `.module.css` files it classifies exported local classes, `composes`, and ICSS import/export records as explicit merge contracts. Cascade-sensitive source-shape changes still block by default, but a host can attach a `css-source-bound-cascade-runtime-proof` / `css-cascade-runtime-proof` that is bound to the exact source path, reason, side, shape key, and base/worker/head/output source hashes to admit that specific change.
236
236
 
237
237
  ## Support Boundary
238
238
 
239
- - Ready evidence: style rules, selectors, specificity, declarations, custom properties, statement-form at-rules, CSS Modules local exports, generated class-name map coverage, JS/TS use-site graph hashes, composition graph hashes, ICSS graph hashes, scoped cascade graph hashes, source spans, stable hashes.
240
- - Safe merge: independent declarations with non-overlapping cascade keys; unchanged statement-form at-rules preserved in canonical output; existing scoped declaration edits when scoped cascade graph proof is supplied; explicit CSS Modules export additions/deletions when generated class-name and JS/TS use-site graph proof is supplied; composition edits when composition graph proof is supplied; ICSS edits when ICSS graph proof is supplied. Output is a canonical CSS render and not a byte/trivia-preserving claim.
239
+ - Ready evidence: style rules, selectors, specificity, declarations, source-bound dependency graph hashes for custom properties, `var()` fallbacks, animations/keyframes, font faces, and URL assets, statement-form at-rules, CSS Modules local exports, generated class-name map coverage, JS/TS use-site graph hashes, composition graph hashes, ICSS graph hashes, scoped cascade graph hashes, source-bound cascade runtime proofs, source spans, stable hashes.
240
+ - Safe merge: independent declarations with non-overlapping cascade keys; unchanged statement-form at-rules preserved in canonical output; existing scoped declaration edits when scoped cascade graph proof is supplied; source-shape/cascade-sensitive edits only when an exact source-bound cascade runtime proof is supplied; explicit CSS Modules export additions/deletions when generated class-name and JS/TS use-site graph proof is supplied; composition edits when composition graph proof is supplied; ICSS edits when ICSS graph proof is supplied. Output is a canonical CSS render and not a byte/trivia-preserving claim.
241
241
  - Review-only gaps: incomplete generated class-name maps, unproved CSS Modules use-site graphs, unproved composition or ICSS graphs, shorthands without longhand expansion, statement-form at-rule order/condition changes, one-sided or structurally changed scoped cascade under `@media` / `@supports` / `@container` / `@layer`, `@keyframes`, `@font-face`, `@page`, browser layout and render equivalence.
242
- - Claims: `autoMergeClaim`, `semanticEquivalenceClaim`, `browserCascadeEquivalenceClaim`, and `browserRenderEquivalenceClaim` remain false.
242
+ - Claims: dependency graph evidence is an admission/review signal only; `autoMergeClaim`, `semanticEquivalenceClaim`, and `browserRenderEquivalenceClaim` remain false. `browserCascadeEquivalenceClaim` is only true on a safe-merge result when a source-bound cascade runtime proof admits the specific cascade-sensitive source-shape change.
@@ -0,0 +1,34 @@
1
+ export interface CssCascadeRuntimeProof {
2
+ readonly id?: string;
3
+ readonly kind: 'css-cascade-runtime-proof' | 'css-source-bound-cascade-runtime-proof' | string;
4
+ readonly status: 'passed' | string;
5
+ readonly proofLevel?: string;
6
+ readonly sourcePath?: string;
7
+ readonly reasonCode?: string;
8
+ readonly reasonCodes?: readonly string[];
9
+ readonly side?: 'worker' | 'head' | string;
10
+ readonly sides?: readonly string[];
11
+ readonly shapeKey?: string;
12
+ readonly shapeKeys?: readonly string[];
13
+ readonly baseSourceText?: string; readonly workerSourceText?: string; readonly headSourceText?: string; readonly outputSourceText?: string; readonly mergedSourceText?: string;
14
+ readonly baseSourceHash?: string; readonly workerSourceHash?: string; readonly headSourceHash?: string; readonly outputSourceHash?: string; readonly mergedSourceHash?: string;
15
+ readonly sourceTexts?: Readonly<Record<string, string>>;
16
+ readonly sources?: Readonly<Record<string, string>>;
17
+ readonly sourceHashes?: Readonly<Record<string, string>>;
18
+ readonly hashes?: Readonly<Record<string, string>>;
19
+ }
20
+
21
+ export interface CssCascadeRuntimeProofRecord {
22
+ readonly id?: string;
23
+ readonly kind: string;
24
+ readonly status: 'passed';
25
+ readonly proofLevel: string;
26
+ readonly reasonCode: string;
27
+ readonly side: string;
28
+ readonly shapeKey: string;
29
+ readonly sourcePath?: string;
30
+ readonly baseSourceHash?: string;
31
+ readonly workerSourceHash?: string;
32
+ readonly headSourceHash?: string;
33
+ readonly outputSourceHash?: string;
34
+ }
@@ -0,0 +1,60 @@
1
+ import type { CssSourceSpan } from './index.js';
2
+
3
+ export interface CssDependencyGraphRecord {
4
+ readonly kind: string;
5
+ readonly cascadeKey?: string;
6
+ readonly property?: string;
7
+ readonly name?: string;
8
+ readonly family?: string;
9
+ readonly url?: string;
10
+ readonly sourceKind?: string;
11
+ readonly hasFallback?: boolean;
12
+ readonly fallbackHash?: string;
13
+ readonly targetDefined?: boolean;
14
+ readonly declarationHash?: string;
15
+ readonly ruleHash?: string;
16
+ readonly atRuleHash?: string;
17
+ readonly selectors?: readonly string[];
18
+ readonly scopes?: readonly string[];
19
+ readonly sourceSpan?: CssSourceSpan;
20
+ readonly sourceHash?: string;
21
+ }
22
+
23
+ export interface CssDependencyGraphRecordSets {
24
+ readonly customPropertyDefinitions?: readonly CssDependencyGraphRecord[];
25
+ readonly customPropertyReferences?: readonly CssDependencyGraphRecord[];
26
+ readonly keyframes?: readonly CssDependencyGraphRecord[];
27
+ readonly animationNameLinks?: readonly CssDependencyGraphRecord[];
28
+ readonly fontFaces?: readonly CssDependencyGraphRecord[];
29
+ readonly fontFaceLinks?: readonly CssDependencyGraphRecord[];
30
+ readonly urlAssetReferences?: readonly CssDependencyGraphRecord[];
31
+ }
32
+
33
+ export interface CssDependencyGraphEvidence {
34
+ readonly kind: 'frontier.lang.cssDependencyGraphEvidence' | 'frontier.lang.cssSafeMergeDependencyGraphEvidence' | string;
35
+ readonly version: 1;
36
+ readonly sourcePath?: string;
37
+ readonly sourceHash?: string;
38
+ readonly hasDependencySurface: boolean;
39
+ readonly dependencySurfaceCount: number;
40
+ readonly dependencyGraphHashPresent: boolean;
41
+ readonly cssDependencyGraphHashPresent?: boolean;
42
+ readonly dependencyGraphHash?: string;
43
+ readonly cssDependencyGraphHash?: string;
44
+ readonly changedDependencySurfaceCount?: number;
45
+ readonly customPropertyDefinitions?: number;
46
+ readonly customPropertyReferences?: number;
47
+ readonly varReferences?: number;
48
+ readonly varFallbackReferences?: number;
49
+ readonly keyframeDefinitions?: number;
50
+ readonly animationNameLinks?: number;
51
+ readonly keyframeLinks?: number;
52
+ readonly fontFaceDefinitions?: number;
53
+ readonly fontFaceLinks?: number;
54
+ readonly urlAssetReferences?: number;
55
+ readonly records?: CssDependencyGraphRecordSets;
56
+ readonly sides?: Readonly<Record<string, CssDependencyGraphEvidence>>;
57
+ readonly browserCascadeEquivalenceClaim?: false;
58
+ readonly browserRenderEquivalenceClaim?: false;
59
+ readonly semanticEquivalenceClaim: false;
60
+ }
@@ -0,0 +1,191 @@
1
+ function createCssDependencyGraphEvidence(records = [], options = {}) {
2
+ const hash = options.hashSemanticValue;
3
+ const definitions = [];
4
+ const references = [];
5
+ const animations = [];
6
+ const keyframes = [];
7
+ const fontFaces = [];
8
+ const fonts = [];
9
+ const assets = [];
10
+ for (const record of records) {
11
+ if (record.kind === 'at-rule' && record.atRuleName === 'keyframes') keyframes.push(keyframeDefinition(record));
12
+ if (record.kind === 'at-rule' && record.atRuleName === 'font-face') {
13
+ for (const family of record.dependencyTokens?.fontFamilies ?? []) fontFaces.push(fontFaceDefinition(record, family));
14
+ for (const url of record.dependencyTokens?.urls ?? []) assets.push(assetReference(record, undefined, url, 'font-face-src'));
15
+ }
16
+ for (const declaration of record.declarations ?? []) {
17
+ if (declaration.property?.startsWith('--')) definitions.push(customPropertyDefinition(record, declaration));
18
+ for (const item of cssVarReferences(declaration.value)) references.push(customPropertyReference(record, declaration, item));
19
+ if (declaration.property === 'animation' || declaration.property === 'animation-name') {
20
+ for (const name of animationNames(declaration.value)) animations.push(animationReference(record, declaration, name));
21
+ }
22
+ if (declaration.property === 'font' || declaration.property === 'font-family') {
23
+ for (const family of fontFamilyNames(declaration.value)) fonts.push(fontReference(record, declaration, family));
24
+ }
25
+ for (const url of cssUrlReferences(declaration.value)) assets.push(assetReference(record, declaration, url, 'declaration-url'));
26
+ }
27
+ }
28
+ const keyframeNames = new Set(keyframes.map((entry) => entry.name));
29
+ const fontNames = new Set(fontFaces.map((entry) => entry.family));
30
+ const linkedAnimations = animations.map((entry) => ({ ...entry, targetDefined: keyframeNames.has(entry.name) }));
31
+ const linkedFonts = fonts.map((entry) => ({ ...entry, targetDefined: fontNames.has(entry.family) }));
32
+ const dependencySurfaceCount = definitions.length + references.length + linkedAnimations.length + keyframes.length + fontFaces.length + linkedFonts.length + assets.length;
33
+ const graphHash = dependencySurfaceCount ? hash?.({
34
+ kind: 'frontier.lang.css.dependencyGraph.v1',
35
+ definitions,
36
+ references,
37
+ animations: linkedAnimations,
38
+ keyframes,
39
+ fontFaces,
40
+ fonts: linkedFonts,
41
+ assets
42
+ }) : undefined;
43
+ return compactRecord({
44
+ kind: 'frontier.lang.cssDependencyGraphEvidence',
45
+ version: 1,
46
+ sourcePath: options.sourcePath,
47
+ sourceHash: options.sourceHash,
48
+ hasDependencySurface: dependencySurfaceCount > 0,
49
+ dependencySurfaceCount,
50
+ dependencyGraphHashPresent: Boolean(graphHash),
51
+ cssDependencyGraphHashPresent: Boolean(graphHash),
52
+ dependencyGraphHash: graphHash,
53
+ cssDependencyGraphHash: graphHash,
54
+ customPropertyDefinitions: definitions.length,
55
+ customPropertyReferences: references.length,
56
+ varReferences: references.length,
57
+ varFallbackReferences: references.filter((entry) => entry.hasFallback).length,
58
+ keyframeDefinitions: keyframes.length,
59
+ animationNameLinks: linkedAnimations.length,
60
+ keyframeLinks: linkedAnimations.filter((entry) => entry.targetDefined).length,
61
+ fontFaceDefinitions: fontFaces.length,
62
+ fontFaceLinks: linkedFonts.length,
63
+ urlAssetReferences: assets.length,
64
+ records: { customPropertyDefinitions: definitions, customPropertyReferences: references, keyframes, animationNameLinks: linkedAnimations, fontFaces, fontFaceLinks: linkedFonts, urlAssetReferences: assets },
65
+ browserCascadeEquivalenceClaim: false,
66
+ browserRenderEquivalenceClaim: false,
67
+ semanticEquivalenceClaim: false
68
+ });
69
+ }
70
+
71
+ function mergeCssDependencyGraphEvidence(sheets, changed = {}) {
72
+ const sides = Object.fromEntries(Object.entries(sheets).map(([side, sheet]) => [side, sheet.dependencyGraphEvidence ?? emptyGraphEvidence(sheet)]));
73
+ const sideValues = Object.values(sides);
74
+ const hasDependencySurface = sideValues.some((side) => side.hasDependencySurface === true);
75
+ const changedKeys = new Set([...(changed.worker ?? []), ...(changed.head ?? [])].flatMap((change) => [change.before?.key, change.after?.key].filter(Boolean)));
76
+ const dependencyKeys = new Set(sideValues.flatMap(dependencySurfaceKeys));
77
+ return {
78
+ kind: 'frontier.lang.cssSafeMergeDependencyGraphEvidence',
79
+ version: 1,
80
+ hasDependencySurface,
81
+ dependencySurfaceCount: Math.max(0, ...sideValues.map((side) => side.dependencySurfaceCount ?? 0)),
82
+ dependencyGraphHashPresent: hasDependencySurface && sideValues.every((side) => side.hasDependencySurface !== true || side.dependencyGraphHashPresent === true),
83
+ cssDependencyGraphHashPresent: hasDependencySurface && sideValues.every((side) => side.hasDependencySurface !== true || side.cssDependencyGraphHashPresent === true),
84
+ changedDependencySurfaceCount: [...changedKeys].filter((key) => dependencyKeys.has(key)).length,
85
+ sides,
86
+ browserCascadeEquivalenceClaim: false,
87
+ browserRenderEquivalenceClaim: false,
88
+ semanticEquivalenceClaim: false
89
+ };
90
+ }
91
+
92
+ function dependencySurfaceKeys(evidence) {
93
+ const records = evidence.records ?? {};
94
+ return [
95
+ ...(records.customPropertyDefinitions ?? []),
96
+ ...(records.customPropertyReferences ?? []),
97
+ ...(records.animationNameLinks ?? []),
98
+ ...(records.fontFaceLinks ?? []),
99
+ ...(records.urlAssetReferences ?? [])
100
+ ].map((entry) => entry.cascadeKey).filter(Boolean);
101
+ }
102
+
103
+ function customPropertyDefinition(record, declaration) {
104
+ return baseDependencyRecord(record, declaration, { kind: 'custom-property-definition', name: declaration.property, valueHash: declaration.valueHash });
105
+ }
106
+
107
+ function customPropertyReference(record, declaration, item) {
108
+ return baseDependencyRecord(record, declaration, { kind: 'custom-property-reference', name: item.name, fallbackHash: item.fallbackHash, hasFallback: item.hasFallback });
109
+ }
110
+
111
+ function animationReference(record, declaration, name) {
112
+ return baseDependencyRecord(record, declaration, { kind: 'animation-name-link', name });
113
+ }
114
+
115
+ function fontReference(record, declaration, family) {
116
+ return baseDependencyRecord(record, declaration, { kind: 'font-face-link', family });
117
+ }
118
+
119
+ function assetReference(record, declaration, url, sourceKind) {
120
+ return baseDependencyRecord(record, declaration, { kind: 'url-asset-reference', url, sourceKind });
121
+ }
122
+
123
+ function keyframeDefinition(record) {
124
+ return { kind: 'keyframes-definition', name: firstCssIdent(record.conditionText), atRuleHash: record.atRuleHash, sourceSpan: record.sourceSpan, sourceHash: record.sourceHash };
125
+ }
126
+
127
+ function fontFaceDefinition(record, family) {
128
+ return { kind: 'font-face-definition', family, atRuleHash: record.atRuleHash, sourceSpan: record.sourceSpan, sourceHash: record.sourceHash };
129
+ }
130
+
131
+ function baseDependencyRecord(record, declaration, extra) {
132
+ return compactRecord({
133
+ ...extra,
134
+ cascadeKey: declaration?.cascadeKey,
135
+ property: declaration?.property,
136
+ declarationHash: declaration?.declarationHash,
137
+ ruleHash: record.ruleHash,
138
+ selectors: record.selectors,
139
+ scopes: record.scopes ?? [],
140
+ sourceSpan: declaration?.sourceSpan ?? record.sourceSpan,
141
+ sourceHash: record.sourceHash
142
+ });
143
+ }
144
+
145
+ function cssVarReferences(value) {
146
+ return [...String(value ?? '').matchAll(/\bvar\(\s*(--[-_A-Za-z][\w-]*)(?:\s*,\s*([^)]*))?\)/g)].map((match) => ({
147
+ name: match[1],
148
+ hasFallback: match[2] !== undefined,
149
+ fallbackHash: match[2] === undefined ? undefined : stableTextHash(match[2].trim())
150
+ }));
151
+ }
152
+
153
+ function cssUrlReferences(value) {
154
+ return [...String(value ?? '').matchAll(/\burl\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*?))\s*\)/g)]
155
+ .map((match) => (match[1] ?? match[2] ?? match[3] ?? '').trim())
156
+ .filter(Boolean);
157
+ }
158
+
159
+ function animationNames(value) {
160
+ const normalized = String(value ?? '').replace(/\bvar\(\s*--[-_A-Za-z][\w-]*(?:\s*,\s*([^)]*))?\)/g, (_, fallback) => fallback ?? '');
161
+ return unique(normalized.split(/[\s,]+/).map((part) => part.trim().replace(/[()]+$/g, '')).filter((part) => part && !AnimationKeywords.has(part) && !/^[\d.]+m?s$/.test(part)));
162
+ }
163
+
164
+ function fontFamilyNames(value) {
165
+ return unique(String(value ?? '').split(',').map((part) => part.trim().replace(/^['"]|['"]$/g, '')).filter((part) => part && !FontKeywords.has(part) && !/^[\d.]+/.test(part)));
166
+ }
167
+
168
+ function firstCssIdent(value) {
169
+ return /^[-_A-Za-z][\w-]*/.exec(String(value ?? '').trim())?.[0];
170
+ }
171
+
172
+ function emptyGraphEvidence(sheet) {
173
+ return { kind: 'frontier.lang.cssDependencyGraphEvidence', version: 1, sourceHash: sheet.sourceHash, hasDependencySurface: false, dependencySurfaceCount: 0, dependencyGraphHashPresent: false, cssDependencyGraphHashPresent: false, semanticEquivalenceClaim: false };
174
+ }
175
+
176
+ function stableTextHash(text) {
177
+ let hash = 2166136261;
178
+ for (let index = 0; index < text.length; index += 1) {
179
+ hash ^= text.charCodeAt(index);
180
+ hash = Math.imul(hash, 16777619);
181
+ }
182
+ return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, '0')}`;
183
+ }
184
+
185
+ function unique(values) { return [...new Set(values.filter(Boolean))]; }
186
+ function compactRecord(record) { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)); }
187
+
188
+ const AnimationKeywords = new Set(['none', 'initial', 'inherit', 'unset', 'revert', 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'infinite', 'alternate', 'forwards', 'backwards', 'both', 'normal', 'reverse', 'running', 'paused']);
189
+ const FontKeywords = new Set(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui', 'inherit', 'initial', 'unset', 'revert']);
190
+
191
+ export { createCssDependencyGraphEvidence, mergeCssDependencyGraphEvidence };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FrontierLangDocument } from '@shapeshift-labs/frontier-lang-kernel';
1
+ import type { FrontierLangDocument } from '@shapeshift-labs/frontier-lang-kernel'; import type { CssCascadeRuntimeProof, CssCascadeRuntimeProofRecord } from './cascade-runtime-proof.js'; import type { CssDependencyGraphEvidence } from './dependency-graph.js'; export type { CssCascadeRuntimeProof, CssCascadeRuntimeProofRecord } from './cascade-runtime-proof.js';
2
2
 
3
3
  export interface CssProjectionOptions {
4
4
  readonly banner?: string;
@@ -13,6 +13,8 @@ export interface CssProjectionOptions {
13
13
  readonly cssModuleCompositionGraphHash?: string;
14
14
  readonly icssGraphHash?: string;
15
15
  readonly scopedCascadeGraphHash?: string;
16
+ readonly cssCascadeRuntimeProof?: CssCascadeRuntimeProof; readonly cssCascadeRuntimeProofs?: readonly CssCascadeRuntimeProof[];
17
+ readonly cssSourceBoundCascadeProof?: CssCascadeRuntimeProof; readonly cssSourceBoundCascadeProofs?: readonly CssCascadeRuntimeProof[];
16
18
  readonly selectorTargetGraphHash?: string;
17
19
  readonly targetPath?: string;
18
20
  readonly semanticIndexId?: string;
@@ -20,36 +22,16 @@ export interface CssProjectionOptions {
20
22
  readonly evidence?: readonly CssProjectionEvidenceRecord[];
21
23
  }
22
24
 
23
- export interface CssProjectionEvidenceRecord {
24
- readonly id: string;
25
- readonly kind?: string;
26
- readonly summary?: string;
27
- readonly [key: string]: unknown;
28
- }
25
+ export interface CssProjectionEvidenceRecord { readonly id: string; readonly kind?: string; readonly summary?: string; readonly [key: string]: unknown; }
29
26
 
30
27
  export interface CssSourceSpan {
31
- readonly path?: string;
32
- readonly startOffset?: number;
33
- readonly endOffset?: number;
34
- readonly startLine: number;
35
- readonly startColumn: number;
36
- readonly endLine: number;
37
- readonly endColumn: number;
28
+ readonly path?: string; readonly startOffset?: number; readonly endOffset?: number;
29
+ readonly startLine: number; readonly startColumn: number; readonly endLine: number; readonly endColumn: number;
38
30
  }
39
31
 
40
- export interface CssParserDiagnostic {
41
- readonly reason?: string;
42
- readonly line?: number;
43
- readonly column?: number;
44
- readonly input?: string;
45
- readonly [key: string]: unknown;
46
- }
32
+ export interface CssParserDiagnostic { readonly reason?: string; readonly line?: number; readonly column?: number; readonly input?: string; readonly [key: string]: unknown; }
47
33
 
48
- export interface CssParserEvidence {
49
- readonly name: 'postcss' | string;
50
- readonly sourceCodeLocationInfo: boolean;
51
- readonly parseErrors: readonly CssParserDiagnostic[];
52
- }
34
+ export interface CssParserEvidence { readonly name: 'postcss' | string; readonly sourceCodeLocationInfo: boolean; readonly parseErrors: readonly CssParserDiagnostic[]; }
53
35
 
54
36
  export interface CssSourceRef {
55
37
  readonly semanticNodeId: string;
@@ -216,7 +198,7 @@ export interface CssSemanticSheet {
216
198
  readonly sourcePath?: string;
217
199
  readonly sourceHash: string;
218
200
  readonly records: readonly CssSemanticRecord[];
219
- readonly cssModules?: CssModuleEvidence;
201
+ readonly cssModules?: CssModuleEvidence; readonly dependencyGraphEvidence: CssDependencyGraphEvidence;
220
202
  readonly sheetHash: string;
221
203
  readonly summary: Readonly<Record<string, number>>;
222
204
  readonly proofGaps: readonly CssSemanticProofGap[];
@@ -227,7 +209,7 @@ export interface CssSemanticMergeEvidence {
227
209
  readonly kind: 'frontier.lang.cssSemanticMergeEvidence'; readonly version: 1;
228
210
  readonly status: 'ready' | 'needs-review' | string; readonly sourcePath?: string; readonly sourceHash: string; readonly sheetHash: string;
229
211
  readonly records: readonly CssSemanticRecord[];
230
- readonly cssModules?: CssModuleEvidence;
212
+ readonly cssModules?: CssModuleEvidence; readonly dependencyGraphEvidence: CssDependencyGraphEvidence;
231
213
  readonly proofGaps: readonly CssSemanticProofGap[];
232
214
  readonly autoMergeClaim: false; readonly semanticEquivalenceClaim: false;
233
215
  readonly cssModuleGeneratedNameEquivalenceClaim: false; readonly cssModuleUseSiteEquivalenceClaim: false;
@@ -242,6 +224,8 @@ export interface CssSafeMergeConflict {
242
224
  export interface CssSafeMergeAdmission {
243
225
  readonly status: 'auto-merge-candidate' | 'blocked' | string; readonly action: 'apply-css' | 'human-review' | string;
244
226
  readonly reviewRequired: boolean; readonly reasonCodes: readonly string[];
227
+ readonly browserCascadeEquivalenceClaim?: true;
228
+ readonly cssCascadeRuntimeProofs?: readonly CssCascadeRuntimeProofRecord[];
245
229
  }
246
230
 
247
231
  export interface CssSafeMergeResult {
@@ -251,10 +235,12 @@ export interface CssSafeMergeResult {
251
235
  readonly conflicts: readonly CssSafeMergeConflict[];
252
236
  readonly admission: CssSafeMergeAdmission;
253
237
  readonly autoMergeClaim: false; readonly semanticEquivalenceClaim: false;
238
+ readonly browserCascadeEquivalenceClaim: boolean; readonly browserRenderEquivalenceClaim: false;
254
239
  readonly baseSheetHash?: string; readonly workerSheetHash?: string; readonly headSheetHash?: string;
255
240
  readonly workerChangedDeclarations?: number; readonly headChangedDeclarations?: number;
256
241
  readonly workerChangedCssModuleContracts?: number; readonly headChangedCssModuleContracts?: number;
257
- readonly parserEvidence?: CssSafeMergeParserEvidence; readonly selectorTargetEvidence?: CssSafeMergeSelectorTargetEvidence;
242
+ readonly parserEvidence?: CssSafeMergeParserEvidence; readonly selectorTargetEvidence?: CssSafeMergeSelectorTargetEvidence; readonly dependencyGraphEvidence?: CssDependencyGraphEvidence;
243
+ readonly cascadeRuntimeProofs?: readonly CssCascadeRuntimeProofRecord[];
258
244
  }
259
245
 
260
246
  export interface CssSafeMergeParserEvidence {
@@ -300,6 +286,14 @@ export interface CssSafeMergeInput {
300
286
  readonly cssModule?: boolean; readonly cssModules?: boolean;
301
287
  readonly generatedClassNameMap?: Readonly<Record<string, string>>;
302
288
  readonly generatedClassNameMapHash?: string; readonly jsTsUseSiteGraphHash?: string; readonly cssModuleCompositionGraphHash?: string; readonly icssGraphHash?: string; readonly scopedCascadeGraphHash?: string;
289
+ readonly cssCascadeRuntimeProof?: CssCascadeRuntimeProof; readonly cssCascadeRuntimeProofs?: readonly CssCascadeRuntimeProof[];
290
+ readonly cssCascadeRuntimeProofsByPath?: Readonly<Record<string, CssCascadeRuntimeProof | readonly CssCascadeRuntimeProof[]>>;
291
+ readonly cssSourceBoundCascadeProof?: CssCascadeRuntimeProof; readonly cssSourceBoundCascadeProofs?: readonly CssCascadeRuntimeProof[];
292
+ readonly cssSourceBoundCascadeProofsByPath?: Readonly<Record<string, CssCascadeRuntimeProof | readonly CssCascadeRuntimeProof[]>>;
293
+ readonly cascadeRuntimeProof?: CssCascadeRuntimeProof; readonly cascadeRuntimeProofs?: readonly CssCascadeRuntimeProof[];
294
+ readonly cascadeRuntimeProofsByPath?: Readonly<Record<string, CssCascadeRuntimeProof | readonly CssCascadeRuntimeProof[]>>;
295
+ readonly sourceBoundCascadeProof?: CssCascadeRuntimeProof; readonly sourceBoundCascadeProofs?: readonly CssCascadeRuntimeProof[];
296
+ readonly sourceBoundCascadeProofsByPath?: Readonly<Record<string, CssCascadeRuntimeProof | readonly CssCascadeRuntimeProof[]>>;
303
297
  readonly selectorTargetGraphHash?: string; readonly selectorTargetEquivalences?: readonly CssSelectorTargetEquivalence[];
304
298
  readonly baseGeneratedClassNameMap?: Readonly<Record<string, string>>; readonly workerGeneratedClassNameMap?: Readonly<Record<string, string>>; readonly headGeneratedClassNameMap?: Readonly<Record<string, string>>;
305
299
  readonly baseGeneratedClassNameMapHash?: string; readonly workerGeneratedClassNameMapHash?: string; readonly headGeneratedClassNameMapHash?: string;
package/dist/index.js CHANGED
@@ -1,12 +1,9 @@
1
1
  import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
2
2
  import { createCssModuleEvidence } from './css-modules.js';
3
+ import { createCssDependencyGraphEvidence } from './dependency-graph.js';
3
4
  import { parsePostcssSemanticRecords } from './postcss-parser-evidence.js';
4
5
  import { safeMergeCssSource as safeMergeCssSourceImpl } from './semantic-merge.js';
5
6
 
6
- const ShorthandProperties = new Set(['all', 'animation', 'background', 'border', 'border-block', 'border-color', 'border-image', 'border-inline', 'border-radius', 'border-style', 'border-width', 'columns', 'flex', 'font', 'gap', 'grid', 'grid-area', 'grid-column', 'grid-row', 'inset', 'list-style', 'margin', 'offset', 'outline', 'overflow', 'padding', 'place-content', 'place-items', 'place-self', 'text-decoration', 'transition']);
7
- const RuntimeAtRules = new Set(['keyframes', 'font-face', 'page', 'property']);
8
- const ScopeAtRules = new Set(['media', 'supports', 'container', 'layer', 'scope']);
9
-
10
7
  export function toCssAst(document, options = {}) {
11
8
  const banner = options.banner ?? 'Generated by @shapeshift-labs/frontier-lang-css.';
12
9
  const rules = [];
@@ -62,6 +59,7 @@ export function parseCssSemanticSheet(sourceText, options = {}) {
62
59
  const parsed = parsePostcssSemanticRecords(sourceText, sourceHash, options);
63
60
  const records = parsed.records;
64
61
  const cssModules = createCssModuleEvidence(records, options, sourceHash);
62
+ const dependencyGraphEvidence = createCssDependencyGraphEvidence(records, { ...options, sourceHash, hashSemanticValue });
65
63
  const proofGaps = [
66
64
  ...parsed.proofGaps,
67
65
  ...records.flatMap((record) => record.proofGaps ?? []),
@@ -74,7 +72,8 @@ export function parseCssSemanticSheet(sourceText, options = {}) {
74
72
  sourceHash,
75
73
  records,
76
74
  cssModules,
77
- sheetHash: hashSemanticValue({ kind: 'frontier.lang.cssSemanticSheet.records.v1', records: records.map(hashableCssRecord), cssModuleHash: cssModules?.moduleHash }),
75
+ dependencyGraphEvidence,
76
+ sheetHash: hashSemanticValue({ kind: 'frontier.lang.cssSemanticSheet.records.v1', records: records.map(hashableCssRecord), cssModuleHash: cssModules?.moduleHash, dependencyGraphHash: dependencyGraphEvidence.dependencyGraphHash }),
78
77
  summary: {
79
78
  rules: records.filter((record) => record.kind === 'rule').length,
80
79
  declarations: records.reduce((sum, record) => sum + (record.declarations?.length ?? 0), 0),
@@ -83,6 +82,13 @@ export function parseCssSemanticSheet(sourceText, options = {}) {
83
82
  cssModuleCompositions: cssModules?.compositions.length ?? 0,
84
83
  icssImports: cssModules?.icssImports.length ?? 0,
85
84
  icssExports: cssModules?.icssExports.length ?? 0,
85
+ dependencySurfaceCount: dependencyGraphEvidence.dependencySurfaceCount,
86
+ customPropertyDefinitions: dependencyGraphEvidence.customPropertyDefinitions,
87
+ customPropertyReferences: dependencyGraphEvidence.customPropertyReferences,
88
+ animationNameLinks: dependencyGraphEvidence.animationNameLinks,
89
+ keyframeDefinitions: dependencyGraphEvidence.keyframeDefinitions,
90
+ fontFaceLinks: dependencyGraphEvidence.fontFaceLinks,
91
+ urlAssetReferences: dependencyGraphEvidence.urlAssetReferences,
86
92
  proofGaps: proofGaps.length,
87
93
  parseErrors: parsed.parser.parseErrors.length
88
94
  },
@@ -102,6 +108,7 @@ export function createCssSemanticMergeEvidence(sourceText, options = {}) {
102
108
  sheetHash: sheet.sheetHash,
103
109
  records: sheet.records,
104
110
  cssModules: sheet.cssModules,
111
+ dependencyGraphEvidence: sheet.dependencyGraphEvidence,
105
112
  proofGaps: sheet.proofGaps,
106
113
  autoMergeClaim: false,
107
114
  semanticEquivalenceClaim: false,
@@ -116,163 +123,6 @@ export function safeMergeCssSource(input = {}) {
116
123
  return safeMergeCssSourceImpl(input, { parseCssSemanticSheet, hashSemanticValue });
117
124
  }
118
125
 
119
- function parseCssBlocks(sourceText, start, end, scopes, lineStarts, sourceHash, options) {
120
- const records = [];
121
- const blockRanges = [];
122
- let index = start;
123
- while (index < end) {
124
- const open = sourceText.indexOf('{', index);
125
- if (open < 0 || open >= end) break;
126
- const close = matchingBrace(sourceText, open, end);
127
- if (close < 0) break;
128
- const preludeStart = previousBoundary(sourceText, index, open);
129
- blockRanges.push([preludeStart, close + 1]);
130
- const prelude = sourceText.slice(preludeStart, open).replace(/\/\*[\s\S]*?\*\//g, '').trim();
131
- const body = sourceText.slice(open + 1, close);
132
- if (prelude.startsWith('@')) {
133
- const at = parseAtRule(prelude, preludeStart, close + 1, lineStarts, sourceHash, scopes, options);
134
- records.push(at);
135
- if (ScopeAtRules.has(at.atRuleName)) records.push(...parseCssBlocks(sourceText, open + 1, close, [...scopes, at.scopeKey], lineStarts, sourceHash, options));
136
- } else if (prelude) {
137
- records.push(cssRuleRecord(prelude, body, preludeStart, close + 1, lineStarts, sourceHash, scopes, options));
138
- }
139
- index = close + 1;
140
- }
141
- records.push(...parseAtRuleStatements(sourceText, start, end, scopes, lineStarts, sourceHash, options, blockRanges));
142
- return records.sort((left, right) => left.sourceSpan.startOffset - right.sourceSpan.startOffset);
143
- }
144
-
145
- function cssRuleRecord(prelude, body, start, end, lineStarts, sourceHash, scopes, options) {
146
- const selectors = prelude.split(',').map((selector) => selector.trim()).filter(Boolean);
147
- const declarations = parseDeclarations(body);
148
- const proofGaps = [
149
- ...declarations.filter((declaration) => ShorthandProperties.has(declaration.property)).map((declaration) => proofGap('css-shorthand-expansion-unproved', `CSS shorthand ${declaration.property} needs longhand expansion evidence.`)),
150
- ...scopes.length && !options.scopedCascadeGraphHash ? [proofGap('css-scoped-cascade-equivalence-unproved', 'Scoped cascade equivalence requires browser/style evidence.')] : []
151
- ];
152
- return compactRecord({
153
- kind: 'rule',
154
- selectors,
155
- selectorHash: hashSemanticValue({ kind: 'frontier.lang.css.selectors.v1', selectors }),
156
- specificity: selectors.map(selectorSpecificity),
157
- scopes,
158
- declarations: declarations.map((declaration, ordinal) => ({
159
- ...declaration,
160
- ordinal,
161
- cascadeKey: [...scopes, selectors.join(','), declaration.property].join('::'),
162
- declarationHash: hashSemanticValue({ kind: 'frontier.lang.css.declaration.v1', scopes, selectors, declaration })
163
- })),
164
- customProperties: declarations.filter((declaration) => declaration.property.startsWith('--')).map((declaration) => declaration.property),
165
- scopedCascadeGraphHash: scopes.length ? options.scopedCascadeGraphHash : undefined,
166
- sourceSpan: sourceSpan(start, end, lineStarts),
167
- sourceHash,
168
- ruleHash: hashSemanticValue({ kind: 'frontier.lang.css.rule.v1', selectors, scopes, declarations }),
169
- proofGaps: proofGaps.length ? proofGaps : undefined
170
- });
171
- }
172
-
173
- function parseAtRule(prelude, start, end, lineStarts, sourceHash, scopes, options) {
174
- const match = /^@([A-Za-z-]+)\s*([\s\S]*)$/.exec(prelude);
175
- const atRuleName = match?.[1]?.toLowerCase() ?? 'unknown';
176
- const conditionText = match?.[2]?.trim() ?? '';
177
- const proofGaps = [];
178
- if (RuntimeAtRules.has(atRuleName)) proofGaps.push(proofGap(`css-${atRuleName}-runtime-equivalence-unproved`, `CSS @${atRuleName} semantics require browser evidence.`));
179
- if (ScopeAtRules.has(atRuleName) && !options.scopedCascadeGraphHash) proofGaps.push(proofGap(`css-${atRuleName}-cascade-scope-unproved`, `CSS @${atRuleName} scoped cascade requires condition evaluation evidence.`));
180
- return compactRecord({
181
- kind: 'at-rule',
182
- atRuleName,
183
- conditionText,
184
- scopeKey: `@${atRuleName} ${conditionText}`.trim(),
185
- scopes,
186
- scopedCascadeGraphHash: ScopeAtRules.has(atRuleName) ? options.scopedCascadeGraphHash : undefined,
187
- sourceSpan: sourceSpan(start, end, lineStarts),
188
- sourceHash,
189
- atRuleHash: hashSemanticValue({ kind: 'frontier.lang.css.atRule.v1', atRuleName, conditionText, scopes }),
190
- proofGaps: proofGaps.length ? proofGaps : undefined
191
- });
192
- }
193
-
194
- function parseAtRuleStatements(sourceText, start, end, scopes, lineStarts, sourceHash, options, blockRanges) {
195
- const records = [];
196
- let index = start;
197
- while (index < end) {
198
- const semicolon = sourceText.indexOf(';', index);
199
- if (semicolon < 0 || semicolon >= end) break;
200
- const range = blockRanges.find(([rangeStart, rangeEnd]) => semicolon >= rangeStart && semicolon < rangeEnd);
201
- if (range) {
202
- index = range[1];
203
- continue;
204
- }
205
- const statementStart = previousBoundary(sourceText, index, semicolon);
206
- const statementText = sourceText.slice(statementStart, semicolon + 1).replace(/\/\*[\s\S]*?\*\//g, '').trim();
207
- if (statementText.startsWith('@') && !statementText.includes('{')) records.push(parseAtRuleStatement(statementText, statementStart, semicolon + 1, lineStarts, sourceHash, scopes, options));
208
- index = semicolon + 1;
209
- }
210
- return records;
211
- }
212
-
213
- function parseAtRuleStatement(statementText, start, end, lineStarts, sourceHash, scopes, options) {
214
- const body = statementText.replace(/;$/, '').trim();
215
- const match = /^@([A-Za-z-]+)\s*([\s\S]*)$/.exec(body);
216
- const atRuleName = match?.[1]?.toLowerCase() ?? 'unknown';
217
- const conditionText = match?.[2]?.trim() ?? '';
218
- const proofGaps = [];
219
- if (atRuleName === 'layer') proofGaps.push(proofGap('css-layer-order-statement-unsupported', 'CSS @layer statement order requires cascade order evidence.'));
220
- else proofGaps.push(proofGap(`css-${atRuleName}-statement-equivalence-unproved`, `CSS @${atRuleName} statement semantics require host evidence.`));
221
- return compactRecord({
222
- kind: 'at-rule-statement',
223
- atRuleName,
224
- conditionText,
225
- statementText,
226
- scopes,
227
- scopedCascadeGraphHash: ScopeAtRules.has(atRuleName) ? options.scopedCascadeGraphHash : undefined,
228
- sourceSpan: sourceSpan(start, end, lineStarts),
229
- sourceHash,
230
- atRuleHash: hashSemanticValue({ kind: 'frontier.lang.css.atRuleStatement.v1', atRuleName, conditionText, scopes, statementText }),
231
- proofGaps
232
- });
233
- }
234
-
235
- function parseDeclarations(body) {
236
- return body
237
- .replace(/\/\*[\s\S]*?\*\//g, '')
238
- .split(';')
239
- .map((part) => part.trim())
240
- .filter(Boolean)
241
- .flatMap((part) => {
242
- const separator = part.indexOf(':');
243
- if (separator <= 0) return [];
244
- const rawProperty = part.slice(0, separator).trim();
245
- const property = rawProperty.toLowerCase();
246
- const value = part.slice(separator + 1).trim();
247
- return [{ property, rawProperty, value, important: /!important\s*$/i.test(value), valueHash: hashSemanticValue({ kind: 'frontier.lang.css.value.v1', value }) }];
248
- });
249
- }
250
-
251
- function selectorSpecificity(selector) {
252
- const withoutStrings = selector.replace(/"[^"]*"|'[^']*'/g, '');
253
- const ids = (withoutStrings.match(/#[\w-]+/g) ?? []).length;
254
- const classes = (withoutStrings.match(/\.[\w-]+|\[[^\]]+\]|:(?!:)[\w-]+(?:\([^)]*\))?/g) ?? []).length;
255
- const elements = (withoutStrings.replace(/#[\w-]+|\.[\w-]+|\[[^\]]+\]|:{1,2}[\w-]+(?:\([^)]*\))?/g, ' ').match(/\b[A-Za-z][\w-]*\b/g) ?? []).length;
256
- return [ids, classes, elements];
257
- }
258
-
259
- function matchingBrace(text, open, end) {
260
- let depth = 0;
261
- for (let index = open; index < end; index += 1) {
262
- if (text[index] === '{') depth += 1;
263
- if (text[index] === '}') {
264
- depth -= 1;
265
- if (depth === 0) return index;
266
- }
267
- }
268
- return -1;
269
- }
270
-
271
- function previousBoundary(text, start, open) {
272
- const candidates = [text.lastIndexOf('}', open - 1), text.lastIndexOf(';', open - 1)].filter((value) => value >= start);
273
- return (candidates.length ? Math.max(...candidates) + 1 : start);
274
- }
275
-
276
126
  function sourceMapMapping(rule, index, startLine, endLine, options) {
277
127
  return compactRecord({
278
128
  id: `map_${idFragment(rule.sourceRef.semanticNodeId)}_${index}`,
@@ -290,22 +140,8 @@ function sourceMapEnvelope(ast, mappings, options) {
290
140
  return compactRecord({ kind: 'frontier.lang.sourceMap', version: 1, id: options.sourceMapId ?? `sourcemap_${idFragment(ast.kind)}_css`, sourcePath: options.sourcePath, sourceHash: options.sourceHash, target: { language: 'css' }, targetPath: options.targetPath, semanticIndexId: options.semanticIndexId, mappings, evidence: options.evidence ?? [], metadata: { precision: 'rule-block', generatedSpanStrategy: 'css-renderer-rule-block' } });
291
141
  }
292
142
 
293
- function sourceSpan(start, end, lineStarts) {
294
- const from = positionAt(start, lineStarts);
295
- const to = positionAt(end, lineStarts);
296
- return { startOffset: start, endOffset: end, startLine: from.line, startColumn: from.column, endLine: to.line, endColumn: to.column };
297
- }
298
-
299
- function positionAt(offset, lineStarts) {
300
- let line = 0;
301
- while (line + 1 < lineStarts.length && lineStarts[line + 1] <= offset) line += 1;
302
- return { line: line + 1, column: offset - lineStarts[line] + 1 };
303
- }
304
-
305
143
  function sourceRef(node, extra = {}) { return { semanticNodeId: node.id, semanticNodeKind: node.kind, semanticNodeName: node.name, ...extra }; }
306
- function proofGap(code, summary) { return { code, status: 'not-claimed', summary, failClosed: true, semanticEquivalenceClaim: false }; }
307
144
  function hashableCssRecord(record) { return { kind: record.kind, selectors: record.selectors, specificity: record.specificity, scopes: record.scopes, atRuleName: record.atRuleName, conditionText: record.conditionText, statementText: record.statementText, declarations: record.declarations?.map((item) => ({ property: item.property, value: item.value, important: item.important })), proofGaps: record.proofGaps?.map((gap) => gap.code) }; }
308
- function computeLineStarts(text) { const starts = [0]; for (let index = 0; index < text.length; index += 1) if (text[index] === '\n') starts.push(index + 1); return starts; }
309
145
  function cssIdentifier(value) { return String(value ?? 'unknown').replace(/[^A-Za-z0-9_-]/g, '-').replace(/^-+/, '') || 'unknown'; }
310
146
  function cssString(value) { return String(value ?? '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); }
311
147
  function typeName(type) { return typeof type === 'string' ? type : type?.name ?? type?.kind ?? 'unknown'; }
@@ -102,6 +102,7 @@ function postcssAtRuleRecord(node, scopes, sourceHash, options) {
102
102
  statementText: kind === 'at-rule-statement' ? rawText : undefined,
103
103
  scopeKey: postcssAtRuleScopeKey(node),
104
104
  scopes,
105
+ dependencyTokens: atRuleDependencyTokens(node, atRuleName),
105
106
  scopedCascadeGraphHash: ScopeAtRules.has(atRuleName) ? options.scopedCascadeGraphHash : undefined,
106
107
  sourceSpan: sourceSpanFromPostcss(node.source, options.sourcePath),
107
108
  sourceHash,
@@ -116,6 +117,26 @@ function postcssAtRuleScopeKey(node) {
116
117
  return `@${String(node.name ?? 'unknown').toLowerCase()} ${String(node.params ?? '').trim()}`.trim();
117
118
  }
118
119
 
120
+ function atRuleDependencyTokens(node, atRuleName) {
121
+ if (atRuleName !== 'font-face') return undefined;
122
+ const declarations = (node.nodes ?? []).filter((child) => child.type === 'decl');
123
+ const fontFamilies = declarations
124
+ .filter((declaration) => String(declaration.prop ?? '').toLowerCase() === 'font-family')
125
+ .flatMap((declaration) => fontFamilyNames(declaration.value));
126
+ const urls = declarations.flatMap((declaration) => cssUrlReferences(declaration.value));
127
+ return compactRecord({ fontFamilies: unique(fontFamilies), urls: unique(urls) });
128
+ }
129
+
130
+ function fontFamilyNames(value) {
131
+ return String(value ?? '').split(',').map((part) => part.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
132
+ }
133
+
134
+ function cssUrlReferences(value) {
135
+ return [...String(value ?? '').matchAll(/\burl\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*?))\s*\)/g)]
136
+ .map((match) => (match[1] ?? match[2] ?? match[3] ?? '').trim())
137
+ .filter(Boolean);
138
+ }
139
+
119
140
  function sourceSpanFromPostcss(source, fallbackPath) {
120
141
  const start = source?.start;
121
142
  const end = source?.end;
@@ -147,6 +168,7 @@ function selectorSpecificity(selector) {
147
168
  }
148
169
 
149
170
  function proofGap(code, summary) { return { code, status: 'not-claimed', summary, failClosed: true, semanticEquivalenceClaim: false }; }
171
+ function unique(values) { return [...new Set(values.filter(Boolean))]; }
150
172
  function compactRecord(record) { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)); }
151
173
 
152
174
  export { parsePostcssSemanticRecords };
@@ -0,0 +1,82 @@
1
+ function admitCascadeRuntimeProofs({ id, sourcePath, input, sourceShapeChanges, binding, hash }) {
2
+ const proofs = cascadeRuntimeProofCandidates(input, sourcePath);
3
+ const admitted = [];
4
+ const conflicts = [];
5
+ for (const change of sourceShapeChanges) {
6
+ const proof = proofs.find((candidate) => isCascadeRuntimeProofForChange(candidate, change, sourcePath, binding, hash));
7
+ if (proof) admitted.push(cascadeRuntimeProofRecord(proof, change, sourcePath, binding, hash));
8
+ else conflicts.push(conflict(id, sourcePath, 'css-source-shape-unsupported', change.reasonCode, change));
9
+ }
10
+ return { proofs: admitted, conflicts };
11
+ }
12
+
13
+ function cascadeRuntimeProofCandidates(input = {}, sourcePath) {
14
+ return [
15
+ input.cssCascadeRuntimeProof,
16
+ input.cssCascadeRuntimeProofs,
17
+ input.cssCascadeRuntimeProofsByPath?.[sourcePath],
18
+ input.cssSourceBoundCascadeProof,
19
+ input.cssSourceBoundCascadeProofs,
20
+ input.cssSourceBoundCascadeProofsByPath?.[sourcePath],
21
+ input.cascadeRuntimeProof,
22
+ input.cascadeRuntimeProofs,
23
+ input.cascadeRuntimeProofsByPath?.[sourcePath],
24
+ input.sourceBoundCascadeProof,
25
+ input.sourceBoundCascadeProofs,
26
+ input.sourceBoundCascadeProofsByPath?.[sourcePath]
27
+ ].flatMap(asArray).filter(Boolean);
28
+ }
29
+
30
+ function isCascadeRuntimeProofForChange(proof, change, sourcePath, binding, hash) {
31
+ return Boolean(proof && typeof proof === 'object') &&
32
+ CascadeRuntimeProofKinds.has(proof.kind) &&
33
+ proof.status === 'passed' &&
34
+ proof.sourcePath === sourcePath &&
35
+ proofCoversValue(proof.reasonCode, proof.reasonCodes, change.reasonCode) &&
36
+ proofCoversValue(proof.side, proof.sides, change.side) &&
37
+ proofCoversValue(proof.shapeKey, proof.shapeKeys, change.shapeKey) &&
38
+ cascadeProofSourceMatches(proof, 'base', binding.base, hash) &&
39
+ cascadeProofSourceMatches(proof, 'worker', binding.worker, hash) &&
40
+ cascadeProofSourceMatches(proof, 'head', binding.head, hash) &&
41
+ cascadeProofSourceMatches(proof, 'output', binding.output, hash);
42
+ }
43
+
44
+ function cascadeProofSourceMatches(proof, role, sourceText, hash) {
45
+ if (typeof sourceText !== 'string') return false;
46
+ const sourceHash = hash?.(sourceText);
47
+ const textFields = role === 'output' ? ['outputSourceText', 'mergedSourceText'] : [`${role}SourceText`];
48
+ const hashFields = role === 'output' ? ['outputSourceHash', 'mergedSourceHash'] : [`${role}SourceHash`];
49
+ const aliases = role === 'output' ? ['output', 'merged'] : [role];
50
+ return textFields.some((field) => proof[field] === sourceText) ||
51
+ aliases.some((alias) => proof.sourceTexts?.[alias] === sourceText || proof.sources?.[alias] === sourceText) ||
52
+ hashFields.some((field) => sourceHash !== undefined && proof[field] === sourceHash) ||
53
+ aliases.some((alias) => sourceHash !== undefined && (proof.sourceHashes?.[alias] === sourceHash || proof.hashes?.[alias] === sourceHash));
54
+ }
55
+
56
+ function cascadeRuntimeProofRecord(proof, change, sourcePath, binding, hash) {
57
+ return {
58
+ id: proof.id,
59
+ kind: proof.kind,
60
+ status: 'passed',
61
+ proofLevel: proof.proofLevel ?? 'css-cascade-runtime-source-bound',
62
+ reasonCode: change.reasonCode,
63
+ side: change.side,
64
+ shapeKey: change.shapeKey,
65
+ sourcePath,
66
+ baseSourceHash: hash?.(binding.base),
67
+ workerSourceHash: hash?.(binding.worker),
68
+ headSourceHash: hash?.(binding.head),
69
+ outputSourceHash: hash?.(binding.output)
70
+ };
71
+ }
72
+
73
+ function conflict(id, sourcePath, code, reasonCode, details = {}) {
74
+ return { code, gateId: 'css-semantic-merge', sourcePath, details: { reasonCode, conflictKey: `css#${id}#${reasonCode}#${details.cascadeKey ?? details.shapeKey ?? sourcePath ?? 'source'}`, ...details } };
75
+ }
76
+
77
+ function proofCoversValue(value, values, expected) { return value === expected || (Array.isArray(values) && values.includes(expected)); }
78
+ function asArray(value) { return Array.isArray(value) ? value : value === undefined ? [] : [value]; }
79
+
80
+ const CascadeRuntimeProofKinds = new Set(['css-cascade-runtime-proof', 'css-source-bound-cascade-runtime-proof']);
81
+
82
+ export { admitCascadeRuntimeProofs };
@@ -131,14 +131,18 @@ function cssModuleOverlapConflicts(id, sourcePath, workerChanges, headChanges) {
131
131
  }
132
132
 
133
133
  function unsupportedSourceShapeConflicts(id, sourcePath, sheets, declarationChanges, hash) {
134
- const sourceShapeChanges = {
135
- worker: unsupportedSourceShapeChanges(sheets.base, sheets.worker, declarationChanges.worker, 'worker', hash),
136
- head: unsupportedSourceShapeChanges(sheets.base, sheets.head, declarationChanges.head, 'head', hash)
137
- };
138
- return [...sourceShapeChanges.worker, ...sourceShapeChanges.head].map((change) => conflict(id, sourcePath, 'css-source-shape-unsupported', change.reasonCode, change));
134
+ return unsupportedSourceShapeChanges(sheets, declarationChanges, hash)
135
+ .map((change) => conflict(id, sourcePath, 'css-source-shape-unsupported', change.reasonCode, change));
136
+ }
137
+
138
+ function unsupportedSourceShapeChanges(sheets, declarationChanges, hash) {
139
+ return [
140
+ ...unsupportedSourceShapeChangesForSide(sheets.base, sheets.worker, declarationChanges.worker, 'worker', hash),
141
+ ...unsupportedSourceShapeChangesForSide(sheets.base, sheets.head, declarationChanges.head, 'head', hash)
142
+ ];
139
143
  }
140
144
 
141
- function unsupportedSourceShapeChanges(baseSheet, currentSheet, declarationChanges, side, hash) {
145
+ function unsupportedSourceShapeChangesForSide(baseSheet, currentSheet, declarationChanges, side, hash) {
142
146
  const baseShape = sourceShapeIndex(baseSheet, hash);
143
147
  const currentShape = sourceShapeIndex(currentSheet, hash);
144
148
  const keys = unique([...baseShape.keys(), ...currentShape.keys()]);
@@ -226,4 +230,4 @@ function uniqueProofGaps(values) {
226
230
  return [...byCode.values()];
227
231
  }
228
232
 
229
- export { cssModuleContractChanges, cssModuleContractConflicts, sheetOptions, unsupportedSourceShapeConflicts };
233
+ export { cssModuleContractChanges, cssModuleContractConflicts, sheetOptions, unsupportedSourceShapeChanges, unsupportedSourceShapeConflicts };
@@ -0,0 +1,16 @@
1
+ function shorthandGroupForProperty(property) {
2
+ if (ShorthandGroups.has(property)) return property;
3
+ for (const [group, longhands] of ShorthandGroups) if (longhands.includes(property)) return group;
4
+ return undefined;
5
+ }
6
+
7
+ const ShorthandGroups = new Map([
8
+ ['background', ['background-attachment', 'background-clip', 'background-color', 'background-image', 'background-origin', 'background-position', 'background-repeat', 'background-size']],
9
+ ['border', ['border-color', 'border-style', 'border-width', 'border-top', 'border-right', 'border-bottom', 'border-left']],
10
+ ['font', ['font-family', 'font-size', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'line-height']],
11
+ ['list-style', ['list-style-image', 'list-style-position', 'list-style-type']],
12
+ ['margin', ['margin-top', 'margin-right', 'margin-bottom', 'margin-left']],
13
+ ['padding', ['padding-top', 'padding-right', 'padding-bottom', 'padding-left']]
14
+ ]);
15
+
16
+ export { shorthandGroupForProperty };
@@ -1,5 +1,8 @@
1
- import { cssModuleContractChanges, cssModuleContractConflicts, sheetOptions, unsupportedSourceShapeConflicts } from './semantic-merge-css-modules.js';
1
+ import { cssModuleContractChanges, cssModuleContractConflicts, sheetOptions, unsupportedSourceShapeChanges } from './semantic-merge-css-modules.js';
2
+ import { admitCascadeRuntimeProofs } from './semantic-merge-cascade-runtime.js';
3
+ import { mergeCssDependencyGraphEvidence } from './dependency-graph.js';
2
4
  import { mergeSelectorTargetEvidence, planSelectorTargetRebase } from './semantic-merge-selector-targets.js';
5
+ import { shorthandGroupForProperty } from './semantic-merge-shorthand.js';
3
6
 
4
7
  function safeMergeCssSource(input = {}, context = {}) {
5
8
  const parseSheet = context.parseCssSemanticSheet;
@@ -29,13 +32,23 @@ function safeMergeCssSource(input = {}, context = {}) {
29
32
  ...shorthandOverlapConflicts(id, sourcePath, changed.worker, changed.head)
30
33
  ];
31
34
  const moduleConflicts = cssModuleContractConflicts(id, sourcePath, moduleChanges);
32
- const sourceShapeConflicts = unsupportedSourceShapeConflicts(id, sourcePath, sheets, changed, hash);
33
35
  const parserEvidence = mergeParserEvidence(sheets);
36
+ const dependencyGraphEvidence = mergeCssDependencyGraphEvidence(sheets, changed);
34
37
  const selectorTargetPlan = planSelectorTargetRebase(id, sourcePath, mergeSelectorTargetEvidence(sheets, changed), changed, input);
35
- const conflicts = [...parserConflicts, ...proofConflicts, ...overlapConflicts, ...moduleConflicts, ...sourceShapeConflicts, ...selectorTargetPlan.conflicts];
36
- if (conflicts.length) return blocked(id, sourcePath, 'css-semantic-merge-conflict', conflicts, { parserEvidence, selectorTargetEvidence: selectorTargetPlan.evidence });
38
+ const shapeChanges = unsupportedSourceShapeChanges(sheets, changed, hash);
37
39
  const mergedIndex = applyDeclarationChanges(applyDeclarationChanges(indexes.base, selectorTargetPlan.changed.head), selectorTargetPlan.changed.worker);
38
- return merged(id, sourcePath, renderDeclarationIndex(mergedIndex), 'semantic-declaration-merge', hash, {
40
+ const mergedSourceText = renderDeclarationIndex(mergedIndex);
41
+ const cascadeRuntimeAdmission = admitCascadeRuntimeProofs({
42
+ id,
43
+ sourcePath,
44
+ input,
45
+ sourceShapeChanges: shapeChanges,
46
+ binding: { base, worker, head, output: mergedSourceText },
47
+ hash
48
+ });
49
+ const conflicts = [...parserConflicts, ...proofConflicts, ...overlapConflicts, ...moduleConflicts, ...cascadeRuntimeAdmission.conflicts, ...selectorTargetPlan.conflicts];
50
+ if (conflicts.length) return blocked(id, sourcePath, 'css-semantic-merge-conflict', conflicts, { parserEvidence, dependencyGraphEvidence, selectorTargetEvidence: selectorTargetPlan.evidence, cascadeRuntimeProofs: cascadeRuntimeAdmission.proofs });
51
+ return merged(id, sourcePath, mergedSourceText, 'semantic-declaration-merge', hash, {
39
52
  baseSheetHash: sheets.base.sheetHash,
40
53
  workerSheetHash: sheets.worker.sheetHash,
41
54
  headSheetHash: sheets.head.sheetHash,
@@ -44,7 +57,10 @@ function safeMergeCssSource(input = {}, context = {}) {
44
57
  workerChangedCssModuleContracts: moduleChanges.worker.length,
45
58
  headChangedCssModuleContracts: moduleChanges.head.length,
46
59
  parserEvidence,
47
- selectorTargetEvidence: selectorTargetPlan.evidence
60
+ dependencyGraphEvidence,
61
+ selectorTargetEvidence: selectorTargetPlan.evidence,
62
+ cascadeRuntimeProofs: cascadeRuntimeAdmission.proofs,
63
+ browserCascadeEquivalenceClaim: cascadeRuntimeAdmission.proofs.length > 0
48
64
  });
49
65
  }
50
66
 
@@ -264,20 +280,25 @@ function blocked(id, sourcePath, reasonCode, conflicts = [], extra = {}) {
264
280
  }
265
281
 
266
282
  function result(id, sourcePath, status, body) {
283
+ const browserCascadeEquivalenceClaim = status === 'merged' && body.browserCascadeEquivalenceClaim === true;
267
284
  return {
268
285
  kind: 'frontier.lang.cssSafeMerge',
269
286
  version: 1,
270
287
  id,
271
288
  sourcePath,
272
289
  status,
290
+ ...body,
273
291
  autoMergeClaim: false,
274
292
  semanticEquivalenceClaim: false,
275
- ...body,
293
+ browserCascadeEquivalenceClaim,
294
+ browserRenderEquivalenceClaim: false,
276
295
  admission: {
277
296
  status: status === 'merged' ? 'auto-merge-candidate' : 'blocked',
278
297
  action: status === 'merged' ? 'apply-css' : 'human-review',
279
298
  reviewRequired: status !== 'merged',
280
- reasonCodes: unique((body.conflicts ?? []).map((item) => item.details.reasonCode))
299
+ reasonCodes: unique((body.conflicts ?? []).map((item) => item.details.reasonCode)),
300
+ browserCascadeEquivalenceClaim: browserCascadeEquivalenceClaim || undefined,
301
+ cssCascadeRuntimeProofs: body.cascadeRuntimeProofs?.length ? body.cascadeRuntimeProofs : undefined
281
302
  }
282
303
  };
283
304
  }
@@ -295,19 +316,4 @@ function proofGapsForDeclaration(record, declaration) {
295
316
  function unique(values) { return [...new Set(values.filter(Boolean))]; }
296
317
  function spaces(count) { return ' '.repeat(Math.max(0, count)); }
297
318
 
298
- function shorthandGroupForProperty(property) {
299
- if (ShorthandGroups.has(property)) return property;
300
- for (const [group, longhands] of ShorthandGroups) if (longhands.includes(property)) return group;
301
- return undefined;
302
- }
303
-
304
- const ShorthandGroups = new Map([
305
- ['background', ['background-attachment', 'background-clip', 'background-color', 'background-image', 'background-origin', 'background-position', 'background-repeat', 'background-size']],
306
- ['border', ['border-color', 'border-style', 'border-width', 'border-top', 'border-right', 'border-bottom', 'border-left']],
307
- ['font', ['font-family', 'font-size', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'line-height']],
308
- ['list-style', ['list-style-image', 'list-style-position', 'list-style-type']],
309
- ['margin', ['margin-top', 'margin-right', 'margin-bottom', 'margin-left']],
310
- ['padding', ['padding-top', 'padding-right', 'padding-bottom', 'padding-left']]
311
- ]);
312
-
313
319
  export { safeMergeCssSource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-css",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "CSS semantic merge evidence and projection adapter for Frontier Lang semantic source documents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",