Skip to content

feat(F-1): the model you named is the model that answers — apr chat never substitutes the demo model, and a sharded SafeTensors index loads - #3050

Open
noahgift wants to merge 4 commits into
mainfrom
agent/F-1
Open

feat(F-1): the model you named is the model that answers — apr chat never substitutes the demo model, and a sharded SafeTensors index loads#3050
noahgift wants to merge 4 commits into
mainfrom
agent/F-1

Conversation

@noahgift

@noahgift noahgift commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Ticket

PMAT-1080 · PP-066 row F-1 · closes #3022, closes #3024 (asks 1, 2, 4; ask 3 is row F-2) · epic #2873

Claim

A model file the user named is the model that answers, or the command refuses. apr chat no longer substitutes its built-in toy demo model for a sharded SafeTensors index, and a model.safetensors.index.json now loads the real model.

The defect, measured on origin/main before anything changed

Binary apr 0.65.2 (c04eda87d), built in this worktree from the tip of main. Model: a 2-shard fixture derived offline from ~/models/qwen2.5-coder-0.5b-instruct-safetensors — no download.

cell origin/main this branch
single-file × apr run ok ok
single-file × apr chat ok — Loaded SafeTensors format in 0.50s (988.1 MB) ok
sharded × apr run okOutput: 2 + 2 equals 4. ok
sharded × apr chat Chat Demo (Tiny Model) · Loaded Demo format in 0.00s (0.0 MB) · [0 tokens in 0.0s] · exit 0 Model Chat (Sharded SafeTensors) · 988.1 MB · [12 tokens in 3.7s = 3.2 tok/s] · Assistant: 2 + 2 equals 4.

Records: evidence/format-honesty/{before,after}/.

One correction to #3022's framing. apr run on a sharded index works on main — row 3, measured, unmodified. The defect is apr chat alone. Whatever failed for the reporter on a real 7B pull is not reproduced by this fixture and stays [U]; the matrix now covers that cell on every run, so a regression there is caught rather than inferred.

Root cause — and why one added arm would not be the fix

Path::extension() returns the LAST dot-segment: on model.safetensors.index.json (the exact filename apr pull writes and then recommends) it is Some("json"), matching no arm and falling through to ModelFormat::Demo. That is the instance.

The class is that two independent decisions existed — print_welcome_banner asked the PATH, ChatSession::new asked the first eight BYTES, nothing compared them — so "banner says SafeTensors, session loads Demo" was a reachable state with no falsifier anywhere in the tree. resolve_chat_format is now the one decision: suffix before extension(), then the magic bytes of an odd-named file, then a refusal (CliError::ModelLoadFailed, exit 6, read from error.rs). Demo is not an outcome for a path that exists.

RED test sha

9d0ddcf31crates/apr-cli/src/commands/chat_format_honesty_tests.rs, 6 rows, both polarities.

Acceptance (.pr/F-1/accept.sh, 6/6)

== python3 scripts/make_sharded_safetensors.py --self-test           7/7 rows   rc=0
== bash scripts/check_format_command_matrix.sh --self-test           7/7 rows   rc=0
== cargo test -p apr-cli --lib -- <the six honesty rows>             6 passed   rc=0
== pv validate contracts/patterns/format-command-honesty-v1.yaml     0 errors   rc=0
== bash scripts/check_guards_are_wired.sh                            PASS (ratcheted)
== bash scripts/check_format_command_matrix.sh --matrix --apr <bin>  4 cells    rc=0

Mutation (RED → GREEN), run locally, both mutants

mutant RED rows restored
detect_format's .safetensors.index.json arm never matches sharded_index_resolves_to_the_sharded_format_not_demo (3 passed / 1 failed) 4/4
resolve_chat_format returns Ok(Demo) instead of refusing a_truncated_file_is_refused…, an_existing_unrecognised_file_is_refused_not_demoted, resolve_never_answers_demo_for_a_file_the_user_named (1 passed / 3 failed) 4/4

And the gate's own RED leg — the whole point of #3024 — same matrix, same fixture, two binaries:

