perf: junction limits scale — executor, rev cache, linear П3/П4, shared masonry pass (#330)

Six cuts, zero verdict changes (spec §3; equivalence pinned by units, the
parity suite and the smokes):

- §4.1 the CPU chain of ws_config_set and ws_plan_optimize runs in the
  executor; write_lock still serialises writes, only the HA event loop is
  freed (2.8 s of blocking per 576-atom write before).
- §4.2 the stored document's violation counts are cached on the runtime by
  rev (store.py junction_baseline); a repeated write never re-judges
  `previous`. validate_junction_limits takes baseline_counts and returns the
  candidate's counts to cache after a successful save.
- §4.3 П3 builds its node index once per check in both mirrors
  (289→11 ms TS, 285→~50 ms py).
- §4.5 П4 uses a bucket grid with the threshold as cell size in both
  mirrors (104→19 ms TS, 372→44 ms py); pair enumeration switches to
  lexicographic order — same verdict set, equivalence pinned against a
  brute-force oracle on cell borders.
- §4.6 a document already carrying the current catalogue is judged as-is:
  a no-op re-migration cost 815 ms py / 69 ms TS. Legacy documents migrate
  exactly as before (the #329 H1 test stays green).
- §4.7 П5 shares one junction-topology pass per check and pays the masonry
  union only when multi-wall nodes exist — and the resize path hands over
  the preflight's own artifact, so a pointermove never builds the union
  twice (4.2 s → 88 ms full candidate on the benchmark grid).

The frontend baseline is cached per (document identity, config epoch): ten
pointermoves make N+1 limit computations, not 2N — pinned by the smoke on a
real pointer gesture.

demo/benchmark_junction_limits.mjs (npm run benchmark:junction-limits) pins
the budgets for both mirrors: TS full candidate ≤100 ms (measured 88), py
warm validate ≤250 ms (measured 45), cold legacy ≤3.5 s — that path is
one-off and lives in the executor.

Issue: #330
User-Visible: yes
This commit is contained in:
Codex
2026-08-28 03:07:11 +03:00
parent 92f476556f
commit c90f5bf052
15 changed files with 934 additions and 237 deletions
+38
View File
@@ -535,6 +535,44 @@ export const MUTANTS = [
replace: ' // mutant: pointer capture removed\n const plan = resolution.plan;',
}],
},
{
id: 'junction-limit-p3-quadratic-again',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node demo/benchmark_junction_limits.mjs',
because: 'П3 rebuilding its node index per segment is the exact O(n²) #330 removed '
+ '(289 ms per call on 576 atoms, twice per pointermove) — the benchmark budget '
+ 'must turn red the day it returns',
patches: [{
file: 'src/junction-limits.ts',
find: ' const units = collinearRunLengthUnits(segment, usable, byNode);',
replace: ' const units = collinearRunLengthUnits(segment, usable);',
}],
},
{
id: 'junction-limit-p4-bruteforce-again',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node demo/benchmark_junction_limits.mjs',
because: 'П4 is architecturally quadratic without the bucket grid (#330 §4.5); feeding '
+ 'every node the whole segment list instead of its cell brings the 104-372 ms cost '
+ 'back and the benchmark must catch it',
patches: [{
file: 'src/junction-limits.ts',
find: ' for (const segment of segmentGrid.get(`${cx},${cy}`) || []) {',
replace: ' for (const segment of usable) {',
}],
},
{
id: 'junction-limit-baseline-cache-stale',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="#330 AC4" test/junction-limits.test.mjs',
because: 'a baseline cache that survives a config epoch change serves verdicts of a '
+ 'plan that no longer exists — the epoch check IS the invalidation contract (#330 §4.4)',
patches: [{
file: 'src/houseplan-card.ts',
find: " if (cached && cached.epoch === this._cfgEpoch && cached.spaceId === spaceId) {",
replace: ' if (cached && cached.spaceId === spaceId) {',
}],
},
{
id: 'junction-limit-backend-raw-baseline',
guard: 'node scripts/backend-test-guard.mjs '