Fix the budget gate and add CI, lint and coverage guards

- check_budget.py escalated a hard index-row violation to 'warn' on both
  branches, so INDEX_ROW_HARD could never fail anything. It fails now, and
  tests/unit/test_budget_measurement.py pins both thresholds.
- The corpus note counted scripts/__pycache__/*.pyc, so the same commit
  reported 27 scripts after the suite had run and 16 on a clean checkout.
- TOKENS_PER_CHAR is re-derived from a measurement (26,795 chars -> 6,065
  tokens = 4.418); the comment's old figure and its arithmetic were both
  wrong for this file.
- check_repo.py's yaml sentinel carries the type ignore, so mypy is clean
  with PyYAML installed as well as without it.
- The CPUID test now skips with the probe's own note on a host without
  CPUID instead of failing, and pytest.ini points at tests/README.md, which
  exists, instead of an EXTENSION file that never did.

Tooling: .flake8 (a measured 120-column limit), .gitattributes, .gitignore
and .github/workflows/ci.yml (the five gates, the suite on 3.11-3.14 and a
lint job), which is what CONTRIBUTING.md already claimed CI runs. The
pull-request template it refers to now exists.

Content: pitfalls P26-P34 -- the analysis-level failures the catalogue was
missing (sampling read as observation, a self-built parser trusted, a
verification that could not have failed, a shell-mangled command line) --
registered in the symptom index and its routing mirror. FINDINGS F11-F15
record the defects above.

Tests: the coverage guard derives its subject from scripts/ rather than a
hand-written list, pe_triage.py and pe_diff.py gain their first tests, and
plan.py / preflight.py are declared uncovered with reasons.
This commit is contained in:
root
2026-10-24 14:55:02 +08:00
parent 25f2286cb6
commit 63b2aca8d8
19 changed files with 812 additions and 14 deletions
+33
View File
@@ -0,0 +1,33 @@
# flake8 configuration for this repository.
#
# Three settings, each with the measurement behind it rather than a preference.
#
# max-line-length = 120
# Measured before this file was added: the longest line across *.py and
# skills/exe-reverse/scripts/*.py is 110 characters (trace_reduce.py:666). 120 leaves room for a
# rule table or a format string written on one line without forcing a split inside a raw regex,
# which either changes the pattern or produces a concatenation harder to read than the long line.
# A line over 120 is a defect to look at, not a style disagreement.
#
# max-complexity is not set, and C901 is excluded below with the measurement: this repository's
# parsers and dispatchers are long flat if/elif chains over finding kinds and PE structures. Setting
# a threshold above the maximum would be a number chosen to pass rather than a standard; splitting
# them into one function per case would move the shape of the format away from the code that reads
# it, and looking like the specification is this repository's argument.
#
# extend-ignore:
# C901 see above -- reported here as a measurement, not enforced
# E203 slice whitespace (`data[start : end]`); flake8 and every autoformatter disagree, and the
# repository has no autoformatter
#
# Run from the repository root:
# flake8 *.py skills/exe-reverse/scripts/*.py
#
# Enforced in CI: the `lint` job in .github/workflows/ci.yml runs this exact command alongside mypy,
# on a bare interpreter, so the configuration is exercised in the poorest environment it must keep
# working in.
[flake8]
max-line-length = 120
extend-ignore = C901,E203
exclude = .git,__pycache__,tools,build,dist
+25
View File
@@ -0,0 +1,25 @@
# Normalize line endings so a Windows checkout does not rewrite committed files.
# Scripts stay LF (they are consumed by python/node on POSIX hosts and by the test suite anywhere).
* text=auto eol=lf
*.py text eol=lf
*.js text eol=lf
*.sh text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.json text eol=lf
# PowerShell, cmd and batch are the one thing that is happier with CRLF on Windows.
*.ps1 text eol=crlf
*.bat text eol=crlf
*.cmd text eol=crlf
# Never normalize binary-ish artifacts.
*.png binary
*.exe binary
*.dll binary
*.pdb binary
*.sys binary
*.lib binary
*.zip binary
+79
View File
@@ -0,0 +1,79 @@
<!--
Fill this in. Three sections are read by a reviewer and one is not yours to fill.
An unchecked box is not evidence of absence: a pull-request template with a blank effect-surface
checklist has not been shown to be documentation-only, it has been shown to be unfinished
(CONTRIBUTING.md, section Review). The readiness declaration is left for a human owner; an agent
leaves it blank rather than writing a claim it cannot make.
-->
## What this changes
One paragraph. What changes, and what a reader can do afterwards that they could not before. If the
change is documentation only, say so here and again in the effect surface.
## Evidence
Every command below was run in this checkout. Paste the output that decides the question; a summary
of confidence is not a result, and `should pass` is not a run.
```
python check_repo.py
python check_refs.py
python check_budget.py
python check_routing.py
python check_commands.py
python -m pytest -q
mypy *.py skills/exe-reverse/scripts/*.py
flake8 *.py skills/exe-reverse/scripts/*.py
```
Output that matters:
```
<paste the decisive lines, and say what they decide>
```
Claims added or changed:
| Claim | Grade | What it rests on (command + result) |
|---|---|---|
| | observed / inferred / unverified | |
- [ ] No claim is graded above the weakest step in its chain.
- [ ] Every `observed` has the command and the output beside it.
- [ ] Every negative result names the range, the pattern and what was searched. A search that
returned nothing is evidence about the search.
- [ ] Every limit cites the measurement or the constraint that set it, rather than a preference.
## Effect surface
Check each line that applies. `Documentation only` is a claim, so make it explicitly rather than
leaving the list blank.
- [ ] `SKILL.md` — the budget gate output is attached above; it is loaded in full on activation.
- [ ] `references/` — a new or renamed file is registered in `references/routing.md`, reachable from
`SKILL.md` or the README, and carries a navigable head if it passes 100 lines.
- [ ] `references/pitfalls.md` — a new entry takes the next number and keeps the same shape.
- [ ] `scripts/` — a new script is in the routing script table, answers `--help` on a bare
interpreter, and its argument table matches every command the docs tell a reader to run.
- [ ] `tests/` — a new script is added to `conftest.SCRIPT_FILES` or declared uncovered with a
reason in `tests/cli/test_cli_coverage.py`; a fixed defect has a test that fails without it.
- [ ] `evidence/` and `docs/tool-verification/` — a capability claim moved in both, and the
measurement record quotes the command it rests on.
- [ ] `README.md` and `README.zh-CN.md` — the two stay in step.
- [ ] `docs/tool-verification/FINDINGS.md` — a defect found by the repository's own tools is
recorded with its symptom, root cause, how it surfaced, and the check that now catches it.
- [ ] Documentation only — no behaviour changed, and the gates above still pass.
## Readiness declaration (human owner only)
An agent does not fill this section in. Leave it blank; the reviewer completes it.
- [ ] The change belongs in this repository rather than in a fork or a downstream project.
- [ ] The boundary of what was not exercised is stated, not implied: every route that needs a
debugger, a driver, a kernel component, a hypervisor, test signing or a second machine is
named as not run here.
- [ ] The serialized evidence is committed, and nothing in it carries target identity or a
machine-specific path.
- [ ] The commit is ready to be fast-forwarded onto `main`.
+125
View File
@@ -0,0 +1,125 @@
# CI for the exe-reverse skill repository.
#
# WHAT THIS RUNS
# * the repository's own structural gates: check_repo.py, check_refs.py, check_budget.py,
# check_routing.py, and check_commands.py
# * the pytest suite: tests/unit, tests/cli, tests/integration
# * lint: mypy and flake8 over the gate scripts and the shipped skill scripts
#
# WHAT THIS DOES NOT RUN, ON PURPOSE
# * anything needing a debugger, a driver, a kernel component, a live target process, a hypervisor,
# test signing or a network peer. Those routes are recorded experiments in tests/benchmark.md,
# and what the suite deliberately does not cover is stated in tests/README.md.
# * anything about agent behaviour. Whether a model routes, classifies or refuses correctly is not
# observable from a unit test; skills/exe-reverse/evals/evals.json is the skeleton for that, and
# it has never been executed.
# * the hardware-observation routes. They need particular silicon and privilege; hwprobe.py
# reports what a runner lacks and the suite skips with that reason instead of failing.
#
# The runner is Linux while the skill targets Windows. That is deliberate: the suite observes the
# scripts' behaviour -- parsing, patching, gate logic, exit codes -- and every check that needs a
# Windows fact asks the probe or skips. A green run here is a statement about the *code*, never
# about a route that needs a Windows host.
#
# Coverage is therefore: static structure, documentation anchors, budget, documented commands,
# routing coherence, pure parsers, CLI contracts, fixture construction, typing/style lint and
# no-debugger flows. Nothing more is claimed.
name: ci
on:
push:
pull_request:
#
# NOTE ON TRIGGERS: no `paths:` filter, deliberately. A filter that watches only `*.py` and the lint
# configuration would skip the structural gates on exactly the changes they exist to catch -- a
# reference index or a documented command edited without touching a script. check_refs.py and
# check_routing.py are documentation gates, so they have to run on documentation commits.
permissions:
contents: read
jobs:
test:
name: python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14"]
env:
PYTHONIOENCODING: utf-8
PYTHONDONTWRITEBYTECODE: "1"
# The gates spawn sibling scripts and decode their output. On a Windows runner the default
# codec is the console's (GBK here), and one script's --help text is not GBK-decodable, which
# is reported as a crash in that script rather than as a harness decoding problem. Pinning
# UTF-8 removes the whole class of failure on every platform.
PYTHONUTF8: "1"
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Install test dependencies
# `tiktoken` is optional for the suite but not for the budget gate's *measurement*: without
# it, check_budget.py falls back to characters/constant, and the two paths must agree.
# Installing it here means CI exercises the real tokenizer path, and the unit tests
# comparing both paths run instead of skipping.
# Not upgrading pip itself: it is not a goal here, and a pip self-upgrade failing would be
# reported as this repository's regression.
run: python -m pip install --upgrade pytest tiktoken
- name: Structural gate - repository layout
run: python check_repo.py
- name: Structural gate - documentation anchors
run: python check_refs.py
- name: Structural gate - context budget
run: python check_budget.py
- name: Structural gate - documented commands
run: python check_commands.py
- name: Structural gate - routing inventory matches the entry point
run: python check_routing.py
- name: Regression suite
run: python -m pytest -q
- name: Summarise skipped coverage
if: always()
run: python -m pytest -q -ra --no-header -p no:cacheprovider 2>&1 | tail -n 40
lint:
# Static checks over the same tracked surface the local commands cover, so what passes locally
# cannot drift from what CI enforces. Both tools read their configuration (mypy.ini, .flake8)
# from the repository root automatically.
#
# The environment is deliberately bare -- only mypy and flake8, none of PyYAML, capstone,
# tiktoken or pyelftools. That is the poorest environment the configuration must keep working
# in, which is what mypy.ini's ignore_missing_imports overrides exist for: CI exercises the
# fallbacks instead of a machine-local install hiding a regression.
#
# One interpreter, not the matrix: the checks are static, and the matrix's job is runtime
# behaviour, which lint does not observe.
name: lint (mypy + flake8)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Install lint dependencies
run: python -m pip install --upgrade mypy flake8
- name: mypy
run: mypy *.py skills/exe-reverse/scripts/*.py
- name: flake8
run: flake8 *.py skills/exe-reverse/scripts/*.py
+61
View File
@@ -0,0 +1,61 @@
# Local build products. The kit ships source only: every PE the tests need is generated into a
# pytest temp directory by skills/exe-reverse/scripts/make_fixture_pe.py, so nothing here is an input
# the repository could not rebuild.
*.obj
*.lib
*.exp
*.ilk
*.idb
*.i64
*.til
*.nam
*.map
# Working data -- never commit samples, dumps or captures.
*.exe
*.dll
*.sys
*.ocx
*.cpl
*.scr
*.pdb
*.zip
*.7z
*.bin
*.dmp
*.etl
*.pcap
*.png
*.jpg
# local scratch
_tmp/
out/
work/
audit_out/
# local extension-pass workspace: toolchains, vendor SDKs, unpacked installers, scratch notes.
# Nothing under tools/ belongs to the repository -- the skill's own evidence record lives in
# docs/tool-verification/ and carries no target identity or vendored binaries.
tools/
# Local copies of the third-party write-ups an absorption pass reads. They are someone else's text,
# not content of this repository: what ships is the absorbed method under an `inferred` label, plus
# the anonymized source line in docs/tool-verification/EXTENSION-absorption-*.md. One `git add -A`
# is all it would take to commit them, so the rule is explicit here rather than resting on "nobody
# has added them yet".
.ref/
# sandbox installs made while testing `npx skills add .` against this repo
.claude/
.agents/
*.log
*.jsonl
# python / editors
__pycache__/
*.pyc
.venv/
.vscode/
.idea/
.DS_Store
+7
View File
@@ -162,6 +162,13 @@ Six tools live at the root and are not part of the installed skill. Each carries
Consistency has a natural counter-pressure — a broken path fails loudly and someone fixes it. Bloat
has none, which is why the budget gate exists.
Those six gates are half of a green run. The other half is `python -m pytest -q` plus the lint pair
(`mypy` and `flake8` over the same surface, configured by `mypy.ini` and `.flake8`).
`.github/workflows/ci.yml` runs all of it on every push — the six gates, the suite on Python
3.11–3.14, and the lint job — and states at the top what it deliberately does not run. A machine
without the optional dependencies is the environment both the lint configuration and the budget
gate's fallback path are required to keep working in.
`tests/benchmark.md` holds the regression matrix (dimension -> public target -> scripts exercised ->
measured result -> strength label), and `docs/tool-verification/` is the evidence record behind it:
what each script actually did, which independent method confirmed it, and what the target could not
+5
View File
@@ -126,6 +126,11 @@ export EXEREV_TOOLS=<dir>:<dir> # POSIX
一致性有天然的反向压力 —— 坏路径会大声失败,然后有人修。膨胀没有,这就是预算门存在的原因。
这六道门只是"全绿"的一半,另一半是 `python -m pytest -q` 与 lint 组合(`mypy` 与 `flake8`
跑在同一批文件上,分别由 `mypy.ini` 和 `.flake8` 配置)。`.github/workflows/ci.yml` 在每次
push 时跑完整套:六道门、Python 3.11–3.14 的测试矩阵、以及 lint 任务,并在文件开头写明它刻意
不跑什么。缺少可选依赖的机器,正是 lint 配置与预算门回退路径都必须能正常工作的环境。
`tests/benchmark.md` 保存回归矩阵(维度 -> 公开目标 -> 该行用到的脚本 -> 实测结果 -> 强度标签),
`docs/tool-verification/` 是它背后的证据记录:每个脚本实际做了什么、哪个独立方法确认了它、以及目标
无法演练哪些场景。
+17 -8
View File
@@ -78,9 +78,10 @@ NAV_LINES = 100 # above this, a reference must open navigably
CORPUS_FILES_NOTE = 110 # references + scripts, reported as a note past this
CORPUS_LINES_NOTE = 26000 # total lines across the same corpus, same treatment
# Characters per token, for the no-tiktoken fallback. **Calibrated on SKILL.md against
# tiktoken/cl100k_base** (47,629 chars -> 11,114 tokens = 4.285), so both measurement paths reach
# the same verdict. Getting this wrong made the gate answer differently on CI than locally.
TOKENS_PER_CHAR = 4.285
# tiktoken/cl100k_base** (26,795 chars -> 6,065 tokens = 4.418, re-measured 2026-10-24), so both
# measurement paths reach the same verdict. Getting this wrong made the gate answer differently on
# CI than locally.
TOKENS_PER_CHAR = 4.418
# A navigable head: one of these must appear in the first NAV_HEAD_LINES lines.
NAV_HEAD_LINES = 40
HEDGE_RE = re.compile(
@@ -145,9 +146,10 @@ def token_estimate(text):
everywhere in this repository, including in this file. When it is not, it falls back to
`characters / TOKENS_PER_CHAR`.
The fallback constant is **calibrated against that measurement** rather than guessed: a
markdown-heavy document tokenizes worse per character than the usual /4 rule of thumb, and this
file measured 0.30 characters-to-tokens on SKILL.md (67,011 chars -> 19,929 tokens).
The fallback constant is **calibrated against that measurement** rather than guessed: this
SKILL.md measures 4.418 characters per token (26,795 chars -> 6,065 tokens, cl100k_base). Agreeing
with the "about four" rule of thumb to within a few percent is not close enough to rely on, so
the number is re-derived when the document changes rather than assumed.
Why the value matters more than the method: the first version used 3.6 and the gate therefore
reached *different verdicts on different machines* -- `WARN over budget` where tiktoken was
@@ -198,7 +200,11 @@ def check_index_rows(lines):
out = []
for section, key, size in long_rows:
hard = size > INDEX_ROW_HARD
out.append(('warn' if hard else 'warn',
# `hard` must actually escalate. This read `'warn' if hard else 'warn'` -- both branches the
# same -- so INDEX_ROW_HARD was defined, computed and never able to fail anything: an index
# row could grow without limit and CI stayed green. Found by injecting an over-long row and
# asking the gate to fail, which is the only way this class of defect shows up.
out.append(('fail' if hard else 'warn',
'%s: row for %s is %d chars (soft limit %d)%s' %
(section, key, size, INDEX_ROW_SOFT,
' -- that is a paragraph, not an index entry' if hard else '')))
@@ -305,8 +311,11 @@ def check_corpus():
green always-loaded count.
"""
refs = sorted(glob.glob(os.path.join(REFS, '**', '*.md'), recursive=True))
# A bytecode cache is not corpus. It is untracked, it appears the first time the suite runs, and
# counting it made this note report 27 scripts where the directory ships 16 -- a measurement that
# changes when a *reader* runs a test is not a measurement of the repository.
scripts = sorted(p for p in glob.glob(os.path.join(SKILL_DIR, 'scripts', '**', '*'), recursive=True)
if os.path.isfile(p))
if os.path.isfile(p) and '__pycache__' not in p.split(os.sep))
def count(paths):
total = 0
+5 -1
View File
@@ -31,7 +31,11 @@ import sys
try:
import yaml
except ImportError: # the skills CLI is the real authority; this is a local gate
yaml = None
# `ignore[assignment]` because the sentinel assignment is only ill-typed where the
# module IS installed (mypy then knows the name as Module); on a bare machine the
# override in mypy.ini makes it Any and the ignore is unused -- warn_unused_ignores
# is off, so both environments stay clean.
yaml = None # type: ignore[assignment]
ROOT = os.path.dirname(os.path.abspath(__file__))
SKILLS_DIR = os.path.join(ROOT, 'skills')
+77 -1
View File
@@ -154,9 +154,85 @@ on this host rather than an error.
*Fix and check:* long values wrap onto continuation lines instead of being cut. A test asserts a long
value survives into the output intact.
## Gate, harness and lint defects
**F11. The hard index-row limit was computed and then unable to fail anything.**
*Symptom:* none. The gate reported `ok` on an index row of 450 characters, past the 400-character
limit it declares, and CI stayed green.
*Root cause:* the finding was appended as `('warn' if hard else 'warn', ...)` -- both branches the
same expression. `INDEX_ROW_HARD` was defined, the comparison was performed, and the level was then
thrown away. The message text even said "that is a paragraph, not an index entry", which is what made
it read as enforcement.
*How it surfaced:* by injecting an over-long row into a scratch copy and requiring the gate to exit
non-zero. It exited 0. A gate that has only ever been seen passing is not evidence that it can see
anything, and this class of defect is invisible to every test that runs the gate on a conforming tree.
*Fix and check:* the escalated level is `fail`. `tests/unit/test_budget_measurement.py` asserts that a
row past `INDEX_ROW_HARD` produces `fail` and a row past `INDEX_ROW_SOFT` produces `warn`, so the two
thresholds cannot silently collapse into each other again.
**F12. The corpus measurement counted a bytecode cache, so its own number changed when a reader ran
the suite.**
*Symptom:* the budget gate reported `36 references + 27 scripts = 63 files` on a tree that ships 16
scripts. The count moved between runs on the same commit.
*Root cause:* the script half of the corpus was `glob(scripts/**/*)` filtered by `isfile`, which
includes `scripts/__pycache__/*.pyc`. Reading a directory is a measurement of the repository; reading
a directory that the reader has been scribbling in is a measurement of the reader.
*How it surfaced:* comparing the number the gate printed against the number of files the routing gate
said it covers. Two gates measuring the same tree disagreed, and that disagreement was the finding.
*Fix and check:* paths under `__pycache__` are excluded from the corpus. The count now matches the
routing inventory, and re-running the suite no longer changes it.
**F13. A test asserted a capability it never checked, so a host without it failed instead of skipping.**
*Symptom:* `test_prober_can_dump_raw_cpuid_registers` failed on a non-x64 host with "a raw dump was
requested but nothing was reported", while the probe's own report said CPUID was unavailable there.
*Root cause:* the test asserted that the dump was non-empty without asking whether the host could
produce one. The script was right and the test encoded a Windows assumption. It also contradicted the
suite's own rule that a missing prerequisite is a skip with a printed reason.
*Fix and check:* the test reads the probe's `cpu` row first and skips with that row's note when the
state is not `available`; the non-empty assertion still runs where CPUID is reachable. The skip text
names the reason, so a green run on Linux is not mistaken for a green run on Windows.
**F14. A configuration comment pointed at a document that does not exist.**
*Symptom:* `pytest.ini` sent a reader to `docs/tool-verification/EXTENSION-test-harness.md` for the
suite's coverage policy. No such file has ever been in this repository.
*Root cause:* the file was inherited from the Android-side repository along with the harness, and the
structural gates check the *skill* tree's references, not a comment in an ini file. A stale path in a
comment fails nothing, which is exactly why it survives.
*Fix and check:* the comment points at `tests/README.md`, which carries a "what it deliberately does
not do" section. Recorded here rather than silently repaired because the class -- a reference in a
non-markdown file that no gate walks -- is still open.
**F15. The type checker rejected a sentinel assignment the moment its optional dependency was installed.**
*Symptom:* `mypy *.py skills/exe-reverse/scripts/*.py` reported `check_repo.py:34: Incompatible types
in assignment (expression has type "None", variable has type Module)` on a machine with PyYAML, and
no error at all on a machine without it.
*Root cause:* `yaml = None` in the `except ImportError` branch is ill-typed only where the import
succeeded, which is the environment the lint job runs in. The repository had no lint job at the time,
so the two environments never had to agree.
*Fix and check:* the assignment carries `# type: ignore[assignment]`, with `warn_unused_ignores` off
(already the configuration) so the bare-machine environment stays clean too. `mypy.ini` and `.flake8`
are now enforced by the `lint` job in CI.
## What this list is for
Ten defects, all in code written during this pass, all of which produced output that looked right. The
Fifteen defects, all in code written or inherited during this pass, all of which produced output that looked right. The
pattern is consistent enough to state as a rule:
**A check that cannot fail is not a check, and a table written from recall is a table that is wrong in
+2 -2
View File
@@ -6,8 +6,8 @@
# no live target process.
# What does *not* live here: anything that needs a running Windows target or a
# kernel-mode component. Those routes stay in tests/benchmark.md as recorded
# experiments, and their coverage status is stated in
# docs/tool-verification/EXTENSION-test-harness.md.
# experiments, and what the suite deliberately does not cover is stated in
# tests/README.md (`## What it deliberately does not do`).
#
# If pytest is not installed:
# python -m pip install pytest
+2
View File
@@ -162,6 +162,8 @@ Load the named file **before your next command**, not after your next three atte
| You are about to publish notes, a script or a screenshot of the work | `references/desensitization.md` |
| The task has run long and you are unsure what is already proven | `references/long-task-discipline.md` |
| A documented command does not run, or a tool "is missing" | `scripts/hwprobe.py` |
| A fix removed the error, and the requester still sees the symptom | `references/pitfalls.md` |
| A verification passed, but you cannot say what would have failed without the change | `references/pitfalls.md` |
| You need to know what this skill does not cover before promising anything | `references/coverage-and-limits.md` |
## Start here: classify the target in thirteen questions
+106
View File
@@ -12,6 +12,11 @@ That is the shape worth fearing. A crash announces itself; these do not.
Each entry states the failure, the mechanism that makes it invisible, and the check that catches it.
P1-P25 are format and mechanism failures of Windows binaries; P26-P34 are failures of the *analysis*
itself — sampling read as observation, a self-built parser trusted, a verification that could not have
failed. Those are written with Windows examples because that is where they were paid for, but the
mechanism does not depend on the platform: read them on a managed assembly and a driver the same way.
---
**P1. A length change, made by someone who thought of it as "just adding a byte".**
@@ -242,3 +247,104 @@ once, it cannot be withdrawn from a history.
*Check:* scan before committing, and be as careful about over-redacting as about under-redacting:
removing tool names and format names destroys the reusable part and removes none of the identity.
**P26. The error disappeared and the symptom did not.**
The log line is gone, the dialog that used to appear is gone, and the report says fixed. The
requester then shows the same problem still on screen. The internal signal and the user-visible
outcome are different claims: suppressing one error path proves that path ran, not that the symptom
had only one producer.
*Check:* define the result as the observable the requester described, reproduce it before the change
so the "after" has something to compare against, and treat a quiet log as absence of evidence rather
than evidence of success.
**P27. "I did not observe it" treated as "it is not there".**
A breakpoint that never fires, an ETW provider that logs nothing, a screenshot loop that never
catches the dialog. Sampling is not observation: a transient state can fall entirely between samples,
a provider can be filtering the very event you want, and a breakpoint on a page that is already warm
misses the access. The captures are real and consistently empty, which reads as corroboration when it
is one blind spot repeated.
*Check:* name what the instrument can and cannot see before drawing a negative conclusion, and ask the
same question with an instrument of a different kind -- a hardware breakpoint instead of a software
one, a file-level search instead of a runtime one, a second provider.
**P28. Waiting for something only a person can advance.**
An automation loop polls for a state that will never arrive on its own: an installer's UAC prompt, a
licence dialog waiting for input, a captcha, a "click to continue" in a trial build. Polling looks
productive -- timestamps advance, the screen is unchanged -- and can run for a long time without the
possibility of progress.
*Check:* before waiting, say what would cause the state to change. If the answer is "a person", drive
the prompt programmatically or hand it back. Bound every wait by change, not by elapsed time.
**P29. "The search returned nothing" treated as "the bytes are not there".**
A scan for a known-present string returns zero hits and the route is written off. Usually the needle
was encoded differently from the haystack: UTF-16LE inside a resource or a wide-char table, a string
split across fragments, a compressed resource section, or text that exists only after decryption at
run time.
*Check:* search more than one encoding before concluding absence (`utf-8` and `utf-16-le` cover most
storage), search the shortest distinctive fragment, look inside containers before declaring the data
missing, and state the negative result with its scope -- "no UTF-8 or UTF-16LE literal in this file"
is a finding; "the string does not exist" is a guess.
**P30. Your own analysis script's defect, recorded as a target finding.**
A purpose-written parser prints a plausible table, count or verdict, and it is wrong. It ran clean and
fast, so it was trusted. Common shapes: an operator-precedence mistake that silently widens a mask, a
local helper whose name shadows a stdlib module inside a third-party package, a "precision" number
computed against an incomplete reference, an offset table built with the wrong RVA-to-file mapping.
*Check:* sanity-check the shape before the content -- a distribution that is implausible for the
format is a bug signal. Cross-check against an independently produced artifact, and exercise any
fallback path against a case whose answer you already know.
**P31. A verification that could not have failed.**
A check "passes" while having tested nothing, because the observable is identical whether or not the
change took effect. The patch removes a dialog that also does not appear in the state you tested in,
or the field you edited is not the field the UI reads.
*Check:* ask, before running it, **what would this look like if the change were absent?** If the
answer is the same, the measurement is void: record it as *not applicable under this condition*, never
as *passed*.
**P32. One observer's failure silently disarming the others.**
Several instruments are installed -- hooks in different modules, a debugger plus a logger, two ETW
sessions -- and one of them faults. On Windows the failure is often swallowed: a vectored exception
handler consumes the fault, a hook chain returns early, a thread dies while the process keeps running.
Everything installed after the failure stops reporting, and the absence is read as "nothing happened
there".
*Check:* after installing N observers, prove each one still fires on a known event. Install them one
at a time when the first fault is unexplained, and treat a sudden drop to zero events as a broken
instrument until proven otherwise.
**P33. Two tools disagree about the same artifact, and the disagreement is your parser.**
A disassembler, a PE viewer and a debugger report different entry points, different imports or
different section sizes. The reflex is to distrust the target. In practice the usual cause is a
difference in what each tool means: RVA versus file offset, virtual versus raw size, the section that
*contains* an address versus the section that *starts* at it, or a cached parse of an older copy of
the file.
*Check:* convert both readings into the same coordinate system (RVA, file offset, module base) before
comparing, and verify one value by hand from the headers. A disagreement you cannot restate in one
coordinate system is not yet a finding.
**P34. The shell mangled the command before the target ever saw it.**
A path with spaces, a quote inside an argument, a `--flag=value` that a wrapper re-splits: the tool
receives something other than what was typed and reports a usage error, a missing file, or -- worse --
succeeds against a different target. PowerShell adds its own layer (argument re-quoting, `--%`, and
`Start-Process -ArgumentList` joining a string differently from an array), and `cmd /c` adds another.
*Check:* when an argument-heavy command fails in a way that does not match its own parser, reproduce
it with the argument written to a file or passed as an array, and print the argv the child actually
received before changing the command.
+2
View File
@@ -52,6 +52,8 @@ without a check.
| You are about to publish notes, a script or a screenshot of the work | `references/desensitization.md` |
| The task has run long and you are unsure what is already proven | `references/long-task-discipline.md` |
| A documented command does not run, or a tool "is missing" | `scripts/hwprobe.py` |
| A fix removed the error, and the requester still sees the symptom | `references/pitfalls.md` |
| A verification passed, but you cannot say what would have failed without the change | `references/pitfalls.md` |
| You need to know what this skill does not cover before promising anything | `references/coverage-and-limits.md` |
## Reference index
+3 -1
View File
@@ -20,9 +20,11 @@ and "the script was measured doing X on this host".
| `unit/test_scan_leaks.py` | Every rule firing on a shaped sample, every exemption class staying silent, the three measured deviations from the exemption list, scan position reporting, and the file walk's skips. |
| `unit/test_capabilities_registry.py` | The capability table: every atom's kind, basis and probe name, the transitive closure against an independent walk, the dependency graph's acyclicity, `resolve` on missing atoms and on `file:` atoms that are and are not on disk, the declaration parser including the documentation template it must reject, and the `declared` / `mapped` / `unknown` classification. |
| `unit/test_doctor.py` | The static dependency pass (stdlib, siblings and a third-party name separated without executing the file), `discover_scripts` scanning rather than listing, the per-script table with an unclassified script still counted, and the environment facts with their keys and severity labels. |
| `unit/test_budget_measurement.py` | The budget gate's two measurement paths agree (real tokenizer versus the calibrated fallback), the fallback constant still matches a fresh measurement of `SKILL.md`, and a row past `INDEX_ROW_HARD` fails rather than warns. |
| `unit/test_dart_snapshot_strings.py` | The ELF reader against a generated container, both literal encodings recovered at the offsets the fixture placed them at, the length and limit gates, and the pool counts -- zero on an image carrying neither idiom and non-zero on hand-built bytes carrying each one. |
| `cli/test_cli_surface.py` | Help and usage exit codes for every script, the documented exit codes for each script's error paths, and the negative case that inspect-only runs write nothing into the working directory. |
| `cli/test_cli_json.py` | Each script's machine-readable output: keys, values, and the conventions that are not what a reader would assume. |
| `cli/test_cli_coverage.py` | Coverage derived from the scripts directory rather than a hand-written list: every shipped `.py` is either exercised or declared uncovered with a reason, and every one answers `--help` with its usage block. A new script fails here until it is registered. |
| `cli/test_cli_json.py` | Each script's machine-readable output: keys, values, and the conventions that are not what a reader would assume -- including `pe_triage.py`'s identity block on a generated image (and that it writes nothing) and `pe_diff.py`'s self-comparison, which is the control for every other diff. |
| `cli/test_cli_registry_surface.py` | The registry and doctor subprocess contracts: `--list` / `--json` / single-capability exit codes and `RESULT=` tokens, the doctor's three blocks and its 0-versus-3 verdict, and the view flags that print one block only. |
| `cli/test_cli_dart_snapshot.py` | The snapshot reader's CLI: the refusals and their exit codes, `--find` case-insensitivity and the exit code a miss produces, `--at` by file offset and by virtual address, and the `--json` document. |
| `integration/test_pipeline_end_to_end.py` | Build, locate, patch, re-read: the patched bytes, the untouched control site, the recomputed checksum, and a control run in which nothing changes. |
+80
View File
@@ -0,0 +1,80 @@
"""Coverage of the shipped scripts, derived from the directory rather than a hand-written list.
Borrowed mechanism: the sibling apk-reverse repository derives its CLI contract cases from
skills/*/scripts/*.py and from each script's own --help output, so a new script is covered the day
it lands instead of the day someone remembers to add it. What is *not* borrowed is that repository's
single 0-4 exit-code table: this kit publishes a per-script vocabulary (pepatch.py uses 3 for "not a
parseable PE", where the table means "the environment lacks a capability"), and a test that rewrote
those meanings would assert a policy this repository deliberately does not have.
The guard here is therefore narrower, and it is about *coverage*, not exit codes:
* every shipped .py is either in conftest.SCRIPT_FILES (some layer of this suite exercises it) or
named in DECLARED_UNCOVERED together with the reason it is not;
* every declared gap carries a real sentence, because "TODO" is how a gap becomes permanent;
* every shipped script -- covered or declared -- answers --help with its usage block, which is the
cheapest possible exercise of its argparse table and its import path.
A new script fails this file until one of those lists changes. That is the point: the alternative
is a suite that looks complete while silently ignoring half the kit.
"""
import subprocess
import sys
import pytest
from conftest import SCRIPTS_DIR, SCRIPT_FILES
pytestmark = pytest.mark.cli
# Scripts no layer exercises yet. Each entry is a recorded gap, not an exemption: the reason says
# what would be needed to close it, so the next pass can decide rather than rediscover.
DECLARED_UNCOVERED = {
"plan.py":
"composes capabilities.py and doctor.py into a route from a stated intent. Both halves have "
"their own tests; the composition reads live host facts, so a fixture that pinned its output "
"would pin this machine's capability set instead of the mapping. Closing it needs a stubbed "
"probe, and that stub does not exist yet.",
"preflight.py":
"answers session readiness from live host facts (free space, route prerequisites) plus one "
"sample. The sample half could be pinned to a generated PE; the host half has no generator, "
"and asserting on the runner's environment would encode this machine as an expectation.",
}
def shipped_scripts():
"""Every runnable python script the skill ships, sorted."""
return sorted(p.name for p in SCRIPTS_DIR.glob("*.py"))
def test_every_shipped_script_is_exercised_or_declared():
"""A script nobody tests must at least be a script somebody decided not to test."""
shipped = set(shipped_scripts())
assert shipped, "no scripts found under %s" % SCRIPTS_DIR
silently_uncovered = shipped - set(SCRIPT_FILES) - set(DECLARED_UNCOVERED)
assert not silently_uncovered, (
"these scripts are shipped but neither exercised nor declared uncovered: %s. Add a test and "
"register the script in conftest.SCRIPT_FILES, or add it to DECLARED_UNCOVERED with the "
"reason." % sorted(silently_uncovered))
def test_declared_entries_are_neither_stale_nor_hollow():
"""A gap that was closed must leave the list; a gap with no reason is a hidden one."""
both = set(SCRIPT_FILES) & set(DECLARED_UNCOVERED)
assert not both, ("these scripts are in SCRIPT_FILES and also declared uncovered: %s -- the "
"declaration is stale" % sorted(both))
for name, reason in DECLARED_UNCOVERED.items():
assert (SCRIPTS_DIR / name).is_file(), "%s is declared uncovered but does not exist" % name
assert len(reason.split()) >= 12, (
"%s: the reason must say what would close the gap, not just mark it" % name)
@pytest.mark.parametrize("script", shipped_scripts())
def test_every_shipped_script_answers_help_with_usage(script):
"""The usage block is the script's published contract; it must render on every host."""
proc = subprocess.run([sys.executable, str(SCRIPTS_DIR / script), "--help"],
capture_output=True, text=True, timeout=120,
encoding="utf-8", errors="replace")
assert proc.returncode == 0, "%s --help exited %d: %s" % (script, proc.returncode, proc.stderr)
assert "usage:" in proc.stdout.lower(), "%s printed no usage block" % script
assert script in proc.stdout, "%s does not name itself in its usage line" % script
+41 -1
View File
@@ -390,8 +390,48 @@ def test_prober_rows_carry_a_measurement_and_a_note(hwprobe_report):
assert row["state"] in ROUTE_STATES or row["state"].startswith("needs-"), row
def test_prober_can_dump_raw_cpuid_registers(run_script):
def test_prober_can_dump_raw_cpuid_registers(run_script, hwprobe_report):
"""A requested CPUID dump must be non-empty -- where CPUID is reachable at all.
The probe's own report says which of those two situations this host is in, so the test reads it
instead of assuming x64 Windows: on a non-x64 or non-Windows host the stub cannot run, and
"nothing was reported" is the correct answer rather than a defect. A missing capability is a
skip with the probe's own note, never a red test (tests/README.md, conftest.py).
"""
cpu = hwprobe_report["cpu"][0]
if cpu["state"] != "available":
pytest.skip("CPUID is not reachable on this host, so there is no register dump to check: %s"
% cpu["note"])
result = run_script("hwprobe.py", "--raw", "--json", timeout=120)
assert result.returncode == 0, result.stderr
payload = json.loads(result.stdout)
assert payload["raw_cpuid"], "a raw dump was requested but nothing was reported"
# --- pe_triage ---------------------------------------------------------------
def test_triage_reports_identity_and_leaves_the_sample_untouched(fixtures, run_script):
"""The first script a task should run: it parses, classifies, and must not write."""
sample = fixtures.pe64
before = sample.data
result = run_script("pe_triage.py", sample.path, "--json")
assert result.returncode == 0, result.stderr
payload = json.loads(result.stdout)
assert payload["sha256"] == sample.sha256
assert payload["identity"]["arch"] == "amd64"
assert payload["identity"]["bitness"] == 64
assert payload["identity"]["kind"] == "exe"
assert sample.data == before, "triage modified the image it was reading"
# --- pe_diff -----------------------------------------------------------------
def test_diff_of_the_same_build_reports_no_material_change(fixtures, run_script):
"""Self-comparison is the control for every other diff: it must find nothing."""
sample = fixtures.pe64
result = run_script("pe_diff.py", sample.path, sample.path, "--json")
assert result.returncode == 0, result.stderr
payload = json.loads(result.stdout)
assert payload["verdict"] == "identical-modulo-normalised-fields"
assert payload["material_change"] is False
assert payload["byte_diff"]["differ_bytes"] == 0
+5
View File
@@ -50,6 +50,11 @@ SCRIPT_FILES = (
"dart_snapshot_strings.py",
"fold.py",
"trace_reduce.py",
# The two route helpers: the first script a task should run on an unknown image, and the
# two-build comparison. Added with tests/cli/test_cli_coverage.py, which fails if a shipped
# script is neither in this tuple nor declared uncovered with a reason.
"pe_triage.py",
"pe_diff.py",
)
# A hang detector, not a performance assertion. Nothing in this suite is meant to be slow.
+137
View File
@@ -0,0 +1,137 @@
"""The budget gate must reach the same verdict whether or not `tiktoken` is installed.
This is a small test for a defect that cost two red CI runs upstream. `check_budget.py` measures the
always-loaded body in tokens, preferring `tiktoken` and falling back to `characters / N` when it is
absent. The first version's `N` was guessed (3.6) instead of calibrated, so a machine with tiktoken
saw ~10,900 tokens and passed while a machine without it saw ~13,200 and warned -- and the gate
runner (CI's oldest job, and any contributor's box) is the one without it.
A gate whose answer depends on which packages the runner happens to have installed is not a gate, so
both paths are forced here and compared.
The third test guards the other half of the same failure: a threshold that is computed and then
unable to fail anything.
"""
from __future__ import annotations
import importlib
import sys
import pytest
from conftest import REPO_ROOT
pytestmark = pytest.mark.unit
SKILL_MD = REPO_ROOT / "skills" / "exe-reverse" / "SKILL.md"
@pytest.fixture
def budget():
sys.path.insert(0, str(REPO_ROOT))
return importlib.import_module("check_budget")
def _has_tiktoken():
"""Whether a real tokenizer is available, reported rather than assumed.
The comparison needs *two* implementations; where there is only one, the honest outcome is a
skip with the install command, not a failure and not a silent pass. CI installs it, so the
property is verified where it matters; a contributor without it has both gates agreeing anyway,
because the fallback is calibrated to the same constant.
"""
try:
import tiktoken # noqa: F401
return True
except Exception:
return False
def test_the_fallback_estimate_agrees_with_the_real_tokenizer(budget):
"""Both measurement paths must land on the same side of the soft budget."""
if not _has_tiktoken():
pytest.skip("tiktoken is not installed here, so there is no measurement to compare against; "
"install it with `python -m pip install tiktoken`")
text = SKILL_MD.read_text(encoding="utf-8")
real, how_real = budget.token_estimate(text)
assert "tiktoken" in how_real, how_real
# Force the fallback path by handing the module a tokenizer that cannot be imported.
saved = sys.modules.get("tiktoken")
sys.modules["tiktoken"] = None # `import tiktoken` raises here
try:
fallback, how_fallback = budget.token_estimate(text)
finally:
if saved is not None:
sys.modules["tiktoken"] = saved
else:
sys.modules.pop("tiktoken", None)
assert "estimate" in how_fallback, how_fallback
drift = abs(real - fallback) / max(1, real)
assert drift < 0.05, (
"the fallback is %.1f%% away from tiktoken (%d vs %d): recalibrate TOKENS_PER_CHAR "
"against a fresh measurement rather than a guess" % (100 * drift, fallback, real))
assert (real <= budget.TOKEN_SOFT) == (fallback <= budget.TOKEN_SOFT), (
"the two paths straddle the soft budget (%d real / %d fallback vs %d): the gate would "
"answer differently on CI than on a developer machine"
% (real, fallback, budget.TOKEN_SOFT))
def test_the_fallback_constant_is_the_calibrated_one(budget):
"""Pin the constant to a measurement: `chars / tokens` on SKILL.md, recomputed here."""
text = SKILL_MD.read_text(encoding="utf-8")
real, how = budget.token_estimate(text)
if "tiktoken" not in how:
pytest.skip("no tiktoken available, so there is no measurement to calibrate against")
expected = len(text) / real
assert abs(budget.TOKENS_PER_CHAR - expected) < 0.25, (
"TOKENS_PER_CHAR is %.3f but this SKILL.md measures %.3f characters per token -- the "
"document changed, so recalibrate" % (budget.TOKENS_PER_CHAR, expected))
def test_an_index_row_past_the_hard_limit_fails_rather_than_warns(budget):
"""The hard index-row limit must escalate, or the limit it names is decorative.
It read `'warn' if hard else 'warn'` -- both branches identical -- so `INDEX_ROW_HARD` was
defined, computed, and unable to fail anything: an index row could grow without bound while
every gate stayed green. That is the same failure mode this file exists for, one level up: the
measurement happened and the verdict did not follow. It was found by injecting an over-long row
and requiring the gate to fail, which is the only way a dead limit shows itself -- a gate that
has only ever been seen passing is not evidence that it can see anything.
"""
def levels(desc_len):
lines = [
"## Symptom index",
"| What you observe | Load first |",
"| a row | %s |" % ("y" * desc_len),
]
return {level for level, _ in budget.check_index_rows(lines)}
assert levels(budget.INDEX_ROW_SOFT + 10) == {"warn"}
assert levels(budget.INDEX_ROW_HARD + 10) == {"fail"}, (
"a row past INDEX_ROW_HARD (%d chars) must fail the gate, not warn about it"
% budget.INDEX_ROW_HARD)
def test_the_corpus_count_ignores_a_bytecode_cache(budget, tmp_path, monkeypatch):
"""A measurement that changes when a reader runs the suite measures the reader.
The corpus note once counted scripts/__pycache__/*.pyc, so the same commit reported 27 scripts
on a machine that had run pytest and 16 on a clean checkout. The routing gate said 16 the whole
time; the disagreement between two gates about one tree is what surfaced it.
"""
refs = tmp_path / "references"
refs.mkdir()
(refs / "one.md").write_text("# one\n", encoding="utf-8")
scripts = tmp_path / "scripts"
scripts.mkdir()
(scripts / "one.py").write_text("print(1)\n", encoding="utf-8")
cache = scripts / "__pycache__"
cache.mkdir()
(cache / "one.cpython-314.pyc").write_bytes(b"\x00" * 10)
monkeypatch.setattr(budget, "REFS", str(refs))
monkeypatch.setattr(budget, "SKILL_DIR", str(tmp_path))
notes = budget.check_corpus()
assert notes[0].startswith("corpus: 1 references + 1 scripts = 2 files"), notes[0]