Skip to content

0.9.3 — accuracy pass: unstick CI, and gate the claims nothing was checking - #1

Merged
joshidikshant merged 13 commits into
mainfrom
docs/0.9.3-accuracy-pass
Aug 2, 2026
Merged

joshidikshant merged 13 commits into
mainfrom
docs/0.9.3-accuracy-pass

Conversation

@joshidikshant

Copy link
Copy Markdown
Owner

CI on main had been red for sixteen consecutive runs across six days (last green 623aa05, 2026-07-26). 0.9.0, 0.9.1 and 0.9.2 were all published on top of it, so all three shipped two real WCAG failures as product defects rather than just a red badge. The a11y gate had in fact never passed on CI once — it landed in d2ca476 pushed alongside 27 other commits, so its first run was already red.

The bug was one line, and platform-specific

Updated rendered item.updated_at as a bare ISO timestamp, while every other value in that card already carried break-all. An ISO string is a single 24-char token with no break opportunity, so at 200% zoom it set a 236px min-content floor that min-w-0 could not shrink. On macOS that measures 301px and passes a 320px viewport; on CI's Linux font metrics the same string is ~325px — exactly the {"s":325,"c":320} in every failing run, and why it never reproduced locally.

Fixed by rendering it like the Age field beside it: humanised text in a <time>, exact value kept in title/dateTime. That removes the unbreakable token instead of breaking it. (The first attempt was break-all; it passed CI but split the timestamp mid-number at desktop width — caught by driving the running app, not by a gate.)

Four gates for four things nothing was checking

  • release-preflight (prepublishOnly) — refuses to publish unless the tree is clean, the commit is pushed and tagged, and ci.yml concluded success on that exact sha. Fails closed: an unreachable GitHub is a refusal, since "unknown" is the state that let three releases through. Override is LOOPBACK_ALLOW_RED_CI=1.
  • docs-facts-gate — re-derives the widget size, tool count, HTTP routes, registry items, canary count and open-on-LAN endpoints from the code. Every figure that had drifted was hand-typed and read by no gate.
  • link-gate — the previous link check had been green since the day it was added while scanning zero links: linkinator ignores every path after the first, and the bare 127.0.0.1 skip matched the root it serves files from. Now asserts a minimum link count per target.
  • init-gate — now covers the seam between the two canonical sources, whose sync was manual and ungated.

All four are mutation-canaried; the sweep is 28. One of them caught my own gate being decorative mid-review, which is the argument for having it.

Docs corrected against the code

The widget was quoted four ways (46KB/15KB, 57KB/19KB, 57,183 B, ~29KB) against a real 59,443 B, with two prior commits each claiming to have fixed it. "The MCP bus — 10 tools" sat above a 9-row table. The HTTP table omitted all three attachment endpoints. The shadcn section documented two registry items of three and had a paragraph cut off mid-clause with a contradictory paste over it — live on GitHub and in the published npm readme, which is why this needs a release rather than a docs commit. The security section said three endpoints stay open on a LAN bind; requireAuth allows four.

Also adds CONTRIBUTING.md, SECURITY.md and issue/PR templates (community health was 42%), and backfills tags v0.9.0–v0.9.2 — tag discipline had stopped exactly when public publishing began.

Verification

All ten gates pass locally on macOS, plus a 28-case canary sweep. The Linux run is what this PR is for: the original failure only reproduced on CI's font metrics, so please let ci.yml confirm the reflow fix rather than trusting the local measurement.

Tag v0.9.3 is deliberately not pushed — it would dangle if this is squash-merged. Tag whatever commit actually lands, then publish.

🤖 Generated with Claude Code

Dikshant and others added 12 commits August 2, 2026 20:43
The gate has never passed on CI. It was added in d2ca476 and pushed in the same
batch as 27 other commits, so its very first run was already red, and it has
stayed red for sixteen consecutive runs across six days (last green 623aa05,
2026-07-26) — through all three npm publishes: 0.9.0, 0.9.1 and 0.9.2.

The failure was real, not flaky. `Updated` rendered item.updated_at as a bare
ISO timestamp with no break-all, while every other value in that card
(project, source/reporter, assignee, route) already had one. An ISO string is
a single 24-char token with no break opportunity, so at 200% text zoom it set
a 236px min-content floor that the Section's min-w-0 could not shrink. On
macOS that lands at 301px and passes at a 320px viewport; on CI's Linux font
metrics the same string measures ~325px, which is exactly the {"s":325,"c":320}
in every failing run — and why this reproduced on CI but not locally.

Adding break-all clears 320px with 40px of headroom: measured passing down to
280px, against a ~24px macOS/Linux metric delta.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0.9.0, 0.9.1 and 0.9.2 were all published while ci.yml was failing. Nothing in
the pipeline objected, because nothing in it ever asked. The gates verify the
source; verify:release deliberately runs after publishing, against published
artifacts, where npm's immutability means it can only report damage.

