Skip to content

Commit f47ba19

Browse files
committed
Merge origin/main into claude/issue-15178-translation-bundle-split
Resolves the one hand-written conflict in packages/spec/src/type-alias-convention.pin.test.ts. Both sides added pins off the same base (784) and both took the next free ids Iso877/Iso878: #17551's three api/analytics.zod.ts dataset-selection pins on main, this branch's two system/translation.zod.ts platform-bundle pins. Both intents stack; only the numbering collided. main's landed ids stand, this branch renumbers to Iso880/Iso881, and the count becomes 789 = 786 union 787. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
2 parents 8dcd6a4 + e9861d2 commit f47ba19

176 files changed

Lines changed: 14144 additions & 1375 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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`liveness/rest_api.json` — `RestApiConfigSchema` (the `api` sub-object of `RestServerConfig`) is now a governed liveness type. Every key it declares carries a status, the evidence that settles it and the producer that populates it: 12 `live`, 14 `dead` across 26 classified properties (#14640).
6+
7+
The ledgers ship inside this package (`files[]` includes `liveness`), so this is one new file in the tarball plus two changed ones — `liveness/README.md`'s index row and heading, and the generated `liveness/state-counts.md`. Nothing else moves: no schema accepts or refuses anything it did not before, no export changes, no runtime behaviour changes, and no CLI author warning is added (no entry is marked `authorWarn` — a `RestServerConfig` is not part of a stack, so the lint that emits those warnings could never reach one).
8+
9+
- **Why it was ungoverned, and why that reason has expired.** #14369 enrolled four of the five `RestServerConfig` sub-objects and deliberately left this one out: the `api` block's consumption seam was then still validate-only, so a census would have recorded a half that was about to move. That half has moved — `RestServer.normalizeConfig` now builds the `api` block from `parseDeclaredApiConfig`'s output rather than discarding it, and the change is released, not in flight. The fence was re-tested before anything here was written; it no longer holds, so the sub-object is measured on the settled seam. The stale sentence is corrected in the gate source and in the four README rows that repeated it.
10+
- **The ledger is `rest_api.json`, never `api.json`.** That name was already taken, by a different `api`: `ApiEndpointSchema`, the registered `api` metadata type, with real consumers in the matcher, executor, policy chain and mapping layer. One spelling, two unrelated meanings inside this package — filing here would have published one file's measurement under the other's name. The gate's own override paragraph now carries that fence too.
11+
- **Twelve keys are `live`.** `version`, `basePath` and `apiPath` become the prefix of every route the server mounts, through `getApiBasePath` — read via a whole-block destructure rather than a property access, which is why the census for the dead keys swept destructuring shapes with their own control instead of relying on a property-access pattern. The eight `enable*` switches each gate a mount, and most of them also the discovery document's capability block, so mount and advertisement move together. `projectResolution` decides whether the unscoped legacy routes mount alongside the scoped ones.
12+
- **Fourteen are `dead`**: the `requireAuth` tombstone, and the two declared containers `documentation` and `responseFormat`, which `normalizeConfig` copies into `this.config.api` and nothing reads back. So `api.responseFormat.envelope: false` unwraps no response, and `api.documentation.title` retitles no served OpenAPI document — that document's `info` block is written by this package's own `build-openapi.ts` and passed through untouched by the REST layer.
13+
- **`documentation` is drilled**, including its nested `contact` and `license` objects, so the change adds no row to the undrilled-container baseline. Those five leaf verdicts rest on a structural argument rather than a spelling sweep, which is stated in each row: `name` / `url` / `email` are too generic to grep, but the container that holds them is unreachable from outside `RestServer` — the field is `private` and `NormalizedRestServerConfig` is module-local with no `export` — so nothing can read a member of an object nothing reads.
14+
- **The two dead containers deliberately do not share one verdict.** This file records status; it decides nothing. The enforce-or-remove call per key (ADR-0049) is a follow-up on the human floor, and the two differ: `documentation`'s members are OpenAPI `info` fields whose enforce route collides with a recorded ownership decision, while `responseFormat`'s enforce route would mean making the response envelope configurable — a larger claim.
15+
16+
For an embedder, the practical read: every key that changes what this server mounts or advertises is marked `live` and evidenced; `documentation` and `responseFormat` are accepted, validated and normalized, and change nothing.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/plugin-dev": patch
3+
---
4+
5+
`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).
6+
7+
Clause-②: no
8+
9+
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.
10+
11+
- **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.
12+
- **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.
13+
- **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".
14+
- **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.
15+
- **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.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
`IMetadataService` declares `loadManyKeyed?` — the keyed plural loader read now sits on the contract beside its two declared siblings `loadMany?` and `loadDiagnosed?` (#15385).
6+
7+
Clause-②: yes
8+
9+
A verb family lives whole on the contract. `MetadataManager.loadManyKeyed(type)` shipped as a public member with no declaration on the interface its siblings are declared on, so the one cross-package caller — the ObjectQL governance audit — narrowed the service slot with a **local structural type** written beside the call site. That local type is deleted in the same change and the call site reads the contract.
10+
11+
The vocabulary is not new: `loadManyKeyed`, and the `{ name, data }` item shape it answers with, are already published on `MetadataLoader`, which declares the same member as optional over its own loader-local options type. What this adds is the member's place on `IMetadataService`.
12+
13+
```ts
14+
loadManyKeyed?<T = unknown>(
15+
type: string,
16+
options?: Record<string, unknown>,
17+
): Promise<Array<{ name: string; data: T }>>;
18+
```
19+
20+
**What it is for.** The key is a fact about the **store** — `register()`'s own `name` argument — and it travels *beside* `data`, never folded into it, so `data` stays byte-identical to what the unkeyed plural read would return and no consumer ever sees a synthesised `name`. An item whose stored body has no top-level `name` is legal and deliberate (an org customization container's identity is the object it targets), and such an item has no identity at all in a plural read keyed by `data.name` — it is dropped, silently. That is why this is a second member rather than a widened return type on the existing one.
21+
22+
**What moves for consumers.** Nothing breaks. The member is **optional**, like `loadMany?` and `loadDiagnosed?` beside it, so every existing `IMetadataService` implementation still satisfies the contract unchanged and the `typeof … === 'function'` probe stays the way a caller asks for it. What changes is that a caller no longer has to declare the shape itself to stay typed: intersecting the slot with a hand-written structural type was the only way to reach the member without erasing the lookup to `any`, and that workaround is now unnecessary. `MetadataManager`, which already implements the member, needs no edit.
23+
24+
This is the position `loadDiagnosed` was in before #4127 batch 4 declared it, and it is resolved the same way. Ruled in decision batch #123 item 5 (2026-09-12), maintainer verbatim: 「同意」.
25+
26+
`content/docs/kernel/contracts/metadata-service.mdx` gains the member in the same change.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: retire the plugin-security scan-result surface — zero consumers after the scanner retirement (#15932)
6+
7+
**BREAKING** — the plugin-security scan-result family is removed. ADR-0049
8+
enforce-or-remove; maintainer ruling 2026-09-07 (director seat, decision batch
9+
#65), adopted verbatim 「同意」.
10+
11+
This is the second half of the scanner retirement — issue 14919, a number since
12+
deleted from the board, landed as PR #15930. That change retired `PluginSecurityScanner`,
13+
the `@objectstack/core` class that shipped as a security control and returned
14+
`status: "passed"` for every plugin it was ever handed. The **schemas** it fed
15+
survived it — and that scanner's type-only import was their only importer of any
16+
kind, so the family went from one type-only importer to **zero consumers** while
17+
staying fully published: 27 authorable rows on `authorable-surface/kernel.json`,
18+
six `api-surface` exports, two authorable defaults and two json-schema manifest
19+
keys, with no `.parse` or `.safeParse` site against either schema anywhere. An
20+
author could write any of it, be accepted, and get nothing. That is the
21+
declared-not-enforced shape, one layer out from the class removed for the same
22+
reason. "Declare an owner to enforce" was refused by name: it would rebuild the
23+
scanner just retired.
24+
25+
### FROM → TO
26+
27+
| removed | what to write instead |
28+
| --- | --- |
29+
| `KernelSecurityScanResult`, `KernelSecurityScanResultParsed`, `KernelSecurityScanResultSchema` (exports) | nothing — delete the import. No replacement type exists. |
30+
| `KernelSecurityVulnerability`, `KernelSecurityVulnerabilityParsed`, `KernelSecurityVulnerabilitySchema` (exports) | nothing — delete the import. No replacement type exists. |
31+
| `PluginSecurityManifest.scanResults` | delete the key |
32+
| `PluginSecurityManifest.vulnerabilities` | delete the key |
33+
| `PluginQualityMetrics.securityScan` | delete the key |
34+
35+
**The one-line fix: delete the keys and every import of the two types.** Plugin
36+
security scanning is not a platform capability and there is no replacement
37+
schema. What the platform does still enforce is unchanged: `permissions` and
38+
`sandbox` on the same `PluginSecurityManifest`, and artifact provenance through
39+
`verifyPluginArtifactIntegrity` and the plugin signature verifier — which tell
40+
you an artifact is the one its publisher signed, and never that it is safe. For
41+
dependency vulnerabilities use the tools built for it against your own project
42+
(`npm audit` / `pnpm audit`, Dependabot, the GitHub Advisory Database, OSV), and
43+
treat an unaudited third-party plugin as untrusted code. A publisher who used
44+
`scanResults` to advertise diligence keeps the surviving `securityContact` and
45+
`vulnerabilityDisclosure` blocks, which are contact terms rather than a verdict.
46+
47+
⚠️ Runtime behaviour is deliberately **unchanged**. Nothing ever read any of
48+
these keys, so deleting one removes no check that was running. A consumer that
49+
gated on `securityScan.passed === true` was gating on nothing — the remediation
50+
is to audit with a real tool, not to find a replacement key.
51+
52+
### The retirement kit
53+
54+
- The two **defs** leave the build whole — `RETIRED_DEFS_BY_MAJOR[18]`
55+
(`kernel/KernelSecurityScanResult`, `kernel/KernelSecurityVulnerability`) —
56+
because nothing parses them, so there is no author a tombstone could reach.
57+
- The three **authorable keys** are `retiredKey()` tombstones registered in
58+
`RETIRED_KEYS_BY_MAJOR[18]`. Neither carrying shape is `.strict()`, so a bare
59+
deletion would strip an authored key in silence (ADR-0104): the tombstone is
60+
audible in both channels — `tsc` (input type `never`) and the parse, which
61+
raises the prescription itself.
62+
- **No D2 conversion.** A plugin security manifest and a plugin registry entry
63+
are package artifacts a publisher ships, never stack collection members and
64+
never stored `sys_metadata` rows, so the chain has no seam that would see one
65+
— the disposition the sibling `kernel-plugin-security-durations-unit-in-key`
66+
entry already records for this same manifest. The D3 semantic entry
67+
`plugin-security-scan-result-surface-retired` carries the judgement.
68+
- `PluginSecurityManifest.vulnerabilities` is a **forced consequence**, not one
69+
of the four names the ruling listed: it was the last authorable referent of
70+
`KernelSecurityVulnerability` and could not outlive the def.
71+
- **No deprecation window** (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」).
72+
73+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec`
74+
is published, so this is breaking for consumers no download, dependent or source
75+
telemetry was consulted for — exactly as that retirement's own changeset says of its
76+
three exports. That was an input to the ruling, not a reason to soften the removal.
77+
78+
⛔ **Untouched, and not checked:** the marketplace `'scanning'` status
79+
(`marketplace.zod.ts`). The ruling made it conditional on a producer grep of
80+
`objectstack-ai/cloud`, and that repository was not reachable from the session
81+
that executed this card, so it stays exactly as it is and its absence from this
82+
diff is not evidence about it.
83+
84+
⚠️ **The two members the ruling paired with it were ALREADY GONE** — measured on
85+
this tree, not assumed. The incident `'malware'` type was a member of
86+
`system/IncidentCategory`, and the whole incident-response family was retired by
87+
#15513 (maintainer ruling 2026-09-05 — two days *before* the 2026-09-07 ruling
88+
that made it conditional). `marketplace-admin.zod.ts` was deleted outright with
89+
the cloud subpath (#16526). Both files return zero tree entries here, against a
90+
lit control where `'scanning'` still returns a live declaration. So the
91+
conditional question is **one** enum member wide, not three.
92+
93+
`Clause-②: yes (narrowing)` — a published surface is removed: six exports leave
94+
the built `.d.ts` and three authorable keys stop being writable, so the accept
95+
set a consumer writes against narrows. Nothing is widened and nothing is
96+
renamed. Contract-review tier.
97+
98+
<!-- adr-0087: registered plugin-security-scan-result-surface-retired -->

‎.changeset/17108-element-text-variant-published-nine.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ Measured on the 17.3.0 declaration, per value, through `ElementTextPropsSchema.s
1414
- **⛔ Nothing is retired.** `heading` and `subheading` become named refusals carrying migration hints in **release 2**, which is a separate card and is blocked on a value-level retirement mechanism that does not exist yet: `retiredKey()` and ADR-0087 D2 retire a *key*, not a *value*. Authors who want to move early can write `h2` for `heading` and `h3` for `subheading`; neither spelling stops working in this release.
1515
- **No renderer changes here.** `element:text`'s renderer, its designer inspector options and its i18n rows are objectui's, on the released pin, and land on objectui's side of the sequence.
1616

17-
Generated projections follow the declaration: `api-surface-declarations/ui.txt` gains the seven members on `ElementTextPropsSchema` and on `ComponentPropsMap['element:text']`, and the `content/docs/references/ui/component.mdx` property table widens. `check:api-surface` reports nothing removed or narrowed.
17+
Generated projections follow the declaration: the `content/docs/references/ui/component.mdx` property table widens. `check:api-surface` reports nothing removed or narrowed.

0 commit comments

Comments
 (0)