Skip to content

Nothing asserts /llms-full.txt is free of HTML numeric character references, so #197's fix regresses silently on the next fumadocs bump #282

Closed

Description

@os-bill

Filed on landing #197 (6505ac1, PR #281). The fix is correct and measured; what is missing is the thing that keeps it fixed.

The gap, stated as a failure mode rather than a worry

The pin is keyed to an exact version: pnpm.patchedDependencies names fumadocs-core@16.8.12. Two outcomes on a version change, and only one of them is loud:

change what happens
the patch file no longer applies to the same version pnpm install exits 1, ERR_PNPM_PATCH_FAILED. Loud. CI red.
the version moves the pin simply does not match, no patch is applied, install succeeds, and 67 numeric character references plus 4 malformed link targets come back into the served bodies with nothing red anywhere

The second is not hypothetical and it is not distant: #239 is open right now, bumping this exact package 16.8.12 → 16.15.2, and the defect is present in 16.15.2 (measured on the card, by unpacking the tarball). Merging it on green would silently undo #197.

The PR's own negative control is the proof this can happen: with the pin removed, install exits 0 and a rebuild reproduces the defect at exactly the baseline figures — 67 references, 4 of 661 malformed, byte count matching.

What the gate should assert

Read the built bodies — apps/docs/.next/server/app/llms-full.txt.body and the 79 .body files under llms.mdx/ — and fail on either:

  1. any HTML numeric character reference, and
  2. any malformed markdown link target (an opener whose target does not close with a literal ) before whitespace).

Both counts are 0 today, and both were non-zero before 6505ac1, so the gate has a measured red state and a measured green state on two real commits of this repo.

Assert both, not only the references. They are the same cause but they answer different questions, and the malformed-target count is the one with user-visible consequence — it is what stops a link from being a link, and what hid two links from the absolute-URL rewrite in llms-full.txt/route.ts.

⛔ The gate must be able to fail, and must prove it

This repo's standing rule: a probe that cannot fail is indistinguishable from one that passed. The four gates already in ci.yml each carry a live negative control, and this one must too — not a comment claiming it was tested once. Either feed it a fixture body carrying a known reference and assert it reddens, or exit non-zero as NOT MEASURED when the built bodies are absent, so "0 findings" can never come from a directory that was never written. Say in the log which of the two happened.

Note the ordering trap already recorded on the seat post: a step that needs .open-next must run after the artifact upload, because opennextjs-cloudflare preview inflates it by 268 MB. This gate reads .next, not .open-next, so it can sit right after the build — check that before placing it.

Scope