release-preflight asks the missing question before the fact: clean tree, commit
actually pushed, and a completed successful ci.yml run for that exact sha. It is
wired as prepublishOnly, so `npm publish` enforces it rather than trusting
anyone to remember.

It fails closed. An unreachable GitHub is a refusal, not a pass — "unknown" is
the state that let the last three releases through. LOOPBACK_ALLOW_RED_CI=1 is
the explicit, auditable override.

One trap found by testing rather than reading: the GitHub API matches head_sha
on the full 40-char sha only, so an abbreviated sha returns an empty run list —
indistinguishable from "never tested". Shas are resolved locally first.

Fail-closed gates have their own failure mode: one that refuses everything would
sail through a canary that only checks for a non-zero exit. So the sweep gets
both directions, anchored to two immutable commits — 10a1c6d (red) must fail,
623aa05 (green) must pass — and `expect` in the harness to express the second.
The canary job now gets GH_TOKEN, without which the red case would pass for the
wrong reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The structural gates were all green while the docs drifted, because none of
them ever read a claim. Everything that had rotted was a hand-typed figure.

Corrected against the code:
  - the widget was quoted four ways — 46KB/15KB, 57KB/19KB, 57,183 B, ~29KB —
    against a real 59,443 B. Two separate commits each said they had "corrected
    the widget size" while fixing one occurrence of two;
  - "The MCP bus — 10 tools" sat above a 9-row table, missing
    loopback_update_feedback, which the 0.8.0 changelog had announced;
  - the HTTP surface table omitted all three attachment endpoints;
  - the shadcn section documented two registry items and shipped three, and its
    prose had a sentence cut off mid-clause with a contradictory paste over it
    and an orphan "free)." — live on GitHub and in the npm readme;
  - the repo map said init writes .claude-plugin/ (it does not) and that
    init-gate re-renders it (it asserts a version), and omitted .github/ and
    .impeccable/ entirely.

docs-facts-gate now re-derives all of it: size, tool count, every registered
route, every registry item, the canary count, the published version.

Measuring it exposed a smaller lie inside the bigger one. "19,751 B gzipped on
the wire" was measured with the gzip(1) CLI, but the server serves gzipSync at
zlib's default level — 19,770 B. Three plausible numbers for one file, so the
gate measures it the way src/http.ts actually produces it.

link-gate replaces a CI step that had never checked anything. `linkinator
README.md docs integrations` silently ignores every path after the first, and
`--skip "127.0.0.1|..."` matched the root linkinator serves the files from, so
the crawl never started: that one pattern took the scan from 24 links to 0, and
"Successfully scanned 0 links" exits 0. It now asserts a minimum link count per
target, because the failure was never a broken link — it was an empty crawl.

Both new gates are canaried, and the sweep is 26 checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every parity check ran from one canonical source down to its renderings —
canonical→installed, instructions-src→AGENTS block, plugin→canonical. Nothing
compared the two canonical sources to EACH OTHER, and instructions-src.md's own
header admits the sync is manual: the skill body "mirrors this text and must be
updated with it". That was the last drift class in the repo with no check.

They are deliberately not byte-identical — the skill carries frontmatter and
resolves its own project slug, the playbook gets {{PROJECT}} substituted — so a
diff cannot be the gate. The invariant is the loop: both must drive the same
tools in the same order. An extra step in one is an agent working a different
loop depending on which file it read.

Also untracks dashboard/tsconfig.tsbuildinfo, tsc's incremental cache and the
only tracked build artifact that was not deliberate. public/ stays committed —
that one IS the distribution mechanism.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Its opening line says "see the attached zip", which dangles here: this file is
committed verbatim from the workspace repo, and no zip exists in this one. The
note names what the archive was (now commit b9d1567) and flags which of its
figures are Phase-0 rather than current — 9 tools, no dashboard, no HTTP auth.

Applied identically in the workspace repo so both copies stay byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tag discipline stopped exactly when public publishing began. v0.3.0 through
v0.8.0 are all tagged; 0.9.0, 0.9.1 and 0.9.2 — the only three versions that
ever reached npm — were not, so the releases people actually installed are the
ones nobody can check out.

v0.9.0 (f472687), v0.9.1 (17bd8ba) and v0.9.2 (2f96190) are now tagged
retroactively, each verified against the package.json version at that commit.

release-preflight now refuses to publish unless v<version> exists AND points at
the commit being published, so this cannot silently lapse again, and `npm run
bump` prints the tag command as part of the release sequence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GitHub scored this repo's community health at 42%: LICENSE and README and
nothing else. For a project whose whole pitch is that an adopter can wire it
into their own agents in two minutes, the absence of a contributing guide and a
private disclosure channel is a real barrier, not a metadata gap.

