@contractspec/module.workspace 0.0.0-canary-20260113162409
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/prompts/code-generation.d.ts +24 -0
- package/dist/ai/prompts/code-generation.d.ts.map +1 -0
- package/dist/ai/prompts/code-generation.js +134 -0
- package/dist/ai/prompts/code-generation.js.map +1 -0
- package/dist/ai/prompts/spec-creation.d.ts +28 -0
- package/dist/ai/prompts/spec-creation.d.ts.map +1 -0
- package/dist/ai/prompts/spec-creation.js +102 -0
- package/dist/ai/prompts/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/example-scan.d.ts +15 -0
- package/dist/analysis/example-scan.d.ts.map +1 -0
- package/dist/analysis/example-scan.js +116 -0
- package/dist/analysis/example-scan.js.map +1 -0
- package/dist/analysis/feature-extractor.js +203 -0
- package/dist/analysis/feature-extractor.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 +56 -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 +18 -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 +67 -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 +74 -0
- package/dist/analysis/snapshot/types.d.ts.map +1 -0
- package/dist/analysis/spec-parser.d.ts +11 -0
- package/dist/analysis/spec-parser.d.ts.map +1 -0
- package/dist/analysis/spec-parser.js +89 -0
- package/dist/analysis/spec-parser.js.map +1 -0
- package/dist/analysis/spec-parsing-utils.d.ts +26 -0
- package/dist/analysis/spec-parsing-utils.d.ts.map +1 -0
- package/dist/analysis/spec-parsing-utils.js +98 -0
- package/dist/analysis/spec-parsing-utils.js.map +1 -0
- package/dist/analysis/spec-scan.d.ts +20 -0
- package/dist/analysis/spec-scan.d.ts.map +1 -0
- package/dist/analysis/spec-scan.js +141 -0
- package/dist/analysis/spec-scan.js.map +1 -0
- package/dist/analysis/utils/matchers.js +77 -0
- package/dist/analysis/utils/matchers.js.map +1 -0
- package/dist/analysis/utils/variables.js +45 -0
- package/dist/analysis/utils/variables.js.map +1 -0
- package/dist/analysis/validate/index.js +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 +455 -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/formatters/index.js +2 -0
- package/dist/formatters/spec-markdown.d.ts +31 -0
- package/dist/formatters/spec-markdown.d.ts.map +1 -0
- package/dist/formatters/spec-markdown.js +263 -0
- package/dist/formatters/spec-markdown.js.map +1 -0
- package/dist/formatters/spec-to-docblock.d.ts +14 -0
- package/dist/formatters/spec-to-docblock.d.ts.map +1 -0
- package/dist/formatters/spec-to-docblock.js +48 -0
- package/dist/formatters/spec-to-docblock.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +39 -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 +107 -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 +62 -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 +199 -0
- package/dist/types/analysis-types.d.ts.map +1 -0
- package/dist/types/generation-types.d.ts +87 -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/llm-types.d.ts +138 -0
- package/dist/types/llm-types.d.ts.map +1 -0
- package/dist/types/rulesync-types.d.ts +24 -0
- package/dist/types/rulesync-types.d.ts.map +1 -0
- package/dist/types/spec-types.d.ts +343 -0
- package/dist/types/spec-types.d.ts.map +1 -0
- package/package.json +63 -0
|
@@ -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":[],"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: string;
|
|
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: string;
|
|
56
|
+
type: 'operation' | 'event';
|
|
57
|
+
}[];
|
|
58
|
+
/** Specs that were removed */
|
|
59
|
+
removedSpecs: {
|
|
60
|
+
key: string;
|
|
61
|
+
version: string;
|
|
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,18 @@
|
|
|
1
|
+
import { extractTestCoverage, extractTestTarget } from "./spec-parsing-utils.js";
|
|
2
|
+
import { inferSpecTypeFromFilePath, scanAllSpecsFromSource, scanSpecSource } from "./spec-scan.js";
|
|
3
|
+
import { isFeatureFile, scanFeatureSource } from "./feature-scan.js";
|
|
4
|
+
import { isExampleFile, scanExampleSource } from "./example-scan.js";
|
|
5
|
+
import { SpecGroupingStrategies, filterFeatures, filterSpecs, getUniqueSpecDomains, getUniqueSpecOwners, getUniqueSpecTags, groupSpecs, groupSpecsToArray } from "./grouping.js";
|
|
6
|
+
import { computeSemanticDiff } from "./diff/semantic.js";
|
|
7
|
+
import { computeFieldDiff, computeFieldsDiff, computeIoDiff, isBreakingChange } from "./diff/deep-diff.js";
|
|
8
|
+
import { addContractNode, buildReverseEdges, createContractGraph, detectCycles, findMissingDependencies, toDot } from "./deps/graph.js";
|
|
9
|
+
import { parseImportedSpecNames } from "./deps/parse-imports.js";
|
|
10
|
+
import { validateSpecStructure } from "./validate/spec-structure.js";
|
|
11
|
+
import "./validate/index.js";
|
|
12
|
+
import { computeHash, normalizeValue, sortFields, sortSpecs, toCanonicalJson } from "./snapshot/normalizer.js";
|
|
13
|
+
import { generateSnapshot } from "./snapshot/snapshot.js";
|
|
14
|
+
import "./snapshot/index.js";
|
|
15
|
+
import { BREAKING_RULES, DEFAULT_RULES, INFO_RULES, NON_BREAKING_RULES, findMatchingRule, getRulesBySeverity } from "./impact/rules.js";
|
|
16
|
+
import { classifyImpact } from "./impact/classifier.js";
|
|
17
|
+
import "./impact/index.js";
|
|
18
|
+
import { loadSpecFromSource } from "./spec-parser.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: string;
|
|
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":";;AAgBA;AA+BA;AAOA;AAQA;AAaA;;;;;;;iBA3DgB,cAAA;;;;iBA+BA,eAAA;;;;iBAOA,WAAA;;;;iBAQA;;;UACP,MACN;;;;iBAWa,UAAA,SACN,0BACP"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { compareVersions } from "compare-versions";
|
|
3
|
+
|
|
4
|
+
//#region src/analysis/snapshot/normalizer.ts
|
|
5
|
+
/**
|
|
6
|
+
* JSON normalization utilities for deterministic snapshots.
|
|
7
|
+
*
|
|
8
|
+
* Ensures that snapshots are stable across ordering, whitespace,
|
|
9
|
+
* and other non-semantic differences.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a value for deterministic JSON serialization.
|
|
13
|
+
* - Sorts object keys alphabetically
|
|
14
|
+
* - Removes undefined values
|
|
15
|
+
* - Preserves null values
|
|
16
|
+
*/
|
|
17
|
+
function normalizeValue(value) {
|
|
18
|
+
if (value === null || value === void 0) return value === null ? null : void 0;
|
|
19
|
+
if (Array.isArray(value)) return value.map(normalizeValue);
|
|
20
|
+
if (typeof value === "object") {
|
|
21
|
+
const obj = value;
|
|
22
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
23
|
+
const normalized = {};
|
|
24
|
+
for (const key of sortedKeys) {
|
|
25
|
+
const normalizedValue = normalizeValue(obj[key]);
|
|
26
|
+
if (normalizedValue !== void 0) normalized[key] = normalizedValue;
|
|
27
|
+
}
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Serialize a value to deterministic JSON string.
|
|
34
|
+
*/
|
|
35
|
+
function toCanonicalJson(value) {
|
|
36
|
+
return JSON.stringify(normalizeValue(value), null, 0);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Compute a SHA-256 hash of canonical JSON representation.
|
|
40
|
+
*/
|
|
41
|
+
function computeHash(value) {
|
|
42
|
+
const canonical = toCanonicalJson(value);
|
|
43
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Sort specs by key and version for deterministic ordering.
|
|
47
|
+
*/
|
|
48
|
+
function sortSpecs(specs) {
|
|
49
|
+
return [...specs].sort((a, b) => {
|
|
50
|
+
const keyCompare = a.key.localeCompare(b.key);
|
|
51
|
+
if (keyCompare !== 0) return keyCompare;
|
|
52
|
+
return compareVersions(a.version, b.version);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Sort field snapshots by name for deterministic ordering.
|
|
57
|
+
*/
|
|
58
|
+
function sortFields(fields) {
|
|
59
|
+
const sorted = {};
|
|
60
|
+
const keys = Object.keys(fields).sort();
|
|
61
|
+
for (const key of keys) sorted[key] = fields[key];
|
|
62
|
+
return sorted;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
export { computeHash, normalizeValue, sortFields, sortSpecs, toCanonicalJson };
|
|
67
|
+
//# sourceMappingURL=normalizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizer.js","names":[],"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';\nimport { compareVersions } from 'compare-versions';\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: string }>(\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 compareVersions(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":";;;;;;;;;;;;;;;;AAgBA,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,MAAM,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,gBAAgB,EAAE,SAAS,EAAE,QAAQ;GAC5C;;;;;AAMJ,SAAgB,WACd,QACyB;CACzB,MAAM,SAAkC,EAAE;CAC1C,MAAM,OAAO,OAAO,KAAK,OAAO,CAAC,MAAM;AACvC,MAAK,MAAM,OAAO,KAChB,QAAO,OAAO,OAAO;AAEvB,QAAO"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ContractSnapshot, SnapshotOptions } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/analysis/snapshot/snapshot.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate a contract snapshot from spec source files.
|
|
7
|
+
*
|
|
8
|
+
* @param specs - Array of { path, content } for each spec file
|
|
9
|
+
* @param options - Snapshot generation options
|
|
10
|
+
* @returns Canonical contract snapshot
|
|
11
|
+
*/
|
|
12
|
+
declare function generateSnapshot(specs: {
|
|
13
|
+
path: string;
|
|
14
|
+
content: string;
|
|
15
|
+
}[], options?: SnapshotOptions): ContractSnapshot;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { generateSnapshot };
|
|
18
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.d.ts","names":[],"sources":["../../../src/analysis/snapshot/snapshot.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;iBA2BgB,gBAAA;;;eAEL,kBACR"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { scanSpecSource } from "../spec-scan.js";
|
|
2
|
+
import { computeHash, sortFields, sortSpecs } from "./normalizer.js";
|
|
3
|
+
|
|
4
|
+
//#region src/analysis/snapshot/snapshot.ts
|
|
5
|
+
/**
|
|
6
|
+
* Contract snapshot generation.
|
|
7
|
+
*
|
|
8
|
+
* Generates canonical, deterministic snapshots from spec source files
|
|
9
|
+
* for comparison and impact detection.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Generate a contract snapshot from spec source files.
|
|
13
|
+
*
|
|
14
|
+
* @param specs - Array of { path, content } for each spec file
|
|
15
|
+
* @param options - Snapshot generation options
|
|
16
|
+
* @returns Canonical contract snapshot
|
|
17
|
+
*/
|
|
18
|
+
function generateSnapshot(specs, options = {}) {
|
|
19
|
+
const snapshots = [];
|
|
20
|
+
for (const { path, content } of specs) {
|
|
21
|
+
const scanned = scanSpecSource(content, path);
|
|
22
|
+
if (options.types && !options.types.includes(scanned.specType)) continue;
|
|
23
|
+
if (scanned.specType === "operation" && scanned.key && scanned.version !== void 0) {
|
|
24
|
+
const opSnapshot = createOperationSnapshot(scanned, content);
|
|
25
|
+
if (opSnapshot) snapshots.push(opSnapshot);
|
|
26
|
+
} else if (scanned.specType === "event" && scanned.key && scanned.version !== void 0) {
|
|
27
|
+
const eventSnapshot = createEventSnapshot(scanned, content);
|
|
28
|
+
if (eventSnapshot) snapshots.push(eventSnapshot);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const sortedSpecs = sortSpecs(snapshots);
|
|
32
|
+
const hash = computeHash({ specs: sortedSpecs });
|
|
33
|
+
return {
|
|
34
|
+
version: "1.0.0",
|
|
35
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
36
|
+
specs: sortedSpecs,
|
|
37
|
+
hash
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create an operation snapshot from scanned spec data.
|
|
42
|
+
*/
|
|
43
|
+
function createOperationSnapshot(scanned, content) {
|
|
44
|
+
if (!scanned.key || scanned.version === void 0) return null;
|
|
45
|
+
const io = extractIoFromSource(content);
|
|
46
|
+
const http = extractHttpBinding(content);
|
|
47
|
+
return {
|
|
48
|
+
type: "operation",
|
|
49
|
+
key: scanned.key,
|
|
50
|
+
version: scanned.version,
|
|
51
|
+
kind: scanned.kind === "command" || scanned.kind === "query" ? scanned.kind : "command",
|
|
52
|
+
stability: scanned.stability ?? "experimental",
|
|
53
|
+
http: http ?? void 0,
|
|
54
|
+
io,
|
|
55
|
+
authLevel: extractAuthLevel(content),
|
|
56
|
+
emittedEvents: scanned.emittedEvents
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create an event snapshot from scanned spec data.
|
|
61
|
+
*/
|
|
62
|
+
function createEventSnapshot(scanned, content) {
|
|
63
|
+
if (!scanned.key || scanned.version === void 0) return null;
|
|
64
|
+
const payload = extractPayloadFromSource(content);
|
|
65
|
+
return {
|
|
66
|
+
type: "event",
|
|
67
|
+
key: scanned.key,
|
|
68
|
+
version: scanned.version,
|
|
69
|
+
stability: scanned.stability ?? "experimental",
|
|
70
|
+
payload
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Extract IO schema from source code.
|
|
75
|
+
* This is a heuristic extraction - not full Zod introspection.
|
|
76
|
+
*/
|
|
77
|
+
function extractIoFromSource(content) {
|
|
78
|
+
const input = extractSchemaFields(content, "input");
|
|
79
|
+
const output = extractSchemaFields(content, "output");
|
|
80
|
+
return {
|
|
81
|
+
input: sortFields(input),
|
|
82
|
+
output: sortFields(output)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extract payload schema from event source code.
|
|
87
|
+
*/
|
|
88
|
+
function extractPayloadFromSource(content) {
|
|
89
|
+
return sortFields(extractSchemaFields(content, "payload"));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Extract schema fields from a specific section of the source.
|
|
93
|
+
*/
|
|
94
|
+
function extractSchemaFields(content, section) {
|
|
95
|
+
const fields = {};
|
|
96
|
+
const sectionPattern = new RegExp(`${section}\\s*:\\s*z\\.object\\(\\{([^}]+)\\}`, "s");
|
|
97
|
+
const sectionMatch = content.match(sectionPattern);
|
|
98
|
+
if (!sectionMatch?.[1]) return fields;
|
|
99
|
+
const sectionContent = sectionMatch[1];
|
|
100
|
+
const fieldPattern = /(\w+)\s*:\s*z\.(\w+)\((.*?)\)/g;
|
|
101
|
+
let match;
|
|
102
|
+
while ((match = fieldPattern.exec(sectionContent)) !== null) {
|
|
103
|
+
const [, fieldName, zodType] = match;
|
|
104
|
+
if (!fieldName || !zodType) continue;
|
|
105
|
+
const isOptional = sectionContent.includes(`${fieldName}:`) && sectionContent.slice(sectionContent.indexOf(`${fieldName}:`)).includes(".optional()");
|
|
106
|
+
const isNullable = sectionContent.includes(`${fieldName}:`) && sectionContent.slice(sectionContent.indexOf(`${fieldName}:`)).includes(".nullable()");
|
|
107
|
+
fields[fieldName] = {
|
|
108
|
+
name: fieldName,
|
|
109
|
+
type: mapZodTypeToFieldType(zodType),
|
|
110
|
+
required: !isOptional,
|
|
111
|
+
nullable: isNullable
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return fields;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Map Zod type to FieldType.
|
|
118
|
+
*/
|
|
119
|
+
function mapZodTypeToFieldType(zodType) {
|
|
120
|
+
return {
|
|
121
|
+
string: "string",
|
|
122
|
+
number: "number",
|
|
123
|
+
boolean: "boolean",
|
|
124
|
+
object: "object",
|
|
125
|
+
array: "array",
|
|
126
|
+
enum: "enum",
|
|
127
|
+
union: "union",
|
|
128
|
+
literal: "literal",
|
|
129
|
+
date: "date",
|
|
130
|
+
coerce: "unknown"
|
|
131
|
+
}[zodType] ?? "unknown";
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Extract HTTP binding from source code.
|
|
135
|
+
*/
|
|
136
|
+
function extractHttpBinding(content) {
|
|
137
|
+
const methodMatch = content.match(/method\s*:\s*['"](\w+)['"]/);
|
|
138
|
+
const pathMatch = content.match(/path\s*:\s*['"]([^'"]+)['"]/);
|
|
139
|
+
if (methodMatch?.[1] && pathMatch?.[1]) {
|
|
140
|
+
const method = methodMatch[1].toUpperCase();
|
|
141
|
+
if ([
|
|
142
|
+
"GET",
|
|
143
|
+
"POST",
|
|
144
|
+
"PUT",
|
|
145
|
+
"PATCH",
|
|
146
|
+
"DELETE"
|
|
147
|
+
].includes(method)) return {
|
|
148
|
+
method,
|
|
149
|
+
path: pathMatch[1]
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Extract auth level from source code.
|
|
156
|
+
*/
|
|
157
|
+
function extractAuthLevel(content) {
|
|
158
|
+
return content.match(/auth\s*:\s*['"](\w+)['"]/)?.[1];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
export { generateSnapshot };
|
|
163
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","names":[],"sources":["../../../src/analysis/snapshot/snapshot.ts"],"sourcesContent":["/**\n * Contract snapshot generation.\n *\n * Generates canonical, deterministic snapshots from spec source files\n * for comparison and impact detection.\n */\n\nimport { scanSpecSource } from '../spec-scan';\nimport { computeHash, sortSpecs, sortFields } from './normalizer';\nimport type {\n ContractSnapshot,\n EventSnapshot,\n FieldSnapshot,\n FieldType,\n IoSnapshot,\n OperationSnapshot,\n SnapshotOptions,\n SpecSnapshot,\n} from './types';\n\n/**\n * Generate a contract snapshot from spec source files.\n *\n * @param specs - Array of { path, content } for each spec file\n * @param options - Snapshot generation options\n * @returns Canonical contract snapshot\n */\nexport function generateSnapshot(\n specs: { path: string; content: string }[],\n options: SnapshotOptions = {}\n): ContractSnapshot {\n const snapshots: SpecSnapshot[] = [];\n\n for (const { path, content } of specs) {\n const scanned = scanSpecSource(content, path);\n\n // Filter by types if specified\n if (\n options.types &&\n !options.types.includes(scanned.specType as 'operation' | 'event')\n ) {\n continue;\n }\n\n if (\n scanned.specType === 'operation' &&\n scanned.key &&\n scanned.version !== undefined\n ) {\n const opSnapshot = createOperationSnapshot(scanned, content);\n if (opSnapshot) {\n snapshots.push(opSnapshot);\n }\n } else if (\n scanned.specType === 'event' &&\n scanned.key &&\n scanned.version !== undefined\n ) {\n const eventSnapshot = createEventSnapshot(scanned, content);\n if (eventSnapshot) {\n snapshots.push(eventSnapshot);\n }\n }\n }\n\n const sortedSpecs = sortSpecs(snapshots);\n const hash = computeHash({ specs: sortedSpecs });\n\n return {\n version: '1.0.0',\n generatedAt: new Date().toISOString(),\n specs: sortedSpecs,\n hash,\n };\n}\n\n/**\n * Create an operation snapshot from scanned spec data.\n */\nfunction createOperationSnapshot(\n scanned: ReturnType<typeof scanSpecSource>,\n content: string\n): OperationSnapshot | null {\n if (!scanned.key || scanned.version === undefined) {\n return null;\n }\n\n const io = extractIoFromSource(content);\n const http = extractHttpBinding(content);\n\n return {\n type: 'operation',\n key: scanned.key,\n version: scanned.version,\n kind:\n scanned.kind === 'command' || scanned.kind === 'query'\n ? scanned.kind\n : 'command',\n stability: scanned.stability ?? 'experimental',\n http: http ?? undefined,\n io,\n authLevel: extractAuthLevel(content),\n emittedEvents: scanned.emittedEvents,\n };\n}\n\n/**\n * Create an event snapshot from scanned spec data.\n */\nfunction createEventSnapshot(\n scanned: ReturnType<typeof scanSpecSource>,\n content: string\n): EventSnapshot | null {\n if (!scanned.key || scanned.version === undefined) {\n return null;\n }\n\n const payload = extractPayloadFromSource(content);\n\n return {\n type: 'event',\n key: scanned.key,\n version: scanned.version,\n stability: scanned.stability ?? 'experimental',\n payload,\n };\n}\n\n/**\n * Extract IO schema from source code.\n * This is a heuristic extraction - not full Zod introspection.\n */\nfunction extractIoFromSource(content: string): IoSnapshot {\n const input = extractSchemaFields(content, 'input');\n const output = extractSchemaFields(content, 'output');\n\n return {\n input: sortFields(input) as Record<string, FieldSnapshot>,\n output: sortFields(output) as Record<string, FieldSnapshot>,\n };\n}\n\n/**\n * Extract payload schema from event source code.\n */\nfunction extractPayloadFromSource(\n content: string\n): Record<string, FieldSnapshot> {\n const fields = extractSchemaFields(content, 'payload');\n return sortFields(fields) as Record<string, FieldSnapshot>;\n}\n\n/**\n * Extract schema fields from a specific section of the source.\n */\nfunction extractSchemaFields(\n content: string,\n section: 'input' | 'output' | 'payload'\n): Record<string, FieldSnapshot> {\n const fields: Record<string, FieldSnapshot> = {};\n\n // Look for z.object({ ... }) patterns within the section\n const sectionPattern = new RegExp(\n `${section}\\\\s*:\\\\s*z\\\\.object\\\\(\\\\{([^}]+)\\\\}`,\n 's'\n );\n const sectionMatch = content.match(sectionPattern);\n\n if (!sectionMatch?.[1]) {\n return fields;\n }\n\n const sectionContent = sectionMatch[1];\n\n // Match field definitions: fieldName: z.string(), z.number(), etc.\n const fieldPattern = /(\\w+)\\s*:\\s*z\\.(\\w+)\\((.*?)\\)/g;\n let match;\n\n while ((match = fieldPattern.exec(sectionContent)) !== null) {\n const [, fieldName, zodType] = match;\n if (!fieldName || !zodType) continue;\n\n const isOptional =\n sectionContent.includes(`${fieldName}:`) &&\n sectionContent\n .slice(sectionContent.indexOf(`${fieldName}:`))\n .includes('.optional()');\n const isNullable =\n sectionContent.includes(`${fieldName}:`) &&\n sectionContent\n .slice(sectionContent.indexOf(`${fieldName}:`))\n .includes('.nullable()');\n\n fields[fieldName] = {\n name: fieldName,\n type: mapZodTypeToFieldType(zodType),\n required: !isOptional,\n nullable: isNullable,\n };\n }\n\n return fields;\n}\n\n/**\n * Map Zod type to FieldType.\n */\nfunction mapZodTypeToFieldType(zodType: string): FieldType {\n const mapping: Record<string, FieldType> = {\n string: 'string',\n number: 'number',\n boolean: 'boolean',\n object: 'object',\n array: 'array',\n enum: 'enum',\n union: 'union',\n literal: 'literal',\n date: 'date',\n coerce: 'unknown',\n };\n return mapping[zodType] ?? 'unknown';\n}\n\n/**\n * Extract HTTP binding from source code.\n */\nfunction extractHttpBinding(content: string): {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n path: string;\n} | null {\n // Look for http: { method: 'X', path: 'Y' } pattern\n const methodMatch = content.match(/method\\s*:\\s*['\"](\\w+)['\"]/);\n const pathMatch = content.match(/path\\s*:\\s*['\"]([^'\"]+)['\"]/);\n\n if (methodMatch?.[1] && pathMatch?.[1]) {\n const method = methodMatch[1].toUpperCase();\n if (['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {\n return {\n method: method as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n path: pathMatch[1],\n };\n }\n }\n\n return null;\n}\n\n/**\n * Extract auth level from source code.\n */\nfunction extractAuthLevel(content: string): string | undefined {\n const authMatch = content.match(/auth\\s*:\\s*['\"](\\w+)['\"]/);\n return authMatch?.[1];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA2BA,SAAgB,iBACd,OACA,UAA2B,EAAE,EACX;CAClB,MAAM,YAA4B,EAAE;AAEpC,MAAK,MAAM,EAAE,MAAM,aAAa,OAAO;EACrC,MAAM,UAAU,eAAe,SAAS,KAAK;AAG7C,MACE,QAAQ,SACR,CAAC,QAAQ,MAAM,SAAS,QAAQ,SAAkC,CAElE;AAGF,MACE,QAAQ,aAAa,eACrB,QAAQ,OACR,QAAQ,YAAY,QACpB;GACA,MAAM,aAAa,wBAAwB,SAAS,QAAQ;AAC5D,OAAI,WACF,WAAU,KAAK,WAAW;aAG5B,QAAQ,aAAa,WACrB,QAAQ,OACR,QAAQ,YAAY,QACpB;GACA,MAAM,gBAAgB,oBAAoB,SAAS,QAAQ;AAC3D,OAAI,cACF,WAAU,KAAK,cAAc;;;CAKnC,MAAM,cAAc,UAAU,UAAU;CACxC,MAAM,OAAO,YAAY,EAAE,OAAO,aAAa,CAAC;AAEhD,QAAO;EACL,SAAS;EACT,8BAAa,IAAI,MAAM,EAAC,aAAa;EACrC,OAAO;EACP;EACD;;;;;AAMH,SAAS,wBACP,SACA,SAC0B;AAC1B,KAAI,CAAC,QAAQ,OAAO,QAAQ,YAAY,OACtC,QAAO;CAGT,MAAM,KAAK,oBAAoB,QAAQ;CACvC,MAAM,OAAO,mBAAmB,QAAQ;AAExC,QAAO;EACL,MAAM;EACN,KAAK,QAAQ;EACb,SAAS,QAAQ;EACjB,MACE,QAAQ,SAAS,aAAa,QAAQ,SAAS,UAC3C,QAAQ,OACR;EACN,WAAW,QAAQ,aAAa;EAChC,MAAM,QAAQ;EACd;EACA,WAAW,iBAAiB,QAAQ;EACpC,eAAe,QAAQ;EACxB;;;;;AAMH,SAAS,oBACP,SACA,SACsB;AACtB,KAAI,CAAC,QAAQ,OAAO,QAAQ,YAAY,OACtC,QAAO;CAGT,MAAM,UAAU,yBAAyB,QAAQ;AAEjD,QAAO;EACL,MAAM;EACN,KAAK,QAAQ;EACb,SAAS,QAAQ;EACjB,WAAW,QAAQ,aAAa;EAChC;EACD;;;;;;AAOH,SAAS,oBAAoB,SAA6B;CACxD,MAAM,QAAQ,oBAAoB,SAAS,QAAQ;CACnD,MAAM,SAAS,oBAAoB,SAAS,SAAS;AAErD,QAAO;EACL,OAAO,WAAW,MAAM;EACxB,QAAQ,WAAW,OAAO;EAC3B;;;;;AAMH,SAAS,yBACP,SAC+B;AAE/B,QAAO,WADQ,oBAAoB,SAAS,UAAU,CAC7B;;;;;AAM3B,SAAS,oBACP,SACA,SAC+B;CAC/B,MAAM,SAAwC,EAAE;CAGhD,MAAM,iBAAiB,IAAI,OACzB,GAAG,QAAQ,sCACX,IACD;CACD,MAAM,eAAe,QAAQ,MAAM,eAAe;AAElD,KAAI,CAAC,eAAe,GAClB,QAAO;CAGT,MAAM,iBAAiB,aAAa;CAGpC,MAAM,eAAe;CACrB,IAAI;AAEJ,SAAQ,QAAQ,aAAa,KAAK,eAAe,MAAM,MAAM;EAC3D,MAAM,GAAG,WAAW,WAAW;AAC/B,MAAI,CAAC,aAAa,CAAC,QAAS;EAE5B,MAAM,aACJ,eAAe,SAAS,GAAG,UAAU,GAAG,IACxC,eACG,MAAM,eAAe,QAAQ,GAAG,UAAU,GAAG,CAAC,CAC9C,SAAS,cAAc;EAC5B,MAAM,aACJ,eAAe,SAAS,GAAG,UAAU,GAAG,IACxC,eACG,MAAM,eAAe,QAAQ,GAAG,UAAU,GAAG,CAAC,CAC9C,SAAS,cAAc;AAE5B,SAAO,aAAa;GAClB,MAAM;GACN,MAAM,sBAAsB,QAAQ;GACpC,UAAU,CAAC;GACX,UAAU;GACX;;AAGH,QAAO;;;;;AAMT,SAAS,sBAAsB,SAA4B;AAazD,QAZ2C;EACzC,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,QAAQ;EACR,OAAO;EACP,MAAM;EACN,OAAO;EACP,SAAS;EACT,MAAM;EACN,QAAQ;EACT,CACc,YAAY;;;;;AAM7B,SAAS,mBAAmB,SAGnB;CAEP,MAAM,cAAc,QAAQ,MAAM,6BAA6B;CAC/D,MAAM,YAAY,QAAQ,MAAM,8BAA8B;AAE9D,KAAI,cAAc,MAAM,YAAY,IAAI;EACtC,MAAM,SAAS,YAAY,GAAG,aAAa;AAC3C,MAAI;GAAC;GAAO;GAAQ;GAAO;GAAS;GAAS,CAAC,SAAS,OAAO,CAC5D,QAAO;GACG;GACR,MAAM,UAAU;GACjB;;AAIL,QAAO;;;;;AAMT,SAAS,iBAAiB,SAAqC;AAE7D,QADkB,QAAQ,MAAM,2BAA2B,GACxC"}
|