@tyroneross/navgator 0.2.2 → 0.9.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/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +21 -7
- package/.claude-plugin/plugin.json +16 -11
- package/.codex-plugin/plugin.json +31 -0
- package/.mcp.json +8 -0
- package/CLAUDE.md +197 -23
- package/LICENSE +202 -21
- package/README.md +220 -33
- package/agents/architecture-advisor.md +6 -2
- package/agents/architecture-investigator.md +163 -0
- package/agents/architecture-planner.md +160 -0
- package/agents/external-resolver.md +97 -0
- package/dist/__tests__/agent-output.test.d.ts +5 -0
- package/dist/__tests__/agent-output.test.d.ts.map +1 -0
- package/dist/__tests__/agent-output.test.js +233 -0
- package/dist/__tests__/agent-output.test.js.map +1 -0
- package/dist/__tests__/architecture-insights-stack.test.d.ts +21 -0
- package/dist/__tests__/architecture-insights-stack.test.d.ts.map +1 -0
- package/dist/__tests__/architecture-insights-stack.test.js +86 -0
- package/dist/__tests__/architecture-insights-stack.test.js.map +1 -0
- package/dist/__tests__/architecture-insights.test.d.ts +2 -0
- package/dist/__tests__/architecture-insights.test.d.ts.map +1 -0
- package/dist/__tests__/architecture-insights.test.js +46 -0
- package/dist/__tests__/architecture-insights.test.js.map +1 -0
- package/dist/__tests__/audit-sampler.test.d.ts +10 -0
- package/dist/__tests__/audit-sampler.test.d.ts.map +1 -0
- package/dist/__tests__/audit-sampler.test.js +172 -0
- package/dist/__tests__/audit-sampler.test.js.map +1 -0
- package/dist/__tests__/audit-spc.test.d.ts +5 -0
- package/dist/__tests__/audit-spc.test.d.ts.map +1 -0
- package/dist/__tests__/audit-spc.test.js +94 -0
- package/dist/__tests__/audit-spc.test.js.map +1 -0
- package/dist/__tests__/audit-verifiers.test.d.ts +5 -0
- package/dist/__tests__/audit-verifiers.test.d.ts.map +1 -0
- package/dist/__tests__/audit-verifiers.test.js +248 -0
- package/dist/__tests__/audit-verifiers.test.js.map +1 -0
- package/dist/__tests__/auto-refresh.test.d.ts +12 -0
- package/dist/__tests__/auto-refresh.test.d.ts.map +1 -0
- package/dist/__tests__/auto-refresh.test.js +236 -0
- package/dist/__tests__/auto-refresh.test.js.map +1 -0
- package/dist/__tests__/bare-imports.test.d.ts +8 -0
- package/dist/__tests__/bare-imports.test.d.ts.map +1 -0
- package/dist/__tests__/bare-imports.test.js +176 -0
- package/dist/__tests__/bare-imports.test.js.map +1 -0
- package/dist/__tests__/classify.test.d.ts +5 -0
- package/dist/__tests__/classify.test.d.ts.map +1 -0
- package/dist/__tests__/classify.test.js +158 -0
- package/dist/__tests__/classify.test.js.map +1 -0
- package/dist/__tests__/cli-commands.test.d.ts +8 -0
- package/dist/__tests__/cli-commands.test.d.ts.map +1 -0
- package/dist/__tests__/cli-commands.test.js +207 -0
- package/dist/__tests__/cli-commands.test.js.map +1 -0
- package/dist/__tests__/consolidated-readers.test.d.ts +23 -0
- package/dist/__tests__/consolidated-readers.test.d.ts.map +1 -0
- package/dist/__tests__/consolidated-readers.test.js +200 -0
- package/dist/__tests__/consolidated-readers.test.js.map +1 -0
- package/dist/__tests__/coverage.test.d.ts +5 -0
- package/dist/__tests__/coverage.test.d.ts.map +1 -0
- package/dist/__tests__/coverage.test.js +120 -0
- package/dist/__tests__/coverage.test.js.map +1 -0
- package/dist/__tests__/deploy-scanner-runtime.test.d.ts +6 -0
- package/dist/__tests__/deploy-scanner-runtime.test.d.ts.map +1 -0
- package/dist/__tests__/deploy-scanner-runtime.test.js +168 -0
- package/dist/__tests__/deploy-scanner-runtime.test.js.map +1 -0
- package/dist/__tests__/env-scanner.test.d.ts +2 -0
- package/dist/__tests__/env-scanner.test.d.ts.map +1 -0
- package/dist/__tests__/env-scanner.test.js +191 -0
- package/dist/__tests__/env-scanner.test.js.map +1 -0
- package/dist/__tests__/freshness/cli-freshness.test.d.ts +2 -0
- package/dist/__tests__/freshness/cli-freshness.test.d.ts.map +1 -0
- package/dist/__tests__/freshness/cli-freshness.test.js +26 -0
- package/dist/__tests__/freshness/cli-freshness.test.js.map +1 -0
- package/dist/__tests__/freshness/dirty-ledger.test.d.ts +2 -0
- package/dist/__tests__/freshness/dirty-ledger.test.d.ts.map +1 -0
- package/dist/__tests__/freshness/dirty-ledger.test.js +39 -0
- package/dist/__tests__/freshness/dirty-ledger.test.js.map +1 -0
- package/dist/__tests__/freshness/drainer.test.d.ts +2 -0
- package/dist/__tests__/freshness/drainer.test.d.ts.map +1 -0
- package/dist/__tests__/freshness/drainer.test.js +103 -0
- package/dist/__tests__/freshness/drainer.test.js.map +1 -0
- package/dist/__tests__/freshness/paths.test.d.ts +2 -0
- package/dist/__tests__/freshness/paths.test.d.ts.map +1 -0
- package/dist/__tests__/freshness/paths.test.js +19 -0
- package/dist/__tests__/freshness/paths.test.js.map +1 -0
- package/dist/__tests__/freshness/scan-lock.test.d.ts +2 -0
- package/dist/__tests__/freshness/scan-lock.test.d.ts.map +1 -0
- package/dist/__tests__/freshness/scan-lock.test.js +40 -0
- package/dist/__tests__/freshness/scan-lock.test.js.map +1 -0
- package/dist/__tests__/freshness/stamp.test.d.ts +2 -0
- package/dist/__tests__/freshness/stamp.test.d.ts.map +1 -0
- package/dist/__tests__/freshness/stamp.test.js +36 -0
- package/dist/__tests__/freshness/stamp.test.js.map +1 -0
- package/dist/__tests__/gitignore-safety.test.d.ts +2 -0
- package/dist/__tests__/gitignore-safety.test.d.ts.map +1 -0
- package/dist/__tests__/gitignore-safety.test.js +110 -0
- package/dist/__tests__/gitignore-safety.test.js.map +1 -0
- package/dist/__tests__/helpers.d.ts +37 -0
- package/dist/__tests__/helpers.d.ts.map +1 -0
- package/dist/__tests__/helpers.js +134 -0
- package/dist/__tests__/helpers.js.map +1 -0
- package/dist/__tests__/impact.test.d.ts +5 -0
- package/dist/__tests__/impact.test.d.ts.map +1 -0
- package/dist/__tests__/impact.test.js +221 -0
- package/dist/__tests__/impact.test.js.map +1 -0
- package/dist/__tests__/lessons-store.test.d.ts +8 -0
- package/dist/__tests__/lessons-store.test.d.ts.map +1 -0
- package/dist/__tests__/lessons-store.test.js +232 -0
- package/dist/__tests__/lessons-store.test.js.map +1 -0
- package/dist/__tests__/llm-dedup.test.d.ts +2 -0
- package/dist/__tests__/llm-dedup.test.d.ts.map +1 -0
- package/dist/__tests__/llm-dedup.test.js +155 -0
- package/dist/__tests__/llm-dedup.test.js.map +1 -0
- package/dist/__tests__/mjs-frontend-fetch.test.d.ts +19 -0
- package/dist/__tests__/mjs-frontend-fetch.test.d.ts.map +1 -0
- package/dist/__tests__/mjs-frontend-fetch.test.js +179 -0
- package/dist/__tests__/mjs-frontend-fetch.test.js.map +1 -0
- package/dist/__tests__/multi-stack-discovery.test.d.ts +11 -0
- package/dist/__tests__/multi-stack-discovery.test.d.ts.map +1 -0
- package/dist/__tests__/multi-stack-discovery.test.js +75 -0
- package/dist/__tests__/multi-stack-discovery.test.js.map +1 -0
- package/dist/__tests__/per-entity-files-gate.test.d.ts +22 -0
- package/dist/__tests__/per-entity-files-gate.test.d.ts.map +1 -0
- package/dist/__tests__/per-entity-files-gate.test.js +160 -0
- package/dist/__tests__/per-entity-files-gate.test.js.map +1 -0
- package/dist/__tests__/prisma-calls.test.d.ts +2 -0
- package/dist/__tests__/prisma-calls.test.d.ts.map +1 -0
- package/dist/__tests__/prisma-calls.test.js +125 -0
- package/dist/__tests__/prisma-calls.test.js.map +1 -0
- package/dist/__tests__/prisma-parser.test.d.ts +2 -0
- package/dist/__tests__/prisma-parser.test.d.ts.map +1 -0
- package/dist/__tests__/prisma-parser.test.js +252 -0
- package/dist/__tests__/prisma-parser.test.js.map +1 -0
- package/dist/__tests__/prompt-detector.test.d.ts +5 -0
- package/dist/__tests__/prompt-detector.test.d.ts.map +1 -0
- package/dist/__tests__/prompt-detector.test.js +75 -0
- package/dist/__tests__/prompt-detector.test.js.map +1 -0
- package/dist/__tests__/queue-scanner.test.d.ts +5 -0
- package/dist/__tests__/queue-scanner.test.d.ts.map +1 -0
- package/dist/__tests__/queue-scanner.test.js +85 -0
- package/dist/__tests__/queue-scanner.test.js.map +1 -0
- package/dist/__tests__/resolve.test.d.ts +5 -0
- package/dist/__tests__/resolve.test.d.ts.map +1 -0
- package/dist/__tests__/resolve.test.js +196 -0
- package/dist/__tests__/resolve.test.js.map +1 -0
- package/dist/__tests__/rules.test.d.ts +2 -0
- package/dist/__tests__/rules.test.d.ts.map +1 -0
- package/dist/__tests__/rules.test.js +343 -0
- package/dist/__tests__/rules.test.js.map +1 -0
- package/dist/__tests__/sandbox.test.d.ts +5 -0
- package/dist/__tests__/sandbox.test.d.ts.map +1 -0
- package/dist/__tests__/sandbox.test.js +189 -0
- package/dist/__tests__/sandbox.test.js.map +1 -0
- package/dist/__tests__/scanner-audit.test.d.ts +9 -0
- package/dist/__tests__/scanner-audit.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-audit.test.js +64 -0
- package/dist/__tests__/scanner-audit.test.js.map +1 -0
- package/dist/__tests__/scanner-characterization.test.d.ts +16 -0
- package/dist/__tests__/scanner-characterization.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-characterization.test.js +167 -0
- package/dist/__tests__/scanner-characterization.test.js.map +1 -0
- package/dist/__tests__/scanner-incremental.test.d.ts +13 -0
- package/dist/__tests__/scanner-incremental.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-incremental.test.js +725 -0
- package/dist/__tests__/scanner-incremental.test.js.map +1 -0
- package/dist/__tests__/scanner-integration.test.d.ts +7 -0
- package/dist/__tests__/scanner-integration.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-integration.test.js +211 -0
- package/dist/__tests__/scanner-integration.test.js.map +1 -0
- package/dist/__tests__/scip-new-catches.test.d.ts +19 -0
- package/dist/__tests__/scip-new-catches.test.d.ts.map +1 -0
- package/dist/__tests__/scip-new-catches.test.js +90 -0
- package/dist/__tests__/scip-new-catches.test.js.map +1 -0
- package/dist/__tests__/subgraph.test.d.ts +5 -0
- package/dist/__tests__/subgraph.test.d.ts.map +1 -0
- package/dist/__tests__/subgraph.test.js +145 -0
- package/dist/__tests__/subgraph.test.js.map +1 -0
- package/dist/__tests__/trace.test.d.ts +5 -0
- package/dist/__tests__/trace.test.d.ts.map +1 -0
- package/dist/__tests__/trace.test.js +221 -0
- package/dist/__tests__/trace.test.js.map +1 -0
- package/dist/agent-output.d.ts +16 -0
- package/dist/agent-output.d.ts.map +1 -0
- package/dist/agent-output.js +142 -0
- package/dist/agent-output.js.map +1 -0
- package/dist/architecture-insights.d.ts +17 -0
- package/dist/architecture-insights.d.ts.map +1 -0
- package/dist/architecture-insights.js +178 -0
- package/dist/architecture-insights.js.map +1 -0
- package/dist/audit/index.d.ts +69 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +255 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/sampler.d.ts +98 -0
- package/dist/audit/sampler.d.ts.map +1 -0
- package/dist/audit/sampler.js +298 -0
- package/dist/audit/sampler.js.map +1 -0
- package/dist/audit/spc.d.ts +62 -0
- package/dist/audit/spc.d.ts.map +1 -0
- package/dist/audit/spc.js +81 -0
- package/dist/audit/spc.js.map +1 -0
- package/dist/audit/verifiers.d.ts +81 -0
- package/dist/audit/verifiers.d.ts.map +1 -0
- package/dist/audit/verifiers.js +366 -0
- package/dist/audit/verifiers.js.map +1 -0
- package/dist/classify.d.ts +19 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +124 -0
- package/dist/classify.js.map +1 -0
- package/dist/cli/commands/connections.d.ts +3 -0
- package/dist/cli/commands/connections.d.ts.map +1 -0
- package/dist/cli/commands/connections.js +125 -0
- package/dist/cli/commands/connections.js.map +1 -0
- package/dist/cli/commands/coverage.d.ts +3 -0
- package/dist/cli/commands/coverage.d.ts.map +1 -0
- package/dist/cli/commands/coverage.js +94 -0
- package/dist/cli/commands/coverage.js.map +1 -0
- package/dist/cli/commands/dead.d.ts +3 -0
- package/dist/cli/commands/dead.d.ts.map +1 -0
- package/dist/cli/commands/dead.js +80 -0
- package/dist/cli/commands/dead.js.map +1 -0
- package/dist/cli/commands/diagram.d.ts +3 -0
- package/dist/cli/commands/diagram.d.ts.map +1 -0
- package/dist/cli/commands/diagram.js +102 -0
- package/dist/cli/commands/diagram.js.map +1 -0
- package/dist/cli/commands/find.d.ts +3 -0
- package/dist/cli/commands/find.d.ts.map +1 -0
- package/dist/cli/commands/find.js +128 -0
- package/dist/cli/commands/find.js.map +1 -0
- package/dist/cli/commands/freshness.d.ts +20 -0
- package/dist/cli/commands/freshness.d.ts.map +1 -0
- package/dist/cli/commands/freshness.js +90 -0
- package/dist/cli/commands/freshness.js.map +1 -0
- package/dist/cli/commands/helpers.d.ts +7 -0
- package/dist/cli/commands/helpers.d.ts.map +1 -0
- package/dist/cli/commands/helpers.js +30 -0
- package/dist/cli/commands/helpers.js.map +1 -0
- package/dist/cli/commands/impact.d.ts +3 -0
- package/dist/cli/commands/impact.d.ts.map +1 -0
- package/dist/cli/commands/impact.js +172 -0
- package/dist/cli/commands/impact.js.map +1 -0
- package/dist/cli/commands/lessons.d.ts +6 -0
- package/dist/cli/commands/lessons.d.ts.map +1 -0
- package/dist/cli/commands/lessons.js +279 -0
- package/dist/cli/commands/lessons.js.map +1 -0
- package/dist/cli/commands/list.d.ts +3 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +91 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/llm-map.d.ts +3 -0
- package/dist/cli/commands/llm-map.d.ts.map +1 -0
- package/dist/cli/commands/llm-map.js +121 -0
- package/dist/cli/commands/llm-map.js.map +1 -0
- package/dist/cli/commands/misc.d.ts +17 -0
- package/dist/cli/commands/misc.d.ts.map +1 -0
- package/dist/cli/commands/misc.js +495 -0
- package/dist/cli/commands/misc.js.map +1 -0
- package/dist/cli/commands/prompts.d.ts +3 -0
- package/dist/cli/commands/prompts.d.ts.map +1 -0
- package/dist/cli/commands/prompts.js +74 -0
- package/dist/cli/commands/prompts.js.map +1 -0
- package/dist/cli/commands/rules.d.ts +3 -0
- package/dist/cli/commands/rules.d.ts.map +1 -0
- package/dist/cli/commands/rules.js +61 -0
- package/dist/cli/commands/rules.js.map +1 -0
- package/dist/cli/commands/scan.d.ts +3 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/scan.js +177 -0
- package/dist/cli/commands/scan.js.map +1 -0
- package/dist/cli/commands/schema.d.ts +3 -0
- package/dist/cli/commands/schema.d.ts.map +1 -0
- package/dist/cli/commands/schema.js +126 -0
- package/dist/cli/commands/schema.js.map +1 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +340 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/subgraph.d.ts +3 -0
- package/dist/cli/commands/subgraph.d.ts.map +1 -0
- package/dist/cli/commands/subgraph.js +55 -0
- package/dist/cli/commands/subgraph.js.map +1 -0
- package/dist/cli/commands/temporal.d.ts +3 -0
- package/dist/cli/commands/temporal.d.ts.map +1 -0
- package/dist/cli/commands/temporal.js +112 -0
- package/dist/cli/commands/temporal.js.map +1 -0
- package/dist/cli/commands/trace.d.ts +3 -0
- package/dist/cli/commands/trace.d.ts.map +1 -0
- package/dist/cli/commands/trace.js +65 -0
- package/dist/cli/commands/trace.js.map +1 -0
- package/dist/cli/index.js +88 -825
- package/dist/cli/index.js.map +1 -1
- package/dist/config.d.ts +13 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +106 -12
- package/dist/config.js.map +1 -1
- package/dist/coverage.d.ts +37 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +177 -0
- package/dist/coverage.js.map +1 -0
- package/dist/diagram.d.ts.map +1 -1
- package/dist/diagram.js +41 -0
- package/dist/diagram.js.map +1 -1
- package/dist/diff.d.ts +57 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +527 -0
- package/dist/diff.js.map +1 -0
- package/dist/enrich/cache.d.ts +41 -0
- package/dist/enrich/cache.d.ts.map +1 -0
- package/dist/enrich/cache.js +97 -0
- package/dist/enrich/cache.js.map +1 -0
- package/dist/enrich/external-enrichment.types.d.ts +91 -0
- package/dist/enrich/external-enrichment.types.d.ts.map +1 -0
- package/dist/enrich/external-enrichment.types.js +38 -0
- package/dist/enrich/external-enrichment.types.js.map +1 -0
- package/dist/enrich/external-resolver.d.ts +95 -0
- package/dist/enrich/external-resolver.d.ts.map +1 -0
- package/dist/enrich/external-resolver.js +222 -0
- package/dist/enrich/external-resolver.js.map +1 -0
- package/dist/enrich/fetchers.d.ts +30 -0
- package/dist/enrich/fetchers.d.ts.map +1 -0
- package/dist/enrich/fetchers.js +89 -0
- package/dist/enrich/fetchers.js.map +1 -0
- package/dist/file-resolve.d.ts +35 -0
- package/dist/file-resolve.d.ts.map +1 -0
- package/dist/file-resolve.js +159 -0
- package/dist/file-resolve.js.map +1 -0
- package/dist/freshness/dirty-ledger.d.ts +7 -0
- package/dist/freshness/dirty-ledger.d.ts.map +1 -0
- package/dist/freshness/dirty-ledger.js +56 -0
- package/dist/freshness/dirty-ledger.js.map +1 -0
- package/dist/freshness/drainer.d.ts +38 -0
- package/dist/freshness/drainer.d.ts.map +1 -0
- package/dist/freshness/drainer.js +88 -0
- package/dist/freshness/drainer.js.map +1 -0
- package/dist/freshness/paths.d.ts +9 -0
- package/dist/freshness/paths.d.ts.map +1 -0
- package/dist/freshness/paths.js +24 -0
- package/dist/freshness/paths.js.map +1 -0
- package/dist/freshness/scan-lock.d.ts +8 -0
- package/dist/freshness/scan-lock.d.ts.map +1 -0
- package/dist/freshness/scan-lock.js +68 -0
- package/dist/freshness/scan-lock.js.map +1 -0
- package/dist/freshness/stamp.d.ts +25 -0
- package/dist/freshness/stamp.d.ts.map +1 -0
- package/dist/freshness/stamp.js +50 -0
- package/dist/freshness/stamp.js.map +1 -0
- package/dist/git.d.ts +12 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +42 -0
- package/dist/git.js.map +1 -0
- package/dist/gitignore-safety.d.ts +41 -0
- package/dist/gitignore-safety.d.ts.map +1 -0
- package/dist/gitignore-safety.js +107 -0
- package/dist/gitignore-safety.js.map +1 -0
- package/dist/impact.d.ts +20 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +89 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +24 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/lessons-store.d.ts +93 -0
- package/dist/lessons-store.d.ts.map +1 -0
- package/dist/lessons-store.js +265 -0
- package/dist/lessons-store.js.map +1 -0
- package/dist/llm-dedup.d.ts +40 -0
- package/dist/llm-dedup.d.ts.map +1 -0
- package/dist/llm-dedup.js +373 -0
- package/dist/llm-dedup.js.map +1 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +87 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +198 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +744 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/metrics/pagerank-louvain.d.ts +44 -0
- package/dist/metrics/pagerank-louvain.d.ts.map +1 -0
- package/dist/metrics/pagerank-louvain.js +128 -0
- package/dist/metrics/pagerank-louvain.js.map +1 -0
- package/dist/parsers/scip-runner.d.ts +63 -0
- package/dist/parsers/scip-runner.d.ts.map +1 -0
- package/dist/parsers/scip-runner.js +179 -0
- package/dist/parsers/scip-runner.js.map +1 -0
- package/dist/projects.d.ts +54 -0
- package/dist/projects.d.ts.map +1 -0
- package/dist/projects.js +153 -0
- package/dist/projects.js.map +1 -0
- package/dist/resolve.d.ts +22 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +128 -0
- package/dist/resolve.js.map +1 -0
- package/dist/rules.d.ts +36 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +484 -0
- package/dist/rules.js.map +1 -0
- package/dist/sandbox.d.ts +33 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +91 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/scan-lock.d.ts +37 -0
- package/dist/scan-lock.d.ts.map +1 -0
- package/dist/scan-lock.js +145 -0
- package/dist/scan-lock.js.map +1 -0
- package/dist/scanner.d.ts +126 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +1711 -235
- package/dist/scanner.js.map +1 -1
- package/dist/scanners/connections/ast-scanner.d.ts +9 -2
- package/dist/scanners/connections/ast-scanner.d.ts.map +1 -1
- package/dist/scanners/connections/ast-scanner.js +19 -4
- package/dist/scanners/connections/ast-scanner.js.map +1 -1
- package/dist/scanners/connections/import-scanner.d.ts +27 -0
- package/dist/scanners/connections/import-scanner.d.ts.map +1 -0
- package/dist/scanners/connections/import-scanner.js +537 -0
- package/dist/scanners/connections/import-scanner.js.map +1 -0
- package/dist/scanners/connections/llm-call-tracer.d.ts +1 -1
- package/dist/scanners/connections/llm-call-tracer.d.ts.map +1 -1
- package/dist/scanners/connections/llm-call-tracer.js +6 -2
- package/dist/scanners/connections/llm-call-tracer.js.map +1 -1
- package/dist/scanners/connections/prisma-calls.d.ts +11 -0
- package/dist/scanners/connections/prisma-calls.d.ts.map +1 -0
- package/dist/scanners/connections/prisma-calls.js +237 -0
- package/dist/scanners/connections/prisma-calls.js.map +1 -0
- package/dist/scanners/connections/service-calls.d.ts +1 -1
- package/dist/scanners/connections/service-calls.d.ts.map +1 -1
- package/dist/scanners/connections/service-calls.js +35 -3
- package/dist/scanners/connections/service-calls.js.map +1 -1
- package/dist/scanners/infrastructure/cron-scanner.d.ts +14 -0
- package/dist/scanners/infrastructure/cron-scanner.d.ts.map +1 -0
- package/dist/scanners/infrastructure/cron-scanner.js +383 -0
- package/dist/scanners/infrastructure/cron-scanner.js.map +1 -0
- package/dist/scanners/infrastructure/deploy-scanner.d.ts +11 -0
- package/dist/scanners/infrastructure/deploy-scanner.d.ts.map +1 -0
- package/dist/scanners/infrastructure/deploy-scanner.js +508 -0
- package/dist/scanners/infrastructure/deploy-scanner.js.map +1 -0
- package/dist/scanners/infrastructure/env-scanner.d.ts +55 -0
- package/dist/scanners/infrastructure/env-scanner.d.ts.map +1 -0
- package/dist/scanners/infrastructure/env-scanner.js +431 -0
- package/dist/scanners/infrastructure/env-scanner.js.map +1 -0
- package/dist/scanners/infrastructure/field-usage-analyzer.d.ts +52 -0
- package/dist/scanners/infrastructure/field-usage-analyzer.d.ts.map +1 -0
- package/dist/scanners/infrastructure/field-usage-analyzer.js +480 -0
- package/dist/scanners/infrastructure/field-usage-analyzer.js.map +1 -0
- package/dist/scanners/infrastructure/prisma-parser.d.ts +21 -0
- package/dist/scanners/infrastructure/prisma-parser.d.ts.map +1 -0
- package/dist/scanners/infrastructure/prisma-parser.js +58 -0
- package/dist/scanners/infrastructure/prisma-parser.js.map +1 -0
- package/dist/scanners/infrastructure/prisma-scanner.d.ts +30 -0
- package/dist/scanners/infrastructure/prisma-scanner.d.ts.map +1 -0
- package/dist/scanners/infrastructure/prisma-scanner.js +329 -0
- package/dist/scanners/infrastructure/prisma-scanner.js.map +1 -0
- package/dist/scanners/infrastructure/queue-scanner.d.ts +14 -0
- package/dist/scanners/infrastructure/queue-scanner.d.ts.map +1 -0
- package/dist/scanners/infrastructure/queue-scanner.js +455 -0
- package/dist/scanners/infrastructure/queue-scanner.js.map +1 -0
- package/dist/scanners/infrastructure/typespec-validator.d.ts +50 -0
- package/dist/scanners/infrastructure/typespec-validator.d.ts.map +1 -0
- package/dist/scanners/infrastructure/typespec-validator.js +407 -0
- package/dist/scanners/infrastructure/typespec-validator.js.map +1 -0
- package/dist/scanners/packages/swift.d.ts +5 -0
- package/dist/scanners/packages/swift.d.ts.map +1 -1
- package/dist/scanners/packages/swift.js +23 -0
- package/dist/scanners/packages/swift.js.map +1 -1
- package/dist/scanners/prompts/detector.d.ts +13 -2
- package/dist/scanners/prompts/detector.d.ts.map +1 -1
- package/dist/scanners/prompts/detector.js +97 -46
- package/dist/scanners/prompts/detector.js.map +1 -1
- package/dist/scanners/prompts/index.d.ts +1 -1
- package/dist/scanners/prompts/index.d.ts.map +1 -1
- package/dist/scanners/prompts/index.js +2 -2
- package/dist/scanners/prompts/index.js.map +1 -1
- package/dist/scanners/swift/code-scanner.d.ts +1 -1
- package/dist/scanners/swift/code-scanner.d.ts.map +1 -1
- package/dist/scanners/swift/code-scanner.js +216 -2
- package/dist/scanners/swift/code-scanner.js.map +1 -1
- package/dist/scanners/swift/swiftui-scanner.d.ts +45 -0
- package/dist/scanners/swift/swiftui-scanner.d.ts.map +1 -0
- package/dist/scanners/swift/swiftui-scanner.js +606 -0
- package/dist/scanners/swift/swiftui-scanner.js.map +1 -0
- package/dist/scanners/xcode/pbxproj-parser.d.ts +32 -0
- package/dist/scanners/xcode/pbxproj-parser.d.ts.map +1 -0
- package/dist/scanners/xcode/pbxproj-parser.js +407 -0
- package/dist/scanners/xcode/pbxproj-parser.js.map +1 -0
- package/dist/scanners/xcode/storyboard-scanner.d.ts +13 -0
- package/dist/scanners/xcode/storyboard-scanner.d.ts.map +1 -0
- package/dist/scanners/xcode/storyboard-scanner.js +236 -0
- package/dist/scanners/xcode/storyboard-scanner.js.map +1 -0
- package/dist/storage/markdown-view.d.ts +49 -0
- package/dist/storage/markdown-view.d.ts.map +1 -0
- package/dist/storage/markdown-view.js +233 -0
- package/dist/storage/markdown-view.js.map +1 -0
- package/dist/storage.d.ts +225 -9
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +945 -86
- package/dist/storage.js.map +1 -1
- package/dist/subgraph.d.ts +30 -0
- package/dist/subgraph.d.ts.map +1 -0
- package/dist/subgraph.js +106 -0
- package/dist/subgraph.js.map +1 -0
- package/dist/temporal/git-store.d.ts +65 -0
- package/dist/temporal/git-store.d.ts.map +1 -0
- package/dist/temporal/git-store.js +166 -0
- package/dist/temporal/git-store.js.map +1 -0
- package/dist/trace.d.ts +38 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +292 -0
- package/dist/trace.js.map +1 -0
- package/dist/types.d.ts +322 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +53 -0
- package/dist/types.js.map +1 -1
- package/hooks/hooks.json +1 -55
- package/hooks/mark-dirty.sh +20 -0
- package/hooks/post-bash-suggest.sh +30 -0
- package/hooks/post-edit-suggest.sh +29 -0
- package/hooks/pre-edit-warn.sh +28 -0
- package/hooks/session-start.sh +19 -0
- package/hooks/stop-suggest.sh +49 -0
- package/package.json +30 -11
- package/scripts/install-codex-plugin.sh +119 -0
- package/scripts/install-plugin.sh +29 -24
- package/skills/architecture-export/SKILL.md +79 -0
- package/skills/architecture-scan/SKILL.md +64 -0
- package/skills/code-review/SKILL.md +368 -0
- package/skills/impact-analysis/SKILL.md +80 -0
- package/skills/infrastructure-scanning.md +42 -0
- package/skills/navgator-setup/SKILL.md +108 -0
- package/web/.next/standalone/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/app-path-routes-manifest.json +3 -0
- package/web/.next/standalone/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/web/.next/required-server-files.json +4 -4
- package/web/.next/standalone/web/.next/routes-manifest.json +18 -0
- package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found/page/next-font-manifest.json +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.rsc +3 -3
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +3 -3
- package/web/.next/standalone/web/.next/server/app/api/prompts/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/api/rules/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/web/.next/server/app/api/rules/route/build-manifest.json +11 -0
- package/web/.next/standalone/web/.next/server/app/api/rules/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/web/.next/server/app/api/rules/route.js +6 -0
- package/web/.next/standalone/web/.next/server/app/api/rules/route.js.map +5 -0
- package/web/.next/standalone/web/.next/server/app/api/rules/route.js.nft.json +1 -0
- package/web/.next/standalone/web/.next/server/app/api/rules/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/web/.next/server/app/api/subgraph/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/web/.next/server/app/api/subgraph/route/build-manifest.json +11 -0
- package/web/.next/standalone/web/.next/server/app/api/subgraph/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/web/.next/server/app/api/subgraph/route.js +6 -0
- package/web/.next/standalone/web/.next/server/app/api/subgraph/route.js.map +5 -0
- package/web/.next/standalone/web/.next/server/app/api/subgraph/route.js.nft.json +1 -0
- package/web/.next/standalone/web/.next/server/app/api/subgraph/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/web/.next/server/app/api/trace/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/web/.next/server/app/api/trace/route/build-manifest.json +11 -0
- package/web/.next/standalone/web/.next/server/app/api/trace/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/web/.next/server/app/api/trace/route.js +6 -0
- package/web/.next/standalone/web/.next/server/app/api/trace/route.js.map +5 -0
- package/web/.next/standalone/web/.next/server/app/api/trace/route.js.nft.json +1 -0
- package/web/.next/standalone/web/.next/server/app/api/trace/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/web/.next/server/app/index.rsc +6 -6
- package/web/.next/standalone/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +6 -6
- package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +3 -3
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +5 -5
- package/web/.next/standalone/web/.next/server/app/page/next-font-manifest.json +2 -2
- package/web/.next/standalone/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app-paths-manifest.json +3 -0
- package/web/.next/standalone/web/.next/server/chunks/2374f_next_dist_esm_build_templates_app-route_0bb4e66a.js +1 -1
- package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__006b837d._.js +1 -1
- package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__0426efe8._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__2e09fec9._.js +1 -1
- package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__38d0390f._.js +1 -1
- package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__594bcf20._.js +1 -1
- package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__b888fadf._.js +1 -1
- package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__cd5f36ce._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__ee6fc95f._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/[root-of-the-server]__fa2ec862._.js +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_171de0df._.js +9 -4
- package/web/.next/standalone/web/.next/server/chunks/web__next-internal_server_app_api_rules_route_actions_3de01bd5.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/web__next-internal_server_app_api_subgraph_route_actions_d8b5a63f.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/web__next-internal_server_app_api_trace_route_actions_b0703ae2.js +3 -0
- package/web/.next/standalone/web/.next/server/next-font-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/next-font-manifest.json +4 -4
- package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/22a09ecf6ba35cfd.js +17 -0
- package/web/.next/standalone/web/.next/static/chunks/9857ba86ce4e82d8.css +2 -0
- package/web/.next/standalone/web/.next/static/chunks/f899547f99ef4b76.css +1 -0
- package/web/.next/standalone/web/.next/static/media/4fa387ec64143e14-s.c36e1862.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/53b9e256198e5412-s.853d50a3.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/5ce348bf30bf5439-s.ebceb24d.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/6306c77e7c8268e4-s.ff4a2084.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/7178b3e590c64307-s.55554cd0.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/797e433ab948586e-s.p.479bea2b.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/7d817b4c03b0c5f1-s.f377b9c4.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/8a480f0b521d4e75-s.ea323500.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/bbc41e54d2fcbd21-s.d1207556.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/caa3a2e1cccd8315-s.p.3b6cae6d.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/fef07dbb0973bf53-s.518e079e.woff2 +0 -0
- package/web/.next/standalone/web/app/api/components/route.ts +1 -1
- package/web/.next/standalone/web/app/api/connections/route.ts +3 -1
- package/web/.next/standalone/web/app/api/graph/route.ts +3 -3
- package/web/.next/standalone/web/app/api/projects/route.ts +1 -1
- package/web/.next/standalone/web/app/api/prompts/route.ts +2 -2
- package/web/.next/standalone/web/app/api/rules/route.ts +213 -0
- package/web/.next/standalone/web/app/api/settings/route.ts +2 -2
- package/web/.next/standalone/web/app/api/status/route.ts +1 -1
- package/web/.next/standalone/web/app/api/subgraph/route.ts +267 -0
- package/web/.next/standalone/web/app/api/trace/route.ts +321 -0
- package/web/.next/standalone/web/app/page.tsx +9 -1
- package/web/.next/standalone/web/components/connections-panel.tsx +23 -6
- package/web/.next/standalone/web/components/coverage-panel.tsx +309 -0
- package/web/.next/standalone/web/components/rules-panel.tsx +156 -0
- package/web/.next/standalone/web/components/sidebar.tsx +8 -0
- package/web/.next/standalone/web/components/status-overview.tsx +24 -1
- package/web/.next/standalone/web/components/subgraph-panel.tsx +382 -0
- package/web/.next/standalone/web/components/trace-panel.tsx +325 -0
- package/web/.next/standalone/web/lib/hooks/index.ts +3 -0
- package/web/.next/standalone/web/lib/hooks/use-coverage.ts +68 -0
- package/web/.next/standalone/web/lib/hooks/use-subgraph.ts +85 -0
- package/web/.next/standalone/web/lib/hooks/use-trace.ts +84 -0
- package/web/.next/standalone/web/lib/types.ts +108 -0
- package/web/.next/standalone/web/package-lock.json +218 -0
- package/web/.next/standalone/web/package.json +4 -2
- package/web/.next/standalone/web/server.js +1 -1
- package/web/.next/static/chunks/22a09ecf6ba35cfd.js +17 -0
- package/web/.next/static/chunks/9857ba86ce4e82d8.css +2 -0
- package/web/.next/static/chunks/f899547f99ef4b76.css +1 -0
- package/web/.next/static/media/4fa387ec64143e14-s.c36e1862.woff2 +0 -0
- package/web/.next/static/media/53b9e256198e5412-s.853d50a3.woff2 +0 -0
- package/web/.next/static/media/5ce348bf30bf5439-s.ebceb24d.woff2 +0 -0
- package/web/.next/static/media/6306c77e7c8268e4-s.ff4a2084.woff2 +0 -0
- package/web/.next/static/media/7178b3e590c64307-s.55554cd0.woff2 +0 -0
- package/web/.next/static/media/797e433ab948586e-s.p.479bea2b.woff2 +0 -0
- package/web/.next/static/media/7d817b4c03b0c5f1-s.f377b9c4.woff2 +0 -0
- package/web/.next/static/media/8a480f0b521d4e75-s.ea323500.woff2 +0 -0
- package/web/.next/static/media/bbc41e54d2fcbd21-s.d1207556.woff2 +0 -0
- package/web/.next/static/media/caa3a2e1cccd8315-s.p.3b6cae6d.woff2 +0 -0
- package/web/.next/static/media/fef07dbb0973bf53-s.518e079e.woff2 +0 -0
- package/skills/check/SKILL.md +0 -64
- package/skills/connections/SKILL.md +0 -54
- package/skills/diagram/SKILL.md +0 -64
- package/skills/export/SKILL.md +0 -49
- package/skills/impact/SKILL.md +0 -58
- package/skills/install/SKILL.md +0 -94
- package/skills/scan/SKILL.md +0 -75
- package/skills/status/SKILL.md +0 -37
- package/skills/ui/SKILL.md +0 -43
- package/skills/update/SKILL.md +0 -43
- package/web/.next/standalone/web/.next/static/chunks/8a80e7184ad3a13f.css +0 -2
- package/web/.next/standalone/web/.next/static/chunks/c056475f5f4424b6.css +0 -1
- package/web/.next/standalone/web/.next/static/chunks/cb3513192b63e480.js +0 -12
- package/web/.next/standalone/web/.next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
- package/web/.next/standalone/web/.next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
- package/web/.next/static/chunks/8a80e7184ad3a13f.css +0 -2
- package/web/.next/static/chunks/c056475f5f4424b6.css +0 -1
- package/web/.next/static/chunks/cb3513192b63e480.js +0 -12
- package/web/.next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
- package/web/.next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
- package/web/.next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
- package/web/.next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
- package/web/.next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
- package/web/.next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
- /package/web/.next/standalone/web/.next/static/{P-ZMQO7_Wnj487ks3guqa → qZVrJ4kmwXfw4Ikgj1oXR}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{P-ZMQO7_Wnj487ks3guqa → qZVrJ4kmwXfw4Ikgj1oXR}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{P-ZMQO7_Wnj487ks3guqa → qZVrJ4kmwXfw4Ikgj1oXR}/_ssgManifest.js +0 -0
- /package/web/.next/static/{P-ZMQO7_Wnj487ks3guqa → qZVrJ4kmwXfw4Ikgj1oXR}/_buildManifest.js +0 -0
- /package/web/.next/static/{P-ZMQO7_Wnj487ks3guqa → qZVrJ4kmwXfw4Ikgj1oXR}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{P-ZMQO7_Wnj487ks3guqa → qZVrJ4kmwXfw4Ikgj1oXR}/_ssgManifest.js +0 -0
package/dist/scanner.js
CHANGED
|
@@ -2,18 +2,291 @@
|
|
|
2
2
|
* NavGator Main Scanner
|
|
3
3
|
* Orchestrates all component and connection scanners
|
|
4
4
|
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
5
7
|
import { glob } from 'glob';
|
|
8
|
+
const DEFAULT_IGNORE_PATTERNS = [
|
|
9
|
+
'**/node_modules/**',
|
|
10
|
+
'**/dist/**',
|
|
11
|
+
'**/build/**',
|
|
12
|
+
'**/.next/**',
|
|
13
|
+
'**/__pycache__/**',
|
|
14
|
+
'**/venv/**',
|
|
15
|
+
'**/.venv/**',
|
|
16
|
+
'**/.git/**',
|
|
17
|
+
'**/.build/**',
|
|
18
|
+
'**/DerivedData/**',
|
|
19
|
+
'**/.swiftpm/**',
|
|
20
|
+
'**/Pods/**',
|
|
21
|
+
'**/coverage/**',
|
|
22
|
+
// Saved-webpage asset directories (Mediasite/Confluence/MHTML exports etc.)
|
|
23
|
+
// contain inert JS that has no runtime role in the project.
|
|
24
|
+
'**/*_files/**',
|
|
25
|
+
];
|
|
26
|
+
function getIgnorePatterns(root) {
|
|
27
|
+
const userFile = path.join(root, '.navgatorignore');
|
|
28
|
+
if (!fs.existsSync(userFile))
|
|
29
|
+
return DEFAULT_IGNORE_PATTERNS;
|
|
30
|
+
try {
|
|
31
|
+
const userPatterns = fs.readFileSync(userFile, 'utf-8')
|
|
32
|
+
.split('\n')
|
|
33
|
+
.map(line => line.trim())
|
|
34
|
+
.filter(line => line && !line.startsWith('#'));
|
|
35
|
+
return [...DEFAULT_IGNORE_PATTERNS, ...userPatterns];
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return DEFAULT_IGNORE_PATTERNS;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
import { getGitInfo } from './git.js';
|
|
42
|
+
import { enrichFromCache } from './enrich/external-resolver.js';
|
|
43
|
+
import { loadCache, makeLookup } from './enrich/cache.js';
|
|
6
44
|
import { scanNpmPackages, detectNpm } from './scanners/packages/npm.js';
|
|
7
45
|
import { scanPipPackages, detectPip } from './scanners/packages/pip.js';
|
|
8
46
|
import { scanSpmPackages, detectSpm } from './scanners/packages/swift.js';
|
|
9
47
|
import { scanInfrastructure } from './scanners/infrastructure/index.js';
|
|
48
|
+
import { scanPrismaSchema, detectPrisma } from './scanners/infrastructure/prisma-scanner.js';
|
|
49
|
+
import { scanEnvVars, detectEnvFiles } from './scanners/infrastructure/env-scanner.js';
|
|
50
|
+
import { scanQueues, detectQueues } from './scanners/infrastructure/queue-scanner.js';
|
|
51
|
+
import { scanCronJobs, detectCrons } from './scanners/infrastructure/cron-scanner.js';
|
|
52
|
+
import { scanDeployConfig } from './scanners/infrastructure/deploy-scanner.js';
|
|
53
|
+
import { scanPrismaCalls } from './scanners/connections/prisma-calls.js';
|
|
54
|
+
import { scanFieldUsage, canAnalyzeFieldUsage } from './scanners/infrastructure/field-usage-analyzer.js';
|
|
55
|
+
import { scanTypeSpecValidation, canValidateTypeSpec } from './scanners/infrastructure/typespec-validator.js';
|
|
10
56
|
import { scanServiceCalls } from './scanners/connections/service-calls.js';
|
|
11
57
|
import { scanWithAST, scanDatabaseOperations } from './scanners/connections/ast-scanner.js';
|
|
12
58
|
import { scanPrompts, convertToArchitecture, formatPromptsOutput } from './scanners/prompts/index.js';
|
|
13
59
|
import { traceLLMCalls } from './scanners/connections/llm-call-tracer.js';
|
|
14
60
|
import { scanSwiftCode } from './scanners/swift/code-scanner.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
61
|
+
import { scanImports } from './scanners/connections/import-scanner.js';
|
|
62
|
+
import { storeComponents, storeConnections, migratePerEntityFiles, buildIndex, buildGraph, buildFileMap, buildSummary, savePromptScan, clearStorage, clearForFiles, loadIndex, loadAllComponents, loadAllConnections, loadReverseDeps, runIntegrityCheck, mergeByStableId, atomicWriteJSON, ensureStableIdPublic, buildReverseDepsIndex, buildDerivedManifest, createSnapshot, computeFileHashes, saveHashes, detectFileChanges, formatFileChangeSummary, } from './storage.js';
|
|
63
|
+
import { getConfig, ensureStorageDirectories, getIndexPath, getStoragePath, SCHEMA_VERSION, getComponentsPath, getConnectionsPath } from './config.js';
|
|
64
|
+
import { acquireLock } from './scan-lock.js';
|
|
65
|
+
import { computeArchitectureDiff, classifySignificance, loadLatestSnapshot, buildCurrentSnapshot, saveTimelineEntry, generateTimelineId, } from './diff.js';
|
|
66
|
+
import { registerProject } from './projects.js';
|
|
67
|
+
import { runAudit, updateEwmaForAudit } from './audit/index.js';
|
|
68
|
+
/**
|
|
69
|
+
* Strip internal scratch fields (prefixed with `__`) before persisting
|
|
70
|
+
* an AuditReport on a TimelineEntry.
|
|
71
|
+
*/
|
|
72
|
+
function stripInternals(report) {
|
|
73
|
+
const { ...clean } = report;
|
|
74
|
+
for (const k of Object.keys(clean)) {
|
|
75
|
+
if (k.startsWith('__')) {
|
|
76
|
+
delete clean[k];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return clean;
|
|
80
|
+
}
|
|
81
|
+
import { classifyAllConnections } from './classify.js';
|
|
82
|
+
import { isSandboxMode } from './sandbox.js';
|
|
83
|
+
import { ensureSafeGitignore } from './gitignore-safety.js';
|
|
84
|
+
// =============================================================================
|
|
85
|
+
// MULTI-STACK ROOT DISCOVERY
|
|
86
|
+
// =============================================================================
|
|
87
|
+
/**
|
|
88
|
+
* Manifest filenames that mark a directory as the root of a discrete stack.
|
|
89
|
+
* Order matters: when we walk one level deep we stop at the first match per
|
|
90
|
+
* subdir, so place the language-canonical manifests first.
|
|
91
|
+
*/
|
|
92
|
+
const STACK_MANIFESTS = [
|
|
93
|
+
'package.json',
|
|
94
|
+
'pyproject.toml',
|
|
95
|
+
'Cargo.toml',
|
|
96
|
+
'go.mod',
|
|
97
|
+
'pom.xml',
|
|
98
|
+
'Gemfile',
|
|
99
|
+
// Catch-all for .NET — we glob the subdir for `*.csproj` separately
|
|
100
|
+
// because there's no fixed filename. Handled via discoverStackRoots.
|
|
101
|
+
];
|
|
102
|
+
/**
|
|
103
|
+
* Walk one level under `root`, return roots to scan. Behavior:
|
|
104
|
+
*
|
|
105
|
+
* - If `root` has any stack manifest, return `[{ path: root, origin: '.' }]`.
|
|
106
|
+
* No further walking — single-stack repos behave exactly as before.
|
|
107
|
+
* - Else, look at every direct child directory (depth 1). Any child that
|
|
108
|
+
* carries a stack manifest is included.
|
|
109
|
+
* - When more than one child stack is found, all of them are scanned and
|
|
110
|
+
* components get an `origin_root` metadata tag so consumers can group.
|
|
111
|
+
*
|
|
112
|
+
* Skips dotfiles, `node_modules`, `dist`, `build`, `__pycache__`, `.venv`,
|
|
113
|
+
* and anything starting with `.` to avoid scanning vendored or generated dirs.
|
|
114
|
+
*/
|
|
115
|
+
export function discoverStackRoots(root, verbose) {
|
|
116
|
+
// Uses the module-level `fs`/`path` namespace imports at the top of
|
|
117
|
+
// the file. ESM-only — no `require()` here.
|
|
118
|
+
const hasManifest = (dir) => {
|
|
119
|
+
for (const m of STACK_MANIFESTS) {
|
|
120
|
+
if (fs.existsSync(path.join(dir, m)))
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
// .NET — any *.csproj
|
|
124
|
+
try {
|
|
125
|
+
const entries = fs.readdirSync(dir);
|
|
126
|
+
if (entries.some(e => e.endsWith('.csproj')))
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// unreadable dir → not a stack root
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
};
|
|
134
|
+
if (hasManifest(root)) {
|
|
135
|
+
return [{ path: root, origin: '.' }];
|
|
136
|
+
}
|
|
137
|
+
const skipDirs = new Set([
|
|
138
|
+
'node_modules', 'dist', 'build', '.git', '.next', '.cache',
|
|
139
|
+
'__pycache__', '.venv', 'venv', '.tox', 'target', 'vendor',
|
|
140
|
+
'coverage', '.pytest_cache', '.navgator', '.ibr', '.bookmark',
|
|
141
|
+
'.claude',
|
|
142
|
+
]);
|
|
143
|
+
let entries;
|
|
144
|
+
try {
|
|
145
|
+
entries = fs.readdirSync(root);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return [{ path: root, origin: '.' }];
|
|
149
|
+
}
|
|
150
|
+
const found = [];
|
|
151
|
+
for (const name of entries) {
|
|
152
|
+
if (name.startsWith('.'))
|
|
153
|
+
continue;
|
|
154
|
+
if (skipDirs.has(name))
|
|
155
|
+
continue;
|
|
156
|
+
const child = path.join(root, name);
|
|
157
|
+
let isDir = false;
|
|
158
|
+
try {
|
|
159
|
+
isDir = fs.statSync(child).isDirectory();
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!isDir)
|
|
165
|
+
continue;
|
|
166
|
+
if (hasManifest(child)) {
|
|
167
|
+
found.push({ path: child, origin: name });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (found.length === 0) {
|
|
171
|
+
// Nothing one level down either — keep legacy behavior so older
|
|
172
|
+
// projects don't silently turn into no-ops.
|
|
173
|
+
if (verbose) {
|
|
174
|
+
console.log(' - No stack manifest at root or any direct child; scanning root anyway');
|
|
175
|
+
}
|
|
176
|
+
return [{ path: root, origin: '.' }];
|
|
177
|
+
}
|
|
178
|
+
if (verbose) {
|
|
179
|
+
console.log(` - Multi-stack project: ${found.length} subroot(s): ` +
|
|
180
|
+
found.map(f => f.origin).join(', '));
|
|
181
|
+
}
|
|
182
|
+
return found;
|
|
183
|
+
}
|
|
184
|
+
// =============================================================================
|
|
185
|
+
// MODE SELECTION (Run 1 — D2)
|
|
186
|
+
// =============================================================================
|
|
187
|
+
/**
|
|
188
|
+
* Files whose presence in fileChanges forces a full scan because they alter
|
|
189
|
+
* the package/dependency graph in ways that ripple through every component.
|
|
190
|
+
*/
|
|
191
|
+
const FULL_SCAN_TRIGGER_FILES = new Set([
|
|
192
|
+
// Lockfiles / manifests — change the package graph
|
|
193
|
+
'package.json',
|
|
194
|
+
'package-lock.json',
|
|
195
|
+
'pnpm-lock.yaml',
|
|
196
|
+
'yarn.lock',
|
|
197
|
+
'pyproject.toml',
|
|
198
|
+
'requirements.txt',
|
|
199
|
+
'requirements-dev.txt',
|
|
200
|
+
'requirements-test.txt',
|
|
201
|
+
'prisma/schema.prisma',
|
|
202
|
+
'Package.swift',
|
|
203
|
+
'Package.resolved',
|
|
204
|
+
// Build / runtime config — change resolution, deploy targets, ignore rules
|
|
205
|
+
'tsconfig.json',
|
|
206
|
+
'vercel.json',
|
|
207
|
+
'fly.toml',
|
|
208
|
+
'railway.json',
|
|
209
|
+
'.gitignore',
|
|
210
|
+
]);
|
|
211
|
+
/** Days × ms in one day — used by stale-full check. */
|
|
212
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
213
|
+
/** Cap on consecutive incremental scans before forcing a full scan. */
|
|
214
|
+
const INCREMENTAL_CAP = 20;
|
|
215
|
+
/**
|
|
216
|
+
* Decide whether to run a full or incremental scan based on the requested
|
|
217
|
+
* mode, the prior index state, and the file changes since last scan.
|
|
218
|
+
*
|
|
219
|
+
* Pure function — no I/O. All inputs precomputed by the caller.
|
|
220
|
+
*
|
|
221
|
+
* Policy (for mode='auto'):
|
|
222
|
+
* 1. No prior index → full / no-prior-state
|
|
223
|
+
* 2. schema_version mismatch (and not 1.0.0 → 1.1.0 soft-upgrade) → full / schema-mismatch
|
|
224
|
+
* 3. Any FULL_SCAN_TRIGGER_FILES in changedFiles → full / manifest-changed
|
|
225
|
+
* 4. now − last_full_scan > 7 days → full / stale-full
|
|
226
|
+
* 5. incrementals_since_full ≥ 20 → full / incremental-cap
|
|
227
|
+
* 6. No file changes at all → noop case (caller handles); we still return
|
|
228
|
+
* 'incremental' here for the no-op flow.
|
|
229
|
+
* 7. Else → incremental / fast-path
|
|
230
|
+
*/
|
|
231
|
+
export function selectScanMode(fileChanges, index, options, now = Date.now()) {
|
|
232
|
+
const mode = options.mode ?? (options.clearFirst ? 'full' : options.incremental ? 'incremental' : 'auto');
|
|
233
|
+
if (mode === 'full') {
|
|
234
|
+
return { mode: 'full', reason: 'flag-full' };
|
|
235
|
+
}
|
|
236
|
+
if (mode === 'incremental') {
|
|
237
|
+
if (!index) {
|
|
238
|
+
return { mode: 'full', reason: 'no-prior-state' };
|
|
239
|
+
}
|
|
240
|
+
return { mode: 'incremental', reason: 'flag-incremental' };
|
|
241
|
+
}
|
|
242
|
+
// mode === 'auto'
|
|
243
|
+
if (!index) {
|
|
244
|
+
return { mode: 'full', reason: 'no-prior-state' };
|
|
245
|
+
}
|
|
246
|
+
// 1.0.0 → 1.1.0 is a soft upgrade (loadIndex injected defaults).
|
|
247
|
+
// Any other mismatch demands a full rebuild.
|
|
248
|
+
const sv = index.schema_version ?? '1.0.0';
|
|
249
|
+
if (sv !== '1.0.0' && sv !== SCHEMA_VERSION) {
|
|
250
|
+
return { mode: 'full', reason: 'schema-mismatch' };
|
|
251
|
+
}
|
|
252
|
+
// Run 2 — D5: prior scan's audit detected EWMA drift breach.
|
|
253
|
+
// Force a full + Cochran audit pass on this run.
|
|
254
|
+
if (index.pending_drift_breach) {
|
|
255
|
+
return { mode: 'full', reason: 'audit-drift-breach' };
|
|
256
|
+
}
|
|
257
|
+
const changed = new Set();
|
|
258
|
+
if (fileChanges) {
|
|
259
|
+
for (const f of fileChanges.added)
|
|
260
|
+
changed.add(f);
|
|
261
|
+
for (const f of fileChanges.modified)
|
|
262
|
+
changed.add(f);
|
|
263
|
+
for (const f of fileChanges.removed)
|
|
264
|
+
changed.add(f);
|
|
265
|
+
}
|
|
266
|
+
for (const trigger of FULL_SCAN_TRIGGER_FILES) {
|
|
267
|
+
if (changed.has(trigger)) {
|
|
268
|
+
return { mode: 'full', reason: 'manifest-changed' };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// New files have no recorded reverse-dep edges yet, so an incremental walk-set
|
|
272
|
+
// can't find their importers. Cleaner to force a full scan than gymnastics
|
|
273
|
+
// (Run 1.6 — item #5).
|
|
274
|
+
if (fileChanges && fileChanges.added.length > 0) {
|
|
275
|
+
return { mode: 'full', reason: 'new-files' };
|
|
276
|
+
}
|
|
277
|
+
const lastFull = index.last_full_scan ?? 0;
|
|
278
|
+
if (lastFull > 0 && now - lastFull > SEVEN_DAYS_MS) {
|
|
279
|
+
return { mode: 'full', reason: 'stale-full' };
|
|
280
|
+
}
|
|
281
|
+
const incCount = index.incrementals_since_full ?? 0;
|
|
282
|
+
if (incCount >= INCREMENTAL_CAP) {
|
|
283
|
+
return { mode: 'full', reason: 'incremental-cap' };
|
|
284
|
+
}
|
|
285
|
+
if (changed.size === 0) {
|
|
286
|
+
return { mode: 'incremental', reason: 'no-changes' };
|
|
287
|
+
}
|
|
288
|
+
return { mode: 'incremental', reason: 'fast-path' };
|
|
289
|
+
}
|
|
17
290
|
// =============================================================================
|
|
18
291
|
// MAIN SCANNER
|
|
19
292
|
// =============================================================================
|
|
@@ -24,25 +297,105 @@ export async function scan(projectRoot, options = {}) {
|
|
|
24
297
|
const startTime = Date.now();
|
|
25
298
|
const root = projectRoot || process.cwd();
|
|
26
299
|
const config = getConfig();
|
|
300
|
+
// R6 footprint fix: CLI/programmatic option overrides config flag.
|
|
301
|
+
if (options.perEntityFiles !== undefined) {
|
|
302
|
+
config.perEntityFiles = options.perEntityFiles;
|
|
303
|
+
}
|
|
304
|
+
// Sandbox mode: restrict scan behavior
|
|
305
|
+
if (isSandboxMode()) {
|
|
306
|
+
options.quick = true;
|
|
307
|
+
options.prompts = false;
|
|
308
|
+
options.useAST = false;
|
|
309
|
+
}
|
|
310
|
+
// Opt-in branch tracking
|
|
311
|
+
let gitInfo;
|
|
312
|
+
if (options.trackBranch) {
|
|
313
|
+
const info = await getGitInfo(root);
|
|
314
|
+
if (info) {
|
|
315
|
+
gitInfo = info;
|
|
316
|
+
if (options.verbose) {
|
|
317
|
+
console.log(`Branch tracking: ${info.branch} @ ${info.commit}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
27
321
|
if (options.verbose) {
|
|
28
322
|
console.log(`Scanning project: ${root}`);
|
|
29
323
|
}
|
|
30
|
-
//
|
|
31
|
-
if (options.clearFirst) {
|
|
32
|
-
await clearStorage(config, root);
|
|
33
|
-
}
|
|
34
|
-
// Ensure storage directories exist
|
|
324
|
+
// Ensure storage directories exist BEFORE we look at any prior state.
|
|
35
325
|
ensureStorageDirectories(config, root);
|
|
36
326
|
// ==========================================================================
|
|
37
|
-
// Phase 0:
|
|
327
|
+
// Phase 0.0: Concurrency lock (Run 1.6 — item #4)
|
|
38
328
|
// ==========================================================================
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
329
|
+
// Prevent two `navgator scan` processes corrupting each other's
|
|
330
|
+
// .navgator/architecture/ output. Stale locks (>10 min OR pid gone)
|
|
331
|
+
// auto-clear. Live contention exits cleanly with code 0.
|
|
332
|
+
const storeDir = getStoragePath(config, root);
|
|
333
|
+
const requestedScanType = options.mode ?? (options.clearFirst ? 'full' : options.incremental ? 'incremental' : 'auto');
|
|
334
|
+
const lock = acquireLock(storeDir, requestedScanType);
|
|
335
|
+
if (!lock.ok) {
|
|
336
|
+
console.log(lock.message);
|
|
337
|
+
const duration = Date.now() - startTime;
|
|
338
|
+
return {
|
|
339
|
+
components: [],
|
|
340
|
+
connections: [],
|
|
341
|
+
warnings: [],
|
|
342
|
+
stats: {
|
|
343
|
+
scan_duration_ms: duration,
|
|
344
|
+
components_found: 0,
|
|
345
|
+
connections_found: 0,
|
|
346
|
+
warnings_count: 0,
|
|
347
|
+
files_scanned: 0,
|
|
348
|
+
files_changed: 0,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
// ==========================================================================
|
|
354
|
+
// Phase 0: File Discovery & Change Detection
|
|
355
|
+
// ==========================================================================
|
|
356
|
+
const sourceFiles = await glob('**/*.{ts,tsx,js,jsx,mjs,cjs,py,swift,h,m}', {
|
|
357
|
+
cwd: root,
|
|
358
|
+
ignore: getIgnorePatterns(root),
|
|
359
|
+
});
|
|
360
|
+
// For change detection, also include manifest files at the project root
|
|
361
|
+
// (and a few well-known nested ones). selectScanMode consults these to
|
|
362
|
+
// decide whether to force a full scan. Manifests are NOT scanned by the
|
|
363
|
+
// per-language scanners — they're tracked here only for change detection.
|
|
364
|
+
const manifestPatterns = [
|
|
365
|
+
'package.json',
|
|
366
|
+
'package-lock.json',
|
|
367
|
+
'pnpm-lock.yaml',
|
|
368
|
+
'yarn.lock',
|
|
369
|
+
'pyproject.toml',
|
|
370
|
+
'requirements.txt',
|
|
371
|
+
'requirements-dev.txt',
|
|
372
|
+
'requirements-test.txt',
|
|
373
|
+
'prisma/schema.prisma',
|
|
374
|
+
'Package.swift',
|
|
375
|
+
'Package.resolved',
|
|
376
|
+
// Build / runtime config — track so changes trigger full scan
|
|
377
|
+
'tsconfig.json',
|
|
378
|
+
'vercel.json',
|
|
379
|
+
'fly.toml',
|
|
380
|
+
'railway.json',
|
|
381
|
+
'.gitignore',
|
|
382
|
+
];
|
|
383
|
+
const manifestFiles = [];
|
|
384
|
+
for (const m of manifestPatterns) {
|
|
385
|
+
try {
|
|
386
|
+
const fs = await import('node:fs');
|
|
387
|
+
if (fs.existsSync(path.join(root, m)))
|
|
388
|
+
manifestFiles.push(m);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// ignore
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const filesForChangeDetection = [...sourceFiles, ...manifestFiles];
|
|
395
|
+
// Detect file changes using prior hashes BEFORE any clearing.
|
|
396
|
+
// (Used by mode selection AND timeline summary even on full scans.)
|
|
397
|
+
let fileChanges;
|
|
398
|
+
fileChanges = await detectFileChanges(filesForChangeDetection, root, config);
|
|
46
399
|
if (options.verbose) {
|
|
47
400
|
console.log(`File changes: ${formatFileChangeSummary(fileChanges)}`);
|
|
48
401
|
if (fileChanges.added.length > 0 && fileChanges.added.length <= 5) {
|
|
@@ -52,272 +405,1314 @@ export async function scan(projectRoot, options = {}) {
|
|
|
52
405
|
console.log(` Modified: ${fileChanges.modified.join(', ')}`);
|
|
53
406
|
}
|
|
54
407
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
408
|
+
// ==========================================================================
|
|
409
|
+
// Phase 0.5: Scan-mode selection (Run 1 — D2)
|
|
410
|
+
// ==========================================================================
|
|
411
|
+
const priorIndex = await loadIndex(config, root);
|
|
412
|
+
const decision = selectScanMode(fileChanges, priorIndex, options);
|
|
413
|
+
// scanType captures the mode the scan ACTUALLY ran in (after potential
|
|
414
|
+
// integrity-check promotion). Initialized to the decision; may be promoted
|
|
415
|
+
// to 'incremental→full' below.
|
|
416
|
+
//
|
|
417
|
+
// Run 1.7 — Problem A: when this scan is the recursive re-entry from a
|
|
418
|
+
// failed integrity check (`_promotedFromIncremental === true`), `decision.mode`
|
|
419
|
+
// is 'full' (clearFirst forces it), but the user-visible scan_type should
|
|
420
|
+
// remain 'incremental→full' so timeline + stats consumers see the promotion
|
|
421
|
+
// evidence (Run 1.6 #3 contract). The actual scan body still runs as full.
|
|
422
|
+
let scanType = options._promotedFromIncremental ? 'incremental→full' : decision.mode;
|
|
423
|
+
if (options.verbose) {
|
|
424
|
+
console.log(`Scan mode: ${decision.mode} (${decision.reason})`);
|
|
425
|
+
}
|
|
426
|
+
// Compute walk-set for incremental: changedFiles ∪ reverseDeps
|
|
427
|
+
const changedSet = new Set();
|
|
428
|
+
if (fileChanges) {
|
|
429
|
+
for (const f of fileChanges.added)
|
|
430
|
+
changedSet.add(f);
|
|
431
|
+
for (const f of fileChanges.modified)
|
|
432
|
+
changedSet.add(f);
|
|
433
|
+
for (const f of fileChanges.removed)
|
|
434
|
+
changedSet.add(f);
|
|
435
|
+
}
|
|
436
|
+
let walkSet = new Set(changedSet);
|
|
437
|
+
if (decision.mode === 'incremental' && changedSet.size > 0) {
|
|
438
|
+
const reverseDeps = await loadReverseDeps(changedSet, config, root);
|
|
439
|
+
for (const f of reverseDeps)
|
|
440
|
+
walkSet.add(f);
|
|
441
|
+
if (options.verbose) {
|
|
442
|
+
console.log(` Walk-set: ${changedSet.size} changed + ${reverseDeps.size} reverse-deps = ${walkSet.size} files`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Pass walkSet to scanners only on incremental. Full scans pass undefined to
|
|
446
|
+
// preserve bit-identical output (regression-locked by characterization snapshot).
|
|
447
|
+
const incWalkSet = decision.mode === 'incremental' ? walkSet : undefined;
|
|
448
|
+
// ==========================================================================
|
|
449
|
+
// Phase 0.6: Noop short-circuit (incremental + zero changes)
|
|
450
|
+
// ==========================================================================
|
|
451
|
+
if (decision.mode === 'incremental' && decision.reason === 'no-changes') {
|
|
452
|
+
// Nothing changed since last scan. Bump last_scan, update incrementals_since_full
|
|
453
|
+
// to 0 (no incremental work was done — but keep it as-is to honor the cap).
|
|
454
|
+
// Save fresh hashes (idempotent), update index timestamp, record noop timeline entry.
|
|
455
|
+
if (priorIndex) {
|
|
456
|
+
priorIndex.last_scan = Date.now();
|
|
457
|
+
// Note: incrementals_since_full and last_full_scan unchanged on noop.
|
|
458
|
+
await atomicWriteJSON(getIndexPath(config, root), priorIndex);
|
|
459
|
+
}
|
|
460
|
+
const fileHashes = await computeFileHashes(filesForChangeDetection, root);
|
|
461
|
+
await saveHashes(fileHashes, config, root);
|
|
462
|
+
const noopTimelineEntry = {
|
|
463
|
+
id: generateTimelineId(),
|
|
464
|
+
timestamp: Date.now(),
|
|
465
|
+
significance: 'patch',
|
|
466
|
+
triggers: [],
|
|
467
|
+
diff: {
|
|
468
|
+
components: { added: [], removed: [], modified: [] },
|
|
469
|
+
connections: { added: [], removed: [] },
|
|
470
|
+
stats: {
|
|
471
|
+
total_changes: 0,
|
|
472
|
+
components_before: priorIndex?.stats.total_components ?? 0,
|
|
473
|
+
components_after: priorIndex?.stats.total_components ?? 0,
|
|
474
|
+
connections_before: priorIndex?.stats.total_connections ?? 0,
|
|
475
|
+
connections_after: priorIndex?.stats.total_connections ?? 0,
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
git: gitInfo,
|
|
479
|
+
scan_type: 'noop',
|
|
480
|
+
files_scanned: 0,
|
|
481
|
+
};
|
|
482
|
+
await saveTimelineEntry(noopTimelineEntry, config, root);
|
|
483
|
+
// Load existing components/connections so callers see the unchanged graph.
|
|
484
|
+
const existingComponents = await loadAllComponents(config, root);
|
|
485
|
+
const existingConnections = await loadAllConnections(config, root);
|
|
486
|
+
const duration = Date.now() - startTime;
|
|
487
|
+
if (options.verbose) {
|
|
488
|
+
console.log(`Scan complete (noop) in ${duration}ms`);
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
components: existingComponents,
|
|
492
|
+
connections: existingConnections,
|
|
493
|
+
warnings: [],
|
|
494
|
+
fileChanges,
|
|
495
|
+
timelineEntry: noopTimelineEntry,
|
|
496
|
+
gitInfo,
|
|
497
|
+
stats: {
|
|
498
|
+
scan_duration_ms: duration,
|
|
499
|
+
components_found: existingComponents.length,
|
|
500
|
+
connections_found: existingConnections.length,
|
|
501
|
+
warnings_count: 0,
|
|
502
|
+
files_scanned: 0,
|
|
503
|
+
files_changed: 0,
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
// For full scans: clear ALL prior data up front (legacy clearFirst semantics).
|
|
508
|
+
// For incremental: defer to Phase 4 (clearForFiles + merge).
|
|
509
|
+
if (decision.mode === 'full' || options.clearFirst) {
|
|
510
|
+
await clearStorage(config, root);
|
|
511
|
+
ensureStorageDirectories(config, root);
|
|
512
|
+
}
|
|
513
|
+
const allComponents = [];
|
|
514
|
+
const allConnections = [];
|
|
515
|
+
const allWarnings = [];
|
|
516
|
+
let promptScanResultHolder;
|
|
517
|
+
let projectMetadata;
|
|
518
|
+
// ==========================================================================
|
|
519
|
+
// Phase 1: Package Detection
|
|
520
|
+
// ==========================================================================
|
|
104
521
|
if (options.verbose) {
|
|
105
|
-
console.log('Phase
|
|
522
|
+
console.log('Phase 1: Scanning packages...');
|
|
106
523
|
}
|
|
107
|
-
|
|
108
|
-
|
|
524
|
+
// Package scanners run in parallel (independent of each other).
|
|
525
|
+
//
|
|
526
|
+
// Multi-stack auto-discovery: if the project root has no stack manifest
|
|
527
|
+
// of its own, walk one level deep and scan each subdir that does. This
|
|
528
|
+
// catches the common monorepo-lite shape — a top-level `frontend/` with
|
|
529
|
+
// package.json + a top-level `backend/` with pyproject.toml — that the
|
|
530
|
+
// legacy single-root behavior silently missed (it would only scan
|
|
531
|
+
// whichever side was at the root).
|
|
532
|
+
//
|
|
533
|
+
// Pass `singleStack: true` (CLI: --single-stack) to force the legacy
|
|
534
|
+
// behavior. Each component scanned from a subroot gets its origin tagged
|
|
535
|
+
// via `metadata.origin_root` so downstream layers can group by stack.
|
|
536
|
+
{
|
|
537
|
+
const stackRoots = options.singleStack
|
|
538
|
+
? [{ path: root, origin: '.' }]
|
|
539
|
+
: discoverStackRoots(root, options.verbose === true);
|
|
540
|
+
const packageTasks = [];
|
|
541
|
+
for (const sr of stackRoots) {
|
|
542
|
+
const tagOrigin = (result) => {
|
|
543
|
+
// Skip when origin is the root — keeps single-stack output identical.
|
|
544
|
+
if (sr.origin === '.')
|
|
545
|
+
return;
|
|
546
|
+
for (const c of result.components) {
|
|
547
|
+
const md = c.metadata ?? {};
|
|
548
|
+
c.metadata = { ...md, origin_root: sr.origin };
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
if (detectNpm(sr.path)) {
|
|
552
|
+
if (options.verbose) {
|
|
553
|
+
console.log(` - Detected npm/yarn/pnpm project (${sr.origin})`);
|
|
554
|
+
}
|
|
555
|
+
packageTasks.push(scanNpmPackages(sr.path).then(result => {
|
|
556
|
+
tagOrigin(result);
|
|
557
|
+
allComponents.push(...result.components);
|
|
558
|
+
allWarnings.push(...result.warnings);
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
if (detectPip(sr.path)) {
|
|
562
|
+
if (options.verbose) {
|
|
563
|
+
console.log(` - Detected Python project (${sr.origin})`);
|
|
564
|
+
}
|
|
565
|
+
packageTasks.push(scanPipPackages(sr.path).then(result => {
|
|
566
|
+
tagOrigin(result);
|
|
567
|
+
allComponents.push(...result.components);
|
|
568
|
+
allWarnings.push(...result.warnings);
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
if (detectSpm(sr.path)) {
|
|
572
|
+
if (options.verbose) {
|
|
573
|
+
console.log(` - Detected Swift/Xcode project (${sr.origin})`);
|
|
574
|
+
}
|
|
575
|
+
packageTasks.push(scanSpmPackages(sr.path).then(result => {
|
|
576
|
+
tagOrigin(result);
|
|
577
|
+
allComponents.push(...result.components);
|
|
578
|
+
allWarnings.push(...result.warnings);
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
await Promise.all(packageTasks);
|
|
583
|
+
}
|
|
584
|
+
// ==========================================================================
|
|
585
|
+
// Phase 2: Infrastructure Detection
|
|
586
|
+
// ==========================================================================
|
|
587
|
+
if (options.verbose) {
|
|
588
|
+
console.log('Phase 2: Scanning infrastructure...');
|
|
589
|
+
}
|
|
590
|
+
const infraResult = await scanInfrastructure(root);
|
|
591
|
+
allComponents.push(...infraResult.components);
|
|
592
|
+
allWarnings.push(...infraResult.warnings);
|
|
593
|
+
// Prisma schema → database models + relations
|
|
594
|
+
if (detectPrisma(root)) {
|
|
109
595
|
if (options.verbose)
|
|
110
|
-
console.log(' -
|
|
596
|
+
console.log(' - Detected Prisma schema');
|
|
111
597
|
try {
|
|
112
|
-
const
|
|
113
|
-
allComponents.push(...
|
|
114
|
-
allConnections.push(...
|
|
115
|
-
allWarnings.push(...
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const dbResult = await scanDatabaseOperations(root);
|
|
120
|
-
allComponents.push(...dbResult.components);
|
|
121
|
-
allConnections.push(...dbResult.connections);
|
|
122
|
-
allWarnings.push(...dbResult.warnings);
|
|
598
|
+
const prismaResult = await scanPrismaSchema(root);
|
|
599
|
+
allComponents.push(...prismaResult.components);
|
|
600
|
+
allConnections.push(...prismaResult.connections);
|
|
601
|
+
allWarnings.push(...prismaResult.warnings);
|
|
602
|
+
if (options.verbose) {
|
|
603
|
+
console.log(` Models: ${prismaResult.components.length}, Relations: ${prismaResult.connections.length}`);
|
|
604
|
+
}
|
|
123
605
|
}
|
|
124
606
|
catch (error) {
|
|
125
607
|
allWarnings.push({
|
|
126
608
|
type: 'parse_error',
|
|
127
|
-
message: `
|
|
609
|
+
message: `Prisma scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
128
610
|
});
|
|
129
|
-
// Fall back to regex scanning
|
|
130
|
-
if (options.verbose)
|
|
131
|
-
console.log(' - Falling back to regex scanning...');
|
|
132
|
-
const serviceResult = await scanServiceCalls(root);
|
|
133
|
-
allComponents.push(...serviceResult.components);
|
|
134
|
-
allConnections.push(...serviceResult.connections);
|
|
135
|
-
allWarnings.push(...serviceResult.warnings);
|
|
136
611
|
}
|
|
137
612
|
}
|
|
138
|
-
|
|
139
|
-
|
|
613
|
+
// DB field usage analyzer (opt-in via FEATURE FLAG: fieldUsage)
|
|
614
|
+
let fieldUsageReportResult;
|
|
615
|
+
if (options.fieldUsage && canAnalyzeFieldUsage(root)) {
|
|
140
616
|
if (options.verbose)
|
|
141
|
-
console.log(' -
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
617
|
+
console.log(' - Analyzing DB field usage...');
|
|
618
|
+
try {
|
|
619
|
+
const fieldResult = await scanFieldUsage(root, incWalkSet);
|
|
620
|
+
allComponents.push(...fieldResult.components);
|
|
621
|
+
allConnections.push(...fieldResult.connections);
|
|
622
|
+
allWarnings.push(...fieldResult.warnings);
|
|
623
|
+
fieldUsageReportResult = fieldResult.report;
|
|
624
|
+
if (options.verbose && fieldResult.report) {
|
|
625
|
+
const r = fieldResult.report;
|
|
626
|
+
console.log(` Fields: ${r.totalFields} total, ${r.unusedFields} unused, ${r.writeOnlyFields} write-only`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
allWarnings.push({
|
|
631
|
+
type: 'parse_error',
|
|
632
|
+
message: `Field usage analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// TypeSpec validator (opt-in via FEATURE FLAG: typeSpec)
|
|
637
|
+
let typeSpecReportResult;
|
|
638
|
+
if (options.typeSpec && canValidateTypeSpec(root)) {
|
|
149
639
|
if (options.verbose)
|
|
150
|
-
console.log(' -
|
|
640
|
+
console.log(' - Validating TypeSpec (Prisma vs TS interfaces)...');
|
|
151
641
|
try {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
642
|
+
const tsResult = await scanTypeSpecValidation(root);
|
|
643
|
+
allWarnings.push(...tsResult.warnings);
|
|
644
|
+
typeSpecReportResult = tsResult.report;
|
|
645
|
+
if (options.verbose && tsResult.report) {
|
|
646
|
+
const r = tsResult.report;
|
|
647
|
+
console.log(` Interfaces: ${r.modelsWithInterfaces}/${r.modelsChecked} matched, ${r.totalMismatches} mismatches`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
allWarnings.push({
|
|
652
|
+
type: 'parse_error',
|
|
653
|
+
message: `TypeSpec validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// Env, queues, and crons are independent — run in parallel
|
|
658
|
+
{
|
|
659
|
+
const infraTasks = [];
|
|
660
|
+
if (detectEnvFiles(root)) {
|
|
661
|
+
if (options.verbose)
|
|
662
|
+
console.log(' - Detected environment files');
|
|
663
|
+
infraTasks.push(scanEnvVars(root, incWalkSet).then(envResult => {
|
|
664
|
+
allComponents.push(...envResult.components);
|
|
665
|
+
allConnections.push(...envResult.connections);
|
|
666
|
+
allWarnings.push(...envResult.warnings);
|
|
667
|
+
if (options.verbose) {
|
|
668
|
+
console.log(` Env vars: ${envResult.components.length}, References: ${envResult.connections.length}`);
|
|
161
669
|
}
|
|
162
|
-
|
|
163
|
-
|
|
670
|
+
}).catch(error => {
|
|
671
|
+
allWarnings.push({
|
|
672
|
+
type: 'parse_error',
|
|
673
|
+
message: `Env scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
674
|
+
});
|
|
675
|
+
}));
|
|
676
|
+
}
|
|
677
|
+
if (detectQueues(root)) {
|
|
678
|
+
if (options.verbose)
|
|
679
|
+
console.log(' - Detected queue system');
|
|
680
|
+
infraTasks.push(scanQueues(root, incWalkSet).then(queueResult => {
|
|
681
|
+
allComponents.push(...queueResult.components);
|
|
682
|
+
allConnections.push(...queueResult.connections);
|
|
683
|
+
allWarnings.push(...queueResult.warnings);
|
|
684
|
+
if (options.verbose) {
|
|
685
|
+
console.log(` Queues: ${queueResult.components.length}, Connections: ${queueResult.connections.length}`);
|
|
164
686
|
}
|
|
687
|
+
}).catch(error => {
|
|
688
|
+
allWarnings.push({
|
|
689
|
+
type: 'parse_error',
|
|
690
|
+
message: `Queue scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
691
|
+
});
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
if (detectCrons(root)) {
|
|
695
|
+
if (options.verbose)
|
|
696
|
+
console.log(' - Detected cron jobs');
|
|
697
|
+
infraTasks.push(scanCronJobs(root, incWalkSet).then(cronResult => {
|
|
698
|
+
allComponents.push(...cronResult.components);
|
|
699
|
+
allConnections.push(...cronResult.connections);
|
|
700
|
+
allWarnings.push(...cronResult.warnings);
|
|
701
|
+
if (options.verbose) {
|
|
702
|
+
console.log(` Cron jobs: ${cronResult.components.length}, Route connections: ${cronResult.connections.length}`);
|
|
703
|
+
}
|
|
704
|
+
}).catch(error => {
|
|
705
|
+
allWarnings.push({
|
|
706
|
+
type: 'parse_error',
|
|
707
|
+
message: `Cron scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
708
|
+
});
|
|
709
|
+
}));
|
|
710
|
+
}
|
|
711
|
+
await Promise.all(infraTasks);
|
|
712
|
+
}
|
|
713
|
+
// Deployment config → detailed infra metadata
|
|
714
|
+
if (options.verbose)
|
|
715
|
+
console.log(' - Scanning deployment config...');
|
|
716
|
+
try {
|
|
717
|
+
const deployResult = await scanDeployConfig(root);
|
|
718
|
+
allComponents.push(...deployResult.components);
|
|
719
|
+
allConnections.push(...deployResult.connections);
|
|
720
|
+
allWarnings.push(...deployResult.warnings);
|
|
721
|
+
if (options.verbose && deployResult.components.length > 0) {
|
|
722
|
+
console.log(` Deploy configs: ${deployResult.components.length}, Entry points: ${deployResult.connections.length}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
allWarnings.push({
|
|
727
|
+
type: 'parse_error',
|
|
728
|
+
message: `Deploy config scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
// Prisma call detection: map source files to database models they query
|
|
732
|
+
const prismaModelComps = allComponents.filter(c => c.type === 'database' && c.tags?.includes('prisma'));
|
|
733
|
+
if (prismaModelComps.length > 0) {
|
|
734
|
+
if (options.verbose)
|
|
735
|
+
console.log(' - Scanning Prisma client calls...');
|
|
736
|
+
try {
|
|
737
|
+
const prismaCallResult = await scanPrismaCalls(root, prismaModelComps, incWalkSet);
|
|
738
|
+
allConnections.push(...prismaCallResult.connections);
|
|
739
|
+
if (options.verbose && prismaCallResult.connections.length > 0) {
|
|
740
|
+
const uniqueModels = new Set(prismaCallResult.connections.map(c => c.description?.split(' queries ')[1]?.split(' ')[0]));
|
|
741
|
+
console.log(` DB queries: ${prismaCallResult.connections.length} file→model connections across ${uniqueModels.size} models`);
|
|
165
742
|
}
|
|
166
743
|
}
|
|
167
744
|
catch (error) {
|
|
168
745
|
allWarnings.push({
|
|
169
746
|
type: 'parse_error',
|
|
170
|
-
message: `
|
|
747
|
+
message: `Prisma call scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
171
748
|
});
|
|
172
749
|
}
|
|
173
750
|
}
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
751
|
+
// ==========================================================================
|
|
752
|
+
// Phase 3: Connection Detection (unless quick mode)
|
|
753
|
+
// ==========================================================================
|
|
754
|
+
if (!options.quick || options.connections) {
|
|
755
|
+
if (options.verbose) {
|
|
756
|
+
console.log('Phase 3: Scanning connections...');
|
|
757
|
+
}
|
|
758
|
+
if (options.useAST) {
|
|
759
|
+
// AST-based scanning (more accurate)
|
|
760
|
+
if (options.verbose)
|
|
761
|
+
console.log(' - Running AST analysis (ts-morph)...');
|
|
762
|
+
try {
|
|
763
|
+
const astResult = await scanWithAST(root, incWalkSet);
|
|
764
|
+
allComponents.push(...astResult.components);
|
|
765
|
+
allConnections.push(...astResult.connections);
|
|
766
|
+
allWarnings.push(...astResult.warnings);
|
|
767
|
+
// Also scan for database operations
|
|
768
|
+
if (options.verbose)
|
|
769
|
+
console.log(' - Scanning database operations...');
|
|
770
|
+
const dbResult = await scanDatabaseOperations(root, incWalkSet);
|
|
771
|
+
allComponents.push(...dbResult.components);
|
|
772
|
+
allConnections.push(...dbResult.connections);
|
|
773
|
+
allWarnings.push(...dbResult.warnings);
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
allWarnings.push({
|
|
777
|
+
type: 'parse_error',
|
|
778
|
+
message: `AST scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
779
|
+
});
|
|
780
|
+
// Fall back to regex scanning
|
|
781
|
+
if (options.verbose)
|
|
782
|
+
console.log(' - Falling back to regex scanning...');
|
|
783
|
+
const serviceResult = await scanServiceCalls(root, incWalkSet);
|
|
784
|
+
allComponents.push(...serviceResult.components);
|
|
785
|
+
allConnections.push(...serviceResult.connections);
|
|
786
|
+
allWarnings.push(...serviceResult.warnings);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
// Regex-based scanning (faster but less accurate)
|
|
791
|
+
if (options.verbose)
|
|
792
|
+
console.log(' - Scanning service calls (regex)...');
|
|
793
|
+
const serviceResult = await scanServiceCalls(root, incWalkSet);
|
|
794
|
+
allComponents.push(...serviceResult.components);
|
|
795
|
+
allConnections.push(...serviceResult.connections);
|
|
796
|
+
allWarnings.push(...serviceResult.warnings);
|
|
797
|
+
}
|
|
798
|
+
// File-level import graph (TS/JS local imports)
|
|
177
799
|
if (options.verbose)
|
|
178
|
-
console.log(' -
|
|
179
|
-
let traceResult;
|
|
800
|
+
console.log(' - Scanning file imports...');
|
|
180
801
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
802
|
+
// Collect npm package components so bare imports (`import X from "react"`)
|
|
803
|
+
// can be resolved to the package component and emitted as `uses-package`
|
|
804
|
+
// edges. Use config_files filter instead of type filter: packages can be
|
|
805
|
+
// classified as 'npm' | 'framework' | 'database' | 'service' depending
|
|
806
|
+
// on FRAMEWORK_SIGNATURES, but all originate from a package.json.
|
|
807
|
+
const knownPackages = allComponents
|
|
808
|
+
.filter(c => c.source.config_files?.some(f => f === 'package.json' || f.endsWith('/package.json')))
|
|
809
|
+
.map(c => ({ name: c.name, component_id: c.component_id }));
|
|
810
|
+
// In incremental mode, restrict the import scan to walk-set files. Falls
|
|
811
|
+
// back to the full sourceFiles list (bit-identical) on full scans.
|
|
812
|
+
const importSourceFiles = incWalkSet
|
|
813
|
+
? sourceFiles.filter(f => incWalkSet.has(f))
|
|
814
|
+
: sourceFiles;
|
|
815
|
+
const importResult = await scanImports(root, importSourceFiles, knownPackages);
|
|
816
|
+
allComponents.push(...importResult.components);
|
|
817
|
+
allConnections.push(...importResult.connections);
|
|
184
818
|
if (options.verbose) {
|
|
185
|
-
|
|
186
|
-
console.log(`
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
819
|
+
const usesPkgCount = importResult.connections.filter(c => c.connection_type === 'uses-package').length;
|
|
820
|
+
console.log(` Found ${importResult.components.length} internal modules, ${importResult.connections.length} file-level imports (${usesPkgCount} uses-package)`);
|
|
821
|
+
}
|
|
822
|
+
// SCIP overlay (T11): when --scip / NAVGATOR_SCIP=1, run the
|
|
823
|
+
// compiler-accurate indexer and ADD any cross-file edges the regex
|
|
824
|
+
// import-scanner missed (re-exports, dynamic imports, type-only refs,
|
|
825
|
+
// etc.). Existing edges from the regex pass are preserved as-is so
|
|
826
|
+
// the characterization snapshots stay stable for non-SCIP runs.
|
|
827
|
+
const scipEnabled = process.env['NAVGATOR_SCIP'] === '1' || options.scip === true;
|
|
828
|
+
if (scipEnabled) {
|
|
829
|
+
try {
|
|
830
|
+
const { runScip, crossFileEdges, hasTsConfig } = await import('./parsers/scip-runner.js');
|
|
831
|
+
if (!hasTsConfig(root)) {
|
|
832
|
+
if (options.verbose)
|
|
833
|
+
console.log(' SCIP requested but no tsconfig.json — skipping');
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
if (options.verbose)
|
|
837
|
+
console.log(' - Running SCIP indexer (compiler-accurate)...');
|
|
838
|
+
const scipResult = await runScip(root, { timeoutMs: 60_000 });
|
|
839
|
+
if (!scipResult.ok) {
|
|
840
|
+
allWarnings.push({
|
|
841
|
+
type: 'parse_error',
|
|
842
|
+
message: `SCIP indexer failed: ${scipResult.error}`,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
const cross = crossFileEdges(scipResult.edges);
|
|
847
|
+
const fileToComponentId = new Map();
|
|
848
|
+
for (const c of importResult.components) {
|
|
849
|
+
const f = c.source?.config_files?.[0];
|
|
850
|
+
if (f)
|
|
851
|
+
fileToComponentId.set(f, c.component_id);
|
|
852
|
+
}
|
|
853
|
+
const existing = new Set(importResult.connections
|
|
854
|
+
.filter((c) => c.connection_type === 'imports')
|
|
855
|
+
.map((c) => `${c.from?.location?.file ?? ''}→${c.code_reference?.file ?? ''}`));
|
|
856
|
+
let added = 0;
|
|
857
|
+
const now = Date.now();
|
|
858
|
+
for (const e of cross) {
|
|
859
|
+
const fromId = fileToComponentId.get(e.from_file);
|
|
860
|
+
const toId = fileToComponentId.get(e.to_file ?? '');
|
|
861
|
+
if (!fromId || !toId)
|
|
862
|
+
continue;
|
|
863
|
+
const key = `${e.from_file}→${e.to_file}`;
|
|
864
|
+
if (existing.has(key))
|
|
865
|
+
continue;
|
|
866
|
+
existing.add(key);
|
|
867
|
+
allConnections.push({
|
|
868
|
+
connection_id: `CONN_imports_scip_${Math.random().toString(36).slice(2, 10)}`,
|
|
869
|
+
from: {
|
|
870
|
+
component_id: fromId,
|
|
871
|
+
location: { file: e.from_file, line: e.from_line + 1 },
|
|
872
|
+
},
|
|
873
|
+
to: { component_id: toId },
|
|
874
|
+
connection_type: 'imports',
|
|
875
|
+
code_reference: {
|
|
876
|
+
file: e.from_file,
|
|
877
|
+
symbol: e.display_name || e.symbol.split('/').pop()?.slice(0, 40) || 'scip-ref',
|
|
878
|
+
symbol_type: e.is_definition ? 'export' : 'import',
|
|
879
|
+
line_start: e.from_line + 1,
|
|
880
|
+
},
|
|
881
|
+
description: 'SCIP-resolved cross-file reference',
|
|
882
|
+
detected_from: 'scip-typescript',
|
|
883
|
+
confidence: 0.99,
|
|
884
|
+
timestamp: now,
|
|
885
|
+
last_verified: now,
|
|
886
|
+
});
|
|
887
|
+
added++;
|
|
888
|
+
}
|
|
889
|
+
if (options.verbose) {
|
|
890
|
+
console.log(` SCIP added ${added} cross-file edges (${scipResult.duration_ms}ms, ${scipResult.documents_indexed} docs)`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
191
894
|
}
|
|
192
|
-
|
|
193
|
-
|
|
895
|
+
catch (err) {
|
|
896
|
+
allWarnings.push({
|
|
897
|
+
type: 'parse_error',
|
|
898
|
+
message: `SCIP overlay failed: ${err.message}`,
|
|
899
|
+
});
|
|
194
900
|
}
|
|
195
901
|
}
|
|
196
902
|
}
|
|
197
903
|
catch (error) {
|
|
198
904
|
allWarnings.push({
|
|
199
905
|
type: 'parse_error',
|
|
200
|
-
message: `
|
|
906
|
+
message: `Import scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
201
907
|
});
|
|
908
|
+
}
|
|
909
|
+
// Swift code analysis (runtime deps, protocols, state, LLM calls)
|
|
910
|
+
if (detectSpm(root)) {
|
|
202
911
|
if (options.verbose)
|
|
203
|
-
console.log(
|
|
912
|
+
console.log(' - Scanning Swift code connections...');
|
|
913
|
+
try {
|
|
914
|
+
const swiftResult = await scanSwiftCode(root, incWalkSet);
|
|
915
|
+
allComponents.push(...swiftResult.components);
|
|
916
|
+
allConnections.push(...swiftResult.connections);
|
|
917
|
+
allWarnings.push(...swiftResult.warnings);
|
|
918
|
+
projectMetadata = swiftResult.projectMeta;
|
|
919
|
+
if (options.verbose) {
|
|
920
|
+
console.log(` Swift: ${swiftResult.components.length} components, ${swiftResult.connections.length} connections`);
|
|
921
|
+
if (swiftResult.projectMeta.platforms) {
|
|
922
|
+
console.log(` Platforms: ${swiftResult.projectMeta.platforms.join(', ')}`);
|
|
923
|
+
}
|
|
924
|
+
if (swiftResult.projectMeta.architecture_pattern) {
|
|
925
|
+
console.log(` Architecture: ${swiftResult.projectMeta.architecture_pattern}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
catch (error) {
|
|
930
|
+
allWarnings.push({
|
|
931
|
+
type: 'parse_error',
|
|
932
|
+
message: `Swift code scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
// Xcode project analysis (.pbxproj + storyboards)
|
|
936
|
+
try {
|
|
937
|
+
const { findXcodeProject } = await import('./scanners/packages/swift.js');
|
|
938
|
+
const pbxprojPath = findXcodeProject(root);
|
|
939
|
+
if (pbxprojPath) {
|
|
940
|
+
if (options.verbose)
|
|
941
|
+
console.log(' - Scanning Xcode project...');
|
|
942
|
+
const { parseXcodeProject, mapTargetToComponent, mapSourceMembership } = await import('./scanners/xcode/pbxproj-parser.js');
|
|
943
|
+
const xcodeData = parseXcodeProject(pbxprojPath);
|
|
944
|
+
const timestamp = Date.now();
|
|
945
|
+
for (const target of xcodeData.targets) {
|
|
946
|
+
const comp = mapTargetToComponent(target, timestamp);
|
|
947
|
+
allComponents.push(comp);
|
|
948
|
+
const memberConns = mapSourceMembership(target, comp.component_id, timestamp);
|
|
949
|
+
allConnections.push(...memberConns);
|
|
950
|
+
}
|
|
951
|
+
// Enrich project metadata with Xcode target info
|
|
952
|
+
if (projectMetadata) {
|
|
953
|
+
projectMetadata.targets = xcodeData.targets.map(t => ({
|
|
954
|
+
name: t.name,
|
|
955
|
+
type: t.type,
|
|
956
|
+
dependencies: t.frameworks,
|
|
957
|
+
}));
|
|
958
|
+
projectMetadata.xcodeProject = {
|
|
959
|
+
path: pbxprojPath,
|
|
960
|
+
targets: xcodeData.targets.map(t => ({
|
|
961
|
+
name: t.name,
|
|
962
|
+
type: t.type,
|
|
963
|
+
bundleId: t.bundleId,
|
|
964
|
+
})),
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
if (options.verbose) {
|
|
968
|
+
console.log(` Xcode: ${xcodeData.targets.length} targets`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// Storyboard/XIB scanning
|
|
972
|
+
const { scanStoryboards } = await import('./scanners/xcode/storyboard-scanner.js');
|
|
973
|
+
const storyboardResult = await scanStoryboards(root);
|
|
974
|
+
allComponents.push(...storyboardResult.components);
|
|
975
|
+
allConnections.push(...storyboardResult.connections);
|
|
976
|
+
if (options.verbose && storyboardResult.components.length > 0) {
|
|
977
|
+
console.log(` Storyboards: ${storyboardResult.components.length} VCs, ${storyboardResult.connections.length} segues`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
catch (error) {
|
|
981
|
+
allWarnings.push({
|
|
982
|
+
type: 'parse_error',
|
|
983
|
+
message: `Xcode project scanning failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
204
986
|
}
|
|
205
|
-
//
|
|
206
|
-
if (options.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
987
|
+
// AI prompts & LLM call tracing
|
|
988
|
+
if (options.prompts) {
|
|
989
|
+
// Step 1: Run anchor-based LLM call tracer (primary detection)
|
|
990
|
+
if (options.verbose)
|
|
991
|
+
console.log(' - Running LLM call tracer (anchor-based)...');
|
|
992
|
+
let traceResult;
|
|
993
|
+
try {
|
|
994
|
+
traceResult = await traceLLMCalls(root, incWalkSet);
|
|
995
|
+
allComponents.push(...traceResult.scanResult.components);
|
|
996
|
+
allConnections.push(...traceResult.scanResult.connections);
|
|
997
|
+
if (options.verbose) {
|
|
998
|
+
console.log(` Traced ${traceResult.calls.length} LLM call sites`);
|
|
999
|
+
console.log(` Wrappers: ${traceResult.wrappers.length}`);
|
|
1000
|
+
const providers = new Map();
|
|
1001
|
+
for (const call of traceResult.calls) {
|
|
1002
|
+
const p = call.provider.name;
|
|
1003
|
+
providers.set(p, (providers.get(p) || 0) + 1);
|
|
1004
|
+
}
|
|
1005
|
+
for (const [provider, count] of providers) {
|
|
1006
|
+
console.log(` ${provider}: ${count} call sites`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
allWarnings.push({
|
|
1012
|
+
type: 'parse_error',
|
|
1013
|
+
message: `LLM call tracer failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1014
|
+
});
|
|
1015
|
+
if (options.verbose)
|
|
1016
|
+
console.log(` LLM tracer error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
1017
|
+
}
|
|
1018
|
+
// Step 2: Run regex prompt detector with corroboration (secondary — catches prompt definitions)
|
|
1019
|
+
if (options.verbose)
|
|
1020
|
+
console.log(' - Running prompt detector (corroboration-filtered)...');
|
|
1021
|
+
promptScanResultHolder = await scanPrompts(root, {
|
|
1022
|
+
includeRawContent: true,
|
|
1023
|
+
detectVariables: true,
|
|
1024
|
+
aggressive: true,
|
|
1025
|
+
}, incWalkSet);
|
|
1026
|
+
// Attach tracer results to prompt scan data (for web UI)
|
|
1027
|
+
if (traceResult) {
|
|
1028
|
+
promptScanResultHolder.tracedCalls = traceResult.calls;
|
|
1029
|
+
promptScanResultHolder.summary.tracedCallSites = traceResult.calls.length;
|
|
1030
|
+
}
|
|
1031
|
+
// Convert prompt definitions to architecture format
|
|
1032
|
+
const promptArchitecture = convertToArchitecture(promptScanResultHolder.prompts);
|
|
1033
|
+
allComponents.push(...promptArchitecture.components);
|
|
1034
|
+
allConnections.push(...promptArchitecture.connections);
|
|
1035
|
+
allWarnings.push(...promptArchitecture.warnings);
|
|
1036
|
+
if (options.verbose) {
|
|
1037
|
+
console.log(` Found ${promptScanResultHolder.prompts.length} prompt definitions`);
|
|
1038
|
+
if (promptScanResultHolder.summary.byProvider) {
|
|
1039
|
+
for (const [provider, count] of Object.entries(promptScanResultHolder.summary.byProvider)) {
|
|
1040
|
+
console.log(` ${provider}: ${count}`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
// ==========================================================================
|
|
1047
|
+
// Phase 3.5: Semantic Classification
|
|
1048
|
+
// ==========================================================================
|
|
1049
|
+
if (allConnections.length > 0 && allComponents.length > 0) {
|
|
224
1050
|
if (options.verbose) {
|
|
225
|
-
console.log(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
1051
|
+
console.log('Phase 3.5: Classifying connections...');
|
|
1052
|
+
}
|
|
1053
|
+
const semantics = classifyAllConnections(allConnections, allComponents);
|
|
1054
|
+
for (const conn of allConnections) {
|
|
1055
|
+
const info = semantics.get(conn.connection_id);
|
|
1056
|
+
if (info) {
|
|
1057
|
+
conn.semantic = info;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (options.verbose) {
|
|
1061
|
+
const byClass = new Map();
|
|
1062
|
+
for (const [, info] of semantics) {
|
|
1063
|
+
byClass.set(info.classification, (byClass.get(info.classification) || 0) + 1);
|
|
1064
|
+
}
|
|
1065
|
+
for (const [cls, count] of byClass) {
|
|
1066
|
+
console.log(` ${cls}: ${count}`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
// ==========================================================================
|
|
1071
|
+
// Phase 4: Deduplicate & Store
|
|
1072
|
+
// ==========================================================================
|
|
1073
|
+
if (options.verbose) {
|
|
1074
|
+
console.log('Phase 4: Storing results...');
|
|
1075
|
+
}
|
|
1076
|
+
// Deduplicate components by (type, name, primary-source-file) within current scan.
|
|
1077
|
+
//
|
|
1078
|
+
// Run 1.7 — Problem B: the prior key was `component.name` alone. That
|
|
1079
|
+
// collided cross-type — e.g., the file-level component for `lib/prisma.ts`
|
|
1080
|
+
// (type='component', name='prisma') vs the Prisma DB component
|
|
1081
|
+
// (type='database', name='prisma'). The DB component won on confidence;
|
|
1082
|
+
// the file component was silently dropped. But the import-scanner had
|
|
1083
|
+
// already emitted edges referencing the dropped file component_id —
|
|
1084
|
+
// 410 orphan edges on atomize-ai, which fired the integrity-promote and
|
|
1085
|
+
// truncated the graph (Problem A's loud symptom).
|
|
1086
|
+
//
|
|
1087
|
+
// The fix keys by `${type}|${name}|${first-config-file}`:
|
|
1088
|
+
// • Different types coexist (was: collided).
|
|
1089
|
+
// • Same-type same-name from different paths coexist (was: collided —
|
|
1090
|
+
// `app/proxy.ts` and `proxy.ts` both produce a file-level component
|
|
1091
|
+
// named `proxy`; both are real, both must be kept). Path
|
|
1092
|
+
// disambiguation matches Run 1.6 verify #6's stable_id contract for
|
|
1093
|
+
// the 6 component types where `name` alone isn't unique.
|
|
1094
|
+
// • Same-type same-name same-file STILL dedupes (the genuine duplicate
|
|
1095
|
+
// case — AST + regex both detecting the same service call), with
|
|
1096
|
+
// highest confidence winning. For components with no config_files
|
|
1097
|
+
// (rare) the key falls back to `${type}|${name}|` and behaves like
|
|
1098
|
+
// the legacy by-name dedup within that type.
|
|
1099
|
+
const componentMap = new Map();
|
|
1100
|
+
for (const component of allComponents) {
|
|
1101
|
+
const primaryFile = component.source?.config_files?.[0] ?? '';
|
|
1102
|
+
const key = `${component.type}|${component.name}|${primaryFile}`;
|
|
1103
|
+
const existing = componentMap.get(key);
|
|
1104
|
+
if (!existing || component.source.confidence > existing.source.confidence) {
|
|
1105
|
+
componentMap.set(key, component);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const uniqueComponents = Array.from(componentMap.values());
|
|
1109
|
+
// Deduplicate connections by composite key (within current scan)
|
|
1110
|
+
// Keeps highest confidence when duplicates found (e.g., regex + AST detect same call)
|
|
1111
|
+
const connectionMap = new Map();
|
|
1112
|
+
for (const conn of allConnections) {
|
|
1113
|
+
const key = `${conn.from.component_id}|${conn.to.component_id}|${conn.connection_type}|${conn.code_reference?.file || ''}:${conn.code_reference?.line_start || ''}`;
|
|
1114
|
+
const existing = connectionMap.get(key);
|
|
1115
|
+
if (!existing || conn.confidence > existing.confidence) {
|
|
1116
|
+
connectionMap.set(key, conn);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
const uniqueConnections = Array.from(connectionMap.values());
|
|
1120
|
+
// (C) Resolve FILE: prefixed connection targets to real component IDs
|
|
1121
|
+
// This enables trace to follow imports from route files instead of dead-ending
|
|
1122
|
+
const compByFile = new Map(); // file path → component_id
|
|
1123
|
+
for (const comp of uniqueComponents) {
|
|
1124
|
+
for (const f of comp.source.config_files || []) {
|
|
1125
|
+
compByFile.set(f, comp.component_id);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
for (const conn of uniqueConnections) {
|
|
1129
|
+
if (conn.to.component_id?.startsWith('FILE:')) {
|
|
1130
|
+
const filePath = conn.to.component_id.slice(5);
|
|
1131
|
+
const realId = compByFile.get(filePath);
|
|
1132
|
+
if (realId) {
|
|
1133
|
+
conn.to.component_id = realId;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (conn.from.component_id?.startsWith('FILE:')) {
|
|
1137
|
+
const filePath = conn.from.component_id.slice(5);
|
|
1138
|
+
const realId = compByFile.get(filePath);
|
|
1139
|
+
if (realId) {
|
|
1140
|
+
conn.from.component_id = realId;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
// Snapshot previous state before overwriting (for change tracking)
|
|
1145
|
+
// Also load the pre-scan snapshot for diff computation
|
|
1146
|
+
let preScanSnapshot = null;
|
|
1147
|
+
if (!options.clearFirst && decision.mode !== 'full') {
|
|
1148
|
+
try {
|
|
1149
|
+
await createSnapshot('pre-scan', config, root);
|
|
1150
|
+
preScanSnapshot = await loadLatestSnapshot(config, root);
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
// No previous data to snapshot — first scan
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
else if (decision.mode === 'full') {
|
|
1157
|
+
// For full scans, still create a snapshot for diff (if prior data exists)
|
|
1158
|
+
try {
|
|
1159
|
+
preScanSnapshot = await loadLatestSnapshot(config, root);
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
// First-ever scan, no prior snapshot.
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
// ==========================================================================
|
|
1166
|
+
// Phase 4 storage decision (Run 1 — D1 + D2):
|
|
1167
|
+
// - 'full': clearStorage was already done up front; now store everything fresh.
|
|
1168
|
+
// - 'incremental': clear ONLY components/connections that originate in the
|
|
1169
|
+
// walk-set, then merge the freshly-scanned uniqueComponents/Connections
|
|
1170
|
+
// with the survivors. Run integrity check; on failure, promote to full.
|
|
1171
|
+
// ==========================================================================
|
|
1172
|
+
let finalComponents = uniqueComponents;
|
|
1173
|
+
let finalConnections = uniqueConnections;
|
|
1174
|
+
if (decision.mode === 'incremental' && !options.clearFirst) {
|
|
1175
|
+
// Snapshot the FULL prior on-disk component set BEFORE clearForFiles —
|
|
1176
|
+
// we need it to remap surviving connections from old random component_ids
|
|
1177
|
+
// to the new ones (since stable_id is the join key but connections
|
|
1178
|
+
// reference component_id, which gets a fresh random suffix per scan).
|
|
1179
|
+
const preClearComponents = await loadAllComponents(config, root);
|
|
1180
|
+
// Clear only the touched subset.
|
|
1181
|
+
await clearForFiles(config, root, walkSet);
|
|
1182
|
+
// Load survivors (everything NOT in walk-set, still on disk).
|
|
1183
|
+
const survivingComponents = await loadAllComponents(config, root);
|
|
1184
|
+
const survivingConnections = await loadAllConnections(config, root);
|
|
1185
|
+
// Populate stable_ids on the in-memory uniqueComponents BEFORE merging.
|
|
1186
|
+
// Disk-loaded survivors get stable_ids from loadAllComponents, but
|
|
1187
|
+
// freshly-scanned components don't have them set until storeComponents
|
|
1188
|
+
// runs — and we merge BEFORE store. Without this, every fresh component
|
|
1189
|
+
// looks like a new entry to mergeByStableId (because its key falls back
|
|
1190
|
+
// to its random component_id), breaking dedup.
|
|
1191
|
+
for (const c of uniqueComponents)
|
|
1192
|
+
ensureStableIdPublic(c);
|
|
1193
|
+
// Merge: incoming wins on stable_id collision (component) or composite key (connection).
|
|
1194
|
+
// Components keyed by stable_id (or component_id fallback).
|
|
1195
|
+
finalComponents = mergeByStableId(survivingComponents, uniqueComponents, (c) => c.stable_id ?? c.component_id);
|
|
1196
|
+
// Build a remap: prior_component_id → new_component_id (via stable_id).
|
|
1197
|
+
// Connections from disk reference OLD random component_ids; the freshly
|
|
1198
|
+
// scanned components have NEW random component_ids. Same stable_id ties
|
|
1199
|
+
// them together. Rewrite surviving connection from/to ids so the merged
|
|
1200
|
+
// graph stays consistent.
|
|
1201
|
+
const stableToNewId = new Map();
|
|
1202
|
+
for (const c of finalComponents) {
|
|
1203
|
+
if (c.stable_id)
|
|
1204
|
+
stableToNewId.set(c.stable_id, c.component_id);
|
|
1205
|
+
}
|
|
1206
|
+
// oldIdToStable: maps every PRIOR-scan component_id (including ones we
|
|
1207
|
+
// just deleted via clearForFiles) to its stable_id. Built from the
|
|
1208
|
+
// pre-clear snapshot so we can resolve connection refs to their new IDs.
|
|
1209
|
+
const oldIdToStable = new Map();
|
|
1210
|
+
for (const c of preClearComponents) {
|
|
1211
|
+
if (c.stable_id)
|
|
1212
|
+
oldIdToStable.set(c.component_id, c.stable_id);
|
|
1213
|
+
}
|
|
1214
|
+
// Also map fresh components' ids → stable (no remap needed but keeps the
|
|
1215
|
+
// rewrite loop a no-op for these instead of leaving them undefined).
|
|
1216
|
+
for (const c of uniqueComponents) {
|
|
1217
|
+
if (c.stable_id)
|
|
1218
|
+
oldIdToStable.set(c.component_id, c.stable_id);
|
|
1219
|
+
}
|
|
1220
|
+
function remapId(id) {
|
|
1221
|
+
if (!id)
|
|
1222
|
+
return id;
|
|
1223
|
+
if (id.startsWith('FILE:'))
|
|
1224
|
+
return id;
|
|
1225
|
+
const stable = oldIdToStable.get(id);
|
|
1226
|
+
if (stable) {
|
|
1227
|
+
const newId = stableToNewId.get(stable);
|
|
1228
|
+
if (newId)
|
|
1229
|
+
return newId;
|
|
1230
|
+
}
|
|
1231
|
+
return id; // No remap available — leave alone, integrity check may catch
|
|
1232
|
+
}
|
|
1233
|
+
// Rewrite surviving connections to use the latest component_ids.
|
|
1234
|
+
for (const conn of survivingConnections) {
|
|
1235
|
+
if (conn.from?.component_id) {
|
|
1236
|
+
conn.from.component_id = remapId(conn.from.component_id) ?? conn.from.component_id;
|
|
1237
|
+
}
|
|
1238
|
+
if (conn.to?.component_id) {
|
|
1239
|
+
conn.to.component_id = remapId(conn.to.component_id) ?? conn.to.component_id;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
// Connections keyed by from|to|type|file:line composite (matches dedup key).
|
|
1243
|
+
const connKey = (c) => `${c.from?.component_id ?? ''}|${c.to?.component_id ?? ''}|${c.connection_type}|${c.code_reference?.file ?? ''}:${c.code_reference?.line_start ?? ''}`;
|
|
1244
|
+
finalConnections = mergeByStableId(survivingConnections, uniqueConnections, connKey);
|
|
1245
|
+
// Integrity check: every connection endpoint must exist; every walk-set
|
|
1246
|
+
// component must reference real source files. On failure → promote to full.
|
|
1247
|
+
const integrity = await runIntegrityCheck(finalComponents, finalConnections, root, walkSet);
|
|
1248
|
+
if (!integrity.ok) {
|
|
1249
|
+
if (options.verbose) {
|
|
1250
|
+
console.log(` Integrity check failed (${integrity.issues.length} issues) — promoting to full scan`);
|
|
1251
|
+
for (const issue of integrity.issues.slice(0, 3)) {
|
|
1252
|
+
console.log(` ${issue}`);
|
|
229
1253
|
}
|
|
230
1254
|
}
|
|
1255
|
+
// ============================================================
|
|
1256
|
+
// Run 1.7 — Problem A (recursive re-entry promote)
|
|
1257
|
+
// ============================================================
|
|
1258
|
+
// The pre-Run-1.7 promote reused the in-memory uniqueComponents/
|
|
1259
|
+
// uniqueConnections that were just computed under the walk-set
|
|
1260
|
+
// restriction. After Run 1.5's walk-set plumbing, those are NOT
|
|
1261
|
+
// the full source tree — only the walk-set's slice of it. Reusing
|
|
1262
|
+
// them on promote truncated the graph (atomize-ai: 6,445 → 58
|
|
1263
|
+
// connections, 2,452 → 58 components).
|
|
1264
|
+
//
|
|
1265
|
+
// Fix: release the scan lock and recursively re-enter scan() with
|
|
1266
|
+
// `mode: 'full', clearFirst: true`. The inner scan walks the full
|
|
1267
|
+
// source tree, the lock re-acquires cleanly inside, and its result
|
|
1268
|
+
// is returned verbatim. The internal `_promotedFromIncremental`
|
|
1269
|
+
// flag tells the inner scan to label its timeline entry and stats
|
|
1270
|
+
// `scan_type: 'incremental→full'` — preserving the Run 1.6 #3
|
|
1271
|
+
// evidence-preservation contract.
|
|
1272
|
+
//
|
|
1273
|
+
// We `return` early so the rest of the outer scan's phases
|
|
1274
|
+
// (storage, timeline, manifest, hashes) don't double-run.
|
|
1275
|
+
lock.release();
|
|
1276
|
+
return await scan(root, {
|
|
1277
|
+
...options,
|
|
1278
|
+
mode: 'full',
|
|
1279
|
+
clearFirst: true,
|
|
1280
|
+
incremental: false,
|
|
1281
|
+
_promotedFromIncremental: true,
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
// ============================================================
|
|
1285
|
+
// Run 1.7 — orphan-disk-file cleanup on successful incremental merge.
|
|
1286
|
+
// ============================================================
|
|
1287
|
+
// `clearForFiles` only deletes disk files whose `source.config_files`
|
|
1288
|
+
// overlap the walk-set. Components produced by always-full scanners
|
|
1289
|
+
// (npm/pip/swift packages, infra, prisma) don't list user source
|
|
1290
|
+
// files in `config_files` (they list manifests / abs paths), so
|
|
1291
|
+
// their disk files survive `clearForFiles`. After the merge, the
|
|
1292
|
+
// freshly-scanned versions get NEW random `component_id`s and are
|
|
1293
|
+
// written to NEW filenames. The OLD survivor files are now orphans:
|
|
1294
|
+
// unreachable from `finalComponents` but still on disk.
|
|
1295
|
+
//
|
|
1296
|
+
// Pre-Run-1.7 this was masked by the always-failing integrity check
|
|
1297
|
+
// on real projects (`clearStorage` on promote wiped the orphans).
|
|
1298
|
+
// Now that integrity passes (Problem B fix), and the promote is
|
|
1299
|
+
// recursive (Problem A fix), this latent bug surfaces as a doubling
|
|
1300
|
+
// of `npm`/`database`/`config`/`infra` components per incremental.
|
|
1301
|
+
//
|
|
1302
|
+
// Fix: after the merge, walk the components/connections directories
|
|
1303
|
+
// and unlink any file whose ID isn't in `finalComponents` /
|
|
1304
|
+
// `finalConnections`. Idempotent and atomic per-file (unlink errors
|
|
1305
|
+
// are silently swallowed — best-effort, matches the integrity-promote
|
|
1306
|
+
// pattern). Connections also need this since their random IDs aren't
|
|
1307
|
+
// stable across scans either.
|
|
1308
|
+
{
|
|
1309
|
+
const finalComponentIds = new Set(finalComponents.map((c) => c.component_id));
|
|
1310
|
+
const finalConnectionIds = new Set(finalConnections.map((c) => c.connection_id));
|
|
1311
|
+
const fsPromises = (await import('node:fs')).promises;
|
|
1312
|
+
const purgeOrphans = async (dir, keepIds) => {
|
|
1313
|
+
try {
|
|
1314
|
+
const files = await fsPromises.readdir(dir);
|
|
1315
|
+
await Promise.all(files
|
|
1316
|
+
.filter((f) => f.endsWith('.json'))
|
|
1317
|
+
.map(async (f) => {
|
|
1318
|
+
const id = f.slice(0, -'.json'.length);
|
|
1319
|
+
if (!keepIds.has(id)) {
|
|
1320
|
+
await fsPromises.unlink(path.join(dir, f)).catch(() => { });
|
|
1321
|
+
}
|
|
1322
|
+
}));
|
|
1323
|
+
}
|
|
1324
|
+
catch {
|
|
1325
|
+
// Dir missing or unreadable — non-fatal.
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
await purgeOrphans(getComponentsPath(config, root), finalComponentIds);
|
|
1329
|
+
await purgeOrphans(getConnectionsPath(config, root), finalConnectionIds);
|
|
231
1330
|
}
|
|
232
1331
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
console.log('Phase 4: Storing results...');
|
|
239
|
-
}
|
|
240
|
-
// Deduplicate components by name (within current scan)
|
|
241
|
-
const componentMap = new Map();
|
|
242
|
-
for (const component of allComponents) {
|
|
243
|
-
const existing = componentMap.get(component.name);
|
|
244
|
-
if (!existing || component.source.confidence > existing.source.confidence) {
|
|
245
|
-
componentMap.set(component.name, component);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const uniqueComponents = Array.from(componentMap.values());
|
|
249
|
-
// Deduplicate connections by composite key (within current scan)
|
|
250
|
-
// Keeps highest confidence when duplicates found (e.g., regex + AST detect same call)
|
|
251
|
-
const connectionMap = new Map();
|
|
252
|
-
for (const conn of allConnections) {
|
|
253
|
-
const key = `${conn.from.component_id}|${conn.to.component_id}|${conn.connection_type}|${conn.code_reference?.file || ''}:${conn.code_reference?.line_start || ''}`;
|
|
254
|
-
const existing = connectionMap.get(key);
|
|
255
|
-
if (!existing || conn.confidence > existing.confidence) {
|
|
256
|
-
connectionMap.set(key, conn);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
const uniqueConnections = Array.from(connectionMap.values());
|
|
260
|
-
// Snapshot previous state before overwriting (for change tracking)
|
|
261
|
-
if (!options.clearFirst) {
|
|
1332
|
+
// External enrichment (structural axis): stamp boundary nodes (npm/service/
|
|
1333
|
+
// llm/infra/spm/...) with cached canonical identity, latest version, docs, and
|
|
1334
|
+
// a freshness verdict. Offline + sync — reads NavGator's own JSON cache, never
|
|
1335
|
+
// the network, so it cannot slow or fail the scan. The freshness axis (network
|
|
1336
|
+
// re-checks) runs separately via refreshExternal / the external-resolver agent.
|
|
262
1337
|
try {
|
|
263
|
-
|
|
1338
|
+
const cache = loadCache();
|
|
1339
|
+
enrichFromCache(finalComponents, makeLookup(cache), Date.now());
|
|
264
1340
|
}
|
|
265
1341
|
catch {
|
|
266
|
-
|
|
1342
|
+
/* enrichment is best-effort — never block persistence on it */
|
|
267
1343
|
}
|
|
1344
|
+
// Store final state (atomic per-file writes — see storage.ts).
|
|
1345
|
+
await storeComponents(finalComponents, config, root);
|
|
1346
|
+
await storeConnections(finalConnections, config, root);
|
|
1347
|
+
// R6 footprint fix: clean up legacy per-entity files when the feature is
|
|
1348
|
+
// disabled (default). Idempotent and best-effort — never blocks the scan.
|
|
1349
|
+
// Surfaces a one-line notice when something actually got cleaned.
|
|
1350
|
+
try {
|
|
1351
|
+
const migrated = await migratePerEntityFiles(config, root);
|
|
1352
|
+
if (options.verbose &&
|
|
1353
|
+
(migrated.componentsRemoved > 0 ||
|
|
1354
|
+
migrated.connectionsRemoved > 0 ||
|
|
1355
|
+
migrated.dirsRemoved > 0)) {
|
|
1356
|
+
console.log(` R6 migration: removed ${migrated.componentsRemoved} legacy component file(s), ${migrated.connectionsRemoved} legacy connection file(s), ${migrated.dirsRemoved} now-empty dir(s)`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
// Best-effort.
|
|
1361
|
+
}
|
|
1362
|
+
// ==========================================================================
|
|
1363
|
+
// Phase 4.5: SQC Audit (Run 2 — D4)
|
|
1364
|
+
//
|
|
1365
|
+
// Sample the just-stored output, run deterministic verifiers (+ structured
|
|
1366
|
+
// LLM-judge payload in MCP mode), and update per-stratum EWMA state.
|
|
1367
|
+
// Audit failure NEVER fails the scan — only updates the index's EWMA so the
|
|
1368
|
+
// NEXT scan can auto-promote on detected drift.
|
|
1369
|
+
// ==========================================================================
|
|
1370
|
+
let auditReport;
|
|
1371
|
+
if (!options.noAudit) {
|
|
1372
|
+
try {
|
|
1373
|
+
const planOpt = options.auditPlan
|
|
1374
|
+
? (options.auditPlan.toUpperCase() === 'AQL'
|
|
1375
|
+
? 'AQL'
|
|
1376
|
+
: options.auditPlan.toUpperCase() === 'SPRT'
|
|
1377
|
+
? 'SPRT'
|
|
1378
|
+
: 'Cochran')
|
|
1379
|
+
: undefined;
|
|
1380
|
+
const auditOpts = {
|
|
1381
|
+
plan: planOpt,
|
|
1382
|
+
isMcpMode: !!options.isMcpMode,
|
|
1383
|
+
priorEwma: priorIndex?.ewma,
|
|
1384
|
+
priorAuditCount: priorIndex?.audit_history_count ?? 0,
|
|
1385
|
+
forceCochran: !!priorIndex?.pending_drift_breach,
|
|
1386
|
+
};
|
|
1387
|
+
const r = await runAudit({ components: finalComponents, connections: finalConnections }, config, root, auditOpts);
|
|
1388
|
+
if (r) {
|
|
1389
|
+
// Update EWMA snapshot from prior index. We do not write the index here —
|
|
1390
|
+
// the post-Phase-5 index annotation already runs (line ~1430) and we
|
|
1391
|
+
// hijack `priorIndex` via the loadIndex/atomicWriteJSON cycle there.
|
|
1392
|
+
const { ewma, anyBreach } = updateEwmaForAudit(priorIndex?.ewma ?? undefined, r);
|
|
1393
|
+
if (anyBreach)
|
|
1394
|
+
r.drift_breach = true;
|
|
1395
|
+
auditReport = r;
|
|
1396
|
+
// Stash on a local for the index-annotation block below to consume.
|
|
1397
|
+
// (We re-read priorIndex after the freshIndex write to merge ewma.)
|
|
1398
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1399
|
+
auditReport.__ewma = ewma;
|
|
1400
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1401
|
+
auditReport.__anyBreach = anyBreach;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
catch (err) {
|
|
1405
|
+
if (process.env['NAVGATOR_DEBUG']) {
|
|
1406
|
+
console.error('[audit] skipped due to error:', err.message);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
// ==========================================================================
|
|
1411
|
+
// Phase 5: Architecture Diff
|
|
1412
|
+
// ==========================================================================
|
|
1413
|
+
let timelineEntry;
|
|
1414
|
+
if (options.verbose) {
|
|
1415
|
+
console.log('Phase 5: Computing architecture diff...');
|
|
1416
|
+
}
|
|
1417
|
+
try {
|
|
1418
|
+
const currentSnapshot = await buildCurrentSnapshot(config, root);
|
|
1419
|
+
const diff = computeArchitectureDiff(preScanSnapshot, currentSnapshot);
|
|
1420
|
+
const { significance, triggers } = classifySignificance(diff);
|
|
1421
|
+
timelineEntry = {
|
|
1422
|
+
id: generateTimelineId(),
|
|
1423
|
+
timestamp: Date.now(),
|
|
1424
|
+
significance,
|
|
1425
|
+
triggers,
|
|
1426
|
+
diff,
|
|
1427
|
+
snapshot_id: currentSnapshot.snapshot_id,
|
|
1428
|
+
git: gitInfo,
|
|
1429
|
+
scan_type: scanType,
|
|
1430
|
+
// Run 1.6 — item #3: report walk-set size for 'incremental' AND for the
|
|
1431
|
+
// legacy in-place 'incremental→full' promote (which kept walkSet
|
|
1432
|
+
// populated), so a silent integrity-promote didn't erase evidence
|
|
1433
|
+
// that an incremental walk-set was attempted.
|
|
1434
|
+
//
|
|
1435
|
+
// Run 1.7 — Problem A: the recursive-re-entry promote runs as a true
|
|
1436
|
+
// full scan (walkSet empty). Reporting `walkSet.size = 0` would be
|
|
1437
|
+
// dishonest — the inner scan really did walk every source file.
|
|
1438
|
+
// Report `sourceFiles.length` in that case. Run 1.6 #3 still holds:
|
|
1439
|
+
// any future in-place promote path (walkSet populated under
|
|
1440
|
+
// 'incremental→full') reports walk-set size.
|
|
1441
|
+
// Use decision.mode (the EFFECTIVE scan mode) instead of scanType
|
|
1442
|
+
// (the user-visible label). On the recursive-re-entry promote (Run 1.7
|
|
1443
|
+
// Problem A), decision.mode='full' even though scanType='incremental→full',
|
|
1444
|
+
// and walkSet may be populated by the still-modified file — but the inner
|
|
1445
|
+
// scan walked the full source tree, so files_scanned must be sourceFiles.length.
|
|
1446
|
+
files_scanned: decision.mode === 'incremental' && walkSet.size > 0
|
|
1447
|
+
? walkSet.size
|
|
1448
|
+
: sourceFiles.length,
|
|
1449
|
+
// Run 2 — D4: audit report (when produced).
|
|
1450
|
+
...(auditReport ? { audit: stripInternals(auditReport) } : {}),
|
|
1451
|
+
};
|
|
1452
|
+
// Only save timeline entry if there are changes (or first scan)
|
|
1453
|
+
if (diff.stats.total_changes > 0 || !preScanSnapshot) {
|
|
1454
|
+
await saveTimelineEntry(timelineEntry, config, root);
|
|
1455
|
+
}
|
|
1456
|
+
if (options.verbose) {
|
|
1457
|
+
console.log(` Significance: ${significance}`);
|
|
1458
|
+
console.log(` Changes: ${diff.stats.total_changes}`);
|
|
1459
|
+
if (triggers.length > 0) {
|
|
1460
|
+
console.log(` Triggers: ${triggers.join(', ')}`);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
catch {
|
|
1465
|
+
// Diff computation is non-critical
|
|
1466
|
+
if (options.verbose) {
|
|
1467
|
+
console.log(' Diff computation skipped (non-critical error)');
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
// Build index, graph, file map, and summary.
|
|
1471
|
+
// R6: pass the in-memory final state so derived files don't depend on
|
|
1472
|
+
// per-entity disk reads (which are now opt-in and empty by default).
|
|
1473
|
+
const derivedData = { components: finalComponents, connections: finalConnections };
|
|
1474
|
+
await buildIndex(config, root, projectMetadata, derivedData);
|
|
1475
|
+
await buildGraph(config, root, derivedData);
|
|
1476
|
+
await buildFileMap(config, root, derivedData);
|
|
1477
|
+
// ==========================================================================
|
|
1478
|
+
// Phase 5.4: Derived reverse-deps index + manifest (Run 1.6 — items #8 + #9)
|
|
1479
|
+
// ==========================================================================
|
|
1480
|
+
// The reverse-deps index lets the next incremental scan compute walk-set
|
|
1481
|
+
// expansion from a single file open instead of walking every per-edge JSON.
|
|
1482
|
+
// The manifest lists all derived artifacts with their generated_at stamps.
|
|
1483
|
+
let reverseDepsEdgeCount;
|
|
1484
|
+
try {
|
|
1485
|
+
const result = await buildReverseDepsIndex(finalComponents, finalConnections, config, root);
|
|
1486
|
+
reverseDepsEdgeCount = result.edge_count;
|
|
1487
|
+
}
|
|
1488
|
+
catch (err) {
|
|
1489
|
+
if (process.env['NAVGATOR_DEBUG']) {
|
|
1490
|
+
console.error('[reverse-deps] index build skipped:', err.message);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
try {
|
|
1494
|
+
await buildDerivedManifest(config, root, { reverseDepsEdgeCount });
|
|
1495
|
+
}
|
|
1496
|
+
catch (err) {
|
|
1497
|
+
if (process.env['NAVGATOR_DEBUG']) {
|
|
1498
|
+
console.error('[manifest] write skipped:', err.message);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
// ==========================================================================
|
|
1502
|
+
// Phase 5.5: Annotate index with mode-tracking fields (Run 1 — D2)
|
|
1503
|
+
// Run AFTER buildIndex so we can annotate the freshly-written index
|
|
1504
|
+
// without buildIndex needing to know about modes.
|
|
1505
|
+
// ==========================================================================
|
|
1506
|
+
try {
|
|
1507
|
+
const freshIndex = await loadIndex(config, root);
|
|
1508
|
+
if (freshIndex) {
|
|
1509
|
+
if (scanType === 'full' || scanType === 'incremental→full') {
|
|
1510
|
+
freshIndex.last_full_scan = Date.now();
|
|
1511
|
+
freshIndex.incrementals_since_full = 0;
|
|
1512
|
+
}
|
|
1513
|
+
else if (scanType === 'incremental') {
|
|
1514
|
+
// Preserve last_full_scan from prior index; bump counter.
|
|
1515
|
+
if (priorIndex) {
|
|
1516
|
+
freshIndex.last_full_scan = priorIndex.last_full_scan ?? priorIndex.last_scan ?? 0;
|
|
1517
|
+
}
|
|
1518
|
+
freshIndex.incrementals_since_full = (priorIndex?.incrementals_since_full ?? 0) + 1;
|
|
1519
|
+
}
|
|
1520
|
+
// Always set schema_version to current build's version.
|
|
1521
|
+
freshIndex.schema_version = SCHEMA_VERSION;
|
|
1522
|
+
// Run 2 — D5: persist EWMA + audit history bookkeeping.
|
|
1523
|
+
if (auditReport) {
|
|
1524
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1525
|
+
const stash = auditReport;
|
|
1526
|
+
if (stash.__ewma) {
|
|
1527
|
+
freshIndex.ewma = stash.__ewma;
|
|
1528
|
+
}
|
|
1529
|
+
freshIndex.audit_history_count = (priorIndex?.audit_history_count ?? 0) + 1;
|
|
1530
|
+
freshIndex.pending_drift_breach = !!stash.__anyBreach;
|
|
1531
|
+
}
|
|
1532
|
+
else if (priorIndex) {
|
|
1533
|
+
// No audit this run — preserve prior values.
|
|
1534
|
+
if (priorIndex.ewma)
|
|
1535
|
+
freshIndex.ewma = priorIndex.ewma;
|
|
1536
|
+
if (priorIndex.audit_history_count !== undefined) {
|
|
1537
|
+
freshIndex.audit_history_count = priorIndex.audit_history_count;
|
|
1538
|
+
}
|
|
1539
|
+
// Clear pending breach if we just promoted on it.
|
|
1540
|
+
if (priorIndex.pending_drift_breach && scanType !== 'incremental') {
|
|
1541
|
+
freshIndex.pending_drift_breach = false;
|
|
1542
|
+
}
|
|
1543
|
+
else if (priorIndex.pending_drift_breach !== undefined) {
|
|
1544
|
+
freshIndex.pending_drift_breach = priorIndex.pending_drift_breach;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
await atomicWriteJSON(getIndexPath(config, root), freshIndex);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
catch {
|
|
1551
|
+
// Non-fatal: mode-tracking annotation is best-effort.
|
|
1552
|
+
}
|
|
1553
|
+
// Compute graph-wide metrics (PageRank + Louvain communities) → metrics.json,
|
|
1554
|
+
// and back-write per-component scores into component metadata so any consumer
|
|
1555
|
+
// that loads a component sees them. Suppressed for graphs <20 nodes.
|
|
1556
|
+
try {
|
|
1557
|
+
const { computeAndStoreMetrics } = await import('./metrics/pagerank-louvain.js');
|
|
1558
|
+
await computeAndStoreMetrics(config, root, {
|
|
1559
|
+
components: finalComponents,
|
|
1560
|
+
connections: finalConnections,
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
catch (err) {
|
|
1564
|
+
// Non-fatal — scan still produces all other artifacts.
|
|
1565
|
+
if (process.env['NAVGATOR_DEBUG']) {
|
|
1566
|
+
console.error('[metrics] PageRank/Louvain skipped:', err.message);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
await buildSummary(config, root, promptScanResultHolder, projectMetadata, timelineEntry, gitInfo, derivedData);
|
|
1570
|
+
// R6: full-shape JSONL writers — the consolidated source of truth when
|
|
1571
|
+
// per-entity files are disabled (the default). These carry the complete
|
|
1572
|
+
// ArchitectureComponent / ArchitectureConnection objects so downstream
|
|
1573
|
+
// loaders never need per-entity files. Always written; cheap (~2MB for
|
|
1574
|
+
// atomize-ai-scale projects vs the 70MB per-entity sprawl).
|
|
1575
|
+
try {
|
|
1576
|
+
const { writeFullComponentsJsonl, writeFullConnectionsJsonl, } = await import('./storage/markdown-view.js');
|
|
1577
|
+
const { getStoragePath: getStoragePathFn } = await import('./config.js');
|
|
1578
|
+
const storeDir = getStoragePathFn(config, root);
|
|
1579
|
+
await writeFullComponentsJsonl(storeDir, finalComponents);
|
|
1580
|
+
await writeFullConnectionsJsonl(storeDir, finalConnections);
|
|
1581
|
+
}
|
|
1582
|
+
catch (err) {
|
|
1583
|
+
if (process.env['NAVGATOR_DEBUG']) {
|
|
1584
|
+
console.error('[full-jsonl] skipped:', err.message);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
// Markdown views + connections.jsonl (T3, trimmed scope).
|
|
1588
|
+
// Derived from in-memory components/connections — JSON remains canonical.
|
|
1589
|
+
// Emits .navgator/architecture/components-md/<type>/<slug>.md (Obsidian-readable,
|
|
1590
|
+
// git-diff-friendly, ripgrep-targetable) and connections.jsonl.
|
|
1591
|
+
// Disable via NAVGATOR_NO_MARKDOWN=1 if downstream tooling chokes.
|
|
1592
|
+
if (process.env['NAVGATOR_NO_MARKDOWN'] !== '1') {
|
|
1593
|
+
try {
|
|
1594
|
+
const { writeComponentMarkdownViews, writeConnectionsJsonl } = await import('./storage/markdown-view.js');
|
|
1595
|
+
const { getStoragePath: getStoragePathFn } = await import('./config.js');
|
|
1596
|
+
const storeDir = getStoragePathFn(config, root);
|
|
1597
|
+
await writeComponentMarkdownViews(storeDir, finalComponents, finalConnections);
|
|
1598
|
+
await writeConnectionsJsonl(storeDir, finalComponents, finalConnections);
|
|
1599
|
+
}
|
|
1600
|
+
catch (err) {
|
|
1601
|
+
if (process.env['NAVGATOR_DEBUG']) {
|
|
1602
|
+
console.error('[markdown-view] skipped:', err.message);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
// Git-backed temporal snapshot (T5). Commits the .navgator/ directory to a
|
|
1607
|
+
// NESTED git store at .navgator/.git — invisible to the parent repo
|
|
1608
|
+
// (gitignored). OPT-IN: enable via NAVGATOR_COMMIT=1 or `--commit` scan
|
|
1609
|
+
// flag. Per-scan git subprocess overhead is ~180ms; default is OFF to
|
|
1610
|
+
// preserve the speed criterion.
|
|
1611
|
+
if (process.env['NAVGATOR_COMMIT'] === '1' || options.commit === true) {
|
|
1612
|
+
try {
|
|
1613
|
+
const { commitScan } = await import('./temporal/git-store.js');
|
|
1614
|
+
const { getStoragePath } = await import('./config.js');
|
|
1615
|
+
const storeDir = getStoragePath(config, root);
|
|
1616
|
+
const sha7 = (gitInfo?.commit ?? '').slice(0, 7);
|
|
1617
|
+
const msg = `scan ${new Date().toISOString()}${sha7 ? ` @ ${sha7}` : ''}`;
|
|
1618
|
+
const result = commitScan(storeDir, msg);
|
|
1619
|
+
if (!result.ok && process.env['NAVGATOR_DEBUG']) {
|
|
1620
|
+
console.error('[temporal] commit failed:', result.error);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
catch (err) {
|
|
1624
|
+
if (process.env['NAVGATOR_DEBUG']) {
|
|
1625
|
+
console.error('[temporal] skipped:', err.message);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
// Persist prompt scan results if available
|
|
1630
|
+
if (promptScanResultHolder) {
|
|
1631
|
+
await savePromptScan(promptScanResultHolder, config, root);
|
|
1632
|
+
}
|
|
1633
|
+
// Register project in global registry
|
|
1634
|
+
try {
|
|
1635
|
+
await registerProject(root, {
|
|
1636
|
+
components: finalComponents.length,
|
|
1637
|
+
connections: finalConnections.length,
|
|
1638
|
+
prompts: promptScanResultHolder?.prompts.length ?? 0,
|
|
1639
|
+
}, timelineEntry?.significance, gitInfo);
|
|
1640
|
+
}
|
|
1641
|
+
catch {
|
|
1642
|
+
// Non-critical
|
|
1643
|
+
}
|
|
1644
|
+
// ==========================================================================
|
|
1645
|
+
// Phase 6: Save File Hashes
|
|
1646
|
+
// ==========================================================================
|
|
1647
|
+
if (options.verbose) {
|
|
1648
|
+
console.log('Phase 6: Saving file hashes...');
|
|
1649
|
+
}
|
|
1650
|
+
// Phase 6: hash both source files AND manifests so manifest edits are
|
|
1651
|
+
// detectable on the next scan (selectScanMode uses this to fire 'manifest-changed').
|
|
1652
|
+
const fileHashes = await computeFileHashes(filesForChangeDetection, root);
|
|
1653
|
+
await saveHashes(fileHashes, config, root);
|
|
1654
|
+
const duration = Date.now() - startTime;
|
|
1655
|
+
const filesChanged = fileChanges
|
|
1656
|
+
? fileChanges.added.length + fileChanges.modified.length + fileChanges.removed.length
|
|
1657
|
+
: sourceFiles.length;
|
|
1658
|
+
if (options.verbose) {
|
|
1659
|
+
console.log(`\nScan complete in ${duration}ms`);
|
|
1660
|
+
console.log(` Components: ${finalComponents.length}`);
|
|
1661
|
+
console.log(` Connections: ${finalConnections.length}`);
|
|
1662
|
+
console.log(` Files scanned: ${sourceFiles.length}`);
|
|
1663
|
+
console.log(` Files changed: ${filesChanged}`);
|
|
1664
|
+
console.log(` Warnings: ${allWarnings.length}`);
|
|
1665
|
+
}
|
|
1666
|
+
// Gitignore safety guard: NavGator's per-config-var component files and
|
|
1667
|
+
// NAVSUMMARY docs include parsed hostnames from .env files. They're
|
|
1668
|
+
// regenerated on every scan, so there's no loss from keeping them local.
|
|
1669
|
+
// Auto-add gitignore entries on first scan so hostnames don't drift into
|
|
1670
|
+
// git history. Silent unless it makes a change.
|
|
1671
|
+
try {
|
|
1672
|
+
const guardResult = await ensureSafeGitignore(root);
|
|
1673
|
+
if (guardResult.action === 'added' && options.verbose) {
|
|
1674
|
+
console.log(` NavGator safety guard: added gitignore block for architecture/components/COMP_config_*.json + NAVSUMMARY*.md`);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
catch {
|
|
1678
|
+
// Non-fatal: scan already completed, gitignore guard is best-effort
|
|
1679
|
+
}
|
|
1680
|
+
return {
|
|
1681
|
+
components: finalComponents,
|
|
1682
|
+
connections: finalConnections,
|
|
1683
|
+
warnings: allWarnings,
|
|
1684
|
+
fileChanges,
|
|
1685
|
+
promptScan: promptScanResultHolder,
|
|
1686
|
+
fieldUsageReport: fieldUsageReportResult,
|
|
1687
|
+
typeSpecReport: typeSpecReportResult,
|
|
1688
|
+
timelineEntry,
|
|
1689
|
+
gitInfo,
|
|
1690
|
+
stats: {
|
|
1691
|
+
scan_duration_ms: duration,
|
|
1692
|
+
components_found: finalComponents.length,
|
|
1693
|
+
connections_found: finalConnections.length,
|
|
1694
|
+
warnings_count: allWarnings.length,
|
|
1695
|
+
// Run 1.6 — item #3 / Run 1.7 — Problem A: walk-set size for incremental
|
|
1696
|
+
// and for an in-place promote (walkSet populated). Recursive-re-entry
|
|
1697
|
+
// promote (walkSet empty) reports actual source-file count.
|
|
1698
|
+
// Use decision.mode (the EFFECTIVE scan mode) instead of scanType
|
|
1699
|
+
// (the user-visible label). On the recursive-re-entry promote (Run 1.7
|
|
1700
|
+
// Problem A), decision.mode='full' even though scanType='incremental→full',
|
|
1701
|
+
// and walkSet may be populated by the still-modified file — but the inner
|
|
1702
|
+
// scan walked the full source tree, so files_scanned must be sourceFiles.length.
|
|
1703
|
+
files_scanned: decision.mode === 'incremental' && walkSet.size > 0
|
|
1704
|
+
? walkSet.size
|
|
1705
|
+
: sourceFiles.length,
|
|
1706
|
+
files_changed: filesChanged,
|
|
1707
|
+
prompts_found: promptScanResultHolder?.prompts.length,
|
|
1708
|
+
},
|
|
1709
|
+
};
|
|
268
1710
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// Store components and connections
|
|
274
|
-
await storeComponents(uniqueComponents, config, root);
|
|
275
|
-
await storeConnections(uniqueConnections, config, root);
|
|
276
|
-
// Build index, graph, file map, and summary
|
|
277
|
-
await buildIndex(config, root, projectMetadata);
|
|
278
|
-
await buildGraph(config, root);
|
|
279
|
-
await buildFileMap(config, root);
|
|
280
|
-
await buildSummary(config, root, promptScanResultHolder, projectMetadata);
|
|
281
|
-
// Persist prompt scan results if available
|
|
282
|
-
if (promptScanResultHolder) {
|
|
283
|
-
await savePromptScan(promptScanResultHolder, config, root);
|
|
284
|
-
}
|
|
285
|
-
// ==========================================================================
|
|
286
|
-
// Phase 5: Save File Hashes
|
|
287
|
-
// ==========================================================================
|
|
288
|
-
if (options.verbose) {
|
|
289
|
-
console.log('Phase 5: Saving file hashes...');
|
|
290
|
-
}
|
|
291
|
-
const fileHashes = await computeFileHashes(sourceFiles, root);
|
|
292
|
-
await saveHashes(fileHashes, config, root);
|
|
293
|
-
const duration = Date.now() - startTime;
|
|
294
|
-
const filesChanged = fileChanges
|
|
295
|
-
? fileChanges.added.length + fileChanges.modified.length + fileChanges.removed.length
|
|
296
|
-
: sourceFiles.length;
|
|
297
|
-
if (options.verbose) {
|
|
298
|
-
console.log(`\nScan complete in ${duration}ms`);
|
|
299
|
-
console.log(` Components: ${uniqueComponents.length}`);
|
|
300
|
-
console.log(` Connections: ${uniqueConnections.length}`);
|
|
301
|
-
console.log(` Files scanned: ${sourceFiles.length}`);
|
|
302
|
-
console.log(` Files changed: ${filesChanged}`);
|
|
303
|
-
console.log(` Warnings: ${allWarnings.length}`);
|
|
1711
|
+
finally {
|
|
1712
|
+
// Run 1.6 — item #4: release the scan lock on every exit path
|
|
1713
|
+
// (success, early-return, throw). Idempotent.
|
|
1714
|
+
lock.release();
|
|
304
1715
|
}
|
|
305
|
-
return {
|
|
306
|
-
components: uniqueComponents,
|
|
307
|
-
connections: uniqueConnections,
|
|
308
|
-
warnings: allWarnings,
|
|
309
|
-
fileChanges,
|
|
310
|
-
promptScan: promptScanResultHolder,
|
|
311
|
-
stats: {
|
|
312
|
-
scan_duration_ms: duration,
|
|
313
|
-
components_found: uniqueComponents.length,
|
|
314
|
-
connections_found: allConnections.length,
|
|
315
|
-
warnings_count: allWarnings.length,
|
|
316
|
-
files_scanned: sourceFiles.length,
|
|
317
|
-
files_changed: filesChanged,
|
|
318
|
-
prompts_found: promptScanResultHolder?.prompts.length,
|
|
319
|
-
},
|
|
320
|
-
};
|
|
321
1716
|
}
|
|
322
1717
|
/**
|
|
323
1718
|
* Quick scan - only packages, no code analysis
|
|
@@ -355,8 +1750,7 @@ export async function scanPromptsOnly(projectRoot, options = {}) {
|
|
|
355
1750
|
const result = await scanPrompts(root, {
|
|
356
1751
|
includeRawContent: true,
|
|
357
1752
|
detectVariables: true,
|
|
358
|
-
|
|
359
|
-
minCorroborationSignals: 2,
|
|
1753
|
+
aggressive: true,
|
|
360
1754
|
});
|
|
361
1755
|
// Attach tracer data
|
|
362
1756
|
if (traceResult) {
|
|
@@ -372,6 +1766,88 @@ export async function scanPromptsOnly(projectRoot, options = {}) {
|
|
|
372
1766
|
export { formatPromptsOutput, formatPromptDetail } from './scanners/prompts/index.js';
|
|
373
1767
|
// Re-export tracer types
|
|
374
1768
|
export { traceLLMCalls } from './scanners/connections/llm-call-tracer.js';
|
|
1769
|
+
/**
|
|
1770
|
+
* In-process debounce: tracks the timestamp of the last refresh ATTEMPT
|
|
1771
|
+
* per resolved project root. If a second call arrives while a first is still
|
|
1772
|
+
* in-flight (or was attempted within staleAfterMs), the second call returns
|
|
1773
|
+
* {refreshed:false, reason:'fresh'} without dispatching a second scan.
|
|
1774
|
+
*
|
|
1775
|
+
* This prevents a polling loop on MCP `status` from fanning out N concurrent
|
|
1776
|
+
* incremental scans. The map is module-scoped so it persists across calls
|
|
1777
|
+
* within the same Node.js process lifetime.
|
|
1778
|
+
*/
|
|
1779
|
+
const _lastRefreshAttemptMs = new Map();
|
|
1780
|
+
export async function autoRefreshIfStale(projectRoot, options = {}) {
|
|
1781
|
+
// Resolve opt-in / opt-out. Programmatic > env > default(true).
|
|
1782
|
+
const envOptOut = process.env['NAVGATOR_AUTO_REFRESH'] === 'false';
|
|
1783
|
+
const enabled = options.enabled ?? !envOptOut;
|
|
1784
|
+
if (!enabled) {
|
|
1785
|
+
return { refreshed: false, reason: 'disabled', message: 'auto-refresh disabled' };
|
|
1786
|
+
}
|
|
1787
|
+
const staleAfterMs = (options.staleAfterMinutes ?? 5) * 60 * 1000;
|
|
1788
|
+
const root = projectRoot || process.cwd();
|
|
1789
|
+
// Debounce: if a refresh was attempted for this root within staleAfterMs,
|
|
1790
|
+
// return 'fresh' immediately rather than piling on a second scan.
|
|
1791
|
+
// Stamp BEFORE any async work to close the race window — concurrent callers
|
|
1792
|
+
// see the guard the moment the first call passes it.
|
|
1793
|
+
const lastAttempt = _lastRefreshAttemptMs.get(root) ?? 0;
|
|
1794
|
+
if (Date.now() - lastAttempt < staleAfterMs) {
|
|
1795
|
+
return { refreshed: false, reason: 'fresh', message: 'refresh already in-flight or recently attempted' };
|
|
1796
|
+
}
|
|
1797
|
+
_lastRefreshAttemptMs.set(root, Date.now());
|
|
1798
|
+
try {
|
|
1799
|
+
const { loadIndex } = await import('./storage.js');
|
|
1800
|
+
const config = getConfig();
|
|
1801
|
+
const index = await loadIndex(config, root);
|
|
1802
|
+
if (!index || !index.last_scan) {
|
|
1803
|
+
return {
|
|
1804
|
+
refreshed: false,
|
|
1805
|
+
reason: 'no-index',
|
|
1806
|
+
message: 'no prior scan — run `navgator scan` first',
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
const ageMs = Date.now() - index.last_scan;
|
|
1810
|
+
if (ageMs < staleAfterMs) {
|
|
1811
|
+
return {
|
|
1812
|
+
refreshed: false,
|
|
1813
|
+
reason: 'fresh',
|
|
1814
|
+
message: `graph fresh (${Math.round(ageMs / 1000)}s old)`,
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
// Stale → run incremental. selectScanMode will pick the right mode
|
|
1818
|
+
// internally; on a "no changes" hit it returns almost immediately.
|
|
1819
|
+
const scanFn = options.scanImpl ?? scan;
|
|
1820
|
+
const result = await scanFn(root, { mode: 'incremental' });
|
|
1821
|
+
const changed = (result.fileChanges?.added.length ?? 0) +
|
|
1822
|
+
(result.fileChanges?.modified.length ?? 0) +
|
|
1823
|
+
(result.fileChanges?.removed.length ?? 0);
|
|
1824
|
+
// Stamp coherence: this incremental scan covered every changed file via
|
|
1825
|
+
// hashes, so the freshness ledger/stamp must be reconciled or the stamp
|
|
1826
|
+
// would lie. Best-effort — never fail the refresh on freshness bookkeeping.
|
|
1827
|
+
try {
|
|
1828
|
+
const { reconcileClean } = await import('./freshness/drainer.js');
|
|
1829
|
+
await reconcileClean(root);
|
|
1830
|
+
}
|
|
1831
|
+
catch {
|
|
1832
|
+
/* freshness subsystem is optional; ignore */
|
|
1833
|
+
}
|
|
1834
|
+
return {
|
|
1835
|
+
refreshed: true,
|
|
1836
|
+
reason: 'stale',
|
|
1837
|
+
filesChanged: changed,
|
|
1838
|
+
message: changed > 0
|
|
1839
|
+
? `↻ refreshed ${changed} changed file(s)`
|
|
1840
|
+
: '↻ refreshed (no file changes)',
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
catch (err) {
|
|
1844
|
+
return {
|
|
1845
|
+
refreshed: false,
|
|
1846
|
+
reason: 'error',
|
|
1847
|
+
message: `auto-refresh failed: ${err instanceof Error ? err.message : 'unknown'}`,
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
375
1851
|
/**
|
|
376
1852
|
* Get scan status/summary without running a full scan
|
|
377
1853
|
*/
|