Repository navigation
Research and Probing Guide
Phase 1 in full detail: how to inventory a subject, stage and run it, fire the probe batteries, write custom suites, handle stateful surfaces, and distil everything into the five research files that make the rest of the run — and any recovery from interruption — cheap. Every technique on this page was executed during this wiki run against the kit itself and, as a live battery demo, against MD.Viewer 2.18.0 (evidence: research/evidence/probe-mdviewer.json, test T14).
- Step 1 — inventory before reading
- Step 2 — stage the subject, do not trust a running one
- Step 3 — fire the batteries
- Step 4 — write a custom suite
- Step 5 — CLI-shaped and library-shaped subjects
- Step 6 — the five research files
- External research — the narrow gate
- See also
scan/repo_scan.py --brief replaces an hour of manual grepping with two minutes of reading. What the brief surfaces, and why each item changes what you write later:
| Brief section | Why it matters |
|---|---|
| git metadata including tag count | Zero tags means versioning must be documented from changelog/commit markers, not releases — the subject of this wiki has exactly that shape (8 commits, no tags) |
| license type and extra attribution clauses | extra clauses change the redistribution advice you can give |
| declared dependencies or their absence | "NO MANIFEST — zero declared dependencies" is itself a headline fact (supply-chain posture) |
| largest files | where the logic lives; your reading order |
| entry points | what to probe |
| version markers | per-file versions (e.g. a viewer at 2.18 and an updater at 3.14 in one repo) |
| every external host | your supply-chain and privacy facts, with the file each URL appears in |
| security-signal counts | 11 regex classes; a spike in raw-html-passthrough points straight at the main security finding to verify |
| configuration-key surface | the skeleton of your Configuration Reference page |
Read the source in ranges — sed -n '661,760p' file — never cat a 200 KB file (PITFALLS B7). Start with header docblocks: in well-maintained codebases they hold a prose changelog with the reasoning behind decisions. This kit's own docblocks are extreme examples — lib/serve.py opens with the incident that made it necessary.
import sys; sys.path.insert(0, 'toolkit/lib')
from serve import ServedApp
with ServedApp('repo/SUBJECT', copy=['md.php', 'css', 'js']) as app:
app.write('md.md', '# Title\n\nbody\n') # fixture document
r = app.get('/md.php')
print(r.status, r.header('Content-Type'))
app.set_ini({'DISABLE_UPLOAD': 'false'}) # flip a config flag
print(app.ls()) # what did the app write to disk?ServedApp guarantees the four properties that hand-rolled serving destroys: a clean docroot (staged copy in a temp dir — no state contamination), a verified-free port (a busy preferred port raises; it never silently drifts to another server), readiness (waits for the socket, then fires one HTTP request to prove the interpreter is not dying on every request), and teardown (kills its own process group, deletes the staging dir unless keep=True).
Non-PHP subjects only need a command template:
python3 lib/serve.py repo/SUBJECT --serve-cmd 'node {root}/server.js' --port 0
python3 lib/serve.py repo/SUBJECT --serve-cmd 'python3 -m http.server {port} -d {root}' --check /index.htmlVerified (T10): the http.server template stages, serves and tears down cleanly — the kit is not PHP-bound, PHP is only the default.
Caution
Before probing, check the runtime floor: python3 setup/check_runtime.py repo/SUBJECT exits 2 when the installed interpreter is below the subject's declared minimum. Probing a subject that cannot boot measures a corpse — every request returns a 500 and an unwary agent documents the error page as behaviour (PITFALLS B10).
python3 probe/webapp_probe.py --repo repo/SUBJECT \
--copy 'md.php,updater.php,css,js' --entry /md.php \
--suite php-mdviewer \
-o research/probe.json --md research/probe.mdWhat one run records per request: group, name, method, path, status, milliseconds, byte count, all response headers, a 180-character body excerpt, and — when you pass expect= — whether the status matched. The five batteries:
| Battery | Question it answers | Classification rule |
|---|---|---|
battery_headers |
Which of 10 security headers are present/MISSING; which of 4 leak headers disclose stack info; cookies | presence check, case-insensitive |
battery_traversal |
Does the path validator hold? (20 payloads through {PAYLOAD} templates) |
body markers, not status — a refusal may be HTTP 200 with a denial banner (deliberate non-disclosure); leak markers checked separately |
battery_injection |
Does output escaping hold? (26 payloads through post/get/raw sinks) | verdicts: REFLECTED-VERBATIM / escaped / reflected-case-changed / TAG-SURVIVED / SSTI-49 / empty-body / not-reflected
|
battery_flags |
Which config flags gate which endpoints? | flip INI keys via the staged app, re-request, compare to expect
|
battery_disk |
What did the app write to disk as a side effect? | full staged-tree listing |
Measured example (T14, MD.Viewer 2.18.0 under PHP 8.2.34): the whole php-mdviewer suite — modes, traversal, injection, flags, API-key shape (mdv_ + 48 hex), a 16-filename upload matrix, disk side-effects — completed in 0.46 seconds, with 19 of 26 injection payloads reflected verbatim and 17 of 20 traversal payloads denied (the remaining three: two harmless 301 normalisations and one NUL-byte transport error). The suite also flagged two expectation mismatches against figures recorded from an older subject build — the expected-vs-actual column is how drift becomes visible instead of silent.
A suite is a function, not a framework:
def suite_my_subject(probe, entry):
out = {}
probe.run("landing page", entry, expect=200, group="modes")
probe.run("health endpoint", "/healthz", expect=200, group="modes")
out["traversal"] = probe.battery_traversal(
["/files/{PAYLOAD}"],
deny_markers=["not found", "denied"], # BODY markers
)
out["injection"] = probe.battery_injection(
[{"name": "search", "path": "/search", "field": "q", "via": "post"}],
)
out["headers"] = probe.battery_headers([entry, "/healthz"])
out["files_written"] = probe.battery_disk()
return out
SUITES["my-subject"] = suite_my_subject # register next to generic/php-mdviewerRules learned in the field:
- Never trust a 200 without a body marker. A front controller answers 200 for almost any path because the router falls through to the home page; the first "successful" probe of the MD.Scribe run was exactly that — a correct status code and a meaningless result. Assert on a substring of the body.
-
Two deployment layouts = two staged instances in one run. When a subject ships a
public/docroot and root-level compatibility shims, some endpoints are physically unreachable in one layout.ServedAppinstances compose:
with ServedApp(repo, copy=['public', 'src'], serve_cmd='php -S {host}:{port} -t {root}/public router.php') as A, \
ServedApp(repo, copy=['md.php', 'src'], serve_cmd='php -S {host}:{port} -t {root}') as B:
pass # probe A's contract and B's contract separately-
Ordered, session-bearing flows use
lib/session.py, not batteries (batteries are order-independent): setup → login (CSRF viacsrf_from_html, six printed shapes) → authenticated actions → logout, with the 302 observable (no auto-redirect), the cookie jar handling multipleSet-Cookieheaders, and the token placed in the body field and a header — never the URL. One missed token shape cascades into dozens of 403s (the run-2 lesson). -
Write evidence through
lib/evidence.write_evidence()so a filesystem hiccup cannot lose a completed run — it retries into<kit>/workspace-fallback/instead of raising.
The full worked example of all four rules is examples/run2-mdscribe/probe_mdscribe.py (1,571 lines, 249 checks, two layouts, a stateful admin flow).
For a CLI, skip serve.py and keep the shape that matters — declarative cases → evidence JSON → Markdown tables:
import json, subprocess
cases = [
(["tool", "--help"], 0, "usage:"),
(["tool", "--version"], 0, r"\d+\.\d+"),
(["tool", "bad-flag"], 2, "unknown option"),
]
results = []
for argv, want_rc, want_out in cases:
p = subprocess.run(argv, capture_output=True, text=True)
results.append({"argv": argv, "rc": p.returncode, "rc_ok": p.returncode == want_rc,
"out_ok": want_out in p.stdout + p.stderr})
json.dump(results, open("research/cli-probe.json", "w"), indent=1)For a library, the documented recipes are the probe surface: execute every snippet from the README in a clean venv and record pass/fail — that is how "the docs say X" becomes "X was reproduced".
Write them before moving on; they are the recovery point if context is compacted, and Phase 3 works from them, not from your memory.
| File | Contents | Failure mode it prevents |
|---|---|---|
summary.md |
what it is, the problem, architecture in one paragraph, security posture, measured feature support, competitive position | re-deriving the framing mid-write |
architecture.md |
component map, request lifecycle, module breakdown, config resolution, function index with line numbers | citing file:line from memory (they drift) |
domain-notes.md |
standards context, security domain, deployment context, canonical glossary | five terms for one concept across 20 pages |
facts.json |
every value with a source: code:file:line | readme | git | test:name | web:url
|
"was this verified or assumed?" becoming unanswerable |
sources.md |
§A citations · §B named empirical tests · §C external sources · §D claims deliberately NOT made | a future pass re-adding a plausible falsehood |
Provenance discipline in facts.json (shape used by the reference run and this one):
{
"injection_verbatim": {
"value": "19 of 26 payloads reflected verbatim by the clipboard-preview sink",
"source": "test:t14-probe-mdviewer.json"
},
"no_tags": { "value": true, "source": "git" },
"runtime_floor": { "value": "Python 3.8+ (claimed)", "source": "readme" }
}Mark every behavioural claim in summary.md [tested] or [read] — the distinction is the one rule made visible.
Web research is allowed only for context the repository cannot give you: specifications (GFM, CommonMark), platform behaviour (GitHub wiki rendering rules), standards (Agent Skills frontmatter), security references (OWASP), and competitor documentation. Every statement about the subject itself comes from the subject — a competitor comparison cites the competitors' docs, never their marketing claims about the subject. Record each external source in sources.md §C with its URL and what it was used for.
- Tool Reference — flags and internals of every Phase-1 tool
- Security Considerations — authorized-use doctrine for the payload batteries
- Sandbox Survival Guide — the environment traps that corrupt research silently
- The Four Phase Workflow — where Phase 1 hands off to Phase 2
- Extending the Kit — registering suites and new batteries permanently
github-wiki-llm-writer 1.2.1 · MIT · Repository · Report an issue · Home · Glossary · FAQ · Troubleshooting
Facts in this wiki are verified against the source at commit 7e17a11; see Changelog and Versioning.
Home Getting Started Features and Verification Model Comparison with Alternatives
Installation and Setup Configuration Reference
The Four Phase Workflow Research and Probing Guide Writing Pages Guide QA and Audit Guide
Publishing and Delivery Security Considerations Sandbox Survival Guide Extending the Kit
Tool Reference Architecture Troubleshooting FAQ Changelog and Versioning Glossary
1.2.1 · MIT