@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.
Files changed (152) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +3 -3
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +79 -25
  6. package/src/cli/commands/ga.js +243 -0
  7. package/src/cli/commands/gates.js +95 -0
  8. package/src/cli/commands/inspect.js +131 -2
  9. package/src/cli/commands/release-check.js +213 -0
  10. package/src/cli/commands/run.js +246 -35
  11. package/src/cli/commands/security-check.js +211 -0
  12. package/src/cli/commands/truth.js +114 -0
  13. package/src/cli/entry.js +304 -67
  14. package/src/cli/util/angular-component-extractor.js +179 -0
  15. package/src/cli/util/angular-navigation-detector.js +141 -0
  16. package/src/cli/util/angular-network-detector.js +161 -0
  17. package/src/cli/util/angular-state-detector.js +162 -0
  18. package/src/cli/util/ast-interactive-detector.js +546 -0
  19. package/src/cli/util/ast-network-detector.js +603 -0
  20. package/src/cli/util/ast-usestate-detector.js +602 -0
  21. package/src/cli/util/bootstrap-guard.js +86 -0
  22. package/src/cli/util/determinism-runner.js +123 -0
  23. package/src/cli/util/determinism-writer.js +129 -0
  24. package/src/cli/util/env-url.js +4 -0
  25. package/src/cli/util/expectation-extractor.js +369 -73
  26. package/src/cli/util/findings-writer.js +126 -16
  27. package/src/cli/util/learn-writer.js +3 -1
  28. package/src/cli/util/observe-writer.js +3 -1
  29. package/src/cli/util/paths.js +3 -12
  30. package/src/cli/util/project-discovery.js +3 -0
  31. package/src/cli/util/project-writer.js +3 -1
  32. package/src/cli/util/run-resolver.js +64 -0
  33. package/src/cli/util/source-requirement.js +55 -0
  34. package/src/cli/util/summary-writer.js +1 -0
  35. package/src/cli/util/svelte-navigation-detector.js +163 -0
  36. package/src/cli/util/svelte-network-detector.js +80 -0
  37. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  38. package/src/cli/util/svelte-state-detector.js +243 -0
  39. package/src/cli/util/vue-navigation-detector.js +177 -0
  40. package/src/cli/util/vue-sfc-extractor.js +162 -0
  41. package/src/cli/util/vue-state-detector.js +215 -0
  42. package/src/verax/cli/finding-explainer.js +56 -3
  43. package/src/verax/core/artifacts/registry.js +154 -0
  44. package/src/verax/core/artifacts/verifier.js +980 -0
  45. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  46. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  47. package/src/verax/core/capabilities/gates.js +499 -0
  48. package/src/verax/core/capabilities/registry.js +475 -0
  49. package/src/verax/core/confidence/confidence-compute.js +137 -0
  50. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  51. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  52. package/src/verax/core/confidence/confidence-weights.js +44 -0
  53. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  54. package/src/verax/core/confidence/confidence.loader.js +79 -0
  55. package/src/verax/core/confidence/confidence.schema.js +94 -0
  56. package/src/verax/core/confidence-engine-refactor.js +484 -0
  57. package/src/verax/core/confidence-engine.js +486 -0
  58. package/src/verax/core/confidence-engine.js.backup +471 -0
  59. package/src/verax/core/contracts/index.js +29 -0
  60. package/src/verax/core/contracts/types.js +185 -0
  61. package/src/verax/core/contracts/validators.js +381 -0
  62. package/src/verax/core/decision-snapshot.js +30 -3
  63. package/src/verax/core/decisions/decision.trace.js +276 -0
  64. package/src/verax/core/determinism/contract-writer.js +89 -0
  65. package/src/verax/core/determinism/contract.js +139 -0
  66. package/src/verax/core/determinism/diff.js +364 -0
  67. package/src/verax/core/determinism/engine.js +221 -0
  68. package/src/verax/core/determinism/finding-identity.js +148 -0
  69. package/src/verax/core/determinism/normalize.js +438 -0
  70. package/src/verax/core/determinism/report-writer.js +92 -0
  71. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  72. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  73. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  74. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  75. package/src/verax/core/evidence-builder.js +487 -0
  76. package/src/verax/core/execution-mode-context.js +77 -0
  77. package/src/verax/core/execution-mode-detector.js +190 -0
  78. package/src/verax/core/failures/exit-codes.js +86 -0
  79. package/src/verax/core/failures/failure-summary.js +76 -0
  80. package/src/verax/core/failures/failure.factory.js +225 -0
  81. package/src/verax/core/failures/failure.ledger.js +132 -0
  82. package/src/verax/core/failures/failure.types.js +196 -0
  83. package/src/verax/core/failures/index.js +10 -0
  84. package/src/verax/core/ga/ga-report-writer.js +43 -0
  85. package/src/verax/core/ga/ga.artifact.js +49 -0
  86. package/src/verax/core/ga/ga.contract.js +434 -0
  87. package/src/verax/core/ga/ga.enforcer.js +86 -0
  88. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  89. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  90. package/src/verax/core/guardrails/policy.loader.js +83 -0
  91. package/src/verax/core/guardrails/policy.schema.js +110 -0
  92. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  93. package/src/verax/core/guardrails-engine.js +505 -0
  94. package/src/verax/core/observe/run-timeline.js +316 -0
  95. package/src/verax/core/perf/perf.contract.js +186 -0
  96. package/src/verax/core/perf/perf.display.js +65 -0
  97. package/src/verax/core/perf/perf.enforcer.js +91 -0
  98. package/src/verax/core/perf/perf.monitor.js +209 -0
  99. package/src/verax/core/perf/perf.report.js +198 -0
  100. package/src/verax/core/pipeline-tracker.js +238 -0
  101. package/src/verax/core/product-definition.js +127 -0
  102. package/src/verax/core/release/provenance.builder.js +271 -0
  103. package/src/verax/core/release/release-report-writer.js +40 -0
  104. package/src/verax/core/release/release.enforcer.js +159 -0
  105. package/src/verax/core/release/reproducibility.check.js +221 -0
  106. package/src/verax/core/release/sbom.builder.js +283 -0
  107. package/src/verax/core/report/cross-index.js +192 -0
  108. package/src/verax/core/report/human-summary.js +222 -0
  109. package/src/verax/core/route-intelligence.js +419 -0
  110. package/src/verax/core/security/secrets.scan.js +326 -0
  111. package/src/verax/core/security/security-report.js +50 -0
  112. package/src/verax/core/security/security.enforcer.js +124 -0
  113. package/src/verax/core/security/supplychain.defaults.json +38 -0
  114. package/src/verax/core/security/supplychain.policy.js +326 -0
  115. package/src/verax/core/security/vuln.scan.js +265 -0
  116. package/src/verax/core/truth/truth.certificate.js +250 -0
  117. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  118. package/src/verax/detect/confidence-engine.js +628 -40
  119. package/src/verax/detect/confidence-helper.js +33 -0
  120. package/src/verax/detect/detection-engine.js +18 -1
  121. package/src/verax/detect/dynamic-route-findings.js +335 -0
  122. package/src/verax/detect/expectation-chain-detector.js +417 -0
  123. package/src/verax/detect/expectation-model.js +3 -1
  124. package/src/verax/detect/findings-writer.js +141 -5
  125. package/src/verax/detect/index.js +229 -5
  126. package/src/verax/detect/journey-stall-detector.js +558 -0
  127. package/src/verax/detect/route-findings.js +218 -0
  128. package/src/verax/detect/ui-feedback-findings.js +207 -0
  129. package/src/verax/detect/verdict-engine.js +57 -3
  130. package/src/verax/detect/view-switch-correlator.js +242 -0
  131. package/src/verax/index.js +413 -45
  132. package/src/verax/learn/action-contract-extractor.js +682 -64
  133. package/src/verax/learn/route-validator.js +4 -1
  134. package/src/verax/observe/index.js +88 -843
  135. package/src/verax/observe/interaction-runner.js +25 -8
  136. package/src/verax/observe/observe-context.js +205 -0
  137. package/src/verax/observe/observe-helpers.js +191 -0
  138. package/src/verax/observe/observe-runner.js +226 -0
  139. package/src/verax/observe/observers/budget-observer.js +185 -0
  140. package/src/verax/observe/observers/console-observer.js +102 -0
  141. package/src/verax/observe/observers/coverage-observer.js +107 -0
  142. package/src/verax/observe/observers/interaction-observer.js +471 -0
  143. package/src/verax/observe/observers/navigation-observer.js +132 -0
  144. package/src/verax/observe/observers/network-observer.js +87 -0
  145. package/src/verax/observe/observers/safety-observer.js +82 -0
  146. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  147. package/src/verax/observe/ui-feedback-detector.js +742 -0
  148. package/src/verax/observe/ui-signal-sensor.js +148 -2
  149. package/src/verax/scan-summary-writer.js +42 -8
  150. package/src/verax/shared/artifact-manager.js +8 -5
  151. package/src/verax/shared/css-spinner-rules.js +204 -0
  152. package/src/verax/shared/view-switch-rules.js +208 -0