CONTRIBUTING states the doctrine the repo already runs on rather than inventing
a new one: a gate that cannot fail is decorative, so a new gate needs a canary
case and a new number in the docs needs a docs-facts-gate check. It lists the
gate suite as CI actually invokes it, and points at the README's repo map
instead of copying it, so it cannot drift.

SECURITY documents the model that already exists in the code, verified line by
line rather than described from memory: origins pinned at startup instead of
read from Host (DNS rebinding), the cross-origin pin projection that keeps
captured response bodies from ever leaving, the token on non-loopback binds
moved into an HttpOnly SameSite=Lax cookie and compared with timingSafeEqual
over SHA-256, the four endpoints deliberately left open, and the 60/min intake
limit. It is equally explicit about what is NOT a vulnerability — unauthenticated
local filing is the product, and deployed public sites are an unsupported
surface since Chrome 142+ blocks them.

Also gives docs-facts-gate and link-gate the `npm run` aliases every other gate
already had.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six coupled version fields via `npm run bump`, plus the changelog entry for the
accuracy pass: the a11y fix that unsticks CI, release-preflight, docs-facts-gate,
link-gate, the canonical-source seam check, and the backfilled 0.9.x tags.

This one has to be a release rather than a docs commit: the mangled shadcn
paragraph is in the published 0.9.2 readme, and an npm readme can only be
replaced by publishing over it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The README said "Three endpoints stay open on a LAN bind" and listed three,
while requireAuth lets four through — GET /health was open in the code and
absent from the table. A wrong count in the security section is the worst place
for one, and it was found by a reviewer reading src/http.ts, not by anything in
CI.

docs-facts-gate now derives the open set from requireAuth itself and checks
three things against it: every open path appears in the table, the row count
matches, and the English number in the sentence above the table matches too.
That sentence is exactly what went stale, so counting only the rows would have
missed it.

Canaried, and the sweep is 28.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The open-endpoint check I added one commit ago substring-searched the README
table for each path requireAuth leaves open. The canary mutation renames that
row to `/health-REMOVED` — which still CONTAINS `/health`, so the check passed
with its subject broken and the sweep correctly reported it as decorative.

It now parses the path out of each row's `METHOD /path` cell and compares sets
in both directions, so a renamed row, a missing row and an invented row all
fail. Re-canaried: the mutation trips it.

Worth noting how close this came to shipping green. The first time I ran this
case in isolation it reported a pass — but the gate was failing on an unrelated
stale canary count, not on the mutation. A gate failing for the wrong reason
looks exactly like a gate working, which is the same false-green class the
sweep's own no-op-mutation guard exists to catch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
My own break-all fix, caught by testing the running app rather than the gate.
It satisfied the 320px reflow rule and passed CI, but at 1280px the grid column
is ~166px and a 24-char ISO string does not fit — so break-all did exactly what
it says and split the timestamp across two lines mid-number.

Rendered like the Age field directly above it instead: humanised text in a
<time>, with the exact value kept in title and dateTime. That removes the
unbreakable token rather than breaking it, so the reflow floor is unchanged
(still clears 320px, measured passing down to 280px) and the desktop rendering
is one clean line. It also makes the two adjacent timestamp fields consistent,
which they were not before.

No gate could have caught this: every a11y assertion is about overflow, and
wrapping mid-token is not overflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9a6acfd made the server.json description scope-honest and took it to 137
characters. The MCP Registry caps it at 100, and `npm run smoke` asserts exactly
that — so main went red the moment it landed. Caught by rebasing onto it, not by
anything that ran before the push.

