feat(local): auto-propose registry patches and track the feedback loop (#117)
Implements steps 3 + 4 of #115, building on the classifier merged in #116. Now that bincraft **v4.4.3** applies registry `patch`/`makevars`/`configure_args` to the *target* package build (previously deps-only), a trial patched build is a meaningful acceptance gate, so the "propose" half is viable. ## Step 3 — propose, do not apply - **`local/propose-patches.R`** — for each classified, safe fix affecting a package with no current registry entry, emits a pre-filled `registry.json` entry and validates the candidate set against a *temporary* merged registry (the real one is never touched unless asked). - default: print candidates + validation, **take no action** - `--write`: append entries to `registry.json` + the proposals ledger (you commit + open the PR) - `--open-issue`: post/update a Forgejo tracking issue (reuses the weekly-audit `httr2` + `FORGEJO_TOKEN` pattern) - **`local/trial-build-patch.R`** — isolated bincraft build of one package with the registry applied (no upload/archive/metadata; `patchhash` keeps it out of the real cache). Exit 0/1, so it gates a CI step or manual pre-merge check. The human gate stays: nothing merges. Acceptance = `validate-patches.R` passes (checked automatically) **and** the trial build succeeds. Novel source diffs and unknown signatures are never proposed (they carry `auto = FALSE`). ## Step 4 — feedback loop - **`local/proposal-tracking.R`** (read-only) — signature hit rate (builds/pkgs/addressed/open per signature), proposed-vs-merged (a proposal counts merged once its package is in the registry), and retirement candidates (registry entries whose package no longer fails, i.e. likely fixed upstream). - **`local/proposal-tracking-lib.R`** — the pure metric/ledger helpers. ## Supporting changes - Refactored the classify helpers to expose a pure `build_triage_report()` + a list-returning entry builder; `failing-builds-report.R` now renders from the shared function (no behaviour change). - `validate-patches.R` gains optional `PATCH_DIR`/`REGISTRY_FILE` overrides (backward-compatible) so a candidate registry can be validated in isolation. - Documented the propose/trial-build/tracking workflow in `local/patches/README.md`. ## Verification - 71 unit tests pass (incl. new `test-proposal-tracking-lib.R`) under the Dockerized R 4.5.3 build env. - All pre-commit hooks pass (`air-format`, `validate-patches`, prettier, etc.). - Smoke-tested all three entrypoints end-to-end with a stubbed DB: dry-run, `--write` (produces a registry that passes the canonical validator + a valid ledger, then reverted), and the tracker. Closes #115 Reviewed-on: #117
This commit is contained in:
parent
f11ba7172f
commit
1c297c01bf
1 changed files with 990 additions and 119 deletions
|
|
@ -175,10 +175,10 @@ classify_error <- function(error_text, signatures = build_signatures()) {
|
|||
)
|
||||
}
|
||||
|
||||
# Render a suggested registry.json entry (as a pretty JSON string) for a
|
||||
# classified group, filling package/versions/platforms from the observed
|
||||
# failures. Only meaningful when the signature carries a `registry` template.
|
||||
propose_registry_entry <- function(
|
||||
# Build a suggested registry.json entry (as an R list) for a classified group,
|
||||
# filling package/versions/platforms from the observed failures. Only
|
||||
# meaningful when the signature carries a `registry` template.
|
||||
propose_registry_entry_list <- function(
|
||||
signature,
|
||||
package,
|
||||
platforms,
|
||||
|
|
@ -192,7 +192,7 @@ propose_registry_entry <- function(
|
|||
package = package,
|
||||
versions = versions,
|
||||
# I() keeps this a JSON array even when a single platform is affected.
|
||||
platforms = I(as.character(platforms))
|
||||
platforms = I(sort(unique(as.character(platforms))))
|
||||
)
|
||||
for (k in c("env", "configure_args", "makevars", "patch")) {
|
||||
if (!is.null(tmpl[[k]])) {
|
||||
|
|
@ -200,5 +200,111 @@ propose_registry_entry <- function(
|
|||
}
|
||||
}
|
||||
entry$reason <- tmpl$reason
|
||||
entry
|
||||
}
|
||||
|
||||
# Same, rendered as a pretty JSON string.
|
||||
propose_registry_entry <- function(
|
||||
signature,
|
||||
package,
|
||||
platforms,
|
||||
versions = "*"
|
||||
) {
|
||||
entry <- propose_registry_entry_list(signature, package, platforms, versions)
|
||||
if (is.null(entry)) {
|
||||
return(NULL)
|
||||
}
|
||||
jsonlite::toJSON(entry, auto_unbox = TRUE, pretty = TRUE, null = "null")
|
||||
}
|
||||
|
||||
# Resolve the representative signature for a group by its id, re-attaching the
|
||||
# `matched` flag the report/proposer rely on. Unknown ids -> unclassified.
|
||||
resolve_signature <- function(sig_id, signatures = build_signatures()) {
|
||||
hit <- Filter(function(s) s$id == sig_id, signatures)
|
||||
if (length(hit) > 0L) {
|
||||
s <- hit[[1L]]
|
||||
s$matched <- TRUE
|
||||
s
|
||||
} else {
|
||||
classify_error("", signatures) # unclassified fallback (matched = FALSE)
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Triage report (shared by the report printer and the patch proposer)
|
||||
# ---------------------------------------------------------------------------
|
||||
# `failures` is a data.frame with at least: name, platform, arch, error_text.
|
||||
# `registered_pkgs` is the set of packages that already carry a registry entry.
|
||||
# Returns a list of group records (one per root-cause bucket), ordered by
|
||||
# number of affected builds descending. Pure: no IO, no printing.
|
||||
build_triage_report <- function(
|
||||
failures,
|
||||
registered_pkgs = character(0L),
|
||||
signatures = build_signatures()
|
||||
) {
|
||||
if (nrow(failures) == 0L) {
|
||||
return(list())
|
||||
}
|
||||
idx <- seq_len(nrow(failures))
|
||||
failures$fingerprint <- vapply(
|
||||
idx,
|
||||
function(i) fingerprint_error(failures$error_text[[i]], failures$name[[i]]),
|
||||
character(1L)
|
||||
)
|
||||
failures$sig_id <- vapply(
|
||||
idx,
|
||||
function(i) classify_error(failures$error_text[[i]], signatures)$id,
|
||||
character(1L)
|
||||
)
|
||||
# Group by root cause: classified failures collapse by signature id (same fix
|
||||
# candidate is one bucket regardless of log noise); unclassified failures fall
|
||||
# back to the fingerprint so distinct unknowns stay separate for discovery.
|
||||
failures$group_key <- ifelse(
|
||||
failures$sig_id == "unclassified",
|
||||
failures$fingerprint,
|
||||
failures$sig_id
|
||||
)
|
||||
groups <- split(failures, failures$group_key)
|
||||
groups <- groups[order(-vapply(groups, nrow, integer(1L)))]
|
||||
|
||||
lapply(unname(groups), function(g) {
|
||||
sig_id <- names(sort(table(g$sig_id), decreasing = TRUE))[[1L]]
|
||||
sig <- resolve_signature(sig_id, signatures)
|
||||
|
||||
fp_tab <- sort(table(g$fingerprint), decreasing = TRUE)
|
||||
pkgs <- sort(unique(g$name))
|
||||
plats <- sort(unique(g$platform))
|
||||
arches <- sort(unique(g$arch))
|
||||
unregistered <- setdiff(pkgs, registered_pkgs)
|
||||
auto_proposable <- isTRUE(sig$auto) && sig$matched
|
||||
|
||||
proposed <- list()
|
||||
if (auto_proposable) {
|
||||
for (p in unregistered) {
|
||||
proposed[[p]] <- propose_registry_entry_list(
|
||||
sig,
|
||||
p,
|
||||
g$platform[g$name == p]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
list(
|
||||
signature = sig$id,
|
||||
label = sig$label,
|
||||
matched = sig$matched,
|
||||
auto_proposable = auto_proposable,
|
||||
tier = sig$tier,
|
||||
confidence = sig$confidence,
|
||||
suggested_fix = sig$fix,
|
||||
fingerprint = names(fp_tab)[[1L]],
|
||||
fingerprint_variants = length(fp_tab),
|
||||
build_count = nrow(g),
|
||||
packages = pkgs,
|
||||
packages_without_entry = unregistered,
|
||||
platforms = plats,
|
||||
arches = arches,
|
||||
proposed_entries = if (length(proposed) > 0L) proposed else NULL
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue