# Release 0.4.7 — Downgrade rendering-equivalent spans instead of spending the bullet budget

**Coordinated packages:** `ds4-context-core`, `ds4-context-reference-adapter`, and `ds4-context-engine` 0.4.7.
**Implementation commit:** `1483bce`.

## Summary

Behaviour patch that removes the source of the recurrent fail-closed fallback.
Until 0.4.6 the repair treated a re-rendered exact value exactly like an invented
one: it deleted whole bullets, and beyond eight affected bullets it refused to
repair at all and handed the session to Pi. The class reports had shown for four
releases that this was the common case — rendering and composition, not
invention. The repair now retracts only the backticks of a span whose information
the evidence holds in another rendering, keeps the text and the fact, and
re-validates. Invented values, unrelated compositions, one-character deviations,
case changes and unanalysed spans keep the existing fail-closed bullet removal.
Validation strictness, the eight-bullet and 25% bounds, the fail-closed fallback,
SQLite schema 16, canonical JSONL history and every provider-facing default are
unchanged.

## Changes

- `packages/core/src/compaction/summary-contract.ts`:
  - `RENDERING_EQUIVALENT_RELATIONS` names the relations that mean "the evidence
    holds this information in a different rendering": `escaped-form-present`,
    `unescaped-form-present`, `whitespace-collapsed-present`,
    `typographic-variant-present` and `composed-adjacent-present`.
  - Deliberately excluded, because prose would hide a wrong claim rather than a
    re-rendered one: `case-variant-present` (a case change can alter an
    identifier), `single-deletion-present` (a one-character deviation is
    indistinguishable from a wrong value), `composed-two-present-parts` (the
    evidence never contains that association) and every `not-classified-*`
    relation (*not analysed* is not evidence of support).
  - `downgradeRenderingEquivalentSpans` removes the two backticks of each
    rendering-equivalent span, byte-range based and applied after the bullet
    removals so every span is matched at its final position.
  - `analyzeUnsupportedExactValueBullets` partitions the rejected spans, applies
    the unchanged bounds to the ones that still need removal, and reports the
    new counters. `ExactValuePruneResult` and `ExactValuePruneAttempt` gained
    `downgradedSpans`, and the attempt status gained `downgraded` and
    `unrepairable`.
  - The span classifier is no longer described as observation-only: it is
    `analyzeSpanClasses` internally, and its relations now also select the repair
    treatment. Validation still never consults them, and the public
    `classifyUnsupportedExactValueSpans` report is unchanged.
- `src/pi-adapter/summary-generator.ts`: the repair outcome is recorded as counts
  only — `unsupported-exact-bullets-pruned` for removed bullets and the new
  `unsupported-exact-spans-unquoted` for retracted quotes.
- `tests/unit/summary-contract.test.ts`: five cases — a composed span with parts
  adjacent in one source is downgraded and the summary still validates; a span
  whose whitespace-collapsed and JSON-unescaped renderings are present is
  downgraded too; unrelated compositions and one-character deviations still cost
  their bullet; twelve affected bullets downgrade where the same summary without
  the adjacent rendering still answers `too-many-bullets`; a mixed summary
  removes only the one bullet that needs it and downgrades the other nine.
- `tests/integration/compaction.test.ts`: eleven affected bullets complete the
  compaction with `validationStatus: "warning"` and
  `validationIssueCodes: ["unsupported-exact-spans-unquoted"]` instead of falling
  back, and the same fixture with the two parts at unrelated positions still
  returns `repair=too-many-bullets` with Pi compacting the session.
- `docs/COMPACTION.md`, `docs/ADR/068-downgrade-rendering-equivalent-spans.md`
  and the ADR index record the decision.

## Why

The affected-bullet series from production was 30/20 → 23/19 → 19/15 → 17/11 →
16/12, every run ending in `compaction.custom_fallback` with
`unsupported-exact-value; repair=too-many-bullets`. The class-only reports
attributed the rejected spans to rendering or composition in every interpretable
run: `escaped-form-present` 11 of 16 in the first, then composition 9 of 14 with
2 one-character deviations, 2 absences and 1 over-length span in the second.
`no-near-miss` — the class that means invention — was the smallest in both.