Keeps the intent (the app you're building, a fix turning the pin green) at 98
characters.

Also lists CONTRIBUTING.md, SECURITY.md and CHANGELOG.md in the repo map, and
corrects "the six gates" to seven now docs-facts-gate is in CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 681407db9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/canary-all.mjs
Comment on lines +341 to +343
cmd: ["node", ["scripts/release-preflight.mjs", "--sha", "623aa05a2d75e4bbeffcc5cfde903babc2a332d7"]],
guards: "a green commit still being publishable",
expect: 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Fetch the historical canary commits

The canary.yml workflow uses a plain actions/checkout@v4, and checkout fetches only the triggering commit by default. Consequently these historical SHAs are absent in CI: release-preflight fails at its local git rev-parse before querying GitHub, so the red case passes for the wrong reason and this green case always returns nonzero, making every canary workflow fail. Configure fetch-depth: 0 or explicitly fetch both anchor commits before running the sweep.

Useful? React with 👍 / 👎.

Comment on lines +109 to +115
const tagged = tryRun("git", ["rev-list", "-n", "1", tag]);
if (!tagged.out) {
bad(`${tag} does not exist — tag the release commit before publishing:\n git tag -a ${tag} -m "${tag}" && git push origin ${tag}`);
} else if (tagged.out !== sha) {
bad(`${tag} points at ${tagged.out.slice(0, 7)}, not the commit being published (${sha.slice(0, 7)})`);
} else {
ok(`${tag} points at the commit being published`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify that the release tag was pushed

When the maintainer creates the version tag locally but forgets git push origin v<version>, this check still passes because git rev-list only inspects the local repository. npm publish can therefore release another version with no corresponding tag on GitHub, despite this gate being intended to prevent exactly that traceability failure. Check the remote tag and its peeled commit—for example via git ls-remote --tags—rather than accepting only a local tag.

Useful? React with 👍 / 👎.

Comment on lines +59 to +64
if (process.env.LOOPBACK_ALLOW_RED_CI === "1") {
console.warn(
"\n⚠️ LOOPBACK_ALLOW_RED_CI=1 — publishing WITHOUT a green CI run.\n" +
" This is the override, not a pass. Say so in the release notes.\n",
);
process.exit(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve integrity checks when overriding red CI

When LOOPBACK_ALLOW_RED_CI=1 is used for its documented emergency purpose, this early exit also skips the clean-tree, pushed-commit, and version-tag checks below. An operator overriding only a known CI failure can therefore accidentally publish dirty files that CI never tested or an unpushed/untagged commit. Record the override here but bypass only the GitHub CI-conclusion check, allowing the local and repository-integrity checks to continue running.

Useful? React with 👍 / 👎.

Comment on lines +123 to +125
const undocumented = routes.filter((r) => {
const path = r.split(" ")[1];
return !readme.includes(path);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict route checks to the HTTP surface table

When a route is removed from the HTTP surface table but remains mentioned elsewhere in the README, this global substring check still reports it as documented. This already affects routes such as /health, /ingest, /feedback, /widget.js, and /mcp, which occur in multiple README sections, so deleting their actual table rows leaves the new gate green despite its stated assertion. Extract the HTTP surface table first and match method/path rows within that section.

Useful? React with 👍 / 👎.

Comment thread scripts/canary-all.mjs
Comment on lines +288 to +291
mutate("README.md", (s) =>
s.replace(
"https://github.com/getsentry/sentry-mcp",
"https://github.com/joshidikshant/loopback/tree/main/this-path-does-not-exist-canary",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Trigger the canary when README anchors change

These new canaries mutate README.md, but the inspected canary.yml push and pull-request path filters omit README.md. A README-only edit can therefore remove or rewrite one of these mutation anchors without running the meta-gate; the ordinary documentation checks may remain green while mutate() would now be a no-op, and the broken canary is only discovered by the weekly run. Add README.md (and any other newly mutated documentation paths) to both workflow path filters.

Useful? React with 👍 / 👎.

Comment on lines +203 to +207
const quoted = [...roadmap.matchAll(/loopback-mcp-server@([\d.]+)/g)].map((m) => m[1]);
for (const v of quoted) {
check(
v === latest || v === pkg.version,
`ROADMAP quotes loopback-mcp-server@${v}; npm latest is ${latest}, tree is ${pkg.version}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the documented npm version valid after publishing

When this 0.9.3 release is published, npm view will return 0.9.3 and pkg.version is already 0.9.3, but the committed ROADMAP still quotes loopback-mcp-server@0.9.2. The condition therefore changes from passing before publication to failing afterward, so the next otherwise-unrelated push or pull request with npm connectivity makes ci.yml red again. Update the ROADMAP to 0.9.3 in this release commit, or avoid making a historical published-version statement fail based on the mutable latest tag.

Useful? React with 👍 / 👎.

Two bugs found by driving the built CLI rather than reading it.

`--version` printed nothing and hung. It was not a known flag, so it fell
through to the default branch, started a stdio server, and waited on a stdin a
human terminal never closes — having already opened the user's real
~/.loopback/loopback.db. Any typo behaved identically. argv is now checked
BEFORE the store is constructed, which is the part that matters: --version and
-v print the version and exit 0, anything unrecognised names itself on stderr
and exits 1, and neither opens a database. The e2e assertion points the run at a
directory that must stay empty afterwards, because an exit code alone would not
have caught the DB being touched.

A whitespace-only title was accepted while an empty string was correctly
rejected: "   " is three characters and cleared min(3), so the queue could hold
an item nothing could act on. The length checks now run on the trimmed value —
title, project, claiming agent, comment author and comment body, on submit and
update alike, since the same mistake was sitting in all of them. It also stops
padded slugs being stored with their padding.

Both canaried, in the direction that matters: drop the trim and "   " is three
characters again; let unknown args fall through and the hang comes back. Sweep
is 30.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@joshidikshant
joshidikshant merged commit 3de62cd into main Aug 2, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant