Skip to content

Commit 9eaa8bc

Browse files
committed
Merge origin/main into claude/issue-8278-rest-published-overlay
2 parents a041d30 + ffa4e01 commit 9eaa8bc

11 files changed

Lines changed: 1501 additions & 195 deletions

.changeset/lucky-schools-smash.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
'@objectstack/service-package': minor
3+
'@objectstack/rest': patch
4+
---
5+
6+
Package publish: a driver fault is answered as a server error, and its driver text no longer reaches the caller
7+
8+
`POST /api/v1/packages/publish` answered **`400 PACKAGE_PUBLISH_FAILED`** when the
9+
`INSERT INTO sys_packages` statement itself failed, carrying the driver's own message as
10+
the caller-facing text. Measured on a real SQLite engine, that was literally:
11+
12+
```
13+
400 {"success":false,"error":{"code":"PACKAGE_PUBLISH_FAILED",
14+
"message":"no such table: sys_packages"}}
15+
400 {"success":false,"error":{"code":"PACKAGE_PUBLISH_FAILED",
16+
"message":"NOT NULL constraint failed: sys_packages.tenant_ref"}}
17+
```
18+
19+
Two defects in one line. The **status** was a client error for a fault the client had no
20+
part in — the mirror of the mislabelling fixed for the throw path, and it hid a real
21+
server fault from every dashboard that buckets by status. The **message** was raw driver
22+
text: a constraint dump naming physical tables and columns.
23+
24+
Fixed at the producer, which is the only place that closes it. A 5xx message withhold
25+
already exists at this door, but it is applied when an error is *thrown*, and this
26+
failure was *returned* — so it never met the withhold at any status. Reclassifying alone
27+
would have moved the driver line from a 400 to a 500 and left it on the wire; that is
28+
measured, and it stays true against the widened leak predicate that now recognises this
29+
phrasing, because nothing on the returned path ever consults one.
30+
31+
Now the driver's text goes to the log and nowhere else — it was already logged, so nothing
32+
an operator sees changes — and the caller gets a stable sentence that names what happened
33+
without quoting the driver.
34+
35+
**Caller-facing 4xx messages are unchanged.** A missing manifest, an invalid manifest, and
36+
any coded refusal thrown from below `publish` all keep their own status, code and
37+
self-correcting message — a `409 DESTRUCTIVE_CHANGE` is still a 409.
38+
39+
**BREAKING — the `PackageService.publish` return shape.** A bare `error` string could not
40+
say which side was at fault, so the door had one status for both and picked the wrong one.
41+
`publish` now reports a broken write as `{ success: false, driverFault: { message } }`;
42+
the `error` field is removed. If you only *call* `publish`, read
43+
`result.driverFault?.message` where you read `result.error`. If you *implement*
44+
`PackageService`, report a broken write through `driverFault` with a message safe to show
45+
a caller, and **throw** — rather than return — a refusal that carries its own `status`, so
46+
the door answers it with that status and code.
47+
48+
<!-- adr-0087: not-required (no-migration-prescription) This change retires no authorable key and adds none. `PackageService` is a runtime TypeScript service interface in `packages/services/service-package`; it has no Zod schema, no `packages/spec` declaration, no metadata type and no stored representation. `packages/spec` is untouched by this PR. Nothing exists for `objectstack migrate meta` to rewrite, because nothing an author writes and nothing persisted in `sys_metadata` or `sys_packages` changes shape — the wire envelope is unchanged too (still ADR-0112 `{ success, error: { code, message } }`), and only the STATUS a driver fault selects and the TEXT the producer puts in it move. Nor is there a FROM/TO rule a ledger entry could state: the ledger's subject is metadata, and the only readers affected here are TypeScript callers of one in-process service — measured as three in-repo consumers (`@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`), of which exactly one, `packages/rest/src/package-routes.ts`, reads the changed field. The channel that reaches an affected reader is strictly more precise than any ledger line: the compiler itself. Reading the removed field is a hard type error at the call site — verified by reinstating it, which fails as `error TS2339: Property 'error' does not exist on type 'PackagePublishResult'` — so no consumer can carry this change silently, and this changeset's CHANGELOG text carries the one-line repair for the reader who hits it. -->

.github/workflows/adr-merge-approval.yml

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
name: ADR Merge Approval
22

