vibecheck-ai 2.0.1 → 5.0.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/bin/.generated +25 -0
- package/bin/_deprecations.js +463 -0
- package/bin/_router.js +46 -0
- package/bin/cli-hygiene.js +241 -0
- package/bin/dev/run-v2-torture.js +30 -0
- package/bin/registry.js +656 -0
- package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
- package/bin/runners/ENHANCEMENT_GUIDE.md +121 -0
- package/bin/runners/REPORT_AUDIT.md +64 -0
- package/bin/runners/cli-utils.js +1070 -0
- package/bin/runners/context/ai-task-decomposer.js +337 -0
- package/bin/runners/context/analyzer.js +513 -0
- package/bin/runners/context/api-contracts.js +427 -0
- package/bin/runners/context/context-diff.js +342 -0
- package/bin/runners/context/context-pruner.js +291 -0
- package/bin/runners/context/dependency-graph.js +414 -0
- package/bin/runners/context/generators/claude.js +107 -0
- package/bin/runners/context/generators/codex.js +108 -0
- package/bin/runners/context/generators/copilot.js +119 -0
- package/bin/runners/context/generators/cursor-enhanced.js +2525 -0
- package/bin/runners/context/generators/cursor.js +514 -0
- package/bin/runners/context/generators/mcp.js +169 -0
- package/bin/runners/context/generators/windsurf.js +180 -0
- package/bin/runners/context/git-context.js +304 -0
- package/bin/runners/context/index.js +1110 -0
- package/bin/runners/context/insights.js +173 -0
- package/bin/runners/context/mcp-server/generate-rules.js +337 -0
- package/bin/runners/context/mcp-server/index.js +1176 -0
- package/bin/runners/context/mcp-server/package.json +24 -0
- package/bin/runners/context/memory.js +200 -0
- package/bin/runners/context/monorepo.js +215 -0
- package/bin/runners/context/multi-repo-federation.js +404 -0
- package/bin/runners/context/patterns.js +253 -0
- package/bin/runners/context/proof-context.js +1264 -0
- package/bin/runners/context/security-scanner.js +541 -0
- package/bin/runners/context/semantic-search.js +350 -0
- package/bin/runners/context/shared.js +264 -0
- package/bin/runners/context/team-conventions.js +336 -0
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -0
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +488 -0
- package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
- package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
- package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +303 -0
- package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
- package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
- package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
- package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
- package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
- package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
- package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
- package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +127 -0
- package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +213 -0
- package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
- package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
- package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
- package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
- package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
- package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
- package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
- package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
- package/bin/runners/lib/agent-firewall/index.js +200 -0
- package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
- package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
- package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
- package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
- package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +308 -0
- package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
- package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
- package/bin/runners/lib/agent-firewall/logger.js +141 -0
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +90 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +103 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +451 -0
- package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +79 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +227 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +191 -0
- package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
- package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
- package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
- package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
- package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
- package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
- package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
- package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
- package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
- package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
- package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
- package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
- package/bin/runners/lib/agent-firewall/session/index.js +26 -0
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
- package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
- package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
- package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +137 -0
- package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
- package/bin/runners/lib/ai-bridge.js +416 -0
- package/bin/runners/lib/analysis-core.js +309 -0
- package/bin/runners/lib/analyzers.js +2500 -0
- package/bin/runners/lib/api-client.js +269 -0
- package/bin/runners/lib/approve-output.js +235 -0
- package/bin/runners/lib/artifact-envelope.js +540 -0
- package/bin/runners/lib/assets/vibecheck-logo.png +0 -0
- package/bin/runners/lib/audit-bridge.js +391 -0
- package/bin/runners/lib/auth-shared.js +977 -0
- package/bin/runners/lib/auth-truth.js +193 -0
- package/bin/runners/lib/auth.js +215 -0
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/backup.js +62 -0
- package/bin/runners/lib/billing.js +107 -0
- package/bin/runners/lib/checkpoint.js +941 -0
- package/bin/runners/lib/claims.js +118 -0
- package/bin/runners/lib/classify-output.js +204 -0
- package/bin/runners/lib/cleanup/engine.js +571 -0
- package/bin/runners/lib/cleanup/index.js +53 -0
- package/bin/runners/lib/cleanup/output.js +375 -0
- package/bin/runners/lib/cleanup/rules.js +1060 -0
- package/bin/runners/lib/cli-output.js +400 -0
- package/bin/runners/lib/cli-ui.js +540 -0
- package/bin/runners/lib/compliance-bridge-new.js +0 -0
- package/bin/runners/lib/compliance-bridge.js +165 -0
- package/bin/runners/lib/contracts/auth-contract.js +202 -0
- package/bin/runners/lib/contracts/env-contract.js +181 -0
- package/bin/runners/lib/contracts/external-contract.js +206 -0
- package/bin/runners/lib/contracts/guard.js +168 -0
- package/bin/runners/lib/contracts/index.js +89 -0
- package/bin/runners/lib/contracts/plan-validator.js +311 -0
- package/bin/runners/lib/contracts/route-contract.js +199 -0
- package/bin/runners/lib/contracts.js +804 -0
- package/bin/runners/lib/default-config.js +127 -0
- package/bin/runners/lib/detect.js +89 -0
- package/bin/runners/lib/detectors-v2.js +622 -0
- package/bin/runners/lib/doctor/autofix.js +254 -0
- package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
- package/bin/runners/lib/doctor/failure-signatures.js +526 -0
- package/bin/runners/lib/doctor/fix-script.js +336 -0
- package/bin/runners/lib/doctor/index.js +37 -0
- package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -0
- package/bin/runners/lib/doctor/modules/index.js +105 -0
- package/bin/runners/lib/doctor/modules/network.js +250 -0
- package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
- package/bin/runners/lib/doctor/modules/project.js +312 -0
- package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
- package/bin/runners/lib/doctor/modules/runtime.js +224 -0
- package/bin/runners/lib/doctor/modules/security.js +350 -0
- package/bin/runners/lib/doctor/modules/system.js +213 -0
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -0
- package/bin/runners/lib/doctor/reporter.js +262 -0
- package/bin/runners/lib/doctor/safe-repair.js +384 -0
- package/bin/runners/lib/doctor/service.js +262 -0
- package/bin/runners/lib/doctor/types.js +113 -0
- package/bin/runners/lib/doctor/ui.js +263 -0
- package/bin/runners/lib/doctor-enhanced.js +233 -0
- package/bin/runners/lib/doctor-output.js +226 -0
- package/bin/runners/lib/doctor-v2.js +608 -0
- package/bin/runners/lib/drift.js +425 -0
- package/bin/runners/lib/enforcement.js +72 -0
- package/bin/runners/lib/engine/ast-cache.js +210 -0
- package/bin/runners/lib/engine/auth-extractor.js +211 -0
- package/bin/runners/lib/engine/billing-extractor.js +112 -0
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
- package/bin/runners/lib/engine/env-extractor.js +207 -0
- package/bin/runners/lib/engine/express-extractor.js +208 -0
- package/bin/runners/lib/engine/extractors.js +849 -0
- package/bin/runners/lib/engine/index.js +207 -0
- package/bin/runners/lib/engine/repo-index.js +514 -0
- package/bin/runners/lib/engine/types.js +124 -0
- package/bin/runners/lib/engines/accessibility-engine.js +190 -0
- package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
- package/bin/runners/lib/engines/ast-cache.js +99 -0
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/engines/code-quality-engine.js +255 -0
- package/bin/runners/lib/engines/console-logs-engine.js +115 -0
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
- package/bin/runners/lib/engines/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
- package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
- package/bin/runners/lib/engines/file-filter.js +131 -0
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
- package/bin/runners/lib/engines/mock-data-engine.js +272 -0
- package/bin/runners/lib/engines/parallel-processor.js +71 -0
- package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
- package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
- package/bin/runners/lib/engines/type-aware-engine.js +152 -0
- package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
- package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
- package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
- package/bin/runners/lib/enterprise-detect.js +603 -0
- package/bin/runners/lib/enterprise-init.js +942 -0
- package/bin/runners/lib/entitlements-v2.js +265 -0
- package/bin/runners/lib/entitlements.generated.js +0 -0
- package/bin/runners/lib/entitlements.js +340 -0
- package/bin/runners/lib/env-resolver.js +417 -0
- package/bin/runners/lib/env-template.js +66 -0
- package/bin/runners/lib/env.js +189 -0
- package/bin/runners/lib/error-handler.js +368 -0
- package/bin/runners/lib/error-messages.js +289 -0
- package/bin/runners/lib/evidence-pack.js +684 -0
- package/bin/runners/lib/exit-codes.js +275 -0
- package/bin/runners/lib/extractors/client-calls.js +990 -0
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -0
- package/bin/runners/lib/extractors/fastify-routes.js +426 -0
- package/bin/runners/lib/extractors/index.js +363 -0
- package/bin/runners/lib/extractors/next-routes.js +524 -0
- package/bin/runners/lib/extractors/proof-graph.js +431 -0
- package/bin/runners/lib/extractors/route-matcher.js +451 -0
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -0
- package/bin/runners/lib/extractors/ui-bindings.js +547 -0
- package/bin/runners/lib/finding-id.js +69 -0
- package/bin/runners/lib/finding-sorter.js +89 -0
- package/bin/runners/lib/findings-schema.js +281 -0
- package/bin/runners/lib/fingerprint.js +377 -0
- package/bin/runners/lib/firewall-prompt.js +50 -0
- package/bin/runners/lib/fix-output.js +228 -0
- package/bin/runners/lib/global-flags.js +250 -0
- package/bin/runners/lib/graph/graph-builder.js +265 -0
- package/bin/runners/lib/graph/html-renderer.js +413 -0
- package/bin/runners/lib/graph/index.js +32 -0
- package/bin/runners/lib/graph/runtime-collector.js +215 -0
- package/bin/runners/lib/graph/static-extractor.js +518 -0
- package/bin/runners/lib/help-formatter.js +413 -0
- package/bin/runners/lib/html-proof-report.js +913 -0
- package/bin/runners/lib/html-report.js +650 -0
- package/bin/runners/lib/init-wizard.js +601 -0
- package/bin/runners/lib/interactive-menu.js +1496 -0
- package/bin/runners/lib/json-output.js +76 -0
- package/bin/runners/lib/llm.js +75 -0
- package/bin/runners/lib/logger.js +38 -0
- package/bin/runners/lib/meter.js +61 -0
- package/bin/runners/lib/missions/briefing.js +427 -0
- package/bin/runners/lib/missions/checkpoint.js +753 -0
- package/bin/runners/lib/missions/evidence.js +126 -0
- package/bin/runners/lib/missions/hardening.js +851 -0
- package/bin/runners/lib/missions/plan.js +648 -0
- package/bin/runners/lib/missions/safety-gates.js +645 -0
- package/bin/runners/lib/missions/schema.js +478 -0
- package/bin/runners/lib/missions/templates.js +317 -0
- package/bin/runners/lib/next-action.js +560 -0
- package/bin/runners/lib/packs/bundle.js +675 -0
- package/bin/runners/lib/packs/evidence-pack.js +671 -0
- package/bin/runners/lib/packs/pack-factory.js +837 -0
- package/bin/runners/lib/packs/permissions-pack.js +686 -0
- package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
- package/bin/runners/lib/patch.js +40 -0
- package/bin/runners/lib/permissions/auth-model.js +213 -0
- package/bin/runners/lib/permissions/idor-prover.js +205 -0
- package/bin/runners/lib/permissions/index.js +45 -0
- package/bin/runners/lib/permissions/matrix-builder.js +198 -0
- package/bin/runners/lib/pkgjson.js +28 -0
- package/bin/runners/lib/policy.js +295 -0
- package/bin/runners/lib/polish/accessibility.js +62 -0
- package/bin/runners/lib/polish/analyzer.js +93 -0
- package/bin/runners/lib/polish/backend.js +87 -0
- package/bin/runners/lib/polish/configuration.js +83 -0
- package/bin/runners/lib/polish/documentation.js +83 -0
- package/bin/runners/lib/polish/frontend.js +817 -0
- package/bin/runners/lib/polish/index.js +27 -0
- package/bin/runners/lib/polish/infrastructure.js +80 -0
- package/bin/runners/lib/polish/internationalization.js +85 -0
- package/bin/runners/lib/polish/libraries.js +180 -0
- package/bin/runners/lib/polish/observability.js +75 -0
- package/bin/runners/lib/polish/performance.js +64 -0
- package/bin/runners/lib/polish/privacy.js +110 -0
- package/bin/runners/lib/polish/resilience.js +92 -0
- package/bin/runners/lib/polish/security.js +78 -0
- package/bin/runners/lib/polish/seo.js +71 -0
- package/bin/runners/lib/polish/styles.js +62 -0
- package/bin/runners/lib/polish/utils.js +104 -0
- package/bin/runners/lib/preflight.js +142 -0
- package/bin/runners/lib/prerequisites.js +149 -0
- package/bin/runners/lib/prove-output.js +220 -0
- package/bin/runners/lib/reality/correlation-detectors.js +359 -0
- package/bin/runners/lib/reality/index.js +318 -0
- package/bin/runners/lib/reality/request-hashing.js +416 -0
- package/bin/runners/lib/reality/request-mapper.js +453 -0
- package/bin/runners/lib/reality/safety-rails.js +463 -0
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -0
- package/bin/runners/lib/reality/toast-detector.js +393 -0
- package/bin/runners/lib/reality-findings.js +84 -0
- package/bin/runners/lib/reality-output.js +231 -0
- package/bin/runners/lib/receipts.js +179 -0
- package/bin/runners/lib/redact.js +29 -0
- package/bin/runners/lib/replay/capsule-manager.js +154 -0
- package/bin/runners/lib/replay/index.js +263 -0
- package/bin/runners/lib/replay/player.js +348 -0
- package/bin/runners/lib/replay/recorder.js +331 -0
- package/bin/runners/lib/report-engine.js +626 -0
- package/bin/runners/lib/report-html.js +1233 -0
- package/bin/runners/lib/report-output.js +366 -0
- package/bin/runners/lib/report-templates.js +967 -0
- package/bin/runners/lib/report.js +135 -0
- package/bin/runners/lib/route-detection.js +1209 -0
- package/bin/runners/lib/route-truth.js +1322 -0
- package/bin/runners/lib/safelist/index.js +96 -0
- package/bin/runners/lib/safelist/integration.js +334 -0
- package/bin/runners/lib/safelist/matcher.js +696 -0
- package/bin/runners/lib/safelist/schema.js +948 -0
- package/bin/runners/lib/safelist/store.js +438 -0
- package/bin/runners/lib/sandbox/index.js +59 -0
- package/bin/runners/lib/sandbox/proof-chain.js +399 -0
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -0
- package/bin/runners/lib/sandbox/worktree.js +174 -0
- package/bin/runners/lib/scan-cache.js +330 -0
- package/bin/runners/lib/scan-output-schema.js +344 -0
- package/bin/runners/lib/scan-output.js +631 -0
- package/bin/runners/lib/scan-runner.js +135 -0
- package/bin/runners/lib/schema-validator.js +350 -0
- package/bin/runners/lib/schemas/ajv-validator.js +464 -0
- package/bin/runners/lib/schemas/contracts.schema.json +160 -0
- package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
- package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
- package/bin/runners/lib/schemas/finding.schema.json +100 -0
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -0
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -0
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -0
- package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
- package/bin/runners/lib/schemas/run-request.schema.json +108 -0
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -0
- package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -0
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -0
- package/bin/runners/lib/schemas/validator.js +465 -0
- package/bin/runners/lib/schemas/verdict.schema.json +140 -0
- package/bin/runners/lib/score-history.js +282 -0
- package/bin/runners/lib/security-bridge.js +249 -0
- package/bin/runners/lib/server-usage.js +513 -0
- package/bin/runners/lib/share-pack.js +239 -0
- package/bin/runners/lib/ship-gate.js +832 -0
- package/bin/runners/lib/ship-manifest.js +1153 -0
- package/bin/runners/lib/ship-output-enterprise.js +239 -0
- package/bin/runners/lib/ship-output.js +1128 -0
- package/bin/runners/lib/snippets.js +67 -0
- package/bin/runners/lib/status-output.js +340 -0
- package/bin/runners/lib/terminal-ui.js +356 -0
- package/bin/runners/lib/truth.js +1691 -0
- package/bin/runners/lib/ui.js +562 -0
- package/bin/runners/lib/unified-cli-output.js +947 -0
- package/bin/runners/lib/unified-output.js +197 -0
- package/bin/runners/lib/upsell.js +410 -0
- package/bin/runners/lib/usage.js +153 -0
- package/bin/runners/lib/validate-patch.js +156 -0
- package/bin/runners/lib/verdict-engine.js +628 -0
- package/bin/runners/lib/verification.js +345 -0
- package/bin/runners/lib/why-tree.js +650 -0
- package/bin/runners/reality/engine.js +917 -0
- package/bin/runners/reality/flows.js +122 -0
- package/bin/runners/reality/report.js +378 -0
- package/bin/runners/reality/session.js +193 -0
- package/bin/runners/runAIAgent.js +229 -0
- package/bin/runners/runAgent.d.ts +5 -0
- package/bin/runners/runAgent.js +161 -0
- package/bin/runners/runAllowlist.js +418 -0
- package/bin/runners/runApprove.js +320 -0
- package/bin/runners/runAudit.js +692 -0
- package/bin/runners/runAuth.js +731 -0
- package/bin/runners/runCI.js +353 -0
- package/bin/runners/runCheckpoint.js +530 -0
- package/bin/runners/runClassify.js +928 -0
- package/bin/runners/runCleanup.js +343 -0
- package/bin/runners/runContext.d.ts +4 -0
- package/bin/runners/runContext.js +175 -0
- package/bin/runners/runDoctor.js +877 -0
- package/bin/runners/runEvidencePack.js +362 -0
- package/bin/runners/runFirewall.d.ts +5 -0
- package/bin/runners/runFirewall.js +134 -0
- package/bin/runners/runFirewallHook.d.ts +5 -0
- package/bin/runners/runFirewallHook.js +56 -0
- package/bin/runners/runFix.js +1355 -0
- package/bin/runners/runForge.js +451 -0
- package/bin/runners/runGuard.js +262 -0
- package/bin/runners/runInit.js +1927 -0
- package/bin/runners/runIntent.js +906 -0
- package/bin/runners/runKickoff.js +878 -0
- package/bin/runners/runLabs.js +424 -0
- package/bin/runners/runLaunch.js +2000 -0
- package/bin/runners/runLink.js +785 -0
- package/bin/runners/runMcp.js +1875 -0
- package/bin/runners/runPacks.js +2089 -0
- package/bin/runners/runPolish.d.ts +4 -0
- package/bin/runners/runPolish.js +390 -0
- package/bin/runners/runPromptFirewall.js +211 -0
- package/bin/runners/runProve.js +1411 -0
- package/bin/runners/runQuickstart.js +531 -0
- package/bin/runners/runReality.js +2260 -0
- package/bin/runners/runReport.js +726 -0
- package/bin/runners/runRuntime.js +110 -0
- package/bin/runners/runSafelist.js +1190 -0
- package/bin/runners/runScan.js +688 -0
- package/bin/runners/runShield.js +1282 -0
- package/bin/runners/runShip.js +1660 -0
- package/bin/runners/runTruth.d.ts +5 -0
- package/bin/runners/runTruth.js +101 -0
- package/bin/runners/runValidate.js +179 -0
- package/bin/runners/runWatch.js +478 -0
- package/bin/runners/utils.js +360 -0
- package/bin/scan.js +617 -0
- package/bin/vibecheck.js +1617 -0
- package/dist/guardrail/index.d.ts +2405 -0
- package/dist/guardrail/index.js +9747 -0
- package/dist/guardrail/index.js.map +1 -0
- package/dist/scanner/index.d.ts +282 -0
- package/dist/scanner/index.js +3395 -0
- package/dist/scanner/index.js.map +1 -0
- package/package.json +123 -104
- package/README.md +0 -491
- package/dist/index.js +0 -99711
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,1691 @@
|
|
|
1
|
+
// bin/runners/lib/truth.js
|
|
2
|
+
const fg = require("fast-glob");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const crypto = require("crypto");
|
|
6
|
+
const parser = require("@babel/parser");
|
|
7
|
+
const traverse = require("@babel/traverse").default;
|
|
8
|
+
const t = require("@babel/types");
|
|
9
|
+
|
|
10
|
+
// Env Truth v1
|
|
11
|
+
const { buildEnvTruth } = require("./env");
|
|
12
|
+
// Auth Truth v1
|
|
13
|
+
const { buildAuthTruth } = require("./auth-truth");
|
|
14
|
+
// Billing Truth v1
|
|
15
|
+
const { buildBillingTruth } = require("./billing");
|
|
16
|
+
// Enforcement Truth v1
|
|
17
|
+
const { buildEnforcementTruth } = require("./enforcement");
|
|
18
|
+
// Multi-framework route detection v2
|
|
19
|
+
const { resolveAllRoutes, detectFrameworks } = require("./route-detection");
|
|
20
|
+
|
|
21
|
+
// ---------- constants ----------
|
|
22
|
+
const IGNORE_GLOBS = [
|
|
23
|
+
"**/node_modules/**",
|
|
24
|
+
"**/.next/**",
|
|
25
|
+
"**/dist/**",
|
|
26
|
+
"**/build/**",
|
|
27
|
+
"**/.turbo/**",
|
|
28
|
+
"**/.git/**",
|
|
29
|
+
"**/.vibecheck/**",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const CODE_FILE_GLOBS = ["**/*.{ts,tsx,js,jsx}"];
|
|
33
|
+
|
|
34
|
+
// ---------- helpers ----------
|
|
35
|
+
function sha256(text) {
|
|
36
|
+
return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function canonicalizeMethod(m) {
|
|
40
|
+
const u = String(m || "").toUpperCase();
|
|
41
|
+
if (u === "ALL" || u === "ANY" || u === "*") return "*";
|
|
42
|
+
return u;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stripQueryHash(s) {
|
|
46
|
+
const v = String(s || "");
|
|
47
|
+
const q = v.indexOf("?");
|
|
48
|
+
const h = v.indexOf("#");
|
|
49
|
+
const cut = (q === -1 ? h : (h === -1 ? q : Math.min(q, h)));
|
|
50
|
+
return cut === -1 ? v : v.slice(0, cut);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Canonical path rules:
|
|
55
|
+
* - ensure leading slash
|
|
56
|
+
* - collapse multiple slashes
|
|
57
|
+
* - strip query/hash
|
|
58
|
+
* - normalize Next dynamic segments:
|
|
59
|
+
* [[...slug]] -> *slug?
|
|
60
|
+
* [...slug] -> *slug
|
|
61
|
+
* [id] -> :id
|
|
62
|
+
*/
|
|
63
|
+
function canonicalizePath(p) {
|
|
64
|
+
let s = stripQueryHash(String(p || "").trim());
|
|
65
|
+
|
|
66
|
+
// If someone passed a full URL, only keep pathname-like portion if possible.
|
|
67
|
+
// (We still require local routes to start with "/" for client refs.)
|
|
68
|
+
const protoIdx = s.indexOf("://");
|
|
69
|
+
if (protoIdx !== -1) {
|
|
70
|
+
// attempt to strip scheme+host
|
|
71
|
+
const slashAfterHost = s.indexOf("/", protoIdx + 3);
|
|
72
|
+
s = slashAfterHost === -1 ? "/" : s.slice(slashAfterHost);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!s.startsWith("/")) s = "/" + s;
|
|
76
|
+
s = s.replace(/\/+/g, "/");
|
|
77
|
+
|
|
78
|
+
// Next dynamic segments (filesystem style)
|
|
79
|
+
s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, "*$1?"); // [[...slug]] -> *slug?
|
|
80
|
+
s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1"); // [...slug] -> *slug
|
|
81
|
+
s = s.replace(/\[([^\]]+)\]/g, ":$1"); // [id] -> :id
|
|
82
|
+
|
|
83
|
+
if (s.length > 1) s = s.replace(/\/$/, "");
|
|
84
|
+
return s;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function joinPaths(prefix, p) {
|
|
88
|
+
const a = canonicalizePath(prefix || "/");
|
|
89
|
+
const b = canonicalizePath(p || "/");
|
|
90
|
+
if (a === "/") return b;
|
|
91
|
+
if (b === "/") return a;
|
|
92
|
+
return canonicalizePath(a + "/" + b);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseFile(code, fileAbsForErrors) {
|
|
96
|
+
// Be permissive: production repos contain decorators, top-level await, etc.
|
|
97
|
+
return parser.parse(code, {
|
|
98
|
+
sourceType: "unambiguous",
|
|
99
|
+
errorRecovery: true,
|
|
100
|
+
allowImportExportEverywhere: true,
|
|
101
|
+
plugins: [
|
|
102
|
+
"typescript",
|
|
103
|
+
"jsx",
|
|
104
|
+
"dynamicImport",
|
|
105
|
+
"importMeta",
|
|
106
|
+
"topLevelAwait",
|
|
107
|
+
"classProperties",
|
|
108
|
+
"classPrivateProperties",
|
|
109
|
+
"classPrivateMethods",
|
|
110
|
+
"optionalChaining",
|
|
111
|
+
"nullishCoalescingOperator",
|
|
112
|
+
"decorators-legacy",
|
|
113
|
+
],
|
|
114
|
+
sourceFilename: fileAbsForErrors || undefined,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// File cache for performance (avoids reading the same file multiple times)
|
|
119
|
+
const _FILE_CACHE = new Map();
|
|
120
|
+
|
|
121
|
+
function safeRead(fileAbs) {
|
|
122
|
+
if (_FILE_CACHE.has(fileAbs)) return _FILE_CACHE.get(fileAbs);
|
|
123
|
+
try {
|
|
124
|
+
const content = fs.readFileSync(fileAbs, "utf8");
|
|
125
|
+
_FILE_CACHE.set(fileAbs, content);
|
|
126
|
+
return content;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Clear cache to free memory after a scan (important for long-running processes)
|
|
133
|
+
function clearCache() {
|
|
134
|
+
_FILE_CACHE.clear();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ensureDir(p) {
|
|
138
|
+
fs.mkdirSync(p, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function evidenceFromLoc({ fileAbs, fileRel, loc, reason }) {
|
|
142
|
+
if (!loc) return null;
|
|
143
|
+
const code = safeRead(fileAbs);
|
|
144
|
+
if (!code) return null;
|
|
145
|
+
|
|
146
|
+
const lines = code.split(/\r?\n/);
|
|
147
|
+
const start = Math.max(1, loc.start?.line || 1);
|
|
148
|
+
const end = Math.max(start, loc.end?.line || start);
|
|
149
|
+
const snippet = lines.slice(start - 1, end).join("\n");
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
153
|
+
file: fileRel,
|
|
154
|
+
lines: `${start}-${end}`,
|
|
155
|
+
snippetHash: sha256(snippet),
|
|
156
|
+
reason,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeRel(repoRoot, fileAbs) {
|
|
161
|
+
return path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function scoreConfidence(c) {
|
|
165
|
+
if (c === "high") return 3;
|
|
166
|
+
if (c === "med") return 2;
|
|
167
|
+
if (c === "low") return 1;
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isRouteGroupSegment(seg) {
|
|
172
|
+
// Next route group: (group)
|
|
173
|
+
return seg.startsWith("(") && seg.endsWith(")");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isParallelSegment(seg) {
|
|
177
|
+
// Next parallel routes: @slot
|
|
178
|
+
return seg.startsWith("@");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------- Next: app router API ----------
|
|
182
|
+
const HTTP_EXPORTS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]);
|
|
183
|
+
|
|
184
|
+
function nextAppApiPathFromRel(fileRel) {
|
|
185
|
+
const idx = fileRel.indexOf("app/api/");
|
|
186
|
+
if (idx === -1) return null;
|
|
187
|
+
|
|
188
|
+
let sub = fileRel.slice(idx + "app/api/".length);
|
|
189
|
+
|
|
190
|
+
// route.ts / route.js / route.tsx / route.jsx
|
|
191
|
+
sub = sub.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
|
|
192
|
+
|
|
193
|
+
// remove route groups + parallel segments from the filesystem path
|
|
194
|
+
const parts = sub.split("/").filter(Boolean).filter((seg) => !isRouteGroupSegment(seg) && !isParallelSegment(seg));
|
|
195
|
+
sub = parts.join("/");
|
|
196
|
+
|
|
197
|
+
return canonicalizePath("/api/" + sub);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function resolveNextAppApiRoutes(repoRoot, stats) {
|
|
201
|
+
const files = await fg(["**/app/api/**/route.@(ts|tsx|js|jsx)"], {
|
|
202
|
+
cwd: repoRoot,
|
|
203
|
+
absolute: true,
|
|
204
|
+
ignore: IGNORE_GLOBS,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const out = [];
|
|
208
|
+
|
|
209
|
+
for (const fileAbs of files) {
|
|
210
|
+
const fileRel = normalizeRel(repoRoot, fileAbs);
|
|
211
|
+
const routePath = nextAppApiPathFromRel(fileRel);
|
|
212
|
+
if (!routePath) continue;
|
|
213
|
+
|
|
214
|
+
const code = safeRead(fileAbs);
|
|
215
|
+
if (!code) continue;
|
|
216
|
+
|
|
217
|
+
let ast;
|
|
218
|
+
try {
|
|
219
|
+
ast = parseFile(code, fileAbs);
|
|
220
|
+
} catch {
|
|
221
|
+
stats.parseErrors++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const methods = [];
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
traverse(ast, {
|
|
229
|
+
// export async function GET() {}
|
|
230
|
+
ExportNamedDeclaration(p) {
|
|
231
|
+
const decl = p.node.declaration;
|
|
232
|
+
|
|
233
|
+
if (t.isFunctionDeclaration(decl) && decl.id?.name) {
|
|
234
|
+
const n = decl.id.name.toUpperCase();
|
|
235
|
+
if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: decl.loc, why: "export function" });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// export const GET = async () => {}
|
|
239
|
+
if (t.isVariableDeclaration(decl)) {
|
|
240
|
+
for (const d of decl.declarations) {
|
|
241
|
+
if (!t.isVariableDeclarator(d)) continue;
|
|
242
|
+
if (!t.isIdentifier(d.id)) continue;
|
|
243
|
+
const n = d.id.name.toUpperCase();
|
|
244
|
+
if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: d.loc || decl.loc, why: "export const" });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// export { GET } from "./handler"
|
|
249
|
+
for (const s of p.node.specifiers || []) {
|
|
250
|
+
if (!t.isExportSpecifier(s)) continue;
|
|
251
|
+
if (!t.isIdentifier(s.exported)) continue;
|
|
252
|
+
const n = s.exported.name.toUpperCase();
|
|
253
|
+
if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: s.loc || p.node.loc, why: "export re-export" });
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
} catch {
|
|
258
|
+
// Babel traverse can fail on some edge-case files; skip them
|
|
259
|
+
stats.parseErrors++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (methods.length === 0) {
|
|
264
|
+
// Still include route.ts, but with "*" and low confidence to avoid missing-route spam.
|
|
265
|
+
out.push({
|
|
266
|
+
method: "*",
|
|
267
|
+
path: routePath,
|
|
268
|
+
handler: fileRel,
|
|
269
|
+
confidence: "low",
|
|
270
|
+
framework: "next",
|
|
271
|
+
evidence: [],
|
|
272
|
+
});
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const m of methods) {
|
|
277
|
+
const ev = evidenceFromLoc({
|
|
278
|
+
fileAbs,
|
|
279
|
+
fileRel,
|
|
280
|
+
loc: m.loc,
|
|
281
|
+
reason: `Next app router ${m.method} (${m.why})`,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
out.push({
|
|
285
|
+
method: m.method,
|
|
286
|
+
path: routePath,
|
|
287
|
+
handler: fileRel,
|
|
288
|
+
confidence: m.why === "export re-export" ? "med" : "high",
|
|
289
|
+
framework: "next",
|
|
290
|
+
evidence: ev ? [ev] : [],
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------- Next: pages router API ----------
|
|
299
|
+
function nextPagesApiPathFromRel(fileRel) {
|
|
300
|
+
const idx = fileRel.indexOf("pages/api/");
|
|
301
|
+
if (idx === -1) return null;
|
|
302
|
+
|
|
303
|
+
let sub = fileRel.slice(idx + "pages/api/".length);
|
|
304
|
+
sub = sub.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
305
|
+
|
|
306
|
+
// pages/api/foo/index.ts -> /api/foo
|
|
307
|
+
if (sub === "index") sub = "";
|
|
308
|
+
sub = sub.replace(/\/index$/, "");
|
|
309
|
+
|
|
310
|
+
return canonicalizePath("/api/" + sub);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function resolveNextPagesApiRoutes(repoRoot, stats) {
|
|
314
|
+
const files = await fg(["**/pages/api/**/*.@(ts|tsx|js|jsx)"], {
|
|
315
|
+
cwd: repoRoot,
|
|
316
|
+
absolute: true,
|
|
317
|
+
ignore: IGNORE_GLOBS,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const out = [];
|
|
321
|
+
|
|
322
|
+
for (const fileAbs of files) {
|
|
323
|
+
const fileRel = normalizeRel(repoRoot, fileAbs);
|
|
324
|
+
|
|
325
|
+
// Skip Next.js special files that aren't API routes (_app, _document, _utils, etc.)
|
|
326
|
+
if (fileRel.includes("/_") && !fileRel.includes("/_next")) continue;
|
|
327
|
+
|
|
328
|
+
const routePath = nextPagesApiPathFromRel(fileRel);
|
|
329
|
+
if (!routePath) continue;
|
|
330
|
+
|
|
331
|
+
const code = safeRead(fileAbs);
|
|
332
|
+
if (!code) continue;
|
|
333
|
+
|
|
334
|
+
// Parse to verify it's actually a route (has export default)
|
|
335
|
+
let ast;
|
|
336
|
+
try {
|
|
337
|
+
ast = parseFile(code, fileAbs);
|
|
338
|
+
} catch {
|
|
339
|
+
stats.parseErrors++;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check for 'export default' (Required for Pages Router API routes)
|
|
344
|
+
// Files without default export are helper files (db.ts, types.ts, utils.ts)
|
|
345
|
+
let hasDefaultExport = false;
|
|
346
|
+
try {
|
|
347
|
+
traverse(ast, {
|
|
348
|
+
ExportDefaultDeclaration(p) {
|
|
349
|
+
hasDefaultExport = true;
|
|
350
|
+
p.stop(); // Found it, stop traversing
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
} catch {
|
|
354
|
+
// Traverse failed, skip this file
|
|
355
|
+
stats.parseErrors++;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (!hasDefaultExport) continue; // It's a helper file, not an API route
|
|
360
|
+
|
|
361
|
+
out.push({
|
|
362
|
+
method: "*", // Pages router handles all methods in one function
|
|
363
|
+
path: routePath,
|
|
364
|
+
handler: fileRel,
|
|
365
|
+
confidence: "med",
|
|
366
|
+
framework: "next",
|
|
367
|
+
evidence: [],
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---------- minimal relative module resolver ----------
|
|
375
|
+
function exists(p) {
|
|
376
|
+
try {
|
|
377
|
+
return fs.statSync(p).isFile();
|
|
378
|
+
} catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function resolveRelativeModule(fromFileAbs, spec) {
|
|
384
|
+
if (!spec || (!spec.startsWith("./") && !spec.startsWith("../"))) return null;
|
|
385
|
+
|
|
386
|
+
const base = path.resolve(path.dirname(fromFileAbs), spec);
|
|
387
|
+
const candidates = [
|
|
388
|
+
base,
|
|
389
|
+
base + ".ts",
|
|
390
|
+
base + ".tsx",
|
|
391
|
+
base + ".js",
|
|
392
|
+
base + ".jsx",
|
|
393
|
+
path.join(base, "index.ts"),
|
|
394
|
+
path.join(base, "index.tsx"),
|
|
395
|
+
path.join(base, "index.js"),
|
|
396
|
+
path.join(base, "index.jsx"),
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
for (const c of candidates) if (exists(c)) return c;
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function extractRequireOrImportSpec(node) {
|
|
404
|
+
// require("./x")
|
|
405
|
+
if (t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "require" })) {
|
|
406
|
+
const a0 = node.arguments && node.arguments[0];
|
|
407
|
+
if (t.isStringLiteral(a0)) return a0.value;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// import("./x")
|
|
411
|
+
if (t.isCallExpression(node) && node.callee && node.callee.type === "Import") {
|
|
412
|
+
const a0 = node.arguments && node.arguments[0];
|
|
413
|
+
if (t.isStringLiteral(a0)) return a0.value;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// await import("./x")
|
|
417
|
+
if (t.isAwaitExpression(node)) {
|
|
418
|
+
return extractRequireOrImportSpec(node.argument);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ---------- Fastify route extraction ----------
|
|
425
|
+
const FASTIFY_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
|
|
426
|
+
|
|
427
|
+
function isFastifyMethod(name) {
|
|
428
|
+
return FASTIFY_METHODS.has(name);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function extractStringLiteral(node) {
|
|
432
|
+
return t.isStringLiteral(node) ? node.value : null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function extractPrefixFromOpts(node) {
|
|
436
|
+
if (!t.isObjectExpression(node)) return null;
|
|
437
|
+
for (const p of node.properties) {
|
|
438
|
+
if (!t.isObjectProperty(p)) continue;
|
|
439
|
+
const key =
|
|
440
|
+
t.isIdentifier(p.key) ? p.key.name :
|
|
441
|
+
t.isStringLiteral(p.key) ? p.key.value :
|
|
442
|
+
null;
|
|
443
|
+
if (key === "prefix" && t.isStringLiteral(p.value)) return p.value.value;
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function extractRouteObject(objExpr) {
|
|
449
|
+
let url = null;
|
|
450
|
+
let methods = [];
|
|
451
|
+
let hasHandler = false;
|
|
452
|
+
const hooks = [];
|
|
453
|
+
|
|
454
|
+
for (const p of objExpr.properties) {
|
|
455
|
+
if (!t.isObjectProperty(p)) continue;
|
|
456
|
+
|
|
457
|
+
const key =
|
|
458
|
+
t.isIdentifier(p.key) ? p.key.name :
|
|
459
|
+
t.isStringLiteral(p.key) ? p.key.value :
|
|
460
|
+
null;
|
|
461
|
+
if (!key) continue;
|
|
462
|
+
|
|
463
|
+
if (key === "url" && t.isStringLiteral(p.value)) url = p.value.value;
|
|
464
|
+
|
|
465
|
+
if (key === "method") {
|
|
466
|
+
if (t.isStringLiteral(p.value)) methods = [p.value.value];
|
|
467
|
+
if (t.isArrayExpression(p.value)) {
|
|
468
|
+
methods = p.value.elements.filter((e) => t.isStringLiteral(e)).map((e) => e.value);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (key === "handler") hasHandler = true;
|
|
473
|
+
if (["preHandler", "onRequest", "preValidation", "preSerialization"].includes(key)) hooks.push(key);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return { url, methods, hasHandler, hooks };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function resolveFastifyRoutes(repoRoot, entryAbs, stats) {
|
|
480
|
+
const seen = new Set();
|
|
481
|
+
const routes = [];
|
|
482
|
+
const gaps = [];
|
|
483
|
+
|
|
484
|
+
function scanFile(fileAbs, prefix) {
|
|
485
|
+
if (!fileAbs || seen.has(fileAbs)) return;
|
|
486
|
+
seen.add(fileAbs);
|
|
487
|
+
|
|
488
|
+
const fileRel = normalizeRel(repoRoot, fileAbs);
|
|
489
|
+
const code = safeRead(fileAbs);
|
|
490
|
+
if (!code) return;
|
|
491
|
+
|
|
492
|
+
let ast;
|
|
493
|
+
try {
|
|
494
|
+
ast = parseFile(code, fileAbs);
|
|
495
|
+
} catch {
|
|
496
|
+
stats.parseErrors++;
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// best-effort: fastify instance identifiers
|
|
501
|
+
const fastifyNames = new Set(["fastify"]);
|
|
502
|
+
|
|
503
|
+
// Static string + local-plugin harvesting (cheap, big impact on false positives)
|
|
504
|
+
const _rawConstInits = new Map(); // name -> init node (evaluated lazily)
|
|
505
|
+
const _constStrings = new Map(); // name -> resolved string
|
|
506
|
+
const _localPlugins = new Map(); // name -> Function node
|
|
507
|
+
|
|
508
|
+
function evalStaticString(node, depth = 0) {
|
|
509
|
+
if (!node || depth > 6) return null;
|
|
510
|
+
|
|
511
|
+
if (t.isStringLiteral(node)) return node.value;
|
|
512
|
+
|
|
513
|
+
if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
|
|
514
|
+
return node.quasis.map((q) => q.value.cooked || "").join("");
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (t.isIdentifier(node)) {
|
|
518
|
+
const name = node.name;
|
|
519
|
+
if (_constStrings.has(name)) return _constStrings.get(name);
|
|
520
|
+
const init = _rawConstInits.get(name);
|
|
521
|
+
if (!init) return null;
|
|
522
|
+
const v = evalStaticString(init, depth + 1);
|
|
523
|
+
if (typeof v === "string") {
|
|
524
|
+
// cap to avoid pathological blowups
|
|
525
|
+
if (v.length <= 4096) _constStrings.set(name, v);
|
|
526
|
+
return v;
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (t.isBinaryExpression(node, { operator: "+" })) {
|
|
532
|
+
const l = evalStaticString(node.left, depth + 1);
|
|
533
|
+
const r = evalStaticString(node.right, depth + 1);
|
|
534
|
+
if (typeof l !== "string" || typeof r !== "string") return null;
|
|
535
|
+
const out = l + r;
|
|
536
|
+
return out.length <= 4096 ? out : null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function extractPrefixFromOptsV3(node) {
|
|
543
|
+
if (!t.isObjectExpression(node)) return null;
|
|
544
|
+
for (const p of node.properties) {
|
|
545
|
+
if (!t.isObjectProperty(p)) continue;
|
|
546
|
+
const key =
|
|
547
|
+
t.isIdentifier(p.key) ? p.key.name :
|
|
548
|
+
t.isStringLiteral(p.key) ? p.key.value :
|
|
549
|
+
null;
|
|
550
|
+
if (key !== "prefix") continue;
|
|
551
|
+
return evalStaticString(p.value);
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function extractRouteObjectV3(objExpr) {
|
|
557
|
+
let url = null;
|
|
558
|
+
let methods = [];
|
|
559
|
+
let hasHandler = false;
|
|
560
|
+
const hooks = [];
|
|
561
|
+
|
|
562
|
+
for (const p of objExpr.properties) {
|
|
563
|
+
if (!t.isObjectProperty(p)) continue;
|
|
564
|
+
|
|
565
|
+
const key =
|
|
566
|
+
t.isIdentifier(p.key) ? p.key.name :
|
|
567
|
+
t.isStringLiteral(p.key) ? p.key.value :
|
|
568
|
+
null;
|
|
569
|
+
if (!key) continue;
|
|
570
|
+
|
|
571
|
+
if (key === "url") {
|
|
572
|
+
const u = evalStaticString(p.value);
|
|
573
|
+
if (typeof u === "string") url = u;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (key === "method") {
|
|
577
|
+
if (t.isStringLiteral(p.value)) methods = [p.value.value];
|
|
578
|
+
if (t.isArrayExpression(p.value)) {
|
|
579
|
+
methods = p.value.elements.filter((e) => t.isStringLiteral(e)).map((e) => e.value);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (key === "handler") hasHandler = true;
|
|
584
|
+
if (["preHandler", "onRequest", "preValidation", "preSerialization"].includes(key)) hooks.push(key);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return { url, methods, hasHandler, hooks };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function unwrapPluginArg(node) {
|
|
591
|
+
// fastify-plugin wrappers are common: fastify.register(fp(plugin))
|
|
592
|
+
// We unwrap 1 layer, best-effort.
|
|
593
|
+
if (!node) return node;
|
|
594
|
+
if (!t.isCallExpression(node)) return node;
|
|
595
|
+
|
|
596
|
+
const calleeName =
|
|
597
|
+
t.isIdentifier(node.callee) ? node.callee.name :
|
|
598
|
+
t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.property) ? node.callee.property.name :
|
|
599
|
+
null;
|
|
600
|
+
|
|
601
|
+
if (!calleeName) return node;
|
|
602
|
+
if (!/^(fp|fastifyPlugin|plugin|fastifyPluginify)$/i.test(calleeName)) return node;
|
|
603
|
+
|
|
604
|
+
const a0 = node.arguments && node.arguments[0];
|
|
605
|
+
return a0 || node;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
traverse(ast, {
|
|
610
|
+
FunctionDeclaration(p) {
|
|
611
|
+
if (t.isIdentifier(p.node.id)) {
|
|
612
|
+
_localPlugins.set(p.node.id.name, p.node);
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
VariableDeclarator(p) {
|
|
616
|
+
if (!t.isIdentifier(p.node.id)) return;
|
|
617
|
+
const id = p.node.id.name;
|
|
618
|
+
const init = p.node.init;
|
|
619
|
+
if (!init) return;
|
|
620
|
+
|
|
621
|
+
_rawConstInits.set(id, init);
|
|
622
|
+
|
|
623
|
+
// local plugin: const routes = async (fastify) => { ... }
|
|
624
|
+
if (t.isFunctionExpression(init) || t.isArrowFunctionExpression(init)) {
|
|
625
|
+
_localPlugins.set(id, init);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// const app = Fastify()
|
|
629
|
+
if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
|
|
630
|
+
const cal = init.callee.name;
|
|
631
|
+
if (cal === "Fastify" || cal === "fastify") fastifyNames.add(id);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// const app = require("fastify")()
|
|
635
|
+
if (t.isCallExpression(init) && t.isCallExpression(init.callee)) {
|
|
636
|
+
const inner = init.callee;
|
|
637
|
+
if (t.isIdentifier(inner.callee, { name: "require" }) && t.isStringLiteral(inner.arguments?.[0], { value: "fastify" })) {
|
|
638
|
+
fastifyNames.add(id);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
} catch {
|
|
644
|
+
// Babel traverse can fail on some edge-case files; skip this step
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// helper: resolve imports for register(pluginIdent,...)
|
|
648
|
+
function resolveImportSpecForLocal(localName) {
|
|
649
|
+
let spec = null;
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
traverse(ast, {
|
|
653
|
+
ImportDeclaration(ip) {
|
|
654
|
+
for (const s of ip.node.specifiers) {
|
|
655
|
+
if ((t.isImportDefaultSpecifier(s) || t.isImportSpecifier(s)) && s.local.name === localName) {
|
|
656
|
+
spec = ip.node.source.value;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
VariableDeclarator(vp) {
|
|
661
|
+
if (!t.isIdentifier(vp.node.id) || vp.node.id.name !== localName) return;
|
|
662
|
+
const init = vp.node.init;
|
|
663
|
+
if (!t.isCallExpression(init)) return;
|
|
664
|
+
if (!t.isIdentifier(init.callee) || init.callee.name !== "require") return;
|
|
665
|
+
const a0 = init.arguments[0];
|
|
666
|
+
if (t.isStringLiteral(a0)) spec = a0.value;
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
} catch {
|
|
670
|
+
// Babel traverse can fail; ignore
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return spec;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
traverse(ast, {
|
|
678
|
+
CallExpression(p) {
|
|
679
|
+
const callee = p.node.callee;
|
|
680
|
+
if (!t.isMemberExpression(callee)) return;
|
|
681
|
+
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
682
|
+
|
|
683
|
+
const obj = callee.object.name;
|
|
684
|
+
const prop = callee.property.name;
|
|
685
|
+
|
|
686
|
+
if (!fastifyNames.has(obj)) return;
|
|
687
|
+
|
|
688
|
+
// fastify.get('/x', ...)
|
|
689
|
+
if (isFastifyMethod(prop)) {
|
|
690
|
+
const routeStr = evalStaticString(p.node.arguments[0]);
|
|
691
|
+
if (!routeStr) return;
|
|
692
|
+
|
|
693
|
+
const fullPath = joinPaths(prefix, routeStr);
|
|
694
|
+
const method = canonicalizeMethod(prop);
|
|
695
|
+
|
|
696
|
+
const ev = evidenceFromLoc({
|
|
697
|
+
fileAbs,
|
|
698
|
+
fileRel,
|
|
699
|
+
loc: p.node.loc,
|
|
700
|
+
reason: `Fastify ${prop.toUpperCase()}("${routeStr}")`,
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
routes.push({
|
|
704
|
+
method,
|
|
705
|
+
path: fullPath,
|
|
706
|
+
handler: fileRel,
|
|
707
|
+
confidence: "med",
|
|
708
|
+
framework: "fastify",
|
|
709
|
+
evidence: ev ? [ev] : [],
|
|
710
|
+
});
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// fastify.route({ method, url, handler })
|
|
715
|
+
if (prop === "route") {
|
|
716
|
+
const arg0 = p.node.arguments[0];
|
|
717
|
+
if (!t.isObjectExpression(arg0)) return;
|
|
718
|
+
|
|
719
|
+
const r = extractRouteObjectV3(arg0);
|
|
720
|
+
if (!r.url) return;
|
|
721
|
+
|
|
722
|
+
const fullPath = joinPaths(prefix, r.url);
|
|
723
|
+
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
724
|
+
|
|
725
|
+
const ev = evidenceFromLoc({
|
|
726
|
+
fileAbs,
|
|
727
|
+
fileRel,
|
|
728
|
+
loc: p.node.loc,
|
|
729
|
+
reason: `Fastify.route({ url: "${r.url}" })`,
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
for (const m of ms) {
|
|
733
|
+
routes.push({
|
|
734
|
+
method: m,
|
|
735
|
+
path: fullPath,
|
|
736
|
+
handler: fileRel,
|
|
737
|
+
hooks: r.hooks,
|
|
738
|
+
confidence: r.hasHandler ? "med" : "low",
|
|
739
|
+
framework: "fastify",
|
|
740
|
+
evidence: ev ? [ev] : [],
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// fastify.register(plugin, { prefix })
|
|
747
|
+
if (prop === "register") {
|
|
748
|
+
const pluginArgRaw = unwrapPluginArg(p.node.arguments[0]);
|
|
749
|
+
const optsArg = p.node.arguments[1];
|
|
750
|
+
|
|
751
|
+
const childPrefixRaw = extractPrefixFromOptsV3(optsArg);
|
|
752
|
+
const childPrefix = childPrefixRaw ? joinPaths(prefix, childPrefixRaw) : prefix;
|
|
753
|
+
|
|
754
|
+
// inline plugin OR local plugin identifier (common in real Fastify codebases)
|
|
755
|
+
let pluginFn = null;
|
|
756
|
+
const localIdentName = t.isIdentifier(pluginArgRaw) ? pluginArgRaw.name : null;
|
|
757
|
+
if (t.isFunctionExpression(pluginArgRaw) || t.isArrowFunctionExpression(pluginArgRaw)) pluginFn = pluginArgRaw;
|
|
758
|
+
if (!pluginFn && localIdentName) pluginFn = _localPlugins.get(localIdentName) || null;
|
|
759
|
+
|
|
760
|
+
if (pluginFn) {
|
|
761
|
+
const param0 = pluginFn.params && pluginFn.params[0];
|
|
762
|
+
const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
|
|
763
|
+
const bodyNode = pluginFn.body;
|
|
764
|
+
if (!t.isBlockStatement(bodyNode)) {
|
|
765
|
+
// We only handle block bodies. Expression-bodied arrows are rare for route plugins.
|
|
766
|
+
if (localIdentName) return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// traverse just the plugin body (best effort)
|
|
770
|
+
try {
|
|
771
|
+
traverse(
|
|
772
|
+
bodyNode,
|
|
773
|
+
{
|
|
774
|
+
CallExpression(pp) {
|
|
775
|
+
const c = pp.node.callee;
|
|
776
|
+
if (!t.isMemberExpression(c)) return;
|
|
777
|
+
if (!t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
|
|
778
|
+
if (c.object.name !== innerName) return;
|
|
779
|
+
|
|
780
|
+
const pr = c.property.name;
|
|
781
|
+
|
|
782
|
+
if (isFastifyMethod(pr)) {
|
|
783
|
+
const rs = evalStaticString(pp.node.arguments[0]);
|
|
784
|
+
if (!rs) return;
|
|
785
|
+
|
|
786
|
+
const fullPath = joinPaths(childPrefix, rs);
|
|
787
|
+
const method = canonicalizeMethod(pr);
|
|
788
|
+
|
|
789
|
+
const ev = evidenceFromLoc({
|
|
790
|
+
fileAbs,
|
|
791
|
+
fileRel,
|
|
792
|
+
loc: pp.node.loc,
|
|
793
|
+
reason: `Fastify plugin ${pr.toUpperCase()}("${rs}") prefix="${childPrefixRaw || ""}"`,
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
routes.push({
|
|
797
|
+
method,
|
|
798
|
+
path: fullPath,
|
|
799
|
+
handler: fileRel,
|
|
800
|
+
confidence: "med",
|
|
801
|
+
framework: "fastify",
|
|
802
|
+
evidence: ev ? [ev] : [],
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (pr === "route") {
|
|
807
|
+
const a0 = pp.node.arguments[0];
|
|
808
|
+
if (!t.isObjectExpression(a0)) return;
|
|
809
|
+
|
|
810
|
+
const r = extractRouteObjectV3(a0);
|
|
811
|
+
if (!r.url) return;
|
|
812
|
+
|
|
813
|
+
const fullPath = joinPaths(childPrefix, r.url);
|
|
814
|
+
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
815
|
+
|
|
816
|
+
const ev = evidenceFromLoc({
|
|
817
|
+
fileAbs,
|
|
818
|
+
fileRel,
|
|
819
|
+
loc: pp.node.loc,
|
|
820
|
+
reason: `Fastify plugin route("${r.url}") prefix="${childPrefixRaw || ""}"`,
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
for (const m of ms) {
|
|
824
|
+
routes.push({
|
|
825
|
+
method: m,
|
|
826
|
+
path: fullPath,
|
|
827
|
+
handler: fileRel,
|
|
828
|
+
confidence: "med",
|
|
829
|
+
framework: "fastify",
|
|
830
|
+
evidence: ev ? [ev] : [],
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
p.scope,
|
|
837
|
+
p
|
|
838
|
+
);
|
|
839
|
+
} catch {
|
|
840
|
+
// Inner traverse can fail; skip this plugin body
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// If this was a local plugin identifier, we've already extracted its routes.
|
|
844
|
+
if (localIdentName) return;
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Resolve dynamic require/import spec directly (fastify.register(require("./x")) / import("./x"))
|
|
849
|
+
const dynSpec = extractRequireOrImportSpec(pluginArgRaw);
|
|
850
|
+
if (dynSpec) {
|
|
851
|
+
const resolved = resolveRelativeModule(fileAbs, dynSpec);
|
|
852
|
+
if (!resolved) {
|
|
853
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec: dynSpec });
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
scanFile(resolved, childPrefix);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// imported plugin identifier
|
|
861
|
+
if (t.isIdentifier(pluginArgRaw)) {
|
|
862
|
+
const localName = pluginArgRaw.name;
|
|
863
|
+
const spec = resolveImportSpecForLocal(localName);
|
|
864
|
+
|
|
865
|
+
if (!spec) {
|
|
866
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, name: localName });
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const resolved = resolveRelativeModule(fileAbs, spec);
|
|
871
|
+
if (!resolved) {
|
|
872
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec });
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
scanFile(resolved, childPrefix);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Anything else: unknown plugin shape. Mark a gap so analyzers can be lenient.
|
|
881
|
+
gaps.push({
|
|
882
|
+
kind: "fastify_plugin_unresolved",
|
|
883
|
+
file: fileRel,
|
|
884
|
+
note: "register() plugin not statically resolvable",
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
} catch {
|
|
890
|
+
// Babel traverse can fail on some edge-case files; skip
|
|
891
|
+
stats.parseErrors++;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
scanFile(entryAbs, "/");
|
|
896
|
+
return { routes, gaps };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ---------- client refs (fetch + axios + template literals best-effort) ----------
|
|
900
|
+
function isAxiosMember(node) {
|
|
901
|
+
return (
|
|
902
|
+
t.isMemberExpression(node) &&
|
|
903
|
+
t.isIdentifier(node.object) &&
|
|
904
|
+
t.isIdentifier(node.property) &&
|
|
905
|
+
["get", "post", "put", "patch", "delete"].includes(node.property.name)
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function isAxiosCallee(node) {
|
|
910
|
+
return t.isIdentifier(node, { name: "axios" }) || isAxiosMember(node);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function extractUrlLike(node) {
|
|
914
|
+
// "literal"
|
|
915
|
+
if (t.isStringLiteral(node)) return { url: node.value, confidence: "high", note: "string" };
|
|
916
|
+
|
|
917
|
+
// `literal`
|
|
918
|
+
if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
|
|
919
|
+
return { url: node.quasis.map((q) => q.value.cooked || "").join(""), confidence: "high", note: "template_static" };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// `/api/x/${id}` -> "/api/x/:id" (med confidence)
|
|
923
|
+
if (t.isTemplateLiteral(node) && node.quasis.length >= 1) {
|
|
924
|
+
const start = node.quasis[0]?.value?.cooked || "";
|
|
925
|
+
if (!start.startsWith("/")) return null;
|
|
926
|
+
|
|
927
|
+
let built = "";
|
|
928
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
929
|
+
built += node.quasis[i].value.cooked || "";
|
|
930
|
+
if (i < node.expressions.length) {
|
|
931
|
+
const expr = node.expressions[i];
|
|
932
|
+
if (t.isIdentifier(expr)) built += `:${expr.name}`;
|
|
933
|
+
else built += "*";
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return { url: built, confidence: "med", note: "template_dynamic" };
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// "/api/x" + "/y" or "/api/x" + id (low confidence)
|
|
940
|
+
if (t.isBinaryExpression(node, { operator: "+" })) {
|
|
941
|
+
if (t.isStringLiteral(node.left) && node.left.value.startsWith("/")) {
|
|
942
|
+
const left = node.left.value;
|
|
943
|
+
let right = "";
|
|
944
|
+
if (t.isStringLiteral(node.right)) right = node.right.value;
|
|
945
|
+
else if (t.isIdentifier(node.right)) right = `:${node.right.name}`;
|
|
946
|
+
else right = "*";
|
|
947
|
+
return { url: left + right, confidence: "low", note: "concat" };
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function extractFetchMethodFromOptions(node) {
|
|
955
|
+
if (!t.isObjectExpression(node)) return "*";
|
|
956
|
+
for (const prop of node.properties) {
|
|
957
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
958
|
+
const key =
|
|
959
|
+
t.isIdentifier(prop.key) ? prop.key.name :
|
|
960
|
+
t.isStringLiteral(prop.key) ? prop.key.value :
|
|
961
|
+
null;
|
|
962
|
+
if (key === "method" && t.isStringLiteral(prop.value)) return canonicalizeMethod(prop.value.value);
|
|
963
|
+
}
|
|
964
|
+
return "*";
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function extractAxiosConfig(node) {
|
|
968
|
+
// axios({ url: "/api/x", method: "post" })
|
|
969
|
+
if (!t.isObjectExpression(node)) return null;
|
|
970
|
+
|
|
971
|
+
let urlNode = null;
|
|
972
|
+
let methodNode = null;
|
|
973
|
+
|
|
974
|
+
for (const prop of node.properties) {
|
|
975
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
976
|
+
const key =
|
|
977
|
+
t.isIdentifier(prop.key) ? prop.key.name :
|
|
978
|
+
t.isStringLiteral(prop.key) ? prop.key.value :
|
|
979
|
+
null;
|
|
980
|
+
if (!key) continue;
|
|
981
|
+
|
|
982
|
+
if (key === "url") urlNode = prop.value;
|
|
983
|
+
if (key === "method") methodNode = prop.value;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const urlInfo = urlNode ? extractUrlLike(urlNode) : null;
|
|
987
|
+
if (!urlInfo) return null;
|
|
988
|
+
|
|
989
|
+
const method =
|
|
990
|
+
methodNode && t.isStringLiteral(methodNode)
|
|
991
|
+
? canonicalizeMethod(methodNode.value)
|
|
992
|
+
: "*";
|
|
993
|
+
|
|
994
|
+
return { method, urlInfo };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async function resolveClientRouteRefs(repoRoot, stats) {
|
|
998
|
+
const files = await fg(CODE_FILE_GLOBS, {
|
|
999
|
+
cwd: repoRoot,
|
|
1000
|
+
absolute: true,
|
|
1001
|
+
ignore: [
|
|
1002
|
+
...IGNORE_GLOBS,
|
|
1003
|
+
"**/test/**",
|
|
1004
|
+
"**/tests/**",
|
|
1005
|
+
"**/__tests__/**",
|
|
1006
|
+
"**/*.test.*",
|
|
1007
|
+
"**/*.spec.*",
|
|
1008
|
+
"**/jest.setup.*",
|
|
1009
|
+
"**/jest.config.*",
|
|
1010
|
+
"**/*.mock.*",
|
|
1011
|
+
"**/mocks/**",
|
|
1012
|
+
"**/fixtures/**",
|
|
1013
|
+
],
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
const out = [];
|
|
1017
|
+
|
|
1018
|
+
for (const fileAbs of files) {
|
|
1019
|
+
const fileRel = normalizeRel(repoRoot, fileAbs);
|
|
1020
|
+
const code = safeRead(fileAbs);
|
|
1021
|
+
if (!code) continue;
|
|
1022
|
+
|
|
1023
|
+
let ast;
|
|
1024
|
+
try {
|
|
1025
|
+
ast = parseFile(code, fileAbs);
|
|
1026
|
+
} catch {
|
|
1027
|
+
stats.parseErrors++;
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
try {
|
|
1032
|
+
traverse(ast, {
|
|
1033
|
+
CallExpression(p) {
|
|
1034
|
+
const callee = p.node.callee;
|
|
1035
|
+
|
|
1036
|
+
// fetch(url, opts)
|
|
1037
|
+
if (t.isIdentifier(callee, { name: "fetch" })) {
|
|
1038
|
+
const a0 = p.node.arguments[0];
|
|
1039
|
+
const a1 = p.node.arguments[1];
|
|
1040
|
+
|
|
1041
|
+
const urlInfo = extractUrlLike(a0);
|
|
1042
|
+
if (!urlInfo) return;
|
|
1043
|
+
|
|
1044
|
+
const url = urlInfo.url;
|
|
1045
|
+
if (!url.startsWith("/")) return;
|
|
1046
|
+
|
|
1047
|
+
const method = extractFetchMethodFromOptions(a1);
|
|
1048
|
+
|
|
1049
|
+
const ev = evidenceFromLoc({
|
|
1050
|
+
fileAbs,
|
|
1051
|
+
fileRel,
|
|
1052
|
+
loc: p.node.loc,
|
|
1053
|
+
reason: `Client fetch(${urlInfo.note}) "${stripQueryHash(url)}"`,
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
out.push({
|
|
1057
|
+
method,
|
|
1058
|
+
path: canonicalizePath(url),
|
|
1059
|
+
source: fileRel,
|
|
1060
|
+
confidence: urlInfo.confidence,
|
|
1061
|
+
kind: "fetch",
|
|
1062
|
+
evidence: ev ? [ev] : [],
|
|
1063
|
+
});
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// axios.get("/api/x") etc
|
|
1068
|
+
if (isAxiosMember(callee)) {
|
|
1069
|
+
const verb = callee.property.name.toUpperCase();
|
|
1070
|
+
const a0 = p.node.arguments[0];
|
|
1071
|
+
|
|
1072
|
+
const urlInfo = extractUrlLike(a0);
|
|
1073
|
+
if (!urlInfo) return;
|
|
1074
|
+
|
|
1075
|
+
const url = urlInfo.url;
|
|
1076
|
+
if (!url.startsWith("/")) return;
|
|
1077
|
+
|
|
1078
|
+
const ev = evidenceFromLoc({
|
|
1079
|
+
fileAbs,
|
|
1080
|
+
fileRel,
|
|
1081
|
+
loc: p.node.loc,
|
|
1082
|
+
reason: `Client axios.${verb.toLowerCase()}(${urlInfo.note}) "${stripQueryHash(url)}"`,
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
out.push({
|
|
1086
|
+
method: canonicalizeMethod(verb),
|
|
1087
|
+
path: canonicalizePath(url),
|
|
1088
|
+
source: fileRel,
|
|
1089
|
+
confidence: urlInfo.confidence,
|
|
1090
|
+
kind: "axios_member",
|
|
1091
|
+
evidence: ev ? [ev] : [],
|
|
1092
|
+
});
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// axios({ url, method })
|
|
1097
|
+
if (t.isIdentifier(callee, { name: "axios" })) {
|
|
1098
|
+
const a0 = p.node.arguments[0];
|
|
1099
|
+
const cfg = extractAxiosConfig(a0);
|
|
1100
|
+
if (!cfg) return;
|
|
1101
|
+
|
|
1102
|
+
const url = cfg.urlInfo.url;
|
|
1103
|
+
if (!url.startsWith("/")) return;
|
|
1104
|
+
|
|
1105
|
+
const ev = evidenceFromLoc({
|
|
1106
|
+
fileAbs,
|
|
1107
|
+
fileRel,
|
|
1108
|
+
loc: p.node.loc,
|
|
1109
|
+
reason: `Client axios(config:${cfg.urlInfo.note}) "${stripQueryHash(url)}"`,
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
out.push({
|
|
1113
|
+
method: cfg.method,
|
|
1114
|
+
path: canonicalizePath(url),
|
|
1115
|
+
source: fileRel,
|
|
1116
|
+
confidence: cfg.urlInfo.confidence === "high" ? "high" : "med",
|
|
1117
|
+
kind: "axios_config",
|
|
1118
|
+
evidence: ev ? [ev] : [],
|
|
1119
|
+
});
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// useSWR("/api/user", fetcher) - Modern React data fetching
|
|
1124
|
+
if (t.isIdentifier(callee, { name: "useSWR" })) {
|
|
1125
|
+
const a0 = p.node.arguments[0];
|
|
1126
|
+
|
|
1127
|
+
const urlInfo = extractUrlLike(a0);
|
|
1128
|
+
if (!urlInfo) return;
|
|
1129
|
+
|
|
1130
|
+
const url = urlInfo.url;
|
|
1131
|
+
if (!url.startsWith("/")) return;
|
|
1132
|
+
|
|
1133
|
+
const ev = evidenceFromLoc({
|
|
1134
|
+
fileAbs,
|
|
1135
|
+
fileRel,
|
|
1136
|
+
loc: p.node.loc,
|
|
1137
|
+
reason: `Client useSWR(${urlInfo.note}) "${stripQueryHash(url)}"`,
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
out.push({
|
|
1141
|
+
method: "GET", // SWR is almost always GET
|
|
1142
|
+
path: canonicalizePath(url),
|
|
1143
|
+
source: fileRel,
|
|
1144
|
+
confidence: urlInfo.confidence,
|
|
1145
|
+
kind: "useSWR",
|
|
1146
|
+
evidence: ev ? [ev] : [],
|
|
1147
|
+
});
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// useQuery (React Query / TanStack Query) - Another popular data fetching library
|
|
1152
|
+
if (t.isIdentifier(callee, { name: "useQuery" })) {
|
|
1153
|
+
// useQuery({ queryKey: [...], queryFn: () => fetch("/api/x") })
|
|
1154
|
+
// or useQuery(["key"], () => fetch("/api/x"))
|
|
1155
|
+
const a0 = p.node.arguments[0];
|
|
1156
|
+
|
|
1157
|
+
// Try to extract URL from the arguments (often in queryFn)
|
|
1158
|
+
if (t.isObjectExpression(a0)) {
|
|
1159
|
+
for (const prop of a0.properties) {
|
|
1160
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
1161
|
+
if (!t.isIdentifier(prop.key, { name: "queryFn" })) continue;
|
|
1162
|
+
|
|
1163
|
+
// queryFn is often an arrow function with fetch inside
|
|
1164
|
+
const fn = prop.value;
|
|
1165
|
+
if (t.isArrowFunctionExpression(fn) || t.isFunctionExpression(fn)) {
|
|
1166
|
+
// Best effort: look for string literals that look like API paths
|
|
1167
|
+
const fnCode = code.slice(fn.start, fn.end);
|
|
1168
|
+
const urlMatch = fnCode.match(/["'`](\/api\/[^"'`]+)["'`]/);
|
|
1169
|
+
if (urlMatch) {
|
|
1170
|
+
const url = urlMatch[1].split("?")[0].split("#")[0];
|
|
1171
|
+
const ev = evidenceFromLoc({
|
|
1172
|
+
fileAbs,
|
|
1173
|
+
fileRel,
|
|
1174
|
+
loc: p.node.loc,
|
|
1175
|
+
reason: `Client useQuery(queryFn) "${url}"`,
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
out.push({
|
|
1179
|
+
method: "GET",
|
|
1180
|
+
path: canonicalizePath(url),
|
|
1181
|
+
source: fileRel,
|
|
1182
|
+
confidence: "low", // Less certain extraction
|
|
1183
|
+
kind: "useQuery",
|
|
1184
|
+
evidence: ev ? [ev] : [],
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
},
|
|
1192
|
+
});
|
|
1193
|
+
} catch {
|
|
1194
|
+
// Babel traverse can fail on some edge-case files; skip
|
|
1195
|
+
stats.parseErrors++;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// ---------- workspace detection (best-effort, no new deps) ----------
|
|
1203
|
+
function readJsonIfExists(abs) {
|
|
1204
|
+
try {
|
|
1205
|
+
return JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
1206
|
+
} catch {
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function detectWorkspaces(repoRoot) {
|
|
1212
|
+
const roots = [];
|
|
1213
|
+
|
|
1214
|
+
const pkg = readJsonIfExists(path.join(repoRoot, "package.json"));
|
|
1215
|
+
if (pkg && pkg.workspaces) {
|
|
1216
|
+
const ws = pkg.workspaces;
|
|
1217
|
+
const patterns = Array.isArray(ws) ? ws : Array.isArray(ws.packages) ? ws.packages : [];
|
|
1218
|
+
for (const pat of patterns) {
|
|
1219
|
+
if (typeof pat === "string") roots.push(pat);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// pnpm-workspace.yaml minimal parser (just handles `packages:` list)
|
|
1224
|
+
const pnpmWs = path.join(repoRoot, "pnpm-workspace.yaml");
|
|
1225
|
+
if (fs.existsSync(pnpmWs)) {
|
|
1226
|
+
const raw = safeRead(pnpmWs) || "";
|
|
1227
|
+
const lines = raw.split(/\r?\n/);
|
|
1228
|
+
let inPackages = false;
|
|
1229
|
+
for (const line of lines) {
|
|
1230
|
+
const l = line.trim();
|
|
1231
|
+
if (!l) continue;
|
|
1232
|
+
if (l.startsWith("packages:")) {
|
|
1233
|
+
inPackages = true;
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (inPackages) {
|
|
1237
|
+
const m = l.match(/^-+\s*['"]?([^'"]+)['"]?\s*$/);
|
|
1238
|
+
if (m && m[1]) roots.push(m[1]);
|
|
1239
|
+
else if (!l.startsWith("-")) inPackages = false;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Expand to actual package.json roots
|
|
1245
|
+
const uniq = Array.from(new Set(roots)).filter(Boolean);
|
|
1246
|
+
const pkgJsonGlobs = uniq.map((p) => (p.endsWith("/") ? p : p + "/") + "package.json");
|
|
1247
|
+
|
|
1248
|
+
const found = pkgJsonGlobs.length
|
|
1249
|
+
? fg.sync(pkgJsonGlobs, { cwd: repoRoot, absolute: true, ignore: IGNORE_GLOBS })
|
|
1250
|
+
: [];
|
|
1251
|
+
|
|
1252
|
+
const workspaces = found
|
|
1253
|
+
.map((abs) => path.dirname(abs))
|
|
1254
|
+
.map((abs) => normalizeRel(repoRoot, abs))
|
|
1255
|
+
.sort();
|
|
1256
|
+
|
|
1257
|
+
return workspaces;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// ---------- fastify entry detection (monorepo-friendly) ----------
|
|
1261
|
+
async function detectFastifyEntries(repoRoot) {
|
|
1262
|
+
// Keep it targeted (fast), but broad enough for monorepos.
|
|
1263
|
+
const candidates = await fg(
|
|
1264
|
+
[
|
|
1265
|
+
"**/{server,app,main,index}.{ts,tsx,js,jsx}",
|
|
1266
|
+
"**/src/{server,app,main,index}.{ts,tsx,js,jsx}",
|
|
1267
|
+
"**/*fastify*.{ts,tsx,js,jsx}",
|
|
1268
|
+
"**/*api*.{ts,tsx,js,jsx}",
|
|
1269
|
+
],
|
|
1270
|
+
{
|
|
1271
|
+
cwd: repoRoot,
|
|
1272
|
+
absolute: true,
|
|
1273
|
+
ignore: IGNORE_GLOBS,
|
|
1274
|
+
}
|
|
1275
|
+
);
|
|
1276
|
+
|
|
1277
|
+
const entries = [];
|
|
1278
|
+
const fastifySignal = /\b(Fastify\s*\(|fastify\s*\(|require\(['"]fastify['"]\)|from\s+['"]fastify['"])\b/;
|
|
1279
|
+
const listenSignal = /\.\s*(listen|ready)\s*\(/;
|
|
1280
|
+
|
|
1281
|
+
for (const fileAbs of candidates) {
|
|
1282
|
+
const code = safeRead(fileAbs);
|
|
1283
|
+
if (!code) continue;
|
|
1284
|
+
// Must look like fastify + server start-ish signal (reduces noise)
|
|
1285
|
+
if (fastifySignal.test(code) && listenSignal.test(code)) {
|
|
1286
|
+
entries.push(fileAbs);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
return Array.from(new Set(entries));
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// ---------- truthpack build/write ----------
|
|
1294
|
+
async function buildTruthpack({ repoRoot, fastifyEntry }) {
|
|
1295
|
+
const stats = {
|
|
1296
|
+
parseErrors: 0,
|
|
1297
|
+
fastifyEntries: 0,
|
|
1298
|
+
fastifyRoutes: 0,
|
|
1299
|
+
nextAppRoutes: 0,
|
|
1300
|
+
nextPagesRoutes: 0,
|
|
1301
|
+
clientRefs: 0,
|
|
1302
|
+
serverRoutes: 0,
|
|
1303
|
+
gaps: 0,
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
// Workspaces (for metadata + future use)
|
|
1307
|
+
const workspaces = detectWorkspaces(repoRoot);
|
|
1308
|
+
|
|
1309
|
+
// Next.js routes (App Router + Pages Router)
|
|
1310
|
+
const nextApp = await resolveNextAppApiRoutes(repoRoot, stats);
|
|
1311
|
+
const nextPages = await resolveNextPagesApiRoutes(repoRoot, stats);
|
|
1312
|
+
|
|
1313
|
+
stats.nextAppRoutes = nextApp.length;
|
|
1314
|
+
stats.nextPagesRoutes = nextPages.length;
|
|
1315
|
+
|
|
1316
|
+
// Fastify routes (monorepo-friendly)
|
|
1317
|
+
let fastify = { routes: [], gaps: [] };
|
|
1318
|
+
|
|
1319
|
+
if (fastifyEntry) {
|
|
1320
|
+
const entryAbs = path.isAbsolute(fastifyEntry) ? fastifyEntry : path.join(repoRoot, fastifyEntry);
|
|
1321
|
+
if (exists(entryAbs)) {
|
|
1322
|
+
const resolved = resolveFastifyRoutes(repoRoot, entryAbs, stats);
|
|
1323
|
+
fastify.routes.push(...resolved.routes);
|
|
1324
|
+
fastify.gaps.push(...resolved.gaps);
|
|
1325
|
+
stats.fastifyEntries = 1;
|
|
1326
|
+
}
|
|
1327
|
+
} else {
|
|
1328
|
+
const entries = await detectFastifyEntries(repoRoot);
|
|
1329
|
+
stats.fastifyEntries = entries.length;
|
|
1330
|
+
|
|
1331
|
+
for (const entryAbs of entries) {
|
|
1332
|
+
const resolved = resolveFastifyRoutes(repoRoot, entryAbs, stats);
|
|
1333
|
+
fastify.routes.push(...resolved.routes);
|
|
1334
|
+
fastify.gaps.push(...resolved.gaps);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
stats.fastifyRoutes = fastify.routes.length;
|
|
1339
|
+
|
|
1340
|
+
// Multi-framework route detection v2 (Express, Flask, FastAPI, Django, Hono, Koa, etc.)
|
|
1341
|
+
const multiFramework = await resolveAllRoutes(repoRoot);
|
|
1342
|
+
const detectedFrameworks = await detectFrameworks(repoRoot);
|
|
1343
|
+
|
|
1344
|
+
// Client refs (JS/TS fetch/axios + Python requests/httpx)
|
|
1345
|
+
const clientRefs = await resolveClientRouteRefs(repoRoot, stats);
|
|
1346
|
+
const allClientRefs = [...clientRefs, ...(multiFramework.clientRefs || [])];
|
|
1347
|
+
|
|
1348
|
+
stats.clientRefs = allClientRefs.length;
|
|
1349
|
+
|
|
1350
|
+
// Merge all server routes (dedupe with priority)
|
|
1351
|
+
const serverRoutesRaw = [...nextApp, ...nextPages, ...(fastify.routes || []), ...(multiFramework.routes || [])];
|
|
1352
|
+
|
|
1353
|
+
const bestByKey = new Map(); // key = method:path
|
|
1354
|
+
for (const r of serverRoutesRaw) {
|
|
1355
|
+
const key = `${canonicalizeMethod(r.method)}:${canonicalizePath(r.path)}`;
|
|
1356
|
+
|
|
1357
|
+
const prev = bestByKey.get(key);
|
|
1358
|
+
if (!prev) {
|
|
1359
|
+
bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Prefer higher confidence, and prefer specific method over "*"
|
|
1364
|
+
const prevScore = scoreConfidence(prev.confidence) + (prev.method === "*" ? 0 : 1);
|
|
1365
|
+
const curScore = scoreConfidence(r.confidence) + (r.method === "*" ? 0 : 1);
|
|
1366
|
+
|
|
1367
|
+
if (curScore > prevScore) {
|
|
1368
|
+
bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const server = Array.from(bestByKey.values());
|
|
1373
|
+
stats.serverRoutes = server.length;
|
|
1374
|
+
|
|
1375
|
+
// Merge gaps
|
|
1376
|
+
const allGaps = [...(fastify.gaps || []), ...(multiFramework.gaps || [])];
|
|
1377
|
+
stats.gaps = allGaps.length;
|
|
1378
|
+
|
|
1379
|
+
// Env Truth v1
|
|
1380
|
+
const env = await buildEnvTruth(repoRoot);
|
|
1381
|
+
|
|
1382
|
+
// Auth Truth v1
|
|
1383
|
+
const auth = await buildAuthTruth(repoRoot, server);
|
|
1384
|
+
|
|
1385
|
+
// Billing Truth v1
|
|
1386
|
+
const billing = await buildBillingTruth(repoRoot);
|
|
1387
|
+
|
|
1388
|
+
// Enforcement Truth v1
|
|
1389
|
+
const enforcement = buildEnforcementTruth(repoRoot, server);
|
|
1390
|
+
|
|
1391
|
+
// Determine frameworks
|
|
1392
|
+
const frameworks = new Set();
|
|
1393
|
+
detectedFrameworks.forEach((f) => frameworks.add(f));
|
|
1394
|
+
server.forEach((r) => r.framework && frameworks.add(r.framework));
|
|
1395
|
+
if (nextApp.length || nextPages.length) frameworks.add("next");
|
|
1396
|
+
if (fastify.routes.length) frameworks.add("fastify");
|
|
1397
|
+
|
|
1398
|
+
const truthpack = {
|
|
1399
|
+
meta: {
|
|
1400
|
+
version: "2.1.0",
|
|
1401
|
+
generatedAt: new Date().toISOString(),
|
|
1402
|
+
repoRoot,
|
|
1403
|
+
commit: { sha: process.env.VIBECHECK_COMMIT_SHA || "unknown" },
|
|
1404
|
+
stats,
|
|
1405
|
+
},
|
|
1406
|
+
project: {
|
|
1407
|
+
frameworks: Array.from(frameworks),
|
|
1408
|
+
workspaces,
|
|
1409
|
+
entrypoints: {
|
|
1410
|
+
fastify: fastifyEntry ? [fastifyEntry] : [], // entries auto-detected are not stored as rel here by default
|
|
1411
|
+
},
|
|
1412
|
+
},
|
|
1413
|
+
routes: { server, clientRefs: allClientRefs, gaps: allGaps },
|
|
1414
|
+
env,
|
|
1415
|
+
auth,
|
|
1416
|
+
billing,
|
|
1417
|
+
enforcement,
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
const hash = sha256(JSON.stringify(truthpack));
|
|
1421
|
+
truthpack.index = { hashes: { truthpackHash: hash }, evidenceRefs: [] };
|
|
1422
|
+
|
|
1423
|
+
return truthpack;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function writeTruthpack(repoRoot, truthpack) {
|
|
1427
|
+
const dir = path.join(repoRoot, ".vibecheck");
|
|
1428
|
+
ensureDir(dir);
|
|
1429
|
+
|
|
1430
|
+
const target = path.join(dir, "truthpack.json");
|
|
1431
|
+
const tmp = path.join(dir, `truthpack.${process.pid}.${Date.now()}.tmp.json`);
|
|
1432
|
+
|
|
1433
|
+
// atomic-ish write: write tmp then rename
|
|
1434
|
+
fs.writeFileSync(tmp, JSON.stringify(truthpack, null, 2));
|
|
1435
|
+
fs.renameSync(tmp, target);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function loadTruthpack(repoRoot) {
|
|
1439
|
+
const specPath = path.join(repoRoot, ".vibecheck", "truthpack.json");
|
|
1440
|
+
const legacyPath = path.join(repoRoot, ".vibecheck", "truth", "truthpack.json");
|
|
1441
|
+
|
|
1442
|
+
try {
|
|
1443
|
+
return JSON.parse(fs.readFileSync(specPath, "utf8"));
|
|
1444
|
+
} catch {
|
|
1445
|
+
try {
|
|
1446
|
+
return JSON.parse(fs.readFileSync(legacyPath, "utf8"));
|
|
1447
|
+
} catch {
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// ---------- RepoIndex-powered build (vNext) ----------
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Build truthpack using RepoIndex for single-pass file indexing
|
|
1457
|
+
* This is the optimized path that shares file reads across all extractors.
|
|
1458
|
+
*
|
|
1459
|
+
* Enable with: VIBECHECK_ENGINE_V2=1
|
|
1460
|
+
*
|
|
1461
|
+
* @param {Object} options
|
|
1462
|
+
* @param {string} options.repoRoot
|
|
1463
|
+
* @param {string} [options.fastifyEntry]
|
|
1464
|
+
* @param {boolean} [options.verbose]
|
|
1465
|
+
* @returns {Promise<Object>}
|
|
1466
|
+
*/
|
|
1467
|
+
async function buildTruthpackV2({ repoRoot, fastifyEntry, verbose }) {
|
|
1468
|
+
const {
|
|
1469
|
+
createIndex,
|
|
1470
|
+
globalASTCache,
|
|
1471
|
+
logIndexSummary,
|
|
1472
|
+
extractNextAppRoutes,
|
|
1473
|
+
extractNextPagesRoutes,
|
|
1474
|
+
extractClientRefs,
|
|
1475
|
+
extractFastifyRoutes,
|
|
1476
|
+
detectFastifyEntries: detectFastifyEntriesV2,
|
|
1477
|
+
buildEnvTruthV2,
|
|
1478
|
+
buildBillingTruthV2,
|
|
1479
|
+
buildAuthTruthV2,
|
|
1480
|
+
buildEnforcementTruthV2,
|
|
1481
|
+
extractExpressRoutes,
|
|
1482
|
+
} = require("./engine");
|
|
1483
|
+
|
|
1484
|
+
const startTime = Date.now();
|
|
1485
|
+
|
|
1486
|
+
// Phase 0: Build RepoIndex (single glob pass)
|
|
1487
|
+
const index = await createIndex(repoRoot);
|
|
1488
|
+
|
|
1489
|
+
if (verbose || process.env.VIBECHECK_VERBOSE) {
|
|
1490
|
+
logIndexSummary(index);
|
|
1491
|
+
console.log(` AST Cache: ${globalASTCache.getSummary().hitRate} hit rate`);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
const stats = {
|
|
1495
|
+
parseErrors: 0,
|
|
1496
|
+
fastifyEntries: 0,
|
|
1497
|
+
fastifyRoutes: 0,
|
|
1498
|
+
nextAppRoutes: 0,
|
|
1499
|
+
nextPagesRoutes: 0,
|
|
1500
|
+
clientRefs: 0,
|
|
1501
|
+
serverRoutes: 0,
|
|
1502
|
+
gaps: 0,
|
|
1503
|
+
indexTimeMs: index.stats.indexTimeMs,
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
// Use signals from index instead of re-detecting
|
|
1507
|
+
const detectedFrameworks = Array.from(index.signals.detectedFrameworks);
|
|
1508
|
+
|
|
1509
|
+
// Workspaces
|
|
1510
|
+
const workspaces = detectWorkspaces(repoRoot);
|
|
1511
|
+
|
|
1512
|
+
// Phase 1: Extract routes using optimized extractors (use RepoIndex + AST cache)
|
|
1513
|
+
|
|
1514
|
+
// Next.js routes - use optimized extractors
|
|
1515
|
+
const nextApp = extractNextAppRoutes(index, stats);
|
|
1516
|
+
const nextPages = extractNextPagesRoutes(index, stats);
|
|
1517
|
+
|
|
1518
|
+
stats.nextAppRoutes = nextApp.length;
|
|
1519
|
+
stats.nextPagesRoutes = nextPages.length;
|
|
1520
|
+
|
|
1521
|
+
// Fastify routes - use optimized extractor
|
|
1522
|
+
let fastify = { routes: [], gaps: [] };
|
|
1523
|
+
|
|
1524
|
+
if (index.hasFramework("fastify") || fastifyEntry) {
|
|
1525
|
+
if (fastifyEntry) {
|
|
1526
|
+
const entryAbs = path.isAbsolute(fastifyEntry) ? fastifyEntry : path.join(repoRoot, fastifyEntry);
|
|
1527
|
+
if (exists(entryAbs)) {
|
|
1528
|
+
const resolved = extractFastifyRoutes(index, entryAbs, stats);
|
|
1529
|
+
fastify.routes.push(...resolved.routes);
|
|
1530
|
+
fastify.gaps.push(...resolved.gaps);
|
|
1531
|
+
stats.fastifyEntries = 1;
|
|
1532
|
+
}
|
|
1533
|
+
} else {
|
|
1534
|
+
const entries = detectFastifyEntriesV2(index);
|
|
1535
|
+
stats.fastifyEntries = entries.length;
|
|
1536
|
+
|
|
1537
|
+
for (const entryAbs of entries) {
|
|
1538
|
+
const resolved = extractFastifyRoutes(index, entryAbs, stats);
|
|
1539
|
+
fastify.routes.push(...resolved.routes);
|
|
1540
|
+
fastify.gaps.push(...resolved.gaps);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
stats.fastifyRoutes = fastify.routes.length;
|
|
1546
|
+
|
|
1547
|
+
// Express routes - use optimized extractor
|
|
1548
|
+
const expressRoutes = extractExpressRoutes(index, stats);
|
|
1549
|
+
|
|
1550
|
+
// Multi-framework routes (Flask, Django, etc. - still uses old resolvers for non-JS frameworks)
|
|
1551
|
+
// Express is handled above via optimized extractor
|
|
1552
|
+
const multiFramework = await resolveAllRoutes(repoRoot, {
|
|
1553
|
+
mode: process.env.VIBECHECK_ROUTE_SCAN_MODE || "smart",
|
|
1554
|
+
verbose: verbose || !!process.env.VIBECHECK_VERBOSE_ROUTES,
|
|
1555
|
+
skipExpress: true, // Skip Express since we handle it above with optimized extractor
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
// Client refs - use optimized extractor
|
|
1559
|
+
const clientRefsOptimized = extractClientRefs(index, stats);
|
|
1560
|
+
const allClientRefs = [...clientRefsOptimized, ...(multiFramework.clientRefs || [])];
|
|
1561
|
+
|
|
1562
|
+
stats.clientRefs = allClientRefs.length;
|
|
1563
|
+
|
|
1564
|
+
// Merge server routes (including optimized Express routes)
|
|
1565
|
+
const serverRoutesRaw = [...nextApp, ...nextPages, ...(fastify.routes || []), ...expressRoutes, ...(multiFramework.routes || [])];
|
|
1566
|
+
|
|
1567
|
+
const bestByKey = new Map();
|
|
1568
|
+
for (const r of serverRoutesRaw) {
|
|
1569
|
+
const key = `${canonicalizeMethod(r.method)}:${canonicalizePath(r.path)}`;
|
|
1570
|
+
const prev = bestByKey.get(key);
|
|
1571
|
+
if (!prev) {
|
|
1572
|
+
bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
const prevScore = scoreConfidence(prev.confidence) + (prev.method === "*" ? 0 : 1);
|
|
1576
|
+
const curScore = scoreConfidence(r.confidence) + (r.method === "*" ? 0 : 1);
|
|
1577
|
+
if (curScore > prevScore) {
|
|
1578
|
+
bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
const server = Array.from(bestByKey.values());
|
|
1583
|
+
stats.serverRoutes = server.length;
|
|
1584
|
+
|
|
1585
|
+
// Merge gaps
|
|
1586
|
+
const allGaps = [...(fastify.gaps || []), ...(multiFramework.gaps || [])];
|
|
1587
|
+
stats.gaps = allGaps.length;
|
|
1588
|
+
|
|
1589
|
+
// Phase 2: Build other truths (env, auth, billing, enforcement)
|
|
1590
|
+
// All use optimized extractors with RepoIndex
|
|
1591
|
+
const env = buildEnvTruthV2(index, stats);
|
|
1592
|
+
const auth = buildAuthTruthV2(index, server);
|
|
1593
|
+
const billing = buildBillingTruthV2(index, stats);
|
|
1594
|
+
const enforcement = buildEnforcementTruthV2(index, server);
|
|
1595
|
+
|
|
1596
|
+
// Determine frameworks
|
|
1597
|
+
const frameworks = new Set(detectedFrameworks);
|
|
1598
|
+
server.forEach((r) => r.framework && frameworks.add(r.framework));
|
|
1599
|
+
if (nextApp.length || nextPages.length) frameworks.add("next");
|
|
1600
|
+
if (fastify.routes.length) frameworks.add("fastify");
|
|
1601
|
+
|
|
1602
|
+
stats.totalTimeMs = Date.now() - startTime;
|
|
1603
|
+
|
|
1604
|
+
const truthpack = {
|
|
1605
|
+
meta: {
|
|
1606
|
+
version: "2.2.0", // Bump version for v2 engine
|
|
1607
|
+
generatedAt: new Date().toISOString(),
|
|
1608
|
+
repoRoot,
|
|
1609
|
+
commit: { sha: process.env.VIBECHECK_COMMIT_SHA || "unknown" },
|
|
1610
|
+
stats,
|
|
1611
|
+
engine: "v2", // Mark as v2 engine
|
|
1612
|
+
},
|
|
1613
|
+
project: {
|
|
1614
|
+
frameworks: Array.from(frameworks),
|
|
1615
|
+
workspaces,
|
|
1616
|
+
entrypoints: {
|
|
1617
|
+
fastify: fastifyEntry ? [fastifyEntry] : [],
|
|
1618
|
+
},
|
|
1619
|
+
},
|
|
1620
|
+
routes: { server, clientRefs: allClientRefs, gaps: allGaps },
|
|
1621
|
+
env,
|
|
1622
|
+
auth,
|
|
1623
|
+
billing,
|
|
1624
|
+
enforcement,
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
const hash = sha256(JSON.stringify(truthpack));
|
|
1628
|
+
truthpack.index = {
|
|
1629
|
+
hashes: { truthpackHash: hash },
|
|
1630
|
+
evidenceRefs: [],
|
|
1631
|
+
repoIndex: {
|
|
1632
|
+
totalFiles: index.stats.totalFiles,
|
|
1633
|
+
totalSize: index.stats.totalSize,
|
|
1634
|
+
indexTimeMs: index.stats.indexTimeMs,
|
|
1635
|
+
},
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
// Clear caches to free memory
|
|
1639
|
+
index.clearContentCache();
|
|
1640
|
+
clearCache();
|
|
1641
|
+
|
|
1642
|
+
return truthpack;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* Smart buildTruthpack - uses v2 engine by default for better performance.
|
|
1647
|
+
* Set VIBECHECK_ENGINE_V1=1 to fall back to v1 for backward compatibility.
|
|
1648
|
+
*
|
|
1649
|
+
* V2 Engine Benefits:
|
|
1650
|
+
* - Single-pass file indexing (RepoIndex)
|
|
1651
|
+
* - Shared AST cache (globalASTCache)
|
|
1652
|
+
* - Token prefiltering for faster file selection
|
|
1653
|
+
* - ~30% faster on typical projects
|
|
1654
|
+
*/
|
|
1655
|
+
async function buildTruthpackSmart(options) {
|
|
1656
|
+
if (process.env.VIBECHECK_ENGINE_V1 === "1") {
|
|
1657
|
+
return buildTruthpack(options);
|
|
1658
|
+
}
|
|
1659
|
+
// V2 is now the default - uses RepoIndex + AST cache
|
|
1660
|
+
return buildTruthpackV2(options);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
module.exports = {
|
|
1664
|
+
canonicalizeMethod,
|
|
1665
|
+
canonicalizePath,
|
|
1666
|
+
buildTruthpack,
|
|
1667
|
+
buildTruthpackV2,
|
|
1668
|
+
buildTruthpackSmart,
|
|
1669
|
+
writeTruthpack,
|
|
1670
|
+
loadTruthpack,
|
|
1671
|
+
clearCache, // Clear file cache to free memory (important for long-running processes)
|
|
1672
|
+
// kept for backward compatibility if other code imports it,
|
|
1673
|
+
// but fastifyEntry is now optional and auto-detected.
|
|
1674
|
+
detectFastifyEntry: function detectFastifyEntry(repoRoot) {
|
|
1675
|
+
const candidates = [
|
|
1676
|
+
"src/server.ts",
|
|
1677
|
+
"src/server.js",
|
|
1678
|
+
"server.ts",
|
|
1679
|
+
"server.js",
|
|
1680
|
+
"src/index.ts",
|
|
1681
|
+
"src/index.js",
|
|
1682
|
+
"index.ts",
|
|
1683
|
+
"index.js",
|
|
1684
|
+
];
|
|
1685
|
+
for (const rel of candidates) {
|
|
1686
|
+
const abs = path.join(repoRoot, rel);
|
|
1687
|
+
if (exists(abs)) return rel;
|
|
1688
|
+
}
|
|
1689
|
+
return null;
|
|
1690
|
+
},
|
|
1691
|
+
};
|