Each earlier patch removed one class (escaped equivalence in 0.4.4, prompt
grading in 0.4.5) while the bound stayed exhausted by the rest, so the fallback
persisted. The information in those spans was present all along; only the
verbatim surface form was not, and discarding a supported fact — or losing the
compaction — is the wrong response to a rendering difference. The repair now
applies deterministically the ladder the 0.4.5 prompt asks the model to follow,
instead of depending on the model complying
([ADR 068](ADR/068-downgrade-rendering-equivalent-spans.md)).

## Privacy and safety

- Only the backticks are removed. Span text is never logged, notified or
  persisted in diagnostics: the recorded effects are two counters, and the
  failure path keeps the class-only report.
- No provider call is added: the downgrade happens after generation, on text the
  model already produced.
- The downgraded text is sanitized output that already passed privacy handling;
  removing backticks does not change its classification.

## Compatibility and persistence

- No configuration key is added or changed. `downgradedSpans` is additive on the
  repair result, and `unsupported-exact-spans-unquoted` is a new issue code in
  the recorded node details.
- The eight-bullet and 25% bounds are not raised: they now count only the bullets
  that still need removal. A span downgraded in place consumes no bullet.
- SQLite schema 16, migrations, the Pi JSONL boundary, tool contracts and the
  reference adapter's public surface are untouched.

## Measured scope and limitations

- Verified by fixtures, not yet by a natural production failure: the first real
  `compaction.custom_fallback` (or its absence) after updating the failing host
  remains the end-to-end confirmation.
- The fallback is still possible — deliberately — when more than eight bullets
  contain genuinely unsupported spans or when removal would exceed 25% of the
  summary. Those are the cases where the evidence does not hold the information.
- A downgraded value is still stated, as prose. DS4 stops asserting exactness it
  cannot verify but does not delete the fact, and the counters make the trade
  visible.
- The repair now consumes the bounded class analysis (default 24000
  evidence-source scans). A span left unanalysed is never downgraded, so budget
  exhaustion keeps the previous fail-closed behaviour.
- `case-variant-present` is intentionally not downgraded, so summaries that only
  differ by letter case still take the bullet-removal path. If production reports
  show that class dominating, that is the next measured decision.

## Validation

- `npm run check` on the release commit: builds, `tsc --noEmit`, and
  **100 test files / 649 tests passed** (7 new: 5 unit cases for the repair rule
  and 2 integration cases, one of which pins the unchanged fallback on unrelated
  composition).
- `npm run pack:check` in a clean consumer: `ds4-context-core@0.4.7`
  (251 files), `ds4-context-reference-adapter@0.4.7` (7 files) and
  `ds4-context-engine@0.4.7` (110 files).
- `npm run quality:compare`: candidate `task-weighted-v0.2-candidate` 0.9875
  against the frozen baseline `static-ranking-v0.1` 0.808156, unchanged.
- `npm run schema:context-persistence` (`passed: true`).
- No provider calls are involved. `jev_verify` is not available in this
  environment (project verification disabled), so the checks above were run
  directly.

## Publication and registry verification

Published manually in dependency order from `bf221b5`: `ds4-context-core@0.4.7`,
then `ds4-context-reference-adapter@0.4.7`, then `ds4-context-engine@0.4.7`, all
with the default `latest` tag, after `npm run check` and `npm run pack:check` on
the same commit.

`npm run registry:check -- 0.4.7` needed five attempts, each with a fresh npm
cache, before the registry CDN served the new packuments (the known propagation
delay after a publish); the fifth reported "Verified all DS4 registry packages at
exact version 0.4.7". All three packages install in a clean consumer, the exact
adapter/core dependencies resolve, the public core and KV exports import, the
compiled reference conformance and packaged quality corpus run, the
`ds4-context-storage` CLI shim responds, and the published Pi extension starts
against isolated offline RPC state. `npm view <package> dist-tags.latest`
resolves to 0.4.7 for all three packages.

Annotated tag `v0.4.7` and the GitHub Release were created from `bf221b5`:
<https://github.com/Alucard24/ds4-context-engine/releases/tag/v0.4.7>. No session
data, credentials, provider payloads or prompt text were read or written while
preparing this release, and no provider call is involved in the release
procedure.