3-
# Machine enforcement of the #6741 ruling (maintainer, verbatim):
4-
#adr 只能由维护者自己确认,人工合并,ai 不得擅自合并。
3+
# Machine enforcement of the 2026-08-12 ruling (maintainer, verbatim):
4+
#门禁改成只要求「APPROVED review 存在」」/「不要指定具体的人
55
#
6-
# A PR whose diff touches docs/adr/** must carry an APPROVED review from the
7-
# maintainer's OWN account before it is mergeable; approvals from the shared
8-
# bot/agent identities deliberately do not count. Prose enforcement was
9-
# measured insufficient the day the ruling landed — two different AI-operated
10-
# seats merged docs/adr/** PRs within the following hour (#6671, #6732; the
11-
# full record and both replays live in scripts/check-adr-merge-approval.mjs
12-
# and its --self-test). Drafting ADR PRs stays open to every seat; only the
13-
# merge is reserved, and the maintainer's own approval + merge is the intended
14-
# zero-extra-friction green path.
6+
# A PR whose diff touches docs/adr/** must carry an APPROVED review before it
7+
# is mergeable. The gate does NOT check WHO approved: any account with review
8+
# rights on this repo — including an AI seat — satisfies it, which is the
9+
# accepted cost of the ruling and is stated in full in the two-clause table at
10+
# the head of scripts/check-adr-merge-approval.mjs. The approval must be
11+
# current: a later CHANGES_REQUESTED or DISMISSED revokes it.
12+
#
13+
# This supersedes the account-identity rule this workflow used to describe
14+
# (#6741 「adr 只能由维护者自己确认,人工合并,ai 不得擅自合并。」, enforced by
15+
# matching the maintainer's numeric account id). That proxy became
16+
# unsatisfiable once cloud sessions began authoring PRs under the maintainer's
17+
# own account, since GitHub forbids self-approval (#8161). #6741's two halves
18+
# survive as convention, not as anything this workflow can measure.
19+
#
20+
# Prose enforcement was measured insufficient the day #6741 landed — two
21+
# different AI-operated seats merged docs/adr/** PRs within the following hour
22+
# (#6671, #6732; the full record and both replays live in
23+
# scripts/check-adr-merge-approval.mjs and its --self-test). Both had ZERO
24+
# reviews of any kind, so both stay red under the widened rule too. Drafting
25+
# ADR PRs stays open to every seat; only the merge is gated.
1526
#
1627
# Deliberately NO `paths` filter, on either trigger — the same choice
1728
# changeset-presence.yml made in objectui (#3769) and for the same reason
@@ -30,9 +41,11 @@ on:
3041
pull_request:
3142
branches: [main]
3243
# An approval does not fire `pull_request`, so without this trigger the
33-
# failed check would sit red after the maintainer approves until someone
34-
# re-ran it by hand. Subscribing to reviews makes the maintainer's approval
35-
# itself re-run the gate — the zero-friction green path the card requires.
44+
# failed check would sit red after the approval lands until someone re-ran
45+
# it by hand. Subscribing to reviews makes the approval itself re-run the
46+
# gate — the zero-friction green path the card requires. `pull-requests:
47+
# read` below covers listing reviews from ANY account, so the widened rule
48+
# needs no extra permission or token scope.
3649
# (On non-ADR PRs a review re-runs the cheap clean path; harmless.)
3750
pull_request_review:
3851
types: [submitted, edited, dismissed]
@@ -55,6 +68,14 @@ permissions:
5568

