Skip to content

0.28.0

Choose a tag to compare

@ShkodnikAI ShkodnikAI released this 03 Oct 15:46
3576887

Security

The audit-driven security release: the v0.27.1 tag (2026-09-29)
predates every fix below. v0.27.1 and earlier are affected — update is
mandatory.

  • №523 (gh#832; the consolidated audit A+D 30.09, N-1, High):
    semantic findings now BLOCK mlog run and mlog serve — the
    liar-string security-check bypass is closed. v0.27.1 blocked only two
    message-substring classes (contains("distill_to"),
    contains("[DENY_")), mlog serve ran NO semantic pass at all, and
    an unknown function returned the STRING
    "[ERROR: unknown function 'x']" as the call's VALUE — a non-empty
    string is truthy in is_truthy, so if is_admn(user) with
    is_admn undefined took the then-branch: the security check became
    its own bypass (a typo in a function name was enough). Now every
    semantic::check_program error blocks the run path and the serve
    startup (exemptions classify ONLY by the structured error kind in
    ONE explicit place — never by message substring), the unknown
    function is the coded [UNDEFINED_FUNCTION] compile error, and the
    refusal carries the stable code at position 0. The tree-walking
    runtime was the affected path; the VM backend's compile stage
    already refused the class — the exploit shape is now refused on BOTH
    backends (pinned in tests/naryad_523_semantic_gate.rs).
  • №526 (gh#835; the consolidated audit A+D 30.09, N-2): the GDPR
    Art. 17 erasure path for voiceprints LANDS — voice_delete(handle)
    (idempotent; the persisted twin runs the SECURE-DELETE path: the
    ciphertext blob is zero-overwritten in place BEFORE the row removal)
    and voice_list() (the informed-deletion basis; the listing never
    decrypts, the embedding bytes never enter any result). Neither is
    feature-gated — the erasure right cannot depend on a build flag.
    v0.27.1 and earlier had ONLY insert/replace: a persisted print
    outlived every consent revocation ("Delete path: ABSENT",
    privacy.md §2.1).
  • №527 (gh#836; the consolidated audit 30.09, N-3): the voiceprint
    ciphertext is BOUND TO ITS SUBJECT — the AES-256-GCM AAD carries the
    (subject_id, registry, schema version) triplet, so a blob
    transplanted onto another subject's row FAILS authentication (the
    swap attack the bare tag accepted is closed), and the decoded key
    buffer lives under Zeroizing. The transitional empty-AAD read
    announced by №527 existed ONLY on main — its deadline is honored IN
    THIS RELEASE: the fallback is removed, a legacy row refuses with
    [VOICEPRINT_DECRYPT], the only path back is re-enroll/re-save. No
    released version ever wrote a legacy row (the GCM store itself ships
    in 0.28.0).
  • №517 (gh#801) + №512 (gh#796; the consolidated audit 28.09
    C-06):
    the honest at-rest crypto for voiceprints — real
    AES-256-GCM with a fresh random 96-bit nonce per write, the key
    ONLY through the secret()-gate semantics (env-sourced hex-256,
    never derived from the name), a keyless save fails closed
    ([VOICE_INSECURE_STORE]); №512 removed the FAKE crypto claims
    first (the pre-№517 store's schema said AES-256-GCM while the bytes
    were name-XOR plaintext). v0.27.1 and earlier persisted voiceprints
    without honest at-rest encryption.

Changed and fixed

  • Release supply-chain assets (CI infrastructure; no language or runtime
    change): the new .github/workflows/release.yml runs after the owner
    publishes a GitHub Release and attaches the mlog binary, a CycloneDX 1.5
    SBOM that describes exactly that binary (cargo-cyclonedx 0.5.9, pinned),
    BUILD-INFO.txt (exact toolchain) and SHA256SUMS, plus two Sigstore-signed
    attestations (SLSA build provenance
    and the SBOM bound to the binary). Until now releases were a tag and notes
    with no assets. Honest limits: reproducibility not claimed or verified, glibc of the
    pinned ubuntu-24.04 runner, no human-held key; releases up to v0.27.1 have
    no assets.
    Verification recipe: SECURITY.md → "Verifying release artifacts";
    operator step: docs/release-checklist.md step 6.

  • The №527 deadline honored in the release prep (issue #836 — the
    limitations.md TRANSITION row, closed in this PR per the №524 rule):
    the transitional empty-AAD read (the second decrypt attempt) is
    REMOVED — a legacy №517-era voiceprint row refuses with
    [VOICEPRINT_DECRYPT] (the message names the legacy possibility and
    the path back: re-enroll/re-save). VoiceprintCryptoStatus loses
    the LegacyNoAad variant (no producer remains); the
    status-returning load keeps its signature (AadBound for every
    readable row). Tests: tests/naryad_527_voice_aad.rs — T2 (the
    legacy row refuses after the deadline), T3 (re-enroll/re-save is
    the ONLY path back; the rebound row refuses the swap again), T4
    (the single-attempt wrong-key refusal); T1/T5 unchanged. No released
    version ever wrote a legacy row — the transitional population was
    main-only.

  • Naryad №550 (issue #911; Wave 25 P0, the release prep): the
    unfreeze-gate DEFAULT target is 0.28 — a bare unfreeze_gate.py run
    now checks the 0.28 ABSOLUTE goals (ADR-0179 §5, the v2 gate)
    instead of the legacy §4-only reading; the legacy reading stays
    available explicitly (--gate-target legacy). The workspace version
    is 0.28.0; the README version badge is re-rendered from Cargo.toml
    (the №497 badge-sync gate).

  • Naryad №551 (issue #912; Wave 25 P0; the audit 02.10 M-1 + §6.2): the
    "merge without CI" class closes on both halves. The POLICY half:
    docs/maintainers.md "When CI is down" — the verbatim audit rule ("No
    merges while GitHub Actions is degraded. A merge without a green
    required-check run is a process violation, not a judgement call"),
    the unverified-≠-green posture ("no red X" is not "green" — the dead
    event delivery of 30.09 looked exactly like that), and the
    branch-protection CHECKLIST for the owner (require PR + the required
    set fact-checked against the ci.yml display names + up-to-date +
    no-bypass; applying it is an admin action the executor cannot
    perform). The MACHINE half: scripts/ci/merge_ci_audit.py + the
    scheduled merge-ci-audit workflow (Mondays 05:00 UTC) — every main
    merge of the last 7 days is re-checked against a GREEN run of the
    required set on its PR's head SHA; a skipped check is not green, an
    unfetchable PR head is a divergence (fail-closed: unknown is never
    green), a divergence = a red run + an automatically filed issue with
    the list; the script only reports (no merge/revert/history edits).
    The first scheduled run is EXPECTED to flag the documented 30.09
    outage merges — the issue asks for verdicts, not for panic. Pinned by
    tests/naryad_551_merge_ci_audit.rs (the fixture self-test driver +
    the required-set sync between the script, the test and the
    checklist).

  • Naryad №566 (issue #928; Wave 25 P2 tail; the audit 02.10 §7.2 W-4):
    the grantors' digest stops lagging the machine metrics —
    docs/PLAN-SUMMARY.md's CURRENT numbers are GENERATED by
    scripts/gen_metrics.py (the same sources as the README metrics block:
    one SSOT, one process computes both, they cannot disagree). The
    audit's exact finding is fixed first: the digest's wave-24 section
    claimed "now 2110 bp" while the live floor is 3195 bp (162/507 =
    31.95%, precise 83/507 = 16.37% — the №560 second share side by
    side); the hand-written current number is GONE, the generated block
    (between explicit markers, the №460 posture) carries the floor and
    the registry size instead — the narrative stays hand-written (the
    №566 boundary: the digest's prose is the publisher's voice; the
    historical wave-23 snapshot "floor 998→2110 bp" is a frozen fact and
    stays). gen_metrics.py --check now verifies BOTH targets and the
    blocking metrics job runs it — a future drift is CI-red by
    construction. Pinned by tests/naryad_566_plan_summary_numbers.rs
    (3 tests): the digest block == an INDEPENDENT recomputation from
    registry.rs (the exact type_signature_share.py regex contract),
    the generator --check green right now, the stale "now 2110 bp"
    claim must not return.

  • Naryad №565 (issue #927; Wave 25 P2 tail; the audit 02.10 §7.2 W-3):
    the TWO-ARGUMENT respond_html(a, b) form is deprecated — its sense
    depends on the CONTENT of the first argument (a first token 100..=599
    reads as an HTTP status, anything else as a document title: the
    audit's respond_html("200 причин выбрать нас", body) served HTTP 200
    and silently dropped the title; a DB- or LLM-sourced string would
    decide the response's meaning). THE EXPLICIT FORMS LAND:
    respond_html_status(status, body) — the body verbatim at the given
    status; the status must open with an HTTP code in 100..=599 or be a
    whole number 100..=599, otherwise a LOUD argument error (no legacy
    silent-200 fallback — the №514 rule); respond_html_doc(title, body) — the title is never read as a status and never dropped, the
    same full-document shape as the office form (№892). Both serve
    Content-Type: text/html; charset=utf-8, both are typed in the
    registry ("Struct" — the HttpResponse shape), classified as
    Sink/Public/Irreversible, and ride EVERY HTML egress gate exactly
    like respond_html (LLM-output, open-redirect, secret-leak,
    recall-taint — the explicit name is not a bypass). The deprecation
    carries a WARN-ONLY semantic pass (the №474 posture): every 2-arg
    call whose first argument is NOT a string literal is announced with
    both migration paths; a literal first argument keeps working
    silently this release; removal no earlier than one release with the
    warning. The 1-arg form is the UNCHANGED SSOT contract (gh#899).
    The floors moved WITH the rows that earned them (the №757
    procedure): the general typed floor 3195 → 3241 bp (165/509), the
    precise floor 1637 → 1650 bp (84/509 — query_param ships typed
    "String", both paths verified), the in-tree compiled lock
    158 → 161. Pinned by tests/naryad_565_respond_html_forms.rs (13):
    the explicit forms on BOTH backends (TW + VM flows), the loud
    strictness, the warning fires on non-literals in patterns AND route
    bodies, no warning for literals, HTML_INJECTION/SECRET_LEAK through
    the new names, the registry facts. The corpus: 241 programs, 0
    warnings; registry-vs-corpus 711 pairs OK; REFERENCE regenerated
    (509 builtins, 0 TODO(doc)). The office migration snapshot: the
    public corpus carries exactly one 2-arg call site
    (examples/p_p01_respond_html_query_param.mlog — two literal-status
    calls) → the migration targets respond_html_status; the private
    office corpus rides the №559 call-shapes snapshot per the dispatch.

  • Naryad №564 (issue #926; Wave 25 P2 tail; the audit 02.10 §7.2 W-2):
    the mirror metric sees the WHOLE src/ tree — the №502 counter
    (vm.rs-only) widens to src/**/*.rs because the mirror CLASS is not
    vm.rs-specific: html_label.rs (№544.3) carries two taint-walk mirrors
    ("the parity demands the EXACT mirror, quirks included") that the
    narrow scan never saw — the same class as M-8 (№558). The fixed
    regex gains the §7.2 audit-sketch markers (mirror of the …,
    exact mirror) beside the №502 pair (continuity: ported verbatim,
    TW-identical, same as the TW stay in the set). The widened fact
    is fixated ONCE — 7 mentions (5 in html_label.rs, 1 in audit.rs doc,
    1 vm.rs №456 banner; the vm.rs fact under the narrow patterns stays
    0) — in the new scripts/ci/src_mirror_baseline.txt (the
    vm.rs-only baseline retires; the same vm-mirrors (blocking) CI
    job, the same unfreeze-gate dedup criterion, movement ONLY down).
    The REGISTRATION (the naryad item 3, for the stage-3 typing gate):
    both html_label.rs walks are tracked mirrors — the BINDING walk
    (binding_label → binding_taint + get_expr_taint shape) and the SINK
    walk (sink_arg_is_untrusted → expr_is_llm_tainted +
    expr_is_learnable_tainted, depth-bounded 3 per №295) — with the
    unification requirement recorded (ONE lane-parameterized walk,
    SECRET/SQL/HTML); html_label.rs itself is NOT touched (the naryad
    boundary). docs/maintainers.md gate list and
    scripts/ci/unfreeze_gate.py moved to the new baseline in the same PR.

  • Naryad №563 (issue #924; Wave 25 P2; the audit 02.10 "the rest"
    line): the read_file soft-missing debt is 5 → 0 — the №531
    transition ENDED with v0.28.0 and read_file of a MISSING file now
    refuses LOUDLY with [IO_ERROR] on BOTH backends (the refusal names
    the explicit-fallback surface read_file_or(path, default)). The
    №254 empty-string contract is gone with the transition release; the
    №514 rule (gh#798) is now total: softness lives in the _or name,
    everything else is loud. read_file_or is UNCHANGED (the only
    remaining missing-file soft surface; no new soft functions — the
    naryad boundary); existing-file reads are byte-identical. The debt
    counter reads 0 with the only-down lock
    (scripts/ci/debt_baseline.txt); the transition marker string is
    GONE from the tree (the counter counts it — the flip is proven by
    the code, not the marker). The limitations.md TRANSITION row closed
    in the same PR (the №524 rule); REFERENCE.md §4.13 + the
    generated index updated. Tests: tests/naryad_563_read_file_loud.rs
    (6 — the loud flip on TW AND VM, the _or twin on both, the
    existing-file byte-identity, the debt-gate pin); the n254/n455/n507
    pins flipped as their own comments promised.

  • Naryad №562 (issue #923; Wave 25 P2; the audit 02.10 §6.1): the
    tag↔main gap gate — the M-3 class ("a High fix lives outside a
    release") becomes a MEASURED state. The new blocking release-gap (blocking) CI job runs scripts/ci/release_gap_gate.py: a pending
    Security entry in [Unreleased] older than the floor FAILS the gate
    with the oldest stamp named; an UNDATED pending Security entry fails
    too (fail-closed — an undated pending fix cannot be measured); no
    Security part in [Unreleased] passes. The floor N=14 days is the
    OWNER's parameter (the naryad proposes, it does not choose; the
    publication itself stays the owner's gate — the criterion only
    measures the limbo). Embedding decision (the naryad's "факт решает"):
    a separate blocking job, NOT a fifth criterion inside unfreeze_gate —
    the unfreeze gate is the ADR-0177 §4 four-criteria machine verdict
    (a frozen list); the release-gap criterion is release hygiene, not an
    unfreeze criterion. The job joined the №551 required set (13 items —
    the script, the sync test and the branch-protection checklist moved
    together). Today's CHANGELOG passes ([Unreleased] carries no Security
    part). Tests: tests/naryad_562_release_gap.rs (6 — the self-test
    driver, the committed-CHANGELOG pass, fresh/over-age/unstamped
    fixtures through the real script, the structural required-set pin);
    n551 3/3 stays green.

  • Naryad №561 (issue #922; Wave 25 P2; the audit 02.10 §7.2 W-1): the
    good-first-issue pool policy — the pool drains from the INSIDE (five
    of the №541 positions closed by owner/agent commits within a day; a
    newcomer on 03.10 saw an EMPTY pool), so the refill rule is now
    written: internal closing is allowed but the refill to 8–10 open
    positions happens in the same wave, the pool-size counter is part of
    the wave report, and the reserved-for-newcomer label marks the
    pool (AGENTS.md §3: agents do NOT take labeled tasks — a recurring
    upkeep task an agent did internally is re-opened as a
    good-first-issue for the next external contributor). The policy
    lands in docs/maintainers.md (the onboarding-lane section) + AGENTS.md;
    the label created on the repository. THE POOL REFILLED TO 9 in the
    same wave, each issue by the №491 filter (self-contained volume, no
    context dependency) with the expected volume and the entry point:
    gh#951 (the 40 examples without a golden .expected sidecar),
    gh#952 (the book syntax page — 41 fenced snippets), gh#953 (the
    tutorial + stdlib pages — 25 snippets), gh#954 (doc-tests.md
    freshness), gh#955 (testing-evidence.md citations), gh#956
    (risk-review-checklist.md links), gh#957 (CONTRIBUTING first-PR
    path), gh#958 (AGENTS.md §1 applied to itself), gh#959
    (PLAN-SUMMARY/REALITY numbers vs the generated metrics SSOT).

Deprecated

  • Naryad №560 (issue #921; Wave 25 P2; the audit 02.10 M-4): the PRECISE
    typed-signature share lands as the second metric beside the general
    one — the audit's finding: 79 of the 162 typed rows are bare
    Struct/List without a parameter (≈49% coarse), so the general 31.95%
    share is reachable by coarse types. The precise set is
    {String, Float, Bool, Unit}; the registry's stage-0 spec strings are
    bare (no List/Struct(Name) forms exist in the source — the
    honest fact the metric records), and the measured fact EXACTLY
    matches the audit: precise 83/507 = 16.37% (49 String + 20 Float +
    9 Unit + 5 Bool) on the audit revision 6e66d3d.
    type_signature_share.py --precise reports and gates the second
    floor; scripts/ci/type_signature_precise_baseline.txt locks it
    (threshold_bp 1637, movement only up — never by loosening the
    PRECISE_TYPES definition; Unknown stays Unknown, the №560 boundary);
    the type-signatures (blocking) CI job gates BOTH floors; the README
    generated-metrics block carries both shares side by side
    (Typed Signatures | 162/507 (31.95%) — precise 83/507 (16.37%)),
    regenerated by gen_metrics.py and validated by --check +
    readme_consistency. The 0.29 precise-share GOAL is the OWNER's
    parameter (§3 ADR-0179) — the proposal is delivered in the issue.

  • Naryad №559 (issue #920; Wave 25 P1; the audit 02.10 M-2 systematic +
    §6.3): the corpus contract is machine-checked — changes that turn a
    warning into an error run against the KNOWN corpus, not wait for a
    user (the respond_html lesson). Three pieces, one blocking surface:
    (1) scripts/ci/extract_call_shapes.py — the call-shape extractor
    (string-aware, comment-stripping, struct-literal-bracket-aware; the
    declaration heads excluded; the name|argc|count triples carry no
    code and no query strings — the №559 privacy boundary);
    (2) scripts/ci/office_call_shapes.txt — the committed snapshot of
    examples/ + self-host/ (712 pairs; the office snapshot is the owner's
    hand-off with the same one-liner; hand-annotated
    SKIP|name|argc|reason lines mark try-wrapped negative fixtures and
    survive as a documented convention); (3)
    scripts/ci/registry_vs_corpus.py — the blocking validator (every
    registry-bound pair must satisfy the registry's arity spec; the
    non-builtin names and the dynamic-arity list — forget, render, the
    semantic.rs posture — are skipped; a divergence FAILs with the name
    and the arity) wired as a step into the existing
    registry-arity-check (blocking) CI job (the required set is
    unchanged — the job IS the blocking surface). The specs reach the
    validator through scripts/ci/registry_arity.txt — the machine-read
    export pinned to BUILTIN_REGISTRY by tests/naryad_559_registry_vs_corpus.rs
    (the file moves with the registry, never alone; the METALOGOS_N559_BLESS=1
    dev path regenerates). The review rule lands in CONTRIBUTING.md.
    The check PAID FOR ITSELF IN THE SAME PR: the snapshot caught a
    LATENT 1-arg session_login(user) call in examples/compat/p6_full_app.mlog
    — the runtime refuses 1-arg (the spec is 2..2) and the branch was
    never executed by any test; the example is fixed to the honest 2-arg
    form (the password is NOT verified — ADR-0172 §6) with the finding
    documented in place. Tests: naryad_559_registry_vs_corpus.rs 4/4
    (the export sync, the dynamic-arity posture pin, the self-test
    driver, the committed-corpus acceptance); the validator self-test
    covers green/divergence/skip/dynamic-arity/range cases + the SKIP
    annotation round-trip.

  • Naryad №558 (issue #919; Wave 25 P1; the audit 02.10 M-8): the ONE
    module-search rule — src/module_path.rs::resolve_module_file is the
    single implementation of the "module_path → file" rule, and all THREE
    call sites call it: the interpreter's runtime loader
    (interpreter/modules.rs), the compiler's resolve_import
    (compiler.rs) and the semantic static resolution
    (resolve_imports_statically, semantic.rs — the third copy that
    lived outside the mirror metrics). The previous forms composed the
    same path two ways (format!("{}.mlog") append vs
    with_extension("mlog") replace) — EQUIVALENT on every reachable
    input, because the import-path grammar admits no dots
    (import_path_segments = { IDENT ~ (SLASH ~ IDENT)* }), so the
    extension-replace could never fire; the append form is the SSOT and
    the equivalence is recorded in the file. The base directory stays a
    site's own choice (base_dir for the loader and the semantic pass,
    std_root for the compiler) — the RULE is shared, the roots differ by
    design. The file-existence rule stays at each site's read: the loud
    per-site refusals keep their exact pinned wording. Behavior is
    byte-identical — pinned by tests/naryad_558_module_path.rs (6): the
    three sites × found/not-found/nested shapes (the flat
    import my_utils, the nested import lib/extra), the semantic
    merged-set assertion (both modules' patterns land), and the
    source-level SSOT pin (every site delegates; no inline composition
    remains). Honest metrics: dup-names 0, mirrors 0 — no new pair
    entered the contour (the collapse REMOVED the unmeasured third copy;
    the leaf module adds no C4 edge). n523 11/11, n535 17/17, n557 7/7
    stay green.

  • Naryad №557 (issue #918; Wave 25 P1; the audit 02.10 M-7): the
    SEMANTIC startup gate lands on every mcp-serve entrypoint — the same
    level of protection mlog serve got in №523. mcp-serve ran NO
    semantic pass at all (grep 0 check_program sites on the audit
    revision): a tool file never mlog check-ed could carry blocking
    semantic findings and serve. Now resolve_imports_statically (the
    same "." base_dir rule as serve) feeds check_program, the exemption
    filter reads ONLY the structured kind (is_exempt_from_blocking — the
    ONE explicit place), and a non-empty blocking list refuses the
    STARTUP with the serve refusal's format: the FIRST blocking finding's
    stable code at position 0 (№479), the onряд stamp "Naryad #557",
    the format_blocking_line list. The gate is wired at ALL THREE
    entrypoints (stdio run_mcp_server, run_mcp_server_network, the test
    harness run_test_mcp_server) right after the №536 Category A gate —
    a structural test pins the both-gates parity (the transports cannot
    drift). The blocking-checks matrix: the mcp-serve "absent
    (зафиксировано)" row is REPLACED by five real cells
    (read/parse/audit/semantic×2, all blocks) — the matrix has no
    mcp-serve absent row anymore; blocking_checks_sync.py asserts the
    five-command coverage (the JIT absent row stays — the honest
    boundary); fact_blocking_check_cells 27 → 31. HONOR BOUNDARY (the
    naryad forbids semantic changes): the check_program walk surface is
    UNCHANGED — pattern and route bodies are walked (the №523 walks),
    tool-method bodies are NOT (the pre-existing blindness both serve and
    mcp-serve share; the honest extension is a check_program semantic
    change = a separate gated decision, recorded as a candidate). The
    fixtures pin the walked surfaces: a pattern-bodied undefined call and
    a missing import refuse the startup loudly and early; a clean tool
    file passes. Tests: tests/naryad_557_mcp_semantic_gate.rs (7); the
    n535 structural pins updated (mcp-serve must NOT regress to absent);
    n535 17/17, n536 10/10, n523 11/11 stay green.

  • Naryad №556 (issue #917; Wave 25 P1; the audit 02.10 M-6; the GDPR
    line №526→№527→№556): the strict-erasure posture for the voice store
    closes the two residuals the audit found. (1) The DELETED page no
    longer survives in the journal: the store setup turns
    PRAGMA secure_delete = ON (verified loudly — a connection refusing
    the posture fails init) and every delete_voiceprint ends with
    PRAGMA wal_checkpoint(TRUNCATE) — a WAL-mode database's log is reset
    to zero length by the call, a busy WAL is a loud error, never silence
    (the n526 zero-overwrite left the page image in the WAL — exactly the
    residual the audit flagged). (2) The audio artifacts stop lying about
    at-rest secrecy (the №512 lesson): the legacy plaintext audio_bytes
    column becomes audio_encrypted — the SAME AES-256-GCM scheme as the
    voiceprints (№517) with the №527 subject binding (the AAD registry is
    voice_artifacts; a blob transplanted onto another name refuses with
    the new [VOICE_ARTIFACT_DECRYPT] code), the mock runtime keeps the
    visible INSECURE-XOR-MOCK mark, and a keyless real-runtime write fails
    closed ([VOICE_INSECURE_STORE]). The migration secures the legacy
    plaintext bytes (zero-overwrite first; the DROP purges the freed pages
    under secure_delete) and rebuilds the table — no released version ever
    wrote an audio row (the write path never shipped), so the dev-era rows
    are not carried. The store-level save/load_audio_artifact pair is
    the AT-REST contour only — wiring a program-facing audio write surface
    is deliberately NOT in №556 (the boundary). The crypto plumbing is one
    shared core (aes_gcm_encrypt/aes_gcm_decrypt + the
    DecryptFailure taxonomy) with the registry-specific AAD composable —
    the voiceprint refusals keep their exact pinned wording; dup-names 0,
    mirrors 0. Tests: tests/naryad_556_voice_secure_delete.rs (9 — the
    PRAGMA posture, the observable WAL truncate, the encrypted roundtrip,
    the wrong-key/swap/keyless refusals, the mock mark, the legacy
    migration); the voiceprint suites (n512/n517/n527) and the 23
    store-unit tests stay green. docs/privacy.md §2.1 records both
    postures with the honest boundary (the store's OWN connection; the
    file-level guarantee stays the DB file owner's).

  • Naryad №555 (issue #916; Wave 25 P1; the audit 02.10 M-5 + Е-3): the
    MSRV contract is CHECKED, not just claimed — a blocking msrv (blocking) CI job pins the toolchain to exactly 1.93.1 (both the
    dtolnay/rust-toolchain@1.93.1 action ref AND the
    RUSTUP_TOOLCHAIN: "1.93.1" env, the documented override over the
    rust-toolchain.toml channel = "stable" — the gh#865 silent-switch
    failure mode cannot recur) and runs cargo check --workspace --all-targets on the floor; clippy stays on stable by design (the
    №500 lint-set precedent). The job joined the №551 required set (the
    script, the sync test and the branch-protection checklist moved
    together in this PR). Dockerfile: the builder patch is PINNED to the
    MSRV (rust:1.93.1-slim-bookworm, the tag verified to exist) — a
    floating rust:1.93 could drift below the declared floor; the
    policy in the file says rust-version and the FROM tag bump in the
    same PR. CONTRIBUTING.md carries the external-auditor note (Е-3):
    rustc ≥ 1.93.1 is mandatory and the pre-1.93.1 manifest refusal is
    the contract working, not a breakage.

  • Naryad №554 (issue #915; Wave 25 P1; the audit 02.10 §3.3, position Е
    on the memory pairs — the dispatch resolution "memory — document"):
    the memory pairs are PERMANENT BY DESIGN. The honest record lands in
    the TW/VM Divergences table of docs/limitations.md ("the memory
    builtins run on TWO ENGINES: memorize/recall/recall_top_k/forget
    (plus the dispatch marshaling twins) — the VM's honest simple-memory
    store and the TW's full store | №442 | PERMANENT BY DESIGN"), with
    live links to the shared module, the baseline and the pinning test.
    src/memory_ops.rs cross-links the record (the module docstring keeps
    the shared-HOME-not-unification statement); the ops-pair baseline
    marks the five pairs [permanent — №554] while the query pair stays
    UNMARKED (a parity-fix candidate, position А — never a design
    decision). Pinned by tests/n554_memory_pairs_permanent.rs (3/3): the
    three artifacts cannot drift apart — a lost record fails the file.
    NO behavior change (the §5.3 rule: the memory pairs are never merged
    at the cost of semantics).

  • Naryad №553 (issue #914; Wave 25 P1; the audit 02.10 §3.3, position А
    on the SQL pairs — the dispatch resolution "SQL — fix"): the
    SQL-params divergence is FIXED, and the two db pairs collapse. THE
    FIX: a non-List params argument is a LOUD refusal on BOTH backends —
    db_execute() second argument must be List, got {} /
    db_execute_with_grant() third argument must be List, got {}. The
    VM lane used to end the params match with _ => Vec::new() — the
    audit's example db_execute("UPDATE accounts SET frozen = 1 WHERE id = ?1", user_id) executed with the placeholder UNBOUND (or, on a
    placeholder-free statement, the argument was silently IGNORED) on
    the serve-default backend, while the TW lane refused loudly. THE
    COLLAPSE (the №540 mechanics): db_execute and
    db_execute_with_grant are ONE suffix-free function over the
    DbAccess trait each — the TW rides TwDbAccess::lock, the VM its
    own trait impl; the ops-pair threshold 8 → 6
    (ops_pair_baseline.txt moved in the same PR). The unified bodies
    keep the RICHER error on each axis per the №484 discipline:
    db_execute gains the №758 not-open remedy on the TW lane, the
    grant fn keeps the name() missing-argument texts on both lanes
    (the former TW upfront-arity text is subsumed). Behavior-neutral on
    the success paths (byte-identical); the observable change is exactly
    the loud refusal the audit demanded. Mutation-parity pinned by
    tests/n553_sql_params_parity.rs — the audit example refused
    IDENTICALLY on both backends (red-before: the VM half failed on the
    pre-№553 revision), the grant twin likewise, and the proper-List
    path still executes in parity; the №465 diff-fuzzer 9/9; the db
    suites (№474/№484/№758/№397, registry checks) green.

  • Naryad №544 (issue #882; Wave 24, P1, compiler/security; stage 2 of the
    type system, №467 canon — the three audit classes on
    Labeled(Box<Type>, Label), executed behind the ADR-0179 gate in three
    sub-PRs): the stage-2 migration is COMPLETE — SECRET_LEAK, SQL_DYNAMIC
    and HTML_INJECTION speak the stage-0 label vocabulary, the bespoke
    verdict logic is gone from all three lanes. SECRET_LEAK (№544.1, PR
    #900): a secret source (env()/secret()) is
    Labeled(String, Private) (src/secret_label.rs); the label joins
    through composition, sanitizers strip it, redact lifts it exactly on
    the one-way policy (ADR-0136 D2); TaintKind::Secret is deleted from
    the enum (consumers 7 → 0). SQL_DYNAMIC (№544.2, PR #901): the literal
    string IS the SQL template (the unlabeled String), every derived
    string carries Labeled(String, Internal) (src/sql_label.rs);
    Private when a secret part joined in; the two parity type rules —
    concatenation is derivation itself (a BinaryOp never yields the
    literal type) and a binding demotes the literal (a variable reference
    is never a literal). HTML_INJECTION (№544.3, PR #902): an LLM-output
    source is Labeled(String, Untrusted) — the third stage-0 label
    (src/html_label.rs); the UserInput kinds mirror as
    Labeled(String, Private) (silent at this lane's sink, №268); both
    mirrored walks keep the pinned quirks — the wider binding-level source
    set (call_llm_schema flags via a binding), the depth-bounded sink walk
    (№295, TAINT_NESTING_MAX_DEPTH = 3), the direct-only learnable
    match; sanitizers lift at any depth; redact passes through (masking
    is not channel sanitization). All three walks are LEAF modules (the C4
    acyclicity ratchet sees no new intra-cycle edge); the walk topologies,
    messages, severities, line resolutions and the №98 compile-time
    refusals are the exact mirrors — the observable behavior is unchanged
    and pinned (leak suite, n172/n274/n292/n322/n325/n386/n455, №123/№201/
    №268/№295, the n98 golden, the №465 diff-fuzzer 9/9 on every step; the
    full 280-file integration sweep green). Audit-surface measure (the
    honest, FINAL one): step 3 alone slimmed audit.rs by −128/+62 (net
    −66: the HTML tracker, the bounded walker and the learnable matcher
    left the file; the remaining taint machinery serves only the
    not-migrated checks); across the whole stage 2 audit.rs moved 7756 →
    7796 (+40 — the walk plumbing and the env threading), while the three
    manual per-class contours were REPLACED by one-line typed questions
    and the lane semantics moved to three typed leaf modules (secret_label
    190 / sql_label 163 / html_label 268 lines); the bespoke verdict
    consumers: 0 (TaintKind::Secret deleted, the syntactic SQL match
    deleted, the HTML tracker deleted). Contracts: tests/n544_step1_secret_label.rs (8),
    tests/n544_step2_sql_label.rs (10), tests/n544_step3_html_label.rs (10)
    — the mechanism surfaces + the end-to-end parity shapes + the №98
    compile-time classes.

  • Issue #892 fix (prod regression on main — introduced by №523's spec
    hardening, never shipped in any release; the FOSVED-office-v2 FORGE
    pages 500/404): the respond_html contract is restored — all three forms in
    one builtin. respond_html(html) (the 1-arg office corpus form) is legal
    again — the registry/semantic spec drops №523's hard 2 back to 1..2, so
    mlog check no longer rejects every office HTML route and the runtime no
    longer 500s with "requires an argument at position 1". The 2-arg form
    disambiguates by fact: when the first argument opens with a valid HTTP
    status token ("200", "404 Not Found") it stays the documented
    (status, html) form with the body VERBATIM; otherwise it is the office's
    (title, body) shape — a full HTML document is built, the title lands
    tag-stripped in <head><title> AND verbatim at the top of <body> (it is
    not dropped anymore). Every form now serves Content-Type: text/html; charset=utf-8: Value::HttpResponse carries an optional content_type
    (None keeps the historical respond() text/plain default) and the server
    honors it (value_to_response + both VM serve paths share the new
    http_response_into_response) — the axum String-body text/plain default
    that actually broke the office pages no longer applies to HTML responses.
    REFERENCE.md §4.13 + the web table document the three forms; contract
    pinned by tests/n892_respond_html_contract.rs (12 tests: all three
    forms, status-vs-title precedence, out-of-range numbers stay titles,
    semantic 1-arg pass / 3-arg fail, registry arity) and 3 server.rs unit
    tests (content-type header, the None default, custom status).

  • Naryad №545 stage 1 (issue #883; Wave 24, P1; the №472 roadmap,
    decision 2-B; docs/refactoring-split-plan.md): the crate-stub of the
    reflex domain lands as a workspace member — metalogos-reflex
    (version lockstep via [workspace.package]; the crate's own candle
    gate mirrors the off-by-default posture; the workspace builds green
    on all profiles). The stub ships NO moved code DELIBERATELY: the
    physical move of the reflex domain (the NN module, the generative
    machinery, the candle surfaces — ADR-0178 §3) is blocked on the
    dependency-direction fork the roadmap explicitly reserves for the
    owner's gate. The inversion inventory is recorded in the stub's
    library docs: Value::Reflex/Value::BpeVocab live in the CORE
    value enum, and 6 core files import crate::nn:: directly
    (vm.rs, distill_hub.rs, builtins/reflex.rs,
    interpreter/{mod,learnable,execution}.rs, builtins/template.rs) —
    moving before these edges are inverted would be a cyclic crate
    dependency. The fork: (i) the core-first inversion through trait
    seams (the №484 DbAccess precedent) or (ii) the tiny
    metalogos-values crate — the owner decides; the follow-up
    sub-PRs (inversion, then the physical move with the stop-list
    manifest following 1:1, then the re-export shell) are sequenced
    behind it. No contour growth: the split is a transfer, not an
    expansion (ADR-0178 §4); the stop-list baseline is untouched.

  • Naryad №545 stages 2-3 (issue #883; Wave 24, P1; the owner's gate
    decision of 2026-10-02: fork (i) — the core-first inversion through
    trait seams, the №484 DbAccess precedent): THE CRATE SPLIT IS
    PHYSICAL. Stage 2 (PR #909): the inversion — ModelKind::Sequence/Gen
    payloads go through the handle contracts SequenceModelHandle /
    GenModelHandle (src/nn/handles.rs, the consumer-owned seam that
    stays in the language crate), register_seq/register_gen take the
    boxed handles; the Value-speaking spec surface (the
    SEQUENCE_LAYER_REGISTRY + the six build_* fns) lands in
    src/nn/sequence_spec.rs — the check-time language surface cannot
    move with the machinery; the shared check_tensor_coverage gets its
    own leaf (src/vision/coverage.rs, now moved with the diffusion
    chain). Stage 3 (this PR): the 16 stop-list files move 1:1 into
    metalogos-reflex (ten nn modules + six vision diffusion
    modules + the coverage check + the accuracy SSOT
    compute_accuracy), the dependency direction lands CORE → REFLEX
    (the reflex crate is Value-free by design — the Value marshaling
    stays with the language builtins), the re-export shells
    (metalogos::nn::*, metalogos::vision::*) preserve every consumer
    path — the no-API-change contract holds (the integration suites run
    UNCHANGED), save_png's write half stays behind the №475 fs gate in
    the language crate (the façade module), and the №463 manifest
    entries follow the files in the same PR with the baseline re-frozen:
    9430 → 9025 LOC (movement only down; no new contour files, no
    expansion — ADR-0178 §4 needs no patch). The tokenizers dependency
    moved to the reflex crate; image stays (the provenance marks).
    Verification: check default/candle/vision/portable green; clippy
    candle/vision/voice -D warnings green; lib tests 894 (default) +
    10 (the reflex crate's own moved unit tests) + 906 (portable); the
    №183/№184×3/№185/№193/№195×4/№179 seam suites and the №210/№211/№212
    vision suites all green; the №463 gate and the core→media gate green.

  • Naryad №549 (issue #887; Wave 24, P2; the NLnet/Restack traction
    lane, M1 2026-11-03): the draft traction package lands in
    metalogos-grants/ — the owner-gated surface (nothing here is
    published by an executor; the №541 postulate). The one-command demo
    script (scripts/demo_traction.sh, three legs over the public
    examples VERBATIM: the static taint refusal — the private camera
    frame denied at compile time with the exact class/rule/node; the
    live serve loop; the action-ledger chain — grant → allow → exhaust
    → deny → key rotation → export → mlog ledger verify, the Ed25519
    chain verified WITHOUT the Metalogos runtime; exit 0 = every leg
    reproduced, cleanup after itself). The metrics one-pager
    (metalogos-grants/nlnet-traction/01-metrics-one-pager.md):
    repository facts with dates, machine-generated where a generator
    exists — no market analysis (the §16.0-7 sanitization). The demo
    narrative (02-demo.md) frames the arc honestly: the three legs
    are the design center — authorization as dataflow, refusals that
    explain themselves, irreversible actions that leave evidence.
    Transmission to the owner: this PR + the naryad report; the
    external wordings and any publication stay the owner's.
    fe6d21a (Naryad 549 (issue #887): the draft NLnet/Restack traction package lands in metalogos-grants - the one-command demo script (three legs over the public examples verbatim: static taint refusal, live serve, the signed ledger chain verified without the runtime), the metrics one-pager (repo facts with dates, no market analysis), the demo narrative; transmission to the owner, publication stays the owner's gate)

  • Naryad №546 (issue #884; Wave 24, P2; ADR-0178 §5 preconditions 4–5):
    the generative contour's embedding seam becomes a NAMED, fail-closed
    boundary instead of an accident of the String argument contract.
    PRECONDITION 4 — the runtime secret check: the secret family
    (Value::Secret/Encrypted/Hash) is refused LOUDLY at the seam
    ([EMBED_SECRET_REJECTED], the error names the legal redact() path)
    at the seam's three entry points — the embed builtin, the shared
    embed_text SSOT helper (the learnable semantic probe, №272/№273)
    and the vault semantic_search builtin (its own manager instance —
    the third seam consumer the audit-style inventory caught). The
    value-level guarantee is the whole runtime surface today: a
    Secret's content cannot become a plain String except through
    redact() (the masked form is by construction not the secret); the
    compile-time №322 labels stay the static lane's job — stated in the
    seam module, no theater. PRECONDITION 5 — the call budget: one unit
    = one embedding operation; the counter is thread-local (the №457
    mechanism), reset by an RAII ContourBudgetScope at the
    per-request/per-tick boundaries (the TW route body, the VM route
    body, execute_tick_call — the same threads the №457 guards mark,
    reset-on-entry because tokio reuses blocking threads); the limit is
    configurable (METALOGOS_CONTOUR_BUDGET, default 64, deliberately
    conservative); over the limit the seam refuses loudly
    ([CONTOUR_BUDGET_EXCEEDED]) BEFORE any embed runs — a multi-unit
    call that would overshoot is refused whole. vec_store/vec_search
    take pre-computed vectors — no seam, no budget unit. Tests: 4 seam
    unit tests + the integration file on BOTH backends (the loud
    refusals pinned on TW and VM, the within-budget path green, the
    scope-reset behavior pinned; serial — the env var and the
    thread-local counter are process/thread-global). ADR-0178 §5 is
    actualized in the same PR: rows 4–5 CLOSED (this PR), row 3 CLOSED
    post factum (№530, PR #859 — the row was not re-read then; recorded
    honestly), and the closing line states explicitly that the closures
    authorize nothing — §6 stays the gate, the stop-list holds
    regardless. The stop-list manifest is untouched (no contour file
    grew; the seam module is a NEW file outside the manifest).

  • Naryad №543 part 2 (issue #881; Wave 24, the typing line to the 0.28
    gate, P1): the pdf package (25 rows) leaves stage 0 — THE NARYAD'S
    STOP CONDITION REACHED. 20 rows typed Struct (the pdf.rs make_struct
    family: pdf_create, pdf_add_page, pdf_write_text, pdf_draw_line,
    pdf_draw_rect, pdf_save, pdf_merge, pdf_split, pdf_metadata,
    pdf_set_metadata, html_to_pdf, pdf_draw_table, pdf_add_image,
    pdf_set_page_header, pdf_set_page_footer, pdf_page_numbers,
    pdf_watermark, pdf_fill_form, pdf_rotate_page, pdf_delete_pages) and
    5 rows typed List — INCLUDING the per-helper fact that distinguishes
    this package: pdf_classify/pdf_to_markdown/pdf_ocr return the
    pdf.rs make_dict, which is a LIST of String items (pdf.rs:363-371),
    NOT a Struct (a naive helper-name match would have typed them wrong);
    pdf_extract_regions/pdf_extract_images are the plain vectors. 0
    Unknown: no mixed paths in the package. Floor: 137/507 → 162/507,
    threshold_bp 2702 → 3195 (+493 bp ≈ +4.9 pp) — 3195 ≥ 3000
    (goal_typed_share_bp, ADR-0179 §2.1 draft); the final measurement is
    in the naryad report. The in-tree lock moves with the baseline:
    133 → 158 (compiled default-feature count; the same 4 gated
    vec/store rows ride the source count only). The final measurement:
    the source count 3195 bp is the CI-gated floor; the compiled count
    is 158/507 = 3118 bp — both over the gate. Runtime/semantics
    untouched.

  • Naryad №543 (issue #881; Wave 24, the typing line to the 0.28 gate,
    P1; ADR-0179 §2.1 goal, §13.10 decision 3-A): the bot package — the
    largest remaining registry package (35 rows) — leaves stage 0. 30 rows
    typed against the VERIFIED handlers (the Ok-path facts, one commit of
    archaeology per family): the make_date_struct family → Struct
    (memory_score, todo_add, todo_update, goal_get, goal_set, goals_add,
    goal_complete, goals_reflect, human_create, human_mood, human_delete,
    learn_preference, ask_approval, read_file_tokens); the row vectors →
    List (todo_list, goals_list, get_profile, human_personas, human_recall,
    extract_entities, check_reminders, list_reminders); the text returns →
    String (human_remember, human_respond, compress_html, extract_param,
    remind, remind_recurring, cancel_remind); estimate_tokens → Float.
    5 rows stay Unknown HONESTLY, not metric-fitted: send_message/
    answer_callback_query/edit_message_text/send_document (String on
    delivery | Unit on the no-TELEGRAM_BOT_TOKEN fallback — the flat
    vocabulary has no env-dependent union) and human_forget (Float deleted
    count on the 1-arg all-persona path | String "ok"/"not_found" on the
    2-arg path — the arity-dependent union). The floor moves up IN THE SAME
    PR (the rule): 107/507 → 137/507 source rows, threshold_bp 2110 → 2702
    (+592 bp ≈ +5.9 pp). The №542 registration is closed here: the in-tree
    double lock (tests/type_signature_metric.rs TYPED_FLOOR) moves in the
    same PR from now on — 50 → 133, and the lock's comment now documents
    the by-design gap (the script counts SOURCE rows; the test counts the
    COMPILED default-feature registry — the feature-gated vec/store-lane
    rows embed/vec_store/vec_search/memory_forget exist in the source
    count only). Runtime/semantics untouched (the naryad's boundary); the
    №486 warn-contour is not affected (the typed rows' categories were
    silent there). Remaining typing mass toward the 3000 bp gate: pdf (25),
    diagram (21), web (19), voice (19) — the descending-weight order per
    the naryad.

  • Naryad №522 (issue #821; the Wave-22 serve-e2e line, P2; the №509
    functional criterion, ADR-0179 §2.5): the cron accumulation arc
    through the SERVING path — tests/naryad_522_cron_serve_e2e.rs, both
    serve backends (the №496 pattern). The arc: a POST route registers
    the job (cron_add("* * * * *", "tick_job") — the KV-backed job
    store, persisted through the memory { persist } SQLite
    write-through); cron_run(id) (the №418 manual force_run) marks the
    job and the SCHEDULER's 5-second pass fires the tick (the №426
    program-context executor) into a pattern that increments a KV
    counter — the accumulation is visible in the /ticks route response;
    the RESTART leg proves BOTH survivals: the job is still found (the
    force re-queues instead of "not_found") and the counter CONTINUES
    from the pre-restart value (the KV SQLite load-back — "SQLite is
    authoritative on init") instead of restarting from zero. THE RED RUN
    CAUGHT THE HARNESS DEFECT, fixed in the same PR: the test boot
    (run_test_server_in_dir_impl) NEVER spawned the scheduler loop —
    only run_server did — so the cron arc was invisible to the
    serve-e2e lane ENTIRELY (a test asserting the arc could never pass,
    and no test asserted it). The loop is now the ONE shared constructor
    (spawn_scheduler, the №480 rule) called verbatim by BOTH boots —
    production behavior byte-unchanged, the test harness gains the
    production scheduler. The №457 tick security posture is NOT
    re-tested here (its own suite pins it). THE SERVE-E2E INVENTORY IS
    NOW FULLY DONE (distill/memory/conversation/cron) — the №509
    functional criterion of the 0.28 gate is MET on all four rows; the
    unfreeze gate v2 recalculates.

  • Naryad №521 (issue #820; the Wave-22 serve-e2e line, P2; the №509
    functional criterion, ADR-0179 §2.5): the conversation accumulation
    arc through the SERVING path — tests/naryad_521_conversation_serve_e2e.rs,
    both serve backends (the №496 pattern). The arc drives six conv_add
    calls against conversation { compress_after: 2, ttl: 3600, max_messages: 5 } (the fields in a NON-canonical order — the boot
    re-pins the №508 free-field-order guarantee) through one POST route
    and pins the honest per-backend posture: the max_messages ROTATION is
    the shared core (the oldest message evicted, both backends); the
    ADR-0053 COMPRESSION tail diverges BY DESIGN — the TW collapses
    everything beyond compress_after into ONE system summary (3 lines:
    system + m5 + m6), the VM keeps the rotated window (5 lines: m2..m6,
    the documented no-op tail); and the per-request conversation
    isolation (№72: "request A's dialogue must not continue in request
    B") is pinned via a fresh request's conv_history reporting the
    conversation NOT FOUND on both backends. THE RED RUNS CAUGHT TWO
    CONFIG-DELIVERY DEFECTS, fixed in the same PR (the №495 class — the
    declared config was dead on the serve path): (1) the TW boot merge
    ran ONE declaration per throwaway interpreter and
    clone_definitions_into copied the conversation_config UNCONDITIONALLY,
    so every later declaration's defaults clobbered the declared config
    before any route ran — the copy is now CONDITIONAL on the source
    having actually SEEN a conversation {} declaration (a declared-flag
    marker; the №381 db_conn clobber class); (2) the VM lane NEVER
    received the declared config at all — the compiler ignored the
    declaration, so the VM used the defaults unconditionally; the config
    now rides the compiled Program (the memory_persist_path plumbing:
    a #[serde(default)] Program field — old .mbc files deserialize
    cleanly — applied in load_program). The ttl field has NO runtime
    enforcement on the access path today — honestly NOT asserted (the
    declaration parses; the enforcement is a separate owner-gated
    decision). The remaining pending serve-e2e row: cron (№522).

  • Naryad №520 (issue #819; the Wave-22 serve-e2e line, P2; the №509
    functional criterion, ADR-0179 §2.5): the memory accumulation arc
    through the SERVING path — tests/naryad_520_memory_serve_e2e.rs, both
    serve backends, one sequential test fn (the №496 pattern). Arc 1, the
    memory { persist } + memorize/recall lane: the same-request
    roundtrip works on BOTH backends; the cross-request accumulation is
    pinned per the honest per-backend posture — on the TW the per-request
    interpreter re-opens the SQLite-backed store at the declared path, so
    request B's recall finds request A's memorize AND the store survives
    a server RESTART; on the VM the simple-memory twin is request-scoped
    BY DESIGN (the №442 posture; Vm::reset_for_reuse clears the store
    between pooled generations — the isolation class pinned by
    naryad_402_step_a), so the test pins the honest ABSENCE — an
    accidental cross-request leak turns RED. Arc 2, the typed-memory lane
    (memory_open/memory_put/memory_read, the №350 surface): the
    container registry is process-global and backend-independent (the
    subject IS the address), so the accumulation arc is identical on both
    backends — a PUT in request A, a READ in request B, and a READ after
    the restart with the persistence anchors (METALOGOS_MEMORY_DB +
    METALOGOS_MEMORY_MASTER, ADR-0173 §3.5) set — the value decrypts
    across the restart. The serve_e2e inventory row (memory) flips to
    done IN THE SAME PR; the security gates (№335 consent, №442 ledger)
    are NOT re-tested here — their own suites pin them.

  • Naryad №541 (issue #851; the wave-23 line 6-A, P1): the
    second-maintainer lane goes operational for the NLnet/Restack
    milestone (03.11.2026) — the pool, the onboarding package and the
    grant wording, finalized. The good-first-issue pool grows from five
    to NINE self-contained tasks: four new guides with the full format
    (the what / the why / the reproducible check / the expected scope /
    the acceptance criteria / the honest boundary) — the book examples
    freshness pass (gh#872), the ADR index freshness review (gh#873),
    the limitations.md anchor walk (gh#874), and the fresh-clone
    quickstart pass (gh#875) — all inside the narrow docs/tests
    perimeter. The onboarding package lands IN THE TREE: CONTRIBUTING.md
    gains "Your First PR — the Good-First-Issue Lane" (the fork → branch
    → CI → PR → report path, the reason-line-per-changed-file rule, and
    the pool's health metric — the time to the first merge, recorded per
    newcomer in the wave reports), and docs/maintainers.md syncs its
    pool section to the full nine with the issue numbers (the in-tree
    list had drifted to three) and cross-references the first-PR path.
    The NLnet application wording is finalized in-tree (the owner's
    draft, updated to the current honest fact: "nine documented
    good-first-issue tasks with reproducible checks and acceptance
    criteria"). The channel-search options and the access/publishing
    decisions remain the OWNER's gate (№541 §3) — the materials are
    prepared in the M2 report, nothing external is opened by the naryad.

  • Naryad №540 (issue #850; the wave-23 line 4-A, P1): the FIRST two
    byte-identical ops pairs collapse into suffix-free functions over the
    DbAccess trait (the №484 mechanics) — query_row and db_insert
    (the ops-pair threshold 10 → 8, moved in the same PR on the actual
    collapse; scripts/ci/ops_pair_counter.py + ops_pair_baseline.txt,
    the source comments on the collapsed sites carry the fact). The
    query_row not-open error unifies on the RICHER №758 named-remedy
    text (the former VM text; the TW lane gains the remedy) and the
    db_insert second-argument error on the RICHER literal-shape suffix
    (the former TW text; the VM lane gains the detail) — both
    behavior-neutral per the №484 richer-text discipline, the Err
    outcomes are unchanged on every path. The REMAINING EIGHT pairs stay:
    the four memory pairs (memorize/forget/recall/recall_top_k + the
    dispatch marshaling twins) are the №442 honest simple-memory twin
    posture (different stores, different algorithms, documented per
    side), and the three db pairs (query, db_execute,
    db_execute_with_grant) carry SEMANTIC divergences on the error paths
    (the TW-loud/VM-silent non-List params handling; the TW
    silent-skip/VM-loud row errors) — named in the docstrings per №480 as
    parity-fix candidates (the №474 class), never pinned inside a
    transfer (the №466 rule: a transfer PR must not change semantics).
    The stale db_execute_vm docstring (it still claimed the pre-№474
    Unit return) is corrected. Verified: the №465 diff-fuzzer, the №503
    outcome-parity gate, the crosscheck and property TW/VM parity suites
    green; the semantics of every caller path unchanged.

  • Naryad №539 (issue #849; the wave-23 typing line 3-A, P1): the memory
    package leaves stage 0 — the wave's largest package (38 rows) types
    35 rows against the verified handlers: kv_set/kv_delete/memorize →
    Unit, kv_get/mem_set/mem_get/mem_delete/session_set/session_get/
    session_clear/ref/deref → String, kv_exists → Bool, kv_list → List,
    memory_put/memory_retain/memory_release/memory_retain_ttl → Unit,
    memory_keys/memory_provenance/memory_retained → List,
    memory_export → String (the exported path), memory_cascade_preview/
    memory_forget_cascade/forget/memory_decay/memory_boost/memory_prune/
    memory_revise/memory_forget/user_profile/vec_store → Struct,
    embed/vec_search → List, recall → String. Three rows stay Unknown
    HONESTLY with the inline reasons: memory_open returns the opaque
    Value::Memory handle (the flat vocabulary has no Memory spelling —
    the №538 Query posture), memory_read is String|Secret by the
    container label (public → String, private → the gated Secret; the
    flat vocabulary has no union, and typing String would false-warn the
    private lane), and recall_top_k is a handlerless row (the spec!
    macro keeps it untyped; the verified intercept fact — both backends
    return the serialized JSON hit array as a String — is recorded
    inline for stage 1). Floor: 72/507 (1420 bp) → 107/507 = 2110 bp
    (+690 bp ≈ +6.9 pp — the package's own mass delivers the +3–5 pp
    wave target; the biggest single-package gain of the typing line).
    The memory semantics are untouched — registry/spec metadata only.

  • Naryad №538 (issue #848; the wave-23 typing line 3-A, P1): the db
    package — the honest exhaustion case. The package holds exactly TWO
    rows: db_execute → Unit (the interpreter path verified; the
    executed surface is the Query pipeline — the №484 DbAccess trait is
    not diverged from), and query stays Unknown HONESTLY (it returns
    the opaque Value::Query wrapper; the flat vocabulary has no Query
    entry, and the execution result type depends on the backend). Floor:
    71/507 (1400 bp) → 72/507 = 1420 bp. THE WAVE ARITHMETIC, named: the
    +3–5 pp per-package target is unsatisfiable on a 2-row package —
    +0.4 pp is the package's entire mass; the honest wave-level
    conclusion (the flat-vocabulary wall + the small packages) is
    recorded in the M2 report; the remaining typing mass toward the 30%
    gate lives in the packages outside the wave-23 line.

  • Naryad №537 (issue #847; the wave-23 typing line 3-A, P1): the list
    package leaves stage 0 — 9 rows typed against the verified handlers
    (slice/zip/sort_by/filter/dedup/condense/chunk/make_list → List;
    matches_any → Float, the 1.0/0.0 numeric verdict form). 5 rows stay
    Unknown HONESTLY with the inline reasons: get/first/last return the
    ELEMENT of a heterogeneous list (first/last add the ADR-0180
    soft-failure "" path), reduce returns the ACCUMULATOR, compact_list
    is List | the struct report{compacted, removed_count} — the flat
    vocabulary has no union and the №486 base-type comparison would warn
    falsely on a bent claim. Floor: 62/507 (1222 bp) → 71/507 = 1400 bp
    (+178 bp ≈ +1.8 pp; the honest exhaustion: of the 17 list rows 3
    were typed before, the 5 polymorphic element/accumulator returns
    cannot take a flat type without lying).

  • Naryad №536 (issue #846; the wave-23 typing line 3-A, P1): the string
    package leaves stage 0 — 10 rows of BUILTIN_REGISTRY carry the
    verified return type (capitalize/title_case/redact/squeeze/
    hashline_read/hashline_edit → String; regex_match → Bool;
    regex_captures → List; regex_replace → String; text_chunk → List),
    each verified against the handler's actual returns, NOT the name. Two
    rows stay Unknown HONESTLY with the inline reasons: reverse is
    polymorphic (String → String, List → List) and fuzzy_find_best is
    Unit | struct{index, candidate, score} — the flat vocabulary has no
    union, and the №486 base-type comparison would warn falsely on a bent
    claim; the metric is not to be gamed. THE BASELINE FIX, named: the
    №757 floor (998 bp) never took effect — the gate parser reads the
    FIRST threshold_bp line and the history kept it at 980; the
    superseded history lines now sit outside the match and ONE live
    triple is enforced. The total drifted 501 → 507 since №757 (the №526
    voice rows, the №507/№514 *_or twins — to_float_or/to_int_or shipped
    typed; the arity rewrites) and is resynced here. Floor: 52/507 actual
    pre-PR (1025 bp) → 62/507 = 1222 bp. The package's +3–5 pp target was
    unreachable inside the package bounds (only 12 untyped rows existed);
    the honest gain is ≈ +2.0 pp, the gap documented in the M2 report.

  • Naryad №534 (issue #843; the consolidated audit 30.09, Д-3 — the base
    №504, the threshold 3, the three sites): the last three TW-mirror
    marks leave src/vm.rs — the distill TRAINING gate sequence collapses
    into the ONE canonical copy, train_distill_snapshot in
    src/interpreter/learnable.rs (the closed label-set mapping, the №456
    holdout gate, the 30-epoch run, the №485 majority-baseline gate and
    its single-class carve-out, the NaN guard — the sequence the VM
    carried as the inline pre-№489 copy). run_distill_training keeps
    its exact contract and becomes the thin TW orchestrator (the short
    metadata lock → snapshot_clone → core → the short locked swap with
    the №530 slot-changed guard); the VM's try_train_distilled_model
    becomes the thin VM orchestrator (snapshot_clone → core → the direct
    post-gate swap). THE BEHAVIOR FIX, named honestly: the VM lane
    trained the LIVE model in place and only then decided the gate — the
    pre-№530 posture the TW path had already dropped; a refused gate on
    the VM path now leaves the weights untouched (pinned by the new
    n534_vm_failed_gate_leaves_weights_untouched test, mutation-checked
    red against the swap-ignores-gate form), and the VM train-error text
    carries the shared distill: training failed: prefix. The
    Distilled-mode PREDICT path remains a deliberate parallel form (the
    state orchestration is the two runtimes: &mut self vs Mutex + the
    №489 spawn) — its outcomes stay pinned by №503/№465; its dedup is
    future-counter work, not this naryad. The mirror baseline moves
    3 → 0 (ONLY DOWN; the marks left with the duplication, the gate runs
    green at 0). Local battery: check clean, clippy -D warnings clean,
    fmt clean, lib 887/887, №503 2/2, №465 fuzzer 9/9, №530 3/3,
    bug_530 VM recall parity 3/3.

  • Naryad №533 (issue #842; the consolidated audit 30.09, N-8,
    P2 docs/examples): the 14 COMPAT-tagged examples (№513) leave the
    live catalog — examples/compat/ is the honest archive. THE MOVE:
    exactly the 14 .mlog files whose FIRST-5-LINES header carries the
    №513 tag (the 15th file, p54_regex_crosscheck.txt, mentions COMPAT in
    its body and stays — the corpus scan verified the count). THE HEADER:
    every archived file now opens with the canonical honesty line —
    "archived to examples/compat/ by №533: historical syntax — not parsed
    since v0.27.x. See rule №513 (gh#797)" — above the original reason.
    THE PATH RULE: the grammar tests and the example-coverage test
    exclude by PATH (the archive is never entered), the №513 tag-scan is
    retired — a COMPAT-tagged file in the LIVE catalog is now debt, not a
    pass (the n513 assert + the simplified debt_counters.py scan, whose
    baseline stays 0). THE README: examples/README.md documents the
    live-catalog contract (sidecar / named check / the suites) and the
    archive rules (never delete; resurrect = rewrite + verify + move
    back, in its own naryad). THE CROSS-REFS: the book's quickstart and
    the architecture tree point at live examples; the two ADRs referencing
    the moved paths (№0045, №0063) carry the archive note. Boundaries
    held: nothing rewritten to the new grammar, nothing deleted.

  • Naryad №532 (issue #841; the consolidated audit 30.09, N-7,
    P2 compiler/hygiene): the wildcard_enum_match_arm lint leaves
    compiler.rs and covers the WHOLE N-1 surface — deny in
    src/interpreter/execution.rs, src/vm.rs and src/semantic.rs.
    THE CORE: is_truthy is now EXPLICIT in BOTH backends — every one of
    the 36 Value variants gets its own arm (the truthy set is
    {non-empty String, non-zero Float, true Bool, non-empty List}; every
    opaque handle, struct, secret, Unit is falsy), the semantics are
    byte-preserved and pinned by the №503 outcome-parity suite — the liar
    string can never again slip through a wildcard. THE TAIL: the 98
    remaining wildcard arms (report/parse/classification/dispatch
    catch-alls where NO security decision reads the arm) survive under
    FUNCTION-LEVEL #[allow] with the shared reason, recorded in the
    grep-protocol ledger docs/wildcard-tail-ledger.md (the per-function
    list); the found clippy quirk is honored — arm-level #[allow] does
    not gate this lint, the function level does. THE GATE: a NEW wildcard
    in these files fails the blocking clippy job until it is enumerated
    or allowed with a reason (the negative test: a probe wildcard was
    verified to trip the build, then removed).

  • HOTFIX (the first real CI run, 30.09→01.10): the two shipped-unseen CI
    defects the dead event delivery hid, both caught by the first
    blocking-checks-sync-carrying run after the Actions
    push/pull_request event outage (the last pull_request CI run was
    14:30 UTC 30.09; everything merged after that ran unchecked). (1)
    №535's tamper-test trap #2 asserted the wrong failure mode:
    validate() RETURNS the error list and never raises ValueError on a
    verdict-domain violation (only load_rows raises, on the column
    count), so the except ValueError branch was unreachable, the
    bogus-verdict trap could never spring, and the blocking job failed on
    every run with "the bogus verdict did NOT trip the check" — the
    gate's own first run caught its own bug; the trap now inspects the
    returned errors exactly like traps #1/#3 do. (2) №528's
    rust-toolchain.toml (channel = "stable") silently overrode the
    fuzz-smoke job's nightly install — cargo-fuzz's -Zsanitizer build
    probes died on the stable rustc ("the option Z is only accepted on
    the nightly compiler"); the job now pins RUSTUP_TOOLCHAIN: nightly
    at the job level (the rustup override precedence: env > the
    toolchain file), the rest of the repo keeps the №528 stable contract.

  • Naryad №535 (issue #844; the consolidated audit 30.09, the gate-0.28
    checklist item 3 — ADR-0179, P1 process/testing): the machine-readable
    record of WHO BLOCKS EXECUTION lands —
    scripts/ci/blocking_checks.tsv: 27 cells over the four commands
    (run/check/compile/serve) × phase (read/parse/audit/semantic/codegen/
    execute) × check × {blocks, warns, absent}, the state after №523.
    THE CELLS: 21 blocks (every read/parse/Cat-A/semantic/codegen/runtime
    refusal per command — the №523 startup gates for serve, the VM-compile
    gate for compile, the static import resolution everywhere), 2 warns
    (check's warning-only exit-0 surface; compile's unconditional
    import-warning noise over a PROCEEDING compile), 4 absent (the honest
    fixes: semantic warnings are silently dropped on the run path — mlog check is the reporting surface; the exemption list is empty today;
    JIT and mcp-serve are recorded as absent (зафиксировано) — outside the
    CLI pipeline by ADR-0022/0073 and ADR-0168). THE BINDING: every
    blocks/warns cell names its proving test — 17 new behavioral pins in
    tests/naryad_535_blocking_checks.rs drive the REAL mlog binary
    through CARGO_BIN_EXE (exit codes + stderr as the verdicts), the
    remaining cells reference the existing n98_/n523_ pins; the blocking
    blocking-checks-sync CI job validates the format, the test bindings
    (a word-boundary grep — a renamed/deleted test = drift = failure), the
    coverage and the absent rows, and the --tamper-test negative mode
    proves the traps spring (an unresolvable id, a bogus verdict, a
    duplicate key). THE GATE WIRING: the table feeds the №525 gate-facts —
    fact_blocking_check_cells: 27 machine-verified by
    blocking_checks_sync.py --count in the gate-facts-sync job (the
    unknown-fact fail-closed trap verified).

  • Naryad №529 (issue #838; the consolidated audit 30.09, Д-5+N-6,
    P1 reliability/hardening): the bounded registries grow the SECOND
    circuit — the total BYTE ceiling next to the №515 count ceiling.
    THE FACT: №515 bounded the NUMBER of entries (64 per registry), but
    neither the volume nor the aging — 64 entries × a large artifact
    stayed unbounded, and a long-lived serve held everything up to the
    count cap regardless of size. THE CIRCUITS: PDF_DOCS_MAX_BYTES
    (16 MiB, the estimated CONTENT bytes — title/author/every element's
    text+rows+path plus a fixed structural overhead, honestly NOT the
    serialized-PDF size), VIDEO_ARTIFACTS_MAX_BYTES (64 MiB — render
    bytes + latent payload f32×4 + the manifest text fields),
    VOICE_ARTIFACTS_MAX_BYTES (32 MiB — the audio bytes and the
    voiceprint embeddings, both maps). THE ORDER: one deterministic
    victim order per registry — the PDF store is true LRU (every
    mutation-path access restamps the seq via pdf_store_touch; the
    eviction victim is least-recently-USED), the in-memory video/voice
    maps evict by insertion age (the lowest monotonic id — their &self
    getters cannot restamp without interior mutability, the honest
    boundary is documented in the inventory). THE HONEST ALLOWANCE: a
    single artifact larger than the byte cap is admitted into an EMPTY
    store — the cap bounds accumulation, not one legit artifact (pinned).
    THE OBSERVABILITY: one loud [REGISTRY_EVICTION] stderr line per
    victim with the reason (count/bytes) and the store state — the
    serve-report primitive — plus the public metrics getters
    (pdf_store_eviction_metrics, video_registry_eviction_metrics,
    voice_registry_eviction_metrics); a structured HTTP metrics
    endpoint does not exist in this codebase and inventing one is outside
    the naryad's boundary. THE RATCHET: the machine-checked SSOT of the
    bounds lands (scripts/ci/registry_bounds_inventory.txt + the
    blocking registry-bounds CI job) — every map-shaped process-global
    static must carry an inventory row with BOTH bounds or an explicit
    exception reason (the LLM stream registry keeps the №263 LOUD
    refusal — silent eviction would kill in-flight streams; the per-stream
    byte growth is bounded upstream by the №757 max_tokens ceiling;
    GLOBAL_TEMPLATES is name-overwrite by design); the job verifies the
    constants against the sources (a silently moved cap = drift = failure)
    and is mutation-verified (a renamed anchor and a drifted value both
    trip it). Tests: the two-circuit pins per registry (1000 small /
    several large — each circuit bites on its own, the eviction order is
    deterministic), the PDF LRU restamp pin, the single-giant allowance;
    the №515 suite stays green unchanged (4/4).

  • Naryad №527 (issue #836; the consolidated audit 30.09, N-3,
    P1 security/voice): the AES-256-GCM ciphertext is now BOUND TO ITS
    SUBJECT — the GCM AAD carries the (subject_id, registry, schema
    version) triplet (the registry voiceprints, the subject name, the
    voiceprints-aad-v1 schema label — joined with the 0x1F unit
    separator so the ordered fields stay unambiguous), so a blob
    transplanted onto another subject's row FAILS authentication (the
    swap attack the bare tag accepted since №517 is closed — the tag
    proved integrity of the bytes, never the binding). THE HONEST
    BOUNDARY kept: the storage schema and the algo mark are untouched
    (№517's T2 pins AES-256-GCM-v1), the secret()-gate semantics are
    untouched; the discriminator between a bound row and a legacy row is
    the GCM authentication itself (try-bound first, then the loud
    transitional fallback). THE TRANSITION: №517-era rows (empty AAD)
    stay readable in the announced window — every such read emits
    [VOICEPRINT_NO_AAD_LEGACY] on stderr AND surfaces the honest
    VoiceprintCryptoStatus (LegacyNoAad) through the new
    load_voiceprint_with_status; every WRITE is AAD-bound, so the
    legacy population only shrinks; the deadline row (the fallback
    removal at v0.28.0) lives in limitations.md (the №524 rule), and
    privacy.md §2.1/§4 carry the new contour with re-verified anchors.
    THE KEY LIFETIME: the decoded 32-byte key buffer lives under
    Zeroizing in BOTH directions (wiped at the operation's scope exit
    — the crate was already in the tree, zero new dependencies). Tests:
    the swap refusal (RED-baseline verified — the binding mutation
    reddens exactly the three binding tests), the loud legacy read with
    the flag, the re-save rebinds story, the wrong-key refusal surviving
    the two-step decrypt, the fresh-writes-are-bound pin; №517 (7/7),
    №512 (4/4) and the store unit tests stay green unchanged.

  • Naryad №531 (issue #840; the consolidated audit A+D 30.09, N-9,
    P1 core/hardening): the LAST silently-soft refusal outside the №514
    naming rule enters its TRANSITION period. read_file of a missing
    file still returns the №254 soft "", but EVERY hit now announces
    [READ_FILE_MISSING] on stderr (the path named, the migration target
    named: read_file_or(path, default)), and the debt gate carries the
    marker counter (read_file_soft_missing in
    scripts/ci/debt_counters.py + the baseline — movement only down; the
    flip release deletes the sites and the counter reaches 0). The
    deadline lives in limitations.md's Error Protocol row (the №524 rule:
    the row closes in the same PR as the flip). The release following the
    №531 merge flips the branch to a loud [IO_ERROR]. The pinning tests
    (№254/№455/№507 suites) carry the transition markers; the functional
    examples needed NO migration (none relies on the empty string of
    absence — verified by the examples scan; the leak corpus is a
    compile-time suite and stays untouched). REFERENCE.md carries the
    contract in both read_file rows: *_or is soft, everything else is
    loud.

  • Naryad №530 (issue #839; the consolidated audit A+D 30.09, N-4,
    P1 reliability/llm): distill training no longer blocks the registry —
    the 30-epoch run happens on a WEIGHTS SNAPSHOT taken under the short
    metadata lock (ReflexModel::snapshot_clone, the Dense-only clone via
    Layer::as_any — the trait is untouched), with NO lock held during
    the epochs; a serve request answering mid-training acquires the
    registry in microseconds (the timed test: every in-training
    acquisition < 100ms, ≥3 acquisitions before the finished line). The
    trained snapshot is swapped back ATOMICALLY after the holdout gate
    passes — one short locked write with a slot-changed guard (a
    redeclared reflex is refused loudly, never clobbered). The honesty
    bonus: a FAILED gate now leaves the live weights untouched (the
    pre-№530 code mutated them in place even when the gate then refused
    the switch) — pinned by the weights-fingerprint test.
    DISTILLED RETRAINING lands: the low-confidence calls keep recording
    fresh (input, llm_output) ground truth in DISTILLED mode, and when
    the new examples cross the SAME threshold that built the model (the
    honest default: the pattern's own distill_after, the ADR-0115 floor),
    a retrain job runs on the single trainer thread; gate passed → the
    weights swap in, the mode stays DISTILLED; gate rejected → the LOUD
    degradation revert to TEACHING (distill.degraded audit) — silent
    degradation is impossible. The №496 e2e stays green (the TEACHING
    semantics byte-preserved).

  • Naryad №528 (issue #837; the consolidated audit A+D 30.09, Д-4+N-5,
    P1 deploy/ci): the MSRV lives in the build contract. rust-version = "1.93.1" in [workspace.package] (mlogpkg/mlog-lsp inherit) — cargo on
    an older toolchain now refuses with a clear resolver error (verified:
    1.85 and 1.89 both name the packages that refuse). rust-toolchain.toml
    pins the COMPONENT contract (clippy + rustfmt — the CI blocking jobs'
    surface); the channel tracks stable BECAUSE the clippy ratchet is
    written for new clippy (the №500 precedent: chunks_exact_to_as_chunks
    does not exist in clippy 1.93.1 — freezing the toolchain at the floor
    would break the -D warnings gate). THE HONEST DEVIATION from the
    naryad's draft (1.85), evidence-backed by the toolchain walk: 1.85 →
    weezl needs 1.88; 1.88 → rustyline needs 1.89 (file_lock); 1.89 →
    serial_test needs 1.93.1; 1.93.1 → the FULL check --workspace --all-targets is green. The README badge and the build-from-source
    wording now name 1.93.1 and point at rust-version as the SSOT. THE
    DEPLOY-PATH BLINDNESS was REAL and DOUBLE:
    the workflow file failed Actions YAML parsing since №511's landing (an
    unquoted backtick in a step name — the file could not parse at all, so
    NO run ever fired: not the weekly schedule, not the push trigger whose
    branches line ([main]) was verifiably intact) and the
    pull_request trigger did not exist. Fixed byte-level and
    YAML-validated; the pull_request trigger
    lands (Dockerfile/.dockerignore/example/src mains/Cargo.toml/Cargo.lock/
    rust-toolchain.toml/the workflow) so a dependency refresh rebuilds the
    image BEFORE the merge, not after. THE PR-TRIGGERED RUNS EXPOSED A
    THIRD LAYER: the dependency-priming step built the dummy source for
    core_benchmarks only while Cargo.toml declares TWO benches
    (stage4_benchmark landed with №482) — the manifest parse failed and
    the image could not build at all; the stub is added. AND the builder
    image tracks the contract (rust:1.85 → rust:1.93 — the 1.85 builder
    refuses the manifest exactly as the new rust-version gate intends)
    and carries libssl-dev (the locked tree's native-tls lane needs the
    system OpenSSL; the slim image predates that dependency lane). The deploy path
    was dead-on-parse AND dead-on-build since №511's landing, unverified
    for the whole window — exactly the blindness the №511 job was built
    to kill.

  • Naryad №526 (issue #835; the consolidated audit A+D 30.09, N-2,
    P1 security/voice): the GDPR Art. 17 erasure path for voiceprints
    LANDS — the right-to-erasure is no longer a document-only promise
    (privacy.md §2.1). THE HOLE: the store had only INSERT OR REPLACE
    (save), no delete, no list, no purge — a persisted print outlived
    every consent revocation. THE PATH: voice_delete(handle) — the
    idempotent registry erasure ("deleted"/"absent", a repeated erase
    succeeds; Voice handles erase the biometric print, Audio handles erase
    the artifact bytes; wrong types refuse loudly) and voice_list() —
    the informed-deletion basis (ids + models/sizes; the listing NEVER
    decrypts, the embedding bytes never enter any result). Neither is
    feature-gated: the erasure right cannot depend on a build flag. The
    PERSISTED twin: VoiceStore::delete_voiceprint runs the
    SECURE-DELETE path — the ciphertext blob is zero-overwritten in place
    BEFORE the row removal and the same-name voice_artifacts row is
    purged the same way ("файл артефакта + запись реестра") — and
    VoiceStore::list_voiceprints enumerates name/model/saved_at/algo/
    ciphertext-length without a key. HONEST BOUNDARIES (loud, not
    silent): SQLite cannot guarantee per-row block-level erasure
    (freelist/WAL page images persist until reused) — the row-level
    overwrite erases the row's live copy, the file-level guarantee stays
    the DB owner's; the consent_ledger rows SURVIVE erasure by design
    (the Art. 9 consent proof). privacy.md §2.1/§3 rewritten in the same
    PR (the №524 rule): "Delete path: ABSENT" → "available since the
    v0.27.x line", voice_delete/voice_list removed from the ABSENT list,
    the anchors re-verified by hand. Tests: the store cycle (save → list
    → delete → list empty), the zero-overwrite pin, the reopen/no-
    resurrection restart test, the artifact-row purge, the registry
    cycle, the builtin idempotency and loud-type refusals; the
    classification SSOT and REFERENCE.md carry both builtins (505→507).

  • Naryad №525 (issue #834; the consolidated audit A+D 30.09, Д-2,
    P1 process/ci): every fact_* record in gate_028_goals.txt is now
    machine-verified against its generating source by the blocking
    gate-facts-sync CI job — a hand-edited drift is a failure. THE
    DESYNC: the goals file carried fact_quorum_num: 9 / den: 9 while
    the single source of truth (count_duplicated_names.py) reports
    ZERO duplicated names since №483 — a liar gate record. THE SYNC: the
    counter gains a --quorum mode (quorum_num = live groups still
    carrying duplicates; quorum_den = the transfer-start group total,
    machine-parsed from the baseline history — the 7 №466 group entries
    plus the №483 FINAL group — and anchored by a new
    groups_at_transfer_start: 8 marker in the baseline header, which
    the mode cross-checks against the counted entries); the honest record
    is now 0/8 (goal 1/3 — MET). fact_open_high_server is verified
    against the live release-block label count via the GitHub API
    (token always present in Actions; locally the check skips loudly —
    the ADR-0179 §6 step-1 sync stays a release-time human step). An
    unknown fact_* without a machine source fails the gate (fail-closed:
    a fact without a checker is the Д-2 class re-opening). The negative
    test (--tamper-test, a CI step): every tampered fact value, and an
    unknown fact key, trips the check — the traps all spring.

  • Naryad №524 (issue #833; the consolidated audit A+D 30.09, Д-1,
    P1 docs): the limitations.md row for the serve-distillation defect
    reads Fixed in №495, v0.27.1 — its own condition had come true
    (№495 merged, v0.27.1 released, the both-backend e2e
    tests/naryad_496_distill_serve_e2e.rs stands), so the row obeyed the
    rule it prescribed: closed IN THE SAME PR, by the naryad number named
    in its own text, the defect text kept as the historical record — never
    removed silently. The closing rule itself (a limitations.md row closes
    in the same PR as its fix, by the number from its text — the gate-0.28
    checklist item, ADR-0179) is formalized as a wave-reporting protocol
    point and a candidate row for the №535 blocking-checks table.

  • Naryad №523 (issue #832; the consolidated audit A+D 30.09, N-1 High,
    P0 release-block): semantic findings now BLOCK mlog run and
    mlog serve — the liar string is gone. THE HOLE: run_program_with_dir
    blocked only two substring classes (contains("distill_to"),
    contains("[DENY_")), mlog serve ran NO semantic pass at all, and the
    tree-walking interpreter returned the string
    "[ERROR: unknown function 'x']" as the call's VALUE — non-empty
    strings are truthy in is_truthy, so if is_admn(user) with no
    is_admn defined took the then-branch: a security check became its own
    bypass. THE FIX: (1) every semantic::check_program error blocks the
    run path and the serve startup; exemptions classify ONLY by the
    structured SemanticErrorKind through semantic::is_exempt_from_blocking
    — the ONE explicit place, empty today — never by message substring;
    (2) the gate resolves the program's import tree STATICALLY first
    (semantic::resolve_imports_statically, the same file-lookup rule as
    the runtime loader and the bytecode compiler's resolve_import,
    visited-set on module path, main-file-wins dedup with first-wins
    module collisions — the runtime flat-merge warns, so the gate must not
    invent duplicate-name errors), which removes the historical
    false-positive class that once justified NOT blocking; (3) both TW
    unknown-function origins (run's call resolution and
    eval_expr_with_env) return the coded err-origin
    [UNDEFINED_FUNCTION] undefined function: 'x' instead of the truthy
    string (the №479 stable code); (4) the refusal carries the FIRST
    blocking finding's stable code at position 0 —
    [UNDEFINED_FUNCTION] Compilation error (Naryad #523): ... — machine
    consumers read the class, not the prose; (5) check_program walks
    ROUTE bodies for calls too (the №264-mutability-walk precedent) — an
    undefined function inside a route body now refuses the TW serve startup
    instead of failing per-request. The gate surfaced three PRE-EXISTING
    spec-vs-implementation arity drifts, fixed at the SSOT:
    session_set 2→3 and session_get 1→2 (the implementations
    hard-require the session-id forms), session_clear 0→1 (left the
    arity-0 "variadic" list that never checked anything),
    respond_html 1→2 (status + html); forget (the №72 memory surface
    1..2 + the registry surface 3..4 — a non-contiguous union) and
    render (template params are data, the 1-arg form is the №448
    taint-lift surface) are DYNAMIC-ARITY names — the static check stays
    silent, the builtin stays the loud runtime validator. Contracts
    repaired per place: examples/p50_unknown_fn flipped from a .expected
    golden (the liar string enshrined as the expected stdout) to a .error
    contract; examples/p2_multi_errors.error updated to the gate text
    (the unknown-type refusal arrives one layer earlier);
    examples/p71_retry_demo dropped its env_get reference (a function
    that existed in NEITHER backend — the №513 crosscheck exclusion dies
    with it) for a placeholder header, since reading env() straight into
    network headers is the №391 PII_EGRESS_NETWORK violation; the
    REFERENCE.md vector sample runs as its own doc-test: skip block
    (embed/vec_store are vec-feature, off by default); the №465
    diff-fuzzer's three phase-family known-classes closed by the ratchet
    (the TW side refuses at the gate with the same №479 code the VM compile
    side always had) — the lines and their corpus examples are removed.
    Tests: tests/naryad_523_semantic_gate.rs — the exploit shape refused on
    TW (gate) and VM (compile), the interpreter err-origin pinned (no liar
    string from any call site), serve startup refusals through the real
    run_server (route body and pattern body shapes), the
    import-using program still runs (no false positives), the №181/№392
    classes still block, the exemption list pinned empty.

  • Naryad №519 (issue #803; the consolidated audit 28.09 C-20, P2 — the
    contribution Г tied to the C-06 voice work): the engineering privacy policy
    LANDS — docs/privacy.md, an honest inventory of what the runtime stores,
    where, how it is deleted, and what it refuses to promise. In the
    REALITY.md discipline every claim carries a verdict — CONFIRMED (code
    anchor given), PARTIAL (implemented with the named caveat), ABSENT (not
    implemented, the load-bearing list) — and the page opens with the
    boundary statement: an engineering policy, NOT a GDPR declaration, NOT a
    legal document; the deployer owns the compliance case. The inventory
    covers the storage model (no hidden data directory — every persistent
    path is program-declared, operator-env-set, or an explicit sandboxed
    save/export), the per-category data map (voiceprints — honest AES-256-GCM
    library code that is UNWIRED, the voice builtins being loud stubs, so a
    shipped runtime stores zero voiceprints; memory lanes with the three
    delete flavors — hard DELETE, the №280/№445 soft forget-ledger with an
    owner-side vacuum, restart eviction; contacts/calendar as CalDAV/CardDAV
    clients with data on the remote server and plaintext session creds in RAM
    as a named gap; the process-local consent ledger and RAM-only likeness
    tokens; sealed media with mandatory sidecar manifests; opt-in
    metadata-only LLM traces; the semantic-cache input-embedding caveat;
    profile as a read-only KV projection; the №515-bounded registries), the
    consolidated SQLite table map, the encryption-status table with the
    plaintext frontier named openly, and the retention defaults. Cross-links:
    threat-model.md (the voice at-rest row now points at the file),
    limitations.md (the voice-pillar removal condition now points at the
    file), README Documentation table + repo tree, llms.txt. The claims never
    exceed what the code does: every ABSENT (voice_delete,
    memory_forget_all, a consent purge, session eviction) is stated as ABSENT.

  • Naryad №518 (issue #802; the consolidated audit 28.09 C-19, P2 — the only
    survived contribution Г not covered by В19): the dependency-tree gate LANDS —
    deny.toml (cargo-deny) plus the blocking cargo-deny CI job. Before this
    naryad nothing verified dependency LICENSES (the cargo-audit job covers
    RUSTSEC advisories only), while the project ships as "MIT OR Apache-2.0" —
    a legal exposure the moment an external adoption audits its transitive
    licenses. The gate runs cargo deny check licenses advisories bans over
    the whole graph, ALL targets (409 crates at check-in). The allow-list is
    the OBSERVED permissive set compatible with the project licensing (MIT,
    Apache-2.0, BSD-3-Clause, ISC, Unicode-3.0, BSL-1.0, Zlib, Unlicense, 0BSD,
    CDLA-Permissive-2.0 — the last one is webpki-roots' root-CA data bundle,
    a certificate list, not code), every entry carries its reason inline (the
    imap/ADR-0109 comment discipline). Advisories: vulnerabilities FAIL, the
    unmaintained findings stay advisory warnings — unmaintained = "none"
    mirrors the cargo-audit job posture (bincode RUSTSEC-2025-0141 and
    ttf-parser RUSTSEC-2026-0192 are the two known warnings at check-in; the
    RUSTSEC visibility for those lives in cargo-audit). Bans: duplicate
    versions warn, never fail — the explicit naryad choice. One license
    exception documented: ical 0.11.0 ships an Apache-2.0 LICENSE file but
    forgets the manifest license field — recorded via a clarify entry, the
    missing-field warning stays visible on every run. The branch-protection
    required-contexts list on main grows to 21 entries ("cargo-deny
    (blocking)"); the README CI badge moves 27 → 28. Mutation-verified: the
    check with an empty allow-list rejects every crate in the graph (red),
    dropping CDLA-Permissive-2.0 from the allow-list turns the run red at
    webpki-roots, unmaintained = "all" turns the advisories run red on the
    two RUSTSEC ids — the gate is load-bearing, not decorative. Scope note:
    the naryad does not update imap and does not touch dependency versions —
    control and documentation only.

  • Naryad №517 (issue #801; the consolidated audit 28.09 C-06 step 2, P2 —
    the wave-21 security/voice): the honest at-rest crypto for voiceprints
    LANDS — the №512 fail-closed refusal lifts where it can be lifted
    honestly. The real runtime persists voiceprints under AES-256-GCM (the
    SAME primitive as the encrypt()/decrypt() builtins, the existing
    aes-gcm crate — no new dependencies); the key arrives ONLY through
    the secret()-gate semantics (env-sourced hex-256, NEVER derived from
    the name); every write carries a fresh random 96-bit nonce; the blob
    is self-contained nonce ‖ ciphertext+tag; the schema carries the
    algo label ('AES-256-GCM-v1'). A keyless save stays fail-closed
    ([VOICE_INSECURE_STORE] — biometric data is never persisted
    unencrypted); a legacy pre-№517 XOR row is NEVER silently read as
    decrypted — the load refuses loudly ([VOICEPRINT_STALE],
    "re-enroll" — the №504 migration posture); a wrong key refuses at the
    GCM auth tag ([VOICEPRINT_DECRYPT]). The mock runtime keeps the
    insecure placeholder path for the skeleton tests with the insecure
    mark VISIBLE IN THE SCHEMA ('INSECURE-XOR-MOCK' — a mock row can never
    masquerade as encrypted). threat-model.md now states the true at-rest
    contract with the residual exposure named (key management belongs to
    the deployment); the №512 honesty pins evolved with the truth (the
    crypto name is REQUIRED in store.rs, FORBIDDEN elsewhere in
    src/voice/). Blocking tests (tests/naryad_517_voice_aes_gcm.rs): the
    keyed roundtrip, the blob-is-ciphertext (not plaintext, not
    name-XOR), the wrong-key refusal, the keyless refusal, the loud
    legacy migration, the per-write nonce uniqueness, the mock schema
    mark. Mutation-verified: a silent legacy read turns T4 red; a fixed
    nonce turns the uniqueness test red. The privacy policy cross-ref
    follows in №519 (docs/privacy.md).

  • Naryad №515 (issue #799; the consolidated audit 28.09 C-12, Low/Medium —
    the wave-21 P2 reliability): the process-global registries are BOUNDED —
    growth under errors no longer linear. THE HOLE: PDF_DOCS (the
    in-progress PDF store) removed an entry ONLY in pdf_save — a route
    that called pdf_create and then errored or was abandoned before the
    save accumulated orphans without limit. Now the store is capped
    (PDF_DOCS_MAX = 64) with OLDEST-sequence eviction on insert (an
    in-flight doc is never the victim unless it outlives MAX newer
    creates); the same patch family lands for the same confirmed class:
    VIDEO_REGISTRY (VIDEO_ARTIFACTS_MAX = 64 — remove_artifact had
    zero production callers) and VOICE_REGISTRY (VOICE_ARTIFACTS_MAX
    = 64, both the audio artifacts and the mock-path voiceprints).
    LLM_STREAM_REGISTRY was already bounded since №263 (the loud
    STREAM_LIMIT_REACHED); GLOBAL_TEMPLATES is keyed by declaration
    NAME (overwrite semantics — bounded by the program surface, not by
    requests). Blocking tests (in the lib suite, serial): 1000 abandoned
    creates leave the store AT the cap; the oldest orphan is gone; the
    newest in-flight doc survives; 1000 video/voice inserts bound both
    registries. Mutation-verified: removing any eviction turns its bound
    test red. The honest boundary is on record in docs/limitations.md:
    bounded ≠ request-scoped — full request-bound isolation is a separate
    infra naryad if a workload ever demonstrates the cap breaking a legit
    program.

  • Naryad №514 (issue #798; the consolidated audit 28.09 C-10, Medium —
    the wave-21 P1 hardening): the ONE soft-failure rule lands for the
    conversions — "silence is visible in the name" (ADR-0180, the №481
    env/env_or naming rule). THE HOLE: to_float("abc") returned 0.0
    silently — the audit's vector (to_float(json_body().amount) with
    "12,50" → 0.0 → a zero-value charge with no error). Now
    to_float/to_int REFUSE a non-numeric string LOUDLY with the
    stable [TYPE_MISMATCH] code, naming the input and pointing at the
    _or twin; the explicit fallback lives in the NEW builtins
    to_float_or(value, default) / to_int_or(value, default)
    (registry 503→505, append-only) — the parsed value when the string
    parses, the default when it does not, every fallback firing announced
    on the audit stderr ([TO_FLOAT_OR]/[TO_INT_OR] — the №326 posture:
    the FACT, never the value). A non-scalar input and a non-Float default
    stay LOUD in the _or twin too — explicit silence covers DATA, not
    type errors; Bool → 1.0/0.0 and float truncation are conversions,
    not silence — unchanged. ADR-0180 §3 publishes the silent/loud
    INVENTORY by group (math, io, memory, http/llm) with every preserved
    soft site carrying its owning naryad (read_file №254, mem_get/
    session_get KV-shape contract); new soft sites are FORBIDDEN — the
    _or form is the only way in. The stale crypto.rs comment claiming
    env() is soft (pre-№481) is corrected and pinned by the
    source-honesty grep. BREAKING (the intended 0.28 change): programs
    converting non-numeric strings with to_float/to_int now fail at
    the exact line — the migration is mechanical
    (to_float(x) → to_float_or(x, 0.0)). Blocking tests
    (tests/naryad_514_loud_conversions.rs): the audit vector, the loud
    refusals, the explicit-silence twins, the unchanged conversions, the
    roundtrip through to_string, the classification drift guard
    (№449), mutation-verified.

  • Naryad №513 (issue #797; the consolidated audit 28.09 C-09, Medium):
    the 35 unverifiable examples are checked or honestly COMPAT-tagged,
    and the hole is fenced. The audit found 45 examples/*.mlog without
    a .expected/.error sidecar and only 10 of them mentioned anywhere
    in tests/CI (the fact-check t85 confirmed both numbers exactly) —
    serve, file I/O and memory, the most dangerous paths, were verified
    by nothing. Now every example has ONE of: a golden sidecar
    (10 .expected: p11_utf8_full, p4_benchmark, p6_kv_memory,
    p8_session_memory, p_p01_respond_html_query_param, p7_cyrillic,
    p161_helper, p161_deep_helper, p9_cyrillic_unicode,
    p6_template_composition, p71_retry_demo — the stdout captured from
    the REAL run, not invented; 1 .error: v05_final_integration whose
    failure IS the contract — the UNTRUSTED_DECISION anti-injection
    invariant fires on its unsafe pattern), a named blocking test
    (tests/naryad_513_example_coverage.rs: the p120_* family through the
    REAL mlog test binary — passing exits 0, failing non-zero, multiple
    reports BOTH failures, filter runs the selected test only; the
    p31/p51 /fast serve goldens — the /slow routes are external-network
    demos and are deliberately not exercised; the vm_serve_realistic_dept
    a/b/c ONE-LINE fragments composed into a server and run on the VM
    backend — their named purpose, "dept-a: ping" per fragment; the
    p7_cyrillic golden — the p7_* exclusion in golden.rs predates the
    example and left it unchecked), or a COMPAT-513 header tag (14 stale
    prototypes whose grammar predates the current parser —
    server { → mlogserver, top-level statements, # comments,
    entity-method patterns: NOT checked with invented behavior, the
    rewrite is a separate naryad; the reason per file). The inventory
    invariant all_examples_have_a_check_or_an_honest_compat_tag is the
    permanent fence: a NEW example without a check fails CI. The debt
    gate grows the example_uncovered counter (debt_counters №513: the
    sidecar-less, mention-less, COMPAT-less .mlog files) with the
    checked-in baseline example_uncovered: 0 — moves ONLY DOWN (the
    №468 rule); the golden.rs error suite learned the per-example .env
    sidecar (the same ADR-0169 §3.4 posture the .expected suite uses).

  • Naryad №511 (issue #795; the consolidated audit 28.09 C-05, Medium —
    the wave-21 P1 deploy fix): the documented deployment path actually
    works and is VERIFIED. The old image died at clap on startup
    (CMD ["mlog", "serve"] — the file argument is NOT optional,
    src/main.rs Serve { file: PathBuf }), shipped no .mlog program,
    carried a DEAD ENV METALOGOS_PORT (nothing in src/ ever read it —
    the port comes from the program's mlogserver { port: ... }
    declaration), and was checked by nothing (0 docker mentions in CI,
    unchanged since 2026-08-27). Now: ENTRYPOINT ["mlog", "serve"] +
    CMD ["/app/main.mlog"] with the shipped example program
    (examples/docker_hello.mlog — volume-mount over /app/main.mlog runs
    YOUR program without a rebuild); the dead env is REMOVED; the
    deploy-time bind host is METALOGOS_HOST — read ONLY when the
    program declares no host: (the declaration always wins, the №164
    loopback default unchanged; the image sets
    ENV METALOGOS_HOST=0.0.0.0); the built-in GET /health route
    answers 200 "ok" with NO side effects (skipped when the program
    declares its own /health — the declaration wins, no duplicate-route
    panic); the mlog health [url] subcommand (default
    http://127.0.0.1:8080/health, METALOGOS_HEALTH_URL override, 5s
    timeout, exit 0/1) is the HEALTHCHECK command WITHOUT curl
    (bookworm-slim has no curl — the audit's curl-based HEALTHCHECK
    would have left the container permanently unhealthy); the base
    images pinned to versioned tags (rust:1.85-slim-bookworm /
    debian:bookworm-slim); .dockerignore keeps the context lean. The CI
    job (.github/workflows/docker.yml — weekly + on Dockerfile/server
    changes + dispatch) builds, runs the container, probes /health,
    exercises mlog health and waits for the Docker HEALTHCHECK
    "healthy" state — this IS the №509 0.28-gate criterion №24.
    Blocking tests (tests/naryad_511_docker_health.rs): the built-in
    /health, the program-declared /health precedence, the bind-host
    ladder (declaration > env > loopback).

  • Naryad №504 (issue #787; the audit 28.09 §3.1 companion, P1 — the
    wave-19 tail): the distillation features leave the byte-position
    simple_embedding (the sum of bytes over i % dim — NOT a semantic
    representation) and land on the hashed TF-IDF form of the
    EmbeddingBackend TF-IDF fallback (src/embeddings.rs): offline,
    deterministic, network-free, bounded memory by construction (no
    tables — the token hashes into its fixed bucket, FNV-1a 64 per the
    №487 stable-hash posture; the SAME tokenizer as TfidfEmbedding).
    ALL distill paths call the ONE canonical copy
    (distill_hub::distill_features → embeddings::hashed_tfidf_vector):
    the hub inference, the TW training/inference, the VM
    training/inference — the vm.rs "Ported verbatim" mirror wrapper is
    DELETED and the №502 mirror metric moves 4 → 3 as the baseline
    planned. The honest measurements (tests/naryad_504_distill_features.rs,
    printed): the legacy cosine is ~0.79-0.90 for ANY pair of strings —
    paraphrases, rewordings, UNRELATED topics (no discriminative power);
    the hashed features separate the worlds (unrelated 0.00, reordered
    paraphrase 0.89). On the 40-utterance 4-intent corpus at the
    PRODUCTION training budget (30 epochs, lr 0.1) both extractors tie
    (holdout 0.375, both stay TEACHING — the Dense head barely converges
    at this budget); at a converging budget (lr 0.5, test-side only) the
    hashed features reach 0.750 vs the legacy 0.250 — the features are no
    longer the ceiling, the training budget is the next lever (the
    owner's semantics decision, NOT silently moved). Feature-signature
    migration: reflex_save persists the extractor signature
    (feature_signature TEXT, the additive ALTER migration — old databases
    extended, never rejected); reflex_load of a pre-№504 row (NULL →
    bytes-pos-v1) SUCCEEDS (weights readable) with a loud
    "FEATURES CHANGED, RETRAIN THE MODEL" message, and the distill paths
    (hub + TW + VM) refuse the confident answer for a signature-mismatched
    model (the [AUDIT] distill.feature-stale line, stay on the LLM path) —
    a successful reflex_train re-binds the weights to the current
    features (the single-point rule in ReflexModel::train). The №485
    separable fixture re-derived for token-level features (the old "aa*"
    corpus keyed on the byte extractor's first-character bucket; the gate
    contract unchanged); the №496 serve e2e unaffected (single-class
    corpus, feature-independent).

  • Naryad №516 (issue #800; the consolidated audit 28.09 C-14, Low): the
    stale match-support ignores lifted. The audit's grep-protocol found
    the whole "TODO: VM compiler does not yet support match with X"
    family obsolete — the support landed long ago (compiler.rs
    MatchArm::StartsWith → MatchTest::StartsWith; the VM executes it
    at both sites; semantic checks the arms) and №510 made the Ne claim
    false too. Six ignores lifted, each proven GREEN before the lift:
    starts_with compiles + full run (phase19_22 ×2, the C-14 target),
    Ne-in-rule-conditions (obsolete by №510), match with string-literal
    arms / contains / compare operators (phase18 ×3). The debt gate
    thresholds moved ONLY DOWN with the revision record:
    ignore 58 → 52, ignore_todo 22 → 16 (dead_code 37 unchanged); the
    remaining ignores in the two suites carry real semantic-checker
    TODOs — not touched. Blocking CI now runs 6 previously hidden tests.

  • Naryad №512 (issue #796; the consolidated audit 28.09 C-06, Medium —
    step 1 of 2): the voiceprint store honesty fix. The comments and the
    schema claimed "encrypted at rest (AES-256-GCM via the secret() stack)"
    while the code XORs the bytes with a key derived from the PUBLIC
    voiceprint name — reversible by anyone who reads the table; a
    voiceprint is biometric data (GDPR Art. 9 special category) and a fake
    crypto label misleads auditors, users and grant reviewers. Now: every
    false crypto mention is gone from src/voice/ (the six store.rs sites +
    the encoder.rs contract line — the №480 class rule: fix at the common
    point, close the class); encrypt_placeholder is renamed
    insecure_placeholder (and decrypt_placeholder → insecure_restore)
    tree-wide; in the REAL runtime (no METALOGOS_MOCK_LLM, the SSOT
    predicate №454) save_voiceprint REFUSES LOUDLY with the new
    [VOICE_INSECURE_STORE] coded error (the №413/ADR-0169 stamp
    convention, deliberately NOT in ORIGIN_STAMPED_CODES — the store has
    no program-facing surface yet) and persists NOTHING; the mock runtime
    keeps the placeholder path for the skeleton tests (the allowed
    insecure-mark boundary). Blocking tests
    (tests/naryad_512_voice_insecure_store.rs): the loud refusal (stamp at
    position 0, nothing persisted), the mock roundtrip unchanged, the
    source-honesty pins (no "AES-256-GCM" / old names in src/voice/,
    tree-wide rename check), the stable class constant.
    threat-model.md "Phase boundaries" + limitations.md "Voice Pillar" rows
    added, cross-referencing the future docs/privacy.md (№519). Step 2 (the
    real AES-256-GCM crypto) is naryad №517.

  • Naryad №509 (issue #792; the audit 28.09 §4 + §3.4п3): the 0.28
    release gate criteria v2 (ADR-0179) — the gate flips from
    no-regress ratchets to ABSOLUTE goals: the typed-signature share
    REACHES the owner-fixed floor (draft 30 bp%, now 9.96%), zero open
    High findings in the server path (the release-block label
    discipline, the fact record synced at the gate run), the transfer
    domain quorum ≤ 1/3 (draft), the №502 mirror metric carried
    only-down, and the FUNCTIONAL criterion — every state-accumulating
    declaration (distill_to, memory, conversation, cron) has a
    both-backend run_test_server e2e (the inventory:
    distill done via №496; memory/conversation/cron pending — follow-up
    naryads). unfreeze_gate.py gains --gate-target 0.28 (fail-closed on
    any missing record; the legacy 0.27.x read unchanged); the release
    checklist (docs/release-checklist.md) names the human steps between
    the evidence and the owner-only decision. The ADR number moved to
    0179 (0178 was taken by the generative-contour boundary in the
    meantime — noted in the naryad report).

  • Naryad №501 (issue #784; the audit 28.09 section 3.2 minor + 3.7):
    the sensitive-path deny vocabulary is CASE-INSENSITIVE — every name
    and component comparison (io.rs sensitive_path_match and
    sensitive_name_match, fs_gate.rs hard_write_name,
    hard_write_path_hit, gate_write_resolved, and the №500 canonical
    re-check through them) runs on the LOWERCASED form, so .ENV,
    App.DB-WAL, APP.MLOG, Metalogos.TOML and a .GIT component
    refuse exactly like their lowercase spellings (on macOS/Windows the
    same on-disk file must not become reachable by spelling alone). The
    hard write-deny classes stay unconditional under any spelling: the
    allowlist crane cannot unlock APP.MLOG, Metalogos.TOML or .GIT
    writes (the №475 image-integrity posture, now case-independent).
    Blocking tests (tests/naryad_501_write_deny_case.rs, 8): the four
    audit spellings on both gate sides, the allowlist-still-locked
    APP.MLOG write, the exact-name crane for App.DB, and the
    legitimate-path boundary. Mutation checks: removing the lowercasing
    at either point turns the pinned spellings red (2 + 5 tests). The
    limitations.md №455 case-sensitivity row is marked fixed; an honest
    row documents the absence of a dedicated macOS CI job
    (comparison-level equivalence + the pinning tests as the current
    verification).

  • Naryad №507 (issue #790; the audit 28.09 §3.7): read_file_or(path, default) — the EXPLICIT-silence twin of read_file, the _or
    symmetry with env_or (№481) the external audit asked to make
    explicit. A MISSING file yields the caller's default (announced on
    the audit stderr — the path is named, the value never, the №326
    posture); every other branch is shared with read_file by
    construction (one read_file_impl body, two missing-file policies):
    sandbox violations and the №455 sensitive-name deny-list stay LOUD
    [SANDBOX_VIOLATION], open/read failures stay LOUD [IO_ERROR] —
    the explicit silence never bypasses them. The read_file contract
    is UNCHANGED (№254, the empty-string soft default stays). Registered
    in BUILTIN_REGISTRY append-only (501→502, the .mbc indices do not
    shift), the SSOT classification/reference regenerated, the
    curated REFERENCE.md rows + example added. Verified: 5 blocking
    tests (tests/naryad_507_read_file_or.rs) — the explicit default
    on a missing file, the unchanged №254 base contract, an existing
    file yields the content, the .. traversal stays loud, the
    deny-list stays loud; lib suite green; clippy -D warnings clean.

  • Naryad №503 (issue #786; the audit 28.09 §3.6 Low/Medium): the
    OUTCOME rule — a TW/VM divergence where one side is ok and the other
    is err is a correctness defect BY SHAPE, un-pinnable in the №465
    corpus (a known-outcome line fails the parse like a blocked- line;
    an Outcome divergence never matches as known at run time). The two
    former outcome classes left the corpus in this PR: (b) the
    entity-in-pattern gap is FIXED — the TW ident read falls back to the
    declaration globals (self.variables, the exact set the VM models
    with StoreGlobal/LoadGlobalByName), local bindings shadow, writes
    stay local; pinned by tests/naryad_503_outcome_parity.rs (both
    backends agree, mutation-verified). (a) the memory_forget compile
    gap is root-caused with the repair plan in the naryad report (the
    №272 vec-gate vs the TW soft unknown-function fallback vs the VM
    loud compile — a semantics decision for the owner). ALSO: the corpus
    seed order is DETERMINISTIC now (stateful seed_*.mlog first — the
    same corpus was green with one read_dir order and red with another);
    the example-reproduces-class ratchet (№479) stays live.

  • Naryad №502 (issue #785; the audit 28.09 §3.3): the THIRD dedup
    metric — the explicit TW-mirror MENTIONS in src/vm.rs
    (scripts/ci/mirror_counter.py + the checked-in baseline
    scripts/ci/vm_mirror_baseline.txt + the vm-mirrors (blocking) CI
    job next to the ops-pairs gate; also folded into the unfreeze-gate
    summary's Dedup criterion 4.2). Neither №462 (name literals) nor
    №484 (ops pairs) saw vm.rs's whole-block mirrors — the distillation
    drift (the №489 background training in the interpreter only,
    vm.rs training synchronously) lived exactly in that blind spot. The
    fixed narrow regex (case-insensitive, over comments and
    doc-comments): mirror of the TW | ported verbatim | TW-identical | same as the TW. Deliberately coarse (the audit's posture): counts
    MENTIONS, a false positive is fixed by rewording the comment, never
    by weakening the metric. Movement ONLY DOWN: growth fails the gate.
    The baseline = the fact at adoption, after №495 collapsed the
    distillation mirror cluster: 4 marks (the №199/ADR-0121 registry
    declaration pass; the №456 holdout gate ×2; the simple_embedding
    mark predating the №495 unification — moves down with №504). The
    per-mark plan (unify / justify) is in the naryad report.

  • Naryad №508 (issue #791; the audit 28.09 §3.7): the free field order
    reaches the four remaining CONFIGURATION bodies — memory_body
    (kv, persist), sandbox_body (allowed, forbidden, timeout),
    eval_body (dataset, metric, threshold) and conversation_body
    (ttl, max_messages, compress_after) — completing the №490 rule for
    the same language constructs. Each body is an unordered repetition
    of its fixed field set (silent field wrappers keep the inner rules
    directly in the child list, so the .find()-based extraction is
    unchanged); a duplicate field is a LOUD parse error naming the field
    and its position (guard_unique_field_groups). The eval dataset
    requirement moved from the grammar (a positional rule) to a loud
    parser error naming the requirement — an explicitly EMPTY
    dataset: [] stays the legal PASS-by-convention case (the eval
    harness contract, total=0 → passed, pinned). The inner kv {...}
    sub-block keeps its fixed order (the №490 posture with the llm
    provider sub-block). Verified: 10 blocking tests
    (tests/naryad_508_grammar_free_order_4_bodies.rs — every
    permutation class parses identically, duplicates loud, the eval
    dataset requirement loud, the №490 llm bodies regression-pinned);
    the eval/conversation/memory/sandbox contract suites green; lib
    872/0. The syntax doc names the free order for all four bodies; the
    live examples (p2_full_adapt.mlog, p_eval_harness.mlog) show
    permuted layouts.

(nothing yet)

  • Naryad №496 (issue #781; the audit 28.09 §3.1 п.3 — the audit's
    headline recommendation): the blocking END-TO-END distillation test
    through the serving path — tests/naryad_496_distill_serve_e2e.rs.
    The audit's finding was that NO test exercising distill_to went
    through run_test_server/serve, which is exactly why the №495
    defect was invisible to CI; this test closes the class. It boots the
    REAL server (run_test_server) on BOTH backends (TW and VM), with
    the learnable pattern declaration carrying distill_after and
    distill_to, drives N+10 POST requests through the route, waits for
    the distill.training-* audit events, verifies the DISTILLED
    answers stop calling the LLM (the MockLlm call counter freezes) and
    that the accumulated examples SURVIVE a server restart (the
    distill_samples table from №495). Red-before pinned at
    merge-base 0b55b6e (the test could not even complete the
    accumulate-then-serve arc there); green-after on №495. Verified:
    green on both serve backends; lib 872/0.

[0.27.1] - 2026-09-29