@toolbaux/guardian 0.1.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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/dist/adapters/csharp-adapter.js +149 -0
  4. package/dist/adapters/go-adapter.js +96 -0
  5. package/dist/adapters/index.js +16 -0
  6. package/dist/adapters/java-adapter.js +122 -0
  7. package/dist/adapters/python-adapter.js +183 -0
  8. package/dist/adapters/runner.js +69 -0
  9. package/dist/adapters/types.js +1 -0
  10. package/dist/adapters/typescript-adapter.js +179 -0
  11. package/dist/benchmarking/framework.js +91 -0
  12. package/dist/cli.js +343 -0
  13. package/dist/commands/analyze-depth.js +43 -0
  14. package/dist/commands/api-spec-extractor.js +52 -0
  15. package/dist/commands/breaking-change-analyzer.js +334 -0
  16. package/dist/commands/config-compliance.js +219 -0
  17. package/dist/commands/constraints.js +221 -0
  18. package/dist/commands/context.js +101 -0
  19. package/dist/commands/data-flow-tracer.js +291 -0
  20. package/dist/commands/dependency-impact-analyzer.js +27 -0
  21. package/dist/commands/diff.js +146 -0
  22. package/dist/commands/discrepancy.js +71 -0
  23. package/dist/commands/doc-generate.js +163 -0
  24. package/dist/commands/doc-html.js +120 -0
  25. package/dist/commands/drift.js +88 -0
  26. package/dist/commands/extract.js +16 -0
  27. package/dist/commands/feature-context.js +116 -0
  28. package/dist/commands/generate.js +339 -0
  29. package/dist/commands/guard.js +182 -0
  30. package/dist/commands/init.js +209 -0
  31. package/dist/commands/intel.js +20 -0
  32. package/dist/commands/license-dependency-auditor.js +33 -0
  33. package/dist/commands/performance-hotspot-profiler.js +42 -0
  34. package/dist/commands/search.js +314 -0
  35. package/dist/commands/security-boundary-auditor.js +359 -0
  36. package/dist/commands/simulate.js +294 -0
  37. package/dist/commands/summary.js +27 -0
  38. package/dist/commands/test-coverage-mapper.js +264 -0
  39. package/dist/commands/verify-drift.js +62 -0
  40. package/dist/config.js +441 -0
  41. package/dist/extract/ai-context-hints.js +107 -0
  42. package/dist/extract/analyzers/backend.js +1704 -0
  43. package/dist/extract/analyzers/depth.js +264 -0
  44. package/dist/extract/analyzers/frontend.js +2221 -0
  45. package/dist/extract/api-usage-tracker.js +19 -0
  46. package/dist/extract/cache.js +53 -0
  47. package/dist/extract/codebase-intel.js +190 -0
  48. package/dist/extract/compress.js +452 -0
  49. package/dist/extract/context-block.js +356 -0
  50. package/dist/extract/contracts.js +183 -0
  51. package/dist/extract/discrepancies.js +233 -0
  52. package/dist/extract/docs-loader.js +110 -0
  53. package/dist/extract/docs.js +2379 -0
  54. package/dist/extract/drift.js +1578 -0
  55. package/dist/extract/duplicates.js +435 -0
  56. package/dist/extract/feature-arcs.js +138 -0
  57. package/dist/extract/graph.js +76 -0
  58. package/dist/extract/html-doc.js +1409 -0
  59. package/dist/extract/ignore.js +45 -0
  60. package/dist/extract/index.js +455 -0
  61. package/dist/extract/llm-client.js +159 -0
  62. package/dist/extract/pattern-registry.js +141 -0
  63. package/dist/extract/product-doc.js +497 -0
  64. package/dist/extract/python.js +1202 -0
  65. package/dist/extract/runtime.js +193 -0
  66. package/dist/extract/schema-evolution-validator.js +35 -0
  67. package/dist/extract/test-gap-analyzer.js +20 -0
  68. package/dist/extract/tests.js +74 -0
  69. package/dist/extract/types.js +1 -0
  70. package/dist/extract/validate-backend.js +30 -0
  71. package/dist/extract/writer.js +11 -0
  72. package/dist/output-layout.js +37 -0
  73. package/dist/project-discovery.js +309 -0
  74. package/dist/schema/architecture.js +350 -0
  75. package/dist/schema/feature-spec.js +89 -0
  76. package/dist/schema/index.js +8 -0
  77. package/dist/schema/ux.js +46 -0
  78. package/package.json +75 -0
