← All studies

Sidestreet · a pre-registered ablation

What does each retrieval layer buy, and what does strictness cost a discovery engine?

16.7%
of the engine's good answers would be put out of reach by restoring the strict filter, to prevent a quality loss the study could not find.[8.0%, 26.6%] · 95% interval, 10,000 resamples of 28 queries · 31 of 186 good-answer pairs; 28 of 40 queries contribute · decided 2026-09-28

Sidestreet's retrieval engine once required a venue to carry an occasion label before it could be returned, and that filter was removed in July 2026. I ran a pre-registered ablation to measure what the filter had cost and what each other retrieval layer contributes.

Restoring the filter moved judged list quality from 3.93 to 3.97 on a 5-point scale, inside the run-to-run spread. A good answer is a venue in a list the engine's judge scored 4 or 5 in most of 10 baseline runs, a classification checked against 30 human-graded items.

Figure 1

The layer stack, and where each arm cuts

The layer stack, and where each arm cuts Diagram of the engine's six layers in query order: Query interpretation, removed by A5; Routing, not cut by a single arm; SQL hard filter, removed by A3; Semantic matching, removed by A4; Cluster layer, removed by A7; L3 scoring and re-sort, removed by A6. Two further arms are not single cuts: A1a and A1b restore a removed filter, A2 replaces the whole stack with one model call. The diagram plots no data. query in Query interpretation one model call parses the query into structured fields A5 Routing name matching, or contextual matching SQL hard filter structural and Google-sourced constraints only A3 Semantic matching embedding similarity, plus content-native expansion A4 Cluster layer membership scoring and a 1.15x boost A7 L3 scoring and re-sort 18 blend variants, then 10 multipliers A6 ranked venues out Two further arms are not single cuts: A1a and A1b restore a removed filter, A2 replaces the whole stack with one model call.
The layer stack, and where each arm cuts Diagram of the engine's six layers in query order: Query interpretation, removed by A5; Routing, not cut by a single arm; SQL hard filter, removed by A3; Semantic matching, removed by A4; Cluster layer, removed by A7; L3 scoring and re-sort, removed by A6. Two further arms are not single cuts: A1a and A1b restore a removed filter, A2 replaces the whole stack with one model call. The diagram plots no data. query in Query interpretation one model call parses the query into structured fields A5 Routing name matching, or contextual matching SQL hard filter structural and Google-sourced constraints only A3 Semantic matching embedding similarity, plus content-native expansion A4 Cluster layer membership scoring and a 1.15x boost A7 L3 scoring and re-sort 18 blend variants, then 10 multipliers A6 ranked venues out Two further arms are not single cuts: A1a and A1b restore a removed filter, A2 replaces the whole stack with one model call.

Scroll sideways to see the whole figure.

The engine's layers in the order a query passes through them, with the arm that removes each one. A1a and A1b restore a removed filter, and A2 replaces the whole stack with 1 model call.

Removal costs are in figure 3 and the arm table. The diagram plots no data.

Source: sidestreet-ablation-study/specs/restyle_layers.py. Drawn by hand and restyled to the house spec by that script.

Findings

  • Restoring the strict occasion filter puts about a sixth of the engine's good answers out of reach.

    16.7% [8.0, 26.6]; 31 of 186 good-answer pairs; the only strict arm that cleared the stability gate

  • Restoring the filter moved judged list quality by 0.04 points, inside the run-to-run spread.

    3.93 to 3.97 on a 5-point scale; per-run judge scores are not published, so this does not recompute from the repo

  • A single run of the engine captures 55.5% of the venues that 10 identical runs reach for a query.

    8.47 venues per query at 1 run, 15.28 at 10 and still rising; 611 venue-query pairs on 40 queries

Figure 2

Venues a query can return, by number of identical runs