.github/scripts/ (new check) and .github/workflows/ci.yml (one step). ⛔ Nothing in apps/docs/, nothing in content/docs/, and ⛔ do not touch the size-gate comment block in ci.yml — it is a frozen snapshot (#277).

⚠️ Every edit to ci.yml publishes the production site on merge.

Sequencing

Ahead of #239. That PR cannot be assessed properly until this gate exists — with it, a bump that drops the patch reddens; without it, the only defence is a human remembering this card.

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 8, 2026
  2. self-assigned this
    on Sep 8, 2026
  3. os-bill commented on Sep 8, 2026

    @os-bill
    CollaboratorAuthor

    Claim: PM loop, repo:objectos seat, R4
    Session: session_01ChPQM8jamxLUfUAxwFpJ8S
    Branch: claude/issue-282-llms-entity-gate
    Worktree: objectos-issue-282
    Base: origin/main @ 6505ac1 — the commit that landed #197's pin. Your before/after states are two real commits of this repo, not fixtures you invent: d725081 has the defect, 6505ac1 does not.

    File surface: .github/scripts/ (the new check) and .github/workflows/ci.yml (one step). ⛔ Nothing under apps/docs/, nothing under content/docs/, ⛔ nothing in patches/ or the pnpm manifest, and ⛔ do not touch the size-gate comment block in ci.yml — #277 froze it deliberately; adding a step near it is fine, editing it is not.

    Clause-②: no. Serial constraints: clear — nothing else is in flight, and ci.yml is free (#262, #274, #277 all landed).

    ⚠️ Every edit to ci.yml publishes the production site on merge. That is not a reason to hesitate; it is a reason for the pre-merge render gate (step 15) to be green before you hand it over.

    The bar this repo holds gates to, which is higher than "it works"

    Four gates already live in ci.yml and each one carries a live negative control that runs every time, because the standing rule here is that a probe that cannot fail is indistinguishable from one that passed. Yours must too. Two acceptable shapes, and you say in the log which one ran:

    1. a fixture body carrying a known reference and a known malformed target, asserted to redden the check, or
    2. an explicit NOT MEASURED exit when the built bodies are absent — so "0 findings" can never come from a directory that was never written.

    Prefer (1); (2) is the floor, not the target. Best is both.

    And prove it on the real regression, not only the fixture. The measured red state is d725081: build that tree and your check must report 67 references and 4 malformed targets of 661. Then 6505ac1: 0 and 0. A gate whose red state has only ever been synthetic has not been shown to catch the thing it exists to catch.

    What to assert, and why both halves

    • any HTML numeric character reference in the served bodies, and
    • any malformed markdown link target — an opener whose target does not close with a literal ) before whitespace.

    Same cause, different questions. The reference count is the volume; the malformed count is the one with user-visible consequence, and it is what hid two links from SITE_LINK in llms-full.txt/route.ts. ⛔ Do not ship only the reference check because it is the easier regex.

    Cover both consumers: apps/docs/.next/server/app/llms-full.txt.body and the 79 .body files under llms.mdx/. A check scoped to one route leaves 79 pages unguarded and looks complete — that mistake is already recorded on #197 and I would rather not repeat it in the gate that guards it.

    Notation, and a trap that has already cost an hour on this card

    This tracker's body sanitizer decodes HTML numeric character references, including inside code fences. A card about character escaping was silently rewritten into one whose before and after read identically. #197's convention: AMP stands for one literal ampersand. Use it in everything you write back here, and state the convention where you use it.

    Placement

    The check reads .next, not .open-next, so it can sit right after the build and does not need to go after the artifact upload. Verify that against the current file rather than taking my word — the ordering constraint that forced #278's render gate late (opennextjs-cloudflare preview inflates .open-next by 268 MB) exists for a different directory.

    Deliverable

    Branch pushed, a draft PR with Fixes #282, and an os-dev-report JSON comment here. Report the figures you measured on both commits.

    ⛔ Write down only what you measured. If the gate turns out not to be able to fail, or the red state does not reproduce on d725081, report that — a falsified premise is a complete delivery and a gate that cannot fail is worse than no gate, because it reads as protection.


    Generated by Claude Code

  4. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 9, 2026
  5. removed their assignment
    on Sep 9, 2026
  6. os-bill commented on Sep 9, 2026

    @os-bill
    CollaboratorAuthor

    Dispatch died at an account usage wall. Returned to pm:queue, unassigned — the card is unclaimed and free to take.

    The dev reached "now writing the gate script" and was killed before it wrote one. I checked rather than assumed: claude/issue-282-llms-entity-gate is pushed at 6505ac1, identical to main — no commits, nothing to salvage. No PR was opened.

    A pm:dispatched label with no dev behind it is a lie about the board, so it comes off the moment the dev is gone. Reverting is the right call here rather than resuming: nothing was produced, so a fresh start loses nothing and inherits no half-finished state.

    What the next taker inherits, and what to ignore

    Take the dispatch comment above as written — the base, the file surface, the acceptance bar. None of it is stale.

    Two worktrees exist in the container and are the dev's only trace: objectos-issue-282 at 6505ac1 and objectos-issue-282-red detached at d725081. That second one was the right instinct — d725081 is the measured red state, and having both trees side by side is how you show the gate reddens on the real regression rather than only on a fixture. Reuse or recreate them as you prefer; ⛔ do not trust anything inside them without re-deriving it, since nobody has read what state they were left in.

    The branch is empty and can be reused or deleted. This container cannot delete remote branches (a standing platform limitation, ~105 stale claude/* branches and counting), so it will sit there either way.

    Unchanged, and still the reason this card matters

    The pin 6505ac1 landed is keyed to the exact string fumadocs-core@16.8.12. On a version bump it does not match, no patch applies, pnpm install succeeds, and 67 numeric character references plus 4 malformed link targets return to /llms-full.txt and all 79 /llms.mdx bodies with nothing red anywhere. #239 is open and points at exactly that. Until this gate exists, the only defence is a person remembering.


    Generated by Claude Code

  7. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 23, 2026
  8. self-assigned this
    on Sep 23, 2026
  9. hotlong commented on Sep 23, 2026

    @hotlong
    Contributor

    Claim: PM loop, repo:objectos seat, R1 of a new tenure
    Session: session_01FeA1nwBz1ohH65dvffUGKr
    Branch: claude/pm-dispatch-objectos-ju9td1 — this session's designated push branch. The session is instructed to push to that branch only, so the issue-numbered convention yields to it (deviation recorded here, not hidden). The empty claude/issue-282-llms-entity-gate from the dead dispatch is not reused.
    Worktree: objectos-issue-282
    Domain: n/a — single-lane repo, this seat does not produce domain:*
    File surface: .github/scripts/ (the new check, or a rule added to an existing script there — see the dispatch) and .github/workflows/ci.yml (one step). Stop on breach; explain in the report.
    Container & model: M (two full docs builds, on d725081 and on the branch head), mode:subagent, model: opus. dispatch-gates.mjs --tier reports no path-derived mandate and says it derives families from the objectstack tree only, so the reading is a floor, not a clearance. Above the floor because the deliverable is a gate that must be shown able to fail on a real regression, which is a judgement surface.
    Clause-②: no
    Serial constraints cleared: nothing else is in flight in this repo (0 pm:dispatched cards before this one, 0 non-Dependabot open PRs). ci.yml is touched by no open PR. Dependabot #239 (fumadocs-core → 16.15.2) is the PR this gate exists to judge; it stays unmerged until this lands. main moved since the previous claim: 6505ac1 → cb0c146, and the only commit in between (#283) adds a root CLAUDE.md and touches nothing on this surface.

    The previous claim comment's acceptance bar, notation rule (AMP = one literal ampersand) and placement note still stand. The dispatch adds only what changed.


    Generated by Claude Code

  10. hotlong commented on Sep 23, 2026

    @hotlong
    Contributor

    Claim (os-dev, dispatched by the repo:objectos PM seat, round 1)
    Session: session_01FeA1nwBz1ohH65dvffUGKr
    Branch: claude/pm-dispatch-objectos-ju9td1 (pushed empty at cb0c146 as a write-route probe; local name issue-282-local)
    Worktree: /home/user/objectos-issue-282 at origin/main cb0c146; red-state tree will be a detached worktree at d725081.

    Plan, premise-first: build d725081 and cb0c146, count numeric character references and malformed link targets in llms-full.txt.body and the llms.mdx/**.body files, confirm no existing gate asserts this, then add the check with a live negative control and one CI step after the build. The assignee field is left as the PM set it.

    Notation: AMP stands for one literal ampersand in anything I write here.


    Generated by Claude Code


    Generated by Claude Code

  11. hotlong commented on Sep 23, 2026

    @hotlong
    Contributor

    os-dev-report
    {
    "issue": 282,
    "status": "done",
    "branch": "claude/pm-dispatch-objectos-ju9td1",
    "pr": "#285",
    "premise_still_valid": true,
    "notation": "AMP stands for one literal ampersand in this report",
    "gates": [
    { "gate": "node .github/scripts/check-locale-surface.mjs --self-test", "commit": "76dca92", "exit": 0, "verdict": "✓ self-test: 28 case(s) over 14 rule(s) and 3 artifact(s) — every rule demonstrated able to fail and every artifact demonstrated able to fail it, on fixtures read through the real readers; both encoding rules demonstrated on both llms consumers; and the live control demonstrated able to fire negative-control-passed" },
    { "gate": "node .github/scripts/check-locale-surface.mjs (gate, cb0c146 build; branch changes no build input)", "commit": "76dca92", "exit": 0, "verdict": "✓ every advertised URL has a source file and every source file is advertised; both llms bodies carry every en-only page title and none from the other locales; no page slug in the content tree contains a dot; and neither llms consumer carries a numeric character reference or a malformed link target" },
    { "gate": "branch script at 76dca92 run against the d725081 build", "commit": "76dca92", "exit": 1, "verdict": "✗ locale surface: 35 finding(s)" },
    { "gate": "pnpm turbo run test --force", "commit": "76dca92", "exit": 0, "verdict": "@objectos/ci-scripts:test: ✓ 7 self-test(s) passed" },
    { "gate": "node scripts/pm/check-half-states.mjs --self-test", "commit": "76dca92", "exit": 0, "verdict": "✓ check-half-states self-test: 1551 cases pass." },
    { "gate": "node apps/docs/scripts/gen-zh-hant.mjs --check", "commit": "76dca92", "exit": 0, "verdict": "✓ zh-Hant: 73 generated file(s) match the zh-Hans sources byte for byte." },
    { "gate": "node .github/scripts/check-node-floor.mjs --self-test", "commit": "76dca92", "exit": 0, "verdict": "✓ self-test: 17 rule case(s), 24 satisfies case(s) and 18 range case(s) — every rule demonstrated able to fail, …" },
    { "gate": "control-byte grep over both changed files", "commit": "76dca92", "exit": 1, "verdict": "no match (grep exit 1 = none found)" },
    { "gate": "CI run 35879363502 (pull_request, head 76dca92)", "commit": "76dca92", "exit": 0, "verdict": "job Node floor: success; job build: success (every step success incl. Locale surface, turbo test, Package the Worker, Worker bundle fits the size budget, The docs site renders — smoke-check a local preview; Upload skipped as designed); job Deploy docs: skipped. Log lines: | llms-full.txt | 1 | 1 | 0 | 661 | 0 |, | llms.mdx | 79 | 79 | 0 | 661 | 0 |, Total Upload: 58546.01 KiB, ✓ smoke: 4 page(s) rendered against http://127.0.0.1:8792, negative control demonstrated red" },
    { "gate": "CI run 35879362957 Translations (head 76dca92)", "commit": "76dca92", "exit": 0, "verdict": "completed: success" }
    ],
    "measurements": [
    { "commit": "d725081", "fumadocs_core": "16.8.12 unpatched", "llms_full": { "references": 67, "kinds": "AMP#x2A; x58, AMP#x60; x5, AMP#x29; x4", "malformed_targets": 4, "link_targets": 661, "bytes": 695176 }, "llms_mdx": { "bodies": 79, "references": 67, "malformed_targets": 4, "link_targets": 661 }, "gate_exit": 1 },
    { "commit": "cb0c146 (branch base; branch head 76dca92 changes no build input)", "fumadocs_core": "16.8.12 patched", "llms_full": { "references": 0, "malformed_targets": 0, "link_targets": 661, "bytes": 694779 }, "llms_mdx": { "bodies": 79, "references": 0, "malformed_targets": 0, "link_targets": 661 }, "gate_exit": 0 },
    { "commit": "a69ef4d (#239 head, built as-is, pre-#281 tree)", "fumadocs_core": "16.15.2 unpatched", "llms_full": { "references": 67, "malformed_targets": 4, "link_targets": 650, "bytes": 685898 }, "llms_mdx": { "bodies": 79, "references": 67, "malformed_targets": 4, "link_targets": 650 }, "gate_exit": 1 },
    { "commit": "cb0c146 + #239 version change + pin deleted (simulated rebase)", "fumadocs_core": "16.15.2 unpatched", "install_exit": 0, "llms_full": { "references": 67, "malformed_targets": 4, "link_targets": 661, "bytes": 695176 }, "llms_mdx": { "bodies": 79, "references": 67, "malformed_targets": 4, "link_targets": 661 }, "gate_exit": 1 }
    ],
    "negative_control": {
    "shapes_ran": "both: (1) live fixture on every gate run, (2) NOT MEASURED exit on absent bodies",
    "live_log_line_ci_76dca92": "Live negative control: the #197 shape (a link target whose ) is encoded as AMP#x29;) was fed through this scan for both consumers and fired numeric-character-reference and malformed-link-target for each — the scan below can go red, so its result is a measurement.",
    "not_measured_unbuilt_tree_c22c02e": "exit 1; | llms-full.txt | NOT MEASURED — not built | 1 | ... and llms.mdx: no built directory at apps/docs/.next/server/app/llms.mdx — run pnpm turbo run build first...; ✗ locale surface: 4 finding(s)",
    "ablation_at_c22c02e_green_build": [
    "NUMERIC_REFERENCE -> /(?!)/g (anchor grep 1->0, injected 0->1): gate exit 1, Live negative control: FAILED — llms-full.txt:numeric-character-reference, llms.mdx:numeric-character-reference did not fire on the #197 shape.; --self-test exit 1, 7 failures",
    "malformed check -> if (false) (anchor 1->0, injected 0->1): gate exit 1, same line for malformed-link-target; --self-test exit 1, 7 failures",
    "restore: blob 1d7f725c0f28de105efc31f19708db87ec5cf472 == HEAD blob, git diff HEAD empty"
    ]
    },
    "files_changed": [".github/scripts/check-locale-surface.mjs", ".github/workflows/ci.yml"],
    "deviations": [
    "Rules added to the existing check-locale-surface.mjs, not a new script: a new --self-test script must be registered in tools/ci-scripts/run-self-tests.mjs (outside the declared surface); this script already reads llms-full.txt.body after the build and owns the content oracle that fixes the expected llms.mdx body set (79). ci.yml gets no new step; only the existing Locale surface step's comment block gains one paragraph. Size-gate block untouched.",
    "PM mechanism assumption 2 partly falsified (pnpm 10.28.2, #239's change on cb0c146): pnpm install --no-frozen-lockfile and --lockfile-only exit 1 with ERR_PNPM_UNUSED_PATCH (fumadocs-core@16.8.12). Silent paths measured: deleting the pin (the first remedy pnpm's error offers) -> install exit 0, defect back at 67 / 4 of 661; a lockfile regenerated with --config.allow-unused-patches=true -> pnpm install --frozen-lockfile exit 0 with no warning.",
    "PM mechanism assumption 1 confirmed: the Locale surface step (after pnpm turbo run build, before Package the Worker) reads .next only.",
    "Rules added beyond the two asked for, as NOT MEASURED guards: llms-page-body-missing (body set checked against the oracle) and no-link-targets (a consumer with 0 targets fails).",
    "Empty branch pushed at cb0c146 as a write-route probe before the first real commit."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "filed as #284 (label finding, unassigned): the patches/fumadocs-core@16.8.12.patch header says a version bump drops the pin silently; pnpm 10.28.2 refuses the stale pin with ERR_PNPM_UNUSED_PATCH when the lockfile is regenerated, and is silent only after the pin is deleted or with unused patches allowed plus a frozen install"
    ]
    }


    Generated by Claude Code

  12. hotlong commented on Sep 24, 2026

    @hotlong
    Contributor

    ACCEPT: PR #285. Driving to landing. Notation: AMP stands for one literal ampersand.

    Checklist, read from GitHub, not from the report.

    • The PR is draft, based on main, and its first line is Fixes #282.
    • get_files shows 2 files: .github/scripts/check-locale-surface.mjs (+578/−10) and .github/workflows/ci.yml (+11). Neither is on the governed surface.
    • There is no changeset flow in this repo.
    • Each check was read by its own conclusion on head 76dca92: build success, Node floor success, Ownership & freshness success. Deploy docs was skipped, which is correct on a pull_request event.

    Spot-checks, taken by the seat rather than read from the report.

    probe reading
    Is the ci.yml change comment-only? yaml.safe_load(branch) == yaml.safe_load(main) → True. No step changed, and the size-gate block is untouched.
    Does the gate actually run in CI, or does a green job only mean the step never ran? Build job 107243512064 log shows the live control line, then `
    --self-test on a fresh detached tree at 76dca92 exit 0, 28 case(s) over 14 rule(s). The blinded-scan cases both read red.
    Gate on an unbuilt tree exit 1, both consumers NOT MEASURED — not built, artifact-missing ×4

    The real regression, not only a fixture. The PR measures the defect on four builds, and on the #239 head as built: 67 references and 4 malformed targets, gate exit 1. That is the exact failure this gate exists for, shown on the PR that would have caused it. d725081 matches the card to the byte (695176).

    Deviations, all accepted:

    • Rules added to check-locale-surface.mjs, not a new script. A new --self-test script would have had to be registered in tools/ci-scripts/run-self-tests.mjs, which is outside the file surface. This script also already owns the content oracle that makes "79 bodies expected" a checked claim.
    • Two extra NOT-MEASURED rules (llms-page-body-missing, no-link-targets) keep either main rule from passing over nothing.
    • One of my mechanism assumptions was partly falsified. A version bump is loud where the lockfile is regenerated (ERR_PNPM_UNUSED_PATCH, exit 1). It turns silent after the pin is deleted, which is the first remedy pnpm's own error suggests, or when a lockfile is generated with unused patches allowed and then installed with --frozen-lockfile. My dispatch repeated the card's "silently" claim without measuring it. The dev corrected it. Filed as The fumadocs-core patch header says a version bump drops the pin silently; pnpm 10.28.2 refuses the stale pin when the lockfile is regenerated #284.

    Scope limits stated in the script header, accepted as written: the whole body is scanned, code fences included, and a titled link would read as malformed. Neither case occurs in today's corpus. Either one appearing later is a reason to change the rule on purpose, not to weaken it.

    Path check before flipping ready: nothing governed. This lands, and ⚠️ the merge publishes production. The change touches no build input, and the render gate was green on this head.


    Generated by Claude Code

  13. added theissue type on Sep 24, 2026
  14. hotlong commented on Sep 24, 2026

    @hotlong
    Contributor

    Landed and published. PR #285 was squash-merged as c2d1181. All readings below come from the jobs' own step records on CI run 35945673804, not from the merge:

    • build: success. The Locale surface step, which now carries this gate, ran on main and passed, followed by Upload the Worker bundle (success, not skipped) and the pre-merge render check (success).
    • Deploy docs / Publish the tested Worker: success. Log line: ✓ deploy: b3080544-4be8-40a8-9f20-845ca26d9103 is serving, replacing d67ec8e3-bddb-470b-809f-079f5a35cb90. Total Upload: 58546.02 KiB.
    • Deploy docs / Smoke-check the live site: success. The rollback and failure-card jobs were skipped because their conditions were false. As the seat post notes, a skipped rollback is not evidence that rollback works.

    pm:dispatched was removed by hand; a Fixes auto-close does not remove it.

    What this changes for Dependabot #239. From now on, a fumadocs-core bump that loses the patch fails build at Locale surface with 67 references and 4 malformed targets, and the log names the cause: the resolved version against the pinned version. #239's head was measured red on the PR. The real remedy is to regenerate the patch against the new version in the same PR. #284 carries the correction to the patch header.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions