Skip to content
Merged
Show file tree
Hide file tree
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
15 changes: 15 additions & 0 deletions .changeset/15292-dev-plugin-malformed-stack-posture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@objectstack/plugin-dev": patch
---

`DevPlugin`'s boot posture on a malformed stack is now written down: dev boot tolerates and reports; refusing belongs to the production doors, which are not uniform about it (#15292).

Clause-②: no

No behaviour changes. `DevPlugin` already degraded a stack the platform would reject, and the posture — ruled, not invented here — is that it should: the contract refuses at the production door, while the developer's inner loop tolerates incomplete input and never hides it. Metadata that is incomplete halfway through an edit is the normal state of a project under active development, so refusing at dev boot would charge the cost to the only user group this plugin exists for, for a consistency the production doors already provide. What was missing was the written posture and one load-bearing correction to it.

- **The two branches are not one defect handled two ways.** `new AppPlugin(stack)` reads `manifest.id` / `manifest.name` and nothing else, so a malformed `packages[]` passes the constructor untouched and is refused one branch later: `AppPlugin.init()`'s LAST statement hands the bundle to the `manifest` service, whose `register()` calls `resolveArtifactPackageOrder` unguarded, and `DevPlugin`'s child-`init()` loop degrades that refusal to an `error` line. The lazy `collections` getter is not on that path at all — it is not read during `init()`, and its first read is in `AppPlugin.start()`, where it reaches the same refusal on the same bytes. Both in-file comments that named the constructor as the stack's parse door (*"a malformed stack throws HERE"*, and §3b's *"twenty lines above, `new AppPlugin(...)` parses the SAME object"*) overclaim for that reason, and both are corrected in this PR.
- **The two malformations are exact complements, measured with a lit control.** An app payload with no `manifest.id` / `manifest.name` throws from the constructor (a bare `Error`, no ADR-0112 `code` / `status`) and is invisible to the package-list parse; a `packages[]` entry that is not a package entry (ADR-0130 D4) is invisible to the constructor and refused by the parse as `INVALID_ARTIFACT_PACKAGE_ENTRY` / `422`. A stack carrying neither is silent on both. So a clean boot past one branch is no evidence about the other — which is why the division is now documented rather than left to be re-derived.
- **Tolerating is not hiding.** The posture's second half is that a boot which skipped something is never byte-identical to a healthy one: a silent degrade lets an author, or a coding agent, read "it started" as "I wrote it correctly".
- **The production doors are not uniform, and every carrier now says so.** A malformed `packages[]` fails `ObjectStackDefinitionSchema` — `packages: z.array(ArtifactPackageSchema)`, the SAME entry schema the runtime parse uses — and both `os validate` and `os build` parse the lowered stack against it and exit 1 (`validate.ts` step 2; `compile.ts` step 3). `lowerCallables` passes a non-`{ manifest: object }` entry through untouched, so the verdict transfers to what the CLI actually parses. An app payload with no `manifest.id` parses green at BOTH: `os validate` reports it only as the structural advisory *"Missing manifest.id — required for deployment"*, which fails only under `--strict` (both exit faces read one `warnings` list — the `--json` ternary and the text face's `if (flags.strict)` block), and `os compile` "never computes them at all" in its own words, so `os build` is silent on it. The flat "`os validate` / build / publish refuse" overstated BOTH doors for that half, and `publish` is simply not a door this card measured, so it is no longer claimed.
- **What ships**: the `DevPlugin` docblock (which reaches the published `dist/*.d.ts`), the two in-file comments named above, and `content/docs/plugins/packages.mdx`, plus a test pinning the posture, its division and the init-time path the refusal actually takes. The wording of the malformed-metadata diagnostic itself is deliberately not pinned — that text is a sibling change.
44 changes: 44 additions & 0 deletions content/docs/plugins/packages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,52 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern

- **Features**: Auto-assembles ObjectQL + in-memory driver + auth + security + Hono server + REST + dispatcher + app metadata, plus optional real services when installed (storage, realtime, i18n); registers no stubs — a slot no plugin fills stays empty, as in production (ADR-0115); refuses to boot with `NODE_ENV=production` (`OS_ALLOW_DEV_PLUGIN` escape hatch, which brands the override in the boot log and on the ready banner instead of overriding silently)
- **When to use**: Zero-config local development and playgrounds
- **Malformed metadata**: Dev boot **tolerates and reports** — it never refuses, and it never stays quiet
- **README**: [View README](https://github.com/objectstack-ai/objectstack/blob/main/packages/plugins/plugin-dev/README.md)

#### Malformed metadata: dev boot tolerates and reports

**`os dev` keeps booting on a stack the platform would reject; refusing belongs to
the doors an artifact leaves your machine through — and those doors are not uniform
about it.** `os validate` and `os build` both parse your stack against the same
protocol schema and exit 1 when it fails; what that schema accepts but a deployment
still needs comes back from `os validate` as an advisory instead, which `--strict`
promotes to a failure. The dev server skips only the part it could not read, boots the
rest, and prints an `error` line naming what was malformed and what was skipped — the
shape every mainstream dev server takes, where the error overlay stays on screen while
the server keeps serving.

This is deliberate, and it is one posture rather than two. Metadata that is incomplete
halfway through an edit is the **normal** state of a project you are actively working
on, so refusing to start would charge the cost to the only people this plugin exists
for. The contract is still enforced — just at the doors where an artifact leaves your
machine. See [Validating metadata](/docs/deployment/validating-metadata) for what
those doors check.

Tolerating is not hiding. A boot that skipped something is never byte-identical to a
healthy one: if the diagnostic were dropped, an author — or a coding agent — would
read "it started" as "I wrote it correctly", which is exactly the outcome this posture
exists to prevent. If you see one of these lines, the app is running **without** the
metadata it names.

<Callout type="warn">
Two different malformations reach this through two different branches, and they are
exact complements — each is invisible to the other, so a clean boot past one is no
evidence about the other. An app payload with no `manifest.id` / `manifest.name` is
refused as the app metadata is constructed. A `packages[]` entry that is not a
package entry (ADR-0130 D4) walks past that construction untouched and is refused
one step later, when the app's own `init()` hands the stack to the kernel's
`manifest` service and its package list is parsed; it reports as
`INVALID_ARTIFACT_PACKAGE_ENTRY` (422) on the `error` line naming the app plugin.

The two differ at the production doors as well. The malformed `packages[]` fails
the protocol schema, so both `os validate` and `os build` exit 1 on it. The missing
`manifest.id` parses green for both: `os validate` reports it as the advisory
*"Missing manifest.id — required for deployment"*, which exits 0 unless you pass
`--strict`, and `os build` does not report it at all. So run `os validate --strict`
if you want that half to fail too — a green `os build` is not evidence about it.
</Callout>

### @objectstack/plugin-approvals

**Approvals Plugin** — Contributes the `approval` flow node (ADR-0019): an approval runs on the one automation engine as a durable-pause node, backed by `sys_approval_request` / `sys_approval_action`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,12 @@ describe('#15232 — DevPlugin i18n auto-detect over a multi-package stack', ()
// (packages/spec/src/assembled-package-body.test.ts). That project boots
// today; a reader that threw here would have stopped it booting — and from
// the block whose only job is deciding whether to register a translation
// service, while `new AppPlugin(...)` twenty lines above degrades the very
// same refusal to a log line.
// service, while the app-metadata branch degrades the very same refusal to
// a log line — `AppPlugin.init()` hands the stack to the `manifest`
// service, whose `register()` reaches the SAME `resolveArtifactPackageOrder`
// parse, and DevPlugin's child-`init()` loop logs it instead of rethrowing.
// ⛔ Not `new AppPlugin(...)`: the constructor reads `manifest.id` /
// `manifest.name` only and never sees this malformation (#15292).
const refused = additiveNoI18nProject();
(refused.packages as Array<{ manifest: Record<string, unknown> }>)[0]
.manifest.objects = ['./src/objects/*.object.ts'];
Expand Down
Loading
Loading