Skip to content

v2.2.26 — the documents say what the code, the tree and the Release page actually do (2026-09-18)

Choose a tag to compare

@github-actions github-actions released this 18 Sep 19:17
· 23 commits to main since this release

v2.2.26 — the documents say what the code, the tree and the Release page actually do (2026-09-18)

  • The published size claim was stale (README, both languages): "~4,400 lines of Python plus
    ~2,600 lines of tests" measured 7,083 / 5,617 on this machine — the engine grew ~60% past its own
    description. Corrected to the measured figures, rounded the same way the sentence rounds.
    tests/test_mechanism_docs.py now counts the tree itself and fails if either README drifts more
    than the rounding band — the figure was only able to rot because nothing was checking it.
  • The mechanism document now says how far a long run carries a redemption-rate study
    (both languages, §5). Written from a live reading rather than an expectation: the on-read buckets
    are object domain × predicted edge × actual edge, so maturity step is not one of them — joined
    against the maturity ledger it degenerates here (every checkable sample sits at the capped step,
    steps 1–3 have no samples, which is not computable, not 0). The capability-library sprout
    origin is likewise still no-samples. And of the misses, none is a mis-chosen edge, so this data
    answers "did the announced action happen", not "was the wrong edge picked" — 0 cases cannot be
    read as 1.00.
  • The privacy scanner's scope is now the publish boundary (tools/privacy_scan.py): it skipped
    a hard-coded directory list, so every local-only folder (handover notes, machine config) needed a
    code edit and forgetting one pitted the local scan against the release gate. It now honours the
    top-level names in .gitignore, the same semantics as static rule R1. Not a free pass — pinned
    with both a positive and a negative case in tests/test_privacy.py, and documented in
    docs/privacy.md / docs/zh/privacy.md.
  • The reserved long_task branch is now guarded (K16, both languages). It stays unwired —
    wiring it needs a mechanical test for "what counts as a long task", a design decision — but
    "reserved" was costing more than a comment: the flag is the only bypass of the lead limit, so a
    quiet assignment somewhere would turn that incident-borne constraint into a suggestion.
    tests/test_sprout_queue.py now fails if any engine module assigns it, and passes when the
    flag (read from an old ledger row) does grant the exemption.
  • The bucket axes in the README and the org prompt now match the code (README both languages
    + prompts/org-session.md). All three claimed "object domain × edge type × maturity step",
    while reconcile._bucket computes (object domain, predicted edge, actual edge) — so the org
    session was told to cite a bucket that cannot be computed, and the public README promised an axis
    the ledgers do not carry. tests/test_mechanism_docs.py pins the correct phrase and fails if the
    old wording returns.
  • N58-① is closed: trigger ④ keeps its row reading (settled, no behaviour change). It was the
    repository's only explicit open decision, and measuring it here settled the question rather than
    deferring it: over ticks 556–628 only 14 of 73 ticks were quiet, the longest run of consecutive
    quiet ticks was 1, and a run of ≥ 10 never occurred — so the literal "ten consecutive quiet
    ticks" reading is not a slower org cadence but an unreachable one. The verdict is recorded in
    both originals and in engine/org_trigger.py, and tests/test_mechanism_docs.py now refuses
    the old "open decision" wording on its way back.
  • A subject-state sentence in the Chinese original is now a dated snapshot
    (docs/zh/growth-subject.md). It enumerated the subject's current top-level directories as
    "journal / archive"; measured here (tick 633) they are journal and app, and archive/ only
    appears at the first journal rotation. The rule next to it (directory limit 10, and raising it is
    a judgement change) was never wrong — the observation masquerading as a fact was. It now carries
    its measurement date and points at infinigrow status for the live reading, and
    tests/test_mechanism_docs.py rejects the old enumeration on its way back.
  • A live knob that no document mentioned is now documented (frozen_review_every, default 20;
    config table + both mechanism originals + tests/test_mechanism_docs.py). The engine sweeps the
    frozen zone every N ticks so a frozen sprout can be re-lit into the active queue — it has been
    doing that since T4/A5, but the setting appeared in neither the config table nor the mechanism
    document, which made IG_FROZEN_REVIEW_EVERY undiscoverable. The new case fails if any integer
    setting is missing from that table, or if a stated default or env-var name disagrees with code:
    a missing row raises no error, it just becomes a switch nobody knows about.
  • org-check's exit code is now written down, and now has a test (both running.md files +
    core/exit_codes.py + tests/test_cli.py). It answers "not this tick" with rc=1 — the
    usage-error slot, borrowed — so a caller that treats rc as pass/fail reads it exactly
    backwards (the || true in the CI cold-start step is that workaround). The command had no test
    at all before; it now has both directions pinned (empty ledger → should_run=true/0, just-ran →
    should_run=false/1, rc and JSON forced to agree). Behaviour is unchanged: a dedicated code is
    an interface change, undecided.
  • Release titles have one source on each side of a measured boundary (tools/sync_release_notes.py
    + CONTRIBUTING.md + tests/test_release_tooling.py). The tool kept a hand-copied title for every
    version and --apply writes whatever that table says — but the table was copied from an older
    reading of the CHANGELOG. Comparing all 28 live Releases (gh release view --json name, 2026-09-18):
    through v2.2.15 the published title is the recorded string, from v2.2.16 it is the CHANGELOG.md
    heading verbatim, date included (that is what the workflow feeds gh release create). So the
    copies were a second source, and applying them would have stripped the date off ten published
    titles and reworded eleven more. title_for() now reads the boundary (TITLES_UNTIL = 2.2.15):
    recorded before it, derived from the CHANGELOG after. Tests pin both halves, the exact set of 11
    pre-boundary wordings that legitimately differ from the CHANGELOG (so nobody "aligns" a published
    Release away), and a negative case proving a hand-copied title can never win after the boundary.
  • The observation-surface limits are now pinned where they are actually written
    (tests/test_mechanism_docs.py; the limits themselves are unchanged). The existing check asked only
    whether "10" and "20" appear anywhere in the mechanism document — and both do, for unrelated
    reasons (stall alert 12 ticks, org cooldown 30 minutes, queue cap 50), so raising
    SUBJECT_DIR_LIMIT or SUBJECT_FILE_LIMIT could never redden it. The check now reads the sentences
    that state the boundary (docs/zh/growth-subject.md table and rule line, docs/growth-subject.md
    "Directory limit 10, file limit 20") against the constants, and carries its own negative case:
    the same phrase with the digits swapped must not be found. Measured: simulate SUBJECT_DIR_LIMIT=12
    and only 1 of the 4 phrases still matches (2 of 4 with SUBJECT_FILE_LIMIT=25) — the check fails,
    which is the point: docs/growth-subject.md promises that changing a limit means changing document,
    code and test together, and until now the third leg was decorative.
  • The org segment's cooldown window measures attempts, not LLM calls — the text now says so
    (engine/org_trigger.py + core/config.py + both mechanism.md originals +
    tests/test_mechanism_docs.py). The reason string read "since the last LLM segment" and the config
    table called the knob "LLM-segment gap", but the ledger behind both is written by any org
    segment: measured here, a state root that only ever ran mechanical ticks already had an
    org-llm.jsonl row at tick 1. The wording was not harmless — it implies a manual debugging tick
    costs nothing, while in fact it consumes the window and delays the real LLM segment by up to
    org_cooldown_min minutes. Behaviour is unchanged (only a mechanism change would alter it: gate the
    write on an actual LLM call — undecided); the four places that state the unit now agree, and the
    new case rejects the old wording on its way back.
  • SECURITY.md's scanner-scope sentence was describing a run that no longer happens
    (SECURITY.md + tests/test_mechanism_docs.py). It said the scanner "also reports ~48 findings
    under archive/legacy-20260914/_venvtest/". After this version moved the skip list to
    .gitignore, the default pass does not enter archive/ at all (measured: 126 files, no hits),
    and the number was never the whole story: pointing the scanner at that tree explicitly
    (--root archive) reports 667 files with 2,017 / 90 / 16 hits by rule. Both cases are now stated
    separately and a case forbids the conflated wording from returning. Part of a full read of the
    remaining public docs against code: upgrading.md's exit codes do match
    core/exit_codes.py (BEHIND=3, REFUSE=4), and the versioning.md / privacy.md /
    SECURITY.md claims that were checked held.