Venues a query can return, by number of identical runs Line chart of the mean number of venues a query can return after 1 to 10 identical runs, over 40 queries: 1 8.47, 2 10.44, 3 11.65, 4 12.51, 5 13.18, 6 13.72, 7 14.18, 8 14.59, 9 14.95, 10 15.28. One run reaches 55.5% of ten, and the curve is still rising at ten. 0 4 8 12 16 1 2 3 4 5 6 7 8 9 10 identical runs venues per query, mean of 40 1 run: 8.47 venues per query, 55.5% of the 15.28 that ten runs reach 2 runs: 10.44 venues per query 3 runs: 11.65 venues per query 4 runs: 12.51 venues per query 5 runs: 13.18 venues per query 6 runs: 13.72 venues per query 7 runs: 14.18 venues per query 8 runs: 14.59 venues per query 9 runs: 14.95 venues per query 10 runs: 15.28 venues per query 1 run: 8.47, 55.5% of ten 15.28
Venues a query can return, by number of identical runs Line chart of the mean number of venues a query can return after 1 to 10 identical runs, over 40 queries: 1 8.47, 2 10.44, 3 11.65, 4 12.51, 5 13.18, 6 13.72, 7 14.18, 8 14.59, 9 14.95, 10 15.28. One run reaches 55.5% of ten, and the curve is still rising at ten. 0 4 8 12 16 1 2 3 4 5 6 7 8 9 10 identical runs venues per query, mean of 40 1 run: 8.47 venues per query, 55.5% of the 15.28 that ten runs reach 2 runs: 10.44 venues per query 3 runs: 11.65 venues per query 4 runs: 12.51 venues per query 5 runs: 13.18 venues per query 6 runs: 13.72 venues per query 7 runs: 14.18 venues per query 8 runs: 14.59 venues per query 9 runs: 14.95 venues per query 10 runs: 15.28 venues per query 1 run: 8.47, 55.5% of ten 15.28

Scroll sideways to see the whole figure.

Mean number of venues a query can return after a given number of identical baseline runs, from 1 to 10, over 40 queries and 611 venue-query pairs. Each point averages over every subset of that many of the 10 runs, so the curve does not depend on run order.

One run returns 8.47 venues per query, which is 55.5% of the 15.28 that 10 runs reach, and the curve is still rising at 10. The emphasized mark is the single run.

Source: sidestreet-ablation-study/specs/make_figures.py. Data in derived/a0_appearance_counts.csv.

  • Removing query interpretation costs the most good answers of any layer, and removing the cluster layer costs the least.

    69.4% [52.8, 84.5] against 11.3% [4.5, 20.0]; both arms ungated, so both figures are directional

Figure 3

Share of good answers lost when one layer is removed

