@contractspec/module.workspace 1.44.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/README.md +50 -0
- package/dist/ai/code-generation.d.ts +28 -0
- package/dist/ai/code-generation.d.ts.map +1 -0
- package/dist/ai/code-generation.js +138 -0
- package/dist/ai/code-generation.js.map +1 -0
- package/dist/ai/spec-creation.d.ts +27 -0
- package/dist/ai/spec-creation.d.ts.map +1 -0
- package/dist/ai/spec-creation.js +102 -0
- package/dist/ai/spec-creation.js.map +1 -0
- package/dist/analysis/deps/graph.d.ts +34 -0
- package/dist/analysis/deps/graph.d.ts.map +1 -0
- package/dist/analysis/deps/graph.js +85 -0
- package/dist/analysis/deps/graph.js.map +1 -0
- package/dist/analysis/deps/parse-imports.d.ts +17 -0
- package/dist/analysis/deps/parse-imports.d.ts.map +1 -0
- package/dist/analysis/deps/parse-imports.js +31 -0
- package/dist/analysis/deps/parse-imports.js.map +1 -0
- package/dist/analysis/diff/deep-diff.d.ts +33 -0
- package/dist/analysis/diff/deep-diff.d.ts.map +1 -0
- package/dist/analysis/diff/deep-diff.js +114 -0
- package/dist/analysis/diff/deep-diff.js.map +1 -0
- package/dist/analysis/diff/semantic.d.ts +11 -0
- package/dist/analysis/diff/semantic.d.ts.map +1 -0
- package/dist/analysis/diff/semantic.js +97 -0
- package/dist/analysis/diff/semantic.js.map +1 -0
- package/dist/analysis/feature-scan.d.ts +15 -0
- package/dist/analysis/feature-scan.d.ts.map +1 -0
- package/dist/analysis/feature-scan.js +152 -0
- package/dist/analysis/feature-scan.js.map +1 -0
- package/dist/analysis/grouping.d.ts +79 -0
- package/dist/analysis/grouping.d.ts.map +1 -0
- package/dist/analysis/grouping.js +115 -0
- package/dist/analysis/grouping.js.map +1 -0
- package/dist/analysis/impact/classifier.d.ts +19 -0
- package/dist/analysis/impact/classifier.d.ts.map +1 -0
- package/dist/analysis/impact/classifier.js +135 -0
- package/dist/analysis/impact/classifier.js.map +1 -0
- package/dist/analysis/impact/index.js +2 -0
- package/dist/analysis/impact/rules.d.ts +35 -0
- package/dist/analysis/impact/rules.d.ts.map +1 -0
- package/dist/analysis/impact/rules.js +154 -0
- package/dist/analysis/impact/rules.js.map +1 -0
- package/dist/analysis/impact/types.d.ts +95 -0
- package/dist/analysis/impact/types.d.ts.map +1 -0
- package/dist/analysis/index.js +14 -0
- package/dist/analysis/snapshot/index.js +2 -0
- package/dist/analysis/snapshot/normalizer.d.ts +36 -0
- package/dist/analysis/snapshot/normalizer.d.ts.map +1 -0
- package/dist/analysis/snapshot/normalizer.js +66 -0
- package/dist/analysis/snapshot/normalizer.js.map +1 -0
- package/dist/analysis/snapshot/snapshot.d.ts +18 -0
- package/dist/analysis/snapshot/snapshot.d.ts.map +1 -0
- package/dist/analysis/snapshot/snapshot.js +163 -0
- package/dist/analysis/snapshot/snapshot.js.map +1 -0
- package/dist/analysis/snapshot/types.d.ts +80 -0
- package/dist/analysis/snapshot/types.d.ts.map +1 -0
- package/dist/analysis/spec-scan.d.ts +34 -0
- package/dist/analysis/spec-scan.d.ts.map +1 -0
- package/dist/analysis/spec-scan.js +349 -0
- package/dist/analysis/spec-scan.js.map +1 -0
- package/dist/analysis/validate/spec-structure.d.ts +29 -0
- package/dist/analysis/validate/spec-structure.d.ts.map +1 -0
- package/dist/analysis/validate/spec-structure.js +139 -0
- package/dist/analysis/validate/spec-structure.js.map +1 -0
- package/dist/formatter.d.ts +42 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +163 -0
- package/dist/formatter.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +33 -0
- package/dist/templates/app-config.d.ts +7 -0
- package/dist/templates/app-config.d.ts.map +1 -0
- package/dist/templates/app-config.js +106 -0
- package/dist/templates/app-config.js.map +1 -0
- package/dist/templates/data-view.d.ts +7 -0
- package/dist/templates/data-view.d.ts.map +1 -0
- package/dist/templates/data-view.js +69 -0
- package/dist/templates/data-view.js.map +1 -0
- package/dist/templates/event.d.ts +11 -0
- package/dist/templates/event.d.ts.map +1 -0
- package/dist/templates/event.js +41 -0
- package/dist/templates/event.js.map +1 -0
- package/dist/templates/experiment.d.ts +7 -0
- package/dist/templates/experiment.d.ts.map +1 -0
- package/dist/templates/experiment.js +88 -0
- package/dist/templates/experiment.js.map +1 -0
- package/dist/templates/handler.d.ts +20 -0
- package/dist/templates/handler.d.ts.map +1 -0
- package/dist/templates/handler.js +96 -0
- package/dist/templates/handler.js.map +1 -0
- package/dist/templates/integration-utils.js +105 -0
- package/dist/templates/integration-utils.js.map +1 -0
- package/dist/templates/integration.d.ts +7 -0
- package/dist/templates/integration.d.ts.map +1 -0
- package/dist/templates/integration.js +63 -0
- package/dist/templates/integration.js.map +1 -0
- package/dist/templates/knowledge.d.ts +7 -0
- package/dist/templates/knowledge.d.ts.map +1 -0
- package/dist/templates/knowledge.js +69 -0
- package/dist/templates/knowledge.js.map +1 -0
- package/dist/templates/migration.d.ts +7 -0
- package/dist/templates/migration.d.ts.map +1 -0
- package/dist/templates/migration.js +61 -0
- package/dist/templates/migration.js.map +1 -0
- package/dist/templates/operation.d.ts +11 -0
- package/dist/templates/operation.d.ts.map +1 -0
- package/dist/templates/operation.js +101 -0
- package/dist/templates/operation.js.map +1 -0
- package/dist/templates/presentation.d.ts +11 -0
- package/dist/templates/presentation.d.ts.map +1 -0
- package/dist/templates/presentation.js +79 -0
- package/dist/templates/presentation.js.map +1 -0
- package/dist/templates/telemetry.d.ts +7 -0
- package/dist/templates/telemetry.d.ts.map +1 -0
- package/dist/templates/telemetry.js +90 -0
- package/dist/templates/telemetry.js.map +1 -0
- package/dist/templates/utils.d.ts +27 -0
- package/dist/templates/utils.d.ts.map +1 -0
- package/dist/templates/utils.js +39 -0
- package/dist/templates/utils.js.map +1 -0
- package/dist/templates/workflow-runner.d.ts +16 -0
- package/dist/templates/workflow-runner.d.ts.map +1 -0
- package/dist/templates/workflow-runner.js +49 -0
- package/dist/templates/workflow-runner.js.map +1 -0
- package/dist/templates/workflow.d.ts +11 -0
- package/dist/templates/workflow.d.ts.map +1 -0
- package/dist/templates/workflow.js +68 -0
- package/dist/templates/workflow.js.map +1 -0
- package/dist/types/analysis-types.d.ts +126 -0
- package/dist/types/analysis-types.d.ts.map +1 -0
- package/dist/types/generation-types.d.ts +84 -0
- package/dist/types/generation-types.d.ts.map +1 -0
- package/dist/types/generation-types.js +21 -0
- package/dist/types/generation-types.js.map +1 -0
- package/dist/types/spec-types.d.ts +345 -0
- package/dist/types/spec-types.d.ts.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grouping.js","names":["items"],"sources":["../../src/analysis/grouping.ts"],"sourcesContent":["/**\n * Grouping and filtering utilities for ContractSpec workspace analysis.\n * Provides services to filter and group scan results.\n */\n\nimport type {\n FeatureScanResult,\n SpecScanResult,\n} from '../types/analysis-types';\nimport type { Stability } from '../types/spec-types';\n\n/**\n * Filter criteria for spec scan results.\n */\nexport interface SpecFilter {\n /** Filter by tags (item must have at least one matching tag) */\n tags?: string[];\n /** Filter by owners (item must have at least one matching owner) */\n owners?: string[];\n /** Filter by stability levels */\n stability?: Stability[];\n /** Filter by spec type */\n specType?: SpecScanResult['specType'][];\n /** Filter by name pattern (glob) */\n namePattern?: string;\n}\n\n/**\n * Grouping key function type.\n */\nexport type GroupKeyFn<T> = (item: T) => string;\n\n/**\n * Grouped items result.\n */\nexport interface GroupedItems<T> {\n key: string;\n items: T[];\n}\n\n/**\n * Pre-built grouping strategies for spec scan results.\n */\nexport const SpecGroupingStrategies = {\n /** Group by first tag. */\n byTag: (item: SpecScanResult): string => item.tags?.[0] ?? 'untagged',\n\n /** Group by first owner. */\n byOwner: (item: SpecScanResult): string => item.owners?.[0] ?? 'unowned',\n\n /** Group by domain (first segment of name). */\n byDomain: (item: SpecScanResult): string => {\n const key = item.key ?? '';\n if (key.includes('.')) {\n return key.split('.')[0] ?? 'default';\n }\n return 'default';\n },\n\n /** Group by stability. */\n byStability: (item: SpecScanResult): string => item.stability ?? 'stable',\n\n /** Group by spec type. */\n bySpecType: (item: SpecScanResult): string => item.specType,\n\n /** Group by file directory. */\n byDirectory: (item: SpecScanResult): string => {\n const parts = item.filePath.split('/');\n // Return parent directory\n return parts.slice(0, -1).join('/') || '.';\n },\n};\n\n/**\n * Filter specs by criteria.\n */\nexport function filterSpecs(\n specs: SpecScanResult[],\n filter: SpecFilter\n): SpecScanResult[] {\n return specs.filter((spec) => {\n // Filter by tags\n if (filter.tags?.length) {\n const hasMatchingTag = filter.tags.some((tag) =>\n spec.tags?.includes(tag)\n );\n if (!hasMatchingTag) return false;\n }\n\n // Filter by owners\n if (filter.owners?.length) {\n const hasMatchingOwner = filter.owners.some((owner) =>\n spec.owners?.includes(owner)\n );\n if (!hasMatchingOwner) return false;\n }\n\n // Filter by stability\n if (filter.stability?.length) {\n if (!filter.stability.includes(spec.stability ?? 'stable')) {\n return false;\n }\n }\n\n // Filter by spec type\n if (filter.specType?.length) {\n if (!filter.specType.includes(spec.specType)) {\n return false;\n }\n }\n\n // Filter by name pattern\n if (filter.namePattern) {\n const key = spec.key ?? '';\n const pattern = filter.namePattern\n .replace(/\\*/g, '.*')\n .replace(/\\?/g, '.');\n const regex = new RegExp(`^${pattern}$`, 'i');\n if (!regex.test(key)) return false;\n }\n\n return true;\n });\n}\n\n/**\n * Group specs by key function.\n */\nexport function groupSpecs<T>(\n items: T[],\n keyFn: GroupKeyFn<T>\n): Map<string, T[]> {\n const groups = new Map<string, T[]>();\n\n for (const item of items) {\n const key = keyFn(item);\n const existing = groups.get(key);\n if (existing) {\n existing.push(item);\n } else {\n groups.set(key, [item]);\n }\n }\n\n return groups;\n}\n\n/**\n * Group specs and return as array.\n */\nexport function groupSpecsToArray<T>(\n items: T[],\n keyFn: GroupKeyFn<T>\n): GroupedItems<T>[] {\n const map = groupSpecs(items, keyFn);\n return Array.from(map.entries())\n .map(([key, items]) => ({ key, items }))\n .sort((a, b) => a.key.localeCompare(b.key));\n}\n\n/**\n * Get unique tags from spec results.\n */\nexport function getUniqueSpecTags(specs: SpecScanResult[]): string[] {\n const tags = new Set<string>();\n for (const spec of specs) {\n for (const tag of spec.tags ?? []) {\n tags.add(tag);\n }\n }\n return Array.from(tags).sort();\n}\n\n/**\n * Get unique owners from spec results.\n */\nexport function getUniqueSpecOwners(specs: SpecScanResult[]): string[] {\n const owners = new Set<string>();\n for (const spec of specs) {\n for (const owner of spec.owners ?? []) {\n owners.add(owner);\n }\n }\n return Array.from(owners).sort();\n}\n\n/**\n * Get unique domains from spec results.\n */\nexport function getUniqueSpecDomains(specs: SpecScanResult[]): string[] {\n const domains = new Set<string>();\n for (const spec of specs) {\n domains.add(SpecGroupingStrategies.byDomain(spec));\n }\n return Array.from(domains).sort();\n}\n\n/**\n * Filter features by criteria.\n */\nexport function filterFeatures(\n features: FeatureScanResult[],\n filter: SpecFilter\n): FeatureScanResult[] {\n return features.filter((feature) => {\n // Filter by tags\n if (filter.tags?.length) {\n const hasMatchingTag = filter.tags.some((tag) =>\n feature.tags?.includes(tag)\n );\n if (!hasMatchingTag) return false;\n }\n\n // Filter by owners\n if (filter.owners?.length) {\n const hasMatchingOwner = filter.owners.some((owner) =>\n feature.owners?.includes(owner)\n );\n if (!hasMatchingOwner) return false;\n }\n\n // Filter by stability\n if (filter.stability?.length) {\n if (!filter.stability.includes(feature.stability ?? 'stable')) {\n return false;\n }\n }\n\n // Filter by name pattern\n if (filter.namePattern) {\n const pattern = filter.namePattern\n .replace(/\\*/g, '.*')\n .replace(/\\?/g, '.');\n const regex = new RegExp(`^${pattern}$`, 'i');\n if (!regex.test(feature.key)) return false;\n }\n\n return true;\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SemanticDiffItem } from "../../types/analysis-types.js";
|
|
2
|
+
import { SpecSnapshot } from "../snapshot/types.js";
|
|
3
|
+
import { ClassifyOptions, ImpactResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
//#region src/analysis/impact/classifier.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Classify the impact of changes between base and head snapshots.
|
|
9
|
+
*
|
|
10
|
+
* @param baseSpecs - Specs from the base (baseline) version
|
|
11
|
+
* @param headSpecs - Specs from the head (current) version
|
|
12
|
+
* @param diffs - Semantic diff items from comparison
|
|
13
|
+
* @param options - Classification options
|
|
14
|
+
* @returns Classified impact result
|
|
15
|
+
*/
|
|
16
|
+
declare function classifyImpact(baseSpecs: SpecSnapshot[], headSpecs: SpecSnapshot[], diffs: SemanticDiffItem[], options?: ClassifyOptions): ImpactResult;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { classifyImpact };
|
|
19
|
+
//# sourceMappingURL=classifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classifier.d.ts","names":[],"sources":["../../../src/analysis/impact/classifier.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;iBA0BgB,cAAA,YACH,2BACA,uBACJ,8BACE,kBACR"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { DEFAULT_RULES, findMatchingRule } from "./rules.js";
|
|
2
|
+
|
|
3
|
+
//#region src/analysis/impact/classifier.ts
|
|
4
|
+
/**
|
|
5
|
+
* Classify the impact of changes between base and head snapshots.
|
|
6
|
+
*
|
|
7
|
+
* @param baseSpecs - Specs from the base (baseline) version
|
|
8
|
+
* @param headSpecs - Specs from the head (current) version
|
|
9
|
+
* @param diffs - Semantic diff items from comparison
|
|
10
|
+
* @param options - Classification options
|
|
11
|
+
* @returns Classified impact result
|
|
12
|
+
*/
|
|
13
|
+
function classifyImpact(baseSpecs, headSpecs, diffs, options = {}) {
|
|
14
|
+
const rules = options.customRules ?? DEFAULT_RULES;
|
|
15
|
+
const deltas = [];
|
|
16
|
+
const baseMap = new Map(baseSpecs.map((s) => [`${s.key}@${s.version}`, s]));
|
|
17
|
+
const headMap = new Map(headSpecs.map((s) => [`${s.key}@${s.version}`, s]));
|
|
18
|
+
const addedSpecs = [];
|
|
19
|
+
for (const spec of headSpecs) {
|
|
20
|
+
const lookupKey = `${spec.key}@${spec.version}`;
|
|
21
|
+
if (!baseMap.has(lookupKey)) addedSpecs.push({
|
|
22
|
+
key: spec.key,
|
|
23
|
+
version: spec.version,
|
|
24
|
+
type: spec.type
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const removedSpecs = [];
|
|
28
|
+
for (const spec of baseSpecs) {
|
|
29
|
+
const lookupKey = `${spec.key}@${spec.version}`;
|
|
30
|
+
if (!headMap.has(lookupKey)) {
|
|
31
|
+
removedSpecs.push({
|
|
32
|
+
key: spec.key,
|
|
33
|
+
version: spec.version,
|
|
34
|
+
type: spec.type
|
|
35
|
+
});
|
|
36
|
+
deltas.push({
|
|
37
|
+
specKey: spec.key,
|
|
38
|
+
specVersion: spec.version,
|
|
39
|
+
specType: spec.type,
|
|
40
|
+
path: `spec.${spec.key}`,
|
|
41
|
+
severity: "breaking",
|
|
42
|
+
rule: "endpoint-removed",
|
|
43
|
+
description: `${spec.type === "operation" ? "Operation" : "Event"} '${spec.key}' was removed`
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const diff of diffs) {
|
|
48
|
+
const matchingRule = findMatchingRule({
|
|
49
|
+
path: diff.path,
|
|
50
|
+
description: diff.description,
|
|
51
|
+
type: diff.type
|
|
52
|
+
}, rules);
|
|
53
|
+
const specInfo = findSpecInfo(extractSpecKey(diff.path, baseSpecs, headSpecs), baseSpecs, headSpecs);
|
|
54
|
+
deltas.push({
|
|
55
|
+
specKey: specInfo?.key ?? "unknown",
|
|
56
|
+
specVersion: specInfo?.version ?? 0,
|
|
57
|
+
specType: specInfo?.type ?? "operation",
|
|
58
|
+
path: diff.path,
|
|
59
|
+
severity: matchingRule?.severity ?? mapDiffTypeToSeverity(diff.type),
|
|
60
|
+
rule: matchingRule?.id ?? "unknown",
|
|
61
|
+
description: diff.description,
|
|
62
|
+
oldValue: diff.oldValue,
|
|
63
|
+
newValue: diff.newValue
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
for (const spec of addedSpecs) deltas.push({
|
|
67
|
+
specKey: spec.key,
|
|
68
|
+
specVersion: spec.version,
|
|
69
|
+
specType: spec.type,
|
|
70
|
+
path: `spec.${spec.key}`,
|
|
71
|
+
severity: "non_breaking",
|
|
72
|
+
rule: "endpoint-added",
|
|
73
|
+
description: `${spec.type === "operation" ? "Operation" : "Event"} '${spec.key}' was added`
|
|
74
|
+
});
|
|
75
|
+
const summary = calculateSummary(deltas, addedSpecs, removedSpecs);
|
|
76
|
+
const hasBreaking = summary.breaking > 0 || summary.removed > 0;
|
|
77
|
+
const hasNonBreaking = summary.nonBreaking > 0 || summary.added > 0;
|
|
78
|
+
return {
|
|
79
|
+
status: determineStatus(hasBreaking, hasNonBreaking),
|
|
80
|
+
hasBreaking,
|
|
81
|
+
hasNonBreaking,
|
|
82
|
+
summary,
|
|
83
|
+
deltas,
|
|
84
|
+
addedSpecs,
|
|
85
|
+
removedSpecs,
|
|
86
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Calculate summary counts from deltas.
|
|
91
|
+
*/
|
|
92
|
+
function calculateSummary(deltas, addedSpecs, removedSpecs) {
|
|
93
|
+
return {
|
|
94
|
+
breaking: deltas.filter((d) => d.severity === "breaking").length,
|
|
95
|
+
nonBreaking: deltas.filter((d) => d.severity === "non_breaking").length,
|
|
96
|
+
info: deltas.filter((d) => d.severity === "info").length,
|
|
97
|
+
added: addedSpecs.length,
|
|
98
|
+
removed: removedSpecs.length
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Determine overall status from flags.
|
|
103
|
+
*/
|
|
104
|
+
function determineStatus(hasBreaking, hasNonBreaking) {
|
|
105
|
+
if (hasBreaking) return "breaking";
|
|
106
|
+
if (hasNonBreaking) return "non-breaking";
|
|
107
|
+
return "no-impact";
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Map semantic diff type to impact severity.
|
|
111
|
+
*/
|
|
112
|
+
function mapDiffTypeToSeverity(type) {
|
|
113
|
+
switch (type) {
|
|
114
|
+
case "breaking": return "breaking";
|
|
115
|
+
case "removed": return "breaking";
|
|
116
|
+
case "added": return "non_breaking";
|
|
117
|
+
case "changed": return "info";
|
|
118
|
+
default: return "info";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Extract spec key from a diff path (heuristic).
|
|
123
|
+
*/
|
|
124
|
+
function extractSpecKey(_path, _baseSpecs, _headSpecs) {}
|
|
125
|
+
/**
|
|
126
|
+
* Find spec info from key.
|
|
127
|
+
*/
|
|
128
|
+
function findSpecInfo(key, baseSpecs, headSpecs) {
|
|
129
|
+
if (!key) return headSpecs[0] ?? baseSpecs[0];
|
|
130
|
+
return headSpecs.find((s) => s.key === key) ?? baseSpecs.find((s) => s.key === key);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
export { classifyImpact };
|
|
135
|
+
//# sourceMappingURL=classifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classifier.js","names":["deltas: ImpactDelta[]","addedSpecs: ImpactResult['addedSpecs']","removedSpecs: ImpactResult['removedSpecs']"],"sources":["../../../src/analysis/impact/classifier.ts"],"sourcesContent":["/**\n * Impact classifier.\n *\n * Classifies contract changes as breaking, non-breaking, or info.\n */\n\nimport type { SemanticDiffItem } from '../../types/analysis-types';\nimport type { SpecSnapshot } from '../snapshot/types';\nimport { DEFAULT_RULES, findMatchingRule } from './rules';\nimport type {\n ClassifyOptions,\n ImpactDelta,\n ImpactResult,\n ImpactStatus,\n ImpactSummary,\n} from './types';\n\n/**\n * Classify the impact of changes between base and head snapshots.\n *\n * @param baseSpecs - Specs from the base (baseline) version\n * @param headSpecs - Specs from the head (current) version\n * @param diffs - Semantic diff items from comparison\n * @param options - Classification options\n * @returns Classified impact result\n */\nexport function classifyImpact(\n baseSpecs: SpecSnapshot[],\n headSpecs: SpecSnapshot[],\n diffs: SemanticDiffItem[],\n options: ClassifyOptions = {}\n): ImpactResult {\n const rules = options.customRules ?? DEFAULT_RULES;\n const deltas: ImpactDelta[] = [];\n\n // Create lookup maps\n const baseMap = new Map(baseSpecs.map((s) => [`${s.key}@${s.version}`, s]));\n const headMap = new Map(headSpecs.map((s) => [`${s.key}@${s.version}`, s]));\n\n // Detect added specs\n const addedSpecs: ImpactResult['addedSpecs'] = [];\n for (const spec of headSpecs) {\n const lookupKey = `${spec.key}@${spec.version}`;\n if (!baseMap.has(lookupKey)) {\n addedSpecs.push({\n key: spec.key,\n version: spec.version,\n type: spec.type,\n });\n }\n }\n\n // Detect removed specs\n const removedSpecs: ImpactResult['removedSpecs'] = [];\n for (const spec of baseSpecs) {\n const lookupKey = `${spec.key}@${spec.version}`;\n if (!headMap.has(lookupKey)) {\n removedSpecs.push({\n key: spec.key,\n version: spec.version,\n type: spec.type,\n });\n\n // Removed spec is always breaking\n deltas.push({\n specKey: spec.key,\n specVersion: spec.version,\n specType: spec.type,\n path: `spec.${spec.key}`,\n severity: 'breaking',\n rule: 'endpoint-removed',\n description: `${spec.type === 'operation' ? 'Operation' : 'Event'} '${spec.key}' was removed`,\n });\n }\n }\n\n // Classify diffs\n for (const diff of diffs) {\n const matchingRule = findMatchingRule(\n { path: diff.path, description: diff.description, type: diff.type },\n rules\n );\n\n // Extract spec key from path (heuristic)\n const specKey = extractSpecKey(diff.path, baseSpecs, headSpecs);\n const specInfo = findSpecInfo(specKey, baseSpecs, headSpecs);\n\n deltas.push({\n specKey: specInfo?.key ?? 'unknown',\n specVersion: specInfo?.version ?? 0,\n specType: specInfo?.type ?? 'operation',\n path: diff.path,\n severity: matchingRule?.severity ?? mapDiffTypeToSeverity(diff.type),\n rule: matchingRule?.id ?? 'unknown',\n description: diff.description,\n oldValue: diff.oldValue,\n newValue: diff.newValue,\n });\n }\n\n // Add added specs as non-breaking changes\n for (const spec of addedSpecs) {\n deltas.push({\n specKey: spec.key,\n specVersion: spec.version,\n specType: spec.type,\n path: `spec.${spec.key}`,\n severity: 'non_breaking',\n rule: 'endpoint-added',\n description: `${spec.type === 'operation' ? 'Operation' : 'Event'} '${spec.key}' was added`,\n });\n }\n\n // Calculate summary\n const summary = calculateSummary(deltas, addedSpecs, removedSpecs);\n\n // Determine status\n const hasBreaking = summary.breaking > 0 || summary.removed > 0;\n const hasNonBreaking = summary.nonBreaking > 0 || summary.added > 0;\n const status = determineStatus(hasBreaking, hasNonBreaking);\n\n return {\n status,\n hasBreaking,\n hasNonBreaking,\n summary,\n deltas,\n addedSpecs,\n removedSpecs,\n timestamp: new Date().toISOString(),\n };\n}\n\n/**\n * Calculate summary counts from deltas.\n */\nfunction calculateSummary(\n deltas: ImpactDelta[],\n addedSpecs: ImpactResult['addedSpecs'],\n removedSpecs: ImpactResult['removedSpecs']\n): ImpactSummary {\n return {\n breaking: deltas.filter((d) => d.severity === 'breaking').length,\n nonBreaking: deltas.filter((d) => d.severity === 'non_breaking').length,\n info: deltas.filter((d) => d.severity === 'info').length,\n added: addedSpecs.length,\n removed: removedSpecs.length,\n };\n}\n\n/**\n * Determine overall status from flags.\n */\nfunction determineStatus(\n hasBreaking: boolean,\n hasNonBreaking: boolean\n): ImpactStatus {\n if (hasBreaking) return 'breaking';\n if (hasNonBreaking) return 'non-breaking';\n return 'no-impact';\n}\n\n/**\n * Map semantic diff type to impact severity.\n */\nfunction mapDiffTypeToSeverity(type: string): ImpactDelta['severity'] {\n switch (type) {\n case 'breaking':\n return 'breaking';\n case 'removed':\n return 'breaking';\n case 'added':\n return 'non_breaking';\n case 'changed':\n return 'info';\n default:\n return 'info';\n }\n}\n\n/**\n * Extract spec key from a diff path (heuristic).\n */\nfunction extractSpecKey(\n _path: string,\n _baseSpecs: SpecSnapshot[],\n _headSpecs: SpecSnapshot[]\n): string | undefined {\n // This is a simplified heuristic; in practice would need more context\n return undefined;\n}\n\n/**\n * Find spec info from key.\n */\nfunction findSpecInfo(\n key: string | undefined,\n baseSpecs: SpecSnapshot[],\n headSpecs: SpecSnapshot[]\n): SpecSnapshot | undefined {\n if (!key) return headSpecs[0] ?? baseSpecs[0];\n return (\n headSpecs.find((s) => s.key === key) ?? baseSpecs.find((s) => s.key === key)\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ImpactRule, ImpactSeverity } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/analysis/impact/rules.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default breaking change rules.
|
|
7
|
+
*/
|
|
8
|
+
declare const BREAKING_RULES: ImpactRule[];
|
|
9
|
+
/**
|
|
10
|
+
* Non-breaking change rules.
|
|
11
|
+
*/
|
|
12
|
+
declare const NON_BREAKING_RULES: ImpactRule[];
|
|
13
|
+
/**
|
|
14
|
+
* Info-level change rules.
|
|
15
|
+
*/
|
|
16
|
+
declare const INFO_RULES: ImpactRule[];
|
|
17
|
+
/**
|
|
18
|
+
* All default rules in priority order (breaking > non_breaking > info).
|
|
19
|
+
*/
|
|
20
|
+
declare const DEFAULT_RULES: ImpactRule[];
|
|
21
|
+
/**
|
|
22
|
+
* Get rules by severity.
|
|
23
|
+
*/
|
|
24
|
+
declare function getRulesBySeverity(severity: ImpactSeverity): ImpactRule[];
|
|
25
|
+
/**
|
|
26
|
+
* Find matching rule for a delta.
|
|
27
|
+
*/
|
|
28
|
+
declare function findMatchingRule(delta: {
|
|
29
|
+
path: string;
|
|
30
|
+
description: string;
|
|
31
|
+
type: string;
|
|
32
|
+
}, rules?: ImpactRule[]): ImpactRule | undefined;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { BREAKING_RULES, DEFAULT_RULES, INFO_RULES, NON_BREAKING_RULES, findMatchingRule, getRulesBySeverity };
|
|
35
|
+
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.d.ts","names":[],"sources":["../../../src/analysis/impact/rules.ts"],"sourcesContent":[],"mappings":";;;;AAqKA;AASA;AAOA;cA1Ka,gBAAgB;;;;cAiFhB,oBAAoB;;;;cA2CpB,YAAY;;;;cA8BZ,eAAe;;;;iBASZ,kBAAA,WAA6B,iBAAiB;;;;iBAO9C,gBAAA;;;;WAEP,eACN"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//#region src/analysis/impact/rules.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default breaking change rules.
|
|
4
|
+
*/
|
|
5
|
+
const BREAKING_RULES = [
|
|
6
|
+
{
|
|
7
|
+
id: "endpoint-removed",
|
|
8
|
+
description: "Endpoint/operation was removed",
|
|
9
|
+
severity: "breaking",
|
|
10
|
+
matches: (delta) => delta.path.includes("spec.") && delta.type === "removed"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "field-removed",
|
|
14
|
+
description: "Field was removed from response",
|
|
15
|
+
severity: "breaking",
|
|
16
|
+
matches: (delta) => delta.path.includes(".output.") && delta.description.includes("removed")
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "field-type-changed",
|
|
20
|
+
description: "Field type was changed",
|
|
21
|
+
severity: "breaking",
|
|
22
|
+
matches: (delta) => delta.path.includes(".type") && delta.description.includes("type changed")
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "field-made-required",
|
|
26
|
+
description: "Optional field became required",
|
|
27
|
+
severity: "breaking",
|
|
28
|
+
matches: (delta) => delta.path.includes(".required") && delta.description.includes("optional to required")
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "enum-value-removed",
|
|
32
|
+
description: "Enum value was removed",
|
|
33
|
+
severity: "breaking",
|
|
34
|
+
matches: (delta) => delta.path.includes(".enumValues") && delta.description.includes("removed")
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "nullable-removed",
|
|
38
|
+
description: "Field is no longer nullable",
|
|
39
|
+
severity: "breaking",
|
|
40
|
+
matches: (delta) => delta.path.includes(".nullable") && delta.description.includes("no longer nullable")
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "method-changed",
|
|
44
|
+
description: "HTTP method was changed",
|
|
45
|
+
severity: "breaking",
|
|
46
|
+
matches: (delta) => delta.path.includes(".http.method") || delta.path.includes(".method")
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "path-changed",
|
|
50
|
+
description: "HTTP path was changed",
|
|
51
|
+
severity: "breaking",
|
|
52
|
+
matches: (delta) => delta.path.includes(".http.path") || delta.path.includes(".path")
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "required-field-added-to-input",
|
|
56
|
+
description: "Required field was added to input",
|
|
57
|
+
severity: "breaking",
|
|
58
|
+
matches: (delta) => delta.path.includes(".input.") && delta.description.includes("Required field")
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "event-payload-field-removed",
|
|
62
|
+
description: "Event payload field was removed",
|
|
63
|
+
severity: "breaking",
|
|
64
|
+
matches: (delta) => delta.path.includes(".payload.") && delta.description.includes("removed")
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
/**
|
|
68
|
+
* Non-breaking change rules.
|
|
69
|
+
*/
|
|
70
|
+
const NON_BREAKING_RULES = [
|
|
71
|
+
{
|
|
72
|
+
id: "optional-field-added",
|
|
73
|
+
description: "Optional field was added",
|
|
74
|
+
severity: "non_breaking",
|
|
75
|
+
matches: (delta) => delta.description.includes("Optional field") && delta.description.includes("added")
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "endpoint-added",
|
|
79
|
+
description: "New endpoint/operation was added",
|
|
80
|
+
severity: "non_breaking",
|
|
81
|
+
matches: (delta) => delta.path.includes("spec.") && delta.type === "added"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "enum-value-added",
|
|
85
|
+
description: "Enum value was added",
|
|
86
|
+
severity: "non_breaking",
|
|
87
|
+
matches: (delta) => delta.path.includes(".enumValues") && delta.description.includes("added")
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "field-made-optional",
|
|
91
|
+
description: "Required field became optional",
|
|
92
|
+
severity: "non_breaking",
|
|
93
|
+
matches: (delta) => delta.path.includes(".required") && delta.description.includes("required to optional")
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "nullable-added",
|
|
97
|
+
description: "Field is now nullable",
|
|
98
|
+
severity: "non_breaking",
|
|
99
|
+
matches: (delta) => delta.path.includes(".nullable") && delta.description.includes("now nullable")
|
|
100
|
+
}
|
|
101
|
+
];
|
|
102
|
+
/**
|
|
103
|
+
* Info-level change rules.
|
|
104
|
+
*/
|
|
105
|
+
const INFO_RULES = [
|
|
106
|
+
{
|
|
107
|
+
id: "stability-changed",
|
|
108
|
+
description: "Stability level was changed",
|
|
109
|
+
severity: "info",
|
|
110
|
+
matches: (delta) => delta.path.includes(".stability")
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "description-changed",
|
|
114
|
+
description: "Description was changed",
|
|
115
|
+
severity: "info",
|
|
116
|
+
matches: (delta) => delta.path.includes(".description")
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "owners-changed",
|
|
120
|
+
description: "Owners were changed",
|
|
121
|
+
severity: "info",
|
|
122
|
+
matches: (delta) => delta.path.includes(".owners")
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "tags-changed",
|
|
126
|
+
description: "Tags were changed",
|
|
127
|
+
severity: "info",
|
|
128
|
+
matches: (delta) => delta.path.includes(".tags")
|
|
129
|
+
}
|
|
130
|
+
];
|
|
131
|
+
/**
|
|
132
|
+
* All default rules in priority order (breaking > non_breaking > info).
|
|
133
|
+
*/
|
|
134
|
+
const DEFAULT_RULES = [
|
|
135
|
+
...BREAKING_RULES,
|
|
136
|
+
...NON_BREAKING_RULES,
|
|
137
|
+
...INFO_RULES
|
|
138
|
+
];
|
|
139
|
+
/**
|
|
140
|
+
* Get rules by severity.
|
|
141
|
+
*/
|
|
142
|
+
function getRulesBySeverity(severity) {
|
|
143
|
+
return DEFAULT_RULES.filter((rule) => rule.severity === severity);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Find matching rule for a delta.
|
|
147
|
+
*/
|
|
148
|
+
function findMatchingRule(delta, rules = DEFAULT_RULES) {
|
|
149
|
+
return rules.find((rule) => rule.matches(delta));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
//#endregion
|
|
153
|
+
export { BREAKING_RULES, DEFAULT_RULES, INFO_RULES, NON_BREAKING_RULES, findMatchingRule, getRulesBySeverity };
|
|
154
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","names":["BREAKING_RULES: ImpactRule[]","NON_BREAKING_RULES: ImpactRule[]","INFO_RULES: ImpactRule[]","DEFAULT_RULES: ImpactRule[]"],"sources":["../../../src/analysis/impact/rules.ts"],"sourcesContent":["/**\n * Impact classification rules.\n *\n * Defines rules for classifying changes as breaking or non-breaking.\n */\n\nimport type { ImpactRule, ImpactSeverity } from './types';\n\n/**\n * Default breaking change rules.\n */\nexport const BREAKING_RULES: ImpactRule[] = [\n {\n id: 'endpoint-removed',\n description: 'Endpoint/operation was removed',\n severity: 'breaking',\n matches: (delta) =>\n delta.path.includes('spec.') && delta.type === 'removed',\n },\n {\n id: 'field-removed',\n description: 'Field was removed from response',\n severity: 'breaking',\n matches: (delta) =>\n delta.path.includes('.output.') && delta.description.includes('removed'),\n },\n {\n id: 'field-type-changed',\n description: 'Field type was changed',\n severity: 'breaking',\n matches: (delta) =>\n delta.path.includes('.type') &&\n delta.description.includes('type changed'),\n },\n {\n id: 'field-made-required',\n description: 'Optional field became required',\n severity: 'breaking',\n matches: (delta) =>\n delta.path.includes('.required') &&\n delta.description.includes('optional to required'),\n },\n {\n id: 'enum-value-removed',\n description: 'Enum value was removed',\n severity: 'breaking',\n matches: (delta) =>\n delta.path.includes('.enumValues') &&\n delta.description.includes('removed'),\n },\n {\n id: 'nullable-removed',\n description: 'Field is no longer nullable',\n severity: 'breaking',\n matches: (delta) =>\n delta.path.includes('.nullable') &&\n delta.description.includes('no longer nullable'),\n },\n {\n id: 'method-changed',\n description: 'HTTP method was changed',\n severity: 'breaking',\n matches: (delta) =>\n delta.path.includes('.http.method') || delta.path.includes('.method'),\n },\n {\n id: 'path-changed',\n description: 'HTTP path was changed',\n severity: 'breaking',\n matches: (delta) =>\n delta.path.includes('.http.path') || delta.path.includes('.path'),\n },\n {\n id: 'required-field-added-to-input',\n description: 'Required field was added to input',\n severity: 'breaking',\n matches: (delta) =>\n delta.path.includes('.input.') &&\n delta.description.includes('Required field'),\n },\n {\n id: 'event-payload-field-removed',\n description: 'Event payload field was removed',\n severity: 'breaking',\n matches: (delta) =>\n delta.path.includes('.payload.') && delta.description.includes('removed'),\n },\n];\n\n/**\n * Non-breaking change rules.\n */\nexport const NON_BREAKING_RULES: ImpactRule[] = [\n {\n id: 'optional-field-added',\n description: 'Optional field was added',\n severity: 'non_breaking',\n matches: (delta) =>\n delta.description.includes('Optional field') &&\n delta.description.includes('added'),\n },\n {\n id: 'endpoint-added',\n description: 'New endpoint/operation was added',\n severity: 'non_breaking',\n matches: (delta) => delta.path.includes('spec.') && delta.type === 'added',\n },\n {\n id: 'enum-value-added',\n description: 'Enum value was added',\n severity: 'non_breaking',\n matches: (delta) =>\n delta.path.includes('.enumValues') && delta.description.includes('added'),\n },\n {\n id: 'field-made-optional',\n description: 'Required field became optional',\n severity: 'non_breaking',\n matches: (delta) =>\n delta.path.includes('.required') &&\n delta.description.includes('required to optional'),\n },\n {\n id: 'nullable-added',\n description: 'Field is now nullable',\n severity: 'non_breaking',\n matches: (delta) =>\n delta.path.includes('.nullable') &&\n delta.description.includes('now nullable'),\n },\n];\n\n/**\n * Info-level change rules.\n */\nexport const INFO_RULES: ImpactRule[] = [\n {\n id: 'stability-changed',\n description: 'Stability level was changed',\n severity: 'info',\n matches: (delta) => delta.path.includes('.stability'),\n },\n {\n id: 'description-changed',\n description: 'Description was changed',\n severity: 'info',\n matches: (delta) => delta.path.includes('.description'),\n },\n {\n id: 'owners-changed',\n description: 'Owners were changed',\n severity: 'info',\n matches: (delta) => delta.path.includes('.owners'),\n },\n {\n id: 'tags-changed',\n description: 'Tags were changed',\n severity: 'info',\n matches: (delta) => delta.path.includes('.tags'),\n },\n];\n\n/**\n * All default rules in priority order (breaking > non_breaking > info).\n */\nexport const DEFAULT_RULES: ImpactRule[] = [\n ...BREAKING_RULES,\n ...NON_BREAKING_RULES,\n ...INFO_RULES,\n];\n\n/**\n * Get rules by severity.\n */\nexport function getRulesBySeverity(severity: ImpactSeverity): ImpactRule[] {\n return DEFAULT_RULES.filter((rule) => rule.severity === severity);\n}\n\n/**\n * Find matching rule for a delta.\n */\nexport function findMatchingRule(\n delta: { path: string; description: string; type: string },\n rules: ImpactRule[] = DEFAULT_RULES\n): ImpactRule | undefined {\n return rules.find((rule) => rule.matches(delta));\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//#region src/analysis/impact/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Impact classification types.
|
|
4
|
+
*
|
|
5
|
+
* Types for classifying contract changes as breaking or non-breaking.
|
|
6
|
+
*/
|
|
7
|
+
/** Impact severity levels */
|
|
8
|
+
type ImpactSeverity = 'breaking' | 'non_breaking' | 'info';
|
|
9
|
+
/** Status of impact detection */
|
|
10
|
+
type ImpactStatus = 'no-impact' | 'non-breaking' | 'breaking';
|
|
11
|
+
/** A single classified change delta */
|
|
12
|
+
interface ImpactDelta {
|
|
13
|
+
/** Key of the affected spec */
|
|
14
|
+
specKey: string;
|
|
15
|
+
/** Version of the affected spec */
|
|
16
|
+
specVersion: number;
|
|
17
|
+
/** Type of the spec (operation, event) */
|
|
18
|
+
specType: 'operation' | 'event';
|
|
19
|
+
/** Path to the changed element */
|
|
20
|
+
path: string;
|
|
21
|
+
/** Severity classification */
|
|
22
|
+
severity: ImpactSeverity;
|
|
23
|
+
/** Rule that triggered this classification */
|
|
24
|
+
rule: string;
|
|
25
|
+
/** Human-readable description */
|
|
26
|
+
description: string;
|
|
27
|
+
/** Previous value (if applicable) */
|
|
28
|
+
oldValue?: unknown;
|
|
29
|
+
/** New value (if applicable) */
|
|
30
|
+
newValue?: unknown;
|
|
31
|
+
}
|
|
32
|
+
/** Summary counts for impact result */
|
|
33
|
+
interface ImpactSummary {
|
|
34
|
+
breaking: number;
|
|
35
|
+
nonBreaking: number;
|
|
36
|
+
info: number;
|
|
37
|
+
added: number;
|
|
38
|
+
removed: number;
|
|
39
|
+
}
|
|
40
|
+
/** Full impact detection result */
|
|
41
|
+
interface ImpactResult {
|
|
42
|
+
/** Overall status */
|
|
43
|
+
status: ImpactStatus;
|
|
44
|
+
/** Whether any breaking changes were detected */
|
|
45
|
+
hasBreaking: boolean;
|
|
46
|
+
/** Whether any non-breaking changes were detected */
|
|
47
|
+
hasNonBreaking: boolean;
|
|
48
|
+
/** Summary counts */
|
|
49
|
+
summary: ImpactSummary;
|
|
50
|
+
/** All classified deltas */
|
|
51
|
+
deltas: ImpactDelta[];
|
|
52
|
+
/** Specs that were added */
|
|
53
|
+
addedSpecs: {
|
|
54
|
+
key: string;
|
|
55
|
+
version: number;
|
|
56
|
+
type: 'operation' | 'event';
|
|
57
|
+
}[];
|
|
58
|
+
/** Specs that were removed */
|
|
59
|
+
removedSpecs: {
|
|
60
|
+
key: string;
|
|
61
|
+
version: number;
|
|
62
|
+
type: 'operation' | 'event';
|
|
63
|
+
}[];
|
|
64
|
+
/** Base commit/ref */
|
|
65
|
+
baseRef?: string;
|
|
66
|
+
/** Head commit/ref */
|
|
67
|
+
headRef?: string;
|
|
68
|
+
/** Detection timestamp */
|
|
69
|
+
timestamp: string;
|
|
70
|
+
}
|
|
71
|
+
/** Options for impact classification */
|
|
72
|
+
interface ClassifyOptions {
|
|
73
|
+
/** Custom rules to apply */
|
|
74
|
+
customRules?: ImpactRule[];
|
|
75
|
+
/** Treat added required fields as info instead of breaking */
|
|
76
|
+
lenientAddedRequired?: boolean;
|
|
77
|
+
}
|
|
78
|
+
/** A classification rule */
|
|
79
|
+
interface ImpactRule {
|
|
80
|
+
/** Unique rule ID */
|
|
81
|
+
id: string;
|
|
82
|
+
/** Rule description */
|
|
83
|
+
description: string;
|
|
84
|
+
/** Severity when rule matches */
|
|
85
|
+
severity: ImpactSeverity;
|
|
86
|
+
/** Matcher function */
|
|
87
|
+
matches: (delta: {
|
|
88
|
+
path: string;
|
|
89
|
+
description: string;
|
|
90
|
+
type: string;
|
|
91
|
+
}) => boolean;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { ClassifyOptions, ImpactDelta, ImpactResult, ImpactRule, ImpactSeverity, ImpactStatus, ImpactSummary };
|
|
95
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/analysis/impact/types.ts"],"sourcesContent":[],"mappings":";;AAOA;AAGA;AAGA;AAsBA;AASA;AAEU,KAvCE,cAAA,GAuCF,UAAA,GAAA,cAAA,GAAA,MAAA;;AAQA,KA5CE,YAAA,GA4CF,WAAA,GAAA,cAAA,GAAA,UAAA;;AAkBO,UA3DA,WAAA,CA2De;EAQf;;;;;;;;;YAzDL;;;;;;;;;;;UAYK,aAAA;;;;;;;;UASA,YAAA;;UAEP;;;;;;WAMC;;UAED;;;;;;;;;;;;;;;;;;;;;UAkBO,eAAA;;gBAED;;;;;UAMC,UAAA;;;;;;YAML"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { extractEmittedEvents, extractPolicyRefs, extractTestRefs, inferSpecTypeFromFilePath, scanAllSpecsFromSource, scanSpecSource } from "./spec-scan.js";
|
|
2
|
+
import { isFeatureFile, scanFeatureSource } from "./feature-scan.js";
|
|
3
|
+
import { SpecGroupingStrategies, filterFeatures, filterSpecs, getUniqueSpecDomains, getUniqueSpecOwners, getUniqueSpecTags, groupSpecs, groupSpecsToArray } from "./grouping.js";
|
|
4
|
+
import { computeSemanticDiff } from "./diff/semantic.js";
|
|
5
|
+
import { computeFieldDiff, computeFieldsDiff, computeIoDiff, isBreakingChange } from "./diff/deep-diff.js";
|
|
6
|
+
import { addContractNode, buildReverseEdges, createContractGraph, detectCycles, findMissingDependencies, toDot } from "./deps/graph.js";
|
|
7
|
+
import { parseImportedSpecNames } from "./deps/parse-imports.js";
|
|
8
|
+
import { validateSpecStructure } from "./validate/spec-structure.js";
|
|
9
|
+
import { computeHash, normalizeValue, sortFields, sortSpecs, toCanonicalJson } from "./snapshot/normalizer.js";
|
|
10
|
+
import { generateSnapshot } from "./snapshot/snapshot.js";
|
|
11
|
+
import "./snapshot/index.js";
|
|
12
|
+
import { BREAKING_RULES, DEFAULT_RULES, INFO_RULES, NON_BREAKING_RULES, findMatchingRule, getRulesBySeverity } from "./impact/rules.js";
|
|
13
|
+
import { classifyImpact } from "./impact/classifier.js";
|
|
14
|
+
import "./impact/index.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/analysis/snapshot/normalizer.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* JSON normalization utilities for deterministic snapshots.
|
|
4
|
+
*
|
|
5
|
+
* Ensures that snapshots are stable across ordering, whitespace,
|
|
6
|
+
* and other non-semantic differences.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a value for deterministic JSON serialization.
|
|
10
|
+
* - Sorts object keys alphabetically
|
|
11
|
+
* - Removes undefined values
|
|
12
|
+
* - Preserves null values
|
|
13
|
+
*/
|
|
14
|
+
declare function normalizeValue(value: unknown): unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Serialize a value to deterministic JSON string.
|
|
17
|
+
*/
|
|
18
|
+
declare function toCanonicalJson(value: unknown): string;
|
|
19
|
+
/**
|
|
20
|
+
* Compute a SHA-256 hash of canonical JSON representation.
|
|
21
|
+
*/
|
|
22
|
+
declare function computeHash(value: unknown): string;
|
|
23
|
+
/**
|
|
24
|
+
* Sort specs by key and version for deterministic ordering.
|
|
25
|
+
*/
|
|
26
|
+
declare function sortSpecs<T extends {
|
|
27
|
+
key: string;
|
|
28
|
+
version: number;
|
|
29
|
+
}>(specs: T[]): T[];
|
|
30
|
+
/**
|
|
31
|
+
* Sort field snapshots by name for deterministic ordering.
|
|
32
|
+
*/
|
|
33
|
+
declare function sortFields(fields: Record<string, unknown>): Record<string, unknown>;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { computeHash, normalizeValue, sortFields, sortSpecs, toCanonicalJson };
|
|
36
|
+
//# sourceMappingURL=normalizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizer.d.ts","names":[],"sources":["../../../src/analysis/snapshot/normalizer.ts"],"sourcesContent":[],"mappings":";;AAeA;AA+BA;AAOA;AAQA;AAaA;;;;;;;iBA3DgB,cAAA;;;;iBA+BA,eAAA;;;;iBAOA,WAAA;;;;iBAQA;;;UACP,MACN;;;;iBAWa,UAAA,SACN,0BACP"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
|
|
3
|
+
//#region src/analysis/snapshot/normalizer.ts
|
|
4
|
+
/**
|
|
5
|
+
* JSON normalization utilities for deterministic snapshots.
|
|
6
|
+
*
|
|
7
|
+
* Ensures that snapshots are stable across ordering, whitespace,
|
|
8
|
+
* and other non-semantic differences.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a value for deterministic JSON serialization.
|
|
12
|
+
* - Sorts object keys alphabetically
|
|
13
|
+
* - Removes undefined values
|
|
14
|
+
* - Preserves null values
|
|
15
|
+
*/
|
|
16
|
+
function normalizeValue(value) {
|
|
17
|
+
if (value === null || value === void 0) return value === null ? null : void 0;
|
|
18
|
+
if (Array.isArray(value)) return value.map(normalizeValue);
|
|
19
|
+
if (typeof value === "object") {
|
|
20
|
+
const obj = value;
|
|
21
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
22
|
+
const normalized = {};
|
|
23
|
+
for (const key of sortedKeys) {
|
|
24
|
+
const normalizedValue = normalizeValue(obj[key]);
|
|
25
|
+
if (normalizedValue !== void 0) normalized[key] = normalizedValue;
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Serialize a value to deterministic JSON string.
|
|
33
|
+
*/
|
|
34
|
+
function toCanonicalJson(value) {
|
|
35
|
+
return JSON.stringify(normalizeValue(value), null, 0);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Compute a SHA-256 hash of canonical JSON representation.
|
|
39
|
+
*/
|
|
40
|
+
function computeHash(value) {
|
|
41
|
+
const canonical = toCanonicalJson(value);
|
|
42
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sort specs by key and version for deterministic ordering.
|
|
46
|
+
*/
|
|
47
|
+
function sortSpecs(specs) {
|
|
48
|
+
return [...specs].sort((a, b) => {
|
|
49
|
+
const keyCompare = a.key.localeCompare(b.key);
|
|
50
|
+
if (keyCompare !== 0) return keyCompare;
|
|
51
|
+
return a.version - b.version;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Sort field snapshots by name for deterministic ordering.
|
|
56
|
+
*/
|
|
57
|
+
function sortFields(fields) {
|
|
58
|
+
const sorted = {};
|
|
59
|
+
const keys = Object.keys(fields).sort();
|
|
60
|
+
for (const key of keys) sorted[key] = fields[key];
|
|
61
|
+
return sorted;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
export { computeHash, normalizeValue, sortFields, sortSpecs, toCanonicalJson };
|
|
66
|
+
//# sourceMappingURL=normalizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizer.js","names":["normalized: Record<string, unknown>","sorted: Record<string, unknown>"],"sources":["../../../src/analysis/snapshot/normalizer.ts"],"sourcesContent":["/**\n * JSON normalization utilities for deterministic snapshots.\n *\n * Ensures that snapshots are stable across ordering, whitespace,\n * and other non-semantic differences.\n */\n\nimport { createHash } from 'crypto';\n\n/**\n * Normalize a value for deterministic JSON serialization.\n * - Sorts object keys alphabetically\n * - Removes undefined values\n * - Preserves null values\n */\nexport function normalizeValue(value: unknown): unknown {\n if (value === null || value === undefined) {\n return value === null ? null : undefined;\n }\n\n if (Array.isArray(value)) {\n return value.map(normalizeValue);\n }\n\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n const sortedKeys = Object.keys(obj).sort();\n const normalized: Record<string, unknown> = {};\n\n for (const key of sortedKeys) {\n const normalizedValue = normalizeValue(obj[key]);\n // Only include defined values\n if (normalizedValue !== undefined) {\n normalized[key] = normalizedValue;\n }\n }\n\n return normalized;\n }\n\n return value;\n}\n\n/**\n * Serialize a value to deterministic JSON string.\n */\nexport function toCanonicalJson(value: unknown): string {\n return JSON.stringify(normalizeValue(value), null, 0);\n}\n\n/**\n * Compute a SHA-256 hash of canonical JSON representation.\n */\nexport function computeHash(value: unknown): string {\n const canonical = toCanonicalJson(value);\n return createHash('sha256').update(canonical).digest('hex').slice(0, 16);\n}\n\n/**\n * Sort specs by key and version for deterministic ordering.\n */\nexport function sortSpecs<T extends { key: string; version: number }>(\n specs: T[]\n): T[] {\n return [...specs].sort((a, b) => {\n const keyCompare = a.key.localeCompare(b.key);\n if (keyCompare !== 0) return keyCompare;\n return a.version - b.version;\n });\n}\n\n/**\n * Sort field snapshots by name for deterministic ordering.\n */\nexport function sortFields(\n fields: Record<string, unknown>\n): Record<string, unknown> {\n const sorted: Record<string, unknown> = {};\n const keys = Object.keys(fields).sort();\n for (const key of keys) {\n sorted[key] = fields[key];\n }\n return sorted;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,eAAe,OAAyB;AACtD,KAAI,UAAU,QAAQ,UAAU,OAC9B,QAAO,UAAU,OAAO,OAAO;AAGjC,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,IAAI,eAAe;AAGlC,KAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,MAAM;EACZ,MAAM,aAAa,OAAO,KAAK,IAAI,CAAC,MAAM;EAC1C,MAAMA,aAAsC,EAAE;AAE9C,OAAK,MAAM,OAAO,YAAY;GAC5B,MAAM,kBAAkB,eAAe,IAAI,KAAK;AAEhD,OAAI,oBAAoB,OACtB,YAAW,OAAO;;AAItB,SAAO;;AAGT,QAAO;;;;;AAMT,SAAgB,gBAAgB,OAAwB;AACtD,QAAO,KAAK,UAAU,eAAe,MAAM,EAAE,MAAM,EAAE;;;;;AAMvD,SAAgB,YAAY,OAAwB;CAClD,MAAM,YAAY,gBAAgB,MAAM;AACxC,QAAO,WAAW,SAAS,CAAC,OAAO,UAAU,CAAC,OAAO,MAAM,CAAC,MAAM,GAAG,GAAG;;;;;AAM1E,SAAgB,UACd,OACK;AACL,QAAO,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM;EAC/B,MAAM,aAAa,EAAE,IAAI,cAAc,EAAE,IAAI;AAC7C,MAAI,eAAe,EAAG,QAAO;AAC7B,SAAO,EAAE,UAAU,EAAE;GACrB;;;;;AAMJ,SAAgB,WACd,QACyB;CACzB,MAAMC,SAAkC,EAAE;CAC1C,MAAM,OAAO,OAAO,KAAK,OAAO,CAAC,MAAM;AACvC,MAAK,MAAM,OAAO,KAChB,QAAO,OAAO,OAAO;AAEvB,QAAO"}
|