Repository navigation
fix(generate): repair lossy regenerations and hold back the rules that still fail - #120
Conversation
…ill drop facts Auto-sync has failed every docs deploy since 2026-09-14 (#118): the fact-retention check rejected one or more lossy rules each run, and one rejected rule failed the whole sync, so no docs change has reached the published skills since. The generator now applies the validator's retention rule itself (moved to lib/retention.mjs so the two cannot disagree). A body that drops a fact the committed body carries and the docs still state, or misses a must_cover string, goes back to the model in the same conversation with the exact list, up to GENERATE_MAX_REPAIRS (default 2) times. A rule that still fails keeps its committed file — before AGENTS.md is assembled, so the round-trip check still holds — and is retried next run; the rest of the sync proceeds. Model errors and max_tokens truncation hold a rule back the same way. --report writes which rules were regenerated, repaired and held back. The sync PR body names held-back rules with what they still lacked, and repaired rules with what was restored. If rules were held back and nothing else changed, the run fails so the failure issue names them. The failure issue now has a stable title and gets a comment per failed run, quoting held-back rules and the validator output; the step runs last so it also covers the PR step. Pins the facts the chronic rules dropped most often across the 27 failed runs: querying-rest-apis (function signatures, the type-conversion table), v5-upgrade (instanceof, import(), logger) and automatic-apis (three Notes facts). Closes #118 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n open PR
Adopts the planning review's findings on the hold-back design:
- The generator now also rejects, repairs or holds back a body that
fails the structural checks the validators apply (H1, minimum length,
leaked MDX), moved to lib/body-checks.mjs and shared. A model response
carrying every fact but an MDX component no longer fails the sync.
- Holding back requires a committed body that passes the same checks;
otherwise the run fails with both reasons, since keeping it could not
produce a valid tree.
- bodyAtHead returns null only when HEAD has no such file, and throws on
any other git failure, so an unreadable baseline cannot switch
retention off in both the generator and the validator.
- With nothing new to commit, the workflow refreshes the held-back
section of an open sync PR's body (between markers, provenance left
alone) instead of failing; it fails only when no PR exists to name
the held-back rules.
- A failed issue lookup is reported as a warning before opening a new
issue, and the docs ref reaches the shell through env, not `${{ }}`.
- GENERATE_MAX_REPAIRS parsing and ruleConversation (via an injectable
createMessage) are unit-tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e splice From the first pre-push review round: - A rule that stops the generator (no committed file to keep, or one that fails the checks too) exits before --report is written, so the failure issue had nothing to say about it. The workflow now tees the generator's output and quotes its tail when that step fails, which also covers source-resolution errors and crashes. - spliceHeldBack matches only a complete marker pair with no other marker inside, so a marker deleted by hand cannot make a later splice swallow the text between a dangling marker and a new one. - The hold-back log line says "existing body": for an uncommitted local rule, the kept file is not a committed one. - Trims the new manifest and workflow comments to their reasons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the second pre-push review round: - A model, auth or truncation error stops the run again (exit 1) instead of holding the rule back. Holding back on an expired key would hold back every rule, and with an open sync PR the run would have refreshed its body and stayed green with nothing synced. - A rule is held back only if its existing file also passes the frontmatter reconciliation validate-generated applies, now shared as frontmatterProblems in lib/render.mjs. - Fatal exits throw StopRun; main() writes --report in a finally, so a run that stops still records what it held back, plus the rule and reason it stopped on. The failure issue renders both (sync-report --format failure-details, renamed from held-back). - bodyAtHead verifies the HEAD commit before reading `HEAD:<path>` (which also exits 1 for an unborn HEAD), and wraps every git failure with the baseline it was reading. - A backtick-wrapped anchor and the bare fact it pins are asked for and reported once. - An end-to-end test runs generate-rules.mjs and validate-generated.mjs in a scratch repository against a local Messages API stand-in. - Un-exports DEFAULT_MAX_REPAIRS, renames one-letter names, and drops comments that only narrated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A report write that fails in main()'s finally is logged instead of replacing the error that stopped the run. - bodyAtHead verifies the HEAD commit once per repository, not per rule. - The baseline tests use scratch repositories instead of the ambient checkout's HEAD. - An end-to-end case covers a hold-back refused because the existing file's frontmatter no longer reconciles with the manifest. - Renames the last one-letter callback names and drops the remaining issue-history comments from the scripts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a robust "repair-then-hold-back" loop for automated rule generation, ensuring that lossy or structurally invalid rule bodies do not block the entire documentation sync process. It extracts core validation logic (such as fact retention, structural checks, and frontmatter reconciliation) into reusable library modules (retention.mjs, body-checks.mjs, regenerate.mjs, and generation-report.mjs) shared between the generator and the validator. Additionally, it updates the sync report to detail held-back or repaired rules in PR bodies and failure issues, backed by comprehensive unit and end-to-end tests. Feedback suggests improving Git baseline error messages by appending err.stderr to the thrown errors when Git commands fail.
…readable bodyAtHead reported the spawn error's message, which wraps git's stderr in "Command failed: <command>". Report the stderr itself, falling back to the spawn error (e.g. ENOENT with no git), and name the unborn-HEAD case plainly. From review feedback on #120. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sync PR body was composed from a snapshot of which rules were stale before this run, so on the rolling auto/docs-sync branch it listed only the rules the latest run changed: a rule regenerated by an earlier run is fresh by then, and every `gh pr edit` replaced the body with the latest run's list. sync-report --format pr-body now derives the list from the branch: every rule file that differs from --base (origin/main), with the docs commit each was last synced from on that base, and the docs range since the oldest of those. Held-back rules come from this run's report; repairs from earlier runs ride along in a hidden, base64-encoded state comment in the body (--previous-body) and are merged forward. Both workflow paths now rebuild the whole body, so a run with nothing new to commit also repairs a body an earlier run failed to update. That replaces the held-back splice and its markers, and the pre-sync snapshot step is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of the provenance change: with the repair history in a hidden comment in the PR body, a run that pushed and then failed to update the body lost its repairs for good, because the next run skips the rules it already regenerated. Each sync commit now carries its run's repairs as `Sync-Repaired:` trailers (sync-report --format commit-trailers), and the PR body reads them back from the branch: the latest commit to touch a rule decides, so a later clean regeneration or hand edit drops an earlier repair. The body is now a function of the branch plus the run's held-back rules, with no hidden state, and --previous-body is gone. Also from the review: - A report that cannot be written fails a run that otherwise succeeded, instead of exiting 0 without it. - Rule files deleted on the branch are listed as deleted, and added ones as new, instead of being dropped. - Trims the remaining narration and issue citations from comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rule that git reports as modified or deleted against --base can still be missing at that ref when --base is not the branch's merge base (only a local run can do that). Treat it as having no recorded baseline, as before, instead of crashing pr-body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ethan-Arrowood
left a comment
There was a problem hiding this comment.
LGTM - dispatch review is complaining about some repo inconsistencies but I disagree with them (like it claims everything should be TS even though we have plenty of existing ESM JS). It found no correctness issues and considering the size of this change and flexibility of this package, I'm comfortable with you merging and us fixing forward anything else that is out of place.
🤖 Reviewed with Codex
From review on #120: keep only the non-obvious why, constraint or invariant in the new comments (the truncated-response rejection, the repair prompt's no-invention guarantee, the trailer record surviving a failed body update), and move bodyAtHead's comment back onto it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…readable bodyAtHead reported the spawn error's message, which wraps git's stderr in "Command failed: <command>". Report the stderr itself, falling back to the spawn error (e.g. ENOENT with no git), and name the unborn-HEAD case plainly. From review feedback on #120. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🎉 This PR is included in version 1.14.1 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
One rule whose regeneration fails its checks no longer fails the whole docs auto-sync. The generator now runs the validators’ checks on each regenerated rule and asks the model to fix exactly what is wrong, up to two times. Any rule that still fails is held back at its committed version, so the sync PR opens with everything else and names the held-back rules. The sync PR body is also rebuilt from the branch on every run, so it lists every rule the PR changes, not only the latest run's, and repairs are recorded in the sync commits. Failures go to one stable issue instead of one issue per docs SHA, and the three chronically lossy rules get
must_coverpins. Closes #118.❓ Your call: post-review delta, not re-reviewed. It has two parts.
627b190adds 6 lines and removes 2 insync-report.mjs: a fallback to "no recorded baseline" when--baseis not where the branch diverged, from the last round's nits.e6e066dtrims comments only, from Ethan's review.For the human reviewer
Planning review:
better-alternative-exists, adopted. The review's alternative: when a run has nothing new to commit but an open sync PR exists, refresh that PR's body instead of failing the run. That became the no-diff refresh, widened in entry 9. The review's other findings were adopted too:env.Overruled, with the deciding fact for each:
scripts/is.mjs, run directly bynodefrompackage.jsonand the workflow.must_cover) instead of a conversation. The 27 failed runs were 27 fresh samples, andquerying-rest-apisfailed every one. It lost at least 18 distinct facts at about 9 per run, a different subset each time, so a fresh retry trades one set of drops for another. A repair edits the draft that already kept the rest. Nothing measures the real model's repair success yet (see Verification).The requirement, as filed. Auto-sync has failed on every docs deploy since 2026-09-14: regeneration keeps dropping facts #118 asks for four things: repair, isolation, pins for the chronic rules, and a single failure issue. Its "Done when" is met by those four, and all four are implemented except one sub-item:
querying-rest-apisis not split. Splitting is a taxonomy change that needs a real-model regeneration to judge, and the pins plus repair target the drops measured in CI. Entry 9's PR-provenance fix began as a separate follow-up task. It is folded in here because it replaces this PR's earlier held-back splice, and because this repo runs CI only on PRs intomain. No other open PR touches the pipeline, and noauto/docs-syncPR is open, so the first run after merge starts clean frommain.Held back with nothing to commit and no open sync PR fails the run, so the failure issue names the rules. The alternative was a green no-op run, which would let a chronically lossy rule stay stale without anyone being told. The fix is cheap to reverse. Cost of "no": held-back rules would show only in the job log.
A model error still stops the run. This applies to API, auth, and
max_tokenstruncation errors alike, and is the same as before except that the report now names the rule. Holding back on these was the first draft. The review showed the problem: an expired key would hold back every rule, and with an open sync PR the run would refresh its body and stay green with nothing synced. Only check failures are held back. Truncation is new as an error: before, a cut-off body was written like any other.The repair budget defaults to 2 (
GENERATE_MAX_REPAIRS), so a rule costs at most 3 model calls. Each repair continues the same conversation and gets the whole body back.17
must_coveranchors added; 3 of them replace the bareselect(/sort(/limit(. Each was dropped in at least 7 of the 27 failed runs, and each is in both the current docs and the committed body.instanceof,loggerandimport()are generic tokens. But each is exactly the fact the retention check enforces, and each occurs once in its source (import()twice), so a passing mention is the fact. If the docs later drop one, that rule is held back and named instead of failing every sync.The failure issue is never closed automatically. A successful sync leaves it open for a human to close, as before.
Two stops kept on purpose.
validate-generatedcheck already refuses.The sync PR body is rebuilt from the branch on every run. Before, it was composed from a snapshot of what was stale before the latest run. So on the rolling branch it listed only that run's rules, and every update replaced the list: given run 2's snapshot, the old code lists only
bwhen the PR also changesa.main, withmain's recorded baseline for each. Human review commits on the branch are included, and so are added and deleted files.Sync-Repaired:trailers on the sync commit, written before the push, and read back from the branch. The latest commit to touch a rule decides. An earlier draft kept them in a hidden comment in the PR body. The review showed that a run which pushed and then failed to update the body lost them for good.maindrops it from the list anyway.Changes
lib/retention.mjs(new): the fact-retention definition, moved out ofvalidate-generated.mjsso the generator and the validator cannot disagree. The semantics are the same, with one exception:bodyAtHeadnow returnsnullonly for a path absent from a real HEAD commit, and throws on any other git failure (an unborn HEAD included) with git's own message.anchorsBeyondFactslets a backtick-wrapped anchor and the bare fact it pins count once.lib/body-checks.mjs(new): the H1 / minimum-length / leaked-MDX checks, shared withvalidate-generated.mjs. The H1 condition mirrorsvalidate-skills.mjs.lib/regenerate.mjs(new):checkBody(every check the validators apply to a generated body), the repair loopregenerateFaithfully, andparseMaxRepairs.lib/llm.mjs:generateRuleBodybecomesruleConversation, withgenerate()andrepair()over one message history and an injectablecreateMessagefor tests. The repair prompt quotes the committed line each dropped fact came from, asks for an anchor only once when it is also a dropped fact, and lists structural problems. Amax_tokensstop is now an error.generate-rules.mjs:validate-generatedchecks:checkBodyand frontmatter reconciliation. Otherwise, or for a rule with no file, or on a model error, the run stops.StopRunthrows.main()writes--reportwith the rule and reason it stopped on. A report that cannot be written fails a run that otherwise succeeded.run(), unindented.--reportandGENERATE_MAX_REPAIRS.lib/render.mjs:frontmatterProblems, the frontmatter reconciliation moved out ofvalidate-generated.mjswith identical messages.lib/generation-report.mjs(new):Sync-Repaired:trailers.sync-report.mjs:--format pr-bodynow derives the changed rules and the repairs from the branch against--base;--format failure-detailsand--format commit-trailers;--fromand the pre-sync snapshot it read.validate-generated.mjs: imports the shared definitions (retention, body checks, frontmatter reconciliation); behavior is unchanged apart from the stricter baseline read.generate.yaml:--report, and tees the generator and Validate output;docs_refand payload refs to the shell throughenv.rules.manifest.yaml: pins for three rules.querying-rest-apis: function signatures replace the bareselect(/sort(/limit(, plus one anchor per type-conversion row.v5-upgrade:instanceof,import(),logger.automatic-apis:Sec-WebSocket-Protocol: mqtt,@hidden,static hidden = true.CONTRIBUTING.MD: documents the checks, repair and hold-back, how the PR body is rebuilt, and the stable issue, what to do about a held-back or repaired rule, and who owns what. It also drops the pre-sync snapshot step from the pipeline description.Verification
Route (c): a live run through the production entry points, with the model replaced at the HTTP boundary. A local fake
/v1/messagesserver (ANTHROPIC_BASE_URL) replays each committed rule body with facts removed:querying-rest-apis: drops thename==/sort(lines, on the first call only.automatic-apis: rewordsSec-WebSocket-Protocol: mqtt, on the first call only.v5-upgrade: dropsinstanceof, on every call.The docs were a fresh build of
HarperFast/documentation@2cfb813(docsmain, 2026-10-06).origin/main(de2c13a):generate-rules.mjswrites all 9 stale rules.validate-generated.mjs --docs-paththen fails with CI's signature: 4 errors, includingquerying-rest-apis: regeneration dropped 10 facts still documented in its sources. The whole sync is refused.automatic-apisis repaired in 1 pass.querying-rest-apisis repaired in 1 pass, restoring 10 facts.v5-upgradeis held back after 2 repairs.validate-generated --docs-pathandnpm run validatepass.v5-upgradeis retried (held back again), 21 rules are unchanged, and there is no diff.Prefer: count=exact) that the committedquerying-rest-apisbody lacks. The run exits 1, reporting that the existing body cannot be kept instead (missing must_cover "Prefer: count=exact"), and nothing is written.aalready fresh,bstale), the oldsync-report --format pr-body --fromlists onlyb.run:blocks, extracted and run against a stubgh:gh pr edit --body-file, and a second identical run makes no edit.::error::.Sync-Repaired:trailer, it pushes, andgh pr create --body-filegets a body listing the changed rule and its repair.✗line.workflow_dispatchrun right after merge would show it.generate-rules.test.mjs(new): an end-to-end test of the realgenerate-rules.mjsin a scratch git repository, against a local Messages API stand-in.sourceCommit, the report lists each, andvalidate-generated --docs-pathpasses on the result.stoppedOnthe second.cannot be kept instead: … "description" diverges, and holds nothing back.--reportpath fails a run that otherwise succeeded.sync-report.test.mjs(new):--format pr-bodyon a scratch sync branch with two runs of commits.main's baselines, and the docs range starts at the oldest baseline.npm test: 86 pass.retention.test.mjs: legacy semantics, plus the strict baseline in scratch repositories: the committed body and not the working tree,nullonly for a missing path, and a throw with git's message outside a repository or before the first commit.body-checks.test.mjs: structural checks.regenerate.test.mjs: the repair loop's accept, repair, hold-back, new-drop, anchor, structural, waiver, error, and budget paths.llm.test.mjs: the prompts, the conversation's message history, and the truncation/empty errors.generation-report.test.mjs: rendering, failure details, andSync-Repaired:trailers.Related PRs: #119 independent, #98 independent, #26 independent
Complexity: medium
Review-Coverage: authored=claude; ran=gemini,cursor-muse,codex; adjudicated=domain; blocked=cursor-grok(no-receipt),claude(fallback)(out-of-budget),cursor-composer(out-of-budget); declined=cursor-kimi; rounds=5; full=1 @ e6e066d
Review-Attention: study ~11m (decisions: repair-record-in-commit-trailers, later-touch-clears-repair, provenance-from-git-diff, stop-on-any-model-error, stop-on-frontmatter-drift, single-rolling-failure-issue, green-refresh-when-held-back-with-open-pr, conversational-repair, do-less-alternative) @ e6e066d