Share of good answers lost when one layer is removed, per arm, with 95% intervals over queries Dot-and-whisker chart, one row per arm, of the share of 186 good answers made unreachable, with 95% query-bootstrap intervals. A0 baseline, 0 by definition; A7 cluster layer 11.3% [4.5, 20.0] ungated; A1a strict filter restored 16.7% [8.0, 26.6] passed; A3 SQL hard filter 39.8% [27.4, 53.7] ungated; A4 semantic matching 44.1% [30.4, 57.4] ungated; A6 L3 scoring and re-sort 51.6% [39.2, 63.6] ungated; A5 query interpretation 69.4% [52.8, 84.5] ungated; A2 single pass over index 87.1% [79.5, 93.4] ungated; A1b not compared, it failed the stability gate. 0% 20% 40% 60% 80% 100% share of the 186 good answers made unreachable A0 baseline A0 baseline: 0 by definition 0 by definition A7 cluster layer A7 cluster layer: 11.3% of the 186 good answers unreachable, 95% interval 4.5 to 20.0, ungated 11.3% [4.5, 20.0] ungated A1a strict filter restored A1a strict filter restored: 16.7% of the 186 good answers unreachable, 95% interval 8.0 to 26.6, passed 16.7% [8.0, 26.6] passed A3 SQL hard filter A3 SQL hard filter: 39.8% of the 186 good answers unreachable, 95% interval 27.4 to 53.7, ungated 39.8% [27.4, 53.7] ungated A4 semantic matching A4 semantic matching: 44.1% of the 186 good answers unreachable, 95% interval 30.4 to 57.4, ungated 44.1% [30.4, 57.4] ungated A6 L3 scoring and re-sort A6 L3 scoring and re-sort: 51.6% of the 186 good answers unreachable, 95% interval 39.2 to 63.6, ungated 51.6% [39.2, 63.6] ungated A5 query interpretation A5 query interpretation: 69.4% of the 186 good answers unreachable, 95% interval 52.8 to 84.5, ungated 69.4% [52.8, 84.5] ungated A2 single pass over index A2 single pass over index: 87.1% of the 186 good answers unreachable, 95% interval 79.5 to 93.4, ungated 87.1% [79.5, 93.4] ungated A1b strict filter, faithful not compared: failed the stability gate
Share of good answers lost when one layer is removed, per arm, with 95% intervals over queries Dot-and-whisker chart, one row per arm, of the share of 186 good answers made unreachable, with 95% query-bootstrap intervals. A0 baseline, 0 by definition; A7 cluster layer 11.3% [4.5, 20.0] ungated; A1a strict filter restored 16.7% [8.0, 26.6] passed; A3 SQL hard filter 39.8% [27.4, 53.7] ungated; A4 semantic matching 44.1% [30.4, 57.4] ungated; A6 L3 scoring and re-sort 51.6% [39.2, 63.6] ungated; A5 query interpretation 69.4% [52.8, 84.5] ungated; A2 single pass over index 87.1% [79.5, 93.4] ungated; A1b not compared, it failed the stability gate. A0 baseline 0 by definition A0 baseline: 0 by definition A7 cluster layer 11.3% [4.5, 20.0] ungated A7 cluster layer: 11.3% of the 186 good answers unreachable, 95% interval 4.5 to 20.0, ungated A1a strict filter restored 16.7% [8.0, 26.6] passed A1a strict filter restored: 16.7% of the 186 good answers unreachable, 95% interval 8.0 to 26.6, passed A3 SQL hard filter 39.8% [27.4, 53.7] ungated A3 SQL hard filter: 39.8% of the 186 good answers unreachable, 95% interval 27.4 to 53.7, ungated A4 semantic matching 44.1% [30.4, 57.4] ungated A4 semantic matching: 44.1% of the 186 good answers unreachable, 95% interval 30.4 to 57.4, ungated A6 L3 scoring and re-sort 51.6% [39.2, 63.6] ungated A6 L3 scoring and re-sort: 51.6% of the 186 good answers unreachable, 95% interval 39.2 to 63.6, ungated A5 query interpretation 69.4% [52.8, 84.5] ungated A5 query interpretation: 69.4% of the 186 good answers unreachable, 95% interval 52.8 to 84.5, ungated A2 single pass over index 87.1% [79.5, 93.4] ungated A2 single pass over index: 87.1% of the 186 good answers unreachable, 95% interval 79.5 to 93.4, ungated A1b strict filter, faithful not compared: failed the stability gate 0% 20% 40% 60% 80% 100% share of the 186 good answers made unreachable

Scroll sideways to see the whole figure.

Share of the engine's 186 good answers made unreachable when 1 layer is removed or the strict filter is restored, per arm. Dots are point estimates, and whiskers are 95% intervals from 10,000 resamples of the 28 queries that contribute good answers, under seed 20261011.

Only A1a, at 16.7% [8.0, 26.6], cleared the stability gate. A2 through A7 are ungated and directional, A1b failed the gate and is not compared, and A0 loses nothing by definition. The rows do not add, because each removes 1 layer with the others intact.

A5, query interpretation, is the emphasized mark, at 69.4% [52.8, 84.5], ungated.

Source: sidestreet-ablation-study/specs/make_figures.py. Data in derived/arm_reachability.csv.

  • The headline metric covers only the 28 of 40 queries that have a good answer, so it says nothing about the 12 the engine answers badly.

    186 pairs from 28 queries; the metric and the answer-set rule were frozen separately and together drop the other 12

  • The arm built to measure the expansion stage's coverage failed the pre-registered stability gate and was not compared.

    Median split-half Jaccard 0.867 against a 0.90 bar; the stability table needs per-run data that is not published

  • The engine's judge returned a passing score whenever its own call failed.

    Fallback signature in 13 of 240 query-runs (5.4%) before the fix and 0 of 280 after; the patch is published, the run counts are not

  • A 30-item blind human check agreed with the judge's good-answer classification on 26 items, and the disagreements did not lean one way.

    Judge higher on 5 items, human higher on 6; on 19 items where 2 model graders disagreed, the human sided with the judge 17 times; I graded it, so it was not blind to intent

What it changed

Should the strict occasion filter be restored?

The ablation showed the strict filter was blocking answers the judge rated good, while judged quality barely differed between the arms: 3.93 and 3.97 on the judge's 5-point scale, inside the noise. Restoring the filter would put 16.7% [8.0%, 26.6%] of good answers out of reach to prevent a quality loss the study could not find. I kept it off.

Decided · what happened after: not yet recorded

Informed by: A1a, the one strict arm that cleared the stability gate. Recorded 2026-10-11 in docs/DECISION_RECORD.md; the decision entered the draft on 2026-09-28 as one line, the rationale followed on 2026-10-05, and the interval is from the 2026-10-11 verifier run.

What was measured

The engine is layered. Query interpretation runs as its own model stage, routing then sends the query to name matching or contextual matching under a loose hard filter, results are re-sorted, and a final stage scores them and labels the match logic. Each of the 9 arms removes 1 stage or restores the old filter, and every arm ran against 40 held-out benchmark queries.

The metric is recall of good answers lost. Of the venues the engine’s own judge grades as good under the full architecture, it counts the share an arm can no longer return. A good answer is a venue that appeared in a list the judge scored 4 or 5 in a majority of 10 baseline runs, which gives 186 venue-query pairs from 28 queries. Intervals resample those 28 queries 10,000 times under a fixed seed, because pairs within a query are not independent.

Answer sets are majority-rule sets over repeated runs. The engine sets no temperature or seed at any model call site, so the same query returns different venues from run to run, and a single run proved too variable to stand for the engine. A single run captures 55.5% of the venues 10 identical runs reach, so across the roughly 30 engine versions benchmarked on single runs, movement inside that spread was never signal.

A stability gate was fixed before the arms ran: an arm had to reproduce its own answer set across independent halves of its runs, at a mean split-half Jaccard of 0.70 and a median of 0.90 on a 5-vs-5 split, before it could be compared. It admitted the baseline and 1 strict arm, and the other 6 arms ran 5 times where the gate needs 10.

The strict arm reconstructs the July filter by running the original July prompt over the benchmark to recover the filter’s own routing decisions, then applying its database predicates unchanged. The routing decisions were resolved in advance by majority vote over repeated parses and frozen as a table.

What the other layers appear to buy

Arm Layer removed Good answers lost 95% interval Judged quality Gate
A0 nothing, baseline 0% 3.93 passed
A1a strict filter restored 16.7% 8.0 to 26.6 3.97 passed
A7 cluster layer 11.3% 4.5 to 20.0 3.88 ungated
A3 SQL hard filter 39.8% 27.4 to 53.7 4.04 ungated
A4 semantic matching 44.1% 30.4 to 57.4 3.82 ungated
A6 L3 scoring and re-sort 51.6% 39.2 to 63.6 3.53 ungated
A5 query interpretation 69.4% 52.8 to 84.5 2.34 ungated
A2 single pass over the index 87.1% 79.5 to 93.4 not reported ungated
A1b strict filter, faithful not compared 3.82 failed, 0.867 median

The 6 ungated arms ran 5 times where the gate needs 10, so their figures are directional. Each row removes 1 layer with the others intact, so the rows cannot be summed into a decomposition of where quality lives. Removing a layer can also raise the average score of what survives while shrinking what survives: A3 loses 39.8% of good answers and has the highest judged quality of any arm, at 4.04. Only A5 degrades on both measures, by a margin no other arm approaches.

The cluster layer is the most elaborate machinery in the system, with 144 emergent clusters over the catalog, 32,535 membership rows, a similarity table of 3.9 million rows and a boost applied at query time. Removing both of its mechanisms cost 11.3% [4.5, 20.0], about a sixth of what removing the query parse cost. Ablating it took a code change at 2 sites, because the system’s own feature flag disabled 1 mechanism and swapped a legacy scorer in for the other. The metric also cannot see what the layer might do for result diversity, which this study did not measure.

Why the architecture is layered at all

The corpus the engine retrieves over is 4,682,278 tokens, 23.4 times a 200,000-token context window, so a single model call over the whole corpus cannot be run. The single-pass arm therefore runs on the largest representation that fits, a 70,003-token structured index of name, type, neighborhood, rating and review count, and loses 87.1% of good answers. That loss is a floor on what layering buys, since the index is a weak stand-in for the reviews and editorial content behind it. Given that index and asked only to return matching venues, the model returned venues that are not in it for 3.3% of the names it returned.

What the checks caught

The engine’s judge returned a passing score of 3 whenever its own call failed, through 3 layers that each preserved the behavior, and its output truncated on about half of large result sets. The fix cut that fallback from 5.4% of query-runs to 0 and moved the mean score from 3.95 to 3.83.

