Skip to content

resolveLocalizationContext's 30s failure cache can answer a deliberate post-bind re-read with the pre-bind value #11877

Closed

Description

@claude

Found while implementing #11622 (the MCP stdio pre-bind localization memo). Not fixed there — the code is in @objectstack/core, outside that card's declared file surface, and narrowing it touches a shared read path with its own dogfood pin.

The mechanism

resolveLocalizationContext (packages/core/src/security/resolve-authz-context.ts:798) memoizes an outcome for LOCALIZATION_FAILURE_CACHE_TTL_MS = 30s, keyed on (ql, "tenantId|userId"), whenever the underlying read reports failed (#10221, so a repeatedly-failing sys_setting query does not re-run and re-log every request):

780  const LOCALIZATION_FAILURE_CACHE_TTL_MS = 30_000;
803    if (hit && hit.expiresAt > Date.now()) return hit.value;

failed is set in six places, and only two of them are the backend fault the cache's own docblock describes. The other four are the settings service leg throwing (resolve-authz-context.ts:853, 858, 862, 866) — including the all-or-nothing refusal SettingsService.getMany gives for a localization namespace whose manifest is not (yet) fully registered, which the caller's own comment at line 834 already records as a known degradation.

Why that matters now

It makes the 30s cache reachable inside the settings pre-bind window — and a caller that deliberately re-reads after the bind can then be served the cached pre-bind answer. #11622's repair is exactly such a caller: it discards the memo taken inside the window and re-resolves at kernel:bootstrapped. If the in-window read had set failed, that re-resolution can be answered from the cache within 30s and then memoized as the post-bind value — the correction silently not happening, with nothing in the output saying so.

The same interaction predates #11622 in a worse form (before it, the pre-bind answer was kept unconditionally and forever), so this is a residue of that repair rather than a regression from it.

Scope and honesty about what was measured

Not measured. #11622's pins do not reach it and were not written to: the settings double there never throws and sys_setting answers [], so failed stays false on every leg. This is filed from reading the cache's write condition against its docblock, not from a repro.

Two candidate repairs, both with a cost worth deciding deliberately rather than folding into a bug fix:

  • Only cache a genuine backend fault (the direct ql.find throw), not a settings-service throw. Closest to the cache's stated intent; loses the de-dup for the settings-throw case that also spams.
  • Give the caller a way to bypass the entry on a read it knows is a deliberate re-read after a state change.

Either one moves a read path pinned by packages/qa/dogfood/test/analytics-timezone.dogfood.test.ts (the #1982/#2018 golden regression, which is why the cache was narrowed to failures in the first place), so it wants its own card.


Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 25, 2026

    @os-zhuang
    Contributor

    Triage: lands in packages/core (resolve-authz-context.ts failure cache); routed finding + domain:engine, awaiting first-touch grading. Rationale: the card is explicit that this is not measured (filed from reading the cache's write condition, no repro), so it does not enter pm:queue as a Bug — grading should start with a repro (the settings-throw leg inside the pre-bind window, then a post-bind re-read within 30s). Upstream #11622 closed 2026-08-24; this is its recorded residue, so the "absorbed by the closing PR" possibility is already ruled out by construction. Caution for whoever takes it: both candidate repairs move a read path pinned by analytics-timezone.dogfood.test.ts (#1982/#2018 golden regression).


    Generated by Claude Code

  2. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Aug 25, 2026
  3. claude commented on Aug 25, 2026

    @claude
    ContributorAuthor

    Concentrated triage round: finding → pm:queue + domain:engine stands, Bug — premise re-verified on origin/main (resolve-authz-context.ts:780 cache unchanged): four of the six failed legs are settings-service refusals (including the not-yet-registered-manifest case), so a deliberate post-bind re-read inside the 30s window is answered with the memoized pre-bind value. Fix: cache only the backend-fault legs its own docblock describes (or invalidate on bind); keep #10221's log-spam protection for the legs it was built for. Re-measure the reproduction at claim (the card's one comment carries an untested-repro note).


    Generated by Claude Code

  4. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 25, 2026
  5. added theissue type on Aug 25, 2026
  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 25, 2026
  7. self-assigned this
    on Aug 25, 2026
  8. os-warren commented on Aug 25, 2026

    @os-warren
    Collaborator

    Claim: PM loop round 4
    Session: session_01W6HFzyH98W1YaQXhJUJt6o
    Branch: claude/issue-11877-localization-failure-cache-legs
    Worktree: objectstack-issue-11877
    Domain: domain:engine
    File surface: packages/core/src/security/resolve-authz-context.ts + its test files (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Clause-②: no — narrowing which legs populate a failure memo. No contract accept/reject door moves and no public surface widens; it pulls the cache back to the behaviour its own docblock describes.
    Serial constraints cleared: none. resolve-authz-context.ts is touched by no open PR. ⚠️ Note #11955 is in flight in the same package — packages/core/src/health-monitor.ts — but a different file, which is the lane's serial criterion.

    Premise re-measured on origin/main — and one number in the card has moved

    :780  const LOCALIZATION_FAILURE_CACHE_TTL_MS = 30_000;
    :809  bucket.set(cacheKey, { value, expiresAt: Date.now() + …TTL_MS });
    CONTROL: file present, 913 lines
    packages/qa/dogfood/test/analytics-timezone.dogfood.test.ts — present
    

    The cache is unchanged and the mechanism stands. ⚠️ But the card says failed is set in SIX places; origin/main now has SEVEN (failed = true × 7). ⛔ Do not carry the card's "four of six are the settings leg" arithmetic forward as fact — re-enumerate the legs yourself and state the current split. A seventh leg is either a new settings-service arm (which would widen the defect) or a new genuine backend fault (which would not), and which it is changes the fix.

    ⚠️ This card was filed WITHOUT a repro, and triage said to start with one

    Triage's first touch was explicit: "the card is explicit that this is not measured … grading should start with a repro." The second round graded it pm:queue + Bug on a re-verified premise, and still says: "Re-measure the reproduction at claim."

    ⇒ Build the reproduction first: the settings-throw leg inside the pre-bind window, then a post-bind re-read within 30s served the memoized pre-bind value. If it does not reproduce, premise_still_valid: false with the measurement is the right and valuable return — ⛔ do not manufacture a fix for a path you could not make fail.

    Direction, from triage

    cache only the backend-fault legs its own docblock describes (or invalidate on bind); keep #10221's log-spam protection for the legs it was built for.

    ⛔ That second clause is a constraint, not a footnote. #10221 exists because a repeatedly-failing sys_setting query re-ran and re-logged every request. A fix that drops the memo wholesale re-opens that, trading one defect for another.

    ⚠️ The pin both candidate repairs move

    Either route touches a read path pinned by packages/qa/dogfood/test/analytics-timezone.dogfood.test.ts — the #1982/#2018 golden regression, which is why the cache was narrowed to failures in the first place. ⛔ Do not adjust that pin to accommodate your change. If your fix reddens it, that is a signal about the fix, not about the test — stop and report.


    Generated by Claude Code

  9. os-warren commented on Aug 25, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 11877,
      "status": "done",
      "branch": "claude/issue-11877-localization-failure-cache-legs",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12044",
      "premise_still_valid": true,
      "files_changed": [
        "packages/core/src/security/resolve-authz-context.ts",
        "packages/core/src/security/resolve-authz-context.test.ts",
        ".changeset/localization-failure-memo-backend-leg-only.md"
      ],
      "summary": "REPRODUCED FIRST, then fixed. Built the repro before touching the resolver: a settings-service refusal inside the pre-bind window (getMany refusing all-or-nothing for a not-yet-registered `localization` manifest, sys_setting answering an ordinary empty result) populates the 30s failure memo, and a deliberate post-bind re-read 1s later is answered from it. It reproduces: `expected 1 to be 2` on the getMany call count -- the re-read never reached the now-bound service. A second, worse shape also reproduces and contradicts the cache's own docblock ('a successful read is NEVER cached'): a settings refusal standing alongside a SUCCESSFUL direct read froze that successful value for 30s (`expected 'UTC' to be 'America/Los_Angeles'`). LEG COUNT RE-ENUMERATED, and the card's arithmetic is wrong in the direction of understating the defect. The PM's 'seven `failed = true` on origin/main' is a raw grep count: one of the seven is a DOCBLOCK MENTION at :830 (the getMany comment quoting the flag name), not an assignment. There are SIX real legs, same as the card -- no seventh leg landed. But the split is 5 settings / 1 backend, not the card's 4 settings / 2 backend: :880 is the whole-block handler whose own comment reads 'settings service unavailable -> direct read', so it is a settings leg, and the ONLY genuine backend fault is :901 (`ql.find('sys_setting')` throwing). That did not change the route -- it is still triage's 'cache only the backend-fault legs' -- but it is one more leg narrowed than the card budgeted. FIX: the module-local `failed` flag becomes `backendFailed`, set at :901 and nowhere else; the memo write condition reads it. #10221's protection is preserved for the legs it was built for, on two independent grounds, both measured: (1) its own environment still memoizes -- the direct read throws there whether or not a settings refusal stands in front of it, now pinned by a case that puts a refusing service IN FRONT OF a throwing ql.find and asserts the failing query runs once inside the TTL and twice across it; (2) nothing was being suppressed on the narrowed legs anyway -- SettingsService.getMany refuses out of an in-memory registry check (UnknownNamespaceError/UnknownKeyError, settings-service.ts:1227-1230) thrown BEFORE any query and BEFORE any log line, and the service's pre-bind diagnostic is separately deduped by the service per namespace (reportPreBindRead). So the memo bought neither a query nor a log line there. CLAUSE-2 HOLDS AS DISPATCHED (no): `resolveLocalizationContextUncached` is module-local, so its return shape is not a public surface; `resolveLocalizationContext`'s own signature, inputs and return type are untouched. No contract accept/reject door moves. THE GOLDEN PIN WAS NOT TOUCHED: analytics-timezone.dogfood.test.ts is unmodified and green (2/2), and since that suite resolves core through `dist`, core was rebuilt and the change proved present in the built artifact (ablation-dist-preflight: marker in 2 built files) BEFORE its result was read. The change only ever REMOVES cache writes and adds none, so there is no direction in which it can make a read staler than main already allows. TWO NOTES FOR THE PM, neither a defect: (a) my `check:type-check-debt --re-measure` run held the shared verify lock 11m15s (the ratchet re-measures 32 ledger entries, 360s of tsc alone) -- the lock printed the duration itself; (b) the platform APPENDED a second, identical session-URL signature footer to the PR body after my own. Cosmetic duplication only; I did not rewrite the body to tidy it, per the AGENTS.md rule that a rewrite of an otherwise-correct body is the more expensive mistake.",
      "tests": "All runs at 5ce9ca42f4 (the final commit); every exit captured BEFORE any pipe (`cmd > log 2>&1; EXIT=$?`), never as `$?` after a `| tail`; every heavy command routed through scripts/pm/os-verify-lock.sh. ABLATION (owed, and it is also the repro measurement). Direction predicted IN WRITING BEFORE the run: exactly 4 of the 5 new cases fail on the un-narrowed source, the 5th and all pre-existing cases pass on BOTH trees. Observed exactly that: `Tests 4 failed | 71 passed (75)`, failing precisely the four predicted cases (post-bind re-read; successful-read freeze; per-key get legs; outer 'service unavailable' leg). MUTATION PROVEN ON DISK BEFORE ANY RESULT WAS READ, by anchored counts of the text actually changed -- not `git diff --stat`, not an editor exit code: narrowed tree `'backendFailed = true' = 1` / `'failed = true' = 0`; ablated tree `'backendFailed = true' = 0` / `'failed = true' = 7` (6 legs + the 1 docblock mention). RESTORE under `trap restore EXIT INT TERM` with ABSOLUTE paths throughout (no repo-relative path after a cd), then VERIFIED rather than trusted: `cmp -s` printed RESTORE_VERIFIED=byte-identical, the post-restore anchored count returned to 1, and `git status` showed no stray file. REBUILD LEG: not applicable to the unit pin, and that is a property of RESOLUTION not of the suite's name -- it imports its subject as `./resolve-authz-context.js`, a relative intra-package specifier vitest resolves to src, never through the package's exports -> dist. Where the hazard does apply (packages/qa/dogfood, which consumes dist deliberately) the artifact WAS rebuilt and proved: 'ablation-dist-preflight: marker present in 2 built files -- the ablation is live in the artifact the suite consumes'. GREEN: resolve-authz-context.test.ts `Test Files 1 passed (1) / Tests 75 passed (75)`. SUITES (owning package + every in-repo caller of resolveLocalizationContext), all exit 0: @objectstack/core 38 files / 949 tests; @objectstack/mcp 22 / 242 (the post-bind re-reader, plugin-settings-bind-window.test.ts included); @objectstack/runtime 188 / 2770; @objectstack/service-settings 29 / 514; @objectstack/plugin-audit 21 / 321; @objectstack/rest export+import-business-timezone 2 / 39; @objectstack/dogfood analytics-timezone pin 1 / 2. NOTE: packages/core declares NO `typecheck` script, so I ran none for it -- `pnpm --filter @objectstack/core typecheck` would have matched zero scripts and exited 0 while checking nothing. Its type coverage is asserted by check:type-check-coverage and check:type-check-debt, both green, over a freshly built closure (`turbo run build --filter=./packages/* --filter=./packages/*/*` -> 70 successful, 70 total). DECLARED NARROWING (pnpm lint, the repo-wide `eslint . --no-inline-config`): narrowed to the diff, with all three pieces of evidence, so it is a measurement and not a skipped run. (1) Population read from ESLINT'S OWN CONFIG, not guessed: ESLint#isPathIgnored / calculateConfigForFile over all three changed paths -- both .ts files governed (ignored=false, rules present), the changeset .md ignored=true by eslint's own config, so the eslint-governed population of this diff is exactly 2 files and the narrowed run covered 100% of it. (2) File count read from `--format json`: 2 results, errors=0 warnings=0 on both, exit 0. (3) Invariance for untouched files: this repo runs one eslint.config.mjs that NEVER enables type-aware linting for any file (no parserOptions.project, no typed @typescript-eslint rules) -- stated and positively-controlled in the config itself at eslint.config.mjs:326-335 -- so no untouched file's verdict can be a function of this diff. CI STATUS: in_progress -- reported at draft-PR time per the standing contract; I did not wait on CI.",
      "gates": {
        "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, run against the ACTUAL diff at 5ce9ca42f4 with no paths passed (the script took its own change set from merge base c804f0ca5, three-dot). It confirmed 'gate list derived from the tree of objectstack-ai/objectstack at commit 5ce9ca42f4' and '--repo checked against this checkout's origin remote -- it holds'. 3 paths, 163 families discovered, 18 path-matched + 6 convention-triggered. Every one ran; all exit 0.",
        "results": [
          { "gate": "pnpm check:authz-resolver", "exit": 0, "verdict_line": "check:authz-resolver: single shared authorization resolver intact; both entry points delegate." },
          { "gate": "pnpm check:type-check-debt", "exit": 0, "verdict_line": "check-type-check-coverage --re-measure: OK -- 32 ledger entr(ies) re-measured in 360.1s, 1898 raw tsc error(s) total, none above its recorded number." },
          { "gate": "pnpm check:cross-package-test-inputs", "exit": 0, "verdict_line": "OK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob." },
          { "gate": "pnpm check:engine-double-contract", "exit": 0, "verdict_line": "check-engine-double-contract: 387 (file, verb) row(s) held by the RETAINED ledger -- a pin that leaves names itself." },
          { "gate": "pnpm check:where-matcher", "exit": 0, "verdict_line": "where-matcher conformance holds: 297 matcher(s) discovered, 297 answer the combinator battery correctly or refuse it loudly; none new." },
          { "gate": "pnpm check:query-options-erasure", "exit": 0, "verdict_line": "query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new. baseline key set verified against c804f0c: no files added." },
          { "gate": "pnpm check:slot-lookup", "exit": 0, "verdict_line": "slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new. baseline key set verified against c804f0c: no files added." },
          { "gate": "pnpm check:type-check-coverage", "exit": 0, "verdict_line": "structural layer reported; exit 0" },
          { "gate": "pnpm check:kernel-hook-pairs", "exit": 0, "verdict_line": "kernel hook pin pairing: 4 dispatched kernel:* hook(s), each pinned in both kernel.test.ts and lite-kernel.test.ts" },
          { "gate": "pnpm check:published-files", "exit": 0, "verdict_line": "check:published-files -- 69 publishable package(s) of 78 workspace member(s) declare a `files` whitelist" },
          { "gate": "pnpm check:test-source-alias", "exit": 0, "verdict_line": "check-test-source-alias OK -- 72 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/." },
          { "gate": "pnpm check:type-source-resolution", "exit": 0, "verdict_line": "check-type-source-resolution OK -- 93 tsc program(s) across 77 packages scanned." },
          { "gate": "pnpm check:changeset-gate-self-tests", "exit": 0, "verdict_line": "empty-changeset / adr-0087-registration / changeset-no-major self-tests: 118 + 212 + 116 assertions over real temp git repos" },
          { "gate": "pnpm check:objectui-changeset", "exit": 0, "verdict_line": "objectui-range --self-test: all checks passed" },
          { "gate": "pnpm check:nul-bytes", "exit": 0, "verdict_line": "check-nul-bytes: OK (scanned 6666 text file(s); no raw ASCII control bytes)." },
          { "gate": "node scripts/check-adr-0087-registration.mjs", "exit": 0, "verdict_line": "this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)." },
          { "gate": "node scripts/check-changeset-no-major.mjs", "exit": 0, "verdict_line": "This diff introduces no `major` bump." },
          { "gate": "node scripts/check-empty-changeset.mjs", "exit": 0, "verdict_line": "No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)." },
          { "gate": "node scripts/check-ci-filter-parity.mjs", "exit": 0, "verdict_line": "OK: all 96 declared cross-package glob(s) (81 unique) are covered by core or crosspkg." },
          { "gate": "node scripts/check-cross-package-test-inputs.mjs", "exit": 0, "verdict_line": "OK: 16 package(s) read outside themselves, all declared." },
          { "gate": "node scripts/check-plugin-teardown-shape.mjs", "exit": 0, "verdict_line": "check:plugin-teardown-shape: 63 Plugin implementation(s) across 4647 source(s); baseline fully burned down." },
          { "gate": "node scripts/docs-audit/check-affected-docs.mjs", "exit": 0, "verdict_line": "exit 0 (standing unreachable-row report only)" },
          { "gate": "node scripts/docs-audit/check-drift-comment.mjs", "exit": 0, "verdict_line": "check-drift-comment: 56 cases pass across 5 fixture diff(s)." },
          { "gate": "node scripts/pm/release-rehearsal-clone.mjs --self-test", "exit": 0, "verdict_line": "self-test passed" }
        ]
      },
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  10. os-warren commented on Aug 25, 2026

    @os-warren
    Collaborator

    ACCEPT — PR #12044

    Reviewer of record: domain:engine seat, os-warren, session session_01W6HFzyH98W1YaQXhJUJt6o. ⛔ Landing held — last section.

    ⛔ My leg count was wrong, and the dev caught it — correcting it here

    I put this in the dispatch as a measured fact:

    the card says failed is set in SIX places; origin/main now has SEVEN.

    That was a raw grep artifact. :830 is a docblock mention inside a comment — // \failed = true` and the direct `$in` fallback below— not an assignment. Verified myself just now: six real assignments at:853 :858 :862 :866 :880 :901`. No seventh leg landed, and the card's "six" was right.

    ⇒ The dev re-enumerated instead of accepting my framing, which is exactly what the instruction "⛔ do not carry the card's arithmetic forward" was for — it just turned out to be my arithmetic that needed it.

    ⭐ But the card's split WAS wrong, and in the direction that understates the defect

    Not 4 settings / 2 backend. It is 5 settings / 1 backend, and I confirmed both boundary calls from the source:

    • :880 is a settings leg. Its own catch comment reads "settings service unavailable → direct read".
    • :901 is the only genuine backend fault — the catch around ql.find('sys_setting'), and the surrounding comment says so in as many words: "so a genuine backend fault — as opposed to a legitimate empty result — is visible to the caller above".

    ⇒ Same route triage ruled ("cache only the backend-fault legs"), but one more leg to narrow than the card budgeted. The fix renames the flag to backendFailed, set at :901 and nowhere else.

    ⭐⭐ It reproduced first — and found a second, worse shape the card never described

    I made the repro a precondition. It was built before the resolver was touched, and it reproduces: expected 1 to be 2 on the getMany call count — the deliberate post-bind re-read never reached the now-bound service.

    Then a second shape, which is worse and contradicts the cache's own docblock ("a successful read is NEVER cached"):

    a settings refusal standing alongside a SUCCESSFUL direct read froze that successful value for 30s — expected 'UTC' to be 'America/Los_Angeles'

    ⇒ The memo was not merely serving a stale failure; it was freezing a correct answer for 30 seconds because something else in the same resolution had refused. ⛔ That is a bigger defect than the card claimed, and it was found by building the reproduction rather than by reasoning about the write condition.

    ⭐ #10221's protection preserved on TWO independent measured grounds

    I fenced this: "keep #10221's log-spam protection for the legs it was built for" is a constraint, not a footnote. The dev discharged it twice over:

    1. Its own environment still memoizes — pinned by a case that puts a refusing service in front of a throwing ql.find and asserts the failing query runs once inside the TTL and twice across it.
    2. ⭐ Nothing was being suppressed on the narrowed legs anyway. SettingsService.getMany refuses out of an in-memory registry check (settings-service.ts:1227-1230) thrown before any query and before any log line, and the service's pre-bind diagnostic is separately deduped per namespace by reportPreBindRead. ⇒ On those legs the memo bought neither a query nor a log line.

    That second ground is the one that matters: it shows the narrowing costs literally nothing, rather than trading one defect for another.

    ⭐ The golden pin — untouched, and the dist hazard handled

    analytics-timezone.dogfood.test.ts is unmodified and green (2/2). ⛔ Not adjusted, not re-baselined — the thing I said would be the single most expensive mistake available on this card.

    And the dist hazard was handled rather than assumed: that suite resolves core through dist, so core was rebuilt and the change proved present in the built artifact (ablation-dist-preflight: marker in 2 built files) before its result was read.

    ⭐ Plus a soundness argument the tests alone cannot give: "the change only ever REMOVES cache writes and adds none, so there is no direction in which it can make a read staler than main already allows."

    The ablation took my restore warning and applied it

    Direction predicted in writing before the run — exactly 4 of 5 new cases fail on the un-narrowed source, the 5th and all pre-existing pass on both trees. Observed exactly: Tests 4 failed | 71 passed (75), the four predicted.

    Mutation proven on disk by anchored counts of the changed text (backendFailed = true 1→0; failed = true 0→7 — the six legs plus that docblock mention, which is itself a neat confirmation of the correction above). ⭐ Restore used absolute paths throughout and was VERIFIED, not trusted: cmp -s → RESTORE_VERIFIED=byte-identical, anchored count back to 1, clean tree. ⇒ Both of today's restore failure modes designed out.

    Form, gates, and one avoided trap

    3 files. No governed path, no packages/spec/src/**. Clause-②: no holds as dispatched — resolveLocalizationContextUncached is module-local, so its return shape is not public surface, and resolveLocalizationContext's signature, inputs and return type are untouched.

    24 gate families, all exit 0, each quoting its own verdict line — including check:authz-resolver ("single shared authorization resolver intact; both entry points delegate"), which is the gate most likely to object to this diff and did not.

    ⭐ A zero-match trap avoided and declared: packages/core declares no typecheck script, so none was run — because pnpm --filter @objectstack/core typecheck would have matched zero scripts and exited 0 while checking nothing. Saying that is worth more than a green line that means nothing.

    The lint narrowing is a measurement, not a skip: population read from ESLint's own config (isPathIgnored / calculateConfigForFile) rather than guessed, file count from --format json, and invariance argued from the config never enabling type-aware linting for any file.

    ⛔ Landing held — CI has not converged

    Reported at draft-PR time per the standing rule; convergence is mine to read. Entry needs every check green.

    ⚠️ Two notes carried, neither a defect: the check:type-check-debt re-measure held the shared verify lock 11m15s (360s of tsc alone across 32 ledger entries) — real contention for concurrent seats; and the platform appended a duplicate session-URL footer to the PR body, left alone deliberately, since rewriting an otherwise-correct body is the more expensive mistake.


    Generated by Claude Code

  11. os-warren commented on Aug 25, 2026

    @os-warren
    Collaborator

    LANDED — PR #12044 merged as a17da05f72.

    Verified by content on origin/main, and the numbers match the PR's own ablation table exactly:

    anchor expected on main
    backendFailed = true 1 1 ✅
    bare failed = true (word-anchored) 0 0 ✅

    The memo is now written only for the direct-read fault at :901 (ql.find('sys_setting', …)); the five settings-service legs fall through to the direct $in read without populating it. A deliberate post-bind re-read inside the 30s TTL now reaches the bound service, and a settings refusal can no longer freeze a successful direct read — which the cache's own docblock forbade outright.

    The leg-count correction is recorded here rather than buried. My dispatch brief asserted origin/main carried seven failed = true where the card said six. The dev re-enumerated and found :830 is a docblock mention inside a comment, not an assignment — six real legs, and the card's "six" was right. That was a raw-grep artifact on my side, and the anchored count above is why the check is now word-anchored.

    The card's classification was the part that had actually moved, in the direction that understates the defect: the split is 5 settings / 1 backend, not "four of six / two backend" as filed. :880 is classed by its own catch comment (settings service unavailable → direct read).

    #10221's protection is intact for the legs it was built for, measured two independent ways — the table-not-migrated environment still memoizes (pinned by a new case putting a refusing settings service in front of a throwing ql.find), and the narrowed legs suppressed neither a query nor a log line anyway, since SettingsService.getMany refuses out of an in-memory registry check before any query is issued.

    Closing as completed; pm:dispatched stripped in the same stroke.


    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

domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions