ADR-0038.
`--mine` mines across languages, groups by dependency family, and never resurrects a ratified DRAFT
Date: 2026-07-20
Status: proposed
Deciders: Thibault Souris (maintainer)
Method: decision-loop — resume-existing audit findings on --mine, reality-checked against mineDrafts/detectEcosystems/renderDraft (characterize.ts:281–358).
Context
--mine is the advisory half of characterize. Four defects, observed in the real code:
- Node-only mining.
depNamesis populated only for Node (characterize.ts:69); Python/Go/Rust/Java leave it[](:88, :95, :108). SoDEP_FAMILIES— which already containsdjango|flask|fastapi|rails|spring-boot|sqlalchemy|gorm|diesel— is dead code off JavaScript. The typical brownfield target (a legacy Python/Java/Go service) gets stack + CI + container candidates but zero dependency-family candidates. Promising "brownfield" to a Python/Java/Go legacy is misleading today. - One DRAFT per dependency, capped globally at 8.
mineDrafts(:312–321) emits one DRAFT per matching dep and stops at 8 in manifest order — so a datastore declared late in the manifest can be purged by the cap, and a repo with many deps in one family gets noise (N near-identical DRAFTs). - Post-ratification resurrection. After the operator ratifies
DRAFT-stack-node.md→ renames itADR-NNNN-…md, the next--minere-emitsDRAFT-stack-node.md(nothing checks theadr/dir for an already-resolved slug) and re-reddens the gate. firstSeencomputed then thrown away.firstSeen()computes the first-seen-in-git date of the manifest and nothing surfaces it. (An earlier draft of this decision promoted it to the DRAFT's**Date**; the doctrine audit rejected that as a fact/hypothesis-boundary breach — see the Decision below.)
Decision
--mine mines dependency families across ecosystems, emits one DRAFT per family, skips already-resolved slugs, and dates a DRAFT honestly: the mining date, with the git first-seen quoted as labeled evidence.
- Cross-language
depNames. Populate names where they are already at hand, offline: Pythonrequirements.txt(already read+split,:86) andpyproject.toml; Gogo.mod(the regex at:94already matches the require lines — capture the module path it currently discards). Mavenpom.xml/ CargoCargo.tomlfollow the same at-rest extraction. This revives the existingDEP_FAMILIESentries rather than adding names blindly. - Group by family, cap per family. Emit one DRAFT per
fam.family(e.g. "data store / ORM"), listing the member deps as evidence, instead of one per dep. Replace the global cap-8 with a per-family cap, so no single family drowns the set and no family is purged by manifest order. - Sober family extension. Extend
DEP_FAMILIESonly for structurally decisive families — auth, observability, IaC, payment — plus the cross-language names for the families that already exist. Do not add ubiquitous-but-low-signal families (test framework, validation): they are noise on the ratification gate. - Idempotence after resolution. Before emitting
DRAFT-<slug>.md, scanrunward/adr/for any of three durable, content-derived signals and skip the candidate: a ratifiedADR-NNNN-<slug>.mdfilename; an ADR carrying a**Draft-slug**: <slug>provenance line (every DRAFT ships one and the ratification instructions say to keep it — so ratifying under a different filename still resolves); or aDRAFT-<slug>.mdmarked**Status**: rejected. The rejected-in-place status is the durable "not a decision": deletion alone is not durable — the next--minewould re-propose the file and re-redden the gate, overriding the operator's call. No mutable ledger file (that would fight "same commit → same output"); theadr/content is the state. - Honest dating (fact vs hypothesis).
**Date**is the mining date for every DRAFT — the only date runward knows as a fact about the document it writes. The git first-seen is quoted in Evidence as a labeled trace ("first git trace …, not the decision date"): first-appearance-in-git is a fact about visible history (history imports, squashes, shallow clones and renames all shift it), and even a faithful history dates the file, never the decision — one decides before writing, or writes long after deciding. The ADR template's "records a decision at the moment it is taken" cannot be satisfied retroactively; pretending otherwise would cross the fact/hypothesis boundary this whole command is built on. The ratification instructions tell the operator to confirm or correct**Date**— the human is the only party who can know when the decision was actually taken. Keep the literal**Date**label (thestatus.tsdate regex depends on it).
Still deterministic, git-archaeology only, no model call (ADR-0014). DRAFTs remain Status: hypothesis, why: UNKNOWN, trigger-mandatory — nothing here loosens the gate.
Alternatives discarded
- Full manifest parsers for every ecosystem. Rejected: the names are already within reach from files
detectEcosystemsreads; a targeted extraction revives deadDEP_FAMILIESentries without vendoring parsers. - Broadly expand
DEP_FAMILIES. Rejected: more families = more DRAFTs to ratify; low-signal ones (test/lint/validation) add ratification noise without a structural decision behind them. Extend only where a real architectural choice lives. - A
.mined-ledgerfile to remember what was emitted. Rejected: mutable state in tension with the determinism invariant (same commit must give same output regardless of prior runs). The ratifiedadr/set is the durable, content-derived signal. - Keep one-DRAFT-per-dep. Rejected: it produces near-duplicate DRAFTs and lets the global cap purge whole families; per-family is both quieter and more complete.
Consequences
- Positive.
--minebecomes real on Python/Go/Java/Rust legacies — the actual brownfield target. Fewer, better DRAFTs (one per family). Re-running--mineafter ratification or rejection no longer re-reddens the gate. DRAFTs date themselves honestly (mining date as fact, git trace as labeled evidence) — the fact/hypothesis boundary holds even on the metadata. - Negative, accepted. Per-family grouping loses per-dep granularity in the DRAFT title (mitigated: members listed as evidence). Cross-language extraction is heuristic per format; a missed name degrades to "no family candidate", never a crash. The ratified-slug scan reads
adr/each run (cheap, deterministic). - On other boundaries.
EcosystemInfo.depNamespopulated for more ecosystems;mineDraftsregroups;renderDraftdates by first-seen;characterizecommand unchanged. The gate stays red until the operator ratifies — untouched.
Reevaluation trigger (mandatory, dated)
Reopen if (a) operators promote grouped DRAFTs unread (the ADR-0014 trigger — add per-ADR confirm friction); (b) a decisive family is still missed on real repos — extend DEP_FAMILIES (soberly); or (c) the ratified-slug skip proves too strict/too loose (skips a genuinely new decision, or resurrects one) — refine the slug-resolution match.
Trigger set on: 2026-07-20 · Watched via: field use of characterize --mine on non-Node repos; DRAFTs promoted without an operator-written why.
References
- ADR-0014 — the deterministic, no-model-call
--minecontract this stays within; its reopening trigger this serves. - ADR-0013 — the hypothesis→accepted lifecycle DRAFTs feed.
src/lib/characterize.ts(detectEcosystems,DEP_FAMILIES,mineDrafts,renderDraft,firstSeen) — edited.- The resume-existing / brownfield audit (2026-07-20).