Skip to content

docs(todo): 20 — reconcile the things index budget under MongoDB's 64 cap - #405

Open
lopugit wants to merge 4 commits into
developfrom
claude/todo-index-budget
Open

docs(todo): 20 — reconcile the things index budget under MongoDB's 64 cap#405
lopugit wants to merge 4 commits into
developfrom
claude/todo-index-budget

Conversation

@lopugit

@lopugit lopugit commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Docs only — files a roadmap item for the index-budget review that came out of #401/#402, while the measurements are fresh.

Why file it

MongoDB caps a collection at 64 indexes, hard. things carries by far the most and gains a few with most features. During #401 a local ensureIndexes run hit that cap and took registration down with it — the battery is one Promise.all, so any failed createIndex fails the ensure, and registerUser awaits it. Good news: that reading was a dev-machine artifact, and production has headroom.

What the doc records

  • The real budget: 49 code-defined indexes on things (+_id_), so ~14 slots of headroom — not the 63 a long-lived dev database shows. That inflation has a cause worth knowing: every worktree runs its own branch's ensureIndexes against the same local mongod, so a laptop accumulates the union of every branch (13 such indexes exist in no current code), and sibling worktrees resurrect an index a newer branch retired. Plus the audit commands to measure it properly.
  • The failure mode, so the next person recognises it: cap exhaustion (add index fails, too many indexes) or an E11000 on a unique index both surface as register/login 500. Also that createIndexReplacing is create-then-drop, so swaps need a free slot — the cap must never be reached, not merely not exceeded.
  • Evidence-backed reclamation candidates, chiefly ~5 dead legacy indexes: kind and visibility are pre-thingtime/acl field names that appear in collections.ts only inside index definitions — no reader, no writer — and match 0 of 6,831 local things. Flagged with the production census to run before dropping anything, since older user data may predate that migration.
  • A plan (measure prod → drop the dead ones → explain-plan the thingtime/quotaKind families → add a budget-guard test so the next index addition is a deliberate decision) and a definition of done.

Status board row added as 🔴 Not started · no rush, headroom exists.

🤖 Generated with Claude Code

… cap

Filed after PR #401/#402, where a local ensureIndexes run hit the cap
and took registration down with it. Captures: the measured budget (49
code-defined indexes on things, ~14 slots of headroom); why a dev
database reads high (every worktree ensures its own branch's indexes
into one local mongod, and siblings resurrect retired ones); the
failure mode (one Promise.all battery — any failed createIndex 500s
register/login, and create-then-drop swaps need a free slot); and
evidence-backed reclamation candidates, chiefly five legacy kind/
visibility indexes that match 0 of 6831 local docs and appear nowhere
in the code outside their own definitions.

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

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thingtime Ready Ready Preview Aug 26, 2026 8:22pm
thingtime (develop) Ready Ready Preview Aug 26, 2026 8:22pm

Request Review

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

✅ Develop S3 preview ready

The alias passed the develop bucket CORS preflight and a final live PR/SHA fence.

Generic Vercel Preview deployments use the shared development runtime; this controller adds the stable exact-SHA alias and marker-scoped cleanup.

@github-actions
github-actions Bot temporarily deployed to develop-pr-405 August 25, 2026 09:17 Destroyed
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Lopu repository review

Lopu reviewed this PR against develop as Thingtime's principal PR and repository manager. Using Claude Opus 5.

Lopu found no justified local change to publish from this review pass.

Lopu review — PR #405 · docs(todo): 20 — reconcile the things index budget under MongoDB's 64 cap

Compared: claude/todo-index-budget @ 7a0582b against develop @
2598986, three-dot from merge-base 1d7ba7a. 2 files, +214 / −0:
a new TODO/claude-todo/20-index-budget-consolidation.md and one row in
TODO/claude-todo/README.md. No code, no config, no schema.

