# v2.0 release notes draft

This file stages the v2.0 release notes for the lock ceremony.
Until the ceremony, it lives here so it does not collide with the
`## [X.Y.Z]` section check in `scripts/release.py`. At lock time,
the maintainer moves the section below into `CHANGELOG.md` as
`## [2.0.0] - YYYY-MM-DD`, populates the date, and runs the
release script.

The draft is inventory-style, not feature-list. v2.0 ships no
new behavior; it locks the surface that the v1.9.x bridge
sequence validated.

---

## [2.0.0] - YYYY-MM-DD (DRAFT — do not move into CHANGELOG.md until lock)

**v2.0 schema-lock-and-tag ceremony.** Locks the public surface
that the v1.9.x bridge sequence validated. No new features, no
new fingerprints, no schema additions beyond the experimental →
stable promotions listed below. Every surface in
`docs/stability.md` carries the **Stable** guarantee from this
tag forward: breaking it requires a major version bump and a
deprecation window in the prior minor release.

For the validation evidence behind the lock, see
`validation/v2.0-validation-summary.md`. For the corpus-firing
truth-of-record, see `validation/v2.0-corpus-run.md`. For the
security-audit closure trail, see
`docs/security-audit-resolutions.md`.

### Promoted from experimental to stable

Each surface below shipped pre-v2.0 with an `EXPERIMENTAL` label.
For most, only the label is removed at the lock. The one exception
is `slug_confidences`, whose shape was changed from a positional
`[slug, posterior]` pair array to an object map `{slug: posterior}`
in the pre-lock schema-hardening pass (SH2, v1.9.92) precisely so it
would not have to be reshaped after the freeze. See the schema-lock
disposition table and the SH track in `docs/roadmap.md` for the
per-surface rationale. The introducing commit is linked so a reader
can verify the surface shipped where this table claims it did.