origin/main  : FAIL  sharded x apr chat   the DEMO model answered for a file the user named (#3022)   rc=1
this branch  : ok    4/4 cells                                                                        rc=0

The three unaffected cells stay GREEN on the defective build, so the gate discriminates rather than merely failing. CI run ids follow on this PR's first green.

Contract

contracts/patterns/format-command-honesty-v1.yamlkind: pattern, 4 equations, 5 proof obligations, 4 falsification tests. pv validate: 0 error(s), 0 warning(s).

The fixture (#3024's "deliberately kept small")

scripts/make_sharded_safetensors.py splits one model.safetensors into N shards plus a HuggingFace index — offline, deterministic, no dependencies. A shard boundary is a property of the index and the split, not of the parameter count: two shards of a 0.5B exercise weight_map, the per-shard header rewrite and the cross-shard lookup exactly as four shards of a 7B do, at 988 MB and seconds instead of a multi-gigabyte nightly fetch. Its 7-row case table proves every tensor's bytes survive, the index names exactly N shards, a second run is byte-identical, and --shards 1 / a non-safetensors input are refused — because a fixture that is not actually sharded would make a green matrix prove nothing.

Quorum

Single-lane (a bounded CLI-correctness row with an executable falsifier on both polarities and a measured RED leg on origin/main); no design fork was open.

Receipt

docs/audits/impl-PMAT-1080-receipt.md

Writes

crates/apr-cli/src/commands/chat*.rs · crates/apr-cli/src/commands/chat_format_honesty_tests.rs (new) · scripts/check_format_command_matrix.sh (new) · scripts/make_sharded_safetensors.py (new) · contracts/patterns/format-command-honesty-v1.yaml (new) · .github/workflows/ci.yml (two guard-tree steps, both cargo-free) · evidence/format-honesty/ · docs/audits/impl-PMAT-1080-receipt.md · .pr/F-1/

Not in this row

🤖 Generated with Claude Code

https://claude.ai/code/session_018RouwmUL7vFfJyCx9qLEoH

…ever substitutes the demo model, and a sharded SafeTensors index loads (#3022, #3024)

apr chat silently loaded its built-in toy model for a sharded SafeTensors model:
`Chat Demo (Tiny Model)`, `Loaded Demo format in 0.00s (0.0 MB)`, zero tokens,
EXIT 0 — with the real model's path printed in the banner directly above. Nothing
failed, so a user reasonably reads it as "my 7B produced an empty answer".

ROOT CAUSE, and why one added arm is not the fix. `Path::extension()` returns the
LAST dot-segment: on `model.safetensors.index.json` — the exact filename `apr pull`
writes and then recommends — it is Some("json"), which matched no arm of
detect_format and fell through to ModelFormat::Demo. That is the instance. The
class is that TWO independent decisions existed: print_welcome_banner asked the
PATH, ChatSession::new asked the first eight BYTES, nothing compared them, and so
"banner says SafeTensors, session loads Demo" was reachable with no falsifier
anywhere in the tree.

  * resolve_chat_format is the ONE decision: suffix before extension(), then the
    magic bytes of an odd-named file, then a REFUSAL. Demo is not an outcome for a
    path that exists — resolve_chat_model has already proven it does.
  * The refusal is CliError::ModelLoadFailed, exit 6, read from error.rs and never
    typed; it names the file, the recognised formats, and #3022.
  * Every consumer — banner, tokenizer, architecture detection, generator — is
    handed that one value, so the banner can no longer name a format the loader did
    not use.
  * ShardedSafeTensors reaches the transformer through the same three calls apr run
    already makes: load_from_index -> load_from_sibling -> convert_sharded.
  * The load line reports index.metadata.total_size. Printing the manifest's own
    ~20 KB would be the same lie under a different name: 0.0 MB.

MEASURED, same fixture, two binaries built in this worktree:
  origin/main   FAIL  sharded x apr chat  the DEMO model answered   rc=1
  this branch   ok    4/4 cells, `Assistant: 2 + 2 equals 4.` at 3.2 tok/s   rc=0
The three unaffected cells are GREEN on both, so the gate discriminates.

CORRECTION to the report: `apr run` on a sharded index WORKS on main (measured,
row 3). The defect is apr chat alone. Whatever failed for the reporter on a real
7B pull is not reproduced by this fixture and stays [U] — and the matrix now covers
that cell on every run, so a regression there is caught rather than inferred.

THE GATE (#3024). qwen-story-daily was green the same night, and structurally
could not have caught this: `grep -c "apr chat" scripts/qwen-story.sh` is 0 and
`grep -c index.json` is 0 — the command and the layout the defect needs are both
absent. check_format_command_matrix.sh judges {sharded, single-file} x {run, chat}
on four rules — the named format is never Demo, exit 0, tokens > 0, reported size
> 0 — which is the GENERAL form of the defect, not the instance.
make_sharded_safetensors.py derives the sharded fixture offline from a model the
story already holds: a shard boundary is a property of the index and the split, not
of the parameter count, so two shards of a 0.5B exercise weight_map, the per-shard
header rewrite and the cross-shard lookup exactly as four shards of a 7B do — at
988 MB and seconds instead of a multi-gigabyte nightly fetch.

Mutants (both RED, restored GREEN): the .safetensors.index.json arm never matches
-> sharded_index_resolves_to_the_sharded_format_not_demo RED; resolve_chat_format
returns Ok(Demo) -> three refusal rows RED.

Contract contracts/patterns/format-command-honesty-v1.yaml (pv validate: 0 errors),
4 falsification tests, all wired: two case tables in ci / gate -> guard-tree, six
unit rows in workspace-test, the live matrix on a host with a model.

Refs #3022, #3024, PMAT-1080

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RouwmUL7vFfJyCx9qLEoH
@noahgift noahgift added pp-066 PP-066 (0.66) DAG row inst:A PP-066 instance claim (I14): inst:A F-1 PP-066 (0.66) DAG row labels Sep 8, 2026
@noahgift
noahgift enabled auto-merge September 8, 2026 12:06
noahgift added a commit that referenced this pull request Sep 8, 2026
… (operator ruling 2026-09-08)

F-1 (PMAT-1080, #3022, P0): `apr chat` silently loaded its built-in Demo model for a
sharded SafeTensors index — exit 0, zero tokens, the real model's path printed in the
banner. `Path::extension()` returns the last dot-segment, so on
`model.safetensors.index.json` (the exact filename `apr pull` writes and recommends) it
is Some("json") and matched no arm. Shipped in PR #3050 as a structural fix, not an
added arm: `resolve_chat_format` is one decision — suffix before extension, then magic
bytes, then a refusal from error.rs — and Demo is not an outcome for a path that exists.

F-2 (PMAT-1081, #3024 ask 3, owner bse): the live matrix runs nightly against real
models, and the two axes still uncovered — an `apr serve` column and a sharded-GGUF row
(merge_gguf_shards, for which no fixture builder exists) — are named rather than dropped.

The rows earn their place in a rescoped 18-row release by claim (1): apr reports
truthfully and never silently substitutes what the user named. #3022 is the strongest
instance of that failure in the tree — the substitution reported SUCCESS — and #3024 is
why it survived: qwen-story-daily was green the same night and structurally could not
have caught it (`grep -c "apr chat"` is 0, `grep -c index.json` is 0).

DAG 103 -> 105 rows, 0.66 lane 32 -> 34; invariants PASS (violations=0); the spec's §5.0
block re-rendered byte-identical; roadmap 824 -> 826, sorted, unique, additive.

Refs #3022, #3024, #2873

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RouwmUL7vFfJyCx9qLEoH
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=3050 head=e7afcbc2cf69ff53170f829a33ab7da70d220da7 verdict=REFUSE class=Q1 arm_rc=1

Shadow mode: this records a verdict and merges nothing. A refusal
to arm is not a block (§13 adds zero rows to §7) — the pull request is
exactly as green as it was.

noahgift and others added 3 commits September 8, 2026 14:44
…r, and sync the generated CONTRACT_COUNT

Two base-owned checks went red on the first push, both for the right reasons.

guard-tree / check_no_competing_harnesses.sh: PERF-009's predicate is textual — a script
that both DRIVES a model and COMPUTES A RATE is a second definition of how this project
measures itself. check_format_command_matrix.sh trips both halves purely through its own
must-match fixtures: `[0 tokens in 0.0s = 0.0 tok/s]` IS the defect row of its case table,
and its header names the axes `{apr run, apr chat}`. It derives no throughput and states no
comparator — it asserts that the format a command NAMES is not the demo model, exit 0,
tokens > 0 and reported size > 0. That is exactly the reason this detector already exempts
ITSELF and check_no_fabricated_baselines.sh, so it takes an allowlist entry with the reason
stated and a falsifiable condition for removing it ("if it ever derives a throughput or a
ratio, delete this line rather than widening the reason"), plus its own row in the
detector's case table so the exemption is covered: 18 passed, 0 broken.

Rewording the script to dodge the predicate was the alternative and was NOT taken: that is
the workaround PMAT-1074 already records as a known weakness of textual classifiers, and it
would leave the next reader with no statement of why the file is not a harness.

guard-cargo / FALSIFY-README-002: the generated CONTRACT_COUNT block stated 1815 against a
merge tree carrying 1816 — this row adds contracts/patterns/format-command-honesty-v1.yaml.
A generated number is an EQUALITY, not a ratchet, so `make readme-sync`.

Refs #3022, #3024, PMAT-1080

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RouwmUL7vFfJyCx9qLEoH
…rries no machine path

Two base-owned guards, both right.

check_no_claim_literals.sh (SPEC-2.0's claims ratchet, shrink-only, 452 known + 1 new):
the receipt quoted the defect's own banner including `= 0.0 tok/s` and the fix's including
`= 3.2 tok/s`. The guard cannot tell a quoted defect from a boast, and it should not have
to: 0.66 delivers no speed, so a tok/s number on a doc surface is a claim this release does
not make. Neither number was load-bearing either — the defect is "zero tokens generated,
exit 0", and a rate is redundant once the token count is zero. The receipt now states the
token counts and drops the rates; every fact survives.

check_hardcoded_paths.sh: `.pr/F-1/accept.sh` hardcoded /home/noah/.cargo/bin/cargo, the
same finding L0-1a's accept.sh carried. `CARGO="${CARGO:-$HOME/.cargo/bin/cargo}"` keeps
the pin (never a bare `cargo`: a shell function of that name overrides CARGO_TARGET_DIR)
without the machine path. Local: claims PASS, hardcoded-paths delta +0.

Refs #3022, #3024, PMAT-1080

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RouwmUL7vFfJyCx9qLEoH
…k takes the union

Both sides add steps to the same job and neither replaces the other: main carries L0-1a's
three manifest steps, this branch carries F-1's two case tables. Verified by counting each
step after the resolution rather than by eye — one occurrence of each of the four.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RouwmUL7vFfJyCx9qLEoH
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

F-1 PP-066 (0.66) DAG row inst:A PP-066 instance claim (I14): inst:A pp-066 PP-066 (0.66) DAG row

Projects

None yet

1 participant