@usefragments/core 1.0.0
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/LICENSE +21 -0
- package/dist/chunk-243QYRUF.js +2145 -0
- package/dist/chunk-243QYRUF.js.map +1 -0
- package/dist/chunk-3LLRNCPX.js +111 -0
- package/dist/chunk-3LLRNCPX.js.map +1 -0
- package/dist/chunk-EIYNNS77.js +48 -0
- package/dist/chunk-EIYNNS77.js.map +1 -0
- package/dist/chunk-JNBFJ34I.js +326 -0
- package/dist/chunk-JNBFJ34I.js.map +1 -0
- package/dist/chunk-R3X7UBHU.js +228 -0
- package/dist/chunk-R3X7UBHU.js.map +1 -0
- package/dist/chunk-SH4KPIYH.js +393 -0
- package/dist/chunk-SH4KPIYH.js.map +1 -0
- package/dist/chunk-V4VQB57N.js +56 -0
- package/dist/chunk-V4VQB57N.js.map +1 -0
- package/dist/chunk-X34IA4LR.js +319 -0
- package/dist/chunk-X34IA4LR.js.map +1 -0
- package/dist/chunk-YF65VYRY.js +493 -0
- package/dist/chunk-YF65VYRY.js.map +1 -0
- package/dist/codes/index.d.ts +40 -0
- package/dist/codes/index.js +23 -0
- package/dist/codes/index.js.map +1 -0
- package/dist/compiled-types/index.d.ts +5 -0
- package/dist/compiled-types/index.js +1 -0
- package/dist/compiled-types/index.js.map +1 -0
- package/dist/generate/index.d.ts +47 -0
- package/dist/generate/index.js +11 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/governance-BOa3KyiJ.d.ts +3279 -0
- package/dist/graph/index.d.ts +66 -0
- package/dist/graph/index.js +17 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +11388 -0
- package/dist/index.js +8711 -0
- package/dist/index.js.map +1 -0
- package/dist/package-identity.d.ts +42 -0
- package/dist/package-identity.js +23 -0
- package/dist/package-identity.js.map +1 -0
- package/dist/preview/index.d.ts +484 -0
- package/dist/preview/index.js +1129 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/react-types.d.ts +22 -0
- package/dist/react-types.js +1 -0
- package/dist/react-types.js.map +1 -0
- package/dist/registry.d.ts +2367 -0
- package/dist/registry.js +62 -0
- package/dist/registry.js.map +1 -0
- package/dist/schemas/index.d.ts +2584 -0
- package/dist/schemas/index.js +55 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/severity.d.ts +18 -0
- package/dist/severity.js +27 -0
- package/dist/severity.js.map +1 -0
- package/dist/test-utils.d.ts +14 -0
- package/dist/test-utils.js +63 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/topology/index.d.ts +48 -0
- package/dist/topology/index.js +7 -0
- package/dist/topology/index.js.map +1 -0
- package/dist/types-xJ2xyp_G.d.ts +119 -0
- package/package.json +120 -0
- package/src/__tests__/contract-parser.test.ts +614 -0
- package/src/__tests__/dtcg-generators.test.ts +439 -0
- package/src/__tests__/dtcg-parser.test.ts +884 -0
- package/src/__tests__/preview-runtime.test.tsx +111 -0
- package/src/__tests__/schema.test.ts +31 -0
- package/src/__tests__/severity.test.ts +34 -0
- package/src/__tests__/style-comparison.test.ts +662 -0
- package/src/__tests__/tokens-compat.test.ts +51 -0
- package/src/agent-format.test.ts +105 -0
- package/src/agent-format.ts +141 -0
- package/src/bundle.ts +149 -0
- package/src/codes/__tests__/codes.test.ts +73 -0
- package/src/codes/codes.ts +463 -0
- package/src/codes/index.ts +7 -0
- package/src/codes/source-finding-bridge.ts +113 -0
- package/src/compiled-types/index.ts +293 -0
- package/src/component-contract.ts +231 -0
- package/src/component-discovery.ts +80 -0
- package/src/composition.test.ts +262 -0
- package/src/composition.ts +318 -0
- package/src/config-paths.test.ts +78 -0
- package/src/config-paths.ts +47 -0
- package/src/config.ts +18 -0
- package/src/conform-prove.test.ts +274 -0
- package/src/conform-prove.ts +326 -0
- package/src/conform.ts +160 -0
- package/src/constants.ts +114 -0
- package/src/context.ts +2 -0
- package/src/contract/fixture.ts +110 -0
- package/src/contract/hash.test.ts +86 -0
- package/src/contract/hash.ts +195 -0
- package/src/contract/index.ts +43 -0
- package/src/contract/preimage.test.ts +254 -0
- package/src/contract/preimage.ts +365 -0
- package/src/contract/stamp.test.ts +100 -0
- package/src/contract/stamp.ts +105 -0
- package/src/contract-parser.ts +141 -0
- package/src/defineFragment.test.ts +257 -0
- package/src/defineFragment.ts +293 -0
- package/src/dtcg-generators.ts +324 -0
- package/src/dtcg-parser.ts +1 -0
- package/src/dtcg.ts +237 -0
- package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
- package/src/facts/builders.ts +835 -0
- package/src/facts/compile.ts +422 -0
- package/src/facts/fact-index.ts +394 -0
- package/src/facts/facts.test.ts +494 -0
- package/src/facts/ids.ts +125 -0
- package/src/facts/index.ts +119 -0
- package/src/facts/types.ts +614 -0
- package/src/figma.ts +263 -0
- package/src/fragment-types.ts +214 -0
- package/src/generate/generate.test.ts +594 -0
- package/src/generate/index.ts +438 -0
- package/src/governance-integrity.test.ts +186 -0
- package/src/governance-integrity.ts +282 -0
- package/src/governance.test.ts +300 -0
- package/src/governance.ts +605 -0
- package/src/governance.type-test.ts +75 -0
- package/src/graph/engine.ts +446 -0
- package/src/graph/graph.test.ts +746 -0
- package/src/graph/index.ts +32 -0
- package/src/graph/serialization.ts +55 -0
- package/src/graph/types.ts +178 -0
- package/src/index.ts +891 -0
- package/src/local-canonical.ts +102 -0
- package/src/mcp-snapshot.ts +222 -0
- package/src/package-identity-match.ts +33 -0
- package/src/package-identity.generated.ts +233 -0
- package/src/package-identity.test.ts +99 -0
- package/src/package-identity.ts +157 -0
- package/src/performance-presets.ts +142 -0
- package/src/preview/comparison-artifact.test.ts +228 -0
- package/src/preview/comparison-artifact.ts +181 -0
- package/src/preview/comparison-context.ts +93 -0
- package/src/preview/comparison-evidence.ts +112 -0
- package/src/preview/comparison-format.ts +21 -0
- package/src/preview/comparison-presentation.ts +187 -0
- package/src/preview/comparison-references.test.ts +430 -0
- package/src/preview/comparison-references.ts +72 -0
- package/src/preview/comparison-score.ts +59 -0
- package/src/preview/comparison-styles.ts +197 -0
- package/src/preview/comparison.test.ts +559 -0
- package/src/preview/comparison.ts +488 -0
- package/src/preview/index.ts +10 -0
- package/src/preview/types.ts +309 -0
- package/src/preview/validation.test.ts +290 -0
- package/src/preview/validation.ts +356 -0
- package/src/preview-runtime.tsx +144 -0
- package/src/raw-html-canonical.test.ts +41 -0
- package/src/raw-html-canonical.ts +179 -0
- package/src/react-types.ts +21 -0
- package/src/registry-install-plan.ts +344 -0
- package/src/registry.test.ts +245 -0
- package/src/registry.ts +369 -0
- package/src/rules/a11y-required-accessible-name.ts +93 -0
- package/src/rules/color-math.test.ts +72 -0
- package/src/rules/color-math.ts +192 -0
- package/src/rules/components-forbidden-prop-value.ts +108 -0
- package/src/rules/components-prefer-library.ts +943 -0
- package/src/rules/components-unknown-prop.ts +94 -0
- package/src/rules/composition-pattern-schema.ts +125 -0
- package/src/rules/composition-pattern.test.ts +320 -0
- package/src/rules/composition-pattern.ts +271 -0
- package/src/rules/emit-gate.test.ts +42 -0
- package/src/rules/emit-gate.ts +60 -0
- package/src/rules/finding.ts +54 -0
- package/src/rules/index.ts +235 -0
- package/src/rules/jsx-preferred-component.ts +68 -0
- package/src/rules/jsx-preferred-import-path.ts +67 -0
- package/src/rules/presets.ts +57 -0
- package/src/rules/props-invalid-value.ts +115 -0
- package/src/rules/rules.test.ts +3636 -0
- package/src/rules/spacing-resolution.ts +184 -0
- package/src/rules/styles-no-raw-color.ts +329 -0
- package/src/rules/styles-no-raw-dimensions.ts +357 -0
- package/src/rules/styles-no-raw-spacing.ts +236 -0
- package/src/rules/styles-no-raw-typography.ts +237 -0
- package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
- package/src/rules/tailwind-arbitrary-color.ts +93 -0
- package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
- package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
- package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
- package/src/rules/tailwind-forbidden-palette.ts +107 -0
- package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
- package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.ts +82 -0
- package/src/rules/tailwind-rule-utils.ts +106 -0
- package/src/rules/tailwind-unknown-class.test.ts +157 -0
- package/src/rules/tailwind-unknown-class.ts +121 -0
- package/src/rules/taxonomy.test.ts +238 -0
- package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
- package/src/rules/tiers.ts +111 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
- package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
- package/src/rules/tokens-require-dual-fallback.ts +78 -0
- package/src/rules/types.ts +27 -0
- package/src/rules/utils.test.ts +110 -0
- package/src/rules/utils.ts +599 -0
- package/src/schema.ts +427 -0
- package/src/schemas/index.ts +275 -0
- package/src/schemas/schemas.test.ts +124 -0
- package/src/severity.ts +54 -0
- package/src/storyAdapter.test.ts +571 -0
- package/src/storyAdapter.ts +761 -0
- package/src/storyFilters.test.ts +350 -0
- package/src/storyFilters.ts +253 -0
- package/src/storybook-csf.ts +11 -0
- package/src/style-comparison.ts +423 -0
- package/src/test-utils.ts +68 -0
- package/src/token-parser.ts +2 -0
- package/src/token-types.ts +287 -0
- package/src/tokens/categories.ts +131 -0
- package/src/tokens/color.ts +193 -0
- package/src/tokens/design-token-parser.test.ts +159 -0
- package/src/tokens/design-token-parser.ts +402 -0
- package/src/tokens/dtcg.ts +428 -0
- package/src/tokens/index.ts +130 -0
- package/src/tokens/lookup.ts +19 -0
- package/src/tokens/scss.ts +171 -0
- package/src/tokens/tailwind-v4.ts +187 -0
- package/src/tokens/types.ts +43 -0
- package/src/topology/index.ts +9 -0
- package/src/topology/resolve-area.test.ts +174 -0
- package/src/topology/resolve-area.ts +215 -0
- package/src/types.ts +1057 -0
package/src/conform.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared contract for the design-system conform engine.
|
|
3
|
+
*
|
|
4
|
+
* These types are the single source of truth for the conform pipeline:
|
|
5
|
+
* - The private Engine implements `conformSnippet(input, context)`.
|
|
6
|
+
* - The private hosted MCP runtime re-exports the result types as its tool contract.
|
|
7
|
+
* - Cloud / CLI / standalone-MCP each build a `DesignSystemContext` from their
|
|
8
|
+
* own data source (Convex catalog, local config, bundle) and call the engine.
|
|
9
|
+
*
|
|
10
|
+
* Keeping them here (a browser-safe, dependency-light package) lets every
|
|
11
|
+
* surface agree on one shape without duplication.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type ConformConfidence = "high" | "medium" | "low";
|
|
15
|
+
|
|
16
|
+
export interface ConformInput {
|
|
17
|
+
/** UI code to conform: JSX/TSX, HTML, or a CSS/SCSS block. */
|
|
18
|
+
code: string;
|
|
19
|
+
/** Filename or extension hint used to pick the parser (defaults to TSX). */
|
|
20
|
+
filename?: string;
|
|
21
|
+
/** deterministic (default) applies safe edits; none returns findings only. */
|
|
22
|
+
apply?: "deterministic" | "none";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ConformLocation {
|
|
26
|
+
line?: number;
|
|
27
|
+
column?: number;
|
|
28
|
+
endLine?: number;
|
|
29
|
+
endColumn?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ConformChange {
|
|
33
|
+
kind: "swap-component" | "rewrite-import" | "rename-prop" | "replace-token" | "add-fallback";
|
|
34
|
+
ruleId?: string;
|
|
35
|
+
/** Versioned diagnostic code, e.g. FUI0601. */
|
|
36
|
+
code?: string;
|
|
37
|
+
severity: "error" | "warning" | "info";
|
|
38
|
+
message: string;
|
|
39
|
+
before?: string;
|
|
40
|
+
after?: string;
|
|
41
|
+
/** Canonical primitive involved in the change, when applicable. */
|
|
42
|
+
canonical?: string;
|
|
43
|
+
confidence: ConformConfidence;
|
|
44
|
+
location?: ConformLocation;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ConformSuggestion {
|
|
48
|
+
kind:
|
|
49
|
+
| "map-prop-value"
|
|
50
|
+
| "choose-among-alternates"
|
|
51
|
+
| "review-low-confidence"
|
|
52
|
+
| "flag-accessibility";
|
|
53
|
+
message: string;
|
|
54
|
+
canonical?: string;
|
|
55
|
+
hint?: string;
|
|
56
|
+
confidence: ConformConfidence;
|
|
57
|
+
location?: ConformLocation;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ConformUnresolved {
|
|
61
|
+
element?: string;
|
|
62
|
+
canonical?: string;
|
|
63
|
+
reason: string;
|
|
64
|
+
location?: ConformLocation;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ConformPropMapping {
|
|
68
|
+
rawProp: string;
|
|
69
|
+
canonicalProp: string;
|
|
70
|
+
/**
|
|
71
|
+
* Confirmed value translation table for cross-library prop values, e.g.
|
|
72
|
+
* `{ primary: "brand" }` to rewrite `color="primary"` into `variant="brand"`.
|
|
73
|
+
* A value is only rewritten when it exactly matches a key here; anything else
|
|
74
|
+
* is surfaced as a `map-prop-value` suggestion rather than guessed.
|
|
75
|
+
*/
|
|
76
|
+
valueMap?: Record<string, string>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ConformComponentMapping {
|
|
80
|
+
/**
|
|
81
|
+
* Canonical primitive name, e.g. "Button". This is the JSX identifier the
|
|
82
|
+
* conform engine rewrites toward.
|
|
83
|
+
*/
|
|
84
|
+
name: string;
|
|
85
|
+
/** Stable canonical vocabulary label when it differs from the component name. */
|
|
86
|
+
canonical?: string;
|
|
87
|
+
/** Native HTML element this component replaces, e.g. "button". */
|
|
88
|
+
htmlEquivalent?: string;
|
|
89
|
+
/** Import source to add or prefer for the canonical component. */
|
|
90
|
+
importPath?: string;
|
|
91
|
+
/** Confirmed prop renames from raw usage to the canonical component API. */
|
|
92
|
+
propMapping?: ConformPropMapping[];
|
|
93
|
+
/** Classifier confidence, when known. Only confirmed mappings should be passed. */
|
|
94
|
+
confidence?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ConformResult {
|
|
98
|
+
/** Code with deterministic edits applied, or the original if apply=none / parse failed. */
|
|
99
|
+
conformed: string;
|
|
100
|
+
changed: boolean;
|
|
101
|
+
summary: string;
|
|
102
|
+
designSystem?: {
|
|
103
|
+
name?: string;
|
|
104
|
+
importsAdded: string[];
|
|
105
|
+
};
|
|
106
|
+
/** Applied, deterministic edits. */
|
|
107
|
+
changes: ConformChange[];
|
|
108
|
+
/** Items needing agent judgment (e.g. cross-library prop-value translation). */
|
|
109
|
+
suggestions: ConformSuggestion[];
|
|
110
|
+
/** Elements/values with no design-system equivalent. */
|
|
111
|
+
unresolved: ConformUnresolved[];
|
|
112
|
+
/**
|
|
113
|
+
* How much of the tenant's design system this run could evaluate against.
|
|
114
|
+
* `evaluated` is false when the catalog supplied no tokens and no usable
|
|
115
|
+
* component mappings — in that case a "clean" result proves nothing, so
|
|
116
|
+
* callers (e.g. `prove_compliant`) must not certify compliance.
|
|
117
|
+
*/
|
|
118
|
+
coverage?: ConformCoverage;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ConformCoverage {
|
|
122
|
+
/** Count of relevant tokens (color/spacing/radius) the engine evaluated against. */
|
|
123
|
+
tokens: number;
|
|
124
|
+
/** Count of usable component mappings the engine evaluated against. */
|
|
125
|
+
components: number;
|
|
126
|
+
/** True when there was at least one token or component to evaluate against. */
|
|
127
|
+
evaluated: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A design-system token as the conform engine consumes it. Each surface maps
|
|
132
|
+
* its own token model into this shape.
|
|
133
|
+
*/
|
|
134
|
+
export interface ConformToken {
|
|
135
|
+
/** Raw value to match against code, e.g. "#2563eb". */
|
|
136
|
+
value: string;
|
|
137
|
+
/**
|
|
138
|
+
* The authored CSS custom property, e.g. "--brand-600", when known. A token
|
|
139
|
+
* is only eligible for an automatic rewrite when this is present — the engine
|
|
140
|
+
* never guesses a variable name.
|
|
141
|
+
*/
|
|
142
|
+
cssVar?: string;
|
|
143
|
+
/** Token category, e.g. "color" | "spacing". */
|
|
144
|
+
category?: string;
|
|
145
|
+
/** Human-readable label for suggestions, e.g. "color.brand.600". */
|
|
146
|
+
label?: string;
|
|
147
|
+
/** Tie-break ordering when two tokens share a value: "canonical" wins. */
|
|
148
|
+
tier?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Everything the conform engine needs about a tenant's design system. Built by
|
|
153
|
+
* each surface (cloud from Convex, CLI from local config, MCP from a bundle).
|
|
154
|
+
*/
|
|
155
|
+
export interface DesignSystemContext {
|
|
156
|
+
designSystemName?: string;
|
|
157
|
+
tokens: ConformToken[];
|
|
158
|
+
/** Confirmed canonical component replacements for this tenant. */
|
|
159
|
+
components?: ConformComponentMapping[];
|
|
160
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand constants for easy rebranding if domain availability requires it.
|
|
3
|
+
* All naming throughout the codebase should reference these constants.
|
|
4
|
+
*/
|
|
5
|
+
export const BRAND = {
|
|
6
|
+
/** Display name (e.g., "Fragments") */
|
|
7
|
+
name: "Fragments",
|
|
8
|
+
|
|
9
|
+
/** Lowercase name for file paths and CLI (e.g., "fragments") */
|
|
10
|
+
nameLower: "fragments",
|
|
11
|
+
|
|
12
|
+
/** File extension for fragment definition files — V2 canonical format */
|
|
13
|
+
fileExtension: ".contract.json",
|
|
14
|
+
|
|
15
|
+
/** Legacy file extension for segments (still supported for migration) */
|
|
16
|
+
legacyFileExtension: ".segment.tsx",
|
|
17
|
+
|
|
18
|
+
/** JSON file extension for compiled output */
|
|
19
|
+
jsonExtension: ".fragment.json",
|
|
20
|
+
|
|
21
|
+
/** Default output file name (e.g., "fragments.json") */
|
|
22
|
+
outFile: "fragments.json",
|
|
23
|
+
|
|
24
|
+
/** Config file name (e.g., "fragments.config.ts") */
|
|
25
|
+
configFile: "fragments.config.ts",
|
|
26
|
+
|
|
27
|
+
/** Legacy config file name (still supported for migration) */
|
|
28
|
+
legacyConfigFile: "segments.config.ts",
|
|
29
|
+
|
|
30
|
+
/** CLI command name (e.g., "fragments") */
|
|
31
|
+
cliCommand: "fragments",
|
|
32
|
+
|
|
33
|
+
/** Directory for storing fragments, registry, and cache */
|
|
34
|
+
dataDir: ".fragments",
|
|
35
|
+
|
|
36
|
+
/** Components subdirectory within .fragments/ */
|
|
37
|
+
componentsDir: "components",
|
|
38
|
+
|
|
39
|
+
/** Registry file name */
|
|
40
|
+
registryFile: "registry.json",
|
|
41
|
+
|
|
42
|
+
/** Default compiler governance report path */
|
|
43
|
+
governanceFile: ".fragments/governance.json",
|
|
44
|
+
|
|
45
|
+
/** Context file name (AI-ready markdown) */
|
|
46
|
+
contextFile: "context.md",
|
|
47
|
+
|
|
48
|
+
/** Screenshots subdirectory */
|
|
49
|
+
screenshotsDir: "screenshots",
|
|
50
|
+
|
|
51
|
+
/** Cache subdirectory (gitignored) */
|
|
52
|
+
cacheDir: "cache",
|
|
53
|
+
|
|
54
|
+
/** Diff output subdirectory (gitignored) */
|
|
55
|
+
diffDir: "diff",
|
|
56
|
+
|
|
57
|
+
/** Manifest filename */
|
|
58
|
+
manifestFile: "manifest.json",
|
|
59
|
+
|
|
60
|
+
/** Prefix for localStorage keys (e.g., "fragments-") */
|
|
61
|
+
storagePrefix: "fragments-",
|
|
62
|
+
|
|
63
|
+
/** Static viewer HTML file name */
|
|
64
|
+
viewerHtmlFile: "fragments-viewer.html",
|
|
65
|
+
|
|
66
|
+
/** MCP tool name prefix (e.g., "fragments_") */
|
|
67
|
+
mcpToolPrefix: "fragments_",
|
|
68
|
+
|
|
69
|
+
/** File extension for block definition files */
|
|
70
|
+
blockFileExtension: ".block.ts",
|
|
71
|
+
|
|
72
|
+
/** @deprecated Use blockFileExtension instead */
|
|
73
|
+
recipeFileExtension: ".recipe.ts",
|
|
74
|
+
|
|
75
|
+
/** Vite plugin namespace */
|
|
76
|
+
vitePluginNamespace: "fragments-core-shim",
|
|
77
|
+
} as const;
|
|
78
|
+
|
|
79
|
+
export type Brand = typeof BRAND;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Default configuration values for the service.
|
|
83
|
+
* These can be overridden in fragments.config.ts
|
|
84
|
+
*/
|
|
85
|
+
export const DEFAULTS = {
|
|
86
|
+
/** Default viewport dimensions */
|
|
87
|
+
viewport: {
|
|
88
|
+
width: 1280,
|
|
89
|
+
height: 800,
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/** Default diff threshold (percentage) */
|
|
93
|
+
diffThreshold: 5,
|
|
94
|
+
|
|
95
|
+
/** Browser pool size */
|
|
96
|
+
poolSize: 3,
|
|
97
|
+
|
|
98
|
+
/** Idle timeout before browser shutdown (ms) - 5 minutes */
|
|
99
|
+
idleTimeoutMs: 5 * 60 * 1000,
|
|
100
|
+
|
|
101
|
+
/** Delay after render before capture (ms) */
|
|
102
|
+
captureDelayMs: 100,
|
|
103
|
+
|
|
104
|
+
/** Font loading timeout (ms) */
|
|
105
|
+
fontTimeoutMs: 3000,
|
|
106
|
+
|
|
107
|
+
/** Default theme */
|
|
108
|
+
theme: "light" as const,
|
|
109
|
+
|
|
110
|
+
/** Dev server port */
|
|
111
|
+
port: 6006,
|
|
112
|
+
} as const;
|
|
113
|
+
|
|
114
|
+
export type Defaults = typeof DEFAULTS;
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `catalog-fixture-a` — the committed reference catalog shared by the contract
|
|
3
|
+
* acceptance cases (contract-identity ACCEPTANCE §1–§4) and the Cloud-side
|
|
4
|
+
* parity test. Small but full-surface: components with props, tokens, one
|
|
5
|
+
* confirmed + one proposed canonical mapping, a policy with rules, a
|
|
6
|
+
* composition pattern, and one authored waiver.
|
|
7
|
+
*
|
|
8
|
+
* `CATALOG_FIXTURE_A_FCID` is the golden value for
|
|
9
|
+
* `contractHash(projectContractPreimage(catalogFixtureA))`. Cloud's Convex
|
|
10
|
+
* parity test imports BOTH from here, so an intentional projection change is
|
|
11
|
+
* updated in exactly one place and every runtime follows (ACCEPTANCE §1).
|
|
12
|
+
*/
|
|
13
|
+
import type { ContractCatalogInput } from "./preimage.js";
|
|
14
|
+
|
|
15
|
+
export const catalogFixtureA: ContractCatalogInput = {
|
|
16
|
+
components: [
|
|
17
|
+
{
|
|
18
|
+
name: "Button",
|
|
19
|
+
props: ["variant", "size", "disabled"],
|
|
20
|
+
contract: { bans: [{ prop: "style", reason: "token-governed surface" }] },
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "Card",
|
|
24
|
+
props: ["padding"],
|
|
25
|
+
compoundChildren: ["Card.Header", "Card.Body"],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "Card.Header",
|
|
29
|
+
parentName: "Card",
|
|
30
|
+
props: ["title"],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
tokens: [
|
|
34
|
+
{ name: "--fui-color-accent", value: "#3366ff", type: "color" },
|
|
35
|
+
{ name: "--fui-space-2", value: "8px", type: "dimension" },
|
|
36
|
+
{ name: "--fui-radius-md", value: "6px", type: "dimension" },
|
|
37
|
+
],
|
|
38
|
+
canonicalMappings: [
|
|
39
|
+
{
|
|
40
|
+
component: "LegacyButton",
|
|
41
|
+
canonical: "Button",
|
|
42
|
+
status: "confirmed",
|
|
43
|
+
propMapping: [{ rawProp: "kind", canonicalProp: "variant" }],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
component: "AppCard",
|
|
47
|
+
canonical: "Card",
|
|
48
|
+
status: "proposed",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
policy: {
|
|
52
|
+
rules: {
|
|
53
|
+
tokens_hardcoded_values: { enabled: true, severity: "error" },
|
|
54
|
+
components_prefer_library: { enabled: true, severity: "warning" },
|
|
55
|
+
},
|
|
56
|
+
compositionPatterns: [
|
|
57
|
+
{ id: "card-structure", region: "Card", require: ["Card.Header"], severity: "warning" },
|
|
58
|
+
],
|
|
59
|
+
waivers: [
|
|
60
|
+
{
|
|
61
|
+
id: "waiver-001",
|
|
62
|
+
target: "FUI4001",
|
|
63
|
+
reason: "legacy promo banner, replacement tracked in #482",
|
|
64
|
+
expiresOn: "2026-09-30",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Golden fcid for {@link catalogFixtureA}. Regenerate only on an intentional
|
|
72
|
+
* projection change: `contractHash(projectContractPreimage(catalogFixtureA))`.
|
|
73
|
+
*/
|
|
74
|
+
export const CATALOG_FIXTURE_A_FCID =
|
|
75
|
+
"41ae847b36618f25a9aa73bda47851f6467e30ce4bcfb5a5aa25d27fee3fee54";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The same fixture expressed as Cloud-shaped mint materials: the
|
|
79
|
+
* `code-contracts` payload body and the DTCG token document that assemble to
|
|
80
|
+
* exactly {@link catalogFixtureA} through the adapters. Kept here so a fixture
|
|
81
|
+
* edit happens in one file — the core §1 test guards the payload↔components
|
|
82
|
+
* equivalence, and Cloud's parity test asserts the golden fcid end to end.
|
|
83
|
+
*/
|
|
84
|
+
export const catalogFixtureAContractPayload = {
|
|
85
|
+
fragments: [
|
|
86
|
+
{
|
|
87
|
+
meta: { name: "Button" },
|
|
88
|
+
props: { variant: {}, size: {}, disabled: {} },
|
|
89
|
+
contract: { bans: [{ prop: "style", reason: "token-governed surface" }] },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
meta: { name: "Card" },
|
|
93
|
+
props: { padding: {} },
|
|
94
|
+
structure: {
|
|
95
|
+
compoundChildren: [{ name: "Card.Header" }, { name: "Card.Body" }],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
meta: { name: "Card.Header", parentComponentName: "Card" },
|
|
100
|
+
props: { title: {} },
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** DTCG form of {@link catalogFixtureA}'s tokens (flattened Cloud-side). */
|
|
106
|
+
export const catalogFixtureATokensDtcg = {
|
|
107
|
+
"--fui-color-accent": { $value: "#3366ff", $type: "color" },
|
|
108
|
+
"--fui-space-2": { $value: "8px", $type: "dimension" },
|
|
109
|
+
"--fui-radius-md": { $value: "6px", $type: "dimension" },
|
|
110
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { canonicalPreimage, contractHash, sha256Hex } from "./hash.js";
|
|
4
|
+
|
|
5
|
+
describe("sha256Hex", () => {
|
|
6
|
+
// FIPS 180-4 / NIST known-answer vectors.
|
|
7
|
+
it("matches the empty-string vector", () => {
|
|
8
|
+
expect(sha256Hex("")).toBe("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("matches the 'abc' vector", () => {
|
|
12
|
+
expect(sha256Hex("abc")).toBe(
|
|
13
|
+
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("matches the 56-byte multi-block vector", () => {
|
|
18
|
+
expect(sha256Hex("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")).toBe(
|
|
19
|
+
"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("hashes UTF-8 multibyte input deterministically", () => {
|
|
24
|
+
// "héllo→" exercises 2- and 3-byte UTF-8 sequences through TextEncoder.
|
|
25
|
+
expect(sha256Hex("héllo→")).toBe(sha256Hex("héllo→"));
|
|
26
|
+
expect(sha256Hex("héllo→")).toHaveLength(64);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("crosses the padding boundary correctly (block-length inputs)", () => {
|
|
30
|
+
for (const n of [55, 56, 57, 63, 64, 65, 119, 120]) {
|
|
31
|
+
expect(sha256Hex("a".repeat(n))).toMatch(/^[0-9a-f]{64}$/);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("canonicalPreimage", () => {
|
|
37
|
+
it("is order-independent for object keys", () => {
|
|
38
|
+
expect(canonicalPreimage({ b: 1, a: 2 })).toBe(canonicalPreimage({ a: 2, b: 1 }));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("drops undefined properties but preserves null", () => {
|
|
42
|
+
expect(canonicalPreimage({ a: 1, b: undefined })).toBe('{"a":1}');
|
|
43
|
+
expect(canonicalPreimage({ a: null })).toBe('{"a":null}');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("normalizes negative zero to zero", () => {
|
|
47
|
+
expect(canonicalPreimage(-0)).toBe("0");
|
|
48
|
+
expect(canonicalPreimage(0)).toBe("0");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("gives non-finite numbers unambiguous, distinct sentinels", () => {
|
|
52
|
+
expect(canonicalPreimage(NaN)).toBe('"@num:nan"');
|
|
53
|
+
expect(canonicalPreimage(Infinity)).toBe('"@num:+inf"');
|
|
54
|
+
expect(canonicalPreimage(-Infinity)).toBe('"@num:-inf"');
|
|
55
|
+
// and they do not collapse to null the way JSON.stringify would
|
|
56
|
+
expect(canonicalPreimage(NaN)).not.toBe(canonicalPreimage(null));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("encodes nested arrays and objects canonically", () => {
|
|
60
|
+
expect(canonicalPreimage({ xs: [1, { z: true, y: "q" }] })).toBe(
|
|
61
|
+
'{"xs":[1,{"y":"q","z":true}]}'
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("contractHash", () => {
|
|
67
|
+
it("is stable across attribute order", () => {
|
|
68
|
+
const a = { primitives: [{ id: "button", html: "button" }], v: 2 };
|
|
69
|
+
const b = { v: 2, primitives: [{ html: "button", id: "button" }] };
|
|
70
|
+
expect(contractHash(a)).toBe(contractHash(b));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("changes when contract content changes", () => {
|
|
74
|
+
const base = { primitives: [{ id: "button" }] };
|
|
75
|
+
const changed = { primitives: [{ id: "button" }, { id: "input" }] };
|
|
76
|
+
expect(contractHash(base)).not.toBe(contractHash(changed));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("ignores undefined optional fields", () => {
|
|
80
|
+
expect(contractHash({ id: "x", note: undefined })).toBe(contractHash({ id: "x" }));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns a 64-char lowercase hex FCID", () => {
|
|
84
|
+
expect(contractHash({ any: "body" })).toMatch(/^[0-9a-f]{64}$/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend Contract hashing — the cross-runtime content-address (FCID).
|
|
3
|
+
*
|
|
4
|
+
* The keystone trust property of the Frontend Contract is that "what the agent
|
|
5
|
+
* was served" and "what CI enforced" are provably the same bytes. That only
|
|
6
|
+
* holds if the contract hash is computed by an *identical* function in every
|
|
7
|
+
* runtime that touches it: the CLI (Node), the Convex isolate, and the browser.
|
|
8
|
+
*
|
|
9
|
+
* The fact-IR `hash64Hex` (FNV-1a, in `../facts/ids.ts`) is deliberately NOT
|
|
10
|
+
* used here: it is a 64-bit non-cryptographic integrity reference for in-process
|
|
11
|
+
* fact dedup, and Convex independently reaches for `crypto.subtle` (async, and a
|
|
12
|
+
* *different* algorithm). Two different hashes over the same preimage cannot back
|
|
13
|
+
* a "same 64 hex chars or the build is rejected" guarantee.
|
|
14
|
+
*
|
|
15
|
+
* So this module ships ONE function: a synchronous, dependency-free SHA-256 over
|
|
16
|
+
* a number-normalizing canonical preimage. Synchronous so it composes with the
|
|
17
|
+
* sync fact-compilation path; dependency-free so it is byte-identical across
|
|
18
|
+
* Node, the Convex isolate, and the browser without a native/optional dep
|
|
19
|
+
* (`crypto.subtle` is async; `@noble/hashes` is not a dependency of this MIT,
|
|
20
|
+
* browser-safe package). `TextEncoder` is the only ambient global used, and it is
|
|
21
|
+
* present in all three runtimes.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Canonical preimage
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalize a number to a stable string. `JSON.stringify` is stable for finite
|
|
30
|
+
* numbers, but renders `NaN`/`Infinity` as `null` (ambiguous) and does not
|
|
31
|
+
* collapse `-0`. Token values flow into the contract body, so a serializer that
|
|
32
|
+
* silently maps `NaN` to `null` would hash two different inputs to the same
|
|
33
|
+
* preimage. Non-finite numbers get unambiguous sentinels instead; `-0` collapses
|
|
34
|
+
* to `0`. This is total (never throws) because it runs on the ingest hot path.
|
|
35
|
+
*/
|
|
36
|
+
function normalizeNumber(value: number): string {
|
|
37
|
+
if (Number.isNaN(value)) return '"@num:nan"';
|
|
38
|
+
if (value === Infinity) return '"@num:+inf"';
|
|
39
|
+
if (value === -Infinity) return '"@num:-inf"';
|
|
40
|
+
if (Object.is(value, -0)) return "0";
|
|
41
|
+
return JSON.stringify(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Deterministic, canonical serialization of a contract body for hashing.
|
|
46
|
+
*
|
|
47
|
+
* - object keys sorted lexicographically; attribute order is irrelevant
|
|
48
|
+
* - `undefined` properties dropped (so optional fields don't perturb the hash)
|
|
49
|
+
* - numbers normalized via {@link normalizeNumber}
|
|
50
|
+
* - `bigint` encoded as its decimal string (JSON cannot represent it natively)
|
|
51
|
+
*
|
|
52
|
+
* This is intentionally a separate serializer from `facts/ids.ts#canonicalJson`:
|
|
53
|
+
* that one targets in-process fact IDs and does not normalize numbers, and we
|
|
54
|
+
* must not change its output (it would silently re-key existing fact IDs).
|
|
55
|
+
*/
|
|
56
|
+
export function canonicalPreimage(value: unknown): string {
|
|
57
|
+
if (value === undefined || value === null) return "null";
|
|
58
|
+
const type = typeof value;
|
|
59
|
+
if (type === "number") return normalizeNumber(value as number);
|
|
60
|
+
if (type === "string" || type === "boolean") return JSON.stringify(value);
|
|
61
|
+
if (type === "bigint") return JSON.stringify((value as bigint).toString());
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
return `[${value.map(canonicalPreimage).join(",")}]`;
|
|
64
|
+
}
|
|
65
|
+
if (type !== "object") {
|
|
66
|
+
// functions, symbols — not valid contract content; encode as null so the
|
|
67
|
+
// serializer stays total rather than throwing on the ingest path.
|
|
68
|
+
return "null";
|
|
69
|
+
}
|
|
70
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
71
|
+
.filter(([, v]) => v !== undefined)
|
|
72
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
73
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalPreimage(v)}`).join(",")}}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// SHA-256 (FIPS 180-4), synchronous, pure JS
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
// First 32 bits of the fractional parts of the cube roots of the first 64 primes.
|
|
81
|
+
const K = new Uint32Array([
|
|
82
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
83
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
84
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
85
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
86
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
87
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
88
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
89
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
function rotr(value: number, bits: number): number {
|
|
93
|
+
return ((value >>> bits) | (value << (32 - bits))) >>> 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* SHA-256 of a UTF-8 string, returned as 64 lowercase hex chars.
|
|
98
|
+
*
|
|
99
|
+
* Deterministic and synchronous in every runtime. Verified against the FIPS
|
|
100
|
+
* 180-4 vectors in `hash.test.ts`.
|
|
101
|
+
*/
|
|
102
|
+
export function sha256Hex(message: string): string {
|
|
103
|
+
const bytes = new TextEncoder().encode(message);
|
|
104
|
+
const length = bytes.length;
|
|
105
|
+
const bitLength = length * 8;
|
|
106
|
+
|
|
107
|
+
// Padding: append 0x80, then zeros, then the 64-bit big-endian bit length,
|
|
108
|
+
// so the total is a multiple of 64 bytes.
|
|
109
|
+
const withMarker = length + 1;
|
|
110
|
+
const zeroPad = (56 - (withMarker % 64) + 64) % 64;
|
|
111
|
+
const total = withMarker + zeroPad + 8;
|
|
112
|
+
const buffer = new Uint8Array(total);
|
|
113
|
+
buffer.set(bytes, 0);
|
|
114
|
+
buffer[length] = 0x80;
|
|
115
|
+
|
|
116
|
+
const view = new DataView(buffer.buffer);
|
|
117
|
+
// 64-bit length: high word then low word, big-endian. bitLength can exceed
|
|
118
|
+
// 2^32 for very large inputs, so split rather than truncate.
|
|
119
|
+
view.setUint32(total - 8, Math.floor(bitLength / 0x100000000));
|
|
120
|
+
view.setUint32(total - 4, bitLength >>> 0);
|
|
121
|
+
|
|
122
|
+
let h0 = 0x6a09e667;
|
|
123
|
+
let h1 = 0xbb67ae85;
|
|
124
|
+
let h2 = 0x3c6ef372;
|
|
125
|
+
let h3 = 0xa54ff53a;
|
|
126
|
+
let h4 = 0x510e527f;
|
|
127
|
+
let h5 = 0x9b05688c;
|
|
128
|
+
let h6 = 0x1f83d9ab;
|
|
129
|
+
let h7 = 0x5be0cd19;
|
|
130
|
+
|
|
131
|
+
const w = new Uint32Array(64);
|
|
132
|
+
for (let chunk = 0; chunk < total; chunk += 64) {
|
|
133
|
+
for (let t = 0; t < 16; t++) w[t] = view.getUint32(chunk + t * 4);
|
|
134
|
+
for (let t = 16; t < 64; t++) {
|
|
135
|
+
const w15 = w[t - 15];
|
|
136
|
+
const w2 = w[t - 2];
|
|
137
|
+
const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3);
|
|
138
|
+
const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10);
|
|
139
|
+
w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let a = h0;
|
|
143
|
+
let b = h1;
|
|
144
|
+
let c = h2;
|
|
145
|
+
let d = h3;
|
|
146
|
+
let e = h4;
|
|
147
|
+
let f = h5;
|
|
148
|
+
let g = h6;
|
|
149
|
+
let h = h7;
|
|
150
|
+
|
|
151
|
+
for (let t = 0; t < 64; t++) {
|
|
152
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
153
|
+
const ch = (e & f) ^ (~e & g);
|
|
154
|
+
const temp1 = (h + s1 + ch + K[t] + w[t]) >>> 0;
|
|
155
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
156
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
157
|
+
const temp2 = (s0 + maj) >>> 0;
|
|
158
|
+
h = g;
|
|
159
|
+
g = f;
|
|
160
|
+
f = e;
|
|
161
|
+
e = (d + temp1) >>> 0;
|
|
162
|
+
d = c;
|
|
163
|
+
c = b;
|
|
164
|
+
b = a;
|
|
165
|
+
a = (temp1 + temp2) >>> 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
h0 = (h0 + a) >>> 0;
|
|
169
|
+
h1 = (h1 + b) >>> 0;
|
|
170
|
+
h2 = (h2 + c) >>> 0;
|
|
171
|
+
h3 = (h3 + d) >>> 0;
|
|
172
|
+
h4 = (h4 + e) >>> 0;
|
|
173
|
+
h5 = (h5 + f) >>> 0;
|
|
174
|
+
h6 = (h6 + g) >>> 0;
|
|
175
|
+
h7 = (h7 + h) >>> 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return [h0, h1, h2, h3, h4, h5, h6, h7]
|
|
179
|
+
.map((value) => value.toString(16).padStart(8, "0"))
|
|
180
|
+
.join("");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Contract hash (FCID)
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The Frontend Contract ID: `sha256(canonicalPreimage(body))`, 64 lowercase hex
|
|
189
|
+
* chars. Identical inputs produce an identical FCID in the CLI, the Convex
|
|
190
|
+
* isolate, and the browser — which is what lets the gen-time agent view and the
|
|
191
|
+
* CI enforcement reference the same contract by hash.
|
|
192
|
+
*/
|
|
193
|
+
export function contractHash(body: unknown): string {
|
|
194
|
+
return sha256Hex(canonicalPreimage(body));
|
|
195
|
+
}
|