@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.
- package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
- package/dist/chunk-MZ2FS7U4.js.map +1 -0
- package/dist/{chunk-RPSEABY3.js → chunk-QPOKQ5H6.js} +30 -34
- package/dist/chunk-QPOKQ5H6.js.map +1 -0
- package/dist/codes/index.d.ts +1 -1
- package/dist/codes/index.js +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-BAsy1k2H.d.ts → governance-hOPXGbbs.d.ts} +3 -3
- package/dist/index.d.ts +145 -6026
- package/dist/index.js +298 -860
- package/dist/index.js.map +1 -1
- package/dist/preview-runtime.d.ts +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/storyAdapter.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/topology/index.d.ts +1 -1
- package/dist/topology/index.js +1 -1
- package/package.json +1 -1
- package/src/approved-contract-tokens.test.ts +39 -0
- package/src/approved-contract-tokens.ts +18 -0
- package/src/canonical-source.ts +41 -0
- package/src/codes/__tests__/codes.test.ts +4 -0
- package/src/codes/codes.ts +10 -0
- package/src/domain-ids.test.ts +9 -26
- package/src/domain-ids.ts +0 -45
- package/src/evaluation/evaluate.ts +17 -7
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +20 -8
- package/src/evaluation/index.ts +1 -1
- package/src/facts/builders.ts +4 -0
- package/src/facts/fact-index.ts +4 -0
- package/src/facts/facts.test.ts +70 -0
- package/src/facts/types.ts +4 -0
- package/src/governance.ts +18 -0
- package/src/identity/usage.test.ts +64 -0
- package/src/identity/usage.ts +7 -1
- package/src/index.ts +15 -79
- package/src/rules/components-prefer-library.ts +110 -33
- package/src/topology/resolve-area.ts +1 -1
- package/src/types.ts +0 -3
- package/dist/chunk-3LLRNCPX.js.map +0 -1
- package/dist/chunk-RPSEABY3.js.map +0 -1
- package/src/feature-plan/digest.ts +0 -217
- package/src/feature-plan/feature-plan-v1.test.ts +0 -529
- package/src/feature-plan/index.ts +0 -65
- 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
|
+
});
|
package/src/identity/usage.ts
CHANGED
|
@@ -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 =
|
|
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(
|
|
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 (
|
|
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 (
|
|
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
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
|
1171
|
-
if (
|
|
1172
|
-
|
|
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
|
|
1185
|
-
|
|
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
|
|
1249
|
+
const definitions = definitionsByFile.get(normalizePath(node.file)) ?? [];
|
|
1189
1250
|
return sources.some((source) => {
|
|
1190
|
-
if (source
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
|
1204
|
-
|
|
1205
|
-
return
|
|
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 `
|
|
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":[]}
|