The arm built to measure what the content-native expansion stage buys in coverage failed the stability gate on its median, 0.867 against 0.90, and was excluded from comparison. Its provisional coverage figure, 8.5 points of recovered good answers, was the number this study most wanted, and it is not in the results. What the pairing showed instead, directionally, is that removing the stage made the arm less able to reproduce its own answer set.

2 arms built exactly as frozen would not have measured what they claimed. The strict arm leaked 15.8% of venues that failed its filter, through the expansion stage, which was introduced 1 commit after the filter reached peak strictness. The SQL filter arm changed nothing, because 23 further conditions are appended after the line it was specified to change. Verifying each arm’s output before running it caught both.

The standing process requires a smoke pilot before the freeze, and this piece’s module list left it out. Written afterward as assertions on properties of the output and pointed at data that already existed, it found 5 defects in 1 pass, 2 of which the build phase had missed and none of which crashed anything.

The benchmark harness truncated venue names at 33 characters before printing them, and the analysis matched on those names, so 144 of 5,751 venues could not be matched. Long names cluster among libraries, botanic gardens, park visitor centers and restaurants with descriptive names, so the defect would have read as the strict filter removing civic and cultural venues.

The judge’s call seeds its reply with an opening brace to get bare JSON, and the model tier used to cross-check it rejects that technique outright. The first cross-check run failed on all 60 items for that reason, and the workaround gave the cross-check a different prompt shape, which weakens the comparison in exchange for making it possible.

That cross-check, a different model tier given only the query and the returned venues, disagreed with the judge’s good-answer classification on 19 of its 60 items (32%) and rated systematically lower. Within the 30-item human-check sample the 2 models disagreed on 19 items (63%), and the human sided with the judge 17 times and with the cross-check twice. The cross-check had been deprived of the judge’s context to make it independent, and that made it the weaker grader.

How layer isolation was arrived at

The layered architecture accumulated one separation at a time, as each part of what began as a single retrieval call failed in a way that could not be diagnosed from outside. The first design labeled every venue at ingest from a fixed list of 11 occasion labels and 3 sequence roles, the labels built the clusters, and the clusters built the relationship edges, so one wrong label could reshape the graph around it. Labels also carried 70% to 78% of the ranking weight, which buried venues the list described badly and kept new venues with no collaborative-filtering history out of results.

I spent several rounds repairing this inside the same framework, with better lists, prompts and filters, and each round made the framework more expensive to abandon. The repairs improved the labels without improving retrieval: a request for cheap late-night food after midnight came back with museums, and the public library ranked ahead of cafes for espresso and pastry. One pass was parsing the query, matching names, filtering on meaning and pulling related results, and a wrong answer gave no way to tell which of the 4 had failed.

The repair rounds ended when the premise turned out to be wrong: the query has the intent and the venue has attributes, and the first design had fused the 2 at ingest. Each layer that followed had to emit an intermediate result that could be inspected and blamed on its own, which is why the evaluation harness this study runs on scores intent, retrieval and result quality separately. Query interpretation took 7 weeks to separate, from its own model stage on June 20 to the label leaving its output schema on August 8. By the time the strict filter came out, on July 13 and 14, nobody could say which layer was carrying quality, because quality had only been measured through all of the layers at once.

Reproducing it

The arm percentages, their intervals and the convergence figures recompute from 3 CSVs in derived/ with python3 specs/verify_published.py, which reads nothing else and exits non-zero on any disagreement, and specs/make_figures.py draws figures 2 and 3 from the same files. The split-half stability table and the judge quality column need per-run data that is not published. The engine is not published either, which limits a reader to re-checking the published figures. The verifier’s first run caught the second wrong version of the single-run figure, which had already passed an independent audit.

