@veraxhq/verax 0.2.1 → 0.3.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/README.md +14 -18
- package/bin/verax.js +7 -0
- package/package.json +3 -3
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +79 -25
- package/src/cli/commands/ga.js +243 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +131 -2
- package/src/cli/commands/release-check.js +213 -0
- package/src/cli/commands/run.js +246 -35
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +304 -67
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +546 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/determinism-runner.js +123 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/env-url.js +4 -0
- package/src/cli/util/expectation-extractor.js +369 -73
- package/src/cli/util/findings-writer.js +126 -16
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +3 -12
- package/src/cli/util/project-discovery.js +3 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +1 -0
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +147 -0
- package/src/cli/util/svelte-state-detector.js +243 -0
- package/src/cli/util/vue-navigation-detector.js +177 -0
- package/src/cli/util/vue-sfc-extractor.js +162 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/verax/cli/finding-explainer.js +56 -3
- package/src/verax/core/artifacts/registry.js +154 -0
- package/src/verax/core/artifacts/verifier.js +980 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +231 -0
- package/src/verax/core/capabilities/gates.js +499 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +137 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +79 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +484 -0
- package/src/verax/core/confidence-engine.js +486 -0
- package/src/verax/core/confidence-engine.js.backup +471 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +185 -0
- package/src/verax/core/contracts/validators.js +381 -0
- package/src/verax/core/decision-snapshot.js +30 -3
- package/src/verax/core/decisions/decision.trace.js +276 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +364 -0
- package/src/verax/core/determinism/engine.js +221 -0
- package/src/verax/core/determinism/finding-identity.js +148 -0
- package/src/verax/core/determinism/normalize.js +438 -0
- package/src/verax/core/determinism/report-writer.js +92 -0
- package/src/verax/core/determinism/run-fingerprint.js +118 -0
- package/src/verax/core/dynamic-route-intelligence.js +528 -0
- package/src/verax/core/evidence/evidence-capture-service.js +307 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +190 -0
- package/src/verax/core/failures/exit-codes.js +86 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +132 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +434 -0
- package/src/verax/core/ga/ga.enforcer.js +86 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +83 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/observe/run-timeline.js +316 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +198 -0
- package/src/verax/core/pipeline-tracker.js +238 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +271 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +159 -0
- package/src/verax/core/release/reproducibility.check.js +221 -0
- package/src/verax/core/release/sbom.builder.js +283 -0
- package/src/verax/core/report/cross-index.js +192 -0
- package/src/verax/core/report/human-summary.js +222 -0
- package/src/verax/core/route-intelligence.js +419 -0
- package/src/verax/core/security/secrets.scan.js +326 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +124 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +326 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/confidence-engine.js +628 -40
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +18 -1
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +3 -1
- package/src/verax/detect/findings-writer.js +141 -5
- package/src/verax/detect/index.js +229 -5
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +57 -3
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/index.js +413 -45
- package/src/verax/learn/action-contract-extractor.js +682 -64
- package/src/verax/learn/route-validator.js +4 -1
- package/src/verax/observe/index.js +88 -843
- package/src/verax/observe/interaction-runner.js +25 -8
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +191 -0
- package/src/verax/observe/observe-runner.js +226 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +148 -2
- package/src/verax/scan-summary-writer.js +42 -8
- package/src/verax/shared/artifact-manager.js +8 -5
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 20 — Angular Component Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts component metadata, template, and class content from Angular TypeScript files.
|
|
5
|
+
* Handles @Component decorators, template files, and component class methods.
|
|
6
|
+
* Deterministic and robust (no external runtime execution).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PHASE 20: Extract Angular component metadata
|
|
11
|
+
*
|
|
12
|
+
* @param {string} content - Full .ts file content
|
|
13
|
+
* @param {string} filePath - Path to the .ts file (for context)
|
|
14
|
+
* @param {string} projectRoot - Project root directory
|
|
15
|
+
* @returns {Object} { componentClass: {content, startLine}, template: {content, path, isInline}, decorator: {content, startLine} }
|
|
16
|
+
*/
|
|
17
|
+
export function extractAngularComponent(content, filePath, projectRoot) {
|
|
18
|
+
const result = {
|
|
19
|
+
componentClass: null,
|
|
20
|
+
template: null,
|
|
21
|
+
decorator: null,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Extract @Component decorator
|
|
26
|
+
const decoratorRegex = /@Component\s*\(\s*\{([\s\S]*?)\}\s*\)/;
|
|
27
|
+
const decoratorMatch = content.match(decoratorRegex);
|
|
28
|
+
|
|
29
|
+
if (decoratorMatch) {
|
|
30
|
+
const decoratorContent = decoratorMatch[0];
|
|
31
|
+
const decoratorConfig = decoratorMatch[1];
|
|
32
|
+
const beforeDecorator = content.substring(0, decoratorMatch.index);
|
|
33
|
+
const decoratorStartLine = (beforeDecorator.match(/\n/g) || []).length + 1;
|
|
34
|
+
|
|
35
|
+
result.decorator = {
|
|
36
|
+
content: decoratorConfig,
|
|
37
|
+
startLine: decoratorStartLine,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Extract template (inline or external file)
|
|
41
|
+
const templateMatch = decoratorConfig.match(/template\s*:\s*['"`]([\s\S]*?)['"`]/);
|
|
42
|
+
const templateUrlMatch = decoratorConfig.match(/templateUrl\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
43
|
+
|
|
44
|
+
if (templateMatch) {
|
|
45
|
+
// Inline template
|
|
46
|
+
result.template = {
|
|
47
|
+
content: templateMatch[1],
|
|
48
|
+
path: filePath,
|
|
49
|
+
isInline: true,
|
|
50
|
+
};
|
|
51
|
+
} else if (templateUrlMatch) {
|
|
52
|
+
// External template file
|
|
53
|
+
const templateUrl = templateUrlMatch[1];
|
|
54
|
+
const templatePath = resolveTemplatePath(templateUrl, filePath, projectRoot);
|
|
55
|
+
result.template = {
|
|
56
|
+
path: templatePath,
|
|
57
|
+
isInline: false,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Extract component class
|
|
63
|
+
const classRegex = /export\s+class\s+(\w+)\s*(?:extends\s+\w+)?\s*\{([\s\S]*?)\n\}/;
|
|
64
|
+
const classMatch = content.match(classRegex);
|
|
65
|
+
|
|
66
|
+
if (classMatch) {
|
|
67
|
+
const beforeClass = content.substring(0, classMatch.index);
|
|
68
|
+
const classStartLine = (beforeClass.match(/\n/g) || []).length + 1;
|
|
69
|
+
|
|
70
|
+
result.componentClass = {
|
|
71
|
+
content: classMatch[2],
|
|
72
|
+
className: classMatch[1],
|
|
73
|
+
startLine: classStartLine,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// Skip if extraction fails
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve template path from templateUrl
|
|
85
|
+
*
|
|
86
|
+
* @param {string} templateUrl - Template URL from decorator
|
|
87
|
+
* @param {string} componentPath - Path to component file
|
|
88
|
+
* @param {string} projectRoot - Project root directory
|
|
89
|
+
* @returns {string} Resolved template path
|
|
90
|
+
*/
|
|
91
|
+
function resolveTemplatePath(templateUrl, componentPath, projectRoot) {
|
|
92
|
+
const { join, dirname, resolve } = require('path');
|
|
93
|
+
|
|
94
|
+
if (templateUrl.startsWith('./') || templateUrl.startsWith('../')) {
|
|
95
|
+
// Relative path
|
|
96
|
+
return resolve(dirname(componentPath), templateUrl);
|
|
97
|
+
} else {
|
|
98
|
+
// Absolute path from project root
|
|
99
|
+
return resolve(projectRoot, templateUrl);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extract template bindings from Angular template
|
|
105
|
+
* Detects event handlers, property bindings, and structural directives
|
|
106
|
+
*
|
|
107
|
+
* @param {string} templateContent - Template content
|
|
108
|
+
* @returns {Object} { eventHandlers: [], propertyBindings: [], structuralDirectives: [] }
|
|
109
|
+
*/
|
|
110
|
+
export function extractTemplateBindings(templateContent) {
|
|
111
|
+
const eventHandlers = [];
|
|
112
|
+
const propertyBindings = [];
|
|
113
|
+
const structuralDirectives = [];
|
|
114
|
+
|
|
115
|
+
// Extract event handlers: (click)="handler()", (submit)="onSubmit()"
|
|
116
|
+
const eventHandlerRegex = /\((\w+)\)\s*=\s*["']([^"']+)["']/g;
|
|
117
|
+
let handlerMatch;
|
|
118
|
+
while ((handlerMatch = eventHandlerRegex.exec(templateContent)) !== null) {
|
|
119
|
+
eventHandlers.push({
|
|
120
|
+
event: handlerMatch[1],
|
|
121
|
+
handler: handlerMatch[2],
|
|
122
|
+
line: (templateContent.substring(0, handlerMatch.index).match(/\n/g) || []).length + 1,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Extract property bindings: [property]="value", [disabled]="isDisabled"
|
|
127
|
+
const propertyBindingRegex = /\[(\w+)\]\s*=\s*["']([^"']+)["']/g;
|
|
128
|
+
let bindingMatch;
|
|
129
|
+
while ((bindingMatch = propertyBindingRegex.exec(templateContent)) !== null) {
|
|
130
|
+
propertyBindings.push({
|
|
131
|
+
property: bindingMatch[1],
|
|
132
|
+
value: bindingMatch[2],
|
|
133
|
+
line: (templateContent.substring(0, bindingMatch.index).match(/\n/g) || []).length + 1,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Extract structural directives: *ngIf="condition", *ngFor="item of items"
|
|
138
|
+
const structuralDirectiveRegex = /\*ng(If|For|Switch)\s*=\s*["']([^"']+)["']/g;
|
|
139
|
+
let directiveMatch;
|
|
140
|
+
while ((directiveMatch = structuralDirectiveRegex.exec(templateContent)) !== null) {
|
|
141
|
+
structuralDirectives.push({
|
|
142
|
+
directive: directiveMatch[1],
|
|
143
|
+
expression: directiveMatch[2],
|
|
144
|
+
line: (templateContent.substring(0, directiveMatch.index).match(/\n/g) || []).length + 1,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
eventHandlers,
|
|
150
|
+
propertyBindings,
|
|
151
|
+
structuralDirectives,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Map template handlers to component class methods
|
|
157
|
+
*
|
|
158
|
+
* @param {Array} eventHandlers - Event handlers from template
|
|
159
|
+
* @param {string} classContent - Component class content
|
|
160
|
+
* @returns {Array} Mapped handlers with method references
|
|
161
|
+
*/
|
|
162
|
+
export function mapTemplateHandlersToClass(eventHandlers, classContent) {
|
|
163
|
+
return eventHandlers.map(handler => {
|
|
164
|
+
// Extract method name from handler expression
|
|
165
|
+
const methodName = handler.handler.split('(')[0].trim();
|
|
166
|
+
|
|
167
|
+
// Try to find method definition in class
|
|
168
|
+
const methodRegex = new RegExp(`(?:public|private|protected)?\\s*(?:async\\s+)?${methodName}\\s*\\(`, 'g');
|
|
169
|
+
const methodMatch = methodRegex.exec(classContent);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...handler,
|
|
173
|
+
methodName,
|
|
174
|
+
methodFound: !!methodMatch,
|
|
175
|
+
methodLine: methodMatch ? (classContent.substring(0, methodMatch.index).match(/\n/g) || []).length + 1 : null,
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 20 — Angular Navigation Detector
|
|
3
|
+
*
|
|
4
|
+
* Detects navigation promises in Angular applications:
|
|
5
|
+
* - routerLink directive in templates
|
|
6
|
+
* - Router.navigate() calls in component methods
|
|
7
|
+
* - ActivatedRoute navigation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { extractAngularComponent, extractTemplateBindings, mapTemplateHandlersToClass } from './angular-component-extractor.js';
|
|
11
|
+
import { parse } from '@babel/parser';
|
|
12
|
+
import traverse from '@babel/traverse';
|
|
13
|
+
import { readFileSync, existsSync } from 'fs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect navigation promises in Angular component
|
|
17
|
+
*
|
|
18
|
+
* @param {string} filePath - Path to .ts file
|
|
19
|
+
* @param {string} content - Full file content
|
|
20
|
+
* @param {string} projectRoot - Project root directory
|
|
21
|
+
* @returns {Array} Array of navigation expectations
|
|
22
|
+
*/
|
|
23
|
+
export function detectAngularNavigation(filePath, content, projectRoot) {
|
|
24
|
+
const expectations = [];
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const component = extractAngularComponent(content, filePath, projectRoot);
|
|
28
|
+
|
|
29
|
+
// Extract navigation from template (routerLink)
|
|
30
|
+
if (component.template) {
|
|
31
|
+
let templateContent = null;
|
|
32
|
+
|
|
33
|
+
if (component.template.isInline) {
|
|
34
|
+
templateContent = component.template.content;
|
|
35
|
+
} else if (existsSync(component.template.path)) {
|
|
36
|
+
templateContent = readFileSync(component.template.path, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (templateContent) {
|
|
40
|
+
const templateBindings = extractTemplateBindings(templateContent);
|
|
41
|
+
|
|
42
|
+
// Extract routerLink directives
|
|
43
|
+
const routerLinkRegex = /routerLink\s*=\s*["']([^"']+)["']/g;
|
|
44
|
+
let linkMatch;
|
|
45
|
+
while ((linkMatch = routerLinkRegex.exec(templateContent)) !== null) {
|
|
46
|
+
const href = linkMatch[1];
|
|
47
|
+
// Skip external links and hash-only links
|
|
48
|
+
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('#')) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const beforeMatch = templateContent.substring(0, linkMatch.index);
|
|
53
|
+
const line = (beforeMatch.match(/\n/g) || []).length + 1;
|
|
54
|
+
|
|
55
|
+
expectations.push({
|
|
56
|
+
type: 'navigation',
|
|
57
|
+
target: href,
|
|
58
|
+
context: 'template',
|
|
59
|
+
sourceRef: {
|
|
60
|
+
file: component.template.isInline ? filePath : component.template.path,
|
|
61
|
+
line,
|
|
62
|
+
snippet: linkMatch[0],
|
|
63
|
+
},
|
|
64
|
+
proof: 'PROVEN_EXPECTATION',
|
|
65
|
+
metadata: {
|
|
66
|
+
navigationType: 'routerLink',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Extract navigation from component class (Router.navigate())
|
|
74
|
+
if (component.componentClass && component.componentClass.content) {
|
|
75
|
+
try {
|
|
76
|
+
const ast = parse(component.componentClass.content, {
|
|
77
|
+
sourceType: 'module',
|
|
78
|
+
plugins: ['typescript', 'decorators-legacy', 'classProperties'],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
traverse.default(ast, {
|
|
82
|
+
CallExpression(path) {
|
|
83
|
+
const { node } = path;
|
|
84
|
+
|
|
85
|
+
// Detect router.navigate() calls
|
|
86
|
+
if (
|
|
87
|
+
node.callee.type === 'MemberExpression' &&
|
|
88
|
+
node.callee.property.name === 'navigate' &&
|
|
89
|
+
node.arguments.length > 0
|
|
90
|
+
) {
|
|
91
|
+
const arg = node.arguments[0];
|
|
92
|
+
let target = null;
|
|
93
|
+
|
|
94
|
+
if (arg.type === 'StringLiteral') {
|
|
95
|
+
target = arg.value;
|
|
96
|
+
} else if (arg.type === 'ArrayExpression' && arg.elements.length > 0) {
|
|
97
|
+
// Router.navigate(['/path']) or Router.navigate(['/path', param])
|
|
98
|
+
const firstElement = arg.elements[0];
|
|
99
|
+
if (firstElement.type === 'StringLiteral') {
|
|
100
|
+
target = firstElement.value;
|
|
101
|
+
}
|
|
102
|
+
} else if (arg.type === 'TemplateLiteral' && arg.quasis.length === 1) {
|
|
103
|
+
target = arg.quasis[0].value.raw;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (target && !target.startsWith('http://') && !target.startsWith('https://') && !target.startsWith('#')) {
|
|
107
|
+
const location = node.loc;
|
|
108
|
+
const line = component.componentClass.startLine + (location ? location.start.line - 1 : 0);
|
|
109
|
+
|
|
110
|
+
expectations.push({
|
|
111
|
+
type: 'navigation',
|
|
112
|
+
target,
|
|
113
|
+
context: 'router-navigate',
|
|
114
|
+
sourceRef: {
|
|
115
|
+
file: filePath,
|
|
116
|
+
line,
|
|
117
|
+
snippet: component.componentClass.content.substring(
|
|
118
|
+
node.start - (ast.program.body[0]?.start || 0),
|
|
119
|
+
node.end - (ast.program.body[0]?.start || 0)
|
|
120
|
+
),
|
|
121
|
+
},
|
|
122
|
+
proof: arg.type === 'StringLiteral' ? 'PROVEN_EXPECTATION' : 'LIKELY_EXPECTATION',
|
|
123
|
+
metadata: {
|
|
124
|
+
navigationType: 'router-navigate',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
} catch (parseError) {
|
|
132
|
+
// Skip if parsing fails
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// Skip if extraction fails
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return expectations;
|
|
140
|
+
}
|
|
141
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 20 — Angular Network Detector
|
|
3
|
+
*
|
|
4
|
+
* Detects network calls (HttpClient, fetch) in Angular component methods and services.
|
|
5
|
+
* Reuses AST network detector but ensures it works with Angular TypeScript files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { extractAngularComponent, extractTemplateBindings, mapTemplateHandlersToClass } from './angular-component-extractor.js';
|
|
9
|
+
import { detectNetworkCallsAST } from './ast-network-detector.js';
|
|
10
|
+
import { relative } from 'path';
|
|
11
|
+
import { readFileSync, existsSync } from 'fs';
|
|
12
|
+
import { parse } from '@babel/parser';
|
|
13
|
+
import traverse from '@babel/traverse';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect network promises in Angular component
|
|
17
|
+
*
|
|
18
|
+
* @param {string} filePath - Path to .ts file
|
|
19
|
+
* @param {string} content - Full file content
|
|
20
|
+
* @param {string} projectRoot - Project root directory
|
|
21
|
+
* @returns {Array} Array of network expectations
|
|
22
|
+
*/
|
|
23
|
+
export function detectAngularNetwork(filePath, content, projectRoot) {
|
|
24
|
+
const expectations = [];
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const component = extractAngularComponent(content, filePath, projectRoot);
|
|
28
|
+
|
|
29
|
+
// Extract event handlers from template to identify UI-bound handlers
|
|
30
|
+
let templateBindings = null;
|
|
31
|
+
if (component.template) {
|
|
32
|
+
let templateContent = null;
|
|
33
|
+
|
|
34
|
+
if (component.template.isInline) {
|
|
35
|
+
templateContent = component.template.content;
|
|
36
|
+
} else if (existsSync(component.template.path)) {
|
|
37
|
+
templateContent = readFileSync(component.template.path, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (templateContent) {
|
|
41
|
+
templateBindings = extractTemplateBindings(templateContent);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const mappedHandlers = component.componentClass && templateBindings
|
|
46
|
+
? mapTemplateHandlersToClass(templateBindings.eventHandlers, component.componentClass.content)
|
|
47
|
+
: [];
|
|
48
|
+
|
|
49
|
+
const uiBoundHandlers = new Set(mappedHandlers.map(h => h.methodName));
|
|
50
|
+
|
|
51
|
+
// Process component class
|
|
52
|
+
if (component.componentClass && component.componentClass.content) {
|
|
53
|
+
// Use AST network detector on class content
|
|
54
|
+
const networkCalls = detectNetworkCallsAST(component.componentClass.content, filePath, relative(projectRoot, filePath));
|
|
55
|
+
|
|
56
|
+
// Also detect HttpClient calls specifically
|
|
57
|
+
const httpClientCalls = detectHttpClientCalls(component.componentClass.content, component.componentClass.startLine);
|
|
58
|
+
|
|
59
|
+
// Combine and filter network calls
|
|
60
|
+
const allNetworkCalls = [...networkCalls, ...httpClientCalls];
|
|
61
|
+
|
|
62
|
+
for (const networkCall of allNetworkCalls) {
|
|
63
|
+
// Check if this is in a UI-bound handler
|
|
64
|
+
const isUIBound = networkCall.context && uiBoundHandlers.has(networkCall.context);
|
|
65
|
+
|
|
66
|
+
// Skip analytics-only calls (filtered by guardrails later)
|
|
67
|
+
if (networkCall.target && (
|
|
68
|
+
networkCall.target.includes('/api/analytics') ||
|
|
69
|
+
networkCall.target.includes('/api/track') ||
|
|
70
|
+
networkCall.target.includes('/api/beacon')
|
|
71
|
+
)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
expectations.push({
|
|
76
|
+
type: 'network',
|
|
77
|
+
target: networkCall.target || networkCall.url,
|
|
78
|
+
method: networkCall.method || 'GET',
|
|
79
|
+
context: networkCall.context || 'component',
|
|
80
|
+
sourceRef: {
|
|
81
|
+
file: filePath,
|
|
82
|
+
line: networkCall.line || component.componentClass.startLine,
|
|
83
|
+
snippet: networkCall.snippet || '',
|
|
84
|
+
},
|
|
85
|
+
proof: networkCall.proof || 'LIKELY_EXPECTATION',
|
|
86
|
+
metadata: {
|
|
87
|
+
isUIBound,
|
|
88
|
+
handlerContext: networkCall.context,
|
|
89
|
+
networkKind: networkCall.kind || 'http',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// Skip if extraction fails
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return expectations;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Detect HttpClient calls (get, post, put, delete, etc.)
|
|
103
|
+
*
|
|
104
|
+
* @param {string} classContent - Component class content
|
|
105
|
+
* @param {number} startLine - Starting line number
|
|
106
|
+
* @returns {Array} Array of network call detections
|
|
107
|
+
*/
|
|
108
|
+
function detectHttpClientCalls(classContent, startLine) {
|
|
109
|
+
const calls = [];
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const ast = parse(classContent, {
|
|
113
|
+
sourceType: 'module',
|
|
114
|
+
plugins: ['typescript', 'decorators-legacy', 'classProperties'],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
traverse.default(ast, {
|
|
118
|
+
CallExpression(path) {
|
|
119
|
+
const { node } = path;
|
|
120
|
+
|
|
121
|
+
// Detect http.get(), http.post(), etc.
|
|
122
|
+
if (
|
|
123
|
+
node.callee.type === 'MemberExpression' &&
|
|
124
|
+
['get', 'post', 'put', 'delete', 'patch'].includes(node.callee.property.name)
|
|
125
|
+
) {
|
|
126
|
+
const method = node.callee.property.name.toUpperCase();
|
|
127
|
+
const firstArg = node.arguments[0];
|
|
128
|
+
let url = null;
|
|
129
|
+
|
|
130
|
+
if (firstArg && firstArg.type === 'StringLiteral') {
|
|
131
|
+
url = firstArg.value;
|
|
132
|
+
} else if (firstArg && firstArg.type === 'TemplateLiteral' && firstArg.quasis.length === 1) {
|
|
133
|
+
url = firstArg.quasis[0].value.raw;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (url) {
|
|
137
|
+
const location = node.loc;
|
|
138
|
+
const line = startLine + (location ? location.start.line - 1 : 0);
|
|
139
|
+
|
|
140
|
+
calls.push({
|
|
141
|
+
target: url,
|
|
142
|
+
method,
|
|
143
|
+
line,
|
|
144
|
+
snippet: classContent.substring(
|
|
145
|
+
node.start - (ast.program.body[0]?.start || 0),
|
|
146
|
+
node.end - (ast.program.body[0]?.start || 0)
|
|
147
|
+
),
|
|
148
|
+
kind: 'httpClient',
|
|
149
|
+
proof: firstArg.type === 'StringLiteral' ? 'PROVEN_EXPECTATION' : 'LIKELY_EXPECTATION',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
} catch (parseError) {
|
|
156
|
+
// Skip if parsing fails
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return calls;
|
|
160
|
+
}
|
|
161
|
+
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 20 — Angular State Detector
|
|
3
|
+
*
|
|
4
|
+
* Detects state mutations (component properties, services) in Angular components.
|
|
5
|
+
* Only emits state promises if state is user-visible (used in template bindings).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { extractAngularComponent, extractTemplateBindings } from './angular-component-extractor.js';
|
|
9
|
+
import { parse } from '@babel/parser';
|
|
10
|
+
import traverse from '@babel/traverse';
|
|
11
|
+
import { readFileSync, existsSync } from 'fs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Detect state promises in Angular component
|
|
15
|
+
*
|
|
16
|
+
* @param {string} filePath - Path to .ts file
|
|
17
|
+
* @param {string} content - Full file content
|
|
18
|
+
* @param {string} projectRoot - Project root directory
|
|
19
|
+
* @returns {Array} Array of state expectations
|
|
20
|
+
*/
|
|
21
|
+
export function detectAngularState(filePath, content, projectRoot) {
|
|
22
|
+
const expectations = [];
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const component = extractAngularComponent(content, filePath, projectRoot);
|
|
26
|
+
|
|
27
|
+
// Extract template bindings to identify user-visible state
|
|
28
|
+
let templateBindings = null;
|
|
29
|
+
let templateContent = null;
|
|
30
|
+
|
|
31
|
+
if (component.template) {
|
|
32
|
+
if (component.template.isInline) {
|
|
33
|
+
templateContent = component.template.content;
|
|
34
|
+
} else if (existsSync(component.template.path)) {
|
|
35
|
+
templateContent = readFileSync(component.template.path, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (templateContent) {
|
|
39
|
+
templateBindings = extractTemplateBindings(templateContent);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Collect all state variables used in template
|
|
44
|
+
const templateStateVars = new Set();
|
|
45
|
+
|
|
46
|
+
// From property bindings: [property]="value"
|
|
47
|
+
if (templateBindings) {
|
|
48
|
+
templateBindings.propertyBindings.forEach(binding => {
|
|
49
|
+
templateStateVars.add(binding.value);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// From structural directives: *ngIf="condition"
|
|
53
|
+
templateBindings.structuralDirectives.forEach(directive => {
|
|
54
|
+
// Extract variable names from expressions
|
|
55
|
+
const varMatch = directive.expression.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
56
|
+
if (varMatch) {
|
|
57
|
+
templateStateVars.add(varMatch[1]);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// From template interpolation: {{ variable }}
|
|
63
|
+
if (templateContent) {
|
|
64
|
+
const interpolationRegex = /\{\{\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\}\}/g;
|
|
65
|
+
let varMatch;
|
|
66
|
+
while ((varMatch = interpolationRegex.exec(templateContent)) !== null) {
|
|
67
|
+
templateStateVars.add(varMatch[1]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Process component class to find state mutations
|
|
72
|
+
if (component.componentClass && component.componentClass.content) {
|
|
73
|
+
try {
|
|
74
|
+
const ast = parse(component.componentClass.content, {
|
|
75
|
+
sourceType: 'module',
|
|
76
|
+
plugins: ['typescript', 'decorators-legacy', 'classProperties'],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
traverse.default(ast, {
|
|
80
|
+
// Detect property assignments: this.property = value
|
|
81
|
+
AssignmentExpression(path) {
|
|
82
|
+
const { node } = path;
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
node.left.type === 'MemberExpression' &&
|
|
86
|
+
node.left.object.type === 'ThisExpression' &&
|
|
87
|
+
node.left.property.type === 'Identifier'
|
|
88
|
+
) {
|
|
89
|
+
const propertyName = node.left.property.name;
|
|
90
|
+
|
|
91
|
+
// Only emit if property is used in template
|
|
92
|
+
if (templateStateVars.has(propertyName)) {
|
|
93
|
+
const location = node.loc;
|
|
94
|
+
const line = component.componentClass.startLine + (location ? location.start.line - 1 : 0);
|
|
95
|
+
|
|
96
|
+
expectations.push({
|
|
97
|
+
type: 'state',
|
|
98
|
+
expectedTarget: propertyName,
|
|
99
|
+
context: 'property-assignment',
|
|
100
|
+
sourceRef: {
|
|
101
|
+
file: filePath,
|
|
102
|
+
line,
|
|
103
|
+
snippet: component.componentClass.content.substring(
|
|
104
|
+
node.start - (ast.program.body[0]?.start || 0),
|
|
105
|
+
node.end - (ast.program.body[0]?.start || 0)
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
proof: 'PROVEN_EXPECTATION',
|
|
109
|
+
metadata: {
|
|
110
|
+
templateUsage: Array.from(templateStateVars).filter(v => v === propertyName).length,
|
|
111
|
+
stateType: 'component-property',
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// Detect property declarations: property: type = value
|
|
119
|
+
ClassProperty(path) {
|
|
120
|
+
const { node } = path;
|
|
121
|
+
|
|
122
|
+
if (node.key.type === 'Identifier') {
|
|
123
|
+
const propertyName = node.key.name;
|
|
124
|
+
|
|
125
|
+
// Only emit if property is used in template and has initializer
|
|
126
|
+
if (templateStateVars.has(propertyName) && node.value) {
|
|
127
|
+
const location = node.loc;
|
|
128
|
+
const line = component.componentClass.startLine + (location ? location.start.line - 1 : 0);
|
|
129
|
+
|
|
130
|
+
expectations.push({
|
|
131
|
+
type: 'state',
|
|
132
|
+
expectedTarget: propertyName,
|
|
133
|
+
context: 'property-declaration',
|
|
134
|
+
sourceRef: {
|
|
135
|
+
file: filePath,
|
|
136
|
+
line,
|
|
137
|
+
snippet: component.componentClass.content.substring(
|
|
138
|
+
node.start - (ast.program.body[0]?.start || 0),
|
|
139
|
+
node.end - (ast.program.body[0]?.start || 0)
|
|
140
|
+
),
|
|
141
|
+
},
|
|
142
|
+
proof: 'PROVEN_EXPECTATION',
|
|
143
|
+
metadata: {
|
|
144
|
+
templateUsage: Array.from(templateStateVars).filter(v => v === propertyName).length,
|
|
145
|
+
stateType: 'component-property',
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
} catch (parseError) {
|
|
153
|
+
// Skip if parsing fails
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// Skip if extraction fails
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return expectations;
|
|
161
|
+
}
|
|
162
|
+
|