@contractspec/module.workspace 1.46.2 → 1.48.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.
Files changed (56) hide show
  1. package/dist/analysis/deps/graph.js.map +1 -1
  2. package/dist/analysis/deps/parse-imports.js.map +1 -1
  3. package/dist/analysis/diff/deep-diff.js.map +1 -1
  4. package/dist/analysis/diff/semantic.js.map +1 -1
  5. package/dist/analysis/example-scan.d.ts.map +1 -1
  6. package/dist/analysis/example-scan.js +2 -37
  7. package/dist/analysis/example-scan.js.map +1 -1
  8. package/dist/analysis/feature-extractor.js +203 -0
  9. package/dist/analysis/feature-extractor.js.map +1 -0
  10. package/dist/analysis/feature-scan.d.ts.map +1 -1
  11. package/dist/analysis/feature-scan.js +20 -121
  12. package/dist/analysis/feature-scan.js.map +1 -1
  13. package/dist/analysis/impact/classifier.js.map +1 -1
  14. package/dist/analysis/impact/rules.js.map +1 -1
  15. package/dist/analysis/index.js +3 -1
  16. package/dist/analysis/snapshot/normalizer.js.map +1 -1
  17. package/dist/analysis/snapshot/snapshot.js.map +1 -1
  18. package/dist/analysis/spec-parsing-utils.d.ts +26 -0
  19. package/dist/analysis/spec-parsing-utils.d.ts.map +1 -0
  20. package/dist/analysis/spec-parsing-utils.js +98 -0
  21. package/dist/analysis/spec-parsing-utils.js.map +1 -0
  22. package/dist/analysis/spec-scan.d.ts +8 -22
  23. package/dist/analysis/spec-scan.d.ts.map +1 -1
  24. package/dist/analysis/spec-scan.js +105 -337
  25. package/dist/analysis/spec-scan.js.map +1 -1
  26. package/dist/analysis/utils/matchers.js +77 -0
  27. package/dist/analysis/utils/matchers.js.map +1 -0
  28. package/dist/analysis/utils/variables.js +45 -0
  29. package/dist/analysis/utils/variables.js.map +1 -0
  30. package/dist/analysis/validate/index.js +1 -0
  31. package/dist/analysis/validate/spec-structure.d.ts.map +1 -1
  32. package/dist/analysis/validate/spec-structure.js +401 -85
  33. package/dist/analysis/validate/spec-structure.js.map +1 -1
  34. package/dist/formatter.js.map +1 -1
  35. package/dist/formatters/index.js +2 -0
  36. package/dist/formatters/spec-markdown.d.ts +4 -1
  37. package/dist/formatters/spec-markdown.d.ts.map +1 -1
  38. package/dist/formatters/spec-markdown.js +12 -4
  39. package/dist/formatters/spec-markdown.js.map +1 -1
  40. package/dist/formatters/spec-to-docblock.d.ts +3 -1
  41. package/dist/formatters/spec-to-docblock.d.ts.map +1 -1
  42. package/dist/formatters/spec-to-docblock.js +2 -2
  43. package/dist/formatters/spec-to-docblock.js.map +1 -1
  44. package/dist/index.d.ts +4 -3
  45. package/dist/index.js +4 -2
  46. package/dist/templates/integration-utils.js.map +1 -1
  47. package/dist/templates/integration.js +3 -4
  48. package/dist/templates/integration.js.map +1 -1
  49. package/dist/templates/knowledge.js.map +1 -1
  50. package/dist/templates/workflow.js.map +1 -1
  51. package/dist/types/analysis-types.d.ts +24 -3
  52. package/dist/types/analysis-types.d.ts.map +1 -1
  53. package/dist/types/generation-types.js.map +1 -1
  54. package/dist/types/llm-types.d.ts +1 -1
  55. package/dist/types/llm-types.d.ts.map +1 -1
  56. 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":";;;;AAkDA;;;KAtCY,YAAA;;;;KAKA,QAAA;;;;UAeK,WAAA;;;;;sCAKqB,WAAW;;;;;iBAajC,qBAAA,+CAGD,cACZ"}
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
- if (!/export\s/.test(code)) errors.push("No exported spec found");
13
- if (fileName.includes(".contracts.") || fileName.includes(".contract.") || fileName.includes(".operations.") || fileName.includes(".operation.")) validateOperationSpec(code, errors, warnings, rulesConfig);
14
- if (fileName.includes(".event.")) validateEventSpec(code, errors, warnings, rulesConfig);
15
- if (fileName.includes(".presentation.")) validatePresentationSpec(code, errors, warnings);
16
- if (fileName.includes(".workflow.")) validateWorkflowSpec(code, errors, warnings, rulesConfig);
17
- if (fileName.includes(".data-view.")) validateDataViewSpec(code, errors, warnings, rulesConfig);
18
- if (fileName.includes(".migration.")) validateMigrationSpec(code, errors, warnings, rulesConfig);
19
- if (fileName.includes(".telemetry.")) validateTelemetrySpec(code, errors, warnings, rulesConfig);
20
- if (fileName.includes(".experiment.")) validateExperimentSpec(code, errors, warnings, rulesConfig);
21
- if (fileName.includes(".app-config.")) validateAppConfigSpec(code, errors, warnings, rulesConfig);
22
- validateCommonFields(code, fileName, errors, warnings, rulesConfig);
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(code, errors, warnings, rulesConfig) {
42
- if (!/define(Command|Query)/.test(code)) errors.push("Missing defineCommand or defineQuery call");
43
- if (!code.includes("meta:")) errors.push("Missing meta section");
44
- if (!code.includes("io:")) errors.push("Missing io section");
45
- if (!code.includes("policy:")) errors.push("Missing policy section");
46
- if (!code.match(/key:\s*['"][^'"]+['"]/)) errors.push("Missing or invalid key field");
47
- if (!code.match(/version:\s*(?:\d+|['"][^'"]+['"])/)) errors.push("Missing or invalid version field");
48
- const hasDefineCommand = /defineCommand\s*\(/.test(code);
49
- const hasDefineQuery = /defineQuery\s*\(/.test(code);
50
- const hasExplicitKind = /kind:\s*['"](?:command|query)['"]/.test(code);
51
- if (!hasDefineCommand && !hasDefineQuery && !hasExplicitKind) errors.push("Missing kind: use defineCommand(), defineQuery(), or explicit kind field");
52
- if (!code.includes("acceptance:")) emitRule("require-acceptance", "operation", "No acceptance scenarios defined", errors, warnings, rulesConfig);
53
- if (!code.includes("examples:")) emitRule("require-examples", "operation", "No examples provided", errors, warnings, rulesConfig);
54
- if (code.includes("TODO")) emitRule("no-todo", "operation", "Contains TODO items that need completion", errors, warnings, rulesConfig);
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(code, errors, warnings, rulesConfig) {
57
- if (!code.match(/:\s*TelemetrySpec\s*=/)) errors.push("Missing TelemetrySpec type annotation");
58
- if (!code.match(/meta:\s*{[\s\S]*name:/)) errors.push("TelemetrySpec.meta is required");
59
- if (!code.includes("events:")) errors.push("TelemetrySpec must declare events");
60
- if (!code.match(/privacy:\s*'(public|internal|pii|sensitive)'/)) emitRule("telemetry-privacy", "telemetry", "No explicit privacy classification found", errors, warnings, rulesConfig);
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(code, errors, warnings, rulesConfig) {
63
- if (!code.match(/:\s*ExperimentSpec\s*=/)) errors.push("Missing ExperimentSpec type annotation");
64
- if (!code.includes("controlVariant")) errors.push("ExperimentSpec must declare controlVariant");
65
- if (!code.includes("variants:")) errors.push("ExperimentSpec must declare variants");
66
- if (!code.match(/allocation:\s*{/)) emitRule("experiment-allocation", "experiment", "ExperimentSpec missing allocation configuration", errors, warnings, rulesConfig);
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(code, errors, warnings, rulesConfig) {
69
- if (!code.match(/:\s*AppBlueprintSpec\s*=/)) errors.push("Missing AppBlueprintSpec type annotation");
70
- if (!code.includes("meta:")) errors.push("AppBlueprintSpec must define meta");
71
- if (!code.includes("appId")) emitRule("app-config-appid", "app-config", "AppBlueprint meta missing appId assignment", errors, warnings, rulesConfig);
72
- if (!code.includes("capabilities")) emitRule("app-config-capabilities", "app-config", "App blueprint spec does not declare capabilities", errors, warnings, rulesConfig);
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(code, errors, warnings, rulesConfig) {
78
- if (!code.includes("defineEvent")) errors.push("Missing defineEvent call");
79
- if (!code.match(/key:\s*['"][^'"]+['"]/)) errors.push("Missing or invalid key field");
80
- if (!code.match(/version:\s*(?:\d+|['"][^'"]+['"])/)) errors.push("Missing or invalid version field");
81
- if (!code.includes("payload:")) errors.push("Missing payload field");
82
- const nameMatch = code.match(/name:\s*['"]([^'"]+)['"]/);
83
- if (nameMatch?.[1]) {
84
- if (!(nameMatch[1].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);
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(code, errors, _warnings) {
91
- if (!code.match(/:\s*PresentationSpec\s*=/)) errors.push("Missing PresentationSpec type annotation");
92
- if (!code.includes("meta:")) errors.push("Missing meta section");
93
- if (!code.includes("source:")) errors.push("Missing source section");
94
- if (!code.match(/type:\s*['"](?:component|blocknotejs)['"]/)) errors.push("Missing or invalid source.type field");
95
- if (!code.includes("targets:")) errors.push("Missing targets section");
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(code, errors, warnings, rulesConfig) {
98
- if (!code.match(/:\s*WorkflowSpec\s*=/)) errors.push("Missing WorkflowSpec type annotation");
99
- if (!code.includes("definition:")) errors.push("Missing definition section");
100
- if (!code.includes("steps:")) errors.push("Workflow must declare steps");
101
- if (!code.includes("transitions:")) emitRule("workflow-transitions", "workflow", "No transitions declared; workflow will complete after first step.", errors, warnings, rulesConfig);
102
- if (!code.match(/title:\s*['"][^'"]+['"]/)) warnings.push("Missing workflow title");
103
- if (!code.match(/domain:\s*['"][^'"]+['"]/)) warnings.push("Missing domain field");
104
- if (code.includes("TODO")) emitRule("no-todo", "workflow", "Contains TODO items that need completion", errors, warnings, rulesConfig);
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(code, errors, warnings, rulesConfig) {
107
- if (!code.match(/:\s*MigrationSpec\s*=/)) errors.push("Missing MigrationSpec type annotation");
108
- if (!code.includes("plan:")) errors.push("Missing plan section");
109
- else if (!code.includes("up:")) errors.push("Migration must define at least one up step");
110
- if (!code.match(/name:\s*['"][^'"]+['"]/)) errors.push("Missing or invalid migration name");
111
- if (!code.match(/version:\s*(?:\d+|['"][^'"]+['"])/)) errors.push("Missing or invalid migration version");
112
- if (code.includes("TODO")) emitRule("no-todo", "migration", "Contains TODO items that need completion", errors, warnings, rulesConfig);
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(code, fileName, errors, warnings, rulesConfig) {
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") && !/from\s+['"]@contractspec\/lib\.schema(\/[^'"]+)?['"]/.test(code) && !isInternalLib) errors.push("Missing import for SchemaModel from @contractspec/lib.schema");
120
- 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(")) && !/from\s+['"]@contractspec\/lib\.contracts(\/[^'"]+)?['"]/.test(code) && !isInternalLib) errors.push("Missing import from @contractspec/lib.contracts");
121
- const ownersMatch = code.match(/owners:\s*\[(.*?)\]/s);
122
- if (ownersMatch?.[1]) {
123
- const ownersContent = ownersMatch[1];
124
- if (!ownersContent.includes("@") && !ownersContent.includes("Enum") && !ownersContent.match(/[A-Z][a-zA-Z0-9_]+/)) emitRule("require-owners-format", "operation", "Owners should start with @ or use an Enum/Constant", errors, warnings, rulesConfig);
125
- }
126
- if (!code.match(/stability:\s*(?:['"](?:experimental|beta|stable|deprecated)['"]|[A-Z][a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)?)/)) emitRule("require-stability", "operation", "Missing or invalid stability field", errors, warnings, rulesConfig);
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(code, errors, warnings, rulesConfig) {
129
- if (!code.match(/:\s*DataViewSpec\s*=/)) errors.push("Missing DataViewSpec type annotation");
130
- if (!code.includes("meta:")) errors.push("Missing meta section");
131
- if (!code.includes("source:")) errors.push("Missing source section");
132
- if (!code.includes("view:")) errors.push("Missing view section");
133
- if (!code.match(/kind:\s*['"](list|table|detail|grid)['"]/)) errors.push("Missing or invalid view.kind (list/table/detail/grid)");
134
- if (!code.match(/fields:\s*\[/)) emitRule("data-view-fields", "data-view", "No fields defined for data view", errors, warnings, rulesConfig);
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