Documentation
Restructuring Retrospective (2026-08)
A record of the multi-day effort that took this repo from bloqr-lists — a
monolith holding compiled filter lists, API clients, and every rules
compiler — to bloqr-core: a focused, JSR-publishing compiler toolkit, with
data/ and the API clients split into their own repos. Written for whoever
picks up Epic #256 next, human or AI, so the reasoning behind the current
shape of things isn't only reachable by re-reading old PR diffs.
What actually happened, in order
- Repo rename + three-way split.
bloqr-lists→bloqr-core.data/→BloqrAI/bloqr-blocklists(public, since a filter list's whole point is public consumption).src/adguard-api-*+src/linear/→BloqrAI/bloqr-apiclients(private, internal tooling). Both splits usedgit subtree split+git subtree addto preserve file history rather than a flat copy — verified after the fact withgit merge-base --is-ancestorthat the moved commits are still real ancestors in the new repos, not just replayed diffs. - JSR extraction.
src/adblock-compiler-corepulled out of the monolith as an AGTree-free, standalone TypeScript/Deno package, published as@bloqr/compiler-core. This is the repo's first JSR package and the reference implementation for everything that follows. - JSR publishing, the hard way. OIDC trusted publishing was the first
attempt and never worked — see "OIDC: the multi-day dead end" below.
Landed on token-based publishing (
JSR_WORKFLOW_TOKEN/JSR_API_TOKEN, org-level secrets scoped to@bloqr) instead, documented indocs/jsr-token-authentication.mdanddocs/jsr-org-standards.md. bloqr-compilerdependency swap. The private commercial monorepo that originally donated this code was still vendoring its own near-duplicate copy. Swapped ~15 formatting-identical files for a real@bloqr/compiler-coreJSR dependency, kept the genuinely-diverged commercial files (AGTree-powered validation, conflict detection, rule optimizer) local, and preserved one real behavioral difference (FilterDownloader's plugin-registry hook) via explicit dependency injection rather than losing it. Closedbloqr-compiler#2200and#1131.- Housekeeping issues #289, #293, #294 — stale post-migration doc
paths + README rewrite, JSR module/symbol documentation (94% → full
marks on the two doc-score checks), and an audit of npm dependencies for
JSR-native replacements (
yaml→@std/yaml,@iarna/toml→@std/toml,chalk→@std/fmt/colors,cli-table3→@cliffy/table;ora,figlet,@inquirer/promptskept on npm with documented reasoning). - Org-wide versioning strategy (
docs/architecture/versioning-strategy.md) — designed and merged before the decomposition epic starts, not after, specifically because bloqr-core is about to become several independently-versioned JSR packages and retrofitting a versioning scheme onto N packages later is much more expensive than establishing the pattern once, now, with one package as the reference implementation.
Obstacles overcome
OIDC: the multi-day dead end
JSR's OIDC trusted publishing failed with InvalidIssuer (invalidOidcToken) on every attempt, across a full scope/package
recreation, a fresh repo link, and confirmation that BloqrAI has no
org-level GitHub OIDC policies configured (checked directly in org
settings — only placeholder/sample values were ever present). This wasn't
a configuration mistake on our end that a docs re-read would fix; it
looks like a genuine JSR-side gap or bug in how org-owned-repo OIDC is
validated. Filed bloqr-core#291 and, once the user found it independently,
cross-linked the upstream report at
jsr-io/jsr#1485.
The lesson: don't burn unbounded time re-attempting a third-party integration that fails identically every time despite correct configuration. Two or three reproductions with a clean environment each time is enough to conclude "this is their bug, not ours" — at that point, switch to the documented, working fallback (token auth) and move on. We did eventually pivot, but only after the user pushed for it; a faster internal call to de-risk (try tokens, keep OIDC as a future upgrade path) would have saved real time.
deno publish --token vs JSR_TOKEN env var
Small but completely blocking: deno publish does not read a
JSR_TOKEN environment variable — it needs --token <value> as an
explicit CLI flag. The first token-based workflow attempt used env: JSR_TOKEN: ${{ secrets.JSR_WORKFLOW_TOKEN }} (a natural pattern to reach
for) and failed with "No means to authenticate." One log read fixed it.
Lesson: when adapting an auth pattern from one tool/doc to another,
verify the exact flag/env-var contract against that tool's own error
message rather than assuming symmetry with a similar tool.
The npm-mirror / minimum-dependency-age collision
Getting @bloqr/compiler-core consumed by bloqr-compiler's Cloudflare
Worker build surfaced a real cross-runtime interop gap: Wrangler's esbuild
bundler has no concept of Deno's jsr: specifiers, so a bare
@bloqr/compiler-core import failed to resolve even though every Deno
task (test/check/lint) resolved it fine. Fix: point the import at JSR's
npm-compatible mirror (npm:@jsr/bloqr__compiler-core) instead, which
Deno does materialize into node_modules for bundler tools to find, plus
a wrangler.toml [alias] entry mapping the bare specifier to the mirror
package name.
That fix then collided with Deno's minimum-dependency-age supply-chain
protection (default 24h) — a brand-new package can't be installed via its
npm-compat mirror until it's aged past that window, even though the exact
same code, addressed as jsr:, has no such restriction. This is a real,
if temporary, tension between "publish and consume same-day" (the
decomposition epic's actual cadence) and a legitimate security control.
Resolved by explicitly relaxing it in CI (--minimum-dependency-age 0,
scoped to CI only — local developer installs keep full protection) with a
tracked reversion issue (bloqr-compiler#2212) rather than either
silently weakening security or blocking indefinitely. Lesson: when a
security control and a stated architectural direction (fast, frequent,
same-org package publishing) are in real tension, don't quietly pick a
side — make the tradeoff, the scope of the relaxation, and the reversion
condition all explicit and written down.
Concurrent background agents sharing one working directory
Three agents were launched in parallel for issues #289/#293/#294 without
isolation: "worktree", so all three ended up doing git checkout -b,
edits, and commits in the same shared clone. Concretely, this caused:
one agent's commit landing on a different agent's branch during a HEAD
race; a subsequent reset attempt getting itself raced by further commits
before it could fully take; and two batches of stale uncommitted changes
left behind as git stash entries. None of it was silently lost — every
agent that hit contamination flagged it explicitly in its own final
report rather than pretending the run was clean — but reconciling it
required a careful multi-step audit: diffing every PR's file list against
every other PR's, cross-checking uncommitted working-tree state against
each PR's actual pushed content (including catching a false-alarm CRLF
line-ending diff along the way), and one deliberate rebase to drop a
stray commit that had landed on the wrong branch.
Lesson, now load-bearing: always pass isolation: "worktree" when
spawning more than one background agent against the same repo in
parallel. It would have prevented all of the above outright. This isn't
a maybe — treat it as a hard requirement for parallel background agents
in this repo going forward.
The JSONC gotcha, caught only by a real final-integration check
sync-version.ts (added by the versioning-strategy PR) used
JSON.parse/JSON.stringify to update deno.json's version field. Deno's
own config loader tolerates // comments in deno.json (JSONC), and a
later, independent PR (#294's dependency audit) legitimately added
explanatory comments there — "no JSR-native library exists for ora,
here's why" and similar. Each PR's own CI was green in isolation: deno check/lint/test/publish --dry-run all use Deno's JSONC-tolerant
parser internally, so none of them ever exercised the strict-JSON code
path in sync-version.ts. The break only surfaced when all four PRs were
actually combined on main and version:sync was run for real, in a
dedicated final verification pass — one that wasn't required by anything,
was't part of any individual PR's checklist, and almost didn't happen.
Lesson: green CI on N individually-correct PRs is not evidence that
their combination is correct, especially when they touch the same
files or adjacent parts of the same config. After merging a batch of
interdependent PRs, do one real end-to-end pass against the actual merged
main — not a rebase-and-recheck of the last PR, the literal post-merge
state — before calling the batch done. This is cheap (minutes) relative to
the cost of the bug shipping unnoticed.
Tricky decisions worth remembering the reasoning for
- Per-package, not per-repo, versioning.
bloqr-compileralready had a workingversion.ts→sync-version.ts→ auto-bump-PR → tag → release.yml pipeline, but for exactly one deployable artifact. Copying that pattern verbatim forbloqr-corewould have created a single repo-wide version — which breaks the moment a second JSR package exists, since bumping@bloqr/compiler-coremust never imply bumping some future sibling package sharing the repo. Solved with a tag-prefix convention (<package-slug>-v<semver>, e.g.compiler-core-v1.2.3) and path-filtered, per-package bump/tag workflows from the start, with a documented onboarding checklist for each future decomposed package. - What stays local vs. moves to the JSR dependency in
bloqr-compiler. Not a blind "delete anything with a same-named file in the new package" pass — realdiff -bBw(whitespace-insensitive) comparisons distinguished pure formatting drift (safe to delete + repoint) from genuine commercial divergence (AGTree-powered validation, conflict detection, the rule optimizer — all correctly kept local). One easy-to-miss case:FilterDownloader'sresolveCustomDownloaderdefault differed between the two copies (core defaults to a no-op; the commercial copy wired in a plugin registry fors3:///gcs://sources) — preserved via explicit dependency injection at every construction site rather than silently dropped when the vendored file was deleted. - Merge ordering for the four housekeeping PRs. #297 (docs/README) and
#295 (versioning) had no file overlap with anything and merged first,
cleanly. #296 (JSDoc) and #298 (dependency swap) both touched the same
three compiler-core files — deliberately merged last, in sequence, so
the one real conflict got resolved exactly once, by rebasing the second
one onto a fully-updated
main, rather than resolved twice or resolved against a moving target.
Where things stand now
@bloqr/compiler-core@1.0.0is live on JSR, publishing via token auth, versioned via the new per-package strategy.bloqr-compilerconsumes it as a real dependency, not a vendored copy.bloqr-core,bloqr-blocklists, andbloqr-apiclientsare cleanly split with preserved history.- Docs are current (root
README.mdrewritten, ~18 stale-path files fixed, JSR doc-score gaps closed). - The versioning strategy is documented and has one working reference implementation, ready to be copied for each package Epic #256's decomposition work produces.
Still open, worth carrying into epic planning
bloqr-core#291— JSR OIDC investigation, tracking upstreamjsr-io/jsr#1485. No action needed from us; revisit if/when JSR responds.bloqr-compiler#2212— revert the CI-only--minimum-dependency-age 0relaxation once same-day publish-then-consume settles down.docs/release-guide.md— still describes the old single-repo-wide-tag release process; needs a pass to reflect the new per-package tag scheme once there's a second package to make the distinction concrete.- Dependabot alerts (54 open on
bloqr-core, 126 onbloqr-compileras of this writing) haven't been triaged as part of this restructuring — worth a look before the epic adds substantially more surface area (the Dashboard app, more compiler wrappers). bloqr-core/.github/dependabot.ymlonly configures thedevcontainersecosystem — no automated version-update PRs for npm, Cargo, NuGet, pip, or GitHub Actions themselves. Worth deciding deliberately whether that's intentional before the epic multiplies the number of dependency manifests in this repo.