Skip to content

Commit 8ba9049

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-19132-prior-rulings-objectui-corpus
2 parents d0d6d35 + 13d5294 commit 8ba9049

52 files changed

Lines changed: 4153 additions & 255 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
Clause-②: yes
6+
7+
`os build` reads package docs from **each package directory** of an ADR-0130 layout — `src/<pkg>/docs/*.md` — and attaches them to the **owning package's body** (`packages[i].manifest.docs`), linted against **that package's own `namespace`** (#18431).
8+
9+
A module can now ship its own docs. Before this, ADR-0046 collection was anchored at exactly one path, `<config dir>/src/docs`, so an ADR-0130 project that moved its docs into their packages lost all of them — loudly since #18428, but lost. The maintainer's ruling (batch #147 item 4) decided the two contract questions that blocked the widening, and both are implemented literally:
10+
11+
- **Where they attach**: to `packages[i]`, ⛔ never the artifact top level. A body's docs are served because the load path **registers every body**: `AppPlugin` hands the whole artifact to `getService('manifest').register(…)`, which runs `resolveArtifactPackageOrder` (every package body, when `packages` is present) and calls `registerApp(body)` for each; `registerApp` feeds `registerMetadataCollections`, whose `METADATA_ARRAY_KEYS` carries `docs`. A doc written onto a body therefore reaches the registry under its owning package, so a flattened copy would buy nothing and would destroy the ownership D1 is about.
12+
- **Whose namespace the lint uses**: the owning package's. A doc outside any package keeps `stack.manifest.namespace`. A multi-package artifact therefore has **one prefix rule per package** and ⛔ no single global prefix — and ⛔ no fallback between the two: a package doc that fails its own package's prefix is refused, never re-tried against the artifact's.
13+
14+
**What it costs — the refused classes measured here.** Three classes of input that `os build` accepted before are refused now. Each follows from the ruled prefix rule — the owning package's `namespace`, ⛔ with no fallback to the artifact's — reaching docs the artifact's own prefix used to judge, or docs the docs lint did not reach at all; each needs the artifact to carry a `packages[]`, which `composeStacks(…, { manifest: 'preserve' })` produces from N authored stacks and which a hand-written entry also parses into (`ArtifactPackageSchema`); and each is pinned in the unit tier rather than only stated here.
15+
16+
**(1) A package doc carrying the ARTIFACT's prefix instead of its own.** A package that declares a namespace DIFFERENT from the artifact manifest's used to have its docs judged by the artifact's prefix; they are judged by its own now.
17+
18+
```
19+
FROM packages[i] with namespace "sales" inside an artifact whose manifest.namespace is "crm"
20+
shipping a doc named crm_orders_guide -> accepted before, REFUSED now
21+
TO rename it to sales_orders_guide (and the file to sales_orders_guide.md)
22+
```
23+
24+
The refusal is `docs/namespace-prefix`, an error, and it names that exact spelling.
25+
26+
**(2) A package that ships docs and declares NO namespace at all.** `manifest.namespace` is optional, so such a body is legal and its docs used to be judged under the artifact's prefix — the one global rule. With one prefix rule per package and no fallback, that package's own namespace is the only one that can answer for its docs, and ADR-0046 §3.2 requires it.
27+
28+
```
29+
FROM packages[i] with NO namespace, inside an artifact whose manifest.namespace is "crm",
30+
shipping docs (inline, or now from src/<pkg>/docs/) -> accepted before, REFUSED now
31+
TO declare namespace: "sales" on that package — its docs then take the "sales_" prefix
32+
or move those docs up to the stack level, where stack.manifest.namespace still judges them
33+
```
34+
35+
The refusal is `docs/namespace-required`, an error, located at `packages[i].manifest.namespace` — the key to add.
36+
37+
**(3) A hand-written `packages[i].manifest.docs` entry with no copy of that doc at the artifact top level.** `packages` is an authorable key of the stack definition, and its docblock says a hand-written entry still parses — it is an assembled body carrying no collections. That body admits every collection the artifact envelope does not keep for itself, `docs` among them, and `DocSchema.name` says a namespace prefix is "recommended, not required". Before this change nothing linted such a doc at all: the CLI's docs pass read the stack's own `docs` and `src/docs/` and never looked at `packages[]`, and no `@objectstack/lint` rule reads `.docs`, so `os build` exited 0 whatever the doc was called. Clause 2 makes it that package's doc, so every docs rule now reaches it under that package's namespace.
38+
39+
```
40+
FROM packages[i] with namespace "sales" carrying docs: [{ name: "playbook", ... }]
41+
and NO copy of that doc at the artifact top level -> exited 0 before, REFUSED now
42+
TO rename it to sales_playbook — or fix whichever rule the message names, because
43+
the whole docs lint reaches it now, not the prefix rule alone
44+
```
45+
46+
The refusal for that example is `docs/namespace-prefix`, an error, at `packages[i].docs/playbook`. A doc name a sibling package also declares is a cross-owner `docs/duplicate-name`; an image is `docs/no-images`; and so on through ADR-0046's v1 bans.
47+
48+
⚠️ **Those are the refused classes this change MEASURED — ⛔ not a claim that they are all of them.** Two of the three were added after a contract review of this card falsified an earlier draft of this note that had called the list complete; the closed claim is therefore dropped rather than re-made one class further out. The boundary that is honest: every refusal above is ONE rule — a package's docs are judged by that package's own `namespace`, with no fallback to the artifact's (the ruling's clause 2) — reaching a set of docs it did not reach before, and a shape nobody has measured yet can meet that rule the same way. If a build that was green fails on a doc, read the rule id the message carries: `docs/namespace-prefix` wants the owning package's prefix, `docs/namespace-required` wants that package to declare a `namespace`, `docs/duplicate-name` names both owners, and the content rules (`docs/no-images`, `docs/no-mdx`, `docs/filename`, `docs/flat-directory`) are ADR-0046's v1 bans, themselves unchanged.
49+
50+
In the other direction the same change is a widening, and the larger half: before it a package's `src/<pkg>/docs/` was not read at all, and a package could not ship a doc under its OWN prefix. An artifact whose every doc is package-owned also no longer needs a `stack.manifest.namespace` of its own.
51+
52+
⚠️ Same-prefix LINKS and metadata-embed references are deliberately NOT partitioned with the naming rule — both resolve across the whole artifact. A doc's prefix says who judges its NAME; a link asks whether the target EXISTS, and ADR-0130 D1 exists so that N packages may share a namespace and cross-link inside it. Partitioning links too would have turned an ordinary cross-package link into `docs/broken-link` and stopped an artifact that built green from building; that was caught by this card's contract review and is pinned in the unit tier.
53+
54+
Also in this change:
55+
56+
- **The #18428 warning stays**, and now says *why* a directory was not read. Unchanged, word for word, for a stack that declares no `packages[]` — where "read from `src/docs/` only" is still the whole truth. For a directory that names **no** package it lists the declared packages and the two spellings a directory is matched against (`id`, and the last dot-segment of that `id`); for one that names **more than one** it names the candidates and refuses to guess. ⛔ `namespace` is not a matching spelling: ADR-0130 D1 exists so that N packages can share one, so matching on it would be ambiguous exactly where it matters.
57+
- **A cross-owner duplicate doc name stays an error.** It PRESERVES a refusal rather than adding one: before the split every doc reached the lint in one flattened array, so two owners declaring one name already raised `docs/duplicate-name`. Splitting the set per package would have dropped that silently, and ADR-0130 D1 lets packages of one artifact share a namespace, so the prefix does not keep them apart. The rule is authoring hygiene — ⛔ not a claim that one registration overwrites the other, which ADR-0048 §3.3/§3.4 retired.
58+
- **`os dev` mirrors `os build`.** The config-load path collects the same per-package directories onto the same bodies, so dev serves what a built artifact serves.
59+
- **The step line counts the whole collection**, package sets included, and says how many came from package directories — a build that read four package docs no longer announces `0 collected`.
60+
61+
Single-package projects are untouched: with no `packages[]` there is nothing to attribute, the flat `src/docs/` keeps attaching exactly where it always did, and the emitted artifact is byte-identical.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`enableOnInstall` is declared in three published schemas; each one now says which of the three governs it, and the two that are not the authority say what they are (#18605).
6+
7+
The install door already honours the key — `POST /api/v1/packages` writes the registry row's `enabled` from `enableOnInstall ?? true` (#18058). What was left was three declarations that looked identical (`z.boolean().default(true)`, same description) with nothing saying which one an author should read.
8+
9+
Clause-②: yes
10+
11+
**The authority**
12+
13+
`PackageInstallRequestSchema` (`api/package-api.zod.ts`) is the one authority, because it is the request contract of the door that honours the key. Its published description now says so: "honoured at POST /api/v1/packages: the installed row's `enabled` is written from this key". Its doc block carries the map to the other two, so a reader never has to guess which of three identical-looking declarations governs.
14+
15+
**`kernel/InstallPackageRequest.enableOnInstall` — a COPY of the request key**
16+
17+
Same type, same default, same meaning, restated on the in-process protocol primitive `ObjectStackProtocol.installPackage`. Its published description now records that this layer does not read it: the implementation reads `manifest` and `settings` only, and the HTTP door does not forward the key down that seam — it calls `installPackage({ manifest, settings })` and performs the enable/disable flip itself, because the durable half must follow the row that door returned rather than the request's intent.
18+
19+
The copy is held to the authority by a **parity pin** rather than by a structural reference. The structural spelling is not available in this direction: the authority is built from `ManifestSchema` and `InstalledPackageSchema`, both declared in `kernel/package-registry.zod.ts`, so `PackageInstallRequestSchema.shape.enableOnInstall` spelled there is an import cycle, and under `OS_EAGER_SCHEMAS=1` — the mode `gen:schema` and `check:authorable-surface` run in — it dies with `ReferenceError: Cannot access 'InstalledPackageSchema' before initialization`. `api/package-install-one-authority.test.ts` parses both declarations over one matrix (absent, `false`, `true`, a string, `null`) and reds on any cell where they disagree.
20+
21+
**`marketplace/MarketplaceInstallRequest.enableOnInstall` — not this key at all**
22+
23+
It stays, and its published description says what it is: the marketplace channel's own install option. That request's subject is a listing (`listingId`, `version`, `licenseKey`, `tenantId`), not a manifest; its door is the control plane's `POST /api/v1/marketplace/install`, of which a runtime mounts only a read-only proxy; and the channel resolves the artefact and validates the licence before mapping what it holds into a platform install. It is one translation upstream of the door key, owned by a different party on a different release cadence, so folding it would let a narrowing at the platform door silently narrow a control-plane contract.
24+
25+
**What does not move**
26+
27+
No key is added, removed, renamed or retyped, and no default changes: the accept set of all three schemas is byte-for-byte what it was, and `api-surface`, `api-surface-declarations`, `authorable-surface` and `authorable-defaults` are all unchanged. What moves is the published description text of three keys and the reference pages generated from it. The `Clause-②` declaration is `yes` as the conservative arm, because three published declarations' stated meaning moves.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/objectql": patch
3+
"@objectstack/runtime": patch
4+
---
5+
6+
Installing a package no longer reverts an operator's most recent enable/disable. The install contract is now 「缺省 = 保持,有旗 = 设置」: an install that was not asked to move the lifecycle state does not move it (#18877).
7+
8+
`SchemaRegistry.initialDisabledPackageIds` is a boot hydration input — filled once, before any registration, from the durable disable file, and never updated by `enablePackage` / `disablePackage`. It was nevertheless consulted by every `installPackage` call, so once an id was in the boot seed set, every re-install within that boot re-landed it DISABLED whatever the operator had most recently done. Since the durable write started following the row the door returns (#18752), that stopped being memory-only:
9+
10+
```text
11+
boot 1 operator disables the package → disk lists the id
12+
boot 2 seeded from disk; the package installs disabled
13+
PATCH /packages/:id/enable → 200, registry true, disk CLEARED
14+
install(m, { overwrite: true }) (no flag) → the seed still listed the id
15+
→ row disabled, disk written DISABLED
16+
boot 3 the operator's enable is gone, with no error anywhere
17+
```
18+
19+
Reachable with nothing exotic: disable → restart → enable in Studio → an SDK upgrade with `overwrite`.
20+
21+
- **`installPackage` reads the ROW first.** An existing row keeps its own `enabled`, `status` and `statusChangedAt`; the boot seed decides only for an id that has no row yet (boot hydration and a genuinely fresh install). A fresh id the seed never named still lands enabled, the declared default.
22+
- **`enableOnInstall` now sets the state in BOTH directions.** `true` ⇒ `enablePackage`, `false` ⇒ `disablePackage`, and an ABSENT flag makes no lifecycle call at all — previously only `false` was read, and the `true` case was carried by the re-install restamping every row enabled. The bare (unwrapped) body form still honours nothing: no schema declares the key there.
23+
- **`DELETE /packages/:id` clears both records.** The id leaves the boot seed set with its row, and its durable disable entry is cleared, so the next install of that id is a fresh install. Previously the durable record was immortal — a delete left a disable behind that named a package that no longer existed.
24+
- ⚠️ **Behaviour change for a flag-absent re-install of an EXISTING row.** It used to return the package to the declared default (enabled); it now preserves what the row says. An upgrade flow that relied on a re-install to clear a disable must now send `enableOnInstall: true` — the same key, the same door, now honoured in that direction. A fresh install is unaffected.
25+
26+
Maintainer decision batch #157 item 5, letter C. Item 4 of that ruling re-rules the #18058 F1 pin 「flag-absent re-install clears the durable disable」 to 「preserves」; F1b stands unchanged.
27+
28+
Clause-②: no
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
`buildSummaryIndex` no longer drops a declared `summary` field silently when the roll-up's `reference` carrier cannot be read — the skip now reports itself at `error`, naming the field, the consequence and the fix (#19082).
6+
7+
The child→parent foreign key is resolved by scanning the child object's `master_detail` / `lookup` fields for one whose `reference` names the parent. That comparison read the carrier raw (`cd.reference === parent.name`), so a carrier **no reader can read** — a non-string, where `FieldSchema.reference` declares an optional string — compared `false` against every name, `fkField` stayed unset, and
8+
9+
```ts
10+
if (!fkField) continue; // can't resolve the relationship — skip
11+
```
12+
13+
removed the roll-up from **both** summary indexes. `recomputeSummaries()` then had nothing to do after every insert / update / delete of the child, so the parent's stored summary value kept whatever it held while each of those writes reported success, and nothing anywhere said so. It is the second way this one function invents *"nothing to recompute"*; the first, its registry read, was closed as #9154.
14+
15+
- **⛔ The resolution rule is deliberately unchanged.** Loosening the comparison would trade a silent stall for a **mis-matched foreign key**, which is more expensive: a roll-up quietly aggregating the wrong children reads exactly like a correct one. PR #18503 recorded this site in its C2 list and the #18550 round left it there on purpose; that boundary still stands. What ends is only the silence.
16+
- **The carrier is read through the one arbiter**, `referenceCarrierOf` — the same accessor #19080 routed the two delete-cascade seams through. Its refusal is **caught** here rather than propagated, because this is a *scan* looking for the foreign key across every relation field: a propagating refusal on one unreadable field would hide a readable sibling that really is the FK, turning a roll-up that works today into a hard failure of every write to that child.
17+
- **`error`, not `warn`**, and said once per index build rather than once per write. A persisted summary that silently stops tracking its children while every write keeps reporting success is the durability class, and the line it prints carries both halves an operator needs: what is not being maintained and will not recompute, and the two ways to fix it — spell the carrier as the target object's name, or name the FK explicitly with `summaryOperations.relationshipField`.
18+
- **Absence is untouched.** `undefined`, `null` and `''` mean "this field names no target", which is a legal thing to declare; they skip silently exactly as before. Every readable carrier resolves exactly as before.
19+
20+
No schema changed, no key was added or removed, and nothing that resolved before resolves differently now. `engine-summary-index-unreadable-carrier.test.ts` pins both directions — the unreadable carrier reporting its skip, and a normal `reference` still resolving `fkField` — because without the second one, a change that simply stopped resolving anything would look identical to a fix.

0 commit comments

Comments
 (0)