Skip to content

Commit d60f82c

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-17385-chartconfig-precedence-half-2
Resolved three modify/delete conflicts by taking main's DELETION: `packages/spec/api-surface-declarations/` was reverted off main whole by #19024 (the declaration-text snapshot is taken back and the 27 signature hashes restored), together with its `check:`/`gen:` scripts. This branch had only regenerated three of those files; with the artefact and its gate gone there is nothing for those edits to be about. ⚠️ Committed BEFORE regenerating, per scripts/pm/os-regen-merge.sh step 3: the os-regen driver exits 0 while silently dropping one side, so the regeneration belongs in its own commit on a known-good base. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
2 parents ff7c8cd + b1d3945 commit d60f82c

66 files changed

Lines changed: 2922 additions & 238730 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.

‎.changeset/16045-spec-declaration-text-snapshots.md‎

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING** for authored metadata — `ObjectSchema.fields` refuses a key named `__proto__`, `constructor` or `prototype`, and `AssignmentConfigSchema.assignments` (the `assignment` flow node's variable map) refuses a key named `__proto__` — both refused with a named, located error at parse time, rather than silently accepted and then silently mishandled (objectstack#17852, objectstack#18847).
6+
7+
## Why
8+
9+
zod's `z.record()` skips a `__proto__` own key entirely, above its own key schema — the record parser's `if (key === "__proto__") continue;` runs before `def.keyType._zod.run`, so no key grammar (a regex, `.refine()`, `.superRefine()`, even a key schema that rejects every string) can ever see that key. A document whose `fields` (or `assignments`) carried a `__proto__` own key — which `JSON.parse` produces routinely — used to parse as SUCCESS with that key silently missing from the output: the validator accepted a document and handed back a *different* document. `os build` writes the release artifact from that returned document, so the failure shape is success, silent, and irreversible into the shipped artifact.
10+
11+
Two independent mechanisms close this, one per name class, because they are not reachable the same way:
12+
13+
- `__proto__` is refused by a **pre-parse guard** that reads the raw input's own keys before the record ever parses, at both `ObjectSchema.fields` and `AssignmentConfigSchema.assignments`.
14+
- `constructor` and `prototype` — which, unlike `__proto__`, DO reach the key schema unskipped — are refused by `ObjectSchema.fields`' own key grammar (they were ordinary lowercase words its regex already admitted). They are **not** refused at `AssignmentConfigSchema.assignments`: that slot's key type carries no grammar at all (`z.string().min(1)`), both names are legal flow-VARIABLE names measured to survive parse intact today, and no ruling narrows that slot's accept set for them — only its `__proto__` half moves.
15+
16+
Measured: zero authored use of any of the three names as a `fields` key or an `assignments` variable name, across this repo, `examples/` and `objectui`.
17+
18+
## Known gap, left open on purpose
19+
20+
The guard runs at parse time only. It does not project into the published JSON Schema (`packages/spec/json-schema/**`) — the general gap that closes is tracked separately (objectstack#18670) and stays open after this change.
21+
22+
Clause-②: yes (narrowing)
23+
24+
<!-- adr-0087: not-required (no-migration-prescription) zero authored use of `__proto__`, `constructor` or `prototype` as a `fields` key or an `assignments` variable name across this repo, examples/ and objectui — nobody has anything to rewrite, so there is no prescription to give. -->
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. -->

0 commit comments

Comments
 (0)