@@ -1,32 +1,142 @@
1
1
  import { atomicWriteJson } from './atomic-write.js';
2
2
  import { resolve } from 'path';
3
3
  import { findingIdFromExpectationId } from './idgen.js';
4
+ import {
5
+ enforceContractsOnFindings,
6
+ FINDING_STATUS,
7
+ CONFIDENCE_LEVEL,
8
+ IMPACT,
9
+ USER_RISK,
10
+ OWNERSHIP,
11
+ } from '../../verax/core/contracts/index.js';
12
+ import { ARTIFACT_REGISTRY, getArtifactVersions } from '../../verax/core/artifacts/registry.js';
4
13
 
5
14
  /**
6
- * Write findings.json artifact with deterministic IDs
15
+ * Write findings.json artifact with deterministic IDs and contract enforcement
7
16
  */
8
17
  export function writeFindingsJson(runDir, findingsData) {
9
- const findingsPath = resolve(runDir, 'findings.json');
10
-
11
- // Add deterministic finding IDs based on expectation IDs
12
- const findingsWithIds = (findingsData.findings || []).map(finding => ({
18
+ const findingsPath = resolve(runDir, ARTIFACT_REGISTRY.findings.filename);
19
+ const normalizedFindings = normalizeFindings(findingsData?.findings || []);
20
+ const enforcement = enforceContractsOnFindings(normalizedFindings);
21
+
22
+ const findingsWithIds = enforcement.valid.map((finding) => ({
13
23
  ...finding,
14
- findingId: findingIdFromExpectationId(finding.id),
24
+ findingId: findingIdFromExpectationId(finding.id || finding.expectationId || ''),
15
25
  }));
16
-
26
+
27
+ const stats = findingsData?.stats || {};
17
28
  const payload = {
29
+ contractVersion: 1,
30
+ artifactVersions: getArtifactVersions(),
18
31
  findings: findingsWithIds,
19
- total: findingsData.stats?.total || 0,
32
+ total: findingsWithIds.length,
20
33
  stats: {
21
- total: findingsData.stats?.total || 0,
22
- silentFailures: findingsData.stats?.silentFailures || 0,
23
- observed: findingsData.stats?.observed || 0,
24
- coverageGaps: findingsData.stats?.coverageGaps || 0,
25
- unproven: findingsData.stats?.unproven || 0,
26
- informational: findingsData.stats?.informational || 0,
34
+ total: findingsWithIds.length,
35
+ silentFailures: stats.silentFailures || 0,
36
+ observed: stats.observed || 0,
37
+ coverageGaps: stats.coverageGaps || 0,
38
+ unproven: stats.unproven || 0,
39
+ informational: stats.informational || 0,
40
+ },
41
+ detectedAt: findingsData?.detectedAt || new Date().toISOString(),
42
+ enforcement: {
43
+ droppedCount: enforcement.dropped.length,
44
+ downgradedCount: enforcement.downgrades.length,
45
+ downgrades: enforcement.downgrades.map((entry) => ({
46
+ reason: entry.reason,
47
+ originalStatus: entry.original?.status,
48
+ downgradeToStatus: entry.downgraded?.status,
49
+ })),
50
+ dropped: enforcement.dropped.map((entry) => ({
51
+ reason: entry.reason,
52
+ })),
27
53
  },
28
- detectedAt: findingsData.detectedAt || new Date().toISOString(),
29
54
  };
30
-
55
+
31
56
  atomicWriteJson(findingsPath, payload);
57
+ return { path: findingsPath, payload };
58
+ }
59
+
60
+ function normalizeFindings(findings) {
61
+ return findings.map((finding) => normalizeFinding(finding));
62
+ }
63
+
64
+ function normalizeFinding(finding) {
65
+ const status = finding.status || mapClassificationToStatus(finding.classification);
66
+ const evidence = normalizeEvidence(finding.evidence);
67
+ const confidence = normalizeConfidence(finding.confidence);
68
+ const signals = normalizeSignals(finding);
69
+ const interaction = finding.interaction || {
70
+ type: finding.type || 'unknown',
71
+ selector: finding.promise?.selector || finding.promise?.value || finding.source?.file || 'unknown',
72
+ };
73
+
74
+ return {
75
+ ...finding,
76
+ status,
77
+ evidence,
78
+ confidence,
79
+ signals,
80
+ interaction,
81
+ what_happened: finding.what_happened || finding.reason || 'Expectation was exercised during scan.',
82
+ what_was_expected: finding.what_was_expected || finding.promise?.value || 'Expectation derived from source code.',
83
+ what_was_observed: finding.what_was_observed || finding.reason || 'Observation recorded for expectation.',
84
+ why_it_matters: finding.why_it_matters || 'Potential silent failure identified by expectation vs observation.',
85
+ };
86
+ }
87
+
88
+ function mapClassificationToStatus(classification) {
89
+ if (classification === 'observed') return FINDING_STATUS.CONFIRMED;
90
+ if (classification === 'silent-failure') return FINDING_STATUS.SUSPECTED;
91
+ if (classification === 'coverage-gap' || classification === 'unproven') return FINDING_STATUS.SUSPECTED;
92
+ return FINDING_STATUS.INFORMATIONAL;
93
+ }
94
+
95
+ function normalizeConfidence(confidence) {
96
+ if (confidence && typeof confidence === 'object' && confidence.level && confidence.score !== undefined) {
97
+ return confidence;
98
+ }
99
+
100
+ const numeric = typeof confidence === 'number' ? Math.max(0, Math.min(1, confidence)) : 0;
101
+ const score = Math.round(numeric * 100);
102
+ let level = CONFIDENCE_LEVEL.UNPROVEN;
103
+ if (score >= 80) level = CONFIDENCE_LEVEL.HIGH;
104
+ else if (score >= 60) level = CONFIDENCE_LEVEL.MEDIUM;
105
+ else if (score > 0) level = CONFIDENCE_LEVEL.LOW;
106
+
107
+ return { level, score };
108
+ }
109
+
110
+ function normalizeEvidence(evidenceArray) {
111
+ const items = Array.isArray(evidenceArray) ? evidenceArray : [];
112
+ const hasNetworkActivity = items.some((item) => item?.type === 'network-log');
113
+ const hasDomChange = items.some((item) => item?.type === 'screenshot');
114
+
115
+ return {
116
+ type: hasNetworkActivity ? 'network_activity' : undefined,
117
+ hasDomChange,
118
+ hasUrlChange: false,
119
+ hasNetworkActivity,
120
+ hasStateChange: false,
121
+ networkRequests: items.filter((item) => item?.type === 'network-log'),
122
+ before: items.find((item) => item?.type === 'screenshot')?.path,
123
+ after: undefined,
124
+ };
125
+ }
126
+
127
+ function normalizeSignals(finding) {
128
+ if (finding.signals && typeof finding.signals === 'object') {
129
+ return finding.signals;
130
+ }
131
+
132
+ const impact = (finding.impact && IMPACT[finding.impact]) ? finding.impact : IMPACT.MEDIUM;
133
+
134
+ return {
135
+ impact,
136
+ userRisk: USER_RISK.CONFUSES,
137
+ ownership: OWNERSHIP.FRONTEND,
138
+ grouping: {
139
+ expectationType: finding.type || 'unknown',
140
+ },
141
+ };
32
142
  }
@@ -1,18 +1,20 @@
1
1
  import { resolve } from 'path';
2
2
  import { atomicWriteJson } from './atomic-write.js';
3
3
  import { compareExpectations } from './idgen.js';
4
+ import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
4
5
 
5
6
  /**
6
7
  * Write learn.json artifact
7
8
  * Maintains deterministic ordering for stable output
8
9
  */
9
10
  export function writeLearnJson(runPaths, expectations, skipped) {
10
- const learnJsonPath = resolve(runPaths.baseDir, 'learn.json');
11
+ const learnJsonPath = resolve(runPaths.baseDir, ARTIFACT_REGISTRY.learn.filename);
11
12
 
12
13
  // Sort expectations deterministically for stable output
13
14
  const sortedExpectations = [...expectations].sort(compareExpectations);
14
15
 
15
16
  const learnJson = {
17
+ contractVersion: 1,
16
18
  expectations: sortedExpectations,
17
19
  stats: {
18
20
  totalExpectations: sortedExpectations.length,
@@ -1,13 +1,15 @@
1
1
  import { atomicWriteJson } from './atomic-write.js';
2
2
  import { resolve } from 'path';
3
+ import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
3
4
 
4
5
  /**
5
6
  * Write observe.json artifact
6
7
  */
7
8
  export function writeObserveJson(runDir, observeData) {
8
- const observePath = resolve(runDir, 'observe.json');
9
+ const observePath = resolve(runDir, ARTIFACT_REGISTRY.observe.filename);
9
10
 
10
11
  const payload = {
12
+ contractVersion: 1,
11
13
  observations: observeData.observations || [],
12
14
  stats: {
13
15
  attempted: observeData.stats?.attempted || 0,
@@ -1,5 +1,6 @@
1
1
  import { join, isAbsolute } from 'path';
2
2
  import { mkdirSync } from 'fs';
3
+ import { buildRunArtifactPaths } from '../../verax/core/artifacts/registry.js';
3
4
 
4
5
  /**
5
6
  * Build run artifact paths
@@ -7,18 +8,8 @@ import { mkdirSync } from 'fs';
7
8
  export function getRunPaths(projectRoot, outDir, runId) {
8
9
  const outBase = isAbsolute(outDir) ? outDir : join(projectRoot, outDir);
9
10
  const baseDir = join(outBase, 'runs', runId);
10
-
11
- return {
12
- baseDir,
13
- runStatusJson: join(baseDir, 'run.status.json'),
14
- runMetaJson: join(baseDir, 'run.meta.json'),
15
- summaryJson: join(baseDir, 'summary.json'),
16
- findingsJson: join(baseDir, 'findings.json'),
17
- tracesJsonl: join(baseDir, 'traces.jsonl'),
18
- evidenceDir: join(baseDir, 'evidence'),
19
- learnJson: join(baseDir, 'learn.json'),
20
- observeJson: join(baseDir, 'observe.json'),
21
- };
11
+
12
+ return buildRunArtifactPaths(baseDir);
22
13
  }
23
14
 
24
15
  /**
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'fs';
2
2
  import { resolve, dirname } from 'path';
3
+ import { assertExecutionBootstrapAllowed } from './bootstrap-guard.js';
3
4
 
4
5
  /**
5
6
  * Project Discovery Module
@@ -21,6 +22,8 @@ import { resolve, dirname } from 'path';
21
22
  * @returns {Promise<ProjectProfile>}
22
23
  */
23
24
  export async function discoverProject(srcPath) {
25
+ // PHASE 21.6.1: Runtime guard - crash if called during inspection
26
+ assertExecutionBootstrapAllowed('discoverProject');
24
27
  const projectRoot = resolve(srcPath);
25
28
 
26
29
  // Find the nearest package.json
@@ -1,13 +1,15 @@
1
1
  import { atomicWriteJson } from './atomic-write.js';
2
2
  import { resolve } from 'path';
3
+ import { ARTIFACT_REGISTRY } from '../../verax/core/artifacts/registry.js';
3
4
 
4
5
  /**
5
6
  * Write project profile artifact
6
7
  */
7
8
  export function writeProjectJson(runPaths, projectProfile) {
8
- const projectJsonPath = resolve(runPaths.baseDir, 'project.json');
9
+ const projectJsonPath = resolve(runPaths.baseDir, ARTIFACT_REGISTRY.project.filename);
9
10
 
10
11
  const projectJson = {
12
+ contractVersion: 1,
11
13
  framework: projectProfile.framework,
12
14
  router: projectProfile.router,
13
15
  sourceRoot: projectProfile.sourceRoot,
@@ -0,0 +1,64 @@
1
+ /**
2
+ * PHASE 21.6.1 — Run Resolver
3
+ *
4
+ * Pure filesystem logic to resolve run IDs.
5
+ * No side effects, no execution dependencies.
6
+ */
7
+
8
+ import { readdirSync, statSync, existsSync } from 'fs';
9
+ import { resolve } from 'path';
10
+
11
+ /**
12
+ * Find the latest run ID from .verax/runs/
13
+ *
14
+ * @param {string} projectDir - Project directory
15
+ * @returns {string|null} Latest run ID or null if no runs found
16
+ */
17
+ export function findLatestRunId(projectDir) {
18
+ const runsDir = resolve(projectDir, '.verax', 'runs');
19
+
20
+ if (!existsSync(runsDir)) {
21
+ return null;
22
+ }
23
+
24
+ try {
25
+ const runs = readdirSync(runsDir, { withFileTypes: true })
26
+ .filter(dirent => dirent.isDirectory())
27
+ .map(dirent => {
28
+ const runPath = resolve(runsDir, dirent.name);
29
+ try {
30
+ const stats = statSync(runPath);
31
+ return {
32
+ name: dirent.name,
33
+ mtimeMs: stats.mtimeMs
34
+ };
35
+ } catch {
36
+ return null;
37
+ }
38
+ })
39
+ .filter(run => run !== null);
40
+
41
+ if (runs.length === 0) {
42
+ return null;
43
+ }
44
+
45
+ // Sort by modification time (descending) and return latest
46
+ runs.sort((a, b) => b.mtimeMs - a.mtimeMs);
47
+ return runs[0].name;
48
+ } catch (error) {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Validate that a run ID exists
55
+ *
56
+ * @param {string} projectDir - Project directory
57
+ * @param {string} runId - Run ID to validate
58
+ * @returns {boolean} Whether run exists
59
+ */
60
+ export function validateRunId(projectDir, runId) {
61
+ const runDir = resolve(projectDir, '.verax', 'runs', runId);
62
+ return existsSync(runDir);
63
+ }
64
+
@@ -0,0 +1,55 @@
1
+ import { existsSync, readdirSync, statSync } from 'fs';
2
+ import { extname, join, resolve } from 'path';
3
+ import { DataError } from './errors.js';
4
+ import { getSourceCodeRequirementBanner } from '../../verax/core/product-definition.js';
5
+
6
+ const CODE_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.html']);
7
+
8
+ function safeReaddir(dirPath) {
9
+ try {
10
+ return readdirSync(dirPath);
11
+ } catch {
12
+ return [];
13
+ }
14
+ }
15
+
16
+ function directoryHasCode(dirPath) {
17
+ const entries = safeReaddir(dirPath);
18
+ for (const entry of entries) {
19
+ const fullPath = join(dirPath, entry);
20
+ let stats;
21
+ try {
22
+ stats = statSync(fullPath);
23
+ } catch {
24
+ continue;
25
+ }
26
+
27
+ if (stats.isFile() && CODE_EXTS.has(extname(entry).toLowerCase())) {
28
+ return true;
29
+ }
30
+
31
+ if (stats.isDirectory() && (entry === 'src' || entry === 'app')) {
32
+ const nested = safeReaddir(fullPath).slice(0, 50);
33
+ if (nested.some((name) => CODE_EXTS.has(extname(name).toLowerCase()))) {
34
+ return true;
35
+ }
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+
41
+ export function assertHasLocalSource(srcPath) {
42
+ const resolved = resolve(srcPath);
43
+ const hasPackageJson = existsSync(join(resolved, 'package.json'));
44
+ const hasIndexHtml = existsSync(join(resolved, 'index.html'));
45
+ const hasCodeFiles = directoryHasCode(resolved);
46
+
47
+ if (!hasPackageJson && !hasIndexHtml && !hasCodeFiles) {
48
+ const banner = getSourceCodeRequirementBanner();
49
+ throw new DataError(
50
+ `${banner} Provide --src pointing to your repository so VERAX can analyze expectations. See docs/README.md for the canonical product contract.`
51
+ );
52
+ }
53
+
54
+ return { hasPackageJson, hasIndexHtml, hasCodeFiles };
55
+ }
@@ -7,6 +7,7 @@ import { atomicWriteJson } from './atomic-write.js';
7
7
  */
8
8
  export function writeSummaryJson(summaryPath, summaryData, stats = {}) {
9
9
  const payload = {
10
+ contractVersion: 1,
10
11
  runId: summaryData.runId,
11
12
  status: summaryData.status,
12
13
  startedAt: summaryData.startedAt,
@@ -0,0 +1,163 @@
1
+ /**
2
+ * PHASE 20 — Svelte Navigation Detector
3
+ *
4
+ * Detects navigation promises in Svelte applications:
5
+ * - <a href="/path"> links
6
+ * - goto() calls from SvelteKit
7
+ * - programmatic navigation
8
+ */
9
+
10
+ import { extractSvelteSFC, extractTemplateBindings, mapTemplateHandlersToScript } from './svelte-sfc-extractor.js';
11
+ import { parse } from '@babel/parser';
12
+ import traverse from '@babel/traverse';
13
+
14
+ /**
15
+ * Detect navigation promises in Svelte SFC
16
+ *
17
+ * @param {string} filePath - Path to .svelte file
18
+ * @param {string} content - Full file content
19
+ * @param {string} projectRoot - Project root directory
20
+ * @returns {Array} Array of navigation expectations
21
+ */
22
+ export function detectSvelteNavigation(filePath, content, projectRoot) {
23
+ const expectations = [];
24
+
25
+ try {
26
+ const sfc = extractSvelteSFC(content);
27
+ const { scriptBlocks, markup } = sfc;
28
+
29
+ // Extract navigation from markup (links)
30
+ if (markup && markup.content) {
31
+ const linkRegex = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>/gi;
32
+ let linkMatch;
33
+ while ((linkMatch = linkRegex.exec(markup.content)) !== null) {
34
+ const href = linkMatch[1];
35
+ // Skip external links and hash-only links
36
+ if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('#')) {
37
+ continue;
38
+ }
39
+
40
+ const beforeMatch = markup.content.substring(0, linkMatch.index);
41
+ const line = markup.startLine + (beforeMatch.match(/\n/g) || []).length;
42
+
43
+ expectations.push({
44
+ type: 'navigation',
45
+ target: href,
46
+ context: 'markup',
47
+ sourceRef: {
48
+ file: filePath,
49
+ line,
50
+ snippet: linkMatch[0],
51
+ },
52
+ proof: 'PROVEN_EXPECTATION',
53
+ metadata: {
54
+ navigationType: 'link',
55
+ },
56
+ });
57
+ }
58
+ }
59
+
60
+ // Extract navigation from script blocks (goto, navigate, etc.)
61
+ for (const scriptBlock of scriptBlocks) {
62
+ if (!scriptBlock.content) continue;
63
+
64
+ try {
65
+ const ast = parse(scriptBlock.content, {
66
+ sourceType: 'module',
67
+ plugins: ['typescript', 'jsx'],
68
+ });
69
+
70
+ traverse.default(ast, {
71
+ CallExpression(path) {
72
+ const { node } = path;
73
+
74
+ // Detect goto() calls (SvelteKit)
75
+ if (
76
+ node.callee.type === 'Identifier' &&
77
+ node.callee.name === 'goto' &&
78
+ node.arguments.length > 0
79
+ ) {
80
+ const arg = node.arguments[0];
81
+ let target = null;
82
+
83
+ if (arg.type === 'StringLiteral') {
84
+ target = arg.value;
85
+ } else if (arg.type === 'TemplateLiteral' && arg.quasis.length === 1) {
86
+ target = arg.quasis[0].value.raw;
87
+ }
88
+
89
+ if (target && !target.startsWith('http://') && !target.startsWith('https://') && !target.startsWith('#')) {
90
+ const location = node.loc;
91
+ const line = scriptBlock.startLine + (location ? location.start.line - 1 : 0);
92
+
93
+ expectations.push({
94
+ type: 'navigation',
95
+ target,
96
+ context: 'goto',
97
+ sourceRef: {
98
+ file: filePath,
99
+ line,
100
+ snippet: scriptBlock.content.substring(
101
+ node.start - (ast.program.body[0]?.start || 0),
102
+ node.end - (ast.program.body[0]?.start || 0)
103
+ ),
104
+ },
105
+ proof: arg.type === 'StringLiteral' ? 'PROVEN_EXPECTATION' : 'LIKELY_EXPECTATION',
106
+ metadata: {
107
+ navigationType: 'goto',
108
+ },
109
+ });
110
+ }
111
+ }
112
+
113
+ // Detect navigate() calls (if imported)
114
+ if (
115
+ node.callee.type === 'MemberExpression' &&
116
+ node.callee.property.name === 'navigate' &&
117
+ node.arguments.length > 0
118
+ ) {
119
+ const arg = node.arguments[0];
120
+ let target = null;
121
+
122
+ if (arg.type === 'StringLiteral') {
123
+ target = arg.value;
124
+ } else if (arg.type === 'TemplateLiteral' && arg.quasis.length === 1) {
125
+ target = arg.quasis[0].value.raw;
126
+ }
127
+
128
+ if (target && !target.startsWith('http://') && !target.startsWith('https://') && !target.startsWith('#')) {
129
+ const location = node.loc;
130
+ const line = scriptBlock.startLine + (location ? location.start.line - 1 : 0);
131
+
132
+ expectations.push({
133
+ type: 'navigation',
134
+ target,
135
+ context: 'navigate',
136
+ sourceRef: {
137
+ file: filePath,
138
+ line,
139
+ snippet: scriptBlock.content.substring(
140
+ node.start - (ast.program.body[0]?.start || 0),
141
+ node.end - (ast.program.body[0]?.start || 0)
142
+ ),
143
+ },
144
+ proof: arg.type === 'StringLiteral' ? 'PROVEN_EXPECTATION' : 'LIKELY_EXPECTATION',
145
+ metadata: {
146
+ navigationType: 'navigate',
147
+ },
148
+ });
149
+ }
150
+ }
151
+ },
152
+ });
153
+ } catch (parseError) {
154
+ // Skip if parsing fails
155
+ }
156
+ }
157
+ } catch (error) {
158
+ // Skip if extraction fails
159
+ }
160
+
161
+ return expectations;
162
+ }
163
+
@@ -0,0 +1,80 @@
1
+ /**
2
+ * PHASE 20 — Svelte Network Detector
3
+ *
4
+ * Detects network calls (fetch, axios) in Svelte component handlers and lifecycle functions.
5
+ * Reuses AST network detector but ensures it works with Svelte SFC script blocks.
6
+ */
7
+
8
+ import { extractSvelteSFC, extractTemplateBindings, mapTemplateHandlersToScript } from './svelte-sfc-extractor.js';
9
+ import { detectNetworkCallsAST } from './ast-network-detector.js';
10
+ import { relative } from 'path';
11
+
12
+ /**
13
+ * Detect network promises in Svelte SFC
14
+ *
15
+ * @param {string} filePath - Path to .svelte file
16
+ * @param {string} content - Full file content
17
+ * @param {string} projectRoot - Project root directory
18
+ * @returns {Array} Array of network expectations
19
+ */
20
+ export function detectSvelteNetwork(filePath, content, projectRoot) {
21
+ const expectations = [];
22
+
23
+ try {
24
+ const sfc = extractSvelteSFC(content);
25
+ const { scriptBlocks, markup } = sfc;
26
+
27
+ // Extract event handlers from markup to identify UI-bound handlers
28
+ const templateBindings = markup ? extractTemplateBindings(markup.content) : { eventHandlers: [] };
29
+ const mappedHandlers = scriptBlocks.length > 0 && templateBindings.eventHandlers.length > 0
30
+ ? mapTemplateHandlersToScript(templateBindings.eventHandlers, scriptBlocks[0].content)
31
+ : [];
32
+
33
+ const uiBoundHandlers = new Set(mappedHandlers.map(h => h.handler));
34
+
35
+ // Process each script block
36
+ for (const scriptBlock of scriptBlocks) {
37
+ if (!scriptBlock.content) continue;
38
+
39
+ // Use AST network detector on script content
40
+ const networkCalls = detectNetworkCallsAST(scriptBlock.content, filePath, relative(projectRoot, filePath));
41
+
42
+ // Filter and enhance network calls
43
+ for (const networkCall of networkCalls) {
44
+ // Check if this is in a UI-bound handler
45
+ const isUIBound = networkCall.context && uiBoundHandlers.has(networkCall.context);
46
+
47
+ // Skip analytics-only calls (filtered by guardrails later)
48
+ if (networkCall.target && (
49
+ networkCall.target.includes('/api/analytics') ||
50
+ networkCall.target.includes('/api/track') ||
51
+ networkCall.target.includes('/api/beacon')
52
+ )) {
53
+ continue;
54
+ }
55
+
56
+ expectations.push({
57
+ type: 'network',
58
+ target: networkCall.target,
59
+ method: networkCall.method || 'GET',
60
+ context: networkCall.context || 'component',
61
+ sourceRef: {
62
+ file: filePath,
63
+ line: networkCall.line || scriptBlock.startLine,
64
+ snippet: networkCall.snippet || '',
65
+ },
66
+ proof: networkCall.proof || 'LIKELY_EXPECTATION',
67
+ metadata: {
68
+ isUIBound,
69
+ handlerContext: networkCall.context,
70
+ },
71
+ });
72
+ }
73
+ }
74
+ } catch (error) {
75
+ // Skip if extraction fails
76
+ }
77
+
78
+ return expectations;
79
+ }
80
+