Skip to content

Commit 74dcce5

Browse files
authored
Merge branch 'main' into claude/issue-17508-repeater-row-property-localisation
2 parents 1cf7f2a + 13d5294 commit 74dcce5

21 files changed

Lines changed: 2211 additions & 50 deletions
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: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
'@objectstack/runtime': minor
3+
---
4+
5+
fix(runtime): `POST /api/v1/packages` parses the manifest's `version` leg instead of installing anything it is handed (#19120)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING for callers of the install door** — a manifest with no `version`, or
10+
one whose `version` does not match the declared semantic grammar, is now refused
11+
`400` / `VALIDATION_ERROR`. It used to install and answer `201`.
12+
13+
The accept set only shrinks back to what the published declaration has always
14+
said. `PackageInstallRequestSchema` binds `manifest: ManifestSchema`, and
15+
`ManifestSchema` declares `version` required with a semantic grammar. The door
16+
parsed nothing at all: `const manifest = body.manifest || body` went straight to
17+
`installPackage`, with an id check as the only gate on the way. That is
18+
«declared ≠ enforced» on a published API contract — and because the install
19+
landed silently, an author could install metadata the platform's own CLI build
20+
step (`os plugin build`) would have refused outright.
21+
22+
The gate asks the declaration **by reference** — `ManifestSchema.shape.version`
23+
— rather than keeping a copy of the grammar. The version-grammar canon is an
24+
open question on its own card; whichever way it is settled, this door follows it
25+
with no further edit.
26+
27+
**What is not affected.** Boot-time and in-process installs reach
28+
`SchemaRegistry.installPackage` / `ObjectQL.registerApp` directly and never pass
29+
through this branch, so nothing about how a package is loaded from disk or
30+
registered by a plugin changes. A well-formed manifest installs exactly as
31+
before, on both body forms (wrapped and bare) and on both install limbs (the
32+
protocol primitive and the bare-registry fallback).
33+
34+
**Scope — the `version` leg alone.** The declaration's own docblock records five
35+
classes this door answers `201` to while the schema refuses them. This change
36+
closes one: `version`. A missing `type`, unknown keys on either body form, a
37+
string-typed `enableOnInstall` / `overwrite`, and install options spelled on the
38+
bare form are each left exactly as they were — measured after the change, all
39+
four still answer `201`. Each is its own reading and its own card.
40+
41+
**If you are refused.** Give the manifest the `version` the schema has always
42+
required — `version: "1.0.0"`, three dot-separated numbers. The refusal names
43+
the key and shows the shape, so the prescription arrives with the `400` rather
44+
than in a changelog.
45+
46+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or reshaped: no spec key, no export, no stored row. `objectstack migrate meta` has nothing to reach, because there is no old spelling that maps to a new one — a caller supplies a key the declaration already required. The refusal itself carries the remedy. -->
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
---
4+
5+
feat(platform-objects): declare the `set_user_manager` row action on `sys_user` (#19249)
6+
7+
`sys_user.manager_id` drives the approvals `{ type: 'manager' }` rung and the ADR-0057 `own_and_reports` read scope, and `POST /api/v1/auth/admin/set-user-manager` (#16678 Phase 3) has been its only product write surface since it landed — with nothing in the Console reaching it. This declares that affordance: a `set_user_manager` row action on `sys_user`, offered from the Users list row menu and the record-detail header, collecting the new manager through an inline `sys_user` lookup and POSTing `{ userId, managerId }` to the admin endpoint.
8+
9+
Three properties of the declaration are decisions rather than detail:
10+
11+
- **It posts the admin endpoint, never the generic data API.** `sys_user` is `managedBy: 'better-auth'` and the ADR-0092 D2 managed-update whitelist is `{name, image, locale}`, so a picker writing `manager_id` through `/api/v1/data` would be refused by the identity write guard — correctly — and would read as a Console bug. The field keeps `readonly: true`; the endpoint reaches the column by system context.
12+
- **Its `visible` predicate carries the directory-sync term and not the self-service one.** A directory-owned identity (`source: 'idp_provisioned'`) is refused by the endpoint, so the button is hidden for one — the same term the three self-service identity actions on this object already spell. Their `record.id == ctx.user.id` half is deliberately not carried over: this is an admin action on someone else's row.
13+
- **No second copy of the server's refusals.** Self-assignment, cycle, depth, cross-organization and directory-owned identity are enforced at the write, in one derivation, and surface from there. Nothing is re-derived client-side.
14+
15+
Additive: no existing action, field or predicate changed. The `manager_id` field and its read-only rendering are untouched, and `sys_business_unit.manager_user_id` (Business Unit Head) is a separate, independent relation that this does not read or write.

‎content/docs/deployment/cli.mdx‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -608,10 +608,22 @@ below — the same fixture backs both output examples. It ships no `src/docs/`
608608
directory, which is why the docs step reads `0 collected`.
609609
610610
The docs step reports what it collected, not what it attempted — a project whose
611-
`src/docs/` is empty, or whose docs directory moved into a package under an
612-
ADR-0130 layout, prints `0 collected` here instead of the same sentence a
611+
`src/docs/` is empty prints `0 collected` here instead of the same sentence a
613612
successful collection prints.
614613
614+
Under an **ADR-0130 multi-package layout**, docs that moved into a package are
615+
collected too: a `src/<pkg>/docs/` directory whose `<pkg>` names one of the
616+
artifact's declared `packages[]` entries is read into **that package's own
617+
body**, and the step line says so:
618+
`12 collected (4 from 2 package directories)`.
619+
`<pkg>` is matched against two spellings of the package: its
620+
`id`, and the last dot-separated segment of that `id`. The display `name` is
621+
⛔ not a directory key — it is free to be re-worded, and a docs binding a
622+
re-wording can break is worse than one that never existed. A directory
623+
that matches **none** of the declared packages, or **more than one**, is still
624+
not read — that case keeps the warning that names the files it skipped and the
625+
spellings it tried, so it is never a silent `0 collected`.
626+
615627
The resulting `dist/objectstack.json` is a **portable, self-describing deployment unit** —
616628
you can hand it to `os start` (locally or on a server), publish it to a CDN, or fetch it
617629
over HTTP from another runtime. See [`os start`](#os-start) and

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -607,10 +607,17 @@ matrix answers the first question only.
607607
each package's body re-read as the stack it was assembled from, handed the
608608
artifact's own `packages[]` as **resolution context** so a sibling package's
609609
object still resolves, while the collections being judged stay that package's
610-
alone. It is de-duplicated against the union run, so what it reports is exactly
611-
the set the union could not see, and every finding it raises carries a
612-
`package '<id>' —` prefix in its `where`. A project with no `packages[]` skips
613-
it: one package by definition, already judged whole by the union run.
610+
alone. It is de-duplicated against the union run, so what it reports is the
611+
set of per-package findings no union finding already carried under the same
612+
rule, `where`, message and non-top-level position — the leading
613+
`collection[N]` index is neutralised, because a package body re-bases its
614+
collections from 0 and one finding would otherwise get two keys. Every
615+
finding it raises carries a `package '<id>' —` prefix in its `where`. That
616+
key is position-insensitive, not collision-proof: two entries that render the
617+
same `where` share one, so the bound is what the key can tell apart and no
618+
wider — `packages/cli/src/utils/artifact-packages.ts` states it, and why. A
619+
project with no `packages[]` skips it: one package by definition, already
620+
judged whole by the union run.
614621

615622
The second pass is not the first one in a stricter mood — it is a different
616623
question. A rule asking *"does this stack's app navigation point at an object

0 commit comments

Comments
 (0)