@@ -0,0 +1,122 @@
1
+ import Java from "tree-sitter-java";
2
+ import Parser from "tree-sitter";
3
+ function text(node) {
4
+ return node ? node.text : "";
5
+ }
6
+ export const JavaAdapter = {
7
+ name: "Java Spring Boot Adapter",
8
+ language: Java,
9
+ fileExtensions: [".java"],
10
+ queries: {
11
+ // Endpoints: Methods annotated with @*Mapping (e.g., @GetMapping, @PostMapping)
12
+ endpoints: `(method_declaration (modifiers (annotation name: (identifier) @mapping (#match? @mapping "Mapping$"))) name: (identifier) @handler) @endpoint`,
13
+ // Models: Classes annotated with @Entity, @Table, @Document (MongoDB), etc.
14
+ models: `(class_declaration (modifiers (annotation name: (identifier) @annot (#match? @annot "Entity|Table|Document|MappedSuperclass"))) name: (identifier) @name) @model`,
15
+ // Tests: Methods annotated with @Test
16
+ tests: `
17
+ (method_declaration
18
+ (modifiers (annotation name: (identifier) @test_annot (#match? @test_annot "Test")))
19
+ name: (identifier) @test_name
20
+ )
21
+ `
22
+ },
23
+ extract(file, source, root) {
24
+ const endpoints = [];
25
+ const models = [];
26
+ const components = []; // Not heavily used in backend Java MVCs
27
+ const tests = [];
28
+ const epQuery = new Parser.Query(this.language, this.queries.endpoints);
29
+ const mdQuery = new Parser.Query(this.language, this.queries.models);
30
+ // Extract Spring Boot Endpoints
31
+ const epMatches = epQuery.matches(root);
32
+ for (const match of epMatches) {
33
+ const mappingNode = match.captures.find(c => c.name === "mapping")?.node;
34
+ const handlerNode = match.captures.find(c => c.name === "handler")?.node;
35
+ const endpointNode = match.captures.find(c => c.name === "endpoint")?.node;
36
+ if (mappingNode && handlerNode && endpointNode) {
37
+ const mappingType = text(mappingNode); // e.g. "GetMapping"
38
+ const method = mappingType.replace("Mapping", "").toUpperCase() || "ANY";
39
+ const handler = text(handlerNode);
40
+ // Find route path: @GetMapping("/api/users") -> "/api/users"
41
+ let routePath = "/";
42
+ const annotationNode = mappingNode.parent;
43
+ if (annotationNode) {
44
+ const strMatch = text(annotationNode).match(/\\"([^\\"]+)\\"/);
45
+ if (strMatch)
46
+ routePath = strMatch[1];
47
+ }
48
+ // Basic payload schemas from parameters
49
+ let requestSchema = null;
50
+ let responseSchema = null;
51
+ // Try to find return type for response schema
52
+ const typeNode = endpointNode.childForFieldName("type");
53
+ if (typeNode)
54
+ responseSchema = text(typeNode);
55
+ // Try to find @RequestBody parameter for request schema
56
+ const paramsNode = endpointNode.childForFieldName("parameters");
57
+ if (paramsNode) {
58
+ for (let i = 0; i < paramsNode.childCount; i++) {
59
+ const child = paramsNode.child(i);
60
+ if (!child)
61
+ continue;
62
+ if (text(child).includes("@RequestBody")) {
63
+ const typeChild = child.childForFieldName("type");
64
+ if (typeChild)
65
+ requestSchema = text(typeChild);
66
+ }
67
+ }
68
+ }
69
+ endpoints.push({
70
+ method,
71
+ path: routePath,
72
+ handler,
73
+ file,
74
+ request_schema: requestSchema,
75
+ response_schema: responseSchema,
76
+ service_calls: []
77
+ });
78
+ }
79
+ }
80
+ // Extract JPA Models
81
+ const mdMatches = mdQuery.matches(root);
82
+ for (const match of mdMatches) {
83
+ const nameNode = match.captures.find(c => c.name === "name")?.node;
84
+ const modelNode = match.captures.find(c => c.name === "model")?.node;
85
+ if (nameNode && modelNode) {
86
+ const name = text(nameNode);
87
+ const fields = [];
88
+ const relationships = [];
89
+ // Scan class body for fields
90
+ const bodyNode = modelNode.childForFieldName("body");
91
+ if (bodyNode) {
92
+ for (let i = 0; i < bodyNode.childCount; i++) {
93
+ const child = bodyNode.child(i);
94
+ if (!child)
95
+ continue;
96
+ if (child.type === "field_declaration") {
97
+ const declaredNode = child.childForFieldName("declarator");
98
+ if (declaredNode) {
99
+ const fieldName = declaredNode.childForFieldName("name");
100
+ const fText = text(fieldName);
101
+ if (fText)
102
+ fields.push(fText);
103
+ // Check annotations on the field for JPA Relations
104
+ if (text(child).includes("@OneToMany") || text(child).includes("@ManyToOne") || text(child).includes("@ManyToMany") || text(child).includes("@OneToOne")) {
105
+ relationships.push(fText);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ models.push({
112
+ name,
113
+ file,
114
+ framework: "jpa-hibernate",
115
+ fields,
116
+ relationships
117
+ });
118
+ }
119
+ }
120
+ return { endpoints, models, components, tests };
121
+ }
122
+ };
@@ -0,0 +1,183 @@
1
+ import Python from "tree-sitter-python";
2
+ import Parser from "tree-sitter";
3
+ // Utility to recursively find children of a certain type
4
+ function findChildren(node, type) {
5
+ const results = [];
6
+ if (node.type === type)
7
+ results.push(node);
8
+ for (const child of node.namedChildren) {
9
+ results.push(...findChildren(child, type));
10
+ }
11
+ return results;
12
+ }
13
+ export const PythonAdapter = {
14
+ name: "python",
15
+ language: Python,
16
+ fileExtensions: [".py"],
17
+ queries: {
18
+ endpoints: `
19
+ (decorated_definition (decorator) @decorator definition: (function_definition name: (identifier) @handler))
20
+ (call function: (identifier) @func_name (#match? @func_name "^path$|^re_path$")) @django_route
21
+ `,
22
+ models: `
23
+ (class_definition name: (identifier) @name body: (block))
24
+ `,
25
+ tests: `
26
+ (class_definition name: (identifier) @suite_name (#match? @suite_name "^Test"))
27
+ (function_definition name: (identifier) @test_name (#match? @test_name "^test_"))
28
+ `
29
+ },
30
+ extract(file, source, root) {
31
+ const endpoints = [];
32
+ const models = [];
33
+ const components = [];
34
+ const tests = [];
35
+ // Helper to get text
36
+ const text = (n) => source.substring(n.startIndex, n.endIndex);
37
+ // 1. Process Endpoints
38
+ const epQuery = new Parser.Query(this.language, this.queries.endpoints);
39
+ const epMatches = epQuery.matches(root);
40
+ for (const match of epMatches) {
41
+ let isFastAPI = false;
42
+ let isDjango = false;
43
+ let method = "GET";
44
+ let routePath = "";
45
+ let handler = "";
46
+ let decoratorNode = null;
47
+ let handlerNode = null;
48
+ for (const cap of match.captures) {
49
+ if (cap.name === "decorator")
50
+ decoratorNode = cap.node;
51
+ if (cap.name === "handler") {
52
+ handlerNode = cap.node;
53
+ handler = text(cap.node);
54
+ }
55
+ if (cap.name === "django_route")
56
+ isDjango = true;
57
+ }
58
+ if (decoratorNode) {
59
+ const decText = text(decoratorNode);
60
+ if (decText.includes(".get(") || decText.includes(".post(") || decText.includes(".put(") || decText.includes(".delete(") || decText.includes(".patch(")) {
61
+ isFastAPI = true;
62
+ if (decText.includes(".post("))
63
+ method = "POST";
64
+ else if (decText.includes(".put("))
65
+ method = "PUT";
66
+ else if (decText.includes(".delete("))
67
+ method = "DELETE";
68
+ else if (decText.includes(".patch("))
69
+ method = "PATCH";
70
+ else
71
+ method = "GET";
72
+ const strMatch = decText.match(/['"](.*?)['"]/);
73
+ routePath = strMatch ? strMatch[1] : "";
74
+ }
75
+ }
76
+ if (!isFastAPI && !isDjango)
77
+ continue; // Not an endpoint
78
+ const service_calls = [];
79
+ let request_schema = null;
80
+ let response_schema = null;
81
+ // Deep Intra-node inspection
82
+ if (handlerNode && handlerNode.parent) {
83
+ // Find request schema from arguments
84
+ const paramsNode = handlerNode.parent.childForFieldName("parameters");
85
+ if (paramsNode) {
86
+ for (const param of paramsNode.namedChildren) {
87
+ if (param.type === "typed_parameter") {
88
+ const typeNode = param.childForFieldName("type");
89
+ if (typeNode) {
90
+ const t = text(typeNode);
91
+ if (t !== "Request" && t !== "Response" && t !== "Session" && t !== "Depends") {
92
+ request_schema = t;
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ // Find service calls
100
+ const bodyNode = handlerNode.parent.childForFieldName("body");
101
+ if (bodyNode) {
102
+ const calls = findChildren(bodyNode, "call");
103
+ for (const call of calls) {
104
+ const funcNode = call.childForFieldName("function");
105
+ if (funcNode) {
106
+ const fname = text(funcNode);
107
+ if (fname !== "Depends")
108
+ service_calls.push(fname);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ // Find response schema from decorator
114
+ if (decoratorNode) {
115
+ const decText = text(decoratorNode);
116
+ const rmMatch = decText.match(/response_model=([a-zA-Z0-9_]+)/);
117
+ if (rmMatch)
118
+ response_schema = rmMatch[1];
119
+ }
120
+ endpoints.push({
121
+ file,
122
+ method,
123
+ path: routePath,
124
+ handler,
125
+ request_schema,
126
+ response_schema,
127
+ service_calls: Array.from(new Set(service_calls))
128
+ });
129
+ }
130
+ // 2. Process Models
131
+ const mdQuery = new Parser.Query(this.language, this.queries.models);
132
+ const mdMatches = mdQuery.matches(root);
133
+ for (const match of mdMatches) {
134
+ let name = "";
135
+ let classNode = null;
136
+ for (const cap of match.captures) {
137
+ if (cap.name === "name") {
138
+ name = text(cap.node);
139
+ classNode = cap.node.parent;
140
+ }
141
+ }
142
+ if (!classNode)
143
+ continue;
144
+ const superclasses = classNode.childForFieldName("superclasses");
145
+ const baseText = superclasses ? text(superclasses) : "";
146
+ let framework = "unknown";
147
+ if (baseText.includes("BaseModel"))
148
+ framework = "pydantic";
149
+ else if (baseText.includes("Model"))
150
+ framework = "django";
151
+ else if (baseText.includes("Base"))
152
+ framework = "sqlalchemy";
153
+ if (framework === "unknown")
154
+ continue; // Not a recognized model
155
+ const fields = [];
156
+ const relationships = [];
157
+ const body = classNode.childForFieldName("body");
158
+ if (body) {
159
+ // Collect class-level assignments
160
+ const assignments = findChildren(body, "expression_statement")
161
+ .map(e => e.namedChildren.find(c => c.type === "assignment" || c.type === "type" || c.type === "typed_parameter") || e);
162
+ for (const stmt of assignments) {
163
+ const stmtText = text(stmt);
164
+ const matchField = stmtText.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\\s*[:=]/);
165
+ if (matchField) {
166
+ fields.push(matchField[1]);
167
+ if (stmtText.includes("ForeignKey") || stmtText.includes("relationship(") || stmtText.includes("ManyTo")) {
168
+ relationships.push(matchField[1]);
169
+ }
170
+ }
171
+ }
172
+ }
173
+ models.push({
174
+ name,
175
+ file,
176
+ framework,
177
+ fields,
178
+ relationships
179
+ });
180
+ }
181
+ return { endpoints, models, components, tests };
182
+ }
183
+ };
@@ -0,0 +1,69 @@
1
+ import Parser from "tree-sitter";
2
+ export function runAdapter(adapter, file, source) {
3
+ const parser = new Parser();
4
+ parser.setLanguage(adapter.language);
5
+ const tree = parser.parse(source);
6
+ if (adapter.extract) {
7
+ return adapter.extract(file, source, tree.rootNode);
8
+ }
9
+ const endpoints = [];
10
+ const models = [];
11
+ const components = [];
12
+ const tests = [];
13
+ if (adapter.queries.endpoints) {
14
+ const query = new Parser.Query(adapter.language, adapter.queries.endpoints);
15
+ const matches = query.matches(tree.rootNode);
16
+ for (const match of matches) {
17
+ let method = "GET";
18
+ let path = "";
19
+ let handler = "";
20
+ for (const capture of match.captures) {
21
+ const text = source.substring(capture.node.startIndex, capture.node.endIndex);
22
+ if (capture.name === "method")
23
+ method = text.replace(/['"]/g, "");
24
+ if (capture.name === "path")
25
+ path = text.replace(/['"]/g, "");
26
+ if (capture.name === "handler")
27
+ handler = text;
28
+ }
29
+ if (handler) {
30
+ endpoints.push({ file, method, path, handler, service_calls: [] });
31
+ }
32
+ }
33
+ }
34
+ if (adapter.queries.models) {
35
+ const query = new Parser.Query(adapter.language, adapter.queries.models);
36
+ const matches = query.matches(tree.rootNode);
37
+ for (const match of matches) {
38
+ let name = "";
39
+ for (const capture of match.captures) {
40
+ if (capture.name === "name") {
41
+ name = source.substring(capture.node.startIndex, capture.node.endIndex);
42
+ }
43
+ }
44
+ if (name) {
45
+ models.push({ name, file, framework: "unknown", fields: [], relationships: [] });
46
+ }
47
+ }
48
+ }
49
+ if (adapter.queries.tests) {
50
+ const query = new Parser.Query(adapter.language, adapter.queries.tests);
51
+ const matches = query.matches(tree.rootNode);
52
+ for (const match of matches) {
53
+ let test_name = "";
54
+ let suite_name = null;
55
+ for (const capture of match.captures) {
56
+ if (capture.name === "test_name") {
57
+ test_name = source.substring(capture.node.startIndex, capture.node.endIndex).replace(/['"]/g, "");
58
+ }
59
+ if (capture.name === "suite_name") {
60
+ suite_name = source.substring(capture.node.startIndex, capture.node.endIndex).replace(/['"]/g, "");
61
+ }
62
+ }
63
+ if (test_name) {
64
+ tests.push({ file, test_name, suite_name });
65
+ }
66
+ }
67
+ }
68
+ return { endpoints, models, components, tests };
69
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,179 @@
1
+ import TypeScript from "tree-sitter-typescript";
2
+ import Parser from "tree-sitter";
3
+ import path from "node:path";
4
+ // Utility to recursively find children of a certain type
5
+ function findChildren(node, type) {
6
+ const results = [];
7
+ if (node.type === type)
8
+ results.push(node);
9
+ for (const child of node.namedChildren) {
10
+ results.push(...findChildren(child, type));
11
+ }
12
+ return results;
13
+ }
14
+ export const TypeScriptAdapter = {
15
+ name: "typescript",
16
+ language: TypeScript.tsx, // We use the TSX grammar to capture both TS and TSX seamlessly
17
+ fileExtensions: [".ts", ".tsx", ".js", ".jsx"],
18
+ queries: {
19
+ // Endpoints: Express "app.get('/route', (req, res) => {})" or NextJS Route Handlers
20
+ endpoints: `
21
+ (call_expression
22
+ function: (member_expression
23
+ object: (identifier) @app_instance
24
+ property: (property_identifier) @method (#match? @method "^get$|^post$|^put$|^delete$|^patch|use$")
25
+ )
26
+ arguments: (arguments (string (string_fragment) @path))
27
+ ) @express_route
28
+
29
+ (export_statement
30
+ declaration: (lexical_declaration
31
+ (variable_declarator
32
+ name: (identifier) @method (#match? @method "^GET$|^POST$|^PUT$|^DELETE$|^PATCH$")
33
+ value: (arrow_function)
34
+ )
35
+ )
36
+ ) @next_route
37
+ `,
38
+ // Components: React functions returning JSX, or default exports
39
+ components: `
40
+ (function_declaration
41
+ name: (identifier) @name
42
+ parameters: (formal_parameters)? @params
43
+ ) @react_component
44
+ `,
45
+ // Tests: Jest/Vitest describe and it/test blocks
46
+ tests: `
47
+ (call_expression
48
+ function: (identifier) @suite_func (#match? @suite_func "^describe$")
49
+ arguments: (arguments (string (string_fragment) @suite_name))
50
+ )
51
+ (call_expression
52
+ function: (identifier) @test_func (#match? @test_func "^it$|^test$")
53
+ arguments: (arguments (string (string_fragment) @test_name))
54
+ )
55
+ `
56
+ },
57
+ extract(file, source, root) {
58
+ const endpoints = [];
59
+ const models = [];
60
+ const components = [];
61
+ const tests = [];
62
+ const text = (n) => source.substring(n.startIndex, n.endIndex);
63
+ const isNextJS = source.includes("next/server") || source.includes("next/router") || file.includes("app/") || file.includes("pages/");
64
+ // 1. Process Endpoints
65
+ const epQuery = new Parser.Query(this.language, this.queries.endpoints);
66
+ const epMatches = epQuery.matches(root);
67
+ for (const match of epMatches) {
68
+ let isExpress = false;
69
+ let isNextRoute = false;
70
+ let method = "GET";
71
+ let routePath = "";
72
+ for (const cap of match.captures) {
73
+ if (cap.name === "express_route")
74
+ isExpress = true;
75
+ if (cap.name === "next_route")
76
+ isNextRoute = true;
77
+ if (cap.name === "method")
78
+ method = text(cap.node).toUpperCase();
79
+ if (cap.name === "path")
80
+ routePath = text(cap.node);
81
+ }
82
+ if (isNextRoute && isNextJS) {
83
+ // NextJS App Router infers path from the filesystem
84
+ const parts = file.split("app/");
85
+ if (parts.length > 1) {
86
+ routePath = "/" + path.dirname(parts[1]);
87
+ if (routePath.endsWith("/."))
88
+ routePath = routePath.slice(0, -2);
89
+ }
90
+ }
91
+ if (isExpress || isNextRoute) {
92
+ endpoints.push({
93
+ file,
94
+ method,
95
+ path: routePath || "/",
96
+ handler: isNextRoute ? method : "anonymous_handler",
97
+ request_schema: null,
98
+ response_schema: null,
99
+ service_calls: []
100
+ });
101
+ }
102
+ }
103
+ // 2. Process React Components
104
+ const compQuery = new Parser.Query(this.language, this.queries.components);
105
+ const compMatches = compQuery.matches(root);
106
+ for (const match of compMatches) {
107
+ let name = path.basename(file, path.extname(file)); // Fallback name
108
+ let export_kind = "named";
109
+ let propsNode = null;
110
+ let id = "";
111
+ for (const cap of match.captures) {
112
+ if (cap.name === "name")
113
+ name = text(cap.node);
114
+ if (cap.name === "default_react_component")
115
+ export_kind = "default";
116
+ if (cap.name === "params")
117
+ propsNode = cap.node;
118
+ }
119
+ id = file + "#" + name;
120
+ const props = [];
121
+ // Extract React Prop definitions if typed
122
+ if (propsNode) {
123
+ const firstParam = propsNode.namedChildren[0];
124
+ if (firstParam && firstParam.type === "required_parameter") {
125
+ const pattern = firstParam.childForFieldName("pattern");
126
+ const typeAnn = firstParam.childForFieldName("type");
127
+ if (pattern && pattern.type === "object_pattern") {
128
+ // Destructured props { title, className }
129
+ for (const prop of pattern.namedChildren) {
130
+ if (prop.type === "shorthand_property_identifier") {
131
+ props.push({ name: text(prop), type: "any" });
132
+ }
133
+ }
134
+ }
135
+ if (typeAnn) {
136
+ // If it's a typed parameter (props: ItemProps)
137
+ const t = text(typeAnn).replace(/^:\\s*/, "");
138
+ if (props.length === 0) {
139
+ props.push({ name: "props", type: t });
140
+ }
141
+ else {
142
+ props.forEach(p => p.type = t); // Assign the generic interface type
143
+ }
144
+ }
145
+ }
146
+ }
147
+ components.push({
148
+ id,
149
+ name,
150
+ file,
151
+ export_kind,
152
+ props
153
+ });
154
+ }
155
+ // 3. Process Tests
156
+ if (this.queries.tests) {
157
+ const testsQuery = new Parser.Query(this.language, this.queries.tests);
158
+ const testsMatches = testsQuery.matches(root);
159
+ let currentSuite = null;
160
+ for (const match of testsMatches) {
161
+ let test_name = "";
162
+ let suite_name = null;
163
+ for (const cap of match.captures) {
164
+ if (cap.name === "suite_name")
165
+ suite_name = text(cap.node);
166
+ if (cap.name === "test_name")
167
+ test_name = text(cap.node);
168
+ }
169
+ // Very basic tracking: if we see a describe block, we remember its name.
170
+ if (suite_name)
171
+ currentSuite = suite_name;
172
+ if (test_name) {
173
+ tests.push({ file, test_name, suite_name: currentSuite });
174
+ }
175
+ }
176
+ }
177
+ return { endpoints, models, components, tests };
178
+ }
179
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * SPECGUARD BENCHMARKING FRAMEWORK
3
+ *
4
+ * Shared framework for measuring:
5
+ * - Token consumption (input/output)
6
+ * - Implementation time
7
+ * - Test coverage
8
+ * - Hallucination risk
9
+ * - Code quality
10
+ */
11
+ /**
12
+ * Calculate costs using accurate GPT-4o pricing
13
+ */
14
+ export function calculateCost(inputTokens, outputTokens) {
15
+ const INPUT_PRICE = 5 / 1_000_000; // $5/1M tokens
16
+ const OUTPUT_PRICE = 15 / 1_000_000; // $15/1M tokens (3x more expensive)
17
+ return inputTokens * INPUT_PRICE + outputTokens * OUTPUT_PRICE;
18
+ }
19
+ /**
20
+ * Calculate token reduction percentage
21
+ */
22
+ export function calculateTokenReduction(withContext, withoutContext) {
23
+ return ((withoutContext - withContext) / withoutContext) * 100;
24
+ }
25
+ /**
26
+ * Calculate cost reduction percentage
27
+ */
28
+ export function calculateCostReduction(withContext, withoutContext) {
29
+ return ((withoutContext - withContext) / withoutContext) * 100;
30
+ }
31
+ /**
32
+ * Calculate time reduction percentage
33
+ */
34
+ export function calculateTimeReduction(withContext, withoutContext) {
35
+ return ((withoutContext - withContext) / withoutContext) * 100;
36
+ }
37
+ /**
38
+ * Build comparative metrics table
39
+ */
40
+ export function buildComparativeTable(features) {
41
+ const rows = features.map((f) => ({
42
+ feature: f.featureName,
43
+ tokenReduction: calculateTokenReduction(f.withContext.totalTokens, f.withoutContext.totalTokens),
44
+ costReduction: calculateTokenReduction(f.withContext.apiCost, f.withoutContext.apiCost),
45
+ timeReduction: calculateTimeReduction(f.withContext.implementationTimeMinutes, f.withoutContext.implementationTimeMinutes),
46
+ hallucReduction: f.withoutContext.hallucationRiskPercent - f.withContext.hallucationRiskPercent,
47
+ }));
48
+ return {
49
+ rows,
50
+ averageTokenReduction: rows.reduce((sum, r) => sum + r.tokenReduction, 0) / rows.length,
51
+ averageCostReduction: rows.reduce((sum, r) => sum + r.costReduction, 0) / rows.length,
52
+ averageTimeReduction: rows.reduce((sum, r) => sum + r.timeReduction, 0) / rows.length,
53
+ averageHallucReduction: rows.reduce((sum, r) => sum + r.hallucReduction, 0) / rows.length,
54
+ };
55
+ }
56
+ /**
57
+ * Generate markdown benchmark report
58
+ */
59
+ export function generateBenchmarkMarkdown(agg) {
60
+ let md = "# SPECGUARD MULTI-FEATURE BENCHMARK REPORT\n\n";
61
+ md += `**Generated:** ${new Date().toISOString()}\n`;
62
+ md += `**Features Tested:** ${agg.features.length}\n\n`;
63
+ // Summary
64
+ md += "## Executive Summary\n\n";
65
+ md += `- **Total Tokens Saved:** ${(agg.summary.totalTokensSaved / 1000).toFixed(1)}K tokens (${calculateTokenReduction(agg.summary.totalTokensSaved, agg.summary.totalTokensSaved + (agg.summary.totalTokensSaved * 9)).toFixed(1)}%)\\n`;
66
+ md += `- **Total Cost Saved:** $${agg.summary.totalCostSaved.toFixed(2)}\\n`;
67
+ md += `- **Total Time Saved:** ${agg.summary.totalTimeSaved.toFixed(1)} hours\\n`;
68
+ md += `- **Avg Hallucination Reduction:** ${agg.summary.averageHallucinationReduction.toFixed(1)}pp\\n\n`;
69
+ // Per-feature breakdown
70
+ md += "## Per-Feature Results\n\n";
71
+ for (const feature of agg.features) {
72
+ md += `### ${feature.featureName} (Feature ${feature.featureId})\n`;
73
+ md += `${feature.description}\n\n`;
74
+ md += `| Metric | WITH Context | WITHOUT Context | Savings |\\n`;
75
+ md += `|--------|--------------|-----------------|---------|\\n`;
76
+ md += `| Tokens | ${feature.withContext.totalTokens.toLocaleString()} | ${feature.withoutContext.totalTokens.toLocaleString()} | ${calculateTokenReduction(feature.withContext.totalTokens, feature.withoutContext.totalTokens).toFixed(1)}% |\\n`;
77
+ md += `| Cost | $${feature.withContext.apiCost.toFixed(3)} | $${feature.withoutContext.apiCost.toFixed(3)} | ${calculateCostReduction(feature.withContext.apiCost, feature.withoutContext.apiCost).toFixed(1)}% |\\n`;
78
+ md += `| Time | ${feature.withContext.implementationTimeMinutes.toFixed(1)}h | ${feature.withoutContext.implementationTimeMinutes.toFixed(1)}h | ${calculateTimeReduction(feature.withContext.implementationTimeMinutes, feature.withoutContext.implementationTimeMinutes).toFixed(1)}% |\\n`;
79
+ md += `| Hallucinations | ${feature.withContext.hallucationRiskPercent.toFixed(1)}% | ${feature.withoutContext.hallucationRiskPercent.toFixed(1)}% | ${(feature.withoutContext.hallucationRiskPercent - feature.withContext.hallucationRiskPercent).toFixed(1)}pp |\\n`;
80
+ md += `| Tests | ${feature.withContext.testsPassing}/${feature.withContext.testsTotal} | - | - |\\n\n`;
81
+ }
82
+ return md;
83
+ }
84
+ export default {
85
+ calculateCost,
86
+ calculateTokenReduction,
87
+ calculateCostReduction,
88
+ calculateTimeReduction,
89
+ buildComparativeTable,
90
+ generateBenchmarkMarkdown,
91
+ };