(The two-dot diffstat looks enormous — ~232k lines across 172 files — but that
is the 130 commits develop has moved since the merge-base showing up as
reverse changes. The PR's real content is the two files above.)

What I looked at

The doc itself, closely, because a planning doc that gets its numbers wrong is
worse than no doc. I checked its measurements against the live code rather than
taking them on trust, and cross-read it against the two PRs in this review
batch it touches on (#68/#373).

Findings

No defects. This is unusually good technical writing and I want to say why.

It is right about the things that are easy to get wrong:

  • It measures from the code, not from a laptop. §1 explicitly calls out that
    a dev machine accumulates the union of every branch ever run against the same
    local mongod (63 local vs 48 real), and that sibling worktrees resurrect
    indexes a newer branch retired. That is a genuinely non-obvious trap and it is
    the single most valuable paragraph in the doc.
  • It knows why grep undercounts. The audit recipe notes that
    grep "name: '…'" misses auto-named indexes and everything created through
    createIndexReplacing(), and gives a stub-driver recipe instead. I ran that
    approach against Clarify cross-deployment account-hint environments #373's branch and it reported exactly the number its own
    guard test asserts — the recipe works.
  • It refuses the tempting wrong answer. §3a's explanation of why a zero
    production count does not license dropping the kind_* indexes — that an
    $or only gets an index-union plan when every branch is indexed, so
    dropping one branch's index collection-scans the feed even when zero documents
    carry the field — is correct, and it is precisely the reasoning that would be
    skipped by someone working from a row count.
  • It separates write-dead from data-dead. The parentId_1_ownerId_1_token_1
    analysis (no reader to retire first, but still the only uniqueness guard over
    surviving legacy kind: 'reaction' docs, and things_reaction_unique has a
    different key shape so it cannot inherit the constraint) is the kind of
    distinction that prevents a silent data-integrity regression.
  • It states the real failure mode. §2 traces the cap to
    ensureIndexes → one Promise.allregisterUser awaits it, therefore
    a single bad index means registration and login 500. And it notes
    createIndexReplacing is create-then-drop, so a swap needs a free slot —
    meaning the cap must never be reached, not merely not exceeded. That
    reframes the ceiling correctly.
  • It does not duplicate work. §4 step 5 explicitly points at the
    indexBudget.test.ts guard already carried on the
    codex/thingtime-mcp-desktop-connectors lineage (Add persistent desktop mesh and live AI chat connectors #68, and Clarify cross-deployment account-hint environments #373 stacked on it)
    and says to adopt that file rather than write a second guard — including the
    detail that it does not port standalone because develop exports none of the
    three symbols it imports.

That last point is now more than theoretical. While reviewing #373 in this
same batch I found its only failing check is exactly that guard:

✖ current Things index plan keeps four slots free below MongoDB hard limit
  AssertionError: Things index plan uses 62/64; reserve 4 slots for safe upgrades

I dumped the plan on that branch: 60 distinct indexes from
createThingsDataIndexes(), plus _id_, plus the home-only
migration_diagnostic_expires_at TTL = 62. So this doc's "48 in use, 15
slots of headroom" is accurate for develop, and the #68 lineage has already
spent that headroom and 2 more — things_device_* ×8, things_external_* ×4,
things_ai_connection_key_unique, plus things_chat_community,
things_dm_key_lookup, things_thread_root.

The doc's framing ("Nothing here is urgent; production has headroom") is true of
develop and I would not change it — but it is worth adding one line noting
that the #68 lineage is already over the guard's ceiling, so the work becomes
blocking the moment that branch merges. That is a content suggestion for the
author, not a defect.

Changes made

None. It is a docs-only PR, its numbers check out against the live code, and
its plan is sound. Editing it would be churn.

Validation run

  • Verified the headline measurement by driving createThingsDataIndexes() with
    a stub collection (the doc's own §1 recipe) on the Clarify cross-deployment account-hint environments #373 worktree: 60 distinct
    names, listed and cross-checked against the file.
  • Confirmed remix/app/api/utils/mongodb/indexBudget.test.ts exists on the
    codex/thingtime-mcp-desktop-connectors lineage and not on develop
    (git cat-file -e 2598986:… → missing; added by ca63802, not an ancestor of
    develop) — exactly as §4 step 5 claims.
  • Confirmed the three symbols it says develop does not export
    (createThingsDataIndexes, RETIRED_THINGS_INDEXES,
    pruneRetiredHomeThingsIndexes) are indeed part of the Add persistent desktop mesh and live AI chat connectors #68 change.
  • Checked the README.md table row renders in the existing column shape and
    links to the right filename.
  • CI on 7a0582b: 27 checks, all pass.

CodeQL

16 open alerts on this head — every one pre-existing on develop, and this PR
adds no code at all. Inspected each; 11 disposed with evidence (4
Math.random() uses in disposable-environment verification scripts →
used in tests; 7 render-time DOM/debug identifiers and one dead unreferenced
stub → false positive).

5 deliberately left open#18/#19 (js/prototype-pollution-utility in
app/smarts/index.tsx), #49 (js/stack-trace-exposure into
app/api/http.ts), #45 (js/cors-permissive-configuration in
deprecated/api), #17 (js/double-escaping in the Raycast helper). Each is
real or not demonstrably inapplicable. Attaching a fix for any of them to a
docs-only TODO PR would be exactly the churn this repo's conventions warn
against; they want their own PRs.

View Lopu workflow run

github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 26, 2026
github-actions Bot added a commit that referenced this pull request Aug 26, 2026
github-actions Bot added a commit that referenced this pull request Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Lopu — I re-derived your numbers from develop's source; they all check out

Rather than review the prose, I measured. I lifted createThingsDataIndexes out of develop's collections.ts at c9bbd46a and counted its index creations statically — valid because I first confirmed the body contains no loops, spreads or .map(), so call sites and indexes are one-to-one:

Claim Measured on develop
47 from createThingsDataIndexes() 36 col.createIndex( + 11 createIndexReplacing( = 47
+1 home-only TTL beside the spread ensureIndexes spreads createThingsDataIndexes(db) and adds exactly one col('things').createIndex(…), migration_diagnostic_expires_at
48 code-defined (+ _id_ = 49) 49
15 slots of headroom 64 − 49 = 15

The §3c family breakdown is exact too — I grouped all 47 key specs by leading field: thingtime ×7, kind ×4, crystal.quotaKind ×4, targetId ×3, ownerId ×3, appId ×2. Every one matches.

And the §4.5 portability claim: RETIRED_THINGS_INDEXES, pruneRetiredHomeThingsIndexes and an exported createThingsDataIndexes appear zero times in develop's collections.ts, exactly as stated — so indexBudget.test.ts genuinely cannot be cherry-picked standalone.

Your warning that grepping name: '…' undercounts is well founded, incidentally: a naive name: grep misses all 11 createIndexReplacing sites.

Two live data points that strengthen §4 step 5

Your "partly done elsewhere" note is accurate and current — I confirmed indexBudget.test.ts on PR #373's base (codex/thingtime-mcp-desktop-connectors, 13a6b7ca) does exactly what you describe. Two things I saw while reviewing that stack are worth adding here:

  1. That test has already caught a real self-contradiction. On Clarify cross-deployment account-hint environments #373's base, things_passkey_link_key_unique was listed in RETIRED_THINGS_INDEXES and created by createThingsDataIndexes — so every ensureIndexes() dropped and rebuilt a live unique constraint that auth/passkeys.ts:511 depends on to resolve its upsert race, leaving a window with no uniqueness. The test failed on it; Clarify cross-deployment account-hint environments #373 fixes it. The guard earning its keep before it is even adopted.
  2. Your §2 failure mode is not hypothetical, and it is getting worse. registerUser awaits ensureIndexes() before its constrained writes, so a heavy bootstrap sits on the critical path of the first sign-up after a deploy. On Clarify cross-deployment account-hint environments #373's stack that bootstrap grew a device-index layout migration and is currently timing out [auth] Service account creation at 12 s in CI — the first account-creating test in the table, and the only one still on the default timeout. Registration is coupled to index work today exactly as §2 says; it just fails slow instead of fast.

Worth adding to §4, or to §5's definition of done: decouple ensureIndexes() from registerUser's request path, the way ensureCustomDataIndexes is already fire-and-forget for custom endpoints. The budget guard stops the cap being hit; it does not stop a slow ensure sitting in front of sign-up.

One more worked example for the rule

PR #382 (components runtime, also in this batch) adds a whole component kind with browse filters on crystal.familyKey, crystal.componentKey and crystal.category — and deliberately adds no new things indexes, riding the thingtime index and filtering in memory. That is the right instinct given this budget, and the first PR I have seen make the trade explicitly. If §4 lands a written rule ("things has an index budget; adding one is a decision"), #382's choice is the example to cite.

Content

§3a's ordering argument is the reasoning I checked hardest, and it is right: for an $or, MongoDB builds an index-union plan only when every branch has a usable index — drop kind_1_createdAt_-1_shareId_1 while postMatch() still names kind and the planner has no choice but a COLLSCAN over things_v2, even at zero matching documents, because it cannot know the count in advance. With postMatch()'s seven call sites covering feed, profile lists, public post count and share-original lookups, getting that order wrong would be site-wide, not local.

Two optional nits: §1's "63 — what a long-lived local dev database showed" row invites a misread as a fourth measurement (worth marking "not a budget reading" in the cell, since it is the number someone will quote from memory); and §4 step 5 proposes a ceiling of 56 while indexBudget.test.ts encodes headroom-of-4 (60) — you already flag the reconciliation, and picking one now would save a round trip when the file is adopted.

No changes requested.

@github-actions

Copy link
Copy Markdown
Contributor

🦉 Lopu — the doc's numbers hold; the PR description contradicts them by one

I re-derived the census independently rather than re-reading my own earlier confirmation — bracket-matched the return [ … ] array of createThingsDataIndexes (collections.ts:425-794) and counted both creation forms:

36 col.createIndex + 11 createIndexReplacing = 47, plus migration_diagnostic_expires_at at collections.ts:896-902 (home-only, created outside the shared battery) = 48 code-defined, plus _id_ = 49, leaving 15 slots under the 64 cap. Exactly what §1 claims, on both this head and develop @ 4618fffe.

Your methodology note is the reason it lands: rejecting grep -oE "name: '…'" as an undercount is right, and the size of the error is worth stating — 11 of the 47 come through createIndexReplacing, so the naive grep would have been off by roughly a quarter.

The one correction — in the description, not the doc

49 code-defined indexes on things (+_id_), so ~14 slots of headroom

The doc says 48 code-defined, _id_ making 49 total, and 15 slots. The body double-counts _id_. The doc is the one that matches the code — worth fixing the description, since this number is the entire point of the file and the summary is what people will quote.

Same paragraph, softer: "kind and visibility … appear in collections.ts only inside index definitions — no reader, no writer." Scoped to collections.ts that's literally true, but it reads as a much stronger claim than §3a makes — and §3a is better than the summary, because it correctly tables the live readers elsewhere (things.ts postMatch/postThingMatch/thingtimeInClause, search.ts, views.ts). I verified postMatch() at things.ts:676 really is { $or: [{ thingtime: 'post' }, { kind: 'post' }], … } with live call sites. §3a's central warning is the load-bearing insight of the whole doc and it's correct: for an $or, MongoDB only builds an index-union plan when every branch is indexed, so dropping kind_1_createdAt_-1_shareId_1 while postMatch() still names kind would collection-scan the feed even at a zero kind document count. Don't let the looser summary be what survives into someone's memory.

CodeQL — repo-wide adjudication recorded on this PR

This PR changes only Markdown, so it introduces none of the 16 open alerts on its head; the identical 16 sit on #382 and #135, which share no code with it or each other. Since alerts are repo-global I adjudicated the set once here rather than writing the same array into four files.

Dispositioned — 12, each with file/line evidence: #7 (userGenerateJWT.ts is a dead stub — both locals unread, returns undefined, zero callers repo-wide), #8 and #14 and #26 (React key / DOM id / CSS animation-name identity only), #27 and #28 (debugUuid is assigned and never read — two occurrences in the file, both assignments), #29 (in-memory undo/redo timeline label, no call site reads .uuid back), #49 (admin-gated behind requireAdmin before anything is built, withAdminPrivateResponse, redacted by captureAdminErrorDiagnostic with its own hostile-accessor test suite, and exact-owner scoped on read), and #77-80 (collision-avoidance suffixes for throwaway accounts in remix/scripts/verify-*.mjsused in tests).

Left open — 4, because I judge them potentially real and won't dismiss a finding to green a check:

  • fix(graphify): atomic graph/manifest merges, untrack graph.html, track semantic cache #45 js/cors-permissive-configurationdeprecated/api/src/index.js:41, cors({ origin: '*' }) plus a socket.io origin: '*'. It lives under deprecated/, but "probably not deployed" is not evidence. Either delete the retired express app or add deprecated/** to the CodeQL paths-ignore.
  • [codex] Document PR #16 follow-up review notes #17 js/double-escapingraycast/…/regexToReplacementConverter.tsx:57. Genuine ordering wart: \{ → { runs before \\ → \, so a doubled backslash is unescaped twice.
  • Add PR review agent guidance #18 / Add AI idling notes and council advice #19 js/prototype-pollution-utilityremix/app/smarts/index.tsx:1225 and :1247. merge() copies Object.keys(value2) onto value1 with no __proto__/constructor/prototype guard, and JSON.parse output carries __proto__ as an own enumerable property. smarts has no API/server importer, so the exposure is browser-side — but attacker-authored thing content does reach the state merge composes. A one-line key guard, in its own PR against develop rather than a drive-by on an unrelated branch.

Nothing changed in this worktree — the doc is accurate and editing it to restate numbers it already states correctly would be churn.

— 🦉 Lopu, Thingtime's PR manager

github-actions Bot added a commit that referenced this pull request Aug 26, 2026
github-actions Bot added a commit that referenced this pull request Aug 26, 2026
github-actions Bot added a commit that referenced this pull request Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Lopu — numbers still hold at the current develop tip; one worked example from this batch

Re-ran the measurement against develop @ b7e2906 (this PR's base) rather than the c9bbd46a I used at 09:13, to check the figure hasn't drifted since:

Measured
createThingsDataIndexes() 36 col.createIndex( + 11 createIndexReplacing( = 47 ✅ unchanged
+1 home-only TTL migration_diagnostic_expires_at, beside the spread in ensureIndexes()
+_id_ 49 total
headroom 64 − 49 = 15

Identical at the PR head and at the develop tip, so §1's 2026-08-25 measurement is still current.

Your undercount warning holds up under a direct count too: only 14 of the 47 carry an explicit name:, so a naive name: grep misses two-thirds of them, not just the createIndexReplacing sites. The stub-driver recipe really is the only honest method.

Also re-confirmed §4.5's portability claim against this exact base: createThingsDataIndexes, RETIRED_THINGS_INDEXES and pruneRetiredHomeThingsIndexes are all three unexported on develop, and indexBudget.test.ts doesn't exist there — so the guard genuinely cannot be cherry-picked standalone.

The createIndexReplacing catch deserves more billing than it gets

Buried in §2 is the sharpest observation in the document: createIndexReplacing is create-then-drop, so an index swap needs a free slot, and error 67 isn't among the 85/86 codes that helper retries. That reframes the whole budget — the cap must never be reached, not merely never exceeded, so "15 slots of headroom" is really "15 minus whatever a future swap needs to breathe". If any single line from this doc ends up in FUNDAMENTALS.md §3, I'd argue for that one over the raw count.

A worked example from this same review batch

#382 (components runtime) adds an entire component thing kind with browse filters on crystal.familyKey, crystal.componentKey and crystal.category — and deliberately adds no new things indexes, riding the thingtime index and filtering in memory instead. That's the first PR I've reviewed that makes this trade explicitly rather than by omission. If §4 step 6 lands a written rule ("things has an index budget; adding one is a decision"), #382 is the example to cite for what complying with it looks like.

CodeQL

16 alerts, none in this PR's two changed files — a docs-only diff can't introduce a JS alert.

New since my last pass: I've dispositioned the 11 that are demonstrably not applicable, each with its own evidence — #7 (the userGenerateJWT stub whose uuid is assigned, never read, in a function no code imports), #8/#14/#26/#27/#28/#29 (React keys, a DOM id, a CSS @keyframes name, two dead debugUuid locals, an in-memory undo/redo timeline id — none a credential), and #77#80 (verify-app-storage.mjs / verify-admin-subscriptions.mjs, developer scripts whose random suffix only uniquifies throwaway test accounts). Those had been sitting open across every develop-based PR in this batch.

Left open as real or unproven: #17, #18, #19, #45, #49. #18/#19 is the one worth acting on — smarts/index.tsx merge() walks Object.keys(value2) and assigns straight through with no __proto__ guard, and JSON.parse produces __proto__ as an own enumerable key, so it's genuine. It wants a dedicated PR: the identical alert rides #135/#185/#382/#405, so four branch-local fixes would collide on merge rather than help.

No changes requested — the document is accurate and I'd merge it as is.

github-actions Bot added a commit that referenced this pull request Aug 26, 2026
github-actions Bot added a commit that referenced this pull request Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Lopu — I re-derived this doc's numbers from develop rather than reading them, and they hold up. One step needed correcting, and there is a new claimant on the budget you should know about.

The measurements reproduce exactly ✅

Driving the real createThingsDataIndexes() with a stub collection — the method the doc itself prescribes — gives 47 distinct index names, so 47 + 1 home-only TTL + _id_ = 49, exactly as written. postMatch() really does have seven call sites (things.ts 2262, 2311, 2345, 2347, 2348, 4214, 4240). Every cited reader, error code and cross-PR claim checks out, including that indexBudget.test.ts on the #68 lineage asserts headroom-of-4 and imports three symbols develop doesn't export.

The §3a insight is the most valuable thing here and it's correct: dropping a kind_* index while postMatch() still $ors on kind collection-scans the feed even at zero matching documents, because the planner cannot know the branch is empty.

⚠️ Correction I made — §4 step 2 could have dropped a live uniqueness guard

This mattered because step 2 was the only action marked safe to do immediately, independent of the census:

Retire parentId_1_ownerId_1_token_1 now — it is inert regardless of the census

That index is { parentId, ownerId, token }, unique, partialFilterExpression: { kind: 'reaction' } (collections.ts:607). It is write-dead, but not data-dead, and the stated justification doesn't hold:

  • things_reaction_unique does not supersede it. That index is { targetId, ownerId, 'crystal.emoji' } (collections.ts:570-577) — the v2 expression of the same product invariant on a different key shape. It can never match a legacy { parentId, token } document.
  • collections.ts:603-605 says so at the definition site: "Legacy relational era … aggregation + dedup indexes stay until the things migration converts those docs to thingtime things."

Dropping it before the migration completes removes the last dedup guard on any surviving kind: 'reaction' doc. I rewrote the three places carrying that claim to say what's true — it's the one entry with no reader to retire first, so it skips steps 2–3 of the read-path work, but it still needs the step-1 census — and added db.things_v2.countDocuments({ kind: 'reaction' }) to that census, since step 2 now depends on it.

📌 New claimant on the budget: PR #382

PR #382 (reviewed in this same batch) lands the component kind with /api/v1/components/browse, and adds no indexes at all — while its stated sibling /api/v1/schemas/browse has two ({thingtime, crystal.schemaId} and {thingtime, crystal.schema} at collections.ts:538-539). Its per-page usageCount aggregate on crystal.componentKey currently rides the thingtime_1_* prefix and then scans every component doc.

I deliberately didn't add it there, precisely because of this doc — spending 1–2 of the 15 free slots is a decision, not a reflex. But it belongs on this plan's ledger: ~15 free today, minus 1–2 for components is the number the ceiling in §4 step 5 should be reconciled against.

CodeQL

I filed 4 used in tests dispositions here (alerts #77#80, the verify-app-storage.mjs / verify-admin-subscriptions.mjs harnesses). They live on this PR's file only because the disposition validator aborts the whole session if one alert number is proposed twice, and these appear in four PRs' snapshots — nothing to do with this PR's content. verify-app-storage.mjs in particular exits 2 unless argv[2] matches ^https?://(?:127\.0\.0\.1|localhost)(?::\d+)?/?$, so it is structurally incapable of targeting anything but loopback.

The other 12 I left open — they're real (smarts/index.tsx prototype pollution, a dead userGenerateJWT.ts stub, deprecated/api CORS) and want one dedicated hygiene PR against develop, not four conflicting copies across unrelated branches.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Deterministic product contract advisory

These examples are warning-only. They do not fail the build/API contexts or block this PR.

  • ✅ Web CI required-context examples
  • ⚠️ Thin-listener topology examples (exit 1)
Sanitized tail
node:internal/modules/run_main:123
    triggerUncaughtException(
    ^

AssertionError [ERR_ASSERTION]: all-branch.yml must stay retired; Lopu PR manager owns its former public triggers

true !== false

    at file:///home/runner/work/thingtime/thingtime/remix/scripts/workflow-caller-contract.mjs:39:10
    at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:681:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5) {
  generatedMessage: false,
  code: 'ERR_ASSERTION',
  actual: true,
  expected: false,
  operator: 'strictEqual',
  diff: 'simple'
}

Node.js v22.23.2

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Lopu — a live counter-example to “nothing here is urgent”: one lineage is already 2 over the ceiling

Nothing to change in the document — my 15:34 verification of §1's numbers still stands against develop. But something moved after that pass that bears directly on §4 step 5, so recording it here.

While reviewing #373 in this batch, I found its only failing check is the very guard this doc points at:

✖ current Things index plan keeps four slots free below MongoDB hard limit
  AssertionError: Things index plan uses 62/64; reserve 4 slots for safe upgrades

Driving createThingsDataIndexes() with the stub recipe from §1 on that branch: 60 distinct indexes, + _id_ + the home-only TTL = 62. Your develop figure of 47 (+2 = 49, 15 slots free) is confirmed correct — the extra 13 all come from the codex/thingtime-mcp-desktop-connectors lineage (#68): things_device_* ×8, things_external_* ×4, things_ai_connection_key_unique, plus things_chat_community, things_dm_key_lookup, things_thread_root.

Two things this validates about the doc:

  1. §4 step 5 was right to point at indexBudget.test.ts instead of writing a second guard. It is doing its job right now — it is the thing that caught this, on a branch nobody was watching the budget on.
  2. Your §1 dev-machine warning was not hypothetical. The names you listed as "exist in no current code" on a laptop — things_device_*, things_external_*, things_ai_connection_key_unique — are exactly the 13. They were never laptop residue; they are a real feature branch's real indexes, which is a sharper version of the same point: always measure from the code, and say which code.

One suggested line

§1's framing — "Nothing here is urgent; production has headroom" — is true of develop and I would keep it. But it is worth one sentence noting that the #68 lineage is already over the guard's ceiling, so this work becomes blocking the moment that branch merges rather than at some indefinite future point. That converts "no rush" from a hope into a dated, checkable claim, which is the register the rest of the document is written in.

I have posted the full breakdown on #68, including the bisection to 126dc2d (the develop merge that crossed the line) and a note that §3c's consolidation map is the right place to start — those eight crystal.*_key_unique single-field partial indexes are the most consolidatable-looking family in the plan.

Still no changes requested here.

— Lopu 🤖

github-actions Bot added a commit that referenced this pull request Aug 26, 2026
github-actions Bot added a commit that referenced this pull request Aug 27, 2026
github-actions Bot added a commit that referenced this pull request Aug 27, 2026
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