diff --git a/tools/corpus_engine.py b/tools/corpus_engine.py index fdbd0805..51ff3247 100755 --- a/tools/corpus_engine.py +++ b/tools/corpus_engine.py @@ -120,6 +120,9 @@ SAVE_LIVE_MIGRATION_AUTHORITY_RE = re.compile( r"migrat(?:e|es|ed|ing|ion|ions)\b", re.I | re.S, ) +SAVE_CLAIM_UNIT_BOUNDARY_RE = re.compile( + r"\n[ \t]*\n|(?=^[ \t]*(?:[-*+]|\d+[.)])[ \t]+)", re.M +) BINDING_DOORWAY_RE = re.compile( r"^Type: (law|spec)$|^Status:|^## Decisions log|^## Acceptance criteria", re.M, @@ -160,6 +163,20 @@ def heading_slugs(text: str) -> set[str]: return {slugify_heading(h) for h in HEADING_RE.findall(text)} +def save_claim_units(text: str) -> list[tuple[int, str]]: + """Return prose paragraphs and Markdown list items with source offsets.""" + units: list[tuple[int, str]] = [] + start = 0 + for boundary in SAVE_CLAIM_UNIT_BOUNDARY_RE.finditer(text): + end = boundary.start() + if end > start and text[start:end].strip(): + units.append((start, text[start:end])) + start = boundary.end() + if start < len(text) and text[start:].strip(): + units.append((start, text[start:])) + return units + + class Engine: def __init__(self, root: Path): self.root = root.resolve() @@ -566,25 +583,35 @@ class Engine: "pre-release loader accepts only the exact current " "schema — name that gate or explicit release-era history" ) - for chunk_match in re.finditer(r"[^.;]+", text): - chunk = chunk_match.group(0) - nums = SAVE_VERSION_NUM_RE.findall(chunk) - if not any(int(n) != current for n in nums): - continue - if not SAVE_FLAVOR_RE.search(chunk): - continue - if not SAVE_MIGRATION_VERB_RE.search(chunk): - continue - if SAVE_CLAIM_HISTORY_RE.search(chunk): - continue - line = text.count("\n", 0, chunk_match.start()) + 1 - claim = " ".join(chunk.split())[:90] - self.bad( - f"{rel}:{line} couples an old save version to live " - f"persistence/migration behavior ('{claim}') but only the " - f"current save version (v{current}) loads — retire the " - "claim to git history or re-word to the current format" - ) + for unit_start, unit in save_claim_units(text): + clauses = list(re.finditer(r"[^.;]+", unit)) + for index, anchor_match in enumerate(clauses): + anchor = anchor_match.group(0) + nums = SAVE_VERSION_NUM_RE.findall(anchor) + if not any(int(n) != current for n in nums): + continue + if not SAVE_FLAVOR_RE.search(anchor): + continue + claim_end = anchor_match.end() + if not SAVE_MIGRATION_VERB_RE.search(anchor): + if index + 1 >= len(clauses): + continue + continuation = clauses[index + 1] + if not SAVE_MIGRATION_VERB_RE.search(continuation.group(0)): + continue + claim_end = continuation.end() + claim_text = unit[anchor_match.start() : claim_end] + if SAVE_CLAIM_HISTORY_RE.search(claim_text): + continue + offset = unit_start + anchor_match.start() + line = text.count("\n", 0, offset) + 1 + claim = " ".join(claim_text.split())[:120] + self.bad( + f"{rel}:{line} couples an old save version to live " + f"persistence/migration behavior ('{claim}') but only the " + f"current save version (v{current}) loads — retire the " + "claim to git history or re-word to the current format" + ) def validate_current_build_line_count(self) -> None: """current-build.md's workspace line-count claim must track the tree. diff --git a/tools/test_corpus_engine.sh b/tools/test_corpus_engine.sh index 2d05675f..6c5f53f1 100755 --- a/tools/test_corpus_engine.sh +++ b/tools/test_corpus_engine.sh @@ -392,6 +392,31 @@ cat >> "$root/wiki/vision/law.md" <<'EOF' Old saves' v20 three-entry arrays load by padding the fourth slot with zero. EOF assert_fails wiki-save-claim-padding --root "$root" --corpus +# A wrapped list item remains one claim when the migration verb moves into a +# second sentence, matching the research.md recurrence that escaped the gate. +cp "$root/wiki/vision/law.md.orig" "$root/wiki/vision/law.md" +cat >> "$root/wiki/vision/law.md" <<'EOF' + +- Ordinary v20 saves may use three entries: Efficiency, Tradecraft, + Perception. Load pads the Routing slot with zero and writes four entries. +EOF +assert_fails wiki-save-claim-padding-followup --root "$root" --corpus +# Separate list items must not combine an old format fact with current policy. +cp "$root/wiki/vision/law.md.orig" "$root/wiki/vision/law.md" +cat >> "$root/wiki/vision/law.md" <<'EOF' + +- Save v20 recorded three research tracks. +- Current saves persist four research tracks. +EOF +assert_ok wiki-save-claim-adjacent-items --root "$root" --corpus +# A migration verb in the preceding sentence does not rewrite a later landed- +# version fact into a live compatibility claim. +cp "$root/wiki/vision/law.md.orig" "$root/wiki/vision/law.md" +cat >> "$root/wiki/vision/law.md" <<'EOF' + +The current loader accepts only SAVE_VERSION. Save v20 introduced four tracks. +EOF +assert_ok wiki-save-claim-preceding-verb --root "$root" --corpus # Dated logs are historical records and exempt. cp "$root/wiki/vision/law.md.orig" "$root/wiki/vision/law.md" mkdir -p "$root/wiki/log" diff --git a/wiki/log/2026-07-23-research-save-claim.md b/wiki/log/2026-07-23-research-save-claim.md new file mode 100644 index 00000000..034b2605 --- /dev/null +++ b/wiki/log/2026-07-23-research-save-claim.md @@ -0,0 +1,46 @@ +# Research save-claim boundary + +``` +Type: log +Date: 2026-07-23 +Scope: wiki/mechanics/research.md, tools/corpus_engine.py +``` + +## Intent + +Close the preserved research save-claim finding: make the binding spec describe +the exact-current pre-release loader, then make the same multi-sentence drift +mechanically reject itself. + +## Finding + +The prior audit repaired criterion 6 and the status note, but the live **Save +compatibility** section still promised that ordinary v20 three-entry arrays load +by padding Routing with zero, upgrade, and write back four entries. Current +`save.rs` rejects every version except `SAVE_VERSION` before full +deserialization, and `deserialize_compatible_track_array` accepts exactly four +entries. The old three-entry padding path survives only in git history. + +The save-claim checker already recognized `loads by` and `pad`, but split prose +at every period. The old version lived in one sentence and the migration verbs +in the next, so neither fragment contained the whole stale claim. + +## Act + +The research spec now states one current four-track format and explicitly retires +the v20 padding path. The corpus checker evaluates a complete prose paragraph or +Markdown list item instead of isolated semicolon/period fragments. Wrapped +continuation lines therefore stay with their claim, while adjacent list items do +not contaminate one another. + +Fixtures reproduce the exact two-sentence research wording and preserve the +boundary between an old-format historical fact in one item and current policy in +the next. + +## Defense + +A save compatibility claim is often grammatically split: one sentence names the +old shape and a following sentence says what load does. Statement-level checking +must follow that Markdown unit rather than punctuation. Explicit `retired`, +`superseded`, or history context remains the escape hatch for truthful historical +records, and dated logs remain exempt. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index e1143043..0f5411ea 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -16,6 +16,11 @@ add or amend a session log, then re-run the generator. - Intent: Complete Voss criterion 8 without a scripted kill button: make blood one player-reachable consequence of the existing social, schedule, carried-work, physical-evidence, and containment systems. - Log: [wiki/log/2026-07-23-voss-blood-route.md](2026-07-23-voss-blood-route.md) +## 2026-07-23 - Research save-claim boundary + +- Intent: Close the preserved research save-claim finding: make the binding spec describe the exact-current pre-release loader, then make the same multi-sentence drift mechanically reject itself. +- Log: [wiki/log/2026-07-23-research-save-claim.md](2026-07-23-research-save-claim.md) + ## 2026-07-23 - Premise re-audit: keep the objective boundary open - Intent: Keep the premise's Objective layer aligned with existing issue #14 without changing either design or runtime. diff --git a/wiki/mechanics/research.md b/wiki/mechanics/research.md index 46b7e1fc..51105e02 100644 --- a/wiki/mechanics/research.md +++ b/wiki/mechanics/research.md @@ -157,15 +157,11 @@ enforces the semantics. ### Save compatibility -Serialized research levels/progress use `Track::ALL` order. The compatibility -boundary accepts exactly two array shapes at load: - -- Current saves use four entries: Efficiency, Tradecraft, Perception, Routing. -- Ordinary v20 saves may use three entries: Efficiency, Tradecraft, - Perception. Load pads the Routing slot with zero, upgrades to the current - save version, and writes back only the four-entry shape. - -Any other research level/progress array length is rejected. +Serialized research levels/progress use `Track::ALL` order. The exact current +schema requires four entries: Efficiency, Tradecraft, Perception, and Routing. +Any other research level/progress array length is rejected. The retired v20 +three-entry padding path lives only in git history with the numbered migration +ladder; it is not an accepted pre-release load shape. ## Player surface diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 42532687..ebe48597 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -57,7 +57,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/mechanics/core.md` | 2026-07-18 | finding | re-audit: criteria 2-5 pins still hold (`overhead_charged_before_allocation`, `loss_with_fallback_rolls_back`, `migration_takes_time_and_moves_host`), the 2026-07-18 current-version-only status note matches save.rs, and criterion 1 stays honestly deferred to rollback; the gap was criterion 6 — the spec demands storage/host capability rejection at fallback designation, but B1 machines carry no capability body and `add_fallback_at` accepts any spare, with no deferral note; status note now marks criterion 6 decided-not-yet-runtime, dispatched to hardware-capability-bodies | | `wiki/mechanics/cursor.md` | 2026-07-18 | finding | re-audit: `fog_at` precedence, `inspect`, `person_label`/`observer_label`/`person_glyph` gates, `Sensor.sees`/`hears` flags, and the cursor's absence from the save all still verify (prior pins stand); the drift was two phrases presenting the retired versioned-migration rule as current (criterion 1's parenthetical, the design-notes save-format bullet) — both now state the current-version-only policy and the release-era ladder owed at first public release; number-free wording, so the save-claim gate could not see it | | `wiki/mechanics/intel.md` | 2026-07-21 | finding | the Storage B alternate route existed only in prose. Fire 131 connects it to Ray's real 23:00 schedule, one exact carried records-box target, the canonical bounded opaque buffer, and ordinary PROCESS consequence; retrieval cannot duplicate the file or reveal Marcus's debt, and v45 preserves/validates exact custody — [log](../log/2026-07-21-storage-b-records.md). Prior recursive custody, magnitude, and consequence-first audits stand. | -| `wiki/mechanics/research.md` | 2026-07-18 | finding | re-audit: track table, per-level multipliers, single-active-track pin, and deterministic-progress claims still verify (constants re-checked in the same-day sim-mechanics sweep); the drift was criterion 6 still asserting the v20 three-entry padding migration loads — the phrasing ("load by padding") slipped past the save-claim gate's verb list, so both moved: the criterion now names the retirement, and the gate matches pad/padded/padding and "loads by" with a new fixture | +| `wiki/mechanics/research.md` | 2026-07-23 | finding | the live Save compatibility section survived the prior criterion repair and still promised that v20 three-entry arrays load by padding, while the current deserializer accepts exactly four tracks and the pre-release loader rejects every old version. The section now states the exact-current format, and save-claim units span wrapped paragraphs/list items so a migration verb in the following sentence cannot evade the corpus gate without explicit retired-history context — [log](../log/2026-07-23-research-save-claim.md) | | `wiki/mechanics/economy.md` + `messages.md` | 2026-07-21 | finding | Fire #146 completes the issue-#11 contract: save v47 binds every settled transfer to one immutable Email or Filing record authored by the orthogonal accounting carrier; ordinary funded TAP captures opaque message custody, PROCESS alone opens its account/flow bindings, and the direct `RawIntelKind::FinancialFlow` snapshot path is gone. Forged purchase orders now move no money when sent and settle only when Priya reads and accepts still-valid persona-bound terms; the accepted transfer emits its own ordinary record. Criterion 8 and the `financial-mail` work order are IMPLEMENTED — [log](../log/2026-07-21-financial-mail-causality.md). Capability foundation: [Fire #145](../log/2026-07-21-financial-channel-retirement.md). | | `wiki/mechanics/income.md` | 2026-07-18 | finding | Beacon feel note 5 verified as a real bug: the embedded contractor-persona field was never written, so the Moonlight card, the start-row persona pricing, and the agent status line all read a dead `None`; earning itself was correct (schemes mirrors the WORK share) but illegible at 0.0. Removed the dead field, routed every reader through `Sim::moonlight_persona` (persona-world link), added the stalled-earning card cue, regression test, and spec amendment — [log](../log/2026-07-18-moonlight-persona-card.md). Prior Wager/egress audit (2026-07-12) stands: Wager constants match (`WAGER_STAKE_CAP` 300, base 0.55, cap 0.75, mult 2x, analysis divisor 400), Marcus's $400/week arrears is the modeled creditor flow (`account.rs`) while the $8,400 principal is narrative-by-design (spec states full payoff is not a B1 requirement), egress/banked-signature present, and all 7 criteria have passing tests (moonlight payout/signature, wager outcomes/cap, egress routes, hands-beat-from-zero, busted-bankroll) | | `wiki/mechanics/schedules.md` | 2026-07-18 | clean | re-audit: `ScheduleBlock` per-instance data, `DAY_TICKS` 400, the `person_glyph` `?`-until-Schedule gate (initial at Schedule/Leverage), and located-witnessing claims all still verify; nothing drifted since the 2026-07-12 prose fix. Prior verdict: mechanism matches code (`ScheduleBlock` start/end/room, single `DAY_TICKS`=400 clock, `person_glyph` `?`-until-Schedule, erratic day-hash drift, per-instance data, all 5 criteria have passing tests); tightened stale Act One prose — Ray patrols dock/stairwell/storage not "corridors", Priya has no "office-off-plane" block, Voss's two blocks are both server-room — to match `People::act_one` | @@ -91,5 +91,4 @@ Types are the five from [tick.md](tick.md): violation, contradiction, question, bug, insecurity — plus `gate` for a checker owed to the recurrence-promotes-to-the-gate rule. -- 2026-07-23 · gate · `wiki/mechanics/research.md` + save-claim checker · the live Save compatibility section still says v20 arrays load by padding despite exact-current-only loading and its own criterion 6; extend the checker for this phrasing and retire the stale section. - 2026-07-23 · bug · Operations workspace selection · the unpaused live object rail still binds selection only to a row index, so reorder or disappearance can silently retarget before confirmation or an immediate action; Bevy also lacks the promised pointer back path. Reimplement the intent of stranded commit `308f98cf` against the current modular UI rather than replaying its obsolete diff.