@contractspec/module.workspace 1.57.0 → 1.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/code-generation.d.ts +25 -0
- package/dist/ai/code-generation.d.ts.map +1 -0
- package/dist/ai/index.d.ts +5 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/prompts/code-generation.d.ts +5 -8
- package/dist/ai/prompts/code-generation.d.ts.map +1 -1
- package/dist/ai/prompts/index.d.ts +3 -0
- package/dist/ai/prompts/index.d.ts.map +1 -0
- package/dist/ai/prompts/spec-creation.d.ts +7 -11
- package/dist/ai/prompts/spec-creation.d.ts.map +1 -1
- package/dist/ai/spec-creation.d.ts +27 -0
- package/dist/ai/spec-creation.d.ts.map +1 -0
- package/dist/analysis/deps/graph.d.ts +14 -13
- package/dist/analysis/deps/graph.d.ts.map +1 -1
- package/dist/analysis/deps/index.d.ts +6 -0
- package/dist/analysis/deps/index.d.ts.map +1 -0
- package/dist/analysis/deps/parse-imports.d.ts +1 -4
- package/dist/analysis/deps/parse-imports.d.ts.map +1 -1
- package/dist/analysis/diff/deep-diff.d.ts +17 -15
- package/dist/analysis/diff/deep-diff.d.ts.map +1 -1
- package/dist/analysis/diff/deep-diff.test.d.ts +5 -0
- package/dist/analysis/diff/deep-diff.test.d.ts.map +1 -0
- package/dist/analysis/diff/index.d.ts +6 -0
- package/dist/analysis/diff/index.d.ts.map +1 -0
- package/dist/analysis/diff/semantic.d.ts +6 -6
- package/dist/analysis/diff/semantic.d.ts.map +1 -1
- package/dist/analysis/example-scan.d.ts +3 -7
- package/dist/analysis/example-scan.d.ts.map +1 -1
- package/dist/analysis/example-scan.test.d.ts +7 -0
- package/dist/analysis/example-scan.test.d.ts.map +1 -0
- package/dist/analysis/feature-extractor.d.ts +25 -0
- package/dist/analysis/feature-extractor.d.ts.map +1 -0
- package/dist/analysis/feature-scan.d.ts +3 -7
- package/dist/analysis/feature-scan.d.ts.map +1 -1
- package/dist/analysis/feature-scan.test.d.ts +2 -0
- package/dist/analysis/feature-scan.test.d.ts.map +1 -0
- package/dist/analysis/grouping.d.ts +41 -35
- package/dist/analysis/grouping.d.ts.map +1 -1
- package/dist/analysis/impact/classifier.d.ts +9 -8
- package/dist/analysis/impact/classifier.d.ts.map +1 -1
- package/dist/analysis/impact/classifier.test.d.ts +5 -0
- package/dist/analysis/impact/classifier.test.d.ts.map +1 -0
- package/dist/analysis/impact/index.d.ts +9 -0
- package/dist/analysis/impact/index.d.ts.map +1 -0
- package/dist/analysis/impact/rules.d.ts +15 -14
- package/dist/analysis/impact/rules.d.ts.map +1 -1
- package/dist/analysis/impact/types.d.ts +73 -76
- package/dist/analysis/impact/types.d.ts.map +1 -1
- package/dist/analysis/index.d.ts +14 -0
- package/dist/analysis/index.d.ts.map +1 -0
- package/dist/analysis/snapshot/index.d.ts +9 -0
- package/dist/analysis/snapshot/index.d.ts.map +1 -0
- package/dist/analysis/snapshot/normalizer.d.ts +7 -10
- package/dist/analysis/snapshot/normalizer.d.ts.map +1 -1
- package/dist/analysis/snapshot/snapshot.d.ts +10 -8
- package/dist/analysis/snapshot/snapshot.d.ts.map +1 -1
- package/dist/analysis/snapshot/snapshot.test.d.ts +5 -0
- package/dist/analysis/snapshot/snapshot.test.d.ts.map +1 -0
- package/dist/analysis/snapshot/types.d.ts +58 -56
- package/dist/analysis/snapshot/types.d.ts.map +1 -1
- package/dist/analysis/spec-parser.d.ts +8 -6
- package/dist/analysis/spec-parser.d.ts.map +1 -1
- package/dist/analysis/spec-parsing-utils.d.ts +20 -10
- package/dist/analysis/spec-parsing-utils.d.ts.map +1 -1
- package/dist/analysis/spec-scan.d.ts +13 -12
- package/dist/analysis/spec-scan.d.ts.map +1 -1
- package/dist/analysis/spec-scan.test.d.ts +2 -0
- package/dist/analysis/spec-scan.test.d.ts.map +1 -0
- package/dist/analysis/utils/matchers.d.ts +39 -0
- package/dist/analysis/utils/matchers.d.ts.map +1 -0
- package/dist/analysis/utils/variables.d.ts +15 -0
- package/dist/analysis/utils/variables.d.ts.map +1 -0
- package/dist/analysis/validate/index.d.ts +5 -0
- package/dist/analysis/validate/index.d.ts.map +1 -0
- package/dist/analysis/validate/spec-structure.d.ts +15 -14
- package/dist/analysis/validate/spec-structure.d.ts.map +1 -1
- package/dist/analysis/validate/spec-structure.test.d.ts +2 -0
- package/dist/analysis/validate/spec-structure.test.d.ts.map +1 -0
- package/dist/formatter.d.ts +28 -27
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatters/index.d.ts +8 -0
- package/dist/formatters/index.d.ts.map +1 -0
- package/dist/formatters/spec-markdown.d.ts +13 -11
- package/dist/formatters/spec-markdown.d.ts.map +1 -1
- package/dist/formatters/spec-markdown.test.d.ts +5 -0
- package/dist/formatters/spec-markdown.test.d.ts.map +1 -0
- package/dist/formatters/spec-to-docblock.d.ts +4 -8
- package/dist/formatters/spec-to-docblock.d.ts.map +1 -1
- package/dist/index.d.ts +13 -42
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4302 -38
- package/dist/node/index.js +4301 -0
- package/dist/templates/app-config.d.ts +2 -6
- package/dist/templates/app-config.d.ts.map +1 -1
- package/dist/templates/app-config.test.d.ts +2 -0
- package/dist/templates/app-config.test.d.ts.map +1 -0
- package/dist/templates/data-view.d.ts +2 -6
- package/dist/templates/data-view.d.ts.map +1 -1
- package/dist/templates/data-view.test.d.ts +2 -0
- package/dist/templates/data-view.test.d.ts.map +1 -0
- package/dist/templates/event.d.ts +6 -6
- package/dist/templates/event.d.ts.map +1 -1
- package/dist/templates/event.test.d.ts +2 -0
- package/dist/templates/event.test.d.ts.map +1 -0
- package/dist/templates/experiment.d.ts +2 -6
- package/dist/templates/experiment.d.ts.map +1 -1
- package/dist/templates/experiment.test.d.ts +2 -0
- package/dist/templates/experiment.test.d.ts.map +1 -0
- package/dist/templates/handler.d.ts +3 -6
- package/dist/templates/handler.d.ts.map +1 -1
- package/dist/templates/handler.test.d.ts +2 -0
- package/dist/templates/handler.test.d.ts.map +1 -0
- package/dist/templates/index.d.ts +18 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/integration-utils.d.ts +13 -0
- package/dist/templates/integration-utils.d.ts.map +1 -0
- package/dist/templates/integration-utils.test.d.ts +2 -0
- package/dist/templates/integration-utils.test.d.ts.map +1 -0
- package/dist/templates/integration.d.ts +2 -6
- package/dist/templates/integration.d.ts.map +1 -1
- package/dist/templates/integration.test.d.ts +2 -0
- package/dist/templates/integration.test.d.ts.map +1 -0
- package/dist/templates/knowledge.d.ts +2 -6
- package/dist/templates/knowledge.d.ts.map +1 -1
- package/dist/templates/knowledge.test.d.ts +2 -0
- package/dist/templates/knowledge.test.d.ts.map +1 -0
- package/dist/templates/migration.d.ts +2 -6
- package/dist/templates/migration.d.ts.map +1 -1
- package/dist/templates/migration.test.d.ts +2 -0
- package/dist/templates/migration.test.d.ts.map +1 -0
- package/dist/templates/operation.d.ts +6 -6
- package/dist/templates/operation.d.ts.map +1 -1
- package/dist/templates/operation.test.d.ts +2 -0
- package/dist/templates/operation.test.d.ts.map +1 -0
- package/dist/templates/presentation.d.ts +6 -6
- package/dist/templates/presentation.d.ts.map +1 -1
- package/dist/templates/presentation.test.d.ts +2 -0
- package/dist/templates/presentation.test.d.ts.map +1 -0
- package/dist/templates/telemetry.d.ts +2 -6
- package/dist/templates/telemetry.d.ts.map +1 -1
- package/dist/templates/telemetry.test.d.ts +2 -0
- package/dist/templates/telemetry.test.d.ts.map +1 -0
- package/dist/templates/utils.d.ts +5 -8
- package/dist/templates/utils.d.ts.map +1 -1
- package/dist/templates/utils.test.d.ts +2 -0
- package/dist/templates/utils.test.d.ts.map +1 -0
- package/dist/templates/workflow-runner.d.ts +6 -13
- package/dist/templates/workflow-runner.d.ts.map +1 -1
- package/dist/templates/workflow-runner.test.d.ts +2 -0
- package/dist/templates/workflow-runner.test.d.ts.map +1 -0
- package/dist/templates/workflow.d.ts +6 -6
- package/dist/templates/workflow.d.ts.map +1 -1
- package/dist/templates/workflow.test.d.ts +2 -0
- package/dist/templates/workflow.test.d.ts.map +1 -0
- package/dist/types/analysis-types.d.ts +135 -136
- package/dist/types/analysis-types.d.ts.map +1 -1
- package/dist/types/generation-types.d.ts +36 -37
- package/dist/types/generation-types.d.ts.map +1 -1
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/llm-types.d.ts +97 -96
- package/dist/types/llm-types.d.ts.map +1 -1
- package/dist/types/rulesync-types.d.ts +17 -18
- package/dist/types/rulesync-types.d.ts.map +1 -1
- package/dist/types/spec-types.d.ts +329 -329
- package/dist/types/spec-types.d.ts.map +1 -1
- package/package.json +20 -16
- package/dist/ai/prompts/code-generation.js +0 -134
- package/dist/ai/prompts/code-generation.js.map +0 -1
- package/dist/ai/prompts/spec-creation.js +0 -102
- package/dist/ai/prompts/spec-creation.js.map +0 -1
- package/dist/analysis/deps/graph.js +0 -85
- package/dist/analysis/deps/graph.js.map +0 -1
- package/dist/analysis/deps/parse-imports.js +0 -31
- package/dist/analysis/deps/parse-imports.js.map +0 -1
- package/dist/analysis/diff/deep-diff.js +0 -114
- package/dist/analysis/diff/deep-diff.js.map +0 -1
- package/dist/analysis/diff/semantic.js +0 -97
- package/dist/analysis/diff/semantic.js.map +0 -1
- package/dist/analysis/example-scan.js +0 -116
- package/dist/analysis/example-scan.js.map +0 -1
- package/dist/analysis/feature-extractor.js +0 -203
- package/dist/analysis/feature-extractor.js.map +0 -1
- package/dist/analysis/feature-scan.js +0 -56
- package/dist/analysis/feature-scan.js.map +0 -1
- package/dist/analysis/grouping.js +0 -115
- package/dist/analysis/grouping.js.map +0 -1
- package/dist/analysis/impact/classifier.js +0 -135
- package/dist/analysis/impact/classifier.js.map +0 -1
- package/dist/analysis/impact/index.js +0 -2
- package/dist/analysis/impact/rules.js +0 -154
- package/dist/analysis/impact/rules.js.map +0 -1
- package/dist/analysis/index.js +0 -18
- package/dist/analysis/snapshot/index.js +0 -2
- package/dist/analysis/snapshot/normalizer.js +0 -67
- package/dist/analysis/snapshot/normalizer.js.map +0 -1
- package/dist/analysis/snapshot/snapshot.js +0 -163
- package/dist/analysis/snapshot/snapshot.js.map +0 -1
- package/dist/analysis/spec-parser.js +0 -89
- package/dist/analysis/spec-parser.js.map +0 -1
- package/dist/analysis/spec-parsing-utils.js +0 -98
- package/dist/analysis/spec-parsing-utils.js.map +0 -1
- package/dist/analysis/spec-scan.js +0 -157
- package/dist/analysis/spec-scan.js.map +0 -1
- package/dist/analysis/utils/matchers.js +0 -77
- package/dist/analysis/utils/matchers.js.map +0 -1
- package/dist/analysis/utils/variables.js +0 -45
- package/dist/analysis/utils/variables.js.map +0 -1
- package/dist/analysis/validate/index.js +0 -1
- package/dist/analysis/validate/spec-structure.js +0 -475
- package/dist/analysis/validate/spec-structure.js.map +0 -1
- package/dist/formatter.js +0 -163
- package/dist/formatter.js.map +0 -1
- package/dist/formatters/index.js +0 -2
- package/dist/formatters/spec-markdown.js +0 -263
- package/dist/formatters/spec-markdown.js.map +0 -1
- package/dist/formatters/spec-to-docblock.js +0 -48
- package/dist/formatters/spec-to-docblock.js.map +0 -1
- package/dist/templates/app-config.js +0 -107
- package/dist/templates/app-config.js.map +0 -1
- package/dist/templates/data-view.js +0 -69
- package/dist/templates/data-view.js.map +0 -1
- package/dist/templates/event.js +0 -41
- package/dist/templates/event.js.map +0 -1
- package/dist/templates/experiment.js +0 -88
- package/dist/templates/experiment.js.map +0 -1
- package/dist/templates/handler.js +0 -96
- package/dist/templates/handler.js.map +0 -1
- package/dist/templates/integration-utils.js +0 -102
- package/dist/templates/integration-utils.js.map +0 -1
- package/dist/templates/integration.js +0 -62
- package/dist/templates/integration.js.map +0 -1
- package/dist/templates/knowledge.js +0 -68
- package/dist/templates/knowledge.js.map +0 -1
- package/dist/templates/migration.js +0 -60
- package/dist/templates/migration.js.map +0 -1
- package/dist/templates/operation.js +0 -101
- package/dist/templates/operation.js.map +0 -1
- package/dist/templates/presentation.js +0 -79
- package/dist/templates/presentation.js.map +0 -1
- package/dist/templates/telemetry.js +0 -90
- package/dist/templates/telemetry.js.map +0 -1
- package/dist/templates/utils.js +0 -39
- package/dist/templates/utils.js.map +0 -1
- package/dist/templates/workflow-runner.js +0 -49
- package/dist/templates/workflow-runner.js.map +0 -1
- package/dist/templates/workflow.js +0 -68
- package/dist/templates/workflow.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,38 +1,4302 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
1
|
+
// @bun
|
|
2
|
+
// src/analysis/utils/matchers.ts
|
|
3
|
+
function escapeRegex(value) {
|
|
4
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
}
|
|
6
|
+
function matchStringField(code, field) {
|
|
7
|
+
const regex = new RegExp(`${escapeRegex(field)}\\s*:\\s*['"]([^'"]+)['"]`);
|
|
8
|
+
const match = code.match(regex);
|
|
9
|
+
return match?.[1] ?? null;
|
|
10
|
+
}
|
|
11
|
+
function matchStringFieldIn(code, field) {
|
|
12
|
+
return matchStringField(code, field);
|
|
13
|
+
}
|
|
14
|
+
function matchVersionField(code, field) {
|
|
15
|
+
const regex = new RegExp(`${escapeRegex(field)}\\s*:\\s*(?:['"]([^'"]+)['"]|(\\d+(?:\\.\\d+)*))`);
|
|
16
|
+
const match = code.match(regex);
|
|
17
|
+
if (match?.[1])
|
|
18
|
+
return match[1];
|
|
19
|
+
if (match?.[2])
|
|
20
|
+
return match[2];
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
function matchStringArrayField(code, field) {
|
|
24
|
+
const regex = new RegExp(`${escapeRegex(field)}\\s*:\\s*\\[([\\s\\S]*?)\\]`);
|
|
25
|
+
const match = code.match(regex);
|
|
26
|
+
if (!match?.[1])
|
|
27
|
+
return;
|
|
28
|
+
const inner = match[1];
|
|
29
|
+
const items = Array.from(inner.matchAll(/['"]([^'"]+)['"]/g)).map((m) => m[1]).filter((value) => typeof value === "string" && value.length > 0);
|
|
30
|
+
return items.length > 0 ? items : undefined;
|
|
31
|
+
}
|
|
32
|
+
function isStability(value) {
|
|
33
|
+
return value === "experimental" || value === "beta" || value === "stable" || value === "deprecated";
|
|
34
|
+
}
|
|
35
|
+
function findMatchingDelimiter(code, startIndex, openChar, closeChar) {
|
|
36
|
+
let depth = 0;
|
|
37
|
+
let inString = false;
|
|
38
|
+
let stringChar = "";
|
|
39
|
+
for (let i = startIndex;i < code.length; i++) {
|
|
40
|
+
const char = code[i];
|
|
41
|
+
const prevChar = i > 0 ? code[i - 1] : "";
|
|
42
|
+
if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
|
|
43
|
+
if (!inString) {
|
|
44
|
+
inString = true;
|
|
45
|
+
stringChar = char;
|
|
46
|
+
} else if (char === stringChar) {
|
|
47
|
+
inString = false;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (inString)
|
|
52
|
+
continue;
|
|
53
|
+
if (char === openChar) {
|
|
54
|
+
depth++;
|
|
55
|
+
} else if (char === closeChar) {
|
|
56
|
+
depth--;
|
|
57
|
+
if (depth === 0) {
|
|
58
|
+
return i;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/analysis/utils/variables.ts
|
|
66
|
+
function extractArrayConstants(code) {
|
|
67
|
+
const variables = new Map;
|
|
68
|
+
const regex = /const\s+(\w+)\s*=\s*\[/g;
|
|
69
|
+
let match;
|
|
70
|
+
while ((match = regex.exec(code)) !== null) {
|
|
71
|
+
const name = match[1];
|
|
72
|
+
const startIndex = match.index + match[0].length - 1;
|
|
73
|
+
const endIndex = findMatchingDelimiter(code, startIndex, "[", "]");
|
|
74
|
+
if (endIndex !== -1) {
|
|
75
|
+
const value = code.substring(startIndex, endIndex + 1);
|
|
76
|
+
if (name) {
|
|
77
|
+
variables.set(name, value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return variables;
|
|
82
|
+
}
|
|
83
|
+
function resolveVariablesInBlock(block, variables) {
|
|
84
|
+
if (variables.size === 0)
|
|
85
|
+
return block;
|
|
86
|
+
return block.replace(/\.\.\.(\w+)/g, (match, name) => {
|
|
87
|
+
const value = variables.get(name);
|
|
88
|
+
if (value) {
|
|
89
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
90
|
+
return value.substring(1, value.length - 1);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
return match;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/analysis/spec-parsing-utils.ts
|
|
99
|
+
function parsePolicy(code) {
|
|
100
|
+
const policyBlock = code.match(/policy\s*:\s*\{([\s\S]*?)\}/);
|
|
101
|
+
if (!policyBlock?.[1])
|
|
102
|
+
return [];
|
|
103
|
+
return extractRefList(policyBlock[1], "policies") ?? [];
|
|
104
|
+
}
|
|
105
|
+
function extractRefList(code, field) {
|
|
106
|
+
const regex = new RegExp(`${escapeRegex(field)}\\s*:\\s*\\[([\\s\\S]*?)\\]`);
|
|
107
|
+
const match = code.match(regex);
|
|
108
|
+
if (!match?.[1])
|
|
109
|
+
return;
|
|
110
|
+
const inner = match[1];
|
|
111
|
+
const items = [];
|
|
112
|
+
const parts = inner.match(/\{[\s\S]*?\}/g);
|
|
113
|
+
if (parts) {
|
|
114
|
+
for (const part of parts) {
|
|
115
|
+
const k = matchStringField(part, "key");
|
|
116
|
+
const v = matchVersionField(part, "version");
|
|
117
|
+
if (k) {
|
|
118
|
+
items.push({ key: k, version: v ?? "1.0.0" });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return items.length > 0 ? items : undefined;
|
|
123
|
+
}
|
|
124
|
+
function extractTestRefs(code) {
|
|
125
|
+
const regex = new RegExp(`testRefs\\s*:\\s*\\[([\\s\\S]*?)\\]`);
|
|
126
|
+
const match = code.match(regex);
|
|
127
|
+
if (!match?.[1])
|
|
128
|
+
return;
|
|
129
|
+
const inner = match[1];
|
|
130
|
+
const items = [];
|
|
131
|
+
const parts = inner.match(/\{[\s\S]*?\}/g);
|
|
132
|
+
if (parts) {
|
|
133
|
+
for (const part of parts) {
|
|
134
|
+
const k = matchStringField(part, "key");
|
|
135
|
+
const v = matchVersionField(part, "version");
|
|
136
|
+
const t = matchStringField(part, "type");
|
|
137
|
+
if (k) {
|
|
138
|
+
items.push({
|
|
139
|
+
key: k,
|
|
140
|
+
version: v ?? "1.0.0",
|
|
141
|
+
type: t === "error" ? "error" : "success"
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return items.length > 0 ? items : undefined;
|
|
147
|
+
}
|
|
148
|
+
function extractTestTarget(code) {
|
|
149
|
+
const targetStartMatch = code.match(/target\s*:\s*\{/);
|
|
150
|
+
if (!targetStartMatch || targetStartMatch.index === undefined)
|
|
151
|
+
return;
|
|
152
|
+
const openBraceIndex = targetStartMatch.index + targetStartMatch[0].length - 1;
|
|
153
|
+
const closeBraceIndex = findMatchingDelimiter(code, openBraceIndex, "{", "}");
|
|
154
|
+
if (closeBraceIndex === -1)
|
|
155
|
+
return;
|
|
156
|
+
const targetBlock = code.substring(openBraceIndex + 1, closeBraceIndex);
|
|
157
|
+
const typeMatch = targetBlock.match(/type\s*:\s*['"](\w+)['"]/);
|
|
158
|
+
if (!typeMatch?.[1])
|
|
159
|
+
return;
|
|
160
|
+
const type = typeMatch[1];
|
|
161
|
+
if (type !== "operation" && type !== "workflow")
|
|
162
|
+
return;
|
|
163
|
+
const flatKey = matchStringField(targetBlock, "key");
|
|
164
|
+
if (flatKey) {
|
|
165
|
+
const flatVersion = matchVersionField(targetBlock, "version");
|
|
166
|
+
return {
|
|
167
|
+
type,
|
|
168
|
+
key: flatKey,
|
|
169
|
+
version: flatVersion
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const refBlockMatch = targetBlock.match(new RegExp(`${type}\\s*:\\s*\\{([\\s\\S]*?)\\}`));
|
|
173
|
+
if (!refBlockMatch?.[1])
|
|
174
|
+
return;
|
|
175
|
+
const refBlock = refBlockMatch[1];
|
|
176
|
+
const key = matchStringField(refBlock, "key");
|
|
177
|
+
if (!key)
|
|
178
|
+
return;
|
|
179
|
+
const version = matchVersionField(refBlock, "version");
|
|
180
|
+
return {
|
|
181
|
+
type,
|
|
182
|
+
key,
|
|
183
|
+
version
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function extractTestCoverage(code) {
|
|
187
|
+
const hasSuccessNew = /expectOutput\s*:/.test(code);
|
|
188
|
+
const hasErrorNew = /expectError\s*:/.test(code);
|
|
189
|
+
const hasSuccessOld = /(['"]?)type\1\s*:\s*['"]expectOutput['"]/.test(code);
|
|
190
|
+
const hasErrorOld = /(['"]?)type\1\s*:\s*['"]expectError['"]/.test(code);
|
|
191
|
+
return {
|
|
192
|
+
hasSuccess: hasSuccessNew || hasSuccessOld,
|
|
193
|
+
hasError: hasErrorNew || hasErrorOld
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/analysis/spec-scan.ts
|
|
198
|
+
function scanAllSpecsFromSource(code, filePath) {
|
|
199
|
+
const results = [];
|
|
200
|
+
const variables = extractArrayConstants(code);
|
|
201
|
+
const definitionRegex = /export\s+const\s+(\w+)\s*=\s*define(Command|Query|Event|Presentation|Capability|Policy|Type|Example|AppConfig|Integration|Workflow|TestSpec|Feature)\s*\(/g;
|
|
202
|
+
let match;
|
|
203
|
+
while ((match = definitionRegex.exec(code)) !== null) {
|
|
204
|
+
const start = match.index;
|
|
205
|
+
const openParenIndex = start + match[0].length - 1;
|
|
206
|
+
const end = findMatchingDelimiter(code, openParenIndex, "(", ")");
|
|
207
|
+
if (end === -1)
|
|
208
|
+
continue;
|
|
209
|
+
let finalEnd = end;
|
|
210
|
+
if (code[finalEnd + 1] === ";") {
|
|
211
|
+
finalEnd++;
|
|
212
|
+
}
|
|
213
|
+
const block = code.substring(start, finalEnd + 1);
|
|
214
|
+
const resolvedBlock = resolveVariablesInBlock(block, variables);
|
|
215
|
+
const result = scanSpecSource(resolvedBlock, filePath);
|
|
216
|
+
if (result) {
|
|
217
|
+
results.push({
|
|
218
|
+
...result,
|
|
219
|
+
sourceBlock: resolvedBlock
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (results.length === 0 && filePath.includes(".spec.")) {
|
|
224
|
+
const result = scanSpecSource(code, filePath);
|
|
225
|
+
if (result.key !== "unknown") {
|
|
226
|
+
const resolvedBlock = resolveVariablesInBlock(code, variables);
|
|
227
|
+
const result2 = scanSpecSource(resolvedBlock, filePath);
|
|
228
|
+
results.push(result2);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return results;
|
|
232
|
+
}
|
|
233
|
+
function inferSpecTypeFromCodeBlock(fileSourceCode) {
|
|
234
|
+
if (fileSourceCode.includes("defineCommand")) {
|
|
235
|
+
return {
|
|
236
|
+
specType: "operation",
|
|
237
|
+
kind: "command"
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (fileSourceCode.includes("defineQuery")) {
|
|
241
|
+
return {
|
|
242
|
+
specType: "operation",
|
|
243
|
+
kind: "query"
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (fileSourceCode.includes("defineEvent")) {
|
|
247
|
+
return {
|
|
248
|
+
specType: "event",
|
|
249
|
+
kind: "event"
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (fileSourceCode.includes("definePresentation")) {
|
|
253
|
+
return {
|
|
254
|
+
specType: "presentation",
|
|
255
|
+
kind: "presentation"
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (fileSourceCode.includes("definePolicy")) {
|
|
259
|
+
return {
|
|
260
|
+
specType: "policy",
|
|
261
|
+
kind: "policy"
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (fileSourceCode.includes("defineCapability")) {
|
|
265
|
+
return {
|
|
266
|
+
specType: "capability",
|
|
267
|
+
kind: "capability"
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (fileSourceCode.includes("defineExample")) {
|
|
271
|
+
return {
|
|
272
|
+
specType: "example",
|
|
273
|
+
kind: "example"
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (fileSourceCode.includes("defineAppConfig") && !fileSourceCode.includes("export const defineAppConfig")) {
|
|
277
|
+
return {
|
|
278
|
+
specType: "app-config",
|
|
279
|
+
kind: "app-config"
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (fileSourceCode.includes("defineIntegration")) {
|
|
283
|
+
return {
|
|
284
|
+
specType: "integration",
|
|
285
|
+
kind: "integration"
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (fileSourceCode.includes("defineWorkflow")) {
|
|
289
|
+
return {
|
|
290
|
+
specType: "workflow",
|
|
291
|
+
kind: "workflow"
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (fileSourceCode.includes("defineTestSpec")) {
|
|
295
|
+
return {
|
|
296
|
+
specType: "test-spec",
|
|
297
|
+
kind: "test-spec"
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (fileSourceCode.includes("defineFeature")) {
|
|
301
|
+
return {
|
|
302
|
+
specType: "feature",
|
|
303
|
+
kind: "feature"
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
specType: "unknown",
|
|
308
|
+
kind: "unknown"
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function scanSpecSource(code, filePath) {
|
|
312
|
+
const keyMatch = code.match(/key\s*:\s*['"]([^'"]+)['"]/) ?? code.match(/export\s+const\s+(\w+)\s*=/);
|
|
313
|
+
const key = keyMatch?.[1] ?? "unknown";
|
|
314
|
+
const version = matchVersionField(code, "version");
|
|
315
|
+
const description = matchStringField(code, "description") ?? undefined;
|
|
316
|
+
const goal = matchStringField(code, "goal") ?? undefined;
|
|
317
|
+
const context = matchStringField(code, "context") ?? undefined;
|
|
318
|
+
const stabilityRaw = matchStringField(code, "stability");
|
|
319
|
+
const stability = isStability(stabilityRaw) ? stabilityRaw : undefined;
|
|
320
|
+
const owners = matchStringArrayField(code, "owners");
|
|
321
|
+
const tags = matchStringArrayField(code, "tags");
|
|
322
|
+
const inferredSpecType = inferSpecTypeFromCodeBlock(code);
|
|
323
|
+
const hasMeta = /meta\s*:\s*\{/.test(code);
|
|
324
|
+
const hasIo = /io\s*:\s*\{/.test(code);
|
|
325
|
+
const hasPolicy = /policy\s*:\s*\{/.test(code);
|
|
326
|
+
const hasPayload = /payload\s*:\s*\{/.test(code);
|
|
327
|
+
const hasContent = /content\s*:\s*\{/.test(code);
|
|
328
|
+
const hasDefinition = /definition\s*:\s*\{/.test(code);
|
|
329
|
+
const emittedEvents = extractRefList(code, "emits") ?? extractRefList(code, "emittedEvents");
|
|
330
|
+
const testRefs = extractTestRefs(code);
|
|
331
|
+
const policyRefs = hasPolicy ? parsePolicy(code) : undefined;
|
|
332
|
+
return {
|
|
333
|
+
filePath,
|
|
334
|
+
key,
|
|
335
|
+
version,
|
|
336
|
+
specType: inferredSpecType.specType,
|
|
337
|
+
kind: inferredSpecType.kind,
|
|
338
|
+
description,
|
|
339
|
+
goal,
|
|
340
|
+
context,
|
|
341
|
+
stability,
|
|
342
|
+
owners,
|
|
343
|
+
tags,
|
|
344
|
+
hasMeta,
|
|
345
|
+
hasIo,
|
|
346
|
+
hasPolicy,
|
|
347
|
+
hasPayload,
|
|
348
|
+
hasContent,
|
|
349
|
+
hasDefinition,
|
|
350
|
+
emittedEvents,
|
|
351
|
+
policyRefs,
|
|
352
|
+
testRefs,
|
|
353
|
+
testTarget: extractTestTarget(code),
|
|
354
|
+
testCoverage: extractTestCoverage(code),
|
|
355
|
+
sourceBlock: code
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function inferSpecTypeFromFilePath(filePath) {
|
|
359
|
+
if (filePath.includes(".contracts.") || /\/operations?\//.test(filePath)) {
|
|
360
|
+
return "operation";
|
|
361
|
+
}
|
|
362
|
+
if (filePath.includes(".event.") || /\/events?\//.test(filePath)) {
|
|
363
|
+
return "event";
|
|
364
|
+
}
|
|
365
|
+
if (filePath.includes(".presentation.") || /\/presentations?\//.test(filePath)) {
|
|
366
|
+
return "presentation";
|
|
367
|
+
}
|
|
368
|
+
if (filePath.includes(".policy.") || /\/policies?\//.test(filePath)) {
|
|
369
|
+
return "policy";
|
|
370
|
+
}
|
|
371
|
+
if (filePath.includes(".feature.") || /\/features?\//.test(filePath)) {
|
|
372
|
+
return "feature";
|
|
373
|
+
}
|
|
374
|
+
if (filePath.includes(".type.") || /\/types?\//.test(filePath)) {
|
|
375
|
+
return "type";
|
|
376
|
+
}
|
|
377
|
+
if (filePath.includes(".example.") || /\/examples?\//.test(filePath)) {
|
|
378
|
+
return "example";
|
|
379
|
+
}
|
|
380
|
+
if (filePath.includes(".app-config.")) {
|
|
381
|
+
return "app-config";
|
|
382
|
+
}
|
|
383
|
+
if (filePath.includes(".workflow.") || /\/workflows?\//.test(filePath)) {
|
|
384
|
+
return "workflow";
|
|
385
|
+
}
|
|
386
|
+
if (filePath.includes(".integration.") || /\/integrations?\//.test(filePath)) {
|
|
387
|
+
return "integration";
|
|
388
|
+
}
|
|
389
|
+
return "unknown";
|
|
390
|
+
}
|
|
391
|
+
// src/analysis/feature-extractor.ts
|
|
392
|
+
import {
|
|
393
|
+
Project,
|
|
394
|
+
Node,
|
|
395
|
+
SyntaxKind
|
|
396
|
+
} from "ts-morph";
|
|
397
|
+
function extractFeatureRefs(code) {
|
|
398
|
+
const result = {
|
|
399
|
+
operations: [],
|
|
400
|
+
events: [],
|
|
401
|
+
presentations: [],
|
|
402
|
+
experiments: [],
|
|
403
|
+
capabilities: {
|
|
404
|
+
provides: [],
|
|
405
|
+
requires: []
|
|
406
|
+
},
|
|
407
|
+
opToPresentationLinks: [],
|
|
408
|
+
presentationsTargets: []
|
|
409
|
+
};
|
|
410
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
411
|
+
const sourceFile = project.createSourceFile("feature.ts", code);
|
|
412
|
+
const extractRefs = (obj, propName, allowKeyOnly = false) => {
|
|
413
|
+
const refs = [];
|
|
414
|
+
const prop = obj.getProperty(propName);
|
|
415
|
+
if (prop && Node.isPropertyAssignment(prop)) {
|
|
416
|
+
const init = prop.getInitializer();
|
|
417
|
+
if (init && Node.isArrayLiteralExpression(init)) {
|
|
418
|
+
for (const elem of init.getElements()) {
|
|
419
|
+
if (Node.isObjectLiteralExpression(elem)) {
|
|
420
|
+
const keyText = getTextFromProp(elem, "key");
|
|
421
|
+
const verText = getTextFromProp(elem, "version");
|
|
422
|
+
if (keyText && verText) {
|
|
423
|
+
refs.push({ key: keyText, version: verText });
|
|
424
|
+
} else if (keyText && allowKeyOnly) {
|
|
425
|
+
refs.push({ key: keyText, version: "1.0.0" });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return refs;
|
|
432
|
+
};
|
|
433
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
434
|
+
for (const call of callExpressions) {
|
|
435
|
+
if (["defineFeature", "defineAppConfig", "defineAppBlueprint"].includes(call.getExpression().getText())) {
|
|
436
|
+
const args = call.getArguments();
|
|
437
|
+
if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) {
|
|
438
|
+
const obj = args[0];
|
|
439
|
+
result.operations.push(...extractRefs(obj, "operations"));
|
|
440
|
+
result.events.push(...extractRefs(obj, "events"));
|
|
441
|
+
result.presentations.push(...extractRefs(obj, "presentations"));
|
|
442
|
+
result.experiments.push(...extractRefs(obj, "experiments"));
|
|
443
|
+
const capsProp = obj.getProperty("capabilities");
|
|
444
|
+
if (capsProp && Node.isPropertyAssignment(capsProp)) {
|
|
445
|
+
const capsObj = capsProp.getInitializer();
|
|
446
|
+
if (capsObj && Node.isObjectLiteralExpression(capsObj)) {
|
|
447
|
+
result.capabilities.provides.push(...extractRefs(capsObj, "provides"));
|
|
448
|
+
result.capabilities.requires.push(...extractRefs(capsObj, "requires", true));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const linksProp = obj.getProperty("opToPresentation");
|
|
452
|
+
if (linksProp && Node.isPropertyAssignment(linksProp)) {
|
|
453
|
+
const linksArr = linksProp.getInitializer();
|
|
454
|
+
if (linksArr && Node.isArrayLiteralExpression(linksArr)) {
|
|
455
|
+
linksArr.getElements().forEach((link) => {
|
|
456
|
+
if (Node.isObjectLiteralExpression(link)) {
|
|
457
|
+
const opProp = link.getProperty("op");
|
|
458
|
+
const presProp = link.getProperty("pres");
|
|
459
|
+
let opRef;
|
|
460
|
+
let presRef;
|
|
461
|
+
if (opProp && Node.isPropertyAssignment(opProp)) {
|
|
462
|
+
const val = opProp.getInitializer();
|
|
463
|
+
if (val && Node.isObjectLiteralExpression(val)) {
|
|
464
|
+
const key = getTextFromProp(val, "key");
|
|
465
|
+
const version = getTextFromProp(val, "version");
|
|
466
|
+
if (key && version)
|
|
467
|
+
opRef = { key, version };
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (presProp && Node.isPropertyAssignment(presProp)) {
|
|
471
|
+
const val = presProp.getInitializer();
|
|
472
|
+
if (val && Node.isObjectLiteralExpression(val)) {
|
|
473
|
+
const key = getTextFromProp(val, "key");
|
|
474
|
+
const version = getTextFromProp(val, "version");
|
|
475
|
+
if (key && version)
|
|
476
|
+
presRef = { key, version };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (opRef && presRef) {
|
|
480
|
+
result.opToPresentationLinks.push({
|
|
481
|
+
op: opRef,
|
|
482
|
+
pres: presRef
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
extractLinks(obj, result.opToPresentationLinks);
|
|
490
|
+
const targetsProp = obj.getProperty("presentationsTargets");
|
|
491
|
+
if (targetsProp && Node.isPropertyAssignment(targetsProp)) {
|
|
492
|
+
const targetsArr = targetsProp.getInitializer();
|
|
493
|
+
if (targetsArr && Node.isArrayLiteralExpression(targetsArr)) {
|
|
494
|
+
targetsArr.getElements().forEach((targetBlock) => {
|
|
495
|
+
if (Node.isObjectLiteralExpression(targetBlock)) {
|
|
496
|
+
const key = getTextFromProp(targetBlock, "key");
|
|
497
|
+
const version = getTextFromProp(targetBlock, "version");
|
|
498
|
+
const targetsList = getTargetsList(targetBlock, "targets");
|
|
499
|
+
if (key && version && targetsList) {
|
|
500
|
+
result.presentationsTargets.push({
|
|
501
|
+
key,
|
|
502
|
+
version,
|
|
503
|
+
targets: targetsList
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
extractPresentationTargets(obj, result.presentationsTargets);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
function extractLinks(obj, opToPresentationLinks) {
|
|
517
|
+
const interactionsProp = obj.getProperty("interactions");
|
|
518
|
+
if (interactionsProp && Node.isPropertyAssignment(interactionsProp)) {
|
|
519
|
+
const interactionsArr = interactionsProp.getInitializer();
|
|
520
|
+
if (interactionsArr && Node.isArrayLiteralExpression(interactionsArr)) {
|
|
521
|
+
interactionsArr.getElements().forEach((interaction) => {
|
|
522
|
+
if (Node.isObjectLiteralExpression(interaction)) {
|
|
523
|
+
const triggerProp = interaction.getProperty("trigger");
|
|
524
|
+
const triggerObj = triggerProp && Node.isPropertyAssignment(triggerProp) && triggerProp.getInitializer();
|
|
525
|
+
const actionProp = interaction.getProperty("action");
|
|
526
|
+
const actionObj = actionProp && Node.isPropertyAssignment(actionProp) && actionProp.getInitializer();
|
|
527
|
+
if (triggerObj && Node.isObjectLiteralExpression(triggerObj) && actionObj && Node.isObjectLiteralExpression(actionObj)) {
|
|
528
|
+
const triggerType = getTextFromProp(triggerObj, "type");
|
|
529
|
+
const actionType = getTextFromProp(actionObj, "type");
|
|
530
|
+
if (triggerType === "presentation" && actionType === "operation") {
|
|
531
|
+
const presRef = extractNestedRef(triggerObj, "presentation");
|
|
532
|
+
const opRef = extractNestedRef(actionObj, "operation");
|
|
533
|
+
if (presRef && opRef) {
|
|
534
|
+
opToPresentationLinks.push({ op: opRef, pres: presRef });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function extractNestedRef(obj, propName) {
|
|
544
|
+
const prop = obj.getProperty(propName);
|
|
545
|
+
if (prop && Node.isPropertyAssignment(prop)) {
|
|
546
|
+
const val = prop.getInitializer();
|
|
547
|
+
if (val && Node.isObjectLiteralExpression(val)) {
|
|
548
|
+
const key = getTextFromProp(val, "key");
|
|
549
|
+
const version = getTextFromProp(val, "version");
|
|
550
|
+
if (key && version)
|
|
551
|
+
return { key, version };
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
function extractPresentationTargets(obj, presentationsTargets) {
|
|
557
|
+
const presentationsProp = obj.getProperty("presentations");
|
|
558
|
+
if (presentationsProp && Node.isPropertyAssignment(presentationsProp)) {
|
|
559
|
+
const presentationsArr = presentationsProp.getInitializer();
|
|
560
|
+
if (presentationsArr && Node.isArrayLiteralExpression(presentationsArr)) {
|
|
561
|
+
for (const elem of presentationsArr.getElements()) {
|
|
562
|
+
if (Node.isObjectLiteralExpression(elem)) {
|
|
563
|
+
const keyText = getTextFromProp(elem, "key");
|
|
564
|
+
const verText = getTextFromProp(elem, "version");
|
|
565
|
+
const targetsList = getTargetsList(elem, "targets");
|
|
566
|
+
if (keyText && verText && targetsList) {
|
|
567
|
+
presentationsTargets.push({
|
|
568
|
+
key: keyText,
|
|
569
|
+
version: verText,
|
|
570
|
+
targets: targetsList
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function getTextFromProp(obj, propName) {
|
|
579
|
+
const prop = obj.getProperty(propName);
|
|
580
|
+
if (prop && Node.isPropertyAssignment(prop)) {
|
|
581
|
+
const init = prop.getInitializer();
|
|
582
|
+
if (init && Node.isStringLiteral(init)) {
|
|
583
|
+
return init.getLiteralText();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
function getTargetsList(obj, propName) {
|
|
589
|
+
const prop = obj.getProperty(propName);
|
|
590
|
+
if (prop && Node.isPropertyAssignment(prop)) {
|
|
591
|
+
const init = prop.getInitializer();
|
|
592
|
+
if (init && Node.isArrayLiteralExpression(init)) {
|
|
593
|
+
return init.getElements().map((e) => {
|
|
594
|
+
if (Node.isStringLiteral(e))
|
|
595
|
+
return e.getLiteralText();
|
|
596
|
+
return { target: e.getText() };
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/analysis/feature-scan.ts
|
|
604
|
+
function isFeatureFile(filePath) {
|
|
605
|
+
return filePath.includes(".feature.") && filePath.endsWith(".ts");
|
|
606
|
+
}
|
|
607
|
+
function scanFeatureSource(code, filePath) {
|
|
608
|
+
const key = matchStringField(code, "key") ?? extractKeyFromFilePath(filePath);
|
|
609
|
+
const versionRaw = matchStringField(code, "version");
|
|
610
|
+
const version = versionRaw ?? "1.0.0";
|
|
611
|
+
const title = matchStringField(code, "title") ?? undefined;
|
|
612
|
+
const description = matchStringField(code, "description") ?? undefined;
|
|
613
|
+
const goal = matchStringField(code, "goal") ?? undefined;
|
|
614
|
+
const context = matchStringField(code, "context") ?? undefined;
|
|
615
|
+
const stabilityRaw = matchStringField(code, "stability");
|
|
616
|
+
const stability = isStability(stabilityRaw) ? stabilityRaw : undefined;
|
|
617
|
+
const owners = matchStringArrayField(code, "owners");
|
|
618
|
+
const tags = matchStringArrayField(code, "tags");
|
|
619
|
+
const refs = extractFeatureRefs(code);
|
|
620
|
+
return {
|
|
621
|
+
filePath,
|
|
622
|
+
key,
|
|
623
|
+
version,
|
|
624
|
+
title,
|
|
625
|
+
description,
|
|
626
|
+
goal,
|
|
627
|
+
context,
|
|
628
|
+
stability,
|
|
629
|
+
owners,
|
|
630
|
+
tags,
|
|
631
|
+
operations: refs.operations,
|
|
632
|
+
events: refs.events,
|
|
633
|
+
presentations: refs.presentations,
|
|
634
|
+
experiments: refs.experiments,
|
|
635
|
+
capabilities: refs.capabilities,
|
|
636
|
+
opToPresentationLinks: refs.opToPresentationLinks,
|
|
637
|
+
presentationsTargets: refs.presentationsTargets,
|
|
638
|
+
sourceBlock: code
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function extractKeyFromFilePath(filePath) {
|
|
642
|
+
const fileName = filePath.split("/").pop() ?? filePath;
|
|
643
|
+
return fileName.replace(/\.feature\.[jt]s$/, "").replace(/[^a-zA-Z0-9-]/g, "-");
|
|
644
|
+
}
|
|
645
|
+
// src/analysis/example-scan.ts
|
|
646
|
+
function isExampleFile(filePath) {
|
|
647
|
+
return filePath.includes("/example.") || filePath.endsWith(".example.ts");
|
|
648
|
+
}
|
|
649
|
+
function scanExampleSource(code, filePath) {
|
|
650
|
+
const key = matchStringField(code, "key") ?? extractKeyFromFilePath2(filePath);
|
|
651
|
+
const versionRaw = matchStringField(code, "version");
|
|
652
|
+
const version = versionRaw ?? undefined;
|
|
653
|
+
const title = matchStringField(code, "title") ?? undefined;
|
|
654
|
+
const description = matchStringField(code, "description") ?? undefined;
|
|
655
|
+
const summary = matchStringField(code, "summary") ?? undefined;
|
|
656
|
+
const kind = matchStringField(code, "kind") ?? undefined;
|
|
657
|
+
const visibility = matchStringField(code, "visibility") ?? undefined;
|
|
658
|
+
const domain = matchStringField(code, "domain") ?? undefined;
|
|
659
|
+
const stabilityRaw = matchStringField(code, "stability");
|
|
660
|
+
const stability = isStability(stabilityRaw) ? stabilityRaw : undefined;
|
|
661
|
+
const owners = matchStringArrayField(code, "owners");
|
|
662
|
+
const tags = matchStringArrayField(code, "tags");
|
|
663
|
+
const docs = extractDocs(code);
|
|
664
|
+
const surfaces = extractSurfaces(code);
|
|
665
|
+
const entrypoints = extractEntrypoints(code);
|
|
666
|
+
return {
|
|
667
|
+
filePath,
|
|
668
|
+
key,
|
|
669
|
+
version,
|
|
670
|
+
title,
|
|
671
|
+
description,
|
|
672
|
+
summary,
|
|
673
|
+
kind,
|
|
674
|
+
visibility,
|
|
675
|
+
domain,
|
|
676
|
+
stability,
|
|
677
|
+
owners,
|
|
678
|
+
tags,
|
|
679
|
+
docs,
|
|
680
|
+
surfaces,
|
|
681
|
+
entrypoints
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function extractDocs(code) {
|
|
685
|
+
const docsMatch = code.match(/docs\s*:\s*\{([\s\S]*?)\}/);
|
|
686
|
+
if (!docsMatch?.[1])
|
|
687
|
+
return;
|
|
688
|
+
const docsContent = docsMatch[1];
|
|
689
|
+
return {
|
|
690
|
+
rootDocId: matchStringFieldIn(docsContent, "rootDocId") ?? undefined,
|
|
691
|
+
goalDocId: matchStringFieldIn(docsContent, "goalDocId") ?? undefined,
|
|
692
|
+
usageDocId: matchStringFieldIn(docsContent, "usageDocId") ?? undefined
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function extractSurfaces(code) {
|
|
696
|
+
const surfaces = {
|
|
697
|
+
templates: false,
|
|
698
|
+
sandbox: { enabled: false, modes: [] },
|
|
699
|
+
studio: { enabled: false, installable: false },
|
|
700
|
+
mcp: { enabled: false }
|
|
701
|
+
};
|
|
702
|
+
surfaces.templates = /surfaces\s*:\s*\{[\s\S]*?templates\s*:\s*true/.test(code);
|
|
703
|
+
const sandboxMatch = code.match(/sandbox\s*:\s*\{\s*enabled\s*:\s*(true|false)\s*,\s*modes\s*:\s*\[([^\]]*)\]/);
|
|
704
|
+
if (sandboxMatch) {
|
|
705
|
+
surfaces.sandbox.enabled = sandboxMatch[1] === "true";
|
|
706
|
+
if (sandboxMatch[2]) {
|
|
707
|
+
surfaces.sandbox.modes = Array.from(sandboxMatch[2].matchAll(/['"]([^'"]+)['"]/g)).map((m) => m[1]).filter((v) => typeof v === "string");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const studioMatch = code.match(/studio\s*:\s*\{\s*enabled\s*:\s*(true|false)\s*,\s*installable\s*:\s*(true|false)/);
|
|
711
|
+
if (studioMatch) {
|
|
712
|
+
surfaces.studio.enabled = studioMatch[1] === "true";
|
|
713
|
+
surfaces.studio.installable = studioMatch[2] === "true";
|
|
714
|
+
}
|
|
715
|
+
const mcpMatch = code.match(/mcp\s*:\s*\{\s*enabled\s*:\s*(true|false)/);
|
|
716
|
+
if (mcpMatch) {
|
|
717
|
+
surfaces.mcp.enabled = mcpMatch[1] === "true";
|
|
718
|
+
}
|
|
719
|
+
return surfaces;
|
|
720
|
+
}
|
|
721
|
+
function extractEntrypoints(code) {
|
|
722
|
+
const entrypoints = {
|
|
723
|
+
packageName: ""
|
|
724
|
+
};
|
|
725
|
+
const entrypointsMatch = code.match(/entrypoints\s*:\s*\{([\s\S]*?)\}(?=\s*[,}])/);
|
|
726
|
+
if (!entrypointsMatch?.[1])
|
|
727
|
+
return entrypoints;
|
|
728
|
+
const content = entrypointsMatch[1];
|
|
729
|
+
entrypoints.packageName = matchStringFieldIn(content, "packageName") ?? "unknown";
|
|
730
|
+
entrypoints.feature = matchStringFieldIn(content, "feature") ?? undefined;
|
|
731
|
+
entrypoints.blueprint = matchStringFieldIn(content, "blueprint") ?? undefined;
|
|
732
|
+
entrypoints.contracts = matchStringFieldIn(content, "contracts") ?? undefined;
|
|
733
|
+
entrypoints.presentations = matchStringFieldIn(content, "presentations") ?? undefined;
|
|
734
|
+
entrypoints.handlers = matchStringFieldIn(content, "handlers") ?? undefined;
|
|
735
|
+
entrypoints.ui = matchStringFieldIn(content, "ui") ?? undefined;
|
|
736
|
+
entrypoints.docs = matchStringFieldIn(content, "docs") ?? undefined;
|
|
737
|
+
return entrypoints;
|
|
738
|
+
}
|
|
739
|
+
function extractKeyFromFilePath2(filePath) {
|
|
740
|
+
const parts = filePath.split("/");
|
|
741
|
+
const examplesIndex = parts.findIndex((p) => p === "examples");
|
|
742
|
+
const exampleName = parts[examplesIndex + 1];
|
|
743
|
+
if (examplesIndex !== -1 && exampleName !== undefined) {
|
|
744
|
+
return exampleName;
|
|
745
|
+
}
|
|
746
|
+
const fileName = parts.pop() ?? filePath;
|
|
747
|
+
return fileName.replace(/\.example\.[jt]s$/, "").replace(/[^a-zA-Z0-9-]/g, "-");
|
|
748
|
+
}
|
|
749
|
+
// src/analysis/grouping.ts
|
|
750
|
+
var SpecGroupingStrategies = {
|
|
751
|
+
byTag: (item) => item.tags?.[0] ?? "untagged",
|
|
752
|
+
byOwner: (item) => item.owners?.[0] ?? "unowned",
|
|
753
|
+
byDomain: (item) => {
|
|
754
|
+
const key = item.key ?? "";
|
|
755
|
+
if (key.includes(".")) {
|
|
756
|
+
return key.split(".")[0] ?? "default";
|
|
757
|
+
}
|
|
758
|
+
return "default";
|
|
759
|
+
},
|
|
760
|
+
byStability: (item) => item.stability ?? "stable",
|
|
761
|
+
bySpecType: (item) => item.specType,
|
|
762
|
+
byDirectory: (item) => {
|
|
763
|
+
const parts = item.filePath.split("/");
|
|
764
|
+
return parts.slice(0, -1).join("/") || ".";
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
function filterSpecs(specs, filter) {
|
|
768
|
+
return specs.filter((spec) => {
|
|
769
|
+
if (filter.tags?.length) {
|
|
770
|
+
const hasMatchingTag = filter.tags.some((tag) => spec.tags?.includes(tag));
|
|
771
|
+
if (!hasMatchingTag)
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
if (filter.owners?.length) {
|
|
775
|
+
const hasMatchingOwner = filter.owners.some((owner) => spec.owners?.includes(owner));
|
|
776
|
+
if (!hasMatchingOwner)
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
if (filter.stability?.length) {
|
|
780
|
+
if (!filter.stability.includes(spec.stability ?? "stable")) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (filter.specType?.length) {
|
|
785
|
+
if (!filter.specType.includes(spec.specType)) {
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (filter.namePattern) {
|
|
790
|
+
const key = spec.key ?? "";
|
|
791
|
+
const pattern = filter.namePattern.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
792
|
+
const regex = new RegExp(`^${pattern}$`, "i");
|
|
793
|
+
if (!regex.test(key))
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
return true;
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
function groupSpecs(items, keyFn) {
|
|
800
|
+
const groups = new Map;
|
|
801
|
+
for (const item of items) {
|
|
802
|
+
const key = keyFn(item);
|
|
803
|
+
const existing = groups.get(key);
|
|
804
|
+
if (existing) {
|
|
805
|
+
existing.push(item);
|
|
806
|
+
} else {
|
|
807
|
+
groups.set(key, [item]);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return groups;
|
|
811
|
+
}
|
|
812
|
+
function groupSpecsToArray(items, keyFn) {
|
|
813
|
+
const map = groupSpecs(items, keyFn);
|
|
814
|
+
return Array.from(map.entries()).map(([key, items2]) => ({ key, items: items2 })).sort((a, b) => a.key.localeCompare(b.key));
|
|
815
|
+
}
|
|
816
|
+
function getUniqueSpecTags(specs) {
|
|
817
|
+
const tags = new Set;
|
|
818
|
+
for (const spec of specs) {
|
|
819
|
+
for (const tag of spec.tags ?? []) {
|
|
820
|
+
tags.add(tag);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return Array.from(tags).sort();
|
|
824
|
+
}
|
|
825
|
+
function getUniqueSpecOwners(specs) {
|
|
826
|
+
const owners = new Set;
|
|
827
|
+
for (const spec of specs) {
|
|
828
|
+
for (const owner of spec.owners ?? []) {
|
|
829
|
+
owners.add(owner);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return Array.from(owners).sort();
|
|
833
|
+
}
|
|
834
|
+
function getUniqueSpecDomains(specs) {
|
|
835
|
+
const domains = new Set;
|
|
836
|
+
for (const spec of specs) {
|
|
837
|
+
domains.add(SpecGroupingStrategies.byDomain(spec));
|
|
838
|
+
}
|
|
839
|
+
return Array.from(domains).sort();
|
|
840
|
+
}
|
|
841
|
+
function filterFeatures(features, filter) {
|
|
842
|
+
return features.filter((feature) => {
|
|
843
|
+
if (filter.tags?.length) {
|
|
844
|
+
const hasMatchingTag = filter.tags.some((tag) => feature.tags?.includes(tag));
|
|
845
|
+
if (!hasMatchingTag)
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
if (filter.owners?.length) {
|
|
849
|
+
const hasMatchingOwner = filter.owners.some((owner) => feature.owners?.includes(owner));
|
|
850
|
+
if (!hasMatchingOwner)
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
if (filter.stability?.length) {
|
|
854
|
+
if (!filter.stability.includes(feature.stability ?? "stable")) {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (filter.namePattern) {
|
|
859
|
+
const pattern = filter.namePattern.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
860
|
+
const regex = new RegExp(`^${pattern}$`, "i");
|
|
861
|
+
if (!regex.test(feature.key))
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
return true;
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
// src/analysis/diff/semantic.ts
|
|
868
|
+
function computeSemanticDiff(aCode, aPath, bCode, bPath, options = {}) {
|
|
869
|
+
const a = scanSpecSource(aCode, aPath);
|
|
870
|
+
const b = scanSpecSource(bCode, bPath);
|
|
871
|
+
const diffs = [];
|
|
872
|
+
compareScalar(diffs, "specType", a.specType, b.specType, {
|
|
873
|
+
breaking: true,
|
|
874
|
+
label: "Spec type"
|
|
875
|
+
});
|
|
876
|
+
compareScalar(diffs, "key", a.key, b.key, {
|
|
877
|
+
breaking: true,
|
|
878
|
+
label: "Key"
|
|
879
|
+
});
|
|
880
|
+
compareScalar(diffs, "version", a.version, b.version, {
|
|
881
|
+
breaking: true,
|
|
882
|
+
label: "Version"
|
|
883
|
+
});
|
|
884
|
+
compareScalar(diffs, "kind", a.kind, b.kind, {
|
|
885
|
+
breaking: true,
|
|
886
|
+
label: "Kind"
|
|
887
|
+
});
|
|
888
|
+
compareScalar(diffs, "stability", a.stability, b.stability, {
|
|
889
|
+
breaking: isStabilityDowngrade(a, b),
|
|
890
|
+
label: "Stability"
|
|
891
|
+
});
|
|
892
|
+
compareArray(diffs, "owners", a.owners ?? [], b.owners ?? [], {
|
|
893
|
+
label: "Owners"
|
|
894
|
+
});
|
|
895
|
+
compareArray(diffs, "tags", a.tags ?? [], b.tags ?? [], { label: "Tags" });
|
|
896
|
+
compareStructuralHints(diffs, a, b);
|
|
897
|
+
const filtered = options.breakingOnly ? diffs.filter((d) => d.type === "breaking") : diffs;
|
|
898
|
+
return filtered;
|
|
899
|
+
}
|
|
900
|
+
function compareScalar(diffs, path, a, b, config) {
|
|
901
|
+
if (a === b)
|
|
902
|
+
return;
|
|
903
|
+
diffs.push({
|
|
904
|
+
type: config.breaking ? "breaking" : "changed",
|
|
905
|
+
path,
|
|
906
|
+
oldValue: a,
|
|
907
|
+
newValue: b,
|
|
908
|
+
description: `${config.label} changed`
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
function compareArray(diffs, path, a, b, config) {
|
|
912
|
+
const aSorted = [...a].sort();
|
|
913
|
+
const bSorted = [...b].sort();
|
|
914
|
+
if (JSON.stringify(aSorted) === JSON.stringify(bSorted))
|
|
915
|
+
return;
|
|
916
|
+
diffs.push({
|
|
917
|
+
type: "changed",
|
|
918
|
+
path,
|
|
919
|
+
oldValue: aSorted,
|
|
920
|
+
newValue: bSorted,
|
|
921
|
+
description: `${config.label} changed`
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
function isStabilityDowngrade(a, b) {
|
|
925
|
+
const order = {
|
|
926
|
+
experimental: 0,
|
|
927
|
+
beta: 1,
|
|
928
|
+
stable: 2,
|
|
929
|
+
deprecated: 3
|
|
930
|
+
};
|
|
931
|
+
const aValue = a.stability ? order[a.stability] ?? 0 : 0;
|
|
932
|
+
const bValue = b.stability ? order[b.stability] ?? 0 : 0;
|
|
933
|
+
return bValue > aValue;
|
|
934
|
+
}
|
|
935
|
+
function compareStructuralHints(diffs, a, b) {
|
|
936
|
+
compareScalar(diffs, "hasMeta", a.hasMeta, b.hasMeta, {
|
|
937
|
+
breaking: a.specType === "operation" || b.specType === "operation",
|
|
938
|
+
label: "meta section presence"
|
|
939
|
+
});
|
|
940
|
+
compareScalar(diffs, "hasIo", a.hasIo, b.hasIo, {
|
|
941
|
+
breaking: a.specType === "operation" || b.specType === "operation",
|
|
942
|
+
label: "io section presence"
|
|
943
|
+
});
|
|
944
|
+
compareScalar(diffs, "hasPolicy", a.hasPolicy, b.hasPolicy, {
|
|
945
|
+
breaking: a.specType === "operation" || b.specType === "operation",
|
|
946
|
+
label: "policy section presence"
|
|
947
|
+
});
|
|
948
|
+
compareScalar(diffs, "hasPayload", a.hasPayload, b.hasPayload, {
|
|
949
|
+
breaking: a.specType === "event" || b.specType === "event",
|
|
950
|
+
label: "payload section presence"
|
|
951
|
+
});
|
|
952
|
+
compareScalar(diffs, "hasContent", a.hasContent, b.hasContent, {
|
|
953
|
+
breaking: a.specType === "presentation" || b.specType === "presentation",
|
|
954
|
+
label: "content section presence"
|
|
955
|
+
});
|
|
956
|
+
compareScalar(diffs, "hasDefinition", a.hasDefinition, b.hasDefinition, {
|
|
957
|
+
breaking: a.specType === "workflow" || b.specType === "workflow",
|
|
958
|
+
label: "definition section presence"
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
// src/analysis/diff/deep-diff.ts
|
|
962
|
+
function computeIoDiff(base, head, options = {}) {
|
|
963
|
+
const diffs = [];
|
|
964
|
+
diffs.push(...computeFieldsDiff(base.input, head.input, "io.input", options));
|
|
965
|
+
diffs.push(...computeFieldsDiff(base.output, head.output, "io.output", options));
|
|
966
|
+
return options.breakingOnly ? diffs.filter((d) => d.type === "breaking") : diffs;
|
|
967
|
+
}
|
|
968
|
+
function computeFieldsDiff(baseFields, headFields, pathPrefix, options = {}) {
|
|
969
|
+
const diffs = [];
|
|
970
|
+
const baseNames = new Set(Object.keys(baseFields));
|
|
971
|
+
const headNames = new Set(Object.keys(headFields));
|
|
972
|
+
for (const name of baseNames) {
|
|
973
|
+
if (!headNames.has(name)) {
|
|
974
|
+
const baseField = baseFields[name];
|
|
975
|
+
diffs.push({
|
|
976
|
+
type: "breaking",
|
|
977
|
+
path: `${pathPrefix}.${name}`,
|
|
978
|
+
oldValue: baseField,
|
|
979
|
+
newValue: undefined,
|
|
980
|
+
description: `Field '${name}' was removed`
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
for (const name of headNames) {
|
|
985
|
+
if (!baseNames.has(name)) {
|
|
986
|
+
const headField = headFields[name];
|
|
987
|
+
const isBreaking = headField?.required === true;
|
|
988
|
+
diffs.push({
|
|
989
|
+
type: isBreaking ? "breaking" : "added",
|
|
990
|
+
path: `${pathPrefix}.${name}`,
|
|
991
|
+
oldValue: undefined,
|
|
992
|
+
newValue: headField,
|
|
993
|
+
description: isBreaking ? `Required field '${name}' was added` : `Optional field '${name}' was added`
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
for (const name of baseNames) {
|
|
998
|
+
if (headNames.has(name)) {
|
|
999
|
+
const baseField = baseFields[name];
|
|
1000
|
+
const headField = headFields[name];
|
|
1001
|
+
if (baseField && headField) {
|
|
1002
|
+
diffs.push(...computeFieldDiff(baseField, headField, `${pathPrefix}.${name}`, options));
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return diffs;
|
|
1007
|
+
}
|
|
1008
|
+
function computeFieldDiff(base, head, path, _options = {}) {
|
|
1009
|
+
const diffs = [];
|
|
1010
|
+
if (base.type !== head.type) {
|
|
1011
|
+
diffs.push({
|
|
1012
|
+
type: "breaking",
|
|
1013
|
+
path: `${path}.type`,
|
|
1014
|
+
oldValue: base.type,
|
|
1015
|
+
newValue: head.type,
|
|
1016
|
+
description: `Field type changed from '${base.type}' to '${head.type}'`
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
if (base.required !== head.required) {
|
|
1020
|
+
const isBreaking = !base.required && head.required;
|
|
1021
|
+
diffs.push({
|
|
1022
|
+
type: isBreaking ? "breaking" : "changed",
|
|
1023
|
+
path: `${path}.required`,
|
|
1024
|
+
oldValue: base.required,
|
|
1025
|
+
newValue: head.required,
|
|
1026
|
+
description: isBreaking ? `Field '${base.name}' changed from optional to required` : `Field '${base.name}' changed from required to optional`
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
if (base.nullable !== head.nullable) {
|
|
1030
|
+
const isBreaking = base.nullable && !head.nullable;
|
|
1031
|
+
diffs.push({
|
|
1032
|
+
type: isBreaking ? "breaking" : "changed",
|
|
1033
|
+
path: `${path}.nullable`,
|
|
1034
|
+
oldValue: base.nullable,
|
|
1035
|
+
newValue: head.nullable,
|
|
1036
|
+
description: isBreaking ? `Field '${base.name}' is no longer nullable` : `Field '${base.name}' is now nullable`
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
if (base.type === "enum" && head.type === "enum") {
|
|
1040
|
+
const baseValues = new Set(base.enumValues ?? []);
|
|
1041
|
+
const headValues = new Set(head.enumValues ?? []);
|
|
1042
|
+
for (const value of baseValues) {
|
|
1043
|
+
if (!headValues.has(value)) {
|
|
1044
|
+
diffs.push({
|
|
1045
|
+
type: "breaking",
|
|
1046
|
+
path: `${path}.enumValues`,
|
|
1047
|
+
oldValue: base.enumValues,
|
|
1048
|
+
newValue: head.enumValues,
|
|
1049
|
+
description: `Enum value '${value}' was removed`
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
for (const value of headValues) {
|
|
1054
|
+
if (!baseValues.has(value)) {
|
|
1055
|
+
diffs.push({
|
|
1056
|
+
type: "added",
|
|
1057
|
+
path: `${path}.enumValues`,
|
|
1058
|
+
oldValue: base.enumValues,
|
|
1059
|
+
newValue: head.enumValues,
|
|
1060
|
+
description: `Enum value '${value}' was added`
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (base.type === "object" && head.type === "object" && base.properties && head.properties) {
|
|
1066
|
+
diffs.push(...computeFieldsDiff(base.properties, head.properties, path, _options));
|
|
1067
|
+
}
|
|
1068
|
+
if (base.type === "array" && head.type === "array" && base.items && head.items) {
|
|
1069
|
+
diffs.push(...computeFieldDiff(base.items, head.items, `${path}.items`, _options));
|
|
1070
|
+
}
|
|
1071
|
+
return diffs;
|
|
1072
|
+
}
|
|
1073
|
+
function isBreakingChange(diff, context) {
|
|
1074
|
+
if (context === "output") {
|
|
1075
|
+
return diff.type === "breaking" || diff.type === "removed";
|
|
1076
|
+
}
|
|
1077
|
+
if (context === "input") {
|
|
1078
|
+
if (diff.type === "added" && diff.description?.includes("Required field")) {
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
return diff.type === "breaking";
|
|
1082
|
+
}
|
|
1083
|
+
return diff.type === "breaking";
|
|
1084
|
+
}
|
|
1085
|
+
// src/analysis/deps/graph.ts
|
|
1086
|
+
function buildReverseEdges(graph) {
|
|
1087
|
+
for (const node of graph.values()) {
|
|
1088
|
+
node.dependents = [];
|
|
1089
|
+
}
|
|
1090
|
+
for (const [key, node] of graph) {
|
|
1091
|
+
for (const dep of node.dependencies) {
|
|
1092
|
+
const depNode = graph.get(dep);
|
|
1093
|
+
if (depNode) {
|
|
1094
|
+
depNode.dependents.push(key);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
for (const node of graph.values()) {
|
|
1099
|
+
node.dependents.sort((a, b) => a.localeCompare(b));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function detectCycles(graph) {
|
|
1103
|
+
const visited = new Set;
|
|
1104
|
+
const stack = new Set;
|
|
1105
|
+
const cycles = [];
|
|
1106
|
+
function dfs(key, path) {
|
|
1107
|
+
if (stack.has(key)) {
|
|
1108
|
+
const start = path.indexOf(key);
|
|
1109
|
+
if (start >= 0)
|
|
1110
|
+
cycles.push([...path.slice(start), key]);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (visited.has(key))
|
|
1114
|
+
return;
|
|
1115
|
+
visited.add(key);
|
|
1116
|
+
stack.add(key);
|
|
1117
|
+
const node = graph.get(key);
|
|
1118
|
+
if (node) {
|
|
1119
|
+
for (const dep of node.dependencies) {
|
|
1120
|
+
dfs(dep, [...path, key]);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
stack.delete(key);
|
|
1124
|
+
}
|
|
1125
|
+
for (const key of graph.keys()) {
|
|
1126
|
+
if (!visited.has(key))
|
|
1127
|
+
dfs(key, []);
|
|
1128
|
+
}
|
|
1129
|
+
return cycles;
|
|
1130
|
+
}
|
|
1131
|
+
function findMissingDependencies(graph) {
|
|
1132
|
+
const missing = [];
|
|
1133
|
+
for (const [key, node] of graph) {
|
|
1134
|
+
const absent = node.dependencies.filter((dep) => !graph.has(dep));
|
|
1135
|
+
if (absent.length > 0) {
|
|
1136
|
+
missing.push({ contract: key, missing: absent });
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return missing;
|
|
1140
|
+
}
|
|
1141
|
+
function toDot(graph) {
|
|
1142
|
+
const lines = [];
|
|
1143
|
+
lines.push("digraph ContractDependencies {");
|
|
1144
|
+
lines.push(" rankdir=LR;");
|
|
1145
|
+
lines.push(" node [shape=box];");
|
|
1146
|
+
for (const [key, node] of graph) {
|
|
1147
|
+
for (const dep of node.dependencies) {
|
|
1148
|
+
lines.push(` "${key}" -> "${dep}";`);
|
|
1149
|
+
}
|
|
1150
|
+
if (node.dependencies.length === 0) {
|
|
1151
|
+
lines.push(` "${key}";`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
lines.push("}");
|
|
1155
|
+
return lines.join(`
|
|
1156
|
+
`);
|
|
1157
|
+
}
|
|
1158
|
+
function createContractGraph() {
|
|
1159
|
+
return new Map;
|
|
1160
|
+
}
|
|
1161
|
+
function addContractNode(graph, key, file, dependencies) {
|
|
1162
|
+
graph.set(key, {
|
|
1163
|
+
key,
|
|
1164
|
+
file,
|
|
1165
|
+
dependencies,
|
|
1166
|
+
dependents: []
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
// src/analysis/deps/parse-imports.ts
|
|
1170
|
+
function parseImportedSpecNames(sourceCode, _fromFilePath) {
|
|
1171
|
+
const imports = [];
|
|
1172
|
+
const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+\.(?:contracts|contract|operation|operations|event|presentation|workflow|data-view|migration|telemetry|experiment|app-config|integration|knowledge)(?:\.[jt]s)?)['"]/g;
|
|
1173
|
+
let match;
|
|
1174
|
+
while ((match = importRegex.exec(sourceCode)) !== null) {
|
|
1175
|
+
const importPath = match[1];
|
|
1176
|
+
if (!importPath)
|
|
1177
|
+
continue;
|
|
1178
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("/"))
|
|
1179
|
+
continue;
|
|
1180
|
+
const pathParts = importPath.split("/");
|
|
1181
|
+
const base = pathParts[pathParts.length - 1] ?? "";
|
|
1182
|
+
const name = base.replace(/\.(ts|js)$/, "").replace(/\.(contracts|contract|operation|operations|event|presentation|workflow|data-view|migration|telemetry|experiment|app-config|integration|knowledge)$/, "");
|
|
1183
|
+
if (name.length > 0) {
|
|
1184
|
+
imports.push(name);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return Array.from(new Set(imports)).sort((a, b) => a.localeCompare(b));
|
|
1188
|
+
}
|
|
1189
|
+
// src/analysis/validate/spec-structure.ts
|
|
1190
|
+
import {
|
|
1191
|
+
Node as Node2,
|
|
1192
|
+
Project as Project2,
|
|
1193
|
+
SyntaxKind as SyntaxKind2
|
|
1194
|
+
} from "ts-morph";
|
|
1195
|
+
var DEFAULT_RULES_CONFIG = {
|
|
1196
|
+
getRule: () => "warn"
|
|
1197
|
+
};
|
|
1198
|
+
function validateSpecStructure(specFile, rulesConfig = DEFAULT_RULES_CONFIG) {
|
|
1199
|
+
const errors = [];
|
|
1200
|
+
const warnings = [];
|
|
1201
|
+
const project = new Project2({ useInMemoryFileSystem: true });
|
|
1202
|
+
const sourceFile = project.createSourceFile(specFile.filePath, specFile.sourceBlock);
|
|
1203
|
+
const hasExport = sourceFile.getExportAssignments().length > 0 || sourceFile.getVariableStatements().some((s) => s.isExported()) || sourceFile.getFunctions().some((f) => f.isExported()) || sourceFile.getClasses().some((c) => c.isExported()) || sourceFile.getExportDeclarations().length > 0;
|
|
1204
|
+
if (!hasExport) {
|
|
1205
|
+
errors.push("No exported spec found");
|
|
1206
|
+
}
|
|
1207
|
+
switch (specFile.specType) {
|
|
1208
|
+
case "operation":
|
|
1209
|
+
validateOperationSpec(sourceFile, errors, warnings, rulesConfig);
|
|
1210
|
+
break;
|
|
1211
|
+
case "event":
|
|
1212
|
+
validateEventSpec(sourceFile, errors, warnings, rulesConfig);
|
|
1213
|
+
break;
|
|
1214
|
+
case "presentation":
|
|
1215
|
+
validatePresentationSpec(sourceFile, errors, warnings);
|
|
1216
|
+
break;
|
|
1217
|
+
case "workflow":
|
|
1218
|
+
validateWorkflowSpec(sourceFile, errors, warnings, rulesConfig);
|
|
1219
|
+
break;
|
|
1220
|
+
case "data-view":
|
|
1221
|
+
validateDataViewSpec(sourceFile, errors, warnings, rulesConfig);
|
|
1222
|
+
break;
|
|
1223
|
+
case "migration":
|
|
1224
|
+
validateMigrationSpec(sourceFile, errors, warnings, rulesConfig);
|
|
1225
|
+
break;
|
|
1226
|
+
case "telemetry":
|
|
1227
|
+
validateTelemetrySpec(sourceFile, errors, warnings, rulesConfig);
|
|
1228
|
+
break;
|
|
1229
|
+
case "app-config":
|
|
1230
|
+
validateAppConfigSpec(sourceFile, errors, warnings, rulesConfig);
|
|
1231
|
+
break;
|
|
1232
|
+
case "experiment":
|
|
1233
|
+
validateExperimentSpec(sourceFile, errors, warnings, rulesConfig);
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
validateCommonFields(sourceFile, specFile.filePath, errors, warnings, rulesConfig);
|
|
1237
|
+
return {
|
|
1238
|
+
valid: errors.length === 0,
|
|
1239
|
+
errors,
|
|
1240
|
+
warnings
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function emitRule(ruleName, specKind, message, errors, warnings, rulesConfig) {
|
|
1244
|
+
const severity = rulesConfig.getRule(ruleName, specKind);
|
|
1245
|
+
if (severity === "off")
|
|
1246
|
+
return;
|
|
1247
|
+
if (severity === "error") {
|
|
1248
|
+
errors.push(message);
|
|
1249
|
+
} else {
|
|
1250
|
+
warnings.push(message);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
function validateOperationSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
1254
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
1255
|
+
const hasDefine = callExpressions.some((call) => {
|
|
1256
|
+
const text = call.getExpression().getText();
|
|
1257
|
+
return text === "defineCommand" || text === "defineQuery";
|
|
1258
|
+
});
|
|
1259
|
+
if (!hasDefine) {
|
|
1260
|
+
errors.push("Missing defineCommand or defineQuery call");
|
|
1261
|
+
}
|
|
1262
|
+
let specObject;
|
|
1263
|
+
for (const call of callExpressions) {
|
|
1264
|
+
const text = call.getExpression().getText();
|
|
1265
|
+
if (text === "defineCommand" || text === "defineQuery") {
|
|
1266
|
+
const args = call.getArguments();
|
|
1267
|
+
if (args.length > 0 && Node2.isObjectLiteralExpression(args[0])) {
|
|
1268
|
+
specObject = args[0];
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (specObject && Node2.isObjectLiteralExpression(specObject)) {
|
|
1274
|
+
if (!specObject.getProperty("meta")) {
|
|
1275
|
+
errors.push("Missing meta section");
|
|
1276
|
+
}
|
|
1277
|
+
if (!specObject.getProperty("io")) {
|
|
1278
|
+
errors.push("Missing io section");
|
|
1279
|
+
}
|
|
1280
|
+
if (!specObject.getProperty("policy")) {
|
|
1281
|
+
errors.push("Missing policy section");
|
|
1282
|
+
}
|
|
1283
|
+
const metaProp = specObject.getProperty("meta");
|
|
1284
|
+
let hasKey = false;
|
|
1285
|
+
let hasVersion = false;
|
|
1286
|
+
if (metaProp && Node2.isPropertyAssignment(metaProp)) {
|
|
1287
|
+
const metaObj = metaProp.getInitializer();
|
|
1288
|
+
if (metaObj && Node2.isObjectLiteralExpression(metaObj)) {
|
|
1289
|
+
if (metaObj.getProperty("key"))
|
|
1290
|
+
hasKey = true;
|
|
1291
|
+
if (metaObj.getProperty("version"))
|
|
1292
|
+
hasVersion = true;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (!hasKey) {
|
|
1296
|
+
if (specObject.getProperty("key"))
|
|
1297
|
+
hasKey = true;
|
|
1298
|
+
}
|
|
1299
|
+
if (!hasKey) {
|
|
1300
|
+
errors.push("Missing or invalid key field");
|
|
1301
|
+
}
|
|
1302
|
+
if (!hasVersion) {
|
|
1303
|
+
if (specObject.getProperty("version"))
|
|
1304
|
+
hasVersion = true;
|
|
1305
|
+
}
|
|
1306
|
+
if (!hasVersion) {
|
|
1307
|
+
errors.push("Missing or invalid version field");
|
|
1308
|
+
}
|
|
1309
|
+
const hasExplicitKind = specObject.getProperty("kind");
|
|
1310
|
+
const callText = callExpressions.find((c) => {
|
|
1311
|
+
const t = c.getExpression().getText();
|
|
1312
|
+
return t === "defineCommand" || t === "defineQuery";
|
|
1313
|
+
})?.getExpression().getText();
|
|
1314
|
+
if (!callText && !hasExplicitKind) {
|
|
1315
|
+
errors.push("Missing kind: use defineCommand(), defineQuery(), or explicit kind field");
|
|
1316
|
+
}
|
|
1317
|
+
if (!specObject.getProperty("acceptance")) {
|
|
1318
|
+
emitRule("require-acceptance", "operation", "No acceptance scenarios defined", errors, warnings, rulesConfig);
|
|
1319
|
+
}
|
|
1320
|
+
if (!specObject.getProperty("examples")) {
|
|
1321
|
+
emitRule("require-examples", "operation", "No examples provided", errors, warnings, rulesConfig);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const fullText = sourceFile.getFullText();
|
|
1325
|
+
if (fullText.includes("TODO")) {
|
|
1326
|
+
emitRule("no-todo", "operation", "Contains TODO items that need completion", errors, warnings, rulesConfig);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
function validateTelemetrySpec(sourceFile, errors, warnings, rulesConfig) {
|
|
1330
|
+
const specObject = getSpecObject(sourceFile, "TelemetrySpec");
|
|
1331
|
+
if (!specObject) {
|
|
1332
|
+
errors.push("Missing TelemetrySpec type annotation");
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if (specObject) {
|
|
1336
|
+
const metaProp = specObject.getProperty("meta");
|
|
1337
|
+
let hasName = false;
|
|
1338
|
+
if (metaProp && Node2.isPropertyAssignment(metaProp)) {
|
|
1339
|
+
const metaObj = metaProp.getInitializer();
|
|
1340
|
+
if (Node2.isObjectLiteralExpression(metaObj)) {
|
|
1341
|
+
if (metaObj.getProperty("name"))
|
|
1342
|
+
hasName = true;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (!hasName) {
|
|
1346
|
+
errors.push("TelemetrySpec.meta is required");
|
|
1347
|
+
}
|
|
1348
|
+
if (!specObject.getProperty("events")) {
|
|
1349
|
+
errors.push("TelemetrySpec must declare events");
|
|
1350
|
+
}
|
|
1351
|
+
const privacyProp = specObject.getProperty("privacy");
|
|
1352
|
+
if (!privacyProp) {
|
|
1353
|
+
emitRule("telemetry-privacy", "telemetry", "No explicit privacy classification found", errors, warnings, rulesConfig);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function validateExperimentSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
1358
|
+
const specObject = getSpecObject(sourceFile, "ExperimentSpec");
|
|
1359
|
+
if (!specObject) {
|
|
1360
|
+
errors.push("Missing ExperimentSpec type annotation");
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
if (!specObject.getProperty("controlVariant")) {
|
|
1364
|
+
errors.push("ExperimentSpec must declare controlVariant");
|
|
1365
|
+
}
|
|
1366
|
+
if (!specObject.getProperty("variants")) {
|
|
1367
|
+
errors.push("ExperimentSpec must declare variants");
|
|
1368
|
+
}
|
|
1369
|
+
if (!specObject.getProperty("allocation")) {
|
|
1370
|
+
emitRule("experiment-allocation", "experiment", "ExperimentSpec missing allocation configuration", errors, warnings, rulesConfig);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function validateAppConfigSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
1374
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
1375
|
+
const defineCall = callExpressions.find((c) => c.getExpression().getText() === "defineAppConfig");
|
|
1376
|
+
let specObject;
|
|
1377
|
+
if (defineCall) {
|
|
1378
|
+
const args = defineCall.getArguments();
|
|
1379
|
+
if (args.length > 0 && Node2.isObjectLiteralExpression(args[0])) {
|
|
1380
|
+
specObject = args[0];
|
|
1381
|
+
}
|
|
1382
|
+
} else {
|
|
1383
|
+
specObject = getSpecObject(sourceFile, "AppBlueprintSpec");
|
|
1384
|
+
}
|
|
1385
|
+
if (!specObject) {
|
|
1386
|
+
errors.push("Missing defineAppConfig call or AppBlueprintSpec type annotation");
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
const metaProp = specObject.getProperty("meta");
|
|
1390
|
+
if (!metaProp) {
|
|
1391
|
+
errors.push("AppBlueprintSpec must define meta");
|
|
1392
|
+
} else if (Node2.isPropertyAssignment(metaProp)) {
|
|
1393
|
+
const metaObj = metaProp.getInitializer();
|
|
1394
|
+
if (Node2.isObjectLiteralExpression(metaObj)) {
|
|
1395
|
+
if (!metaObj.getProperty("appId")) {
|
|
1396
|
+
emitRule("app-config-appid", "app-config", "AppBlueprint meta missing appId assignment", errors, warnings, rulesConfig);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
if (!specObject.getProperty("capabilities")) {
|
|
1401
|
+
emitRule("app-config-capabilities", "app-config", "App blueprint spec does not declare capabilities", errors, warnings, rulesConfig);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function validateEventSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
1405
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
1406
|
+
const defineEventCall = callExpressions.find((c) => c.getExpression().getText() === "defineEvent");
|
|
1407
|
+
if (!defineEventCall) {
|
|
1408
|
+
errors.push("Missing defineEvent call");
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
let specObject;
|
|
1412
|
+
const args = defineEventCall.getArguments();
|
|
1413
|
+
if (args.length > 0 && Node2.isObjectLiteralExpression(args[0])) {
|
|
1414
|
+
specObject = args[0];
|
|
1415
|
+
}
|
|
1416
|
+
if (specObject && Node2.isObjectLiteralExpression(specObject)) {
|
|
1417
|
+
const metaProp = specObject.getProperty("meta");
|
|
1418
|
+
let hasKey = false;
|
|
1419
|
+
let hasVersion = false;
|
|
1420
|
+
if (metaProp && Node2.isPropertyAssignment(metaProp)) {
|
|
1421
|
+
const metaObj = metaProp.getInitializer();
|
|
1422
|
+
if (Node2.isObjectLiteralExpression(metaObj)) {
|
|
1423
|
+
const keyP = metaObj.getProperty("key");
|
|
1424
|
+
if (keyP && Node2.isPropertyAssignment(keyP)) {
|
|
1425
|
+
const init = keyP.getInitializer();
|
|
1426
|
+
if (init && Node2.isStringLiteral(init)) {
|
|
1427
|
+
hasKey = true;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (metaObj.getProperty("version"))
|
|
1431
|
+
hasVersion = true;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
if (!hasKey) {
|
|
1435
|
+
const kp = specObject.getProperty("key");
|
|
1436
|
+
if (kp && Node2.isPropertyAssignment(kp)) {
|
|
1437
|
+
const init = kp.getInitializer();
|
|
1438
|
+
if (init && Node2.isStringLiteral(init)) {
|
|
1439
|
+
hasKey = true;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (!hasVersion && specObject.getProperty("version"))
|
|
1444
|
+
hasVersion = true;
|
|
1445
|
+
if (!hasKey) {
|
|
1446
|
+
errors.push("Missing or invalid key field");
|
|
1447
|
+
}
|
|
1448
|
+
if (!hasVersion) {
|
|
1449
|
+
errors.push("Missing or invalid version field");
|
|
1450
|
+
}
|
|
1451
|
+
if (!specObject.getProperty("payload")) {
|
|
1452
|
+
errors.push("Missing payload field");
|
|
1453
|
+
}
|
|
1454
|
+
let name = "";
|
|
1455
|
+
const getName = (obj) => {
|
|
1456
|
+
const init = obj.getInitializer();
|
|
1457
|
+
if (init && Node2.isStringLiteral(init)) {
|
|
1458
|
+
return init.getLiteralText();
|
|
1459
|
+
}
|
|
1460
|
+
return "";
|
|
1461
|
+
};
|
|
1462
|
+
if (metaProp && Node2.isPropertyAssignment(metaProp)) {
|
|
1463
|
+
const metaObj = metaProp.getInitializer();
|
|
1464
|
+
if (Node2.isObjectLiteralExpression(metaObj)) {
|
|
1465
|
+
const nameP = metaObj.getProperty("name");
|
|
1466
|
+
if (nameP && Node2.isPropertyAssignment(nameP)) {
|
|
1467
|
+
name = getName(nameP);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (!name) {
|
|
1472
|
+
const nameP = specObject.getProperty("name");
|
|
1473
|
+
if (nameP && Node2.isPropertyAssignment(nameP)) {
|
|
1474
|
+
name = getName(nameP);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
if (name) {
|
|
1478
|
+
const eventName = name.split(".").pop() ?? "";
|
|
1479
|
+
if (!eventName.match(/(ed|created|updated|deleted|completed)$/i)) {
|
|
1480
|
+
emitRule("event-past-tense", "event", 'Event name should use past tense (e.g., "created", "updated")', errors, warnings, rulesConfig);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
function validatePresentationSpec(sourceFile, errors, _warnings) {
|
|
1486
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
1487
|
+
const defineCall = callExpressions.find((c) => c.getExpression().getText() === "definePresentation");
|
|
1488
|
+
let specObject;
|
|
1489
|
+
if (defineCall) {
|
|
1490
|
+
const args = defineCall.getArguments();
|
|
1491
|
+
if (args.length > 0 && Node2.isObjectLiteralExpression(args[0])) {
|
|
1492
|
+
specObject = args[0];
|
|
1493
|
+
}
|
|
1494
|
+
} else {
|
|
1495
|
+
specObject = getSpecObject(sourceFile, "PresentationSpec");
|
|
1496
|
+
}
|
|
1497
|
+
if (!specObject) {
|
|
1498
|
+
errors.push("Missing definePresentation call or PresentationSpec type annotation");
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (!specObject.getProperty("meta")) {
|
|
1502
|
+
errors.push("Missing meta section");
|
|
1503
|
+
}
|
|
1504
|
+
const sourceProp = specObject.getProperty("source");
|
|
1505
|
+
if (!sourceProp) {
|
|
1506
|
+
errors.push("Missing source section");
|
|
1507
|
+
} else if (Node2.isPropertyAssignment(sourceProp)) {
|
|
1508
|
+
const sourceObj = sourceProp.getInitializer();
|
|
1509
|
+
if (Node2.isObjectLiteralExpression(sourceObj)) {
|
|
1510
|
+
const typeProp = sourceObj.getProperty("type");
|
|
1511
|
+
if (!typeProp) {
|
|
1512
|
+
errors.push("Missing or invalid source.type field");
|
|
1513
|
+
} else if (Node2.isPropertyAssignment(typeProp)) {
|
|
1514
|
+
const init = typeProp.getInitializer();
|
|
1515
|
+
if (init && Node2.isStringLiteral(init)) {
|
|
1516
|
+
const val = init.getLiteralText();
|
|
1517
|
+
if (val !== "component" && val !== "blocknotejs") {
|
|
1518
|
+
errors.push("Missing or invalid source.type field");
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (!specObject.getProperty("targets")) {
|
|
1525
|
+
errors.push("Missing targets section");
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function validateWorkflowSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
1529
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
1530
|
+
const defineCall = callExpressions.find((c) => c.getExpression().getText() === "defineWorkflow");
|
|
1531
|
+
let specObject;
|
|
1532
|
+
if (defineCall) {
|
|
1533
|
+
const args = defineCall.getArguments();
|
|
1534
|
+
if (args.length > 0 && Node2.isObjectLiteralExpression(args[0])) {
|
|
1535
|
+
specObject = args[0];
|
|
1536
|
+
}
|
|
1537
|
+
} else {
|
|
1538
|
+
specObject = getSpecObject(sourceFile, "WorkflowSpec");
|
|
1539
|
+
}
|
|
1540
|
+
if (!specObject) {
|
|
1541
|
+
errors.push("Missing defineWorkflow call or WorkflowSpec type annotation");
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (!specObject.getProperty("definition")) {
|
|
1545
|
+
errors.push("Missing definition section");
|
|
1546
|
+
} else {
|
|
1547
|
+
const defProp = specObject.getProperty("definition");
|
|
1548
|
+
if (defProp && Node2.isPropertyAssignment(defProp)) {
|
|
1549
|
+
const defObj = defProp.getInitializer();
|
|
1550
|
+
if (Node2.isObjectLiteralExpression(defObj)) {
|
|
1551
|
+
if (!defObj.getProperty("steps")) {
|
|
1552
|
+
errors.push("Workflow must declare steps");
|
|
1553
|
+
}
|
|
1554
|
+
if (!defObj.getProperty("transitions")) {
|
|
1555
|
+
emitRule("workflow-transitions", "workflow", "No transitions declared; workflow will complete after first step.", errors, warnings, rulesConfig);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
let titleFound = false;
|
|
1561
|
+
let domainFound = false;
|
|
1562
|
+
const metaProp = specObject.getProperty("meta");
|
|
1563
|
+
if (metaProp && Node2.isPropertyAssignment(metaProp)) {
|
|
1564
|
+
const metaObj = metaProp.getInitializer();
|
|
1565
|
+
if (Node2.isObjectLiteralExpression(metaObj)) {
|
|
1566
|
+
if (metaObj.getProperty("title"))
|
|
1567
|
+
titleFound = true;
|
|
1568
|
+
if (metaObj.getProperty("domain"))
|
|
1569
|
+
domainFound = true;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
if (!titleFound && specObject.getProperty("title"))
|
|
1573
|
+
titleFound = true;
|
|
1574
|
+
if (!domainFound && specObject.getProperty("domain"))
|
|
1575
|
+
domainFound = true;
|
|
1576
|
+
if (!titleFound) {
|
|
1577
|
+
warnings.push("Missing workflow title");
|
|
1578
|
+
}
|
|
1579
|
+
if (!domainFound) {
|
|
1580
|
+
warnings.push("Missing domain field");
|
|
1581
|
+
}
|
|
1582
|
+
if (sourceFile.getFullText().includes("TODO")) {
|
|
1583
|
+
emitRule("no-todo", "workflow", "Contains TODO items that need completion", errors, warnings, rulesConfig);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
function validateMigrationSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
1587
|
+
const specObject = getSpecObject(sourceFile, "MigrationSpec");
|
|
1588
|
+
if (!specObject) {
|
|
1589
|
+
errors.push("Missing MigrationSpec type annotation");
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const planProp = specObject.getProperty("plan");
|
|
1593
|
+
if (!planProp) {
|
|
1594
|
+
errors.push("Missing plan section");
|
|
1595
|
+
} else if (Node2.isPropertyAssignment(planProp)) {
|
|
1596
|
+
const planObj = planProp.getInitializer();
|
|
1597
|
+
if (Node2.isObjectLiteralExpression(planObj)) {
|
|
1598
|
+
if (!planObj.getProperty("up")) {
|
|
1599
|
+
errors.push("Migration must define at least one up step");
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
let nameFound = false;
|
|
1604
|
+
let versionFound = false;
|
|
1605
|
+
const metaProp = specObject.getProperty("meta");
|
|
1606
|
+
if (metaProp && Node2.isPropertyAssignment(metaProp)) {
|
|
1607
|
+
const metaObj = metaProp.getInitializer();
|
|
1608
|
+
if (Node2.isObjectLiteralExpression(metaObj)) {
|
|
1609
|
+
if (metaObj.getProperty("name"))
|
|
1610
|
+
nameFound = true;
|
|
1611
|
+
if (metaObj.getProperty("version"))
|
|
1612
|
+
versionFound = true;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
if (!nameFound && specObject.getProperty("name"))
|
|
1616
|
+
nameFound = true;
|
|
1617
|
+
if (!versionFound && specObject.getProperty("version"))
|
|
1618
|
+
versionFound = true;
|
|
1619
|
+
if (!nameFound) {
|
|
1620
|
+
errors.push("Missing or invalid migration name");
|
|
1621
|
+
}
|
|
1622
|
+
if (!versionFound) {
|
|
1623
|
+
errors.push("Missing or invalid migration version");
|
|
1624
|
+
}
|
|
1625
|
+
if (sourceFile.getFullText().includes("TODO")) {
|
|
1626
|
+
emitRule("no-todo", "migration", "Contains TODO items that need completion", errors, warnings, rulesConfig);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
function validateCommonFields(sourceFile, fileName, errors, warnings, rulesConfig) {
|
|
1630
|
+
const code = sourceFile.getFullText();
|
|
1631
|
+
const isInternalLib = fileName.includes("/libs/contracts/") || fileName.includes("/libs/contracts-transformers/") || fileName.includes("/libs/schema/");
|
|
1632
|
+
if (code.includes("SchemaModel") && !isInternalLib) {
|
|
1633
|
+
const imports = sourceFile.getImportDeclarations();
|
|
1634
|
+
const hasSchemaImport = imports.some((i) => i.getModuleSpecifierValue().includes("@contractspec/lib.schema"));
|
|
1635
|
+
if (!hasSchemaImport) {
|
|
1636
|
+
errors.push("Missing import for SchemaModel from @contractspec/lib.schema");
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
const usesSpecTypes = code.includes("OperationSpec") || code.includes("PresentationSpec") || code.includes("EventSpec") || code.includes("FeatureSpec") || code.includes("WorkflowSpec") || code.includes("DataViewSpec") || code.includes("MigrationSpec") || code.includes("TelemetrySpec") || code.includes("ExperimentSpec") || code.includes("AppBlueprintSpec") || code.includes("defineCommand") || code.includes("defineQuery") || code.includes("defineEvent") || code.includes("definePresentation") || code.includes("defineWorkflow") || code.includes("defineDataView") || code.includes("defineAppConfig") || code.includes("defineFeature") || code.includes("defineExperiment") || code.includes("defineTelemetry") || code.includes("defineMigration");
|
|
1640
|
+
if (usesSpecTypes && !isInternalLib) {
|
|
1641
|
+
const imports = sourceFile.getImportDeclarations();
|
|
1642
|
+
const hasContractsImport = imports.some((i) => i.getModuleSpecifierValue().includes("@contractspec/lib.contracts"));
|
|
1643
|
+
if (!hasContractsImport) {
|
|
1644
|
+
errors.push("Missing import from @contractspec/lib.contracts");
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
const specObject = findMainExportedObject(sourceFile);
|
|
1648
|
+
if (specObject && Node2.isObjectLiteralExpression(specObject)) {
|
|
1649
|
+
const ownersProp = specObject.getProperty("owners");
|
|
1650
|
+
let ownersArr = undefined;
|
|
1651
|
+
const checkOwners = (prop) => {
|
|
1652
|
+
if (Node2.isPropertyAssignment(prop)) {
|
|
1653
|
+
const init = prop.getInitializer();
|
|
1654
|
+
if (init && Node2.isArrayLiteralExpression(init)) {
|
|
1655
|
+
return init;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return;
|
|
1659
|
+
};
|
|
1660
|
+
if (ownersProp)
|
|
1661
|
+
ownersArr = checkOwners(ownersProp);
|
|
1662
|
+
if (!ownersArr) {
|
|
1663
|
+
const metaProp = specObject.getProperty("meta");
|
|
1664
|
+
if (metaProp && Node2.isPropertyAssignment(metaProp)) {
|
|
1665
|
+
const metaObj = metaProp.getInitializer();
|
|
1666
|
+
if (metaObj && Node2.isObjectLiteralExpression(metaObj)) {
|
|
1667
|
+
const o = metaObj.getProperty("owners");
|
|
1668
|
+
if (o)
|
|
1669
|
+
ownersArr = checkOwners(o);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
if (ownersArr) {
|
|
1674
|
+
for (const elem of ownersArr.getElements()) {
|
|
1675
|
+
if (Node2.isStringLiteral(elem)) {
|
|
1676
|
+
const val = elem.getLiteralText();
|
|
1677
|
+
if (!val.includes("@") && !val.includes("Enum") && !val.match(/[A-Z][a-zA-Z0-9_]+/)) {
|
|
1678
|
+
emitRule("require-owners-format", "operation", "Owners should start with @ or use an Enum/Constant", errors, warnings, rulesConfig);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
let stabilityFound = false;
|
|
1684
|
+
const stabilityProp = specObject.getProperty("stability");
|
|
1685
|
+
if (stabilityProp)
|
|
1686
|
+
stabilityFound = true;
|
|
1687
|
+
if (!stabilityFound) {
|
|
1688
|
+
const metaProp = specObject.getProperty("meta");
|
|
1689
|
+
if (metaProp && Node2.isPropertyAssignment(metaProp)) {
|
|
1690
|
+
const metaObj = metaProp.getInitializer();
|
|
1691
|
+
if (Node2.isObjectLiteralExpression(metaObj)) {
|
|
1692
|
+
if (metaObj.getProperty("stability"))
|
|
1693
|
+
stabilityFound = true;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
if (!stabilityFound) {
|
|
1698
|
+
emitRule("require-stability", "operation", "Missing or invalid stability field", errors, warnings, rulesConfig);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
function validateDataViewSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
1703
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
1704
|
+
const defineCall = callExpressions.find((c) => c.getExpression().getText() === "defineDataView");
|
|
1705
|
+
let specObject;
|
|
1706
|
+
if (defineCall) {
|
|
1707
|
+
const args = defineCall.getArguments();
|
|
1708
|
+
if (args.length > 0 && Node2.isObjectLiteralExpression(args[0])) {
|
|
1709
|
+
specObject = args[0];
|
|
1710
|
+
}
|
|
1711
|
+
} else {
|
|
1712
|
+
specObject = getSpecObject(sourceFile, "DataViewSpec");
|
|
1713
|
+
}
|
|
1714
|
+
if (!specObject) {
|
|
1715
|
+
errors.push("Missing defineDataView call or DataViewSpec type annotation");
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
if (!specObject.getProperty("meta")) {
|
|
1719
|
+
errors.push("Missing meta section");
|
|
1720
|
+
}
|
|
1721
|
+
if (!specObject.getProperty("source")) {
|
|
1722
|
+
errors.push("Missing source section");
|
|
1723
|
+
}
|
|
1724
|
+
const viewProp = specObject.getProperty("view");
|
|
1725
|
+
if (!viewProp) {
|
|
1726
|
+
errors.push("Missing view section");
|
|
1727
|
+
errors.push("Missing or invalid view.kind (list/table/detail/grid)");
|
|
1728
|
+
} else if (Node2.isPropertyAssignment(viewProp)) {
|
|
1729
|
+
const viewObj = viewProp.getInitializer();
|
|
1730
|
+
if (Node2.isObjectLiteralExpression(viewObj)) {
|
|
1731
|
+
const kindProp = viewObj.getProperty("kind");
|
|
1732
|
+
if (!kindProp) {
|
|
1733
|
+
errors.push("Missing or invalid view.kind (list/table/detail/grid)");
|
|
1734
|
+
} else if (Node2.isPropertyAssignment(kindProp)) {
|
|
1735
|
+
const init = kindProp.getInitializer();
|
|
1736
|
+
if (init && Node2.isStringLiteral(init)) {
|
|
1737
|
+
const val = init.getLiteralText();
|
|
1738
|
+
if (!["list", "table", "detail", "grid"].includes(val)) {
|
|
1739
|
+
errors.push("Missing or invalid view.kind (list/table/detail/grid)");
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
let fieldsFound = false;
|
|
1746
|
+
if (viewProp && Node2.isPropertyAssignment(viewProp)) {
|
|
1747
|
+
const viewObj = viewProp.getInitializer();
|
|
1748
|
+
if (Node2.isObjectLiteralExpression(viewObj)) {
|
|
1749
|
+
if (viewObj.getProperty("fields"))
|
|
1750
|
+
fieldsFound = true;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
if (!fieldsFound && specObject.getProperty("fields"))
|
|
1754
|
+
fieldsFound = true;
|
|
1755
|
+
if (!fieldsFound) {
|
|
1756
|
+
emitRule("data-view-fields", "data-view", "No fields defined for data view", errors, warnings, rulesConfig);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
function getSpecObject(sourceFile, typeName) {
|
|
1760
|
+
const varStmts = sourceFile.getVariableStatements();
|
|
1761
|
+
for (const stmt of varStmts) {
|
|
1762
|
+
if (stmt.isExported()) {
|
|
1763
|
+
for (const decl of stmt.getDeclarations()) {
|
|
1764
|
+
const typeNode = decl.getTypeNode();
|
|
1765
|
+
if (typeNode && typeNode.getText().includes(typeName)) {
|
|
1766
|
+
const init = decl.getInitializer();
|
|
1767
|
+
if (init && Node2.isObjectLiteralExpression(init)) {
|
|
1768
|
+
return init;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
const exportAssign = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
1775
|
+
if (exportAssign) {
|
|
1776
|
+
const expr = exportAssign.getExpression();
|
|
1777
|
+
if (Node2.isAsExpression(expr)) {
|
|
1778
|
+
if (expr.getTypeNode()?.getText().includes(typeName)) {
|
|
1779
|
+
const inner = expr.getExpression();
|
|
1780
|
+
if (Node2.isObjectLiteralExpression(inner))
|
|
1781
|
+
return inner;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (Node2.isObjectLiteralExpression(expr)) {
|
|
1785
|
+
return expr;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
function findMainExportedObject(sourceFile) {
|
|
1791
|
+
const varStmts = sourceFile.getVariableStatements();
|
|
1792
|
+
for (const stmt of varStmts) {
|
|
1793
|
+
if (stmt.isExported()) {
|
|
1794
|
+
for (const decl of stmt.getDeclarations()) {
|
|
1795
|
+
const init = decl.getInitializer();
|
|
1796
|
+
if (init) {
|
|
1797
|
+
if (Node2.isObjectLiteralExpression(init))
|
|
1798
|
+
return init;
|
|
1799
|
+
if (Node2.isCallExpression(init)) {
|
|
1800
|
+
const args = init.getArguments();
|
|
1801
|
+
if (args.length > 0 && Node2.isObjectLiteralExpression(args[0]))
|
|
1802
|
+
return args[0];
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
const exportAssign = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
1809
|
+
if (exportAssign) {
|
|
1810
|
+
const expr = exportAssign.getExpression();
|
|
1811
|
+
if (Node2.isObjectLiteralExpression(expr))
|
|
1812
|
+
return expr;
|
|
1813
|
+
if (Node2.isAsExpression(expr) && Node2.isObjectLiteralExpression(expr.getExpression()))
|
|
1814
|
+
return expr.getExpression();
|
|
1815
|
+
if (Node2.isCallExpression(expr)) {
|
|
1816
|
+
const args = expr.getArguments();
|
|
1817
|
+
if (args.length > 0 && Node2.isObjectLiteralExpression(args[0]))
|
|
1818
|
+
return args[0];
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
// src/analysis/snapshot/normalizer.ts
|
|
1824
|
+
import { createHash } from "crypto";
|
|
1825
|
+
import { compareVersions } from "compare-versions";
|
|
1826
|
+
function normalizeValue(value) {
|
|
1827
|
+
if (value === null || value === undefined) {
|
|
1828
|
+
return value === null ? null : undefined;
|
|
1829
|
+
}
|
|
1830
|
+
if (Array.isArray(value)) {
|
|
1831
|
+
return value.map(normalizeValue);
|
|
1832
|
+
}
|
|
1833
|
+
if (typeof value === "object") {
|
|
1834
|
+
const obj = value;
|
|
1835
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
1836
|
+
const normalized = {};
|
|
1837
|
+
for (const key of sortedKeys) {
|
|
1838
|
+
const normalizedValue = normalizeValue(obj[key]);
|
|
1839
|
+
if (normalizedValue !== undefined) {
|
|
1840
|
+
normalized[key] = normalizedValue;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
return normalized;
|
|
1844
|
+
}
|
|
1845
|
+
return value;
|
|
1846
|
+
}
|
|
1847
|
+
function toCanonicalJson(value) {
|
|
1848
|
+
return JSON.stringify(normalizeValue(value), null, 0);
|
|
1849
|
+
}
|
|
1850
|
+
function computeHash(value) {
|
|
1851
|
+
const canonical = toCanonicalJson(value);
|
|
1852
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
1853
|
+
}
|
|
1854
|
+
function sortSpecs(specs) {
|
|
1855
|
+
return [...specs].sort((a, b) => {
|
|
1856
|
+
const keyCompare = a.key.localeCompare(b.key);
|
|
1857
|
+
if (keyCompare !== 0)
|
|
1858
|
+
return keyCompare;
|
|
1859
|
+
return compareVersions(a.version, b.version);
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
function sortFields(fields) {
|
|
1863
|
+
const sorted = {};
|
|
1864
|
+
const keys = Object.keys(fields).sort();
|
|
1865
|
+
for (const key of keys) {
|
|
1866
|
+
sorted[key] = fields[key];
|
|
1867
|
+
}
|
|
1868
|
+
return sorted;
|
|
1869
|
+
}
|
|
1870
|
+
// src/analysis/snapshot/snapshot.ts
|
|
1871
|
+
function generateSnapshot(specs, options = {}) {
|
|
1872
|
+
const snapshots = [];
|
|
1873
|
+
for (const { path, content } of specs) {
|
|
1874
|
+
const scanned = scanSpecSource(content, path);
|
|
1875
|
+
if (options.types && !options.types.includes(scanned.specType)) {
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
if (scanned.specType === "operation" && scanned.key && scanned.version !== undefined) {
|
|
1879
|
+
const opSnapshot = createOperationSnapshot(scanned, content);
|
|
1880
|
+
if (opSnapshot) {
|
|
1881
|
+
snapshots.push(opSnapshot);
|
|
1882
|
+
}
|
|
1883
|
+
} else if (scanned.specType === "event" && scanned.key && scanned.version !== undefined) {
|
|
1884
|
+
const eventSnapshot = createEventSnapshot(scanned, content);
|
|
1885
|
+
if (eventSnapshot) {
|
|
1886
|
+
snapshots.push(eventSnapshot);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
const sortedSpecs = sortSpecs(snapshots);
|
|
1891
|
+
const hash = computeHash({ specs: sortedSpecs });
|
|
1892
|
+
return {
|
|
1893
|
+
version: "1.0.0",
|
|
1894
|
+
generatedAt: new Date().toISOString(),
|
|
1895
|
+
specs: sortedSpecs,
|
|
1896
|
+
hash
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
function createOperationSnapshot(scanned, content) {
|
|
1900
|
+
if (!scanned.key || scanned.version === undefined) {
|
|
1901
|
+
return null;
|
|
1902
|
+
}
|
|
1903
|
+
const io = extractIoFromSource(content);
|
|
1904
|
+
const http = extractHttpBinding(content);
|
|
1905
|
+
return {
|
|
1906
|
+
type: "operation",
|
|
1907
|
+
key: scanned.key,
|
|
1908
|
+
version: scanned.version,
|
|
1909
|
+
kind: scanned.kind === "command" || scanned.kind === "query" ? scanned.kind : "command",
|
|
1910
|
+
stability: scanned.stability ?? "experimental",
|
|
1911
|
+
http: http ?? undefined,
|
|
1912
|
+
io,
|
|
1913
|
+
authLevel: extractAuthLevel(content),
|
|
1914
|
+
emittedEvents: scanned.emittedEvents
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
function createEventSnapshot(scanned, content) {
|
|
1918
|
+
if (!scanned.key || scanned.version === undefined) {
|
|
1919
|
+
return null;
|
|
1920
|
+
}
|
|
1921
|
+
const payload = extractPayloadFromSource(content);
|
|
1922
|
+
return {
|
|
1923
|
+
type: "event",
|
|
1924
|
+
key: scanned.key,
|
|
1925
|
+
version: scanned.version,
|
|
1926
|
+
stability: scanned.stability ?? "experimental",
|
|
1927
|
+
payload
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
function extractIoFromSource(content) {
|
|
1931
|
+
const input = extractSchemaFields(content, "input");
|
|
1932
|
+
const output = extractSchemaFields(content, "output");
|
|
1933
|
+
return {
|
|
1934
|
+
input: sortFields(input),
|
|
1935
|
+
output: sortFields(output)
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
function extractPayloadFromSource(content) {
|
|
1939
|
+
const fields = extractSchemaFields(content, "payload");
|
|
1940
|
+
return sortFields(fields);
|
|
1941
|
+
}
|
|
1942
|
+
function extractSchemaFields(content, section) {
|
|
1943
|
+
const fields = {};
|
|
1944
|
+
const sectionPattern = new RegExp(`${section}\\s*:\\s*z\\.object\\(\\{([^}]+)\\}`, "s");
|
|
1945
|
+
const sectionMatch = content.match(sectionPattern);
|
|
1946
|
+
if (!sectionMatch?.[1]) {
|
|
1947
|
+
return fields;
|
|
1948
|
+
}
|
|
1949
|
+
const sectionContent = sectionMatch[1];
|
|
1950
|
+
const fieldPattern = /(\w+)\s*:\s*z\.(\w+)\((.*?)\)/g;
|
|
1951
|
+
let match;
|
|
1952
|
+
while ((match = fieldPattern.exec(sectionContent)) !== null) {
|
|
1953
|
+
const [, fieldName, zodType] = match;
|
|
1954
|
+
if (!fieldName || !zodType)
|
|
1955
|
+
continue;
|
|
1956
|
+
const isOptional = sectionContent.includes(`${fieldName}:`) && sectionContent.slice(sectionContent.indexOf(`${fieldName}:`)).includes(".optional()");
|
|
1957
|
+
const isNullable = sectionContent.includes(`${fieldName}:`) && sectionContent.slice(sectionContent.indexOf(`${fieldName}:`)).includes(".nullable()");
|
|
1958
|
+
fields[fieldName] = {
|
|
1959
|
+
name: fieldName,
|
|
1960
|
+
type: mapZodTypeToFieldType(zodType),
|
|
1961
|
+
required: !isOptional,
|
|
1962
|
+
nullable: isNullable
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
return fields;
|
|
1966
|
+
}
|
|
1967
|
+
function mapZodTypeToFieldType(zodType) {
|
|
1968
|
+
const mapping = {
|
|
1969
|
+
string: "string",
|
|
1970
|
+
number: "number",
|
|
1971
|
+
boolean: "boolean",
|
|
1972
|
+
object: "object",
|
|
1973
|
+
array: "array",
|
|
1974
|
+
enum: "enum",
|
|
1975
|
+
union: "union",
|
|
1976
|
+
literal: "literal",
|
|
1977
|
+
date: "date",
|
|
1978
|
+
coerce: "unknown"
|
|
1979
|
+
};
|
|
1980
|
+
return mapping[zodType] ?? "unknown";
|
|
1981
|
+
}
|
|
1982
|
+
function extractHttpBinding(content) {
|
|
1983
|
+
const methodMatch = content.match(/method\s*:\s*['"](\w+)['"]/);
|
|
1984
|
+
const pathMatch = content.match(/path\s*:\s*['"]([^'"]+)['"]/);
|
|
1985
|
+
if (methodMatch?.[1] && pathMatch?.[1]) {
|
|
1986
|
+
const method = methodMatch[1].toUpperCase();
|
|
1987
|
+
if (["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
|
1988
|
+
return {
|
|
1989
|
+
method,
|
|
1990
|
+
path: pathMatch[1]
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
return null;
|
|
1995
|
+
}
|
|
1996
|
+
function extractAuthLevel(content) {
|
|
1997
|
+
const authMatch = content.match(/auth\s*:\s*['"](\w+)['"]/);
|
|
1998
|
+
return authMatch?.[1];
|
|
1999
|
+
}
|
|
2000
|
+
// src/analysis/impact/rules.ts
|
|
2001
|
+
var BREAKING_RULES = [
|
|
2002
|
+
{
|
|
2003
|
+
id: "endpoint-removed",
|
|
2004
|
+
description: "Endpoint/operation was removed",
|
|
2005
|
+
severity: "breaking",
|
|
2006
|
+
matches: (delta) => delta.path.includes("spec.") && delta.type === "removed"
|
|
2007
|
+
},
|
|
2008
|
+
{
|
|
2009
|
+
id: "field-removed",
|
|
2010
|
+
description: "Field was removed from response",
|
|
2011
|
+
severity: "breaking",
|
|
2012
|
+
matches: (delta) => delta.path.includes(".output.") && delta.description.includes("removed")
|
|
2013
|
+
},
|
|
2014
|
+
{
|
|
2015
|
+
id: "field-type-changed",
|
|
2016
|
+
description: "Field type was changed",
|
|
2017
|
+
severity: "breaking",
|
|
2018
|
+
matches: (delta) => delta.path.includes(".type") && delta.description.includes("type changed")
|
|
2019
|
+
},
|
|
2020
|
+
{
|
|
2021
|
+
id: "field-made-required",
|
|
2022
|
+
description: "Optional field became required",
|
|
2023
|
+
severity: "breaking",
|
|
2024
|
+
matches: (delta) => delta.path.includes(".required") && delta.description.includes("optional to required")
|
|
2025
|
+
},
|
|
2026
|
+
{
|
|
2027
|
+
id: "enum-value-removed",
|
|
2028
|
+
description: "Enum value was removed",
|
|
2029
|
+
severity: "breaking",
|
|
2030
|
+
matches: (delta) => delta.path.includes(".enumValues") && delta.description.includes("removed")
|
|
2031
|
+
},
|
|
2032
|
+
{
|
|
2033
|
+
id: "nullable-removed",
|
|
2034
|
+
description: "Field is no longer nullable",
|
|
2035
|
+
severity: "breaking",
|
|
2036
|
+
matches: (delta) => delta.path.includes(".nullable") && delta.description.includes("no longer nullable")
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
id: "method-changed",
|
|
2040
|
+
description: "HTTP method was changed",
|
|
2041
|
+
severity: "breaking",
|
|
2042
|
+
matches: (delta) => delta.path.includes(".http.method") || delta.path.includes(".method")
|
|
2043
|
+
},
|
|
2044
|
+
{
|
|
2045
|
+
id: "path-changed",
|
|
2046
|
+
description: "HTTP path was changed",
|
|
2047
|
+
severity: "breaking",
|
|
2048
|
+
matches: (delta) => delta.path.includes(".http.path") || delta.path.includes(".path")
|
|
2049
|
+
},
|
|
2050
|
+
{
|
|
2051
|
+
id: "required-field-added-to-input",
|
|
2052
|
+
description: "Required field was added to input",
|
|
2053
|
+
severity: "breaking",
|
|
2054
|
+
matches: (delta) => delta.path.includes(".input.") && delta.description.includes("Required field")
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
id: "event-payload-field-removed",
|
|
2058
|
+
description: "Event payload field was removed",
|
|
2059
|
+
severity: "breaking",
|
|
2060
|
+
matches: (delta) => delta.path.includes(".payload.") && delta.description.includes("removed")
|
|
2061
|
+
}
|
|
2062
|
+
];
|
|
2063
|
+
var NON_BREAKING_RULES = [
|
|
2064
|
+
{
|
|
2065
|
+
id: "optional-field-added",
|
|
2066
|
+
description: "Optional field was added",
|
|
2067
|
+
severity: "non_breaking",
|
|
2068
|
+
matches: (delta) => delta.description.includes("Optional field") && delta.description.includes("added")
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
id: "endpoint-added",
|
|
2072
|
+
description: "New endpoint/operation was added",
|
|
2073
|
+
severity: "non_breaking",
|
|
2074
|
+
matches: (delta) => delta.path.includes("spec.") && delta.type === "added"
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
id: "enum-value-added",
|
|
2078
|
+
description: "Enum value was added",
|
|
2079
|
+
severity: "non_breaking",
|
|
2080
|
+
matches: (delta) => delta.path.includes(".enumValues") && delta.description.includes("added")
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
id: "field-made-optional",
|
|
2084
|
+
description: "Required field became optional",
|
|
2085
|
+
severity: "non_breaking",
|
|
2086
|
+
matches: (delta) => delta.path.includes(".required") && delta.description.includes("required to optional")
|
|
2087
|
+
},
|
|
2088
|
+
{
|
|
2089
|
+
id: "nullable-added",
|
|
2090
|
+
description: "Field is now nullable",
|
|
2091
|
+
severity: "non_breaking",
|
|
2092
|
+
matches: (delta) => delta.path.includes(".nullable") && delta.description.includes("now nullable")
|
|
2093
|
+
}
|
|
2094
|
+
];
|
|
2095
|
+
var INFO_RULES = [
|
|
2096
|
+
{
|
|
2097
|
+
id: "stability-changed",
|
|
2098
|
+
description: "Stability level was changed",
|
|
2099
|
+
severity: "info",
|
|
2100
|
+
matches: (delta) => delta.path.includes(".stability")
|
|
2101
|
+
},
|
|
2102
|
+
{
|
|
2103
|
+
id: "description-changed",
|
|
2104
|
+
description: "Description was changed",
|
|
2105
|
+
severity: "info",
|
|
2106
|
+
matches: (delta) => delta.path.includes(".description")
|
|
2107
|
+
},
|
|
2108
|
+
{
|
|
2109
|
+
id: "owners-changed",
|
|
2110
|
+
description: "Owners were changed",
|
|
2111
|
+
severity: "info",
|
|
2112
|
+
matches: (delta) => delta.path.includes(".owners")
|
|
2113
|
+
},
|
|
2114
|
+
{
|
|
2115
|
+
id: "tags-changed",
|
|
2116
|
+
description: "Tags were changed",
|
|
2117
|
+
severity: "info",
|
|
2118
|
+
matches: (delta) => delta.path.includes(".tags")
|
|
2119
|
+
}
|
|
2120
|
+
];
|
|
2121
|
+
var DEFAULT_RULES = [
|
|
2122
|
+
...BREAKING_RULES,
|
|
2123
|
+
...NON_BREAKING_RULES,
|
|
2124
|
+
...INFO_RULES
|
|
2125
|
+
];
|
|
2126
|
+
function getRulesBySeverity(severity) {
|
|
2127
|
+
return DEFAULT_RULES.filter((rule) => rule.severity === severity);
|
|
2128
|
+
}
|
|
2129
|
+
function findMatchingRule(delta, rules = DEFAULT_RULES) {
|
|
2130
|
+
return rules.find((rule) => rule.matches(delta));
|
|
2131
|
+
}
|
|
2132
|
+
// src/analysis/impact/classifier.ts
|
|
2133
|
+
function classifyImpact(baseSpecs, headSpecs, diffs, options = {}) {
|
|
2134
|
+
const rules = options.customRules ?? DEFAULT_RULES;
|
|
2135
|
+
const deltas = [];
|
|
2136
|
+
const baseMap = new Map(baseSpecs.map((s) => [`${s.key}@${s.version}`, s]));
|
|
2137
|
+
const headMap = new Map(headSpecs.map((s) => [`${s.key}@${s.version}`, s]));
|
|
2138
|
+
const addedSpecs = [];
|
|
2139
|
+
for (const spec of headSpecs) {
|
|
2140
|
+
const lookupKey = `${spec.key}@${spec.version}`;
|
|
2141
|
+
if (!baseMap.has(lookupKey)) {
|
|
2142
|
+
addedSpecs.push({
|
|
2143
|
+
key: spec.key,
|
|
2144
|
+
version: spec.version,
|
|
2145
|
+
type: spec.type
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
const removedSpecs = [];
|
|
2150
|
+
for (const spec of baseSpecs) {
|
|
2151
|
+
const lookupKey = `${spec.key}@${spec.version}`;
|
|
2152
|
+
if (!headMap.has(lookupKey)) {
|
|
2153
|
+
removedSpecs.push({
|
|
2154
|
+
key: spec.key,
|
|
2155
|
+
version: spec.version,
|
|
2156
|
+
type: spec.type
|
|
2157
|
+
});
|
|
2158
|
+
deltas.push({
|
|
2159
|
+
specKey: spec.key,
|
|
2160
|
+
specVersion: spec.version,
|
|
2161
|
+
specType: spec.type,
|
|
2162
|
+
path: `spec.${spec.key}`,
|
|
2163
|
+
severity: "breaking",
|
|
2164
|
+
rule: "endpoint-removed",
|
|
2165
|
+
description: `${spec.type === "operation" ? "Operation" : "Event"} '${spec.key}' was removed`
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
for (const diff of diffs) {
|
|
2170
|
+
const matchingRule = findMatchingRule({ path: diff.path, description: diff.description, type: diff.type }, rules);
|
|
2171
|
+
const specKey = extractSpecKey(diff.path, baseSpecs, headSpecs);
|
|
2172
|
+
const specInfo = findSpecInfo(specKey, baseSpecs, headSpecs);
|
|
2173
|
+
deltas.push({
|
|
2174
|
+
specKey: specInfo?.key ?? "unknown",
|
|
2175
|
+
specVersion: specInfo?.version ?? "1.0.0",
|
|
2176
|
+
specType: specInfo?.type ?? "operation",
|
|
2177
|
+
path: diff.path,
|
|
2178
|
+
severity: matchingRule?.severity ?? mapDiffTypeToSeverity(diff.type),
|
|
2179
|
+
rule: matchingRule?.id ?? "unknown",
|
|
2180
|
+
description: diff.description,
|
|
2181
|
+
oldValue: diff.oldValue,
|
|
2182
|
+
newValue: diff.newValue
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
for (const spec of addedSpecs) {
|
|
2186
|
+
deltas.push({
|
|
2187
|
+
specKey: spec.key,
|
|
2188
|
+
specVersion: spec.version,
|
|
2189
|
+
specType: spec.type,
|
|
2190
|
+
path: `spec.${spec.key}`,
|
|
2191
|
+
severity: "non_breaking",
|
|
2192
|
+
rule: "endpoint-added",
|
|
2193
|
+
description: `${spec.type === "operation" ? "Operation" : "Event"} '${spec.key}' was added`
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
const summary = calculateSummary(deltas, addedSpecs, removedSpecs);
|
|
2197
|
+
const hasBreaking = summary.breaking > 0 || summary.removed > 0;
|
|
2198
|
+
const hasNonBreaking = summary.nonBreaking > 0 || summary.added > 0;
|
|
2199
|
+
const status = determineStatus(hasBreaking, hasNonBreaking);
|
|
2200
|
+
return {
|
|
2201
|
+
status,
|
|
2202
|
+
hasBreaking,
|
|
2203
|
+
hasNonBreaking,
|
|
2204
|
+
summary,
|
|
2205
|
+
deltas,
|
|
2206
|
+
addedSpecs,
|
|
2207
|
+
removedSpecs,
|
|
2208
|
+
timestamp: new Date().toISOString()
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
function calculateSummary(deltas, addedSpecs, removedSpecs) {
|
|
2212
|
+
return {
|
|
2213
|
+
breaking: deltas.filter((d) => d.severity === "breaking").length,
|
|
2214
|
+
nonBreaking: deltas.filter((d) => d.severity === "non_breaking").length,
|
|
2215
|
+
info: deltas.filter((d) => d.severity === "info").length,
|
|
2216
|
+
added: addedSpecs.length,
|
|
2217
|
+
removed: removedSpecs.length
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
function determineStatus(hasBreaking, hasNonBreaking) {
|
|
2221
|
+
if (hasBreaking)
|
|
2222
|
+
return "breaking";
|
|
2223
|
+
if (hasNonBreaking)
|
|
2224
|
+
return "non-breaking";
|
|
2225
|
+
return "no-impact";
|
|
2226
|
+
}
|
|
2227
|
+
function mapDiffTypeToSeverity(type) {
|
|
2228
|
+
switch (type) {
|
|
2229
|
+
case "breaking":
|
|
2230
|
+
return "breaking";
|
|
2231
|
+
case "removed":
|
|
2232
|
+
return "breaking";
|
|
2233
|
+
case "added":
|
|
2234
|
+
return "non_breaking";
|
|
2235
|
+
case "changed":
|
|
2236
|
+
return "info";
|
|
2237
|
+
default:
|
|
2238
|
+
return "info";
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
function extractSpecKey(_path, _baseSpecs, _headSpecs) {
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
function findSpecInfo(key, baseSpecs, headSpecs) {
|
|
2245
|
+
if (!key)
|
|
2246
|
+
return headSpecs[0] ?? baseSpecs[0];
|
|
2247
|
+
return headSpecs.find((s) => s.key === key) ?? baseSpecs.find((s) => s.key === key);
|
|
2248
|
+
}
|
|
2249
|
+
// src/analysis/spec-parser.ts
|
|
2250
|
+
import { readFile } from "fs/promises";
|
|
2251
|
+
async function loadSpecFromSource(filePath) {
|
|
2252
|
+
try {
|
|
2253
|
+
const code = await readFile(filePath, "utf-8");
|
|
2254
|
+
if (isFeatureFile(filePath)) {
|
|
2255
|
+
const featureResult = scanFeatureSource(code, filePath);
|
|
2256
|
+
return [mapFeatureResultToParsedSpec(featureResult)];
|
|
2257
|
+
}
|
|
2258
|
+
const specResults = scanAllSpecsFromSource(code, filePath);
|
|
2259
|
+
return specResults.map(mapSpecResultToParsedSpec);
|
|
2260
|
+
} catch (error) {
|
|
2261
|
+
console.warn(`Failed to parse spec from ${filePath}:`, error);
|
|
2262
|
+
return [];
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
function mapFeatureResultToParsedSpec(result) {
|
|
2266
|
+
const meta = {
|
|
2267
|
+
key: result.key,
|
|
2268
|
+
version: "1.0.0",
|
|
2269
|
+
description: result.description,
|
|
2270
|
+
stability: result.stability,
|
|
2271
|
+
owners: result.owners,
|
|
2272
|
+
tags: result.tags,
|
|
2273
|
+
goal: result.goal,
|
|
2274
|
+
context: result.context
|
|
2275
|
+
};
|
|
2276
|
+
return {
|
|
2277
|
+
meta,
|
|
2278
|
+
specType: "feature",
|
|
2279
|
+
filePath: result.filePath,
|
|
2280
|
+
sourceBlock: result.sourceBlock,
|
|
2281
|
+
operations: result.operations.map(mapToSpecRef),
|
|
2282
|
+
events: result.events.map(mapToSpecRef),
|
|
2283
|
+
presentations: result.presentations.map(mapToSpecRef)
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
function mapSpecResultToParsedSpec(result) {
|
|
2287
|
+
const meta = {
|
|
2288
|
+
key: result.key ?? "unknown",
|
|
2289
|
+
version: result.version ?? "1.0.0",
|
|
2290
|
+
description: result.description,
|
|
2291
|
+
stability: result.stability,
|
|
2292
|
+
owners: result.owners,
|
|
2293
|
+
tags: result.tags,
|
|
2294
|
+
goal: result.goal,
|
|
2295
|
+
context: result.context
|
|
2296
|
+
};
|
|
2297
|
+
return {
|
|
2298
|
+
meta,
|
|
2299
|
+
specType: result.specType,
|
|
2300
|
+
kind: result.kind,
|
|
2301
|
+
hasIo: result.hasIo,
|
|
2302
|
+
hasPolicy: result.hasPolicy,
|
|
2303
|
+
hasPayload: result.hasPayload,
|
|
2304
|
+
hasContent: result.hasContent,
|
|
2305
|
+
hasDefinition: result.hasDefinition,
|
|
2306
|
+
emittedEvents: result.emittedEvents?.map(mapToSpecRef),
|
|
2307
|
+
policyRefs: result.policyRefs?.map(mapToSpecRef),
|
|
2308
|
+
testRefs: result.testRefs?.map(mapToSpecRef),
|
|
2309
|
+
filePath: result.filePath,
|
|
2310
|
+
sourceBlock: result.sourceBlock
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
function mapToSpecRef(ref) {
|
|
2314
|
+
return {
|
|
2315
|
+
name: ref.key,
|
|
2316
|
+
version: ref.version
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
// src/templates/utils.ts
|
|
2320
|
+
function toCamelCase(str) {
|
|
2321
|
+
const pascal = toPascalCase(str);
|
|
2322
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
2323
|
+
}
|
|
2324
|
+
function toPascalCase(str) {
|
|
2325
|
+
return str.split(/[-_.]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
2326
|
+
}
|
|
2327
|
+
function toKebabCase(str) {
|
|
2328
|
+
return str.replace(/\./g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
2329
|
+
}
|
|
2330
|
+
function capitalize(str) {
|
|
2331
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
2332
|
+
}
|
|
2333
|
+
function escapeString(value) {
|
|
2334
|
+
return value.replace(/'/g, "\\'");
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/templates/operation.ts
|
|
2338
|
+
function generateOperationSpec(data) {
|
|
2339
|
+
const {
|
|
2340
|
+
name,
|
|
2341
|
+
version,
|
|
2342
|
+
kind,
|
|
2343
|
+
description,
|
|
2344
|
+
goal,
|
|
2345
|
+
context,
|
|
2346
|
+
stability,
|
|
2347
|
+
owners,
|
|
2348
|
+
tags,
|
|
2349
|
+
auth,
|
|
2350
|
+
flags
|
|
2351
|
+
} = data;
|
|
2352
|
+
const specVarName = toPascalCase(name.split(".").pop() ?? "Unknown") + "Spec";
|
|
2353
|
+
const inputSchemaName = specVarName.replace("Spec", "Input");
|
|
2354
|
+
const outputSchemaName = specVarName.replace("Spec", "Output");
|
|
2355
|
+
return `import { define${capitalize(kind)} } from '@contractspec/lib.contracts';
|
|
2356
|
+
import { ScalarTypeEnum, SchemaModel } from '@contractspec/lib.schema';
|
|
2357
|
+
|
|
2358
|
+
// TODO: Define input schema
|
|
2359
|
+
export const ${inputSchemaName} = new SchemaModel({
|
|
2360
|
+
name: '${inputSchemaName}',
|
|
2361
|
+
description: 'Input for ${name}',
|
|
2362
|
+
fields: {
|
|
2363
|
+
// Add your fields here
|
|
2364
|
+
// example: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
2365
|
+
},
|
|
2366
|
+
});
|
|
2367
|
+
|
|
2368
|
+
// TODO: Define output schema
|
|
2369
|
+
export const ${outputSchemaName} = new SchemaModel({
|
|
2370
|
+
name: '${outputSchemaName}',
|
|
2371
|
+
description: 'Output for ${name}',
|
|
2372
|
+
fields: {
|
|
2373
|
+
// Add your fields here
|
|
2374
|
+
ok: { type: ScalarTypeEnum.Boolean(), isOptional: false },
|
|
2375
|
+
},
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
export const ${specVarName} = define${capitalize(kind)}({
|
|
2379
|
+
meta: {
|
|
2380
|
+
key: '${name}',
|
|
2381
|
+
version: ${version},
|
|
2382
|
+
stability: '${stability}',
|
|
2383
|
+
owners: [${owners.map((o) => `'${o}'`).join(", ")}],
|
|
2384
|
+
tags: [${tags.map((t) => `'${t}'`).join(", ")}],
|
|
2385
|
+
description: '${description}',
|
|
2386
|
+
goal: '${goal}',
|
|
2387
|
+
context: '${context}',
|
|
2388
|
+
},
|
|
2389
|
+
|
|
2390
|
+
io: {
|
|
2391
|
+
input: ${inputSchemaName},
|
|
2392
|
+
output: ${outputSchemaName},
|
|
2393
|
+
errors: {
|
|
2394
|
+
// Define possible errors
|
|
2395
|
+
// EXAMPLE_ERROR: {
|
|
2396
|
+
// description: 'Example error description',
|
|
2397
|
+
// http: 400,
|
|
2398
|
+
// when: 'When this error occurs',
|
|
2399
|
+
// },
|
|
2400
|
+
},
|
|
2401
|
+
},
|
|
2402
|
+
|
|
2403
|
+
policy: {
|
|
2404
|
+
auth: '${auth}',
|
|
2405
|
+
${flags.length > 0 ? `flags: [${flags.map((f) => `'${f}'`).join(", ")}],` : "// flags: [],"}
|
|
2406
|
+
},
|
|
2407
|
+
|
|
2408
|
+
sideEffects: {
|
|
2409
|
+
${data.emitsEvents ? `emits: [
|
|
2410
|
+
// Define events to emit
|
|
2411
|
+
// { ref: SomeEventSpec, when: 'always' }
|
|
2412
|
+
],` : "// emits: [],"}
|
|
2413
|
+
analytics: [
|
|
2414
|
+
// Define analytics events
|
|
2415
|
+
],
|
|
2416
|
+
},
|
|
2417
|
+
|
|
2418
|
+
transport: {
|
|
2419
|
+
rest: { method: '${kind === "command" ? "POST" : "GET"}' },
|
|
2420
|
+
gql: { field: '${name.replace(/\./g, "_")}' },
|
|
2421
|
+
mcp: { toolName: '${name}.v${version}' },
|
|
2422
|
+
},
|
|
2423
|
+
|
|
2424
|
+
acceptance: {
|
|
2425
|
+
scenarios: [
|
|
2426
|
+
{
|
|
2427
|
+
name: 'Happy path',
|
|
2428
|
+
given: ['preconditions'],
|
|
2429
|
+
when: ['action taken'],
|
|
2430
|
+
then: ['expected outcome'],
|
|
2431
|
+
},
|
|
2432
|
+
],
|
|
2433
|
+
examples: [
|
|
2434
|
+
{
|
|
2435
|
+
name: 'Example usage',
|
|
2436
|
+
input: { /* example input */ },
|
|
2437
|
+
output: { ok: true },
|
|
2438
|
+
},
|
|
2439
|
+
],
|
|
2440
|
+
},
|
|
2441
|
+
});
|
|
2442
|
+
`;
|
|
2443
|
+
}
|
|
2444
|
+
// src/templates/event.ts
|
|
2445
|
+
function generateEventSpec(data) {
|
|
2446
|
+
const { name, version, description, stability, owners, tags, piiFields } = data;
|
|
2447
|
+
const eventVarName = toPascalCase(name.replace(/\./g, "_")) + "V" + version;
|
|
2448
|
+
const payloadSchemaName = eventVarName + "Payload";
|
|
2449
|
+
return `import { defineEvent } from '@contractspec/lib.contracts';
|
|
2450
|
+
import { ScalarTypeEnum, SchemaModel } from '@contractspec/lib.schema';
|
|
2451
|
+
|
|
2452
|
+
// TODO: Define event payload schema
|
|
2453
|
+
export const ${payloadSchemaName} = new SchemaModel({
|
|
2454
|
+
name: '${payloadSchemaName}',
|
|
2455
|
+
description: 'Payload for ${name}',
|
|
2456
|
+
fields: {
|
|
2457
|
+
// Add your payload fields here
|
|
2458
|
+
// example: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
2459
|
+
},
|
|
2460
|
+
});
|
|
2461
|
+
|
|
2462
|
+
export const ${eventVarName} = defineEvent({
|
|
2463
|
+
meta: {
|
|
2464
|
+
name: '${name}',
|
|
2465
|
+
version: ${version},
|
|
2466
|
+
description: '${description}',
|
|
2467
|
+
stability: '${stability}',
|
|
2468
|
+
owners: [${owners.map((o) => `'${o}'`).join(", ")}],
|
|
2469
|
+
tags: [${tags.map((t) => `'${t}'`).join(", ")}],
|
|
2470
|
+
},
|
|
2471
|
+
${piiFields.length > 0 ? `pii: [${piiFields.map((f) => `'${f}'`).join(", ")}],` : "// pii: [],"}
|
|
2472
|
+
payload: ${payloadSchemaName},
|
|
2473
|
+
});
|
|
2474
|
+
`;
|
|
2475
|
+
}
|
|
2476
|
+
// src/templates/presentation.ts
|
|
2477
|
+
function generatePresentationSpec(data) {
|
|
2478
|
+
const {
|
|
2479
|
+
name,
|
|
2480
|
+
version,
|
|
2481
|
+
description,
|
|
2482
|
+
stability,
|
|
2483
|
+
owners,
|
|
2484
|
+
tags,
|
|
2485
|
+
presentationKind
|
|
2486
|
+
} = data;
|
|
2487
|
+
const varName = toPascalCase(name.replace(/\./g, "_")) + "Presentation";
|
|
2488
|
+
let contentBlock = "";
|
|
2489
|
+
switch (presentationKind) {
|
|
2490
|
+
case "web_component":
|
|
2491
|
+
contentBlock = ` content: {
|
|
2492
|
+
kind: 'web_component',
|
|
2493
|
+
framework: 'react',
|
|
2494
|
+
componentKey: '${name.replace(/\./g, "_")}',
|
|
2495
|
+
props: new SchemaModel({
|
|
2496
|
+
name: '${varName}Props',
|
|
2497
|
+
description: 'Props for ${name}',
|
|
2498
|
+
fields: {
|
|
2499
|
+
// TODO: Define component props
|
|
2500
|
+
},
|
|
2501
|
+
}),
|
|
2502
|
+
analytics: [
|
|
2503
|
+
// TODO: Define analytics events
|
|
2504
|
+
],
|
|
2505
|
+
},`;
|
|
2506
|
+
break;
|
|
2507
|
+
case "markdown":
|
|
2508
|
+
contentBlock = ` content: {
|
|
2509
|
+
kind: 'markdown',
|
|
2510
|
+
content: \`
|
|
2511
|
+
# ${description}
|
|
2512
|
+
|
|
2513
|
+
TODO: Add markdown content here
|
|
2514
|
+
\`,
|
|
2515
|
+
// Or use resourceUri: 'feature://${name}/guide.md'
|
|
2516
|
+
},`;
|
|
2517
|
+
break;
|
|
2518
|
+
case "data":
|
|
2519
|
+
contentBlock = ` content: {
|
|
2520
|
+
kind: 'data',
|
|
2521
|
+
mimeType: 'application/json',
|
|
2522
|
+
model: new SchemaModel({
|
|
2523
|
+
name: '${varName}Data',
|
|
2524
|
+
description: 'Data structure for ${name}',
|
|
2525
|
+
fields: {
|
|
2526
|
+
// TODO: Define data structure
|
|
2527
|
+
},
|
|
2528
|
+
}),
|
|
2529
|
+
},`;
|
|
2530
|
+
break;
|
|
2531
|
+
}
|
|
2532
|
+
return `import type { PresentationSpec } from '@contractspec/lib.contracts/presentations';
|
|
2533
|
+
import { SchemaModel, ScalarTypeEnum } from '@contractspec/lib.schema';
|
|
2534
|
+
|
|
2535
|
+
export const ${varName}: PresentationSpec = {
|
|
2536
|
+
meta: {
|
|
2537
|
+
key: '${name}',
|
|
2538
|
+
version: ${version},
|
|
2539
|
+
stability: '${stability}',
|
|
2540
|
+
owners: [${owners.map((o) => `'${o}'`).join(", ")}],
|
|
2541
|
+
tags: [${tags.map((t) => `'${t}'`).join(", ")}],
|
|
2542
|
+
description: '${description}',
|
|
2543
|
+
},
|
|
2544
|
+
|
|
2545
|
+
policy: {
|
|
2546
|
+
// flags: [],
|
|
2547
|
+
// pii: [],
|
|
2548
|
+
},
|
|
2549
|
+
|
|
2550
|
+
${contentBlock}
|
|
2551
|
+
};
|
|
2552
|
+
`;
|
|
2553
|
+
}
|
|
2554
|
+
// src/templates/workflow.ts
|
|
2555
|
+
function generateWorkflowSpec(data) {
|
|
2556
|
+
const specVarName = toPascalCase(data.name.split(".").pop() ?? "Workflow") + "Workflow";
|
|
2557
|
+
const stepsCode = data.steps.map((step) => formatStep(step)).join(`,
|
|
2558
|
+
`);
|
|
2559
|
+
const transitionsCode = data.transitions.map((transition) => ` {
|
|
2560
|
+
from: '${transition.from}',
|
|
2561
|
+
to: '${transition.to}',
|
|
2562
|
+
${transition.condition ? ` condition: '${escapeString(transition.condition)}',` : ""}
|
|
2563
|
+
}`).join(`,
|
|
2564
|
+
`);
|
|
2565
|
+
return `import type { WorkflowSpec } from '@contractspec/lib.contracts/workflow';
|
|
2566
|
+
|
|
2567
|
+
/**
|
|
2568
|
+
* Workflow generated via contractspec CLI.
|
|
2569
|
+
* TODO:
|
|
2570
|
+
* - Review step definitions and descriptions.
|
|
2571
|
+
* - Wire automation steps to actual operations.
|
|
2572
|
+
* - Provide form renderers for human steps.
|
|
2573
|
+
* - Add guards/conditions as needed.
|
|
2574
|
+
*/
|
|
2575
|
+
export const ${specVarName}: WorkflowSpec = {
|
|
2576
|
+
meta: {
|
|
2577
|
+
key: '${data.name}',
|
|
2578
|
+
version: ${data.version},
|
|
2579
|
+
title: '${escapeString(data.title)}',
|
|
2580
|
+
description: '${escapeString(data.description)}',
|
|
2581
|
+
domain: '${escapeString(data.domain)}',
|
|
2582
|
+
stability: '${data.stability}',
|
|
2583
|
+
owners: [${data.owners.map((owner) => `'${owner}'`).join(", ")}],
|
|
2584
|
+
tags: [${data.tags.map((tag) => `'${tag}'`).join(", ")}],
|
|
2585
|
+
},
|
|
2586
|
+
definition: {
|
|
2587
|
+
${data.entryStepId ? ` entryStepId: '${data.entryStepId}',
|
|
2588
|
+
` : ""} steps: [
|
|
2589
|
+
${stepsCode}
|
|
2590
|
+
],
|
|
2591
|
+
transitions: [
|
|
2592
|
+
${transitionsCode}
|
|
2593
|
+
],
|
|
2594
|
+
},
|
|
2595
|
+
${data.policyFlags.length > 0 ? `policy: {
|
|
2596
|
+
flags: [${data.policyFlags.map((flag) => `'${flag}'`).join(", ")}],
|
|
2597
|
+
},` : "// policy: { flags: [] },"}
|
|
2598
|
+
};
|
|
2599
|
+
`;
|
|
2600
|
+
}
|
|
2601
|
+
function formatStep(step) {
|
|
2602
|
+
const lines = [
|
|
2603
|
+
` {`,
|
|
2604
|
+
` id: '${step.id}',`,
|
|
2605
|
+
` type: '${step.type}',`,
|
|
2606
|
+
` label: '${escapeString(step.label)}',`
|
|
2607
|
+
];
|
|
2608
|
+
if (step.description) {
|
|
2609
|
+
lines.push(` description: '${escapeString(step.description)}',`);
|
|
2610
|
+
}
|
|
2611
|
+
const actionLines = [];
|
|
2612
|
+
if (step.operation) {
|
|
2613
|
+
actionLines.push(`operation: { name: '${step.operation.name}', version: ${step.operation.version} }`);
|
|
2614
|
+
}
|
|
2615
|
+
if (step.form) {
|
|
2616
|
+
actionLines.push(`form: { key: '${step.form.key}', version: ${step.form.version} }`);
|
|
2617
|
+
}
|
|
2618
|
+
if (actionLines.length) {
|
|
2619
|
+
lines.push(` action: { ${actionLines.join(", ")} },`);
|
|
2620
|
+
}
|
|
2621
|
+
lines.push(` }`);
|
|
2622
|
+
return lines.join(`
|
|
2623
|
+
`);
|
|
2624
|
+
}
|
|
2625
|
+
// src/templates/workflow-runner.ts
|
|
2626
|
+
function generateWorkflowRunnerTemplate({
|
|
2627
|
+
exportName,
|
|
2628
|
+
specImportPath,
|
|
2629
|
+
runnerName,
|
|
2630
|
+
workflowName
|
|
2631
|
+
}) {
|
|
2632
|
+
return `import {
|
|
2633
|
+
InMemoryStateStore,
|
|
2634
|
+
WorkflowRegistry,
|
|
2635
|
+
WorkflowRunner,
|
|
2636
|
+
} from '@contractspec/lib.contracts/workflow';
|
|
2637
|
+
import { ${exportName} } from '${specImportPath}';
|
|
2638
|
+
|
|
2639
|
+
/**
|
|
2640
|
+
* Runner wiring for ${workflowName}.
|
|
2641
|
+
*
|
|
2642
|
+
* TODO:
|
|
2643
|
+
* - Replace the in-memory state store with a persistent adapter if needed.
|
|
2644
|
+
* - Implement opExecutor to invoke the correct contract handlers.
|
|
2645
|
+
* - Wire eventEmitter to telemetry sinks.
|
|
2646
|
+
*/
|
|
2647
|
+
const registry = new WorkflowRegistry();
|
|
2648
|
+
registry.register(${exportName});
|
|
2649
|
+
|
|
2650
|
+
const stateStore = new InMemoryStateStore();
|
|
2651
|
+
|
|
2652
|
+
export const ${runnerName} = new WorkflowRunner({
|
|
2653
|
+
registry,
|
|
2654
|
+
stateStore,
|
|
2655
|
+
opExecutor: async (operation, input, ctx) => {
|
|
2656
|
+
// TODO: route to the appropriate contract handler
|
|
2657
|
+
// Example: return contractRegistry.execute(operation.name, operation.version, input, ctx);
|
|
2658
|
+
throw new Error(
|
|
2659
|
+
\`opExecutor for \${operation.name}.v\${operation.version} is not implemented\`
|
|
2660
|
+
);
|
|
2661
|
+
},
|
|
2662
|
+
// appConfigProvider: async (state) => {
|
|
2663
|
+
// // TODO: return the ResolvedAppConfig for this workflow run (tenant/environment)
|
|
2664
|
+
// return undefined;
|
|
2665
|
+
// },
|
|
2666
|
+
// enforceCapabilities: async (operation, context) => {
|
|
2667
|
+
// // TODO: ensure required capabilities are satisfied using context.integrations/context.resolvedAppConfig
|
|
2668
|
+
// },
|
|
2669
|
+
eventEmitter: (_event, _payload) => {
|
|
2670
|
+
// TODO: forward workflow events to telemetry or logging sinks
|
|
2671
|
+
},
|
|
2672
|
+
});
|
|
2673
|
+
`;
|
|
2674
|
+
}
|
|
2675
|
+
// src/templates/data-view.ts
|
|
2676
|
+
function generateDataViewSpec(data) {
|
|
2677
|
+
const viewVarName = toPascalCase(data.name.split(".").pop() ?? "DataView") + "DataView";
|
|
2678
|
+
const fields = data.fields.map((field) => ` {
|
|
2679
|
+
key: '${escapeString(field.key)}',
|
|
2680
|
+
label: '${escape(field.label)}',
|
|
2681
|
+
dataPath: '${escapeString(field.dataPath)}',
|
|
2682
|
+
${field.format ? `format: '${escapeString(field.format)}',` : ""}
|
|
2683
|
+
${field.sortable ? "sortable: true," : ""}
|
|
2684
|
+
${field.filterable ? "filterable: true," : ""}
|
|
2685
|
+
}`).join(`,
|
|
2686
|
+
`);
|
|
2687
|
+
const secondaryFields = data.secondaryFields?.length ? `secondaryFields: [${data.secondaryFields.map((key) => `'${escapeString(key)}'`).join(", ")}],` : "";
|
|
2688
|
+
const itemOperation = data.itemOperation ? `item: { name: '${escapeString(data.itemOperation.name)}', version: ${data.itemOperation.version} },` : "";
|
|
2689
|
+
return `import type { DataViewSpec } from '@contractspec/lib.contracts/data-views';
|
|
2690
|
+
|
|
2691
|
+
export const ${viewVarName}: DataViewSpec = {
|
|
2692
|
+
meta: {
|
|
2693
|
+
key: '${escapeString(data.name)}',
|
|
2694
|
+
version: ${data.version},
|
|
2695
|
+
entity: '${escapeString(data.entity)}',
|
|
2696
|
+
title: '${escape(data.title)}',
|
|
2697
|
+
description: '${escape(data.description || "Describe the purpose of this data view.")}',
|
|
2698
|
+
domain: '${escape(data.domain || data.entity)}',
|
|
2699
|
+
owners: [${data.owners.map((owner) => `'${escapeString(owner)}'`).join(", ")}],
|
|
2700
|
+
tags: [${data.tags.map((tag) => `'${escapeString(tag)}'`).join(", ")}],
|
|
2701
|
+
stability: '${data.stability}',
|
|
2702
|
+
},
|
|
2703
|
+
source: {
|
|
2704
|
+
primary: {
|
|
2705
|
+
name: '${escapeString(data.primaryOperation.name)}',
|
|
2706
|
+
version: ${data.primaryOperation.version},
|
|
2707
|
+
},
|
|
2708
|
+
${itemOperation}
|
|
2709
|
+
refreshEvents: [
|
|
2710
|
+
// { name: 'entity.updated', version: '1.0.0' },
|
|
2711
|
+
],
|
|
2712
|
+
},
|
|
2713
|
+
view: {
|
|
2714
|
+
kind: '${data.kind}',
|
|
2715
|
+
fields: [
|
|
2716
|
+
${fields}
|
|
2717
|
+
],
|
|
2718
|
+
${data.primaryField ? `primaryField: '${escapeString(data.primaryField)}',` : ""}
|
|
2719
|
+
${secondaryFields}
|
|
2720
|
+
filters: [
|
|
2721
|
+
// Example filter:
|
|
2722
|
+
// { key: 'search', label: 'Search', field: 'fullName', type: 'search' },
|
|
2723
|
+
],
|
|
2724
|
+
actions: [
|
|
2725
|
+
// Example action:
|
|
2726
|
+
// { key: 'open', label: 'Open', kind: 'navigation' },
|
|
2727
|
+
],
|
|
2728
|
+
},
|
|
2729
|
+
states: {
|
|
2730
|
+
// empty: { name: 'app.data.empty', version: '1.0.0' },
|
|
2731
|
+
// error: { name: 'app.data.error', version: '1.0.0' },
|
|
2732
|
+
},
|
|
2733
|
+
};
|
|
2734
|
+
`;
|
|
2735
|
+
}
|
|
2736
|
+
function escape(value) {
|
|
2737
|
+
return value.replace(/'/g, "\\'");
|
|
2738
|
+
}
|
|
2739
|
+
// src/templates/telemetry.ts
|
|
2740
|
+
function generateTelemetrySpec(data) {
|
|
2741
|
+
const specVar = toPascalCase(data.name.split(".").pop() ?? "Telemetry") + "Telemetry";
|
|
2742
|
+
const providers = data.providers?.length ? `providers: [
|
|
2743
|
+
${data.providers.map((provider) => ` {
|
|
2744
|
+
type: '${provider.type}',
|
|
2745
|
+
config: ${formatConfigValue(provider.config)},
|
|
2746
|
+
}`).join(`,
|
|
2747
|
+
`)}
|
|
2748
|
+
],` : "";
|
|
2749
|
+
const events = data.events.map((event) => {
|
|
2750
|
+
const properties = event.properties.map((prop) => ` '${prop.name}': {
|
|
2751
|
+
type: '${prop.type}',
|
|
2752
|
+
${prop.required ? "required: true," : ""}
|
|
2753
|
+
${prop.pii ? "pii: true," : ""}
|
|
2754
|
+
${prop.redact ? "redact: true," : ""}
|
|
2755
|
+
${prop.description ? `description: '${escapeString2(prop.description)}',` : ""}
|
|
2756
|
+
}`).join(`,
|
|
2757
|
+
`);
|
|
2758
|
+
const anomalyRules = event.anomalyRules?.length ? ` anomalyDetection: {
|
|
2759
|
+
enabled: true,
|
|
2760
|
+
${typeof event.anomalyMinimumSample === "number" ? `minimumSample: ${event.anomalyMinimumSample},` : ""}
|
|
2761
|
+
thresholds: [
|
|
2762
|
+
${event.anomalyRules.map((rule) => ` {
|
|
2763
|
+
metric: '${escapeString2(rule.metric)}',
|
|
2764
|
+
${typeof rule.min === "number" ? `min: ${rule.min},` : ""}
|
|
2765
|
+
${typeof rule.max === "number" ? `max: ${rule.max},` : ""}
|
|
2766
|
+
}`).join(`,
|
|
2767
|
+
`)}
|
|
2768
|
+
],
|
|
2769
|
+
actions: [${(event.anomalyActions ?? []).map((action) => `'${action}'`).join(", ")}],
|
|
2770
|
+
},` : event.anomalyEnabled ? ` anomalyDetection: {
|
|
2771
|
+
enabled: true,
|
|
2772
|
+
${typeof event.anomalyMinimumSample === "number" ? `minimumSample: ${event.anomalyMinimumSample},` : ""}
|
|
2773
|
+
},` : "";
|
|
2774
|
+
return ` {
|
|
2775
|
+
name: '${escapeString2(event.name)}',
|
|
2776
|
+
version: ${event.version},
|
|
2777
|
+
semantics: {
|
|
2778
|
+
what: '${escapeString2(event.what)}',
|
|
2779
|
+
${event.who ? `who: '${escapeString2(event.who)}',` : ""}
|
|
2780
|
+
${event.why ? `why: '${escapeString2(event.why)}',` : ""}
|
|
2781
|
+
},
|
|
2782
|
+
privacy: '${event.privacy}',
|
|
2783
|
+
properties: {
|
|
2784
|
+
${properties}
|
|
2785
|
+
},
|
|
2786
|
+
${typeof event.retentionDays === "number" ? `retention: { days: ${event.retentionDays}, ${event.retentionPolicy ? `policy: '${event.retentionPolicy}'` : ""} },` : ""}
|
|
2787
|
+
${typeof event.samplingRate === "number" ? `sampling: { rate: ${event.samplingRate}${event.samplingConditions ? `, conditions: ['${escapeString2(event.samplingConditions)}']` : ""} },` : ""}
|
|
2788
|
+
${anomalyRules}
|
|
2789
|
+
${event.tags?.length ? `tags: [${event.tags.map((tag) => `'${escapeString2(tag)}'`).join(", ")}],` : ""}
|
|
2790
|
+
}`;
|
|
2791
|
+
}).join(`,
|
|
2792
|
+
`);
|
|
2793
|
+
return `import type { TelemetrySpec } from '@contractspec/lib.contracts/telemetry';
|
|
2794
|
+
|
|
2795
|
+
export const ${specVar}: TelemetrySpec = {
|
|
2796
|
+
meta: {
|
|
2797
|
+
key: '${escapeString2(data.name)}',
|
|
2798
|
+
version: ${data.version},
|
|
2799
|
+
title: '${escapeString2(data.name)} telemetry',
|
|
2800
|
+
description: '${escapeString2(data.description || "Describe the purpose of this telemetry spec.")}',
|
|
2801
|
+
domain: '${escapeString2(data.domain)}',
|
|
2802
|
+
owners: [${data.owners.map((owner) => `'${escapeString2(owner)}'`).join(", ")}],
|
|
2803
|
+
tags: [${data.tags.map((tag) => `'${escapeString2(tag)}'`).join(", ")}],
|
|
2804
|
+
stability: '${data.stability}',
|
|
2805
|
+
},
|
|
2806
|
+
config: {
|
|
2807
|
+
${typeof data.defaultRetentionDays === "number" ? `defaultRetentionDays: ${data.defaultRetentionDays},` : ""}
|
|
2808
|
+
${typeof data.defaultSamplingRate === "number" ? `defaultSamplingRate: ${data.defaultSamplingRate},` : ""}
|
|
2809
|
+
${data.anomalyEnabled ? `anomalyDetection: { enabled: true${typeof data.anomalyCheckIntervalMs === "number" ? `, checkIntervalMs: ${data.anomalyCheckIntervalMs}` : ""} },` : ""}
|
|
2810
|
+
${providers}
|
|
2811
|
+
},
|
|
2812
|
+
events: [
|
|
2813
|
+
${events}
|
|
2814
|
+
],
|
|
2815
|
+
};
|
|
2816
|
+
`;
|
|
2817
|
+
}
|
|
2818
|
+
function escapeString2(value) {
|
|
2819
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2820
|
+
}
|
|
2821
|
+
function formatConfigValue(value) {
|
|
2822
|
+
const trimmed = value.trim();
|
|
2823
|
+
if (!trimmed)
|
|
2824
|
+
return "{}";
|
|
2825
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
2826
|
+
return trimmed;
|
|
2827
|
+
}
|
|
2828
|
+
return `'${escapeString2(trimmed)}'`;
|
|
2829
|
+
}
|
|
2830
|
+
// src/templates/experiment.ts
|
|
2831
|
+
function generateExperimentSpec(data) {
|
|
2832
|
+
const specVar = toPascalCase(data.name.split(".").pop() ?? "Experiment") + "Experiment";
|
|
2833
|
+
const variants = data.variants.map((variant) => {
|
|
2834
|
+
const overrides = variant.overrides?.length ? ` overrides: [
|
|
2835
|
+
${variant.overrides.map((override) => ` {
|
|
2836
|
+
type: '${override.type}',
|
|
2837
|
+
target: '${escapeString3(override.target)}',
|
|
2838
|
+
${typeof override.version === "number" ? `version: ${override.version},` : ""}
|
|
2839
|
+
}`).join(`,
|
|
2840
|
+
`)}
|
|
2841
|
+
],` : "";
|
|
2842
|
+
return ` {
|
|
2843
|
+
id: '${escapeString3(variant.id)}',
|
|
2844
|
+
name: '${escapeString3(variant.name)}',
|
|
2845
|
+
${variant.description ? `description: '${escapeString3(variant.description)}',` : ""}
|
|
2846
|
+
${typeof variant.weight === "number" ? `weight: ${variant.weight},` : ""}
|
|
2847
|
+
${overrides}
|
|
2848
|
+
}`;
|
|
2849
|
+
}).join(`,
|
|
2850
|
+
`);
|
|
2851
|
+
const allocation = renderAllocation(data.allocation);
|
|
2852
|
+
const metrics = data.successMetrics?.length ? ` successMetrics: [
|
|
2853
|
+
${data.successMetrics.map((metric) => ` {
|
|
2854
|
+
name: '${escapeString3(metric.name)}',
|
|
2855
|
+
telemetryEvent: { name: '${escapeString3(metric.eventName)}', version: ${metric.eventVersion} },
|
|
2856
|
+
aggregation: '${metric.aggregation}',
|
|
2857
|
+
${typeof metric.target === "number" ? `target: ${metric.target},` : ""}
|
|
2858
|
+
}`).join(`,
|
|
2859
|
+
`)}
|
|
2860
|
+
],` : "";
|
|
2861
|
+
return `import type { ExperimentSpec } from '@contractspec/lib.contracts/experiments';
|
|
2862
|
+
|
|
2863
|
+
export const ${specVar}: ExperimentSpec = {
|
|
2864
|
+
meta: {
|
|
2865
|
+
key: '${escapeString3(data.name)}',
|
|
2866
|
+
version: ${data.version},
|
|
2867
|
+
title: '${escapeString3(data.name)} experiment',
|
|
2868
|
+
description: '${escapeString3(data.description || "Describe the experiment goal.")}',
|
|
2869
|
+
domain: '${escapeString3(data.domain)}',
|
|
2870
|
+
owners: [${data.owners.map((owner) => `'${escapeString3(owner)}'`).join(", ")}],
|
|
2871
|
+
tags: [${data.tags.map((tag) => `'${escapeString3(tag)}'`).join(", ")}],
|
|
2872
|
+
stability: '${data.stability}',
|
|
2873
|
+
},
|
|
2874
|
+
controlVariant: '${escapeString3(data.controlVariant)}',
|
|
2875
|
+
variants: [
|
|
2876
|
+
${variants}
|
|
2877
|
+
],
|
|
2878
|
+
allocation: ${allocation},
|
|
2879
|
+
${metrics}
|
|
2880
|
+
};
|
|
2881
|
+
`;
|
|
2882
|
+
}
|
|
2883
|
+
function renderAllocation(allocation) {
|
|
2884
|
+
switch (allocation.type) {
|
|
2885
|
+
case "random":
|
|
2886
|
+
return `{
|
|
2887
|
+
type: 'random',
|
|
2888
|
+
${allocation.salt ? `salt: '${escapeString3(allocation.salt)}',` : ""}
|
|
2889
|
+
}`;
|
|
2890
|
+
case "sticky":
|
|
2891
|
+
return `{
|
|
2892
|
+
type: 'sticky',
|
|
2893
|
+
attribute: '${allocation.attribute}',
|
|
2894
|
+
${allocation.salt ? `salt: '${escapeString3(allocation.salt)}',` : ""}
|
|
2895
|
+
}`;
|
|
2896
|
+
case "targeted":
|
|
2897
|
+
return `{
|
|
2898
|
+
type: 'targeted',
|
|
2899
|
+
rules: [
|
|
2900
|
+
${allocation.rules.map((rule) => ` {
|
|
2901
|
+
variantId: '${escapeString3(rule.variantId)}',
|
|
2902
|
+
${typeof rule.percentage === "number" ? `percentage: ${rule.percentage},` : ""}
|
|
2903
|
+
${rule.policy ? `policy: { name: '${escapeString3(rule.policy.name)}'${typeof rule.policy.version === "number" ? `, version: ${rule.policy.version}` : ""} },` : ""}
|
|
2904
|
+
${rule.expression ? `expression: '${escapeString3(rule.expression)}',` : ""}
|
|
2905
|
+
}`).join(`,
|
|
2906
|
+
`)}
|
|
2907
|
+
],
|
|
2908
|
+
fallback: '${allocation.fallback ?? "control"}',
|
|
2909
|
+
}`;
|
|
2910
|
+
default:
|
|
2911
|
+
return renderUnsupportedAllocation(allocation);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
function escapeString3(value) {
|
|
2915
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2916
|
+
}
|
|
2917
|
+
function renderUnsupportedAllocation(allocation) {
|
|
2918
|
+
throw new Error(`Unsupported allocation type ${allocation}`);
|
|
2919
|
+
}
|
|
2920
|
+
// src/templates/app-config.ts
|
|
2921
|
+
function generateAppBlueprintSpec(data) {
|
|
2922
|
+
const exportName = toPascalCase(data.name.split(".").pop() ?? "App") + "AppConfig";
|
|
2923
|
+
const capabilitiesSection = buildCapabilitiesSection(data);
|
|
2924
|
+
const featuresSection = buildFeaturesSection(data);
|
|
2925
|
+
const dataViewsSection = buildMappingSection("dataViews", data.dataViews);
|
|
2926
|
+
const workflowsSection = buildMappingSection("workflows", data.workflows);
|
|
2927
|
+
const policiesSection = buildPolicySection(data);
|
|
2928
|
+
const themeSection = buildThemeSection(data);
|
|
2929
|
+
const telemetrySection = buildTelemetrySection(data);
|
|
2930
|
+
const experimentsSection = buildExperimentsSection(data);
|
|
2931
|
+
const flagsSection = buildFeatureFlagsSection(data);
|
|
2932
|
+
const routesSection = buildRoutesSection(data);
|
|
2933
|
+
const notesSection = data.notes ? ` notes: '${escapeString4(data.notes)}',
|
|
2934
|
+
` : "";
|
|
2935
|
+
return `import type { AppBlueprintSpec } from '@contractspec/lib.contracts/app-config';
|
|
2936
|
+
|
|
2937
|
+
export const ${exportName}: AppBlueprintSpec = {
|
|
2938
|
+
meta: {
|
|
2939
|
+
key: '${escapeString4(data.name)}',
|
|
2940
|
+
version: '${data.version}',
|
|
2941
|
+
title: '${escapeString4(data.title)}',
|
|
2942
|
+
description: '${escapeString4(data.description)}',
|
|
2943
|
+
domain: '${escapeString4(data.domain)}',
|
|
2944
|
+
owners: [${data.owners.map((owner) => `'${escapeString4(owner)}'`).join(", ")}],
|
|
2945
|
+
tags: [${data.tags.map((tag) => `'${escapeString4(tag)}'`).join(", ")}],
|
|
2946
|
+
stability: '${data.stability}',
|
|
2947
|
+
appId: '${escapeString4(data.appId)}',
|
|
2948
|
+
},
|
|
2949
|
+
${capabilitiesSection}${featuresSection}${dataViewsSection}${workflowsSection}${policiesSection}${themeSection}${telemetrySection}${experimentsSection}${flagsSection}${routesSection}${notesSection}};
|
|
2950
|
+
`;
|
|
2951
|
+
}
|
|
2952
|
+
function buildCapabilitiesSection(data) {
|
|
2953
|
+
if (data.capabilitiesEnabled.length === 0 && data.capabilitiesDisabled.length === 0) {
|
|
2954
|
+
return "";
|
|
2955
|
+
}
|
|
2956
|
+
const enabled = data.capabilitiesEnabled.length > 0 ? ` enabled: [${data.capabilitiesEnabled.map((key) => formatCapabilityRef(key)).join(", ")}],
|
|
2957
|
+
` : "";
|
|
2958
|
+
const disabled = data.capabilitiesDisabled.length > 0 ? ` disabled: [${data.capabilitiesDisabled.map((key) => formatCapabilityRef(key)).join(", ")}],
|
|
2959
|
+
` : "";
|
|
2960
|
+
return ` capabilities: {
|
|
2961
|
+
${enabled}${disabled} },
|
|
2962
|
+
`;
|
|
2963
|
+
}
|
|
2964
|
+
function buildFeaturesSection(data) {
|
|
2965
|
+
if (data.featureIncludes.length === 0 && data.featureExcludes.length === 0) {
|
|
2966
|
+
return "";
|
|
2967
|
+
}
|
|
2968
|
+
const include = data.featureIncludes.length > 0 ? ` include: [${data.featureIncludes.map((key) => `{ key: '${escapeString4(key)}' }`).join(", ")}],
|
|
2969
|
+
` : "";
|
|
2970
|
+
const exclude = data.featureExcludes.length > 0 ? ` exclude: [${data.featureExcludes.map((key) => `{ key: '${escapeString4(key)}' }`).join(", ")}],
|
|
2971
|
+
` : "";
|
|
2972
|
+
return ` features: {
|
|
2973
|
+
${include}${exclude} },
|
|
2974
|
+
`;
|
|
2975
|
+
}
|
|
2976
|
+
function buildMappingSection(prop, mappings) {
|
|
2977
|
+
if (mappings.length === 0)
|
|
2978
|
+
return "";
|
|
2979
|
+
const body = mappings.map((mapping) => ` ${mapping.slot}: {
|
|
2980
|
+
name: '${escapeString4(mapping.name)}',
|
|
2981
|
+
version: '${mapping.version}',
|
|
2982
|
+
}`).join(`,
|
|
2983
|
+
`);
|
|
2984
|
+
return ` ${prop}: {
|
|
2985
|
+
${body}
|
|
2986
|
+
},
|
|
2987
|
+
`;
|
|
2988
|
+
}
|
|
2989
|
+
function buildPolicySection(data) {
|
|
2990
|
+
if (data.policyRefs.length === 0)
|
|
2991
|
+
return "";
|
|
2992
|
+
const entries = data.policyRefs.map((policy) => ` {
|
|
2993
|
+
name: '${escapeString4(policy.name)}',
|
|
2994
|
+
version: '${policy.version}',
|
|
2995
|
+
}`).join(`,
|
|
2996
|
+
`);
|
|
2997
|
+
return ` policies: [
|
|
2998
|
+
${entries}
|
|
2999
|
+
],
|
|
3000
|
+
`;
|
|
3001
|
+
}
|
|
3002
|
+
function buildThemeSection(data) {
|
|
3003
|
+
if (!data.theme)
|
|
3004
|
+
return "";
|
|
3005
|
+
const primary = ` primary: { name: '${escapeString4(data.theme.name)}', version: ${data.theme.version} },
|
|
3006
|
+
`;
|
|
3007
|
+
const fallbacks = data.themeFallbacks.length > 0 ? ` fallbacks: [${data.themeFallbacks.map((theme) => `{ name: '${escapeString4(theme.name)}', version: '${theme.version}' }`).join(", ")}],
|
|
3008
|
+
` : "";
|
|
3009
|
+
return ` theme: {
|
|
3010
|
+
${primary}${fallbacks} },
|
|
3011
|
+
`;
|
|
3012
|
+
}
|
|
3013
|
+
function buildTelemetrySection(data) {
|
|
3014
|
+
if (!data.telemetry)
|
|
3015
|
+
return "";
|
|
3016
|
+
return ` telemetry: {
|
|
3017
|
+
spec: {
|
|
3018
|
+
name: '${escapeString4(data.telemetry.name)}'${data.telemetry.version !== undefined ? `,
|
|
3019
|
+
version: '${data.telemetry.version}'` : ""}
|
|
3020
|
+
},
|
|
3021
|
+
},
|
|
3022
|
+
`;
|
|
3023
|
+
}
|
|
3024
|
+
function buildExperimentsSection(data) {
|
|
3025
|
+
if (data.activeExperiments.length === 0 && data.pausedExperiments.length === 0) {
|
|
3026
|
+
return "";
|
|
3027
|
+
}
|
|
3028
|
+
const active = data.activeExperiments.length > 0 ? ` active: [${data.activeExperiments.map((exp) => formatExperimentRef(exp)).join(", ")}],
|
|
3029
|
+
` : "";
|
|
3030
|
+
const paused = data.pausedExperiments.length > 0 ? ` paused: [${data.pausedExperiments.map((exp) => formatExperimentRef(exp)).join(", ")}],
|
|
3031
|
+
` : "";
|
|
3032
|
+
return ` experiments: {
|
|
3033
|
+
${active}${paused} },
|
|
3034
|
+
`;
|
|
3035
|
+
}
|
|
3036
|
+
function buildFeatureFlagsSection(data) {
|
|
3037
|
+
if (data.featureFlags.length === 0)
|
|
3038
|
+
return "";
|
|
3039
|
+
const flags = data.featureFlags.map((flag) => ` {
|
|
3040
|
+
key: '${escapeString4(flag.key)}',
|
|
3041
|
+
enabled: ${flag.enabled},
|
|
3042
|
+
${flag.variant ? `variant: '${escapeString4(flag.variant)}',` : ""}
|
|
3043
|
+
${flag.description ? `description: '${escapeString4(flag.description)}',` : ""}
|
|
3044
|
+
}`).join(`,
|
|
3045
|
+
`);
|
|
3046
|
+
return ` featureFlags: [
|
|
3047
|
+
${flags}
|
|
3048
|
+
],
|
|
3049
|
+
`;
|
|
3050
|
+
}
|
|
3051
|
+
function buildRoutesSection(data) {
|
|
3052
|
+
if (data.routes.length === 0)
|
|
3053
|
+
return "";
|
|
3054
|
+
const routes = data.routes.map((route) => {
|
|
3055
|
+
const entries = [
|
|
3056
|
+
`path: '${escapeString4(route.path)}'`,
|
|
3057
|
+
route.label ? `label: '${escapeString4(route.label)}'` : null,
|
|
3058
|
+
route.dataView ? `dataView: '${escapeString4(route.dataView)}'` : null,
|
|
3059
|
+
route.workflow ? `workflow: '${escapeString4(route.workflow)}'` : null,
|
|
3060
|
+
route.guardName ? `guard: { name: '${escapeString4(route.guardName)}'${route.guardVersion !== undefined ? `, version: '${route.guardVersion}'` : ""} }` : null,
|
|
3061
|
+
route.featureFlag ? `featureFlag: '${escapeString4(route.featureFlag)}'` : null,
|
|
3062
|
+
route.experimentName ? `experiment: { name: '${escapeString4(route.experimentName)}'${route.experimentVersion !== undefined ? `, version: '${route.experimentVersion}'` : ""} }` : null
|
|
3063
|
+
].filter(Boolean);
|
|
3064
|
+
return ` { ${entries.join(", ")} }`;
|
|
3065
|
+
}).join(`,
|
|
3066
|
+
`);
|
|
3067
|
+
return ` routes: [
|
|
3068
|
+
${routes}
|
|
3069
|
+
],
|
|
3070
|
+
`;
|
|
3071
|
+
}
|
|
3072
|
+
function formatCapabilityRef(key) {
|
|
3073
|
+
return `{ key: '${escapeString4(key)}' }`;
|
|
3074
|
+
}
|
|
3075
|
+
function formatExperimentRef(exp) {
|
|
3076
|
+
const version = typeof exp.version === "string" ? `, version: ${exp.version}` : "";
|
|
3077
|
+
return `{ name: '${escapeString4(exp.name)}'${version} }`;
|
|
3078
|
+
}
|
|
3079
|
+
function escapeString4(value) {
|
|
3080
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
3081
|
+
}
|
|
3082
|
+
// src/templates/migration.ts
|
|
3083
|
+
function generateMigrationSpec(data) {
|
|
3084
|
+
const specName = toPascalCase(data.name.split(".").pop() ?? "Migration");
|
|
3085
|
+
const migrationVar = `${specName}Migration`;
|
|
3086
|
+
const dependencies = data.dependencies.length > 0 ? `dependencies: [${data.dependencies.map((dep) => `'${escapeString(dep)}'`).join(", ")}],` : "";
|
|
3087
|
+
return `import type { MigrationSpec } from '@contractspec/lib.contracts/migrations';
|
|
3088
|
+
|
|
3089
|
+
export const ${migrationVar}: MigrationSpec = {
|
|
3090
|
+
meta: {
|
|
3091
|
+
key: '${escapeString(data.name)}',
|
|
3092
|
+
version: ${data.version},
|
|
3093
|
+
title: '${escape2(data.title)}',
|
|
3094
|
+
description: '${escape2(data.description ?? "")}',
|
|
3095
|
+
domain: '${escape2(data.domain)}',
|
|
3096
|
+
owners: [${data.owners.map((owner) => `'${escapeString(owner)}'`).join(", ")}],
|
|
3097
|
+
tags: [${data.tags.map((tag) => `'${escapeString(tag)}'`).join(", ")}],
|
|
3098
|
+
stability: '${data.stability}',
|
|
3099
|
+
},
|
|
3100
|
+
plan: {
|
|
3101
|
+
up: [
|
|
3102
|
+
${renderSteps(data.up)}
|
|
3103
|
+
],${data.down && data.down.length ? `
|
|
3104
|
+
down: [
|
|
3105
|
+
${renderSteps(data.down)}
|
|
3106
|
+
],` : ""}
|
|
3107
|
+
},
|
|
3108
|
+
${dependencies}
|
|
3109
|
+
};
|
|
3110
|
+
`;
|
|
3111
|
+
}
|
|
3112
|
+
function renderSteps(steps) {
|
|
3113
|
+
return steps.map((step) => {
|
|
3114
|
+
const description = step.description ? `description: '${escape2(step.description)}',` : "";
|
|
3115
|
+
switch (step.kind) {
|
|
3116
|
+
case "schema":
|
|
3117
|
+
return ` {
|
|
3118
|
+
kind: 'schema',
|
|
3119
|
+
${description}
|
|
3120
|
+
sql: \`${escape2(step.sql ?? "")}\`,
|
|
3121
|
+
}`;
|
|
3122
|
+
case "data":
|
|
3123
|
+
return ` {
|
|
3124
|
+
kind: 'data',
|
|
3125
|
+
${description}
|
|
3126
|
+
script: \`${escape2(step.script ?? "")}\`,
|
|
3127
|
+
}`;
|
|
3128
|
+
case "validation":
|
|
3129
|
+
default:
|
|
3130
|
+
return ` {
|
|
3131
|
+
kind: 'validation',
|
|
3132
|
+
${description}
|
|
3133
|
+
assertion: \`${escape2(step.assertion ?? "")}\`,
|
|
3134
|
+
}`;
|
|
3135
|
+
}
|
|
3136
|
+
}).join(`,
|
|
3137
|
+
`);
|
|
3138
|
+
}
|
|
3139
|
+
function escape2(value) {
|
|
3140
|
+
return value.replace(/`/g, "\\`").replace(/'/g, "\\'");
|
|
3141
|
+
}
|
|
3142
|
+
// src/templates/integration-utils.ts
|
|
3143
|
+
function renderConfigSchema(fields) {
|
|
3144
|
+
const requiredFields = fields.filter((field) => field.required);
|
|
3145
|
+
const requiredBlock = requiredFields.length > 0 ? ` required: [${requiredFields.map((field) => `'${field.key}'`).join(", ")}],
|
|
3146
|
+
` : "";
|
|
3147
|
+
const properties = fields.length ? fields.map((field) => {
|
|
3148
|
+
const description = field.description ? `, description: '${escape3(field.description)}'` : "";
|
|
3149
|
+
return ` ${field.key}: { type: '${mapConfigType(field.type)}'${description} }`;
|
|
3150
|
+
}).join(`,
|
|
3151
|
+
`) : "";
|
|
3152
|
+
return ` schema: {
|
|
3153
|
+
type: 'object',
|
|
3154
|
+
${requiredBlock} properties: {
|
|
3155
|
+
${properties || " "}
|
|
3156
|
+
},
|
|
3157
|
+
},
|
|
3158
|
+
`;
|
|
3159
|
+
}
|
|
3160
|
+
function renderSecretSchema(fields) {
|
|
3161
|
+
const requiredFields = fields.filter((field) => field.required);
|
|
3162
|
+
const requiredBlock = requiredFields.length > 0 ? ` required: [${requiredFields.map((field) => `'${field.key}'`).join(", ")}],
|
|
3163
|
+
` : "";
|
|
3164
|
+
const properties = fields.length ? fields.map((field) => {
|
|
3165
|
+
const description = field.description ? `, description: '${escape3(field.description)}'` : "";
|
|
3166
|
+
return ` ${field.key}: { type: 'string'${description} }`;
|
|
3167
|
+
}).join(`,
|
|
3168
|
+
`) : "";
|
|
3169
|
+
return ` schema: {
|
|
3170
|
+
type: 'object',
|
|
3171
|
+
${requiredBlock} properties: {
|
|
3172
|
+
${properties || " "}
|
|
3173
|
+
},
|
|
3174
|
+
},
|
|
3175
|
+
`;
|
|
3176
|
+
}
|
|
3177
|
+
function renderConfigExample(fields) {
|
|
3178
|
+
if (fields.length === 0) {
|
|
3179
|
+
return `{}`;
|
|
3180
|
+
}
|
|
3181
|
+
const exampleEntries = fields.map((field) => {
|
|
3182
|
+
switch (field.type) {
|
|
3183
|
+
case "number":
|
|
3184
|
+
return ` ${field.key}: 0`;
|
|
3185
|
+
case "boolean":
|
|
3186
|
+
return ` ${field.key}: true`;
|
|
3187
|
+
case "string":
|
|
3188
|
+
default:
|
|
3189
|
+
return ` ${field.key}: '${field.key.toUpperCase()}_VALUE'`;
|
|
3190
|
+
}
|
|
3191
|
+
});
|
|
3192
|
+
return `{
|
|
3193
|
+
${exampleEntries.join(`,
|
|
3194
|
+
`)}
|
|
3195
|
+
}`;
|
|
3196
|
+
}
|
|
3197
|
+
function renderSecretExample(fields) {
|
|
3198
|
+
if (fields.length === 0) {
|
|
3199
|
+
return `{}`;
|
|
3200
|
+
}
|
|
3201
|
+
const exampleEntries = fields.map((field) => ` ${field.key}: '${field.key.toUpperCase()}_SECRET'`);
|
|
3202
|
+
return `{
|
|
3203
|
+
${exampleEntries.join(`,
|
|
3204
|
+
`)}
|
|
3205
|
+
}`;
|
|
3206
|
+
}
|
|
3207
|
+
function renderConstraints(rpm, rph) {
|
|
3208
|
+
if (rpm == null && rph == null)
|
|
3209
|
+
return "";
|
|
3210
|
+
const entries = [];
|
|
3211
|
+
if (rpm != null)
|
|
3212
|
+
entries.push(` rpm: ${rpm}`);
|
|
3213
|
+
if (rph != null)
|
|
3214
|
+
entries.push(` rph: ${rph}`);
|
|
3215
|
+
return ` constraints: {
|
|
3216
|
+
rateLimit: {
|
|
3217
|
+
${entries.join(`,
|
|
3218
|
+
`)}
|
|
3219
|
+
},
|
|
3220
|
+
},
|
|
3221
|
+
`;
|
|
3222
|
+
}
|
|
3223
|
+
function renderByokSetup(modes, instructions, scopes) {
|
|
3224
|
+
if (!modes.includes("byok")) {
|
|
3225
|
+
return "";
|
|
3226
|
+
}
|
|
3227
|
+
const instructionsLine = instructions ? ` setupInstructions: '${escape3(instructions)}',
|
|
3228
|
+
` : "";
|
|
3229
|
+
const scopesLine = scopes && scopes.length ? ` requiredScopes: [${scopes.map((scope) => `'${escape3(scope)}'`).join(", ")}],
|
|
3230
|
+
` : "";
|
|
3231
|
+
if (!instructionsLine && !scopesLine) {
|
|
3232
|
+
return "";
|
|
3233
|
+
}
|
|
3234
|
+
return ` byokSetup: {
|
|
3235
|
+
${instructionsLine}${scopesLine} },
|
|
3236
|
+
`;
|
|
3237
|
+
}
|
|
3238
|
+
function mapConfigType(type) {
|
|
3239
|
+
switch (type) {
|
|
3240
|
+
case "number":
|
|
3241
|
+
return "number";
|
|
3242
|
+
case "boolean":
|
|
3243
|
+
return "boolean";
|
|
3244
|
+
case "string":
|
|
3245
|
+
default:
|
|
3246
|
+
return "string";
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
function stabilityToEnum(stability) {
|
|
3250
|
+
switch (stability) {
|
|
3251
|
+
case "beta":
|
|
3252
|
+
return "Beta";
|
|
3253
|
+
case "stable":
|
|
3254
|
+
return "Stable";
|
|
3255
|
+
case "deprecated":
|
|
3256
|
+
return "Deprecated";
|
|
3257
|
+
case "experimental":
|
|
3258
|
+
default:
|
|
3259
|
+
return "Experimental";
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
function renderProvides(data) {
|
|
3263
|
+
return data.capabilitiesProvided.map((cap) => ` { key: '${cap.key}', version: ${cap.version} }`).join(`,
|
|
3264
|
+
`);
|
|
3265
|
+
}
|
|
3266
|
+
function renderRequires(data) {
|
|
3267
|
+
if (data.capabilitiesRequired.length === 0)
|
|
3268
|
+
return "";
|
|
3269
|
+
return ` requires: [
|
|
3270
|
+
${data.capabilitiesRequired.map((req) => {
|
|
3271
|
+
const version = typeof req.version === "number" ? `, version: ${req.version}` : "";
|
|
3272
|
+
const optional = req.optional ? ", optional: true" : "";
|
|
3273
|
+
const reason = req.reason ? `, reason: '${escape3(req.reason)}'` : "";
|
|
3274
|
+
return ` { key: '${req.key}'${version}${optional}${reason} }`;
|
|
3275
|
+
}).join(`,
|
|
3276
|
+
`)}
|
|
3277
|
+
],`;
|
|
3278
|
+
}
|
|
3279
|
+
function escape3(value) {
|
|
3280
|
+
return value.replace(/`/g, "\\`").replace(/'/g, "\\'");
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
// src/templates/integration.ts
|
|
3284
|
+
function generateIntegrationSpec(data) {
|
|
3285
|
+
const specName = toPascalCase(data.name.split(".").pop() ?? "Integration");
|
|
3286
|
+
const varName = `${specName}IntegrationSpec`;
|
|
3287
|
+
const registerFn = `register${specName}Integration`;
|
|
3288
|
+
const supportedModes = data.supportedModes.length ? data.supportedModes : ["managed"];
|
|
3289
|
+
const supportedModesLine = supportedModes.map((mode) => `'${mode}'`).join(", ");
|
|
3290
|
+
const provides = renderProvides(data);
|
|
3291
|
+
const requires = renderRequires(data);
|
|
3292
|
+
const configSchema = renderConfigSchema(data.configFields);
|
|
3293
|
+
const configExample = renderConfigExample(data.configFields);
|
|
3294
|
+
const secretSchema = renderSecretSchema(data.secretFields);
|
|
3295
|
+
const secretExample = renderSecretExample(data.secretFields);
|
|
3296
|
+
const docsUrl = data.docsUrl ? ` docsUrl: '${escape3(data.docsUrl)}',
|
|
3297
|
+
` : "";
|
|
3298
|
+
const constraints = renderConstraints(data.rateLimitRpm, data.rateLimitRph);
|
|
3299
|
+
const byokSetup = renderByokSetup(supportedModes, data.byokSetupInstructions, data.byokRequiredScopes);
|
|
3300
|
+
return `import { StabilityEnum, defineIntegration } from '@contractspec/lib.contracts';
|
|
3301
|
+
import type { IntegrationSpecRegistry } from '@contractspec/lib.contracts/integrations/spec';
|
|
3302
|
+
|
|
3303
|
+
export const ${varName} = defineIntegration({
|
|
3304
|
+
meta: {
|
|
3305
|
+
key: '${escape3(data.name)}',
|
|
3306
|
+
version: ${data.version},
|
|
3307
|
+
category: '${data.category}',
|
|
3308
|
+
displayName: '${escape3(data.displayName)}',
|
|
3309
|
+
title: '${escape3(data.title)}',
|
|
3310
|
+
description: '${escape3(data.description)}',
|
|
3311
|
+
domain: '${escape3(data.domain)}',
|
|
3312
|
+
owners: [${data.owners.map((owner) => `'${escape3(owner)}'`).join(", ")}],
|
|
3313
|
+
tags: [${data.tags.map((tag) => `'${escape3(tag)}'`).join(", ")}],
|
|
3314
|
+
stability: StabilityEnum.${stabilityToEnum(data.stability)},
|
|
3315
|
+
},
|
|
3316
|
+
supportedModes: [${supportedModesLine}],
|
|
3317
|
+
capabilities: {
|
|
3318
|
+
provides: [
|
|
3319
|
+
${provides}
|
|
3320
|
+
],
|
|
3321
|
+
${requires.length > 0 ? `${requires}
|
|
3322
|
+
` : ""} },
|
|
3323
|
+
configSchema: {
|
|
3324
|
+
${configSchema} example: ${configExample},
|
|
3325
|
+
},
|
|
3326
|
+
secretSchema: {
|
|
3327
|
+
${secretSchema} example: ${secretExample},
|
|
3328
|
+
},
|
|
3329
|
+
${docsUrl}${constraints}${byokSetup} healthCheck: {
|
|
3330
|
+
method: '${data.healthCheckMethod}',
|
|
3331
|
+
timeoutMs: ${data.healthCheckTimeoutMs ?? 5000},
|
|
3332
|
+
},
|
|
3333
|
+
});
|
|
3334
|
+
|
|
3335
|
+
export function ${registerFn}(registry: IntegrationSpecRegistry): IntegrationSpecRegistry {
|
|
3336
|
+
return registry.register(${varName});
|
|
3337
|
+
}
|
|
3338
|
+
`;
|
|
3339
|
+
}
|
|
3340
|
+
// src/templates/knowledge.ts
|
|
3341
|
+
function generateKnowledgeSpaceSpec(data) {
|
|
3342
|
+
const specName = toPascalCase(data.name.split(".").pop() ?? "KnowledgeSpace");
|
|
3343
|
+
const varName = `${specName}KnowledgeSpace`;
|
|
3344
|
+
const registerFn = `register${specName}KnowledgeSpace`;
|
|
3345
|
+
const retention = renderRetention(data);
|
|
3346
|
+
const access = renderAccess(data);
|
|
3347
|
+
const indexing = renderIndexing(data);
|
|
3348
|
+
const policyComment = data.policyName && !data.policyVersion ? ` // defaults to latest version` : "";
|
|
3349
|
+
return `import { StabilityEnum } from '@contractspec/lib.contracts/ownership';
|
|
3350
|
+
import type { KnowledgeSpaceSpec } from '@contractspec/lib.contracts/knowledge/spec';
|
|
3351
|
+
import type { KnowledgeSpaceRegistry } from '@contractspec/lib.contracts/knowledge/spec';
|
|
3352
|
+
|
|
3353
|
+
export const ${varName}: KnowledgeSpaceSpec = {
|
|
3354
|
+
meta: {
|
|
3355
|
+
key: '${escapeString(data.name)}',
|
|
3356
|
+
version: ${data.version},
|
|
3357
|
+
category: '${data.category}',
|
|
3358
|
+
displayName: '${escape4(data.displayName)}',
|
|
3359
|
+
title: '${escape4(data.title)}',
|
|
3360
|
+
description: '${escape4(data.description)}',
|
|
3361
|
+
domain: '${escape4(data.domain)}',
|
|
3362
|
+
owners: [${data.owners.map((owner) => `'${escapeString(owner)}'`).join(", ")}],
|
|
3363
|
+
tags: [${data.tags.map((tag) => `'${escapeString(tag)}'`).join(", ")}],
|
|
3364
|
+
stability: StabilityEnum.${stabilityToEnum2(data.stability)},
|
|
3365
|
+
},
|
|
3366
|
+
retention: ${retention},
|
|
3367
|
+
access: {
|
|
3368
|
+
${access}${data.policyName ? ` policy: { name: '${escapeString(data.policyName)}',${data.policyVersion ? ` version: ${data.policyVersion}` : ""} },${policyComment}
|
|
3369
|
+
` : ""} },
|
|
3370
|
+
${indexing} description: '${escape4(data.description || data.displayName)}',
|
|
3371
|
+
};
|
|
3372
|
+
|
|
3373
|
+
export function ${registerFn}(registry: KnowledgeSpaceRegistry): KnowledgeSpaceRegistry {
|
|
3374
|
+
return registry.register(${varName});
|
|
3375
|
+
}
|
|
3376
|
+
`;
|
|
3377
|
+
}
|
|
3378
|
+
function renderRetention(data) {
|
|
3379
|
+
const ttl = data.retention.ttlDays === null ? "null" : typeof data.retention.ttlDays === "number" ? String(data.retention.ttlDays) : "null";
|
|
3380
|
+
const archive = typeof data.retention.archiveAfterDays === "number" ? `, archiveAfterDays: ${data.retention.archiveAfterDays}` : "";
|
|
3381
|
+
return `{ ttlDays: ${ttl}${archive} }`;
|
|
3382
|
+
}
|
|
3383
|
+
function renderAccess(data) {
|
|
3384
|
+
const trustLine = ` trustLevel: '${data.trustLevel}',
|
|
3385
|
+
`;
|
|
3386
|
+
const automationLine = ` automationWritable: ${data.automationWritable},
|
|
3387
|
+
`;
|
|
3388
|
+
return `${trustLine}${automationLine}`;
|
|
3389
|
+
}
|
|
3390
|
+
function renderIndexing(data) {
|
|
3391
|
+
const entries = [];
|
|
3392
|
+
if (data.embeddingModel) {
|
|
3393
|
+
entries.push(` embeddingModel: '${escape4(data.embeddingModel)}'`);
|
|
3394
|
+
}
|
|
3395
|
+
if (typeof data.chunkSize === "number") {
|
|
3396
|
+
entries.push(` chunkSize: ${data.chunkSize}`);
|
|
3397
|
+
}
|
|
3398
|
+
if (data.vectorDbIntegration) {
|
|
3399
|
+
entries.push(` vectorDbIntegration: '${escape4(data.vectorDbIntegration)}'`);
|
|
3400
|
+
}
|
|
3401
|
+
if (entries.length === 0) {
|
|
3402
|
+
return "";
|
|
3403
|
+
}
|
|
3404
|
+
return ` indexing: {
|
|
3405
|
+
${entries.join(`,
|
|
3406
|
+
`)}
|
|
3407
|
+
},
|
|
3408
|
+
`;
|
|
3409
|
+
}
|
|
3410
|
+
function stabilityToEnum2(stability) {
|
|
3411
|
+
switch (stability) {
|
|
3412
|
+
case "beta":
|
|
3413
|
+
return "Beta";
|
|
3414
|
+
case "stable":
|
|
3415
|
+
return "Stable";
|
|
3416
|
+
case "deprecated":
|
|
3417
|
+
return "Deprecated";
|
|
3418
|
+
case "experimental":
|
|
3419
|
+
default:
|
|
3420
|
+
return "Experimental";
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
function escape4(value) {
|
|
3424
|
+
return value.replace(/`/g, "\\`").replace(/'/g, "\\'");
|
|
3425
|
+
}
|
|
3426
|
+
// src/templates/handler.ts
|
|
3427
|
+
function generateHandlerTemplate(specName, kind) {
|
|
3428
|
+
const handlerName = toCamelCase(specName.split(".").pop() ?? "unknown") + "Handler";
|
|
3429
|
+
const specVarName = toPascalCase(specName.split(".").pop() ?? "Unknown") + "Spec";
|
|
3430
|
+
return `import type { ContractHandler } from '@contractspec/lib.contracts';
|
|
3431
|
+
import { ${specVarName} } from '../contracts/${toKebabCase(specName)}.contracts';
|
|
3432
|
+
|
|
3433
|
+
/**
|
|
3434
|
+
* Handler for ${specName}
|
|
3435
|
+
*/
|
|
3436
|
+
export const ${handlerName}: ContractHandler<typeof ${specVarName}> = async (
|
|
3437
|
+
input,
|
|
3438
|
+
context
|
|
3439
|
+
) => {
|
|
3440
|
+
// TODO: Implement ${kind} logic
|
|
3441
|
+
|
|
3442
|
+
try {
|
|
3443
|
+
// 1. Validate prerequisites
|
|
3444
|
+
// 2. Perform business logic
|
|
3445
|
+
// 3. Emit events if needed
|
|
3446
|
+
// 4. Return result
|
|
3447
|
+
|
|
3448
|
+
return {
|
|
3449
|
+
ok: true,
|
|
3450
|
+
};
|
|
3451
|
+
} catch (error) {
|
|
3452
|
+
// Handle and map errors to spec.io.errors
|
|
3453
|
+
throw error;
|
|
3454
|
+
}
|
|
3455
|
+
};
|
|
3456
|
+
`;
|
|
3457
|
+
}
|
|
3458
|
+
function generateComponentTemplate(componentName, description) {
|
|
3459
|
+
const pascalName = toPascalCase(componentName);
|
|
3460
|
+
return `import React from 'react';
|
|
3461
|
+
|
|
3462
|
+
interface ${pascalName}Props {
|
|
3463
|
+
// TODO: Define props based on presentation spec
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
/**
|
|
3467
|
+
* ${description}
|
|
3468
|
+
*/
|
|
3469
|
+
export const ${pascalName}: React.FC<${pascalName}Props> = (props) => {
|
|
3470
|
+
return (
|
|
3471
|
+
<div>
|
|
3472
|
+
{/* TODO: Implement component UI */}
|
|
3473
|
+
<p>Component: ${pascalName}</p>
|
|
3474
|
+
</div>
|
|
3475
|
+
);
|
|
3476
|
+
};
|
|
3477
|
+
`;
|
|
3478
|
+
}
|
|
3479
|
+
function generateTestTemplate(targetName, type) {
|
|
3480
|
+
const importPath = type === "handler" ? "../handlers" : "../components";
|
|
3481
|
+
const testName = toPascalCase(targetName);
|
|
3482
|
+
return `import { describe, it, expect } from 'bun:test';
|
|
3483
|
+
import { ${testName} } from '${importPath}/${toKebabCase(targetName)}';
|
|
3484
|
+
|
|
3485
|
+
describe('${testName}', () => {
|
|
3486
|
+
it('should ${type === "handler" ? "handle valid input" : "render correctly"}', async () => {
|
|
3487
|
+
// TODO: Implement test
|
|
3488
|
+
expect(true).toBe(true);
|
|
3489
|
+
});
|
|
3490
|
+
|
|
3491
|
+
it('should handle edge cases', async () => {
|
|
3492
|
+
// TODO: Test edge cases
|
|
3493
|
+
});
|
|
3494
|
+
|
|
3495
|
+
${type === "handler" ? `it('should handle errors appropriately', async () => {
|
|
3496
|
+
// TODO: Test error scenarios
|
|
3497
|
+
});` : `it('should be accessible', async () => {
|
|
3498
|
+
// TODO: Test accessibility
|
|
3499
|
+
});`}
|
|
3500
|
+
});
|
|
3501
|
+
`;
|
|
3502
|
+
}
|
|
3503
|
+
// src/ai/prompts/spec-creation.ts
|
|
3504
|
+
function buildOperationSpecPrompt(description, kind) {
|
|
3505
|
+
return `You are a senior software architect creating a contract specification for an operation.
|
|
3506
|
+
|
|
3507
|
+
The operation is a ${kind} (${kind === "command" ? "changes state, has side effects" : "read-only, idempotent"}).
|
|
3508
|
+
|
|
3509
|
+
User description: ${description}
|
|
3510
|
+
|
|
3511
|
+
Create a complete contract specification following these guidelines:
|
|
3512
|
+
|
|
3513
|
+
1. **Name**: Use dot notation like "domain.operationName" (e.g., "user.signup", "payment.charge")
|
|
3514
|
+
2. **Version**: Start at 1
|
|
3515
|
+
3. **Description**: Clear, concise summary (1-2 sentences)
|
|
3516
|
+
4. **Goal**: Business purpose - why this operation exists
|
|
3517
|
+
5. **Context**: Background, constraints, scope (what it does and doesn't do)
|
|
3518
|
+
6. **Input/Output**: Describe the shape (we'll create schemas separately)
|
|
3519
|
+
7. **Auth**: Who can call this - anonymous, user, or admin
|
|
3520
|
+
8. **Feature Flags**: Any flags that gate this operation
|
|
3521
|
+
9. **Side Effects**: What events might be emitted, analytics to track
|
|
3522
|
+
|
|
3523
|
+
Respond with a structured spec.`;
|
|
3524
|
+
}
|
|
3525
|
+
function buildEventSpecPrompt(description) {
|
|
3526
|
+
return `You are a senior software architect creating an event specification.
|
|
3527
|
+
|
|
3528
|
+
User description: ${description}
|
|
3529
|
+
|
|
3530
|
+
Create a complete event specification following these guidelines:
|
|
3531
|
+
|
|
3532
|
+
1. **Name**: Use dot notation like "domain.event_name" (e.g., "user.signup_completed", "payment.charged")
|
|
3533
|
+
2. **Version**: Start at 1
|
|
3534
|
+
3. **Description**: Clear description of when this event is emitted
|
|
3535
|
+
4. **Payload**: Describe what data the event carries
|
|
3536
|
+
5. **PII Fields**: List any personally identifiable information fields (e.g., ["email", "name"])
|
|
3537
|
+
|
|
3538
|
+
Events represent things that have already happened and should use past tense.
|
|
3539
|
+
|
|
3540
|
+
Respond with a structured spec.`;
|
|
3541
|
+
}
|
|
3542
|
+
function buildPresentationSpecPrompt(description, kind) {
|
|
3543
|
+
const kindDescriptions = {
|
|
3544
|
+
web_component: "a React component with props schema",
|
|
3545
|
+
markdown: "markdown/MDX documentation or guide",
|
|
3546
|
+
data: "structured data export (JSON/XML)"
|
|
3547
|
+
};
|
|
3548
|
+
return `You are a senior software architect creating a presentation specification.
|
|
3549
|
+
|
|
3550
|
+
This is a ${kind} presentation - ${kindDescriptions[kind]}.
|
|
3551
|
+
|
|
3552
|
+
User description: ${description}
|
|
3553
|
+
|
|
3554
|
+
Create a complete presentation specification following these guidelines:
|
|
3555
|
+
|
|
3556
|
+
1. **Name**: Use dot notation like "domain.presentation_name" (e.g., "user.profile_card", "docs.api_guide")
|
|
3557
|
+
2. **Version**: Start at 1
|
|
3558
|
+
3. **Description**: What this presentation shows/provides
|
|
3559
|
+
4. **Kind-specific details**:
|
|
3560
|
+
${kind === "web_component" ? `- Component key (symbolic, resolved by host app)
|
|
3561
|
+
- Props structure
|
|
3562
|
+
- Analytics events to track` : kind === "markdown" ? `- Content or resource URI
|
|
3563
|
+
- Target audience` : `- MIME type (e.g., application/json)
|
|
3564
|
+
- Data structure description`}
|
|
3565
|
+
|
|
3566
|
+
Respond with a structured spec.`;
|
|
3567
|
+
}
|
|
3568
|
+
function getSystemPrompt() {
|
|
3569
|
+
return `You are an expert software architect specializing in API design and contract-driven development.
|
|
3570
|
+
|
|
3571
|
+
You create clear, well-documented specifications that serve as the single source of truth for operations, events, and presentations.
|
|
3572
|
+
|
|
3573
|
+
Your specs are:
|
|
3574
|
+
- Precise and unambiguous
|
|
3575
|
+
- Following TypeScript conventions
|
|
3576
|
+
- Business-oriented (capturing the "why" not just "what")
|
|
3577
|
+
- Designed for both humans and AI agents to understand
|
|
3578
|
+
|
|
3579
|
+
Always use proper dot notation for names and ensure all metadata is meaningful and accurate.`;
|
|
3580
|
+
}
|
|
3581
|
+
function addExampleContext(basePrompt, examples) {
|
|
3582
|
+
if (examples.length === 0)
|
|
3583
|
+
return basePrompt;
|
|
3584
|
+
return `${basePrompt}
|
|
3585
|
+
|
|
3586
|
+
Here are some good examples for reference:
|
|
3587
|
+
|
|
3588
|
+
${examples.join(`
|
|
3589
|
+
|
|
3590
|
+
`)}
|
|
3591
|
+
|
|
3592
|
+
Follow this structure and quality level.`;
|
|
3593
|
+
}
|
|
3594
|
+
// src/ai/prompts/code-generation.ts
|
|
3595
|
+
function buildHandlerPrompt(specCode) {
|
|
3596
|
+
return `You are a senior TypeScript developer implementing a handler for a contract specification.
|
|
3597
|
+
|
|
3598
|
+
Here is the contract spec:
|
|
3599
|
+
|
|
3600
|
+
\`\`\`typescript
|
|
3601
|
+
${specCode}
|
|
3602
|
+
\`\`\`
|
|
3603
|
+
|
|
3604
|
+
Generate a complete handler implementation that:
|
|
3605
|
+
|
|
3606
|
+
1. **Matches the spec signature**: Input/output types from the spec
|
|
3607
|
+
2. **Handles errors**: Implement error cases defined in spec.io.errors
|
|
3608
|
+
3. **Emits events**: Use the events declared in spec.sideEffects.emits
|
|
3609
|
+
4. **Validates input**: Use zod validation from the schema
|
|
3610
|
+
5. **Follows best practices**: Clean, type-safe TypeScript
|
|
3611
|
+
6. **Includes comments**: Explain business logic
|
|
3612
|
+
|
|
3613
|
+
The handler should be production-ready with proper error handling, logging points, and clear structure.
|
|
3614
|
+
|
|
3615
|
+
Return only the TypeScript code for the handler function.`;
|
|
3616
|
+
}
|
|
3617
|
+
function buildComponentPrompt(specCode) {
|
|
3618
|
+
return `You are a senior React developer creating a component for a presentation specification.
|
|
3619
|
+
|
|
3620
|
+
Here is the presentation spec:
|
|
3621
|
+
|
|
3622
|
+
\`\`\`typescript
|
|
3623
|
+
${specCode}
|
|
3624
|
+
\`\`\`
|
|
3625
|
+
|
|
3626
|
+
Generate a complete React component that:
|
|
3627
|
+
|
|
3628
|
+
1. **Props interface**: Typed props from the spec
|
|
3629
|
+
2. **Accessibility**: Proper ARIA labels, roles, keyboard navigation
|
|
3630
|
+
3. **Mobile-first**: Optimized for small screens and touch
|
|
3631
|
+
4. **Clean UI**: Simple, intuitive interface
|
|
3632
|
+
5. **Type-safe**: Full TypeScript with no 'any' types
|
|
3633
|
+
6. **Best practices**: React hooks, proper state management
|
|
3634
|
+
|
|
3635
|
+
The component should follow Atomic Design principles and be reusable.
|
|
3636
|
+
|
|
3637
|
+
Return only the TypeScript/TSX code for the component.`;
|
|
3638
|
+
}
|
|
3639
|
+
function buildFormPrompt(specCode) {
|
|
3640
|
+
return `You are a senior React developer creating a form component from a form specification.
|
|
3641
|
+
|
|
3642
|
+
Here is the form spec:
|
|
3643
|
+
|
|
3644
|
+
\`\`\`typescript
|
|
3645
|
+
${specCode}
|
|
3646
|
+
\`\`\`
|
|
3647
|
+
|
|
3648
|
+
Generate a complete form component using react-hook-form that:
|
|
3649
|
+
|
|
3650
|
+
1. **Form validation**: Use zod schema for validation
|
|
3651
|
+
2. **Field types**: Proper inputs for each field type
|
|
3652
|
+
3. **Conditional logic**: Support visibleWhen, enabledWhen, requiredWhen predicates
|
|
3653
|
+
4. **Error handling**: Clear, user-friendly error messages
|
|
3654
|
+
5. **Accessibility**: Labels, hints, ARIA attributes
|
|
3655
|
+
6. **Mobile-optimized**: Touch-friendly, appropriate input types
|
|
3656
|
+
7. **Type-safe**: Full TypeScript
|
|
3657
|
+
|
|
3658
|
+
The form should provide excellent UX with real-time validation and helpful feedback.
|
|
3659
|
+
|
|
3660
|
+
Return only the TypeScript/TSX code for the form component.`;
|
|
3661
|
+
}
|
|
3662
|
+
function buildTestPrompt(specCode, implementationCode, testType) {
|
|
3663
|
+
const testFocus = testType === "handler" ? `
|
|
3664
|
+
- Test all acceptance scenarios from the spec
|
|
3665
|
+
- Test error cases defined in spec.io.errors
|
|
3666
|
+
- Verify events are emitted correctly
|
|
3667
|
+
- Test input validation
|
|
3668
|
+
- Test happy path and edge cases` : `
|
|
3669
|
+
- Test rendering with various props
|
|
3670
|
+
- Test user interactions
|
|
3671
|
+
- Test accessibility (a11y)
|
|
3672
|
+
- Test responsive behavior
|
|
3673
|
+
- Test error states`;
|
|
3674
|
+
return `You are a senior developer writing comprehensive tests.
|
|
3675
|
+
|
|
3676
|
+
Spec:
|
|
3677
|
+
\`\`\`typescript
|
|
3678
|
+
${specCode}
|
|
3679
|
+
\`\`\`
|
|
3680
|
+
|
|
3681
|
+
Implementation:
|
|
3682
|
+
\`\`\`typescript
|
|
3683
|
+
${implementationCode}
|
|
3684
|
+
\`\`\`
|
|
3685
|
+
|
|
3686
|
+
Generate complete test suite using Vitest that:
|
|
3687
|
+
${testFocus}
|
|
3688
|
+
|
|
3689
|
+
Use clear test descriptions and follow AAA pattern (Arrange, Act, Assert).
|
|
3690
|
+
|
|
3691
|
+
Return only the TypeScript test code.`;
|
|
3692
|
+
}
|
|
3693
|
+
function getCodeGenSystemPrompt() {
|
|
3694
|
+
return `You are an expert TypeScript developer with deep knowledge of:
|
|
3695
|
+
- Type-safe API design
|
|
3696
|
+
- React and modern hooks
|
|
3697
|
+
- Test-driven development
|
|
3698
|
+
- Accessibility best practices
|
|
3699
|
+
- Clean code principles
|
|
3700
|
+
|
|
3701
|
+
Generate production-ready code that is:
|
|
3702
|
+
- Fully typed (no 'any' or type assertions unless absolutely necessary)
|
|
3703
|
+
- Well-documented with TSDoc comments
|
|
3704
|
+
- Following project conventions
|
|
3705
|
+
- Defensive and error-safe
|
|
3706
|
+
- Easy to maintain and extend
|
|
3707
|
+
|
|
3708
|
+
Always prioritize code quality, safety, and user experience.`;
|
|
3709
|
+
}
|
|
3710
|
+
// src/formatter.ts
|
|
3711
|
+
import { exec } from "child_process";
|
|
3712
|
+
import { promisify } from "util";
|
|
3713
|
+
import { existsSync } from "fs";
|
|
3714
|
+
import { dirname, resolve } from "path";
|
|
3715
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
3716
|
+
var execAsync = promisify(exec);
|
|
3717
|
+
var FORMATTER_CONFIG_FILES = {
|
|
3718
|
+
prettier: [
|
|
3719
|
+
".prettierrc",
|
|
3720
|
+
".prettierrc.json",
|
|
3721
|
+
".prettierrc.yaml",
|
|
3722
|
+
".prettierrc.yml",
|
|
3723
|
+
".prettierrc.js",
|
|
3724
|
+
".prettierrc.cjs",
|
|
3725
|
+
".prettierrc.mjs",
|
|
3726
|
+
"prettier.config.js",
|
|
3727
|
+
"prettier.config.cjs",
|
|
3728
|
+
"prettier.config.mjs"
|
|
3729
|
+
],
|
|
3730
|
+
eslint: [
|
|
3731
|
+
".eslintrc",
|
|
3732
|
+
".eslintrc.json",
|
|
3733
|
+
".eslintrc.yaml",
|
|
3734
|
+
".eslintrc.yml",
|
|
3735
|
+
".eslintrc.js",
|
|
3736
|
+
".eslintrc.cjs",
|
|
3737
|
+
"eslint.config.js",
|
|
3738
|
+
"eslint.config.mjs",
|
|
3739
|
+
"eslint.config.cjs"
|
|
3740
|
+
],
|
|
3741
|
+
biome: ["biome.json", "biome.jsonc"],
|
|
3742
|
+
dprint: ["dprint.json", ".dprint.json"],
|
|
3743
|
+
custom: []
|
|
3744
|
+
};
|
|
3745
|
+
var FORMATTER_PACKAGES = {
|
|
3746
|
+
prettier: ["prettier"],
|
|
3747
|
+
eslint: ["eslint"],
|
|
3748
|
+
biome: ["@biomejs/biome"],
|
|
3749
|
+
dprint: ["dprint"],
|
|
3750
|
+
custom: []
|
|
3751
|
+
};
|
|
3752
|
+
async function detectFormatter(cwd = process.cwd()) {
|
|
3753
|
+
const priority = ["prettier", "biome", "eslint", "dprint"];
|
|
3754
|
+
for (const formatter of priority) {
|
|
3755
|
+
if (await isFormatterAvailable(formatter, cwd)) {
|
|
3756
|
+
return formatter;
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
return null;
|
|
3760
|
+
}
|
|
3761
|
+
async function isFormatterAvailable(formatter, cwd) {
|
|
3762
|
+
const configFiles = FORMATTER_CONFIG_FILES[formatter] || [];
|
|
3763
|
+
for (const configFile of configFiles) {
|
|
3764
|
+
if (existsSync(resolve(cwd, configFile))) {
|
|
3765
|
+
return true;
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
const packageJsonPath = resolve(cwd, "package.json");
|
|
3769
|
+
if (existsSync(packageJsonPath)) {
|
|
3770
|
+
try {
|
|
3771
|
+
const packageJson = JSON.parse(await readFile2(packageJsonPath, "utf-8"));
|
|
3772
|
+
const allDeps = {
|
|
3773
|
+
...packageJson.dependencies,
|
|
3774
|
+
...packageJson.devDependencies
|
|
3775
|
+
};
|
|
3776
|
+
const packages = FORMATTER_PACKAGES[formatter] || [];
|
|
3777
|
+
for (const pkg of packages) {
|
|
3778
|
+
if (pkg in allDeps) {
|
|
3779
|
+
return true;
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
} catch {}
|
|
3783
|
+
}
|
|
3784
|
+
const parentDir = dirname(cwd);
|
|
3785
|
+
if (parentDir !== cwd && parentDir !== "/") {
|
|
3786
|
+
return isFormatterAvailable(formatter, parentDir);
|
|
3787
|
+
}
|
|
3788
|
+
return false;
|
|
3789
|
+
}
|
|
3790
|
+
function getFormatterCommand(type, files, config) {
|
|
3791
|
+
const fileArgs = files.map((f) => `"${f}"`).join(" ");
|
|
3792
|
+
const extraArgs = config?.args?.join(" ") || "";
|
|
3793
|
+
switch (type) {
|
|
3794
|
+
case "prettier":
|
|
3795
|
+
return `bunx prettier --write ${extraArgs} ${fileArgs}`;
|
|
3796
|
+
case "eslint":
|
|
3797
|
+
return `bunx eslint --fix ${extraArgs} ${fileArgs}`;
|
|
3798
|
+
case "biome":
|
|
3799
|
+
return `bunx @biomejs/biome format --write ${extraArgs} ${fileArgs}`;
|
|
3800
|
+
case "dprint":
|
|
3801
|
+
return `bunx dprint fmt ${extraArgs} ${fileArgs}`;
|
|
3802
|
+
case "custom":
|
|
3803
|
+
if (!config?.command) {
|
|
3804
|
+
throw new Error("Custom formatter requires a command to be specified in config");
|
|
3805
|
+
}
|
|
3806
|
+
return `${config.command} ${extraArgs} ${fileArgs}`;
|
|
3807
|
+
default:
|
|
3808
|
+
throw new Error(`Unknown formatter type: ${type}`);
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
async function formatFiles(files, config, options, logger) {
|
|
3812
|
+
const startTime = Date.now();
|
|
3813
|
+
const fileList = Array.isArray(files) ? files : [files];
|
|
3814
|
+
const cwd = options?.cwd || process.cwd();
|
|
3815
|
+
if (options?.skip || config?.enabled === false) {
|
|
3816
|
+
return {
|
|
3817
|
+
success: true,
|
|
3818
|
+
formatted: false,
|
|
3819
|
+
duration: Date.now() - startTime
|
|
3820
|
+
};
|
|
3821
|
+
}
|
|
3822
|
+
let formatterType = options?.type || config?.type;
|
|
3823
|
+
if (!formatterType) {
|
|
3824
|
+
const detected = await detectFormatter(cwd);
|
|
3825
|
+
if (!detected) {
|
|
3826
|
+
return {
|
|
3827
|
+
success: true,
|
|
3828
|
+
formatted: false,
|
|
3829
|
+
duration: Date.now() - startTime
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3832
|
+
formatterType = detected;
|
|
3833
|
+
}
|
|
3834
|
+
const command = getFormatterCommand(formatterType, fileList, config);
|
|
3835
|
+
const timeout = config?.timeout || 30000;
|
|
3836
|
+
if (!options?.silent && logger) {
|
|
3837
|
+
logger.log(`\uD83C\uDFA8 Formatting ${fileList.length} file(s) with ${formatterType}...`);
|
|
3838
|
+
}
|
|
3839
|
+
try {
|
|
3840
|
+
await execAsync(command, {
|
|
3841
|
+
cwd,
|
|
3842
|
+
timeout
|
|
3843
|
+
});
|
|
3844
|
+
if (!options?.silent && logger) {
|
|
3845
|
+
logger.success(`\u2705 Formatted ${fileList.length} file(s)`);
|
|
3846
|
+
}
|
|
3847
|
+
return {
|
|
3848
|
+
success: true,
|
|
3849
|
+
formatted: true,
|
|
3850
|
+
duration: Date.now() - startTime,
|
|
3851
|
+
formatterUsed: formatterType
|
|
3852
|
+
};
|
|
3853
|
+
} catch (error) {
|
|
3854
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3855
|
+
if (!options?.silent && logger) {
|
|
3856
|
+
logger.warn(`\u26A0\uFE0F Formatting failed (continuing anyway): ${errorMessage}`);
|
|
3857
|
+
}
|
|
3858
|
+
return {
|
|
3859
|
+
success: false,
|
|
3860
|
+
formatted: false,
|
|
3861
|
+
error: errorMessage,
|
|
3862
|
+
duration: Date.now() - startTime,
|
|
3863
|
+
formatterUsed: formatterType
|
|
3864
|
+
};
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
async function formatFilesBatch(files, config, options, logger) {
|
|
3868
|
+
return formatFiles(files, config, options, logger);
|
|
3869
|
+
}
|
|
3870
|
+
// src/formatters/spec-markdown.ts
|
|
3871
|
+
import * as path from "path";
|
|
3872
|
+
function specToMarkdown(spec, format, optionsOrDepth = 0) {
|
|
3873
|
+
const options = typeof optionsOrDepth === "number" ? { depth: optionsOrDepth } : optionsOrDepth;
|
|
3874
|
+
const depth = options.depth ?? 0;
|
|
3875
|
+
const rootPath = options.rootPath;
|
|
3876
|
+
const lines = [];
|
|
3877
|
+
const indent = depth > 0 ? " ".repeat(depth) : "";
|
|
3878
|
+
const headerLevel = Math.min(depth + 1, 6);
|
|
3879
|
+
const headerPrefix = "#".repeat(headerLevel);
|
|
3880
|
+
lines.push(`${indent}${headerPrefix} ${spec.meta.key}`);
|
|
3881
|
+
lines.push("");
|
|
3882
|
+
if (spec.meta.description) {
|
|
3883
|
+
lines.push(`${indent}${spec.meta.description}`);
|
|
3884
|
+
lines.push("");
|
|
3885
|
+
}
|
|
3886
|
+
if (format === "context") {
|
|
3887
|
+
return formatContextMode(spec, lines, indent);
|
|
3888
|
+
}
|
|
3889
|
+
if (format === "prompt") {
|
|
3890
|
+
return formatPromptMode(spec, lines, indent);
|
|
3891
|
+
}
|
|
3892
|
+
return formatFullMode(spec, lines, indent, rootPath);
|
|
3893
|
+
}
|
|
3894
|
+
function formatContextMode(spec, lines, indent) {
|
|
3895
|
+
const metaParts = [];
|
|
3896
|
+
metaParts.push(`**${spec.specType}**`);
|
|
3897
|
+
if (spec.kind && spec.kind !== "unknown")
|
|
3898
|
+
metaParts.push(`(${spec.kind})`);
|
|
3899
|
+
metaParts.push(`v${spec.meta.version}`);
|
|
3900
|
+
if (spec.meta.stability)
|
|
3901
|
+
metaParts.push(`[${spec.meta.stability}]`);
|
|
3902
|
+
lines.push(`${indent}${metaParts.join(" ")}`);
|
|
3903
|
+
lines.push("");
|
|
3904
|
+
if (spec.specType === "feature") {
|
|
3905
|
+
const counts = [];
|
|
3906
|
+
if (spec.operations?.length)
|
|
3907
|
+
counts.push(`${spec.operations.length} operation(s)`);
|
|
3908
|
+
if (spec.events?.length)
|
|
3909
|
+
counts.push(`${spec.events.length} event(s)`);
|
|
3910
|
+
if (spec.presentations?.length)
|
|
3911
|
+
counts.push(`${spec.presentations.length} presentation(s)`);
|
|
3912
|
+
if (counts.length > 0) {
|
|
3913
|
+
lines.push(`${indent}Contains: ${counts.join(", ")}`);
|
|
3914
|
+
lines.push("");
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
if (spec.specType === "operation") {
|
|
3918
|
+
const indicators = [];
|
|
3919
|
+
if (spec.hasIo)
|
|
3920
|
+
indicators.push("I/O");
|
|
3921
|
+
if (spec.hasPolicy)
|
|
3922
|
+
indicators.push("Policy");
|
|
3923
|
+
if (spec.emittedEvents?.length)
|
|
3924
|
+
indicators.push(`${spec.emittedEvents.length} event(s)`);
|
|
3925
|
+
if (indicators.length > 0) {
|
|
3926
|
+
lines.push(`${indent}Includes: ${indicators.join(", ")}`);
|
|
3927
|
+
lines.push("");
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
return lines.join(`
|
|
3931
|
+
`);
|
|
3932
|
+
}
|
|
3933
|
+
function formatPromptMode(spec, lines, indent) {
|
|
3934
|
+
lines.push(`${indent}**Type**: ${spec.specType}${spec.kind && spec.kind !== "unknown" ? ` (${spec.kind})` : ""}`);
|
|
3935
|
+
lines.push(`${indent}**Version**: ${spec.meta.version}`);
|
|
3936
|
+
if (spec.meta.stability)
|
|
3937
|
+
lines.push(`${indent}**Stability**: ${spec.meta.stability}`);
|
|
3938
|
+
if (spec.meta.owners?.length)
|
|
3939
|
+
lines.push(`${indent}**Owners**: ${spec.meta.owners.join(", ")}`);
|
|
3940
|
+
if (spec.meta.tags?.length)
|
|
3941
|
+
lines.push(`${indent}**Tags**: ${spec.meta.tags.join(", ")}`);
|
|
3942
|
+
lines.push("");
|
|
3943
|
+
if (spec.meta.goal) {
|
|
3944
|
+
lines.push(`${indent}**Goal**: ${spec.meta.goal}`);
|
|
3945
|
+
lines.push("");
|
|
3946
|
+
}
|
|
3947
|
+
if (spec.meta.context) {
|
|
3948
|
+
lines.push(`${indent}**Context**: ${spec.meta.context}`);
|
|
3949
|
+
lines.push("");
|
|
3950
|
+
}
|
|
3951
|
+
if (spec.specType === "operation") {
|
|
3952
|
+
const structure = [];
|
|
3953
|
+
if (spec.hasIo)
|
|
3954
|
+
structure.push("Input/Output Schema");
|
|
3955
|
+
if (spec.hasPolicy)
|
|
3956
|
+
structure.push("Policy Enforcement");
|
|
3957
|
+
if (spec.hasPayload)
|
|
3958
|
+
structure.push("Payload");
|
|
3959
|
+
if (spec.hasContent)
|
|
3960
|
+
structure.push("Content");
|
|
3961
|
+
if (structure.length > 0) {
|
|
3962
|
+
lines.push(`${indent}**Structure**: ${structure.join(", ")}`);
|
|
3963
|
+
lines.push("");
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
if (spec.specType === "feature") {
|
|
3967
|
+
appendFeatureRefs(spec, lines, indent);
|
|
3968
|
+
}
|
|
3969
|
+
if (spec.emittedEvents?.length) {
|
|
3970
|
+
lines.push(`${indent}**Emits**: ${formatRefs(spec.emittedEvents)}`);
|
|
3971
|
+
lines.push("");
|
|
3972
|
+
}
|
|
3973
|
+
lines.push(`${indent}---`);
|
|
3974
|
+
lines.push("");
|
|
3975
|
+
lines.push(`${indent}**Implementation Instructions**:`);
|
|
3976
|
+
lines.push("");
|
|
3977
|
+
appendImplementationInstructions(spec, lines, indent);
|
|
3978
|
+
return lines.join(`
|
|
3979
|
+
`);
|
|
3980
|
+
}
|
|
3981
|
+
function formatFullMode(spec, lines, indent, rootPath) {
|
|
3982
|
+
lines.push(`${indent}## Metadata`);
|
|
3983
|
+
lines.push("");
|
|
3984
|
+
lines.push(`${indent}- **Type**: ${spec.specType}${spec.kind && spec.kind !== "unknown" ? ` (${spec.kind})` : ""}`);
|
|
3985
|
+
lines.push(`${indent}- **Version**: ${spec.meta.version}`);
|
|
3986
|
+
if (spec.meta.stability) {
|
|
3987
|
+
lines.push(`${indent}- **Stability**: ${spec.meta.stability}`);
|
|
3988
|
+
}
|
|
3989
|
+
if (spec.meta.owners?.length) {
|
|
3990
|
+
lines.push(`${indent}- **Owners**: ${spec.meta.owners.join(", ")}`);
|
|
3991
|
+
}
|
|
3992
|
+
if (spec.meta.tags?.length) {
|
|
3993
|
+
lines.push(`${indent}- **Tags**: ${spec.meta.tags.join(", ")}`);
|
|
3994
|
+
}
|
|
3995
|
+
if (spec.filePath) {
|
|
3996
|
+
const displayPath = rootPath ? path.relative(rootPath, spec.filePath) : spec.filePath;
|
|
3997
|
+
lines.push(`${indent}- **File**: \`${displayPath}\``);
|
|
3998
|
+
}
|
|
3999
|
+
lines.push("");
|
|
4000
|
+
if (spec.meta.goal) {
|
|
4001
|
+
lines.push(`${indent}## Goal`);
|
|
4002
|
+
lines.push("");
|
|
4003
|
+
lines.push(`${indent}${spec.meta.goal}`);
|
|
4004
|
+
lines.push("");
|
|
4005
|
+
}
|
|
4006
|
+
if (spec.meta.context) {
|
|
4007
|
+
lines.push(`${indent}## Context`);
|
|
4008
|
+
lines.push("");
|
|
4009
|
+
lines.push(`${indent}${spec.meta.context}`);
|
|
4010
|
+
lines.push("");
|
|
4011
|
+
}
|
|
4012
|
+
if (spec.specType === "feature") {
|
|
4013
|
+
appendFeatureSections(spec, lines, indent);
|
|
4014
|
+
}
|
|
4015
|
+
if (spec.emittedEvents?.length) {
|
|
4016
|
+
lines.push(`${indent}## Emitted Events`);
|
|
4017
|
+
lines.push("");
|
|
4018
|
+
for (const ev of spec.emittedEvents) {
|
|
4019
|
+
lines.push(`${indent}- \`${ev.name}\` (v${ev.version})`);
|
|
4020
|
+
}
|
|
4021
|
+
lines.push("");
|
|
4022
|
+
}
|
|
4023
|
+
if (spec.policyRefs?.length) {
|
|
4024
|
+
lines.push(`${indent}## Policy References`);
|
|
4025
|
+
lines.push("");
|
|
4026
|
+
for (const policy of spec.policyRefs) {
|
|
4027
|
+
lines.push(`${indent}- \`${policy.name}\` (v${policy.version})`);
|
|
4028
|
+
}
|
|
4029
|
+
lines.push("");
|
|
4030
|
+
}
|
|
4031
|
+
if (spec.testRefs?.length) {
|
|
4032
|
+
lines.push(`${indent}## Test Specifications`);
|
|
4033
|
+
lines.push("");
|
|
4034
|
+
for (const test of spec.testRefs) {
|
|
4035
|
+
lines.push(`${indent}- \`${test.name}\` (v${test.version})`);
|
|
4036
|
+
}
|
|
4037
|
+
lines.push("");
|
|
4038
|
+
}
|
|
4039
|
+
if (spec.sourceBlock) {
|
|
4040
|
+
lines.push(`${indent}## Source Definition`);
|
|
4041
|
+
lines.push("");
|
|
4042
|
+
lines.push(`${indent}\`\`\`typescript`);
|
|
4043
|
+
const sourceLines = spec.sourceBlock.split(`
|
|
4044
|
+
`);
|
|
4045
|
+
for (const sourceLine of sourceLines) {
|
|
4046
|
+
lines.push(`${indent}${sourceLine}`);
|
|
4047
|
+
}
|
|
4048
|
+
lines.push(`${indent}\`\`\``);
|
|
4049
|
+
lines.push("");
|
|
4050
|
+
}
|
|
4051
|
+
return lines.join(`
|
|
4052
|
+
`);
|
|
4053
|
+
}
|
|
4054
|
+
function formatRefs(refs) {
|
|
4055
|
+
return refs.map((r) => `\`${r.name}\``).join(", ");
|
|
4056
|
+
}
|
|
4057
|
+
function appendFeatureRefs(spec, lines, indent) {
|
|
4058
|
+
if (spec.operations?.length) {
|
|
4059
|
+
lines.push(`${indent}**Operations**: ${formatRefs(spec.operations)}`);
|
|
4060
|
+
}
|
|
4061
|
+
if (spec.events?.length) {
|
|
4062
|
+
lines.push(`${indent}**Events**: ${formatRefs(spec.events)}`);
|
|
4063
|
+
}
|
|
4064
|
+
if (spec.presentations?.length) {
|
|
4065
|
+
lines.push(`${indent}**Presentations**: ${formatRefs(spec.presentations)}`);
|
|
4066
|
+
}
|
|
4067
|
+
lines.push("");
|
|
4068
|
+
}
|
|
4069
|
+
function appendFeatureSections(spec, lines, indent) {
|
|
4070
|
+
if (spec.operations?.length) {
|
|
4071
|
+
lines.push(`${indent}## Operations (${spec.operations.length})`);
|
|
4072
|
+
lines.push("");
|
|
4073
|
+
for (const op of spec.operations) {
|
|
4074
|
+
lines.push(`${indent}- \`${op.name}\` (v${op.version})`);
|
|
4075
|
+
}
|
|
4076
|
+
lines.push("");
|
|
4077
|
+
}
|
|
4078
|
+
if (spec.events?.length) {
|
|
4079
|
+
lines.push(`${indent}## Events (${spec.events.length})`);
|
|
4080
|
+
lines.push("");
|
|
4081
|
+
for (const ev of spec.events) {
|
|
4082
|
+
lines.push(`${indent}- \`${ev.name}\` (v${ev.version})`);
|
|
4083
|
+
}
|
|
4084
|
+
lines.push("");
|
|
4085
|
+
}
|
|
4086
|
+
if (spec.presentations?.length) {
|
|
4087
|
+
lines.push(`${indent}## Presentations (${spec.presentations.length})`);
|
|
4088
|
+
lines.push("");
|
|
4089
|
+
for (const pres of spec.presentations) {
|
|
4090
|
+
lines.push(`${indent}- \`${pres.name}\` (v${pres.version})`);
|
|
4091
|
+
}
|
|
4092
|
+
lines.push("");
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
function appendImplementationInstructions(spec, lines, indent) {
|
|
4096
|
+
if (spec.specType === "operation") {
|
|
4097
|
+
lines.push(`${indent}Implement the \`${spec.meta.key}\` ${spec.kind ?? "operation"} ensuring:`);
|
|
4098
|
+
if (spec.hasIo) {
|
|
4099
|
+
lines.push(`${indent}- Input validation per schema`);
|
|
4100
|
+
lines.push(`${indent}- Output matches expected schema`);
|
|
4101
|
+
}
|
|
4102
|
+
if (spec.hasPolicy) {
|
|
4103
|
+
lines.push(`${indent}- Policy rules are enforced`);
|
|
4104
|
+
}
|
|
4105
|
+
if (spec.emittedEvents?.length) {
|
|
4106
|
+
lines.push(`${indent}- Events are emitted on success`);
|
|
4107
|
+
}
|
|
4108
|
+
} else if (spec.specType === "feature") {
|
|
4109
|
+
lines.push(`${indent}Implement the \`${spec.meta.key}\` feature including:`);
|
|
4110
|
+
if (spec.operations?.length) {
|
|
4111
|
+
lines.push(`${indent}- ${spec.operations.length} operation(s)`);
|
|
4112
|
+
}
|
|
4113
|
+
if (spec.presentations?.length) {
|
|
4114
|
+
lines.push(`${indent}- ${spec.presentations.length} presentation(s)`);
|
|
4115
|
+
}
|
|
4116
|
+
} else if (spec.specType === "event") {
|
|
4117
|
+
lines.push(`${indent}Implement the \`${spec.meta.key}\` event ensuring:`);
|
|
4118
|
+
lines.push(`${indent}- Payload matches expected schema`);
|
|
4119
|
+
lines.push(`${indent}- Event is properly typed`);
|
|
4120
|
+
} else if (spec.specType === "presentation") {
|
|
4121
|
+
lines.push(`${indent}Implement the \`${spec.meta.key}\` presentation ensuring:`);
|
|
4122
|
+
lines.push(`${indent}- Component renders correctly`);
|
|
4123
|
+
lines.push(`${indent}- Accessibility requirements are met`);
|
|
4124
|
+
} else {
|
|
4125
|
+
lines.push(`${indent}Implement the \`${spec.meta.key}\` ${spec.specType} according to the specification.`);
|
|
4126
|
+
}
|
|
4127
|
+
lines.push("");
|
|
4128
|
+
}
|
|
4129
|
+
function specToMarkdownWithOptions(spec, options) {
|
|
4130
|
+
return specToMarkdown(spec, options.format, options.depth ?? 0);
|
|
4131
|
+
}
|
|
4132
|
+
function generateSpecsSummaryHeader(specs, format) {
|
|
4133
|
+
const lines = [];
|
|
4134
|
+
lines.push("# ContractSpec Export");
|
|
4135
|
+
lines.push("");
|
|
4136
|
+
lines.push(`**Format**: ${format}`);
|
|
4137
|
+
lines.push(`**Specs**: ${specs.length}`);
|
|
4138
|
+
lines.push("");
|
|
4139
|
+
const byType = new Map;
|
|
4140
|
+
for (const spec of specs) {
|
|
4141
|
+
byType.set(spec.specType, (byType.get(spec.specType) ?? 0) + 1);
|
|
4142
|
+
}
|
|
4143
|
+
if (byType.size > 0) {
|
|
4144
|
+
lines.push("**Contents**:");
|
|
4145
|
+
for (const [type, count] of byType) {
|
|
4146
|
+
lines.push(`- ${count} ${type}(s)`);
|
|
4147
|
+
}
|
|
4148
|
+
lines.push("");
|
|
4149
|
+
}
|
|
4150
|
+
lines.push("---");
|
|
4151
|
+
lines.push("");
|
|
4152
|
+
return lines.join(`
|
|
4153
|
+
`);
|
|
4154
|
+
}
|
|
4155
|
+
function combineSpecMarkdowns(specs, format) {
|
|
4156
|
+
const header = generateSpecsSummaryHeader(specs, format);
|
|
4157
|
+
const specMarkdowns = specs.map((spec) => specToMarkdown(spec, format));
|
|
4158
|
+
return header + specMarkdowns.join(`
|
|
4159
|
+
---
|
|
4160
|
+
|
|
4161
|
+
`);
|
|
4162
|
+
}
|
|
4163
|
+
// src/formatters/spec-to-docblock.ts
|
|
4164
|
+
function convertSpecToDocBlock(spec, options) {
|
|
4165
|
+
const body = specToMarkdown(spec, "full", { rootPath: options?.rootPath });
|
|
4166
|
+
return {
|
|
4167
|
+
id: spec.meta.key,
|
|
4168
|
+
title: spec.meta.description ? `${spec.meta.key} - ${spec.meta.description}` : spec.meta.key,
|
|
4169
|
+
body,
|
|
4170
|
+
summary: spec.meta.description,
|
|
4171
|
+
kind: mapSpecTypeToDocKind(spec.specType),
|
|
4172
|
+
visibility: "public",
|
|
4173
|
+
version: spec.meta.version,
|
|
4174
|
+
tags: spec.meta.tags,
|
|
4175
|
+
owners: spec.meta.owners,
|
|
4176
|
+
stability: spec.meta.stability,
|
|
4177
|
+
domain: inferDomain(spec.meta.key),
|
|
4178
|
+
relatedSpecs: extractRelatedSpecs(spec)
|
|
4179
|
+
};
|
|
4180
|
+
}
|
|
4181
|
+
function mapSpecTypeToDocKind(specType) {
|
|
4182
|
+
switch (specType) {
|
|
4183
|
+
case "feature":
|
|
4184
|
+
return "goal";
|
|
4185
|
+
case "operation":
|
|
4186
|
+
return "reference";
|
|
4187
|
+
case "event":
|
|
4188
|
+
return "reference";
|
|
4189
|
+
case "presentation":
|
|
4190
|
+
return "usage";
|
|
4191
|
+
default:
|
|
4192
|
+
return "reference";
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
function inferDomain(key) {
|
|
4196
|
+
const parts = key.split(".");
|
|
4197
|
+
if (parts.length > 2) {
|
|
4198
|
+
return parts[0];
|
|
4199
|
+
}
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4202
|
+
function extractRelatedSpecs(spec) {
|
|
4203
|
+
const related = [];
|
|
4204
|
+
if (spec.emittedEvents) {
|
|
4205
|
+
related.push(...spec.emittedEvents.map((r) => r.name));
|
|
4206
|
+
}
|
|
4207
|
+
if (spec.operations) {
|
|
4208
|
+
related.push(...spec.operations.map((r) => r.name));
|
|
4209
|
+
}
|
|
4210
|
+
if (spec.events) {
|
|
4211
|
+
related.push(...spec.events.map((r) => r.name));
|
|
4212
|
+
}
|
|
4213
|
+
if (spec.testRefs) {
|
|
4214
|
+
related.push(...spec.testRefs.map((r) => r.name));
|
|
4215
|
+
}
|
|
4216
|
+
return [...new Set(related)];
|
|
4217
|
+
}
|
|
4218
|
+
export {
|
|
4219
|
+
validateSpecStructure,
|
|
4220
|
+
toPascalCase,
|
|
4221
|
+
toKebabCase,
|
|
4222
|
+
toDot,
|
|
4223
|
+
toCanonicalJson,
|
|
4224
|
+
toCamelCase,
|
|
4225
|
+
specToMarkdownWithOptions,
|
|
4226
|
+
specToMarkdown,
|
|
4227
|
+
sortSpecs,
|
|
4228
|
+
sortFields,
|
|
4229
|
+
scanSpecSource,
|
|
4230
|
+
scanFeatureSource,
|
|
4231
|
+
scanExampleSource,
|
|
4232
|
+
scanAllSpecsFromSource,
|
|
4233
|
+
parseImportedSpecNames,
|
|
4234
|
+
normalizeValue,
|
|
4235
|
+
loadSpecFromSource,
|
|
4236
|
+
isFeatureFile,
|
|
4237
|
+
isExampleFile,
|
|
4238
|
+
isBreakingChange,
|
|
4239
|
+
inferSpecTypeFromFilePath,
|
|
4240
|
+
inferSpecTypeFromCodeBlock,
|
|
4241
|
+
groupSpecsToArray,
|
|
4242
|
+
groupSpecs,
|
|
4243
|
+
getUniqueSpecTags,
|
|
4244
|
+
getUniqueSpecOwners,
|
|
4245
|
+
getUniqueSpecDomains,
|
|
4246
|
+
getSystemPrompt,
|
|
4247
|
+
getRulesBySeverity,
|
|
4248
|
+
getCodeGenSystemPrompt,
|
|
4249
|
+
generateWorkflowSpec,
|
|
4250
|
+
generateWorkflowRunnerTemplate,
|
|
4251
|
+
generateTestTemplate,
|
|
4252
|
+
generateTelemetrySpec,
|
|
4253
|
+
generateSpecsSummaryHeader,
|
|
4254
|
+
generateSnapshot,
|
|
4255
|
+
generatePresentationSpec,
|
|
4256
|
+
generateOperationSpec,
|
|
4257
|
+
generateMigrationSpec,
|
|
4258
|
+
generateKnowledgeSpaceSpec,
|
|
4259
|
+
generateIntegrationSpec,
|
|
4260
|
+
generateHandlerTemplate,
|
|
4261
|
+
generateExperimentSpec,
|
|
4262
|
+
generateEventSpec,
|
|
4263
|
+
generateDataViewSpec,
|
|
4264
|
+
generateComponentTemplate,
|
|
4265
|
+
generateAppBlueprintSpec,
|
|
4266
|
+
formatFilesBatch,
|
|
4267
|
+
formatFiles,
|
|
4268
|
+
findMissingDependencies,
|
|
4269
|
+
findMatchingRule,
|
|
4270
|
+
filterSpecs,
|
|
4271
|
+
filterFeatures,
|
|
4272
|
+
extractTestTarget,
|
|
4273
|
+
extractTestCoverage,
|
|
4274
|
+
escapeString,
|
|
4275
|
+
detectFormatter,
|
|
4276
|
+
detectCycles,
|
|
4277
|
+
createContractGraph,
|
|
4278
|
+
convertSpecToDocBlock,
|
|
4279
|
+
computeSemanticDiff,
|
|
4280
|
+
computeIoDiff,
|
|
4281
|
+
computeHash,
|
|
4282
|
+
computeFieldsDiff,
|
|
4283
|
+
computeFieldDiff,
|
|
4284
|
+
combineSpecMarkdowns,
|
|
4285
|
+
classifyImpact,
|
|
4286
|
+
capitalize,
|
|
4287
|
+
buildTestPrompt,
|
|
4288
|
+
buildReverseEdges,
|
|
4289
|
+
buildPresentationSpecPrompt,
|
|
4290
|
+
buildOperationSpecPrompt,
|
|
4291
|
+
buildHandlerPrompt,
|
|
4292
|
+
buildFormPrompt,
|
|
4293
|
+
buildEventSpecPrompt,
|
|
4294
|
+
buildComponentPrompt,
|
|
4295
|
+
addExampleContext,
|
|
4296
|
+
addContractNode,
|
|
4297
|
+
SpecGroupingStrategies,
|
|
4298
|
+
NON_BREAKING_RULES,
|
|
4299
|
+
INFO_RULES,
|
|
4300
|
+
DEFAULT_RULES,
|
|
4301
|
+
BREAKING_RULES
|
|
4302
|
+
};
|