Development record
every-citation-abcd-publishes-is-provably-alive-and-honestly
Summary
spc-17 delivers itd-101's citation gate: a deterministic, zero-network lint
family that runs in the commit gate, and an explicit on-demand refresh verb
(abcd docs cite refresh) that does the live fetching and writes a committed
baseline the lint then enforces offline. The design decisions below were
settled by the 2026-07-27 grill (recorded in the intent's Grill Settlements);
this spec records them together with the mechanism — it does not reopen them.
Settled constraints (from the grill)
- Zero network in the gate. The citations lint family reads only committed config and the committed baseline. All fetching lives in the refresh verb.
- Refresh is manual now. It is surfaced by ahoy status and release-preflight nagging; a scheduled-CI wrapper is a later, separately signed-off change and is out of this spec's scope.
- Staleness policy. A baseline entry older than 180 days makes docs lint warn; one older than 365 days blocks at the release gate only — commits are never calendar-blocked. Human-verified entries age on the same clock and re-enter the manual queue when stale.
- Honest labelling, never method. Each baseline entry records whether verification was automatic or manual, and when — never how a human verified.
- The row-has-footnote rule is homed here. The structural rule deferred from spc-15 (every crosswalk table row carries at least one footnote) is implemented in this intent or not at all.
Mechanism
Offline lint family (commit gate)
The docs-lint family gains citation rules, evaluated with zero network:
- Structure: footnote markers and definitions in bijection per page; every crosswalk table row carries at least one footnote (the spc-15 deferral).
- Syntax: cited URLs and DOIs are well-formed.
- Source policy: aggregator domains are refused, from committed config.
- Baseline enforcement: no cited URL absent from the baseline, no entry recorded as broken, no entry whose recorded final address has drifted from the cited URL's current resolution record, and the staleness clock above (180-day warn here; the 365-day blocker binds in the release gate, not the commit gate).
Severities mirror the existing docs-lint family (blocker/warn), and findings render with the same successor-carrying shape.
Committed baseline
abcd docs cite refresh fetches every cited URL and writes the baseline, a
committed machine record under .abcd/ (one file, schema-versioned). Per URL
it records: the final resolved address after redirects, when it was last
checked, the outcome, and the verification label — automatic, or manual
with its date. Nothing else: no fetch transcript, no method, no headers.
Manual queue, one receipt schema
Sources that block automated fetchers join the manual queue:
- Rung 1 (this spec's floor): the refresh verb prints the queue as a checklist; the maintainer clears it link by link and a confirm verb writes the dated receipt into the baseline (verification: manual + date).
- Rung 2 (same schema, later rung): a generated, disposable checklist page hands back a receipt file that the same confirm verb ingests. One receipt schema serves both rungs — the page is a different producer of the same input, not a second pathway.
Engine and seam
The fetch engine is native and dependency-free (net/http, bounded redirects, explicit timeouts). A specialist link checker can later slot in behind the refresh seam as an adapter without changing gate semantics: the baseline schema and the lint rules are the contract; the fetcher is a replaceable producer.
Surfacing
ahoy status reports the baseline's age summary (stale/overdue counts), and the release preflight nags when entries approach or pass the 365-day blocker. Both planes are wired at delivery: CLI verbs and the plugin markdown surface.
Acceptance-criteria mapping
- AC 1 (zero-network gate: structure, syntax, policy from committed inputs) → Offline lint family.
- AC 2 (baseline records final address, checked-when, auto-vs-manual + date, never how) → Committed baseline.
- AC 3 (180-day warn in docs lint; 365-day release-gate blocker; manual verifications age the same) → Settled constraints + Offline lint family.
- AC 4 (printed checklist + confirm verb; generated page hands back a receipt the same verb ingests; one schema) → Manual queue, one receipt schema.
- AC 5 (specialist checker slots behind the seam without gate changes) → Engine and seam.
Out of scope
- The scheduled-CI refresh wrapper (explicitly out of this intent, per the grill; needs its own sign-off).
- The generated checklist page itself may land as the later rung; only the receipt schema it must emit is fixed here.
- Any change to non-citation docs-lint rules.
Frontmatter
| id | spc-17 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-17-every-citation-abcd-publishes-is.md
open on GitHub ↗ · commit history ↗
References1
- itd-101**Every citation abcd publishes is provably alive and honestly labelled — and the gate that enforces it never flakes.** A cited reference page is only as trustworthy as its links, and links rot silently: pages retitle, URLs redirect, whole platforms announce their own shutdown. abcd splits citation validation across a deterministic gate and an explicit refresh. An offline lint family checks structure and source policy on every commit — footnote markers and definitions in bijection, URLs and DOIs well-formed, aggregator domains refused — with zero network in the gate. `abcd docs cite refresh` does the live fetching on demand and writes a committed baseline: per URL, the final resolved address, when it was last checked, and whether verification was automatic or manual — never how. Sources that block automated fetchers join a manual queue the maintainer clears link by link: printed as a checklist first, later as a generated, disposable checklist page that hands back a receipt file the verb ingests. The lint then enforces the baseline offline — no broken entries, no stale entries, no cited URL without a receipt, no citation whose recorded final address has drifted. The engine is native and dependency-free first; a specialist link checker can slot in later behind the same seam. "My release gate stays deterministic and my citations stay alive," said Kira, an open-source maintainer. "When a source needs human eyes, abcd hands me the exact list to click through and records that I did — not how I did it."implements
mentions9
- dcn-26072700000000062026-07-27: itd-101 spec minted as spc-16 collided with unmerged #156 (iss-80 class, both spc-N and iss-N allocators); renumbered to spc-17 by hand, duplicate capture folded into iss-80 — ids on this branch deliberately skip 16.
- dcn-26072700000000072026-07-27 — itd-101 part (a) settled four choices spc-17 left open. (1) The committed baseline lives at `.abcd/citations-baseline.json`, alongside docs-lint.json/record-lint.json/rules.json — it is config the gate reads every commit, not development record; rejected `.abcd/development/` (wrong tier) and `.abcd/work/` (not a session artefact). (2) A table is a CROSSWALK when its nearest preceding heading matches `(?i)crosswalk` (configurable) — the narrowest heuristic that selects docs/reference/terminology.md's table and leaves every ordinary directory-map/comparison table alone; rejected column-header and any-table-in-docs identification (both sweep up README.md and docs/README.md tables that carry no citations by design). (3) A page's CITATION CORPUS is its footnote definitions, including wrapped continuation lines — not its prose; a URL in a body paragraph stays links_resolve's business, which is what keeps the syntax and source-policy rules off ordinary text. (4) `refused_domains` ships EMPTY: the repo states its admission rule in prose ("no single-author coinages, no aggregators", docs/reference/terminology.md + ACKNOWLEDGEMENTS.md) but nowhere names a domain, and the gate must not invent an editorial blacklist the project never agreed. Also: staleness is measured from `last_checked` for automatic and manual entries alike (AC 3's "same clock"), and the 365-day threshold surfaces as a distinct rule id `citation_baseline_overdue` at warn — the commit gate never calendar-blocks, so promotion to blocker is the release gate's job in part (b).
- dcn-26072700000000082026-07-27 — itd-101 part (b) settled seven choices spc-17 left open, and consolidated one primitive. (1) The SSRF fetch guard moved from `internal/core/memory` to `internal/urlguard` rather than being copied for the second fetch path; its address predicate became a parameter so a fetch path can be exercised against an httptest server (which binds loopback) without the shipped policy ever being relaxed. (2) BLOCKED-FOR-AUTOMATION is classified by status code ONLY — 401, 403, 406, 429 — with no body sniffing for challenge-page markers: a heuristic over page text would be unreliable AND a reason to start reading content, and the challenge pages that matter answer with one of those codes anyway. Everything else non-2xx is broken. (3) One GET per URL, body never read and never retried: liveness is the status line, so a citation to a huge file costs a response header; a retry loop would make a run's duration a function of how many links are failing. (4) A blocked URL with NO prior entry writes NOTHING — the gate then reports it as unreceipted, which is exactly true; recording it broken would put a lie in a committed record the gate enforces. (5) A CURRENT manual receipt is preserved verbatim and not even re-requested; a STALE one is re-checked (AC 3's one clock) and, if the source still blocks, KEPT rather than deleted so the gate keeps warning honestly. Rejected: refetching every manual entry every run (downgrades a human's receipt to a robot's failure), and never refetching them (buys a permanent exemption from ageing). (6) `confirm` records ALIVE only — it is "I looked and it is there", never a channel for recording a link dead — and takes URLs positionally or a receipt file, both assembling ONE schema so the later generated checklist page is a different producer, not a second pathway. (7) `refresh` exits ZERO after recording broken links: it records, the gate decides; a verb that failed on a dead link could never write the record that reports it. Also: the release-gate promotion reuses `ArmReceiptGate`'s shape (`lint.ArmCitationOverdue`, armed by `abcd docs lint --release-gate`) with the FLAG as trust root, and the `abcd launch --dry-run` citation gate takes its measurement from the CLI as data because `core/lint` imports `core/launch` for its semver — the same shape the cobra-tree walk already uses.
- iss-80Record id allocators (itd-N, spc-N, iss-N) are branch-local: parallel agents on separate branches each scan for max+1 and mint the SAME id. Two intents both claimed itd-82 and both merged to main (PRs #46, #47). The iss-N allocator hit the same class before (iss-77 collision, manually renumbered to iss-79; class recorded as iss-74). Resolving a collision forces a renumber, which breaks the record's stated 'ids are capture-stable, never renumbered' invariant -- so the minting scheme, not the renumber, is the defect. This capture is the MINTING half: a collision-free allocation scheme is needed (forge-minted / random-suffix / timestamp / reserve-registry -- SOTA under research). The DETECTION half is armed separately: intent_lifecycle now blocks duplicate intent ids.
- iss-2609230734017018itd-101 ac-4 promises that the later generated checklist page hands back a receipt file the confirm verb ingests; delivered reality: docs cite confirm --receipt ingests the one Receipt schema (internal/core/cite/confirm.go) and refresh prints the manual queue, but no generated checklist page exists in the tree — spc-17 records it as a rung that may land later, so the receipt-file half of the criterion has a consumer and no producer. Found by the itd-101 fidelity audit (rcp-c9215c58f907).
- itd-20`/abcd` Tells You Where You Are
- itd-24Completed Releases Get A Retrospective
- itd-163A cited source always resolves and an adopted inspiration is always acknowledged: the reference-closure and acknowledgements-mirror gate
- spc-15alice-reads-one-page-and-knows-exactly-where-abcd-stands-in