Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions docs/adr/0076-objectql-core-tiering.md
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
# ADR-0076: objectql is the data engine — relocate metadata management (protocol) out of it; enforce the boundary; defer the engine repo-split

**Status**: Accepted — D1/D2/D4/D5/D6/D8/D9 implemented; D10/D12 partially landed; **D11 implemented on the dispatcher half and *decided* on the rest-server half — delineated, not split** (maintainer ruling 2026-08-15; revision recorded under D11 by #9024); D3 unbuilt; D7 deliberately deferred (proposed 2026-06-28, rev. 9 · calibrated 2026-08-05 by #5063 · D11 revised 2026-08-16). This ADR has been binding for some time — `AGENTS.md`, `packages/runtime/src/domains/*` (D11's own output), `packages/objectql`'s core boundary and ADR-0121 all cite it as a settled decision — while the header still read `Proposed`; per Prime Directive #13 an accepted decision binds until superseded, so the header is corrected here and, following #4919's calibration of ADR-0078, **per decision** rather than by a single flip. Verified against source (not against the rev.9 narrative), 2026-08-05:
**Status**: Accepted — D1/D2/D4/D5/D6/D8/D9 implemented; D10/D12 partially landed; **D11 implemented on the dispatcher half and *decided* on the rest-server half — delineated, not split** (maintainer ruling 2026-08-15; revision recorded under D11 by #9024); D3 unbuilt; D7 deliberately deferred (proposed 2026-06-28, rev. 9 · calibrated 2026-08-05 by #5063 · D11 revised 2026-08-16 · D11's replacement metric and reopen threshold retired 2026-08-20 by #10076). This ADR has been binding for some time — `AGENTS.md`, `packages/runtime/src/domains/*` (D11's own output), `packages/objectql`'s core boundary and ADR-0121 all cite it as a settled decision — while the header still read `Proposed`; per Prime Directive #13 an accepted decision binds until superseded, so the header is corrected here and, following #4919's calibration of ADR-0078, **per decision** rather than by a single flip. Verified against source (not against the rev.9 narrative), 2026-08-05:

- **D1 — implemented, with OQ#2 unexecuted.** `packages/metadata-protocol/` owns `protocol.ts`, `sys-metadata-repository.ts` and `metadata-diagnostics.ts`; `packages/objectql` depends on it one-way (`packages/objectql/package.json`, `src/plugin.ts` imports `assembleMetadataProtocol`). **But `SysMetadataEngine` never moved to `@objectstack/metadata-core`** — it is still declared in `packages/metadata-protocol/src/sys-metadata-repository.ts` and `packages/metadata-core/src/` has no occurrence of the name. OQ#2 is therefore *unexecuted*, not resolved; the D1 bullet below states the intent, not the state.
- **D2 — implemented.** `packages/objectql/src/core-boundary.ratchet.test.ts` walks the `core.ts` import closure and forbids `@objectstack/metadata-protocol` plus the local `plugin` / `kernel-factory` modules.
- **D3 — unbuilt. The one decision here with no implementation at all.** No `profile: 'trusted' | 'internal'` exists in `packages/objectql/src`, `packages/core/src` or `packages/spec/src/kernel`; nothing derives required capabilities from object declarations; boot is **not** fail-closed — `packages/core/src/kernel.ts` only *warns* ("System started with degraded capabilities. Missing core services: …"). The `requiredCapabilities` / `provides` keys that do exist belong to the unrelated plugin-conformance schema `packages/spec/src/kernel/plugin-capability.zod.ts`, not to this decision. (Zero-hit counter-checked against live neighbours in the same search surface: `registerMiddleware`, `isServiceServeable`, `__serviceInfo` all match.)
- **D4/D5/D6/D8 — ratifications, all still true.** `@objectstack/formula` remains an `objectql` dependency used by `engine.ts` / `hook-wrappers.ts` / `validation/rule-validator.ts` (D4). `@objectstack/spec` carries only `zod` as a runtime dependency, and `examples/embed-objectql/` boots `@objectstack/objectql/core` against an ordinary `ObjectSchema.create` object — the same definition shape a full backend ships (D5, and Phasing P4). `engine.ts`'s `registerMiddleware` is the attachment point `plugin-security` and `plugin-sharing` actually use (D6), and the engine still hard-codes no governance (D8) — the D2 ratchet is what keeps it that way.
- **D9 — implemented, past step-1, at its own rev.7 end-state.** `packages/spec/src/api/protocol.zod.ts` declares ten per-domain contracts (`DataProtocol`, `MetadataProtocol`, `AnalyticsProtocol`, `AutomationProtocol`, `PackageProtocol`, `ViewProtocol`, `PermissionProtocol`, `RealtimeProtocol`, `NotificationProtocol`, `I18nProtocol`), and **no `ObjectStackProtocol` union type is declared or exported anywhere in the repo** — the transitional composed alias is *dissolved*, not merely deprecated. Consumers are narrowing accordingly: `packages/rest/src/rest-server.ts` types its dependency as `RestProtocol = DataProtocol & MetadataProtocol`.
- **D10 — partially implemented.** Landed: the feed domain and then the analytics fallback left the facade (analytics now has exactly one implementation, in `service-analytics`), and OQ#7 resolved to keep the package name. Outstanding: the central facade class this decision rules out **still exists** — `packages/metadata-protocol/src/protocol.ts` declares `class ObjectStackProtocolImplementation implements DataProtocol, MetadataProtocol, PackageProtocol` — so the `DataProtocol` implementation has not moved to the engine-adjacent/transport layer and **OQ#6 remains open**.
- **D11 — dispatcher half implemented; rest-server half *decided*: delineated, not split.** The dispatcher decomposed exactly as OQ#9's verdict describes: every domain body lives in `packages/runtime/src/domains/*`, routing goes through `packages/runtime/src/domain-handler-registry.ts`'s `DomainHandlerRegistry`, and `http-dispatcher.ts` is down to ~2.1k LOC from the ~3.8k recorded below (2,087 lines measured at `dc280d326`; this bullet recorded ~1.7k at the 2026-08-05 calibration). Multi-adapter is validated (OQ#10) by `packages/qa/http-conformance`. The second half — `packages/rest/src/rest-server.ts` — is **not** decomposed and, by the maintainer ruling of 2026-08-15, **will not be**: it is already delineated by domain inside the file, and the decomposition programme does not open. See the **2026-08-16 revision at the end of D11** for the structural reading, the price that decision knowingly accepts, and the metric that replaces the retired line count. *(Until 2026-08-16 this bullet read "the rest-server half is untouched" and compared two raw line counts — i.e. it recorded an open obligation, and measured it with the proxy the ruling retired. Both are corrected: the obligation is a closed decision, and the raw-LOC comparison is gone rather than refreshed.)*
- **D11 — dispatcher half implemented; rest-server half *decided*: delineated, not split.** The dispatcher decomposed exactly as OQ#9's verdict describes: every domain body lives in `packages/runtime/src/domains/*`, routing goes through `packages/runtime/src/domain-handler-registry.ts`'s `DomainHandlerRegistry`, and `http-dispatcher.ts` is down to ~2.1k LOC from the ~3.8k recorded below (2,087 lines measured at `dc280d326`; this bullet recorded ~1.7k at the 2026-08-05 calibration). Multi-adapter is validated (OQ#10) by `packages/qa/http-conformance`. The second half — `packages/rest/src/rest-server.ts` — is **not** decomposed and, by the maintainer ruling of 2026-08-15, **will not be**: it is already delineated by domain inside the file, and the decomposition programme does not open. See the **2026-08-16 revision at the end of D11** for the structural reading and the price that decision knowingly accepts. The metric that replaced the retired line count is **itself retired** by the **2026-08-20 revision**, together with the `~1/round` reopen threshold it fed — nothing ever computed either, so there is now no standing metric on this file and no automatic reopen trigger. *(Until 2026-08-16 this bullet read "the rest-server half is untouched" and compared two raw line counts — i.e. it recorded an open obligation, and measured it with the proxy the ruling retired. Both are corrected: the obligation is a closed decision, and the raw-LOC comparison is gone rather than refreshed.)*
- **D12 — framework side implemented; console side cross-repo and unverified from here.** `packages/spec/src/api/discovery.zod.ts` carries the `unavailable` / `degraded` / `stub` statuses plus `ServiceSelfInfoSchema` + `readServiceSelfInfo`; `svcAvailable` in `packages/runtime/src/http-dispatcher.ts` derives `status`/`handlerReady` from that marker for every slot (no hardcodes left); `packages/runtime/src/service-serveable.ts`'s `isServiceServeable` is the single predicate the dispatcher-owned domains, the route-mount gate and discovery all read. The consumer half ("trust only `handlerReady: true`") lives in cloud/objectui and cannot be confirmed from this repo.
- **D7 — deferred as designed, and now instrumented.** `packages/objectql` is still in the monorepo. New since rev.9: `scripts/check-engine-split-ratio.mjs` and `.github/workflows/engine-split-metric.yml` compute and report the cross-package commit ratio that gates the split; it is deliberately **report-only** because the threshold itself is still OQ#5.

Expand DownExpand Up@@ -153,13 +153,15 @@ D11 names two central transport layers. The dispatcher half was decomposed (OQ#9

**② The price this decision knowingly accepts — B is defensible, and it is not free.** Merge conflicts, affected-subset CI, and the PM lane's batch-independence rule all operate on **file paths**. A delineation expressed as methods inside one file is invisible to all three. Two agents editing `registerApprovalsEndpoints` and `registerReportsEndpoints` — blocks that share no line — still collide, still serialise, and still dirty the same CI subset. **Adopting B accepts that permanently, until someone reverses this decision.** It is written into the ADR so the price arrives with the decision instead of being discovered by whoever pays it.

**③ The replacement metric, and the reopen condition.**
**③ The replacement metric, and the reopen condition. ⛔ Both RETIRED 2026-08-20 — see the revision at the end of D11.** The block is kept, not deleted, because the 2026-08-16 decision was argued from it and a later reader needs to see what was withdrawn rather than find a gap.

The line-count series this question was argued over is **retired, not paused**. It used raw `wc -l` on a file whose code and prose are close to the same size: measured with a comment-aware classifier at `dc280d326`, **5,665 code lines against 4,690 comment/blank (45.3%)** — and those comments are dense explanatory documentation (why a refusal is shaped as it is, why route order is load-bearing), not commented-out code. A raw line delta over such a file cannot separate "a new endpoint landed inline" from "someone documented a subtle refusal properly", so it cannot answer the question it was being used to answer.

> **Replacement metric — per PM round: the number of times two dispatchable cards could not run concurrently because both land in `packages/rest/src/rest-server.ts`.**
>
> ⛔ **RETIRED 2026-08-20 (#10076).** Nothing ever computed this counter. Do not resume the series, and do not file readings into #5949 — that card has been closed since 2026-08-16.

It measures the cost this file demonstrably creates rather than its volume, and the dispatch loop already computes batch independence, so it needs no `git log` window — which is what repairs the 2026-08-08 hold's "measured merge-conflict tax" arm after shallow clones defeated two separate seats trying to measure it. **Reopen A if this counter sustains above ~1/round.**
It measures the cost this file demonstrably creates rather than its volume, and the dispatch loop already computes batch independence, so it needs no `git log` window — which is what repairs the 2026-08-08 hold's "measured merge-conflict tax" arm after shallow clones defeated two separate seats trying to measure it. **Reopen A if this counter sustains above ~1/round.** ⛔ **WITHDRAWN 2026-08-20 (#10076)** — the threshold rested on a counter no mechanism ever produced, so the condition could never fire. The sentence is left standing rather than struck because the 2026-08-16 decision was argued with it; reopening A is now an ordinary new decision under Prime Directive #13. See the revision below.

- **First datapoint (2026-08-15 rounds, recorded with the ruling): 0.** Re-verified rather than restated: #8087, the one queued card that would land in this file, is still open and still blocked for a reason unrelated to it, so the collision never materialised.
- **Re-derived for the 2026-08-16 round: 0.** None of the 24 open `pm:dispatched` cards lands in `rest-server.ts`.
Expand All@@ -169,6 +171,27 @@ It measures the cost this file demonstrably creates rather than its volume, and

**Which ruling figures moved.** The 18 methods and the domain axis are unchanged. The whole-file composition the ruling quoted — 6,008 code against 5,400 comment/blank, 11,408 total — was taken at `5047cb8ab` and is **superseded**: #8850's prologue extraction (landed as `8664a2c99`) moved 1,296 lines of ADR-0112 error/fault classification out to `error-response.ts` after that measurement. The same classifier used above reproduces `6,008 / 5,400` exactly at `5047cb8ab`, so the two readings differ because the file changed, not because the method did. `registerMetadataEndpointsInner` reads 2,324 lines on that tree against the ruling's ~2,328 (a method-boundary convention, not a disagreement) and 2,545 today, the `/meta` work since having landed inside it. Recorded as measured; ⛔ deliberately not averaged, rounded together, or reconciled in either direction.

#### Revision 2026-08-20 (#10076) — the per-round contention metric is RETIRED, and the reopen threshold goes with it [ruled]

**Provenance.** Maintainer, decision-inbox session 2026-08-20, verbatim 「其他接受你的建议。」, accepting option **B** at [#10076](https://github.com/objectstack-ai/objectstack/issues/10076) — retire the contention-sequence reporting commitment, paired with this note. ⛔ Deliberately **not** a silent retirement: the condition of the ruling was that this record stop claiming a reopen threshold nothing feeds, in writing, with the reason attached.

**What is retired.** ③'s replacement metric — the per-PM-round count of dispatchable cards that could not run concurrently because both land in `packages/rest/src/rest-server.ts` — **and the `~1/round` reopen condition that rested on it**. Neither is measured, reported nor consumed from this date. ⛔ Do not resume the series, and ⛔ do not file readings into #5949: posting measurements into a closed card is the precise shape this retirement exists to end.

**Why — three findings, all re-measured at head for this revision.**

1. **No mechanism ever computed it.** ③ reasoned that "the dispatch loop already computes batch independence, so it needs no `git log` window". Batch independence is computed per round to *schedule* work; it is not persisted, and the counter was never derived from it. Measured 2026-08-20: no script, gate, workflow or agent-instruction file in this repo produces, stores or reads the series. It lived in prose in one seat post, handed from seat to seat — which is a carrier, not a mechanism.
2. **Its anchor has been closed since 2026-08-16.** #5949 — the finding this whole revision line descends from — is `closed`/`completed` as of 2026-08-16T06:47:27Z. Every reading owed after that date had nowhere live to land, and the readings that were filed went into a closed card that no longer appears in any open-issue view.
3. **The delivery record shows the failure was structural, not personal.** Over rounds 15–28: round 23 filed a reading into the already-closed anchor and flagged it; rounds 24–25 produced no entry at all; rounds 26–28 were filed only retroactively, during the audit that produced this note. Three seats, thirteen rounds, one gap. ⭐ **An obligation with no live named reader and no trigger cannot fail loudly, so it reads as satisfied right up until somebody audits it** — the same defect class this repo has measured in `Restart-when:` hand-offs that delegate work to an unrelated card.

**The threshold could not have carried a decision even if the series had been maintained.** Every reading from round 18 onward sat at or below `~1/round`, and the only two rounds ever re-checked (19 and 20) were **both** found mis-counted. So "it was once sustained above the threshold" is itself unverified. That weakens the case *for* reopening A and the case *against* it equally, and is recorded here rather than resolved in either direction.

**⛔ What this revision does NOT do.**

- It does **not** revive the raw `wc -l` line-count series. That was retired by the 2026-08-16 revision above on its own merits — a file that is ~45% explanatory prose cannot be judged by a line delta — and retiring its *replacement* does not hand the question back to it. There is now **no standing metric on this file**, deliberately.
- It does **not** make B permanent or unchallengeable. ②'s price stands unchanged: a delineation expressed as methods inside one file is invisible to merge conflicts, affected-subset CI and batch independence, and that is still the reason someone may want A. What changes is the route — reopening A is an ordinary new decision under Prime Directive #13, argued from a measurement taken **for that purpose**, rather than an automatic trigger nothing was ever going to pull.
- It does **not** touch ①'s structural reading, ④'s `registerMetadataEndpointsInner` carve-out (#8851), or the 2026-08-15 ruling recorded above that B rests on.


### D12 — Honest capabilities: discovery must distinguish real services from stubs/fallbacks [new — kernel review]

**Root cause of agents being misled.** Several plugins register stub / dev / fallback services under canonical names, and the discovery builder reports *any* present service as fully real: `runtime/http-dispatcher.ts`'s `svcAvailable` hardcodes `{ enabled: true, status: 'available', handlerReady: true }` for every registered service — it **ignores stub markers** (its own comment even says "handlerReady:false … may be served by a stub", but the code never computes it). So `discovery.services.*` claims capabilities that are only stubbed, and consumers (AI agents, the console) trust them. A dev AI stub advertised this way has already confused an agent.
Expand Down
Loading