@usefragments/core 1.11.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
  2. package/dist/chunk-MZ2FS7U4.js.map +1 -0
  3. package/dist/{chunk-RPSEABY3.js → chunk-QPOKQ5H6.js} +30 -34
  4. package/dist/chunk-QPOKQ5H6.js.map +1 -0
  5. package/dist/codes/index.d.ts +1 -1
  6. package/dist/codes/index.js +1 -1
  7. package/dist/compiled-types/index.d.ts +1 -1
  8. package/dist/generate/index.d.ts +1 -1
  9. package/dist/{governance-BAsy1k2H.d.ts → governance-hOPXGbbs.d.ts} +3 -3
  10. package/dist/index.d.ts +145 -6026
  11. package/dist/index.js +298 -860
  12. package/dist/index.js.map +1 -1
  13. package/dist/preview-runtime.d.ts +1 -1
  14. package/dist/react-types.d.ts +1 -1
  15. package/dist/schemas/index.js +1 -1
  16. package/dist/storyAdapter.d.ts +1 -1
  17. package/dist/test-utils.d.ts +1 -1
  18. package/dist/topology/index.d.ts +1 -1
  19. package/dist/topology/index.js +1 -1
  20. package/package.json +1 -1
  21. package/src/approved-contract-tokens.test.ts +39 -0
  22. package/src/approved-contract-tokens.ts +18 -0
  23. package/src/canonical-source.ts +41 -0
  24. package/src/codes/__tests__/codes.test.ts +4 -0
  25. package/src/codes/codes.ts +10 -0
  26. package/src/domain-ids.test.ts +9 -26
  27. package/src/domain-ids.ts +0 -45
  28. package/src/evaluation/evaluate.ts +17 -7
  29. package/src/evaluation/evaluation-v2-receipt-v1.test.ts +20 -8
  30. package/src/evaluation/index.ts +1 -1
  31. package/src/facts/builders.ts +4 -0
  32. package/src/facts/fact-index.ts +4 -0
  33. package/src/facts/facts.test.ts +70 -0
  34. package/src/facts/types.ts +4 -0
  35. package/src/governance.ts +18 -0
  36. package/src/identity/usage.test.ts +64 -0
  37. package/src/identity/usage.ts +7 -1
  38. package/src/index.ts +15 -79
  39. package/src/rules/components-prefer-library.ts +110 -33
  40. package/src/topology/resolve-area.ts +1 -1
  41. package/src/types.ts +0 -3
  42. package/dist/chunk-3LLRNCPX.js.map +0 -1
  43. package/dist/chunk-RPSEABY3.js.map +0 -1
  44. package/src/feature-plan/digest.ts +0 -217
  45. package/src/feature-plan/feature-plan-v1.test.ts +0 -529
  46. package/src/feature-plan/index.ts +0 -65
  47. package/src/feature-plan/types.ts +0 -628
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ FactIndex,
4
+ asComponentId,
5
+ makeComponentDefinitionFact,
6
+ makeUsageComponentFact,
7
+ makeUsageNodeFact,
8
+ } from "../index.js";
9
+ import { indexComponentIdentityUsage } from "./usage.js";
10
+
11
+ describe("component identity usage joins", () => {
12
+ const key = "src/Button.tsx#Button";
13
+ const catalogId = asComponentId("cloud:source#source::src/Button.tsx::Button");
14
+
15
+ it("preserves fact identity and counts a node once with exact definition evidence", () => {
16
+ const ix = new FactIndex();
17
+ const node = makeUsageNodeFact({
18
+ file: "src/App.tsx",
19
+ nodePath: "0:0",
20
+ element: "Button",
21
+ location: { file: "src/App.tsx", line: 1, column: 0 },
22
+ });
23
+ const original = makeUsageComponentFact({ nodeId: node.id, componentId: catalogId });
24
+ const resolved = makeUsageComponentFact({
25
+ nodeId: node.id,
26
+ componentId: catalogId,
27
+ definitionKey: key,
28
+ });
29
+ expect(resolved.id).toBe(original.id);
30
+ ix.addMany([
31
+ node,
32
+ resolved,
33
+ makeUsageComponentFact({ nodeId: node.id, componentId: asComponentId(key) }),
34
+ makeComponentDefinitionFact({
35
+ file: "src/Button.tsx",
36
+ exportName: "Button",
37
+ componentKey: key,
38
+ propSurface: [],
39
+ renderRoot: { resolution: "intrinsic", tag: "button" },
40
+ }),
41
+ ]);
42
+ expect(indexComponentIdentityUsage(ix).get(key)).toMatchObject({
43
+ usageCount: 1,
44
+ blastRadius: 1,
45
+ });
46
+ expect(indexComponentIdentityUsage(ix).has(catalogId)).toBe(false);
47
+ });
48
+
49
+ it("does not synthesize a definition from catalog evidence", () => {
50
+ const ix = new FactIndex();
51
+ const node = makeUsageNodeFact({
52
+ file: "src/App.tsx",
53
+ nodePath: "0:0",
54
+ element: "Button",
55
+ location: { file: "src/App.tsx", line: 1, column: 0 },
56
+ });
57
+ ix.addMany([
58
+ node,
59
+ makeUsageComponentFact({ nodeId: node.id, componentId: catalogId, definitionKey: key }),
60
+ ]);
61
+ expect(indexComponentIdentityUsage(ix).has(key)).toBe(false);
62
+ expect(indexComponentIdentityUsage(ix).get(catalogId)?.usageCount).toBe(1);
63
+ });
64
+ });
@@ -12,12 +12,18 @@ export function indexComponentIdentityUsage(
12
12
  ix: FactIndex
13
13
  ): ReadonlyMap<string, ComponentIdentityUsage> {
14
14
  const nodesById = new Map(ix.byKind("usage_node").map((node) => [node.id, node]));
15
+ const definitions = new Set(
16
+ ix.byKind("component_definition").map((definition) => definition.componentKey)
17
+ );
15
18
  const nodesByComponent = new Map<string, Map<string, UsageNodeFact>>();
16
19
 
17
20
  for (const usage of ix.byKind("usage_component")) {
18
21
  const node = nodesById.get(usage.nodeId);
19
22
  if (!node) continue;
20
- const componentKey = String(usage.componentId);
23
+ const componentKey =
24
+ usage.definitionKey && definitions.has(usage.definitionKey)
25
+ ? usage.definitionKey
26
+ : String(usage.componentId);
21
27
  const nodes = nodesByComponent.get(componentKey) ?? new Map<string, UsageNodeFact>();
22
28
  nodes.set(node.id, node);
23
29
  nodesByComponent.set(componentKey, nodes);
package/src/index.ts CHANGED
@@ -508,6 +508,7 @@ export type { InferProps } from "./defineFragment.js";
508
508
  export {
509
509
  componentGovernanceRecordSchema,
510
510
  componentGovernanceRecordsSchema,
511
+ parseComponentGovernancePolicyJson,
511
512
  canonicalBridgeV1Schema,
512
513
  g,
513
514
  globalGovernanceRecordSchema,
@@ -683,6 +684,11 @@ export {
683
684
  type RuleConfigView,
684
685
  } from "./rules/rule-config.js";
685
686
  export { indexComponentIdentityUsage, type ComponentIdentityUsage } from "./identity/usage.js";
687
+ export {
688
+ isCanonicalSourceDefinition,
689
+ canonicalSourceContainsFile,
690
+ canonicalSourceIncludesExport,
691
+ } from "./canonical-source.js";
686
692
  export { nearestSignedScaleValue } from "./rules/utils.js";
687
693
  export {
688
694
  buildSpacingTokenLookup,
@@ -1060,25 +1066,11 @@ export {
1060
1066
  mintEvaluationReceiptId,
1061
1067
  evaluationReceiptIdSchema,
1062
1068
  evaluationReceiptIdStringSchema,
1063
- featurePlanIdFromEntropy,
1064
- featurePlanIdSchema,
1065
- featurePlanIdStringSchema,
1066
- featureRevisionIdFromDigest,
1067
- featureRevisionIdSchema,
1068
- featureRevisionIdStringSchema,
1069
1069
  parseAnalysisPlanId,
1070
1070
  parseDigestHex,
1071
1071
  parseEvaluationReceiptId,
1072
- parseFeaturePlanId,
1073
- parseFeatureRevisionId,
1074
- } from "./domain-ids.js";
1075
- export type {
1076
- AnalysisPlanId,
1077
- DigestHex,
1078
- EvaluationReceiptId,
1079
- FeaturePlanId,
1080
- FeatureRevisionId,
1081
1072
  } from "./domain-ids.js";
1073
+ export type { AnalysisPlanId, DigestHex, EvaluationReceiptId } from "./domain-ids.js";
1082
1074
  export {
1083
1075
  PROVIDER_ID_MAX_BYTES_V1,
1084
1076
  REPOSITORY_BINDING_ID_MAX_BYTES_V1,
@@ -1118,70 +1110,6 @@ export type {
1118
1110
  CoverageSummaryV1,
1119
1111
  } from "./analysis-plan/index.js";
1120
1112
 
1121
- export {
1122
- AGENT_FEATURE_CONTEXT_MAX_BYTES_V1,
1123
- FEATURE_PLAN_MAX_ACCEPTANCE_ITEMS_V1,
1124
- FEATURE_PLAN_MAX_DECISIONS_V1,
1125
- FEATURE_PLAN_MAX_EVIDENCE_REFS_V1,
1126
- FEATURE_PLAN_MAX_GAPS_V1,
1127
- FEATURE_PLAN_MAX_SCENARIOS_V1,
1128
- FEATURE_PLAN_MAX_TEXT_BYTES_V1,
1129
- FEATURE_PLAN_MAX_USES_PER_KIND_V1,
1130
- agentFeatureContextV1Schema,
1131
- approvedFeatureManifestInputV1Schema,
1132
- approvedFeatureManifestRevisionDigestV1,
1133
- approvedFeatureManifestV1Schema,
1134
- buildApprovedFeatureManifestV1,
1135
- buildFeatureProposalV1,
1136
- capabilityGapV1Schema,
1137
- componentUseDecisionV1Schema,
1138
- decisionRequestV1Schema,
1139
- decisionResolutionV1Schema,
1140
- evidenceRefV1Schema,
1141
- featureAcceptanceItemV1Schema,
1142
- featurePinsV1Schema,
1143
- featureProposalInputV1Schema,
1144
- featureProposalRevisionDigestV1,
1145
- featureProposalV1Schema,
1146
- gapDispositionV1Schema,
1147
- layoutConcernV1Schema,
1148
- layoutOwnershipV1Schema,
1149
- normalizeApprovedFeatureManifestInputV1,
1150
- normalizeFeatureProposalInputV1,
1151
- parseAgentFeatureContextV1,
1152
- parseApprovedFeatureManifestV1,
1153
- parseFeatureProposalV1,
1154
- patternStatusV1Schema,
1155
- patternUseDecisionV1Schema,
1156
- placementDecisionV1Schema,
1157
- responsiveDecisionV1Schema,
1158
- scenarioRequirementV1Schema,
1159
- tokenUseDecisionV1Schema,
1160
- } from "./feature-plan/index.js";
1161
- export type {
1162
- AgentFeatureContextV1,
1163
- ApprovedFeatureManifestInputV1,
1164
- ApprovedFeatureManifestV1,
1165
- CapabilityGapV1,
1166
- ComponentUseDecisionV1,
1167
- DecisionRequestV1,
1168
- DecisionResolutionV1,
1169
- EvidenceRefV1,
1170
- FeatureAcceptanceItemV1,
1171
- FeaturePinsV1,
1172
- FeatureProposalInputV1,
1173
- FeatureProposalV1,
1174
- GapDispositionV1,
1175
- LayoutConcernV1,
1176
- LayoutOwnershipV1,
1177
- PatternStatusV1,
1178
- PatternUseDecisionV1,
1179
- PlacementDecisionV1,
1180
- ResponsiveDecisionV1,
1181
- ScenarioRequirementV1,
1182
- TokenUseDecisionV1,
1183
- } from "./feature-plan/index.js";
1184
-
1185
1113
  // Topology — map file paths to product areas (area-scoped governance).
1186
1114
  // Also available at the `@usefragments/core/topology` subpath.
1187
1115
  export { resolveArea } from "./topology/index.js";
@@ -1203,6 +1131,8 @@ export {
1203
1131
  EVALUATOR_VERSION_V2,
1204
1132
  effectiveEvaluationWaiverV2Schema,
1205
1133
  evaluate,
1134
+ evaluationInputDigestV2,
1135
+ evaluationResultDigestV2,
1206
1136
  evaluationInputV2Schema,
1207
1137
  evaluationReceiptDigestV1,
1208
1138
  evaluationReceiptMintInputV1Schema,
@@ -1245,3 +1175,9 @@ export type {
1245
1175
  ProviderProofSemanticV1,
1246
1176
  ProviderProofV1,
1247
1177
  } from "./evaluation/index.js";
1178
+
1179
+ export {
1180
+ APPROVED_CONTRACT_TOKEN_MAX_NAMES,
1181
+ APPROVED_CONTRACT_TOKEN_MAX_NAME_LENGTH,
1182
+ isApprovedContractTokenNames,
1183
+ } from "./approved-contract-tokens.js";
@@ -2,6 +2,8 @@ import type { CanonicalSource, GovernanceSeverity } from "../governance.js";
2
2
  import type {
3
3
  FactId,
4
4
  FactIndex,
5
+ ComponentDefinitionFact,
6
+ FactLocation,
5
7
  UsageImportFact,
6
8
  UsageNodeFact,
7
9
  UsagePropResolvedFact,
@@ -15,6 +17,8 @@ import {
15
17
  type RawHtmlPrecisionTier,
16
18
  } from "../raw-html-canonical.js";
17
19
  import { ownedImportMatchesRoot, ownedImportsEqual } from "../package-identity-match.js";
20
+ import { indexComponentIdentityUsage } from "../identity/usage.js";
21
+ import { canonicalSourceContainsFile, canonicalSourceIncludesExport } from "../canonical-source.js";
18
22
  import {
19
23
  canonicalDirectionConflictsTarget,
20
24
  projectCanonicalDirectionConflicts,
@@ -262,6 +266,13 @@ export function ruleComponentsPreferLibrary(ix: FactIndex): Finding[] {
262
266
  const configuredSources = canonicalSourcesFromPolicy(policy.options?.canonicalSources);
263
267
  const conflicts = projectCanonicalDirectionConflicts(ix, configuredSources);
264
268
  const sources = configuredSources.map((source) => suppressConflictedExports(source, conflicts));
269
+ const approvedLocalDefinitionKeys = Array.isArray(policy.options?.approvedLocalDefinitionKeys)
270
+ ? new Set(
271
+ policy.options.approvedLocalDefinitionKeys.filter(
272
+ (key): key is string => typeof key === "string"
273
+ )
274
+ )
275
+ : undefined;
265
276
  const mappings = canonicalMappingsFromPolicy(policy.options?.canonicalMappings).filter(
266
277
  (mapping) => !canonicalDirectionConflictsTarget(conflicts, mapping.importPath, mapping.name)
267
278
  );
@@ -278,16 +289,41 @@ export function ruleComponentsPreferLibrary(ix: FactIndex): Finding[] {
278
289
  // canonical primitives imported by a relative/`@/` path; a browser-safe string
279
290
  // comparison of the raw specifier could never recognize them, so the resolved
280
291
  // fact is the identity of record. Never flag them as import impostors.
281
- const canonicalDirectoryResolvedNodes = indexCanonicalDirectoryResolvedNodes(ix, sources);
292
+ const canonicalDirectoryResolvedNodes = indexCanonicalDirectoryResolvedNodes(
293
+ ix,
294
+ sources,
295
+ approvedLocalDefinitionKeys
296
+ );
282
297
  const shadowRenderRootNodeIds =
283
298
  ix.policy.ruleConfig("components/shadow-component")?.enabled === true
284
299
  ? indexShadowRenderRootNodeIds(ix)
285
300
  : new Set<FactId>();
286
301
  const findings: Finding[] = [];
287
302
  const seenImportFixes = new Set<string>();
303
+ const canonicalIdentities = new Map(
304
+ ix
305
+ .byKind("component_identity")
306
+ .map((identity) => [identity.componentKey, identity.state === "canonical"])
307
+ );
308
+ const definitionsByFile = new Map<string, ComponentDefinitionFact[]>();
309
+ for (const definition of ix.byKind("component_definition")) {
310
+ const file = normalizePath(definition.file);
311
+ const definitions = definitionsByFile.get(file) ?? [];
312
+ definitions.push(definition);
313
+ definitionsByFile.set(file, definitions);
314
+ }
288
315
 
289
316
  for (const node of ix.byKind("usage_node")) {
290
- if (isCanonicalSourceImplementationFile(node.file, sources)) continue;
317
+ if (
318
+ isCanonicalSourceImplementation(
319
+ node,
320
+ sources,
321
+ definitionsByFile,
322
+ canonicalIdentities,
323
+ approvedLocalDefinitionKeys
324
+ )
325
+ )
326
+ continue;
291
327
  if (node.element.includes(".")) continue;
292
328
  if (canonicalDirectoryResolvedNodes.has(node.id)) continue;
293
329
  if (shadowRenderRootNodeIds.has(node.id)) continue;
@@ -299,7 +335,17 @@ export function ruleComponentsPreferLibrary(ix: FactIndex): Finding[] {
299
335
  node.element === "input" ? readStaticStringProp(props, "type") : undefined;
300
336
  const mapped = findMapping(node, nodeInputType, mappings);
301
337
  if (mapped && imported && isMappedCanonicalImport(imported, mapped)) continue;
302
- if (imported && isCanonicalImport(imported, node.element, sources)) continue;
338
+ if (
339
+ imported &&
340
+ isCanonicalImport(
341
+ imported,
342
+ node.element,
343
+ approvedLocalDefinitionKeys === undefined
344
+ ? sources
345
+ : sources.filter((source) => source.kind !== "directory")
346
+ )
347
+ )
348
+ continue;
303
349
 
304
350
  if (mapped) {
305
351
  if (!shouldSuggestMapped(node, nodeInputType, imported, mapped)) continue;
@@ -1154,22 +1200,34 @@ function canonicalImportPath(source: CanonicalSource): string | undefined {
1154
1200
  */
1155
1201
  function indexCanonicalDirectoryResolvedNodes(
1156
1202
  ix: FactIndex,
1157
- sources: readonly CanonicalSource[]
1203
+ sources: readonly CanonicalSource[],
1204
+ approvedLocalDefinitionKeys?: ReadonlySet<string>
1158
1205
  ): Set<FactId> {
1159
1206
  const out = new Set<FactId>();
1160
- const directoryPaths = sources
1161
- .filter(
1162
- (source): source is Extract<CanonicalSource, { kind: "directory" }> =>
1163
- source.kind === "directory"
1164
- )
1165
- .map((source) => normalizePath(source.path));
1166
- if (directoryPaths.length === 0) return out;
1167
- for (const fact of ix.byKind("usage_component")) {
1168
- const module = componentIdModule(fact.componentId);
1207
+ const directorySources = sources.filter(
1208
+ (source): source is Extract<CanonicalSource, { kind: "directory" }> =>
1209
+ source.kind === "directory"
1210
+ );
1211
+ if (directorySources.length === 0) return out;
1212
+ const identities = new Map(
1213
+ ix.byKind("component_identity").map((identity) => [identity.componentKey, identity])
1214
+ );
1215
+ for (const [componentKey, usage] of indexComponentIdentityUsage(ix)) {
1216
+ if (approvedLocalDefinitionKeys !== undefined && !approvedLocalDefinitionKeys.has(componentKey))
1217
+ continue;
1218
+ const identity = identities.get(componentKey);
1219
+ if (identity && identity.state !== "canonical") continue;
1220
+ const module = componentIdModule(componentKey);
1169
1221
  if (module === undefined) continue;
1170
- const normalizedModule = normalizePath(module);
1171
- if (directoryPaths.some((path) => isPathInSource(normalizedModule, path))) {
1172
- out.add(fact.nodeId);
1222
+ const exportName = componentKey.slice(componentKey.indexOf("#") + 1);
1223
+ if (
1224
+ directorySources.some(
1225
+ (source) =>
1226
+ canonicalSourceContainsFile(source, module) &&
1227
+ canonicalSourceIncludesExport(source, exportName)
1228
+ )
1229
+ ) {
1230
+ for (const node of usage.nodes) out.add(node.id);
1173
1231
  }
1174
1232
  }
1175
1233
  return out;
@@ -1181,28 +1239,47 @@ function componentIdModule(componentId: string): string | undefined {
1181
1239
  return hash > 0 ? componentId.slice(0, hash) : undefined;
1182
1240
  }
1183
1241
 
1184
- function isCanonicalSourceImplementationFile(
1185
- file: string,
1186
- sources: readonly CanonicalSource[]
1242
+ function isCanonicalSourceImplementation(
1243
+ node: UsageNodeFact,
1244
+ sources: readonly CanonicalSource[],
1245
+ definitionsByFile: ReadonlyMap<string, readonly ComponentDefinitionFact[]>,
1246
+ canonicalIdentities: ReadonlyMap<string, boolean>,
1247
+ approvedLocalDefinitionKeys?: ReadonlySet<string>
1187
1248
  ): boolean {
1188
- const normalizedFile = normalizePath(file);
1249
+ const definitions = definitionsByFile.get(normalizePath(node.file)) ?? [];
1189
1250
  return sources.some((source) => {
1190
- if (source.kind === "npm" && source.implementationPath) {
1191
- return isPathInSource(normalizedFile, source.implementationPath);
1192
- }
1193
- if (source.kind === "directory") {
1194
- return isPathInSource(normalizedFile, source.path);
1195
- }
1196
- if (source.kind === "registry") {
1197
- return isPathInSource(normalizedFile, source.installPath);
1198
- }
1199
- return false;
1251
+ if (!canonicalSourceContainsFile(source, node.file)) return false;
1252
+ // Legacy package/registry implementation roots declare file ownership;
1253
+ // their export filters describe the public API. A frozen local inventory
1254
+ // always narrows that ownership to exact approved definition regions.
1255
+ if (
1256
+ approvedLocalDefinitionKeys === undefined &&
1257
+ (source.kind !== "directory" || (source.include === undefined && !source.exclude?.length))
1258
+ )
1259
+ return true;
1260
+ const owners = definitions.filter(
1261
+ (definition) => definition.location && locationContains(definition.location, node.location)
1262
+ );
1263
+ // Unlocated/ambiguous source cannot exempt arbitrary application code.
1264
+ return (
1265
+ owners.length > 0 &&
1266
+ owners.every(
1267
+ (definition) =>
1268
+ (approvedLocalDefinitionKeys === undefined ||
1269
+ approvedLocalDefinitionKeys.has(definition.componentKey)) &&
1270
+ canonicalIdentities.get(definition.componentKey) !== false &&
1271
+ canonicalSourceIncludesExport(source, definition.exportName)
1272
+ )
1273
+ );
1200
1274
  });
1201
1275
  }
1202
1276
 
1203
- function isPathInSource(file: string, sourcePath: string): boolean {
1204
- const normalizedSource = normalizePath(sourcePath).replace(/\/$/, "");
1205
- return file === normalizedSource || file.startsWith(`${normalizedSource}/`);
1277
+ function locationContains(region: FactLocation, node: FactLocation): boolean {
1278
+ if (region.endLine === undefined || region.endColumn === undefined) return false;
1279
+ return (
1280
+ (node.line > region.line || (node.line === region.line && node.column >= region.column)) &&
1281
+ (node.line < region.endLine || (node.line === region.endLine && node.column < region.endColumn))
1282
+ );
1206
1283
  }
1207
1284
 
1208
1285
  function normalizePath(path: string): string {
@@ -6,7 +6,7 @@
6
6
  * preview, cross-repo reconciliation) import it; none reimplements glob/area
7
7
  * logic. Pure and browser-safe — no Node, no parser deps.
8
8
  *
9
- * See `apps/cloud/docs/topology/01-architecture.md §1`.
9
+ * See `docs/fragments-v1/ARCHITECTURE.md §1`.
10
10
  */
11
11
 
12
12
  export type AreaCriticality = "low" | "medium" | "high" | "revenue" | "regulated";
package/src/types.ts CHANGED
@@ -922,9 +922,6 @@ export interface ThemeSeeds {
922
922
  /** Neutral palette name */
923
923
  neutral?: "stone" | "ice" | "earth" | "sand" | "fire" | "fragments";
924
924
 
925
- /** Spacing density scale */
926
- density?: "compact" | "default" | "relaxed";
927
-
928
925
  /** Border radius style */
929
926
  radiusStyle?: "sharp" | "subtle" | "default" | "rounded" | "pill";
930
927
 
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/topology/resolve-area.ts"],"sourcesContent":["/**\n * Topology — map a repo-relative file path to its product area.\n *\n * This is the DRY keystone of the topology layer: the ONLY area-matching\n * implementation in the monorepo. Consumers (CLI scan, Cloud managed-editor\n * preview, cross-repo reconciliation) import it; none reimplements glob/area\n * logic. Pure and browser-safe — no Node, no parser deps.\n *\n * See `apps/cloud/docs/topology/01-architecture.md §1`.\n */\n\nexport type AreaCriticality = \"low\" | \"medium\" | \"high\" | \"revenue\" | \"regulated\";\n\nexport interface Area {\n id: string;\n name: string;\n criticality: AreaCriticality;\n owners: string[];\n /** Globs matched against repo-relative file paths in v0. */\n files: string[];\n /** Stored for later route-topology adapters; NOT matched in v0. */\n routes?: string[];\n /** Tie-break: higher wins. Defaults to declaration order. */\n priority?: number;\n}\n\nexport interface Topology {\n /** Bump signals \"re-tag\" to consumers that cache. */\n version: number;\n /** Whether area file globs are matched relative to app.path or repo root. Defaults to \"app\". */\n base?: \"app\" | \"repo\";\n /** Array (not a keyed record) so precedence is deterministic. */\n areas: Area[];\n}\n\nexport interface AreaMatch {\n areaId: string;\n areaName: string;\n criticality: AreaCriticality;\n owners: string[];\n /** The winning pattern — surfaced in reports for transparency. */\n matchedGlob: string;\n}\n\n/**\n * Compile a minimal glob to an anchored RegExp. We intentionally do NOT depend\n * on picomatch: core stays lean, the surface we need is small, and we want\n * exact control over Next.js route-group parens (`(marketing)` is a literal\n * path segment here, not an extglob group).\n *\n * Semantics:\n * - `/**` at a segment boundary (end-of-glob or before `/`) → \"this directory\n * and everything under it\": the leading slash is optional, so\n * `src/app/checkout/**` matches `src/app/checkout` AND any descendant.\n * - a bare/leading `**` → matches across path separators (`.*`). Note a\n * leading globstar-then-slash does NOT match the root (use a lone `**`\n * for \"everything\").\n * - `*` → matches within a single segment (`[^/]*`)\n * - `?` → a single non-separator char\n * - `{a,b}` → alternation `(?:a|b)`\n * - every other regex-significant char (`.`, `(`, `)`, `+`, …) is literal\n *\n * Throws on a malformed pattern (e.g. an unbalanced `{`). Callers compile via\n * `compile()`, which catches the throw so a config typo degrades one glob to a\n * non-match instead of aborting the whole scan.\n */\nfunction globToRegExp(glob: string): RegExp {\n let out = \"\";\n let braceDepth = 0;\n let i = 0;\n while (i < glob.length) {\n const ch = glob[i];\n\n // `/**` at a segment boundary → optional slash + anything-below. Matches\n // the directory itself (`a/**` ⇒ `a`) and every descendant.\n if (\n ch === \"/\" &&\n glob[i + 1] === \"*\" &&\n glob[i + 2] === \"*\" &&\n (glob[i + 3] === undefined || glob[i + 3] === \"/\")\n ) {\n out += \"(?:/.*)?\";\n i += 3; // consume `/**`; a trailing `/` (in `a/**/b`) is handled next pass\n continue;\n }\n\n // Bare/leading globstar — crosses path separators.\n if (ch === \"*\" && glob[i + 1] === \"*\") {\n out += \".*\";\n i += 2;\n continue;\n }\n\n switch (ch) {\n case \"*\":\n out += \"[^/]*\"; // single segment\n break;\n case \"?\":\n out += \"[^/]\";\n break;\n case \"{\":\n braceDepth += 1;\n out += \"(?:\";\n break;\n case \"}\":\n if (braceDepth > 0) {\n braceDepth -= 1;\n out += \")\";\n } else {\n out += \"\\\\}\";\n }\n break;\n case \",\":\n out += braceDepth > 0 ? \"|\" : \",\";\n break;\n // Regex-significant chars kept literal (parens cover route groups).\n case \".\":\n case \"(\":\n case \")\":\n case \"+\":\n case \"^\":\n case \"$\":\n case \"|\":\n case \"[\":\n case \"]\":\n case \"\\\\\":\n out += `\\\\${ch}`;\n break;\n default:\n out += ch;\n }\n i += 1;\n }\n return new RegExp(`^${out}$`);\n}\n\ninterface CompiledArea {\n area: Area;\n matchers: { glob: string; re: RegExp }[];\n}\n\n/**\n * Cache compiled matchers per topology object. Consumers pass the same\n * `topology` for every finding in a run, so this turns N×globs recompiles into\n * one. A reloaded config is a new object → fresh entry, so `version` bumps need\n * no manual invalidation.\n */\nconst compiledCache = new WeakMap<Topology, CompiledArea[]>();\n\nfunction compile(topology: Topology): CompiledArea[] {\n const cached = compiledCache.get(topology);\n if (cached) return cached;\n\n // Order by (priority desc, declaration order). Decorate-sort-undecorate keeps\n // the sort stable across engines.\n const ordered = topology.areas\n .map((area, index) => ({ area, index }))\n .sort((a, b) => {\n const pa = a.area.priority ?? 0;\n const pb = b.area.priority ?? 0;\n if (pa !== pb) return pb - pa;\n return a.index - b.index;\n })\n .map(({ area }) => ({\n area,\n matchers: area.files.flatMap((glob) => {\n try {\n return [{ glob, re: globToRegExp(glob) }];\n } catch {\n // A malformed glob (e.g. unbalanced `{`) compiles to an invalid\n // RegExp. Skip it rather than aborting the entire scan — topology\n // config is user-authored, so one typo must not take down\n // `fragments check`. Its files simply fall through to \"Unassigned\".\n if (typeof console !== \"undefined\") {\n console.warn(\n `[topology] ignoring invalid glob in area \"${area.id}\": ${JSON.stringify(glob)}`\n );\n }\n return [];\n }\n }),\n }));\n\n compiledCache.set(topology, ordered);\n return ordered;\n}\n\n/** Normalize to a POSIX, repo-relative path before matching. */\nfunction normalizePath(repoRelPath: string): string {\n return repoRelPath.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\").replace(/^\\/+/, \"\");\n}\n\n/**\n * Resolve the product area for a repo-relative path. First match by\n * `(priority desc, declaration order)`. Returns `null` when nothing matches —\n * the caller buckets that as the explicit \"Unassigned\" area (never dropped;\n * dropping unmatched evidence would make coverage lie).\n */\nexport function resolveArea(repoRelPath: string, topology: Topology): AreaMatch | null {\n const path = normalizePath(repoRelPath);\n for (const { area, matchers } of compile(topology)) {\n for (const { glob, re } of matchers) {\n if (re.test(path)) {\n return {\n areaId: area.id,\n areaName: area.name,\n criticality: area.criticality,\n owners: area.owners,\n matchedGlob: glob,\n };\n }\n }\n }\n return null;\n}\n"],"mappings":"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","names":[]}