fallow 2.88.3 → 2.90.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fallow",
3
- "version": "2.88.3",
3
+ "version": "2.90.0",
4
4
  "description": "Deterministic codebase intelligence for TypeScript and JavaScript. Quality, risk, architecture, dependencies, duplication, and safe cleanup evidence for humans, CI, and agents. Optional runtime intelligence layer (Fallow Runtime) adds production execution evidence. Rust-native, sub-second, 96 framework plugins.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -83,13 +83,13 @@
83
83
  "@tanstack/intent": "0.0.41"
84
84
  },
85
85
  "optionalDependencies": {
86
- "@fallow-cli/darwin-arm64": "2.88.3",
87
- "@fallow-cli/darwin-x64": "2.88.3",
88
- "@fallow-cli/linux-x64-gnu": "2.88.3",
89
- "@fallow-cli/linux-arm64-gnu": "2.88.3",
90
- "@fallow-cli/linux-x64-musl": "2.88.3",
91
- "@fallow-cli/linux-arm64-musl": "2.88.3",
92
- "@fallow-cli/win32-arm64-msvc": "2.88.3",
93
- "@fallow-cli/win32-x64-msvc": "2.88.3"
86
+ "@fallow-cli/darwin-arm64": "2.90.0",
87
+ "@fallow-cli/darwin-x64": "2.90.0",
88
+ "@fallow-cli/linux-x64-gnu": "2.90.0",
89
+ "@fallow-cli/linux-arm64-gnu": "2.90.0",
90
+ "@fallow-cli/linux-x64-musl": "2.90.0",
91
+ "@fallow-cli/linux-arm64-musl": "2.90.0",
92
+ "@fallow-cli/win32-arm64-msvc": "2.90.0",
93
+ "@fallow-cli/win32-x64-msvc": "2.90.0"
94
94
  }
95
95
  }
package/schema.json CHANGED
@@ -1188,11 +1188,11 @@
1188
1188
  ]
1189
1189
  },
1190
1190
  "SecurityConfig": {
1191
- "description": "Scopes the data-driven security matcher catalogue used by `fallow security`.\nAn absent block (or both `include`/`exclude` unset) admits every category.",
1191
+ "description": "Scopes the security categories used by `fallow security`. An absent block\nadmits every catalogue category. `hardcoded-secret` is include-required and\nonly runs when explicitly listed in `security.categories.include`.",
1192
1192
  "type": "object",
1193
1193
  "properties": {
1194
1194
  "categories": {
1195
- "description": "Include/exclude filter over catalogue category ids (e.g. `dangerous-html`).",
1195
+ "description": "Include/exclude filter over category ids (e.g. `dangerous-html`).",
1196
1196
  "anyOf": [
1197
1197
  {
1198
1198
  "$ref": "#/$defs/SecurityCategories"
@@ -1206,7 +1206,7 @@
1206
1206
  "additionalProperties": false
1207
1207
  },
1208
1208
  "SecurityCategories": {
1209
- "description": "Include/exclude lists scoping the active security matcher categories. When\n`include` is set, only those categories are active; `exclude` removes\ncategories from the admitted set. Both unset admits every category.",
1209
+ "description": "Include/exclude lists scoping the active security categories. When `include`\nis set, only those categories are active; `exclude` removes categories from\nthe admitted set. Both unset admits catalogue categories. `hardcoded-secret`\nstill requires explicit inclusion.",
1210
1210
  "type": "object",
1211
1211
  "properties": {
1212
1212
  "include": {
@@ -1762,7 +1762,15 @@
1762
1762
  "CacheConfig": {
1763
1763
  "type": "object",
1764
1764
  "properties": {
1765
+ "dir": {
1766
+ "description": "Directory for fallow's persistent analysis cache. Relative paths resolve\nfrom the project root.",
1767
+ "type": [
1768
+ "string",
1769
+ "null"
1770
+ ]
1771
+ },
1765
1772
  "maxSizeMb": {
1773
+ "description": "Maximum size of the persistent extraction cache, in megabytes.",
1766
1774
  "type": [
1767
1775
  "integer",
1768
1776
  "null"
@@ -75,7 +75,7 @@ cargo install fallow-cli # build from source
75
75
  | `health` | Function complexity analysis (also covers Angular templates as synthetic `<template>` findings: external `.html` files via `templateUrl` AND inline `@Component({ template: \`...\` })` literals; suppress external with `<!-- fallow-ignore-file complexity -->` at the top of the `.html` file, suppress inline with `// fallow-ignore-next-line complexity` directly above the `@Component` decorator) | `--complexity`, `--max-cyclomatic`, `--max-cognitive`, `--max-crap`, `--top`, `--sort`, `--file-scores`, `--hotspots`, `--ownership`, `--ownership-emails`, `--targets`, `--effort`, `--score`, `--min-score`, `--since`, `--min-commits`, `--save-snapshot`, `--trend`, `--coverage-gaps`, `--coverage`, `--coverage-root`, `--runtime-coverage`, `--min-invocations-hot`, `--min-observation-volume`, `--low-traffic-threshold`, `--workspace`, `--changed-workspaces`, `--baseline`, `--save-baseline` |
76
76
  | `audit` | Combined dead-code + complexity + duplication for changed files | `--base`, `--gate`, `--production`, `--production-dead-code`, `--production-health`, `--production-dupes`, `--workspace`, `--changed-workspaces`, `--ci`, `--fail-on-issues`, `--explain`, `--explain-skipped`, `--dead-code-baseline`, `--health-baseline`, `--dupes-baseline`, `--max-crap`, `--coverage`, `--coverage-root`, `--include-entry-exports` |
77
77
  | `flags` | Detect feature flag patterns (env vars, SDK calls, config objects) | `--top` |
78
- | `security` | Surface opt-in local security candidates for agent verification (not confirmed vulnerabilities). Two rule families: the graph rule `client-server-leak` (a `"use client"` file reaching a non-public `process.env` secret) and a data-driven `tainted-sink` catalogue across 9 CWE categories (dangerous-html, command-injection, code-injection, sql-injection, ssrf, path-traversal, open-redirect, weak-crypto, unsafe-deserialization). Conservative non-literal trigger; parameterized SQL not flagged. Rules default off; suppress a file with `// fallow-ignore-file security-sink`; scope categories with `security.categories`. | `--format human|json|sarif`, `--changed-since`, `--diff-file`, `--workspace`, `--changed-workspaces`, `--ci`, `--fail-on-issues`, `--sarif-file`, `--summary` |
78
+ | `security` | Surface opt-in local security candidates for agent verification (not confirmed vulnerabilities). Rule families include the graph rule `client-server-leak`, a data-driven `tainted-sink` catalogue, and the include-required `hardcoded-secret` category for provider-prefix credentials and high-entropy literals assigned to secret-shaped identifiers. Most catalogue rows require non-literal input; narrowly literal-aware rows flag deterministic unsafe literals. Rules default off; suppress a file with `// fallow-ignore-file security-sink`; scope categories with `security.categories`. `hardcoded-secret` runs only when listed in `security.categories.include`. | `--format human|json|sarif`, `--changed-since`, `--file`, `--diff-file`, `--workspace`, `--changed-workspaces`, `--ci`, `--fail-on-issues`, `--sarif-file`, `--summary` |
79
79
  | `explain` | Explain one issue type without running analysis | `<issue-type>`, `--format json` |
80
80
  | `license` | Manage the local license JWT for continuous/cloud runtime monitoring (activate, status, refresh, deactivate) | `activate --trial --email <addr>`, `activate --from-file`, `activate --stdin`, `status`, `refresh`, `deactivate` |
81
81
  | `telemetry` | Manage opt-in, off-by-default product telemetry (never collects code, paths, or names). Agents must not enable it; only the user may | `status`, `enable`, `disable`, `inspect --example` |
@@ -118,11 +118,11 @@ When using fallow via MCP (`fallow-mcp`), the following tools are available:
118
118
  |------|-------------|
119
119
  | `analyze` | Full dead code analysis (unused files/exports/types/dependencies/members + circular dependencies + re-export cycles (barrel files that form a structural loop, silently breaking re-exports) + boundary violations + stale suppressions). Private type leaks are an opt-in API hygiene check via `issue_types: ["private-type-leaks"]`. Set `boundary_violations: true` as a convenience alias for `issue_types: ["boundary-violations"]`. Set `group_by` to `"owner"`, `"directory"`, `"package"`, or `"section"` to partition results. The `section` mode reads GitLab CODEOWNERS `[Section]` headers and emits `owners` metadata per group |
120
120
  | `check_changed` | Incremental analysis of files changed since a git ref |
121
- | `security_candidates` | Unverified local security candidates, not confirmed vulnerabilities (`fallow security --format json`). Read `security_findings[]` for category, CWE, evidence, and trace; verify trace and evidence before editing code. Supports `root`, `config`, `workspace`, `changed_since`, `changed_workspaces`, `no_cache`, and `threads`; inherits `FALLOW_DIFF_FILE` from the server environment for line-level diff scoping; raise `FALLOW_TIMEOUT_SECS` for large repos. |
121
+ | `security_candidates` | Unverified local security candidates, not confirmed vulnerabilities (`fallow security --format json`). Read `security_findings[]` for category, CWE, evidence, trace, optional `reachability`, and blind-spot counters. Each finding also carries an agent-actionable `candidate` (`source_kind`/`sink`/`boundary`), an optional `taint_flow` source-to-sink triple, and a stable `finding_id` (equal to the SARIF fingerprint) for cross-run correlation; there is no `impact` field (deciding exploitability is the agent's job). `reachability.untrusted_source_trace` is module-level import context only and does not prove value flow. Verify trace, reachability context, and evidence before editing code. Supports `root`, `config`, `workspace`, `paths`, `changed_since`, `changed_workspaces`, `no_cache`, and `threads`; `paths` forwards repeated `fallow security --file` filters for finding anchors, trace hops, or untrusted-source reachability trace hops. Inherits `FALLOW_DIFF_FILE` from the server environment for line-level diff scoping; raise `FALLOW_TIMEOUT_SECS` for large repos. |
122
122
  | `find_dupes` | Code duplication detection. Set `changed_since` to scope to changed files since a git ref. Set `min_occurrences` (≥ 2, default 2) to hide pair-only clones and focus on widespread copy-paste; JSON gains `stats.clone_groups_below_min_occurrences` when the filter hides anything. Each `clone_groups[]` entry carries a stable `fingerprint`, usually `dup:<8hex>` and widened only on rare report collisions; pass it to `trace_clone` to deep-dive that group |
123
123
  | `fix_preview` | Dry-run auto-fix preview |
124
124
  | `fix_apply` | Apply auto-fixes (destructive) |
125
- | `check_health` | Complexity metrics, health scores, hotspots, and refactoring targets. Optional `runtime_coverage` merges a V8 or Istanbul dump; tune it with `min_invocations_hot` (default 100), `min_observation_volume` (default 5000), and `low_traffic_threshold` (default 0.001). When runtime evidence combines with static usage, test coverage, CRAP/complexity, ownership, or change scope, read `coverage_intelligence` for stable `fallow:coverage-intel:<hash>` recommendations. Set `group_by` to `owner`, `directory`, `package`, or `section` for per-group `vital_signs` / `health_score`; SARIF results gain `properties.group`, CodeClimate issues gain a top-level `group` field |
125
+ | `check_health` | Complexity metrics, health scores, hotspots, and refactoring targets. Set `complexity_breakdown: true` to add a per-decision-point `contributions[]` array to each complexity finding (each `else-if`, nested `if`, boolean operator, loop, `case`, etc. with its source line and cyclomatic/cognitive weight) so you can explain WHY a function scored high and pinpoint refactor targets. Optional `runtime_coverage` merges a V8 or Istanbul dump; tune it with `min_invocations_hot` (default 100), `min_observation_volume` (default 5000), and `low_traffic_threshold` (default 0.001). When runtime evidence combines with static usage, test coverage, CRAP/complexity, ownership, or change scope, read `coverage_intelligence` for stable `fallow:coverage-intel:<hash>` recommendations. Set `group_by` to `owner`, `directory`, `package`, or `section` for per-group `vital_signs` / `health_score`; SARIF results gain `properties.group`, CodeClimate issues gain a top-level `group` field |
126
126
  | `check_runtime_coverage` | Merge V8 or Istanbul runtime-coverage data into the health report. One local capture is free; continuous/cloud or multi-capture runtime monitoring is paid. Required `coverage` param (V8 dir, V8 JSON, or Istanbul `coverage-final.json`). Tuning knobs: `min_invocations_hot` (default 100), `min_observation_volume` (default 5000), `low_traffic_threshold` (default 0.001), `max_crap` (default 30.0), `top`, `group_by`. Cloud runtime rows can expose `resolutionStatus` / `mappingQuality` on function-list JSON and `resolution_status` / `mapping_quality` in runtime-context JSON. Use `coverage_intelligence` and the confidence table below before acting on file-level runtime signals. Long dumps may exceed the 120s MCP timeout; raise `FALLOW_TIMEOUT_SECS`. Pick this over `check_health` when you have a coverage dump. |
127
127
  | `get_hot_paths` | Runtime-context slice over the same runtime coverage pipeline. Same params as `check_runtime_coverage`; read `runtime_coverage.hot_paths` for production hot paths. |
128
128
  | `get_blast_radius` | Runtime-context slice for blast-radius review. Same params as `check_runtime_coverage`; read `runtime_coverage.blast_radius` for stable `fallow:blast:<hash>` IDs, caller counts, traffic-weighted caller reach, optional cloud deploy touch counts, and low/medium/high risk bands. |
@@ -366,6 +366,8 @@ Config fields:
366
366
  - `ignoreExportsUsedInFile`: knip-compatible; suppress unused-export findings when the exported symbol is referenced inside the file that declares it. Boolean (`true` covers all kinds) or `{ "type": true, "interface": true }` object form for knip parity. Fallow groups type aliases and interfaces under the same `unused-types` issue, so both type-kind fields behave identically. References inside the export specifier itself (`export { foo }`, `export default foo`) do not count as same-file uses; those exports are still reported when no other in-file expression references the binding
367
367
  - `publicPackages`: workspace packages that are public libraries; exported API surface from these packages is not flagged as unused
368
368
  - `dynamicallyLoaded`: glob patterns for files loaded at runtime (plugin dirs, locale files); treated as always-used
369
+ - `cache.dir`: override the persistent extraction cache directory. `FALLOW_CACHE_DIR` wins over this config field, and `--no-cache` disables caching entirely
370
+ - `cache.maxSizeMb`: cap the serialized extraction cache size in megabytes. `FALLOW_CACHE_MAX_SIZE` wins over this config field
369
371
  - `usedClassMembers`: class method/property names that extend the built-in Angular/React lifecycle allowlist with framework-invoked names. Each entry is a plain string (global suppression) or a scoped object `{ extends?, implements?, members }` matching only classes with the given heritage. Strings can be exact names (`"agInit"`) or glob patterns (`"*"` matches every member, `"enter*"` prefix, `"*Handler"` suffix, `"on*Event"` combined). Use scoped rules for common names like `refresh` or `execute` to avoid false negatives on unrelated classes; global strings for unique names like `agInit`. Example: `["agInit", { "implements": "ICellRendererAngularComp", "members": ["refresh"] }, { "extends": "BaseCommand", "members": ["execute"] }, { "extends": "GrammarBaseListener", "members": ["enter*", "exit*"] }]`. Glob patterns that match zero members emit a `WARN` so dead allowlist entries surface. An unconstrained scoped rule (no `extends` or `implements`) is rejected at load time. Use plugin-level `usedClassMembers` in a `.fallow/plugins/*.jsonc` file for library-specific allowlists
370
372
  - `resolve.conditions`: additional package.json `exports` / `imports` condition names to honor during module resolution. Baseline conditions (`development`, `import`, `require`, `default`, `types`, `node`, plus `react-native` / `browser` under RN/Expo) are always included; user entries prepend ahead of them. Use for community conditions like `worker`, `edge-light`, `deno`, or custom bundler conditions. Example: `{ "resolve": { "conditions": ["worker", "edge-light"] } }`
371
373
 
@@ -40,9 +40,11 @@ Analyzes the project for unused files, exports, dependencies, types, members, an
40
40
  |------|------|---------|-------------|
41
41
  | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab` | `human` | Output format |
42
42
  | `--quiet` | bool | `false` | Suppress progress bars and timing on stderr |
43
+ | `-o, --output-file` | path | (none) | Write the report to a file instead of stdout, for any `--format` (no ANSI codes). Progress and a confirmation stay on stderr (suppressed by `--quiet`). Valid with dead-code/dupes/health/security/bare; composes with `--sarif-file`. |
43
44
  | `--legacy-envelope` | bool | `false` | Remove the top-level `kind` field from typed JSON roots for one migration cycle |
44
45
  | `--changed-since` | string | — | Only analyze files changed since a git ref (e.g., `main`, `HEAD~3`) |
45
46
  | `--production` | bool | `false` | Exclude test/dev files, only start/build scripts (applies to every analysis) |
47
+ | `--no-production` | bool | `false` | Force production mode off, overriding a project config's `production: true` (applies to every analysis; conflicts with `--production`) |
46
48
  | `--production-dead-code` | bool | `false` | Per-analysis production mode for dead-code. Bare combined runs and `fallow audit` only. |
47
49
  | `--production-health` | bool | `false` | Per-analysis production mode for health. Bare combined runs and `fallow audit` only. |
48
50
  | `--production-dupes` | bool | `false` | Per-analysis production mode for duplication. Bare combined runs and `fallow audit` only. |
@@ -51,7 +53,14 @@ Analyzes the project for unused files, exports, dependencies, types, members, an
51
53
  | `--workspace` | string | — | Scope to one or more workspaces. Comma-separated values, globs (`apps/*`, `@scope/*`), and `!`-prefixed negation (`!apps/legacy`) supported. Matched against package name AND workspace path relative to repo root. |
52
54
  | `--changed-workspaces` | string (git ref) | — | Git-derived monorepo CI scoping: scope to workspaces containing any file changed since `REF` (e.g. `origin/main`). Auto-derives the workspace set from `git diff`. Mutually exclusive with `--workspace`. Missing ref is a hard error (exit 2), not silent full-scope fallback. |
53
55
  | `--include-dupes` | bool | `false` | Cross-reference with duplication findings |
56
+ | `--dupes-mode` | enum | `config` | Override duplicate detection mode in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --mode`. |
57
+ | `--dupes-threshold` | number | `config` | Override the duplication percentage failure threshold in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --threshold`. |
58
+ | `--dupes-min-tokens` | number | `config` | Override the minimum token count for clone detection in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --min-tokens`. |
59
+ | `--dupes-min-lines` | number | `config` | Override the minimum line count for clone detection in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --min-lines`. |
54
60
  | `--dupes-min-occurrences` | number | `config` | Override the minimum clone occurrences in combined mode (must be >= 2). Falls back to the config value when unset. Mirrors the standalone `dupes --min-occurrences`. |
61
+ | `--dupes-skip-local` | bool | `config` | Only report cross-directory duplicates in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --skip-local`. |
62
+ | `--dupes-cross-language` | bool | `config` | Enable TypeScript to JavaScript duplicate matching in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --cross-language`. |
63
+ | `--dupes-ignore-imports` | bool | `config` | Exclude import declarations from duplicate detection in combined mode. Falls back to the config value when unset. Mirrors the standalone `dupes --ignore-imports`. |
55
64
  | `--file` | path (multiple) | — | Scope output to specific files. Only issues in the specified files are reported. Project-wide dependency issues are suppressed. Warns on non-existent paths. Useful for lint-staged |
56
65
  | `--include-entry-exports` | bool | `false` | Report unused exports in entry files (package.json `main`/`exports`, framework pages). Catches typos like `meatdata` vs `metadata`. Global flag, also accepted on combined mode (`fallow --include-entry-exports`) and `fallow audit`. Also configurable as `includeEntryExports: true` in fallow config |
57
66
  | `--trace` | `FILE:EXPORT` | — | Trace export usage chain |
@@ -227,7 +236,7 @@ Auto-removes unused exports, dependencies, enum members, and pnpm catalog entrie
227
236
 
228
237
  ### On-disk drift protection
229
238
 
230
- `fallow fix` captures every parsed source file's xxh3 content hash during the in-process analysis and recomputes it at fix time. Files whose hash drifted between analysis and write (parallel editor save, CI rebase, concurrent tool) are skipped with `{"type": "skipped", "path": "...", "skipped": true, "skip_reason": "content_changed"}` in the JSON output and `Skipping <path>: file content changed since fallow check ran. Re-run fallow fix to refresh the analysis first.` on stderr (gated on non-quiet). A run with any content-changed skip exits with code 2 so CI does not treat the partial run as a clean no-op. The JSON envelope's top-level `skipped_content_changed: number` is always present and disjoint from `skipped` (which still tallies catalog / YAML guard skips only). Per-file writes are batched: each rewrite is staged to a sibling temp file, and the orchestrator promotes the batch only after every stage succeeds. A stage failure leaves every target file at its original content. Hash precondition covers source files (TS, JS, Vue, Svelte, Astro, MDX); `package.json` and `pnpm-workspace.yaml` are not in the captured hash map because the extract layer does not parse them, but the dep and catalog fixers re-parse those files at fix time as the natural safety net.
239
+ `fallow fix` captures every parsed source file's xxh3 content hash during the in-process analysis and recomputes it at fix time. Files whose hash drifted between analysis and write (parallel editor save, CI rebase, concurrent tool) are skipped with `{"type": "skipped", "path": "...", "skipped": true, "skip_reason": "content_changed"}` in the JSON output and `Skipping <path>: file content changed since fallow dead-code ran. Re-run fallow fix to refresh the analysis first.` on stderr (gated on non-quiet). A run with any content-changed skip exits with code 2 so CI does not treat the partial run as a clean no-op. The JSON envelope's top-level `skipped_content_changed: number` is always present and disjoint from `skipped` (which still tallies catalog / YAML guard skips only). Per-file writes are batched: each rewrite is staged to a sibling temp file, and the orchestrator promotes the batch only after every stage succeeds. A stage failure leaves every target file at its original content. Hash precondition covers source files (TS, JS, Vue, Svelte, Astro, MDX); `package.json` and `pnpm-workspace.yaml` are not in the captured hash map because the extract layer does not parse them, but the dep and catalog fixers re-parse those files at fix time as the natural safety net.
231
240
 
232
241
  ### Low-confidence export removals
233
242
 
@@ -236,7 +245,7 @@ Issue #602: `fallow fix` withholds unused-export removals when the consumer may
236
245
  - **Off-graph consumer directories.** The file is under any of `__mocks__`, `__fixtures__`, `fixtures`, `e2e`, `e2e-tests`, `cypress`, `playwright`, `examples`, `evals`, `golden` (matched on any path segment). Catches Vitest mock aliases, off-workspace e2e suites, and fixture / golden harnesses. Plain `test` / `tests` / `__tests__` are deliberately NOT on the list, so genuinely-dead test helpers still auto-remove.
237
246
  - **Files with an unresolved import.** The file itself imports something fallow could not resolve, so its local usage graph is incomplete.
238
247
 
239
- JSON output carries `{"type": "skipped", "path": "...", "skipped": true, "skip_reason": "low_confidence_off_graph"}` (or `"low_confidence_unresolved_imports"`) plus a top-level counter `skipped_low_confidence_exports: number` (always present), disjoint from `skipped`. Unlike the drift and encoding skips this is INTENTIONAL and does NOT change the exit code; the export stays reported by `fallow check` for manual review. High-confidence exports in normal source files are removed unchanged. The AI agent should report kept exports to the user and let them decide whether the export is truly unused before removing it by hand.
248
+ JSON output carries `{"type": "skipped", "path": "...", "skipped": true, "skip_reason": "low_confidence_off_graph"}` (or `"low_confidence_unresolved_imports"`) plus a top-level counter `skipped_low_confidence_exports: number` (always present), disjoint from `skipped`. Unlike the drift and encoding skips this is INTENTIONAL and does NOT change the exit code; the export stays reported by `fallow dead-code` for manual review. High-confidence exports in normal source files are removed unchanged. The AI agent should report kept exports to the user and let them decide whether the export is truly unused before removing it by hand.
240
249
 
241
250
  ### File encoding contract
242
251
 
@@ -287,7 +296,7 @@ fallow list --workspaces --format json --quiet
287
296
  fallow workspaces --format json --quiet # alias of `fallow list --workspaces`
288
297
  ```
289
298
 
290
- The `--workspaces` JSON output carries `workspaces[]` (name, project-root-relative path, `is_internal_dependency` bool) plus `workspace_diagnostics[]`. Each diagnostic has a `kind` discriminator (`undeclared-workspace`, `malformed-package-json`, `glob-matched-no-package-json`, `malformed-tsconfig`, `tsconfig-reference-dir-missing`) with a typed payload (`error`, `pattern`, or none). The same `workspace_diagnostics[]` array is also surfaced on `fallow check --format json`, `fallow dupes --format json`, and `fallow health --format json` envelopes (omitted when empty). A malformed ROOT `package.json` exits 2 at config load; everything else warns and continues.
299
+ The `--workspaces` JSON output carries `workspaces[]` (name, project-root-relative path, `is_internal_dependency` bool) plus `workspace_diagnostics[]`. Each diagnostic has a `kind` discriminator (`undeclared-workspace`, `malformed-package-json`, `glob-matched-no-package-json`, `malformed-tsconfig`, `tsconfig-reference-dir-missing`) with a typed payload (`error`, `pattern`, or none). The same `workspace_diagnostics[]` array is also surfaced on `fallow dead-code --format json`, `fallow dupes --format json`, and `fallow health --format json` envelopes (omitted when empty). A malformed ROOT `package.json` exits 2 at config load; everything else warns and continues.
291
300
 
292
301
  The `--boundaries` JSON output carries `boundaries.logical_groups[]` alongside the existing `zones[]` / `rules[]` arrays. Each logical-group entry surfaces a user-authored `autoDiscover` parent zone (which expansion otherwise flattens into per-child zones like `features/auth` / `features/billing`): `name`, `children`, `auto_discover` (verbatim user strings), `status` (`ok` / `empty` / `invalid_path`), `source_zone_index`, summed `file_count`, optional `authored_rule` (the pre-expansion `{ allow, allowTypeOnly }` keyed on the parent), optional `fallback_zone` cross-reference when the parent also kept its own `patterns` (Bulletproof case), optional `merged_from` (parent zone indices when the user declared the same parent name twice; surfaces the duplicate in JSON instead of only in `tracing::warn!`), optional `original_zone_root` (echo of the parent's `root` subtree scope for monorepo patchers), and optional `child_source_indices` (parallel to `children`, attributing each child to a specific `auto_discover` entry when multiple paths were authored). The full shape is documented in `docs/output-schema.json` under `ListBoundariesOutput`.
293
302
 
@@ -368,6 +377,7 @@ Angular templates contribute synthetic `<template>` complexity findings whenever
368
377
  | `--top` | number | — | Only show the top N most complex functions (and file scores/hotspots/targets) |
369
378
  | `--sort` | `cyclomatic\|cognitive\|lines\|severity` | `cyclomatic` | Sort order for complexity findings |
370
379
  | `--complexity` | bool | `false` | Show only function complexity findings. When no section flags are set, all sections are shown by default. |
380
+ | `--complexity-breakdown` | bool | `false` | Add a per-decision-point `contributions[]` array to each complexity finding in `--format json`. Each entry names the construct (`if`, `else-if`, `ternary`, boolean operator, loop, `case`, `catch`, `optional-chain`, ...) and carries its source line, the metric it adds to (`cyclomatic` or `cognitive`), its weight, and the nesting depth, so a consumer can explain WHY a function scored high. Off by default (no change to existing JSON/SARIF/markdown). Used by the VS Code inline editor breakdown and the MCP `check_health` `complexity_breakdown` param. |
371
381
  | `--file-scores` | bool | `false` | Show only per-file health scores (maintainability index, LOC, fan-in, fan-out, dead code ratio, complexity density, CRAP risk). Runs the full analysis pipeline. Sorted by risk-aware triage concern: lower maintainability index and higher CRAP risk first. When no section flags are set, all sections are shown by default. |
372
382
  | `--hotspots` | bool | `false` | Show only hotspots: files that are both complex and frequently changing. Combines git churn history with complexity data. Requires a git repository. When no section flags are set, all sections are shown by default. |
373
383
  | `--targets` | bool | `false` | Show only refactoring targets: ranked recommendations based on complexity, coupling, churn, and dead code signals. Categories: churn+complexity, circular dep, high impact, dead code, complexity, coupling. When no section flags are set, all sections are shown by default. |
@@ -376,8 +386,9 @@ Angular templates contribute synthetic `<template>` complexity findings whenever
376
386
  | `--min-score` | number | — | Fail (exit 1) only when the health score is below this threshold. Implies `--score`. Authoritative CI quality gate: when set, complexity findings are demoted to informational and the exit code is driven solely by the score, so `--min-score 0` always exits 0. Composes with `--min-severity`. |
377
387
  | `--min-severity` | `moderate\|high\|critical` | — | Only exit with an error for findings at or above this severity. Composes with `--min-score` (the run fails if either gate trips). |
378
388
  | `--report-only` | bool | `false` | Print the score and findings but never fail CI (always exit 0). Advisory mode. Mutually exclusive with `--min-score` and `--min-severity`. |
379
- | `--since` | string | `6m` | Git history window for hotspot analysis. Accepts durations (`6m`, `90d`, `1y`, `2w`) or ISO dates (`2025-06-01`). |
389
+ | `--since` | string | `6m` | Git history window for hotspot analysis. Accepts durations (`6m`, `90d`, `1y`, `2w`) or ISO dates (`2025-06-01`). Ignored when `--churn-file` is set. |
380
390
  | `--min-commits` | number | `3` | Minimum number of commits for a file to be included in hotspot ranking. |
391
+ | `--churn-file` | string | (none) | Import change history from a `fallow-churn/v1` JSON file (`{schema, events:[{path, timestamp, author, added, deleted}]}`, one entry per changed file per commit) instead of `git log`, so `--hotspots`/`--ownership`/`--targets` work on non-git VCS (Yandex Arc, Mercurial, Perforce). Resolved relative to `--root`; wins over git. Authoritative for the window, so `--since` then only labels output. Malformed file exits 2; `audit`/`impact`/`--changed-since` still require git. |
381
392
  | `--ownership` | bool | `false` | Attach ownership signals to hotspot entries: bus factor (Avelino truck factor), contributor count, top contributor with stale-days, recent contributors (top-3), `suggested_reviewers`, declared CODEOWNERS owner, `ownership_state`, ownership drift, unowned-hotspot detection. Human output gains a project-level summary line. JSON adds `low-bus-factor`, `unowned-hotspot`, `ownership-drift` action types. Test files get a `[test]` tag. Implies `--hotspots`. Requires git. |
382
393
  | `--ownership-emails` | `raw\|handle\|anonymized\|hash` | `handle` | Privacy mode for author emails. `handle` shows the local-part only (default, with GitHub noreply unwrap and deterministic same-handle disambiguation). `anonymized` emits stable `xxh3:` pseudonyms; `hash` remains accepted as the legacy spelling. `raw` shows full addresses. Use `anonymized` in regulated environments. Implies `--ownership`. Configure default via `health.ownership.emailMode`. |
383
394
  | `--changed-since` | string | — | Only analyze files changed since a git ref |
@@ -486,7 +497,7 @@ fallow health --format json --quiet --trend
486
497
  {
487
498
  "kind": "health",
488
499
  "schema_version": 7,
489
- "version": "2.88.3",
500
+ "version": "2.90.0",
490
501
  "elapsed_ms": 32,
491
502
  "summary": {
492
503
  "files_analyzed": 482,
@@ -796,6 +807,7 @@ Audits changed files for dead code, complexity, and duplication. Returns a verdi
796
807
  | `--base` | string | auto-detect | Git ref to compare against (alias for `--changed-since`) |
797
808
  | `--gate` | `new-only\|all` | `new-only` | Which findings affect the verdict. `new-only` gates only introduced findings; `all` gates every finding in changed files and skips the extra base-snapshot attribution pass. |
798
809
  | `--production` | bool | false | Exclude test/story/dev files (applies to dead-code, health, and dupes) |
810
+ | `--no-production` | bool | false | Force production mode off, overriding a project config's `production: true` (conflicts with `--production`) |
799
811
  | `--production-dead-code` | bool | false | Per-analysis production mode for the dead-code sub-analysis only |
800
812
  | `--production-health` | bool | false | Per-analysis production mode for the health sub-analysis only |
801
813
  | `--production-dupes` | bool | false | Per-analysis production mode for the duplication sub-analysis only |
@@ -876,7 +888,7 @@ fallow audit \
876
888
  {
877
889
  "kind": "audit",
878
890
  "schema_version": 7,
879
- "version": "2.88.3",
891
+ "version": "2.90.0",
880
892
  "command": "audit",
881
893
  "verdict": "fail",
882
894
  "changed_files_count": 12,
@@ -949,7 +961,7 @@ fallow flags --format json --quiet --workspace my-package
949
961
  ```json
950
962
  {
951
963
  "schema_version": 7,
952
- "version": "2.88.3",
964
+ "version": "2.90.0",
953
965
  "elapsed_ms": 116,
954
966
  "feature_flags": [],
955
967
  "total_flags": 0
@@ -964,21 +976,57 @@ Surfaces local security candidates for agent or human verification. The first ru
964
976
 
965
977
  Findings are not confirmed vulnerabilities. Use the structural trace to verify whether the value can actually reach client-bundled code. Public env conventions (`NODE_ENV`, `NEXT_PUBLIC_*`, `VITE_*`, `NUXT_PUBLIC_*`, `REACT_APP_*`, `PUBLIC_*`, `GATSBY_*`, `EXPO_PUBLIC_*`, `STORYBOOK_*`) are excluded.
966
978
 
967
- The second rule family is a data-driven `tainted-sink` catalogue: syntactic dangerous-sink candidates across 9 CWE categories. A candidate fires only when the relevant argument is non-literal, so a fully-literal value (`el.innerHTML = "<b>x</b>"`, `child_process.exec("ls")`) never fires; fallow prefers false-negatives over false-positives.
979
+ The second rule family is a data-driven `tainted-sink` catalogue: syntactic dangerous-sink candidates across the catalogue categories listed below. Most rows require a non-literal argument; narrowly literal-aware rows flag deterministic unsafe literals such as wildcard `postMessage` origins, weak crypto algorithms, disabled TLS validation, and JWT algorithm issues. Fallow prefers false-negatives over false-positives.
968
980
 
969
981
  | Category | CWE | Sink |
970
982
  |----------|-----|------|
971
983
  | `dangerous-html` | 79 | `innerHTML` / `outerHTML` / `insertAdjacentHTML` / `dangerouslySetInnerHTML` |
984
+ | `template-escape-bypass` | 79 | template-engine `SafeString(...)` wrapping a non-literal value |
972
985
  | `command-injection` | 78 | `child_process` `exec` / `execSync` / `spawn` / `spawnSync` (provenance-gated to `node:child_process`) |
973
986
  | `code-injection` | 94 | `eval` / `vm.runInNewContext` |
987
+ | `dynamic-regex` | 1333 | `RegExp(...)` / `new RegExp(...)` with a non-literal pattern |
988
+ | `redos-regex` | 1333 | vulnerable regex literals tested with source-backed input |
989
+ | `resource-amplification` | 400 | source-backed size into `Array(...)` / `new Array(...)` / `Buffer.alloc*` / `String.prototype.repeat` / `padStart` / `padEnd` (directly `Math.min`-clamped sizes stay quiet) |
990
+ | `dynamic-module-load` | 95 | dynamic `require(...)` |
974
991
  | `sql-injection` | 89 | string concat or interpolated template into `.query()` / `.execute()`, and `sql.raw(...)`. Parameterized `` sql`${x}` `` and the object form `.execute({ sql, args })` are NOT flagged |
975
- | `ssrf` | 918 | `fetch` / `axios` / `http(s).request` |
976
- | `path-traversal` | 22 | `fs.*` / `path.join` / `path.resolve` |
977
- | `open-redirect` | 601 | `res.redirect` |
992
+ | `ssrf` | 918 | `fetch` / `got` / `ky` / `needle` / `request` / `axios` / `superagent` / `undici` / `http(s).request` |
993
+ | `path-traversal` | 22 | `path.join` / `path.resolve` / `node:fs` path methods / route `sendFile` |
994
+ | `header-injection` | 113 | response `setHeader` / `writeHead` |
995
+ | `open-redirect` | 601 | `res.redirect` / `location.href` / `location.assign` / `window.open` |
996
+ | `postmessage-wildcard-origin` | 346 | `postMessage(..., "*")` |
997
+ | `tls-validation-disabled` | 295 | HTTPS/TLS options with `rejectUnauthorized: false`, plus `NODE_TLS_REJECT_UNAUTHORIZED = "0"` |
998
+ | `cleartext-transport` | 319 | cleartext `http://` URLs in fetch-like calls and WebSocket constructors |
999
+ | `electron-unsafe-webpreferences` | 1188 | Electron `webPreferences` with unsafe literal options |
1000
+ | `world-writable-permission` | 732 | `chmod` / `chmodSync` with world-writable modes |
1001
+ | `insecure-temp-file` | 377 | predictable temporary file paths in `fs` writes |
1002
+ | `mysql-multiple-statements` | 89 | MySQL connection options with `multipleStatements: true` |
1003
+ | `permissive-cors` | 942 | CORS wildcard origin with credentials |
1004
+ | `insecure-cookie` | 614 | cookie options missing or disabling `httpOnly` / `secure` |
1005
+ | `mass-assignment` | 915 | source-backed `Object.assign(target, source)` |
978
1006
  | `weak-crypto` | 327 | runtime-selectable hash / cipher algorithm |
1007
+ | `deprecated-cipher` | 327 | `crypto.createCipher` / `createDecipher` |
1008
+ | `insecure-randomness` | 338 | `crypto.pseudoRandomBytes(...)` and token-like `Math.random()` use |
1009
+ | `jwt-alg-none` | 347 | JWT signing with algorithm `none` |
1010
+ | `jwt-verify-missing-algorithms` | 347 | `jsonwebtoken` verify calls missing an `algorithms` allowlist |
1011
+ | `unsafe-buffer-alloc` | 1188 | `Buffer.allocUnsafe` / `allocUnsafeSlow` |
979
1012
  | `unsafe-deserialization` | 502 | `js-yaml` `load` / `node-serialize` |
980
-
981
- Build-config and test files are excluded from candidate generation. Both rule families default to `off` and are surfaced only by `fallow security`, never under bare `fallow` or the `audit` gate. Scope which catalogue categories run with `security.categories` include / exclude lists in config.
1013
+ | `angular-trusted-html` | 79 | Angular `bypassSecurityTrust*` |
1014
+ | `nextjs-open-redirect` | 601 | Next.js `redirect` / `permanentRedirect` |
1015
+ | `dom-document-write` | 79 | `document.write` / `document.writeln` |
1016
+ | `jquery-html` | 79 | jQuery `.html(value)` |
1017
+ | `route-send-file` | 22 | Express / Fastify / Hono route `sendFile` |
1018
+ | `webview-injection` | 94 | react-native-webview injected JavaScript |
1019
+ | `prototype-pollution` | 1321 | `__proto__` writes and recursive merge sources |
1020
+ | `zip-slip` | 22 | archive extraction destination paths |
1021
+ | `nosql-injection` | 943 | Mongo / Mongoose query object passthrough |
1022
+ | `ssti` | 1336 | template engine compile / render calls |
1023
+ | `xxe` | 611 | XML parse calls |
1024
+ | `secret-pii-log` | 532 | source-backed secrets or request PII reaching logs |
1025
+ | `hardcoded-secret` | 798 | provider-prefix credentials and high-entropy literals assigned to secret-shaped identifiers (include-required) |
1026
+ | `secret-to-network` | 201 | a non-public `process.env` / `import.meta.env` secret reaching a network call body (`fetch` / `axios` / `got` / ...) via same-identifier flow (include-required) |
1027
+ | `xpath-injection` | 643 | `xpath.select` / `select1` with a non-literal expression |
1028
+
1029
+ Build-config and test files are excluded from candidate generation. Security rule families default to `off` and are surfaced only by `fallow security`, never under bare `fallow` or the `audit` gate. Scope which catalogue categories run with `security.categories` include / exclude lists in config. `hardcoded-secret` and `secret-to-network` are intentionally include-required and only run when listed in `security.categories.include` (`secret-to-network` is opt-in because legitimate auth is also a secret reaching a network call). Public-by-convention env vars (`NEXT_PUBLIC_`, `VITE_`, ...) are never treated as secrets.
982
1030
 
983
1031
  ### Flags
984
1032
 
@@ -991,7 +1039,10 @@ Build-config and test files are excluded from candidate generation. Both rule fa
991
1039
  | `--fail-on-issues` | bool | `false` | Exit 1 when candidates are found |
992
1040
  | `--sarif-file` | path | none | Write SARIF in addition to the primary output |
993
1041
  | `--changed-since` | git ref | none | Scope to candidates whose client anchor or trace hops touch changed files |
1042
+ | `--file` | path, repeatable | none | Scope output to candidates whose finding anchor or trace hop matches the selected file. The full graph is still analyzed |
994
1043
  | `--diff-file` | path | none | Scope candidates to added hunks on the client anchor or import trace. Secret-source hops use file-level retention because member-access spans are not yet stored. Use `-` for stdin |
1044
+ | `--diff-stdin` | bool | `false` | Read the unified diff from stdin (equivalent to `--diff-file -`) for line-level scoping and the regression gate |
1045
+ | `--gate` | `new` | none | Fail (exit code **8**) only when the change introduces a NEW security-sink candidate in the changed lines, not on the whole candidate backlog. Requires a diff source (`--changed-since`, `--diff-file`, or `--diff-stdin`); a diff the gate cannot compute is a loud exit 2, never a green gate. Human output says `REVIEW REQUIRED` (not `FAIL`); SARIF keeps every result at `level: note` with the verdict in `run.properties.fallowGate`; `--format json` carries an additive `gate` block (`mode` / `verdict` / `new_count`) |
995
1046
  | `--workspace` | string | none | Scope to selected workspace packages |
996
1047
  | `--changed-workspaces` | git ref | none | Scope to workspaces changed since a git ref |
997
1048
 
@@ -1001,6 +1052,9 @@ Build-config and test files are excluded from candidate generation. Both rule fa
1001
1052
  fallow security --format json --quiet
1002
1053
  fallow security --ci --sarif-file fallow-security.sarif
1003
1054
  git diff --unified=0 origin/main...HEAD | fallow security --diff-file -
1055
+ # Regression gate: fail (exit 8) only on candidates introduced in the changed lines
1056
+ fallow security --gate new --changed-since origin/main
1057
+ git diff --cached --unified=0 | fallow security --gate new --diff-stdin
1004
1058
  ```
1005
1059
 
1006
1060
  ### JSON Output Structure
@@ -1015,7 +1069,17 @@ git diff --unified=0 origin/main...HEAD | fallow security --diff-file -
1015
1069
  }
1016
1070
  ```
1017
1071
 
1018
- Each finding includes `kind`, `path`, `line`, `col`, `evidence`, `trace`, and `actions`. `tainted-sink` findings additionally carry `category` (the catalogue id, e.g. `"dangerous-html"`) and `cwe`; `client-server-leak` findings omit both. `unresolved_edge_files` (client-server-leak) and `unresolved_callee_sites` (tainted-sink) are in-band blind-spot counters: a zero finding count with a non-zero counter is not a clean bill. Suppress a verified false positive with `// fallow-ignore-file security-client-server-leak` (client-server-leak) or `// fallow-ignore-file security-sink` (any tainted-sink category).
1072
+ Each finding includes `kind`, `path`, `line`, `col`, `evidence`, `trace`, `actions`, and optional `reachability`. `tainted-sink` findings additionally carry `category` (the catalogue id, e.g. `"dangerous-html"`) and `cwe`; `client-server-leak` findings omit both. `tainted-sink` findings can also include `reachability.untrusted_source_trace` when a module with a known untrusted source imports the sink module; it is ranking and triage context only, not proof that a specific value reaches the sink. `unresolved_edge_files` (client-server-leak) and `unresolved_callee_sites` (tainted-sink) are in-band blind-spot counters: a zero finding count with a non-zero counter is not a clean bill. Suppress a verified false positive with `// fallow-ignore-file security-client-server-leak` (client-server-leak) or `// fallow-ignore-file security-sink` (any tainted-sink category).
1073
+
1074
+ Every finding also carries an agent-actionable `candidate { source_kind, sink, boundary }`, an optional `taint_flow { source, sink, path }`, and a stable `finding_id`:
1075
+
1076
+ - `candidate.source_kind`: the untrusted-input kind that reaches the sink, as a stable catalogue id (`"http-request-input"`, `"process-env"`, `"process-argv"`, `"message-event-data"`, `"location-input"`, ...). Absent when no source matched (always absent for `client-server-leak`). Treat an unknown id as an untrusted source of unknown kind; never drop the candidate on that basis.
1077
+ - `candidate.sink`: a self-contained sink (`path`, `line`, `col`, `category`, `cwe`, `callee`), actionable without reading the rest of the finding.
1078
+ - `candidate.boundary`: `client_server` (a `"use client"` file in the trace), `cross_module` (the source reaches the sink across import hops), and optional `architecture_zone` (`from`/`to`) when the anchor also crosses a declared architecture boundary.
1079
+ - `candidate.network`: present only on `secret-to-network` (#890) candidates. `destination` is the network call's URL when it is a static literal (usually intended auth) or absent when the destination is dynamic (the higher-signal exfil case). Use it to triage exfil from intended auth without re-reading source.
1080
+ - There is no `impact` field: deciding exploitability is the verifying agent's job.
1081
+ - `taint_flow`: present only when an untrusted source is import-reachable to the sink. `path` is the compact `{ intra_module, cross_module_hops }` shape; the full ordered hops stay in `reachability.untrusted_source_trace`.
1082
+ - `finding_id`: a stable correlation id, identical across runs for the same rule/path/line and identical to the SARIF `partialFingerprints` value, for tracking a candidate across runs and joining JSON with SARIF.
1019
1083
 
1020
1084
  ---
1021
1085
 
@@ -1387,8 +1451,10 @@ Available on all commands:
1387
1451
  | `FALLOW_BIN` | Path to fallow binary (used by the MCP server). |
1388
1452
  | `FALLOW_TIMEOUT_SECS` | MCP server subprocess timeout in seconds (default: `120`). Increase for very large codebases. |
1389
1453
  | `FALLOW_EXTENDS_TIMEOUT_SECS` | Timeout for fetching remote config inheritance in seconds (default: `5`). Do not raise this for untrusted sources. |
1454
+ | `FALLOW_CACHE_DIR` | Override the persistent extraction cache directory. Wins over `cache.dir`. Useful for read-only checkouts or CI cache volumes. `--no-cache` disables this knob. |
1390
1455
  | `FALLOW_CACHE_MAX_SIZE` | Maximum on-disk extraction cache (`.fallow/cache.bin`) size in megabytes (default: `256`). Triggers LRU eviction when crossed. Wins over `cache.maxSizeMb` config field. Intended for CI runners with disk quotas. `--no-cache` short-circuits this knob. |
1391
1456
  | `FALLOW_AUDIT_CACHE_MAX_AGE_DAYS` | Max age (in days since last reuse or fresh create) of a persistent reusable `fallow audit` base-snapshot worktree cache. Older entries are reclaimed at the top of the next `fallow audit` invocation (default: `30`). Wins over `audit.cacheMaxAgeDays` config field. `0` disables the GC; invalid values silently fall back to config / default. |
1457
+ | `FALLOW_UPDATE_CHECK` | Set to `off`, `0`, `false`, `disabled`, or `no` to disable the human-TTY upgrade nudge and its background latest-version check. `DO_NOT_TRACK`, `FALLOW_TELEMETRY_DISABLED`, and CI also suppress it. |
1392
1458
  | `FALLOW_COMMAND` | GitLab CI: command to run (default: `dead-code`). |
1393
1459
  | `FALLOW_FAIL_ON_ISSUES` | GitLab CI: set to `true` to exit 1 if issues found. |
1394
1460
  | `FALLOW_CHANGED_SINCE` | GitLab CI: git ref for incremental analysis. Auto-detected in MR pipelines. |
@@ -1488,7 +1554,7 @@ The HTTP layer mirrors the bash `gh_api_retry` / `curl_retry` helpers: `FALLOW_A
1488
1554
  {
1489
1555
  "kind": "dead-code",
1490
1556
  "schema_version": 7,
1491
- "version": "2.88.3",
1557
+ "version": "2.90.0",
1492
1558
  "elapsed_ms": 45,
1493
1559
  "total_issues": 12,
1494
1560
  "entry_points": {
@@ -1648,7 +1714,7 @@ When `--baseline` is used in combined output, the JSON includes a `baseline_delt
1648
1714
  {
1649
1715
  "kind": "dupes",
1650
1716
  "schema_version": 7,
1651
- "version": "2.88.3",
1717
+ "version": "2.90.0",
1652
1718
  "elapsed_ms": 82,
1653
1719
  "total_clones": 15,
1654
1720
  "total_lines_duplicated": 230,
@@ -1692,11 +1758,11 @@ When running `fallow` with no subcommand (all analyses), the JSON output combine
1692
1758
  {
1693
1759
  "kind": "combined",
1694
1760
  "schema_version": 7,
1695
- "version": "2.88.3",
1761
+ "version": "2.90.0",
1696
1762
  "elapsed_ms": 159,
1697
1763
  "check": {
1698
1764
  "schema_version": 7,
1699
- "version": "2.88.3",
1765
+ "version": "2.90.0",
1700
1766
  "elapsed_ms": 45,
1701
1767
  "total_issues": 12,
1702
1768
  "unused_files": [],
@@ -1804,6 +1870,12 @@ Config files are searched in priority order: `.fallowrc.json` > `.fallowrc.jsonc
1804
1870
  "ignorePatterns": ["**/*.generated.ts"]
1805
1871
  },
1806
1872
 
1873
+ // Extraction cache settings. FALLOW_CACHE_DIR overrides cache.dir.
1874
+ "cache": {
1875
+ "dir": "/tmp/fallow-cache",
1876
+ "maxSizeMb": 256
1877
+ },
1878
+
1807
1879
  // Architecture boundaries (preset, custom zones/rules, or auto-discovered feature zones)
1808
1880
  // Presets: "layered", "hexagonal", "feature-sliced", "bulletproof"
1809
1881
  // Rules accept an optional `allowTypeOnly: [zones]` list that admits type-only imports
@@ -61,6 +61,8 @@ kind: "coverage-setup"
61
61
  kind: "coverage-analyze"
62
62
  }) | (ListBoundariesOutput & {
63
63
  kind: "list-boundaries"
64
+ }) | (WorkspacesOutput & {
65
+ kind: "list-workspaces"
64
66
  }) | (HealthOutput & {
65
67
  kind: "health"
66
68
  }) | (DupesOutput & {
@@ -353,6 +355,18 @@ export type CoverageTier = ("none" | "partial" | "high")
353
355
  * Provenance of a CRAP finding's coverage signal.
354
356
  */
355
357
  export type CoverageSource = ("istanbul" | "estimated" | "estimated_component_inherited")
358
+ /**
359
+ * Which complexity metric a [`ComplexityContribution`] adds to.
360
+ */
361
+ export type ComplexityMetric = ("cyclomatic" | "cognitive")
362
+ /**
363
+ * The syntactic construct that produced a single complexity increment.
364
+ *
365
+ * Mirrors `SonarSource` cognitive-complexity vocabulary where it overlaps.
366
+ * `Case` means a `case` label carrying a test; a bare `default` adds nothing
367
+ * to cyclomatic complexity and so produces no contribution.
368
+ */
369
+ export type ComplexityContributionKind = ("if" | "else" | "else-if" | "ternary" | "logical-and" | "logical-or" | "nullish-coalescing" | "logical-assignment" | "optional-chain" | "for" | "for-in" | "for-of" | "while" | "do-while" | "switch" | "case" | "catch" | "labeled-break" | "labeled-continue")
356
370
  /**
357
371
  * Discriminant for [`HealthFindingAction::kind`]. Mirrors the action types
358
372
  * emitted by `build_health_finding_actions`. A single finding's `actions`
@@ -587,6 +601,17 @@ export type ImpactTrendDirection = ("improving" | "declining" | "stable")
587
601
  * from the main contract, mirroring `ImpactReportSchemaVersion`.
588
602
  */
589
603
  export type SecuritySchemaVersion = "1"
604
+ /**
605
+ * Gate mode for `fallow security --gate <mode>` (issue #886). Tier 2 reserves
606
+ * the value `newly-reachable`.
607
+ */
608
+ export type SecurityGateMode = "new"
609
+ /**
610
+ * Gate verdict on the wire. `fail` is the CI-state token; human output renders
611
+ * it as "REVIEW REQUIRED" because these stay unverified candidates, never
612
+ * confirmed vulnerabilities.
613
+ */
614
+ export type SecurityGateVerdict = ("pass" | "fail")
590
615
  /**
591
616
  * The kind of security candidate. Findings are CANDIDATES for downstream agent
592
617
  * verification, NOT verified vulnerabilities.
@@ -595,7 +620,20 @@ export type SecurityFindingKind = ("client-server-leak" | "tainted-sink")
595
620
  /**
596
621
  * The role a hop plays in a security finding's structural import trace.
597
622
  */
598
- export type TraceHopRole = ("client-boundary" | "intermediate" | "secret-source" | "sink")
623
+ export type TraceHopRole = ("client-boundary" | "untrusted-source" | "intermediate" | "secret-source" | "sink")
624
+ /**
625
+ * Dead-code issue kind linked to a security candidate.
626
+ */
627
+ export type SecurityDeadCodeKind = ("unused-file" | "unused-export")
628
+ /**
629
+ * Runtime coverage state for the function enclosing a security sink.
630
+ * This is production-observation evidence, not an exploitability verdict.
631
+ */
632
+ export type SecurityRuntimeState = ("runtime-hot" | "runtime-cold" | "never-executed" | "low-traffic" | "coverage-unavailable" | "runtime-unknown")
633
+ /**
634
+ * Defensive control family detected on a source to sink path.
635
+ */
636
+ export type SecurityControlKind = ("sanitization" | "validation" | "authentication" | "authorization")
599
637
  /**
600
638
  * Discriminator value for [`CodeClimateIssue::kind`].
601
639
  */
@@ -1654,6 +1692,15 @@ line: number
1654
1692
  * 0-based byte column offset of the import that starts the cycle.
1655
1693
  */
1656
1694
  col: number
1695
+ /**
1696
+ * Per-file import anchors, one entry per hop in cycle order: `edges[i]`
1697
+ * is the import in `files[i]` pointing to `files[(i + 1) % len]`. Always
1698
+ * the same length as `files`. Drives the per-file LSP diagnostic
1699
+ * squiggly. `#[serde(default)]` so pre-`edges` baselines deserialize;
1700
+ * always emitted on output but intentionally not in the schema's
1701
+ * `required` set (see the struct doc).
1702
+ */
1703
+ edges?: CircularDependencyEdge[]
1657
1704
  /**
1658
1705
  * Whether this cycle crosses workspace package boundaries.
1659
1706
  */
@@ -1669,6 +1716,34 @@ actions: IssueAction[]
1669
1716
  */
1670
1717
  introduced?: (AuditIntroduced | null)
1671
1718
  }
1719
+ /**
1720
+ * One import hop in a circular dependency: the file containing the import
1721
+ * and where that import statement sits.
1722
+ *
1723
+ * `edges[i]` is the import IN `path` (the hop SOURCE, equal to the cycle's
1724
+ * `files[i]`) that points to the NEXT file in the cycle
1725
+ * (`files[(i + 1) % files.len()]`); the target is not repeated here to keep
1726
+ * the wire compact. Enables a per-file diagnostic squiggly anchored under
1727
+ * the offending import rather than a single squiggly on the first file.
1728
+ *
1729
+ * `col` is a 0-based BYTE column, matching the cycle's top-level `col`;
1730
+ * converting it to a UTF-16 code-unit column for LSP clients is a tracked
1731
+ * follow-up shared with the existing field.
1732
+ */
1733
+ export interface CircularDependencyEdge {
1734
+ /**
1735
+ * The file containing the import (the hop SOURCE; equal to `files[i]`).
1736
+ */
1737
+ path: string
1738
+ /**
1739
+ * 1-based line number of the import statement pointing to the next file.
1740
+ */
1741
+ line: number
1742
+ /**
1743
+ * 0-based byte column offset of the import statement.
1744
+ */
1745
+ col: number
1746
+ }
1672
1747
  /**
1673
1748
  * Wire-shape envelope for a [`ReExportCycle`] finding. Mirrors
1674
1749
  * [`CircularDependencyFinding`]: flattens the bare finding and carries a
@@ -2200,6 +2275,15 @@ line_count: number
2200
2275
  * each group in the human listing.
2201
2276
  */
2202
2277
  fingerprint: string
2278
+ /**
2279
+ * Best-effort human-readable name for the clone: the dominant repeated
2280
+ * identifier across the duplicated fragment (e.g. a shared `parseCsv`
2281
+ * function). `None` when the clone has no clear dominant name (generic or
2282
+ * tied identifiers); consumers then fall back to a file-based label. Lets
2283
+ * editors and agents label a clone by what it is rather than an opaque
2284
+ * ordinal.
2285
+ */
2286
+ suggested_name?: (string | null)
2203
2287
  /**
2204
2288
  * Suggested next steps: an `extract-shared` primary and a
2205
2289
  * `suppress-line` secondary. Always emitted (possibly empty for
@@ -2534,6 +2618,13 @@ coverage_tier?: (CoverageTier | null)
2534
2618
  coverage_source?: (CoverageSource | null)
2535
2619
  inherited_from?: (string | null)
2536
2620
  component_rollup?: (ComponentRollup | null)
2621
+ /**
2622
+ * Per-decision-point complexity breakdown explaining WHICH constructs drove
2623
+ * the cyclomatic and cognitive scores. Populated only when the caller opts
2624
+ * in via `health --complexity-breakdown`; empty (and omitted from JSON)
2625
+ * otherwise so default and CI output stay lean.
2626
+ */
2627
+ contributions?: ComplexityContribution[]
2537
2628
  /**
2538
2629
  * Machine-actionable fix and suppress hints.
2539
2630
  */
@@ -2553,6 +2644,36 @@ template_path: string
2553
2644
  template_cyclomatic: number
2554
2645
  template_cognitive: number
2555
2646
  }
2647
+ /**
2648
+ * A single complexity increment, located at its source line/column.
2649
+ *
2650
+ * `weight` is the amount this construct added to `metric`; for nested
2651
+ * cognitive increments `weight == 1 + nesting`. Consumers that render inline
2652
+ * (the VS Code editor breakdown) group contributions by `line` and sum the
2653
+ * weights, deferring the per-kind list to a hover.
2654
+ */
2655
+ export interface ComplexityContribution {
2656
+ /**
2657
+ * 1-based line number where the construct begins.
2658
+ */
2659
+ line: number
2660
+ /**
2661
+ * 0-based byte column where the construct begins.
2662
+ */
2663
+ col: number
2664
+ metric: ComplexityMetric
2665
+ kind: ComplexityContributionKind
2666
+ /**
2667
+ * The amount added to `metric` at this site (`1 + nesting` for nested
2668
+ * cognitive increments, otherwise `1`).
2669
+ */
2670
+ weight: number
2671
+ /**
2672
+ * The nesting depth at the increment site (`0` when not nested). Lets a
2673
+ * consumer explain a cognitive `+3` as "+1 base, +2 nesting".
2674
+ */
2675
+ nesting: number
2676
+ }
2556
2677
  /**
2557
2678
  * Suggested action attached to a [`ComplexityViolation`].
2558
2679
  *
@@ -4073,6 +4194,46 @@ allow: string[]
4073
4194
  */
4074
4195
  allow_type_only?: string[]
4075
4196
  }
4197
+ /**
4198
+ * `fallow workspaces --format json` envelope.
4199
+ */
4200
+ export interface WorkspacesOutput {
4201
+ /**
4202
+ * Number of workspace package entries in `workspaces`.
4203
+ */
4204
+ workspace_count: number
4205
+ /**
4206
+ * Workspace packages discovered from package manager and tsconfig workspace
4207
+ * declarations. Paths are project-root-relative and use forward slashes.
4208
+ */
4209
+ workspaces: WorkspaceInfo[]
4210
+ /**
4211
+ * Workspace discovery diagnostics produced while reading workspace
4212
+ * declarations. Present for compatibility with the current wire contract,
4213
+ * even when empty.
4214
+ */
4215
+ workspace_diagnostics: WorkspaceDiagnostic[]
4216
+ }
4217
+ /**
4218
+ * One workspace package emitted by `fallow workspaces --format json`.
4219
+ */
4220
+ export interface WorkspaceInfo {
4221
+ /**
4222
+ * Package name from the workspace package.json. This is the value accepted
4223
+ * by `--workspace <name>`.
4224
+ */
4225
+ name: string
4226
+ /**
4227
+ * Project-root-relative path to the workspace directory, normalized to
4228
+ * forward slashes for cross-platform JSON consumers.
4229
+ */
4230
+ path: string
4231
+ /**
4232
+ * Whether the package is a generated or platform-specific dependency
4233
+ * package rather than a hand-authored workspace.
4234
+ */
4235
+ is_internal_dependency: boolean
4236
+ }
4076
4237
  /**
4077
4238
  * Envelope emitted by `fallow health --format json` (plus the `health` block
4078
4239
  * inside the combined and audit envelopes).
@@ -4696,15 +4857,27 @@ git_sha?: (string | null)
4696
4857
  timestamp: string
4697
4858
  }
4698
4859
  /**
4699
- * The `fallow security --format json` envelope. `security_findings` is the
4700
- * unique required field used for untagged narrowing in `FallowOutput`.
4860
+ * The `fallow security --format json` envelope. `FallowOutput` discriminates it
4861
+ * by the `kind: "security"` tag; the optional `gate` block is additive and is
4862
+ * not part of that discrimination.
4701
4863
  */
4702
4864
  export interface SecurityOutput {
4703
4865
  schema_version: SecuritySchemaVersion
4866
+ /**
4867
+ * Gate verdict, present only when `--gate <mode>` was set (issue #886).
4868
+ * Emitted on pass too (`verdict: "pass"`, `new_count: 0`) so consumers
4869
+ * distinguish "gate ran and passed" from "gate did not run" (absent).
4870
+ */
4871
+ gate?: (SecurityGate | null)
4704
4872
  /**
4705
4873
  * Security candidates. Paths are project-root-relative, forward-slash.
4706
4874
  */
4707
4875
  security_findings: SecurityFinding[]
4876
+ /**
4877
+ * Opt-in attack-surface inventory from untrusted entry points to reachable
4878
+ * sinks. Present only when `--surface` was requested.
4879
+ */
4880
+ attack_surface?: (SecurityAttackSurfaceEntry[] | null)
4708
4881
  /**
4709
4882
  * In-band blind spot: number of `"use client"` files whose transitive
4710
4883
  * import cone contains a dynamic `import()` the reachability BFS could not
@@ -4720,6 +4893,18 @@ unresolved_edge_files: number
4720
4893
  */
4721
4894
  unresolved_callee_sites: number
4722
4895
  }
4896
+ /**
4897
+ * The `gate` block on `SecurityOutput`, present only when `--gate <mode>` ran.
4898
+ * Invariant: `verdict == Fail IFF exit code 8 IFF new_count > 0`.
4899
+ */
4900
+ export interface SecurityGate {
4901
+ mode: SecurityGateMode
4902
+ verdict: SecurityGateVerdict
4903
+ /**
4904
+ * Number of candidates introduced in the changed lines.
4905
+ */
4906
+ new_count: number
4907
+ }
4723
4908
  /**
4724
4909
  * A local security CANDIDATE for downstream agent verification, NOT a verified
4725
4910
  * vulnerability. Emitted only by `fallow security`, never under bare `fallow`
@@ -4728,6 +4913,14 @@ unresolved_callee_sites: number
4728
4913
  * (its hops and length) is the only honest signal.
4729
4914
  */
4730
4915
  export interface SecurityFinding {
4916
+ /**
4917
+ * Stable per-finding correlation id, identical across runs for the same
4918
+ * rule + anchor path + line. An autonomous agent that triaged this
4919
+ * candidate on a prior run uses it to correlate the candidate after a
4920
+ * rebase. Equal to the SARIF `partialFingerprints` value for the same
4921
+ * finding (one shared helper computes both).
4922
+ */
4923
+ finding_id: string
4731
4924
  kind: SecurityFindingKind
4732
4925
  /**
4733
4926
  * The catalogue category id (e.g. `"dangerous-html"`). `None` for
@@ -4781,12 +4974,37 @@ trace: TraceHop[]
4781
4974
  */
4782
4975
  actions: IssueAction[]
4783
4976
  /**
4784
- * Graph-derived reachability ranking signal (issue #860). `None` until the
4785
- * post-detection ranking pass fills it; additive on the wire (skipped when
4786
- * absent). Drives the order findings are emitted in: candidates reachable
4787
- * from a runtime entry point with a wider blast radius sort first.
4977
+ * Dead-code cross-link when the same sink candidate sits in code fallow also
4978
+ * reports as removable. Agents should verify the dead-code finding and delete
4979
+ * the code instead of hardening the sink when deletion is safe.
4980
+ */
4981
+ dead_code?: (SecurityDeadCodeContext | null)
4982
+ /**
4983
+ * Graph-derived reachability ranking signal (issues #860 and #885). `None`
4984
+ * until the post-detection ranking pass fills it; additive on the wire
4985
+ * (skipped when absent). Drives the order findings are emitted in:
4986
+ * runtime-reachable candidates sort first, followed by source-backed and
4987
+ * source-reachable candidates, then wider blast radius.
4788
4988
  */
4789
4989
  reachability?: (SecurityReachability | null)
4990
+ candidate: SecurityCandidate
4991
+ /**
4992
+ * Source-to-sink taint-flow triple, present only when an untrusted source
4993
+ * is import-reachable to this sink. Absent (skipped) otherwise.
4994
+ */
4995
+ taint_flow?: (SecurityTaintFlow | null)
4996
+ /**
4997
+ * Production runtime coverage context for the function enclosing this
4998
+ * security sink. Present only when `fallow security --runtime-coverage`
4999
+ * runs and the candidate is a `tainted-sink`.
5000
+ */
5001
+ runtime?: (SecurityRuntimeContext | null)
5002
+ /**
5003
+ * Internal projection used by `fallow security --surface`. The CLI strips
5004
+ * this from per-finding JSON and promotes it to the top-level
5005
+ * `attack_surface` field only when requested.
5006
+ */
5007
+ attack_surface?: (SecurityAttackSurfaceEntry | null)
4790
5008
  }
4791
5009
  /**
4792
5010
  * One hop in a security finding's structural trace. Stored as an absolute path
@@ -4810,11 +5028,31 @@ line: number
4810
5028
  col: number
4811
5029
  role: TraceHopRole
4812
5030
  }
5031
+ /**
5032
+ * Dead-code cross-link attached to a security candidate when fallow's dead-code
5033
+ * pass reports the same anchor as removable code.
5034
+ */
5035
+ export interface SecurityDeadCodeContext {
5036
+ kind: SecurityDeadCodeKind
5037
+ /**
5038
+ * Unused export name when `kind` is `unused-export`.
5039
+ */
5040
+ export_name?: (string | null)
5041
+ /**
5042
+ * Dead-code finding line when available.
5043
+ */
5044
+ line?: (number | null)
5045
+ /**
5046
+ * Agent-facing guidance for deciding between deletion and hardening.
5047
+ */
5048
+ guidance: string
5049
+ }
4813
5050
  /**
4814
5051
  * Graph-derived reachability ranking signal for a security candidate. Computed
4815
- * from the EXISTING module graph (runtime reachability + reverse-dep fan-in)
4816
- * after detection, never proven exploitable. Used to surface candidates that
4817
- * sit on a request/runtime-reachable surface above isolated helpers or scripts.
5052
+ * from the existing module graph after detection, never proven exploitable.
5053
+ * Used to surface candidates that sit on a request/runtime-reachable surface,
5054
+ * receive same-module source evidence, or are import-reachable from an
5055
+ * untrusted-source module above isolated helpers or scripts.
4818
5056
  *
4819
5057
  * This is a relative-ordering signal, NOT a `confidence` or `signal_strength`
4820
5058
  * score: fallow does not prove the path is exploitable.
@@ -4827,6 +5065,23 @@ export interface SecurityReachability {
4827
5065
  * only from test entry points does not count.
4828
5066
  */
4829
5067
  reachable_from_entry: boolean
5068
+ /**
5069
+ * Whether the anchor module is reachable over value imports from a module
5070
+ * that reads a known untrusted input source. Module-level only: this does
5071
+ * not prove a specific source value reaches the sink argument.
5072
+ */
5073
+ reachable_from_untrusted_source?: boolean
5074
+ /**
5075
+ * Number of value-import hops from the untrusted-source module to the sink
5076
+ * module when `reachable_from_untrusted_source` is true.
5077
+ */
5078
+ untrusted_source_hop_count?: (number | null)
5079
+ /**
5080
+ * Module-level import path from the untrusted-source module to the sink
5081
+ * anchor. Empty when no source module reaches this candidate. The path is a
5082
+ * ranking explanation, not a value-flow proof.
5083
+ */
5084
+ untrusted_source_trace?: TraceHop[]
4830
5085
  /**
4831
5086
  * Number of distinct modules that transitively depend on the anchor module
4832
5087
  * (fan-in via the graph's reverse-dependency index). A higher value means a
@@ -4841,6 +5096,256 @@ blast_radius: number
4841
5096
  */
4842
5097
  crosses_boundary: boolean
4843
5098
  }
5099
+ /**
5100
+ * An agent-actionable candidate record on a [`SecurityFinding`]. fallow fills
5101
+ * `source_kind`, `sink`, and `boundary`. The exploitability IMPACT is
5102
+ * deliberately NOT a field: deciding severity / exploitability is the consuming
5103
+ * agent's job, not fallow's, and a perpetually-null `impact` key would only
5104
+ * train consumers to ignore it. The agent reads this record, then writes its
5105
+ * own impact verdict downstream.
5106
+ */
5107
+ export interface SecurityCandidate {
5108
+ /**
5109
+ * The kind of untrusted input that reaches the sink, as a stable catalogue
5110
+ * source id (`"http-request-input"`, `"process-env"`, `"process-argv"`,
5111
+ * `"message-event-data"`, `"location-input"`, ...). `None`/absent when no
5112
+ * untrusted source was matched (always `None` for `client-server-leak`).
5113
+ * This is an OPEN string set, driven by the data-driven source catalogue; a
5114
+ * consumer should treat an unknown id as "untrusted source of unknown kind"
5115
+ * and never drop the candidate on that basis.
5116
+ */
5117
+ source_kind?: (string | null)
5118
+ sink: SecurityCandidateSink
5119
+ boundary: SecurityCandidateBoundary
5120
+ /**
5121
+ * Network-destination context, present only on `secret-to-network` (#890)
5122
+ * candidates: the host the secret-bearing call targets, so an agent can
5123
+ * triage exfil from intended auth. Absent for every other category.
5124
+ */
5125
+ network?: (SecurityNetworkContext | null)
5126
+ }
5127
+ /**
5128
+ * The sink slot of a [`SecurityCandidate`]: a self-contained description of the
5129
+ * matched sink site. Echoes the finding's own span (`path`/`line`/`col`) plus
5130
+ * the catalogue `category`/`cwe` and the captured `callee`, so an agent can act
5131
+ * on `candidate.sink` in isolation (e.g. after fanning a finding out to a
5132
+ * sub-agent) without reading the parent finding.
5133
+ */
5134
+ export interface SecurityCandidateSink {
5135
+ /**
5136
+ * File of the sink site. Absolute internally; JSON strips the project root
5137
+ * via `serde_path::serialize`.
5138
+ */
5139
+ path: string
5140
+ /**
5141
+ * 1-based line of the sink site.
5142
+ */
5143
+ line: number
5144
+ /**
5145
+ * 0-based byte column of the sink site.
5146
+ */
5147
+ col: number
5148
+ /**
5149
+ * Catalogue category id of the sink (e.g. `"dangerous-html"`). `None` for
5150
+ * `client-server-leak`.
5151
+ */
5152
+ category?: (string | null)
5153
+ /**
5154
+ * CWE number declared by the catalogue entry. `None` for
5155
+ * `client-server-leak`; never fabricated beyond the catalogue's value.
5156
+ */
5157
+ cwe?: (number | null)
5158
+ /**
5159
+ * The sink callee (the dangerous function or member path, e.g.
5160
+ * `"el.innerHTML"`, `"child_process.exec"`) captured by the catalogue match.
5161
+ * `None` for `client-server-leak` and matches that name no callee.
5162
+ */
5163
+ callee?: (string | null)
5164
+ }
5165
+ /**
5166
+ * The boundary slot of a [`SecurityCandidate`]: which structural boundaries the
5167
+ * candidate's flow crosses. A flow that crosses a client/server or module
5168
+ * boundary is a stronger review target than a self-contained one; the boundary
5169
+ * is fallow's structural signal over a pure source-sink match.
5170
+ *
5171
+ * Two further boundary kinds are RESERVED for a follow-up and are deliberately
5172
+ * absent here rather than emitted as always-false: `export_visibility` (is the
5173
+ * sink on a publicly-exported symbol?) and a package boundary (does the flow
5174
+ * cross an npm-package edge?). Both need new graph derivation that does not
5175
+ * exist today; emitting them as `false` would misreport "we checked and it does
5176
+ * not cross" when fallow has not checked at all.
5177
+ */
5178
+ export interface SecurityCandidateBoundary {
5179
+ /**
5180
+ * Whether the finding crosses a client/server boundary (a `"use client"`
5181
+ * file appears in the trace). True only for `client-server-leak` today;
5182
+ * `tainted-sink` candidates carry no client/server marker.
5183
+ */
5184
+ client_server: boolean
5185
+ /**
5186
+ * Whether an untrusted source reaches the sink across one or more
5187
+ * value-import (module) hops. Derived from the reachability hop count.
5188
+ */
5189
+ cross_module: boolean
5190
+ /**
5191
+ * The architecture-zone crossing when the anchor participates in a declared
5192
+ * boundary-rule violation in the same run. `None` when it crosses no
5193
+ * declared zone boundary.
5194
+ */
5195
+ architecture_zone?: (SecurityZoneCrossing | null)
5196
+ }
5197
+ /**
5198
+ * A declared architecture-zone crossing, recovered by correlating a finding's
5199
+ * anchor against the run's architecture-boundary violations.
5200
+ */
5201
+ export interface SecurityZoneCrossing {
5202
+ /**
5203
+ * Zone the importing side belongs to.
5204
+ */
5205
+ from: string
5206
+ /**
5207
+ * Zone the imported side belongs to.
5208
+ */
5209
+ to: string
5210
+ }
5211
+ /**
5212
+ * Network-destination context for a `secret-to-network` candidate (#890): where
5213
+ * the secret-bearing network call sends its data. Present only on
5214
+ * network-category candidates. A consuming agent uses it to triage exfil
5215
+ * (dynamic / untrusted destination) from intended auth (a literal provider
5216
+ * host) without re-reading source.
5217
+ */
5218
+ export interface SecurityNetworkContext {
5219
+ /**
5220
+ * The network call's destination as a static URL string literal, or absent
5221
+ * when the destination is DYNAMIC (not a literal). A dynamic destination is
5222
+ * the higher-signal exfil case; a literal provider host is usually intended
5223
+ * auth.
5224
+ */
5225
+ destination?: (string | null)
5226
+ }
5227
+ /**
5228
+ * A source-to-sink taint-flow triple, emitted only when an untrusted source is
5229
+ * import-reachable to the sink (`reachability.reachable_from_untrusted_source`).
5230
+ * The `{ source, sink, path }` shape matches the model agent SAST tooling
5231
+ * expects (cf. Semgrep `taint_source` / `taint_sink`, SARIF `threadFlows`).
5232
+ */
5233
+ export interface SecurityTaintFlow {
5234
+ source: TaintEndpoint
5235
+ sink: TaintEndpoint
5236
+ path: TaintPath
5237
+ }
5238
+ /**
5239
+ * One endpoint (source or sink node) of a [`SecurityTaintFlow`].
5240
+ */
5241
+ export interface TaintEndpoint {
5242
+ /**
5243
+ * File of the endpoint. Absolute internally; JSON strips the project root.
5244
+ */
5245
+ path: string
5246
+ /**
5247
+ * 1-based line of the endpoint.
5248
+ */
5249
+ line: number
5250
+ /**
5251
+ * 0-based byte column of the endpoint.
5252
+ */
5253
+ col: number
5254
+ }
5255
+ /**
5256
+ * Compact taint-flow path shape. The ordered per-hop trace is NOT duplicated
5257
+ * here: it lives on [`SecurityReachability::untrusted_source_trace`]. This
5258
+ * carries only the flow's structural summary (intra-module flow plus the
5259
+ * cross-module hop count) so consumers do not parse two copies of the hops.
5260
+ */
5261
+ export interface TaintPath {
5262
+ /**
5263
+ * Whether the source and sink sit in the same module (no import hop between
5264
+ * them); the source-to-sink association is intra-module.
5265
+ */
5266
+ intra_module: boolean
5267
+ /**
5268
+ * Number of value-import hops from the untrusted-source module to the sink
5269
+ * module. Zero for an intra-module flow.
5270
+ */
5271
+ cross_module_hops: number
5272
+ }
5273
+ /**
5274
+ * Runtime coverage context attached to a security candidate when
5275
+ * `fallow security --runtime-coverage` is supplied.
5276
+ */
5277
+ export interface SecurityRuntimeContext {
5278
+ state: SecurityRuntimeState
5279
+ /**
5280
+ * Enclosing function name from static extraction.
5281
+ */
5282
+ function: string
5283
+ /**
5284
+ * 1-based line where the enclosing function starts.
5285
+ */
5286
+ line: number
5287
+ /**
5288
+ * Observed invocation count when the runtime report provides it.
5289
+ */
5290
+ invocations?: (number | null)
5291
+ /**
5292
+ * Runtime coverage stable function id, when available.
5293
+ */
5294
+ stable_id?: (string | null)
5295
+ /**
5296
+ * Short candidate-framed explanation of the runtime evidence.
5297
+ */
5298
+ evidence?: (string | null)
5299
+ }
5300
+ /**
5301
+ * One untrusted entry to reachable sink path for `fallow security --surface`.
5302
+ */
5303
+ export interface SecurityAttackSurfaceEntry {
5304
+ source: TaintEndpoint
5305
+ sink: SecurityCandidateSink
5306
+ /**
5307
+ * Ordered source to sink path. Same shape as the reachability trace so
5308
+ * consumers can reuse existing path handling.
5309
+ */
5310
+ path: TraceHop[]
5311
+ defensive_boundary: SecurityDefensiveBoundary
5312
+ }
5313
+ /**
5314
+ * Agent-facing defensive-boundary verification context for one surface path.
5315
+ */
5316
+ export interface SecurityDefensiveBoundary {
5317
+ /**
5318
+ * Known controls detected along this path.
5319
+ */
5320
+ controls: SecurityDefensiveControl[]
5321
+ /**
5322
+ * Verification question for the consuming agent. It is a prompt, not a
5323
+ * missing-guard verdict.
5324
+ */
5325
+ verification_prompt: string
5326
+ }
5327
+ /**
5328
+ * Defensive control found on an attack-surface path.
5329
+ */
5330
+ export interface SecurityDefensiveControl {
5331
+ kind: SecurityControlKind
5332
+ /**
5333
+ * File of the control site. Absolute internally; JSON strips the project root.
5334
+ */
5335
+ path: string
5336
+ /**
5337
+ * 1-based line of the control site.
5338
+ */
5339
+ line: number
5340
+ /**
5341
+ * 0-based byte column of the control site.
5342
+ */
5343
+ col: number
5344
+ /**
5345
+ * Flattened callee path or a stable synthetic guard name.
5346
+ */
5347
+ callee: string
5348
+ }
4844
5349
  /**
4845
5350
  * Bare `fallow --format json` envelope.
4846
5351
  */