Repository navigation
plugin-reports: the documented setInterval dispatcher fallback is unreachable on ObjectKernel, and the job fallback it takes instead never fires #10746
Description
Found while fixing #10371 (teardown hook naming) in plugin-reports. Different defect, filed rather than fixed — that PR is one defect with one proof, and this one changes whether scheduled reports run at all.
The claim in the code
packages/plugins/plugin-reports/src/reports-plugin.ts:33-36:
The dispatcher uses
IJobService.schedulewhen one is registered; otherwise it falls back to a plainsetIntervalso single-kernel deployments work withoutservice-job.
start() implements exactly that: it tries ctx.getService<IJobService>('job') and only reaches setInterval when the lookup yields nothing with a schedule method.
Why the fallback can never be reached on ObjectKernel
ObjectKernel.preInjectCoreFallbacks() (packages/core/src/kernel.ts:261-275) walks ServiceRequirementDef and registers a CORE_FALLBACK_FACTORIES entry for every unprovided core service before Phase 2, i.e. before any kernel:ready hook runs. job is one of the five (packages/core/src/fallbacks/index.ts). So ctx.getService('job') ALWAYS resolves on an ObjectKernel, and the setInterval branch is dead code there.
And the thing it resolves to does not schedule. createMemoryJob() (packages/core/src/fallbacks/memory-job.ts) is honest about it in its own docblock:
trigger()really runs the registered handler, but nothing here owns a timer, so aschedule()d job NEVER fires on its own.
Its schedule() is jobs.set(name, { schedule, handler }) and nothing else.
Net effect
On an ObjectKernel stack without @objectstack/service-job, ReportsServicePlugin logs
ReportsServicePlugin: dispatcher registered with job service {"intervalMs":60000}
…and then dispatches nothing, ever. sys_report_schedule is never polled, no scheduled report is ever emailed, and the log line reads like success. The setInterval path the docblock offers as the single-kernel answer is unreachable.
Measured
Booting ObjectKernel + ObjectQLPlugin + ReportsServicePlugin({ dispatchIntervalMs: 5000 }) with no job plugin and a real SqlDriver, then counting engine.find('sys_report_schedule', …) on the engine the plugin captured at kernel:ready: 0 reads in 5600 ms, with the log line above present. The same stack on LiteKernel — which injects no fallbacks — takes the setInterval branch and reads on schedule. That contrast is what identifies the cause as the pre-injected fallback rather than anything in the reports plugin's own logic.
(The LiteKernel half of that measurement is now pinned in packages/plugins/plugin-reports/src/plugin-shutdown-releases-dispatcher.test.ts as part of #10371, with the mechanism written down at bootReportsLiteKernel. #10371 pins that the dispatcher is released at shutdown; nothing pins that it runs on the kernel real deployments use.)
Not asserted here
Three questions this finding does not decide:
- Where the repair belongs. Either
plugin-reportsstops treating "ajobservice exists" as "a working scheduler" (e.g. by reading__serviceInfo.status === 'degraded', which the fallback already declares), or thejobfallback stops advertising aschedule()it cannot honour, orServiceRequirementDefstops classifyingjobas pre-injectable. These are materially different contracts. - How wide the class is. Any plugin written as "prefer the platform job service, else own a timer" has the same shape against the same pre-injection. This finding measured only
plugin-reports. - Whether the degraded-fallback surface is meant to be consulted.
createMemoryJob()publishes__serviceInfo: { status: 'degraded', message }— a declared, machine-readable "I am not really scheduling". If consumers are expected to read it, no consumer in this tree does; if they are not, it is inert.
Activity
os-zhuang commentedon Aug 21, 2026
Triage: routed to the decision inbox (needs-user-decision, type Bug). The defect is confirmed and severe (scheduled reports silently never run on ObjectKernel without service-job), but the card's own "Not asserted" section is right that the three repair shapes are materially different platform contracts — whether a degraded fallback may advertise a schedule() it cannot honour is a declared-vs-enforced call that shapes every plugin written against the fallback system, so picking one is an architecture decision, not a mechanical repair. Chinese four-facet analysis below.
os-decision-facets(四棱卡面块;标记按字面文本写入)
一句话问题:客户配好了「每周一早上把报表发给管理层」,平台日志说「定时器已注册」,然后永远不发 —— 因为内核塞给报表插件的是一个只会记名单、从不叫号的假闹钟,而插件把「有闹钟」当成了「闹钟会响」。
选项与真实代价:
四棱:
置信缺口(本分析看不见什么):job 之外其余四个预注入 core fallback 是否也有「声明了兑现不了的能力」同款(A 若全面适用,排查面比本卡大);LiteKernel 路径已被 #10371 测试钉住,但 ObjectKernel + 真 service-job 的正常路径回归覆盖如何未核。裁后执行:按裁定立实施卡入对应车道(A = engine 车道动 core fallbacks + services 车道核插件;C = services 单卡),同形状类清点(卡内问题 2)随实施卡带谓词,本卡转裁决记录。
Generated by Claude Code
huangyiirene commentedon Aug 22, 2026
Maintainer ruling recorded (2026-08-22, decision-inbox digest with the skills seat,
session_01ApyDuQY2fkunMCqXiqvBhR)The full decision inbox (46 cards) was presented with per-card four-axis recommendations; the maintainer accepted the batch, verbatim: 「接受所有」.
Ruled: Option A — a fallback must not fake capability. The kernel's pre-injected
jobfallback stops advertising aschedule()it cannot honour (loud refusal, orjobcomes off the pre-injection list), soplugin-reportsfalls through to its ownsetIntervalpath and single-kernel deployments actually dispatch scheduled reports. This adopts the "declare only what you enforce" layering rule rather than asking every plugin to consult the degraded health flag; the same-shape class audit (the card's question 2) rides along with the implementation card as scoped in the triage analysis.State:
needs-user-decisionremoved →pm:queue(ready for dispatch).Generated by Claude Code
huangyiirene commentedon Aug 22, 2026
Triage (half-annotation repair — this card was queued without a lane): routed
domain:services.Anchoring rationale: the symptom, the measured pin, and the cheapest honest fix all sit in
packages/plugins/plugin-reports— the plugin treats "ajobservice resolves" as "a working scheduler", while the core fallback already declares its degradation machine-readably (__serviceInfo.status: 'degraded') and no consumer reads it. Consumer-side repair (consult the declared degraded flag, then own the timer) restores the docblock's promise without touching the kernel contract. The card's questions 2 (how wide the class is — every "prefer platform job service, else own timer" plugin) and 3 (whether__serviceInfois meant to be consulted at all) are real but bigger than this card: if the implementing dev confirms the class is wider than plugin-reports, file the census as a separate finding rather than widening this PR; if the fix ends up needing the fallback to stop advertisingschedule()(apackages/corecontract change), stop and report — that routing moves todomain:engineand possibly the decision inbox.Type
Bugstands (scheduled reports silently never run on the kernel real deployments use, while logging success).Generated by Claude Code
os-warren commentedon Aug 22, 2026
⛔ NOT DISPATCHED — this card's last two comments point in opposite directions
Services PM seat. I ran this card's premise against
origin/mainbefore dispatching, per the standing rule, and stopped at a contradiction between the ruling and the note written ten hours after it. I am not adjudicating it and I have changed no label.The two texts, quoted
Maintainer ruling, 2026-08-22 02:24Z — Option A:
Triage half-annotation repair, 2026-08-22 12:31Z:
The later note describes option B — the one the ruling explicitly declined — as the anchoring repair, and treats option A — the one that was ruled — as a stop-and-report fork. The directions are inverted.
Why this blocks dispatch rather than being a wording nit
packages/core(the fallback, orServiceRequirementDef's pre-injection list). This card is labelleddomain:services. If the ruling stands, the implementation is not this lane's to take — and the 12:31Z note's own words agree: "that routing moves todomain:engine."What I need, in one line
domain:engine(with the same-shape class audit riding the implementation card, as the ruling scoped). Not mine to dispatch.Unchanged either way
The defect itself is not in question and both texts agree on it: on an
ObjectKernelwithout@objectstack/service-job,preInjectCoreFallbacks()guaranteesctx.getService('job')resolves,createMemoryJob()'s own docblock says aschedule()d job never fires on its own, and the plugin logsdispatcher registered with job serviceand then dispatches nothing. Measured: 0 reads ofsys_report_schedulein 5600 ms, with the success line present.Nothing about this comment weakens that. It is only about which of two ruled contracts the repair implements, and in which lane.
⛔ No label changed, no assignee set, card left in
pm:queue. Recording only.Generated by Claude Code
os-warren commentedon Aug 23, 2026
Resolved by precedence, not by judgement — re-laned
domain:services→domain:engineMy previous comment escalated the contradiction between the 02:24Z ruling and the 12:31Z triage note and asked for direction. That was over-cautious: the contradiction resolves by rule, and applying the rule is the PM's job, not the maintainer's.
A maintainer ruling is not superseded by a triage seat's later annotation. That is the whole point of having a decision hierarchy — otherwise a ruling decays the moment anyone writes below it. So:
jobfallback stops advertising aschedule()it cannot honour. The 12:31Z note's anchoring rationale describes option B, which the ruling explicitly declined ("rather than asking every plugin to consult the degraded health flag"), so that rationale does not govern.packages/core— the fallback itself, orjobcoming offServiceRequirementDef's pre-injection list. The 12:31Z note agrees on this point even while disagreeing on the direction: "that routing moves todomain:engine."⇒ Re-laned to
domain:engine,pm:queueretained. It is dispatchable there; it was never dispatchable indomain:services, because this lane cannot editpackages/core.What is NOT re-litigated
The defect and its measurement, which both texts agree on: on an
ObjectKernelwithout@objectstack/service-job,preInjectCoreFallbacks()guaranteesctx.getService('job')resolves;createMemoryJob()'s own docblock says aschedule()d job never fires on its own; the plugin logsdispatcher registered with job serviceand then dispatches nothing. Measured: 0 reads ofsys_report_schedulein 5600 ms with the success line present.Also not re-litigated: the ruling scoped the same-shape class audit (the card's question 2 — every "prefer the platform job service, else own a timer" plugin) to ride the implementation card. That instruction travels with it to the engine lane.
For the engine seat that picks this up
schedule(),plugin-reportsfalls through to its ownsetInterval. That path exists and is pinned onLiteKernel(plugin-shutdown-releases-dispatcher.test.ts,bootReportsLiteKernel, from #10371) — but ⛔ nothing pins that it RUNS onObjectKernel, which is the kernel real deployments use. That pin is the acceptance evidence for this fix, and its absence is why the defect was invisible.PM note on my own handling
I left this card sitting in
pm:queueafter deciding not to dispatch it. That is the worst of the available states — it reads as live work and is dead work. Routing it is the fix; annotating it was not.Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Claim: PM loop round 9
Session:
session_01RfyXxZ2WPjcjhuXpiQQc3yBranch:
claude/issue-10746-job-fallback-must-not-fake-capabilityWorktree:
objectstack-issue-10746Domain:
domain:engineFile surface:
packages/core/src/fallbacks/memory-job.tsand/orpackages/core/src/kernel.ts(preInjectCoreFallbacks) /ServiceRequirementDef— whichever Option A's shape requires — plus an acceptance pin inpackages/plugins/plugin-reports+ changeset (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: claude-fable-5Clause-②: yes — a pre-injected
jobfallback that today resolves and acceptsschedule()will begin to refuse (or stop being injected). That changes accept/reject behaviour on a core service seam. Tier read fromscripts/pm/dispatch-gates.mjs:2722.Serial constraints cleared:
packages/corecarries nothing in flight. Sibling dev holdsdriver-sql's introspection region (#11161 group);plugin-reportsis untouched by it. Lane in-flight at claim: 1.裁决(照抄不译)
维护者 2026-08-22 02:24Z,决策箱批量,原话 「接受所有」,本卡记为 Option A:
A triage half-annotation at 12:31Z describes Option B (consumer reads the degraded flag) as the anchoring repair and treats Option A as a stop-and-report fork. That is inverted relative to the ruling. The services seat caught it, escalated, then resolved it by precedence at 00:49Z and re-laned the card here — their words, and they are right:
⇒ Option A stands. ⛔ Do not implement Option B. The 12:31Z note is left in place deliberately (deleting a seat's reasoning to make a card read cleanly is its own bad habit) but it is superseded.
The defect — not in question, both texts agree
On an
ObjectKernelwithout@objectstack/service-job:preInjectCoreFallbacks()(packages/core/src/kernel.ts:261-275) registers ajobfallback before Phase 2, soctx.getService('job')always resolves.createMemoryJob()'s own docblock says aschedule()d job never fires on its own.plugin-reportslogsdispatcher registered with job serviceand dispatches nothing, ever. Measured: 0 reads ofsys_report_schedulein 5600 ms, success line present. ThesetIntervalfallback its docblock offers as the single-kernel answer is dead code there.⭐ The acceptance evidence, named by the services seat
So: a pin that boots
ObjectKernel+ObjectQLPlugin+ReportsServicePluginwith no job plugin and assertssys_report_scheduleis actually polled. Demonstrate the failing baseline first (0 reads today). TheLiteKernelhalf is already pinned (plugin-shutdown-releases-dispatcher.test.ts,bootReportsLiteKernel, from #10371) — ⛔ that one does not substitute; it pins release at shutdown on the kernel real deployments do not use.⛔ Boundaries
schedule(), orjoboff the pre-injection list. Pick one, say why, and check the other four pre-injected core fallbacks for the same "declares what it cannot honour" shape ⛔ as a census you report, not as fixes you make (the ruling scoped that audit to ride this card).__serviceInfo.status === 'degraded'). It was declined by name.jobalways resolving, stop and report the fork — do not soften the refusal to make it pass.packages/specoff limits. ⛔ Never editcontent/docs/releases/. Breaking changes ship asminor, nevermajor.packages/drivers/driver-sql/src/sql-driver.tsis held by a sibling dev — out of bounds.Generated by Claude Code
fix(core): stop pre-injecting the job fallback — a fallback must not …
os-zhuang commentedon Aug 23, 2026
os-dev-report
{ "issue": 10746, "status": "done", "branch": "claude/issue-10746-job-fallback-must-not-fake-capability", "pr": "https://github.com/objectstack-ai/objectstack/pull/11205", "premise_still_valid": true, "summary": "Implemented the ruled Option A by taking `job` OFF the pre-injection list: removed the `job: createMemoryJob` entry from CORE_FALLBACK_FACTORIES (packages/core/src/fallbacks/index.ts) — the one map both preInjectCoreFallbacks() and validateSystemRequirements() consult, so getService('job') now throws on an ObjectKernel without a job plugin, plugin-reports falls through to its own setInterval, and validateSystemRequirements() warns 'Core service missing, functionality may be degraded: job' at boot (ServiceRequirementDef in packages/spec untouched). Chose removal over a throwing schedule() because a refusal only half-honors 'declare only what you enforce' (the probe surface `typeof job.schedule === 'function'` would still lie to all five consumers that use it), a refusing schedule() leaves trigger() unreachable and the service a husk, and absence is the already-tested state (every production consumer runs on LiteKernel, which injects no fallbacks, and has a documented absence path — the boundary-2 fork was NOT reached). createMemoryJob stays exported for deliberate explicit registration. Acceptance pin added (dispatcher-runs-on-object-kernel.test.ts): ObjectKernel + ObjectQLPlugin + ReportsServicePlugin, no job plugin, asserts sys_report_schedule is polled — demonstrated RED on unmodified origin/main first ('expected 0 to be greater than 0', reproducing the issue's 0-reads-in-5600ms measurement), GREEN after the fix. Core-side mechanism pin added in kernel.test.ts (validation-on boot pre-injects metadata/cache/queue/i18n, refuses job). Census 1 (the ruling's rider, REPORT ONLY): the other four pre-injected fallbacks do NOT have the fake-capability shape — job was structurally unique as the only contract requiring autonomous future action; metadata/cache/queue/i18n honour every accepted call at call time and their declared degradation is scope, not a dropped promise (near-miss examined and cleared: memory-queue getQueueSize()->0 is a true answer under synchronous delivery). No separate card needed. Census 2 (issue question 2): the consumer class IS wider than plugin-reports — plugin-approvals SLA escalation, runtime app-plugin declarative jobs, trigger-schedule/time-relative probes, and service-automation wait-timer re-arm all scheduled into the void on ObjectKernel today — and all are repaired at the root by this change with ZERO consumer edits (their documented absence paths engage; suites verified green). Discovery-honesty inventory gates updated intent-preservingly to keep the memory-job product covered via the still-exported factory. Changeset: @objectstack/core minor with FROM->TO and the one-line fix; the adr-0087 gate itself confirms 'no declared-breaking changeset'. PR CI: in_progress at report time (draft just opened).", "tests": "Failing baseline FIRST, on unmodified origin/main tree (2f93d0f53) + pin only: 'FAIL src/dispatcher-runs-on-object-kernel.test.ts > polls sys_report_schedule — AssertionError: expected 0 to be greater than 0' (os-verify-lock VERDICT command-exit 1). After fix, all at final commit 857b214d1 (single commit; every run below is of that tree), each under scripts/pm/os-verify-lock.sh with 'VERDICT command-exit 0': plugin-reports 76/76 (5 files, incl. the new pin green); core 895/895 (37 files); runtime 2706/2706 (183 files); objectql 4049/4049 (229 files); plugin-approvals 565/565; trigger-schedule 57/57 (one earlier trigger-schedule file-load failure was an unbuilt service-automation dist — resolved by building the dependency closure, not a code change). typecheck green for plugin-reports/runtime/objectql ('Done' per package, script names echoed — no zero-match); core is a DEBT-ledger package (tsup DTS build passed = src type-checked). Gates derived by 'node scripts/pm/dispatch-gates.mjs' with NO hand-fed paths (16 path-matched + 5 convention-triggered): all exit 0, exit codes captured via redirect-then-$? (no pipes). Gate-printed verdict lines: 'check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)' · 'This diff introduces no `major` bump' · 'check-engine-double-contract: OK — 384 pinned, 133 in the DEBT ledger, 2 exempt' · 'check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 329.3s, 1896 raw tsc error(s) total, none above its recorded number' (run after building the full ./packages/* closure per lint.yml) · 'check-nul-bytes: OK (scanned 6489 text file(s))'. Repo-wide 'pnpm lint' (eslint . --no-inline-config): exit 0 in 2m00s under the lock — full measurement, no narrowing claimed. No ablation beyond the red->green baseline (no dist-resolution pair involved: the pin lives in plugin-reports and resolves @objectstack/core via its rebuilt dist; core was rebuilt before the green leg, VERDICT command-exit 0 on the build).", "open_questions": [], "out_of_scope_findings": ["filed as #11204: [finding] @objectstack/trigger-record-change TEST_DEBT records 9 while tsc reports 0 — graduation candidate the ratchet deliberately leaves alone (gate-printed info line; finding label, unassigned, no pm:queue)"] }Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
ACCEPT — PR #11205, reviewed against the ruling and against the diff, not against the report
Seat
domain:engine, sessionsession_01RfyXxZ2WPjcjhuXpiQQc3y.The ruling is satisfied by its own terms
The ruling named two acceptable shapes: "loud refusal, or
jobcomes off the pre-injection list". The dev took the second. That is inside the ruling, not an interpretation of it — no adjudication was needed and none was made.The dev's reason for preferring removal over a throwing
schedule()is the stronger argument and I'm recording it because it generalises: a refusingschedule()still leavestypeof job.schedule === 'function'answeringtrue, so the probe surface keeps lying to the five consumers that use it, andtrigger()becomes unreachable — the service survives as a husk. Removal is the only shape that makes "declare only what you enforce" true at every surface, not just the call.What I verified myself rather than taking on trust
1. The "inventory gates updated intent-preservingly" claim — read the diff, it holds, and it's stronger than the lazy alternative. This was the phrase I most wanted to check, because a gate edit is where a weakening hides. Once
jobleftCORE_FALLBACK_FACTORIES, three gates that iterate that map would have silently stopped covering thejobslot — coverage lost with no diff to show for it. Instead the dev re-injected the product explicitly:in both
protocol-discovery.test.tsandhttp-dispatcher.test.ts, with a comment saying why — so "however it reaches a slot, discovery must never call itavailable" stays enforced for a factory that is still exported and still explicitly registrable. Coverage preserved, not dropped.2. The 5→4 count pin is a rewrite, not a loosening.
fallbacks.test.tsstill asserts an exacttoEqual(['metadata','cache','queue','i18n']). It was not softened to atoContain, a length check, or anot.toContain('job'). The pin still fails if a fifth entry appears.3. No ratchet ceiling raised, no test skipped, disabled or quarantined. Diff is 8 files, +238/−18; I read every non-test hunk.
4. The acceptance evidence is the one the services seat specified. They wrote: "nothing pins that it RUNS on
ObjectKernel… that pin is the acceptance evidence for this fix, and its absence is why the defect was invisible."dispatcher-runs-on-object-kernel.test.tsis exactly that pin, and it was demonstrated red first on unmodifiedorigin/main(expected 0 to be greater than 0) — reproducing the card's own 0-reads-in-5600 ms measurement — then green. Red-first on the untouched tree is the strongest evidence shape available and it was produced without being asked twice.The two censuses the ruling attached, both delivered as report-only
jobwas structurally unique as the only contract requiring autonomous future action; the rest honour every accepted call at call time, and their degradation is reduced scope, not a dropped promise. The near-miss was examined and cleared on the record (memory-queue.getQueueSize() → 0is a true answer under synchronous delivery). Correctly reported rather than fixed — the ruling scoped this as an audit.plugin-reports—plugin-approvalsSLA escalation, runtime app-plugin declarative jobs, trigger-schedule/time-relative probes, and service-automation wait-timer re-arm were all scheduling into the void onObjectKernel. All four are repaired at the root with zero consumer edits, because their documented absence paths now become reachable. Suites verified green. This is the most valuable finding in the PR: the defect was four times larger than the card that found it, and the ruled fix covered all of it for free.Two notes, neither blocking
objectstack migrate metato rewrite) — but whoever compiles the release notes will see two spellings for one kind of change. Flagging for that reader, not asking for an edit.#11204filed as afinding, unassigned, nopm:queue— correct shape for a first-grading candidate; it goes to the triage round, not this lane's queue.The card's own history is worth preserving
This card was ruled on 2026-08-22, then had a triage note written ten hours later that described the declined option as the anchoring repair and the ruled option as a stop-and-report fork. The services seat caught the inversion, escalated, then resolved it by precedence and re-laned the card here — and their statement of the rule is the one I'd want any seat to apply: "A maintainer ruling is not superseded by a triage seat's later annotation. That is the whole point of having a decision hierarchy — otherwise a ruling decays the moment anyone writes below it." They also left the wrong note in place rather than deleting it, which is right: deleting a seat's reasoning to make a card read cleanly hides the near-miss.
Cost of the inversion: this card sat undispatchable for roughly 12 hours. It was recovered by a seat reading the thread in order and applying a rule — not by escalating to the maintainer.
Enqueueing on green CI.
Generated by Claude Code
docs(kernel): services-checklist no longer advertises a pre-injected …
os-zhuang commentedon Aug 23, 2026
os-dev-report
Supersedes my report at issuecomment-5383926294 — same deliverable plus the PM-requested docs correction (second commit
973336d6bon the same branch/PR).{ "issue": 10746, "status": "done", "branch": "claude/issue-10746-job-fallback-must-not-fake-capability", "pr": "https://github.com/objectstack-ai/objectstack/pull/11205", "premise_still_valid": true, "summary": "Option A implemented as 'job off the pre-injection list' (CORE_FALLBACK_FACTORIES in packages/core/src/fallbacks/index.ts; ServiceRequirementDef untouched — job stays core criticality, which is what produces the loud boot warn). Full rationale, acceptance pin (red baseline 0-reads-in-5600ms then green on ObjectKernel), four-fallback census (no other entry has the fake-capability shape; job was structurally unique — the only contract requiring autonomous future action), and consumer-class census (wider than plugin-reports: approvals SLA clock, app-plugin declarative jobs, trigger-schedule/time-relative, wait-timer re-arm — all repaired at the root, zero consumer edits) are in the prior report and the PR body. ADDENDUM (PM request after the docs-drift flag): content/docs/kernel/services-checklist.mdx asserted a pre-injected job fallback in FOUR places, all corrected in commit 973336d6b in the page's voice with job's core criticality explicitly preserved: (1) ~line 19 key-architecture slot list — job now named beside auth as deliberately without a kernel fallback, with the one-line reason; (2) ~line 82 Service Overview row 15 — '✅ Built-in (in-memory fallback)' -> '❌ Plugin Required (no pre-injected fallback since #10746 — with no job plugin, getService throws and the boot warns)'; (3) ~line 483 Infrastructure Services intro — cache/queue keep the fallback claim, job called out as deliberately off CORE_FALLBACK_FACTORIES with the remedy (install @objectstack/service-job, or register createMemoryJob() explicitly); (4) ~line 491 infrastructure table job row — fallback claim removed. Flagged lines judged STILL TRUE and left alone, with reasons: ~25 (the ⚠️ Framework legend is a generic marker definition for slots that have a fallback; it names no job and stays true of metadata/cache/queue/i18n), ~31 ('i18n has a kernel in-memory fallback' — i18n remains on the list), ~54 (the ASCII diagram lists job under the Plugin Layer, i.e. plugin-delivered with no fallback claim — more accurate now, not less), ~429 (i18n implementations row — about i18n only), ~516 (scheduled-tasks provider row names service-job, no fallback claim). Swept the rest of hand-written content/docs for equally specific job-pre-injection claims: none (packages.mdx:272 is the queue service's DB-backed sys_job_queue adapter — true and unrelated). No other doc touched; no code touched in the addendum commit; content/docs/releases/ untouched.", "tests": "First commit 857b214d1 evidence unchanged (prior report: suites 895/895 core, 76/76 plugin-reports incl. the new pin, 2706/2706 runtime, 4049/4049 objectql, 565/565 approvals, 57/57 trigger-schedule; derived gates all exit 0 incl. 'check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured … none above its recorded number'; repo-wide pnpm lint exit 0). Docs commit 973336d6b: gate set re-derived with 'node scripts/pm/dispatch-gates.mjs' (no hand-fed paths; the docs file pulls in additional families), then run at that head, all exit 0 captured via redirect-then-$?: check:doc-anchors, check:doc-authoring, check:docs-audit-scope, check:docs-redirects, check:published-readme-links, check:role-word, check-doc-frontmatter, check-affected-docs, spec liveness family (check:empty-state, check:liveness, check:strictness-ledger, check:variant-docs), lint-package doc gates with their own verdict lines ('✓ check:doc-formula-expressions (spec TSDoc, #6763): 9 @example(s) judged clean across 981 packages/spec/src files' · '✅ 26 ObjectSchema.create example(s) in 227 marked block(s) across 237 prose file(s) in 2 root(s) carry an os validate-clean security posture'), the full cheap union re-run (adr-0087, changeset-no-major, empty-changeset, nul-bytes, slot-lookup, cross-package-test-inputs, ci-filter-parity, plugin-teardown-shape, engine-split-ratio, kernel-hook-pairs, durability-log-level, test-source-alias, type-source-resolution, changeset-gate-self-tests, objectui-changeset — all exit 0), and repo-wide pnpm lint again (VERDICT command-exit 0, 1m36s under the lock). The two lint-package gates first failed with ERR_MODULE_NOT_FOUND on the fresh worktree's unbuilt dists — treated as 'could not run', not a result; green after building the @objectstack/lint closure (VERDICT command-exit 0). DECLARED NARROWING at 973336d6b: package test suites and the check:type-check-debt ratchet not re-run there — evidence: (a) population read from the checkers' own inputs (tsc programs per ledger entry / vitest suites over package sources); (b) delta enumerated by git itself: 'git diff --stat 857b214d1..HEAD' = 'content/docs/kernel/services-checklist.mdx | 8 ++++----', 1 file; (c) invariance: no tsc program or vitest suite takes .mdx as input and the packages/ subtree is byte-identical to the tree those runs measured green.", "open_questions": [], "out_of_scope_findings": ["filed as #11204: [finding] @objectstack/trigger-record-change TEST_DEBT records 9 while tsc reports 0 — graduation candidate (finding label, unassigned, no pm:queue)"] }Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Enqueued — doc correction verified, PR #11205 released
Seat
domain:engine. Out of draft, auto-merge (squash) enabled, in that order (the draft→ready conversion clears auto-merge, so enabling first would have been a no-op).CI read by name at
973336d6b, not from the aggregate: 35 check runs, 34success, 1skipped(Console Pin Gate— conditional, and this diff touches no.objectui-sha; skipped is not red). Zero non-green, zero still running.The doc correction, verified against the diff
All four false claims in
content/docs/kernel/services-checklist.mdxare repaired, and — the part I was most concerned about — the distinction survived the edit. The page now says outright:That mattered because demoting
jobout ofcoreto make the page read cleanly would have silenced the very boot warning this fix depends on: a worse falsehood than the one being corrected.The dev also did the thing I explicitly declined to pre-decide. I flagged three lines without judging them and asked for its own reading; it returned five lines judged still true, each with a reason — ~25 is a generic legend that names no service, ~31 and ~429 are i18n-only, ~54 draws
jobunder the Plugin Layer (which this change makes more accurate, not less), ~516 names the provider without a fallback claim. It then swept the rest ofcontent/docsfor equally specific claims and found none, clearing the one near-miss on the record (packages.mdx:272is the queue service'ssys_job_queueadapter — true and unrelated).Two process notes worth keeping
"Could not run" was reported as "could not run." Two lint-package gates threw
ERR_MODULE_NOT_FOUNDagainst the fresh worktree's unbuilt dists. The dev recorded that as not a result, built the@objectstack/lintclosure, and re-ran to green. A module-resolution failure is unmeasured, never a pass — and it took no reminding.The declared narrowing came with an argument, and I checked it rather than accepting it. Package suites and the
check:type-check-debtratchet were not re-run at the docs commit, justified by: population read from the checkers' own inputs, delta enumerated by git, and invariance (no tsc program or vitest suite takes.mdxas input). I verified the load-bearing middle claim independently —git diff --stat 857b214d1..973336d6bis exactly one.mdxfile,packages/is byte-identical, and a positive control confirmed the same command does report differences where they exist. The green results from the first commit therefore still hold at this head.Nothing further outstanding on this card. Closing on merge via
Fixes #10746.Generated by Claude Code
fix(core): the pre-injected job fallback must not fake capability — t…