Skip to content

fix(generate): repair lossy regenerations and hold back the rules that still fail - #120

Merged
dawsontoth merged 10 commits into
mainfrom
fix/auto-sync-hold-back-lossy-rules
Oct 7, 2026
Merged

dawsontoth merged 10 commits into
mainfrom
fix/auto-sync-hold-back-lossy-rules

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

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_cover pins. Closes #118.

❓ Your call: post-review delta, not re-reviewed. It has two parts. 627b190 adds 6 lines and removes 2 in sync-report.mjs: a fallback to "no recorded baseline" when --base is not where the branch diverged, from the last round's nits. e6e066d trims comments only, from Ethan's review.

For the human reviewer

  1. 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:

    • The generator now applies the structural checks (H1, minimum length, leaked MDX).
    • A rule is held back only when its committed body passes the same checks.
    • The HEAD baseline is strict: only a missing path counts as "no baseline".
    • A failed issue lookup is logged.
    • The docs ref reaches the shell through env.

    Overruled, with the deciding fact for each:

    • TypeScript for the new modules. Every script under scripts/ is .mjs, run directly by node from package.json and the workflow.
    • Retrying with a fresh call (plain, or with an augmented must_cover) instead of a conversation. The 27 failed runs were 27 fresh samples, and querying-rest-apis failed 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).
    • Not opening an issue when the lookup fails. Skipping would leave the failure with no trace, which is what the 2026-09-07 run left. A duplicate issue is one click to close.
  2. 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-apis is 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 into main. No other open PR touches the pipeline, and no auto/docs-sync PR is open, so the first run after merge starts clean from main.

  3. 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.

  4. A model error still stops the run. This applies to API, auth, and max_tokens truncation 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.

  5. 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.

  6. 17 must_cover anchors added; 3 of them replace the bare select(/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, logger and import() 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.

  7. The failure issue is never closed automatically. A successful sync leaves it open for a human to close, as before.

  8. Two stops kept on purpose.

    • Any model error stops the run, including a 529 that outlasts the SDK's retries. That blocks every rule, as it did before this PR, but an auth failure can no longer go green.
    • A hold-back is refused when the rule's frontmatter no longer reconciles with the manifest. That state can only arise if a manifest edit lands without its regenerated rule, which the PR-time validate-generated check already refuses.
  9. 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 b when the PR also changes a.

    • It now lists every rule file the branch changes against main, with main's recorded baseline for each. Human review commits on the branch are included, and so are added and deleted files.
    • Repairs are recorded as 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.
    • The whole body is overwritten on every run, including runs with nothing new to commit, so hand edits to it don't survive. Before, they were already overwritten on every run that committed.
    • A merge commit does not clear a rule's repair record. The record is advisory, and a merge that makes the rule match main drops it from the list anyway.

Changes

Verification

Route (c): a live run through the production entry points, with the model replaced at the HTTP boundary. A local fake /v1/messages server (ANTHROPIC_BASE_URL) replays each committed rule body with facts removed:

  • querying-rest-apis: drops the name==/sort( lines, on the first call only.
  • automatic-apis: rewords Sec-WebSocket-Protocol: mqtt, on the first call only.
  • v5-upgrade: drops instanceof, on every call.

The docs were a fresh build of HarperFast/documentation@2cfb813 (docs main, 2026-10-06).

  • Base origin/main (de2c13a): generate-rules.mjs writes all 9 stale rules. validate-generated.mjs --docs-path then fails with CI's signature: 4 errors, including querying-rest-apis: regeneration dropped 10 facts still documented in its sources. The whole sync is refused.
  • This branch, run 1:
    • automatic-apis is repaired in 1 pass.
    • querying-rest-apis is repaired in 1 pass, restoring 10 facts.
    • v5-upgrade is held back after 2 repairs.
    • Result: 8 regenerated and 1 held back. validate-generated --docs-path and npm run validate pass.
  • This branch, run 2, on top of run 1's commit: only v5-upgrade is retried (held back again), 21 rules are unchanged, and there is no diff.
  • Ineligible fallback: I added an anchor (Prefer: count=exact) that the committed querying-rest-apis body lacks. The run exits 1, reporting that the existing body cannot be kept instead (missing must_cover "Prefer: count=exact"), and nothing is written.
  • Old PR-body composer: given run 2's pre-generation snapshot (a already fresh, b stale), the old sync-report --format pr-body --from lists only b.
  • Workflow bash: the changed run: blocks, extracted and run against a stub gh:
    • No diff, open PR: the rebuilt body is posted via gh pr edit --body-file, and a second identical run makes no edit.
    • No diff and no open PR: exits 1 with the ::error::.
    • PR lookup fails: exits 1.
    • Commit path, against a scratch bare remote: the sync commit carries the Sync-Repaired: trailer, it pushes, and gh pr create --body-file gets a body listing the changed rule and its repair.
    • Failure issue already open: one REST comment carrying the details and the log tail.
    • No failure issue: creates it and sets the type.
    • Issue lookup fails: a warning, then a new issue.
    • Generator stops on a rule: the comment quotes its ✗ line.
  • Not exercised: how well the real model repairs. No API key was available locally, so the first CI run after merge is the first real repair. A manual workflow_dispatch run right after merge would show it.
  • generate-rules.test.mjs (new): an end-to-end test of the real generate-rules.mjs in a scratch git repository, against a local Messages API stand-in.
    • First case: one rule stays lossy through 2 repairs, and another is repaired. The lossy rule's file is byte-identical afterwards, the repaired one carries the new sourceCommit, the report lists each, and validate-generated --docs-path passes on the result.
    • Second case: an HTTP 400 on the second rule exits 1, and the report still records the first rule as held back and stoppedOn the second.
    • Third case: a manifest whose description moved on without the rule file stops the run with cannot be kept instead: … "description" diverges, and holds nothing back.
    • Fourth case: an unwritable --report path fails a run that otherwise succeeded.
  • sync-report.test.mjs (new): --format pr-body on a scratch sync branch with two runs of commits.
  • npm test: 86 pass.
  • All 17 added anchors were checked against the current docs sources and the committed bodies; each is present in both.

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

dawsontoth and others added 5 commits October 6, 2026 13:02
…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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/generation/lib/retention.mjs Outdated
dawsontoth and others added 4 commits October 6, 2026 14:37
…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>
@dawsontoth
dawsontoth marked this pull request as ready for review October 7, 2026 15:33
@dawsontoth
dawsontoth requested a review from a team as a code owner October 7, 2026 15:33

@Ethan-Arrowood Ethan-Arrowood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread scripts/generation/lib/regenerate.mjs
Comment thread scripts/generation/lib/llm.mjs Outdated
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>
@dawsontoth
dawsontoth merged commit a06f489 into main Oct 7, 2026
3 checks passed
dawsontoth added a commit that referenced this pull request Oct 7, 2026
…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>
@dawsontoth
dawsontoth deleted the fix/auto-sync-hold-back-lossy-rules branch October 7, 2026 19:16
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.14.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-sync has failed on every docs deploy since 2026-09-14: regeneration keeps dropping facts

2 participants