diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 966f1ec0..3876d018 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,31 +21,32 @@ jobs: runs-on: ubuntu-latest if: github.event_name != 'schedule' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: persist-credentials: false - - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: true python-version: "3.13" - run: uv sync --all-extras --group dev - run: uv run ruff check src tests - run: uv run ruff format --check src tests - - name: Layering contracts (import-linter, report-only) - # Report-only: `|| true` keeps this non-blocking. Encodes intended core/ layering - # (taint engine must not import the attestation layer). BROKEN today; the layering - # agent moves the code (wardline-9ec283d168). Surfaces drift without gating CI. - run: uv run lint-imports || true + - name: Layering contracts (import-linter, enforcing) + # Gating: the engine and policy tiers must not import up into orchestration, + # output, or federation (engine-purity + policy-purity, both KEPT). See + # [tool.importlinter] in pyproject.toml for the tier model; intra-tier cycles + # are guarded by tests/conformance/test_import_layering.py. + run: uv run lint-imports typecheck: name: Types (mypy strict) runs-on: ubuntu-latest if: github.event_name != 'schedule' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: persist-credentials: false - - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: true python-version: "3.13" @@ -60,10 +61,10 @@ jobs: matrix: python-version: ["3.12", "3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: persist-credentials: false - - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: true python-version: ${{ matrix.python-version }} @@ -76,10 +77,10 @@ jobs: needs: test if: github.event_name != 'schedule' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: persist-credentials: false - - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: true python-version: "3.13" @@ -120,10 +121,10 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'schedule' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: persist-credentials: false - - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: true python-version: "3.13" @@ -163,10 +164,10 @@ jobs: - name: Warpline marker: warpline_e2e steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: persist-credentials: false - - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: true python-version: "3.13" @@ -188,3 +189,58 @@ jobs: echo "- Trigger: \`${{ github.event_name }}\`" echo "- Missing local services, secrets, or capabilities fail this required oracle run." } >> "$GITHUB_STEP_SUMMARY" + + # Producer-source drift: byte-compares wardline's vendored fixtures against the + # authoritative sibling SOURCE on origin/main (loomweave SEI oracle, warpline + # reverify-worklist wire). The default PR suite already fail-closes on a vendored-copy + # EDIT via the Layer-1 blob-pins; this job closes the other half — a silent divergence + # between the vendored copy and the upstream HEAD — which no PR-time test can see without + # the sibling source. Weekly/dispatch only (a sibling change must never block a wardline + # PR); needs WARDLINE_SIBLING_SOURCE_TOKEN (read-only) to check out the private siblings. + # crit-3b: wardline-79ba05f464 (SEI) / wardline-c0563eee74 (warpline worklist). + source-drift: + name: Sibling-source drift (weekly/manual, fail-closed) + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Check out loomweave source (read-only) + uses: actions/checkout@v7 + with: + repository: foundryside-dev/loomweave + token: ${{ secrets.WARDLINE_SIBLING_SOURCE_TOKEN }} + path: _siblings/loomweave + persist-credentials: false + - name: Check out warpline source (read-only) + uses: actions/checkout@v7 + with: + repository: foundryside-dev/warpline + token: ${{ secrets.WARDLINE_SIBLING_SOURCE_TOKEN }} + path: _siblings/warpline + persist-credentials: false + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + enable-cache: true + python-version: "3.13" + - run: uv sync --all-extras --group dev + - name: Source-byte drift vs sibling origin/main (fail-closed) + # WARDLINE_LIVE_ORACLE_REQUIRED=1 turns a missing-source SKIP into a FAILURE + # (sei_drift/worklist_drift are in LIVE_ORACLE_MARKERS), so a broken checkout fails + # the required run instead of passing green. A genuine drift (a sibling shipped a new + # contract) reds this until wardline re-vendors per each module's RE-VENDOR PROCEDURE. + run: uv run pytest tests/conformance -m "sei_drift or worklist_drift" -v + env: + WARDLINE_LIVE_ORACLE_REQUIRED: "1" + WARDLINE_LOOMWEAVE_REPO: ${{ github.workspace }}/_siblings/loomweave + WARDLINE_WARPLINE_REPO: ${{ github.workspace }}/_siblings/warpline + - name: Summarize source-drift + if: always() + run: | + { + echo "### Sibling-source drift" + echo "- Markers: \`sei_drift\`, \`worklist_drift\` (byte-compare vs loomweave + warpline origin/main)" + echo "- Trigger: \`${{ github.event_name }}\`" + echo "- A vendored-fixture drift, or a failed sibling checkout, fails this required run." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 51e65f19..dbc82a9a 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -4,7 +4,7 @@ # copied verbatim into the build output). It consumes the shared @weft/site-kit, # which lives in a SUBDIRECTORY of a DIFFERENT repo (foundryside-dev/weft). # npm cannot install a git subdirectory as a file: dep directly, so the build -# sparse-fetches packages/site-kit into site/vendor/site-kit first +# sparse-fetches a pinned packages/site-kit commit into site/vendor/site-kit first # (scripts/fetch-site-kit.mjs), then `npm install` resolves the file: dep and # `astro build` compiles it. The fetch also runs as a preinstall hook, but the # explicit step keeps the order legible. @@ -29,6 +29,11 @@ concurrency: group: pages cancel-in-progress: false +env: + # Privileged Pages builds must consume an immutable site-kit revision. Update + # this SHA deliberately when promoting a new foundryside-dev/weft site kit. + WEFT_SITE_KIT_REF: a8f9a6a77458d2ec697cfbc1f71dd88a51962cb7 + jobs: build: runs-on: ubuntu-latest @@ -37,13 +42,13 @@ jobs: working-directory: site steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Configure Pages # Pin the Pages source to GitHub Actions (build_type=workflow); enables # Pages if needed. Without this, deploy-pages fails when the repo is # still set to "Deploy from a branch". - uses: actions/configure-pages@v5 + uses: actions/configure-pages@v6 with: enablement: true @@ -78,4 +83,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 11feadeb..d28b24d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,10 +12,10 @@ jobs: name: Build distributions runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: true python-version: "3.13" @@ -59,4 +59,4 @@ jobs: # twine reject it ("Unknown distribution format") and blocks the release. # Verification above already consumed it. run: rm -f dist/SHA256SUMS - - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 diff --git a/.gitignore b/.gitignore index 4a6b1c12..52cd191c 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,12 @@ output/ # port (a live, never-committed runtime artifact, not tracked state). .weft/*/ephemeral.port +# Local sibling tool stores are runtime/tooling state for this checkout. Keep +# wardline's own .weft/wardline/ suppression state visible and auditable. +.weft/filigree/ +.weft/loomweave/ +.weft/warpline/ + # Filigree issue tracker .filigree/ .env @@ -56,7 +62,6 @@ coverage.json loomweave.yaml # Filigree issue tracker -.weft/ .filigree.conf .agents/skills/loomweave-workflow/.fingerprint .agents/skills/loomweave-workflow/SKILL.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..a48a87f5 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,9 @@ +## 2025-02-14 - Prevent Git Config Code Execution +**Vulnerability:** Invoking `git` via `subprocess` against untrusted directories without overriding config can allow malicious repositories to execute code via `.git/config` hooks like `core.fsmonitor`. +**Learning:** `git` uses configurations from the `.git/config` file in the current working directory or `cwd` argument, which could be controlled by an attacker when analyzing untrusted codebases. +**Prevention:** Explicitly pass `("-c", "core.fsmonitor=false")` as `_SAFE_GIT_CONFIG` to all `git` subprocess commands in the codebase. + +## 2026-06-21 - [Add Unsafe PyYAML Loaders to Taint Tracking] +**Vulnerability:** The static analyzer was missing `yaml.unsafe_load` and `yaml.full_load` in its `_SERIALISATION_SINKS` mapping, potentially leading to false negatives when tracking untrusted data flowing into these dangerous deserialization functions. +**Learning:** Even if functions are listed in rule specifications (like `_SINK_SPECS`), they also need to be properly categorized in the core taint propagation logic (`_SERIALISATION_SINKS`) to ensure the analyzer correctly sheds validation provenance (converting output to `UNKNOWN_RAW`). +**Prevention:** When adding new sinks to rule definitions, always verify if they need to be added to core propagation mappings like `_SERIALISATION_SINKS` or `_PROPAGATING_BUILTINS`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 07915642..c31c319e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,96 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.0] - 2026-06-29 + +### Changed +- **BREAKING (unreleased contract):** attest bundle schema bumped `wardline-attest-1` → `wardline-attest-2`; each boundary now carries `content_hash` (entity-body span blake3 binding key, null when unresolved). `wardline-attest-1` bundles no longer verify. +- Default scan artifacts now anchor to the weft-project root (the `weft.toml` directory) + rather than the scan cwd, so a subdirectory scan writes to `/.wardline/`. + Retention is therefore project-root-wide across heterogeneous subdir/root scans sharing + one `.wardline/`. **Migration:** the artifact moves to the project root; `wardline doctor + --repair` sweeps now-stale per-subdir `.wardline/` dirs — update any CI/automation reading + a hardcoded `/.wardline/*-findings.jsonl` path. + +### Added +- **FastAPI / Starlette request-type taint source coverage.** `wardline` now seeds + untrusted taint from the raw request accessors on `fastapi.Request` / + `starlette.requests.Request` receivers — the `query_params` / `path_params` / + `headers` / `cookies` properties and the `json` / `body` / `form` / `stream` + methods — so request data flowing into a declared `@trusted` sink is flagged + without hand-annotating every access. Type-aware: a non-request attribute on the + same receiver (e.g. `req.app.state.db`) does not fire. Still annotation-driven — + it lights up on top of your declared trusted core, not on an unannotated app. +- **Inert-gate visibility (enforcement-posture honesty).** Scan output now always + carries a `resolution.inert` posture field (agent-summary + MCP). An armed gate + (`--fail-on`) that passes while the scan recognized **zero** trust boundaries + prints a reliance-gated stderr banner — closing the "green while checking nothing" + false-assurance gap on annotation-driven scans. Calibrated to stay silent on bare/ + advisory scans and on legitimately boundary-free pure-logic code. +- **Third-party trust-vocabulary pack-bridge.** A wardline pack can now map another + project's own trust-boundary decorator vocabulary onto a wardline `BoundaryType`, + so a team with existing trust annotations in a different grammar gets enforcement + by referencing one pack under `[wardline] packs` in `weft.toml` (or `--trust-pack`) + — no re-annotation. +- **`wardline doctor` engine self-test + loomweave-dep flag.** New `engine.selftest` + check runs the analyzer on a built-in trusted source→sink and asserts the taint + rule fires ("taint analysis fires correctly"); new `loomweave.dep` check flags a + configured-but-missing `[loomweave]` extra. +- **`wardline doctor` repo-binding store-read check.** A present-but-unreadable + baseline store now reports a machine-readable reason and `binding_ok=false` — the + seam can say "I can't read my store" instead of returning a confident empty. +- Delta-scan scope block now declares its `scope_source` and echoes warpline's unverified `producer_completeness` (warpline's `data.impact_completeness` — combined completeness+staleness object) across CLI/SARIF/MCP; MCP scope schema is key-parity-tested against `DeltaScopeReport`. Replaces the removed `producer_generated_at` (which read a phantom `data.generated_at` key warpline never emitted). +- `wardline doctor --repair` gitignores the artifacts dir and sweeps stray managed + artifacts; deletion is available on both the CLI and the MCP `doctor` tool (`repair:true`, + advertised `destructiveHint: true`), bounded to managed (timestamped) files inside + non-standard `.wardline/` dirs under the project root; emptied dirs are removed + best-effort (non-empty dirs are left in place). +- **`wardline doctor` detects and clears stale sibling `ephemeral.port` files** (new + `stale_sibling_ports` check, CLI + MCP `doctor` tool). A sibling's + `.weft//ephemeral.port` advertises a live `serve` instance; when that process + has exited or wedged, the file lingers and every scan dials a dead/hung origin — + stalling the agent gate up to the federation 30s `urlopen` timeout per round-trip on a + purely advisory emission (the reported `wardline scan` hang). The check probes each + advertised port (filigree, loomweave) at the host the scan dials, with a short 2s + deadline (not 30s): an unreachable port — connection refused **or** no HTTP reply within + the deadline (a wedged server) — is stale, and `--repair`/`fix:true` deletes the file so + the scan stops dialing it. A live server (any HTTP status) is never touched; the delete + is regular-file / no-follow confined (a symlinked `ephemeral.port` is never followed). + Advisory like the stray-artifact sweep — a stale port never flips the aggregate doctor + verdict. + +### Security +- **Cross-interpreter fingerprint determinism (3.12 == 3.13).** `entity_source_fingerprint` + previously hashed `ast.dump`, whose output changed in CPython 3.13 + (`show_empty=False` omits empty-list fields), making the cross-tool JOIN KEY + (baseline / waiver / judged stores + the Filigree wire) interpreter-dependent — a + silent-drop-on-join hazard where a finding suppressed under one interpreter could + reappear under another. The fingerprint now uses a structural, version-stable + canonical dump; the join key is byte-identical across 3.12 / 3.13, proven by the CI + matrix. **No scheme bump** — 3.13 reference values are unchanged. +- **Agent-surface hardening batch.** Closed a set of MCP / federation policy-bypass + and resource-exhaustion holes: MCP network-policy gates on `waiver_add` entity + resolution and `rekey` cache-dir writes; rejection of untrusted MCP sibling and + doctor-caller URLs; rekey snapshot-provenance hardening; stdlib-submodule + spoof-taint prevention; Filigree diagnostic-URL redaction, response-body-amplification + bound, and unsafe-mint-token soft-fail; a quadratic foreign-fence scan bound (and, + under Fixed, the cubic candidate-set scan-DoS). Enforced by the soundness oracle + + the security regression suite; a new fail-open hole is treated as a P0. + +### Fixed +- **Candidate-set merge no longer scales cubically (scan DoS).** The Level-2 + branch-join merges for lambda bindings (`_merge_branch_bindings`) and + receiver-type candidates (`_merge_branch_types`) deduplicated with a nested + linear scan of the growing candidate list — O(bucket²) per merge, O(N³) + across a chain of `N` one-armed branches rebinding the same name. An + attacker-authored file (~1100 such branches) could drive a default-gate scan + to ~15s and exhaust CPU on every local and CI run. Both merges now dedup via + an identity/equality set (O(bucket) per merge, O(N²) cumulative), preserving + the exact candidate set and insertion order; the demonstrated 1100-branch case + drops from seconds to milliseconds. No analysis behavior changes — the + candidate sets are identical, so no false negative is introduced. + Reviewed regression source: `eff4eed2` (wardline-c797baf28b). + ## [1.0.7] - 2026-06-24 ### Fixed @@ -1353,6 +1443,7 @@ for Python — enterprise-class trust-boundary analysis at small-team weight. - **Packaging** — MIT-licensed; optional extras `scanner` (config + CLI) and `weft` (HTTP integrations). +[Unreleased]: https://github.com/foundryside-dev/wardline/compare/v1.0.6...HEAD [1.0.6]: https://github.com/foundryside-dev/wardline/compare/v1.0.5...v1.0.6 [1.0.5]: https://github.com/foundryside-dev/wardline/compare/v1.0.4...v1.0.5 [1.0.4]: https://github.com/foundryside-dev/wardline/compare/v1.0.3...v1.0.4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a56ad9f..54ef6a99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,7 @@ see `CLAUDE.md`). - **TDD.** Write the failing test first. - Keep PRs focused — one logical change per PR. -- New behavior needs tests. New `wardline.yaml` keys need a `config_schema.py` update. +- New behavior needs tests. New `[wardline]` keys in `weft.toml` need a `config_schema.py` update. - No back-compat shims for unreleased specs — make clean changes. - Wardline scans its own source as a CI gate; keep the tree finding-clean (or baselined). diff --git a/Makefile b/Makefile index 6968ab16..8411575e 100644 --- a/Makefile +++ b/Makefile @@ -7,9 +7,10 @@ help: ## Show this help message install: ## Install all extras + dev tooling uv sync --all-extras --group dev -lint: ## Run linter + format check +lint: ## Run linter + format check + layering contracts uv run ruff check src tests uv run ruff format --check src tests + uv run lint-imports format: ## Auto-format and fix lint uv run ruff format src tests diff --git a/README.md b/README.md index 3947a9ee..f5dcd7e2 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ def build_record(req): ```console $ wardline scan . --fail-on ERROR -scanned 1 file(s); 3 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 active -> .wardline/20260620T153012Z-findings.jsonl +scanned 1 file(s); 2 finding(s) — 0 suppressed (0 baseline / 0 waiver / 0 judged), 1 active -> .wardline/20260620T153012Z-findings.jsonl $ echo $? 1 ``` @@ -33,8 +33,8 @@ The gate trips (exit 1) and the findings land in timestamped JSON Lines under `.wardline/` by default (`--output PATH` writes to an exact path; `--format sarif` emits SARIF for GitHub code scanning). Wardline is agent-first — you don't read that file by hand. Your coding agent does: ask it *"why did the scan -fail?"* and it surfaces the one active defect (the other two findings are -`NONE`-severity engine facts): +fail?"* and it surfaces the one active defect (the other finding is a +`NONE`-severity engine fact): > **`demo.build_record`** declares return trust `ASSURED` but actually returns > `EXTERNAL_RAW` (less trusted) — untrusted data reaches a trusted producer. @@ -137,7 +137,7 @@ Prefer `weft_markers` in application code. Wardline still recognizes | `scanner` | pyyaml, jsonschema, click | the `wardline` CLI and `wardline mcp` server | | `loomweave` | blake3 | persisting taint facts to a Loomweave store | | `rust` | scanner extra, tree-sitter, tree-sitter-rust | `wardline scan --lang rust` | -| `docs` | mkdocs, mkdocs-material | building the documentation site | +| `docs` | mkdocs, mkdocs-material | a local MkDocs render of `docs/` | The LLM triage judge (`wardline judge`) is dependency-free (stdlib `urllib` → OpenRouter) and needs no extra. @@ -150,8 +150,9 @@ wardline install This injects a hash-fenced instruction block into `CLAUDE.md`/`AGENTS.md`, installs the `wardline-gate` skill, merges a `wardline` entry into `.mcp.json`, -and writes Codex's `~/.codex/config.toml` MCP entry. Agents then run the scan → -explain → fix-at-boundary → rescan loop natively. The `wardline mcp` server +writes Codex's `~/.codex/config.toml` MCP entry, detects Loomweave/Filigree +siblings, mints an attest signing key, and adds pre-commit hook config. Agents +then run the scan → explain → fix-at-boundary → rescan loop natively. The `wardline mcp` server exposes the primary tool surface over JSON-RPC with no SDK, including scan, filtered findings, explain-taint, fix, judge, baseline/waiver, doctor, rekey, assure, attest, dossier, and Filigree filing tools. @@ -196,23 +197,23 @@ It is **not** the right tool when you need: ## Documentation -Full documentation lives at ****. +Full documentation lives in the [`docs/`](https://github.com/foundryside-dev/wardline/tree/main/docs) tree. | Document | Description | |----------|-------------| -| [Getting Started](https://foundryside-dev.github.io/wardline/getting-started/) | Install, decorate, first scan | -| [Taint & Trust Model](https://foundryside-dev.github.io/wardline/concepts/model/) | The lattice, decorators, and propagation | -| [Rules](https://foundryside-dev.github.io/wardline/concepts/rules/) | The boundary, exception-flow, and sink rules | -| [Configuration](https://foundryside-dev.github.io/wardline/guides/configuration/) | `weft.toml` `[wardline]`: rules, severity, excludes | -| [Suppression](https://foundryside-dev.github.io/wardline/guides/suppression/) | Baselines and waivers | -| [LLM Triage Judge](https://foundryside-dev.github.io/wardline/guides/judge/) | Opt-in TRUE/FALSE-positive labelling | -| [Rust Support](https://foundryside-dev.github.io/wardline/guides/rust-preview/) | Preview Rust command-injection frontend | -| [Weft Integration](https://foundryside-dev.github.io/wardline/guides/weft/) | SARIF, Filigree, Loomweave, and sibling URL resolution | -| [Assurance Posture](https://foundryside-dev.github.io/wardline/guides/assurance-posture/) | Coverage posture, attestations, and trust-surface evidence | -| [Loomweave Taint Store](https://foundryside-dev.github.io/wardline/guides/loomweave-taint-store/) | Persisting taint facts | -| [CLI Reference](https://foundryside-dev.github.io/wardline/reference/cli/) | Every command and flag | -| [Trust Vocabulary](https://foundryside-dev.github.io/wardline/reference/vocabulary/) | The decorators and their arguments | -| [Agent Integration](https://foundryside-dev.github.io/wardline/guides/agents/) | Using Wardline from a coding agent | +| [Getting Started](https://github.com/foundryside-dev/wardline/blob/main/docs/getting-started.md) | Install, decorate, first scan | +| [Taint & Trust Model](https://github.com/foundryside-dev/wardline/blob/main/docs/concepts/model.md) | The lattice, decorators, and propagation | +| [Rules](https://github.com/foundryside-dev/wardline/blob/main/docs/concepts/rules.md) | The boundary, exception-flow, and sink rules | +| [Configuration](https://github.com/foundryside-dev/wardline/blob/main/docs/guides/configuration.md) | `weft.toml` `[wardline]`: rules, severity, excludes | +| [Suppression](https://github.com/foundryside-dev/wardline/blob/main/docs/guides/suppression.md) | Baselines and waivers | +| [LLM Triage Judge](https://github.com/foundryside-dev/wardline/blob/main/docs/guides/judge.md) | Opt-in TRUE/FALSE-positive labelling | +| [Rust Support](https://github.com/foundryside-dev/wardline/blob/main/docs/guides/rust-preview.md) | Preview Rust command-injection frontend | +| [Weft Integration](https://github.com/foundryside-dev/wardline/blob/main/docs/guides/weft.md) | SARIF, Filigree, Loomweave, and sibling URL resolution | +| [Assurance Posture](https://github.com/foundryside-dev/wardline/blob/main/docs/guides/assurance-posture.md) | Coverage posture, attestations, and trust-surface evidence | +| [Loomweave Taint Store](https://github.com/foundryside-dev/wardline/blob/main/docs/guides/loomweave-taint-store.md) | Persisting taint facts | +| [CLI Reference](https://github.com/foundryside-dev/wardline/blob/main/docs/reference/cli.md) | Every command and flag | +| [Trust Vocabulary](https://github.com/foundryside-dev/wardline/blob/main/docs/reference/vocabulary.md) | The decorators and their arguments | +| [Agent Integration](https://github.com/foundryside-dev/wardline/blob/main/docs/guides/agents.md) | Using Wardline from a coding agent | ## Development diff --git a/ROADMAP.md b/ROADMAP.md index 500b9a7a..cc0c8ed4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -5,24 +5,26 @@ a direction sketch, not a commitment — dates are deliberately omitted. ## Where we are -**0.3.0 — shipped.** The staged build (SP0–SP9) is complete: +**1.0.6 — shipped.** The staged build (SP0–SP9) is complete: - Function-, variable-, and project-level taint over an inter-module call graph (L1–L2 with an L3 fixed point). - The NG-25 trust vocabulary and three opt-in decorators. -- Four policy rules (PY-WL-101..104), severity/enable config, baselines + waivers. +- 26 Python policy rules (PY-WL-101..126) plus Rust preview rules + (RS-WL-108/112), severity/enable config, baselines + waivers. - JSONL + SARIF + native Filigree emit. - Dependency-free MCP-over-stdio server (`wardline mcp`). - Opt-in LLM triage judge (`wardline judge`). - `wardline install` agent enablement. - Opt-in Loomweave taint-store integration. -- Published to PyPI; docs site live; CI dogfoods Wardline on its own source. +- Published to PyPI; CI dogfoods Wardline on its own source. ## Scope Wardline is deliberately **L1–L2 with an L3 project fixed point**, not an -exhaustive path-sensitive whole-program prover, and Python-only. We favor a -small, precise, opt-in rule set over broad SAST coverage. +exhaustive path-sensitive whole-program prover, and Python-first (with a Rust +preview, `wardline scan --lang rust`). We favor a small, precise, opt-in rule +set over broad SAST coverage. ## Near-term threads @@ -39,6 +41,6 @@ Tracked in the project's Filigree issues: ## Out of scope (for now) -- Languages other than Python. +- Broad multi-language coverage beyond the Python core and Rust preview. - A general-purpose, dozens-of-rules SAST suite. - A hosted/cloud service — Wardline stays local-first. diff --git a/docs/arch-analysis-2026-06-10/05-quality-assessment.md b/docs/arch-analysis-2026-06-10/05-quality-assessment.md deleted file mode 100644 index a287a4d7..00000000 --- a/docs/arch-analysis-2026-06-10/05-quality-assessment.md +++ /dev/null @@ -1,83 +0,0 @@ -# Architecture Quality Assessment — High-Risk Areas - -**Source:** Four-lens parallel specialist review (security / engine soundness / architecture / verification), each grounded in direct file reads of the working tree at `rc5` @ `21aeffaa`. -**Assessed:** 2026-06-10 -**Assessor:** architecture-critic synthesis (panel: threat-analyst, python-code-reviewer, architecture-critic, coverage-gap-analyst) - -## Executive Summary - -The codebase's security architecture is strong — THREAT-001 path confinement holds across every new MCP tool, the secure-by-default gate population is structurally enforced, and rekey cannot launder verdicts. The highest risks are elsewhere: **the CI self-hosting scan has no gate** (wardline does not fail its own build on its own findings), **active false-negative soundness gaps in the taint engine's chokepoint** (`_resolve_expr`/`_resolve_call` and loop merging), and **a structural drift seam** (un-layered `core/` held together by 158 deferred imports; the Rust frontend bolted alongside Python with no frontend abstraction). 2 Critical, 8 High findings. - -## Findings by Severity - -### Critical - -1. **CI self-hosting scan is SARIF-upload only — no gate** — verified: `.github/workflows/ci.yml:76` runs `wardline scan src/ --format sarif` with no `--fail-on`; the step succeeds regardless of findings. `tests/test_self_hosting.py` is additionally vacuous by construction (no decorators in own source → tier-gated rules never fire). - - **Impact:** wardline can ship a trust-boundary violation in itself with a green build — direct credibility risk for a trust-boundary gate product. - - **Fix:** add `--fail-on ERROR` to the CI step; add an annotated fixture module so self-scan exercises the real pipeline. Effort: S. - -2. **All live wire-contract oracles are weekly/manual only** — verified: `pyproject.toml:121` addopts excludes `loomweave_e2e`/`legis_e2e`/`filigree_e2e`/`rust_e2e`; the live job is `schedule`/`workflow_dispatch`-gated. Hermetic pins (legis key-set freeze, loomweave HMAC golden vector) do run in PR CI, but a sibling-side wire change surfaces only at the next weekly run. Known ticket `wardline-79ba05f464` (G6) covers the SEI-oracle half. - - **Impact:** wire-breaking drift across the federation seams merges green and fails up to a week later. - - **Fix:** fail-closed weekly job + a required `WARDLINE_LIVE_ORACLE_REQUIRED=1` escape hatch; land G6. Effort: M. - -### High — Engine soundness (the product's core value) - -3. **Zero-trip loop FN is real and the merge structure confirms it** (`scanner/taint/variable_level.py:1350–1394`): the post-loop state never re-merges `pre_loop` as a fallthrough arm, so a loop that *cleans* a RAW variable hides the zero-iteration path where it stayed RAW. Matches open bug `wardline-d6af917bde` (currently P3 — under-prioritized for a soundness FN). Fix is localized: combine post-loop state with `pre_loop`. Effort: S. - -4. **Raw-receiver `taint_map` bypass** (`variable_level.py:740–828`): for attribute calls, the `taint_map` hit returns early *before* the RAW_ZONE receiver guard, so `raw_obj.trusted_method()` can launder taint when the receiver's type isn't tracked. Same family as the scrub's PY-WL-105/stale-var_types cluster. Fix: apply/combine the receiver guard before the `taint_map` lookup. Effort: S–M. *(Confidence: Moderate — trigger needs an untyped raw receiver; confirm with a unit test first.)* - -5. **`collect_attribute_writes` is flow-insensitive with its own uncoordinated `var_types`** (`variable_level.py:1617–1703`): resolves RHS taint against final-state `var_taints`, not per-statement snapshots; branch-stale types can dispatch to `@trusted` summaries for now-raw receivers. *(Confidence: Moderate — the L2 fixed-point re-run in `analyzer.py:476` may partially compensate; verify empirically.)* - -### High — Structure - -6. **`core/` is un-layered: real import cycles + an engine→policy inversion, masked by 158 function-local deferred imports.** Verified cycles include `core.run → scanner.analyzer → … → core.attest → core.assure → core.run`; the inversion is `scanner/taint/project_resolver.py:143` importing `core.attest.ruleset_hash` (taint engine depending on the attestation layer). Every refactor near these modules risks surfacing masked breakage. Fix: `import-linter` contracts in report-only mode, then move `ruleset_hash` down a tier. Effort: M. - -7. **Rust frontend is a parallel vertical, not a plugged-in frontend.** Only `Finding`/`TaintState` are shared (`core/protocols.py:17`); context, rules, vocabulary, dataflow, qualname, and Finding-assembly are all reimplemented under `rust/`. A third language costs a third full vertical, and rule/severity/identity semantics will drift between the two rule trees. Fix: lift a `LanguageFrontend` interface before any third language. Effort: L. - -8. **MCP-vs-CLI surface drift seam is structural.** The `run_scan` spine is genuinely shared (strength), but the federation-status envelope is duplicated (`mcp/server.py:73,94` vs `cli/scan.py:450,480`) — exactly where the two dogfood drift incidents occurred — and `_register_tools` (`server.py:822–1271`) is a 450-line change-magnet. Fix: one shared status projector + per-tool schema declarations. Effort: M. - -### High — Verification gaps in trust-critical paths - -9. **Rekey adversarial scenarios untested**: mixed-scheme partial-migration state (one leg done, source changed before resume) and multi-store rollback (only single-store restore is tested, `tests/unit/core/test_rekey_rollback.py` has 2 tests). Rekey moves user trust decisions; rollback is the recovery path. Effort: S–M. - -10. **Tier-suppression negative tests absent**: no unit tests assert rules do NOT fire below the tier gate (e.g. `UNKNOWN_RAW` context); a silently loosened gate would only be caught if the pattern happens to exist in the labeled corpus. Effort: S. - -### Medium - -- **Symlinked `.env`/federation-token reads bypass `safe_project_file`** (`filigree/config.py:49,68`, `core/judge_run.py:67` — vs `attest_key.py:28`/`legis.py:143` which do it right): an attacker-authored repo can symlink `.env` out of root and wardline sends the first matching line as a bearer token to the configured sibling URL. The one security finding warranting a code change. Effort: S. -- **Judge prompt-injection surface is structural but contained**: attacker-authored source reaches the LLM; an injected FALSE_POSITIVE verdict feeds `judged.yaml` — but the secure-default gate ignores judged without `--trust-suppressions`, so no silent gate clear. Keep that invariant; document advisory status. -- **Rust qualname corpus has no drift alarm**: vendored byte-pinned at a loomweave blob; ADR-049 has already moved three times, each needing a manual re-vendor with no CI cross-check. Plus the known reserved-colon locator bug (`wardline-be5ee9cc34`) emits invalid locators with no degrade gate → fingerprint churn on the eventual fix. -- **Rust `write!`/`writeln!`/`format_args!` not modelled** in `rust/dataflow.py:202–214` (`format!` only) — tainted format strings through writers don't fire. -- **Three federation clients hand-roll the urllib transport independently** (`core/filigree_emit.py:229`, `filigree/dossier_client.py:50`, `loomweave/client.py`); only `read_response_text` is shared. Auth ladders should stay separate; the transport should not. -- **Corpus FP-rate gate has zero FALSE_POSITIVE-labeled entries** — the 5% budget math is never exercised against real corpus data. - -### Low (selected) - -- `resolve_under_root` is escape-rejecting but not symlink-refusing (unlike `safe_project_file`) — fine for today's read-only consumers; document the contract before any write path uses it. -- `attest`/`verify_attestation` is sound (timing-safe compare, schema/key_id binding); missing-`schema`-key and non-dict-payload edges untested. -- L2 loop-convergence backstop truncates silently (`variable_level.py:1362,1393`) with no `WLN-ENGINE-*` diagnostic, unlike the L3 bound. -- `time.sleep(0.1)` polling in `tests/e2e/test_loomweave_live.py:108,121`. - -## Cross-Cutting Concerns - -**Security:** Strong posture. Path confinement uniform across all MCP tools (including args never opened); secure-by-default gate population architecturally enforced (`run.py:87,298–301`); rekey carry keyed on finding-derived fingerprints (no laundering primitive); `install/block.py` injector hardened; secrets never read from `weft.toml`. The residual real item is the symlink token-read inconsistency. - -**Correctness:** The lattice discipline (`combine` vs `taint_join`) is rigorous and the L3 kernel monotone-guarded, but FN risk concentrates in `_resolve_expr`/`_resolve_call` (`variable_level.py:449,647`) — the shared chokepoint where the historical bug record also clusters. That chokepoint, not the file's 1,885-line length, is the real change-magnet; don't split the file for size's sake, invest in differential/property tests around the chokepoint. - -**Maintainability:** `core/paths.py` is a genuinely clean single source of truth for stores/config; the zero-dep constraint is well-contained. The debts are the un-layered `core/`, the duplicated Rust vertical, and the duplicated surface envelopes. - -## Priority Recommendations - -1. **Gate the CI self-scan** (`--fail-on ERROR` + non-vacuous fixture) — Critical, Effort S. A trust-gate product that doesn't gate itself is the cheapest, highest-credibility fix available. -2. **Fix the zero-trip loop FN** (re-merge `pre_loop`) and **confirm/fix the raw-receiver `taint_map` bypass** — Critical-class soundness in the core engine, Effort S each. Re-prioritize `wardline-d6af917bde` above P3. -3. **Close the symlinked token-read gap** (route filigree/judge `.env`+token reads through `safe_project_file` + regression test) — Medium severity, Effort S, restores the codebase's own established discipline. -4. **Land `import-linter` contracts (report-only) + fix the `project_resolver → core.attest` inversion** — High, Effort M; unblocks all future structural work. -5. **Rekey adversarial tests** (mixed-scheme partial, multi-store rollback) + **tier-suppression negative tests** — High, Effort S–M; protects user trust decisions. -6. **Before a third language: `LanguageFrontend` interface** — High, Effort L; the one item that caps the product ceiling. - -## Limitations - -- High-level risk review, not an exhaustive audit. Not reviewed: `core/triage.py`, `core/source_excerpt.py`, full `install/*`, `loomweave/client.py` send-side internals, LSP beyond delegation check. -- Engine findings 4–5 are structurally verified but not empirically reproduced — write the confirming unit tests before fixing (`wardline-d6af917bde`'s pattern: naive fixes here have regressed before). -- Loomweave index was empty (`never_analyzed`); the import-cycle graph was hand-built via AST and should be cross-checked after `loomweave analyze .`. -- No tests were executed; severity ratings assume the documented threat model (attacker authors repo content scanned by wardline). diff --git a/docs/arch-analysis-2026-06-28-0749/00-coordination.md b/docs/arch-analysis-2026-06-28-0749/00-coordination.md new file mode 100644 index 00000000..bdf344c2 --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/00-coordination.md @@ -0,0 +1,66 @@ +# 00 — Coordination Plan + +## Analysis Configuration +- **Target**: `wardline` — generic semantic-tainting static analyzer for Python (with a Rust preview frontend) +- **Scope**: `src/wardline/` (182 Python files, ~42.5K LOC) + supporting config; `tests/` (367 files) for evidence only +- **Deliverables**: **Option C — Architect-Ready** (discovery + catalog + diagrams + report + quality + handover) +- **Strategy**: **PARALLEL** subsystem exploration (≥5 loosely-coupled subsystems, 42K LOC) — justified below +- **Time constraint**: none stated +- **Complexity estimate**: **High** (taint dataflow engine, multi-surface product: CLI + MCP + federation clients + Rust frontend) +- **Commit analyzed**: `e4668abc` (branch `release/consolidation-2026-06-26`) +- **Date**: 2026-06-28 + +## Tooling leverage +- **Loomweave** index is FRESH at `e4668abc` (7729 entities, 20068 edges, 540 subsystem clusters, SEI populated). Used for entity/edge/subsystem queries instead of re-grepping. +- **Filigree** tracker live (25 ready issues) — used as the authoritative source of *known* debt/roadmap, cross-referenced against findings. + +## Orchestration decision +PARALLEL is selected per the command's rule (≥5 independent subsystems AND 20K+ LOC). The product +decomposes into 11 cohesive subsystems with mostly one-directional coupling (CLI/MCP → core → scanner). +Each subsystem is handed to one `codebase-explorer` subagent producing a schema-conforming catalog entry; +entries are merged into `02-subsystem-catalog.md`. A validation subagent (`analysis-validator`) gates the +catalog before synthesis. Discovery, diagrams, report, quality, and handover are authored by the +orchestrator (holistic, cross-subsystem judgement). + +### Subsystem decomposition (12) — VERIFIED EXHAUSTIVE +Canonical file→subsystem map: `temp/file-map.tsv`. **All 182 source files assigned exactly once** +(0 gaps / 0 ghosts / 0 duplicates, verified by `comm` diff against `find`). Decomposition refined from +11→12 on advisor feedback: the original "Identity, SEI & Federation" unit was oversized and split into +**S8 Identity & SEI** + **S9 Federation Clients** for review depth; six top-level package modules +(`__init__`, `_version`, `_live_oracle`, `lsp`, `weft_decorator_coverage`, `weft_dossier`) that the +brace-expansion lists had silently dropped are now explicitly assigned. + +1. **S1 Scanner Engine** (8 files) — `scanner/{analyzer,pipeline,context,grammar,index,ast_primitives,flow_trace,diagnostics}.py` +2. **S2 Rule Lattice** (35) — `scanner/rules/*` + `decorators/*` +3. **S3 Taint Engine** (15) — `scanner/taint/*` +4. **S4 Core Orchestration & Config** (21) — `core/{run,scan_jobs,scan_file_workflow,config,config_schema,ruleset,descriptor,discovery,registry,errors,protocols,paths,optional_deps,frontends,taints,gitignore,safe_paths}.py` + package roots +5. **S5 Findings, Outputs & Emit** (10) — `core/{finding,finding_query,emit,sarif,filigree_emit,filigree_issue,source_excerpt,agent_summary,artifacts,explain}.py` +6. **S6 Gate Discipline & Remediation** (8) — `core/{baseline,waivers,suppression,triage,delta,delta_resolve,delta_scope,autofix}.py` +7. **S7 Trust Evidence & Judge** (10) — `core/{attest,attest_key,assure,dossier,judge,judge_run,judged,decorator_coverage}.py` + `weft_{decorator_coverage,dossier}.py` +8. **S8 Identity & SEI** (7) — `core/{identity,sei_resolution,fingerprint_v0,finding_identity,qualname,rekey,node_id}.py` +9. **S9 Federation Clients** (15) — `core/{http,federation_status,legis}.py` + `loomweave/*` + `filigree/*` + `_live_oracle.py` +10. **S10 MCP & LSP Server** (9) — `mcp/*` + `lsp.py` +11. **S11 CLI & Install/Activation** (29) — `cli/*` + `install/*` +12. **S12 Rust Frontend** (15) — `rust/*` + +### Validation scope (advisor point 3 — recorded deviation) +The command says validate after EVERY major document. Plan: +- **`02-subsystem-catalog.md`** — MANDATORY parallel `analysis-validator` gate (12 parallel-authored entries, highest error risk). +- **`04-final-report.md` + `06-architect-handover.md`** — validation subagent gate (architect-facing; synthesis is not single-subsystem/<30min, so self-validation is not contract-permitted). +- **`01`, `03`, `05`** — orchestrator self-validation with documented evidence (all authored personally from primary sources + the validated catalog). This is the recorded deviation; it is narrower than a subagent gate per doc but every downstream doc draws on the subagent-validated catalog. + +## Execution Log +- [07:49] Created workspace `docs/arch-analysis-2026-06-28-0749/` +- [07:50] User selected **Option C (Architect-Ready)** +- [07:51] Holistic orientation: layout, LOC, pyproject, import-linter contracts, Loomweave status +- [07:52] Wrote coordination plan + discovery findings +- [07:55] Advisor review → adopted all 4 points: exhaustive file map, Loomweave-grounded deps, broadened validation scope, Filigree-ID wiring for quality/handover +- [07:58] Built + verified exhaustive 182-file map (`temp/file-map.tsv`); split S8/S9; wrote `temp/catalog-spec.md` +- [08:10] Dispatched 12 `codebase-explorer` agents (PARALLEL); 11 succeeded first pass, S12 hit a transient rate-limit and was re-dispatched. All 12 entries written to `temp/catalog-S*.md`. +- [08:25] Cross-agent signal: S1/S3/S7 independently report the tracked layering violation already remediated. **Orchestrator verified directly:** `lint-imports` → "1 kept, 0 broken"; no `core.attest` import in `scanner/`. Assembled `02-subsystem-catalog.md` (12 entries + cross-cutting synthesis). +- [08:30] `analysis-validator` gate on catalog → **PASS-WITH-NOTES** (`temp/validation-catalog.md`); validator independently reproduced `lint-imports`. Applied the one HIGH correction (S3's stale "still broken" claim). +- [08:45] Confirmed via Filigree `issue_get`: `wardline-9ec283d168` CLOSED 2026-06-20 (narrow contract done; broad `core/` layering residual = ~102 deferred imports, untracked). Wrote `03-diagrams`, `04-final-report`, `05-quality-assessment` (Filigree-ID mapped), `06-architect-handover`. +- [09:00] Second `analysis-validator` gate on synthesis (`04`+`06`) → **BLOCK** on 2 must-fixes: `wardline-82f49ec3c3` mis-stated OPEN (actually CLOSED 2026-06-01 — propagated from stale `ROADMAP.md`) + a false footer attestation. Verified the ID CLOSED via `issue_get`; corrected `05` L4 + `06` Tier 3 + footer; addressed NOTE-1. Re-validation requested. +- [09:10] Synthesis re-validation → **PASS-WITH-NOTES** (BLOCK cleared; `temp/validation-synthesis.md`). Tracker fidelity 11/11; both must-fixes verified; only non-blocking NOTE-2 (report §4 thematic ordering) / NOTE-3 (absolute phrasings, routed to STRIDE) remain by choice. +- [09:20] User confirmed → **filed 11 Filigree issues** under `wardline-bf004e2aea`, label `arch-analysis-2026-06-28` (IDs in `06-architect-handover.md` §5b): 7971cbcf9e, a0eaa7dd12, 8a1399a8b5, 5e4a4ee246, 3932db542c, 83c416811a, da175547cf, a8c1815e64, e2487c053a, 00beb310e0, f3ef15adb2. +- **STATUS: COMPLETE.** All 6 deliverables written and durable; both validation gates PASS-WITH-NOTES; 11 untracked findings filed as tracked issues. diff --git a/docs/arch-analysis-2026-06-28-0749/01-discovery-findings.md b/docs/arch-analysis-2026-06-28-0749/01-discovery-findings.md new file mode 100644 index 00000000..8bf24add --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/01-discovery-findings.md @@ -0,0 +1,101 @@ +# 01 — Discovery Findings (Holistic Assessment) + +**Target:** `wardline` @ `e4668abc` (branch `release/consolidation-2026-06-26`) +**Date:** 2026-06-28 · **Analyst:** orchestrator + Loomweave index (fresh) + Filigree tracker + +--- + +## 1. What the system is + +Wardline is a **deterministic, opt-in semantic-tainting static analyzer for Python** (with a preview +Rust command-injection frontend). It reads code statically — never executes it — and tracks a *trust +level* (taint) for values through function bodies and the project call graph, flagging where untrusted +data reaches a trusted producer or a dangerous sink without validation in between. + +Its defining design stance, stated in the README and enforced throughout: **silent until opt-in.** +Undecorated code is "unknown-trust" and produces no findings; the developer declares trust with three +decorators (`@external_boundary`, `@trust_boundary`, `@trusted`) and only then is enforcement active. +This is what lets it scan a large codebase (including its own) with zero noise. + +It is one tool in the **Weft** suite (sibling tools: Loomweave code-archaeology, Filigree issue tracker, +legis governance). Wardline *analyzes* trust; it federates with siblings but does not depend on them at +runtime — they are opt-in integrations behind extras. + +**Maturity:** `Development Status :: 5 - Production/Stable`, version 1.0.6 shipped to PyPI. Staged build +SP0–SP9 complete. + +## 2. Technology stack + +| Dimension | Choice | Notes | +|-----------|--------|-------| +| Language | Python ≥ 3.12 | `from __future__ import annotations` throughout; typed (`Typing :: Typed`) | +| Build | hatchling 1.30.1, `uv` | version from `src/wardline/_version.py` | +| **Base runtime deps** | **none** | `dependencies = []` — the zero-dep base is a hard product invariant | +| Extras | `scanner` (pyyaml/jsonschema/click), `docs` (mkdocs), `loomweave` (blake3), `rust` (tree-sitter + tree-sitter-rust) | power is opt-in *activation*, not opt-in config | +| Parsing | stdlib `ast` (Python); `tree-sitter` (Rust preview) | no third-party Python parser | +| Dev tooling | pytest (+cov, +randomly, hypothesis), ruff, mypy strict, **import-linter** | `make ci = lint typecheck test-cov`, 90% coverage gate | +| Wire/crypto | stdlib `hashlib`/`hmac`/`urllib` only | HMAC-signed federation hops; no `requests`/`httpx` | + +## 3. Size & shape + +- **182 Python source files, ~42,584 LOC** in `src/wardline/`; **367 test files** in `tests/` (≈2:1 test:source by file count — strong test investment). +- Loomweave index (fresh @ HEAD): **7,729 entities, 20,068 edges, 540 subsystem clusters**; SEI populated. +- LOC by top-level module: + +| Module | LOC | Role | +|--------|-----|------| +| `scanner/` | 14,088 | The analysis engine: AST primitives, taint dataflow, the rule lattice | +| `core/` | 13,550 | Orchestration, config, findings/outputs, gate discipline, trust-evidence, identity, federation | +| `mcp/` | 5,453 | Dependency-free MCP-over-stdio server (the "primary agent surface") | +| `rust/` | 3,156 | Tree-sitter Rust command-injection preview frontend | +| `cli/` | 2,641 | `click` command surface (one module per command) | +| `install/` | 1,766 | Agent enablement: pre-commit, `.mcp.json`, packs, skills | +| `loomweave/` | 1,002 | Loomweave/Clarion taint-store HTTP client (HMAC wire) | +| `filigree/` | 262 | Filigree emitter + dossier client | +| `decorators/` | 148 | The runtime trust-vocabulary grammar (the 3 decorators) | + +## 4. Entry points & runtime flow + +- **Console script:** `wardline = wardline.cli.entrypoint:main` — a dependency-light shim that imports + `cli.main:cli` and prints a clean "install `wardline[scanner]`" error if the scanner extra is missing. +- **CLI surface:** `cli/main.py` wires ~20 command modules (scan, explain_taint, judge, fix, doctor, + assure, attest, dossier, rekey, baseline/waiver via findings, install, mcp, lsp, scan_job(+worker), …). +- **MCP server:** `wardline mcp` → `mcp/server.py` — JSON-RPC 2.0 over stdio, no SDK; 15+ tools. +- **Shared core:** both CLI and MCP call `core/run.py:run_scan` / `gate_decision` — *identical by + construction* (the SP8 keystone extraction). `core/baseline.generate_baseline` and + `core/judge_run.run_judge` are the other two extracted shared entry points. +- **Pipeline (per scan):** discover sources → frontend analyzer (`scanner/pipeline.py`) builds an + `AnalysisContext` (AST + taint over call graph) → rule lattice emits `Finding`s → suppression + (baseline/waivers/judged) → gate decision → emit (JSONL/SARIF/Filigree/legis). + +## 5. Intended architecture & known boundary debt + +The `[tool.importlinter]` config encodes intended `core/` layering and **documents a live violation**: + +> "Taint engine must not import the attestation layer" — **BROKEN today**: +> `wardline.scanner.pipeline` / `wardline.scanner.taint.project_resolver` import `wardline.core.attest`. +> Contract is report-only (`lint-imports || true`); the fix is tracked as `wardline-9ec283d168`. + +This is a real, self-acknowledged architectural concern: the layering is **scanner (engine) ← core +(orchestration/evidence)** in intent, but the engine reaches up into the attestation layer. Carried into +the quality assessment. + +## 6. Proposed subsystem decomposition (11) + +Coupling is largely one-directional: **CLI / MCP → core → scanner**, with `loomweave`/`filigree`/`legis` +as leaf federation clients and `rust`/`decorators` as leaf providers. The 11 cohesive units (see +`00-coordination.md` for file membership): + +1. Scanner Engine · 2. Rule Lattice (+ decorators) · 3. Taint Engine · 4. Core Orchestration & Config · +5. Findings, Outputs & Emit · 6. Gate Discipline · 7. Trust Evidence & Judge · 8. Identity, SEI & +Federation · 9. MCP Server · 10. CLI & Install/Activation · 11. Rust Frontend + +## 7. Structural oddities noted (low severity) +- `src/src/` (empty) and `src/wardline/src/wardline/` (perm-restricted stray) — likely build/worktree + artifacts, not packaged; verify they are gitignored / excluded from sdist. +- `src/wardline/skills/wardline-gate/` — a bundled agent skill payload shipped inside the package. + +## 8. Confidence +**High** for structure, stack, entry points, and the layering-debt finding (all from primary sources: +pyproject, import-linter config, source headers, Loomweave index). Per-subsystem internals are delegated +to the parallel catalog pass (next), each entry carrying its own evidence-based confidence. diff --git a/docs/arch-analysis-2026-06-28-0749/02-subsystem-catalog.md b/docs/arch-analysis-2026-06-28-0749/02-subsystem-catalog.md new file mode 100644 index 00000000..4e1ec113 --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/02-subsystem-catalog.md @@ -0,0 +1,751 @@ +# 02 — Subsystem Catalog + +**Target:** `wardline` @ `e4668abc` · **Date:** 2026-06-28 · **Strategy:** 12 parallel `codebase-explorer` +agents, one per subsystem, each grounded by `temp/catalog-spec.md` (Loomweave-derived dependency edges + +`file:line` evidence). File→subsystem map: `temp/file-map.tsv` (all 182 source files assigned exactly once). + +> **Validation status:** entries below were authored in parallel and gated by an `analysis-validator` +> subagent — see `temp/validation-catalog.md`. The headline layering finding was additionally verified by +> the orchestrator directly (`lint-imports`: 1 kept / 0 broken — see §Cross-cutting). + +## Cross-cutting findings (orchestrator synthesis across the 12 entries) + +These recur across multiple subsystems and are the load-bearing inputs to `05-quality-assessment.md`: + +1. **Tracker/doc drift — the layering "violation" is already fixed (VERIFIED).** `pyproject.toml:170-182` + still declares the taint-engine→attestation contract "BROKEN today" and CI runs `lint-imports || true` + (non-gating). Reality at HEAD: **no `core.attest` import exists in `scanner/`** (S1, S3, S7 all confirm; + orchestrator ran `lint-imports` → **1 kept, 0 broken**). Both former offenders now import + `core.ruleset.ruleset_hash` (`pipeline.py:134`, `taint/project_resolver.py:28`). The fix (`wardline-9ec283d168`) + landed; the comment, the `|| true`, and possibly the issue are stale. Several other tracked tickets are + likewise largely-already-done (S9: `wardline-18499aaa2d` WeftHttp extraction, `wardline-80e457bc41` + federation envelope). +2. **God-functions/modules are the dominant structural risk.** `analyzer._analyze_inner` (~857 lines, + S1), `core.run.run_scan` (~374 lines, S4), `taint/variable_level.py` (~2,481 LOC, S3), + `install/doctor.py` (~947 lines, S11). All are defended by conformance tests + invariants rather than + decomposition — high change-risk, low unit-testability. +3. **Cross-subsystem private-name reach.** Multiple subsystems import each other's `_private` symbols: + S2 rules import `decorator_provider._is_builtin_decorator_fqn` (S3); S1 `grammar.py` imports + `variable_level._SERIALISATION_SINKS` (S3); S6 `delta_resolve.py:350` reaches `sei_resolver._client` + (S9); S11 `doctor.py` reaches seven `install.doctor` privates. Encapsulation is porous at the seams. +4. **Security invariants are split across caller/callee (correct but fragile).** Secure-by-default gating + (S6) depends on S4 passing the un-suppressed population; THREAT-001 confinement is split `_resolve_under_root` + (S10 `mcp/tooling.py`) + `confine_to_root`/`safe_paths.py` (S4); fingerprint determinism (S8) is enforced + in `scanner/rules/_fingerprint.py` (S1/S2). Each is guarded only by conformance tests, not a local guard. +5. **`pytest`-coupled handshake bypass (S10).** `_initialized = "pytest" in sys.modules` + (`mcp/protocol.py:43-46`) disables the MCP "not initialized" gate whenever pytest is importable. + +--- + + +## S1 — Scanner Engine + +**Location:** `src/wardline/scanner/{analyzer,pipeline,context,grammar,index,ast_primitives,flow_trace,diagnostics}.py` + +**Responsibility:** Orchestrates the per-project AST walk — parse → entity index → L1 trust-decorator seeding → L3 transitive fixed point → L2 flow-sensitive variable taints — and exposes the result as a frozen, read-only `AnalysisContext` for the rule lattice (S2), plus the engine's own self-diagnostic findings. + +**Key Components:** +- `analyzer.py` — `WardlineAnalyzer` (`analyzer.py:218`) and its end-to-end driver `_analyze_inner` (`analyzer.py:249`); `build_analyzer(...)` factory (`analyzer.py:1108`). This is the orchestration core: it runs the parse stage, calls S3's project resolver, drives the bounded L2 fixed-point loop (`analyzer.py:806`), assembles the `AnalysisContext` (`analyzer.py:969`), and runs each rule in per-rule isolation (`analyzer.py:1054`). +- `pipeline.py` — typed pipeline-stage DTOs and stages: `run_parse_project_stage` (`pipeline.py:87`) reads/parses/indexes/seeds/cache-classifies files into `ParseProjectOutput`; `run_l2_function_stage` (`pipeline.py:307`) runs one function's variable-level walk. Frozen-dataclass I/O contracts (`ParsedFile`, `ParseProjectInput/Output`, `L2FunctionInput/Output`). +- `context.py` — `AnalysisContext` (`context.py:42`), the frozen engine-output DTO consumed by rules; deep-freezes every nested mapping to `MappingProxyType` in `__post_init__` (`context.py:138`). `RuleRegistry` (`context.py:191`) is the rule-dispatch seam (`run` → `rule.check(context)`, `context.py:204`). +- `grammar.py` — the extensible trust grammar (`BoundaryType` `grammar.py:56`, `TrustGrammar` `grammar.py:149`, `default_grammar()` `grammar.py:223`): an open meta-model letting agents add trust vocabulary/rules without editing engine source, with a load-time tripwire that fails closed if builtins drift from `core.registry.REGISTRY` (`grammar.py:138`). +- `index.py` — per-file entity discovery: `discover_file_entities` (`index.py:57`) and `discover_class_qualnames` (`index.py:30`) build `Entity` rows (`index.py:20`); handles `@overload` drop, last-wins rebinding, `@property` setter/deleter identity. +- `ast_primitives.py` — stdlib-only AST helpers: `build_import_alias_map` (`ast_primitives.py:21`), `iter_calls_in_function_body` (`ast_primitives.py:96`), `resolve_call_fqn` (`ast_primitives.py:171`), `resolve_self_method_fqn` (`ast_primitives.py:142`). +- `diagnostics.py` — engine-diagnostic `Finding` builders: `build_collision_findings` (the proactive fingerprint-uniqueness guard, `diagnostics.py:106`), `build_unknown_import_findings`/`diagnose_unknown_imports` (`diagnostics.py:196`/`295`), `build_metric_finding`, `build_diagnostic_findings`. +- `flow_trace.py` — `build_finding_code_flow` (`flow_trace.py:163`) projects S3 taint provenance into a stable `FindingCodeFlow` DTO for S5 formatters/explain. + +**Public surface / entry points:** +- `WardlineAnalyzer.analyze(files, config, *, root)` (`analyzer.py:240`) — implements `core.protocols.Analyzer`; the engine's single run entry. +- `build_analyzer(*, grammar, summary_cache)` (`analyzer.py:1108`) — agent-facing factory (default = `default_grammar()`). +- `AnalysisContext` (`context.py:42`) + `RuleRegistry.run` (`context.py:204`) — the seam S2 rules read. +- `run_parse_project_stage` / `run_l2_function_stage` (`pipeline.py:87`/`307`) — stage functions (also used directly by tests). +- `default_grammar()` / `TrustGrammar.extend(...)` (`grammar.py:223`/`160`) — the grammar extension plane. +- `build_finding_code_flow` (`flow_trace.py:163`), `discover_file_entities` (`index.py:57`), `build_import_alias_map` (`ast_primitives.py:21`). + +**Dependencies (graph-derived):** +- Inbound (who calls into this): + - **S4 Core Orchestration** → `WardlineAnalyzer.analyze` (`core/run.py:381`, graph-resolved attribute-receiver) and `build_analyzer` via `core.frontends.PythonFrontend.build_analyzer` (`core/frontends.py:70`, graph-resolved edge), and `frontend.build_analyzer` (`core/run.py:332`). + - **S2 Rule Lattice** consumes the `AnalysisContext` output (passed in via `RuleRegistry.run → rule.check(context)`, `context.py:207`). + - **S5 Findings/Outputs** and **S7 Trust Evidence** read the context downstream — e.g. `core/assure.py` reads `context.declared_qualnames` as the coverage denominator (documented at `context.py:96-98`); `flow_trace.build_finding_code_flow` feeds explain/SARIF. (Production read paths corroborated by the context docstrings; the only graph callers of the `AnalysisContext` constructor are `analyzer.py:969` plus test fixtures under `tests/unit/core/test_assure.py`, `test_dossier_assembler.py`, `test_sarif.py`.) +- Outbound (what this calls), all from the `_analyze_inner` neighborhood (graph-resolved callees): + - **S3 Taint Engine** (the entire `scanner/taint/*` package): `project_resolver.resolve_project_taints` (`analyzer.py:291`), `decorator_provider.DecoratorTaintSourceProvider`/`vocabulary_star_exports` (`analyzer.py:252`), `variable_level.analyze_function_variables`/`attribute_write_recording`/`project_attribute_writes`, `call_taint_map.build_call_taint_map` (`analyzer.py:659`), `module_summariser.collect_module_global_raw_seeds`/`own_scope_global_names`, `function_level.seed_function_taints` (via `pipeline.py:159`), `summary.compute_cache_key`/`summary_cache.SummaryCache`. + - **S2 Rule Lattice**: `rules.build_default_registry` (`analyzer.py:960`) and `rules._sink_helpers.collect_sink_bindings` (`analyzer.py:562`). + - **S5 Findings model**: `core.finding` (`Finding`, `Kind`, `Location`, `Severity`, `ENGINE_PATH`) used throughout. + - **Shared core taint lattice**: `core.taints` (`TaintState`, `combine`, `RAW_ZONE`, `TRUST_RANK`) — `analyzer.py:20`, foundational taint type system (closest label S3, but it physically lives in `core/`). + - **S4 Core** low-tier helpers: `core.qualname.module_dotted_name` (`pipeline.py:13`), `core.ruleset.ruleset_hash` (`pipeline.py:134`), `core.registry.REGISTRY` (`grammar.py:24`). + +**Patterns Observed:** +- Staged pipeline with frozen, slotted dataclass I/O contracts between stages (`ParseProjectInput/Output`, `L2FunctionInput/Output` in `pipeline.py`); `AnalysisContext` deep-freezes nested mappings to `MappingProxyType` so engine output is genuinely read-only (`context.py:138-177`). +- Secure-by-default / fail-closed diagnostics: an unparseable or unexpectedly-failing file becomes a *gate-eligible* `ERROR`/`DEFECT` (`WLN-ENGINE-PARSE-ERROR` `pipeline.py:192`, `WLN-ENGINE-FILE-FAILED` `pipeline.py:223`/`analyzer.py:594`, `WLN-ENGINE-FUNCTION-SKIPPED` `analyzer.py:627`), never a silent skip — so `scan --fail-on ERROR` trips rather than reading green over unanalyzed code. +- Engine self-soundness guards: a proactive fingerprint-collision check over the full emitted set (`diagnostics.build_collision_findings`, run last at `analyzer.py:1104`), L2/L3 convergence bounds (`WLN-ENGINE-L2-CONVERGENCE-BOUND` `analyzer.py:935`) and monotonicity diagnostics. +- Bounded fixed points: L2 iteration bound = lattice height × cells (`analyzer.py:497`); a per-function candidate-key budget `_CANDIDATE_KEY_BUDGET = 250_000` (`analyzer.py:80`) bounds adversarial blow-up; non-convergence demotes affected returns to `UNKNOWN_RAW` (fail-safe over-taint). +- Explicit O(n²)-avoidance discipline, heavily documented: the pruned per-function taint map (`_pruned_method_taint_map` `analyzer.py:83`), once-per-module sibling/call maps, and a memoized fixed-point keyed only on iteration-varying inputs (`_L2InputKey` `analyzer.py:63`). +- Extensibility as a *code* seam, zero-dep: `TrustGrammar.extend()` appends agent boundary types/rules without engine edits; builtins are pinned byte-identical and a load-time tripwire fails closed on REGISTRY drift (`grammar.py:138`). + +**Concerns:** +- **God-method.** `_analyze_inner` is a single ~857-line method (`analyzer.py:249-1105`) wrapping ~17 nested closures and the full L2 fixed-point loop. It is the dominant structural risk for S1: very high cyclomatic complexity, no in-isolation seams (the graph shows it is only ever reached via `analyze`; all coverage is end-to-end through `analyzer.analyze`), so changes are hard to test narrowly and easy to regress. +- **Stale layering-violation tracking (wardline-9ec283d168) — the watch-item premise is outdated.** The tracked violation "`scanner.pipeline` imports `wardline.core.attest`" is NOT present at this commit. `pipeline.py:134` imports `from wardline.core.ruleset import ruleset_hash` (used at `pipeline.py:144` as `scan_policy_hash` in `compute_cache_key`), and a `grep` over all of `scanner/` finds no `attest` import anywhere (only doc-comment mentions in `taint/summary.py`). `core/ruleset.py:10-16` documents the remediation: `ruleset_hash` was rehomed to a low-tier module so the engine and `core.attest` both import *down*, replacing "a function-local deferred import in `scanner.taint.project_resolver`." Yet `pyproject.toml:173-182` still declares the contract "BROKEN today" naming both `scanner.pipeline` and `.taint.project_resolver`, and that import-linter contract is report-only (`lint-imports || true`). Scope: I authoritatively confirm the `scanner.pipeline → core.attest` direct edge is absent (my file, fully read); `project_resolver.py` is S3's, but my `scanner/`-wide grep shows zero `attest` substring there too. I do NOT claim the whole contract passes (transitive edges through other `core.*` modules are out of S1 scope). Net: documentation/process drift, not a live S1 violation. +- **Mutable `set` on a frozen dataclass.** `AnalysisContext.flow_insensitive_fallbacks: set[str]` (`context.py:136`) is a deliberate diagnostics side-channel that breaks the otherwise-strict immutability invariant `__post_init__` enforces on every other field. Documented, but a sharp edge: a rule/consumer holding the context can mutate engine output through it. +- **Identity-keyed maps.** Call-site maps key on `id(call)`/`id(stmt)` (`context.py:76-90`, `analyzer.py:737`). Sound only because AST nodes are retained for the scan lifetime; `_with_module_global_params` (`analyzer.py:158`) must carefully share the *original* body statement objects on its synthetic wrapper to keep those keys valid — a fragile invariant flagged in-source (`analyzer.py:166-173`). +- **Broad `except Exception` (BLE001) per-file/per-rule isolation** at `pipeline.py:217`, `analyzer.py:732`, `:880`, `:1059`. Intentional and well-justified (each becomes a loud gate-eligible DEFECT), but the pattern's soundness depends entirely on the surrounding fail-closed discipline staying intact. + +**Confidence:** High — Read all 8 S1 files in full (`analyzer.py` 1120 lines, `pipeline.py` 325, `context.py`, `grammar.py`, `index.py`, `ast_primitives.py`, `flow_trace.py`, `diagnostics.py`). Dependencies are graph-derived from Loomweave (`entity_neighborhood_get` on `_analyze_inner`, `entity_callers_list` on `analyze`/`build_analyzer`/`AnalysisContext`) and cross-checked against source `file:line`. The attest finding is corroborated three ways: full read of `pipeline.py`, a `grep` over all of `scanner/`, and the `core/ruleset.py:10-16` remediation docstring + `pyproject.toml:173-182` contract. Lower-confidence item, flagged inline: production consumers of `AnalysisContext` (S5/S7) rest on context docstrings + test-fixture constructions rather than graph-resolved production call edges. + +--- + +## S2 — Rule Lattice + +**Location:** `src/wardline/scanner/rules/` (26 PY-WL rules + shared infra), `src/wardline/decorators/` (the 3 runtime trust markers) + +**Responsibility:** Define the trust-vocabulary defect rules — each a self-contained `check(context) -> list[Finding]` over the engine's `AnalysisContext` — plus the rule descriptor/severity model, the shared AST/sink helpers they run on, and the three static-analysis decorators (`@external_boundary` / `@trust_boundary` / `@trusted`) whose declarations the rules police. + +**Key Components:** +- `scanner/rules/__init__.py` — registry factory + the canonical rule ordering. `_ALL_RULE_CLASSES`/`BUILTIN_RULE_CLASSES` (`__init__.py:52-86`, hand-ordered tuple; "registration order = emission order"); `build_default_registry(config, rules=None)` (`__init__.py:235`) honours `rules.enable` (fnmatch include) and `rules.severity` (per-rule base override); malformed config is surfaced as the engine self-diagnostic `WLN-ENGINE-POLICY-CONFIG` via `_PolicyConfigRule` (`__init__.py:88-135`). +- `scanner/rules/metadata.py` — `RuleMetadata` frozen descriptor (`metadata.py:15`): `rule_id`, `base_severity`, `kind`, `description`, examples, `maturity`, and the load-bearing `multi_emit` flag (`metadata.py:23-32`) that gates which fingerprint discriminator a rule may use. +- `scanner/rules/severity_model.py` — `modulate(base, taint)` (`severity_model.py:47`): the ~10-line tier-modulation matrix — trusted tiers keep base, partial tiers downgrade one step, freedom/fail-closed zone → `NONE`. The freedom-zone suppression is what keeps undecorated code (`UNKNOWN_RAW`) silent and the project self-host clean. +- `scanner/rules/_ast_helpers.py` — the boundary-integrity predicate library (~648 lines): own-scope reachability mini-CFG (`_reachable_statements_in_block`, `_stmt_always_terminates`), rejection-path detection (`has_rejection_path`/`has_real_rejection`/`asserts_are_sole_rejection`), one-hop same-module helper resolution (`rejecting_helper_calls`), fail-open detection (`handler_substitutes_on_failure`), and broad/silent-handler predicates. Encodes the 4-way boundary partition invariant (`_ast_helpers.py:382-392`). +- `scanner/rules/_sink_helpers.py` — the dangerous-sink machinery + `TaintedSinkRule` base (`_sink_helpers.py:735`, Loomweave-tagged `exported-api`). Name/alias canonicalization (`canonical_call_name`), binding-aware call resolution (`collect_sink_bindings`/`resolved_sink_calls`), the single fail-closed arg-taint resolver (`resolved_arg_taints`, `:483`), `ArgSpec` slot precision, and `build_sink_finding` (`:680`). `TaintedSinkRule.check` (`:843`) is the one loop the whole sink family runs. +- `scanner/rules/_fingerprint.py` — `entity_source_fingerprint` (`_fingerprint.py:49`): a position-free canonical-AST sha256, made byte-identical across CPython 3.12/3.13 (`_canonical_ast_dump`), used as the singleton-rule `taint_path` discriminator. +- `decorators/_base.py` + `decorators/trust.py` + `decorators/__init__.py` — `apply_marker` (`_base.py:39`) validates a marker against `core.registry.REGISTRY`, stamps `_wardline_*` attrs, and returns the function UNCHANGED (no runtime wrapper); `trust.py` exposes `external_boundary` / `trust_boundary(to_level=)` / `trusted(level=)`. +- The 26 rule modules (one class each, `rule_id`+`metadata`+`check`). 15 STABLE, 11 PREVIEW (`maturity=Maturity.PREVIEW`): + - `untrusted_reaches_trusted.py` — **PY-WL-101** (ERROR): an anchored `@trusted` producer whose actual return is less-trusted than declared. + - `boundary_without_rejection.py` — **PY-WL-102** (ERROR): a `@trust_boundary` with no rejection path of any shape. + - `broad_exception.py` — **PY-WL-103** (WARN, tier-mod): bare/`Exception`/`BaseException` handler in a trusted-tier fn. + - `silent_exception.py` — **PY-WL-104** (WARN, tier-mod): exception swallowed (`pass`/`...`) with no handling. + - `untrusted_to_trusted_callee.py` — **PY-WL-105** (ERROR): provably-untrusted data passed to a trusted callee at a call site. + - `untrusted_to_deserialization.py` — **PY-WL-106** (WARN, sink): untrusted bytes → pickle/yaml/marshal/dill/torch/numpy deserialization (CWE-502). + - `untrusted_to_exec.py` — **PY-WL-107** (WARN, sink): untrusted → `eval`/`exec`/`compile`. + - `untrusted_to_command.py` — **PY-WL-108** (ERROR, sink): untrusted → `os.system`/`subprocess` program-exec. + - `none_leak.py` — **PY-WL-109** (WARN): `None` leaks out of a trusted producer. + - `contradictory_trust.py` — **PY-WL-110** (WARN): ≥2 distinct trust markers on one entity (silently resolved clash). + - `assert_only_boundary.py` — **PY-WL-111** (ERROR): boundary whose only rejection is `assert` (stripped under `-O`, CWE-617). + - `untrusted_to_shell_subprocess.py` — **PY-WL-112** (ERROR, sink): untrusted → `shell=True` subprocess. + - `failopen_boundary.py` — **PY-WL-113** (ERROR): a real rejection defeated by a fail-open handler (CWE-636/703). + - `invalid_decorator_level.py` — **PY-WL-114** (ERROR): statically-readable but invalid/out-of-range decorator level (e.g. typo `'ASURED'` silently disables the gate). + - `untrusted_to_import.py` — **PY-WL-115** (WARN, sink): untrusted → dynamic `import`/`__import__`/module-load. + - `path_traversal.py` — **PY-WL-116** (WARN, sink, PREVIEW): untrusted → filesystem-path sink. + - `ssrf.py` — **PY-WL-117** (WARN, sink, PREVIEW): untrusted → HTTP-client URL (SSRF). + - `sql_injection.py` — **PY-WL-118** (ERROR, sink, PREVIEW): untrusted → SQL/DB execute. + - `degenerate_boundary.py` — **PY-WL-119** (ERROR, PREVIEW): no-op `return ` validator. + - `stored_taint.py` — **PY-WL-120** (ERROR, PREVIEW): stored/persisted taint reaches trusted state un-validated (suppress-and-delegate to 101). + - `untrusted_to_xml.py` — **PY-WL-121** (ERROR, sink, PREVIEW): untrusted → XML parse (XXE, CWE-611). + - `untrusted_to_template.py` — **PY-WL-122** (ERROR, sink, PREVIEW): untrusted → server-side template compile (SSTI, CWE-1336). + - `untrusted_to_reflection.py` — **PY-WL-123** (WARN, sink, PREVIEW): tainted attribute NAME → `setattr`/`getattr` (CWE-915). + - `untrusted_to_native.py` — **PY-WL-124** (ERROR, sink, PREVIEW): untrusted path → native-library load (CWE-114). + - `untrusted_to_log.py` — **PY-WL-125** (INFO, sink, PREVIEW): untrusted as log format string (CWE-117). + - `untrusted_to_mail.py` — **PY-WL-126** (WARN, sink, PREVIEW): untrusted recipient/message → `SMTP.sendmail` (CWE-93). + +**Public surface / entry points:** +- `build_default_registry(config, rules=None) -> RuleRegistry` (`scanner/rules/__init__.py:235`) — THE registry factory. +- `BUILTIN_RULE_CLASSES` (`scanner/rules/__init__.py:86`) — single source of truth shared with the grammar. +- `RuleMetadata` (`scanner/rules/metadata.py:15`) and `modulate` (`scanner/rules/severity_model.py:47`). +- `TaintedSinkRule` (`scanner/rules/_sink_helpers.py:735`) — sink-rule base (template method). +- `external_boundary` / `trust_boundary` / `trusted` (`decorators/__init__.py:6`) — the trust vocabulary applied to user code. +- Each rule class satisfies the duck-typed `Rule` protocol (rule_id, metadata, `check`); they are registered, not called directly. + +**Dependencies (graph-derived):** +- Inbound (who calls into this): + - **S1 Scanner Engine** → `build_default_registry` — `WardlineAnalyzer._analyze_inner` imports it (`scanner/analyzer.py:31`) and calls it (`scanner/analyzer.py:960`); verified via `entity_callers_list` (one resolved caller: `_analyze_inner`). + - **S1 Scanner Engine** → `BUILTIN_RULE_CLASSES` — `default_grammar()` imports and wraps it into the `TrustGrammar` (`scanner/grammar.py:228-230`). + - **S3 Taint Engine** consumes the decorators (not a call edge): `trusted`/`trust_boundary`/`external_boundary` have ZERO project callers (`entity_callers_list` → empty) because they are AST markers. The taint `decorator_provider` reads the decorator SYNTAX from `entity.node.decorator_list` and matches by resolved FQN (`_is_builtin_decorator_fqn`) — the static analyzer parses source, so it never sees the runtime `_wardline_*` stamps, which exist only on a live function object (`decorators/_base.py:59-62`). +- Outbound (what this calls/imports): + - **S1 Scanner Engine** — `scanner.context` (`AnalysisContext`, `RuleRegistry`, `_RuleClass` protocol), `scanner.grammar.BUILTIN_BOUNDARY_TYPES`, `scanner.index.Entity`, `scanner.ast_primitives.resolve_call_fqn`. + - **S3 Taint Engine** — `core.taints` (the `TaintState` lattice, `RAW_ZONE`, `TRUST_RANK`); `scanner.taint.decorator_provider._is_builtin_decorator_fqn` + `_shadowed_builtin_roots` (used by PY-WL-110/114 — `contradictory_trust.py:30`, `invalid_decorator_level.py:20`). + - **S5 Findings** — `core.finding.{Finding, Kind, Severity, Location, Maturity, ENGINE_PATH}` (the finding model every rule constructs). + - **S8 Identity & SEI** — `core.finding.compute_finding_fingerprint` (every finding's identity). S2 owns its own `_fingerprint.entity_source_fingerprint`, which only feeds the `taint_path` argument to that S8 call. + - **S4 Core Orchestration & Config** — `core.registry.REGISTRY` (decorator validation, `decorators/_base.py:16`), `core.protocols.Rule`, and `WardlineConfig` (`rules_enable`/`rules_severity`). + +**Patterns Observed:** +- **Duck-typed, base-less rule contract.** Every rule is a plain class with `rule_id`, `metadata`, `__init__(self, base_severity=None)`, and `check(context) -> list[Finding]`; it structurally satisfies the `Rule` Protocol (S4) — no ABC, no inheritance except the sink family. Confirmed across all 26 files (e.g. `untrusted_reaches_trusted.py:79-86`). +- **Explicit central registration, not auto-discovery.** `_ALL_RULE_CLASSES` is a hand-ordered tuple (`__init__.py:52-80`) whose order is the deterministic emission order; `BUILTIN_RULE_CLASSES` is the single alias the grammar reuses so the two construction paths cannot drift (`__init__.py:83-86`). Config-malformation fails LOUD to a `WLN-ENGINE-POLICY-CONFIG` ERROR finding rather than silently mis-enabling rules. +- **Two gating regimes.** Declaration-gated rules (101/102/105/110/111/113/114/119) emit at base severity — the decorator IS the opt-in. Tier-modulated rules (103/104 + whole sink family) scale base by the resolved taint tier via `modulate` and go silent in the developer-freedom zone (`UNKNOWN_RAW → NONE`), so undecorated code stays quiet (`severity_model.py:47-53`, `broad_exception.py:49-52`, `_sink_helpers.py:849`). +- **Decorators are inert markers, read from the AST.** `apply_marker` stamps `_wardline_*` and returns the function unchanged — no wrapper, no runtime enforcement (`decorators/_base.py:1-10, 39-63`). This is the deliberate lightweight departure from wardline.old's runtime-enforcing factory. +- **Fingerprint-discriminator discipline (wlfp2).** `multi_emit` rules must discriminate co-located findings by entity-relative span/ordinal (call-line − def-line + col span, or PY-WL-114's decorator ordinal) since `line_start` left the hash; singletons use the position-free interpreter-stable `entity_source_fingerprint` so a comment move is stable but a body change is not (`metadata.py:23-32`, `build_sink_finding` `_sink_helpers.py:680-732`, `invalid_decorator_level.py:189-205`). +- **Template-method sink base.** `TaintedSinkRule.check` is the single check loop; subclasses set `SINKS`/`SINK_SPECS`/`SINK_SEVERITIES` and override `_accept_call`/`_arg_guarded`/`_taint_anchor_call`; `__init_subclass__` fails at import if required attrs are missing (`_sink_helpers.py:780-784`). Consolidated 2026-06-10 from two former mixins. +- **Fail-closed taint resolution in one place.** `resolved_arg_taints` is the sole arg resolver; a missing L2 snapshot yields a pessimistic `UNKNOWN_RAW` per arg and records the degradation as a per-scan FACT finding (not a `UserWarning`, so a warnings-as-error embedder can't abort a rule) — `_sink_helpers.py:483-520`. + +**Concerns:** +- **Rules import engine-internal PRIVATE symbols across the subsystem boundary.** PY-WL-110 and PY-WL-114 import `_is_builtin_decorator_fqn` and `_shadowed_builtin_roots` from S3's `scanner.taint.decorator_provider` (`contradictory_trust.py:30`, `invalid_decorator_level.py:20`). Deliberate (the rules must use the engine's exact seeding predicate so they "cannot drift"), but it couples the rule layer to underscore-prefixed engine internals — a refactor of the provider's private API silently breaks two security rules. +- **Sink rules write into the "read-only" `AnalysisContext`.** `resolved_arg_taints` mutates `context.flow_insensitive_fallbacks` (`_sink_helpers.py:512`), the one field of the otherwise frozen/`MappingProxyType`-wrapped context left as a plain mutable `set` (`scanner/context.py:136`). Documented as a diagnostics side channel, but it punches a hole through the read-only contract every other field upholds, and it means a rule's `check()` has a side effect on shared engine state. +- **~648-line hand-rolled reachability/CFG in `_ast_helpers.py` is the soundness-critical surface.** The fail-open / no-rejection-path detection for the boundary-integrity family (102/111/113/119) lives entirely in intricate own-scope statement-walking with a documented 4-way partition invariant (`_ast_helpers.py:382-392`). A subtle bug in `_stmt_always_terminates`/`handler_substitutes_on_failure` directly produces FN/FP in security rules, and the logic duplicates control-flow reasoning rather than reusing the taint engine's. +- **Latent cross-subsystem invariant dependency (MIXED_RAW).** PY-WL-101 and `modulate` would DISAGREE on `MIXED_RAW` (101 fires; modulate suppresses) — currently inert only because the S3 engine guarantees `MIXED_RAW` is unreachable (`severity_model.py:21-36`, `untrusted_reaches_trusted.py:37-57`). The rule lattice's soundness here is hostage to an invariant maintained elsewhere; if S3's parser guards ever regress, the disagreement becomes live. +- **~42% of the lattice is PREVIEW.** 11 of 26 rules carry `Maturity.PREVIEW` (116-126 family + 119/120); `RuleRegistry.run` stamps that maturity onto every finding (`scanner/context.py:208-211`). Expected for the 2026-06-10 coverage-gap families, but nearly half the vocabulary is non-STABLE and the catalog should not treat sink coverage as settled. +- **Minor: reserved-but-unused parameters.** `module_prefix` on `collect_sink_bindings` (`_sink_helpers.py:249`) and `resolve_bound_call_fqn` (`:400`) are `# noqa: ARG001 — reserved` dead params for a local-class-constructor feature "not in v1" — mild YAGNI carried in the hot path. +- No base-package zero-dep violation observed (decorators import only `core.registry`/`core.taints`; rules use only stdlib `ast`/`hashlib`/`fnmatch`); `from __future__ import annotations` is universal. + +**Confidence:** High — Read in full: `rules/__init__.py`, `metadata.py`, `severity_model.py`, `_ast_helpers.py`, `_sink_helpers.py`, `_fingerprint.py`, all three `decorators/*`, the S1 `scanner/context.py` contract, and 6 representative rules across families (101/102 boundary, 103 exception, 106 sink, 110/114 decorator-policing); skimmed `rule_id`/`severity`/`maturity`/docstring for the remaining 20 rules. Cross-subsystem edges are graph-derived (`entity_callers_list` confirmed S1 `_analyze_inner` → `build_default_registry` and zero runtime callers of the decorators) and corroborated with `file:line` imports (`analyzer.py:31/960`, `grammar.py:228-230`). Lower-confidence point: `core.taints`/`core.finding` are physically in `core/` and split across S3/S5/S8 by responsibility per the spec's label table rather than by an owned-file boundary; the exact S-label split of those modules is the owning agents' call. + +--- + +## S3 — Taint Engine + +**Location:** `src/wardline/scanner/taint/` (14 modules, 5,658 LOC; `__init__.py`, `provider.py`, `decorator_provider.py`, `function_level.py`, `callgraph.py`, `module_summariser.py`, `summary.py`, `summary_cache.py`, `propagation.py`, `project_resolver.py`, `resolver_metadata.py`, `reverse_edge_index.py`, `call_taint_map.py`, `stdlib_taint.py`, `variable_level.py` + the bundled `stdlib_taint.yaml`) + +**Responsibility:** The analytical heart — seed each function's declared trust taint, build the inter-module call graph, run a monotone SCC fixed-point that refines function-summary taints across calls, and expose per-variable (L2) flow-sensitive taint so the rule lattice can fire sink findings. + +**Key Components:** +- `provider.py` — the pluggable taint-source seam: `TaintSourceProvider` Protocol (`taint_for` → `SeedResult`, `fingerprint` for cache-keying) plus the no-opinion `DefaultTaintSourceProvider` (`provider.py:77`, `provider.py:95`). Provider silence ⇒ fail-closed `UNKNOWN_RAW`. +- `decorator_provider.py` — the *live* provider: `DecoratorTaintSourceProvider` reads `@external_boundary`/`@trust_boundary`/`@trusted` off the AST, alias-resolved, mapping to a `FunctionTaint` (`decorator_provider.py:293`); builtins match only exact known exports and fail closed when their marker root is project-shadowed (`_is_builtin_decorator_fqn:113`, `_shadowed_builtin_roots:96`); `_grammar_digest`/`fingerprint_for_project` feed the cache key (`decorator_provider.py:275`, `:345`). +- `function_level.py` — L1 seeding: `seed_function_taints` maps each entity through the provider, else `UNKNOWN_RAW` (`function_level.py:44`); whole L1 precedence is `provider > UNKNOWN_RAW`. +- `callgraph.py` — call-edge extraction with a flow-sensitive reaching-definitions pass (`_candidate_receiver_classes:65`) for variable-typed dispatch (branch-union at joins, straight-line REPLACE, loop fixpoint); `build_call_edges` returns edges, resolved/unresolved counts, and candidate-callee sets (`callgraph.py:220`). +- `summary.py` — `FunctionSummary` (the cacheable per-function taint contract) + `compute_cache_key`, the content-addressed correctness gate (`summary.py:34`, `:56`). +- `summary_cache.py` — process-local cache keyed on the module `cache_key`, with optional HMAC-authenticated atomic disk persistence (`SummaryCache:99`); deserialise rejects the unreachable taint trio (`_parse_cache_taint:75`) so a tampered file cannot inject `MIXED_RAW`/`UNKNOWN_GUARDED`/`UNKNOWN_ASSURED`. +- `propagation.py` — the L3 kernel: iterative Tarjan SCC (`compute_sccs:689`) + per-SCC fixed point (`propagate_callgraph_taints:189`) using the weakest-link `combine` meet, an `8·|SCC|+8` convergence bound, monotonicity-violation diagnostics, and post-fixed-point assertions that anchored taints never change and module-default taints never upgrade. +- `project_resolver.py` — whole-project orchestration: assembles `ModuleInput`s, always recomputes edges/counts fresh, summarises (cache-hit reuse for clean modules), runs the kernel, and returns `ResolverResult` (`resolve_project_taints:142`). +- `module_summariser.py` — per-module `FunctionSummary` emission + the module-global taint channel helpers (`collect_module_global_raw_seeds:76`, `own_scope_global_names:143`). +- `call_taint_map.py` — per-file `{call-site-name → return-taint}` map folding L3 project returns + stdlib table + serialisation-sink alias closure (`build_call_taint_map:81`). +- `stdlib_taint.py` — YAML-backed curated `(pkg, fn) → return-taint` table, lazily loaded via the optional-deps gate, constrained to a legal return set (`load_stdlib_taint:116`). +- `variable_level.py` — L2: flow-sensitive per-variable taint walk over a function body (`compute_variable_taints:605`, `analyze_function_variables:372`); 2,481 LOC, the engine's largest module. +- `reverse_edge_index.py` / `resolver_metadata.py` — module-granular reverse-edge dirty-frontier (performance-only) + immutable return-shape carriers (`ResolverResult`, `ResolverRunMetadata`). + +**Public surface / entry points:** +- `resolve_project_taints(...)` → `ResolverResult` — `project_resolver.py:142` (whole-project L3). +- `seed_function_taints(...)` → `{qualname: FunctionSeed}` — `function_level.py:44` (L1). +- `build_call_taint_map(...)` → `{name: TaintState}` — `call_taint_map.py:81` (per-file L2 resolution map). +- `compute_variable_taints(...)` / `analyze_function_variables(...)` — `variable_level.py:605`, `:372` (L2 walk + result carrier). +- `compute_return_taint` / `compute_return_callee` — `variable_level.py:2310`, `:2340` (function output tier + contributing callee). +- `DecoratorTaintSourceProvider` (+ `vocabulary_star_exports`) — `decorator_provider.py:293`, `:44`. +- `SummaryCache` (+ `summary_cache_auth_secret_from_env`) — `summary_cache.py:99`, `:284`. +- `project_attribute_writes` / `attribute_write_recording` — `variable_level.py:326`, `:308` (attribute-write channel). + +**Dependencies (graph-derived):** +- **Inbound** (Loomweave `entity_callers_list` / `entity_neighborhood_get`, resolved edges, corroborated by reads): + - **S1 Scanner Engine** — `analyzer.WardlineAnalyzer._analyze_inner` calls `resolve_project_taints` and `build_call_taint_map`; `pipeline.run_parse_project_stage` calls `seed_function_taints`; `analyzer`/`flow_trace` import `variable_level`; `grammar.build_sanitiser_collision_findings` reaches into `variable_level._SERIALISATION_SINKS` (`grammar.py:196`). S1 is the sole driver of the L1→L3→L2 pipeline. + - **S2 Rule Lattice** — `rules.contradictory_trust` and `rules.invalid_decorator_level` import private decorator helpers from `decorator_provider` (`contradictory_trust.py:30`, `invalid_decorator_level.py:20`). +- **Outbound** (import lines read + `imports_out` graph): + - **S1 Scanner Engine** — `scanner.ast_primitives` (`resolve_call_fqn`, `iter_calls_in_function_body`, `resolve_self_method_fqn`: `callgraph.py:35`), `scanner.index.Entity` (`provider.py:24`, `function_level.py:20`, `callgraph.py:40`), `scanner.grammar` (`BUILTIN_BOUNDARY_TYPES`, `BoundaryType`: `decorator_provider.py:21`). + - **S4 Core Orchestration & Config** — `core.taints` (the taint lattice/algebra: `combine`, `TaintState`, `TRUST_RANK`, `RAW_ZONE`, `_PROVENANCE_CLASH`), `core.config.WardlineConfig` (`project_resolver.py:27`), `core.ruleset.ruleset_hash` (`project_resolver.py:28`), `core.registry` (`REGISTRY`, `REGISTRY_VERSION`: `decorator_provider.py:19`), `core.optional_deps.require_yaml` (`stdlib_taint.py:19`). + - **Internal only** (no other subsystem): the L1→L3→L2 modules wire to each other; no edge to S5–S12. + +**Patterns Observed:** +- **Staged, fail-closed dataflow.** Real composition is *not* bottom-up L1→L2→L3; it is L1 seeds (`function_level`) + call graph (`callgraph`) → **L3 SCC fixed point** (`project_resolver`/`propagation`) → per-file **call-taint-map** (`call_taint_map`, which folds in `ResolverResult.taint_map`) → **L2 walk** (`compute_variable_taints`) running *last* and *consuming* the refined L3 output. Every unknown defaults to `UNKNOWN_RAW`; an unresolved callee propagates the worst of seed+args (no laundering). +- **Weakest-link meet, deliberately not the join.** All aggregation (kernel callee sets, L2 expression/control-flow merges) uses the rank-meet `least_trusted`/`combine`, never `taint_join`, so two clean-but-different-family inputs do not spuriously clash to `MIXED_RAW` (a RAW_ZONE false positive) — `propagation.py:10-19`, `variable_level.py:10-17`. +- **Monotone fixed point with guardrails.** Bounded iteration (`_scc_convergence_bound:57`), strict monotonicity commit guard + `L3_MONOTONICITY_VIOLATION`/`L3_CONVERGENCE_BOUND`/`L3_LOW_RESOLUTION` diagnostics, and post-fixed-point assertions (anchored unchanged, module_default never upgraded) that fall back to seed-only provenance on violation (`propagation.py:506-548`). +- **Content-addressed cache soundness.** `cache_key` binds module path + source bytes + schema/resolver version + provider fingerprint + `scan_policy_hash`, each length-prefixed (`summary.py:56-98`); edges/counts are always recomputed fresh, so a warm run is byte-identical to cold — the key is the correctness gate and the reverse-edge dirty frontier is a pure performance over-approximation (`project_resolver.py:10-17`, `reverse_edge_index.py:14-18`). Disk cache is HMAC-authenticated so repository JSON cannot become analyzer truth (`summary_cache.py:13-23`). +- **Immutability + ambient-config discipline.** `MappingProxyType`/frozen-slots dataclasses on all result carriers (`resolver_metadata.py`); `contextvars` thread provenance-clash/alias-map/work-budget, always set/reset in `try/finally` (`variable_level.py:648-686`, `propagation.py:206-219`). Determinism via `sorted()` iteration in SCC traversal and rounds. + +**Concerns:** +- **Watch item is STALE for S3 — the layering violation is remediated here.** `project_resolver.py:28` imports `from wardline.core.ruleset import ruleset_hash`, **not** `wardline.core.attest`; a `grep` finds **zero** `core.attest` references in any `taint/*.py`. `ruleset.py:10-16` documents that `ruleset_hash` was extracted into a low-tier dependency-free module precisely to remove the engine→attest inversion "formerly masked by a function-local deferred import in `scanner.taint.project_resolver`." The resolver needs the effective-scan-policy hash to key its summary cache (`wardline-9d6a81b9e7`); it now imports *down* into `core.ruleset` instead of *up* into `core.attest`. The report-only import-linter contract (`pyproject.toml:178-182`, run as `lint-imports || true`) **now PASSES** — orchestrator + validator both ran `lint-imports` → **"1 kept, 0 broken"**, and `scanner/pipeline.py:134` also imports `core.ruleset.ruleset_hash` (no `core.attest` anywhere in `scanner/`). _[CORRECTED post-validation: an earlier draft of this entry claimed the contract was "still broken by `scanner/pipeline.py`" — that was wrong; it came from trusting the spec's stale MANDATE #3 premise instead of verifying. The pyproject comment at `:174-176` and `wardline-9ec283d168` are the stale artifacts, not the code.]_ +- **Cross-subsystem private-name leakage (two instances).** S1 `grammar.py:196` imports S3 `variable_level._SERIALISATION_SINKS`; S2 `contradictory_trust.py:30` and `invalid_decorator_level.py:20` import S3 `decorator_provider._is_builtin_decorator_fqn`/`_shadowed_builtin_roots`. Leading-underscore privates crossing subsystem boundaries are fragile coupling; the grammar import is function-local (likely cycle avoidance, since `decorator_provider → grammar` at module level). These would be the natural extraction points (e.g. a shared `boundary_fqn` helper). +- **God-module.** `variable_level.py` is 2,481 LOC (~44% of the subsystem) with ~50 functions in one file — outsized review/maintenance surface for the most correctness-sensitive component (the L2 walk). +- **Stale staging docstrings (doc drift, not behavior bug).** `provider.py:5-8` still says "SP1 ships only the trivial `DefaultTaintSourceProvider` … no decorator vocabulary in SP1," but `decorator_provider.py:298-304` is the analyzer's live default; `resolver_metadata.py:30-34` calls SCC metadata "Dormant in SP1." A reader is misled about live wiring. (Same staleness instinct that flagged the attest watch item.) +- **`module_default` taint-source class is dormant.** `module_summariser.py:8-10`/`:35-38` only emit `anchored`/`fallback`; `propagation.py` classifies a `floating_down` bucket nothing currently populates via the live provider — a kept-for-future branch that is untestable through the shipped provider. +- **Documented, deliberate soundness under-approximations (fail-closed, FN-bearing).** Star imports are not materialised for edge resolution (`project_resolver.py:16`); the module-global channel is a v1 approximation — only direct top-level statements, single-Name targets, last-binding-wins, bounded FN on conditional module init (`module_summariser.py:99-106`); an aliased serialisation sink absent from the curated stdlib table retains a residual under-taint (`call_taint_map.py:30-33`). Each under-approximates (never over-trusts), so they are precision/recall debt, not unsoundness. + +**Confidence:** High — read 13 of 14 modules in full; `variable_level.py` (2,481 LOC) read as top + `compute_variable_taints` entry point + full public-surface enumeration (grep of `def`s + Loomweave `contained`/`imports_in`/`imports_out`). Inbound/outbound edges are graph-derived (`entity_callers_list`, `entity_neighborhood_get`, resolved confidence) and corroborated against import lines I read. The attest finding is verified by grep (zero `core.attest` refs in `taint/`) plus `ruleset.py` source, overturning the watch item from primary evidence. The one inference: deep L2 corner-case soundness claims rest on `variable_level`'s docstrings, not full reads of its ~50 internal helpers. + +--- + +## S4 — Core Orchestration & Config + +**Location:** `src/wardline/core/{run,scan_jobs,scan_file_workflow,config,config_schema,ruleset,descriptor,discovery,registry,errors,protocols,paths,optional_deps,frontends,taints,gitignore,safe_paths}.py`, `src/wardline/scanner/__init__.py`, `src/wardline/{__init__,_version}.py`, `src/wardline/core/__init__.py` + +**Responsibility:** The surface-agnostic scan core — discover → analyze → suppress → gate — plus config/schema loading, ruleset identity, the language-frontend registry, and the path-confinement guards that every surface (CLI, MCP, LSP, scan-jobs) shares so they are identical by construction. + +**Key Components:** +- `core/run.py` — THE keystone. `run_scan` (`run.py:221`) is the pure discover→analyze→apply-suppressions function; `gate_decision` (`run.py:629`) turns a scan into a pass/fail verdict. Carries the data shapes `ScanSummary` (`run.py:68`), `ScanResult` (`run.py:92`, incl. the `gate_findings`/`honors_suppressions` sentinel at `run.py:110`/`run.py:130`), `GateDecision` (`run.py:151`) whose `__post_init__` invariants (`run.py:181`) forbid a tripped gate ever serialising as PASSED, and `baseline_migration_hint` (`run.py:706`). +- `core/scan_jobs.py` — file-backed, daemon-free async scan jobs under `.weft/wardline/jobs/`; `run_scan_job_worker` (`scan_jobs.py:331`) drives scan→emit→gate in a subprocess, `start_scan_job` (`scan_jobs.py:298`) spawns it from a trusted cwd (`scan_jobs.py:319`), `cancel_scan_job` (`scan_jobs.py:111`) guarded by `_pid_is_scan_job_worker` (`scan_jobs.py:87`) so a forged `status.json` pid cannot `killpg` an unrelated process group. +- `core/scan_file_workflow.py` — `scan_file_findings` (`scan_file_workflow.py:95`): one-shot scan→explain→optional Filigree-promote→identity-attach workflow for agents. +- `core/config.py` — `WardlineConfig` (`config.py:51`) + `load` (`config.py:155`) for the `weft.toml [wardline]` table; explicit-vs-implicit failure policy (`config.py:182-208`), trust-grammar pack gating (`_is_local_pack`, `config.py:109`; stat-only, never imports project code), and the federation endpoint resolvers `resolve_loomweave_url`/`resolve_filigree_url` (`config.py:517`/`config.py:556`) + `_filigree_server_scope` (`config.py:375`). `JudgeSettings`/`parse_judge_settings` (`config.py:567`/`config.py:579`). +- `core/config_schema.py` — `WARDLINE_SCHEMA` (`config_schema.py:13`), draft-2020-12 JSON-Schema with `additionalProperties:false` so a typo'd key is a hard `ConfigError`. +- `core/ruleset.py` — `ruleset_hash` (`ruleset.py:148`): deterministic `"sha256:"` over the effective scan policy. Deliberately housed BELOW both engine and attest layers to break an engine→attest layering inversion (`ruleset.py:10-21`). +- `core/discovery.py` — `discover` (`discovery.py:72`) walks `source_roots` (stdlib `os.walk`); `confine_to_root` rejects escaping roots (`discovery.py:109-115`) and skips out-of-root file symlinks (`discovery.py:142-149`, THREAT-001 read-confinement); `missing_source_roots` (`discovery.py:192`) surfaces silent under-scans. +- `core/safe_paths.py` — the WRITE-side confinement layer: `safe_project_path` (`safe_paths.py:12`) refuses symlink/escape writes, `explicit_output_target` (`safe_paths.py:45`), `write_text_no_follow` (O_NOFOLLOW, `safe_paths.py:76`), `safe_read_text_if_regular` (`safe_paths.py:129`). +- `core/paths.py` — single source of Weft on-disk locations; `weft_state_dir` (`paths.py:50`) and `artifacts_dir` (`paths.py:150`) confine an untrusted `weft.toml [wardline].store_dir`/`artifacts.dir` under root; `enclosing_project_root` (`paths.py:110`) backs the nested-scan-root FACT. +- `core/frontends.py` — `FRONTENDS` registry (`frontends.py:125`) + `LanguageFrontend` Protocol (`frontends.py:37`); `PythonFrontend`/`RustFrontend` lazily build the per-language `Analyzer` so `run_scan` never changes per language. +- `core/protocols.py` — the `Analyzer` (`protocols.py:17`) and `Rule` (`protocols.py:24`) plug-point Protocols. +- `core/registry.py` + `core/descriptor.py` — canonical trust-decorator `REGISTRY` (`registry.py:61`, `REGISTRY_VERSION="wardline-generic-2"`) and its read-instead-of-import NG-25 export `build_vocabulary_descriptor` (`descriptor.py:42`). +- `core/taints.py` — the 8-state `TaintState` lattice (`taints.py:19`), `TRUST_RANK`, and the `least_trusted`/`taint_join`/`combine` operators (`taints.py:107-121`). +- `core/errors.py` — `WardlineError` hierarchy (`errors.py:4`); `core/optional_deps.py` — `require_yaml`/`require_jsonschema` extra gates; `core/gitignore.py` — stdlib `GitignoreMatcher` (`gitignore.py:121`, trusted opt-in pruning only); `_version.py` — `__version__ = "1.0.7"`. + +**Public surface / entry points:** `run_scan` and `gate_decision` (`run.py:221`/`run.py:629`) are the shared scan/gate contract called by every surface; `ScanResult`/`ScanSummary`/`GateDecision` are the data shapes those surfaces serialise. Other entry points: `config.load` + `WardlineConfig`, `ruleset_hash` (`ruleset.py:148`), `FRONTENDS`/`LanguageFrontend`, `Analyzer`/`Rule` protocols, `REGISTRY`/`build_vocabulary_descriptor`, the `safe_paths`/`paths` confinement helpers, and the `scan_jobs`/`scan_file_findings` workflow functions. + +**Dependencies (graph-derived):** +- Inbound (who calls into S4): + - **S11 CLI** → `run_scan` (`cli/scan.py:scan`, `cli/findings.py`, `cli/fix.py`, `cli/rekey.py`) and `gate_decision` (`cli/scan.py:scan`); also the `config` URL resolvers and `safe_paths` writers. + - **S10 MCP & LSP** → `run_scan`/`gate_decision` (`mcp/server.py:_scan`, `_fix`, `_rekey`; `lsp.py:run_scan` wrapper). + - **S7 Trust Evidence & Judge** → `run_scan` (`core/attest._build_payload`, `core/assure.build_posture`, `core/dossier.build_dossier`, `core/judge_run.run_judge`, `core/decorator_coverage.build_decorator_coverage`) and `ruleset_hash` (`core/attest._build_payload`). + - **S6 Gate Discipline & Remediation** → `run_scan` (`core/baseline.collect_and_write_baseline`). + - **S5 Findings/Outputs/Explain** → `run_scan` (`core/explain._explain_local`, `core/filigree_issue._finding_for_fingerprint`). + - **S9 Federation Clients** → `ruleset_hash` (`core/legis.build_legis_artifact`); the `config` federation-URL resolvers feed S9 clients. + - **S1 Scanner Engine** → `ruleset_hash` (`scanner/pipeline.run_parse_project_stage`, resolved edge). **S3 Taint Engine** → `ruleset_hash` for summary-cache keying (`scanner/taint/project_resolver`, per `ruleset.py:14-16` docstring + `tests/unit/scanner/taint/test_project_resolver.py` — corroborated inference, not a resolved graph edge). + - **S4 self** → `scan_jobs.run_scan_job_worker` and `scan_file_workflow.scan_file_findings` call `run_scan`/`gate_decision`. +- Outbound (what S4 calls, from `run_scan` callees + references_out): + - **S1 Scanner Engine** — `Analyzer.analyze` resolves to the concrete `WardlineAnalyzer` built via `frontends.PythonFrontend.build_analyzer` → `scanner.analyzer.build_analyzer`/`scanner.grammar`. + - **S3 Taint Engine** — `scanner.taint.summary_cache.SummaryCache.load/save/summary_cache_auth_secret_from_env`. + - **S5 Findings, Outputs & Emit** — `finding.{Finding,Kind,Severity,SuppressionState,Location}`; `scan_jobs`/`scan_file_workflow` also call `agent_summary`, `emit.JsonlSink`, `sarif.SarifSink`, `federation_status`, `filigree_emit`/`filigree_issue`, `explain`. + - **S6 Gate Discipline & Remediation** — `baseline.load_baseline`, `waivers.load_project_waivers`, `suppression.apply_suppressions`/`gate_trips`/`gate_breakdown`, `delta`/`delta_resolve`/`delta_scope` (the `--affected` + `--new-since` machinery). + - **S8 Identity & SEI** — `loomweave.identity.SeiResolver` (injected; never constructed here, so `run_scan` stays network-free). + - **S12 Rust Frontend** — `frontends.RustFrontend.build_analyzer` → `rust.analyzer.RustAnalyzer` (lazy). + +**Patterns Observed:** +- One keystone, two surfaces, identical by construction: `run_scan`/`gate_decision` are the single scan/gate implementation CLI and MCP both call (`run.py:2-7`; graph-confirmed `cli/scan.scan` and `mcp/server._scan`), so findings/`active`/gate can never drift between surfaces. +- Secure-by-default gate: a separately-built UNSUPPRESSED `gate_findings` population (`run.py:486-489`) means a repo-committed baseline/waiver cannot clear the `--fail-on` gate; the posture is carried EXPLICITLY (`honors_suppressions`, `run.py:130-142`) not inferred, and `GateDecision.__post_init__` (`run.py:181-218`) hard-rejects a tripped-but-PASSED decision (the dogfood-#2 regression guard). +- Fail-closed honesty as a house style: explicit `--config` missing/malformed RAISES while the implicit default WARNS and falls back (`config.py:182-208`); an empty `--affected` scope runs the FULL tree as `full-fallback` (`run.py:366-369`); unscanned/escaping/nested-root cases become gate-eligible or FACT findings rather than silent gaps (`run.py:391-460`). +- Layered confinement against an untrusted scan tree: read-confinement in `discover` (`discovery.py:109-115`, `:142-149`), write-confinement in `safe_paths` (`safe_paths.py:19-30`, O_NOFOLLOW `:82-93`), and config-value confinement in `paths.weft_state_dir`/`artifacts_dir` (`paths.py:50-74`, `:150-167`) — three independent guards because `weft.toml` is untrusted input. +- Zero-dep base + lazy plug-points: every S4 base module is stdlib-only; third-party deps are reached only through `require_yaml`/`require_jsonschema` (`optional_deps.py`), and a new language is "add a `LanguageFrontend` class to `FRONTENDS`" with `run_scan` unchanged (`frontends.py:110-128`). + +**Concerns:** +- `run_scan` is a ~374-line god-function (`run.py:221-594`) folding discovery, `--affected` delta scoping, suppression, gate-population materialisation, the `--new-since` ratchet, and scope reporting inline; its safety net is the `GateDecision` invariants plus a large conformance suite (`tests/unit/core/test_run_affected.py`, `tests/conformance/test_warpline_delta_scope.py`) rather than decomposition — high change-risk for the most load-bearing function in the tool. +- `config.py` carries two responsibilities: the `[wardline]` policy loader AND the federation endpoint resolver (`resolve_loomweave_url`/`resolve_filigree_url`, `config.py:517`/`config.py:556`, and `_filigree_server_scope` reading the home-global `~/.config/filigree/server.json`, `config.py:363-425`). The federation half is S9-adjacent and reads OUTSIDE the project tree (fail-soft) — a cohesion smell sitting in the config module. +- Implicit-global coupling via the `_PROVENANCE_CLASH` contextvar (`taints.py:16`, read by `combine` at `taints.py:119`): `run_scan` only sets/resets it around `cache.load()` (`run.py:308-314`) so the summary-cache key reflects clash mode, while the analysis-time setter lives in S1/S3 — the per-scan `provenance_clash` config flag travels through process-global state instead of an explicit parameter. Observation, not a correctness defect (the mechanism is in code S4 doesn't own). +- `safe_paths` confinement is check-then-write: `safe_project_path` resolves+validates (`safe_paths.py:19-30`), then `safe_write_text` opens with O_NOFOLLOW on the FINAL component only (`safe_paths.py:82-93`) — a parent-directory symlink swap between check and open is a residual (narrow) TOCTOU window; explicit outputs OUTSIDE root deliberately keep no-follow-only (`safe_paths.py:57-63`), matching released CLI sibling-source behavior. +- Minor doc tension: `taint_join` is documented "RETAINED, NO PRODUCTION CALL SITE" (`taints.py:88-97`), but `combine` dispatches to it when `provenance_clash` is enabled (`taints.py:117-121`), so "no production call site" holds only for the DEFAULT path. Deliberate ADR-cited retention — flagged for the nuance, not a bug. +- Zero-dep posture verified clean: read all base modules — `config.py` (`json`/`keyword`/`os`/`tomllib`/`warnings`), plus `ruleset`/`paths`/`taints`/`errors`/`discovery`/`gitignore`/`safe_paths`/`registry`/`protocols`/`frontends` — none import a third-party package at base; `jsonschema`/`PyYAML` are reached only via the `require_*` guards (`optional_deps.py`). (`scanner/__init__.py` eagerly imports `WardlineAnalyzer`, but that is the S1 `scanner` extra's own package, not the base.) + +**Confidence:** High — read all 19 assigned files in full. Inbound edges derived from `entity_callers_list` on `run_scan`/`gate_decision`/`ruleset_hash`; outbound from `entity_neighborhood_get` callees + references_out on `run_scan`; the `_resolve_under_root` watch-item was located via `entity_find` and confirmed to live in `wardline.mcp.tooling` (S10), so S4 owns only `confine_to_root` read-confinement + `safe_paths` write guards. One edge is lower-assurance: S3's use of `ruleset_hash` (project-resolver / summary-cache keying) is docstring + test corroborated, not a resolved graph edge. The legis→`ruleset_hash` caller is mapped to S9 per the subsystem legend. + +--- + +## S5 — Findings, Outputs & Emit + +**Location:** `src/wardline/core/{finding,finding_query,emit,sarif,filigree_emit,filigree_issue,source_excerpt,agent_summary,artifacts,explain}.py` + +**Responsibility:** Define the `Finding` data model (the cross-tool analysis-fact contract) and every projection of it — query lens, JSONL/SARIF/agent-summary serialization, native Filigree scan-results emission + single-finding promote, taint-chain explanation, and timestamped on-disk artifacts. + +**Key Components:** +- `core/finding.py` — the frozen/slots `Finding` dataclass + the `Severity`/`Kind`/`SuppressionState`/`Maturity`/`Location` enums and `to_jsonl()` (`finding.py:97`); the self-describing fingerprint scheme infra `compute_finding_fingerprint`/`FINGERPRINT_SCHEME="wlfp2"`/`format`/`parse`/`require_fingerprint_scheme` (`finding.py:188-261`); the `UNANALYZED_RULE_IDS` / `INCOMPLETE_ANALYSIS_RULE_IDS` under-scan frozensets (`finding.py:42-57`); the Filigree wire mapping `to_filigree_metadata`/`severity_to_filigree` (`finding.py:265-308`). Documented as stdlib-only; finding *lifecycle* (status/issue_id) is deliberately absent — that is Filigree's domain (`finding.py:5-8`). +- `core/finding_query.py` — `filter_findings`, a pure conjunctive read-lens (`finding_query.py:69`) shared by the MCP `scan` `where` and CLI `findings`; closed-vocab predicates normalize case and reject out-of-domain values loudly (`_normalize_closed_vocab`, `finding_query.py:36`). +- `core/emit.py` — `JsonlSink`, the SP0 default output sink behind the `Sink` Protocol (`emit.py:18`); writes via S4 `safe_paths`. +- `core/sarif.py` — SARIF 2.1.0 builder `build_sarif` + `SarifSink` (`sarif.py:128`); excludes `Kind.METRIC` telemetry, rides suppression on `result.suppressions` and the fingerprint on `partialFingerprints`, and (when given an `AnalysisContext`) emits `codeFlows` from S1's `flow_trace`. +- `core/filigree_emit.py` — pure `build_scan_results_body` (`filigree_emit.py:80`) + injectable-transport `FiligreeEmitter.emit` (`filigree_emit.py:758`); URL dialect parser `filigree_api_base_url` (`:177`), path-aware chunking that preserves Filigree's `mark_unseen` sweep + INV-5 delta-scan disable (`_scan_result_chunks`, `:391`), and the PDR-0023 honesty types `EmitResult`/`FailedFinding` with derived `failed`/`auth_rejected` (`:260-367`). +- `core/filigree_issue.py` — `FiligreeIssueFiler.file`, promote-one-fingerprint-by-fingerprint (`filigree_issue.py:165`), plus the Loomweave SEI identity-attach path `attach_loomweave_identity_for_*` / `resolve_entity_binding_input` (`:313-430`). +- `core/agent_summary.py` — `AgentSummary`/`build_agent_summary` (`agent_summary.py:44`,`:363`): the compact end-of-scan handoff; one ordered union (active → suppressed → engine-facts → informational) is the pagination unit, counts stay whole-project, and `_next_actions_for` is gate-aware (`:289`). +- `core/explain.py` — taint-provenance projection: `explanation_from_context` (`explain.py:141`), the Loomweave-store chain walk `explain_chain` (`:412`), and the unified `explain_taint_result` (`:681`) shared by CLI + MCP; remediation/honesty helpers `remediation_to_dict`/`source_resolution_to_dict` (`:526-678`). +- `core/source_excerpt.py` — `extract_excerpt` (`source_excerpt.py:18`), the single path-contained chokepoint shipping local source bytes to the SP5 triage judge (escape = hard error). +- `core/artifacts.py` — `write_scan_artifact`/`timestamped_scan_artifact` (`artifacts.py:34-58`): timestamped, exclusive-create, retention-pruned artifact files under the project root. + +**Public surface / entry points:** `Finding` + enums + `compute_finding_fingerprint`/`format_fingerprint`/`parse_fingerprint`/`require_fingerprint_scheme`/`to_filigree_metadata` (`finding.py`); `filter_findings` (`finding_query.py:69`); `JsonlSink` (`emit.py:18`); `build_sarif`/`SarifSink` (`sarif.py:128`,`:176`); `FiligreeEmitter`/`build_scan_results_body`/`filigree_api_base_url`/`filigree_disabled_reason` (`filigree_emit.py`); `FiligreeIssueFiler`/`build_promote_body`/`attach_loomweave_identity_for_qualname` (`filigree_issue.py`); `build_agent_summary` (`agent_summary.py:363`); `explain_taint_result`/`explanation_to_dict`/`explain_chain` (`explain.py`); `extract_excerpt` (`source_excerpt.py:18`); `write_scan_artifact` (`artifacts.py:45`). + +**Dependencies (graph-derived):** +- Inbound (who calls into this): + - **S11 CLI** — `cli/scan.py:scan` → `build_agent_summary`, `build_sarif`/`SarifSink.write`, `JsonlSink.write`, `FiligreeEmitter.emit`, `write_scan_artifact`; `cli/explain_taint.py` → `explain_taint_result`; `cli/findings.py` → `filter_findings`; `cli/file_finding.py` → `FiligreeIssueFiler.file` (all `entity_callers_list`, resolved edges). + - **S10 MCP/LSP** — `mcp/server.py:_scan` → `build_agent_summary`, `filter_findings`, `FiligreeEmitter.emit`; `mcp/server.py:_explain_taint` → `explain_taint_result`; `mcp/server.py` → `FiligreeIssueFiler.file` (`server.py:158`); the MCP/LSP surface also consumes the `Finding` model for diagnostics. + - **S4 Core Orchestration** — `core/scan_jobs.py:_write_scan_artifact` → `build_agent_summary`/`JsonlSink.write`/`SarifSink.write`; `core/scan_jobs.py:run_scan_job_worker` and `core/scan_file_workflow.py` → `FiligreeEmitter.emit` + `FiligreeIssueFiler.file`. + - **S7 Trust Evidence & Judge** — `core/judge_run.py:run_judge` → `extract_excerpt` (`source_excerpt.py`). + - **S8 Identity/Rekey** — `core/rekey.py` → `FiligreeEmitter.emit` (re-emit after a fingerprint rekey, `rekey.py:599`). + - **S1/S2/S3 (producers)** — construct `Finding`/`Location` objects (the universal product of the analysis pipeline; construction sites are diffuse, characterized rather than enumerated). +- Outbound (what this calls): + - **S1 Scanner Engine** — `explain.py` reads `AnalysisContext` provenance maps (`call_site_callees`/`function_return_taints`/`function_return_callee`/`taint_provenance`/`entities`, `explain.py:58-191`); `sarif.py` → `scanner.flow_trace.build_finding_code_flow` (`sarif.py:19,67`) + `scanner.context.AnalysisContext`. + - **S3 Taint Engine** — `explain.py` → `core.taints.RAW_ZONE` (`explain.py:21,65`). + - **S4 Core Orchestration & Config** — `explain.py`/`filigree_issue.py` → `core.run.run_scan` (`explain.py:235`, `filigree_issue.py:276`); `agent_summary.py` → `core.run.GateDecision`/`ScanResult` (`:15`); `artifacts.py` → `core.config`/`core.paths`/`core.safe_paths`; `emit.py`/`sarif.py` → `core.safe_paths`; broad `core.errors.*` use. + - **S8 Identity & SEI** — `filigree_issue.py` → `loomweave.identity.SeiResolver` (`:27,340,409`) (verified import-clean: that module is contract stdlib-only, `loomweave/identity.py:14-15`). + - **S9 Federation Clients** — `filigree_emit.py` → `core.http.WeftHttp` (`:33,613`); `filigree_issue.py` → `core.http.read_response_text`; `agent_summary.py` → `core.federation_status` (`:13`); `explain.py`/`filigree_issue.py` → duck-typed Loomweave store client (`batch_get`/`resolve`/`get_taint_fact`). + - **loomweave extra** — `explain.py` → lazy `wardline.loomweave.require_blake3` for local content-hash freshness (`explain.py:249`). + +**Patterns Observed:** +- **Pure-builder + injectable-transport seam, repeated.** Every emitter splits a pure data→dict/string builder from a thin IO wrapper: `build_scan_results_body`/`FiligreeEmitter`, `build_promote_body`/`FiligreeIssueFiler`, `build_sarif`/`SarifSink`, `explanation_from_context`/`explain_finding`. Transports are `Protocol`s injected for test (`filigree_emit.py:602`, `filigree_issue.py:127`). +- **Construction-time-consistency guards make contradictory states unrepresentable.** `EmitResult.__post_init__` forbids a reachable result carrying an error status (`filigree_emit.py:358`); `failed`/`auth_rejected` are *derived* properties over `failures`/`status` so a count can never disagree with its reasons (`:343-356`); `AgentSummary.__post_init__` refuses negative `max_findings`/`offset` (`agent_summary.py:67`). +- **Honesty / explicit-degrade invariants over silent nulls.** PDR-0023 per-finding `FailedFinding` reasons mean an empty `failures` tuple is *earned* not assumed; `source_resolution_to_dict` and the chain `"unavailable"` block name the missing capability + enablement rather than returning nulls (`explain.py:526,738`); `mark_unseen` is suppressed whenever any `INCOMPLETE_ANALYSIS_RULE_IDS` finding is present so missing findings are never read as fixed (`filigree_emit.py:99-105`). +- **Single-source-of-truth helpers prevent surface drift.** `filigree_api_base_url` is the one URL dialect parser shared by emit + promote + work-join (`filigree_emit.py:177`); `filigree_disabled_reason` is shared by CLI + MCP status blocks (`:564`); `explanation_to_dict` is the single shaper for CLI + MCP `explain_taint` (`explain.py:554`). +- **Fingerprint is a source-derived join key, defended at runtime.** The fingerprint deliberately excludes `line_start` and resolved taint tiers (`finding.py:152-198`); the scheme prefix (`wlfp2`) lets stores loud-fail on formula drift (`require_fingerprint_scheme`, `:243`). +- **Lazy-import discipline for both optional extras and acyclic decoupling** — `require_blake3` imported in-function (`explain.py:249`), `SchemeMismatchError` imported lazily to keep `finding.py` import-thin (`finding.py:256`); `from __future__ import annotations` universal; base emitters stay stdlib (urllib) — zero-runtime-dep contract upheld. + +**Concerns:** +- **Standalone explain/attach triggers a full re-scan.** `_explain_local`→`run_scan` (`explain.py:235`) and `_finding_for_fingerprint`→`run_scan` (`filigree_issue.py:274-277`) re-analyze the whole project to explain or attach identity to a *single* finding; only the opt-in Loomweave store fast-path avoids it (`explain.py:483-512`). Cost scales with project size and the mitigation is not default. +- **N-hop provenance is single-hop without the optional store (by design, but a real completeness gap).** Standalone `explain` resolves one hop (`explain.py:8-9,141-146`); the full walk `explain_chain` requires a Loomweave taint store (`:412-454`). The sink-*argument* return-indirection case is patched by `_sink_taint_source` (in-source ticket `weft-0d24cf9152`, `explain.py:82-138`), but imported/dynamic sources stay unresolvable single-scan (honestly degraded). Watch-item `wardline-82f49ec3c3` tracks this territory; **no Filigree issue is currently attached** to these entities (`entity_issue_list` → `no_matches`), so these refs come from source comments, not live tickets. +- **`explain.py` couples tightly to S1/S3 engine internals.** It reaches directly into `AnalysisContext` attribute maps and `RAW_ZONE` with no narrow interface (`explain.py:58-191`), so an engine refactor of those provenance maps would silently break explanation projection. +- **S5/S8 boundary cohabitation in `finding.py`.** The fingerprint algorithm + self-describing scheme infra (`compute_finding_fingerprint`/`FINGERPRINT_SCHEME`/`format`/`parse`/`require_fingerprint_scheme`, `finding.py:188-261`) physically live in S5's central data-model file but are conceptually S8's identity domain — a deliberate but notable cross-subsystem coupling (cross-ref S8). +- **Shallow immutability on a frozen dataclass.** `Finding.properties` is a `Mapping` that is *not* deep-frozen — "treated as read-only by convention" (`finding.py:109-111`); a caller mutating it would corrupt the frozen-identity contract. Minor: the `taint_path_v0` migration breadcrumb is documented as removable post-migration dead weight (`finding.py:115-124`), and the store-served vs re-run explanation projections are two parallel code paths to keep in sync (`explanation_from_context` vs `_explanation_from_blob`, `explain.py:141`,`:345`). + +**Confidence:** High — read all 10 assigned files in full. Inbound edges are graph-derived (`entity_callers_list`, resolved edges) from `cli.scan`, `mcp.server._scan`/`_explain_taint`, `core.scan_jobs`, `core.scan_file_workflow`, `core.judge_run`, `core.rekey`, `cli.findings`, `cli.file_finding`; outbound edges corroborated by reading actual call sites and `AnalysisContext`/`RAW_ZONE` attribute usage (not import lines alone), and the `loomweave.identity` import was verified import-clean against its source. `entity_issue_list` returned `no_matches`, so ticket refs are from in-source comments. Lower-confidence item: the diffuse set of `Finding`-construction sites across S1/S2/S3 was characterized, not exhaustively enumerated. + +--- + +## S6 — Gate Discipline & Remediation + +**Location:** `src/wardline/core/{baseline,waivers,suppression,triage,delta,delta_resolve,delta_scope,autofix}.py` + +**Responsibility:** Decide what a scan's findings *cost* — apply the git-committable baseline, expiry-aware waivers, and judge verdicts to the finding stream, evaluate the secure-by-default `--fail-on` gate predicate over the un-suppressed population, and offer the inner-loop remediation surfaces (advisory delta/`--affected` scan and the PY-WL-111 assert→raise autofix). + +**Key Components:** +- `core/suppression.py` — the suppression-application + gate predicates. `apply_suppressions` (suppression.py:33) annotates each `Kind.DEFECT` with a `SuppressionState` by delegating precedence to S8's `resolve_identity`; `gate_trips` (suppression.py:88) is the `--fail-on` predicate; `gate_breakdown` (suppression.py:102) counts the gate-relevant DEFECTs split into `(active, suppressed)`; `severity_gates` (suppression.py:81) is the rank test. `SEVERITY_ORDER`/`_RANK` (suppression.py:29-30) define the gate severity lattice (`NONE` is absent → facts/metrics never gate). +- `core/baseline.py` — the `.weft/wardline/baseline.yaml` accepted-finding snapshot, fingerprint-keyed. `Baseline` (baseline.py:45), `generate_baseline`/`collect_and_write_baseline` (baseline.py:260/207), `load_baseline`/`_build_baseline` (baseline.py:289/300), and `inspect_baseline_store` (baseline.py:80) — a read-only "can I read my own store" probe for the doctor repo-binding check (the 2026-06-26 stale-binary analog). +- `core/waivers.py` — machine-written, expiry-aware, fingerprint-keyed waivers under `.weft/wardline/waivers.yaml`. `Waiver` (waivers.py:33) with optional `entity_sei`/`entity_locator` (the rename-surviving doctrine spine), `Waiver.is_active` (waivers.py:48), `add_waiver` (waivers.py:157), `load_project_waivers` (waivers.py:128), `WaiverSet` (waivers.py:226). +- `core/triage.py` — drives the LLM judge over active DEFECTs. `run_triage` (triage.py:62) with injected `read_excerpt`/`judge_caller`; `TriageResult`/`TriageVerdict` (triage.py:27/21); `finding_to_request` (triage.py:46). +- `core/delta_scope.py` — untrusted `--affected` scope *input* parsing. `parse_affected_scope`/`parse_affected_scope_text`/`load_affected_scope` (delta_scope.py:72/98/120), `AffectedEntity`/`AffectedScope` (delta_scope.py:45/55), `ScopeParseError` (delta_scope.py:33), and the `DeltaScopeReport` honesty block + `BOUNDARY_CAVEAT` (delta_scope.py:247/239). +- `core/delta_resolve.py` — scope *resolution*. `resolve_affected_scope` (delta_resolve.py:203) turns entities into a file set + filter set; `build_qualname_index` (delta_resolve.py:130) builds the taint-free qualname→file index + structural call graph; `filter_to_affected` (delta_resolve.py:279) is the pure displayed-finding drop-filter; `_expand_callers` (delta_resolve.py:359) does the reverse-call-graph caller closure. +- `core/delta.py` — git change detection. `get_changed_files_since` (delta.py:17, the `--new-since` path, shells out to git) and `get_affected_entities` (delta.py:98, reverse-call-graph BFS). +- `core/autofix.py` — single-rule mechanical codemod. `run_autofix` (autofix.py:88) rewrites PY-WL-111 `assert` statements into `raise ` in place; `has_comment_in_span` (autofix.py:21), `get_assertion_replacement` (autofix.py:69). + +**Public surface / entry points:** +- `apply_suppressions(findings, baseline, waivers, *, today, judged)` (suppression.py:33), `gate_trips(findings, fail_on)` (suppression.py:88), `gate_breakdown` (suppression.py:102) — consumed by S4 `run_scan`/`gate_decision`. +- `generate_baseline(...)` (baseline.py:260), `load_baseline` (baseline.py:289), `load_project_waivers` (waivers.py:128), `add_waiver(...)` (waivers.py:157), `WaiverSet` (waivers.py:226). +- `run_triage(...)` (triage.py:62) — consumed by S7 `judge_run.run_judge`. +- `parse_affected_scope_text`/`load_affected_scope` (delta_scope.py:98/120), `build_qualname_index` + `resolve_affected_scope` + `filter_to_affected` (delta_resolve.py:130/203/279), `get_changed_files_since` (delta.py:17) — consumed by S4 `run_scan`. +- `run_autofix(findings, config, root, *, dry_run, confirm_cb)` (autofix.py:88) — consumed by S11 CLI `fix`/`scan` and S10 MCP `_fix`. + +**Dependencies (graph-derived):** +- Inbound (callers — `entity_callers_list`/`entity_neighborhood_get`): + - **S4 Core Orchestration** → `run.run_scan` calls `apply_suppressions` (run.py:221+), `resolve_affected_scope` + `build_qualname_index` (run.py ~19454/19507); `run.gate_decision` (run.py:629) and `run._would_trip_at` (run.py:597) call `gate_trips`. (S4 `scan_jobs`/`scan_file_workflow` reach the gate via `gate_decision`.) + - **S7 Trust Evidence & Judge** → `judge_run.run_judge` (judge_run.py:131) calls `run_triage`. + - **S11 CLI** → `cli.fix.fix` (cli/fix.py:16) + `cli.scan.scan` (cli/scan.py:39) call `run_autofix`; `cli.main._generate_baseline` (cli/main.py:72) calls `generate_baseline`. + - **S10 MCP & LSP Server** → `mcp.server._fix` calls `run_autofix`, `_baseline` (server.py:3568) calls `generate_baseline`, `_waiver_add` (server.py:3661) calls `add_waiver`. +- Outbound (callees/references — `entity_neighborhood_get`): + - **S5 Findings** → the `Finding`/`Kind`/`Severity`/`SuppressionState`/`Maturity` model (referenced across suppression, baseline, triage, autofix, delta_resolve). + - **S8 Identity & SEI** → `apply_suppressions` calls `finding_identity.resolve_identity` (the waiver>judged>baseline precedence JOIN); `delta_resolve` calls `sei_resolution.locator_to_qualname`; `build_qualname_index` calls `core.qualname.module_dotted_name`. + - **S7 Trust Evidence & Judge** → `apply_suppressions` consumes `judged.JudgedSet`; `triage` builds `core.judge.JudgeRequest`/`JudgeResponse`/`JudgeVerdict`. + - **S9 Federation Clients** → `resolve_affected_scope` references `loomweave.identity.SeiResolver` (and `_resolve_sei_qualname` reaches its bound `SeiClient.resolve_sei`). + - **S1 Scanner Engine** → `build_qualname_index` calls `scanner.index.discover_file_entities`/`discover_class_qualnames` (+ `Entity`) and `scanner.ast_primitives.build_import_alias_map`/`iter_calls_in_function_body`/`resolve_call_fqn`/`resolve_self_method_fqn`. + - **S4 Core Orchestration & Config** → `autofix` reads `config.WardlineConfig.boundary_exception`; the layer uses `errors.{ConfigError,WardlineError,DiscoveryError,JudgeTransportError}`, `paths.{baseline_path,waivers_path}`, `safe_paths.{safe_write_text,write_text_no_follow,safe_project_file}`, `optional_deps.require_yaml`; `collect_and_write_baseline` lazy-imports S4 `run.run_scan` (baseline.py:233, breaking an S6↔S4 import cycle). + +**Patterns Observed:** +- **Secure-by-default gate is population-driven, not predicate-driven.** `gate_trips` is population-agnostic — it trips only on `f.suppressed is SuppressionState.ACTIVE` (suppression.py:92), skips PREVIEW (suppression.py:94), and never gates `NONE` (suppression.py:97). The "evaluate the *as-if-unsuppressed* population unless `--trust-suppressions`" security property is therefore enforced by the **caller (S4 `run_scan`/`gate_decision`) choosing which population to hand the predicate**, not by this module. `gate_breakdown` (suppression.py:102-126) exists precisely to let the verdict say which population tripped; its docstring: the suppressed count "is exactly the set that gates only because suppressions are ignored — the number an agent clears with `--trust-suppressions`/`--new-since`." +- **Single-JOIN precedence.** waiver > judged > baseline is resolved once in S8 `resolve_identity`; `apply_suppressions` (suppression.py:68-77) only maps the returned `matched_on` tag to a `SuppressionState` (BASELINED carries no reason; WAIVED/JUDGED carry the resolver's reason). +- **Hermetic pure layer via injection.** `today: date` is injected into all suppression/gate/waiver-expiry calls; `read_excerpt` + `judge_caller` are injected into `run_triage`. The whole layer tests without a clock or network. Judge transport/excerpt failures skip-and-count (triage.py:87-96); a malformed *model* verdict (`JudgeContractError`) is deliberately uncaught so the audit primitive surfaces (triage.py:6-9). +- **Fail-loud fingerprint stores.** Baseline/waiver loaders enforce empty-guard → `fingerprint_scheme` → version → 64-hex + duplicate checks (baseline.py:300-324, waivers.py:144-154, 68-96); a malformed store raises `ConfigError` rather than silently suppressing. Baselining excludes actively-waived findings so a waiver's expiry stays observable (baseline.py:247-249), and excludes PREVIEW (`_is_baselineable_finding`, baseline.py:171). +- **Delta is advisory, never a gate, and structurally cannot narrow the gate.** INV-2: `filter_to_affected` is a pure drop-filter that re-mints no fingerprint (delta_resolve.py:44-46, 279-306). INV-4/THREAT-001: the filter touches only the *displayed* `findings`, never `gate_findings` — S4 keeps the gate population as the unfiltered analyzed set (delta_resolve.py:42-46, 294-297). INV-3: when the scope resolves zero files the result is `mode="full-fallback"`/`gate_authority="gate-of-record"` (fail-closed honesty); in delta mode `gate_authority="advisory"` (delta_scope.py:255-258). Caller-closure expansion expands only the *analyzed file set* over the reverse call graph (taint anchors caller-side) while the filter set stays the base set (delta_resolve.py:33-46, 359-390). Untrusted scope input is parsed defensively with DoS caps (4 MiB / 50k items, byte-cap-before-parse) and a loud-vs-empty split (delta_scope.py:26-43, 98-117). +- **Hardened external-process & filesystem boundaries.** `get_changed_files_since` rejects refs starting with `-`, resolves the ref through `rev-parse --verify --end-of-options`, and disables fsmonitor (delta.py:14, 21-22, 43). Autofix is AST-located, applies replacements bottom-to-top so earlier char offsets stay valid (delta_resolve sort, autofix.py:150-153), fail-closes when a comment falls inside the target span (autofix.py:46-49, 177-178), confines writes via `is_relative_to(root)` (autofix.py:106-113), and gates each write behind optional `confirm_cb`/`dry_run`. + +**Concerns:** +- **Lineless-DEFECT downgrade is a gate-relevant, fail-open-leaning rewrite.** A `Kind.DEFECT` with `location.line_start is None` (and not `ENGINE_PATH`) is *replaced* by a `Severity.NONE` `Kind.FACT` (`WLN-ENGINE-LINELESS-DEFECT`, suppression.py:47-67), so it no longer gates. This is deliberate (avoids fingerprint-collision risk on lineless defects) and mitigated by the always-emitted warning fact, but it means a class of DEFECT silently leaves the gate population — worth keeping under review. +- **Private-attribute reach into S9.** `_resolve_sei_qualname` calls `sei_resolver._client.resolve_sei(...)` (delta_resolve.py:350), coupling S6 to a private member of the S9 `SeiResolver` because the public surface returns an `EntityBinding`, not the raw `current_locator`. Documented in-code, but it will break silently on an S9 rename and is exactly the kind of edge import-linter cannot see. +- **"Codemod engine" framing oversells a single-rule fixer.** `autofix.py:1` advertises an "Autofix/codemod engine for mechanical fixes," but `run_autofix` hard-codes `if f.rule_id == "PY-WL-111"` (autofix.py:110) and the only remediation is assert→raise. Adding a second fix requires restructuring the dispatch; today the generic name and the single-rule reality diverge. +- **The secure-default invariant is not locally enforceable in S6.** Because `gate_trips`/`gate_breakdown` are population-agnostic, a future S4 caller that hands the gate the already-suppressed population would silently defeat secure-by-default. The property lives at the S6/S4 seam, guarded only by conformance tests (`test_axis7_gate_population_not_narrowed`, `test_delta_trust_suppressions_cannot_forge_green` in tests/conformance + tests/unit/core/test_run_affected). Robust, but a split invariant rather than a type-enforced one. +- **S6↔S4 import cycle, worked around by a lazy import.** `collect_and_write_baseline` defers `from wardline.core.run import run_scan` to call time (baseline.py:232-233) because S4 `run` imports the baseline loaders. The cycle is real and only resolved by import ordering. + +**Confidence:** High — all eight assigned files read in full; both directions of every cross-subsystem edge graph-derived via `entity_callers_list` + `entity_neighborhood_get` (inbound S4/S7/S10/S11; outbound S1/S5/S7/S8/S9/S4), corroborated against `file:line` in source rather than import lines. Lower confidence on two points stated as contracts, not verified internals: the exact population-selection wiring in S4 `run.py` (not in my file set — I assert the S6 predicate semantics, attribute population choice to S4) and `judged.py`'s internals (another agent's file — referenced only as the `JudgedSet` input). + +--- + +## S7 — Trust Evidence & Judge + +**Location:** `src/wardline/core/{attest.py, attest_key.py, assure.py, dossier.py, judge.py, judge_run.py, judged.py, decorator_coverage.py}`, `src/wardline/{weft_dossier.py, weft_decorator_coverage.py}` + +**Responsibility:** Turn a scan into trust *evidence* an agent can act on — a signed reproducible attestation bundle, a trust-surface coverage posture (`assure`), per-entity decorator-coverage rows, a cross-tool entity dossier, and an opt-in network-fenced LLM triage judge whose FALSE_POSITIVE verdicts become committed suppressions. + +**Key Components:** +- `core/attest.py` — build / sign / verify the `wardline-attest-2` evidence bundle: `build_attestation` (`attest.py:239`), `verify_attestation` (`attest.py:287`), the pure shared derivation `_build_payload` (`attest.py:167`), HMAC-SHA256 signer `_sign` (`attest.py:116`) over canonical key-sorted JSON `_canonical_bytes` (`attest.py:107`), `git_state` commit/dirty probe (`attest.py:67`), and lazy fail-soft SEI enrichment `_enrich_seis` (`attest.py:129`). Threat model (shared-secret, not asymmetric) documented at `attest.py:2`. +- `core/attest_key.py` — mint/load the HMAC signing secret from `WARDLINE_ATTEST_KEY` env / `.env`: `mint_attest_key` (`attest_key.py:57`, refuses to mint into a git-tracked `.env` at `:75`), `load_attest_key` (`attest_key.py:38`), non-secret `key_id` short id (`attest_key.py:110`). +- `core/assure.py` — trust-surface COVERAGE rollup (verdict-reached-either-way, denominator = anchored entities only): pure core `posture_from_scan` (`assure.py:181`), I/O shell `build_posture` (`assure.py:248`), `AssurancePosture` (`assure.py:92`), `_empty_posture` (`assure.py:138`); `coverage_pct=None` when there is no surface (`assure.py:226`) to avoid a vacuous-100% false-green. +- `core/dossier.py` — the one-call `EntityDossier` envelope (`dossier.py:323`) + assembler `build_dossier` (`dossier.py:760`); **`classify_entity_trust` (`dossier.py:588`) is the single source of truth** for the three-valued defect/clean/unknown verdict reused everywhere; `bound_to_budget` token trimmer (`dossier.py:437`) with `DOSSIER_TOKEN_BUDGET=2000` (`dossier.py:67`) and honest `Truncation` markers; `_entity_not_found_message` qualname-coupling remedy (`dossier.py:725`). +- `core/judge.py` — dependency-free LLM triage: `call_judge` (`judge.py:316`), the frozen generic policy prompt `_STATIC_POLICY_BLOCK` (`judge.py:73`) + its `JUDGE_POLICY_HASH` (`judge.py:200`), `build_messages` (`judge.py:235`), stdlib `UrllibTransport` → OpenRouter (`judge.py:296`), strict `_parse_verdict_payload` (`judge.py:415`). `JudgeVerdict` is TRUE/FALSE_POSITIVE only (`judge.py:41`). +- `core/judge_run.py` — CLI/MCP-shared orchestration `run_judge` (`judge_run.py:131`); the only core path that touches the network, and only when the default caller is actually invoked (`judge_run.py:150`); `_persist` writes FALSE_POSITIVEs at/above the confidence floor (`judge_run.py:104`); project policy gated behind `trust_judge_policy` (`judge_run.py:90`). +- `core/judged.py` — machine-managed `.weft/wardline/judged.yaml` records: `write_judged`/`load_judged` (`judged.py:78`/`:87`), `JudgedFP`/`JudgedSet` (`judged.py:28`/`:41`); loader requires `verdict: FALSE_POSITIVE` (`judged.py:122`) and full provenance (model/policy_hash/confidence, `judged.py:124`) so an unauditable suppression cannot be smuggled in. +- `core/decorator_coverage.py` — row-level sibling of `assure`: `build_decorator_coverage` (`decorator_coverage.py:226`), `decorator_coverage_from_scan` (`:186`), `DecoratorCoverageRow` (`:78`) with per-row identity/work/finding-state. +- `weft_dossier.py` — live orchestrator `build_weft_dossier` (`weft_dossier.py:61`): joins Wardline posture + Loomweave linkages + Filigree work on the opaque SEI, with a `sei:`-prefixed resolve branch (`weft_dossier.py:83`). +- `weft_decorator_coverage.py` — live wiring `build_weft_decorator_coverage` (`weft_decorator_coverage.py:29`) + `LoomweaveBindingProvider` (`:17`). + +**Public surface / entry points:** `build_attestation` / `verify_attestation` (`attest.py:239`/`:287`), `mint_attest_key` / `load_attest_key` (`attest_key.py:57`/`:38`), `build_posture` (`assure.py:248`), `build_dossier` (`dossier.py:760`) + `build_weft_dossier` (`weft_dossier.py:61`), `run_judge` (`judge_run.py:131`), `build_decorator_coverage` (`decorator_coverage.py:226`) + `build_weft_decorator_coverage` (`weft_decorator_coverage.py:29`); plus `classify_entity_trust` (`dossier.py:588`) consumed internally, and `load_judged` / `JudgedSet` (`judged.py:87`/`:41`) consumed by the suppression pipeline. + +**Dependencies (graph-derived):** +- **Inbound** (callers verified via `entity_callers_list`): + - **S11 CLI** → `cli/attest.py:148` build_attestation, `cli/attest.py:124` verify_attestation, `cli/attest.py:99` load_attest_key; `cli/install.py:68` mint_attest_key; `cli/judge.py` run_judge (caller `wardline.cli.judge.judge`); `cli/assure.py`→build_posture, `cli/dossier.py`→build_weft_dossier, `cli/decorator_coverage.py`→build_weft_decorator_coverage. + - **S10 MCP** → `mcp/server.py` `_attest` (`server.py:3117`)→build_attestation, `_verify_attestation` (`server.py:3355`), `_judge` (`server.py:3449`)→run_judge, plus `_assure`/`_dossier`/`_decorator_coverage`. + - **S9 Federation (legis)** → `core/legis.py:45` imports `attest.git_state`. + - **S4 Orchestration / S6 Suppression / S8 Rekey consume `judged.py`** (the judge→suppression feed): `core/run.py:476` `load_judged`, `core/suppression.py:18,41` `JudgedSet` (stamps `SuppressionState.JUDGED`), `core/rekey.py` `carry_judged_forward` / `JUDGED_VERSION`. + - **Internal S7→S7:** `attest._build_payload`→`assure.posture_from_scan` + `dossier.classify_entity_trust`; `assure.posture_from_scan`→`classify_entity_trust`; `decorator_coverage_from_scan`→`classify_entity_trust`; `weft_dossier`→`build_dossier`; `weft_decorator_coverage`→`build_decorator_coverage`. +- **Outbound** (by S-label, corroborated by source imports): + - **S4 Core Orchestration & Config** — `run_scan` (every `build_*`), `config.load`, `ruleset_hash` (from `core.ruleset`, `attest.py:58`), `paths` (`weft_config_path`/`judged_path`), `errors` (`AttestError`/`DossierError`/`Judge*Error`), `safe_paths`, `optional_deps.require_yaml` (`judged.py`). + - **S6 Gate Discipline & Remediation** — `waivers.load_project_waivers`/`Waiver` (`assure.py:42`), `triage.run_triage` (`judge_run.py:33`), `SuppressionState`. + - **S5 Findings, Outputs & Emit** — `core.finding` (`Kind`, `SuppressionState`, `FINGERPRINT_SCHEME`, `INCOMPLETE_ANALYSIS_RULE_IDS`), `core.source_excerpt.extract_excerpt` (`judge_run.py:32`). + - **S8 Identity & SEI** — `core.identity` (`EntityBinding`/`IdentityStatus`/`ContentStatus`), `core.sei_resolution.locator_to_qualname` (`weft_dossier.py:30`). + - **S3 Taint Engine** — `core.taints.TaintState` for the `UNKNOWN_TIERS` set (`dossier.py:45`). + - **S9 Federation Clients** — `loomweave.identity` (`SeiResolver`/`SeiCapability`), `loomweave.dossier_sources` (`resolve_entity_binding`/`LoomweaveLinkageProvider`), `loomweave.client.LinkageResult`, `filigree.dossier_client.FiligreeWorkProvider`, `filigree.config.load_filigree_token`, `core.http.read_response_text` (judge transport). + - **External** — OpenRouter HTTPS (`judge.py:36`), reached only by the judge and only when actually invoked. + +**Patterns Observed:** +- **Pure-core + I/O-shell split** so the unit tests exercise logic without disk/scan: `posture_from_scan`/`build_posture` (`assure.py:181`/`:248`), `decorator_coverage_from_scan`/`build_decorator_coverage`, and `_build_payload` shared by both build and verify (`attest.py:167`) so re-derivation is apples-to-apples. +- **Single source of truth for the trust verdict** — `classify_entity_trust` (`dossier.py:588`) is reused by `assure`, `decorator_coverage`, the dossier `TrustSection`, and attest boundaries, so a rollup and a per-entity report can never disagree (`assure.py:19`, `dossier.py:580`). +- **No-false-green discipline everywhere** — three-valued `gate_verdict` (defect/clean/unknown, `dossier.py:179`), `coverage_pct=None` on an empty surface (`assure.py:226`), honest elision markers in `bound_to_budget` (`dossier.py:437`), and `judged.yaml` requiring `verdict: FALSE_POSITIVE` + provenance (`judged.py:122`). +- **Fail-soft on optional cross-tool sources with a two-axis freshness model** — Loomweave/Filigree degrade to an honest `unavailable` section, never a crash (`dossier._linkages_from`/`_work_from` `:661`/`:678`; `attest._enrich_seis` `:129`); identity (alive/orphaned/unavailable) and content (fresh/stale/unknown) axes are never collapsed (`dossier.py:13`). +- **Determinism as a reproducibility contract** — canonical key-sorted compact JSON, every list sorted on a stable key, and the HMAC binds the outer `schema` (`attest.py:15`/`:116`); `assure` and `build_judged_document` sort identically (`assure.py:28`, `judged.py:56`). +- **Zero-dependency base held via lazy extras** — stdlib `hmac`/`hashlib`/`urllib`/`subprocess` only; Loomweave imported lazily inside `_enrich_seis` (`attest.py:143`), `require_yaml` lazily in `judged.py:78`; the judge is a dependency-free urllib POST. Untrusted-data discipline in the judge prompt: system policy vs untrusted user/code/project-policy separation + injection preamble (`judge.py:202`), project policy gated behind `trust_judge_policy` (`judge_run.py:93`). + +**Concerns:** +- **Stale layering comment + report-only contract (documentation rot).** `pyproject.toml:173-177` still asserts "This contract is BROKEN today (wardline.scanner.pipeline / .taint.project_resolver import wardline.core.attest)", but the inversion is **resolved**: `scanner/pipeline.py:134` and `scanner/taint/project_resolver.py:28` import `ruleset_hash` from `wardline.core.ruleset`, a grep finds **no production scanner import of `core.attest`**, the tracking issue `wardline-9ec283d168` is **closed** (2026-06-20; close note: "lint-imports reports 1 kept, 0 broken … the old engine-to-attestation inversion is gone"). CI still runs `uv run lint-imports || true` (`.github/workflows/ci.yml:38`), so the now-passing contract is non-gating and the comment misleads a reader. The contract could be promoted to enforcing and the comment corrected. +- **`verify_attestation` edge cases unpinned** (`wardline-d59f35c626`, open P3): a missing top-level `schema` correctly yields `signature_valid=False` (the `schema == ATTEST_SCHEMA` gate at `attest.py:336`) and a non-dict `payload` raises `AttestError` (`attest.py:329`) — the behavior exists but is not directly unit-tested in `tests/unit/core/test_attest.py`. +- **HMAC is shared-secret tamper-evidence, not asymmetric authorship proof** (`attest.py:2-13`). Anyone holding the project key can both sign and verify; a bundle does not bind to a specific signer. This is honestly documented in-module and forced by the zero-dep base (no Ed25519), but a downstream consumer could over-trust an `attest` bundle as non-repudiable proof. +- **Judge verdicts are model-dependent and cost real tokens.** A `judged.yaml` suppression ultimately rests on an LLM verdict whose only audit primitive is the verbatim rationale (`judged.py:8`); mitigated by `temperature=0`, the network-fence, opt-in invocation, and `write_confidence_floor` (`judge_run.py:215`), but it is a softer suppression source than a hand-authored waiver. +- **Minor duplication in `weft_dossier`** — the `sei:` branch and the qualname branch each independently build the capabilities probe + `SeiResolver` (`weft_dossier.py:86-87` vs `:105-106`); harmless but a small refactor target. + +**Confidence:** High — read all 10 owned files in full, plus `pyproject.toml` (import-linter contract), `.github/workflows/ci.yml`, `scanner/pipeline.py`/`scanner/taint/project_resolver.py` (the layering claim), and two Filigree issues (`wardline-9ec283d168` closed, `wardline-d59f35c626` open). Cross-subsystem edges are graph-derived (`entity_callers_list` on `attest.build_attestation`, `dossier.classify_entity_trust`, `judge_run.run_judge`) and corroborated with targeted greps for the judged→suppression feed and the legis→attest edge. The one place I contradicted the brief's watch-item (the layering violation) is backed by primary source rather than the stale in-repo comment. + +--- + +## S8 — Identity & SEI + +**Location:** `src/wardline/core/{identity,sei_resolution,fingerprint_v0,finding_identity,qualname,rekey,node_id}.py` + +**Responsibility:** The baseline-stability backbone — computes the stable identity primitives (qualname, finding fingerprint, cross-tool entity binding) that let baselines/waivers/judged verdicts re-join the same finding across runs, interpreters, and source moves, resolves rename-stable SEI addresses, and migrates fingerprint-keyed stores across scheme changes. + +**Key Components:** +- `core/finding_identity.py` — the ONE suppression JOIN predicate: `resolve_identity` joins a bare fingerprint against all three stores honouring waiver > judged > baseline precedence (`finding_identity.py:44`); `drifted_from` is reserved for future rekey provenance and is always `None` today (`finding_identity.py:58-64`). +- `core/qualname.py` — Loomweave-aligned qualname PRODUCER (stdlib `ast` only): `module_dotted_name` (one-leading-`src/`-strip rule, `qualname.py:24`), `reconstruct_qualname` (CPython `__qualname__`/`` reconstruction, `qualname.py:63`), `is_overload_stub` (`qualname.py:86`). Single source of truth for both halves of `metadata.wardline.qualname`. +- `core/rekey.py` — `wardline rekey`, the one-shot scan-driven wlfp1→wlfp2 fingerprint migration brain (829 lines): dual-fingerprint computation (`compute_old_new_fingerprints`, `rekey.py:125`), injective remap with collision/fan-out orphaning (`build_remap`, `rekey.py:187`), pre-flight snapshot (`snapshot_stores`, `rekey.py:283`), resumable journal (`Journal`, `rekey.py:421`), per-leg-atomic apply (`apply_pending_legs`, `rekey.py:540`), read-only `probe` (`rekey.py:695`), `rollback` (`rekey.py:801`), and orchestrators `run_rekey`/`resume_rekey` (`rekey.py:744`/`783`). +- `core/sei_resolution.py` — SEI addressing for findings queries: `resolve_query_filters` resolves a `where["qualname"]` beginning `sei:` to its current qualname via the S9 SeiResolver (`sei_resolution.py:28`); `locator_to_qualname` maps a Loomweave locator (`python:function:…`/`python:method:…`) back to a Wardline qualname, method-prefix before catch-all (`sei_resolution.py:13`). +- `core/identity.py` — neutral, provider-agnostic cross-tool binding model: `EntityBinding` (SEI-keyed when present, degrades to locator, `identity.py:30`), the two orthogonal status enums `IdentityStatus`/`ContentStatus` (`identity.py:14`/`22`), and pure `content_status` hash comparison (`identity.py:57`). +- `core/fingerprint_v0.py` — FROZEN byte-exact copy of the pre-P3 wlfp1 formula (`line_start` IN the hash) used ONLY by the rekey migration; never imported by the production scan path (`fingerprint_v0.py:22`, `FINGERPRINT_SCHEME_V0 = "wlfp1"` at `:19`). +- `core/node_id.py` — the shared `NodeId = NewType("NodeId", int)` per-file pre-order node-identity contract; defined neutrally so frontends share it (`node_id.py:22`). + +**Public surface / entry points:** +- `resolve_identity(fingerprint, *, baseline, waivers, judged, today)` — `finding_identity.py:44` (called by the suppression layer). +- `resolve_query_filters(where, root, config_path, …)` — `sei_resolution.py:28`; `locator_to_qualname(locator)` — `sei_resolution.py:13`. +- `run_rekey`/`resume_rekey`/`probe`/`rollback` — `rekey.py:744`/`783`/`695`/`801`. +- `module_dotted_name`/`reconstruct_qualname`/`is_overload_stub` — `qualname.py:24`/`63`/`86`. +- `EntityBinding`, `IdentityStatus`, `ContentStatus`, `content_status` — `identity.py:30`/`14`/`22`/`57`. +- `compute_finding_fingerprint_v0`, `FINGERPRINT_SCHEME_V0` — `fingerprint_v0.py:22`/`19`. +- `NodeId` — `node_id.py:22`. + +**Dependencies (graph-derived):** +- Inbound (who calls into this): + - **S6 Gate Discipline & Remediation** → `resolve_identity` (graph-resolved: `core.suppression.apply_suppressions`, `suppression.py:71`); → `module_dotted_name`/`locator_to_qualname` for delta scan (graph-resolved: `core.delta_resolve.build_qualname_index`/`_resolve_sei_qualname`, `delta_resolve.py:152`/`356`). + - **S11 CLI & Install** → `resolve_query_filters` (graph-resolved: `cli.findings.findings`, `findings.py:70`); → `run_rekey`/`probe`/`rollback`/`resume_rekey` (graph-resolved: `cli.rekey.rekey`, `cli/rekey.py:21,197`). + - **S10 MCP & LSP Server** → `resolve_query_filters` (graph-resolved: `mcp.server._scan`, `server.py:845`); → `run_rekey` et al. (graph-resolved: `mcp.server._rekey`, `server.py:4082-4096`). + - **S1 Scanner Engine** → `module_dotted_name` (graph-resolved: `scanner.pipeline.run_parse_project_stage`, `scanner.flow_trace._find_sink_contributor`/`build_finding_code_flow`); → `reconstruct_qualname`/`is_overload_stub` (graph-resolved: `scanner.index.discover_file_entities`, `index.py:45,128`). + - **S2 Rule Lattice** → `module_dotted_name` (lazy in-function import, `scanner/rules/untrusted_to_trusted_callee.py:119`). + - **S7 Trust Evidence & Judge** → `EntityBinding`/`locator_to_qualname` (dynamic-constructor edges corroborated by Read: `weft_dossier.py:95-96`, `weft_decorator_coverage.py:23`). + - **S9 Federation Clients** → `EntityBinding`/`IdentityStatus`/`ContentStatus` (dynamic-constructor edges corroborated by Read: `loomweave.identity.SeiResolver.resolve_locator`, `loomweave/identity.py:141`; `loomweave/dossier_sources.py:21`). + - **S12 Rust Frontend** → `NodeId` (`rust/nodeid.py:23`, Read-corroborated) — the *only* current consumer of `node_id.py`. +- Outbound (what this calls): + - **S6 stores** — `finding_identity` reads `Baseline`/`JudgedSet`/`WaiverSet` (`finding_identity.py:23-25,56-62`); `rekey` reads the three store version constants + carries them (`rekey.py:23-30`). + - **S5 Findings, Outputs & Emit** — `rekey` imports `FINGERPRINT_SCHEME`/`Finding`/`Kind` (`rekey.py:25`) and re-emits via the injected Filigree emitter (`rekey.py:599`); `fingerprint_v0` is the frozen mirror of `core.finding.compute_finding_fingerprint` (`finding.py:188`). + - **S9 Federation Clients** — `sei_resolution` lazily builds `LoomweaveClient` + `SeiResolver`/`SeiCapability` (`sei_resolution.py:48-63`). + - **S4 Core Orchestration & Config** — `sei_resolution` → `resolve_loomweave_url`/`WardlineError` (`sei_resolution.py:45,59`); `rekey` → `paths`/`safe_paths`/`errors`/`optional_deps` (`rekey.py:22,28-30`). + +**Patterns Observed:** +- **Two strictly-orthogonal status axes.** Identity ("same entity?") and content ("code changed?") are never collapsed: separate enums (`identity.py:14-27`), mirrored verbatim in the SEI resolver (`loomweave/identity.py:19-22`). `ORPHANED`/`STALE` are asserted only on explicit positive evidence, never guessed from a malformed body (`loomweave/identity.py:148-167`) — no false-green. +- **SEI opacity + rename-stability by delegation.** The `loomweave:eid:` token is carried verbatim and compared by equality only; S8 never parses it (`sei_resolution.py` resolves via `resolve_sei`, opacity stated at `loomweave/identity.py:17`). Rename-stability is a *property of the sibling resolver* returning a `current_locator`, not something S8 computes — `EntityBinding.binding_key` prefers the SEI and degrades honestly to the locator (`identity.py:52-54`). +- **One JOIN predicate, one precedence.** The whole suppression layer asks `resolve_identity` rather than re-implementing waiver > judged > baseline inline (`finding_identity.py:1-16,56-64`); `today` is injected so waiver expiry is hermetic. +- **Self-describing scheme + frozen legacy formula.** Live `wlfp2` (`finding.py:188-212`) vs frozen `wlfp1` (`fingerprint_v0.py`); stamping a scheme onto the store/wire makes a wrong-formula store LOUD-FAIL (`SchemeMismatchError`) instead of silently orphaning every verdict. +- **Crash-safe, snapshot-sourced migration.** `rekey` snapshots stores FIRST (the sole provenance source), journals only remap + per-leg done-flags (never content, to prevent divergence, `rekey.py:421-447`), carries from the immutable snapshot so a crash-then-resume reproduces identical content, applies legs per-leg-atomically/idempotently, and on injectivity collisions orphans-and-reports rather than aborting the whole run (`rekey.py:148-216`). Store writes are symlink-hardened through `safe_paths` (`rekey.py:283-307,477-491`). +- **Determinism contract.** Invariant: identical source → identical fingerprint, byte-stable across CPython 3.12/3.13. NOTE: the mechanism that guarantees it for entity-body findings lives OUTSIDE S8 — `_canonical_ast_dump` in `scanner/rules/_fingerprint.py:12-46` (S1/S2) reproduces 3.13's `show_empty=False` form on every interpreter — and is pinned by `tests/unit/scanner/rules/test_entity_fingerprint_stability.py` plus the byte-green identity oracle `tests/golden/identity/test_identity_parity.py`. The qualname half is pinned cross-tool by `tests/conformance/qualnames.json` + `test_loomweave_qualname_parity.py`. +- **Stdlib-only identity primitives preserve the zero-dep base.** `identity`/`qualname`/`node_id`/`fingerprint_v0` import only `dataclasses`/`enum`/`ast`/`hashlib`/`typing`; `rekey`'s `yaml` and `sei_resolution`'s Loomweave client are reached only lazily behind their extras (`rekey.py:28` `require_yaml`, `sei_resolution.py:48-56`). + +**Concerns:** +- **S8's central invariant is enforced in a sibling subsystem.** The interpreter-stable join key depends on `scanner/rules/_fingerprint.py:_canonical_ast_dump` (S1/S2) being correct; S8 owns no guard for the entity-body discriminator. The 2026-06-28 3.12↔3.13 drift this fix repaired (`_fingerprint.py:18-24`) shows the blast radius: a regression there silently breaks every baseline/waiver re-join with no S8-local failure. Cross-subsystem coupling worth a tracked invariant. +- **`node_id.py` is a defined-but-Python-unadopted contract.** `NodeId` is consumed only by the S12 Rust frontend (`rust/nodeid.py:23`); the Python frontend still keys call-site maps on raw `id(node)` ints (`node_id.py:9-16` documents the deferred SP1 migration). Not dead, but currently inert on the Python path — a half-laid contract. +- **`fingerprint_v0.py` is an intentional code clone of `finding.py`'s live formula.** The duplication is the point (frozen pre-P3 form) and is guarded by the "do not edit" docstring + the byte-green identity oracle, but a well-meaning "fix" that re-syncs it to the live engine would silently mis-reconstruct every `old_fp` and orphan verdicts on migration (`fingerprint_v0.py:1-13`). Guarded, low severity, but fragile by construction. +- **`_POLICY_CONFIG_RULE_ID` is a scanner constant hand-mirrored into core.** `rekey.py:54-60` duplicates `scanner.rules._POLICY_CONFIG_RULE_ID` to respect the core-must-not-import-scanner layering, kept in sync only by a drift test (`test_rekey_population.py`). Correct, but a maintenance trap if that drift test is ever dropped. +- **`resolve_query_filters` reduces the SeiResolver to a capability gate, then bypasses it.** It builds the resolver via raw `SeiResolver(...)` rather than the canonical `SeiResolver.detect()` factory, uses it only for `.capability.supported` (`sei_resolution.py:63-65`), then calls the RAW `loomweave_client.resolve_sei(qval)` for the actual resolve (`sei_resolution.py:67`). It also constructs a live network client *inside a query-filter resolver* (`sei_resolution.py:44-56`) — a hidden IO dependency triggered by a `sei:`-prefixed filter value. Functionally correct; minor layering smell. + +**Confidence:** High — read all 7 owned files end-to-end plus the paired live formula (`finding.py:188-212`), the S9 SEI resolver (`loomweave/identity.py`), the determinism fix (`scanner/rules/_fingerprint.py`), and its pinning test. Inbound edges are graph-derived via `entity_callers_list` on the public symbols (resolved callers for `resolve_identity`, `resolve_query_filters`, `run_rekey`, `module_dotted_name`; the graph added `scanner.pipeline`/`flow_trace` callers my grep missed and contradicted none). The lone inferred class is `EntityBinding`: as a dataclass its call sites surface as dynamic-constructor `unresolved_candidates` (S7 `weft_dossier`, S9 `loomweave/identity`), which I corroborated by reading those files. + +--- + +## S9 — Federation Clients + +**Location:** `src/wardline/core/http.py`, `src/wardline/core/federation_status.py`, `src/wardline/core/legis.py`, `src/wardline/loomweave/`, `src/wardline/filigree/`, `src/wardline/_live_oracle.py` + +**Responsibility:** Hand-rolled, stdlib-urllib-only signed HTTP transport and wire projections that let an optional Wardline scan talk to the three sibling Weft tools — Loomweave (taint-fact store + SEI identity), Filigree (finding emit + dossier work), and legis (signed governance artifact) — each pinned byte-exactly to that sibling's verifier, each fail-soft so a sibling outage never breaks a scan. + +**Key Components:** +- `core/http.py` — `WeftHttp` (`core/http.py:47`): the ONE shared round-trip (scheme-gate, `Request` build, `urlopen`-with-timeout, `HTTPError`→`HttpResult` status-band conversion, 64 KiB bounded body read) every client repeated; `fetch` (`:91`), `read_response_text` (`:22`), `HttpResult` (`:33`). `URLError`/`OSError` are deliberately NOT swallowed — the caller owns outage policy (`:67-71`). +- `loomweave/_hmac.py` — Loomweave's request signature reproduced byte-exactly from the Rust verifier `canonical_hmac_message` (auth.rs:220-234): `canonical_message` (`:25`) is `METHOD\nPATH_AND_QUERY\nsha256_hex(body)\nTIMESTAMP\nNONCE`, `sign_request` (`:36`) is lowercase-hex HMAC-SHA256; header is `X-Weft-Component: loomweave:` plus `X-Weft-Timestamp`/`X-Weft-Nonce` (replay cache, 300s window). +- `loomweave/client.py` — `LoomweaveClient` (`:154`): the `/api/wardline/*` + `/api/v1/identity/*` + `/api/v1/entities/{id}/callers|callees` client. Soft-vs-loud band routing lives in `_send` (`:193`, outage/`>=500`→None) and `_require_ok` (`:225`, non-2xx→`LoomweaveError`). Surfaces: `resolve`/`write_taint_facts`/`batch_get`/`batch_get_by_sei` (SEI read, `:341`), SEI identity wire (`capabilities`/`resolve_identity`/`resolve_sei`, `:403-420`), call-graph linkages (`get_callers`/`get_callees`, `:447-456`). Module-local `UrllibTransport` (`:48`) + `Response` (`:39`) wrap `WeftHttp`. +- `core/legis.py` — B4 signed Wardline→legis hop; legis is a PRODUCER, not a client (no HTTP). `build_legis_artifact` (`:266`) builds the verbatim-postable `scan` over the GATE population; `sign_artifact` (`:114`) is `hmac-sha256:v2:` over `canonical_json(scan-minus-signature)` (`:105`), a replica of legis's `canonical`/`signing`; `project_finding` (`:167`) is the typed projection onto legis's accepted trust vocabulary; `legis_artifact_outcome` (`:397`) and `load_legis_artifact_key` (`:135`). +- `core/federation_status.py` — the ONE canonical `{filigree_emit, loomweave_write}` status-envelope builder + JSON-schema `$defs` source: `filigree_emit_status` (`:35`), `filigree_emit_status_from_block` (`:95`, the wider MCP shape), `loomweave_write_status` (`:132`), `*_schema` builders. Reproduces each surface's current bytes via explicit flags rather than collapsing them. +- `loomweave/identity.py` — SEI client abstraction (stdlib-only by contract): `SeiResolver` (`:99`), fail-closed `SeiCapability.from_capabilities` (`:51`) and `TaintStoreCapability.from_capabilities` (`:77`); SEI carried opaque, two orthogonal axes (identity/content) never collapsed. +- `loomweave/facts.py` — `build_taint_facts` (`:55`): pure projection of the scan into `wardline-taint-1` blobs; `content_hash_at_compute` is whole-file raw-byte blake3 matching Loomweave's `current_content_hash` (`:124`, lazy blake3). +- `loomweave/dossier_sources.py` / `filigree/dossier_client.py` — live dossier sources behind the `LinkageProvider`/`WorkProvider` seams: `LoomweaveLinkageProvider` (`dossier_sources.py:43`) + `resolve_entity_binding` (`:82`); `FiligreeWorkProvider.work` (`dossier_client.py:97`) reads ADR-029 entity-associations over BEARER auth and does the drift compare itself. +- `loomweave/config.py` / `filigree/config.py` — credential loaders (env / `.env` ONLY, never `weft.toml`): `load_loomweave_token` (`config.py:16`), `resolve_project_name` (`:33`), and Filigree's 6-rung bearer ladder `load_filigree_token` (`filigree/config.py:79`). +- `loomweave/write.py` — `write_facts_to_loomweave` (`:24`), the fail-soft scan-time write orchestration; `loomweave/__init__.py` — `require_blake3` (`:16`) lazy import keeping the base package zero-dep. +- `_live_oracle.py` — `live_oracle_required`/`should_fail_live_oracle_skip` (`:25`,`:33`) + `LIVE_ORACLE_MARKERS` (`:18`): the conftest hook that turns a SKIP into a FAILURE under armed CI for `*_e2e` live oracles and `sei_drift`/`worklist_drift` source-byte rechecks (crit-3b fail-closed). + +**Public surface / entry points:** +- Transport: `WeftHttp(...).fetch` (`core/http.py:91`) — consumed by all three urllib clients. +- legis: `build_legis_artifact` (`core/legis.py:266`), `legis_artifact_outcome` (`:397`), `load_legis_artifact_key` (`:135`). +- Status envelope: `filigree_emit_status`/`_from_block`/`loomweave_write_status`/`*_schema` (`core/federation_status.py`). +- Loomweave: `LoomweaveClient` (`client.py:154`) + `resolve`/`write_taint_facts`/`batch_get_by_sei`/`get_callers`/`get_callees`/SEI wire; `SeiResolver.detect`/`resolve_locator` (`identity.py:117`,`:125`); `LoomweaveLinkageProvider` (`dossier_sources.py:43`); `write_facts_to_loomweave` (`write.py:24`); `build_taint_facts` (`facts.py:55`). +- Filigree: `FiligreeWorkProvider` (`dossier_client.py:89`); `load_filigree_token` (`config.py:79`). +- Test infra: live-oracle skip-to-fail helpers (`_live_oracle.py`). + +**Dependencies (graph-derived):** +- Inbound (callers, via `entity_callers_list`): + - **S11 CLI & Install** → `build_legis_artifact` (`cli/scan.py:scan`, byte 16293), `write_facts_to_loomweave` (`cli/scan.py:scan`, byte 19938), `LoomweaveClient` (`cli/{attest,decorator_coverage,dossier,explain_taint,file_finding,scan,scan_file_findings}.py`), `filigree_emit_status` (`cli/scan._filigree_status:663`), `WeftHttp` (`install/doctor.py:754`), and the token loaders. + - **S10 MCP & LSP** → `build_legis_artifact` (`mcp/server._attach_legis_artifact:2087`), `write_facts_to_loomweave` (`mcp/server._scan:727`), `filigree_emit_status_from_block` (`mcp/server._filigree_emit_status:138`), `LoomweaveClient` (`mcp/server.py:4569`). + - **S8 Identity & SEI** → `LoomweaveClient` + `SeiResolver` (`core/sei_resolution.py:52`). + - **S7 Trust Evidence & Judge (dossier)** → `FiligreeWorkProvider` (`weft_dossier.py:111`, `weft_decorator_coverage.py:42`), `LoomweaveLinkageProvider` + `resolve_entity_binding` (`weft_dossier.py:103,108`). + - **S4 Core Orchestration & Config** → `filigree_emit_status` (`core/scan_jobs._filigree_status:262`, `core/scan_file_workflow._emit_to_dict:49`). + - **S5 Findings, Outputs & Emit** → `WeftHttp` (`core/filigree_emit.py:613,620` — the Filigree finding-emit WRITE path). +- Outbound (imports corroborated by `Read`): + - **S4 Core Orchestration & Config** — `core.errors` (`LoomweaveError`/`FiligreeEmitError`/`LegisArtifactError`), `core.ruleset.ruleset_hash`, `core.safe_paths` (`safe_project_file`/`safe_read_text_if_regular`, used in `legis.py:49`, `filigree/config.py:32`), `core.run.ScanResult`, `core.config.WardlineConfig`. + - **S5 Findings, Outputs & Emit** — `core.filigree_emit` (`EmitResult`/`filigree_destination`/`filigree_disabled_reason`/`filigree_api_base_url`, `federation_status.py:28`, `dossier_client.py:28`), `core.finding` (`Finding`/`FINGERPRINT_SCHEME`/`SuppressionState`, `legis.py:47`). + - **S7 Trust Evidence & Judge** — `core.attest.git_state` (provenance for the signed legis artifact, `legis.py:45`), `core.dossier` seams (`LinkagesSection`/`TicketRef`/`WorkSection`, `dossier_sources.py:20`, `dossier_client.py:26`). + - **S8 Identity & SEI** — `core.identity` (`ContentStatus`/`EntityBinding`/`IdentityStatus`/`content_status`, `identity.py:30`, `dossier_sources.py:21`, `dossier_client.py:30`). + - **S3 Taint Engine** — `core.taints.TaintState` (the trust-tier lattice that seeds legis `TRUST_TIERS`, `legis.py:50,78`). **S1 Scanner Engine** — `scanner.context.AnalysisContext` (`facts.py:32`, TYPE_CHECKING). + - **Stdlib/extra only:** `urllib`/`hmac`/`hashlib`/`secrets`/`json`/`subprocess` (git, in legis); `blake3` is the sole third-party dep, lazy behind `wardline[loomweave]` (`__init__.py:16`). + +**Patterns Observed:** +- **Stdlib-urllib-only, byte-exact replicas of sibling verifiers.** No `requests`/`httpx`. Each HMAC canonical message is pinned to a named line in the sibling's source (`_hmac.py:5-7` cites Rust `auth.rs:220-234`; `legis.py:7-9` cites legis `canonical.py`/`signing.py`/`ingest.py`) and locked by golden vectors + contract-freeze tests. +- **Three distinct auth schemes, one transport.** Loomweave = per-request HMAC (`X-Weft-Component`, fresh nonce, `client.py:209-215`); legis = a `hmac-sha256:v2:` artifact signature over canonical JSON (`legis.py:114`, agent posts the bytes); Filigree = opt-in `Authorization: Bearer` over loopback (`config.py` explicitly "no HMAC"). Credentials are env/`.env`-only, never `weft.toml`. +- **Soft-vs-loud band routing as a first-class contract.** `WeftHttp.fetch` converts any HTTP status to `HttpResult` but lets `URLError`/`OSError` propagate; `_send` (`client.py:193`) maps outage/`>=500`/`403` → soft `None`, `_require_ok` raises `LoomweaveError` on other non-2xx; SEI reads use a separate `_send_json_soft` that fail-softs every non-happy band (`client.py:378`) so a pre-SEI sibling degrades, never crashes. +- **Opaque SEI, two orthogonal axes, fail-closed capability detection.** The SEI (`loomweave:eid:`) is carried verbatim and compared only by equality (`identity.py:17-21`); identity (alive/orphaned/unavailable) and content (fresh/stale/unknown) are never inferred from each other; `SeiCapability`/`TaintStoreCapability.from_capabilities` default to unsupported on any malformed body (`identity.py:51-86`). +- **Injectable `Transport` Protocol seam.** Every client takes a `transport`/double so no test touches the network (`client.py:44`, `dossier_client.py:41`); the `urlopen` symbol is resolved live at call time to preserve the monkeypatch seam (`http.py:73-75`). + +**Concerns:** +- **`wardline-18499aaa2d` (P3, open) is largely satisfied in source but stale-open.** The ticket asked to "extract shared WeftHttp" from three hand-rolled clients; `WeftHttp` (`core/http.py:47`) now exists and is consumed by all three (`loomweave/client.py:54`, `filigree/dossier_client.py:52`, `core/filigree_emit.py:613,620`). The RESIDUAL duplication is the per-client adapter shell — a near-identical `UrllibTransport` + module-local `Response` dataclass + `Transport` Protocol re-declared in each (`loomweave/client.py:39-65`, `filigree/dossier_client.py:35-62`, `core/filigree_emit.py:602-638`). The watch-item framing ("legis hand-rolls a transport") is inaccurate: legis has NO HTTP client (`legis.py:28`), so the three transport clients are loomweave/filigree-dossier/filigree-emit, not legis. +- **`wardline-80e457bc41` (P2, open) is also largely consolidated.** `core/federation_status.py` IS the single status-envelope builder and is now consumed by CLI (`cli/scan._filigree_status:663`), scan jobs (`core/scan_jobs._filigree_status:262`), the one-shot workflow (`core/scan_file_workflow._emit_to_dict:49`), and MCP (`mcp/server._filigree_emit_status:138`). The residual is thin per-surface wrapper functions; the ticket stays open for them. Both tickets warrant a triage close-or-rescope, not net-new code. +- **Replica-drift is the standing risk of the whole subsystem.** Correctness depends on byte-for-byte fidelity to EXTERNAL sources (Loomweave Rust HMAC, legis canonical/signing/ingest). Mitigated by golden vectors, contract-freeze tests, and the armed `sei_drift`/`worklist_drift` source-byte CI (`_live_oracle.py:18`), but the coupling is real and silent on the unhappy path. +- **legis fail-OPEN is acknowledged in-source, owned across a boundary.** A silent producer-key rename (`FINDINGS_FIELD`/`DIRTY_FIELD`, `legis.py:60-74`) would route zero defects into legis under a green `verified` status; legis reads those keys with defaults. The fail-open is legis's to close, the drift trigger is Wardline's; only a contract-freeze test prevents it. +- **`write_taint_facts` is non-atomic across chunks (`client.py:281-306`).** A mid-batch soft failure leaves earlier chunks committed while `written` undercounts; the documented remedy is re-running the whole scan. Correct-by-design for an idempotent per-entity replace, but a caller foot-gun worth surfacing. +- **Token loaders are themselves duplicated** across `loomweave/config.py:16`, `filigree/config.py`, and `legis.load_legis_artifact_key` (`legis.py:135`) (each mirrors `attest_key`) — four near-identical env/`.env` readers; minor, lower priority than the two tracked tickets. + +**Confidence:** High — Read all 15 owned files in full; derived every inbound edge from Loomweave `entity_callers_list` (WeftHttp, build_legis_artifact, filigree_emit_status, LoomweaveClient, FiligreeWorkProvider, LoomweaveLinkageProvider, write_facts_to_loomweave) and corroborated outbound edges against actual import lines with file:line; verified both tracked tickets via `filigree issue_get` and cross-checked the WeftHttp usage in the S5 emitter (`core/filigree_emit.py:602-638`). One minor inference: the exact S-label of `core.taints.TaintState` (the trust-tier lattice) is attributed to S3 by judgment, not a manifest. + +--- + +## S10 — MCP & LSP Server + +**Location:** `src/wardline/mcp/` (`server.py`, `protocol.py`, `tooling.py`, `resources.py`, `prompts.py`, `freshness.py`, `lsp.py`) + `src/wardline/lsp.py` + +**Responsibility:** The dependency-free, hand-rolled JSON-RPC-2.0 MCP-over-stdio server that is Wardline's primary agent surface (18 tools + 4 resources + 1 prompt, all delegating to S4 core), plus a sibling stdlib LSP diagnostics server. + +**Key Components:** +- `mcp/protocol.py` — hand-rolled JSON-RPC 2.0 + MCP envelope, no SDK (docstring `protocol.py:1-5`: "No SDK — the same stdlib discipline as the SP5 urllib judge"). `JsonRpcServer` (`protocol.py:37`); `dispatch()` is a pure, unit-testable message function (`protocol.py:62`); `run_stdio()` is the newline-framed read/write loop (`protocol.py:124`). Protocol-version negotiation: `SUPPORTED_PROTOCOL_VERSIONS = ("2025-06-18","2025-03-26","2024-11-05")` (`protocol.py:16`), echoed-if-supported else latest in `_initialize` (`protocol.py:51-60`). `McpError` carries the JSON-RPC error code for protocol faults (`protocol.py:28`). 10 MB line cap with overflow drain (`protocol.py:137-149`). +- `mcp/server.py` (5003 lines) — the whole tool/resource/prompt surface. `WardlineMCPServer` (`server.py:4525`) registers 18 tools (`_register_tools`, `server.py:4614-4802`), wires `tools/call`, `tools/list`, `resources/*`, `prompts/*` (`_wire`, `server.py:4811`). Each tool's full spec (input_schema/output_schema/annotations/capabilities) is a module-level dict colocated with its handler; ~3000 of the 5003 lines are inline JSON Schema. Dual emission + the split error model live in `_tools_call` (`server.py:4915-4984`). +- `mcp/tooling.py` — shared plumbing: `Tool` frozen dataclass (`tooling.py:46`), `ToolCapability` StrEnum READ/WRITE/NETWORK (`tooling.py:23`), `ToolPolicy.denial` (`tooling.py:38`), `ToolError` (`tooling.py:15`), and the THREAT-001 confinement `resolve_under_root` (`tooling.py:78-83`). +- `mcp/freshness.py` — `server_identity`/`attach_server_identity` source-mtime freshness verdict for the `doctor` tool; flips `ok` false and lands a `server.freshness` check when on-disk source is newer than process start (the 2026-06-06 stale-long-lived-server incident; `freshness.py:48-84`). +- `mcp/resources.py` — 4-entry resource catalog `wardline://vocab|rules|config|config-schema` (`resources.py:16-21`); findings are deliberately never a resource. +- `mcp/prompts.py` — single `wardline:loop` prompt teaching the scan→explain→fix→rescan loop (`prompts.py:9-32`). +- `mcp/lsp.py` — 8-line compat re-export of `wardline.lsp`. +- `lsp.py` — `LspServer` stdlib LSP diagnostics (`lsp.py:26`): Content-Length framing (`lsp.py:48-58`), initialize/didOpen/didSave/didClose handlers (`lsp.py:121-177`), `run_and_publish` runs S4 `run_scan(confine_to_root=True)` and emits `publishDiagnostics` (`lsp.py:179-224`). Own launch-root confinement, separate from the MCP guard (`_is_under_launch_root`/`_is_allowed_root`, `lsp.py:296-302`); 10 MB content cap + drain (`lsp.py:16,87-90`). + +**Tool surface (18, in published order):** `scan`, `scan_job_start`, `scan_job_status`, `scan_job_cancel`, `explain_taint`, `dossier`, `assure`, `decorator_coverage`, `attest`, `verify_attestation`, `file_finding`, `scan_file_findings`, `judge`, `baseline`, `waiver_add`, `fix`, `doctor`, `rekey` (`server.py:4618-4802`; count corroborated by the live `mcp__wardline__*` surface). + +**Public surface / entry points:** +- `WardlineMCPServer(root=, loomweave_url=, filigree_url=, allow_write=, allow_network=)` then `.rpc.run_stdio()` — the stdio MCP server (`server.py:4525`). +- `LspServer(root=root).run()` — the LSP server (`lsp.py:26`). +- `JsonRpcServer.dispatch(message)` — the pure dispatch boundary (`protocol.py:62`), tested without I/O. +- `resolve_under_root(root, arg)` — path-escape guard re-used by every path/config-taking handler (`tooling.py:78`). + +**Dependencies (graph-derived):** +- Inbound (S11 only — S10 is a leaf facade reached solely via CLI launch): `entity_callers_list` on `WardlineMCPServer` returns only tests + `src/wardline/cli/mcp.py:53` (verified: `cli/mcp.py:53` constructs it, `:61` calls `.rpc.run_stdio()`); `LspServer` is launched at `cli/lsp.py:22` (`LspServer(root=root).run()`). +- Outbound (from `_scan` neighborhood + read imports `server.py:18-53`): **S4 Core/Orchestration** — `run_scan`/`gate_decision`/`baseline_migration_hint` (`core/run.py`), config, paths, `scan_jobs`, `sei_resolution`, `delta_scope`, `safe_paths` confinement; **S5 Findings/Outputs** — `agent_summary.build_agent_summary`, `explain`, `Finding`, `filigree_emit`, `federation_status`; **S6 Gate/Remediation** — `baseline`, `waivers`, `finding_query.filter_findings`, `delta_scope`, `scan_file_workflow`; **S7 Trust Evidence/Judge** — `assure.build_posture`, `attest`, `weft_dossier`, `weft_decorator_coverage`, `judge_run`, `legis`; **S8 Identity/SEI** — `sei_resolution`, `loomweave.identity.SeiResolver`; **S9 Federation** — `FiligreeEmitter`, `FiligreeIssueFiler`, `LoomweaveClient`, `loomweave.write`; **S2 Rule Lattice** — `resources.py:37` reads `scanner.rules._ALL_RULE_CLASSES`. + +**Patterns Observed:** +- **Hand-rolled JSON-RPC, no MCP SDK** — stdlib `json`/`sys` only (`protocol.py`), preserving the base zero-dep posture. +- **Identical-by-construction core, agent-shaped surface.** CLI, MCP, and LSP all route through the same S4 `run_scan`+`gate_decision` (`server.py:792-827`; `lsp.py:182`), so findings/gate are identical; MCP layers agent-shaping *on top* — bounded pagination, inline explain, fail-soft enrichment, legis artifact. The project asserts this parity in `tests/unit/core/test_cli_mcp_parity.py`. +- **Split error model** (watch item, confirmed): protocol faults stay JSON-RPC errors — unknown tool/method/bad-args raise `McpError`, which `_tools_call` deliberately re-raises for `dispatch` to map (`server.py:4928-4931`, `:4964-4967`; `protocol.py:104,107-108`); tool-execution errors (`ToolError`/`WardlineError`/unexpected crash) become `{isError:true, content:[text]}` results so MCP clients reliably relay them (`server.py:4943-4977`, `_is_error` at `:5000`). +- **Structured-output dual emission** (B1/B2): every successful call returns text `content` byte-identical to the pre-B1 shape AND `structuredContent` with the same payload; isError results never carry structuredContent (`server.py:4978-4984`). `tools/list` advertises `outputSchema`/`title`/`annotations` alongside the homegrown `capabilities` key, mapped not replaced (`server.py:4822-4839`). +- **Two-layer capability enforcement:** tools advertise a conservative possible-effects superset statically (e.g. `scan` = READ+WRITE+NETWORK, `server.py:1937`), but `_effective_tool_capabilities` recomputes the real per-call set (a local scan with no integration URL is READ-only) and `ToolPolicy.denial` gates it before the handler runs (`server.py:4857-4913,4952-4954`). +- **THREAT-001 path confinement:** `resolve_under_root` rejects escapes on every MCP path/config arg (`tooling.py:78-83`), and handlers pass `confine_to_root=True` into S4 read paths (`server.py:796,2185`; `lsp.py:182`) — S10 is the write-side guard, S4 the read-side. +- **Bounded-by-default payloads (context-overflow hardening):** `scan` returns ≤25 finding bodies by default with `offset`/`full` paging and a `truncation` descriptor (`server.py:93,860-885,1852-1877`); `explain:true` inlining is capped at 10 (`server.py:89`); `summary_only` suppresses the ~56 KB legis artifact (`server.py:2121-2126`). +- **Fail-soft enrichment:** Filigree/Loomweave/legis writes report into their blocks but never fail the scan (`server.py:96-135,806-826,2104-2107`); URLs are redacted in diagnostics (`redact_url_for_diagnostics`, `server.py:699-708`). +- **Lazy `jsonschema` validation preserving zero-dep:** `jsonschema` lives only in the `scanner` extra (`pyproject.toml:13,24` — base `dependencies = []`); `_tools_call` imports it under `try/except ImportError` and degrades to the handlers' own manual checks (`_bool_arg`, `_fail_on_arg`) when absent (`server.py:4933-4943`). + +**Concerns:** +- **`decorator_coverage` is unbounded on the MCP surface** (watch item wardline-550ea44e53, OPEN, P2). Its input_schema is only `{path, config}` — no `where`, no `max_findings`/`offset`, no truncation marker (`_DECORATOR_COVERAGE_TOOL`, `server.py:3092-3115`) — unlike `scan`'s bounding contract (`server.py:1852-1877`). The handler returns the full one-row-per-decorated-entity inventory (`server.py:2886-2903`); the S7 builder it calls has no internal cap either (corroboration, not my analysis). On a large trust surface this is the exact context-overflow class `scan` was hardened against. +- **`scan_file_findings` is the same unbounded class, lower volume:** its `active_defects` array carries every active defect with no page control (schema `server.py:399-560`, handler `server.py:296-332`) — one-line analogue of the above, worth the same `where`/paging treatment. +- **pytest-coupled handshake bypass:** `JsonRpcServer.__init__` sets `_initialized = _initializing = ("pytest" in sys.modules)` (`protocol.py:43-46`), so under pytest the server is born already-initialized and the `"server not initialized"` gate (`protocol.py:99-100`) is silently disabled. A test affordance that couples production code to the test runner's module table; harmless today but would misfire in any embedding that imports pytest. +- **Global stdout redirect:** `run_stdio` mutates `sys.stdout = sys.stderr` to keep stray prints off the JSON-RPC channel, restoring in `finally` (`protocol.py:133-135,165-168`). Correct for a process-owning server, but a global side effect that would interfere with in-process embedding. +- **5003-line `server.py`:** a single module holding all 18 handlers + ~3000 lines of inline JSON Schema. Deliberate colocation (declaration-next-to-handler, `server.py:4614-4617`), not a defect, but a maintainability/navigation cost worth noting. + +**Confidence:** High — Read in full: `protocol.py`, `tooling.py`, `resources.py`, `prompts.py`, `freshness.py`, `mcp/lsp.py`, `lsp.py`; `server.py` read structurally (tool registration, `_tools_call` dispatch + split error model, `_effective_tool_capabilities`, and the `scan`/`explain_taint`/`decorator_coverage`/`scan_file_findings`/legis handlers), with remaining handler bodies and the ~3000 lines of inline schema skimmed via grep. Dependencies are graph-derived (`entity_callers_list`, `entity_neighborhood_get` on `_scan`/`WardlineMCPServer`/`resolve_under_root`) and corroborated by `Read` (`cli/mcp.py:53,61`; `cli/lsp.py:22`; `core/run` imports). The wardline-550ea44e53 status and the empty `weft_decorator_coverage` cap grep were verified live. Tool count cross-checked against the session's `mcp__wardline__*` surface. + +--- + +## S11 — CLI & Install/Activation + +**Location:** `src/wardline/cli/` (the `click` command surface, one module per command), `src/wardline/install/` (agent-enablement: CLAUDE.md/AGENTS.md blocks, `.mcp.json`, Codex MCP, skills, pre-commit, sibling detection, doctor health/repair) + +**Responsibility:** Expose every Wardline capability as a thin `click` command that delegates to the shared core engines, and push the "opt-in activation, not configuration" surface — instruction blocks, MCP wiring, skill packs, pre-commit hooks — into a project so an agent gets a working gate first-run. + +**Key Components:** +- `cli/entrypoint.py` — the dependency-light console-script shim (`main`, `entrypoint.py:10`); imports `cli.main.cli` inside a `try/except ModuleNotFoundError` and, if `click`/`jsonschema`/`yaml` are absent (`_SCANNER_EXTRA_IMPORTS`, `entrypoint.py:7`), exits 2 telling the user to install `wardline[scanner]`. This is the seam that keeps the base package zero-dep while the command itself needs the extra. Console-script `wardline = wardline.cli.entrypoint:main` (`pyproject.toml:55`); graph-confirmed zero in-repo callers (it is the OS entry point). +- `cli/main.py` — the `@click.group` (`main.py:36`) wiring 17 subcommands via `cli.add_command(...)` (`main.py:42-58`), plus two inline commands defined in this file rather than their own module: `vocab` (`main.py:61`, emits the NG-25 descriptor YAML) and the `baseline` group with `create`/`update` (`main.py:105-217`, delegating to S6 `collect_and_write_baseline`). +- `cli/scan.py` — the central command and the widest CLI surface (~675 lines, ~30 options). Delegates analysis to S4 `run_scan` (`scan.py:293`, re-run after `--fix` at `:325`) and the gate verdict to S4 `gate_decision` (`scan.py:450`, `:603`); everything else here is option parsing, multi-format dispatch (jsonl/sarif/agent-summary/legis), fail-soft sibling emission, and human-facing rendering of a decision computed in core. +- `cli/{assure,attest,dossier,judge,explain_taint,decorator_coverage,file_finding,findings,rekey,fix,scan_file_findings}.py` — one thin delegator each; each docstring states it calls "the same core function the MCP tool calls — CLI and MCP identical by construction" (`assure.py:5`, `dossier.py:3`, `explain_taint.py:3`, `attest.py:4`, `judge.py:3`). +- `cli/{mcp,lsp}.py` — launchers for the S10 servers: `mcp` resolves sibling URLs then starts `WardlineMCPServer(...).rpc.run_stdio()` (`mcp.py:53`); `lsp` starts `LspServer(root=root).run()` (`lsp.py:22`). +- `cli/scan_job.py` + `cli/scan_job_worker.py` — file-backed long-scan surface (`start`/`status`/`cancel`) over S4 `core.scan_jobs`; the worker is a `python -m` subprocess entrypoint (`scan_job_worker.py:12`). +- `install/block.py` — renders + idempotently injects the hash-fenced wardline instruction block into shared `CLAUDE.md`/`AGENTS.md` (`inject_block`, `block.py:182`). Implements the weft C-4 multi-owner managed-block contract: own-namespace-only rewrite, never crosses a real foreign block, atomic write with a refuse-to-empty guard (`_atomic_write_text`, `block.py:144-179`). +- `install/mcp_json.py` — writes the project `.mcp.json` `wardline` stdio entry (`merge_mcp_entry`, `mcp_json.py:171`) and the global Codex `~/.codex/config.toml` entry (`install_codex_mcp`, `:282`). Treats `.mcp.json` as repo-controlled input: non-loopback `--filigree-url`/`--loomweave-url` pins are dropped, loopback pins preserved unless Filigree server mode supplies a scoped target (`_desired_sibling_url`, `:111`). +- `install/skill.py` — copies the bundled `wardline-gate` skill into `.claude/skills` and `.agents/skills` (`install_skill`, `skill.py:17`), refusing to overwrite a symlink. +- `install/detect.py` — detection-only sibling discovery (`detect_siblings`, `detect.py:128`); explicitly never persists config (the shared `weft.toml` is operator-owned; live URLs resolve on demand from `.weft//ephemeral.port`). +- `install/pre_commit.py` — appends a `wardline-scan` hook block to `.pre-commit-config.yaml` (`install_pre_commit_hook`, `pre_commit.py:9`). +- `install/pack.py` — `activate_pack` (`pack.py:7`) returns operator *guidance text only*; it never writes `weft.toml` and never imports/executes the pack (packs run code, so activation stays operator-authored + `--trust-pack`-gated in core). +- `install/doctor.py` — the install-health/repair engine (~947 lines): `machine_readable_doctor` (`doctor.py:840`), `repair_install` (`:928`), `check_install` (`:907`), plus per-check helpers (config, mcp-registration, decorator-grammar, gitignore, stray-artifact sweep, stale-sibling-port probe, `.env` filigree-token repair). Shared by both the CLI and the MCP `doctor` tool. +- `cli/install.py` / `cli/doctor.py` — the `install` and `doctor` commands that orchestrate the `install/` package; `cli/doctor.py` re-assembles the human-readable report from `install.doctor` private helpers. + +**Public surface / entry points:** `wardline.cli.entrypoint:main` (the console script, `pyproject.toml:55`); `wardline.cli.main.cli` (the click group). The install package's reusable surface — `install.doctor.machine_readable_doctor` / `repair_install`, `install.block.inject_block`, `install.mcp_json.merge_mcp_entry`, `install.skill.install_skill` — is consumed by the S10 MCP server as well as by these CLI commands. + +**Dependencies (graph-derived):** +- Inbound: Essentially none from inside the repo — S11 sits at the top of the call stack. `entity_callers_list` on `cli.entrypoint.main` returns `[]` with `traversal_complete:true` (OS-invoked console script). The one cross-subsystem inbound edge is **S10 MCP & LSP Server → `install.doctor.machine_readable_doctor`** (`mcp/server.py:3853`, graph-confirmed `_doctor` caller), so the MCP `doctor` tool reuses S11's install-health engine; a pinning test (`tests/unit/mcp/test_server_doctor.test_doctor_matches_cli_machine_readable_envelope`) holds the two envelopes equal. The `install/` helpers' other resolved callers (`cli.install.install`, `install.doctor.repair_install`) are intra-S11. +- Outbound (S11 is outbound to almost everything — it is the orchestration crust): + - **S4 Core Orchestration & Config** (dominant): `core.run.run_scan`/`gate_decision`/`baseline_migration_hint` (`scan.py:31`), `core.config.load`/`resolve_filigree_url`/`resolve_loomweave_url` (`scan.py:13-22`), `core.scan_jobs` (`scan_job.py:12`), `core.scan_file_workflow` (`scan_file_findings.py:14`), `core.safe_paths`/`paths`/`errors`/`descriptor`/`artifacts`, and (in `install/`) `core.config`/`paths`/`safe_paths`/`artifacts`. (All of `scan.scan`'s dominant callees graph-confirmed via `entity_neighborhood_get`: `run_scan`, `gate_decision`, `baseline_migration_hint`, `config.load`, `resolve_filigree_url`/`resolve_loomweave_url`, `write_scan_artifact`, `explicit_output_target`.) + - **S5 Findings, Outputs & Emit**: `core.emit.JsonlSink`, `core.sarif`, `core.agent_summary`, `core.filigree_emit`, `core.federation_status` (`scan.py:20-33`); `core.explain.explain_taint_result` (`explain_taint.py:82`); `core.finding_query` (`findings.py:16`). + - **S6 Gate Discipline & Remediation**: `core.baseline.collect_and_write_baseline` (`main.py:29`) / `inspect_baseline_store` (`doctor.py:20`), `core.autofix.run_autofix` (`fix.py:10`, `scan.py:309`; graph-confirmed callee of `scan.scan`), `core.delta_scope` (the `--affected` delta scan; graph-confirmed callee `parse_affected_scope_text`). + - **S7 Trust Evidence & Judge**: `core.assure` (`assure.py:37`), `core.attest`/`attest_key` (`attest.py`), `weft_dossier` (`dossier.py:57`), `core.judge_run.run_judge`/`core.judge` (`judge.py:18-24`), `weft_decorator_coverage` (`decorator_coverage.py:39`). + - **S8 Identity & SEI**: `core.rekey` (`rekey.py:21`), `core.sei_resolution.resolve_query_filters` (`findings.py:18`). + - **S9 Federation Clients**: `loomweave.client`/`config`/`write`, `filigree.config`, `core.legis`, `core.filigree_issue`, `core.http` (lazy-imported in `scan.py:406-444`, `file_finding.py`, `scan_file_findings.py:14-17`, `doctor.py:22-26`). + - **S10 MCP & LSP Server**: `mcp.server.WardlineMCPServer` (`mcp.py:10`), `lsp.LspServer` (`lsp.py:10`). + - **S1/S2** (narrow): `install.doctor._check_decorator_grammar` imports `scanner.grammar.BUILTIN_BOUNDARY_TYPES` (S1) and `core.registry.REGISTRY` (S2) to verify the decorator grammar is wired (`doctor.py:405-417`). + +**Patterns Observed:** +- **Thin-command / shared-core delegation.** Every command parses options, calls one core function, and renders output; the analysis/decision logic lives in S4–S9. The anti-drift discipline is explicit and load-bearing: `scan.py:601-602` notes the gate uses "the same [decision] the MCP scan tool serialises — so the surfaces cannot drift (A4)," and the `install.doctor.machine_readable_doctor` engine is literally shared between `cli.doctor` and the MCP `doctor` tool (graph-confirmed) with a byte-equal pinning test. +- **Zero-dep base preserved via a guarded shim.** `cli`/`install` freely import `click` (and `pyyaml`/`jsonschema` transitively), which is legitimate because they live behind the `scanner` extra; `entrypoint.py` catches the missing-extra `ModuleNotFoundError` and emits an actionable message instead of a traceback (`entrypoint.py:13-20`). `pyproject.toml:13` confirms `dependencies = []`. +- **Activation, not configuration, in code.** The `install/` package is the product thesis made concrete: it *adds* instruction blocks, MCP entries, skills, and hooks but never edits the operator-owned `weft.toml` (`pack.py` returns guidance text; `detect.py` persists nothing). Power is opt-in via what gets installed, not via config the operator must hand-author. +- **Hardened file mutation on untrusted trees.** Install writes go through `safe_project_file`/`safe_project_path` confinement; `block.py` writes atomically with a refuse-to-empty guard and a foreign-namespace-safe managed-block contract; `doctor._rewrite_env_token` refuses a symlinked `.env` and opens with `O_NOFOLLOW` (`doctor.py:486-505`); `doctor._is_loopback` uses strict IP parsing so a bearer token is never probed against `127.attacker.com` (`doctor.py:615-628`). This is a clear strength, not a gap. +- **Fail-soft federation, loud local gate.** Sibling emission (Filigree POST, Loomweave write) is best-effort and never changes the scan exit code (`scan.py:424-446`), while a hard `WardlineError` maps to `SystemExit(2)` and a tripped gate to `SystemExit(1)` — consistently across commands. + +**Concerns:** +- **CLI↔MCP config-resolution asymmetry (latent divergence).** `cli/mcp.py:46-50` passes `None` as the reserved `config_path` to `resolve_*_url_with_source`, whereas `cli/scan.py` threads `weft_config_path(root)`. Documented as harmless until the hub sibling-endpoint key lands (`weft-a2f4cf95c7`), but it is a real surface asymmetry that will silently diverge once that key is read; it should be threaded for parity when the hook arrives. +- **`cli/doctor.py` reaches into `install.doctor` private API.** It imports seven underscore-prefixed helpers (`_check_config`, `_check_filigree_auth`, `_check_gitignore`, `_check_stale_sibling_ports`, `_resolve_probe_target`, `_sweep_stray_artifacts`; `cli/doctor.py:12-22`) to build the human report, while the MCP path goes through the single public `machine_readable_doctor`. The human and machine renderings can drift because they assemble the checks independently; promoting a public check-list surface would close that. +- **`install/doctor.py` is a 947-line multi-concern module / coupling hotspot.** It mixes install-wiring health, federation auth probing, artifact-hygiene sweeping, and stale-port probing, and reaches into other modules' private API (`core.artifacts._managed_artifact_pattern`/`_is_regular_file_no_follow`, `paths._has_project_markers`, `core.config._filigree_published_url`; `doctor.py:138,163,558`). High internal cohesion around "doctor," but the private cross-module reach is brittle to refactors in S4/S5. +- **`install/pre_commit.py` is the weakest-engineered install writer.** It detects existing hooks by substring (`"id: wardline-scan" in content`, `pre_commit.py:19`), edits YAML by string concatenation rather than a parser (contrast the careful `block.py`/`mcp_json.py`), and swallows `except Exception` into status strings (`:16`, `:42`). A `.pre-commit-config.yaml` whose structure differs from the assumed shape can get a malformed hook appended. +- **Minor duplication / pattern inconsistency.** The `--fix` confirm callback is duplicated between `scan.py:315-321` and `fix.py:57-63`; and `vocab`/`baseline` are defined inline in `main.py` rather than as their own `cli/.py` modules, breaking the otherwise-uniform one-module-per-command convention. (`cli.install.install:82` `importlib.import_module(pack)` is a benign presence check only — it does not execute the pack.) + +**Confidence:** High — Read all 30 assigned files in full (the larger `scan.py` and `install/doctor.py` end-to-end). Dependency edges corroborated against the Loomweave graph: `entity_callers_list` confirmed `cli.entrypoint.main` has zero in-repo callers, `inject_block`/`merge_mcp_entry`/`install_skill` are called only by `cli.install.install` + `install.doctor.repair_install`, and `machine_readable_doctor` is called by both `cli.doctor.doctor` (S11) and `mcp.server._doctor` (S10) with a pinning test. Zero-dep base + `scanner` extra + console-script verified in `pyproject.toml:13,24,55`. The dominant outbound edges are graph-derived, not just read: `entity_neighborhood_get` on `cli.scan.scan` returned its full resolved callee set (`run_scan`, `gate_decision`, `baseline_migration_hint`, `config.load`/`resolve_*_url`, `write_scan_artifact`, `run_autofix`, `parse_affected_scope_text`, `build_agent_summary`, `FiligreeEmitter.emit`, `build_sarif`, `JsonlSink.write`, `build_legis_artifact`, the loomweave write trio) with `traversal_complete:true`, and confirmed its only in-repo reference-in is the `cli.main` `add_command` wiring. The remaining commands' S-label mapping is from read import+call sites cross-checked against the spec's subsystem table (not each individually graph-probed), but each was read in full. + +--- + +## S12 — Rust Frontend + +**Location:** `src/wardline/rust/` (15 modules: `__init__.py`, `analyzer.py`, `context.py`, `crate_roots.py`, `dataflow.py`, `edges.py`, `index.py`, `mounts.py`, `nodeid.py`, `parse.py`, `provider.py`, `qualname.py`, `rules.py`, `_tree_sitter.py`, `vocabulary.py` + bundled `rust_taint.yaml`) — entirely behind the opt-in `wardline[rust]` extra. + +**Responsibility:** A preview, opt-in tree-sitter Rust frontend that implements the engine `Analyzer` protocol and contributes a narrow Tier-A command-injection taint slice (rules RS-WL-108/112) over `std::process::Command`, emitting crate-prefixed, baseline-eligible `Finding`s into the shared pipeline via `wardline scan --lang rust`. + +**Key Components:** +- `analyzer.py` — `RustAnalyzer` (`analyzer.py:59`): the engine `Analyzer` impl. `analyze(files, config, *, root)` (`analyzer.py:84`) discovers Cargo crate roots once, builds `#[path]` mount overlays, routes each `.rs` file to its crate-prefixed module, and runs the per-file pipeline; `_analyze_tree` (`analyzer.py:157`) parses once, mints one `NodeIdMap`, and threads it through index → provider (trust-seed) → dataflow → rules (spec §5: a re-parse would mint divergent NodeIds and fail quietly). +- `_tree_sitter.py` — `require_rust()` (`_tree_sitter.py:23`): the sole lazy import of `tree_sitter`/`tree_sitter_rust`; raises `RustToolingError` with an install hint if the extra is absent (mirrors `loomweave.require_blake3`). +- `parse.py` — `parse_rust`/`has_errors` (`parse.py:21`/`parse.py:28`): thin parse seam; `has_errors` detects tree-sitter error-recovery so a half-parsed file is never reported clean. +- `nodeid.py` — `NodeIdMap` + `mint_node_ids` (`nodeid.py:31`/`nodeid.py:72`): the single pre-order NodeId keying authority; the map **pins its source `Tree`** so a cross-tree lookup is a loud `KeyError`, not a silent false hit (`nodeid.py:31-44`). +- `index.py` — `index_entities` (`index.py:115`): the full ten-kind ADR-049 entity surface (module/struct/fn/enum/trait/type_alias/const/static/macro/impl + semantic `method`), including the impl residual-collision ladder (stage @cfg → S → T → method-@cfg). +- `qualname.py` — the ADR-049 qualname dialect (`qualname.py:1-41`): Wardline is the *second producer*, minting byte-identical loomweave locators (pinned by the vendored corpus `tests/conformance/qualnames_rust.json`). +- `crate_roots.py` — `discover_crate_roots` (`crate_roots.py:81`): `Cargo.toml [package].name` crate discovery via stdlib `tomllib`, symlink-safe, longest-prefix file→crate mapping; mirrors the loomweave oracle. +- `mounts.py` — `build_mount_overlay`/`MountOverlay.logical_module_path` (`mounts.py:152`/`mounts.py:110`): ADR-049 Amendment 8 `#[path]` mount routing with a filesystem fallback. +- `vocabulary.py` — `load_rust_taint` (`vocabulary.py:145`): the frozen, validated sources/sinks table keyed by `(crate, path)`; `RUST_TAINT_VERSION = 3` (`vocabulary.py:31`) folds into the provider fingerprint. Slice-1 sink set is just `{"command"}` (`vocabulary.py:29`). +- `provider.py` — `RustTrustProvider` (`provider.py:47`): the Rust analog of the Python trust decorators — `/// @trusted(level=ASSURED|GUARDED)` outer-doc-comment markers, anchored with `re.match` (`provider.py:39`) so only a leading directive seeds trust; an unmarked fn yields `None` → fail-closed default. +- `dataflow.py` — `analyze_command_dataflow` (`dataflow.py:72`): the genuinely-new builder-dataflow that reconstructs each `Command` invocation (stepwise and fluent chains) into a `CommandTrigger` (`dataflow.py:50`) per terminal `.output()`/`.spawn()`/`.status()`. +- `rules.py` — `RustProgramInjectionRule` RS-WL-108 (`rules.py:34`, base ERROR) + `RustShellInjectionRule` RS-WL-112 (`rules.py:52`, base WARN): the verdict layer; both modulate by the containing fn's declared trust tier and de-conflict (a tainted program is 108's; 112 stays silent — `rules.py:63`). +- `context.py` — `RustAnalysisContext`/`RustTriggerContext`/`RustRule` (`context.py:45`/`context.py:27`/`context.py:58`): a *separate* rule protocol that never plugs into the Python `RuleRegistry`. +- `edges.py` — `discover_rust_edges`/`index_rust_file` (`edges.py:107`/`edges.py:98`): anchored `imports`/`implements` edge discovery (resolved-or-dropped). NOT in the scan/runtime surface — identity-corpus only (`edges.py:38-44`). + +**Public surface / entry points:** +- `RustAnalyzer` (`analyzer.py:59`) is constructed by `RustFrontend.build_analyzer` (`core/frontends.py:99-107`) and registered as `FRONTENDS["rust"]` (`core/frontends.py:127`); both `config` and `summary_cache` are ignored (`RustAnalyzer()` is zero-config). +- `RustAnalyzer.analyze(files, config, *, root) -> Sequence[Finding]` (`analyzer.py:84`) is the engine protocol method `run_scan` drives. +- `RustAnalyzer.analyze_source` (`analyzer.py:150`) — single in-memory string entry for rule tests; `discover_rust_entities`/`index_rust_file`/`discover_rust_edges` are the identity-corpus capture entries. + +**Dependencies (graph-derived):** +- Inbound (who calls in): + - **S4 Core Orchestration & Config** — `RustFrontend.build_analyzer` → `RustAnalyzer()` (`core/frontends.py:107`; Loomweave `entity_callers_list` confirms this dynamic call site), and `run_scan` → `analyzer.analyze(...)` (`core/run.py:381`; frontend selected by `FRONTENDS[lang]` at `core/run.py:285-332`). Loomweave callers are otherwise all under `tests/`. + - **S11 CLI & Install/Activation** — reaches the frontend transitively through `run_scan` via `wardline scan --lang rust` (per `pyproject.toml:32`); no direct edge into `rust/`. +- Outbound (what this calls, by S-label) — all confirmed via Loomweave `entity_neighborhood_get` `imports_out`/`references_out` on the `analyzer`/`rules`/`dataflow`/`provider` modules: + - **S5 Findings, Outputs & Emit** — `Finding`/`Kind`/`Location`/`Severity`/`ENGINE_PATH` from `wardline.core.finding` (`analyzer.py:26`, `rules.py:16`; graph: `analyzer`/`rules` → `core.finding`). + - **S8 Identity & SEI** — `NodeId` from `wardline.core.node_id` (`nodeid.py:23`, `dataflow.py:23`; graph: `dataflow` → `core.node_id`) and `compute_finding_fingerprint` from `wardline.core.finding` (`rules.py:16`, `rules.py:158`). + - **S3 Taint Engine** — `FunctionTaint` from `wardline.scanner.taint.provider` (`provider.py:22`; graph: `provider` → `scanner.taint.provider`); the taint lattice `TaintState`/`RAW_ZONE`/`least_trusted` from `wardline.core.taints` (`analyzer.py:27`, `dataflow.py:24`, `rules.py:17`; graph: all four modules → `core.taints`) — the shared taint-state vocabulary (physically in `core/`, conceptually the taint engine's lattice). + - **S2 Rule Lattice** — `modulate` from `wardline.scanner.rules.severity_model` (`rules.py:18`; graph: `rules` → `scanner.rules.severity_model`). + - **S1 Scanner Engine** — `AnalysisContext` from `wardline.scanner.context` (`analyzer.py:46`, `TYPE_CHECKING`-only; graph: `analyzer` `imports_out`/`references_out` → `scanner.context.AnalysisContext` via the `last_context` annotation). A type-only edge — the property always returns `None` (`analyzer.py:67-76`). + - **S4 Core Orchestration & Config** — `WardlineConfig` (`analyzer.py:45`; graph: `analyzer` → `core.config`), `RustToolingError` from `wardline.core.errors` (`_tree_sitter.py:15`), `require_yaml` from `wardline.core.optional_deps` (`vocabulary.py:19`), and the `LanguageFrontend`/`FRONTENDS` registry it plugs into (`core/frontends.py`). + +**Patterns Observed:** +- **Lazy-extra isolation (zero-base-dep preservation):** every module keeps `tree_sitter` under `TYPE_CHECKING`; only `require_rust()` imports it at call time (`_tree_sitter.py:23`, `parse.py:5-7`, `nodeid.py:15-17`). `core/frontends.py` imports `rust.analyzer` only inside `build_analyzer` (`core/frontends.py:105`). Base and `scanner` extras never pull `rust/`. +- **Single-parse / single keying authority:** one parse → one `NodeIdMap` threaded through index/provider/dataflow/rules (`analyzer.py:157-216`); `NodeIdMap` pins its tree to make node-id reuse hazards loud (`nodeid.py:31-44`). +- **Fail-closed identity, anti-false-green metric:** an unmarked fn fails closed to `UNKNOWN_RAW` and suppresses findings (`analyzer.py:50`, `analyzer.py:183`), while dataflow is default-clean (taint flows only from proven vocabulary sources/tainted locals — `dataflow.py:201-221`). The `WLN-RUST-COVERAGE` metric (`analyzer.py:357`) exposes declared/total so a vacuously-green un-annotated scan is distinguishable from analyzed-and-safe. Parse/analysis failure surfaces gate-eligible `WLN-ENGINE-PARSE-ERROR`/`WLN-ENGINE-FILE-FAILED` defects, never a silent skip (`analyzer.py:121-133`). +- **Move-stable fingerprints (wlfp2):** trigger positions fold into the fingerprint entity-relative (line delta + NodeId delta against the containing fn) so an edit above the entity does not rekey (`rules.py:123-145`, `context.py:27-43`); finding identity routes through the shared `compute_finding_fingerprint`, keeping Rust findings baseline-eligible like Python's. +- **Conformance second-producer:** qualname/crate-root/mount routing mints byte-identical loomweave (ADR-049) locators; out-of-scope files get `#out`-branded routes that carry no cross-tool conformance claim and structurally cannot collide with a loomweave locator (`analyzer.py:269-328`, `crate_roots.py:22-34`). +- **Crate-consistent vocabulary matching:** sources/sinks keyed on the crate-qualified full path with a two-segment-floor suffix match, so a foreign crate's like-named `Command::new` cannot match the std entry (`dataflow.py:293-306`). + +**Concerns:** +- **Known FN gap — mid-chain `?`/`.await`:** `_unwrap_to_call`/`_unwind` only peel try/await/return wrappers at the outer statement position; an interior `Command::new(p).arg(x)?.output()` is dropped, so no RS-WL-108 fires (`dataflow.py:254-262`, `dataflow.py:265-281`). Tracked open as `wardline-535c9531cc`. A false-negative, not a wrong gate verdict. +- **Known marker-grammar gaps (all fail-closed):** the provider walk guard is only `("line_comment", "attribute_item")` (`provider.py:68`), so block doc comments `/** @trusted */`, a `/** */` interposed before the fn, and inner `//!` markers never seed trust (`provider.py:64-83`). All suppress (never a false positive). Tracked open as `wardline-9c3a76b257`. NOTE: sub-item #4 of that issue ("typo'd marker silently suppressed, no diagnostic") appears since-addressed — `analyzer.py` now emits a gate-eligible `WLN-ENGINE-RUST-INVALID-TRUST-MARKER` ERROR on a rejected level (`analyzer.py:175-181`, `analyzer.py:342-354`); the issue text is stale on that one sub-item. +- **`config` accepted but unused:** `weft.toml` severity overrides do not reach the Rust rules — base severities are hardcoded (`analyzer.py:88-91`; `RustFrontend.build_analyzer` ignores both params, `core/frontends.py:99-107`). A documented preview surface gap. +- **`last_context` is always `None`** (`analyzer.py:67-76`): the Rust-native context is not an engine `AnalysisContext`, so delta-scope and SARIF code-flow consumers degrade to file-level scoping for Rust. Deliberate slice-1 limitation, but it means delta/SARIF richness is unavailable on this frontend. +- **`edges.py` is unwired:** `imports`/`implements` discovery is built and tested via the identity corpus only — not emitted by the analyzer/scan surface or over the Weft wire (`edges.py:38-44`). It carries maintenance weight ahead of any runtime consumer. +- **ABI/loader fragility:** `tree-sitter-rust==0.24.2` is ABI 15, loadable only by `tree-sitter` core `>=0.25`; the grammar self-declares a stale `tree-sitter~=0.22` that must NOT be trusted, and the `<0.26` upper cap is conservative/unverified (`pyproject.toml:27-35`). A core/grammar version skew breaks `Language(...)` loading; the pin is narrow by necessity. +- **Single-block dataflow:** intra-function, single-block only; nested control flow and sanitizers are invisible (an accepted bounded FP) (`dataflow.py:9-16`). Scope-appropriate for the preview, but a soundness ceiling worth stating. +- Checked and clean: no third-party import on the base/scanner path (all tree-sitter imports lazy/`TYPE_CHECKING`); `from __future__ import annotations` universal; per-file isolation present (`analyzer.py:127-133`); parse-failure and invalid-marker error handling present. + +**Confidence:** High — read all 15 `rust/` modules (full reads of analyzer/dataflow/rules/provider/index/edges/mounts/crate_roots/vocabulary/nodeid/parse/context/_tree_sitter/`__init__`; `qualname.py` header + `__all__`), plus `core/frontends.py`, the `run_scan` driver site (`core/run.py:381`, selection `:285-332`), and the `pyproject.toml` rust-extra/ABI block. Dependencies are graph-derived: inbound via `entity_callers_list` (`frontends.py:107` + `run.py:381` call sites) and outbound via `entity_neighborhood_get` `imports_out`/`references_out` on the analyzer/rules/dataflow/provider modules (which surfaced the S1 `AnalysisContext` type-edge that imports alone underweighted), each cross-checked against a real import at `file:line`. Both FN-gap issues verified open in Filigree. Residual inference: the S1/S2/S3/S8 label mapping for `scanner.context`/`severity_model`/`core.taints`/`core.node_id` (symbols physically in `core/` or `scanner/` but conceptually owned by those subsystems) is my judgment and is flagged inline. + +--- diff --git a/docs/arch-analysis-2026-06-28-0749/03-diagrams.md b/docs/arch-analysis-2026-06-28-0749/03-diagrams.md new file mode 100644 index 00000000..151e71db --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/03-diagrams.md @@ -0,0 +1,200 @@ +# 03 — Architecture Diagrams + +**Target:** `wardline` @ `e4668abc` · **Date:** 2026-06-28. Edges below are taken from the +Loomweave-graph-derived dependency sections of `02-subsystem-catalog.md` (not import inference). + +--- + +## C1 — System Context + +Who uses Wardline and what it talks to. Wardline is **local-first**: every external link is opt-in and +fail-soft — a sibling outage degrades a section, never breaks a scan. + +```mermaid +graph TB + agent["Coding agent / developer
(humans on the loop)"] + subgraph wl["Wardline (local CLI + MCP + LSP)"] + core["Taint engine + rules + gate"] + end + loom["Loomweave
(taint-fact store + SEI identity)
HMAC over urllib"] + fil["Filigree
(issue tracker / finding lifecycle)
Bearer over loopback"] + legis["legis
(governance)
signed artifact, agent-posted"] + orouter["OpenRouter
(LLM triage judge)
opt-in, network-fenced"] + code["Target source tree
(.py / .rs — untrusted input)"] + + agent -->|"wardline scan / MCP tools"| wl + wl -->|reads statically, never executes| code + wl -.->|opt-in: persist taint facts / resolve SEI| loom + wl -.->|opt-in: emit findings / dossier work| fil + wl -.->|opt-in: signed scan artifact| legis + wl -.->|opt-in: judge findings| orouter +``` + +--- + +## C2 — Containers / Packaging + +The **zero-dependency base** is a hard product invariant; capability ships behind small extras. One +package, several runnable surfaces, all routing through one shared core. + +```mermaid +graph TB + subgraph pkg["wardline (PyPI package — base dependencies = [] )"] + console["console script
wardline.cli.entrypoint:main
(dep-light shim)"] + subgraph base["base (stdlib only)"] + coremodel["core finding model, taint lattice,
config, paths, safe_paths, identity"] + end + subgraph scannerx["[scanner] extra — pyyaml/jsonschema/click"] + cli["CLI (S11)"] + mcp["MCP + LSP server (S10)"] + engine["Scanner engine + rules + taint (S1/S2/S3)"] + orch["Core orchestration + gate + evidence (S4/S5/S6/S7/S8)"] + install["install / activation (S11)"] + end + subgraph loomx["[loomweave] extra — blake3"] + fed["Federation clients (S9)"] + end + subgraph rustx["[rust] extra — tree-sitter + tree-sitter-rust"] + rust["Rust frontend (S12)"] + end + subgraph docsx["[docs] extra — mkdocs"] + docs["docs site build"] + end + end + console --> cli + cli --> orch + mcp --> orch + orch --> engine + orch -.lazy.-> rust + orch -.lazy.-> fed +``` + +--- + +## C3 — Components (the 12 subsystems) & their dependency edges + +Coupling is largely one-directional **surfaces → orchestration → engine**, with federation/identity as +leaves and a few back-edges (noted). `run_scan`/`gate_decision` (S4) is the keystone both surfaces share. + +```mermaid +graph TD + classDef surface fill:#e8f0ff,stroke:#4a73c0; + classDef orch fill:#fff3e0,stroke:#c08a3a; + classDef engine fill:#e9f7e9,stroke:#4aa04a; + classDef leaf fill:#f3e8ff,stroke:#8a4ac0; + + S11["S11 · CLI & Install"]:::surface + S10["S10 · MCP & LSP Server"]:::surface + + S4["S4 · Core Orchestration & Config
(run_scan / gate_decision — keystone)"]:::orch + S5["S5 · Findings, Outputs & Emit"]:::orch + S6["S6 · Gate Discipline & Remediation"]:::orch + S7["S7 · Trust Evidence & Judge"]:::orch + S8["S8 · Identity & SEI"]:::orch + + S1["S1 · Scanner Engine"]:::engine + S2["S2 · Rule Lattice (+decorators)"]:::engine + S3["S3 · Taint Engine"]:::engine + S12["S12 · Rust Frontend"]:::engine + + S9["S9 · Federation Clients"]:::leaf + + S11 --> S4 & S5 & S6 & S7 & S8 & S9 & S10 + S10 --> S4 & S5 & S6 & S7 & S8 & S9 + S4 --> S1 & S3 & S5 & S6 & S8 + S4 -.lazy.-> S12 + S4 -.injected.-> S8 + S6 -.->|lazy import: breaks S6↔S4 cycle| S4 + S1 --> S2 & S3 + S2 -.->|imports _private decorator helpers| S3 + S3 --> S4 + S12 --> S5 & S8 & S3 & S2 + S7 --> S4 & S5 & S6 & S8 & S9 + S8 --> S6 & S9 + S5 --> S1 & S9 + S9 --> S4 & S5 & S7 & S8 +``` + +**Back-edges / cycles worth seeing (from the catalog):** +- **S6 ↔ S4** — `core/run.py` imports the S6 baseline/suppression loaders; `baseline.collect_and_write_baseline` + lazy-imports `run.run_scan` at call time (`baseline.py:232`) to break the cycle. +- **S2 → S3 (private)** — rules import `decorator_provider._is_builtin_decorator_fqn` / `_shadowed_builtin_roots`. +- **S3 → S4 (`core.taints`/`core.ruleset`)** — the taint *lattice* and `ruleset_hash` physically live in + `core/` but are the engine's vocabulary; `ruleset_hash` was deliberately rehomed *below* both engine and + attest to remove the old engine→attest inversion (closed `wardline-9ec283d168`). +- **S5 → S1** — `explain.py` reads `AnalysisContext` provenance maps directly (no narrow interface). + +--- + +## Intended layering vs reality + +The closed ticket `wardline-9ec283d168` defines the intended layering **engine ⇦ policy ⇦ surface ⇦ +federation**. The single import-linter contract that encodes one slice of it (`scanner ⇏ core.attest`) +**passes** (1 kept/0 broken). But the broader goal is only *partially* realized — the catalog and the +ticket's own close-note show real `core/` cycles still broken by **~102 function-local imports** (down +from 158). + +```mermaid +graph TB + subgraph T5["Surfaces (S10, S11)"] + direction LR + A1["CLI"]; A2["MCP / LSP"] + end + subgraph T4["Federation clients (S9)"] + B1["Loomweave / Filigree / legis"] + end + subgraph T3["Evidence & outputs (S5, S7)"] + C1["emit / sarif / explain"]; C2["attest / assure / dossier / judge"] + end + subgraph T2["Policy / orchestration (S4, S6, S8)"] + D1["run_scan / gate"]; D2["baseline / waivers / suppression / delta"]; D3["identity / SEI"] + end + subgraph T1["Engine floor (S1, S2, S3, S12)"] + E1["scanner engine"]; E2["rules + decorators"]; E3["taint"]; E4["rust"] + end + subgraph T0["Shared stdlib kernel"] + F1["taints · finding-model · config · paths · safe_paths · ruleset · registry · errors"] + end + T5 --> T2 + T5 --> T3 + T3 --> T2 + T4 --> T2 + T2 --> T1 + T1 --> T0 + T3 --> T0 + T4 --> T0 + note["⚠ Real residual cycles through T0/T2 (e.g. run → … → attest → assure → run)
still broken by ~102 deferred imports — the broad layering goal is partially done"] +``` + +--- + +## Scan pipeline (sequence) + +The behaviour both `wardline scan` (S11) and the MCP `scan` tool (S10) share — **identical by +construction** because both call `run_scan`/`gate_decision`. + +```mermaid +sequenceDiagram + participant Surf as Surface (CLI / MCP / LSP) + participant Run as S4 run_scan + participant Disc as S4 discover (confined) + participant Eng as S1 analyzer + participant Taint as S3 taint (L1→L3→L2) + participant Rules as S2 rule lattice + participant Supp as S6 suppression + participant Gate as S4 gate_decision + participant Emit as S5 emit / S9 federation + + Surf->>Run: run_scan(root, config, …) + Run->>Disc: discover source_roots (read-confined, THREAT-001) + Run->>Eng: Analyzer.analyze(files) + Eng->>Taint: resolve_project_taints + build_call_taint_map + Taint-->>Eng: ResolverResult (fixed point) + L2 var taints + Eng->>Rules: RuleRegistry.run(AnalysisContext) + Rules-->>Eng: Finding[] (defects + facts) + Eng-->>Run: Finding[] + Run->>Supp: apply_suppressions(baseline/waivers/judged) → un-suppressed gate population + Run->>Gate: gate_decision(gate_findings, fail_on) + Gate-->>Surf: GateDecision (tripped⇒never PASSED) + Surf-)Emit: JSONL / SARIF / Filigree / legis (fail-soft) +``` diff --git a/docs/arch-analysis-2026-06-28-0749/04-final-report.md b/docs/arch-analysis-2026-06-28-0749/04-final-report.md new file mode 100644 index 00000000..ada05baf --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/04-final-report.md @@ -0,0 +1,139 @@ +# 04 — Architecture Analysis: Final Report + +**Target:** `wardline` — generic semantic-tainting static analyzer for Python (+ preview Rust frontend) +**Commit:** `e4668abc` (branch `release/consolidation-2026-06-26`) · **Version:** 1.0.7 (dev) / 1.0.6 shipped +**Date:** 2026-06-28 · **Deliverable:** Option C (Architect-Ready) +**Method:** Loomweave-indexed holistic scan → 12 parallel `codebase-explorer` agents (graph-derived +dependencies, `file:line` evidence) → `analysis-validator` gate (PASS-WITH-NOTES) → orchestrator synthesis. +Headline findings re-verified directly against source + `lint-imports` + Filigree. + +--- + +## 1. Executive summary + +Wardline is a **mature, deliberately-scoped, production-stable** static analyzer (~42.5K LOC Python, 182 +source files, 367 test files — a ~2:1 test:source file ratio). Its thesis is sharp and consistently +executed: **silent until opt-in.** Undecorated code is "unknown-trust" and produces no findings; a +developer declares trust with three decorators and only then is enforcement active. This is what lets it +scan large codebases (including its own) with zero noise, and it is visible in the code, not just the docs. + +Two engineering invariants are upheld with unusual discipline across all 12 subsystems: + +1. **Zero-dependency base.** `dependencies = []`; every capability (scanner, Rust, Loomweave, judge, docs) + lives behind a small extra reached through lazy `require_*` gates. The four validators independently + confirmed no base module imports a third-party package — even crypto and HTTP are stdlib + (`hmac`/`hashlib`/`urllib`). +2. **Identical-by-construction surfaces.** CLI, MCP, and LSP all route through one shared + `run_scan`/`gate_decision` keystone (S4), so findings and gate verdicts cannot drift between surfaces — + asserted by a parity test and enforced by a `GateDecision` invariant that makes a "tripped-but-PASSED" + verdict unrepresentable. + +The codebase is **security-conscious by default**: secure-by-default gating (the gate evaluates the +*un-suppressed* population unless `--trust-suppressions`), layered path-confinement against an untrusted +scan tree (read-side `discover`, write-side `safe_paths` O_NOFOLLOW, config-value confinement, MCP +`resolve_under_root`), fail-closed engine diagnostics (an unparseable file becomes a gate-eligible DEFECT, +never a silent skip), and HMAC-authenticated caches/wires. + +**Overall architecture grade: B+ / strong.** The design is coherent, the boundaries are real, and the +test investment is serious. The debt is concentrated and well-understood: a handful of **god-functions** +(`run_scan`, `_analyze_inner`, `variable_level.py`), **porous private-name coupling at subsystem seams**, +and a set of **security invariants enforced across a caller/callee seam rather than locally** — correct +today, fragile to refactor. Most strikingly, the analysis surfaced **tracker/documentation drift**: the +single most-cited "known violation" is already fixed. + +--- + +## 2. What the system does (architecture in one pass) + +A scan flows **discover → analyze → suppress → gate → emit** (see `03-diagrams.md` sequence): + +- **Engine floor (S1/S2/S3).** `WardlineAnalyzer` parses with stdlib `ast`, indexes entities, and drives a + staged taint computation: L1 trust-decorator seeding → **L3 SCC fixed point** over the inter-module call + graph → L2 flow-sensitive per-variable walk *consuming* the refined L3 output. All aggregation uses a + weakest-link meet (never a join), every unknown defaults to `UNKNOWN_RAW` (fail-closed, never launders + untrusted→trusted). The result is a frozen, deep-immutable `AnalysisContext`. The **rule lattice** (26 + PY-WL rules, 15 stable + 11 preview) is a duck-typed, centrally-registered set where each rule is a + `check(context) -> list[Finding]`; the dangerous-sink family shares a `TaintedSinkRule` template. +- **Policy / orchestration (S4/S6/S8).** `run_scan` composes discovery, optional `--affected` delta + scoping, suppression (baseline/waivers/judged), gate-population materialisation, and scope reporting; + `gate_decision` turns it into a pass/fail verdict. S6 owns the secure-by-default gate predicates, + git-committable baseline, expiry-aware waivers, the advisory (never-narrows-the-gate) delta scan, and a + single-rule autofix. S8 is the **baseline-stability backbone**: the interpreter-stable finding + fingerprint, qualname production, SEI resolution, and the crash-safe `rekey` migration. +- **Evidence & outputs (S5/S7).** S5 is the `Finding` model + every projection (JSONL, SARIF 2.1.0, native + Filigree emit, agent-summary, taint-chain explain). S7 turns a scan into *evidence*: a signed + reproducible `attest` bundle, the `assure` coverage posture, a cross-tool `dossier`, and the opt-in, + network-fenced LLM triage `judge` whose FALSE_POSITIVE verdicts become auditable committed suppressions. +- **Federation (S9).** Stdlib-urllib-only, byte-exact replicas of each sibling's verifier (Loomweave HMAC, + legis canonical-signing, Filigree bearer), all fail-soft. +- **Surfaces (S10/S11).** A dependency-free hand-rolled JSON-RPC MCP server (18 tools, the "primary agent + surface") + an LSP diagnostics server; and a thin `click` CLI (one module per command) plus the + `install` package that pushes activation (instruction blocks, `.mcp.json`, skills, pre-commit) into a + project so an agent gets a working gate first-run. +- **Rust frontend (S12).** A preview tree-sitter frontend implementing the same `Analyzer` protocol, + contributing a narrow command-injection slice (RS-WL-108/112) with crate-prefixed, baseline-eligible + identity. + +--- + +## 3. Architectural strengths (evidence-backed) + +1. **The opt-in thesis is enforced in the type system and severity model, not just docs.** `modulate` + sends the developer-freedom zone (`UNKNOWN_RAW`) to `NONE`; declaration-gated rules emit only when a + decorator is present. (`severity_model.py:47`, `_sink_helpers.py:849`) +2. **Surface parity is a structural guarantee.** One keystone, two-plus surfaces, a parity test, and a + `GateDecision.__post_init__` that hard-rejects an inconsistent verdict — the dogfood-#2 regression can't + recur. (`run.py:181`, `test_cli_mcp_parity.py`) +3. **Fail-closed is the house style.** Unparseable file → gate-eligible DEFECT; non-convergent fixed point + → demote-to-`UNKNOWN_RAW`; empty `--affected` scope → full-tree fallback; malformed store → loud + `ConfigError`. The analyzer prefers a loud over-approximation to a silent gap everywhere. +4. **Defense-in-depth against an untrusted corpus.** Three independent path-confinement layers + O_NOFOLLOW + writes + HMAC-authenticated summary cache (a repo JSON cannot become analyzer truth) + strict + loopback-IP parsing before a bearer token is sent. +5. **Determinism as a contract.** Position-free canonical-AST fingerprint, byte-stable across CPython + 3.12/3.13, pinned by a golden identity oracle — the thing baselines/waivers/SEI all rest on. +6. **Honest degradation over false-green.** `coverage_pct=None` on an empty surface, three-valued trust + verdicts, per-finding failure reasons (PDR-0023), `mark_unseen` suppressed when analysis is incomplete — + the product refuses to read "nothing found" as "all clear." + +--- + +## 4. Key risks & debt (the cross-cutting five) + +Detailed, file-cited in `05-quality-assessment.md`; summarized here by blast radius. + +1. **God-functions/modules** — `run_scan` (~374 lines), `_analyze_inner` (~857 lines), + `variable_level.py` (~2,481 LOC), `server.py` (5,003 lines), `install/doctor.py` (~947 lines). High + change-risk, defended by conformance suites rather than decomposition. +2. **Security invariants split across a seam** — secure-by-default gating, THREAT-001 confinement, and + fingerprint determinism are each enforced by a *cooperating pair* of subsystems, not locally. Correct, + but a refactor on one side silently defeats the property; guarded only by tests. +3. **Porous private-name coupling** — rules import engine `_private` helpers; `explain` reads engine + internals; S6 reaches `sei_resolver._client`; `cli/doctor` reaches seven `install.doctor` privates. + Import-linter cannot see these. +4. **Tracker/documentation drift** — the layering "violation" is fixed and its issue closed, yet + `pyproject.toml` still says "BROKEN" and CI runs the now-passing contract non-gating (`|| true`). + Several other open tickets are largely-already-done. The map has drifted from the territory. +5. **`pytest`-coupled production code** — `_initialized = "pytest" in sys.modules` disables the MCP + not-initialized handshake gate whenever pytest is importable. (`protocol.py:43`) + +--- + +## 5. Scope & non-goals (correctly bounded) + +Wardline is deliberately **L1–L2 with an L3 project fixed point**, not a path-sensitive whole-program +prover, and **Python-first** with a command-injection-only Rust *preview*. It is not a broad SAST suite +(26 rules, 11 preview), not a hosted service (local-first), and finds nothing on un-annotated code *by +design*. The team has resisted scope creep; the documented under-approximations (star imports, module- +global channel, single-block Rust dataflow) all **under-approximate (fail-closed)** — precision/recall +debt, not unsoundness. This is a healthy, defensible boundary, not a gap to close. + +--- + +## 6. Verdict + +A focused, well-tested, security-literate tool that does exactly what it claims and refuses to do more. +The architecture is sound; the risks are concentrated and named; the most actionable immediate wins are +*cleanups of drift the team has already half-finished* (close the layering loop, retire stale tickets) +rather than new construction. Proceed to `05-quality-assessment.md` for the prioritized debt catalog and +`06-architect-handover.md` for the tracked-issue routing. diff --git a/docs/arch-analysis-2026-06-28-0749/05-quality-assessment.md b/docs/arch-analysis-2026-06-28-0749/05-quality-assessment.md new file mode 100644 index 00000000..d17cba28 --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/05-quality-assessment.md @@ -0,0 +1,146 @@ +# 05 — Code Quality Assessment + +**Target:** `wardline` @ `e4668abc` · **Date:** 2026-06-28 +**Basis:** the 12 validated catalog entries (`02-subsystem-catalog.md`), re-verified against source for the +headline items, with each concern mapped to a live Filigree issue ID or flagged genuinely-new. Severity is +by **blast radius** (would this produce a wrong gate verdict / silent security regression / broad refactor +hazard), not by line count. + +## Scorecard + +| Dimension | Grade | Note | +|---|---|---| +| Correctness discipline | A | Fail-closed everywhere; weakest-link meet; monotone fixed point with guardrails | +| Security posture | A− | Secure-by-default gate, layered confinement, HMAC caches/wires; invariants split across seams | +| Test investment | A− | 367 test files (~2:1), conformance + golden + hypothesis + `clarion_e2e`; 90% coverage gate | +| Dependency hygiene | A | Zero-dep base verified clean by all 12 reviewers; lazy `require_*` extras | +| Modularity / cohesion | B− | Real subsystem boundaries, but god-functions + porous private-name seams | +| Layering / acyclicity | B | One contract passes; ~102 deferred imports still mask real `core/` cycles | +| Docs/tracker fidelity | B− | Stale "BROKEN" comment, non-gating CI, several largely-done open tickets | +| **Overall** | **B+** | Strong, coherent, well-tested; debt is concentrated and well-understood | + +--- + +## High severity + +### H1 — `core/` layering goal is only partially realized; the passing contract is left non-gating, with a stale "BROKEN" comment +- **Evidence:** `pyproject.toml:170-182` declares the contract "BROKEN today (wardline.scanner.pipeline / + .taint.project_resolver import wardline.core.attest)"; reality at HEAD — no `core.attest` import in + `scanner/`, both files import `core.ruleset.ruleset_hash` (`pipeline.py:134`, `project_resolver.py:28`), + `lint-imports` → **1 kept, 0 broken** (run by orchestrator + validator). CI runs it as + `uv run lint-imports || true` (`.github/workflows/ci.yml:38`) so the now-passing contract is non-gating. + The narrow ticket `wardline-9ec283d168` is **CLOSED (2026-06-20)** — but its close-note records the + *broad* goal as residual: deferred-import count 158→**102**, "remaining graph hotspots can be tracked by + future layering work," with real cycles still latent (`run → … → attest → assure → run`). +- **Impact:** Two distinct problems. (a) Cheap correctness-of-record fix: the comment misleads every reader + and the gate is needlessly off. (b) Real structural debt: ~102 function-local imports mask cycles, so a + reorg or a new top-level import can surface masked breakage at runtime. +- **Filigree:** narrow contract = `wardline-9ec283d168` (CLOSED, correctly). The **broad layering residual + is untracked** → **NEW issue** recommended (parent `wardline-bf004e2aea`). Quick win (flip CI to gating + + fix comment) is also untracked → fold into the new issue or a tiny PR. + +### H2 — Security invariants are enforced across a caller/callee seam, not locally (3 instances) +- **Evidence:** (a) Secure-by-default gating — `gate_trips`/`gate_breakdown` are population-agnostic + (`suppression.py:88-126`); the "evaluate the un-suppressed population" property is enforced by S4 + *choosing* which population to pass (`run.py:486-489`). A future caller handing the suppressed population + silently defeats it. (b) THREAT-001 confinement is split `resolve_under_root` (S10 `mcp/tooling.py:78`) + + `confine_to_root`/`safe_paths` (S4). (c) Fingerprint determinism — the join key every baseline/waiver + rests on is computed in `scanner/rules/_fingerprint.py:_canonical_ast_dump` (S1/S2), with **no S8-local + guard** (S8 owns the identity domain). +- **Impact:** Each is correct today and guarded by conformance tests + (`test_axis7_gate_population_not_narrowed`, the identity oracle), but each is a *split invariant* a + refactor on one side can break with no local failure — and the fingerprint case already drifted once + (the 3.12↔3.13 fix at `_fingerprint.py:18-24`). +- **Filigree:** untracked as an architectural class → **NEW issue** recommended (umbrella: "promote split + security invariants to type-/assertion-enforced where feasible; document the seam contracts"). + +### H3 — `pytest`-coupled handshake bypass in production MCP code +- **Evidence:** `JsonRpcServer.__init__` sets `_initialized = _initializing = ("pytest" in sys.modules)` + (`mcp/protocol.py:43-46`), so under pytest the server is born initialized and the "server not + initialized" gate (`protocol.py:99-100`) is silently disabled. +- **Impact:** Harmless in the shipped CLI launch, but it couples production code to the test runner's + module table; any embedding that imports pytest (or a future in-process test of the real handshake) + misfires. A test affordance leaking into a protocol-correctness gate. +- **Filigree:** untracked → **NEW issue** recommended (small, surgical: inject the initialized-state for + tests instead of sniffing `sys.modules`). + +--- + +## Medium severity + +### M1 — God-functions / god-modules (change-risk hotspots) +- **Evidence:** `core/run.py:run_scan` ~374 lines (`run.py:221-594`); `scanner/analyzer.py:_analyze_inner` + ~857 lines + ~17 closures (`analyzer.py:249-1105`); `scanner/taint/variable_level.py` ~2,481 LOC; + `mcp/server.py` 5,003 lines (~3,000 inline JSON Schema); `install/doctor.py` ~947 lines multi-concern. +- **Impact:** High cyclomatic complexity, only end-to-end testable, easy to regress. The `server.py` size is + partly deliberate (declaration-next-to-handler) and lower-risk; `run_scan`/`_analyze_inner`/`variable_level` + are the load-bearing ones. +- **Filigree:** untracked → **NEW issue(s)** recommended, scoped per-function with the conformance suite as + the safety net (this is a refactor, not a behavior change). + +### M2 — `decorator_coverage` (and `scan_file_findings`) return unbounded inventories on the MCP surface +- **Evidence:** `_DECORATOR_COVERAGE_TOOL` input schema is only `{path, config}` — no `where`/`offset`/ + truncation (`server.py:3092-3115`); handler returns one row per decorated entity (`server.py:2886-2903`). + `scan_file_findings.active_defects` is the same class, lower volume (`server.py:399-560`). This is the + exact context-overflow class `scan` was already hardened against. +- **Filigree:** `decorator_coverage` = **`wardline-550ea44e53` (OPEN, P2)** — confirmed live in the ready + queue; `scan_file_findings` where/paging = **`wardline-a3eacc5d36` (OPEN, P3)**; the sibling judge-slice + is **`wardline-88104b44f1` (OPEN, P3)**. All tracked — no new issue. + +### M3 — Lineless-DEFECT downgrade leaves the gate population (fail-open-leaning) +- **Evidence:** a `Kind.DEFECT` with `location.line_start is None` (not `ENGINE_PATH`) is *replaced* by a + `Severity.NONE` `Kind.FACT` (`WLN-ENGINE-LINELESS-DEFECT`, `suppression.py:47-67`), so it no longer gates. +- **Impact:** Deliberate (avoids fingerprint-collision on lineless defects) and mitigated by an + always-emitted warning fact, but a class of DEFECT silently exits the gate population. Worth a guard/test + that this can only happen for known-safe rule families. +- **Filigree:** untracked → **NEW issue** recommended (low-medium; document + pin the safe set). + +### M4 — Porous private-name coupling across subsystem seams +- **Evidence:** S2 rules import `decorator_provider._is_builtin_decorator_fqn`/`_shadowed_builtin_roots` + (`contradictory_trust.py:30`, `invalid_decorator_level.py:20`); S1 `grammar.py:196` imports + `variable_level._SERIALISATION_SINKS`; S6 `delta_resolve.py:350` reaches `sei_resolver._client`; S11 + `cli/doctor.py:12-22` reaches seven `install.doctor` privates; S8 `rekey.py:54` hand-mirrors a scanner + constant. Import-linter cannot see most of these. +- **Impact:** A refactor of an underscore-prefixed helper silently breaks a consumer (two of them security + rules). The natural fix is to promote each to a small shared public surface. +- **Filigree:** untracked → **NEW issue** recommended (umbrella: "promote cross-subsystem `_private` + dependencies to public seams"). + +### M5 — `cli/doctor` and the MCP `doctor` render from independent assemblies (human/machine drift) +- **Evidence:** the MCP path goes through the single public `machine_readable_doctor` (`server.py:3853`, + pinning test exists), but `cli/doctor.py` re-assembles the human report from seven `install.doctor` + private helpers independently (`cli/doctor.py:12-22`) — so the two renderings can drift. +- **Filigree:** untracked → fold into **M4's** new issue or a small dedicated one. + +--- + +## Low severity + +| ID | Finding | Evidence | Filigree | +|----|---------|----------|----------| +| L1 | `safe_paths` parent-dir TOCTOU window (O_NOFOLLOW on final component only) | `safe_paths.py:82-93` | NEW (low; narrow window, documented) | +| L2 | `install/pre_commit.py` is the weakest install writer (substring detect, string-concat YAML, bare `except`) | `pre_commit.py:16,19,42` | NEW (low; harden to a YAML parser like `block.py`/`mcp_json.py`) | +| L3 | `autofix.py` "codemod engine" framing oversells a single hard-coded PY-WL-111 fixer | `autofix.py:1,110` | NEW (doc/naming, or generalize dispatch) | +| L4 | N-hop taint provenance is single-hop without the optional Loomweave store; standalone explain/attach triggers a full re-scan | `explain.py:8-9,235`; `filigree_issue.py:276` | `wardline-82f49ec3c3` is **CLOSED (2026-06-01)** — it shipped *single-hop* return-indirection only; the broader **N-hop-without-store completeness residual is UNTRACKED → NEW**. (Note: `ROADMAP.md` still lists this as a near-term thread — itself stale, another tracker-drift instance.) By design; completeness gap | +| L5 | `verify_attestation` missing-schema / non-dict-payload paths exist but are unpinned | `attest.py:329,336` | `wardline-d59f35c626` (OPEN, P3) — tracked | +| L6 | Federation transport/envelope extraction largely landed; residual is thin per-client adapter shells | `core/http.py:47`, `federation_status.py` | `wardline-18499aaa2d` (P3) + `wardline-80e457bc41` (P2) — **triage close-or-rescope**, not net-new | +| L7 | Rust FN gaps (mid-chain `?`/`.await`; block/inner doc-comment markers) — all fail-closed | `dataflow.py:254-281`; `provider.py:64-83` | `wardline-535c9531cc` (P3) + `wardline-9c3a76b257` (P3); note sub-item #4 of the latter is since-addressed (`analyzer.py:175-181`) | +| L8 | Rust `config` accepted-but-unused; `last_context` always `None` (delta/SARIF degrade to file scope); `edges.py` unwired | `analyzer.py:67-76,88-91`; `edges.py:38-44` | NEW or fold into a Rust-preview tracker | +| L9 | Stale staging docstrings ("SP1 ships only…") in the taint provider | `provider.py:5-8`, `resolver_metadata.py:30-34` | NEW (doc rot; trivial) | +| L10 | `node_id.py` `NodeId` contract consumed only by Rust; Python path still keys on raw `id(node)` | `node_id.py:9-16` | NEW (half-laid contract; finish or document deferral) | + +--- + +## Tracker-fidelity summary (the meta-finding) + +The analysis repeatedly found **the code ahead of its own tracker/docs**. Confirmed: +- `wardline-9ec283d168` (layering) — **CLOSED**, fix landed, but `pyproject.toml` + CI still say/treat + "BROKEN." +- `wardline-18499aaa2d`, `wardline-80e457bc41` — **OPEN but largely already implemented** (shared `WeftHttp` + + `federation_status` exist and are consumed by all surfaces); residual is thin shells → triage. +- `wardline-9c3a76b257` sub-item #4 — **since-addressed** in source; issue text stale. + +**Recommendation:** a short tracker-reconciliation pass (re-triage the largely-done tickets, fix the +stale comment, flip CI to gating) is the highest-leverage, lowest-risk work available — it converts +"silent partial completion" into accurate state before any new construction. Routing in +`06-architect-handover.md`. diff --git a/docs/arch-analysis-2026-06-28-0749/06-architect-handover.md b/docs/arch-analysis-2026-06-28-0749/06-architect-handover.md new file mode 100644 index 00000000..e0591b69 --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/06-architect-handover.md @@ -0,0 +1,127 @@ +# 06 — Architect Handover + +**Purpose:** hand this analysis to architecture/improvement planning. **Target:** `wardline` @ `e4668abc`. +**Read first:** `04-final-report.md` (synthesis) and `05-quality-assessment.md` (prioritized debt with +Filigree mapping). This document routes the debt to tracked work and sequences the next moves. + +--- + +## 1. Current-state in one paragraph + +A production-stable (1.0.6 shipped), deliberately-scoped Python taint analyzer with a coherent +**surfaces → orchestration → engine** architecture, a hard zero-dependency-base invariant, identical-by- +construction CLI/MCP/LSP surfaces, and serious test investment (~2:1 test:source, conformance + golden + +hypothesis). The debt is **concentrated and well-understood**, not diffuse: a few god-functions, porous +private-name seams, security invariants enforced across cooperating subsystems rather than locally, and — +most actionable — **drift between the code and its own tracker/docs** (the headline "violation" is already +fixed). There is no live security hole and no architectural emergency; the highest-leverage work is +*finishing and recording* partial completions before new construction. + +## 2. The refactor safety net (use it) + +Every god-function flagged for decomposition is backed by a conformance/oracle suite that pins behavior — +decomposition is therefore *low-risk if the suite stays green*: +- Gate/suppression/delta: `tests/conformance/test_warpline_delta_scope.py`, `tests/unit/core/test_run_affected.py`, + `test_axis7_gate_population_not_narrowed`, `test_delta_trust_suppressions_cannot_forge_green`. +- Identity/fingerprint: `tests/golden/identity/test_identity_parity.py`, + `tests/unit/scanner/rules/test_entity_fingerprint_stability.py`, `tests/conformance/qualnames*.json`. +- Surface parity: `tests/unit/core/test_cli_mcp_parity.py`, `tests/unit/mcp/test_server_doctor.py`. +- Federation wire: golden vectors + the armed `sei_drift`/`worklist_drift` CI + `*_e2e` live oracles. +Run `make ci` (`lint typecheck test-cov`, 90% gate) as the gate for any change. + +## 3. Prioritized improvement roadmap (mapped to tracker) + +Ordered by **leverage ÷ risk**. "NEW" = no current Filigree issue; recommend filing under the holistic +risk-review parent `wardline-bf004e2aea` with label `arch-analysis-2026-06-28`. + +### Tier 0 — Tracker/doc reconciliation (highest leverage, near-zero risk) — do first +| Action | Source | Filigree | +|---|---|---| +| Fix the stale "BROKEN today" import-linter comment in `pyproject.toml:170-182` | H1 | NEW (or reopen-note on closed `wardline-9ec283d168`) | +| Flip CI `lint-imports \|\| true` → gating now that the contract passes | H1 | NEW (tiny) | +| Re-triage `wardline-18499aaa2d` (WeftHttp) + `wardline-80e457bc41` (envelope): close or rescope to the residual adapter shells (`80e457bc41` was partially re-triaged 2026-06-20 but remains open) | L6 | **`wardline-18499aaa2d`**, **`wardline-80e457bc41`** (both OPEN) | +| Trim `wardline-9c3a76b257` sub-item #4 (since-addressed: `analyzer.py:175-181`) | L7 | **`wardline-9c3a76b257`** (OPEN) | +| Fix stale "SP1 ships only…" docstrings in the taint provider | L9 | NEW (trivial) | + +### Tier 1 — Structural integrity (real debt, bounded) +| Action | Source | Filigree | +|---|---|---| +| Track + drive down the **broad `core/` layering** residual (102 deferred imports; real `run→…→attest→assure→run` cycles) — the part the closed ticket explicitly left to "future layering work" | H1 | **NEW** (parent `wardline-bf004e2aea`) | +| Harden the 3 **split security invariants** (gate-population, THREAT-001 confinement, fingerprint determinism) toward type-/assertion-enforced + documented seam contracts | H2 | **NEW** | +| Remove the **pytest-coupled handshake bypass** (`protocol.py:43`) — inject test state instead of sniffing `sys.modules` | H3 | **NEW** (surgical) | +| Promote cross-subsystem `_private` dependencies to public seams (rules→`decorator_provider`, `grammar`→`variable_level`, S6→`sei_resolver._client`, `cli/doctor`→`install.doctor` privates) | M4, M5 | **NEW** (umbrella) | + +### Tier 2 — Maintainability & surface hardening +| Action | Source | Filigree | +|---|---|---| +| Decompose the load-bearing god-functions (`run_scan`, `_analyze_inner`, `variable_level.py`) behind the conformance net | M1 | **NEW** (per-function) | +| Add `where`/paging/truncation to `decorator_coverage` (+ `scan_file_findings`, judge slice) | M2 | **`wardline-550ea44e53`** (P2), **`wardline-a3eacc5d36`** (P3), **`wardline-88104b44f1`** (P3) — all OPEN | +| Guard the lineless-DEFECT downgrade so only known-safe rule families can leave the gate population | M3 | **NEW** | +| Harden `install/pre_commit.py` to a YAML parser (match `block.py`/`mcp_json.py`) | L2 | **NEW** | + +### Tier 3 — Completeness / preview (by-design gaps, lower priority) +| Action | Source | Filigree | +|---|---|---| +| N-hop explain completeness without the optional store; avoid full re-scan for single-finding explain/attach | L4 | **`wardline-82f49ec3c3` is CLOSED** (2026-06-01 — single-hop return-indirection only); the broader N-hop-without-store residual is **untracked → NEW**. `ROADMAP.md`'s "near-term" listing is stale | +| Pin `verify_attestation` missing-schema / non-dict-payload edge tests | L5 | **`wardline-d59f35c626`** (OPEN, P3) | +| Rust FN gaps (mid-chain `?`/`.await`; block/inner doc markers) | L7 | **`wardline-535c9531cc`** (P3), **`wardline-9c3a76b257`** (P3) | +| Rust preview surface gaps (`config` unused, `last_context` None ⇒ delta/SARIF degrade, `edges.py` unwired); finish/retire the `node_id` Python contract | L8, L10 | **NEW** (Rust-preview tracker) | + +## 4. Suggested sequencing + +1. **Tier 0 reconciliation sprint** (hours, not days) — converts silent partial completion into accurate + state; closes/rescopes 4 tickets and removes a misleading comment + a needlessly-off CI gate. +2. **H3 + M4** — small, high-clarity coupling/correctness fixes that de-risk everything downstream. +3. **H1 broad-layering + H2 split-invariants** — the genuine structural work; do behind the conformance + net, contracts-report-only-first, then enforcing (the same playbook the closed ticket used). +4. **M1 god-function decomposition** — opportunistic, one function per PR, suite-gated. + +## 5. What NOT to do +- Don't "fix" `fingerprint_v0.py` to match the live formula — it is an intentional frozen clone; re-syncing + it silently orphans every migrated verdict (`fingerprint_v0.py:1-13`). +- Don't broaden Wardline's scope (more languages, broad SAST, hosted service) — the L1–L2+L3 / Python-first + / local-first boundary is a deliberate, defended product decision (`ROADMAP.md`, + `project_product_thesis`). +- Don't treat the documented under-approximations as bugs — they fail *closed* (precision/recall debt). + +## 5b. Filed issues (the "NEW" items are now tracked) + +Filed 2026-06-28 under parent `wardline-bf004e2aea`, label `arch-analysis-2026-06-28`: + +| Issue | Tier | Source finding | Pri | +|---|---|---|---| +| `wardline-7971cbcf9e` | 0 | H1 — fix stale pyproject "BROKEN" comment + make CI `lint-imports` gating | P2 | +| `wardline-a0eaa7dd12` | 1 | H1 broad — drive down `core/` layering residual (~102 deferred imports) | P2 | +| `wardline-8a1399a8b5` | 1 | H2 — harden 3 split security invariants | P2 | +| `wardline-5e4a4ee246` | 1 | H3 — remove pytest-coupled MCP handshake bypass | P2 | +| `wardline-3932db542c` | 1 | M4/M5 — promote cross-subsystem `_private` deps to public seams | P3 | +| `wardline-83c416811a` | 2 | M1 — decompose god-functions behind the conformance net | P3 | +| `wardline-da175547cf` | 2 | M3 — guard the lineless-DEFECT→FACT gate-population exit | P3 | +| `wardline-a8c1815e64` | 2 | L2 — harden `install/pre_commit.py` to a YAML parser | P3 | +| `wardline-e2487c053a` | 3 | L4 — N-hop explain completeness residual (+ stale ROADMAP entry) | P3 | +| `wardline-00beb310e0` | 3 | L8/L10 — Rust preview surface gaps | P3 | +| `wardline-f3ef15adb2` | low | L9/L3/L1 — doc/naming cleanups + TOCTOU note | P4 | + +Tracked-but-existing items (re-triage / continue): `wardline-550ea44e53`, `wardline-a3eacc5d36`, +`wardline-88104b44f1` (M2 bounding); `wardline-18499aaa2d`, `wardline-80e457bc41` (L6 close/rescope); +`wardline-d59f35c626` (L5); `wardline-535c9531cc`, `wardline-9c3a76b257` (L7). + +## 6. Cross-pack recommendations +- **Quality/architecture deep-dive:** `axiom-system-architect` (`/assess-architecture`, `/catalog-debt`, + `/prioritize-improvements`) to turn §3 into a costed improvement backlog. +- **Threat modeling:** `ordis-security-architect` (`/threat-model`) — the split security invariants (H2), + the lineless-DEFECT gate exit (M3), and the federation replica-drift surface (L6/legis fail-open) are + worth a STRIDE pass even though no live hole was found. +- **Static-analyzer-specific:** `axiom-static-analysis-engineering` (false-positive-economics, + rule/lattice design) — relevant to the 11 PREVIEW rules and the MIXED_RAW latent-disagreement invariant. + +--- + +### Validation +This handover and `04-final-report.md` were gated by a second `analysis-validator` pass — see +`temp/validation-synthesis.md`. The catalog gate is `temp/validation-catalog.md` (PASS-WITH-NOTES). +Filigree IDs cited here were checked against the live tracker on 2026-06-28. **CLOSED/done:** +`wardline-9ec283d168` (layering), `wardline-82f49ec3c3` (single-hop return-indirection). **OPEN:** +`wardline-550ea44e53` (P2), `wardline-80e457bc41` (P2), `wardline-18499aaa2d` (P3), +`wardline-d59f35c626` (P3), `wardline-535c9531cc` (P3), `wardline-9c3a76b257` (P3), +`wardline-a3eacc5d36` (P3), `wardline-88104b44f1` (P3). Items marked **NEW** have no current issue. diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-S1.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S1.md new file mode 100644 index 00000000..3dc82780 --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S1.md @@ -0,0 +1,52 @@ +## S1 — Scanner Engine + +**Location:** `src/wardline/scanner/{analyzer,pipeline,context,grammar,index,ast_primitives,flow_trace,diagnostics}.py` + +**Responsibility:** Orchestrates the per-project AST walk — parse → entity index → L1 trust-decorator seeding → L3 transitive fixed point → L2 flow-sensitive variable taints — and exposes the result as a frozen, read-only `AnalysisContext` for the rule lattice (S2), plus the engine's own self-diagnostic findings. + +**Key Components:** +- `analyzer.py` — `WardlineAnalyzer` (`analyzer.py:218`) and its end-to-end driver `_analyze_inner` (`analyzer.py:249`); `build_analyzer(...)` factory (`analyzer.py:1108`). This is the orchestration core: it runs the parse stage, calls S3's project resolver, drives the bounded L2 fixed-point loop (`analyzer.py:806`), assembles the `AnalysisContext` (`analyzer.py:969`), and runs each rule in per-rule isolation (`analyzer.py:1054`). +- `pipeline.py` — typed pipeline-stage DTOs and stages: `run_parse_project_stage` (`pipeline.py:87`) reads/parses/indexes/seeds/cache-classifies files into `ParseProjectOutput`; `run_l2_function_stage` (`pipeline.py:307`) runs one function's variable-level walk. Frozen-dataclass I/O contracts (`ParsedFile`, `ParseProjectInput/Output`, `L2FunctionInput/Output`). +- `context.py` — `AnalysisContext` (`context.py:42`), the frozen engine-output DTO consumed by rules; deep-freezes every nested mapping to `MappingProxyType` in `__post_init__` (`context.py:138`). `RuleRegistry` (`context.py:191`) is the rule-dispatch seam (`run` → `rule.check(context)`, `context.py:204`). +- `grammar.py` — the extensible trust grammar (`BoundaryType` `grammar.py:56`, `TrustGrammar` `grammar.py:149`, `default_grammar()` `grammar.py:223`): an open meta-model letting agents add trust vocabulary/rules without editing engine source, with a load-time tripwire that fails closed if builtins drift from `core.registry.REGISTRY` (`grammar.py:138`). +- `index.py` — per-file entity discovery: `discover_file_entities` (`index.py:57`) and `discover_class_qualnames` (`index.py:30`) build `Entity` rows (`index.py:20`); handles `@overload` drop, last-wins rebinding, `@property` setter/deleter identity. +- `ast_primitives.py` — stdlib-only AST helpers: `build_import_alias_map` (`ast_primitives.py:21`), `iter_calls_in_function_body` (`ast_primitives.py:96`), `resolve_call_fqn` (`ast_primitives.py:171`), `resolve_self_method_fqn` (`ast_primitives.py:142`). +- `diagnostics.py` — engine-diagnostic `Finding` builders: `build_collision_findings` (the proactive fingerprint-uniqueness guard, `diagnostics.py:106`), `build_unknown_import_findings`/`diagnose_unknown_imports` (`diagnostics.py:196`/`295`), `build_metric_finding`, `build_diagnostic_findings`. +- `flow_trace.py` — `build_finding_code_flow` (`flow_trace.py:163`) projects S3 taint provenance into a stable `FindingCodeFlow` DTO for S5 formatters/explain. + +**Public surface / entry points:** +- `WardlineAnalyzer.analyze(files, config, *, root)` (`analyzer.py:240`) — implements `core.protocols.Analyzer`; the engine's single run entry. +- `build_analyzer(*, grammar, summary_cache)` (`analyzer.py:1108`) — agent-facing factory (default = `default_grammar()`). +- `AnalysisContext` (`context.py:42`) + `RuleRegistry.run` (`context.py:204`) — the seam S2 rules read. +- `run_parse_project_stage` / `run_l2_function_stage` (`pipeline.py:87`/`307`) — stage functions (also used directly by tests). +- `default_grammar()` / `TrustGrammar.extend(...)` (`grammar.py:223`/`160`) — the grammar extension plane. +- `build_finding_code_flow` (`flow_trace.py:163`), `discover_file_entities` (`index.py:57`), `build_import_alias_map` (`ast_primitives.py:21`). + +**Dependencies (graph-derived):** +- Inbound (who calls into this): + - **S4 Core Orchestration** → `WardlineAnalyzer.analyze` (`core/run.py:381`, graph-resolved attribute-receiver) and `build_analyzer` via `core.frontends.PythonFrontend.build_analyzer` (`core/frontends.py:70`, graph-resolved edge), and `frontend.build_analyzer` (`core/run.py:332`). + - **S2 Rule Lattice** consumes the `AnalysisContext` output (passed in via `RuleRegistry.run → rule.check(context)`, `context.py:207`). + - **S5 Findings/Outputs** and **S7 Trust Evidence** read the context downstream — e.g. `core/assure.py` reads `context.declared_qualnames` as the coverage denominator (documented at `context.py:96-98`); `flow_trace.build_finding_code_flow` feeds explain/SARIF. (Production read paths corroborated by the context docstrings; the only graph callers of the `AnalysisContext` constructor are `analyzer.py:969` plus test fixtures under `tests/unit/core/test_assure.py`, `test_dossier_assembler.py`, `test_sarif.py`.) +- Outbound (what this calls), all from the `_analyze_inner` neighborhood (graph-resolved callees): + - **S3 Taint Engine** (the entire `scanner/taint/*` package): `project_resolver.resolve_project_taints` (`analyzer.py:291`), `decorator_provider.DecoratorTaintSourceProvider`/`vocabulary_star_exports` (`analyzer.py:252`), `variable_level.analyze_function_variables`/`attribute_write_recording`/`project_attribute_writes`, `call_taint_map.build_call_taint_map` (`analyzer.py:659`), `module_summariser.collect_module_global_raw_seeds`/`own_scope_global_names`, `function_level.seed_function_taints` (via `pipeline.py:159`), `summary.compute_cache_key`/`summary_cache.SummaryCache`. + - **S2 Rule Lattice**: `rules.build_default_registry` (`analyzer.py:960`) and `rules._sink_helpers.collect_sink_bindings` (`analyzer.py:562`). + - **S5 Findings model**: `core.finding` (`Finding`, `Kind`, `Location`, `Severity`, `ENGINE_PATH`) used throughout. + - **Shared core taint lattice**: `core.taints` (`TaintState`, `combine`, `RAW_ZONE`, `TRUST_RANK`) — `analyzer.py:20`, foundational taint type system (closest label S3, but it physically lives in `core/`). + - **S4 Core** low-tier helpers: `core.qualname.module_dotted_name` (`pipeline.py:13`), `core.ruleset.ruleset_hash` (`pipeline.py:134`), `core.registry.REGISTRY` (`grammar.py:24`). + +**Patterns Observed:** +- Staged pipeline with frozen, slotted dataclass I/O contracts between stages (`ParseProjectInput/Output`, `L2FunctionInput/Output` in `pipeline.py`); `AnalysisContext` deep-freezes nested mappings to `MappingProxyType` so engine output is genuinely read-only (`context.py:138-177`). +- Secure-by-default / fail-closed diagnostics: an unparseable or unexpectedly-failing file becomes a *gate-eligible* `ERROR`/`DEFECT` (`WLN-ENGINE-PARSE-ERROR` `pipeline.py:192`, `WLN-ENGINE-FILE-FAILED` `pipeline.py:223`/`analyzer.py:594`, `WLN-ENGINE-FUNCTION-SKIPPED` `analyzer.py:627`), never a silent skip — so `scan --fail-on ERROR` trips rather than reading green over unanalyzed code. +- Engine self-soundness guards: a proactive fingerprint-collision check over the full emitted set (`diagnostics.build_collision_findings`, run last at `analyzer.py:1104`), L2/L3 convergence bounds (`WLN-ENGINE-L2-CONVERGENCE-BOUND` `analyzer.py:935`) and monotonicity diagnostics. +- Bounded fixed points: L2 iteration bound = lattice height × cells (`analyzer.py:497`); a per-function candidate-key budget `_CANDIDATE_KEY_BUDGET = 250_000` (`analyzer.py:80`) bounds adversarial blow-up; non-convergence demotes affected returns to `UNKNOWN_RAW` (fail-safe over-taint). +- Explicit O(n²)-avoidance discipline, heavily documented: the pruned per-function taint map (`_pruned_method_taint_map` `analyzer.py:83`), once-per-module sibling/call maps, and a memoized fixed-point keyed only on iteration-varying inputs (`_L2InputKey` `analyzer.py:63`). +- Extensibility as a *code* seam, zero-dep: `TrustGrammar.extend()` appends agent boundary types/rules without engine edits; builtins are pinned byte-identical and a load-time tripwire fails closed on REGISTRY drift (`grammar.py:138`). + +**Concerns:** +- **God-method.** `_analyze_inner` is a single ~857-line method (`analyzer.py:249-1105`) wrapping ~17 nested closures and the full L2 fixed-point loop. It is the dominant structural risk for S1: very high cyclomatic complexity, no in-isolation seams (the graph shows it is only ever reached via `analyze`; all coverage is end-to-end through `analyzer.analyze`), so changes are hard to test narrowly and easy to regress. +- **Stale layering-violation tracking (wardline-9ec283d168) — the watch-item premise is outdated.** The tracked violation "`scanner.pipeline` imports `wardline.core.attest`" is NOT present at this commit. `pipeline.py:134` imports `from wardline.core.ruleset import ruleset_hash` (used at `pipeline.py:144` as `scan_policy_hash` in `compute_cache_key`), and a `grep` over all of `scanner/` finds no `attest` import anywhere (only doc-comment mentions in `taint/summary.py`). `core/ruleset.py:10-16` documents the remediation: `ruleset_hash` was rehomed to a low-tier module so the engine and `core.attest` both import *down*, replacing "a function-local deferred import in `scanner.taint.project_resolver`." Yet `pyproject.toml:173-182` still declares the contract "BROKEN today" naming both `scanner.pipeline` and `.taint.project_resolver`, and that import-linter contract is report-only (`lint-imports || true`). Scope: I authoritatively confirm the `scanner.pipeline → core.attest` direct edge is absent (my file, fully read); `project_resolver.py` is S3's, but my `scanner/`-wide grep shows zero `attest` substring there too. I do NOT claim the whole contract passes (transitive edges through other `core.*` modules are out of S1 scope). Net: documentation/process drift, not a live S1 violation. +- **Mutable `set` on a frozen dataclass.** `AnalysisContext.flow_insensitive_fallbacks: set[str]` (`context.py:136`) is a deliberate diagnostics side-channel that breaks the otherwise-strict immutability invariant `__post_init__` enforces on every other field. Documented, but a sharp edge: a rule/consumer holding the context can mutate engine output through it. +- **Identity-keyed maps.** Call-site maps key on `id(call)`/`id(stmt)` (`context.py:76-90`, `analyzer.py:737`). Sound only because AST nodes are retained for the scan lifetime; `_with_module_global_params` (`analyzer.py:158`) must carefully share the *original* body statement objects on its synthetic wrapper to keep those keys valid — a fragile invariant flagged in-source (`analyzer.py:166-173`). +- **Broad `except Exception` (BLE001) per-file/per-rule isolation** at `pipeline.py:217`, `analyzer.py:732`, `:880`, `:1059`. Intentional and well-justified (each becomes a loud gate-eligible DEFECT), but the pattern's soundness depends entirely on the surrounding fail-closed discipline staying intact. + +**Confidence:** High — Read all 8 S1 files in full (`analyzer.py` 1120 lines, `pipeline.py` 325, `context.py`, `grammar.py`, `index.py`, `ast_primitives.py`, `flow_trace.py`, `diagnostics.py`). Dependencies are graph-derived from Loomweave (`entity_neighborhood_get` on `_analyze_inner`, `entity_callers_list` on `analyze`/`build_analyzer`/`AnalysisContext`) and cross-checked against source `file:line`. The attest finding is corroborated three ways: full read of `pipeline.py`, a `grep` over all of `scanner/`, and the `core/ruleset.py:10-16` remediation docstring + `pyproject.toml:173-182` contract. Lower-confidence item, flagged inline: production consumers of `AnalysisContext` (S5/S7) rest on context docstrings + test-fixture constructions rather than graph-resolved production call edges. diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-S10.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S10.md new file mode 100644 index 00000000..9d693b62 --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S10.md @@ -0,0 +1,47 @@ +## S10 — MCP & LSP Server + +**Location:** `src/wardline/mcp/` (`server.py`, `protocol.py`, `tooling.py`, `resources.py`, `prompts.py`, `freshness.py`, `lsp.py`) + `src/wardline/lsp.py` + +**Responsibility:** The dependency-free, hand-rolled JSON-RPC-2.0 MCP-over-stdio server that is Wardline's primary agent surface (18 tools + 4 resources + 1 prompt, all delegating to S4 core), plus a sibling stdlib LSP diagnostics server. + +**Key Components:** +- `mcp/protocol.py` — hand-rolled JSON-RPC 2.0 + MCP envelope, no SDK (docstring `protocol.py:1-5`: "No SDK — the same stdlib discipline as the SP5 urllib judge"). `JsonRpcServer` (`protocol.py:37`); `dispatch()` is a pure, unit-testable message function (`protocol.py:62`); `run_stdio()` is the newline-framed read/write loop (`protocol.py:124`). Protocol-version negotiation: `SUPPORTED_PROTOCOL_VERSIONS = ("2025-06-18","2025-03-26","2024-11-05")` (`protocol.py:16`), echoed-if-supported else latest in `_initialize` (`protocol.py:51-60`). `McpError` carries the JSON-RPC error code for protocol faults (`protocol.py:28`). 10 MB line cap with overflow drain (`protocol.py:137-149`). +- `mcp/server.py` (5003 lines) — the whole tool/resource/prompt surface. `WardlineMCPServer` (`server.py:4525`) registers 18 tools (`_register_tools`, `server.py:4614-4802`), wires `tools/call`, `tools/list`, `resources/*`, `prompts/*` (`_wire`, `server.py:4811`). Each tool's full spec (input_schema/output_schema/annotations/capabilities) is a module-level dict colocated with its handler; ~3000 of the 5003 lines are inline JSON Schema. Dual emission + the split error model live in `_tools_call` (`server.py:4915-4984`). +- `mcp/tooling.py` — shared plumbing: `Tool` frozen dataclass (`tooling.py:46`), `ToolCapability` StrEnum READ/WRITE/NETWORK (`tooling.py:23`), `ToolPolicy.denial` (`tooling.py:38`), `ToolError` (`tooling.py:15`), and the THREAT-001 confinement `resolve_under_root` (`tooling.py:78-83`). +- `mcp/freshness.py` — `server_identity`/`attach_server_identity` source-mtime freshness verdict for the `doctor` tool; flips `ok` false and lands a `server.freshness` check when on-disk source is newer than process start (the 2026-06-06 stale-long-lived-server incident; `freshness.py:48-84`). +- `mcp/resources.py` — 4-entry resource catalog `wardline://vocab|rules|config|config-schema` (`resources.py:16-21`); findings are deliberately never a resource. +- `mcp/prompts.py` — single `wardline:loop` prompt teaching the scan→explain→fix→rescan loop (`prompts.py:9-32`). +- `mcp/lsp.py` — 8-line compat re-export of `wardline.lsp`. +- `lsp.py` — `LspServer` stdlib LSP diagnostics (`lsp.py:26`): Content-Length framing (`lsp.py:48-58`), initialize/didOpen/didSave/didClose handlers (`lsp.py:121-177`), `run_and_publish` runs S4 `run_scan(confine_to_root=True)` and emits `publishDiagnostics` (`lsp.py:179-224`). Own launch-root confinement, separate from the MCP guard (`_is_under_launch_root`/`_is_allowed_root`, `lsp.py:296-302`); 10 MB content cap + drain (`lsp.py:16,87-90`). + +**Tool surface (18, in published order):** `scan`, `scan_job_start`, `scan_job_status`, `scan_job_cancel`, `explain_taint`, `dossier`, `assure`, `decorator_coverage`, `attest`, `verify_attestation`, `file_finding`, `scan_file_findings`, `judge`, `baseline`, `waiver_add`, `fix`, `doctor`, `rekey` (`server.py:4618-4802`; count corroborated by the live `mcp__wardline__*` surface). + +**Public surface / entry points:** +- `WardlineMCPServer(root=, loomweave_url=, filigree_url=, allow_write=, allow_network=)` then `.rpc.run_stdio()` — the stdio MCP server (`server.py:4525`). +- `LspServer(root=root).run()` — the LSP server (`lsp.py:26`). +- `JsonRpcServer.dispatch(message)` — the pure dispatch boundary (`protocol.py:62`), tested without I/O. +- `resolve_under_root(root, arg)` — path-escape guard re-used by every path/config-taking handler (`tooling.py:78`). + +**Dependencies (graph-derived):** +- Inbound (S11 only — S10 is a leaf facade reached solely via CLI launch): `entity_callers_list` on `WardlineMCPServer` returns only tests + `src/wardline/cli/mcp.py:53` (verified: `cli/mcp.py:53` constructs it, `:61` calls `.rpc.run_stdio()`); `LspServer` is launched at `cli/lsp.py:22` (`LspServer(root=root).run()`). +- Outbound (from `_scan` neighborhood + read imports `server.py:18-53`): **S4 Core/Orchestration** — `run_scan`/`gate_decision`/`baseline_migration_hint` (`core/run.py`), config, paths, `scan_jobs`, `sei_resolution`, `delta_scope`, `safe_paths` confinement; **S5 Findings/Outputs** — `agent_summary.build_agent_summary`, `explain`, `Finding`, `filigree_emit`, `federation_status`; **S6 Gate/Remediation** — `baseline`, `waivers`, `finding_query.filter_findings`, `delta_scope`, `scan_file_workflow`; **S7 Trust Evidence/Judge** — `assure.build_posture`, `attest`, `weft_dossier`, `weft_decorator_coverage`, `judge_run`, `legis`; **S8 Identity/SEI** — `sei_resolution`, `loomweave.identity.SeiResolver`; **S9 Federation** — `FiligreeEmitter`, `FiligreeIssueFiler`, `LoomweaveClient`, `loomweave.write`; **S2 Rule Lattice** — `resources.py:37` reads `scanner.rules._ALL_RULE_CLASSES`. + +**Patterns Observed:** +- **Hand-rolled JSON-RPC, no MCP SDK** — stdlib `json`/`sys` only (`protocol.py`), preserving the base zero-dep posture. +- **Identical-by-construction core, agent-shaped surface.** CLI, MCP, and LSP all route through the same S4 `run_scan`+`gate_decision` (`server.py:792-827`; `lsp.py:182`), so findings/gate are identical; MCP layers agent-shaping *on top* — bounded pagination, inline explain, fail-soft enrichment, legis artifact. The project asserts this parity in `tests/unit/core/test_cli_mcp_parity.py`. +- **Split error model** (watch item, confirmed): protocol faults stay JSON-RPC errors — unknown tool/method/bad-args raise `McpError`, which `_tools_call` deliberately re-raises for `dispatch` to map (`server.py:4928-4931`, `:4964-4967`; `protocol.py:104,107-108`); tool-execution errors (`ToolError`/`WardlineError`/unexpected crash) become `{isError:true, content:[text]}` results so MCP clients reliably relay them (`server.py:4943-4977`, `_is_error` at `:5000`). +- **Structured-output dual emission** (B1/B2): every successful call returns text `content` byte-identical to the pre-B1 shape AND `structuredContent` with the same payload; isError results never carry structuredContent (`server.py:4978-4984`). `tools/list` advertises `outputSchema`/`title`/`annotations` alongside the homegrown `capabilities` key, mapped not replaced (`server.py:4822-4839`). +- **Two-layer capability enforcement:** tools advertise a conservative possible-effects superset statically (e.g. `scan` = READ+WRITE+NETWORK, `server.py:1937`), but `_effective_tool_capabilities` recomputes the real per-call set (a local scan with no integration URL is READ-only) and `ToolPolicy.denial` gates it before the handler runs (`server.py:4857-4913,4952-4954`). +- **THREAT-001 path confinement:** `resolve_under_root` rejects escapes on every MCP path/config arg (`tooling.py:78-83`), and handlers pass `confine_to_root=True` into S4 read paths (`server.py:796,2185`; `lsp.py:182`) — S10 is the write-side guard, S4 the read-side. +- **Bounded-by-default payloads (context-overflow hardening):** `scan` returns ≤25 finding bodies by default with `offset`/`full` paging and a `truncation` descriptor (`server.py:93,860-885,1852-1877`); `explain:true` inlining is capped at 10 (`server.py:89`); `summary_only` suppresses the ~56 KB legis artifact (`server.py:2121-2126`). +- **Fail-soft enrichment:** Filigree/Loomweave/legis writes report into their blocks but never fail the scan (`server.py:96-135,806-826,2104-2107`); URLs are redacted in diagnostics (`redact_url_for_diagnostics`, `server.py:699-708`). +- **Lazy `jsonschema` validation preserving zero-dep:** `jsonschema` lives only in the `scanner` extra (`pyproject.toml:13,24` — base `dependencies = []`); `_tools_call` imports it under `try/except ImportError` and degrades to the handlers' own manual checks (`_bool_arg`, `_fail_on_arg`) when absent (`server.py:4933-4943`). + +**Concerns:** +- **`decorator_coverage` is unbounded on the MCP surface** (watch item wardline-550ea44e53, OPEN, P2). Its input_schema is only `{path, config}` — no `where`, no `max_findings`/`offset`, no truncation marker (`_DECORATOR_COVERAGE_TOOL`, `server.py:3092-3115`) — unlike `scan`'s bounding contract (`server.py:1852-1877`). The handler returns the full one-row-per-decorated-entity inventory (`server.py:2886-2903`); the S7 builder it calls has no internal cap either (corroboration, not my analysis). On a large trust surface this is the exact context-overflow class `scan` was hardened against. +- **`scan_file_findings` is the same unbounded class, lower volume:** its `active_defects` array carries every active defect with no page control (schema `server.py:399-560`, handler `server.py:296-332`) — one-line analogue of the above, worth the same `where`/paging treatment. +- **pytest-coupled handshake bypass:** `JsonRpcServer.__init__` sets `_initialized = _initializing = ("pytest" in sys.modules)` (`protocol.py:43-46`), so under pytest the server is born already-initialized and the `"server not initialized"` gate (`protocol.py:99-100`) is silently disabled. A test affordance that couples production code to the test runner's module table; harmless today but would misfire in any embedding that imports pytest. +- **Global stdout redirect:** `run_stdio` mutates `sys.stdout = sys.stderr` to keep stray prints off the JSON-RPC channel, restoring in `finally` (`protocol.py:133-135,165-168`). Correct for a process-owning server, but a global side effect that would interfere with in-process embedding. +- **5003-line `server.py`:** a single module holding all 18 handlers + ~3000 lines of inline JSON Schema. Deliberate colocation (declaration-next-to-handler, `server.py:4614-4617`), not a defect, but a maintainability/navigation cost worth noting. + +**Confidence:** High — Read in full: `protocol.py`, `tooling.py`, `resources.py`, `prompts.py`, `freshness.py`, `mcp/lsp.py`, `lsp.py`; `server.py` read structurally (tool registration, `_tools_call` dispatch + split error model, `_effective_tool_capabilities`, and the `scan`/`explain_taint`/`decorator_coverage`/`scan_file_findings`/legis handlers), with remaining handler bodies and the ~3000 lines of inline schema skimmed via grep. Dependencies are graph-derived (`entity_callers_list`, `entity_neighborhood_get` on `_scan`/`WardlineMCPServer`/`resolve_under_root`) and corroborated by `Read` (`cli/mcp.py:53,61`; `cli/lsp.py:22`; `core/run` imports). The wardline-550ea44e53 status and the empty `weft_decorator_coverage` cap grep were verified live. Tool count cross-checked against the session's `mcp__wardline__*` surface. diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-S11.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S11.md new file mode 100644 index 00000000..881e609a --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S11.md @@ -0,0 +1,51 @@ +## S11 — CLI & Install/Activation + +**Location:** `src/wardline/cli/` (the `click` command surface, one module per command), `src/wardline/install/` (agent-enablement: CLAUDE.md/AGENTS.md blocks, `.mcp.json`, Codex MCP, skills, pre-commit, sibling detection, doctor health/repair) + +**Responsibility:** Expose every Wardline capability as a thin `click` command that delegates to the shared core engines, and push the "opt-in activation, not configuration" surface — instruction blocks, MCP wiring, skill packs, pre-commit hooks — into a project so an agent gets a working gate first-run. + +**Key Components:** +- `cli/entrypoint.py` — the dependency-light console-script shim (`main`, `entrypoint.py:10`); imports `cli.main.cli` inside a `try/except ModuleNotFoundError` and, if `click`/`jsonschema`/`yaml` are absent (`_SCANNER_EXTRA_IMPORTS`, `entrypoint.py:7`), exits 2 telling the user to install `wardline[scanner]`. This is the seam that keeps the base package zero-dep while the command itself needs the extra. Console-script `wardline = wardline.cli.entrypoint:main` (`pyproject.toml:55`); graph-confirmed zero in-repo callers (it is the OS entry point). +- `cli/main.py` — the `@click.group` (`main.py:36`) wiring 17 subcommands via `cli.add_command(...)` (`main.py:42-58`), plus two inline commands defined in this file rather than their own module: `vocab` (`main.py:61`, emits the NG-25 descriptor YAML) and the `baseline` group with `create`/`update` (`main.py:105-217`, delegating to S6 `collect_and_write_baseline`). +- `cli/scan.py` — the central command and the widest CLI surface (~675 lines, ~30 options). Delegates analysis to S4 `run_scan` (`scan.py:293`, re-run after `--fix` at `:325`) and the gate verdict to S4 `gate_decision` (`scan.py:450`, `:603`); everything else here is option parsing, multi-format dispatch (jsonl/sarif/agent-summary/legis), fail-soft sibling emission, and human-facing rendering of a decision computed in core. +- `cli/{assure,attest,dossier,judge,explain_taint,decorator_coverage,file_finding,findings,rekey,fix,scan_file_findings}.py` — one thin delegator each; each docstring states it calls "the same core function the MCP tool calls — CLI and MCP identical by construction" (`assure.py:5`, `dossier.py:3`, `explain_taint.py:3`, `attest.py:4`, `judge.py:3`). +- `cli/{mcp,lsp}.py` — launchers for the S10 servers: `mcp` resolves sibling URLs then starts `WardlineMCPServer(...).rpc.run_stdio()` (`mcp.py:53`); `lsp` starts `LspServer(root=root).run()` (`lsp.py:22`). +- `cli/scan_job.py` + `cli/scan_job_worker.py` — file-backed long-scan surface (`start`/`status`/`cancel`) over S4 `core.scan_jobs`; the worker is a `python -m` subprocess entrypoint (`scan_job_worker.py:12`). +- `install/block.py` — renders + idempotently injects the hash-fenced wardline instruction block into shared `CLAUDE.md`/`AGENTS.md` (`inject_block`, `block.py:182`). Implements the weft C-4 multi-owner managed-block contract: own-namespace-only rewrite, never crosses a real foreign block, atomic write with a refuse-to-empty guard (`_atomic_write_text`, `block.py:144-179`). +- `install/mcp_json.py` — writes the project `.mcp.json` `wardline` stdio entry (`merge_mcp_entry`, `mcp_json.py:171`) and the global Codex `~/.codex/config.toml` entry (`install_codex_mcp`, `:282`). Treats `.mcp.json` as repo-controlled input: non-loopback `--filigree-url`/`--loomweave-url` pins are dropped, loopback pins preserved unless Filigree server mode supplies a scoped target (`_desired_sibling_url`, `:111`). +- `install/skill.py` — copies the bundled `wardline-gate` skill into `.claude/skills` and `.agents/skills` (`install_skill`, `skill.py:17`), refusing to overwrite a symlink. +- `install/detect.py` — detection-only sibling discovery (`detect_siblings`, `detect.py:128`); explicitly never persists config (the shared `weft.toml` is operator-owned; live URLs resolve on demand from `.weft//ephemeral.port`). +- `install/pre_commit.py` — appends a `wardline-scan` hook block to `.pre-commit-config.yaml` (`install_pre_commit_hook`, `pre_commit.py:9`). +- `install/pack.py` — `activate_pack` (`pack.py:7`) returns operator *guidance text only*; it never writes `weft.toml` and never imports/executes the pack (packs run code, so activation stays operator-authored + `--trust-pack`-gated in core). +- `install/doctor.py` — the install-health/repair engine (~947 lines): `machine_readable_doctor` (`doctor.py:840`), `repair_install` (`:928`), `check_install` (`:907`), plus per-check helpers (config, mcp-registration, decorator-grammar, gitignore, stray-artifact sweep, stale-sibling-port probe, `.env` filigree-token repair). Shared by both the CLI and the MCP `doctor` tool. +- `cli/install.py` / `cli/doctor.py` — the `install` and `doctor` commands that orchestrate the `install/` package; `cli/doctor.py` re-assembles the human-readable report from `install.doctor` private helpers. + +**Public surface / entry points:** `wardline.cli.entrypoint:main` (the console script, `pyproject.toml:55`); `wardline.cli.main.cli` (the click group). The install package's reusable surface — `install.doctor.machine_readable_doctor` / `repair_install`, `install.block.inject_block`, `install.mcp_json.merge_mcp_entry`, `install.skill.install_skill` — is consumed by the S10 MCP server as well as by these CLI commands. + +**Dependencies (graph-derived):** +- Inbound: Essentially none from inside the repo — S11 sits at the top of the call stack. `entity_callers_list` on `cli.entrypoint.main` returns `[]` with `traversal_complete:true` (OS-invoked console script). The one cross-subsystem inbound edge is **S10 MCP & LSP Server → `install.doctor.machine_readable_doctor`** (`mcp/server.py:3853`, graph-confirmed `_doctor` caller), so the MCP `doctor` tool reuses S11's install-health engine; a pinning test (`tests/unit/mcp/test_server_doctor.test_doctor_matches_cli_machine_readable_envelope`) holds the two envelopes equal. The `install/` helpers' other resolved callers (`cli.install.install`, `install.doctor.repair_install`) are intra-S11. +- Outbound (S11 is outbound to almost everything — it is the orchestration crust): + - **S4 Core Orchestration & Config** (dominant): `core.run.run_scan`/`gate_decision`/`baseline_migration_hint` (`scan.py:31`), `core.config.load`/`resolve_filigree_url`/`resolve_loomweave_url` (`scan.py:13-22`), `core.scan_jobs` (`scan_job.py:12`), `core.scan_file_workflow` (`scan_file_findings.py:14`), `core.safe_paths`/`paths`/`errors`/`descriptor`/`artifacts`, and (in `install/`) `core.config`/`paths`/`safe_paths`/`artifacts`. (All of `scan.scan`'s dominant callees graph-confirmed via `entity_neighborhood_get`: `run_scan`, `gate_decision`, `baseline_migration_hint`, `config.load`, `resolve_filigree_url`/`resolve_loomweave_url`, `write_scan_artifact`, `explicit_output_target`.) + - **S5 Findings, Outputs & Emit**: `core.emit.JsonlSink`, `core.sarif`, `core.agent_summary`, `core.filigree_emit`, `core.federation_status` (`scan.py:20-33`); `core.explain.explain_taint_result` (`explain_taint.py:82`); `core.finding_query` (`findings.py:16`). + - **S6 Gate Discipline & Remediation**: `core.baseline.collect_and_write_baseline` (`main.py:29`) / `inspect_baseline_store` (`doctor.py:20`), `core.autofix.run_autofix` (`fix.py:10`, `scan.py:309`; graph-confirmed callee of `scan.scan`), `core.delta_scope` (the `--affected` delta scan; graph-confirmed callee `parse_affected_scope_text`). + - **S7 Trust Evidence & Judge**: `core.assure` (`assure.py:37`), `core.attest`/`attest_key` (`attest.py`), `weft_dossier` (`dossier.py:57`), `core.judge_run.run_judge`/`core.judge` (`judge.py:18-24`), `weft_decorator_coverage` (`decorator_coverage.py:39`). + - **S8 Identity & SEI**: `core.rekey` (`rekey.py:21`), `core.sei_resolution.resolve_query_filters` (`findings.py:18`). + - **S9 Federation Clients**: `loomweave.client`/`config`/`write`, `filigree.config`, `core.legis`, `core.filigree_issue`, `core.http` (lazy-imported in `scan.py:406-444`, `file_finding.py`, `scan_file_findings.py:14-17`, `doctor.py:22-26`). + - **S10 MCP & LSP Server**: `mcp.server.WardlineMCPServer` (`mcp.py:10`), `lsp.LspServer` (`lsp.py:10`). + - **S1/S2** (narrow): `install.doctor._check_decorator_grammar` imports `scanner.grammar.BUILTIN_BOUNDARY_TYPES` (S1) and `core.registry.REGISTRY` (S2) to verify the decorator grammar is wired (`doctor.py:405-417`). + +**Patterns Observed:** +- **Thin-command / shared-core delegation.** Every command parses options, calls one core function, and renders output; the analysis/decision logic lives in S4–S9. The anti-drift discipline is explicit and load-bearing: `scan.py:601-602` notes the gate uses "the same [decision] the MCP scan tool serialises — so the surfaces cannot drift (A4)," and the `install.doctor.machine_readable_doctor` engine is literally shared between `cli.doctor` and the MCP `doctor` tool (graph-confirmed) with a byte-equal pinning test. +- **Zero-dep base preserved via a guarded shim.** `cli`/`install` freely import `click` (and `pyyaml`/`jsonschema` transitively), which is legitimate because they live behind the `scanner` extra; `entrypoint.py` catches the missing-extra `ModuleNotFoundError` and emits an actionable message instead of a traceback (`entrypoint.py:13-20`). `pyproject.toml:13` confirms `dependencies = []`. +- **Activation, not configuration, in code.** The `install/` package is the product thesis made concrete: it *adds* instruction blocks, MCP entries, skills, and hooks but never edits the operator-owned `weft.toml` (`pack.py` returns guidance text; `detect.py` persists nothing). Power is opt-in via what gets installed, not via config the operator must hand-author. +- **Hardened file mutation on untrusted trees.** Install writes go through `safe_project_file`/`safe_project_path` confinement; `block.py` writes atomically with a refuse-to-empty guard and a foreign-namespace-safe managed-block contract; `doctor._rewrite_env_token` refuses a symlinked `.env` and opens with `O_NOFOLLOW` (`doctor.py:486-505`); `doctor._is_loopback` uses strict IP parsing so a bearer token is never probed against `127.attacker.com` (`doctor.py:615-628`). This is a clear strength, not a gap. +- **Fail-soft federation, loud local gate.** Sibling emission (Filigree POST, Loomweave write) is best-effort and never changes the scan exit code (`scan.py:424-446`), while a hard `WardlineError` maps to `SystemExit(2)` and a tripped gate to `SystemExit(1)` — consistently across commands. + +**Concerns:** +- **CLI↔MCP config-resolution asymmetry (latent divergence).** `cli/mcp.py:46-50` passes `None` as the reserved `config_path` to `resolve_*_url_with_source`, whereas `cli/scan.py` threads `weft_config_path(root)`. Documented as harmless until the hub sibling-endpoint key lands (`weft-a2f4cf95c7`), but it is a real surface asymmetry that will silently diverge once that key is read; it should be threaded for parity when the hook arrives. +- **`cli/doctor.py` reaches into `install.doctor` private API.** It imports seven underscore-prefixed helpers (`_check_config`, `_check_filigree_auth`, `_check_gitignore`, `_check_stale_sibling_ports`, `_resolve_probe_target`, `_sweep_stray_artifacts`; `cli/doctor.py:12-22`) to build the human report, while the MCP path goes through the single public `machine_readable_doctor`. The human and machine renderings can drift because they assemble the checks independently; promoting a public check-list surface would close that. +- **`install/doctor.py` is a 947-line multi-concern module / coupling hotspot.** It mixes install-wiring health, federation auth probing, artifact-hygiene sweeping, and stale-port probing, and reaches into other modules' private API (`core.artifacts._managed_artifact_pattern`/`_is_regular_file_no_follow`, `paths._has_project_markers`, `core.config._filigree_published_url`; `doctor.py:138,163,558`). High internal cohesion around "doctor," but the private cross-module reach is brittle to refactors in S4/S5. +- **`install/pre_commit.py` is the weakest-engineered install writer.** It detects existing hooks by substring (`"id: wardline-scan" in content`, `pre_commit.py:19`), edits YAML by string concatenation rather than a parser (contrast the careful `block.py`/`mcp_json.py`), and swallows `except Exception` into status strings (`:16`, `:42`). A `.pre-commit-config.yaml` whose structure differs from the assumed shape can get a malformed hook appended. +- **Minor duplication / pattern inconsistency.** The `--fix` confirm callback is duplicated between `scan.py:315-321` and `fix.py:57-63`; and `vocab`/`baseline` are defined inline in `main.py` rather than as their own `cli/.py` modules, breaking the otherwise-uniform one-module-per-command convention. (`cli.install.install:82` `importlib.import_module(pack)` is a benign presence check only — it does not execute the pack.) + +**Confidence:** High — Read all 30 assigned files in full (the larger `scan.py` and `install/doctor.py` end-to-end). Dependency edges corroborated against the Loomweave graph: `entity_callers_list` confirmed `cli.entrypoint.main` has zero in-repo callers, `inject_block`/`merge_mcp_entry`/`install_skill` are called only by `cli.install.install` + `install.doctor.repair_install`, and `machine_readable_doctor` is called by both `cli.doctor.doctor` (S11) and `mcp.server._doctor` (S10) with a pinning test. Zero-dep base + `scanner` extra + console-script verified in `pyproject.toml:13,24,55`. The dominant outbound edges are graph-derived, not just read: `entity_neighborhood_get` on `cli.scan.scan` returned its full resolved callee set (`run_scan`, `gate_decision`, `baseline_migration_hint`, `config.load`/`resolve_*_url`, `write_scan_artifact`, `run_autofix`, `parse_affected_scope_text`, `build_agent_summary`, `FiligreeEmitter.emit`, `build_sarif`, `JsonlSink.write`, `build_legis_artifact`, the loomweave write trio) with `traversal_complete:true`, and confirmed its only in-repo reference-in is the `cli.main` `add_command` wiring. The remaining commands' S-label mapping is from read import+call sites cross-checked against the spec's subsystem table (not each individually graph-probed), but each was read in full. diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-S12.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S12.md new file mode 100644 index 00000000..d3b03d2f --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S12.md @@ -0,0 +1,58 @@ +## S12 — Rust Frontend + +**Location:** `src/wardline/rust/` (15 modules: `__init__.py`, `analyzer.py`, `context.py`, `crate_roots.py`, `dataflow.py`, `edges.py`, `index.py`, `mounts.py`, `nodeid.py`, `parse.py`, `provider.py`, `qualname.py`, `rules.py`, `_tree_sitter.py`, `vocabulary.py` + bundled `rust_taint.yaml`) — entirely behind the opt-in `wardline[rust]` extra. + +**Responsibility:** A preview, opt-in tree-sitter Rust frontend that implements the engine `Analyzer` protocol and contributes a narrow Tier-A command-injection taint slice (rules RS-WL-108/112) over `std::process::Command`, emitting crate-prefixed, baseline-eligible `Finding`s into the shared pipeline via `wardline scan --lang rust`. + +**Key Components:** +- `analyzer.py` — `RustAnalyzer` (`analyzer.py:59`): the engine `Analyzer` impl. `analyze(files, config, *, root)` (`analyzer.py:84`) discovers Cargo crate roots once, builds `#[path]` mount overlays, routes each `.rs` file to its crate-prefixed module, and runs the per-file pipeline; `_analyze_tree` (`analyzer.py:157`) parses once, mints one `NodeIdMap`, and threads it through index → provider (trust-seed) → dataflow → rules (spec §5: a re-parse would mint divergent NodeIds and fail quietly). +- `_tree_sitter.py` — `require_rust()` (`_tree_sitter.py:23`): the sole lazy import of `tree_sitter`/`tree_sitter_rust`; raises `RustToolingError` with an install hint if the extra is absent (mirrors `loomweave.require_blake3`). +- `parse.py` — `parse_rust`/`has_errors` (`parse.py:21`/`parse.py:28`): thin parse seam; `has_errors` detects tree-sitter error-recovery so a half-parsed file is never reported clean. +- `nodeid.py` — `NodeIdMap` + `mint_node_ids` (`nodeid.py:31`/`nodeid.py:72`): the single pre-order NodeId keying authority; the map **pins its source `Tree`** so a cross-tree lookup is a loud `KeyError`, not a silent false hit (`nodeid.py:31-44`). +- `index.py` — `index_entities` (`index.py:115`): the full ten-kind ADR-049 entity surface (module/struct/fn/enum/trait/type_alias/const/static/macro/impl + semantic `method`), including the impl residual-collision ladder (stage @cfg → S → T → method-@cfg). +- `qualname.py` — the ADR-049 qualname dialect (`qualname.py:1-41`): Wardline is the *second producer*, minting byte-identical loomweave locators (pinned by the vendored corpus `tests/conformance/qualnames_rust.json`). +- `crate_roots.py` — `discover_crate_roots` (`crate_roots.py:81`): `Cargo.toml [package].name` crate discovery via stdlib `tomllib`, symlink-safe, longest-prefix file→crate mapping; mirrors the loomweave oracle. +- `mounts.py` — `build_mount_overlay`/`MountOverlay.logical_module_path` (`mounts.py:152`/`mounts.py:110`): ADR-049 Amendment 8 `#[path]` mount routing with a filesystem fallback. +- `vocabulary.py` — `load_rust_taint` (`vocabulary.py:145`): the frozen, validated sources/sinks table keyed by `(crate, path)`; `RUST_TAINT_VERSION = 3` (`vocabulary.py:31`) folds into the provider fingerprint. Slice-1 sink set is just `{"command"}` (`vocabulary.py:29`). +- `provider.py` — `RustTrustProvider` (`provider.py:47`): the Rust analog of the Python trust decorators — `/// @trusted(level=ASSURED|GUARDED)` outer-doc-comment markers, anchored with `re.match` (`provider.py:39`) so only a leading directive seeds trust; an unmarked fn yields `None` → fail-closed default. +- `dataflow.py` — `analyze_command_dataflow` (`dataflow.py:72`): the genuinely-new builder-dataflow that reconstructs each `Command` invocation (stepwise and fluent chains) into a `CommandTrigger` (`dataflow.py:50`) per terminal `.output()`/`.spawn()`/`.status()`. +- `rules.py` — `RustProgramInjectionRule` RS-WL-108 (`rules.py:34`, base ERROR) + `RustShellInjectionRule` RS-WL-112 (`rules.py:52`, base WARN): the verdict layer; both modulate by the containing fn's declared trust tier and de-conflict (a tainted program is 108's; 112 stays silent — `rules.py:63`). +- `context.py` — `RustAnalysisContext`/`RustTriggerContext`/`RustRule` (`context.py:45`/`context.py:27`/`context.py:58`): a *separate* rule protocol that never plugs into the Python `RuleRegistry`. +- `edges.py` — `discover_rust_edges`/`index_rust_file` (`edges.py:107`/`edges.py:98`): anchored `imports`/`implements` edge discovery (resolved-or-dropped). NOT in the scan/runtime surface — identity-corpus only (`edges.py:38-44`). + +**Public surface / entry points:** +- `RustAnalyzer` (`analyzer.py:59`) is constructed by `RustFrontend.build_analyzer` (`core/frontends.py:99-107`) and registered as `FRONTENDS["rust"]` (`core/frontends.py:127`); both `config` and `summary_cache` are ignored (`RustAnalyzer()` is zero-config). +- `RustAnalyzer.analyze(files, config, *, root) -> Sequence[Finding]` (`analyzer.py:84`) is the engine protocol method `run_scan` drives. +- `RustAnalyzer.analyze_source` (`analyzer.py:150`) — single in-memory string entry for rule tests; `discover_rust_entities`/`index_rust_file`/`discover_rust_edges` are the identity-corpus capture entries. + +**Dependencies (graph-derived):** +- Inbound (who calls in): + - **S4 Core Orchestration & Config** — `RustFrontend.build_analyzer` → `RustAnalyzer()` (`core/frontends.py:107`; Loomweave `entity_callers_list` confirms this dynamic call site), and `run_scan` → `analyzer.analyze(...)` (`core/run.py:381`; frontend selected by `FRONTENDS[lang]` at `core/run.py:285-332`). Loomweave callers are otherwise all under `tests/`. + - **S11 CLI & Install/Activation** — reaches the frontend transitively through `run_scan` via `wardline scan --lang rust` (per `pyproject.toml:32`); no direct edge into `rust/`. +- Outbound (what this calls, by S-label) — all confirmed via Loomweave `entity_neighborhood_get` `imports_out`/`references_out` on the `analyzer`/`rules`/`dataflow`/`provider` modules: + - **S5 Findings, Outputs & Emit** — `Finding`/`Kind`/`Location`/`Severity`/`ENGINE_PATH` from `wardline.core.finding` (`analyzer.py:26`, `rules.py:16`; graph: `analyzer`/`rules` → `core.finding`). + - **S8 Identity & SEI** — `NodeId` from `wardline.core.node_id` (`nodeid.py:23`, `dataflow.py:23`; graph: `dataflow` → `core.node_id`) and `compute_finding_fingerprint` from `wardline.core.finding` (`rules.py:16`, `rules.py:158`). + - **S3 Taint Engine** — `FunctionTaint` from `wardline.scanner.taint.provider` (`provider.py:22`; graph: `provider` → `scanner.taint.provider`); the taint lattice `TaintState`/`RAW_ZONE`/`least_trusted` from `wardline.core.taints` (`analyzer.py:27`, `dataflow.py:24`, `rules.py:17`; graph: all four modules → `core.taints`) — the shared taint-state vocabulary (physically in `core/`, conceptually the taint engine's lattice). + - **S2 Rule Lattice** — `modulate` from `wardline.scanner.rules.severity_model` (`rules.py:18`; graph: `rules` → `scanner.rules.severity_model`). + - **S1 Scanner Engine** — `AnalysisContext` from `wardline.scanner.context` (`analyzer.py:46`, `TYPE_CHECKING`-only; graph: `analyzer` `imports_out`/`references_out` → `scanner.context.AnalysisContext` via the `last_context` annotation). A type-only edge — the property always returns `None` (`analyzer.py:67-76`). + - **S4 Core Orchestration & Config** — `WardlineConfig` (`analyzer.py:45`; graph: `analyzer` → `core.config`), `RustToolingError` from `wardline.core.errors` (`_tree_sitter.py:15`), `require_yaml` from `wardline.core.optional_deps` (`vocabulary.py:19`), and the `LanguageFrontend`/`FRONTENDS` registry it plugs into (`core/frontends.py`). + +**Patterns Observed:** +- **Lazy-extra isolation (zero-base-dep preservation):** every module keeps `tree_sitter` under `TYPE_CHECKING`; only `require_rust()` imports it at call time (`_tree_sitter.py:23`, `parse.py:5-7`, `nodeid.py:15-17`). `core/frontends.py` imports `rust.analyzer` only inside `build_analyzer` (`core/frontends.py:105`). Base and `scanner` extras never pull `rust/`. +- **Single-parse / single keying authority:** one parse → one `NodeIdMap` threaded through index/provider/dataflow/rules (`analyzer.py:157-216`); `NodeIdMap` pins its tree to make node-id reuse hazards loud (`nodeid.py:31-44`). +- **Fail-closed identity, anti-false-green metric:** an unmarked fn fails closed to `UNKNOWN_RAW` and suppresses findings (`analyzer.py:50`, `analyzer.py:183`), while dataflow is default-clean (taint flows only from proven vocabulary sources/tainted locals — `dataflow.py:201-221`). The `WLN-RUST-COVERAGE` metric (`analyzer.py:357`) exposes declared/total so a vacuously-green un-annotated scan is distinguishable from analyzed-and-safe. Parse/analysis failure surfaces gate-eligible `WLN-ENGINE-PARSE-ERROR`/`WLN-ENGINE-FILE-FAILED` defects, never a silent skip (`analyzer.py:121-133`). +- **Move-stable fingerprints (wlfp2):** trigger positions fold into the fingerprint entity-relative (line delta + NodeId delta against the containing fn) so an edit above the entity does not rekey (`rules.py:123-145`, `context.py:27-43`); finding identity routes through the shared `compute_finding_fingerprint`, keeping Rust findings baseline-eligible like Python's. +- **Conformance second-producer:** qualname/crate-root/mount routing mints byte-identical loomweave (ADR-049) locators; out-of-scope files get `#out`-branded routes that carry no cross-tool conformance claim and structurally cannot collide with a loomweave locator (`analyzer.py:269-328`, `crate_roots.py:22-34`). +- **Crate-consistent vocabulary matching:** sources/sinks keyed on the crate-qualified full path with a two-segment-floor suffix match, so a foreign crate's like-named `Command::new` cannot match the std entry (`dataflow.py:293-306`). + +**Concerns:** +- **Known FN gap — mid-chain `?`/`.await`:** `_unwrap_to_call`/`_unwind` only peel try/await/return wrappers at the outer statement position; an interior `Command::new(p).arg(x)?.output()` is dropped, so no RS-WL-108 fires (`dataflow.py:254-262`, `dataflow.py:265-281`). Tracked open as `wardline-535c9531cc`. A false-negative, not a wrong gate verdict. +- **Known marker-grammar gaps (all fail-closed):** the provider walk guard is only `("line_comment", "attribute_item")` (`provider.py:68`), so block doc comments `/** @trusted */`, a `/** */` interposed before the fn, and inner `//!` markers never seed trust (`provider.py:64-83`). All suppress (never a false positive). Tracked open as `wardline-9c3a76b257`. NOTE: sub-item #4 of that issue ("typo'd marker silently suppressed, no diagnostic") appears since-addressed — `analyzer.py` now emits a gate-eligible `WLN-ENGINE-RUST-INVALID-TRUST-MARKER` ERROR on a rejected level (`analyzer.py:175-181`, `analyzer.py:342-354`); the issue text is stale on that one sub-item. +- **`config` accepted but unused:** `weft.toml` severity overrides do not reach the Rust rules — base severities are hardcoded (`analyzer.py:88-91`; `RustFrontend.build_analyzer` ignores both params, `core/frontends.py:99-107`). A documented preview surface gap. +- **`last_context` is always `None`** (`analyzer.py:67-76`): the Rust-native context is not an engine `AnalysisContext`, so delta-scope and SARIF code-flow consumers degrade to file-level scoping for Rust. Deliberate slice-1 limitation, but it means delta/SARIF richness is unavailable on this frontend. +- **`edges.py` is unwired:** `imports`/`implements` discovery is built and tested via the identity corpus only — not emitted by the analyzer/scan surface or over the Weft wire (`edges.py:38-44`). It carries maintenance weight ahead of any runtime consumer. +- **ABI/loader fragility:** `tree-sitter-rust==0.24.2` is ABI 15, loadable only by `tree-sitter` core `>=0.25`; the grammar self-declares a stale `tree-sitter~=0.22` that must NOT be trusted, and the `<0.26` upper cap is conservative/unverified (`pyproject.toml:27-35`). A core/grammar version skew breaks `Language(...)` loading; the pin is narrow by necessity. +- **Single-block dataflow:** intra-function, single-block only; nested control flow and sanitizers are invisible (an accepted bounded FP) (`dataflow.py:9-16`). Scope-appropriate for the preview, but a soundness ceiling worth stating. +- Checked and clean: no third-party import on the base/scanner path (all tree-sitter imports lazy/`TYPE_CHECKING`); `from __future__ import annotations` universal; per-file isolation present (`analyzer.py:127-133`); parse-failure and invalid-marker error handling present. + +**Confidence:** High — read all 15 `rust/` modules (full reads of analyzer/dataflow/rules/provider/index/edges/mounts/crate_roots/vocabulary/nodeid/parse/context/_tree_sitter/`__init__`; `qualname.py` header + `__all__`), plus `core/frontends.py`, the `run_scan` driver site (`core/run.py:381`, selection `:285-332`), and the `pyproject.toml` rust-extra/ABI block. Dependencies are graph-derived: inbound via `entity_callers_list` (`frontends.py:107` + `run.py:381` call sites) and outbound via `entity_neighborhood_get` `imports_out`/`references_out` on the analyzer/rules/dataflow/provider modules (which surfaced the S1 `AnalysisContext` type-edge that imports alone underweighted), each cross-checked against a real import at `file:line`. Both FN-gap issues verified open in Filigree. Residual inference: the S1/S2/S3/S8 label mapping for `scanner.context`/`severity_model`/`core.taints`/`core.node_id` (symbols physically in `core/` or `scanner/` but conceptually owned by those subsystems) is my judgment and is flagged inline. diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-S2.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S2.md new file mode 100644 index 00000000..926c45e5 --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S2.md @@ -0,0 +1,81 @@ +## S2 — Rule Lattice + +**Location:** `src/wardline/scanner/rules/` (26 PY-WL rules + shared infra), `src/wardline/decorators/` (the 3 runtime trust markers) + +**Responsibility:** Define the trust-vocabulary defect rules — each a self-contained `check(context) -> list[Finding]` over the engine's `AnalysisContext` — plus the rule descriptor/severity model, the shared AST/sink helpers they run on, and the three static-analysis decorators (`@external_boundary` / `@trust_boundary` / `@trusted`) whose declarations the rules police. + +**Key Components:** +- `scanner/rules/__init__.py` — registry factory + the canonical rule ordering. `_ALL_RULE_CLASSES`/`BUILTIN_RULE_CLASSES` (`__init__.py:52-86`, hand-ordered tuple; "registration order = emission order"); `build_default_registry(config, rules=None)` (`__init__.py:235`) honours `rules.enable` (fnmatch include) and `rules.severity` (per-rule base override); malformed config is surfaced as the engine self-diagnostic `WLN-ENGINE-POLICY-CONFIG` via `_PolicyConfigRule` (`__init__.py:88-135`). +- `scanner/rules/metadata.py` — `RuleMetadata` frozen descriptor (`metadata.py:15`): `rule_id`, `base_severity`, `kind`, `description`, examples, `maturity`, and the load-bearing `multi_emit` flag (`metadata.py:23-32`) that gates which fingerprint discriminator a rule may use. +- `scanner/rules/severity_model.py` — `modulate(base, taint)` (`severity_model.py:47`): the ~10-line tier-modulation matrix — trusted tiers keep base, partial tiers downgrade one step, freedom/fail-closed zone → `NONE`. The freedom-zone suppression is what keeps undecorated code (`UNKNOWN_RAW`) silent and the project self-host clean. +- `scanner/rules/_ast_helpers.py` — the boundary-integrity predicate library (~648 lines): own-scope reachability mini-CFG (`_reachable_statements_in_block`, `_stmt_always_terminates`), rejection-path detection (`has_rejection_path`/`has_real_rejection`/`asserts_are_sole_rejection`), one-hop same-module helper resolution (`rejecting_helper_calls`), fail-open detection (`handler_substitutes_on_failure`), and broad/silent-handler predicates. Encodes the 4-way boundary partition invariant (`_ast_helpers.py:382-392`). +- `scanner/rules/_sink_helpers.py` — the dangerous-sink machinery + `TaintedSinkRule` base (`_sink_helpers.py:735`, Loomweave-tagged `exported-api`). Name/alias canonicalization (`canonical_call_name`), binding-aware call resolution (`collect_sink_bindings`/`resolved_sink_calls`), the single fail-closed arg-taint resolver (`resolved_arg_taints`, `:483`), `ArgSpec` slot precision, and `build_sink_finding` (`:680`). `TaintedSinkRule.check` (`:843`) is the one loop the whole sink family runs. +- `scanner/rules/_fingerprint.py` — `entity_source_fingerprint` (`_fingerprint.py:49`): a position-free canonical-AST sha256, made byte-identical across CPython 3.12/3.13 (`_canonical_ast_dump`), used as the singleton-rule `taint_path` discriminator. +- `decorators/_base.py` + `decorators/trust.py` + `decorators/__init__.py` — `apply_marker` (`_base.py:39`) validates a marker against `core.registry.REGISTRY`, stamps `_wardline_*` attrs, and returns the function UNCHANGED (no runtime wrapper); `trust.py` exposes `external_boundary` / `trust_boundary(to_level=)` / `trusted(level=)`. +- The 26 rule modules (one class each, `rule_id`+`metadata`+`check`). 15 STABLE, 11 PREVIEW (`maturity=Maturity.PREVIEW`): + - `untrusted_reaches_trusted.py` — **PY-WL-101** (ERROR): an anchored `@trusted` producer whose actual return is less-trusted than declared. + - `boundary_without_rejection.py` — **PY-WL-102** (ERROR): a `@trust_boundary` with no rejection path of any shape. + - `broad_exception.py` — **PY-WL-103** (WARN, tier-mod): bare/`Exception`/`BaseException` handler in a trusted-tier fn. + - `silent_exception.py` — **PY-WL-104** (WARN, tier-mod): exception swallowed (`pass`/`...`) with no handling. + - `untrusted_to_trusted_callee.py` — **PY-WL-105** (ERROR): provably-untrusted data passed to a trusted callee at a call site. + - `untrusted_to_deserialization.py` — **PY-WL-106** (WARN, sink): untrusted bytes → pickle/yaml/marshal/dill/torch/numpy deserialization (CWE-502). + - `untrusted_to_exec.py` — **PY-WL-107** (WARN, sink): untrusted → `eval`/`exec`/`compile`. + - `untrusted_to_command.py` — **PY-WL-108** (ERROR, sink): untrusted → `os.system`/`subprocess` program-exec. + - `none_leak.py` — **PY-WL-109** (WARN): `None` leaks out of a trusted producer. + - `contradictory_trust.py` — **PY-WL-110** (WARN): ≥2 distinct trust markers on one entity (silently resolved clash). + - `assert_only_boundary.py` — **PY-WL-111** (ERROR): boundary whose only rejection is `assert` (stripped under `-O`, CWE-617). + - `untrusted_to_shell_subprocess.py` — **PY-WL-112** (ERROR, sink): untrusted → `shell=True` subprocess. + - `failopen_boundary.py` — **PY-WL-113** (ERROR): a real rejection defeated by a fail-open handler (CWE-636/703). + - `invalid_decorator_level.py` — **PY-WL-114** (ERROR): statically-readable but invalid/out-of-range decorator level (e.g. typo `'ASURED'` silently disables the gate). + - `untrusted_to_import.py` — **PY-WL-115** (WARN, sink): untrusted → dynamic `import`/`__import__`/module-load. + - `path_traversal.py` — **PY-WL-116** (WARN, sink, PREVIEW): untrusted → filesystem-path sink. + - `ssrf.py` — **PY-WL-117** (WARN, sink, PREVIEW): untrusted → HTTP-client URL (SSRF). + - `sql_injection.py` — **PY-WL-118** (ERROR, sink, PREVIEW): untrusted → SQL/DB execute. + - `degenerate_boundary.py` — **PY-WL-119** (ERROR, PREVIEW): no-op `return ` validator. + - `stored_taint.py` — **PY-WL-120** (ERROR, PREVIEW): stored/persisted taint reaches trusted state un-validated (suppress-and-delegate to 101). + - `untrusted_to_xml.py` — **PY-WL-121** (ERROR, sink, PREVIEW): untrusted → XML parse (XXE, CWE-611). + - `untrusted_to_template.py` — **PY-WL-122** (ERROR, sink, PREVIEW): untrusted → server-side template compile (SSTI, CWE-1336). + - `untrusted_to_reflection.py` — **PY-WL-123** (WARN, sink, PREVIEW): tainted attribute NAME → `setattr`/`getattr` (CWE-915). + - `untrusted_to_native.py` — **PY-WL-124** (ERROR, sink, PREVIEW): untrusted path → native-library load (CWE-114). + - `untrusted_to_log.py` — **PY-WL-125** (INFO, sink, PREVIEW): untrusted as log format string (CWE-117). + - `untrusted_to_mail.py` — **PY-WL-126** (WARN, sink, PREVIEW): untrusted recipient/message → `SMTP.sendmail` (CWE-93). + +**Public surface / entry points:** +- `build_default_registry(config, rules=None) -> RuleRegistry` (`scanner/rules/__init__.py:235`) — THE registry factory. +- `BUILTIN_RULE_CLASSES` (`scanner/rules/__init__.py:86`) — single source of truth shared with the grammar. +- `RuleMetadata` (`scanner/rules/metadata.py:15`) and `modulate` (`scanner/rules/severity_model.py:47`). +- `TaintedSinkRule` (`scanner/rules/_sink_helpers.py:735`) — sink-rule base (template method). +- `external_boundary` / `trust_boundary` / `trusted` (`decorators/__init__.py:6`) — the trust vocabulary applied to user code. +- Each rule class satisfies the duck-typed `Rule` protocol (rule_id, metadata, `check`); they are registered, not called directly. + +**Dependencies (graph-derived):** +- Inbound (who calls into this): + - **S1 Scanner Engine** → `build_default_registry` — `WardlineAnalyzer._analyze_inner` imports it (`scanner/analyzer.py:31`) and calls it (`scanner/analyzer.py:960`); verified via `entity_callers_list` (one resolved caller: `_analyze_inner`). + - **S1 Scanner Engine** → `BUILTIN_RULE_CLASSES` — `default_grammar()` imports and wraps it into the `TrustGrammar` (`scanner/grammar.py:228-230`). + - **S3 Taint Engine** consumes the decorators (not a call edge): `trusted`/`trust_boundary`/`external_boundary` have ZERO project callers (`entity_callers_list` → empty) because they are AST markers. The taint `decorator_provider` reads the decorator SYNTAX from `entity.node.decorator_list` and matches by resolved FQN (`_is_builtin_decorator_fqn`) — the static analyzer parses source, so it never sees the runtime `_wardline_*` stamps, which exist only on a live function object (`decorators/_base.py:59-62`). +- Outbound (what this calls/imports): + - **S1 Scanner Engine** — `scanner.context` (`AnalysisContext`, `RuleRegistry`, `_RuleClass` protocol), `scanner.grammar.BUILTIN_BOUNDARY_TYPES`, `scanner.index.Entity`, `scanner.ast_primitives.resolve_call_fqn`. + - **S3 Taint Engine** — `core.taints` (the `TaintState` lattice, `RAW_ZONE`, `TRUST_RANK`); `scanner.taint.decorator_provider._is_builtin_decorator_fqn` + `_shadowed_builtin_roots` (used by PY-WL-110/114 — `contradictory_trust.py:30`, `invalid_decorator_level.py:20`). + - **S5 Findings** — `core.finding.{Finding, Kind, Severity, Location, Maturity, ENGINE_PATH}` (the finding model every rule constructs). + - **S8 Identity & SEI** — `core.finding.compute_finding_fingerprint` (every finding's identity). S2 owns its own `_fingerprint.entity_source_fingerprint`, which only feeds the `taint_path` argument to that S8 call. + - **S4 Core Orchestration & Config** — `core.registry.REGISTRY` (decorator validation, `decorators/_base.py:16`), `core.protocols.Rule`, and `WardlineConfig` (`rules_enable`/`rules_severity`). + +**Patterns Observed:** +- **Duck-typed, base-less rule contract.** Every rule is a plain class with `rule_id`, `metadata`, `__init__(self, base_severity=None)`, and `check(context) -> list[Finding]`; it structurally satisfies the `Rule` Protocol (S4) — no ABC, no inheritance except the sink family. Confirmed across all 26 files (e.g. `untrusted_reaches_trusted.py:79-86`). +- **Explicit central registration, not auto-discovery.** `_ALL_RULE_CLASSES` is a hand-ordered tuple (`__init__.py:52-80`) whose order is the deterministic emission order; `BUILTIN_RULE_CLASSES` is the single alias the grammar reuses so the two construction paths cannot drift (`__init__.py:83-86`). Config-malformation fails LOUD to a `WLN-ENGINE-POLICY-CONFIG` ERROR finding rather than silently mis-enabling rules. +- **Two gating regimes.** Declaration-gated rules (101/102/105/110/111/113/114/119) emit at base severity — the decorator IS the opt-in. Tier-modulated rules (103/104 + whole sink family) scale base by the resolved taint tier via `modulate` and go silent in the developer-freedom zone (`UNKNOWN_RAW → NONE`), so undecorated code stays quiet (`severity_model.py:47-53`, `broad_exception.py:49-52`, `_sink_helpers.py:849`). +- **Decorators are inert markers, read from the AST.** `apply_marker` stamps `_wardline_*` and returns the function unchanged — no wrapper, no runtime enforcement (`decorators/_base.py:1-10, 39-63`). This is the deliberate lightweight departure from wardline.old's runtime-enforcing factory. +- **Fingerprint-discriminator discipline (wlfp2).** `multi_emit` rules must discriminate co-located findings by entity-relative span/ordinal (call-line − def-line + col span, or PY-WL-114's decorator ordinal) since `line_start` left the hash; singletons use the position-free interpreter-stable `entity_source_fingerprint` so a comment move is stable but a body change is not (`metadata.py:23-32`, `build_sink_finding` `_sink_helpers.py:680-732`, `invalid_decorator_level.py:189-205`). +- **Template-method sink base.** `TaintedSinkRule.check` is the single check loop; subclasses set `SINKS`/`SINK_SPECS`/`SINK_SEVERITIES` and override `_accept_call`/`_arg_guarded`/`_taint_anchor_call`; `__init_subclass__` fails at import if required attrs are missing (`_sink_helpers.py:780-784`). Consolidated 2026-06-10 from two former mixins. +- **Fail-closed taint resolution in one place.** `resolved_arg_taints` is the sole arg resolver; a missing L2 snapshot yields a pessimistic `UNKNOWN_RAW` per arg and records the degradation as a per-scan FACT finding (not a `UserWarning`, so a warnings-as-error embedder can't abort a rule) — `_sink_helpers.py:483-520`. + +**Concerns:** +- **Rules import engine-internal PRIVATE symbols across the subsystem boundary.** PY-WL-110 and PY-WL-114 import `_is_builtin_decorator_fqn` and `_shadowed_builtin_roots` from S3's `scanner.taint.decorator_provider` (`contradictory_trust.py:30`, `invalid_decorator_level.py:20`). Deliberate (the rules must use the engine's exact seeding predicate so they "cannot drift"), but it couples the rule layer to underscore-prefixed engine internals — a refactor of the provider's private API silently breaks two security rules. +- **Sink rules write into the "read-only" `AnalysisContext`.** `resolved_arg_taints` mutates `context.flow_insensitive_fallbacks` (`_sink_helpers.py:512`), the one field of the otherwise frozen/`MappingProxyType`-wrapped context left as a plain mutable `set` (`scanner/context.py:136`). Documented as a diagnostics side channel, but it punches a hole through the read-only contract every other field upholds, and it means a rule's `check()` has a side effect on shared engine state. +- **~648-line hand-rolled reachability/CFG in `_ast_helpers.py` is the soundness-critical surface.** The fail-open / no-rejection-path detection for the boundary-integrity family (102/111/113/119) lives entirely in intricate own-scope statement-walking with a documented 4-way partition invariant (`_ast_helpers.py:382-392`). A subtle bug in `_stmt_always_terminates`/`handler_substitutes_on_failure` directly produces FN/FP in security rules, and the logic duplicates control-flow reasoning rather than reusing the taint engine's. +- **Latent cross-subsystem invariant dependency (MIXED_RAW).** PY-WL-101 and `modulate` would DISAGREE on `MIXED_RAW` (101 fires; modulate suppresses) — currently inert only because the S3 engine guarantees `MIXED_RAW` is unreachable (`severity_model.py:21-36`, `untrusted_reaches_trusted.py:37-57`). The rule lattice's soundness here is hostage to an invariant maintained elsewhere; if S3's parser guards ever regress, the disagreement becomes live. +- **~42% of the lattice is PREVIEW.** 11 of 26 rules carry `Maturity.PREVIEW` (116-126 family + 119/120); `RuleRegistry.run` stamps that maturity onto every finding (`scanner/context.py:208-211`). Expected for the 2026-06-10 coverage-gap families, but nearly half the vocabulary is non-STABLE and the catalog should not treat sink coverage as settled. +- **Minor: reserved-but-unused parameters.** `module_prefix` on `collect_sink_bindings` (`_sink_helpers.py:249`) and `resolve_bound_call_fqn` (`:400`) are `# noqa: ARG001 — reserved` dead params for a local-class-constructor feature "not in v1" — mild YAGNI carried in the hot path. +- No base-package zero-dep violation observed (decorators import only `core.registry`/`core.taints`; rules use only stdlib `ast`/`hashlib`/`fnmatch`); `from __future__ import annotations` is universal. + +**Confidence:** High — Read in full: `rules/__init__.py`, `metadata.py`, `severity_model.py`, `_ast_helpers.py`, `_sink_helpers.py`, `_fingerprint.py`, all three `decorators/*`, the S1 `scanner/context.py` contract, and 6 representative rules across families (101/102 boundary, 103 exception, 106 sink, 110/114 decorator-policing); skimmed `rule_id`/`severity`/`maturity`/docstring for the remaining 20 rules. Cross-subsystem edges are graph-derived (`entity_callers_list` confirmed S1 `_analyze_inner` → `build_default_registry` and zero runtime callers of the decorators) and corroborated with `file:line` imports (`analyzer.py:31/960`, `grammar.py:228-230`). Lower-confidence point: `core.taints`/`core.finding` are physically in `core/` and split across S3/S5/S8 by responsibility per the spec's label table rather than by an owned-file boundary; the exact S-label split of those modules is the owning agents' call. diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-S3.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S3.md new file mode 100644 index 00000000..c98b5ef7 --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S3.md @@ -0,0 +1,56 @@ +## S3 — Taint Engine + +**Location:** `src/wardline/scanner/taint/` (14 modules, 5,658 LOC; `__init__.py`, `provider.py`, `decorator_provider.py`, `function_level.py`, `callgraph.py`, `module_summariser.py`, `summary.py`, `summary_cache.py`, `propagation.py`, `project_resolver.py`, `resolver_metadata.py`, `reverse_edge_index.py`, `call_taint_map.py`, `stdlib_taint.py`, `variable_level.py` + the bundled `stdlib_taint.yaml`) + +**Responsibility:** The analytical heart — seed each function's declared trust taint, build the inter-module call graph, run a monotone SCC fixed-point that refines function-summary taints across calls, and expose per-variable (L2) flow-sensitive taint so the rule lattice can fire sink findings. + +**Key Components:** +- `provider.py` — the pluggable taint-source seam: `TaintSourceProvider` Protocol (`taint_for` → `SeedResult`, `fingerprint` for cache-keying) plus the no-opinion `DefaultTaintSourceProvider` (`provider.py:77`, `provider.py:95`). Provider silence ⇒ fail-closed `UNKNOWN_RAW`. +- `decorator_provider.py` — the *live* provider: `DecoratorTaintSourceProvider` reads `@external_boundary`/`@trust_boundary`/`@trusted` off the AST, alias-resolved, mapping to a `FunctionTaint` (`decorator_provider.py:293`); builtins match only exact known exports and fail closed when their marker root is project-shadowed (`_is_builtin_decorator_fqn:113`, `_shadowed_builtin_roots:96`); `_grammar_digest`/`fingerprint_for_project` feed the cache key (`decorator_provider.py:275`, `:345`). +- `function_level.py` — L1 seeding: `seed_function_taints` maps each entity through the provider, else `UNKNOWN_RAW` (`function_level.py:44`); whole L1 precedence is `provider > UNKNOWN_RAW`. +- `callgraph.py` — call-edge extraction with a flow-sensitive reaching-definitions pass (`_candidate_receiver_classes:65`) for variable-typed dispatch (branch-union at joins, straight-line REPLACE, loop fixpoint); `build_call_edges` returns edges, resolved/unresolved counts, and candidate-callee sets (`callgraph.py:220`). +- `summary.py` — `FunctionSummary` (the cacheable per-function taint contract) + `compute_cache_key`, the content-addressed correctness gate (`summary.py:34`, `:56`). +- `summary_cache.py` — process-local cache keyed on the module `cache_key`, with optional HMAC-authenticated atomic disk persistence (`SummaryCache:99`); deserialise rejects the unreachable taint trio (`_parse_cache_taint:75`) so a tampered file cannot inject `MIXED_RAW`/`UNKNOWN_GUARDED`/`UNKNOWN_ASSURED`. +- `propagation.py` — the L3 kernel: iterative Tarjan SCC (`compute_sccs:689`) + per-SCC fixed point (`propagate_callgraph_taints:189`) using the weakest-link `combine` meet, an `8·|SCC|+8` convergence bound, monotonicity-violation diagnostics, and post-fixed-point assertions that anchored taints never change and module-default taints never upgrade. +- `project_resolver.py` — whole-project orchestration: assembles `ModuleInput`s, always recomputes edges/counts fresh, summarises (cache-hit reuse for clean modules), runs the kernel, and returns `ResolverResult` (`resolve_project_taints:142`). +- `module_summariser.py` — per-module `FunctionSummary` emission + the module-global taint channel helpers (`collect_module_global_raw_seeds:76`, `own_scope_global_names:143`). +- `call_taint_map.py` — per-file `{call-site-name → return-taint}` map folding L3 project returns + stdlib table + serialisation-sink alias closure (`build_call_taint_map:81`). +- `stdlib_taint.py` — YAML-backed curated `(pkg, fn) → return-taint` table, lazily loaded via the optional-deps gate, constrained to a legal return set (`load_stdlib_taint:116`). +- `variable_level.py` — L2: flow-sensitive per-variable taint walk over a function body (`compute_variable_taints:605`, `analyze_function_variables:372`); 2,481 LOC, the engine's largest module. +- `reverse_edge_index.py` / `resolver_metadata.py` — module-granular reverse-edge dirty-frontier (performance-only) + immutable return-shape carriers (`ResolverResult`, `ResolverRunMetadata`). + +**Public surface / entry points:** +- `resolve_project_taints(...)` → `ResolverResult` — `project_resolver.py:142` (whole-project L3). +- `seed_function_taints(...)` → `{qualname: FunctionSeed}` — `function_level.py:44` (L1). +- `build_call_taint_map(...)` → `{name: TaintState}` — `call_taint_map.py:81` (per-file L2 resolution map). +- `compute_variable_taints(...)` / `analyze_function_variables(...)` — `variable_level.py:605`, `:372` (L2 walk + result carrier). +- `compute_return_taint` / `compute_return_callee` — `variable_level.py:2310`, `:2340` (function output tier + contributing callee). +- `DecoratorTaintSourceProvider` (+ `vocabulary_star_exports`) — `decorator_provider.py:293`, `:44`. +- `SummaryCache` (+ `summary_cache_auth_secret_from_env`) — `summary_cache.py:99`, `:284`. +- `project_attribute_writes` / `attribute_write_recording` — `variable_level.py:326`, `:308` (attribute-write channel). + +**Dependencies (graph-derived):** +- **Inbound** (Loomweave `entity_callers_list` / `entity_neighborhood_get`, resolved edges, corroborated by reads): + - **S1 Scanner Engine** — `analyzer.WardlineAnalyzer._analyze_inner` calls `resolve_project_taints` and `build_call_taint_map`; `pipeline.run_parse_project_stage` calls `seed_function_taints`; `analyzer`/`flow_trace` import `variable_level`; `grammar.build_sanitiser_collision_findings` reaches into `variable_level._SERIALISATION_SINKS` (`grammar.py:196`). S1 is the sole driver of the L1→L3→L2 pipeline. + - **S2 Rule Lattice** — `rules.contradictory_trust` and `rules.invalid_decorator_level` import private decorator helpers from `decorator_provider` (`contradictory_trust.py:30`, `invalid_decorator_level.py:20`). +- **Outbound** (import lines read + `imports_out` graph): + - **S1 Scanner Engine** — `scanner.ast_primitives` (`resolve_call_fqn`, `iter_calls_in_function_body`, `resolve_self_method_fqn`: `callgraph.py:35`), `scanner.index.Entity` (`provider.py:24`, `function_level.py:20`, `callgraph.py:40`), `scanner.grammar` (`BUILTIN_BOUNDARY_TYPES`, `BoundaryType`: `decorator_provider.py:21`). + - **S4 Core Orchestration & Config** — `core.taints` (the taint lattice/algebra: `combine`, `TaintState`, `TRUST_RANK`, `RAW_ZONE`, `_PROVENANCE_CLASH`), `core.config.WardlineConfig` (`project_resolver.py:27`), `core.ruleset.ruleset_hash` (`project_resolver.py:28`), `core.registry` (`REGISTRY`, `REGISTRY_VERSION`: `decorator_provider.py:19`), `core.optional_deps.require_yaml` (`stdlib_taint.py:19`). + - **Internal only** (no other subsystem): the L1→L3→L2 modules wire to each other; no edge to S5–S12. + +**Patterns Observed:** +- **Staged, fail-closed dataflow.** Real composition is *not* bottom-up L1→L2→L3; it is L1 seeds (`function_level`) + call graph (`callgraph`) → **L3 SCC fixed point** (`project_resolver`/`propagation`) → per-file **call-taint-map** (`call_taint_map`, which folds in `ResolverResult.taint_map`) → **L2 walk** (`compute_variable_taints`) running *last* and *consuming* the refined L3 output. Every unknown defaults to `UNKNOWN_RAW`; an unresolved callee propagates the worst of seed+args (no laundering). +- **Weakest-link meet, deliberately not the join.** All aggregation (kernel callee sets, L2 expression/control-flow merges) uses the rank-meet `least_trusted`/`combine`, never `taint_join`, so two clean-but-different-family inputs do not spuriously clash to `MIXED_RAW` (a RAW_ZONE false positive) — `propagation.py:10-19`, `variable_level.py:10-17`. +- **Monotone fixed point with guardrails.** Bounded iteration (`_scc_convergence_bound:57`), strict monotonicity commit guard + `L3_MONOTONICITY_VIOLATION`/`L3_CONVERGENCE_BOUND`/`L3_LOW_RESOLUTION` diagnostics, and post-fixed-point assertions (anchored unchanged, module_default never upgraded) that fall back to seed-only provenance on violation (`propagation.py:506-548`). +- **Content-addressed cache soundness.** `cache_key` binds module path + source bytes + schema/resolver version + provider fingerprint + `scan_policy_hash`, each length-prefixed (`summary.py:56-98`); edges/counts are always recomputed fresh, so a warm run is byte-identical to cold — the key is the correctness gate and the reverse-edge dirty frontier is a pure performance over-approximation (`project_resolver.py:10-17`, `reverse_edge_index.py:14-18`). Disk cache is HMAC-authenticated so repository JSON cannot become analyzer truth (`summary_cache.py:13-23`). +- **Immutability + ambient-config discipline.** `MappingProxyType`/frozen-slots dataclasses on all result carriers (`resolver_metadata.py`); `contextvars` thread provenance-clash/alias-map/work-budget, always set/reset in `try/finally` (`variable_level.py:648-686`, `propagation.py:206-219`). Determinism via `sorted()` iteration in SCC traversal and rounds. + +**Concerns:** +- **Watch item is STALE for S3 — the layering violation is remediated here.** `project_resolver.py:28` imports `from wardline.core.ruleset import ruleset_hash`, **not** `wardline.core.attest`; a `grep` finds **zero** `core.attest` references in any `taint/*.py`. `ruleset.py:10-16` documents that `ruleset_hash` was extracted into a low-tier dependency-free module precisely to remove the engine→attest inversion "formerly masked by a function-local deferred import in `scanner.taint.project_resolver`." The resolver needs the effective-scan-policy hash to key its summary cache (`wardline-9d6a81b9e7`); it now imports *down* into `core.ruleset` instead of *up* into `core.attest`. The report-only import-linter contract (`pyproject.toml:178-182`, run as `lint-imports || true`) is **still broken — but only by S1's `scanner/pipeline.py`**, not by any S3 module; the pyproject comment (`pyproject.toml:174-176`) and `wardline-9ec283d168` over-name `project_resolver`. +- **Cross-subsystem private-name leakage (two instances).** S1 `grammar.py:196` imports S3 `variable_level._SERIALISATION_SINKS`; S2 `contradictory_trust.py:30` and `invalid_decorator_level.py:20` import S3 `decorator_provider._is_builtin_decorator_fqn`/`_shadowed_builtin_roots`. Leading-underscore privates crossing subsystem boundaries are fragile coupling; the grammar import is function-local (likely cycle avoidance, since `decorator_provider → grammar` at module level). These would be the natural extraction points (e.g. a shared `boundary_fqn` helper). +- **God-module.** `variable_level.py` is 2,481 LOC (~44% of the subsystem) with ~50 functions in one file — outsized review/maintenance surface for the most correctness-sensitive component (the L2 walk). +- **Stale staging docstrings (doc drift, not behavior bug).** `provider.py:5-8` still says "SP1 ships only the trivial `DefaultTaintSourceProvider` … no decorator vocabulary in SP1," but `decorator_provider.py:298-304` is the analyzer's live default; `resolver_metadata.py:30-34` calls SCC metadata "Dormant in SP1." A reader is misled about live wiring. (Same staleness instinct that flagged the attest watch item.) +- **`module_default` taint-source class is dormant.** `module_summariser.py:8-10`/`:35-38` only emit `anchored`/`fallback`; `propagation.py` classifies a `floating_down` bucket nothing currently populates via the live provider — a kept-for-future branch that is untestable through the shipped provider. +- **Documented, deliberate soundness under-approximations (fail-closed, FN-bearing).** Star imports are not materialised for edge resolution (`project_resolver.py:16`); the module-global channel is a v1 approximation — only direct top-level statements, single-Name targets, last-binding-wins, bounded FN on conditional module init (`module_summariser.py:99-106`); an aliased serialisation sink absent from the curated stdlib table retains a residual under-taint (`call_taint_map.py:30-33`). Each under-approximates (never over-trusts), so they are precision/recall debt, not unsoundness. + +**Confidence:** High — read 13 of 14 modules in full; `variable_level.py` (2,481 LOC) read as top + `compute_variable_taints` entry point + full public-surface enumeration (grep of `def`s + Loomweave `contained`/`imports_in`/`imports_out`). Inbound/outbound edges are graph-derived (`entity_callers_list`, `entity_neighborhood_get`, resolved confidence) and corroborated against import lines I read. The attest finding is verified by grep (zero `core.attest` refs in `taint/`) plus `ruleset.py` source, overturning the watch item from primary evidence. The one inference: deep L2 corner-case soundness claims rest on `variable_level`'s docstrings, not full reads of its ~50 internal helpers. diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-S4.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S4.md new file mode 100644 index 00000000..5e0cb8cb --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S4.md @@ -0,0 +1,58 @@ +## S4 — Core Orchestration & Config + +**Location:** `src/wardline/core/{run,scan_jobs,scan_file_workflow,config,config_schema,ruleset,descriptor,discovery,registry,errors,protocols,paths,optional_deps,frontends,taints,gitignore,safe_paths}.py`, `src/wardline/scanner/__init__.py`, `src/wardline/{__init__,_version}.py`, `src/wardline/core/__init__.py` + +**Responsibility:** The surface-agnostic scan core — discover → analyze → suppress → gate — plus config/schema loading, ruleset identity, the language-frontend registry, and the path-confinement guards that every surface (CLI, MCP, LSP, scan-jobs) shares so they are identical by construction. + +**Key Components:** +- `core/run.py` — THE keystone. `run_scan` (`run.py:221`) is the pure discover→analyze→apply-suppressions function; `gate_decision` (`run.py:629`) turns a scan into a pass/fail verdict. Carries the data shapes `ScanSummary` (`run.py:68`), `ScanResult` (`run.py:92`, incl. the `gate_findings`/`honors_suppressions` sentinel at `run.py:110`/`run.py:130`), `GateDecision` (`run.py:151`) whose `__post_init__` invariants (`run.py:181`) forbid a tripped gate ever serialising as PASSED, and `baseline_migration_hint` (`run.py:706`). +- `core/scan_jobs.py` — file-backed, daemon-free async scan jobs under `.weft/wardline/jobs/`; `run_scan_job_worker` (`scan_jobs.py:331`) drives scan→emit→gate in a subprocess, `start_scan_job` (`scan_jobs.py:298`) spawns it from a trusted cwd (`scan_jobs.py:319`), `cancel_scan_job` (`scan_jobs.py:111`) guarded by `_pid_is_scan_job_worker` (`scan_jobs.py:87`) so a forged `status.json` pid cannot `killpg` an unrelated process group. +- `core/scan_file_workflow.py` — `scan_file_findings` (`scan_file_workflow.py:95`): one-shot scan→explain→optional Filigree-promote→identity-attach workflow for agents. +- `core/config.py` — `WardlineConfig` (`config.py:51`) + `load` (`config.py:155`) for the `weft.toml [wardline]` table; explicit-vs-implicit failure policy (`config.py:182-208`), trust-grammar pack gating (`_is_local_pack`, `config.py:109`; stat-only, never imports project code), and the federation endpoint resolvers `resolve_loomweave_url`/`resolve_filigree_url` (`config.py:517`/`config.py:556`) + `_filigree_server_scope` (`config.py:375`). `JudgeSettings`/`parse_judge_settings` (`config.py:567`/`config.py:579`). +- `core/config_schema.py` — `WARDLINE_SCHEMA` (`config_schema.py:13`), draft-2020-12 JSON-Schema with `additionalProperties:false` so a typo'd key is a hard `ConfigError`. +- `core/ruleset.py` — `ruleset_hash` (`ruleset.py:148`): deterministic `"sha256:"` over the effective scan policy. Deliberately housed BELOW both engine and attest layers to break an engine→attest layering inversion (`ruleset.py:10-21`). +- `core/discovery.py` — `discover` (`discovery.py:72`) walks `source_roots` (stdlib `os.walk`); `confine_to_root` rejects escaping roots (`discovery.py:109-115`) and skips out-of-root file symlinks (`discovery.py:142-149`, THREAT-001 read-confinement); `missing_source_roots` (`discovery.py:192`) surfaces silent under-scans. +- `core/safe_paths.py` — the WRITE-side confinement layer: `safe_project_path` (`safe_paths.py:12`) refuses symlink/escape writes, `explicit_output_target` (`safe_paths.py:45`), `write_text_no_follow` (O_NOFOLLOW, `safe_paths.py:76`), `safe_read_text_if_regular` (`safe_paths.py:129`). +- `core/paths.py` — single source of Weft on-disk locations; `weft_state_dir` (`paths.py:50`) and `artifacts_dir` (`paths.py:150`) confine an untrusted `weft.toml [wardline].store_dir`/`artifacts.dir` under root; `enclosing_project_root` (`paths.py:110`) backs the nested-scan-root FACT. +- `core/frontends.py` — `FRONTENDS` registry (`frontends.py:125`) + `LanguageFrontend` Protocol (`frontends.py:37`); `PythonFrontend`/`RustFrontend` lazily build the per-language `Analyzer` so `run_scan` never changes per language. +- `core/protocols.py` — the `Analyzer` (`protocols.py:17`) and `Rule` (`protocols.py:24`) plug-point Protocols. +- `core/registry.py` + `core/descriptor.py` — canonical trust-decorator `REGISTRY` (`registry.py:61`, `REGISTRY_VERSION="wardline-generic-2"`) and its read-instead-of-import NG-25 export `build_vocabulary_descriptor` (`descriptor.py:42`). +- `core/taints.py` — the 8-state `TaintState` lattice (`taints.py:19`), `TRUST_RANK`, and the `least_trusted`/`taint_join`/`combine` operators (`taints.py:107-121`). +- `core/errors.py` — `WardlineError` hierarchy (`errors.py:4`); `core/optional_deps.py` — `require_yaml`/`require_jsonschema` extra gates; `core/gitignore.py` — stdlib `GitignoreMatcher` (`gitignore.py:121`, trusted opt-in pruning only); `_version.py` — `__version__ = "1.0.7"`. + +**Public surface / entry points:** `run_scan` and `gate_decision` (`run.py:221`/`run.py:629`) are the shared scan/gate contract called by every surface; `ScanResult`/`ScanSummary`/`GateDecision` are the data shapes those surfaces serialise. Other entry points: `config.load` + `WardlineConfig`, `ruleset_hash` (`ruleset.py:148`), `FRONTENDS`/`LanguageFrontend`, `Analyzer`/`Rule` protocols, `REGISTRY`/`build_vocabulary_descriptor`, the `safe_paths`/`paths` confinement helpers, and the `scan_jobs`/`scan_file_findings` workflow functions. + +**Dependencies (graph-derived):** +- Inbound (who calls into S4): + - **S11 CLI** → `run_scan` (`cli/scan.py:scan`, `cli/findings.py`, `cli/fix.py`, `cli/rekey.py`) and `gate_decision` (`cli/scan.py:scan`); also the `config` URL resolvers and `safe_paths` writers. + - **S10 MCP & LSP** → `run_scan`/`gate_decision` (`mcp/server.py:_scan`, `_fix`, `_rekey`; `lsp.py:run_scan` wrapper). + - **S7 Trust Evidence & Judge** → `run_scan` (`core/attest._build_payload`, `core/assure.build_posture`, `core/dossier.build_dossier`, `core/judge_run.run_judge`, `core/decorator_coverage.build_decorator_coverage`) and `ruleset_hash` (`core/attest._build_payload`). + - **S6 Gate Discipline & Remediation** → `run_scan` (`core/baseline.collect_and_write_baseline`). + - **S5 Findings/Outputs/Explain** → `run_scan` (`core/explain._explain_local`, `core/filigree_issue._finding_for_fingerprint`). + - **S9 Federation Clients** → `ruleset_hash` (`core/legis.build_legis_artifact`); the `config` federation-URL resolvers feed S9 clients. + - **S1 Scanner Engine** → `ruleset_hash` (`scanner/pipeline.run_parse_project_stage`, resolved edge). **S3 Taint Engine** → `ruleset_hash` for summary-cache keying (`scanner/taint/project_resolver`, per `ruleset.py:14-16` docstring + `tests/unit/scanner/taint/test_project_resolver.py` — corroborated inference, not a resolved graph edge). + - **S4 self** → `scan_jobs.run_scan_job_worker` and `scan_file_workflow.scan_file_findings` call `run_scan`/`gate_decision`. +- Outbound (what S4 calls, from `run_scan` callees + references_out): + - **S1 Scanner Engine** — `Analyzer.analyze` resolves to the concrete `WardlineAnalyzer` built via `frontends.PythonFrontend.build_analyzer` → `scanner.analyzer.build_analyzer`/`scanner.grammar`. + - **S3 Taint Engine** — `scanner.taint.summary_cache.SummaryCache.load/save/summary_cache_auth_secret_from_env`. + - **S5 Findings, Outputs & Emit** — `finding.{Finding,Kind,Severity,SuppressionState,Location}`; `scan_jobs`/`scan_file_workflow` also call `agent_summary`, `emit.JsonlSink`, `sarif.SarifSink`, `federation_status`, `filigree_emit`/`filigree_issue`, `explain`. + - **S6 Gate Discipline & Remediation** — `baseline.load_baseline`, `waivers.load_project_waivers`, `suppression.apply_suppressions`/`gate_trips`/`gate_breakdown`, `delta`/`delta_resolve`/`delta_scope` (the `--affected` + `--new-since` machinery). + - **S8 Identity & SEI** — `loomweave.identity.SeiResolver` (injected; never constructed here, so `run_scan` stays network-free). + - **S12 Rust Frontend** — `frontends.RustFrontend.build_analyzer` → `rust.analyzer.RustAnalyzer` (lazy). + +**Patterns Observed:** +- One keystone, two surfaces, identical by construction: `run_scan`/`gate_decision` are the single scan/gate implementation CLI and MCP both call (`run.py:2-7`; graph-confirmed `cli/scan.scan` and `mcp/server._scan`), so findings/`active`/gate can never drift between surfaces. +- Secure-by-default gate: a separately-built UNSUPPRESSED `gate_findings` population (`run.py:486-489`) means a repo-committed baseline/waiver cannot clear the `--fail-on` gate; the posture is carried EXPLICITLY (`honors_suppressions`, `run.py:130-142`) not inferred, and `GateDecision.__post_init__` (`run.py:181-218`) hard-rejects a tripped-but-PASSED decision (the dogfood-#2 regression guard). +- Fail-closed honesty as a house style: explicit `--config` missing/malformed RAISES while the implicit default WARNS and falls back (`config.py:182-208`); an empty `--affected` scope runs the FULL tree as `full-fallback` (`run.py:366-369`); unscanned/escaping/nested-root cases become gate-eligible or FACT findings rather than silent gaps (`run.py:391-460`). +- Layered confinement against an untrusted scan tree: read-confinement in `discover` (`discovery.py:109-115`, `:142-149`), write-confinement in `safe_paths` (`safe_paths.py:19-30`, O_NOFOLLOW `:82-93`), and config-value confinement in `paths.weft_state_dir`/`artifacts_dir` (`paths.py:50-74`, `:150-167`) — three independent guards because `weft.toml` is untrusted input. +- Zero-dep base + lazy plug-points: every S4 base module is stdlib-only; third-party deps are reached only through `require_yaml`/`require_jsonschema` (`optional_deps.py`), and a new language is "add a `LanguageFrontend` class to `FRONTENDS`" with `run_scan` unchanged (`frontends.py:110-128`). + +**Concerns:** +- `run_scan` is a ~374-line god-function (`run.py:221-594`) folding discovery, `--affected` delta scoping, suppression, gate-population materialisation, the `--new-since` ratchet, and scope reporting inline; its safety net is the `GateDecision` invariants plus a large conformance suite (`tests/unit/core/test_run_affected.py`, `tests/conformance/test_warpline_delta_scope.py`) rather than decomposition — high change-risk for the most load-bearing function in the tool. +- `config.py` carries two responsibilities: the `[wardline]` policy loader AND the federation endpoint resolver (`resolve_loomweave_url`/`resolve_filigree_url`, `config.py:517`/`config.py:556`, and `_filigree_server_scope` reading the home-global `~/.config/filigree/server.json`, `config.py:363-425`). The federation half is S9-adjacent and reads OUTSIDE the project tree (fail-soft) — a cohesion smell sitting in the config module. +- Implicit-global coupling via the `_PROVENANCE_CLASH` contextvar (`taints.py:16`, read by `combine` at `taints.py:119`): `run_scan` only sets/resets it around `cache.load()` (`run.py:308-314`) so the summary-cache key reflects clash mode, while the analysis-time setter lives in S1/S3 — the per-scan `provenance_clash` config flag travels through process-global state instead of an explicit parameter. Observation, not a correctness defect (the mechanism is in code S4 doesn't own). +- `safe_paths` confinement is check-then-write: `safe_project_path` resolves+validates (`safe_paths.py:19-30`), then `safe_write_text` opens with O_NOFOLLOW on the FINAL component only (`safe_paths.py:82-93`) — a parent-directory symlink swap between check and open is a residual (narrow) TOCTOU window; explicit outputs OUTSIDE root deliberately keep no-follow-only (`safe_paths.py:57-63`), matching released CLI sibling-source behavior. +- Minor doc tension: `taint_join` is documented "RETAINED, NO PRODUCTION CALL SITE" (`taints.py:88-97`), but `combine` dispatches to it when `provenance_clash` is enabled (`taints.py:117-121`), so "no production call site" holds only for the DEFAULT path. Deliberate ADR-cited retention — flagged for the nuance, not a bug. +- Zero-dep posture verified clean: read all base modules — `config.py` (`json`/`keyword`/`os`/`tomllib`/`warnings`), plus `ruleset`/`paths`/`taints`/`errors`/`discovery`/`gitignore`/`safe_paths`/`registry`/`protocols`/`frontends` — none import a third-party package at base; `jsonschema`/`PyYAML` are reached only via the `require_*` guards (`optional_deps.py`). (`scanner/__init__.py` eagerly imports `WardlineAnalyzer`, but that is the S1 `scanner` extra's own package, not the base.) + +**Confidence:** High — read all 19 assigned files in full. Inbound edges derived from `entity_callers_list` on `run_scan`/`gate_decision`/`ruleset_hash`; outbound from `entity_neighborhood_get` callees + references_out on `run_scan`; the `_resolve_under_root` watch-item was located via `entity_find` and confirmed to live in `wardline.mcp.tooling` (S10), so S4 owns only `confine_to_root` read-confinement + `safe_paths` write guards. One edge is lower-assurance: S3's use of `ruleset_hash` (project-resolver / summary-cache keying) is docstring + test corroborated, not a resolved graph edge. The legis→`ruleset_hash` caller is mapped to S9 per the subsystem legend. diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-S5.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S5.md new file mode 100644 index 00000000..b1fdfdcf --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S5.md @@ -0,0 +1,52 @@ +## S5 — Findings, Outputs & Emit + +**Location:** `src/wardline/core/{finding,finding_query,emit,sarif,filigree_emit,filigree_issue,source_excerpt,agent_summary,artifacts,explain}.py` + +**Responsibility:** Define the `Finding` data model (the cross-tool analysis-fact contract) and every projection of it — query lens, JSONL/SARIF/agent-summary serialization, native Filigree scan-results emission + single-finding promote, taint-chain explanation, and timestamped on-disk artifacts. + +**Key Components:** +- `core/finding.py` — the frozen/slots `Finding` dataclass + the `Severity`/`Kind`/`SuppressionState`/`Maturity`/`Location` enums and `to_jsonl()` (`finding.py:97`); the self-describing fingerprint scheme infra `compute_finding_fingerprint`/`FINGERPRINT_SCHEME="wlfp2"`/`format`/`parse`/`require_fingerprint_scheme` (`finding.py:188-261`); the `UNANALYZED_RULE_IDS` / `INCOMPLETE_ANALYSIS_RULE_IDS` under-scan frozensets (`finding.py:42-57`); the Filigree wire mapping `to_filigree_metadata`/`severity_to_filigree` (`finding.py:265-308`). Documented as stdlib-only; finding *lifecycle* (status/issue_id) is deliberately absent — that is Filigree's domain (`finding.py:5-8`). +- `core/finding_query.py` — `filter_findings`, a pure conjunctive read-lens (`finding_query.py:69`) shared by the MCP `scan` `where` and CLI `findings`; closed-vocab predicates normalize case and reject out-of-domain values loudly (`_normalize_closed_vocab`, `finding_query.py:36`). +- `core/emit.py` — `JsonlSink`, the SP0 default output sink behind the `Sink` Protocol (`emit.py:18`); writes via S4 `safe_paths`. +- `core/sarif.py` — SARIF 2.1.0 builder `build_sarif` + `SarifSink` (`sarif.py:128`); excludes `Kind.METRIC` telemetry, rides suppression on `result.suppressions` and the fingerprint on `partialFingerprints`, and (when given an `AnalysisContext`) emits `codeFlows` from S1's `flow_trace`. +- `core/filigree_emit.py` — pure `build_scan_results_body` (`filigree_emit.py:80`) + injectable-transport `FiligreeEmitter.emit` (`filigree_emit.py:758`); URL dialect parser `filigree_api_base_url` (`:177`), path-aware chunking that preserves Filigree's `mark_unseen` sweep + INV-5 delta-scan disable (`_scan_result_chunks`, `:391`), and the PDR-0023 honesty types `EmitResult`/`FailedFinding` with derived `failed`/`auth_rejected` (`:260-367`). +- `core/filigree_issue.py` — `FiligreeIssueFiler.file`, promote-one-fingerprint-by-fingerprint (`filigree_issue.py:165`), plus the Loomweave SEI identity-attach path `attach_loomweave_identity_for_*` / `resolve_entity_binding_input` (`:313-430`). +- `core/agent_summary.py` — `AgentSummary`/`build_agent_summary` (`agent_summary.py:44`,`:363`): the compact end-of-scan handoff; one ordered union (active → suppressed → engine-facts → informational) is the pagination unit, counts stay whole-project, and `_next_actions_for` is gate-aware (`:289`). +- `core/explain.py` — taint-provenance projection: `explanation_from_context` (`explain.py:141`), the Loomweave-store chain walk `explain_chain` (`:412`), and the unified `explain_taint_result` (`:681`) shared by CLI + MCP; remediation/honesty helpers `remediation_to_dict`/`source_resolution_to_dict` (`:526-678`). +- `core/source_excerpt.py` — `extract_excerpt` (`source_excerpt.py:18`), the single path-contained chokepoint shipping local source bytes to the SP5 triage judge (escape = hard error). +- `core/artifacts.py` — `write_scan_artifact`/`timestamped_scan_artifact` (`artifacts.py:34-58`): timestamped, exclusive-create, retention-pruned artifact files under the project root. + +**Public surface / entry points:** `Finding` + enums + `compute_finding_fingerprint`/`format_fingerprint`/`parse_fingerprint`/`require_fingerprint_scheme`/`to_filigree_metadata` (`finding.py`); `filter_findings` (`finding_query.py:69`); `JsonlSink` (`emit.py:18`); `build_sarif`/`SarifSink` (`sarif.py:128`,`:176`); `FiligreeEmitter`/`build_scan_results_body`/`filigree_api_base_url`/`filigree_disabled_reason` (`filigree_emit.py`); `FiligreeIssueFiler`/`build_promote_body`/`attach_loomweave_identity_for_qualname` (`filigree_issue.py`); `build_agent_summary` (`agent_summary.py:363`); `explain_taint_result`/`explanation_to_dict`/`explain_chain` (`explain.py`); `extract_excerpt` (`source_excerpt.py:18`); `write_scan_artifact` (`artifacts.py:45`). + +**Dependencies (graph-derived):** +- Inbound (who calls into this): + - **S11 CLI** — `cli/scan.py:scan` → `build_agent_summary`, `build_sarif`/`SarifSink.write`, `JsonlSink.write`, `FiligreeEmitter.emit`, `write_scan_artifact`; `cli/explain_taint.py` → `explain_taint_result`; `cli/findings.py` → `filter_findings`; `cli/file_finding.py` → `FiligreeIssueFiler.file` (all `entity_callers_list`, resolved edges). + - **S10 MCP/LSP** — `mcp/server.py:_scan` → `build_agent_summary`, `filter_findings`, `FiligreeEmitter.emit`; `mcp/server.py:_explain_taint` → `explain_taint_result`; `mcp/server.py` → `FiligreeIssueFiler.file` (`server.py:158`); the MCP/LSP surface also consumes the `Finding` model for diagnostics. + - **S4 Core Orchestration** — `core/scan_jobs.py:_write_scan_artifact` → `build_agent_summary`/`JsonlSink.write`/`SarifSink.write`; `core/scan_jobs.py:run_scan_job_worker` and `core/scan_file_workflow.py` → `FiligreeEmitter.emit` + `FiligreeIssueFiler.file`. + - **S7 Trust Evidence & Judge** — `core/judge_run.py:run_judge` → `extract_excerpt` (`source_excerpt.py`). + - **S8 Identity/Rekey** — `core/rekey.py` → `FiligreeEmitter.emit` (re-emit after a fingerprint rekey, `rekey.py:599`). + - **S1/S2/S3 (producers)** — construct `Finding`/`Location` objects (the universal product of the analysis pipeline; construction sites are diffuse, characterized rather than enumerated). +- Outbound (what this calls): + - **S1 Scanner Engine** — `explain.py` reads `AnalysisContext` provenance maps (`call_site_callees`/`function_return_taints`/`function_return_callee`/`taint_provenance`/`entities`, `explain.py:58-191`); `sarif.py` → `scanner.flow_trace.build_finding_code_flow` (`sarif.py:19,67`) + `scanner.context.AnalysisContext`. + - **S3 Taint Engine** — `explain.py` → `core.taints.RAW_ZONE` (`explain.py:21,65`). + - **S4 Core Orchestration & Config** — `explain.py`/`filigree_issue.py` → `core.run.run_scan` (`explain.py:235`, `filigree_issue.py:276`); `agent_summary.py` → `core.run.GateDecision`/`ScanResult` (`:15`); `artifacts.py` → `core.config`/`core.paths`/`core.safe_paths`; `emit.py`/`sarif.py` → `core.safe_paths`; broad `core.errors.*` use. + - **S8 Identity & SEI** — `filigree_issue.py` → `loomweave.identity.SeiResolver` (`:27,340,409`) (verified import-clean: that module is contract stdlib-only, `loomweave/identity.py:14-15`). + - **S9 Federation Clients** — `filigree_emit.py` → `core.http.WeftHttp` (`:33,613`); `filigree_issue.py` → `core.http.read_response_text`; `agent_summary.py` → `core.federation_status` (`:13`); `explain.py`/`filigree_issue.py` → duck-typed Loomweave store client (`batch_get`/`resolve`/`get_taint_fact`). + - **loomweave extra** — `explain.py` → lazy `wardline.loomweave.require_blake3` for local content-hash freshness (`explain.py:249`). + +**Patterns Observed:** +- **Pure-builder + injectable-transport seam, repeated.** Every emitter splits a pure data→dict/string builder from a thin IO wrapper: `build_scan_results_body`/`FiligreeEmitter`, `build_promote_body`/`FiligreeIssueFiler`, `build_sarif`/`SarifSink`, `explanation_from_context`/`explain_finding`. Transports are `Protocol`s injected for test (`filigree_emit.py:602`, `filigree_issue.py:127`). +- **Construction-time-consistency guards make contradictory states unrepresentable.** `EmitResult.__post_init__` forbids a reachable result carrying an error status (`filigree_emit.py:358`); `failed`/`auth_rejected` are *derived* properties over `failures`/`status` so a count can never disagree with its reasons (`:343-356`); `AgentSummary.__post_init__` refuses negative `max_findings`/`offset` (`agent_summary.py:67`). +- **Honesty / explicit-degrade invariants over silent nulls.** PDR-0023 per-finding `FailedFinding` reasons mean an empty `failures` tuple is *earned* not assumed; `source_resolution_to_dict` and the chain `"unavailable"` block name the missing capability + enablement rather than returning nulls (`explain.py:526,738`); `mark_unseen` is suppressed whenever any `INCOMPLETE_ANALYSIS_RULE_IDS` finding is present so missing findings are never read as fixed (`filigree_emit.py:99-105`). +- **Single-source-of-truth helpers prevent surface drift.** `filigree_api_base_url` is the one URL dialect parser shared by emit + promote + work-join (`filigree_emit.py:177`); `filigree_disabled_reason` is shared by CLI + MCP status blocks (`:564`); `explanation_to_dict` is the single shaper for CLI + MCP `explain_taint` (`explain.py:554`). +- **Fingerprint is a source-derived join key, defended at runtime.** The fingerprint deliberately excludes `line_start` and resolved taint tiers (`finding.py:152-198`); the scheme prefix (`wlfp2`) lets stores loud-fail on formula drift (`require_fingerprint_scheme`, `:243`). +- **Lazy-import discipline for both optional extras and acyclic decoupling** — `require_blake3` imported in-function (`explain.py:249`), `SchemeMismatchError` imported lazily to keep `finding.py` import-thin (`finding.py:256`); `from __future__ import annotations` universal; base emitters stay stdlib (urllib) — zero-runtime-dep contract upheld. + +**Concerns:** +- **Standalone explain/attach triggers a full re-scan.** `_explain_local`→`run_scan` (`explain.py:235`) and `_finding_for_fingerprint`→`run_scan` (`filigree_issue.py:274-277`) re-analyze the whole project to explain or attach identity to a *single* finding; only the opt-in Loomweave store fast-path avoids it (`explain.py:483-512`). Cost scales with project size and the mitigation is not default. +- **N-hop provenance is single-hop without the optional store (by design, but a real completeness gap).** Standalone `explain` resolves one hop (`explain.py:8-9,141-146`); the full walk `explain_chain` requires a Loomweave taint store (`:412-454`). The sink-*argument* return-indirection case is patched by `_sink_taint_source` (in-source ticket `weft-0d24cf9152`, `explain.py:82-138`), but imported/dynamic sources stay unresolvable single-scan (honestly degraded). Watch-item `wardline-82f49ec3c3` tracks this territory; **no Filigree issue is currently attached** to these entities (`entity_issue_list` → `no_matches`), so these refs come from source comments, not live tickets. +- **`explain.py` couples tightly to S1/S3 engine internals.** It reaches directly into `AnalysisContext` attribute maps and `RAW_ZONE` with no narrow interface (`explain.py:58-191`), so an engine refactor of those provenance maps would silently break explanation projection. +- **S5/S8 boundary cohabitation in `finding.py`.** The fingerprint algorithm + self-describing scheme infra (`compute_finding_fingerprint`/`FINGERPRINT_SCHEME`/`format`/`parse`/`require_fingerprint_scheme`, `finding.py:188-261`) physically live in S5's central data-model file but are conceptually S8's identity domain — a deliberate but notable cross-subsystem coupling (cross-ref S8). +- **Shallow immutability on a frozen dataclass.** `Finding.properties` is a `Mapping` that is *not* deep-frozen — "treated as read-only by convention" (`finding.py:109-111`); a caller mutating it would corrupt the frozen-identity contract. Minor: the `taint_path_v0` migration breadcrumb is documented as removable post-migration dead weight (`finding.py:115-124`), and the store-served vs re-run explanation projections are two parallel code paths to keep in sync (`explanation_from_context` vs `_explanation_from_blob`, `explain.py:141`,`:345`). + +**Confidence:** High — read all 10 assigned files in full. Inbound edges are graph-derived (`entity_callers_list`, resolved edges) from `cli.scan`, `mcp.server._scan`/`_explain_taint`, `core.scan_jobs`, `core.scan_file_workflow`, `core.judge_run`, `core.rekey`, `cli.findings`, `cli.file_finding`; outbound edges corroborated by reading actual call sites and `AnalysisContext`/`RAW_ZONE` attribute usage (not import lines alone), and the `loomweave.identity` import was verified import-clean against its source. `entity_issue_list` returned `no_matches`, so ticket refs are from in-source comments. Lower-confidence item: the diffuse set of `Finding`-construction sites across S1/S2/S3 was characterized, not exhaustively enumerated. diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-S6.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S6.md new file mode 100644 index 00000000..dccbae95 --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S6.md @@ -0,0 +1,53 @@ +## S6 — Gate Discipline & Remediation + +**Location:** `src/wardline/core/{baseline,waivers,suppression,triage,delta,delta_resolve,delta_scope,autofix}.py` + +**Responsibility:** Decide what a scan's findings *cost* — apply the git-committable baseline, expiry-aware waivers, and judge verdicts to the finding stream, evaluate the secure-by-default `--fail-on` gate predicate over the un-suppressed population, and offer the inner-loop remediation surfaces (advisory delta/`--affected` scan and the PY-WL-111 assert→raise autofix). + +**Key Components:** +- `core/suppression.py` — the suppression-application + gate predicates. `apply_suppressions` (suppression.py:33) annotates each `Kind.DEFECT` with a `SuppressionState` by delegating precedence to S8's `resolve_identity`; `gate_trips` (suppression.py:88) is the `--fail-on` predicate; `gate_breakdown` (suppression.py:102) counts the gate-relevant DEFECTs split into `(active, suppressed)`; `severity_gates` (suppression.py:81) is the rank test. `SEVERITY_ORDER`/`_RANK` (suppression.py:29-30) define the gate severity lattice (`NONE` is absent → facts/metrics never gate). +- `core/baseline.py` — the `.weft/wardline/baseline.yaml` accepted-finding snapshot, fingerprint-keyed. `Baseline` (baseline.py:45), `generate_baseline`/`collect_and_write_baseline` (baseline.py:260/207), `load_baseline`/`_build_baseline` (baseline.py:289/300), and `inspect_baseline_store` (baseline.py:80) — a read-only "can I read my own store" probe for the doctor repo-binding check (the 2026-06-26 stale-binary analog). +- `core/waivers.py` — machine-written, expiry-aware, fingerprint-keyed waivers under `.weft/wardline/waivers.yaml`. `Waiver` (waivers.py:33) with optional `entity_sei`/`entity_locator` (the rename-surviving doctrine spine), `Waiver.is_active` (waivers.py:48), `add_waiver` (waivers.py:157), `load_project_waivers` (waivers.py:128), `WaiverSet` (waivers.py:226). +- `core/triage.py` — drives the LLM judge over active DEFECTs. `run_triage` (triage.py:62) with injected `read_excerpt`/`judge_caller`; `TriageResult`/`TriageVerdict` (triage.py:27/21); `finding_to_request` (triage.py:46). +- `core/delta_scope.py` — untrusted `--affected` scope *input* parsing. `parse_affected_scope`/`parse_affected_scope_text`/`load_affected_scope` (delta_scope.py:72/98/120), `AffectedEntity`/`AffectedScope` (delta_scope.py:45/55), `ScopeParseError` (delta_scope.py:33), and the `DeltaScopeReport` honesty block + `BOUNDARY_CAVEAT` (delta_scope.py:247/239). +- `core/delta_resolve.py` — scope *resolution*. `resolve_affected_scope` (delta_resolve.py:203) turns entities into a file set + filter set; `build_qualname_index` (delta_resolve.py:130) builds the taint-free qualname→file index + structural call graph; `filter_to_affected` (delta_resolve.py:279) is the pure displayed-finding drop-filter; `_expand_callers` (delta_resolve.py:359) does the reverse-call-graph caller closure. +- `core/delta.py` — git change detection. `get_changed_files_since` (delta.py:17, the `--new-since` path, shells out to git) and `get_affected_entities` (delta.py:98, reverse-call-graph BFS). +- `core/autofix.py` — single-rule mechanical codemod. `run_autofix` (autofix.py:88) rewrites PY-WL-111 `assert` statements into `raise ` in place; `has_comment_in_span` (autofix.py:21), `get_assertion_replacement` (autofix.py:69). + +**Public surface / entry points:** +- `apply_suppressions(findings, baseline, waivers, *, today, judged)` (suppression.py:33), `gate_trips(findings, fail_on)` (suppression.py:88), `gate_breakdown` (suppression.py:102) — consumed by S4 `run_scan`/`gate_decision`. +- `generate_baseline(...)` (baseline.py:260), `load_baseline` (baseline.py:289), `load_project_waivers` (waivers.py:128), `add_waiver(...)` (waivers.py:157), `WaiverSet` (waivers.py:226). +- `run_triage(...)` (triage.py:62) — consumed by S7 `judge_run.run_judge`. +- `parse_affected_scope_text`/`load_affected_scope` (delta_scope.py:98/120), `build_qualname_index` + `resolve_affected_scope` + `filter_to_affected` (delta_resolve.py:130/203/279), `get_changed_files_since` (delta.py:17) — consumed by S4 `run_scan`. +- `run_autofix(findings, config, root, *, dry_run, confirm_cb)` (autofix.py:88) — consumed by S11 CLI `fix`/`scan` and S10 MCP `_fix`. + +**Dependencies (graph-derived):** +- Inbound (callers — `entity_callers_list`/`entity_neighborhood_get`): + - **S4 Core Orchestration** → `run.run_scan` calls `apply_suppressions` (run.py:221+), `resolve_affected_scope` + `build_qualname_index` (run.py ~19454/19507); `run.gate_decision` (run.py:629) and `run._would_trip_at` (run.py:597) call `gate_trips`. (S4 `scan_jobs`/`scan_file_workflow` reach the gate via `gate_decision`.) + - **S7 Trust Evidence & Judge** → `judge_run.run_judge` (judge_run.py:131) calls `run_triage`. + - **S11 CLI** → `cli.fix.fix` (cli/fix.py:16) + `cli.scan.scan` (cli/scan.py:39) call `run_autofix`; `cli.main._generate_baseline` (cli/main.py:72) calls `generate_baseline`. + - **S10 MCP & LSP Server** → `mcp.server._fix` calls `run_autofix`, `_baseline` (server.py:3568) calls `generate_baseline`, `_waiver_add` (server.py:3661) calls `add_waiver`. +- Outbound (callees/references — `entity_neighborhood_get`): + - **S5 Findings** → the `Finding`/`Kind`/`Severity`/`SuppressionState`/`Maturity` model (referenced across suppression, baseline, triage, autofix, delta_resolve). + - **S8 Identity & SEI** → `apply_suppressions` calls `finding_identity.resolve_identity` (the waiver>judged>baseline precedence JOIN); `delta_resolve` calls `sei_resolution.locator_to_qualname`; `build_qualname_index` calls `core.qualname.module_dotted_name`. + - **S7 Trust Evidence & Judge** → `apply_suppressions` consumes `judged.JudgedSet`; `triage` builds `core.judge.JudgeRequest`/`JudgeResponse`/`JudgeVerdict`. + - **S9 Federation Clients** → `resolve_affected_scope` references `loomweave.identity.SeiResolver` (and `_resolve_sei_qualname` reaches its bound `SeiClient.resolve_sei`). + - **S1 Scanner Engine** → `build_qualname_index` calls `scanner.index.discover_file_entities`/`discover_class_qualnames` (+ `Entity`) and `scanner.ast_primitives.build_import_alias_map`/`iter_calls_in_function_body`/`resolve_call_fqn`/`resolve_self_method_fqn`. + - **S4 Core Orchestration & Config** → `autofix` reads `config.WardlineConfig.boundary_exception`; the layer uses `errors.{ConfigError,WardlineError,DiscoveryError,JudgeTransportError}`, `paths.{baseline_path,waivers_path}`, `safe_paths.{safe_write_text,write_text_no_follow,safe_project_file}`, `optional_deps.require_yaml`; `collect_and_write_baseline` lazy-imports S4 `run.run_scan` (baseline.py:233, breaking an S6↔S4 import cycle). + +**Patterns Observed:** +- **Secure-by-default gate is population-driven, not predicate-driven.** `gate_trips` is population-agnostic — it trips only on `f.suppressed is SuppressionState.ACTIVE` (suppression.py:92), skips PREVIEW (suppression.py:94), and never gates `NONE` (suppression.py:97). The "evaluate the *as-if-unsuppressed* population unless `--trust-suppressions`" security property is therefore enforced by the **caller (S4 `run_scan`/`gate_decision`) choosing which population to hand the predicate**, not by this module. `gate_breakdown` (suppression.py:102-126) exists precisely to let the verdict say which population tripped; its docstring: the suppressed count "is exactly the set that gates only because suppressions are ignored — the number an agent clears with `--trust-suppressions`/`--new-since`." +- **Single-JOIN precedence.** waiver > judged > baseline is resolved once in S8 `resolve_identity`; `apply_suppressions` (suppression.py:68-77) only maps the returned `matched_on` tag to a `SuppressionState` (BASELINED carries no reason; WAIVED/JUDGED carry the resolver's reason). +- **Hermetic pure layer via injection.** `today: date` is injected into all suppression/gate/waiver-expiry calls; `read_excerpt` + `judge_caller` are injected into `run_triage`. The whole layer tests without a clock or network. Judge transport/excerpt failures skip-and-count (triage.py:87-96); a malformed *model* verdict (`JudgeContractError`) is deliberately uncaught so the audit primitive surfaces (triage.py:6-9). +- **Fail-loud fingerprint stores.** Baseline/waiver loaders enforce empty-guard → `fingerprint_scheme` → version → 64-hex + duplicate checks (baseline.py:300-324, waivers.py:144-154, 68-96); a malformed store raises `ConfigError` rather than silently suppressing. Baselining excludes actively-waived findings so a waiver's expiry stays observable (baseline.py:247-249), and excludes PREVIEW (`_is_baselineable_finding`, baseline.py:171). +- **Delta is advisory, never a gate, and structurally cannot narrow the gate.** INV-2: `filter_to_affected` is a pure drop-filter that re-mints no fingerprint (delta_resolve.py:44-46, 279-306). INV-4/THREAT-001: the filter touches only the *displayed* `findings`, never `gate_findings` — S4 keeps the gate population as the unfiltered analyzed set (delta_resolve.py:42-46, 294-297). INV-3: when the scope resolves zero files the result is `mode="full-fallback"`/`gate_authority="gate-of-record"` (fail-closed honesty); in delta mode `gate_authority="advisory"` (delta_scope.py:255-258). Caller-closure expansion expands only the *analyzed file set* over the reverse call graph (taint anchors caller-side) while the filter set stays the base set (delta_resolve.py:33-46, 359-390). Untrusted scope input is parsed defensively with DoS caps (4 MiB / 50k items, byte-cap-before-parse) and a loud-vs-empty split (delta_scope.py:26-43, 98-117). +- **Hardened external-process & filesystem boundaries.** `get_changed_files_since` rejects refs starting with `-`, resolves the ref through `rev-parse --verify --end-of-options`, and disables fsmonitor (delta.py:14, 21-22, 43). Autofix is AST-located, applies replacements bottom-to-top so earlier char offsets stay valid (delta_resolve sort, autofix.py:150-153), fail-closes when a comment falls inside the target span (autofix.py:46-49, 177-178), confines writes via `is_relative_to(root)` (autofix.py:106-113), and gates each write behind optional `confirm_cb`/`dry_run`. + +**Concerns:** +- **Lineless-DEFECT downgrade is a gate-relevant, fail-open-leaning rewrite.** A `Kind.DEFECT` with `location.line_start is None` (and not `ENGINE_PATH`) is *replaced* by a `Severity.NONE` `Kind.FACT` (`WLN-ENGINE-LINELESS-DEFECT`, suppression.py:47-67), so it no longer gates. This is deliberate (avoids fingerprint-collision risk on lineless defects) and mitigated by the always-emitted warning fact, but it means a class of DEFECT silently leaves the gate population — worth keeping under review. +- **Private-attribute reach into S9.** `_resolve_sei_qualname` calls `sei_resolver._client.resolve_sei(...)` (delta_resolve.py:350), coupling S6 to a private member of the S9 `SeiResolver` because the public surface returns an `EntityBinding`, not the raw `current_locator`. Documented in-code, but it will break silently on an S9 rename and is exactly the kind of edge import-linter cannot see. +- **"Codemod engine" framing oversells a single-rule fixer.** `autofix.py:1` advertises an "Autofix/codemod engine for mechanical fixes," but `run_autofix` hard-codes `if f.rule_id == "PY-WL-111"` (autofix.py:110) and the only remediation is assert→raise. Adding a second fix requires restructuring the dispatch; today the generic name and the single-rule reality diverge. +- **The secure-default invariant is not locally enforceable in S6.** Because `gate_trips`/`gate_breakdown` are population-agnostic, a future S4 caller that hands the gate the already-suppressed population would silently defeat secure-by-default. The property lives at the S6/S4 seam, guarded only by conformance tests (`test_axis7_gate_population_not_narrowed`, `test_delta_trust_suppressions_cannot_forge_green` in tests/conformance + tests/unit/core/test_run_affected). Robust, but a split invariant rather than a type-enforced one. +- **S6↔S4 import cycle, worked around by a lazy import.** `collect_and_write_baseline` defers `from wardline.core.run import run_scan` to call time (baseline.py:232-233) because S4 `run` imports the baseline loaders. The cycle is real and only resolved by import ordering. + +**Confidence:** High — all eight assigned files read in full; both directions of every cross-subsystem edge graph-derived via `entity_callers_list` + `entity_neighborhood_get` (inbound S4/S7/S10/S11; outbound S1/S5/S7/S8/S9/S4), corroborated against `file:line` in source rather than import lines. Lower confidence on two points stated as contracts, not verified internals: the exact population-selection wiring in S4 `run.py` (not in my file set — I assert the S6 predicate semantics, attribute population choice to S4) and `judged.py`'s internals (another agent's file — referenced only as the `JudgedSet` input). diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-S7.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S7.md new file mode 100644 index 00000000..1ec88c9d --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S7.md @@ -0,0 +1,52 @@ +## S7 — Trust Evidence & Judge + +**Location:** `src/wardline/core/{attest.py, attest_key.py, assure.py, dossier.py, judge.py, judge_run.py, judged.py, decorator_coverage.py}`, `src/wardline/{weft_dossier.py, weft_decorator_coverage.py}` + +**Responsibility:** Turn a scan into trust *evidence* an agent can act on — a signed reproducible attestation bundle, a trust-surface coverage posture (`assure`), per-entity decorator-coverage rows, a cross-tool entity dossier, and an opt-in network-fenced LLM triage judge whose FALSE_POSITIVE verdicts become committed suppressions. + +**Key Components:** +- `core/attest.py` — build / sign / verify the `wardline-attest-2` evidence bundle: `build_attestation` (`attest.py:239`), `verify_attestation` (`attest.py:287`), the pure shared derivation `_build_payload` (`attest.py:167`), HMAC-SHA256 signer `_sign` (`attest.py:116`) over canonical key-sorted JSON `_canonical_bytes` (`attest.py:107`), `git_state` commit/dirty probe (`attest.py:67`), and lazy fail-soft SEI enrichment `_enrich_seis` (`attest.py:129`). Threat model (shared-secret, not asymmetric) documented at `attest.py:2`. +- `core/attest_key.py` — mint/load the HMAC signing secret from `WARDLINE_ATTEST_KEY` env / `.env`: `mint_attest_key` (`attest_key.py:57`, refuses to mint into a git-tracked `.env` at `:75`), `load_attest_key` (`attest_key.py:38`), non-secret `key_id` short id (`attest_key.py:110`). +- `core/assure.py` — trust-surface COVERAGE rollup (verdict-reached-either-way, denominator = anchored entities only): pure core `posture_from_scan` (`assure.py:181`), I/O shell `build_posture` (`assure.py:248`), `AssurancePosture` (`assure.py:92`), `_empty_posture` (`assure.py:138`); `coverage_pct=None` when there is no surface (`assure.py:226`) to avoid a vacuous-100% false-green. +- `core/dossier.py` — the one-call `EntityDossier` envelope (`dossier.py:323`) + assembler `build_dossier` (`dossier.py:760`); **`classify_entity_trust` (`dossier.py:588`) is the single source of truth** for the three-valued defect/clean/unknown verdict reused everywhere; `bound_to_budget` token trimmer (`dossier.py:437`) with `DOSSIER_TOKEN_BUDGET=2000` (`dossier.py:67`) and honest `Truncation` markers; `_entity_not_found_message` qualname-coupling remedy (`dossier.py:725`). +- `core/judge.py` — dependency-free LLM triage: `call_judge` (`judge.py:316`), the frozen generic policy prompt `_STATIC_POLICY_BLOCK` (`judge.py:73`) + its `JUDGE_POLICY_HASH` (`judge.py:200`), `build_messages` (`judge.py:235`), stdlib `UrllibTransport` → OpenRouter (`judge.py:296`), strict `_parse_verdict_payload` (`judge.py:415`). `JudgeVerdict` is TRUE/FALSE_POSITIVE only (`judge.py:41`). +- `core/judge_run.py` — CLI/MCP-shared orchestration `run_judge` (`judge_run.py:131`); the only core path that touches the network, and only when the default caller is actually invoked (`judge_run.py:150`); `_persist` writes FALSE_POSITIVEs at/above the confidence floor (`judge_run.py:104`); project policy gated behind `trust_judge_policy` (`judge_run.py:90`). +- `core/judged.py` — machine-managed `.weft/wardline/judged.yaml` records: `write_judged`/`load_judged` (`judged.py:78`/`:87`), `JudgedFP`/`JudgedSet` (`judged.py:28`/`:41`); loader requires `verdict: FALSE_POSITIVE` (`judged.py:122`) and full provenance (model/policy_hash/confidence, `judged.py:124`) so an unauditable suppression cannot be smuggled in. +- `core/decorator_coverage.py` — row-level sibling of `assure`: `build_decorator_coverage` (`decorator_coverage.py:226`), `decorator_coverage_from_scan` (`:186`), `DecoratorCoverageRow` (`:78`) with per-row identity/work/finding-state. +- `weft_dossier.py` — live orchestrator `build_weft_dossier` (`weft_dossier.py:61`): joins Wardline posture + Loomweave linkages + Filigree work on the opaque SEI, with a `sei:`-prefixed resolve branch (`weft_dossier.py:83`). +- `weft_decorator_coverage.py` — live wiring `build_weft_decorator_coverage` (`weft_decorator_coverage.py:29`) + `LoomweaveBindingProvider` (`:17`). + +**Public surface / entry points:** `build_attestation` / `verify_attestation` (`attest.py:239`/`:287`), `mint_attest_key` / `load_attest_key` (`attest_key.py:57`/`:38`), `build_posture` (`assure.py:248`), `build_dossier` (`dossier.py:760`) + `build_weft_dossier` (`weft_dossier.py:61`), `run_judge` (`judge_run.py:131`), `build_decorator_coverage` (`decorator_coverage.py:226`) + `build_weft_decorator_coverage` (`weft_decorator_coverage.py:29`); plus `classify_entity_trust` (`dossier.py:588`) consumed internally, and `load_judged` / `JudgedSet` (`judged.py:87`/`:41`) consumed by the suppression pipeline. + +**Dependencies (graph-derived):** +- **Inbound** (callers verified via `entity_callers_list`): + - **S11 CLI** → `cli/attest.py:148` build_attestation, `cli/attest.py:124` verify_attestation, `cli/attest.py:99` load_attest_key; `cli/install.py:68` mint_attest_key; `cli/judge.py` run_judge (caller `wardline.cli.judge.judge`); `cli/assure.py`→build_posture, `cli/dossier.py`→build_weft_dossier, `cli/decorator_coverage.py`→build_weft_decorator_coverage. + - **S10 MCP** → `mcp/server.py` `_attest` (`server.py:3117`)→build_attestation, `_verify_attestation` (`server.py:3355`), `_judge` (`server.py:3449`)→run_judge, plus `_assure`/`_dossier`/`_decorator_coverage`. + - **S9 Federation (legis)** → `core/legis.py:45` imports `attest.git_state`. + - **S4 Orchestration / S6 Suppression / S8 Rekey consume `judged.py`** (the judge→suppression feed): `core/run.py:476` `load_judged`, `core/suppression.py:18,41` `JudgedSet` (stamps `SuppressionState.JUDGED`), `core/rekey.py` `carry_judged_forward` / `JUDGED_VERSION`. + - **Internal S7→S7:** `attest._build_payload`→`assure.posture_from_scan` + `dossier.classify_entity_trust`; `assure.posture_from_scan`→`classify_entity_trust`; `decorator_coverage_from_scan`→`classify_entity_trust`; `weft_dossier`→`build_dossier`; `weft_decorator_coverage`→`build_decorator_coverage`. +- **Outbound** (by S-label, corroborated by source imports): + - **S4 Core Orchestration & Config** — `run_scan` (every `build_*`), `config.load`, `ruleset_hash` (from `core.ruleset`, `attest.py:58`), `paths` (`weft_config_path`/`judged_path`), `errors` (`AttestError`/`DossierError`/`Judge*Error`), `safe_paths`, `optional_deps.require_yaml` (`judged.py`). + - **S6 Gate Discipline & Remediation** — `waivers.load_project_waivers`/`Waiver` (`assure.py:42`), `triage.run_triage` (`judge_run.py:33`), `SuppressionState`. + - **S5 Findings, Outputs & Emit** — `core.finding` (`Kind`, `SuppressionState`, `FINGERPRINT_SCHEME`, `INCOMPLETE_ANALYSIS_RULE_IDS`), `core.source_excerpt.extract_excerpt` (`judge_run.py:32`). + - **S8 Identity & SEI** — `core.identity` (`EntityBinding`/`IdentityStatus`/`ContentStatus`), `core.sei_resolution.locator_to_qualname` (`weft_dossier.py:30`). + - **S3 Taint Engine** — `core.taints.TaintState` for the `UNKNOWN_TIERS` set (`dossier.py:45`). + - **S9 Federation Clients** — `loomweave.identity` (`SeiResolver`/`SeiCapability`), `loomweave.dossier_sources` (`resolve_entity_binding`/`LoomweaveLinkageProvider`), `loomweave.client.LinkageResult`, `filigree.dossier_client.FiligreeWorkProvider`, `filigree.config.load_filigree_token`, `core.http.read_response_text` (judge transport). + - **External** — OpenRouter HTTPS (`judge.py:36`), reached only by the judge and only when actually invoked. + +**Patterns Observed:** +- **Pure-core + I/O-shell split** so the unit tests exercise logic without disk/scan: `posture_from_scan`/`build_posture` (`assure.py:181`/`:248`), `decorator_coverage_from_scan`/`build_decorator_coverage`, and `_build_payload` shared by both build and verify (`attest.py:167`) so re-derivation is apples-to-apples. +- **Single source of truth for the trust verdict** — `classify_entity_trust` (`dossier.py:588`) is reused by `assure`, `decorator_coverage`, the dossier `TrustSection`, and attest boundaries, so a rollup and a per-entity report can never disagree (`assure.py:19`, `dossier.py:580`). +- **No-false-green discipline everywhere** — three-valued `gate_verdict` (defect/clean/unknown, `dossier.py:179`), `coverage_pct=None` on an empty surface (`assure.py:226`), honest elision markers in `bound_to_budget` (`dossier.py:437`), and `judged.yaml` requiring `verdict: FALSE_POSITIVE` + provenance (`judged.py:122`). +- **Fail-soft on optional cross-tool sources with a two-axis freshness model** — Loomweave/Filigree degrade to an honest `unavailable` section, never a crash (`dossier._linkages_from`/`_work_from` `:661`/`:678`; `attest._enrich_seis` `:129`); identity (alive/orphaned/unavailable) and content (fresh/stale/unknown) axes are never collapsed (`dossier.py:13`). +- **Determinism as a reproducibility contract** — canonical key-sorted compact JSON, every list sorted on a stable key, and the HMAC binds the outer `schema` (`attest.py:15`/`:116`); `assure` and `build_judged_document` sort identically (`assure.py:28`, `judged.py:56`). +- **Zero-dependency base held via lazy extras** — stdlib `hmac`/`hashlib`/`urllib`/`subprocess` only; Loomweave imported lazily inside `_enrich_seis` (`attest.py:143`), `require_yaml` lazily in `judged.py:78`; the judge is a dependency-free urllib POST. Untrusted-data discipline in the judge prompt: system policy vs untrusted user/code/project-policy separation + injection preamble (`judge.py:202`), project policy gated behind `trust_judge_policy` (`judge_run.py:93`). + +**Concerns:** +- **Stale layering comment + report-only contract (documentation rot).** `pyproject.toml:173-177` still asserts "This contract is BROKEN today (wardline.scanner.pipeline / .taint.project_resolver import wardline.core.attest)", but the inversion is **resolved**: `scanner/pipeline.py:134` and `scanner/taint/project_resolver.py:28` import `ruleset_hash` from `wardline.core.ruleset`, a grep finds **no production scanner import of `core.attest`**, the tracking issue `wardline-9ec283d168` is **closed** (2026-06-20; close note: "lint-imports reports 1 kept, 0 broken … the old engine-to-attestation inversion is gone"). CI still runs `uv run lint-imports || true` (`.github/workflows/ci.yml:38`), so the now-passing contract is non-gating and the comment misleads a reader. The contract could be promoted to enforcing and the comment corrected. +- **`verify_attestation` edge cases unpinned** (`wardline-d59f35c626`, open P3): a missing top-level `schema` correctly yields `signature_valid=False` (the `schema == ATTEST_SCHEMA` gate at `attest.py:336`) and a non-dict `payload` raises `AttestError` (`attest.py:329`) — the behavior exists but is not directly unit-tested in `tests/unit/core/test_attest.py`. +- **HMAC is shared-secret tamper-evidence, not asymmetric authorship proof** (`attest.py:2-13`). Anyone holding the project key can both sign and verify; a bundle does not bind to a specific signer. This is honestly documented in-module and forced by the zero-dep base (no Ed25519), but a downstream consumer could over-trust an `attest` bundle as non-repudiable proof. +- **Judge verdicts are model-dependent and cost real tokens.** A `judged.yaml` suppression ultimately rests on an LLM verdict whose only audit primitive is the verbatim rationale (`judged.py:8`); mitigated by `temperature=0`, the network-fence, opt-in invocation, and `write_confidence_floor` (`judge_run.py:215`), but it is a softer suppression source than a hand-authored waiver. +- **Minor duplication in `weft_dossier`** — the `sei:` branch and the qualname branch each independently build the capabilities probe + `SeiResolver` (`weft_dossier.py:86-87` vs `:105-106`); harmless but a small refactor target. + +**Confidence:** High — read all 10 owned files in full, plus `pyproject.toml` (import-linter contract), `.github/workflows/ci.yml`, `scanner/pipeline.py`/`scanner/taint/project_resolver.py` (the layering claim), and two Filigree issues (`wardline-9ec283d168` closed, `wardline-d59f35c626` open). Cross-subsystem edges are graph-derived (`entity_callers_list` on `attest.build_attestation`, `dossier.classify_entity_trust`, `judge_run.run_judge`) and corroborated with targeted greps for the judged→suppression feed and the legis→attest edge. The one place I contradicted the brief's watch-item (the layering violation) is backed by primary source rather than the stale in-repo comment. diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-S8.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S8.md new file mode 100644 index 00000000..b99a4766 --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S8.md @@ -0,0 +1,57 @@ +## S8 — Identity & SEI + +**Location:** `src/wardline/core/{identity,sei_resolution,fingerprint_v0,finding_identity,qualname,rekey,node_id}.py` + +**Responsibility:** The baseline-stability backbone — computes the stable identity primitives (qualname, finding fingerprint, cross-tool entity binding) that let baselines/waivers/judged verdicts re-join the same finding across runs, interpreters, and source moves, resolves rename-stable SEI addresses, and migrates fingerprint-keyed stores across scheme changes. + +**Key Components:** +- `core/finding_identity.py` — the ONE suppression JOIN predicate: `resolve_identity` joins a bare fingerprint against all three stores honouring waiver > judged > baseline precedence (`finding_identity.py:44`); `drifted_from` is reserved for future rekey provenance and is always `None` today (`finding_identity.py:58-64`). +- `core/qualname.py` — Loomweave-aligned qualname PRODUCER (stdlib `ast` only): `module_dotted_name` (one-leading-`src/`-strip rule, `qualname.py:24`), `reconstruct_qualname` (CPython `__qualname__`/`` reconstruction, `qualname.py:63`), `is_overload_stub` (`qualname.py:86`). Single source of truth for both halves of `metadata.wardline.qualname`. +- `core/rekey.py` — `wardline rekey`, the one-shot scan-driven wlfp1→wlfp2 fingerprint migration brain (829 lines): dual-fingerprint computation (`compute_old_new_fingerprints`, `rekey.py:125`), injective remap with collision/fan-out orphaning (`build_remap`, `rekey.py:187`), pre-flight snapshot (`snapshot_stores`, `rekey.py:283`), resumable journal (`Journal`, `rekey.py:421`), per-leg-atomic apply (`apply_pending_legs`, `rekey.py:540`), read-only `probe` (`rekey.py:695`), `rollback` (`rekey.py:801`), and orchestrators `run_rekey`/`resume_rekey` (`rekey.py:744`/`783`). +- `core/sei_resolution.py` — SEI addressing for findings queries: `resolve_query_filters` resolves a `where["qualname"]` beginning `sei:` to its current qualname via the S9 SeiResolver (`sei_resolution.py:28`); `locator_to_qualname` maps a Loomweave locator (`python:function:…`/`python:method:…`) back to a Wardline qualname, method-prefix before catch-all (`sei_resolution.py:13`). +- `core/identity.py` — neutral, provider-agnostic cross-tool binding model: `EntityBinding` (SEI-keyed when present, degrades to locator, `identity.py:30`), the two orthogonal status enums `IdentityStatus`/`ContentStatus` (`identity.py:14`/`22`), and pure `content_status` hash comparison (`identity.py:57`). +- `core/fingerprint_v0.py` — FROZEN byte-exact copy of the pre-P3 wlfp1 formula (`line_start` IN the hash) used ONLY by the rekey migration; never imported by the production scan path (`fingerprint_v0.py:22`, `FINGERPRINT_SCHEME_V0 = "wlfp1"` at `:19`). +- `core/node_id.py` — the shared `NodeId = NewType("NodeId", int)` per-file pre-order node-identity contract; defined neutrally so frontends share it (`node_id.py:22`). + +**Public surface / entry points:** +- `resolve_identity(fingerprint, *, baseline, waivers, judged, today)` — `finding_identity.py:44` (called by the suppression layer). +- `resolve_query_filters(where, root, config_path, …)` — `sei_resolution.py:28`; `locator_to_qualname(locator)` — `sei_resolution.py:13`. +- `run_rekey`/`resume_rekey`/`probe`/`rollback` — `rekey.py:744`/`783`/`695`/`801`. +- `module_dotted_name`/`reconstruct_qualname`/`is_overload_stub` — `qualname.py:24`/`63`/`86`. +- `EntityBinding`, `IdentityStatus`, `ContentStatus`, `content_status` — `identity.py:30`/`14`/`22`/`57`. +- `compute_finding_fingerprint_v0`, `FINGERPRINT_SCHEME_V0` — `fingerprint_v0.py:22`/`19`. +- `NodeId` — `node_id.py:22`. + +**Dependencies (graph-derived):** +- Inbound (who calls into this): + - **S6 Gate Discipline & Remediation** → `resolve_identity` (graph-resolved: `core.suppression.apply_suppressions`, `suppression.py:71`); → `module_dotted_name`/`locator_to_qualname` for delta scan (graph-resolved: `core.delta_resolve.build_qualname_index`/`_resolve_sei_qualname`, `delta_resolve.py:152`/`356`). + - **S11 CLI & Install** → `resolve_query_filters` (graph-resolved: `cli.findings.findings`, `findings.py:70`); → `run_rekey`/`probe`/`rollback`/`resume_rekey` (graph-resolved: `cli.rekey.rekey`, `cli/rekey.py:21,197`). + - **S10 MCP & LSP Server** → `resolve_query_filters` (graph-resolved: `mcp.server._scan`, `server.py:845`); → `run_rekey` et al. (graph-resolved: `mcp.server._rekey`, `server.py:4082-4096`). + - **S1 Scanner Engine** → `module_dotted_name` (graph-resolved: `scanner.pipeline.run_parse_project_stage`, `scanner.flow_trace._find_sink_contributor`/`build_finding_code_flow`); → `reconstruct_qualname`/`is_overload_stub` (graph-resolved: `scanner.index.discover_file_entities`, `index.py:45,128`). + - **S2 Rule Lattice** → `module_dotted_name` (lazy in-function import, `scanner/rules/untrusted_to_trusted_callee.py:119`). + - **S7 Trust Evidence & Judge** → `EntityBinding`/`locator_to_qualname` (dynamic-constructor edges corroborated by Read: `weft_dossier.py:95-96`, `weft_decorator_coverage.py:23`). + - **S9 Federation Clients** → `EntityBinding`/`IdentityStatus`/`ContentStatus` (dynamic-constructor edges corroborated by Read: `loomweave.identity.SeiResolver.resolve_locator`, `loomweave/identity.py:141`; `loomweave/dossier_sources.py:21`). + - **S12 Rust Frontend** → `NodeId` (`rust/nodeid.py:23`, Read-corroborated) — the *only* current consumer of `node_id.py`. +- Outbound (what this calls): + - **S6 stores** — `finding_identity` reads `Baseline`/`JudgedSet`/`WaiverSet` (`finding_identity.py:23-25,56-62`); `rekey` reads the three store version constants + carries them (`rekey.py:23-30`). + - **S5 Findings, Outputs & Emit** — `rekey` imports `FINGERPRINT_SCHEME`/`Finding`/`Kind` (`rekey.py:25`) and re-emits via the injected Filigree emitter (`rekey.py:599`); `fingerprint_v0` is the frozen mirror of `core.finding.compute_finding_fingerprint` (`finding.py:188`). + - **S9 Federation Clients** — `sei_resolution` lazily builds `LoomweaveClient` + `SeiResolver`/`SeiCapability` (`sei_resolution.py:48-63`). + - **S4 Core Orchestration & Config** — `sei_resolution` → `resolve_loomweave_url`/`WardlineError` (`sei_resolution.py:45,59`); `rekey` → `paths`/`safe_paths`/`errors`/`optional_deps` (`rekey.py:22,28-30`). + +**Patterns Observed:** +- **Two strictly-orthogonal status axes.** Identity ("same entity?") and content ("code changed?") are never collapsed: separate enums (`identity.py:14-27`), mirrored verbatim in the SEI resolver (`loomweave/identity.py:19-22`). `ORPHANED`/`STALE` are asserted only on explicit positive evidence, never guessed from a malformed body (`loomweave/identity.py:148-167`) — no false-green. +- **SEI opacity + rename-stability by delegation.** The `loomweave:eid:` token is carried verbatim and compared by equality only; S8 never parses it (`sei_resolution.py` resolves via `resolve_sei`, opacity stated at `loomweave/identity.py:17`). Rename-stability is a *property of the sibling resolver* returning a `current_locator`, not something S8 computes — `EntityBinding.binding_key` prefers the SEI and degrades honestly to the locator (`identity.py:52-54`). +- **One JOIN predicate, one precedence.** The whole suppression layer asks `resolve_identity` rather than re-implementing waiver > judged > baseline inline (`finding_identity.py:1-16,56-64`); `today` is injected so waiver expiry is hermetic. +- **Self-describing scheme + frozen legacy formula.** Live `wlfp2` (`finding.py:188-212`) vs frozen `wlfp1` (`fingerprint_v0.py`); stamping a scheme onto the store/wire makes a wrong-formula store LOUD-FAIL (`SchemeMismatchError`) instead of silently orphaning every verdict. +- **Crash-safe, snapshot-sourced migration.** `rekey` snapshots stores FIRST (the sole provenance source), journals only remap + per-leg done-flags (never content, to prevent divergence, `rekey.py:421-447`), carries from the immutable snapshot so a crash-then-resume reproduces identical content, applies legs per-leg-atomically/idempotently, and on injectivity collisions orphans-and-reports rather than aborting the whole run (`rekey.py:148-216`). Store writes are symlink-hardened through `safe_paths` (`rekey.py:283-307,477-491`). +- **Determinism contract.** Invariant: identical source → identical fingerprint, byte-stable across CPython 3.12/3.13. NOTE: the mechanism that guarantees it for entity-body findings lives OUTSIDE S8 — `_canonical_ast_dump` in `scanner/rules/_fingerprint.py:12-46` (S1/S2) reproduces 3.13's `show_empty=False` form on every interpreter — and is pinned by `tests/unit/scanner/rules/test_entity_fingerprint_stability.py` plus the byte-green identity oracle `tests/golden/identity/test_identity_parity.py`. The qualname half is pinned cross-tool by `tests/conformance/qualnames.json` + `test_loomweave_qualname_parity.py`. +- **Stdlib-only identity primitives preserve the zero-dep base.** `identity`/`qualname`/`node_id`/`fingerprint_v0` import only `dataclasses`/`enum`/`ast`/`hashlib`/`typing`; `rekey`'s `yaml` and `sei_resolution`'s Loomweave client are reached only lazily behind their extras (`rekey.py:28` `require_yaml`, `sei_resolution.py:48-56`). + +**Concerns:** +- **S8's central invariant is enforced in a sibling subsystem.** The interpreter-stable join key depends on `scanner/rules/_fingerprint.py:_canonical_ast_dump` (S1/S2) being correct; S8 owns no guard for the entity-body discriminator. The 2026-06-28 3.12↔3.13 drift this fix repaired (`_fingerprint.py:18-24`) shows the blast radius: a regression there silently breaks every baseline/waiver re-join with no S8-local failure. Cross-subsystem coupling worth a tracked invariant. +- **`node_id.py` is a defined-but-Python-unadopted contract.** `NodeId` is consumed only by the S12 Rust frontend (`rust/nodeid.py:23`); the Python frontend still keys call-site maps on raw `id(node)` ints (`node_id.py:9-16` documents the deferred SP1 migration). Not dead, but currently inert on the Python path — a half-laid contract. +- **`fingerprint_v0.py` is an intentional code clone of `finding.py`'s live formula.** The duplication is the point (frozen pre-P3 form) and is guarded by the "do not edit" docstring + the byte-green identity oracle, but a well-meaning "fix" that re-syncs it to the live engine would silently mis-reconstruct every `old_fp` and orphan verdicts on migration (`fingerprint_v0.py:1-13`). Guarded, low severity, but fragile by construction. +- **`_POLICY_CONFIG_RULE_ID` is a scanner constant hand-mirrored into core.** `rekey.py:54-60` duplicates `scanner.rules._POLICY_CONFIG_RULE_ID` to respect the core-must-not-import-scanner layering, kept in sync only by a drift test (`test_rekey_population.py`). Correct, but a maintenance trap if that drift test is ever dropped. +- **`resolve_query_filters` reduces the SeiResolver to a capability gate, then bypasses it.** It builds the resolver via raw `SeiResolver(...)` rather than the canonical `SeiResolver.detect()` factory, uses it only for `.capability.supported` (`sei_resolution.py:63-65`), then calls the RAW `loomweave_client.resolve_sei(qval)` for the actual resolve (`sei_resolution.py:67`). It also constructs a live network client *inside a query-filter resolver* (`sei_resolution.py:44-56`) — a hidden IO dependency triggered by a `sei:`-prefixed filter value. Functionally correct; minor layering smell. + +**Confidence:** High — read all 7 owned files end-to-end plus the paired live formula (`finding.py:188-212`), the S9 SEI resolver (`loomweave/identity.py`), the determinism fix (`scanner/rules/_fingerprint.py`), and its pinning test. Inbound edges are graph-derived via `entity_callers_list` on the public symbols (resolved callers for `resolve_identity`, `resolve_query_filters`, `run_rekey`, `module_dotted_name`; the graph added `scanner.pipeline`/`flow_trace` callers my grep missed and contradicted none). The lone inferred class is `EntityBinding`: as a dataclass its call sites surface as dynamic-constructor `unresolved_candidates` (S7 `weft_dossier`, S9 `loomweave/identity`), which I corroborated by reading those files. diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-S9.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S9.md new file mode 100644 index 00000000..2472d470 --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-S9.md @@ -0,0 +1,59 @@ +## S9 — Federation Clients + +**Location:** `src/wardline/core/http.py`, `src/wardline/core/federation_status.py`, `src/wardline/core/legis.py`, `src/wardline/loomweave/`, `src/wardline/filigree/`, `src/wardline/_live_oracle.py` + +**Responsibility:** Hand-rolled, stdlib-urllib-only signed HTTP transport and wire projections that let an optional Wardline scan talk to the three sibling Weft tools — Loomweave (taint-fact store + SEI identity), Filigree (finding emit + dossier work), and legis (signed governance artifact) — each pinned byte-exactly to that sibling's verifier, each fail-soft so a sibling outage never breaks a scan. + +**Key Components:** +- `core/http.py` — `WeftHttp` (`core/http.py:47`): the ONE shared round-trip (scheme-gate, `Request` build, `urlopen`-with-timeout, `HTTPError`→`HttpResult` status-band conversion, 64 KiB bounded body read) every client repeated; `fetch` (`:91`), `read_response_text` (`:22`), `HttpResult` (`:33`). `URLError`/`OSError` are deliberately NOT swallowed — the caller owns outage policy (`:67-71`). +- `loomweave/_hmac.py` — Loomweave's request signature reproduced byte-exactly from the Rust verifier `canonical_hmac_message` (auth.rs:220-234): `canonical_message` (`:25`) is `METHOD\nPATH_AND_QUERY\nsha256_hex(body)\nTIMESTAMP\nNONCE`, `sign_request` (`:36`) is lowercase-hex HMAC-SHA256; header is `X-Weft-Component: loomweave:` plus `X-Weft-Timestamp`/`X-Weft-Nonce` (replay cache, 300s window). +- `loomweave/client.py` — `LoomweaveClient` (`:154`): the `/api/wardline/*` + `/api/v1/identity/*` + `/api/v1/entities/{id}/callers|callees` client. Soft-vs-loud band routing lives in `_send` (`:193`, outage/`>=500`→None) and `_require_ok` (`:225`, non-2xx→`LoomweaveError`). Surfaces: `resolve`/`write_taint_facts`/`batch_get`/`batch_get_by_sei` (SEI read, `:341`), SEI identity wire (`capabilities`/`resolve_identity`/`resolve_sei`, `:403-420`), call-graph linkages (`get_callers`/`get_callees`, `:447-456`). Module-local `UrllibTransport` (`:48`) + `Response` (`:39`) wrap `WeftHttp`. +- `core/legis.py` — B4 signed Wardline→legis hop; legis is a PRODUCER, not a client (no HTTP). `build_legis_artifact` (`:266`) builds the verbatim-postable `scan` over the GATE population; `sign_artifact` (`:114`) is `hmac-sha256:v2:` over `canonical_json(scan-minus-signature)` (`:105`), a replica of legis's `canonical`/`signing`; `project_finding` (`:167`) is the typed projection onto legis's accepted trust vocabulary; `legis_artifact_outcome` (`:397`) and `load_legis_artifact_key` (`:135`). +- `core/federation_status.py` — the ONE canonical `{filigree_emit, loomweave_write}` status-envelope builder + JSON-schema `$defs` source: `filigree_emit_status` (`:35`), `filigree_emit_status_from_block` (`:95`, the wider MCP shape), `loomweave_write_status` (`:132`), `*_schema` builders. Reproduces each surface's current bytes via explicit flags rather than collapsing them. +- `loomweave/identity.py` — SEI client abstraction (stdlib-only by contract): `SeiResolver` (`:99`), fail-closed `SeiCapability.from_capabilities` (`:51`) and `TaintStoreCapability.from_capabilities` (`:77`); SEI carried opaque, two orthogonal axes (identity/content) never collapsed. +- `loomweave/facts.py` — `build_taint_facts` (`:55`): pure projection of the scan into `wardline-taint-1` blobs; `content_hash_at_compute` is whole-file raw-byte blake3 matching Loomweave's `current_content_hash` (`:124`, lazy blake3). +- `loomweave/dossier_sources.py` / `filigree/dossier_client.py` — live dossier sources behind the `LinkageProvider`/`WorkProvider` seams: `LoomweaveLinkageProvider` (`dossier_sources.py:43`) + `resolve_entity_binding` (`:82`); `FiligreeWorkProvider.work` (`dossier_client.py:97`) reads ADR-029 entity-associations over BEARER auth and does the drift compare itself. +- `loomweave/config.py` / `filigree/config.py` — credential loaders (env / `.env` ONLY, never `weft.toml`): `load_loomweave_token` (`config.py:16`), `resolve_project_name` (`:33`), and Filigree's 6-rung bearer ladder `load_filigree_token` (`filigree/config.py:79`). +- `loomweave/write.py` — `write_facts_to_loomweave` (`:24`), the fail-soft scan-time write orchestration; `loomweave/__init__.py` — `require_blake3` (`:16`) lazy import keeping the base package zero-dep. +- `_live_oracle.py` — `live_oracle_required`/`should_fail_live_oracle_skip` (`:25`,`:33`) + `LIVE_ORACLE_MARKERS` (`:18`): the conftest hook that turns a SKIP into a FAILURE under armed CI for `*_e2e` live oracles and `sei_drift`/`worklist_drift` source-byte rechecks (crit-3b fail-closed). + +**Public surface / entry points:** +- Transport: `WeftHttp(...).fetch` (`core/http.py:91`) — consumed by all three urllib clients. +- legis: `build_legis_artifact` (`core/legis.py:266`), `legis_artifact_outcome` (`:397`), `load_legis_artifact_key` (`:135`). +- Status envelope: `filigree_emit_status`/`_from_block`/`loomweave_write_status`/`*_schema` (`core/federation_status.py`). +- Loomweave: `LoomweaveClient` (`client.py:154`) + `resolve`/`write_taint_facts`/`batch_get_by_sei`/`get_callers`/`get_callees`/SEI wire; `SeiResolver.detect`/`resolve_locator` (`identity.py:117`,`:125`); `LoomweaveLinkageProvider` (`dossier_sources.py:43`); `write_facts_to_loomweave` (`write.py:24`); `build_taint_facts` (`facts.py:55`). +- Filigree: `FiligreeWorkProvider` (`dossier_client.py:89`); `load_filigree_token` (`config.py:79`). +- Test infra: live-oracle skip-to-fail helpers (`_live_oracle.py`). + +**Dependencies (graph-derived):** +- Inbound (callers, via `entity_callers_list`): + - **S11 CLI & Install** → `build_legis_artifact` (`cli/scan.py:scan`, byte 16293), `write_facts_to_loomweave` (`cli/scan.py:scan`, byte 19938), `LoomweaveClient` (`cli/{attest,decorator_coverage,dossier,explain_taint,file_finding,scan,scan_file_findings}.py`), `filigree_emit_status` (`cli/scan._filigree_status:663`), `WeftHttp` (`install/doctor.py:754`), and the token loaders. + - **S10 MCP & LSP** → `build_legis_artifact` (`mcp/server._attach_legis_artifact:2087`), `write_facts_to_loomweave` (`mcp/server._scan:727`), `filigree_emit_status_from_block` (`mcp/server._filigree_emit_status:138`), `LoomweaveClient` (`mcp/server.py:4569`). + - **S8 Identity & SEI** → `LoomweaveClient` + `SeiResolver` (`core/sei_resolution.py:52`). + - **S7 Trust Evidence & Judge (dossier)** → `FiligreeWorkProvider` (`weft_dossier.py:111`, `weft_decorator_coverage.py:42`), `LoomweaveLinkageProvider` + `resolve_entity_binding` (`weft_dossier.py:103,108`). + - **S4 Core Orchestration & Config** → `filigree_emit_status` (`core/scan_jobs._filigree_status:262`, `core/scan_file_workflow._emit_to_dict:49`). + - **S5 Findings, Outputs & Emit** → `WeftHttp` (`core/filigree_emit.py:613,620` — the Filigree finding-emit WRITE path). +- Outbound (imports corroborated by `Read`): + - **S4 Core Orchestration & Config** — `core.errors` (`LoomweaveError`/`FiligreeEmitError`/`LegisArtifactError`), `core.ruleset.ruleset_hash`, `core.safe_paths` (`safe_project_file`/`safe_read_text_if_regular`, used in `legis.py:49`, `filigree/config.py:32`), `core.run.ScanResult`, `core.config.WardlineConfig`. + - **S5 Findings, Outputs & Emit** — `core.filigree_emit` (`EmitResult`/`filigree_destination`/`filigree_disabled_reason`/`filigree_api_base_url`, `federation_status.py:28`, `dossier_client.py:28`), `core.finding` (`Finding`/`FINGERPRINT_SCHEME`/`SuppressionState`, `legis.py:47`). + - **S7 Trust Evidence & Judge** — `core.attest.git_state` (provenance for the signed legis artifact, `legis.py:45`), `core.dossier` seams (`LinkagesSection`/`TicketRef`/`WorkSection`, `dossier_sources.py:20`, `dossier_client.py:26`). + - **S8 Identity & SEI** — `core.identity` (`ContentStatus`/`EntityBinding`/`IdentityStatus`/`content_status`, `identity.py:30`, `dossier_sources.py:21`, `dossier_client.py:30`). + - **S3 Taint Engine** — `core.taints.TaintState` (the trust-tier lattice that seeds legis `TRUST_TIERS`, `legis.py:50,78`). **S1 Scanner Engine** — `scanner.context.AnalysisContext` (`facts.py:32`, TYPE_CHECKING). + - **Stdlib/extra only:** `urllib`/`hmac`/`hashlib`/`secrets`/`json`/`subprocess` (git, in legis); `blake3` is the sole third-party dep, lazy behind `wardline[loomweave]` (`__init__.py:16`). + +**Patterns Observed:** +- **Stdlib-urllib-only, byte-exact replicas of sibling verifiers.** No `requests`/`httpx`. Each HMAC canonical message is pinned to a named line in the sibling's source (`_hmac.py:5-7` cites Rust `auth.rs:220-234`; `legis.py:7-9` cites legis `canonical.py`/`signing.py`/`ingest.py`) and locked by golden vectors + contract-freeze tests. +- **Three distinct auth schemes, one transport.** Loomweave = per-request HMAC (`X-Weft-Component`, fresh nonce, `client.py:209-215`); legis = a `hmac-sha256:v2:` artifact signature over canonical JSON (`legis.py:114`, agent posts the bytes); Filigree = opt-in `Authorization: Bearer` over loopback (`config.py` explicitly "no HMAC"). Credentials are env/`.env`-only, never `weft.toml`. +- **Soft-vs-loud band routing as a first-class contract.** `WeftHttp.fetch` converts any HTTP status to `HttpResult` but lets `URLError`/`OSError` propagate; `_send` (`client.py:193`) maps outage/`>=500`/`403` → soft `None`, `_require_ok` raises `LoomweaveError` on other non-2xx; SEI reads use a separate `_send_json_soft` that fail-softs every non-happy band (`client.py:378`) so a pre-SEI sibling degrades, never crashes. +- **Opaque SEI, two orthogonal axes, fail-closed capability detection.** The SEI (`loomweave:eid:`) is carried verbatim and compared only by equality (`identity.py:17-21`); identity (alive/orphaned/unavailable) and content (fresh/stale/unknown) are never inferred from each other; `SeiCapability`/`TaintStoreCapability.from_capabilities` default to unsupported on any malformed body (`identity.py:51-86`). +- **Injectable `Transport` Protocol seam.** Every client takes a `transport`/double so no test touches the network (`client.py:44`, `dossier_client.py:41`); the `urlopen` symbol is resolved live at call time to preserve the monkeypatch seam (`http.py:73-75`). + +**Concerns:** +- **`wardline-18499aaa2d` (P3, open) is largely satisfied in source but stale-open.** The ticket asked to "extract shared WeftHttp" from three hand-rolled clients; `WeftHttp` (`core/http.py:47`) now exists and is consumed by all three (`loomweave/client.py:54`, `filigree/dossier_client.py:52`, `core/filigree_emit.py:613,620`). The RESIDUAL duplication is the per-client adapter shell — a near-identical `UrllibTransport` + module-local `Response` dataclass + `Transport` Protocol re-declared in each (`loomweave/client.py:39-65`, `filigree/dossier_client.py:35-62`, `core/filigree_emit.py:602-638`). The watch-item framing ("legis hand-rolls a transport") is inaccurate: legis has NO HTTP client (`legis.py:28`), so the three transport clients are loomweave/filigree-dossier/filigree-emit, not legis. +- **`wardline-80e457bc41` (P2, open) is also largely consolidated.** `core/federation_status.py` IS the single status-envelope builder and is now consumed by CLI (`cli/scan._filigree_status:663`), scan jobs (`core/scan_jobs._filigree_status:262`), the one-shot workflow (`core/scan_file_workflow._emit_to_dict:49`), and MCP (`mcp/server._filigree_emit_status:138`). The residual is thin per-surface wrapper functions; the ticket stays open for them. Both tickets warrant a triage close-or-rescope, not net-new code. +- **Replica-drift is the standing risk of the whole subsystem.** Correctness depends on byte-for-byte fidelity to EXTERNAL sources (Loomweave Rust HMAC, legis canonical/signing/ingest). Mitigated by golden vectors, contract-freeze tests, and the armed `sei_drift`/`worklist_drift` source-byte CI (`_live_oracle.py:18`), but the coupling is real and silent on the unhappy path. +- **legis fail-OPEN is acknowledged in-source, owned across a boundary.** A silent producer-key rename (`FINDINGS_FIELD`/`DIRTY_FIELD`, `legis.py:60-74`) would route zero defects into legis under a green `verified` status; legis reads those keys with defaults. The fail-open is legis's to close, the drift trigger is Wardline's; only a contract-freeze test prevents it. +- **`write_taint_facts` is non-atomic across chunks (`client.py:281-306`).** A mid-batch soft failure leaves earlier chunks committed while `written` undercounts; the documented remedy is re-running the whole scan. Correct-by-design for an idempotent per-entity replace, but a caller foot-gun worth surfacing. +- **Token loaders are themselves duplicated** across `loomweave/config.py:16`, `filigree/config.py`, and `legis.load_legis_artifact_key` (`legis.py:135`) (each mirrors `attest_key`) — four near-identical env/`.env` readers; minor, lower priority than the two tracked tickets. + +**Confidence:** High — Read all 15 owned files in full; derived every inbound edge from Loomweave `entity_callers_list` (WeftHttp, build_legis_artifact, filigree_emit_status, LoomweaveClient, FiligreeWorkProvider, LoomweaveLinkageProvider, write_facts_to_loomweave) and corroborated outbound edges against actual import lines with file:line; verified both tracked tickets via `filigree issue_get` and cross-checked the WeftHttp usage in the S5 emitter (`core/filigree_emit.py:602-638`). One minor inference: the exact S-label of `core.taints.TaintState` (the trust-tier lattice) is attributed to S3 by judgment, not a manifest. diff --git a/docs/arch-analysis-2026-06-28-0749/temp/catalog-spec.md b/docs/arch-analysis-2026-06-28-0749/temp/catalog-spec.md new file mode 100644 index 00000000..63c47a12 --- /dev/null +++ b/docs/arch-analysis-2026-06-28-0749/temp/catalog-spec.md @@ -0,0 +1,79 @@ +# Catalog Contract (read this fully before writing your entry) + +You are one of 12 parallel `codebase-explorer` agents documenting the **wardline** codebase +(`/home/john/wardline`, a Python semantic-tainting static analyzer @ commit `e4668abc`). You own **one +subsystem**. Produce **one schema-conforming catalog entry** and write it to your assigned +`temp/catalog-SX.md` file. Do not analyze files outside your assigned list (other agents own them), but +you MUST name cross-subsystem dependencies using the shared labels below. + +## The 12 subsystems (use these exact labels for all cross-references) + +| ID | Label | Owns (summary) | +|----|-------|----------------| +| S1 | **Scanner Engine** | AST walk / pipeline / analysis context / grammar / index | +| S2 | **Rule Lattice** | the 26 PY-WL rules + rule metadata/severity + the 3 runtime trust decorators | +| S3 | **Taint Engine** | callgraph, propagation, summaries, providers, fixed point | +| S4 | **Core Orchestration & Config** | run_scan/gate, scan jobs, config + schema, ruleset, discovery, registry, errors, protocols, path confinement (`safe_paths.py`) | +| S5 | **Findings, Outputs & Emit** | Finding model, JSONL/SARIF/Filigree emit, agent-summary, explain | +| S6 | **Gate Discipline & Remediation** | baseline, waivers, suppression, triage, delta scan, autofix | +| S7 | **Trust Evidence & Judge** | attest, assure, dossier, judge, decorator-coverage posture | +| S8 | **Identity & SEI** | finding/node identity, fingerprint, qualname, SEI resolution, rekey | +| S9 | **Federation Clients** | HMAC HTTP, federation-status, legis, Loomweave + Filigree clients, live oracle | +| S10 | **MCP & LSP Server** | JSON-RPC MCP-over-stdio server, tools/resources/prompts, LSP diagnostics | +| S11 | **CLI & Install/Activation** | click command surface + agent enablement (pre-commit, .mcp.json, packs, skills) | +| S12 | **Rust Frontend** | tree-sitter Rust command-injection preview | + +## MANDATES (these are what make the catalog trustworthy) + +1. **Dependencies come from the graph, not from guessing.** This repo has a fresh Loomweave index + (`mcp__loomweave__*`). For your subsystem's key entities, derive Inbound/Outbound edges from + `entity_callers_list`, `entity_neighborhood_get`, and `entity_relation_list` — get ids via + `entity_find` / `entity_at`. Corroborate with real `Read`s (cite `file:line`). **Do NOT infer + dependencies from `import` lines alone.** Map each cross-subsystem edge to one of the S1–S12 labels above. +2. **Every claim carries evidence.** Cite `path:line` from files you actually read. No file:line → mark it + an inference and lower confidence. +3. **Corroborate the known layering violation if it touches you.** The import-linter contract says the + taint engine must not import the attestation layer, but `scanner/pipeline.py` and + `scanner/taint/project_resolver.py` import `wardline.core.attest` (tracked as `wardline-9ec283d168`). + If you own S1, S3, or S7, confirm this from source with exact `file:line` (don't just restate the comment). +4. **Read meaningfully, not exhaustively.** You don't have to read every line of every file; read enough + to characterize responsibility, key components, patterns, and concerns with evidence. Big files: read + the top + the public surface + anything your dependency probes flag. +5. **NEVER run git.** Do not run `git` in any form — no status/log/diff/add/stash/checkout/restore/reset/commit. + You are read-only on the tree except for your single `temp/catalog-SX.md` output file. +6. **Return channel:** WRITE your full entry to `docs/arch-analysis-2026-06-28-0749/temp/catalog-SX.md` + (replace X with your number). Your final chat message back should be ≤5 lines: the subsystem label, + confidence, and the 1–2 most important concerns. The file is the deliverable; the message is a receipt. + +## Catalog entry template (fill exactly this shape) + +```markdown +## SX —