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:
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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`.
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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
|
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.
|
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 ->
|
`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:
|
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
|
what each script actually did, which independent method confirmed it, and what the target could not
|
||||||
|
|||||||
@@ -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` 保存回归矩阵(维度 -> 公开目标 -> 该行用到的脚本 -> 实测结果 -> 强度标签),
|
`tests/benchmark.md` 保存回归矩阵(维度 -> 公开目标 -> 该行用到的脚本 -> 实测结果 -> 强度标签),
|
||||||
`docs/tool-verification/` 是它背后的证据记录:每个脚本实际做了什么、哪个独立方法确认了它、以及目标
|
`docs/tool-verification/` 是它背后的证据记录:每个脚本实际做了什么、哪个独立方法确认了它、以及目标
|
||||||
无法演练哪些场景。
|
无法演练哪些场景。
|
||||||
|
|||||||
+17
-8
@@ -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_FILES_NOTE = 110 # references + scripts, reported as a note past this
|
||||||
CORPUS_LINES_NOTE = 26000 # total lines across the same corpus, same treatment
|
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
|
# 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
|
# tiktoken/cl100k_base** (26,795 chars -> 6,065 tokens = 4.418, re-measured 2026-10-24), so both
|
||||||
# the same verdict. Getting this wrong made the gate answer differently on CI than locally.
|
# measurement paths reach the same verdict. Getting this wrong made the gate answer differently on
|
||||||
TOKENS_PER_CHAR = 4.285
|
# CI than locally.
|
||||||
|
TOKENS_PER_CHAR = 4.418
|
||||||
# A navigable head: one of these must appear in the first NAV_HEAD_LINES lines.
|
# A navigable head: one of these must appear in the first NAV_HEAD_LINES lines.
|
||||||
NAV_HEAD_LINES = 40
|
NAV_HEAD_LINES = 40
|
||||||
HEDGE_RE = re.compile(
|
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
|
everywhere in this repository, including in this file. When it is not, it falls back to
|
||||||
`characters / TOKENS_PER_CHAR`.
|
`characters / TOKENS_PER_CHAR`.
|
||||||
|
|
||||||
The fallback constant is **calibrated against that measurement** rather than guessed: a
|
The fallback constant is **calibrated against that measurement** rather than guessed: this
|
||||||
markdown-heavy document tokenizes worse per character than the usual /4 rule of thumb, and this
|
SKILL.md measures 4.418 characters per token (26,795 chars -> 6,065 tokens, cl100k_base). Agreeing
|
||||||
file measured 0.30 characters-to-tokens on SKILL.md (67,011 chars -> 19,929 tokens).
|
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
|
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
|
reached *different verdicts on different machines* -- `WARN over budget` where tiktoken was
|
||||||
@@ -198,7 +200,11 @@ def check_index_rows(lines):
|
|||||||
out = []
|
out = []
|
||||||
for section, key, size in long_rows:
|
for section, key, size in long_rows:
|
||||||
hard = size > INDEX_ROW_HARD
|
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' %
|
'%s: row for %s is %d chars (soft limit %d)%s' %
|
||||||
(section, key, size, INDEX_ROW_SOFT,
|
(section, key, size, INDEX_ROW_SOFT,
|
||||||
' -- that is a paragraph, not an index entry' if hard else '')))
|
' -- that is a paragraph, not an index entry' if hard else '')))
|
||||||
@@ -305,8 +311,11 @@ def check_corpus():
|
|||||||
green always-loaded count.
|
green always-loaded count.
|
||||||
"""
|
"""
|
||||||
refs = sorted(glob.glob(os.path.join(REFS, '**', '*.md'), recursive=True))
|
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)
|
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):
|
def count(paths):
|
||||||
total = 0
|
total = 0
|
||||||
|
|||||||
+5
-1
@@ -31,7 +31,11 @@ import sys
|
|||||||
try:
|
try:
|
||||||
import yaml
|
import yaml
|
||||||
except ImportError: # the skills CLI is the real authority; this is a local gate
|
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__))
|
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
SKILLS_DIR = os.path.join(ROOT, 'skills')
|
SKILLS_DIR = os.path.join(ROOT, 'skills')
|
||||||
|
|||||||
@@ -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
|
*Fix and check:* long values wrap onto continuation lines instead of being cut. A test asserts a long
|
||||||
value survives into the output intact.
|
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
|
## 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:
|
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
|
**A check that cannot fail is not a check, and a table written from recall is a table that is wrong in
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
# no live target process.
|
# no live target process.
|
||||||
# What does *not* live here: anything that needs a running Windows target or a
|
# 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
|
# kernel-mode component. Those routes stay in tests/benchmark.md as recorded
|
||||||
# experiments, and their coverage status is stated in
|
# experiments, and what the suite deliberately does not cover is stated in
|
||||||
# docs/tool-verification/EXTENSION-test-harness.md.
|
# tests/README.md (`## What it deliberately does not do`).
|
||||||
#
|
#
|
||||||
# If pytest is not installed:
|
# If pytest is not installed:
|
||||||
# python -m pip install pytest
|
# python -m pip install pytest
|
||||||
|
|||||||
@@ -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` |
|
| 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` |
|
| 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 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` |
|
| 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
|
## Start here: classify the target in thirteen questions
|
||||||
|
|||||||
@@ -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.
|
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".**
|
**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:
|
*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.
|
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.
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ without a check.
|
|||||||
| You are about to publish notes, a script or a screenshot of the work | `references/desensitization.md` |
|
| 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` |
|
| 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 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` |
|
| You need to know what this skill does not cover before promising anything | `references/coverage-and-limits.md` |
|
||||||
|
|
||||||
## Reference index
|
## Reference index
|
||||||
|
|||||||
+3
-1
@@ -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_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_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_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. |
|
| `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_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_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. |
|
| `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. |
|
| `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. |
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
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)
|
result = run_script("hwprobe.py", "--raw", "--json", timeout=120)
|
||||||
assert result.returncode == 0, result.stderr
|
assert result.returncode == 0, result.stderr
|
||||||
payload = json.loads(result.stdout)
|
payload = json.loads(result.stdout)
|
||||||
assert payload["raw_cpuid"], "a raw dump was requested but nothing was reported"
|
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
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ SCRIPT_FILES = (
|
|||||||
"dart_snapshot_strings.py",
|
"dart_snapshot_strings.py",
|
||||||
"fold.py",
|
"fold.py",
|
||||||
"trace_reduce.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.
|
# A hang detector, not a performance assertion. Nothing in this suite is meant to be slow.
|
||||||
|
|||||||
@@ -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]
|
||||||
Reference in New Issue
Block a user