@contractspec/module.workspace 1.46.2 → 1.47.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/analysis/deps/graph.js.map +1 -1
- package/dist/analysis/deps/parse-imports.js.map +1 -1
- package/dist/analysis/diff/deep-diff.js.map +1 -1
- package/dist/analysis/diff/semantic.js.map +1 -1
- package/dist/analysis/example-scan.d.ts.map +1 -1
- package/dist/analysis/example-scan.js +2 -37
- package/dist/analysis/example-scan.js.map +1 -1
- package/dist/analysis/feature-extractor.js +203 -0
- package/dist/analysis/feature-extractor.js.map +1 -0
- package/dist/analysis/feature-scan.d.ts.map +1 -1
- package/dist/analysis/feature-scan.js +20 -121
- package/dist/analysis/feature-scan.js.map +1 -1
- package/dist/analysis/impact/classifier.js.map +1 -1
- package/dist/analysis/impact/rules.js.map +1 -1
- package/dist/analysis/index.js +3 -1
- package/dist/analysis/snapshot/normalizer.js.map +1 -1
- package/dist/analysis/snapshot/snapshot.js.map +1 -1
- package/dist/analysis/spec-parsing-utils.d.ts +26 -0
- package/dist/analysis/spec-parsing-utils.d.ts.map +1 -0
- package/dist/analysis/spec-parsing-utils.js +98 -0
- package/dist/analysis/spec-parsing-utils.js.map +1 -0
- package/dist/analysis/spec-scan.d.ts +8 -22
- package/dist/analysis/spec-scan.d.ts.map +1 -1
- package/dist/analysis/spec-scan.js +105 -337
- package/dist/analysis/spec-scan.js.map +1 -1
- package/dist/analysis/utils/matchers.js +77 -0
- package/dist/analysis/utils/matchers.js.map +1 -0
- package/dist/analysis/utils/variables.js +45 -0
- package/dist/analysis/utils/variables.js.map +1 -0
- package/dist/analysis/validate/index.js +1 -0
- package/dist/analysis/validate/spec-structure.d.ts.map +1 -1
- package/dist/analysis/validate/spec-structure.js +401 -85
- package/dist/analysis/validate/spec-structure.js.map +1 -1
- package/dist/formatter.js.map +1 -1
- package/dist/formatters/index.js +2 -0
- package/dist/formatters/spec-markdown.d.ts +4 -1
- package/dist/formatters/spec-markdown.d.ts.map +1 -1
- package/dist/formatters/spec-markdown.js +12 -4
- package/dist/formatters/spec-markdown.js.map +1 -1
- package/dist/formatters/spec-to-docblock.d.ts +3 -1
- package/dist/formatters/spec-to-docblock.d.ts.map +1 -1
- package/dist/formatters/spec-to-docblock.js +2 -2
- package/dist/formatters/spec-to-docblock.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -2
- package/dist/templates/integration-utils.js.map +1 -1
- package/dist/templates/integration.js +3 -4
- package/dist/templates/integration.js.map +1 -1
- package/dist/templates/knowledge.js.map +1 -1
- package/dist/templates/workflow.js.map +1 -1
- package/dist/types/analysis-types.d.ts +24 -3
- package/dist/types/analysis-types.d.ts.map +1 -1
- package/dist/types/generation-types.js.map +1 -1
- package/dist/types/llm-types.d.ts +1 -1
- package/dist/types/llm-types.d.ts.map +1 -1
- package/package.json +9 -10
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { findMatchingDelimiter } from "./matchers.js";
|
|
2
|
+
|
|
3
|
+
//#region src/analysis/utils/variables.ts
|
|
4
|
+
/**
|
|
5
|
+
* Variable resolution utilities.
|
|
6
|
+
* Handles simple static variable extraction and substitution in source blocks.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Extract top-level array constants from source code.
|
|
10
|
+
* Example: const OWNERS = ['alice', 'bob'] as const;
|
|
11
|
+
*/
|
|
12
|
+
function extractArrayConstants(code) {
|
|
13
|
+
const variables = /* @__PURE__ */ new Map();
|
|
14
|
+
const regex = /const\s+(\w+)\s*=\s*\[/g;
|
|
15
|
+
let match;
|
|
16
|
+
while ((match = regex.exec(code)) !== null) {
|
|
17
|
+
const name = match[1];
|
|
18
|
+
const startIndex = match.index + match[0].length - 1;
|
|
19
|
+
const endIndex = findMatchingDelimiter(code, startIndex, "[", "]");
|
|
20
|
+
if (endIndex !== -1) {
|
|
21
|
+
const value = code.substring(startIndex, endIndex + 1);
|
|
22
|
+
if (name) variables.set(name, value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return variables;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Substitute spread variables in a source block with their resolved values.
|
|
29
|
+
* Example: owners: [...OWNERS] -> owners: ['alice', 'bob']
|
|
30
|
+
*/
|
|
31
|
+
function resolveVariablesInBlock(block, variables) {
|
|
32
|
+
if (variables.size === 0) return block;
|
|
33
|
+
return block.replace(/\.\.\.(\w+)/g, (match, name) => {
|
|
34
|
+
const value = variables.get(name);
|
|
35
|
+
if (value) {
|
|
36
|
+
if (value.startsWith("[") && value.endsWith("]")) return value.substring(1, value.length - 1);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
return match;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { extractArrayConstants, resolveVariablesInBlock };
|
|
45
|
+
//# sourceMappingURL=variables.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"variables.js","names":[],"sources":["../../../src/analysis/utils/variables.ts"],"sourcesContent":["/**\n * Variable resolution utilities.\n * Handles simple static variable extraction and substitution in source blocks.\n */\n\nimport { findMatchingDelimiter } from './matchers';\n\n/**\n * Extract top-level array constants from source code.\n * Example: const OWNERS = ['alice', 'bob'] as const;\n */\nexport function extractArrayConstants(code: string): Map<string, string> {\n const variables = new Map<string, string>();\n\n // Regex to find potential array constants\n // Matches: const NAME = [ ...\n const regex = /const\\s+(\\w+)\\s*=\\s*\\[/g;\n let match;\n\n while ((match = regex.exec(code)) !== null) {\n const name = match[1];\n const startIndex = match.index + match[0].length - 1; // pointing to [\n const endIndex = findMatchingDelimiter(code, startIndex, '[', ']');\n\n if (endIndex !== -1) {\n // Extract the full array string: ['a', 'b']\n const value = code.substring(startIndex, endIndex + 1);\n if (name) {\n variables.set(name, value);\n }\n }\n }\n\n return variables;\n}\n\n/**\n * Substitute spread variables in a source block with their resolved values.\n * Example: owners: [...OWNERS] -> owners: ['alice', 'bob']\n */\nexport function resolveVariablesInBlock(\n block: string,\n variables: Map<string, string>\n): string {\n if (variables.size === 0) return block;\n\n // Look for spreads: ...NAME\n return block.replace(/\\.\\.\\.(\\w+)/g, (match, name) => {\n const value = variables.get(name);\n if (value) {\n // Remove the surrounding brackets from the value if we are spreading into an array\n // But ... is also used in objects.\n // In the array case: owners: [...OWNERS] -> owners: ['a', 'b']\n // OWNERS = ['a', 'b']\n\n // We need to strip the outer [ and ] from the value to \"spread\" it\n if (value.startsWith('[') && value.endsWith(']')) {\n return value.substring(1, value.length - 1);\n }\n return value;\n }\n return match;\n });\n}\n"],"mappings":";;;;;;;;;;;AAWA,SAAgB,sBAAsB,MAAmC;CACvE,MAAM,4BAAY,IAAI,KAAqB;CAI3C,MAAM,QAAQ;CACd,IAAI;AAEJ,SAAQ,QAAQ,MAAM,KAAK,KAAK,MAAM,MAAM;EAC1C,MAAM,OAAO,MAAM;EACnB,MAAM,aAAa,MAAM,QAAQ,MAAM,GAAG,SAAS;EACnD,MAAM,WAAW,sBAAsB,MAAM,YAAY,KAAK,IAAI;AAElE,MAAI,aAAa,IAAI;GAEnB,MAAM,QAAQ,KAAK,UAAU,YAAY,WAAW,EAAE;AACtD,OAAI,KACF,WAAU,IAAI,MAAM,MAAM;;;AAKhC,QAAO;;;;;;AAOT,SAAgB,wBACd,OACA,WACQ;AACR,KAAI,UAAU,SAAS,EAAG,QAAO;AAGjC,QAAO,MAAM,QAAQ,iBAAiB,OAAO,SAAS;EACpD,MAAM,QAAQ,UAAU,IAAI,KAAK;AACjC,MAAI,OAAO;AAOT,OAAI,MAAM,WAAW,IAAI,IAAI,MAAM,SAAS,IAAI,CAC9C,QAAO,MAAM,UAAU,GAAG,MAAM,SAAS,EAAE;AAE7C,UAAO;;AAET,SAAO;GACP"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { validateSpecStructure } from "./spec-structure.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec-structure.d.ts","names":[],"sources":["../../../src/analysis/validate/spec-structure.ts"],"sourcesContent":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"spec-structure.d.ts","names":[],"sources":["../../../src/analysis/validate/spec-structure.ts"],"sourcesContent":[],"mappings":";;;;AA2DA;;;KAtCY,YAAA;;;;KAKA,QAAA;;;;UAeK,WAAA;;;;;sCAKqB,WAAW;;;;;iBAajC,qBAAA,+CAGD,cACZ"}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
2
|
+
|
|
1
3
|
//#region src/analysis/validate/spec-structure.ts
|
|
2
4
|
/**
|
|
5
|
+
* Spec structure validation utilities.
|
|
6
|
+
* Extracted from cli-contractspec/src/commands/validate/spec-checker.ts
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
3
9
|
* Default rules config that returns 'warn' for all rules.
|
|
4
10
|
*/
|
|
5
11
|
const DEFAULT_RULES_CONFIG = { getRule: () => "warn" };
|
|
@@ -9,17 +15,18 @@ const DEFAULT_RULES_CONFIG = { getRule: () => "warn" };
|
|
|
9
15
|
function validateSpecStructure(code, fileName, rulesConfig = DEFAULT_RULES_CONFIG) {
|
|
10
16
|
const errors = [];
|
|
11
17
|
const warnings = [];
|
|
12
|
-
|
|
13
|
-
if (
|
|
14
|
-
if (fileName.includes(".
|
|
15
|
-
if (fileName.includes(".
|
|
16
|
-
if (fileName.includes(".
|
|
17
|
-
if (fileName.includes(".
|
|
18
|
-
if (fileName.includes(".
|
|
19
|
-
if (fileName.includes(".
|
|
20
|
-
if (fileName.includes(".
|
|
21
|
-
if (fileName.includes(".
|
|
22
|
-
|
|
18
|
+
const sourceFile = new Project({ useInMemoryFileSystem: true }).createSourceFile(fileName, code);
|
|
19
|
+
if (!(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)) errors.push("No exported spec found");
|
|
20
|
+
if (fileName.includes(".contracts.") || fileName.includes(".contract.") || fileName.includes(".operations.") || fileName.includes(".operation.")) validateOperationSpec(sourceFile, errors, warnings, rulesConfig);
|
|
21
|
+
if (fileName.includes(".event.")) validateEventSpec(sourceFile, errors, warnings, rulesConfig);
|
|
22
|
+
if (fileName.includes(".presentation.")) validatePresentationSpec(sourceFile, errors, warnings);
|
|
23
|
+
if (fileName.includes(".workflow.")) validateWorkflowSpec(sourceFile, errors, warnings, rulesConfig);
|
|
24
|
+
if (fileName.includes(".data-view.")) validateDataViewSpec(sourceFile, errors, warnings, rulesConfig);
|
|
25
|
+
if (fileName.includes(".migration.")) validateMigrationSpec(sourceFile, errors, warnings, rulesConfig);
|
|
26
|
+
if (fileName.includes(".telemetry.")) validateTelemetrySpec(sourceFile, errors, warnings, rulesConfig);
|
|
27
|
+
if (fileName.includes(".experiment.")) validateExperimentSpec(sourceFile, errors, warnings, rulesConfig);
|
|
28
|
+
if (fileName.includes(".app-config.")) validateAppConfigSpec(sourceFile, errors, warnings, rulesConfig);
|
|
29
|
+
validateCommonFields(sourceFile, fileName, errors, warnings, rulesConfig);
|
|
23
30
|
return {
|
|
24
31
|
valid: errors.length === 0,
|
|
25
32
|
errors,
|
|
@@ -38,100 +45,409 @@ function emitRule(ruleName, specKind, message, errors, warnings, rulesConfig) {
|
|
|
38
45
|
/**
|
|
39
46
|
* Validate operation spec
|
|
40
47
|
*/
|
|
41
|
-
function validateOperationSpec(
|
|
42
|
-
|
|
43
|
-
if (!
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
function validateOperationSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
49
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
50
|
+
if (!callExpressions.some((call) => {
|
|
51
|
+
const text = call.getExpression().getText();
|
|
52
|
+
return text === "defineCommand" || text === "defineQuery";
|
|
53
|
+
})) errors.push("Missing defineCommand or defineQuery call");
|
|
54
|
+
let specObject;
|
|
55
|
+
for (const call of callExpressions) {
|
|
56
|
+
const text = call.getExpression().getText();
|
|
57
|
+
if (text === "defineCommand" || text === "defineQuery") {
|
|
58
|
+
const args = call.getArguments();
|
|
59
|
+
if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) {
|
|
60
|
+
specObject = args[0];
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (specObject && Node.isObjectLiteralExpression(specObject)) {
|
|
66
|
+
if (!specObject.getProperty("meta")) errors.push("Missing meta section");
|
|
67
|
+
if (!specObject.getProperty("io")) errors.push("Missing io section");
|
|
68
|
+
if (!specObject.getProperty("policy")) errors.push("Missing policy section");
|
|
69
|
+
const metaProp = specObject.getProperty("meta");
|
|
70
|
+
let hasKey = false;
|
|
71
|
+
let hasVersion = false;
|
|
72
|
+
if (metaProp && Node.isPropertyAssignment(metaProp)) {
|
|
73
|
+
const metaObj = metaProp.getInitializer();
|
|
74
|
+
if (metaObj && Node.isObjectLiteralExpression(metaObj)) {
|
|
75
|
+
if (metaObj.getProperty("key")) hasKey = true;
|
|
76
|
+
if (metaObj.getProperty("version")) hasVersion = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!hasKey) {
|
|
80
|
+
if (specObject.getProperty("key")) hasKey = true;
|
|
81
|
+
}
|
|
82
|
+
if (!hasKey) errors.push("Missing or invalid key field");
|
|
83
|
+
if (!hasVersion) {
|
|
84
|
+
if (specObject.getProperty("version")) hasVersion = true;
|
|
85
|
+
}
|
|
86
|
+
if (!hasVersion) errors.push("Missing or invalid version field");
|
|
87
|
+
const hasExplicitKind = specObject.getProperty("kind");
|
|
88
|
+
if (!callExpressions.find((c) => {
|
|
89
|
+
const t = c.getExpression().getText();
|
|
90
|
+
return t === "defineCommand" || t === "defineQuery";
|
|
91
|
+
})?.getExpression().getText() && !hasExplicitKind) errors.push("Missing kind: use defineCommand(), defineQuery(), or explicit kind field");
|
|
92
|
+
if (!specObject.getProperty("acceptance")) emitRule("require-acceptance", "operation", "No acceptance scenarios defined", errors, warnings, rulesConfig);
|
|
93
|
+
if (!specObject.getProperty("examples")) emitRule("require-examples", "operation", "No examples provided", errors, warnings, rulesConfig);
|
|
94
|
+
}
|
|
95
|
+
if (sourceFile.getFullText().includes("TODO")) emitRule("no-todo", "operation", "Contains TODO items that need completion", errors, warnings, rulesConfig);
|
|
55
96
|
}
|
|
56
|
-
function validateTelemetrySpec(
|
|
57
|
-
|
|
58
|
-
if (!
|
|
59
|
-
|
|
60
|
-
|
|
97
|
+
function validateTelemetrySpec(sourceFile, errors, warnings, rulesConfig) {
|
|
98
|
+
const specObject = getSpecObject(sourceFile, "TelemetrySpec");
|
|
99
|
+
if (!specObject) {
|
|
100
|
+
errors.push("Missing TelemetrySpec type annotation");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (specObject) {
|
|
104
|
+
const metaProp = specObject.getProperty("meta");
|
|
105
|
+
let hasName = false;
|
|
106
|
+
if (metaProp && Node.isPropertyAssignment(metaProp)) {
|
|
107
|
+
const metaObj = metaProp.getInitializer();
|
|
108
|
+
if (Node.isObjectLiteralExpression(metaObj)) {
|
|
109
|
+
if (metaObj.getProperty("name")) hasName = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!hasName) errors.push("TelemetrySpec.meta is required");
|
|
113
|
+
if (!specObject.getProperty("events")) errors.push("TelemetrySpec must declare events");
|
|
114
|
+
if (!specObject.getProperty("privacy")) emitRule("telemetry-privacy", "telemetry", "No explicit privacy classification found", errors, warnings, rulesConfig);
|
|
115
|
+
}
|
|
61
116
|
}
|
|
62
|
-
function validateExperimentSpec(
|
|
63
|
-
|
|
64
|
-
if (!
|
|
65
|
-
|
|
66
|
-
|
|
117
|
+
function validateExperimentSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
118
|
+
const specObject = getSpecObject(sourceFile, "ExperimentSpec");
|
|
119
|
+
if (!specObject) {
|
|
120
|
+
errors.push("Missing ExperimentSpec type annotation");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!specObject.getProperty("controlVariant")) errors.push("ExperimentSpec must declare controlVariant");
|
|
124
|
+
if (!specObject.getProperty("variants")) errors.push("ExperimentSpec must declare variants");
|
|
125
|
+
if (!specObject.getProperty("allocation")) emitRule("experiment-allocation", "experiment", "ExperimentSpec missing allocation configuration", errors, warnings, rulesConfig);
|
|
67
126
|
}
|
|
68
|
-
function validateAppConfigSpec(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
127
|
+
function validateAppConfigSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
128
|
+
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((c) => c.getExpression().getText() === "defineAppConfig");
|
|
129
|
+
let specObject;
|
|
130
|
+
if (defineCall) {
|
|
131
|
+
const args = defineCall.getArguments();
|
|
132
|
+
if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) specObject = args[0];
|
|
133
|
+
} else specObject = getSpecObject(sourceFile, "AppBlueprintSpec");
|
|
134
|
+
if (!specObject) {
|
|
135
|
+
errors.push("Missing defineAppConfig call or AppBlueprintSpec type annotation");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const metaProp = specObject.getProperty("meta");
|
|
139
|
+
if (!metaProp) errors.push("AppBlueprintSpec must define meta");
|
|
140
|
+
else if (Node.isPropertyAssignment(metaProp)) {
|
|
141
|
+
const metaObj = metaProp.getInitializer();
|
|
142
|
+
if (Node.isObjectLiteralExpression(metaObj)) {
|
|
143
|
+
if (!metaObj.getProperty("appId")) emitRule("app-config-appid", "app-config", "AppBlueprint meta missing appId assignment", errors, warnings, rulesConfig);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!specObject.getProperty("capabilities")) emitRule("app-config-capabilities", "app-config", "App blueprint spec does not declare capabilities", errors, warnings, rulesConfig);
|
|
73
147
|
}
|
|
74
148
|
/**
|
|
75
149
|
* Validate event spec
|
|
76
150
|
*/
|
|
77
|
-
function validateEventSpec(
|
|
78
|
-
|
|
79
|
-
if (!
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
151
|
+
function validateEventSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
152
|
+
const defineEventCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((c) => c.getExpression().getText() === "defineEvent");
|
|
153
|
+
if (!defineEventCall) {
|
|
154
|
+
errors.push("Missing defineEvent call");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let specObject;
|
|
158
|
+
const args = defineEventCall.getArguments();
|
|
159
|
+
if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) specObject = args[0];
|
|
160
|
+
if (specObject && Node.isObjectLiteralExpression(specObject)) {
|
|
161
|
+
const metaProp = specObject.getProperty("meta");
|
|
162
|
+
let hasKey = false;
|
|
163
|
+
let hasVersion = false;
|
|
164
|
+
if (metaProp && Node.isPropertyAssignment(metaProp)) {
|
|
165
|
+
const metaObj = metaProp.getInitializer();
|
|
166
|
+
if (Node.isObjectLiteralExpression(metaObj)) {
|
|
167
|
+
const keyP = metaObj.getProperty("key");
|
|
168
|
+
if (keyP && Node.isPropertyAssignment(keyP)) {
|
|
169
|
+
const init = keyP.getInitializer();
|
|
170
|
+
if (init && Node.isStringLiteral(init)) hasKey = true;
|
|
171
|
+
}
|
|
172
|
+
if (metaObj.getProperty("version")) hasVersion = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!hasKey) {
|
|
176
|
+
const kp = specObject.getProperty("key");
|
|
177
|
+
if (kp && Node.isPropertyAssignment(kp)) {
|
|
178
|
+
const init = kp.getInitializer();
|
|
179
|
+
if (init && Node.isStringLiteral(init)) hasKey = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!hasVersion && specObject.getProperty("version")) hasVersion = true;
|
|
183
|
+
if (!hasKey) errors.push("Missing or invalid key field");
|
|
184
|
+
if (!hasVersion) errors.push("Missing or invalid version field");
|
|
185
|
+
if (!specObject.getProperty("payload")) errors.push("Missing payload field");
|
|
186
|
+
let name = "";
|
|
187
|
+
const getName = (obj) => {
|
|
188
|
+
const init = obj.getInitializer();
|
|
189
|
+
if (init && Node.isStringLiteral(init)) return init.getLiteralText();
|
|
190
|
+
return "";
|
|
191
|
+
};
|
|
192
|
+
if (metaProp && Node.isPropertyAssignment(metaProp)) {
|
|
193
|
+
const metaObj = metaProp.getInitializer();
|
|
194
|
+
if (Node.isObjectLiteralExpression(metaObj)) {
|
|
195
|
+
const nameP = metaObj.getProperty("name");
|
|
196
|
+
if (nameP && Node.isPropertyAssignment(nameP)) name = getName(nameP);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!name) {
|
|
200
|
+
const nameP = specObject.getProperty("name");
|
|
201
|
+
if (nameP && Node.isPropertyAssignment(nameP)) name = getName(nameP);
|
|
202
|
+
}
|
|
203
|
+
if (name) {
|
|
204
|
+
if (!(name.split(".").pop() ?? "").match(/(ed|created|updated|deleted|completed)$/i)) emitRule("event-past-tense", "event", "Event name should use past tense (e.g., \"created\", \"updated\")", errors, warnings, rulesConfig);
|
|
205
|
+
}
|
|
85
206
|
}
|
|
86
207
|
}
|
|
87
208
|
/**
|
|
88
209
|
* Validate presentation spec (V2 format)
|
|
89
210
|
*/
|
|
90
|
-
function validatePresentationSpec(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
211
|
+
function validatePresentationSpec(sourceFile, errors, _warnings) {
|
|
212
|
+
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((c) => c.getExpression().getText() === "definePresentation");
|
|
213
|
+
let specObject;
|
|
214
|
+
if (defineCall) {
|
|
215
|
+
const args = defineCall.getArguments();
|
|
216
|
+
if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) specObject = args[0];
|
|
217
|
+
} else specObject = getSpecObject(sourceFile, "PresentationSpec");
|
|
218
|
+
if (!specObject) {
|
|
219
|
+
errors.push("Missing definePresentation call or PresentationSpec type annotation");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!specObject.getProperty("meta")) errors.push("Missing meta section");
|
|
223
|
+
const sourceProp = specObject.getProperty("source");
|
|
224
|
+
if (!sourceProp) errors.push("Missing source section");
|
|
225
|
+
else if (Node.isPropertyAssignment(sourceProp)) {
|
|
226
|
+
const sourceObj = sourceProp.getInitializer();
|
|
227
|
+
if (Node.isObjectLiteralExpression(sourceObj)) {
|
|
228
|
+
const typeProp = sourceObj.getProperty("type");
|
|
229
|
+
if (!typeProp) errors.push("Missing or invalid source.type field");
|
|
230
|
+
else if (Node.isPropertyAssignment(typeProp)) {
|
|
231
|
+
const init = typeProp.getInitializer();
|
|
232
|
+
if (init && Node.isStringLiteral(init)) {
|
|
233
|
+
const val = init.getLiteralText();
|
|
234
|
+
if (val !== "component" && val !== "blocknotejs") errors.push("Missing or invalid source.type field");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (!specObject.getProperty("targets")) errors.push("Missing targets section");
|
|
96
240
|
}
|
|
97
|
-
function validateWorkflowSpec(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (
|
|
241
|
+
function validateWorkflowSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
242
|
+
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((c) => c.getExpression().getText() === "defineWorkflow");
|
|
243
|
+
let specObject;
|
|
244
|
+
if (defineCall) {
|
|
245
|
+
const args = defineCall.getArguments();
|
|
246
|
+
if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) specObject = args[0];
|
|
247
|
+
} else specObject = getSpecObject(sourceFile, "WorkflowSpec");
|
|
248
|
+
if (!specObject) {
|
|
249
|
+
errors.push("Missing defineWorkflow call or WorkflowSpec type annotation");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (!specObject.getProperty("definition")) errors.push("Missing definition section");
|
|
253
|
+
else {
|
|
254
|
+
const defProp = specObject.getProperty("definition");
|
|
255
|
+
if (defProp && Node.isPropertyAssignment(defProp)) {
|
|
256
|
+
const defObj = defProp.getInitializer();
|
|
257
|
+
if (Node.isObjectLiteralExpression(defObj)) {
|
|
258
|
+
if (!defObj.getProperty("steps")) errors.push("Workflow must declare steps");
|
|
259
|
+
if (!defObj.getProperty("transitions")) emitRule("workflow-transitions", "workflow", "No transitions declared; workflow will complete after first step.", errors, warnings, rulesConfig);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
let titleFound = false;
|
|
264
|
+
let domainFound = false;
|
|
265
|
+
const metaProp = specObject.getProperty("meta");
|
|
266
|
+
if (metaProp && Node.isPropertyAssignment(metaProp)) {
|
|
267
|
+
const metaObj = metaProp.getInitializer();
|
|
268
|
+
if (Node.isObjectLiteralExpression(metaObj)) {
|
|
269
|
+
if (metaObj.getProperty("title")) titleFound = true;
|
|
270
|
+
if (metaObj.getProperty("domain")) domainFound = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!titleFound && specObject.getProperty("title")) titleFound = true;
|
|
274
|
+
if (!domainFound && specObject.getProperty("domain")) domainFound = true;
|
|
275
|
+
if (!titleFound) warnings.push("Missing workflow title");
|
|
276
|
+
if (!domainFound) warnings.push("Missing domain field");
|
|
277
|
+
if (sourceFile.getFullText().includes("TODO")) emitRule("no-todo", "workflow", "Contains TODO items that need completion", errors, warnings, rulesConfig);
|
|
105
278
|
}
|
|
106
|
-
function validateMigrationSpec(
|
|
107
|
-
|
|
108
|
-
if (!
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
279
|
+
function validateMigrationSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
280
|
+
const specObject = getSpecObject(sourceFile, "MigrationSpec");
|
|
281
|
+
if (!specObject) {
|
|
282
|
+
errors.push("Missing MigrationSpec type annotation");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const planProp = specObject.getProperty("plan");
|
|
286
|
+
if (!planProp) errors.push("Missing plan section");
|
|
287
|
+
else if (Node.isPropertyAssignment(planProp)) {
|
|
288
|
+
const planObj = planProp.getInitializer();
|
|
289
|
+
if (Node.isObjectLiteralExpression(planObj)) {
|
|
290
|
+
if (!planObj.getProperty("up")) errors.push("Migration must define at least one up step");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
let nameFound = false;
|
|
294
|
+
let versionFound = false;
|
|
295
|
+
const metaProp = specObject.getProperty("meta");
|
|
296
|
+
if (metaProp && Node.isPropertyAssignment(metaProp)) {
|
|
297
|
+
const metaObj = metaProp.getInitializer();
|
|
298
|
+
if (Node.isObjectLiteralExpression(metaObj)) {
|
|
299
|
+
if (metaObj.getProperty("name")) nameFound = true;
|
|
300
|
+
if (metaObj.getProperty("version")) versionFound = true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (!nameFound && specObject.getProperty("name")) nameFound = true;
|
|
304
|
+
if (!versionFound && specObject.getProperty("version")) versionFound = true;
|
|
305
|
+
if (!nameFound) errors.push("Missing or invalid migration name");
|
|
306
|
+
if (!versionFound) errors.push("Missing or invalid migration version");
|
|
307
|
+
if (sourceFile.getFullText().includes("TODO")) emitRule("no-todo", "migration", "Contains TODO items that need completion", errors, warnings, rulesConfig);
|
|
113
308
|
}
|
|
114
309
|
/**
|
|
115
310
|
* Validate common fields across all spec types
|
|
116
311
|
*/
|
|
117
|
-
function validateCommonFields(
|
|
312
|
+
function validateCommonFields(sourceFile, fileName, errors, warnings, rulesConfig) {
|
|
313
|
+
const code = sourceFile.getFullText();
|
|
118
314
|
const isInternalLib = fileName.includes("/libs/contracts/") || fileName.includes("/libs/contracts-transformers/") || fileName.includes("/libs/schema/");
|
|
119
|
-
if (code.includes("SchemaModel") &&
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (
|
|
315
|
+
if (code.includes("SchemaModel") && !isInternalLib) {
|
|
316
|
+
if (!sourceFile.getImportDeclarations().some((i) => i.getModuleSpecifierValue().includes("@contractspec/lib.schema"))) errors.push("Missing import for SchemaModel from @contractspec/lib.schema");
|
|
317
|
+
}
|
|
318
|
+
if ((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")) && !isInternalLib) {
|
|
319
|
+
if (!sourceFile.getImportDeclarations().some((i) => i.getModuleSpecifierValue().includes("@contractspec/lib.contracts"))) errors.push("Missing import from @contractspec/lib.contracts");
|
|
320
|
+
}
|
|
321
|
+
const specObject = findMainExportedObject(sourceFile);
|
|
322
|
+
if (specObject && Node.isObjectLiteralExpression(specObject)) {
|
|
323
|
+
const ownersProp = specObject.getProperty("owners");
|
|
324
|
+
let ownersArr = void 0;
|
|
325
|
+
const checkOwners = (prop) => {
|
|
326
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
327
|
+
const init = prop.getInitializer();
|
|
328
|
+
if (init && Node.isArrayLiteralExpression(init)) return init;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
if (ownersProp) ownersArr = checkOwners(ownersProp);
|
|
332
|
+
if (!ownersArr) {
|
|
333
|
+
const metaProp = specObject.getProperty("meta");
|
|
334
|
+
if (metaProp && Node.isPropertyAssignment(metaProp)) {
|
|
335
|
+
const metaObj = metaProp.getInitializer();
|
|
336
|
+
if (metaObj && Node.isObjectLiteralExpression(metaObj)) {
|
|
337
|
+
const o = metaObj.getProperty("owners");
|
|
338
|
+
if (o) ownersArr = checkOwners(o);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (ownersArr) {
|
|
343
|
+
for (const elem of ownersArr.getElements()) if (Node.isStringLiteral(elem)) {
|
|
344
|
+
const val = elem.getLiteralText();
|
|
345
|
+
if (!val.includes("@") && !val.includes("Enum") && !val.match(/[A-Z][a-zA-Z0-9_]+/)) emitRule("require-owners-format", "operation", "Owners should start with @ or use an Enum/Constant", errors, warnings, rulesConfig);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
let stabilityFound = false;
|
|
349
|
+
if (specObject.getProperty("stability")) stabilityFound = true;
|
|
350
|
+
if (!stabilityFound) {
|
|
351
|
+
const metaProp = specObject.getProperty("meta");
|
|
352
|
+
if (metaProp && Node.isPropertyAssignment(metaProp)) {
|
|
353
|
+
const metaObj = metaProp.getInitializer();
|
|
354
|
+
if (Node.isObjectLiteralExpression(metaObj)) {
|
|
355
|
+
if (metaObj.getProperty("stability")) stabilityFound = true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!stabilityFound) emitRule("require-stability", "operation", "Missing or invalid stability field", errors, warnings, rulesConfig);
|
|
360
|
+
}
|
|
127
361
|
}
|
|
128
|
-
function validateDataViewSpec(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
362
|
+
function validateDataViewSpec(sourceFile, errors, warnings, rulesConfig) {
|
|
363
|
+
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((c) => c.getExpression().getText() === "defineDataView");
|
|
364
|
+
let specObject;
|
|
365
|
+
if (defineCall) {
|
|
366
|
+
const args = defineCall.getArguments();
|
|
367
|
+
if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) specObject = args[0];
|
|
368
|
+
} else specObject = getSpecObject(sourceFile, "DataViewSpec");
|
|
369
|
+
if (!specObject) {
|
|
370
|
+
errors.push("Missing defineDataView call or DataViewSpec type annotation");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (!specObject.getProperty("meta")) errors.push("Missing meta section");
|
|
374
|
+
if (!specObject.getProperty("source")) errors.push("Missing source section");
|
|
375
|
+
const viewProp = specObject.getProperty("view");
|
|
376
|
+
if (!viewProp) {
|
|
377
|
+
errors.push("Missing view section");
|
|
378
|
+
errors.push("Missing or invalid view.kind (list/table/detail/grid)");
|
|
379
|
+
} else if (Node.isPropertyAssignment(viewProp)) {
|
|
380
|
+
const viewObj = viewProp.getInitializer();
|
|
381
|
+
if (Node.isObjectLiteralExpression(viewObj)) {
|
|
382
|
+
const kindProp = viewObj.getProperty("kind");
|
|
383
|
+
if (!kindProp) errors.push("Missing or invalid view.kind (list/table/detail/grid)");
|
|
384
|
+
else if (Node.isPropertyAssignment(kindProp)) {
|
|
385
|
+
const init = kindProp.getInitializer();
|
|
386
|
+
if (init && Node.isStringLiteral(init)) {
|
|
387
|
+
const val = init.getLiteralText();
|
|
388
|
+
if (![
|
|
389
|
+
"list",
|
|
390
|
+
"table",
|
|
391
|
+
"detail",
|
|
392
|
+
"grid"
|
|
393
|
+
].includes(val)) errors.push("Missing or invalid view.kind (list/table/detail/grid)");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
let fieldsFound = false;
|
|
399
|
+
if (viewProp && Node.isPropertyAssignment(viewProp)) {
|
|
400
|
+
const viewObj = viewProp.getInitializer();
|
|
401
|
+
if (Node.isObjectLiteralExpression(viewObj)) {
|
|
402
|
+
if (viewObj.getProperty("fields")) fieldsFound = true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (!fieldsFound && specObject.getProperty("fields")) fieldsFound = true;
|
|
406
|
+
if (!fieldsFound) emitRule("data-view-fields", "data-view", "No fields defined for data view", errors, warnings, rulesConfig);
|
|
407
|
+
}
|
|
408
|
+
function getSpecObject(sourceFile, typeName) {
|
|
409
|
+
const varStmts = sourceFile.getVariableStatements();
|
|
410
|
+
for (const stmt of varStmts) if (stmt.isExported()) for (const decl of stmt.getDeclarations()) {
|
|
411
|
+
const typeNode = decl.getTypeNode();
|
|
412
|
+
if (typeNode && typeNode.getText().includes(typeName)) {
|
|
413
|
+
const init = decl.getInitializer();
|
|
414
|
+
if (init && Node.isObjectLiteralExpression(init)) return init;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const exportAssign = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
418
|
+
if (exportAssign) {
|
|
419
|
+
const expr = exportAssign.getExpression();
|
|
420
|
+
if (Node.isAsExpression(expr)) {
|
|
421
|
+
if (expr.getTypeNode()?.getText().includes(typeName)) {
|
|
422
|
+
const inner = expr.getExpression();
|
|
423
|
+
if (Node.isObjectLiteralExpression(inner)) return inner;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (Node.isObjectLiteralExpression(expr)) return expr;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function findMainExportedObject(sourceFile) {
|
|
430
|
+
const varStmts = sourceFile.getVariableStatements();
|
|
431
|
+
for (const stmt of varStmts) if (stmt.isExported()) for (const decl of stmt.getDeclarations()) {
|
|
432
|
+
const init = decl.getInitializer();
|
|
433
|
+
if (init) {
|
|
434
|
+
if (Node.isObjectLiteralExpression(init)) return init;
|
|
435
|
+
if (Node.isCallExpression(init)) {
|
|
436
|
+
const args = init.getArguments();
|
|
437
|
+
if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) return args[0];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const exportAssign = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
442
|
+
if (exportAssign) {
|
|
443
|
+
const expr = exportAssign.getExpression();
|
|
444
|
+
if (Node.isObjectLiteralExpression(expr)) return expr;
|
|
445
|
+
if (Node.isAsExpression(expr) && Node.isObjectLiteralExpression(expr.getExpression())) return expr.getExpression();
|
|
446
|
+
if (Node.isCallExpression(expr)) {
|
|
447
|
+
const args = expr.getArguments();
|
|
448
|
+
if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) return args[0];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
135
451
|
}
|
|
136
452
|
|
|
137
453
|
//#endregion
|