127 lines
7.2 KiB
Markdown
127 lines
7.2 KiB
Markdown
# parity: how detz is proven equal to tai.py
|
|||
|
|
|
||
|
|
Goal: detz behaves exactly like `../tai.py`. This document is the spec of the proof
|
||
|
|
machinery. It states what exists today; gaps are listed at the end.
|
||
|
|
|
||
|
|
## Pipeline
|
||
|
|
|
||
|
|
`tools/parity.sh run|check` runs, in order:
|
||
|
|
|
||
|
|
1. `tools/parity_gen_cases.py` - regenerates `tests/diff/cases/cli_generated.json` from
|
||
|
|
`build_parser()` in tai.py (every argparse error and choice message). `--check` fails if stale.
|
||
|
|
2. `tools/parity_diff.py` - the differential harness (below).
|
||
|
|
3. `tools/parity_tests.py` - mines `../test_tai.py` into `build/parity/tests-map.{json,md}`.
|
||
|
|
4. `tools/parity_ledger.py --publish` - builds the symbol ledger and writes `PARITY.md`
|
||
|
|
(generated, never hand-edited). `--check` makes the pipeline exit 1 while anything is
|
||
|
|
missing or unverified.
|
||
|
|
|
||
|
|
All output goes to `build/parity/`; only `PARITY.md` is published to the tree.
|
||
|
|
|
||
|
|
## Symbol statuses
|
||
|
|
|
||
|
|
| status | meaning | who sets it |
|
||
|
|
|---|---|---|
|
||
|
|
| missing | no identifier in `src/` or `main.cpp` matches the normalized name | ledger scan |
|
||
|
|
| name-only | an identifier matches; behavior unknown | ledger scan |
|
||
|
|
| verified | a passing differential case names the symbol | harness result only |
|
||
|
|
|
||
|
|
`verified` is never inferred. A case names a symbol as `kind:name` or `kind:name#facet`
|
||
|
|
(`#parse`, `#output`, `#listed`, ...). The facet is printed in PARITY.md, so "verified" always
|
||
|
|
says what was proven (`flag:--debug-format#parse` is argument parsing, not the flag's effect).
|
||
|
|
Evidence is rejected as stale when the sha256 of tai.py or of the detz binary differs from
|
||
|
|
the one recorded in `diff-results.json`, and a filtered run (`--filter`) writes
|
||
|
|
`diff-results-partial.json`, which the ledger ignores. A case naming a symbol the ledger does not
|
||
|
|
know is an error. Kinds: class, method, function, tool, flag, setting, command (a `/command` of
|
||
|
|
`HELP_TEXT`).
|
||
|
|
|
||
|
|
## Case format (`tests/diff/cases/*.json`)
|
||
|
|
|
||
|
|
```json
|
||
|
|
{"cases": [{
|
||
|
|
"id": "repl.help",
|
||
|
|
"doc": "what this proves",
|
||
|
|
"symbols": ["command:/help"],
|
||
|
|
"symbols_from_stdout": [{"prefix": "tool:", "pattern": "^ ([a-z_]+)$", "facet": "listed", "step": 0}],
|
||
|
|
"tests": ["CapCliTests.test_cap_manifest_flag_prints_valid_toml"],
|
||
|
|
"steps": [{"argv": [], "stdin": "", "env": {}, "files": {"rel/in/cwd": "text"},
|
||
|
|
"home_files": {"rel/in/home": "text"}, "mode": "pipe|pty", "timeout": 60}],
|
||
|
|
"compare": {"stdout": true, "stderr": true, "exit": true, "vault": false,
|
||
|
|
"files": [{"root": "home|cwd", "glob": "**/*"}],
|
||
|
|
"vault_ignore_tables": [], "vault_ignore_why": ""},
|
||
|
|
"normalize": [{"on": ["stdout"], "pattern": "regex", "replace": "<X>", "dotall": false, "why": "required"}],
|
||
|
|
"llm": false, "attempts": 1, "min_pass": 1, "checks": [], "expected_fail": "reason"
|
||
|
|
}]}
|
||
|
|
```
|
||
|
|
|
||
|
|
Semantics:
|
||
|
|
|
||
|
|
- Each side (`python3 ../tai.py`, `build/detz`) runs every step in order, sharing one throwaway
|
||
|
|
`TAI_HOME` and cwd per side (`build/parity/work/<case>/<side>-<n>/`). `HOME` is a third
|
||
|
|
throwaway dir so provider CLIs never touch the real one. Inherited `TAI_*`, `CAP_*`, `TMUX*`
|
||
|
|
variables are dropped (they leak the caller's session); `COLUMNS=80`, `LINES=24`,
|
||
|
|
`TERM=dumb`, `TAI_PASSPHRASE=` are set. A step's `env` overrides.
|
||
|
|
- Compared channels: stdout, stderr, exit code per step, vault, declared files. `mode: pty` uses
|
||
|
|
`tai_test.ManagedProcess` (stderr is merged into stdout there).
|
||
|
|
- Vault = a deterministic dump of every `*.db` under the home: table DDL, rows sorted as JSON,
|
||
|
|
index names. sqlite internals and `fts_docs_*` shadow tables are skipped globally. Anything
|
||
|
|
else skipped must be listed in `vault_ignore_tables` with `vault_ignore_why`.
|
||
|
|
- Normalization is per case, per channel, regex only, and every rule needs a `why`. There is
|
||
|
|
no global scrubbing. Rules that matched nothing on either side are listed in the report
|
||
|
|
as suspect.
|
||
|
|
- `symbols_from_stdout` adds symbols read out of the reference's own stdout (for example every
|
||
|
|
tool `/tools` prints); they count only if the case passes, so detz had to print the same names.
|
||
|
|
- `tests` lists test_tai.py tests (`Class` or `Class.test_name`) whose assertions the case
|
||
|
|
reproduces through the real binaries. This is a claim for a reviewer to read, not inferred.
|
||
|
|
- `expected_fail` marks a known gap with its reason. It still counts as not verified and as
|
||
|
|
unverified in `parity-check`; it only keeps the harness exit code at 0. If such a case
|
||
|
|
passes it is reported `xpass` and fails the run, forcing the marker to be removed.
|
||
|
|
|
||
|
|
### LLM cases
|
||
|
|
|
||
|
|
`"llm": true` follows tests/README.md: a real model call on both sides, no mocks, no database
|
||
|
|
writes standing in for the agent. Outputs are not equal-compared. Instead `checks` must hold
|
||
|
|
on each side independently: `{"kind":"exit","step":0,"value":0}`,
|
||
|
|
`{"kind":"stdout_regex","step":1,"pattern":"..."}` (also `stderr_regex`),
|
||
|
|
`{"kind":"vault_sql","sql":"select count(*) ...","op":">=","value":2}`; add `"side":"ref"|"cpp"`
|
||
|
|
to scope one. The free rotating backend is flaky by design: `attempts` (cap 5) and `min_pass`
|
||
|
|
report a ratio per side (`2/3`) in the result instead of demanding 100 percent. `--no-llm`
|
||
|
|
skips them; skipped cases are never verified.
|
||
|
|
|
||
|
|
## Caps
|
||
|
|
|
||
|
|
jobs 6, steps per case 8, step timeout 300 s, captured output 2 MB per stream, diff lines shown
|
||
|
|
40 per channel, LLM attempts 5. A timed-out step kills the whole process group.
|
||
|
|
|
||
|
|
## Test-suite map
|
||
|
|
|
||
|
|
`build/parity/tests-map.md` lists every test class of `../test_tai.py` (150 classes, 1233
|
||
|
|
tests when written), the tai.py symbols it touches (ast, from `tai.X`, `tai.X.y` and
|
||
|
|
`"tai.X.y"` patch strings; names that are not code symbols show as `data:`), the number of its
|
||
|
|
tests reproduced by a passing case, and the case ids or `uncovered`.
|
||
|
|
|
||
|
|
## Seed cases (what exists today)
|
||
|
|
|
||
|
|
- 56 generated cli cases: every missing-value, invalid-choice, invalid-int, explicit-value
|
||
|
|
error, unknown flags, ambiguous prefixes, valid prefix, `--version`, `-h`, `--help`,
|
||
|
|
`--cap-manifest`, each choice accepted.
|
||
|
|
- REPL cases: `/help`, `/tools` (names), `/tools <name>`, unknown command, profiles and bots,
|
||
|
|
empty listings, plus `boot.vault` (seeded settings catalog, schema, input_history).
|
||
|
|
- `persist.restart_history`: LLM-driven restart persistence with the same `TAI_PLEXER_PANE`
|
||
|
|
in both runs. Marked `expected_fail` until the foundation agent lands the store and history
|
||
|
|
resume. On tai.py it passed 2 of 3 attempts on the first run (free model, flaky).
|
||
|
|
|
||
|
|
## Gaps (each with what closes it)
|
||
|
|
|
||
|
|
- Full tool schema JSON and `SETTINGS_CATALOG` entries are not exposed by any CLI flag, so only
|
||
|
|
tool names (`/tools`) and the seeded settings rows (`boot.vault`) are diffable today. Closing
|
||
|
|
it needs a read-only dump command in BOTH implementations, or a Python-side oracle (a script
|
||
|
|
importing tai.py) plus a detz dump flag.
|
||
|
|
- Startup banner lines (self-backup name, ctx id, live backend probes) are stripped by a
|
||
|
|
declared per-case rule; they cannot be compared byte for byte.
|
||
|
|
- tai.py snapshots itself into the vault on boot (audit, records, chunks, tags, fts_docs). detz
|
||
|
|
has no such file; `boot.vault` excludes those tables on purpose and says so. Needs an owner
|
||
|
|
decision, not a harness change.
|
||
|
|
- Module constants, TUI rendering, mplex, grr need their own cases (pty replay); none exist.
|
||
|
|
- Ledger "name-only" is a normalized-identifier match, weak by nature; it only separates
|
||
|
|
"nothing written" from "something written".
|