| Surface | Introduced | Introducing commit |
|---|---|---|
| `--fusion` CLI flag | v0.11 → v1.9.0 (fusion layer) | [`65cedb0`](https://github.com/blisspixel/recon/commit/65cedb0) |
| `--explain-dag` CLI flag | v1.9.0 | [`65cedb0`](https://github.com/blisspixel/recon/commit/65cedb0) |
| `slug_confidences` JSON field | v0.11 | [`890454b`](https://github.com/blisspixel/recon/commit/890454b) |
| `posterior_observations` JSON field | v1.9.0 | [`65cedb0`](https://github.com/blisspixel/recon/commit/65cedb0) |
| MCP `get_posteriors` tool | v1.9.0 | [`65cedb0`](https://github.com/blisspixel/recon/commit/65cedb0) |
| MCP `explain_dag` tool | v1.9.0 | [`65cedb0`](https://github.com/blisspixel/recon/commit/65cedb0) |
| `chain_motifs` JSON field | v1.7.0 | [`8bafe3f`](https://github.com/blisspixel/recon/commit/8bafe3f) |
| `wildcard_sibling_clusters` field (in `cert_summary`) | v1.7.0 | [`8bafe3f`](https://github.com/blisspixel/recon/commit/8bafe3f) |
| `deployment_bursts` field (in `cert_summary`) | v1.7.0 | [`8bafe3f`](https://github.com/blisspixel/recon/commit/8bafe3f) |
| `infrastructure_clusters` JSON field | v1.8.0 | [`3088b9a`](https://github.com/blisspixel/recon/commit/3088b9a) |
| `ecosystem_hyperedges` JSON field (batch wrapper) | v1.8.0 | [`3088b9a`](https://github.com/blisspixel/recon/commit/3088b9a) |
| `evidence_conflicts` JSON field | v1.7.0 | [`8bafe3f`](https://github.com/blisspixel/recon/commit/8bafe3f) |

### Schema lock

- `docs/recon-schema.json` description bumped from "Stable v1.0
  contract" to "Stable v2.0 contract."
- The locked surface was prepared by the pre-2.0 schema-hardening
  track (SH1 to SH9, v1.9.91 to v1.9.95), from a four-lens pre-lock
  review. It reshaped the experimental and v2.0-new fields into forms
  that can grow additively rather than forcing a future major bump:
  `slug_confidences` to an object map (parallel to `detection_scores`),
  `wildcard_sibling_clusters` to named `{names: [...]}` objects, plus
  new fields `fusion_enabled`, a `record_type` and `schema_version`
  discriminator on every object-shaped output mode, and a
  machine-readable `error_kind` on the batch error record. The CT
  telemetry fields (`ct_provider_used`, `ct_cache_age_days`,
  `ct_attempt_outcome`) moved out of the required set, the
  `cloud_instance` enum was opened, and `--include-ecosystem` now
  always emits its wrapper. The bulk of the v1.0 field set is unchanged
  in shape and semantics.
- The stable JSON fields previously covered by the v1.0 contract
  remain covered; the Bayesian fusion fields (`slug_confidences`,
  `posterior_observations`) and the v1.7 / v1.8 structural fields
  move into the stable set per the table above.
- The data-file schemas (`fingerprints`, `signals`, `profiles`,
  per-domain CT cache, TenantInfo result cache,
  `RECON_CONFIG_DIR` environment variable) remain stable as
  documented in `docs/stability.md`.

### MCP tools

All 22 MCP tools listed in `docs/stability.md` remain stable.
The two MCP fusion tools (`get_posteriors`, `explain_dag`) move
from experimental to stable per the promotion table above.

### CLI commands and flags

Every CLI command and flag listed as **Stable** in
`docs/stability.md` (as of v1.9.14) carries that guarantee
forward. The two CLI fusion flags (`--fusion`, `--explain-dag`)
move from experimental to stable per the promotion table above.

### Default behavior change: Bayesian fusion is now default-on

Through v1.9.x, the Bayesian inference layer was opt-in behind
`--fusion`. v2.0 flips the default. The math runs on every
lookup; the default panel keeps the same clean shape it has had
since v1.x; the credible interval surfaces in `--json` always
and in the panel only when it modifies the rule-based verdict.
Users who want the v1.x rule-based-only output can pass
`--no-fusion`.

| Behavior | v1.x | v2.0 |
|---|---|---|
| Bayesian inference computation | Opt-in via `--fusion` | Always-on |
| `posterior_observations` in `--json` | Only when `--fusion` set | Always present |
| `slug_confidences` in `--json` | Only when `--fusion` set | Always present |
| Credible interval in default panel | Not shown (panel has no fusion path) | Shown only when `sparse=true` on a reported node OR the posterior mode disagrees with the deterministic slug |
| Credible interval under `--verbose` | Shown when combined with `--fusion` | Always shown |
| Opt-out flag | n/a | `--no-fusion` (new; documented stable) |
| `--fusion` flag | Required to enable | Deprecated no-op, kept for v2.0.x back-compat, removed in v2.1 |
| `--explain-dag` | Opt-in, implies `--fusion` | Opt-in, unchanged |

The disclosure rule means the default panel is quiet when the
rule-based and Bayesian layers agree (the easy case, roughly 70
percent of corpus runs) and loud when they disagree (hardened
targets, sparse evidence, conflicting signals). v1.9.4
hardened-adversarial validation showed 64 percent sparse-flag
firing on hardened targets; that is the population the rule is
designed to escalate.

The change is additive to `--json`: every consumer that ignores
unknown fields continues to work without modification. SIEM
mappings that target the rule-based slugs (`docs/schema.md`
canonical field set) need no change. SIEM consumers who want
the v1.x rule-based-only payload can pass `--no-fusion`.

### Python support

Tested range: Python 3.11 through 3.14. The floor moved from 3.10 to 3.12 in the
engineering-elevation series (v1.9.29) and was relaxed back to 3.11 in v1.9.43 so
3.11 consumers can still install while 3.11 receives security fixes; 3.14 was
added to the CI matrix in v1.9.28.

### Bayesian network

The v1.9.10 stability disposition table holds at v2.0. All 9
nodes are `stable`; `okta_idp` is kept with the corpus-exposure
caveat recorded in `recon_tool/data/bayesian_network.yaml`. No
topology change, no CPT change since v1.9.6.

### Security

No new security work in v2.0. The audit trail spans the original
May-2026 cycle (closed through v1.9.4 / v1.9.9 / v1.9.13 / v1.9.14)
plus six follow-on adversarial rounds: rounds 1 to 5 in v1.9.15 to
v1.9.21 (internal-DNS-leak guards, CT / BIMI-VMC ingestion, MCP / CLI /
output injection, DoS / resource caps, detector isolation) and round
six in v1.9.81 (output-sink control stripping for source-derived
service strings and the DMARC `p=` value). All are recorded in
`docs/security-audit-resolutions.md`. Scanner re-flags against
pre-closure commits are resolved in favor of the closure record;
this is the project's authoritative disposition policy.

| Topic | Severity | Closed in | Closing commit |
|---|---|---|---|
| MCP doctor/install can execute shadowed `recon_tool` package | High | v1.9.9 | [v1.9.9 tag](https://github.com/blisspixel/recon/releases/tag/v1.9.9) |
| A/AAAA CNAME validation can trigger internal DNS lookups | Medium | v1.9.4 (initial) | [`6abbae1`](https://github.com/blisspixel/recon/commit/6abbae1) |
| Same finding, re-introduced by v1.9.13 terminus check | Medium | v1.9.14 (re-closure on type-dependent-answer path) | [`2eed239`](https://github.com/blisspixel/recon/commit/2eed239) |
| CNAME chain walking can query and leak internal DNS names | Medium | Mitigated v1.9.3.5 / v1.9.4 / v1.9.13 / v1.9.14 (residual documented) | [`f8b12dd`](https://github.com/blisspixel/recon/commit/f8b12dd), [`6abbae1`](https://github.com/blisspixel/recon/commit/6abbae1), [`bbf68e7`](https://github.com/blisspixel/recon/commit/bbf68e7), [`2eed239`](https://github.com/blisspixel/recon/commit/2eed239) |
| Splunk SIEM example uses unescaped regex slugs | Informational | v1.9.4 | [`6abbae1`](https://github.com/blisspixel/recon/commit/6abbae1) |

### Pre-v2.0 release sequence (validation evidence)

The v1.9.x bridge sequence cleared the per-release gates in
`docs/roadmap.md`'s release table. Each row below lists the
release, the gate cleared, the introducing commit, and the
validation memo a reviewer can read to verify the work landed
as described.

| Release | Gate cleared | Tag | Memo |
|---|---|---|---|
| v1.9.3 | Bayesian-network topology surgery | [v1.9.3](https://github.com/blisspixel/recon/releases/tag/v1.9.3) | `validation/v1.9.3-calibration.md` |
| v1.9.4 | Hardened-adversarial validation across 50-domain stratified corpus | [v1.9.4](https://github.com/blisspixel/recon/releases/tag/v1.9.4) | `validation/v1.9.4-calibration.md` |
| v1.9.5 | Per-node stability dispositions (8 of 9 stable; `okta_idp` `not yet`) | [v1.9.5](https://github.com/blisspixel/recon/releases/tag/v1.9.5) | `validation/v1.9.5-stability.md` |
| v1.9.6 | CPT-change discipline + audit-finding closures | [v1.9.6](https://github.com/blisspixel/recon/releases/tag/v1.9.6) | `validation/v1.9.6-stability-update.md` |
| v1.9.7 | Metadata-coverage gate flip + 298-detection backfill | [v1.9.7](https://github.com/blisspixel/recon/releases/tag/v1.9.7) | `CHANGELOG.md` v1.9.7 |
| v1.9.8 | Catalog metadata richness on three signals | [v1.9.8](https://github.com/blisspixel/recon/releases/tag/v1.9.8) | `validation/v1.9.8-metadata-audit.md` |
| v1.9.9 | Detection-gap UX surfaces; MCP shadow-load closure on Python 3.10 | [v1.9.9](https://github.com/blisspixel/recon/releases/tag/v1.9.9) | `validation/v1.9.9-detection-gap-ux.md` |
| v1.9.10 | Stratified-corpus pre-lock validation; Bayesian re-validation | [v1.9.10](https://github.com/blisspixel/recon/releases/tag/v1.9.10) | `validation/v1.9.10-pre-lock.md` |
| v1.9.11 | Documentation polish + v2.0 prep worklist cleared | [v1.9.11](https://github.com/blisspixel/recon/releases/tag/v1.9.11) | `validation/v1.9.11-trend-table.md` |
| v1.9.12 | Panel-display polish + doctor schema-fields + Mermaid evidence-DAG | [v1.9.12](https://github.com/blisspixel/recon/releases/tag/v1.9.12) | `CHANGELOG.md` v1.9.12 |
| v1.9.13 | CNAME chain walker hardening (entry-point + redirect_domain filter) | [v1.9.13](https://github.com/blisspixel/recon/releases/tag/v1.9.13) | `docs/security-audit-resolutions.md` |
| v1.9.14 | Revert v1.9.13 terminus-only A/AAAA check after type-dependent-answer path re-opened v1.9.4 leak | [v1.9.14](https://github.com/blisspixel/recon/releases/tag/v1.9.14) | `validation/v2.0-corpus-run.md`, `validation/v2.0-validation-summary.md` |

The work after v1.9.14 is captured thematically below by version range;
per-release detail (one entry per tag) lives in `CHANGELOG.md`, which is the
exhaustive record. Each range cleared the same per-patch discipline: one story
per patch, the full CI gate (ruff + pyright + pytest branch-coverage + the
fingerprint/label validators), version bump last.

| Release range | Theme | Reference |
|---|---|---|
| v1.9.15 to v1.9.21 | Security-audit rounds 1 to 5: generalized internal-DNS-leak guards, CT / BIMI-VMC ingestion hardening (SSRF, ANSI/newline injection), MCP / CLI / output-injection, DoS / resource caps, detector exception isolation | `docs/security-audit-resolutions.md` |
| v1.9.16, v1.9.20, v1.9.70 | Dependency-advisory hygiene (idna, starlette, pyjwt; documented ignore for the disputed no-fix pyjwt advisory) | `CHANGELOG.md`, `docs/security-audit-resolutions.md` |
| v1.9.22 to v1.9.25 | Fingerprint catalog expansion (459 to 808 entries across cname_target / TXT / SPF / MX / NS / DMARC-rua) plus CT-pipeline resilience (AIMD limiter, circuit breaker, multi-session retry) | `CHANGELOG.md` |
| v1.9.26 | Schema-contract polish: JSON schema scoped to single-domain success output; batch / NDJSON error-record shape declared | `validation/v1.9.26-schema-contract.md` |
| v1.9.27 | MCP-onboarding UX: `recon doctor --client=<name>` config verification across the six supported clients | `CHANGELOG.md` |
| v1.9.28 to v1.9.37 | Engineering-elevation series: `py.typed`, Python floor moves, branch coverage, `deal` Design-by-Contract (inference core + matchers), build-provenance attestation, PEP 735 dependency groups, the C901 complexity gate | `CHANGELOG.md` |
| v1.9.38 to v1.9.54 | Pre-2.0 hardening: golden-output characterization tests and the full `formatter.py` decomposition (`render_tenant_panel`, the markdown renderer, the categorizers) under the C901 cap, output held byte-identical | `CHANGELOG.md` |
| v1.9.55 to v1.9.60 | CLI / agent quality-of-life (Track E): exit-code reference, `batch` stdin, shell-completion docs, autoApprove guidance, the `recon://schema` discovery resource, data-not-instructions demarcation | `CHANGELOG.md` |
| v1.9.61 to v1.9.71 | Validator / loader / `sources/dns` / `cert_providers` / `explain_insights` decomposition, plus Bayesian-core calibration (CAL7 evidence-group correction, CAL14 node-dependent missingness, spf_strict recalibration) | `CHANGELOG.md`, `docs/correlation.md` |
| v1.9.72 to v1.9.76 | Complexity decomposition complete (Track A): the behaviour-heavy tail (`signals_show`, `_doctor`, `_lookup`, `_batch`, `merge_results`, the `server` pair) decomposed; zero `# noqa: C901` markers remain | `CHANGELOG.md` |
| v1.9.77 to v1.9.80 | Test and validation rigor complete (Track B): `deal` contracts on the boundary validators, the cache-lifecycle Hypothesis stateful machine, source-boundary fault injection, and the server-tool coverage lift | `CHANGELOG.md` |
| v1.9.81 | Round-six ingestion audit (Track D): output-sink control stripping for source-derived service strings, the DMARC `p=` value, and `google_idp_name` | `docs/security-audit-resolutions.md` |
| v1.9.82 to v1.9.86 | Catalog growth (Track C, C1): 21 vetted `cname_target` fingerprints from local live-analysis batches, 808 to 829 entries, fictional-brand examples only and no real-company data committed | `CHANGELOG.md` |
| v1.9.87 to v1.9.89 | Catalog growth (Track C, C2): full-corpus gap mining over a 5,241-domain scan, 13 vetted vendors merged (829 to 841 entries), org-internal GSLB termini correctly excluded; aggregate-only trail | `CHANGELOG.md`, `validation/v1.9.87-c2-corpus-batch.md` |
| v1.9.90 | Bayesian node-dependent missingness (CAL14): the email-policy node becomes declarative (absence of a public DMARC / SPF / MTA-STS declaration is disconfirming evidence), with a corpus-grounded prior and likelihoods; the synthetic conditional ECE for the node drops from about 0.31 to about 0.03 | `CHANGELOG.md`, `validation/cal14-missingness-design.md`, `docs/correlation.md` |
| v1.9.91 to v1.9.95 | Pre-2.0 schema hardening (SH1 to SH9) from a four-lens pre-lock review: description reconciliations, CT telemetry out of the required set, `cloud_instance` enum dropped, `slug_confidences` to an object map, `wildcard_sibling_clusters` to named objects, a `fusion_enabled` flag, the `record_type` and `schema_version` discriminator, machine-readable `error_kind`, and the `--include-ecosystem` always-wrapper fix | `CHANGELOG.md`, `docs/roadmap.md` |

The real-corpus validation gate
(`validation/v2.0-corpus-run.md`) was populated against v1.9.14
with 4264 counted domains; multi-cloud rollup fires on 37.2%,
the `okta_idp` Bayesian-network node clears the
criterion-(c) N≥10 threshold by an order of magnitude (134
strict slug firings, 543 with the lower-tier signal included),
and the ceiling-firing divergence vs synthetic corpus is
documented as structural (sparse synthetic fixtures vs rich
real apexes).

### Lock-ceremony checklist (maintainer)

- [x] `validation/v2.0-corpus-run.md` populated (no `<TO BE POPULATED>` strings remain). Done in commit `cca815c` from the v1.9.14 corpus scan.
- [ ] `docs/recon-schema.json` description bumped from "v1.0 contract" to "v2.0 contract."
- [ ] Bayesian-fusion default-on flip applied. Code changes:
      - Remove `if fusion or explain_dag:` gate at `recon_tool/cli.py:2374`; compute Bayesian inference unconditionally.
      - Add `--no-fusion` opt-out flag with typer `--fusion/--no-fusion` pair semantics (default True).
      - Gate panel-rendering of the credible interval in `recon_tool/formatter.py` on `--verbose` OR on the disagreement rule (`sparse=true` on a reported node, or posterior-mode/deterministic-slug disagreement).
      - Update `docs/stability.md` `--fusion` row to "Bayesian fusion control (default on v2.0+; `--no-fusion` opts out)" and document `--no-fusion` as a stable surface.
      - Update README example panel to show both the easy case (panel unchanged from v1.x) and the disagreement case (interval surfaces inline).
- [ ] This section moved from `validation/v2.0-release-notes-draft.md` into `CHANGELOG.md` under the next `## [2.0.0] - <date>` heading; date populated to the day the script runs.
- [ ] `scripts/release.py` from a clean `main` checkout. Script enforces ruff / pyright / pytest gates and prompts before tagging.
- [ ] After tag push, GHA `release.yml` runs test → build → publish-pypi (OIDC) → github-release.
- [ ] After PyPI publish completes, delete this draft file (`rm validation/v2.0-release-notes-draft.md`).

The script does the version bump, commit, tag, and push. The
GHA workflow does the PyPI upload via the existing OIDC
Trusted Publisher; no static tokens involved.
