| name | webseclist-review-references |
| description | Reviews and repairs references already published in the Top 10 Web Hacking Techniques Markdown-and-PDF archive. Use for one article, one collection such as 2019 or YYYY-ai, or a bounded period when asked to audit, validate, verify, QA, sanity-check or proofread archived references; investigate wrong-page captures, gibberish, damaged PDF text, bad titles, missing or wrong figures, missing document-derived bylines, broken or stale PDFs, missing or orphaned files, stale metadata, Markdown that renders wrong such as an unrecognised or unclosed code fence, a mangled code listing or a broken table, or a website footer, comments, related posts, subscribe pitch or other publisher tail left after the article. Also use before a release or website rebuild. Do not use for new or never-archived citations (webseclist-archive-references), removal (webseclist-remove-reference), a credit supplied by the user (webseclist-credit-author), research-value judgement (webseclist-judge-reference), or announcement snapshots (webseclist-archive-listings). Read year lists; never edit them. |
Review archived references and repair what is wrong
Set the boundary
The archive aims to hold a Markdown and PDF copy of every cited resource.
Acquisition is mechanical, so it succeeds loudly: a run reports "stored" whether
it captured the research or a consent wall, a product page or a decoded font
stream. Use this workflow to read what was actually published and fix it.
Audit documents that already exist. Route a reference that has never had archived
Markdown to webseclist-archive-references. Handle an absent Markdown file as
follows:
- One article, and its Markdown is missing. Stop, and say so. There is nothing
to audit, so do not acquire it yourself and do not review its manifest entry
instead — report that the document is absent, name the workflow that fetches it,
and let the user decide.
- A year, or a period. Keep going. A missing document is one entry out of
eighty or more, and halting the sweep for it wastes the run. Review everything
that does have a document and collect the rest into one list at the end of the
report — that list is the work-list an acquisition run starts from.
Tell two cases apart before recording them:
- If the manifest advertises a published file and the tree lacks it, file a
faulty capture as required by
CLAUDE.md.
- If acquisition never produced a document, do not manufacture a review result.
Confirm it is already represented in generated
archived-references/document-gaps.md and include it in the handoff.
Keep the three gap reports distinct:
| Report | Document state | Missing item | Editing rule |
|---|
document-gaps.md | not archived | the document | generated by refs.py index |
review-gaps.md | archived | a human judgement | maintained by hand |
store-gaps.md | archived | source bytes | generated by refs.py index |
Read review-gaps.md for a pack that overlaps the requested scope. Remove or
adjust only a hand-maintained item this review actually settles; never overwrite
the file during indexing. Never hand-edit either generated report.
Resolve the scope and prerequisites
What to review. Take it from the invocation arguments, which name one of three
scopes:
| Asked for | Scope | Select with |
|---|
| an article — a URL, a title, a slug | that one reference | --only <url-substring> |
| a year or a preliminary collection | every reference in it | --collection <2019 | 2019-ai | 2016-17> |
| a period — "2019 to 2021", "the -ai collections" | each collection in turn | one --collection run per collection |
If no scope was given, ask which, and offer what is on disk
(archived-references/md/*/). Reviewing "everything" is rarely what someone means
and is rarely affordable — confirm before sweeping more than two collections. Work
a period one collection at a time and report per collection: refs.py takes one
collection per run, and a sweep that blurs several together is how a run loses
track of which reference it changed.
Resolve an article selector before running a command. The CLI's --only
implementation matches the normalized URL key in
archived-references/manifest.json; it does not match a title or slug. Use the
title or slug to find the manifest entry, then pass a distinctive substring of
that key. Inspect the matches first and require exactly one for a one-article
operation. A command exiting successfully after matching nothing is not success.
Run from the repository root. Commands below abbreviate
python3 tools/references/refs.py as refs.py. Use the environment's Python 3
launcher if it has a different name.
Preserve the starting state. Record the initial diff names and the baseline
refs.py verify failure/warning classes before editing. Existing changes belong
to the user. Pay particular attention to pre-existing changes in the manifest,
overrides, archive files and year lists; do not overwrite or attribute them to
this review.
Use the durable content store. Set WEBSEC_REFS_STORE to the existing durable
store before commands that read, write or assess stored objects, including
acquire, import, wayback, images, papers, pdf-text, translate,
pdf, index and verify. Never guess the path or record it in a tracked file.
attribution --check and attribution --rewrite are the deliberate store-free
exception. Without the variable, other commands silently fall back to a
git-ignored workspace cache and can publish or report against the wrong bytes.
If the store is unavailable, continue a read-only document review when useful,
but do not publish a repair. The one exception is the repository-mandated filing
of a faulty capture: update its manifest content_gap, leave that edit in place,
and report that refs.py index remains required once the durable store is set.
Scratch space. Put scanners, extracts and import staging in a temporary
directory outside the repository, so a half-finished audit never lands in a diff.
Exclude concurrent writers
Mutating refs.py commands load the whole manifest and save it whole, so two
writers can silently lose one another's changes. Check the process list for an
active refs.py process and inspect the manifest's modification time and diff.
A recent timestamp alone is not proof of another writer; an unexplained active
process or a file changing during the check is. Ask before proceeding when the
state is ambiguous. Run one mutating refs.py command at a time, keep any
parallel review read-only, and never run a corpus-wide acquire --force.
Sweep mechanically, then judge
Write a scanner into scratch space rather than opening files one by one — a
collection is 80-100 documents and the interesting faults are rare. For a single
article there is nothing to sweep: read the document and its manifest entry
straight through, against the same classes. Worth checking:
| Class | What to look for |
|---|
| Frontmatter | missing type/title/resource/slug/kind, empty title, slug that disagrees with the filename |
| Gibberish | mojibake (é, ’), runs of U+FFFD, control characters |
| Font damage | a PDF whose glyphs were read without the font: €ngerprinting, certi“cate, h�p://, Eicient — reads as prose, so no length or quality check catches it |
| Binary garbage | decoded font/image streams published as prose |
| Titles | a PDF's file stem (BHUS26 Heyes CSS WP), site chrome (… › Vendor, Owner/Repo: file.md), a blog masthead |
| Body | empty or implausibly thin, truncated mid-sentence, page furniture as content |
| Figures | broken or relative image targets, placeholders, anti-hotlink banners, wrong or missing images, preserved figures absent from the rendered PDF |
| Markdown | syntax this repository's renderer does not accept, so the PDF shows something other than the document: a ~~~ fence, an unclosed fence, an unfenced code listing |
| Tail | the last blocks are the site, not the article: a comment thread, "read more" teasers, a subscribe pitch, footer navigation, an author-bio card |
| Attribution | no author where the document names one |
| Files | manifest entries whose md/pdf is absent, and published files no entry claims |
| PDFs | %PDF- header, a plausible %%EOF near the end or successful parser check, plausible size |
| Metadata | cited_by line numbers that land on a line with no link; translation pairs not cross-linked by translation_file/translation_of |
| Coverage | a link in the list with no manifest entry (unwrap web.archive.org/…/<url> first — the archive files a capture under the CAPTURED url) |
|
A damaged PDF reads as prose, so look at the words. The in-process parser
reads byte strings without applying a font's encoding, and when that font is
unusual the result still passes every gate: right length, right shape, real
sentences — with fi, ff and tt quietly wrong inside words. An IEEE S&P
paper about fingerprinting was archived with 422 instances of €ngerprinting,
and a CCS paper lost a third of its text and titled itself Eicient. The
extractor now escalates such a document to poppler in the container, which reads
the font properly; acquire does that automatically, and refs.py pdf-text --only <url-substring> --into <dir> then refs.py import --redo --only <url-substring> <dir> is the manual route for a reference a previous import
filed. The archived PDF is never rewritten — the container tool only produces
the Markdown.
Detecting binary garbage is the one worth getting right. Decoded font bytes
are mostly accented Latin-1, so a test based on character class calls them
"alphanumeric" and sees nothing. What separates them from prose is shape: almost
no runs of three or more ASCII letters, combined with a high share of bytes above
0x7F. Require both, and exclude CJK first — CJK prose also lacks Latin word runs
and is perfectly legitimate.
A valid PDF can still omit the research's figures. For figure-dependent
documents, especially slide decks, compare the Markdown's image/background
references, the manifest's image inventory and store objects, and the rendered
PDF. Open enough pages to confirm the right images appear in the right places.
Classify a missing essential source figure as an incomplete document and file it
under CLAUDE.md; classify a discovery, policy or rendering failure on an
otherwise present document as a tooling/review defect, not a document gap. Do not
lower a global size floor or broaden URL discovery for one asset; treat a narrow
exception as a separate image-policy change with tests. Do not rasterise SVG in
this review workflow; use a maintainer-approved safe fallback. After a tested
repair, rerun refs.py images --force --only <url-substring> and the scoped PDF
command.
Expect false positives, and verify before acting
Most of what a scanner flags in this corpus is real content. Confirm each class
against the actual text before changing anything:
- Very long unbroken tokens are encoded payloads, ffmpeg protocol strings and CDN
URLs — the substance of the research, not corruption.
- Repeated paragraphs are usually a slide deck's build-up slides, or a writeup
restating its exploit as it evolves.
- Non-Latin text usually means a genuinely foreign document; check for a
translation pair before treating it as a fault.
- A body that does not end in punctuation is usually just an acknowledgements
slide, a licence section or a byline.
- Titles like
Code, Tool, Slides or PoC come from the citing list's own
idiom, where a technique's siblings are [Slides](…) [Code](…) on one line.
Those are intended. A PDF's file stem is not.
Report the false-positive classes you cleared. A reviewer needs to know what was
looked at, not only what was changed.
Correct Markdown only where it changes what the reader sees
While a document is open, read its Markdown as Markdown. Nearly everything a
style linter flags in this corpus is correct as it stands, and the faults that
matter are invisible in the source and obvious in the PDF.
The body is quoted evidence, not your prose. Everything under the UNTRUSTED
SOURCE TEXT banner is third-party material. Backslashes are Windows paths, tabs
are an exploit's indentation, <script> is the finding itself, * and +
bullets are terminal output, and # # # Begin Advisory # # # is the advisory's
own banner. Never normalise, reflow, re-wrap or re-indent it. Tidying rewrites a
researcher's words and can break the payload the citation exists to preserve.
Fix rendering, never taste.
Judge against this repository's renderer, not against CommonMark.
refslib/makepdf.py implements a deliberate subset, so a document that lints
clean elsewhere can still render wrong here:
| The renderer | Consequence |
|---|
fences are backticks only — _FENCE is ^\s* plus three backticks | a ~~~ fence is not a fence at all |
| any line opening with the same marker closes the fence | a stray marker inside a listing closes it early |
| an unclosed fence consumes the rest of the file | everything after it becomes one code block |
a table needs a header row and a ---|--- separator row | bare pipe rows print as literal pipes |
---, *** and ___ are horizontal rules, and there is no setext support | --- under a line of text is a rule, never a heading |
| raw HTML is escaped and displayed | a payload is shown, never silently swallowed |
| a heading over 300 characters prints as a paragraph | its # markers are stripped, by design |
These are normal here. Clear them, and say you did. Measured across the
1,672 archived documents: a second # H1 in the body (765 documents — the
archive's own template emits # <title> above ## Content, and the captured
page keeps its own heading); heading levels that skip a step (268); pipe rows
with no separator row (267, all of them HTML layout tables flattened by the
converter); three or more consecutive blank lines (244); trailing whitespace
(123); raw HTML at column 0 (68, escaped and displayed, and usually the
research); plus tabs, mixed bullet markers, backslash escapes, and
[](link) misread as an empty link text. None of these change what a
reader sees. None is worth a diff.
These are worth fixing, and they are rare. Each destroys content in the PDF
while the Markdown file still reads as healthy:
- A
~~~ fence. The renderer does not recognise it, so the listing falls
through to the paragraph branch and is flowed: class Foo(Serializable): bar: int baz: str on a single line with the indentation gone, and __main__
rendered as a bold main because the underscores became emphasis. Two
2026-ai documents carry tilde fences; the other 535 fenced documents all use
backticks. Converting the fences to backticks is the fix.
- A fence that never closes. The rest of the document renders as one code
block. No document is in this state today, so treat an occurrence as new
damage rather than as the archive's normal condition. Walk the fences the way
the renderer does — open on a marker, close on the next line starting with the
same marker — instead of counting markers, because an indented fence and a
marker inside a listing both break a simple count.
- A code listing that was never fenced at all. The same flowing damage, with
no marker to find it by. Look wherever prose and code alternate.
- An image or link target that resolves nowhere, which the Figures class
above also covers.
Publishing a Markdown fix uses the tail cut's route and carries its warning.
The published .md is generated and overrides.json has no body field, so a fix
to a fetched document is a controlled hand edit below the frontmatter, followed
by refs.py pdf --only <url-substring> --force when steps.pdf.source is
markdown. The store still holds the unfixed text, so acquire --force,
import and translate --apply each restore it — state that in the report. For
a hand import, stage the corrected body and run refs.py import --redo --only <url-substring> <dir>, which rewrites the stored text as well. Never set
content_gap for a Markdown fault: any non-empty value puts a document that
exists onto generated document-gaps.md.
A repeatable class belongs in the converter, not in the document. The rule
that governs boilerplate.py governs this too — do not hand-fix in one document
what every future capture will reproduce. Tilde fences and separator-less tables
are converter behaviour; change them there, with corpus-wide tests, as a
separate piece of work, and report the class rather than papering over one
instance of it.
Remove a website tail only after reading it
Capture conservatively trims publisher furniture from both edges.
refslib/boilerplate.py stops at the first unrecognised block, examines no more
than eight blocks per edge, refuses a block over 400 characters or containing a
fenced code block, and refuses to remove more than a quarter of the document.
Residual comments, related posts, subscribe prompts, share buttons, footer menus
or author cards can therefore remain, often behind one ordinary-looking block.
Judge the tail by reading it. Do not change boilerplate.py for a single
document: one new pattern affects every future capture. Treat a repeatable class
across several documents as a separate extractor change with corpus-wide tests.
The question is whose voice the block is in: the researcher's document, or the
site it was published on. "Leave a Reply", "3 Responses", "Trackbacks and
pingbacks", "You might also like", a card carrying another post's headline and
date, "Posted in Research | Tagged xss", "Sign up for our newsletter", a cookie
notice, "Back to all posts" — all site. Anything the researcher wrote is the
document, however unlike prose it looks.
Four rules for the cut itself, because deleting content is worse than keeping an
advert:
- A cut is a suffix. From one boundary to the end of the file, nothing else.
Furniture in the middle stays; the document is not yours to rewrite.
- These END documents and are not furniture:
## References, ## See also,
## Conclusion, a disclosure timeline, credits, greetz and thanks (a talk's
last slide is usually exactly that), acknowledgements, a licence the research
itself carries, and ## Presentation Video with nothing under it — that
heading is the archive's only remaining trace that a recording exists, because
its <iframe> was removed by design.
- A comment thread can be the citation. Some cited pages are a discussion,
and plenty of authors answer a correction below their own article. If the
replies carry technique, they are the document.
- Quote the words you cut from and count the characters. A cut nobody can see
is a cut nobody can review.
Publish a fetched document's cut as a controlled hand edit. This is the one
generated-file exception in this workflow: overrides.json has no body field.
Do not set content_gap; any non-empty value puts the reference on generated
document-gaps.md, even though the document exists. Instead:
- edit
archived-references/md/<collection>/<slug>.md below its frontmatter,
leaving the frontmatter itself alone;
- record the exact first removed line and removed character count;
- inspect
steps.pdf.source on the entry. If it is markdown, run refs.py pdf --only <url-substring> --force. If it is original-pdf or linked-paper,
the preserved publisher PDF should remain unchanged;
- run the scoped and guarded closing checks below.
The store still holds the untrimmed text, so say so in the report. The body's
source of truth is the content object behind content_sha256, and that is what
acquire --force, import and translate --apply render from — any of them
publishes the furniture again. If the reference has or awaits a translation,
inspect both published halves: applying or re-rendering a translation rebuilds
the original from the untrimmed store, while cutting only the original can leave
an existing translation inconsistent. Record any paired hand edits explicitly.
For a hand-imported document, prefer the durable route: put the trimmed BODY ONLY
into a scratch directory. A staged .md is treated as source text, not parsed as
an archived page, so frontmatter left at the top becomes document body. Name the
file after the document, since matching still runs against the whole manifest,
and run refs.py import --redo --only <url-substring> <dir>.
This rewrites the stored text as well as the file. It also re-grades the document
and withdraws any translation pair it holds. Never omit --only: broad --redo
reopens every reference a previous import filed. A healthy fetched reference is
ineligible and reports already archived from its own source; do not make it
eligible by filing a false fault.
Wrong-page captures are the most valuable find
A capture of the wrong page reads as a healthy document: full frontmatter, real
prose and a plausible title. Compare the citation, cited context and expected
title with the captured body. A surprising host change between original_url
and retrieved_from is a useful lead, not proof: archive snapshots, mirrors and
publisher migrations can be legitimate.
When you find one, file it before anything else, exactly as CLAUDE.md requires:
set content_gap on the manifest entry to
faulty capture: <what is wrong>; <remedy> (reported <date>), then run
refs.py index when the durable store is available. If it is not, leave the
manifest filing in place and report the pending index. document-gaps.md is
generated; never hand-edit it.
Then try to recover it with refs.py wayback --only <url-substring>. Scope it
with --only:
--faulty-captures, --document-gaps and --missing-store select every entry in
that state across the whole archive, so filing one fault and running the flag form
works the entire pre-existing backlog. If a recovered capture then extracts as
binary noise, check the stored object for a 1f 8b gzip magic number before
concluding the snapshot was bad — a capture can be stored still-compressed.
wayback stores candidate raw bytes; it does not publish the document. When it
reports a stored capture, run refs.py acquire --force --only <url-substring>,
then read the resulting document against the citation before accepting the repair
or allowing the fault to disappear from the generated gap report. Compare its
body length with the pre-repair file and investigate any unexplained shrink.
Publishing a correction
Published .md files are generated. Correct the source of truth instead: state
the fix in tools/references/overrides.json under decisions[<url>], keyed by the
manifest's normalized URL. Match the existing entries' shape, preserve the file's
formatting and key order so the diff shows only your change, and always include
outcome and class — a decision with no outcome is read as "keep no document"
and will strip the entry's grade.
Then pick the route that fits what you are publishing:
| To publish | Route | Why this one |
|---|
authors / publisher | preview with refs.py attribution --check, then run refs.py attribution --rewrite | offline and in place; reaches hand imports and references whose stored bytes are gone |
title on a fetched reference | after proving raw_sha256 exists in the durable store, run refs.py acquire --force --only <url-substring> | honours the curated title and rebuilds the slug without a refetch |
title or durable body correction on a hand import | stage the source body and run refs.py import --redo --only <url-substring> <dir> | the import path replaces the stored text, honours the title and rebuilds the slug |
| website tail on a fetched reference | controlled Markdown edit, then scoped PDF handling | no durable body-override field exists; follow "Remove a website tail only after reading it" |
attribution has no --only option. Its preview and rewrite examine every
pending curated attribution in the archive. Treat exit status 1 from --check
as "changes found", inspect the entire preview, and proceed only if every listed
change is understood. Verify the final published frontmatter; the success count
alone is insufficient.
Never reach for --replace-imports to force a title through. An import exists
because no fetch worked, and that flag throws the hand-obtained copy away.
On naming a researcher: take a name only when you can quote the words you read
it from, and put that quote in reason. A missing byline says the archive does not
know; a wrong one credits a stranger with someone's work and reads as fact. A
handle the author publishes under is a real credit — a hostname is not. Most
unattributed documents genuinely name nobody, and leaving those alone is the
correct outcome, not a gap.
Fan out only large read-only reviews
A normal collection review needs no delegation. When subagents are available,
use them only for a review that lands roughly 40 or more documents, such as an
unaudited year or a corpus-wide fault class. Delegate reading and judgement;
keep every write and every refs.py command in the coordinating agent.
Split by BYTES, not by file count. Archived documents run from 2 KB to over
150 KB, so ten files can be 20 KB or 1.5 MB. A batch built by counting files
either wastes most of an agent's context or blows through it in the middle of a
document. Sum the .md sizes and cut the batch when the sum reaches the budget.
Size the budget to the work, not to a fixed fraction. Around a fifth of the
agent's context leaves comfortable room for the document, its manifest entry and
the reasoning. Go higher when it saves tokens overall — a run of small documents
that would otherwise need three agents and three lots of instructions is better
as one — and lower when the documents are large or the judgement is delicate. A
single document that exceeds the budget on its own is its own batch.
Within a batch, read one document at a time. Agents may run in parallel with
one another, but each batch is a sequence: read a document, decide it, report it,
then start the next. This prevents evidence from one document being attached to
another.
Omit a model override so each agent inherits the session's model and standard.
Do not let review agents edit the manifest, overrides or published files, and do
not let them run refs.py.
Tell each agent what a finished answer looks like: the slug, the fault class, the
evidence quoted from the document, and the route that fits (attribution --rewrite, a corrected title, a recapture, a tail to cut, or nothing at all).
Ask for "no fault found" explicitly — an agent that reports only problems will
invent one.
Two findings must come back precise enough to apply without re-reading the
document. A tail needs the exact line the cut starts at, quoted, the last line of
the file, and how many characters go; an agent that reports "ends with some
footer junk" has sent the reading back to you. A Markdown fault needs the line
numbers of the markers involved and the replacement, and it needs the
false-positive list in front of it — an agent asked for "Markdown problems"
without it will return the 765-document second-H1 class and the converter's
pipe rows as though they were work.
Two traps that will cost you a document
acquire --force is offline only until the bytes are missing. It is documented
as a re-extract from the content store, and it is — but when the raw object it
wants is absent it goes back to the network and publishes whatever answers. A
JavaScript-driven page answers with its app shell. Store objects go missing often
in this archive (antivirus has deleted them in bulk), so check the entry's
raw_sha256 is actually present in the store before re-rendering, and afterwards
compare every document's body length with the pre-command file and, when that file
was clean at baseline, with HEAD. Investigate every unexplained shrink. A silent
2,000-character loss looks exactly like success.
A run's success count is not evidence. acquire --force skips hand-imports
silently and by design, so a pass can report every reference processed while a
curated title reached none of them. Verify against the published files: every
stated title and every stated author should appear in the frontmatter of the file
its manifest entry names. Check this before you believe the work is done.
If you drive refs.py from a shell loop, make sure the selector values carry no
trailing carriage return — a --only argument with a stray \r matches nothing,
the command exits 0, and the loop reports success having done nothing.
Renames, collisions and cleanup
A corrected title rebuilds the slug, so the file is renamed on the next re-render
and the old one is orphaned. Two siblings given the SAME corrected title — a
talk's slides and its whitepaper, a paper and its project page — collide, and the
second becomes <slug>-2, a name that identifies nothing. Keep them apart with
the format word the citation itself uses, in parentheses: … (Slides), … (Whitepaper), … (Paper).
index --prune-files deletes every unclaimed archive file, not only an old name
created by this review. Use the mechanical sweep to list the complete orphan set,
compare it with the baseline, and inspect every target first. Run the prune only
when the entire deletion set is expected; otherwise leave the unrelated files and
report them.
Summarise and tag while the document is open
You have just read the document end to end. That is the one moment when writing
its summary costs nothing, and it is why this belongs here rather than in a pass
of its own: a later run would have to read all of it again to say the same thing.
refs.py digest --queue work/digests.json --collection <collection> # offline
# ... one entry per document, from what you read ...
refs.py digest --apply work/reviewed.json --check # offline
refs.py digest --apply work/reviewed.json
Do this AFTER the repairs, not before. A summary records of, the content hash
it was written from, so one written before a tail cut or a re-render is stale
the moment the fix lands and --queue will offer the document again.
The summary is what the source found and how, aiming at 400 characters -
the mechanism, two or three sentences. Write it from the document, not from the
citation's link text, and not from the abstract alone if the body contradicts
it. Do not appraise the research and do not describe the archive's copy of it:
faults belong in content_gap, not here. --apply refuses above 500 and
reports anything over the aim; it will not truncate a rambling summary into a
fragment, because doing that once kept an opening line and deleted the four
findings beneath it. Rewrite such a summary into sentences instead.
Tags come from archived-references/tag-vocabulary.json, at most 10 per
document - at most, because a genuinely narrow document is better served by two
than padded up to a threshold with tags that do not apply. The JSON is the
record and tag-vocabulary.md is a reading of it; both are generated.
Use a word the archive already uses before inventing one - a reader
searching prototype-pollution should find every document about it, not the
ones that happened to pick that spelling. A word the vocabulary lacks is still
allowed: write it, and it is adopted and reported as new. Refusing it threw away
the one moment someone had actually read the document. ?like-this still marks
a word you want a maintainer to look at, and it is kept rather than stripped.
Case and punctuation are folded before anything is written, so XSS and xss
can never both exist. If the word you need means something the archive already
names, say so in the report: the fix is an aliases entry in the JSON, which
retires the old spelling everywhere, not a second word beside it.
Never tag an OWASP category by hand. Categories are derived from the
technique tags by the mapping in the JSON, and reach the file as
owasp-a03-2021.
The tags MUST name the techniques the document actually uses - that is the
rule, and no count can check it. You have just read the document, so this is the
one moment you can tell a technique the research USES from one it merely cites.
There is no minimum: two true tags beat four that include two which do not
apply.
A review that changes no document still owes its collection a digest: a
correctly archived reference nobody can find is not much better than a broken
one.
Finishing
refs.py pdf --only <url-substring> --force # each changed Markdown-backed article
refs.py pdf --collection <collection> # collection review: fill missing PDFs
refs.py digest --vocabulary # if any digest changed
refs.py index # regenerate reports; delete nothing
refs.py verify
git diff --check
git status --short
Do not run an unscoped pdf --stale merely as cleanup: it selects every stale
PDF in scope. Use the exact --only form for hand edits, or review the full
collection selection before accepting it. If the inspected orphan set consists
only of expected stale names, run refs.py index --prune-files, then run
refs.py index and refs.py verify again.
verify will report a standing set of missing store object failures that belong
to store-gaps.md and are not this review's work. Record the count before you start
so you can say whether it moved, confirm none of them names a reference you touched,
and confirm no new failure class appeared. Reporting "verify is clean" when it never
was is worse than reporting the real number.
Compare git status with the recorded baseline. This workflow must introduce no
year-list change (YYYY.md, YYYY-ai.md); preserve any pre-existing year-list
edits and do not claim them. If a year list changes during the run, stop and find
the writer before proceeding.
Report
Say what was actually wrong and what you did about it, then:
- the false-positive classes you cleared, so nobody re-investigates them;
- every tail you cut: the slug, the first removed words and character count; for
a fetched reference, state that the stored text remains untrimmed and a later
re-render will restore it; for a re-import, state that the store was updated
and any translation pair was withdrawn;
- every Markdown correction: the slug, the syntax that was wrong, what it did to
the PDF, and the same store warning a tail cut carries. Name separately any
fault you judged to be converter behaviour and left alone, because that is a
tooling change someone has to schedule;
- for a year or a period, every reference with no archived Markdown, gathered into
one list at the end and marked as either a file the manifest advertises but the
tree lacks (a fault, which you filed) or a reference the archive never got a
document for (already on
document-gaps.md). Give each one its URL, so the
acquisition run can act on the list without redoing the sweep;
- the summaries and tags recorded: how many, and every tag that entered the
vocabulary as NEW, with the document that asked for it. A new word nobody
writes down gets re-invented next year under a slightly different spelling —
and name any that duplicate an existing tag, because those want an
aliases
entry rather than a place in the list;
- anything you deliberately left, and why;
- judgement calls the maintainer should review — a new convention, an invented
disambiguation, a credit resting on a handle rather than a stated name;
- any mistake you made and corrected mid-run, plainly. A repair you had to make is
the most useful thing in the report, because it is what the next run must avoid.