5669
jobs:
5770
adr-merge-approval:
71+
# ⛔ Do NOT rename this job. Its name IS the required status-context string
72+
# in the `main` ruleset (#7022), and it is registered under that exact
73+
# spelling in scripts/check-required-contexts.mjs — renaming it here alone
74+
# leaves the ruleset waiting for a context that never reports, which hangs
75+
# the merge queue until the 60-minute timeout. The word "maintainer" now
76+
# over-claims (see this file's header: any approver counts); correcting it
77+
# is a settings action nobody in CI can perform, so it is tracked as
78+
# follow-up work rather than done here.
5879
name: ADR maintainer approval
5980
runs-on: ubuntu-latest
6081
timeout-minutes: 5
@@ -77,7 +98,7 @@ jobs:
7798
# install, no build. The self-test runs first (repo convention), then
7899
# the gate. GITHUB_TOKEN is only read on the gated path (a docs/adr/**
79100
# diff needs the PR's review list); the clean path does zero lookups.
80-
- name: Require the maintainer's own approval on docs/adr/** diffs
101+
- name: Require an APPROVED review on docs/adr/** diffs
81102
env:
82103
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83104
run: node scripts/check-adr-merge-approval.mjs --self-test && node scripts/check-adr-merge-approval.mjs
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# ADR-0001: Withdrawn — this number is retired and must not be reused
2+
3+
**Status**: **Withdrawn (2026-02-11)**. This file is a *tombstone*, not a decision: nothing in it is in force, and the number must never be reassigned.
4+
**Deciders**: ObjectStack Protocol Architects (recorded retroactively — see [How this differs from ADR-0107](#how-this-differs-from-adr-0107))
5+
**The record that held this number**: *"Metadata Service Architecture"*`docs/adr/0001-metadata-service-architecture.md`, Status *Accepted (2026-02-10)*
6+
**Landed by**: `908d95c82`, 2026-02-10 09:14 UTC
7+
**Deleted by**: `9da8e3e72`, 2026-02-11 23:35 +0800, about thirty hours later — a 37-path documentation sweep, not an ADR decision
8+
**Tracking**: [#7329](https://github.com/objectstack-ai/objectstack/issues/7329) (the citation-resolution work that named this case), [#7866](https://github.com/objectstack-ai/objectstack/issues/7866) (this tombstone), [#6634](https://github.com/objectstack-ai/objectstack/issues/6634) (the squat failure mode both are written against)
9+
**Consumers**: none. No code is governed by this number, and none may be — see [Do not anchor to this number](#do-not-anchor-to-this-number).
10+
11+
---
12+
13+
## TL;DR
14+
15+
A real record occupied ADR-0001 on `main` for about thirty hours in February 2026 and
16+
was then deleted. Unlike [ADR-0107](./0107-withdrawn-hook-body-write-set-static-gap.md),
17+
it was **not withdrawn on the merits** — it was swept away as collateral in a bulk
18+
documentation cleanup that took the entire `docs/adr/` registry with it, including its
19+
`README.md` and the ADR-0002 of that era.
20+
21+
This file exists so the number resolves to that explanation instead of to nothing, and so
22+
it is never handed to an unrelated decision. Reassigning it would retroactively re-point
23+
every historical "ADR-0001" at a document its author never meant — the squat failure mode
24+
[#6634](https://github.com/objectstack-ai/objectstack/issues/6634) was filed for, where
25+
one number had silently accumulated 77 citations it did not resolve.
26+
27+
**A new record takes the next free number. Not this one.**
28+
29+
## What happened
30+
31+
| When | What | Evidence |
32+
|---|---|---|
33+
| 2026-02-10 09:14 UTC | `docs/adr/0001-metadata-service-architecture.md` written and merged, Status *Accepted*, authored by an automated agent | `908d95c82` |
34+
| 2026-02-11 23:35 +0800 | Deleted, together with `docs/adr/0002-database-driven-metadata-storage.md` and `docs/adr/README.md`, inside a 37-path docs sweep | `9da8e3e72` |
35+
| 2026-08-08 | The bare number is grandfathered onto `check-adr-anchors`'s citation allowlist, pending this tombstone | [#6634](https://github.com/objectstack-ai/objectstack/issues/6634) |
36+
| 2026-08-11 | [PR #7838](https://github.com/objectstack-ai/objectstack/pull/7838) makes a tombstone refuse *anchors* while still resolving *citations*, and leaves this number open because it needs a file under `docs/adr/` | [#7329](https://github.com/objectstack-ai/objectstack/issues/7329) |
37+
38+
The deleting commit's subject is *"feat(docs): add comprehensive analysis of Permission
39+
Protocol with AI-enhanced security controls and RLS implementation"*; its body is empty
40+
and it names no ADR. Alongside the registry it removed the whole `docs/METADATA_*`
41+
documentation family and the `examples/metadata-objectql` package. **No reasoning for
42+
retiring the decision was recorded anywhere**, which is the substantive difference from
43+
ADR-0107 and the reason this tombstone reconstructs the record from the tree rather than
44+
quoting a withdrawal.
45+
46+
## Do not resurrect it
47+
48+
Independently of how it was deleted, the record's substance is now **contradicted by
49+
shipped code**, so restoring the text would plant a false statement in the decision log.
50+
51+
Its selected option was a **hybrid dual-provider** architecture: *both* `@objectstack/objectql`
52+
and `@objectstack/metadata` may provide the `metadata` service, MetadataPlugin taking
53+
precedence when loaded and **ObjectQL registering itself as the fallback provider** when
54+
it is not. That fallback is exactly what the code stopped doing:
55+
56+
- **MetadataPlugin is the sole provider** of the `metadata` service — the one
57+
`registerService('metadata', …)` in the tree is
58+
[`packages/metadata/src/plugin.ts`](../../packages/metadata/src/plugin.ts).
59+
- **ObjectQL is a consumer, never a provider.**
60+
[`packages/objectql/src/plugin.ts`](../../packages/objectql/src/plugin.ts) registers
61+
`objectql`, `data`, `manifest` and `lifecycle` — and no longer `metadata`. It reads the
62+
`metadata` service and degrades to its own internal registry when none is present,
63+
which is not the same thing as claiming the slot.
64+
- **The shared-interface principle survived, as a spec contract**: `IMetadataService` in
65+
[`packages/spec/src/contracts/metadata-service.ts`](../../packages/spec/src/contracts/metadata-service.ts).
66+
67+
The live account of that surface is the "Metadata service architecture" section of
68+
[`ARCHITECTURE.md`](../../ARCHITECTURE.md). Read that, never this file, for what is true
69+
today. The withdrawn text is recoverable in full at
70+
`git show 908d95c82:docs/adr/0001-metadata-service-architecture.md` and is deliberately
71+
kept in history rather than reprinted here — a withdrawn record reprinted inside its own
72+
tombstone reads as a record.
73+
74+
Note that the single-provider architecture which replaced it **has no ADR record of its
75+
own**. That gap is real and is not closed by this file; re-homing it is a maintainer call.
76+
77+
## What ADR-0002 actually cites
78+
79+
Recorded because it is the first thing a reader arriving from ADR-0002 will want, and
80+
because it was mis-stated in `check-adr-anchors.mjs` until this tombstone was written.
81+
82+
[`0002-environment-database-isolation.md`](./0002-environment-database-isolation.md) says,
83+
of a rejected alternative: *"One global DB + tenant column. Was never on the table —
84+
already discarded in v3.4's ADR-0001."* That is **not** a reference to the record above.
85+
The record that held this number decided how the `metadata` *service* is registered and
86+
says nothing about tenancy or database topology.
87+
88+
Two distinct things carry the string "ADR-0001" in this repository's past, and only one
89+
of them was ever a file here:
90+
91+
- **The v3.4-era "ADR-0001" ADR-0002 is pointing at.** Nothing matching `*0001-*` was ever
92+
added under any path, on any branch, other than the metadata-service record — see
93+
[Archaeology](#archaeology). Today's ADR-0002 is itself dated 2026-04-19 and supersedes
94+
the v3.4/v4.0 per-organization database model, so its "v3.4's ADR-0001" is a reference to
95+
a pre-registry document that this repository has never contained.
96+
- **`docs/adr/0001-metadata-service-architecture.md`**, the record this tombstone retires.
97+
98+
The citation is therefore historical narration rather than a pointer into `docs/adr/`, and
99+
this file is what it now resolves to. Correcting ADR-0002's wording is an edit to an
100+
accepted record and is deliberately not made here.
101+
102+
## Do not anchor to this number
103+
104+
`scripts/check-adr-anchors.mjs` requires every `ADR-NNNN` cited in a tracked file to name a
105+
record under `docs/adr/`. This tombstone satisfies that check — deliberately, because the
106+
citations below are legitimate references to a deleted record. It is **not** a licence to
107+
cite ADR-0001 as governing anything: an anchor entry must state the invariant its ADR
108+
decided, and this number decides nothing today.
109+
110+
The citations that exist are all discussion of the deletion itself:
111+
112+
- [`ARCHITECTURE.md`](../../ARCHITECTURE.md) — names the deleted path as plain text and
113+
states the current single-provider architecture in its place;
114+
- [`0002-environment-database-isolation.md`](./0002-environment-database-isolation.md)
115+
the v3.4 reference dissected above;
116+
- `scripts/check-adr-anchors.mjs` — the gate, describing this case.
117+
118+
## Archaeology
119+
120+
Recorded so the next reader does not repeat it. Run against full history with the clone
121+
**unshallowed** — a shallow clone silently answers "never existed", and the default clone
122+
in this project's agent containers is 50 commits deep:
123+
124+
```bash
125+
git fetch --unshallow
126+
git log --all --diff-filter=AD -- 'docs/adr/0001*' # -> exactly 2 commits, both above
127+
git log --all --diff-filter=AD -- '*0001-*' # -> the same 2 commits, any path
128+
git log --all --oneline -S'ADR-0001' # -> 10 commits, all accounted for
129+
```
130+
131+
**Nothing else ever claimed this number**, on any branch, at any time: the second query
132+
widens the first from `docs/adr/` to the whole tree and returns the same two commits, so
133+
there is no second era of "ADR-0001" as a file and no lost content beyond the record named
134+
above.
135+
136+
## How this differs from ADR-0107
137+
138+
Both are tombstones and both are unreusable, but the two cases are not the same and the
139+
distinction is worth keeping:
140+
141+
| | ADR-0107 | ADR-0001 |
142+
|---|---|---|
143+
| Why it left | Owner decision, reasoning stated in the withdrawing commit | Collateral in an unrelated 37-path docs sweep; no reasoning recorded |
144+
| Lifetime | Nine hours | About thirty hours |
145+
| Deciders line | The withdrawal was itself the decision | Reconstructed from the tree in 2026-08; nobody decided the number should retire at the time |
146+
| Why not resurrect | Substance reversed by later shipped code | Same — the fallback-provider half is contradicted by the code today |
147+
148+
The shared conclusion is the one that matters: an ADR number that has ever named a record
149+
does not become free again by that record's removal, however the removal happened.

0 commit comments

Comments
 (0)