Limits

  • The judge scores lists and the metric counts venues. A venue counts as a good answer when it appeared in a list scored 4 or 5, no venue was judged on its own, and the direction of the resulting error is not established. The human check graded the same unit, so it does not bound this.
  • The judge shares a model snapshot with the stages it grades, including the labeling pass the strict arm filters on. The 30-item human check narrows this. I graded it myself, so it was not blind to what the engine is meant to do, and a second independent grader has not been run.
  • 6 of 9 arms ran 5 times where the stability gate needs 10, so everything attributed to them is directional. Measured at the 2-vs-2 split available to them, the baseline itself fails the gate at 0.708 mean and 0.800 median.
  • The arm table does not add. Each row removes 1 layer with the others intact, and by my account of the system's development, which no committed record documents, the layers interact, so the cost of removing 2 layers is unmeasured.
  • The strict arm runs the byte-identical July prompt on a different model from the one that ran it in July, and its routing input is a modal vote over a nondeterministic parse, frozen in advance as a table that records the vote counts.
  • Engine and judge spend was $63.26, measured, against a ceiling frozen at $40 with a $6 per-arm cap and raised to $110 on 2026-09-28.
  • To afford 10 runs of 3 arms, the study cached the query-interpretation system prompt in the study copy, which modifies the system under study. Every arm ran with it, so no arm comparison is affected, and the single-run figure is a floor on the uncached engine's variability.
  • 3 pre-registered commitments are not met. Unscored queries were measured and not reported as a covariate (section 1.6), the metric was never crossed against review count, content-unit count or cluster depth (section 1.7), and every figure is pooled where per-query pairing was promised (section 1.8).
  • The pre-registration's counts of 2,302 unlabeled and 81 never-evaluated venues do not reproduce under any predicate tried. The freeze is published with the discrepancy intact, and the write-up reports the recomputed 2,245 and 99 with their predicate stated.
  • 1 of the original filter's 5 branches is reachable from no routing rule and never executed.
  • The split-half stability table and the judge quality column need per-run data that is not published, so they cannot be recomputed from the repo.
Check this study yourself
Freeze
freeze-piece-two at 373c881
Pre-registered
2026-09-25, UTC date of commit 373c881, made 2026-09-25 00:41 -07:00
Timestamp
git-dated only; the date rests on author-controlled git metadata, and no third-party timestamp attestation exists for this freeze.
# run 2026-10-13 against a local clone of the study repo at b1ef6ef
$ git rev-parse freeze-piece-two^{commit}
373c881f3c0fbcc93d2ad96b60367ad5509a14f9
$ git log -1 --format=%cI freeze-piece-two
2026-09-25T00:41:09-07:00
$ python3 specs/verify_published.py | tail -1
  all checks passed
PASS freeze-piece-two resolves to 373c881, committed 2026-09-25 07:41:09 UTC

Evidence sources

  • Published-figure verifier sidestreet-ablation-study/specs/verify_published.pyReads only derived/; 30 checks OK, exit 0, run 2026-10-13 at b1ef6ef; repo private until release; links activate then.Recomputes the denominator, every arm percentage and interval, and the convergence figures, and exits non-zero on any disagreement.
  • Derived answer sets sidestreet-ablation-study/derived/arm_reachability.csvWith good_answer_set.csv and a0_appearance_counts.csv, about 248KB; repo private until release; links activate then.The 186-pair good-answer denominator, each arm's reachability over it, and per-pair appearance counts across the 10 baseline runs.
  • Pre-registration sidestreet-ablation-study/docs/FREEZE.mdTag freeze-piece-two, commit 373c881; 5 dated amendments; repo private until release; links activate then.Arm set, good-answer rule, stability gate, runs per arm, pairing plan, analysis code and cost ceiling, fixed before any arm ran.
  • Human check, as graded sidestreet-ablation-study/docs/spotcheck.html30 items, blind, stratified toward the score boundary; repo private until release; links activate then.The sheet and grades behind the 26-of-30 agreement and the 17-of-19 split against the model cross-check.
  • Judge fix sidestreet-ablation-study/specs/judge-fix.patch.mdThe one engine change published with the study; repo private until release; links activate then.Shows the 3 layers that turned a failed judge call into a passing score, and the change that fixed them.
  • Findings record sidestreet-ablation-study/docs/FINDINGS.md28 findings, recorded as they happened; repo private until release; links activate then.The study's own account of each defect and correction, with dated notes where a figure changed.
  • Decision record sidestreet-ablation-study/docs/DECISION_RECORD.mdDecided 2026-09-28; recorded 2026-10-11; repo private until release; links activate then.The question, the decision and its rationale, with the dated provenance of each part. The decision is my own account.
  • Write-up sidestreet-ablation-study/docs/PIECE_TWO_DRAFT.mdDraft 2026-10-11; repo private until release; links activate then.The full argument, with the judged-quality and stability tables that do not recompute from the repo.