@usefragments/core 1.10.2 → 2.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/dist/{chunk-MZ4SW3TP.js → chunk-3IOWHECM.js} +8 -1
- package/dist/{chunk-MZ4SW3TP.js.map → chunk-3IOWHECM.js.map} +1 -1
- package/dist/chunk-7ULGH74M.js +66 -0
- package/dist/chunk-7ULGH74M.js.map +1 -0
- package/dist/{chunk-XN3LSDPY.js → chunk-BMPYIUZE.js} +612 -344
- package/dist/chunk-BMPYIUZE.js.map +1 -0
- package/dist/{chunk-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
- package/dist/chunk-ML5S6QNU.js.map +1 -0
- package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
- package/dist/chunk-MZ2FS7U4.js.map +1 -0
- package/dist/chunk-PWIJMOI4.js +202 -0
- package/dist/chunk-PWIJMOI4.js.map +1 -0
- package/dist/chunk-RYFULE43.js +578 -0
- package/dist/chunk-RYFULE43.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +3 -2
- package/dist/compiled-types/index.d.ts +327 -2
- package/dist/compiled-types/index.js +1 -1
- package/dist/generate/index.d.ts +3 -2
- package/dist/{governance-CLk_wkP9.d.ts → governance-hOPXGbbs.d.ts} +474 -515
- package/dist/governance-telemetry.d.ts +6 -0
- package/dist/governance-telemetry.js +1 -1
- package/dist/{index-_sxhUNqx.d.ts → index-C8bcXVav.d.ts} +448 -448
- package/dist/index.d.ts +3280 -1305
- package/dist/index.js +2414 -350
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +228 -0
- package/dist/manifest.js +24 -0
- package/dist/manifest.js.map +1 -0
- package/dist/preview/index.js +45 -1
- package/dist/preview/index.js.map +1 -1
- package/dist/preview-runtime.d.ts +1 -2
- package/dist/preview-runtime.js +150 -14
- package/dist/preview-runtime.js.map +1 -1
- package/dist/react-types.d.ts +1 -2
- package/dist/registry.d.ts +1412 -203
- package/dist/registry.js +30 -3
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +3 -2
- package/dist/storyAdapter.d.ts +1 -2
- package/dist/storyAdapter.js +11 -49
- package/dist/storyAdapter.js.map +1 -1
- package/dist/test-utils.d.ts +3 -2
- package/dist/topology/index.d.ts +1 -1
- package/dist/topology/index.js +1 -1
- package/package.json +8 -2
- package/src/__tests__/contract-parser.test.ts +318 -277
- package/src/__tests__/preview-runtime-hook.test.tsx +315 -0
- package/src/__tests__/preview-runtime.test.tsx +30 -8
- package/src/__tests__/schema.test.ts +191 -14
- package/src/analysis-plan/analysis-plan-v1.test.ts +320 -0
- package/src/analysis-plan/coverage.ts +181 -0
- package/src/analysis-plan/digest.ts +141 -0
- package/src/analysis-plan/index.ts +34 -0
- package/src/analysis-plan/types.ts +207 -0
- package/src/approved-contract-tokens.test.ts +39 -0
- package/src/approved-contract-tokens.ts +18 -0
- package/src/codes/__tests__/codes.test.ts +13 -0
- package/src/codes/codes.ts +40 -0
- package/src/compiled-types/index.ts +640 -39
- package/src/compiled-types/parse.test.ts +145 -4
- package/src/component-contract.ts +95 -53
- package/src/composition.ts +7 -13
- package/src/constants.ts +3 -6
- package/src/contract/hash.test.ts +20 -0
- package/src/contract/hash.ts +66 -9
- package/src/contract/index.ts +24 -1
- package/src/contract/manifest.test.ts +94 -0
- package/src/contract/manifest.ts +68 -0
- package/src/contract/preimage.test.ts +219 -1
- package/src/contract/preimage.ts +326 -6
- package/src/contract/stamp.test.ts +3 -0
- package/src/contract/stamp.ts +1 -1
- package/src/contract-parser.ts +54 -30
- package/src/defineFragment.test.ts +476 -91
- package/src/defineFragment.ts +204 -114
- package/src/domain-ids.test.ts +35 -0
- package/src/domain-ids.ts +61 -0
- package/src/evaluation/evaluate.test.ts +522 -0
- package/src/evaluation/evaluate.ts +690 -0
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +772 -0
- package/src/evaluation/index.ts +58 -0
- package/src/evaluation/receipt.ts +753 -0
- package/src/evaluation/types.ts +406 -0
- package/src/facts/builders.ts +2 -0
- package/src/facts/compile.ts +29 -6
- package/src/facts/fact-index.ts +13 -3
- package/src/facts/fact-integrity-v1.test.ts +172 -0
- package/src/facts/facts.test.ts +15 -0
- package/src/facts/ids.ts +46 -3
- package/src/facts/index.ts +14 -1
- package/src/facts/integrity.ts +134 -0
- package/src/facts/types.ts +36 -0
- package/src/governance-integrity.test.ts +1 -0
- package/src/governance-integrity.ts +5 -3
- package/src/governance-telemetry.ts +8 -0
- package/src/governance.ts +70 -8
- package/src/index.ts +230 -37
- package/src/preview/validation.test.ts +62 -0
- package/src/preview/validation.ts +48 -2
- package/src/preview-runtime.tsx +227 -20
- package/src/registry-install-plan.ts +200 -109
- package/src/registry-shards.test.ts +263 -0
- package/src/registry.ts +237 -0
- package/src/repository-binding.test.ts +50 -0
- package/src/repository-binding.ts +96 -0
- package/src/rules/families.test.ts +36 -0
- package/src/rules/finding.ts +7 -2
- package/src/rules/index.ts +17 -1
- package/src/rules/rule-config.test.ts +66 -0
- package/src/rules/rule-config.ts +73 -0
- package/src/rules/rules.test.ts +26 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +51 -2
- package/src/rules/tokens-css-vars-must-be-defined.ts +34 -1
- package/src/schema.ts +293 -113
- package/src/schemas/index.ts +1 -1
- package/src/storyAdapter.test.ts +68 -12
- package/src/storyAdapter.ts +44 -75
- package/src/topology/resolve-area.ts +1 -1
- package/src/types.ts +258 -40
- package/dist/chunk-3LLRNCPX.js.map +0 -1
- package/dist/chunk-RANPUC6C.js +0 -72
- package/dist/chunk-RANPUC6C.js.map +0 -1
- package/dist/chunk-XN3LSDPY.js.map +0 -1
- package/dist/chunk-YF65VYRY.js.map +0 -1
- package/src/fragment-types.ts +0 -214
- package/src/react-create-element.test.ts +0 -22
- package/src/react-create-element.ts +0 -12
- package/src/storyFilters.test.ts +0 -350
- package/src/storyFilters.ts +0 -253
package/src/contract/preimage.ts
CHANGED
|
@@ -24,9 +24,35 @@
|
|
|
24
24
|
*
|
|
25
25
|
* Runtime-portable by construction: no Node APIs, no Convex imports, no React.
|
|
26
26
|
*/
|
|
27
|
-
import type { GovernanceConfig } from "../governance.js";
|
|
27
|
+
import type { ComponentGovernanceRecord, GovernanceConfig } from "../governance.js";
|
|
28
|
+
import { resolveGovernanceRecordsForIdentity } from "../governance.js";
|
|
28
29
|
import { projectV1OwnedComponentId, projectV1OwnedImportIdentity } from "../package-identity.js";
|
|
29
|
-
import { canonicalPreimage, contractHash } from "./hash.js";
|
|
30
|
+
import { artifactContentHash, canonicalPreimage, contractHash } from "./hash.js";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Positive pick-list of per-component `contract` fields that enter the FCID.
|
|
34
|
+
* Display/guidance fields (`propsSummary`, `canonicalUsage`, `scenarioTags`,
|
|
35
|
+
* story `states`/`variants`, descriptions) are intentionally absent — adapters
|
|
36
|
+
* may still pass them, but {@link projectEnforcementContract} never copies them.
|
|
37
|
+
*/
|
|
38
|
+
export const CONTRACT_ENFORCEMENT_FIELDS = [
|
|
39
|
+
"a11yRules",
|
|
40
|
+
"bans",
|
|
41
|
+
"performanceBudget",
|
|
42
|
+
"compoundChildren",
|
|
43
|
+
"composition",
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
export type ContractEnforcementField = (typeof CONTRACT_ENFORCEMENT_FIELDS)[number];
|
|
47
|
+
|
|
48
|
+
/** Display-only contract keys that must never enter the FCID positive pick-list. */
|
|
49
|
+
export const CONTRACT_DISPLAY_ONLY_FIELDS = [
|
|
50
|
+
"propsSummary",
|
|
51
|
+
"canonicalUsage",
|
|
52
|
+
"scenarioTags",
|
|
53
|
+
"states",
|
|
54
|
+
"variants",
|
|
55
|
+
] as const;
|
|
30
56
|
|
|
31
57
|
// ---------------------------------------------------------------------------
|
|
32
58
|
// Domains
|
|
@@ -56,11 +82,15 @@ export interface ContractComponentInput {
|
|
|
56
82
|
/** Prop names the component accepts. */
|
|
57
83
|
props?: readonly string[];
|
|
58
84
|
/**
|
|
59
|
-
* The per-component
|
|
60
|
-
*
|
|
61
|
-
* by definition.
|
|
85
|
+
* The per-component contract as authored. Only {@link CONTRACT_ENFORCEMENT_FIELDS}
|
|
86
|
+
* are projected into the FCID — display/guidance keys are stripped structurally.
|
|
62
87
|
*/
|
|
63
88
|
contract?: unknown;
|
|
89
|
+
/**
|
|
90
|
+
* Resolved component governance records (`govern:` / `governance`). Editing
|
|
91
|
+
* these changes what CI allows, so they are FCID identity.
|
|
92
|
+
*/
|
|
93
|
+
governance?: readonly ComponentGovernanceRecord[];
|
|
64
94
|
/** Required compound-children names, when the structure is declared. */
|
|
65
95
|
compoundChildren?: readonly string[];
|
|
66
96
|
}
|
|
@@ -143,6 +173,18 @@ export interface ContractCatalogInput {
|
|
|
143
173
|
policy?: ContractPolicyInput | null;
|
|
144
174
|
}
|
|
145
175
|
|
|
176
|
+
/** Deterministic runtime refusal for malformed or ambiguous FCID inputs. */
|
|
177
|
+
export class ContractCatalogValidationError extends TypeError {
|
|
178
|
+
readonly issues: readonly string[];
|
|
179
|
+
|
|
180
|
+
constructor(issues: readonly string[]) {
|
|
181
|
+
const sortedIssues = [...issues].sort();
|
|
182
|
+
super(["Invalid contract catalog:", ...sortedIssues.map((issue) => ` - ${issue}`)].join("\n"));
|
|
183
|
+
this.name = "ContractCatalogValidationError";
|
|
184
|
+
this.issues = sortedIssues;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
146
188
|
// ---------------------------------------------------------------------------
|
|
147
189
|
// Preimage
|
|
148
190
|
// ---------------------------------------------------------------------------
|
|
@@ -226,13 +268,259 @@ function sortCanonical<T>(items: readonly T[]): T[] {
|
|
|
226
268
|
.map(({ item }) => item);
|
|
227
269
|
}
|
|
228
270
|
|
|
271
|
+
function isPlainRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
272
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
273
|
+
const prototype = Object.getPrototypeOf(value);
|
|
274
|
+
return prototype === Object.prototype || prototype === null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function requireNonEmptyString(value: unknown, path: string, issues: string[]): void {
|
|
278
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
279
|
+
issues.push(`${path}: expected a non-empty string`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function validateStringArray(value: unknown, path: string, issues: string[]): void {
|
|
284
|
+
if (!Array.isArray(value)) {
|
|
285
|
+
issues.push(`${path}: expected an array`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const seen = new Set<string>();
|
|
289
|
+
for (const [index, entry] of value.entries()) {
|
|
290
|
+
requireNonEmptyString(entry, `${path}[${index}]`, issues);
|
|
291
|
+
if (typeof entry === "string" && entry.length > 0) {
|
|
292
|
+
if (seen.has(entry)) issues.push(`${path}: duplicate value "${entry}"`);
|
|
293
|
+
seen.add(entry);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function validateContractCatalog(catalog: unknown): asserts catalog is ContractCatalogInput {
|
|
299
|
+
const issues: string[] = [];
|
|
300
|
+
if (!isPlainRecord(catalog)) {
|
|
301
|
+
throw new ContractCatalogValidationError(["(root): expected a plain object"]);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const componentIdentities = new Set<string>();
|
|
305
|
+
if (catalog.components !== undefined) {
|
|
306
|
+
if (!Array.isArray(catalog.components)) {
|
|
307
|
+
issues.push("components: expected an array");
|
|
308
|
+
} else {
|
|
309
|
+
for (const [index, value] of catalog.components.entries()) {
|
|
310
|
+
const path = `components[${index}]`;
|
|
311
|
+
if (!isPlainRecord(value)) {
|
|
312
|
+
issues.push(`${path}: expected a plain object`);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
requireNonEmptyString(value.name, `${path}.name`, issues);
|
|
316
|
+
if (value.parentName !== undefined) {
|
|
317
|
+
requireNonEmptyString(value.parentName, `${path}.parentName`, issues);
|
|
318
|
+
}
|
|
319
|
+
if (typeof value.name === "string" && value.name.length > 0) {
|
|
320
|
+
const identity = `${typeof value.parentName === "string" ? value.parentName : ""}#${value.name}`;
|
|
321
|
+
if (componentIdentities.has(identity)) {
|
|
322
|
+
issues.push(`components: duplicate identity "${identity}"`);
|
|
323
|
+
}
|
|
324
|
+
componentIdentities.add(identity);
|
|
325
|
+
}
|
|
326
|
+
if (value.props !== undefined) validateStringArray(value.props, `${path}.props`, issues);
|
|
327
|
+
if (value.compoundChildren !== undefined) {
|
|
328
|
+
validateStringArray(value.compoundChildren, `${path}.compoundChildren`, issues);
|
|
329
|
+
}
|
|
330
|
+
if (value.contract !== undefined && !isPlainRecord(value.contract)) {
|
|
331
|
+
issues.push(`${path}.contract: expected a plain object`);
|
|
332
|
+
}
|
|
333
|
+
if (value.governance !== undefined && !Array.isArray(value.governance)) {
|
|
334
|
+
issues.push(`${path}.governance: expected an array`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const tokenNames = new Set<string>();
|
|
341
|
+
if (catalog.tokens !== undefined) {
|
|
342
|
+
if (!Array.isArray(catalog.tokens)) {
|
|
343
|
+
issues.push("tokens: expected an array");
|
|
344
|
+
} else {
|
|
345
|
+
for (const [index, value] of catalog.tokens.entries()) {
|
|
346
|
+
const path = `tokens[${index}]`;
|
|
347
|
+
if (!isPlainRecord(value)) {
|
|
348
|
+
issues.push(`${path}: expected a plain object`);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
requireNonEmptyString(value.name, `${path}.name`, issues);
|
|
352
|
+
if (typeof value.name === "string" && value.name.length > 0) {
|
|
353
|
+
if (tokenNames.has(value.name)) issues.push(`tokens: duplicate identity "${value.name}"`);
|
|
354
|
+
tokenNames.add(value.name);
|
|
355
|
+
}
|
|
356
|
+
if (
|
|
357
|
+
(typeof value.value !== "string" && typeof value.value !== "number") ||
|
|
358
|
+
(typeof value.value === "number" && !Number.isFinite(value.value))
|
|
359
|
+
) {
|
|
360
|
+
issues.push(`${path}.value: expected a finite number or string`);
|
|
361
|
+
}
|
|
362
|
+
if (value.type !== undefined) requireNonEmptyString(value.type, `${path}.type`, issues);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const confirmedMappings = new Set<string>();
|
|
368
|
+
if (catalog.canonicalMappings !== undefined) {
|
|
369
|
+
if (!Array.isArray(catalog.canonicalMappings)) {
|
|
370
|
+
issues.push("canonicalMappings: expected an array");
|
|
371
|
+
} else {
|
|
372
|
+
for (const [index, value] of catalog.canonicalMappings.entries()) {
|
|
373
|
+
const path = `canonicalMappings[${index}]`;
|
|
374
|
+
if (!isPlainRecord(value)) {
|
|
375
|
+
issues.push(`${path}: expected a plain object`);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
requireNonEmptyString(value.component, `${path}.component`, issues);
|
|
379
|
+
requireNonEmptyString(value.canonical, `${path}.canonical`, issues);
|
|
380
|
+
if (!(["confirmed", "proposed", "unknown"] as const).includes(value.status as never)) {
|
|
381
|
+
issues.push(`${path}.status: expected confirmed, proposed, or unknown`);
|
|
382
|
+
}
|
|
383
|
+
if (value.status === "confirmed" && typeof value.component === "string") {
|
|
384
|
+
if (confirmedMappings.has(value.component)) {
|
|
385
|
+
issues.push(`canonicalMappings: duplicate confirmed identity "${value.component}"`);
|
|
386
|
+
}
|
|
387
|
+
confirmedMappings.add(value.component);
|
|
388
|
+
}
|
|
389
|
+
if (value.importPath !== undefined) {
|
|
390
|
+
requireNonEmptyString(value.importPath, `${path}.importPath`, issues);
|
|
391
|
+
}
|
|
392
|
+
if (value.propMapping !== undefined && !Array.isArray(value.propMapping)) {
|
|
393
|
+
issues.push(`${path}.propMapping: expected an array`);
|
|
394
|
+
}
|
|
395
|
+
if (value.resolves !== undefined && !Array.isArray(value.resolves)) {
|
|
396
|
+
issues.push(`${path}.resolves: expected an array`);
|
|
397
|
+
}
|
|
398
|
+
if (value.bridge !== undefined && !isPlainRecord(value.bridge)) {
|
|
399
|
+
issues.push(`${path}.bridge: expected a plain object`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (catalog.policy !== undefined && catalog.policy !== null) {
|
|
406
|
+
if (!isPlainRecord(catalog.policy)) {
|
|
407
|
+
issues.push("policy: expected a plain object or null");
|
|
408
|
+
} else {
|
|
409
|
+
if (catalog.policy.rules !== undefined && !isPlainRecord(catalog.policy.rules)) {
|
|
410
|
+
issues.push("policy.rules: expected a plain object");
|
|
411
|
+
}
|
|
412
|
+
if (catalog.policy.codes !== undefined && !isPlainRecord(catalog.policy.codes)) {
|
|
413
|
+
issues.push("policy.codes: expected a plain object");
|
|
414
|
+
}
|
|
415
|
+
if (
|
|
416
|
+
catalog.policy.compositionPatterns !== undefined &&
|
|
417
|
+
!Array.isArray(catalog.policy.compositionPatterns)
|
|
418
|
+
) {
|
|
419
|
+
issues.push("policy.compositionPatterns: expected an array");
|
|
420
|
+
}
|
|
421
|
+
if (catalog.policy.waivers !== undefined) {
|
|
422
|
+
if (!Array.isArray(catalog.policy.waivers)) {
|
|
423
|
+
issues.push("policy.waivers: expected an array");
|
|
424
|
+
} else {
|
|
425
|
+
const waiverIds = new Set<string>();
|
|
426
|
+
for (const [index, value] of catalog.policy.waivers.entries()) {
|
|
427
|
+
const path = `policy.waivers[${index}]`;
|
|
428
|
+
if (!isPlainRecord(value)) {
|
|
429
|
+
issues.push(`${path}: expected a plain object`);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
requireNonEmptyString(value.id, `${path}.id`, issues);
|
|
433
|
+
requireNonEmptyString(value.target, `${path}.target`, issues);
|
|
434
|
+
requireNonEmptyString(value.reason, `${path}.reason`, issues);
|
|
435
|
+
if (value.expiresOn !== undefined) {
|
|
436
|
+
requireNonEmptyString(value.expiresOn, `${path}.expiresOn`, issues);
|
|
437
|
+
}
|
|
438
|
+
if (typeof value.id === "string" && value.id.length > 0) {
|
|
439
|
+
if (waiverIds.has(value.id)) {
|
|
440
|
+
issues.push(`policy.waivers: duplicate identity "${value.id}"`);
|
|
441
|
+
}
|
|
442
|
+
waiverIds.add(value.id);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (issues.length > 0) throw new ContractCatalogValidationError(issues);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Project only enforcement fields from a per-component `contract` blob.
|
|
455
|
+
* Unknown / display keys are dropped; an all-display contract becomes `undefined`.
|
|
456
|
+
*/
|
|
457
|
+
export function projectEnforcementContract(contract: unknown): unknown {
|
|
458
|
+
if (!contract || typeof contract !== "object" || Array.isArray(contract)) {
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
const source = contract as Readonly<Record<string, unknown>>;
|
|
462
|
+
const projected: Record<string, unknown> = {};
|
|
463
|
+
|
|
464
|
+
if (Array.isArray(source.a11yRules)) {
|
|
465
|
+
projected.a11yRules = sortCanonical([...source.a11yRules]);
|
|
466
|
+
}
|
|
467
|
+
if (Array.isArray(source.bans)) {
|
|
468
|
+
projected.bans = sortCanonical([...source.bans]);
|
|
469
|
+
}
|
|
470
|
+
if (typeof source.performanceBudget === "number") {
|
|
471
|
+
projected.performanceBudget = source.performanceBudget;
|
|
472
|
+
}
|
|
473
|
+
if (Array.isArray(source.composition)) {
|
|
474
|
+
projected.composition = sortCanonical([...source.composition]);
|
|
475
|
+
}
|
|
476
|
+
const compoundChildren = projectContractCompoundChildren(source.compoundChildren);
|
|
477
|
+
if (compoundChildren !== undefined) {
|
|
478
|
+
projected.compoundChildren = compoundChildren;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return Object.keys(projected).length > 0 ? projected : undefined;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Compound-children structure is enforcement (required / accepts); prose
|
|
486
|
+
* descriptions are display-only and never enter the FCID.
|
|
487
|
+
*/
|
|
488
|
+
function projectContractCompoundChildren(value: unknown): unknown {
|
|
489
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
490
|
+
const entries = Object.entries(value as Readonly<Record<string, unknown>>)
|
|
491
|
+
.map(([name, raw]) => {
|
|
492
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
493
|
+
return [name, {}] as const;
|
|
494
|
+
}
|
|
495
|
+
const child = raw as Readonly<Record<string, unknown>>;
|
|
496
|
+
const projected: Record<string, unknown> = {};
|
|
497
|
+
if (typeof child.required === "boolean") projected.required = child.required;
|
|
498
|
+
if (Array.isArray(child.accepts)) {
|
|
499
|
+
projected.accepts = [...child.accepts]
|
|
500
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
501
|
+
.sort();
|
|
502
|
+
}
|
|
503
|
+
return [name, projected] as const;
|
|
504
|
+
})
|
|
505
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
506
|
+
return Object.fromEntries(entries);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function projectGovernanceRecords(
|
|
510
|
+
records: readonly ComponentGovernanceRecord[] | undefined
|
|
511
|
+
): unknown {
|
|
512
|
+
if (!records || records.length === 0) return undefined;
|
|
513
|
+
return sortCanonical([...records]);
|
|
514
|
+
}
|
|
515
|
+
|
|
229
516
|
function projectComponents(components: readonly ContractComponentInput[] | undefined): unknown {
|
|
230
517
|
return sortCanonical(
|
|
231
518
|
(components ?? []).map((component) => ({
|
|
232
519
|
name: component.name,
|
|
233
520
|
parentName: component.parentName,
|
|
234
521
|
props: component.props ? [...component.props].sort() : undefined,
|
|
235
|
-
contract: component.contract,
|
|
522
|
+
contract: projectEnforcementContract(component.contract),
|
|
523
|
+
governance: projectGovernanceRecords(component.governance),
|
|
236
524
|
compoundChildren: component.compoundChildren
|
|
237
525
|
? [...component.compoundChildren].sort()
|
|
238
526
|
: undefined,
|
|
@@ -351,6 +639,7 @@ function projectPolicy(policy: ContractPolicyInput | null | undefined): unknown
|
|
|
351
639
|
* is the fcid.
|
|
352
640
|
*/
|
|
353
641
|
export function projectContractPreimage(catalog: ContractCatalogInput): ContractPreimage {
|
|
642
|
+
validateContractCatalog(catalog);
|
|
354
643
|
return {
|
|
355
644
|
schema: CONTRACT_PREIMAGE_SCHEMA,
|
|
356
645
|
domains: {
|
|
@@ -418,6 +707,8 @@ export function contractComponentsFromFragments(
|
|
|
418
707
|
meta?: { name?: unknown; parentComponentName?: unknown };
|
|
419
708
|
props?: Record<string, unknown>;
|
|
420
709
|
contract?: unknown;
|
|
710
|
+
governance?: unknown;
|
|
711
|
+
govern?: unknown;
|
|
421
712
|
structure?: { compoundChildren?: unknown };
|
|
422
713
|
} | null;
|
|
423
714
|
const name = fragment?.meta?.name;
|
|
@@ -429,13 +720,42 @@ export function contractComponentsFromFragments(
|
|
|
429
720
|
.map((child) => (child as { name?: unknown } | null)?.name)
|
|
430
721
|
.filter((childName): childName is string => typeof childName === "string")
|
|
431
722
|
: undefined;
|
|
723
|
+
const governance = resolveGovernanceRecordsForIdentity({
|
|
724
|
+
governance: Array.isArray(fragment?.governance)
|
|
725
|
+
? (fragment.governance as ComponentGovernanceRecord[])
|
|
726
|
+
: undefined,
|
|
727
|
+
govern: typeof fragment?.govern === "function" ? (fragment.govern as never) : undefined,
|
|
728
|
+
});
|
|
432
729
|
components.push({
|
|
433
730
|
name,
|
|
434
731
|
parentName: typeof parentName === "string" ? parentName : undefined,
|
|
435
732
|
props: Object.keys(fragment?.props ?? {}),
|
|
436
733
|
contract: fragment?.contract,
|
|
734
|
+
governance,
|
|
437
735
|
compoundChildren,
|
|
438
736
|
});
|
|
439
737
|
}
|
|
440
738
|
return components;
|
|
441
739
|
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Derive a preview `artifactId` from modules, states, guidance/render content,
|
|
743
|
+
* and runtime config. Never an FCID — callers must not assign
|
|
744
|
+
* `derivedFromContractHash` into `artifactId` (or the reverse).
|
|
745
|
+
*/
|
|
746
|
+
export function deriveArtifactId(content: {
|
|
747
|
+
modules?: unknown;
|
|
748
|
+
states?: unknown;
|
|
749
|
+
guidance?: unknown;
|
|
750
|
+
render?: unknown;
|
|
751
|
+
runtime?: unknown;
|
|
752
|
+
}): string {
|
|
753
|
+
return artifactContentHash({
|
|
754
|
+
schema: "artifact-content:v1",
|
|
755
|
+
modules: content.modules,
|
|
756
|
+
states: content.states,
|
|
757
|
+
guidance: content.guidance,
|
|
758
|
+
render: content.render,
|
|
759
|
+
runtime: content.runtime,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
@@ -75,6 +75,9 @@ Prefer canonical components over raw HTML elements.
|
|
|
75
75
|
// Header without the fcid line (and vice versa) is not a stamp.
|
|
76
76
|
expect(parseContractStamp("contract: fragments contract v12 · a3f9e21\n")).toBeNull();
|
|
77
77
|
expect(parseContractStamp(`contract-fcid: ${FCID}\n`)).toBeNull();
|
|
78
|
+
expect(
|
|
79
|
+
parseContractStamp(`contract: fragments contract v0 · a3f9e21\ncontract-fcid: ${FCID}\n`)
|
|
80
|
+
).toBeNull();
|
|
78
81
|
// Short fcid in the header must prefix the full fcid.
|
|
79
82
|
expect(
|
|
80
83
|
parseContractStamp(`contract: fragments contract v12 · 0000000\ncontract-fcid: ${FCID}\n`)
|
package/src/contract/stamp.ts
CHANGED
|
@@ -87,7 +87,7 @@ export function parseContractStamp(text: string): ContractStampRecord | null {
|
|
|
87
87
|
const fcidLine = STAMP_FCID_LINE.exec(text);
|
|
88
88
|
if (!header || !fcidLine) return null;
|
|
89
89
|
const contractVersion = Number(header[1]);
|
|
90
|
-
if (!Number.isSafeInteger(contractVersion)) return null;
|
|
90
|
+
if (!Number.isSafeInteger(contractVersion) || contractVersion < 1) return null;
|
|
91
91
|
const fcid = fcidLine[1];
|
|
92
92
|
if (!fcid.startsWith(header[2])) return null;
|
|
93
93
|
|
package/src/contract-parser.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* (MCP tools, governance, search) work unchanged.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { componentContractSchema } from
|
|
10
|
-
import type { ComponentContract } from
|
|
9
|
+
import { componentContractSchema } from "./component-contract.js";
|
|
10
|
+
import type { ComponentContract } from "./component-contract.js";
|
|
11
11
|
|
|
12
12
|
// Use the context package's CompiledFragment type via re-export from types.ts
|
|
13
13
|
// Since core can't depend on context, we define the minimal output shape here
|
|
@@ -20,7 +20,7 @@ export interface CompiledContractOutput {
|
|
|
20
20
|
description: string;
|
|
21
21
|
category: string;
|
|
22
22
|
tags?: string[];
|
|
23
|
-
status?:
|
|
23
|
+
status?: "stable" | "beta" | "deprecated" | "experimental";
|
|
24
24
|
dependencies?: Array<{ name: string; version: string; reason?: string }>;
|
|
25
25
|
figma?: string;
|
|
26
26
|
figmaProps?: Record<string, unknown>;
|
|
@@ -31,17 +31,27 @@ export interface CompiledContractOutput {
|
|
|
31
31
|
guidelines?: string[];
|
|
32
32
|
accessibility?: string[];
|
|
33
33
|
};
|
|
34
|
-
props: Record<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
props: Record<
|
|
35
|
+
string,
|
|
36
|
+
{
|
|
37
|
+
type: string;
|
|
38
|
+
description: string;
|
|
39
|
+
default?: unknown;
|
|
40
|
+
required?: boolean;
|
|
41
|
+
values?: string[];
|
|
42
|
+
constraints?: string[];
|
|
43
|
+
}
|
|
44
|
+
>;
|
|
42
45
|
relations?: Array<{
|
|
43
46
|
component: string;
|
|
44
|
-
relationship:
|
|
47
|
+
relationship:
|
|
48
|
+
| "alternative"
|
|
49
|
+
| "parent"
|
|
50
|
+
| "child"
|
|
51
|
+
| "sibling"
|
|
52
|
+
| "composition"
|
|
53
|
+
| "complementary"
|
|
54
|
+
| "used-by";
|
|
45
55
|
note: string;
|
|
46
56
|
}>;
|
|
47
57
|
variants: Array<{
|
|
@@ -55,19 +65,32 @@ export interface CompiledContractOutput {
|
|
|
55
65
|
scenarioTags?: string[];
|
|
56
66
|
a11yRules?: string[];
|
|
57
67
|
bans?: Array<{ pattern: string; message: string }>;
|
|
58
|
-
compoundChildren?: Record<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
compoundChildren?: Record<
|
|
69
|
+
string,
|
|
70
|
+
{
|
|
71
|
+
required?: boolean;
|
|
72
|
+
accepts?: string[];
|
|
73
|
+
description?: string;
|
|
74
|
+
}
|
|
75
|
+
>;
|
|
63
76
|
canonicalUsage?: string[];
|
|
64
77
|
performanceBudget?: number;
|
|
65
78
|
};
|
|
79
|
+
/** Per-component preview configuration */
|
|
80
|
+
preview?: {
|
|
81
|
+
setupModule?: string;
|
|
82
|
+
wrapperModule?: string;
|
|
83
|
+
wrapperExport?: string;
|
|
84
|
+
css?: string[];
|
|
85
|
+
theme?: "light" | "dark";
|
|
86
|
+
};
|
|
87
|
+
/** Design-token references used by this component */
|
|
88
|
+
tokens?: string[];
|
|
66
89
|
/** Framework hint from contract, used by extractor adapter selection */
|
|
67
90
|
framework?: string;
|
|
68
91
|
/** AI metadata for compound component generation */
|
|
69
92
|
ai?: {
|
|
70
|
-
compositionPattern?:
|
|
93
|
+
compositionPattern?: "compound" | "simple" | "controlled" | "wrapper";
|
|
71
94
|
subComponents?: string[];
|
|
72
95
|
requiredChildren?: string[];
|
|
73
96
|
commonPatterns?: string[];
|
|
@@ -92,16 +115,13 @@ export interface CompiledContractOutput {
|
|
|
92
115
|
* Check if a file path is a component contract file.
|
|
93
116
|
*/
|
|
94
117
|
export function isContractFile(filePath: string): boolean {
|
|
95
|
-
return filePath.endsWith(
|
|
118
|
+
return filePath.endsWith(".contract.json");
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
/**
|
|
99
122
|
* Parse a component contract JSON file into CompiledFragment-compatible output.
|
|
100
123
|
*/
|
|
101
|
-
export function parseComponentContract(
|
|
102
|
-
content: string,
|
|
103
|
-
filePath: string,
|
|
104
|
-
): CompiledContractOutput {
|
|
124
|
+
export function parseComponentContract(content: string, filePath: string): CompiledContractOutput {
|
|
105
125
|
const raw = JSON.parse(content);
|
|
106
126
|
const validated: ComponentContract = componentContractSchema.parse(raw);
|
|
107
127
|
|
|
@@ -126,13 +146,17 @@ export function parseComponentContract(
|
|
|
126
146
|
code: ex.code,
|
|
127
147
|
args: ex.args,
|
|
128
148
|
})),
|
|
129
|
-
contract: validated.contract
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
149
|
+
contract: validated.contract
|
|
150
|
+
? {
|
|
151
|
+
...validated.contract,
|
|
152
|
+
// Merge top-level propsSummary into contract.propsSummary if not already set
|
|
153
|
+
propsSummary: validated.contract.propsSummary ?? validated.propsSummary,
|
|
154
|
+
}
|
|
155
|
+
: {
|
|
156
|
+
propsSummary: validated.propsSummary,
|
|
157
|
+
},
|
|
158
|
+
preview: validated.preview,
|
|
159
|
+
tokens: validated.tokens,
|
|
136
160
|
framework: validated.framework,
|
|
137
161
|
ai: validated.ai,
|
|
138
162
|
propsSummary: validated.propsSummary,
|