Repository navigation
Fix fumadocs-mdx validation: flatten nested pages in concepts meta.json - #210
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Update specification references in project
Fix fumadocs-mdx validation: flatten nested pages in concepts meta.json
Jan 26, 2026
hotlong
marked this pull request as ready for review
January 26, 2026 04:32
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a CI build failure by updating the fumadocs-mdx metadata structure in the concepts documentation. The nested "Protocol Namespaces" grouping structure was causing validation errors because fumadocs-mdx expects flat string arrays in the pages field, not nested objects.
Changes:
- Flattened the
pagesarray structure in both English and Chinese concept meta files - Removed the nested "Protocol Namespaces" grouping object
- Maintained all 11 protocol pages at the top level in the same sequential order
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| content/docs/concepts/meta.json | Flattened pages array from nested structure to flat list of protocol page references |
| content/docs/concepts/meta.cn.json | Flattened pages array from nested structure to flat list (Chinese version) |
This was referenced Sep 21, 2026
This was referenced Sep 22, 2026
This was referenced Sep 23, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
… the parse (objectstack-ai#19690) Fixes objectstack-ai#19273 Clause-②: yes Ruling batch objectstack-ai#210 item 4 · letter A · maintainer 「210 同意」 (`5770455384`, 2026-09-22T02:41Z). The direction was ruled, not chosen here. ## The defect `packages/runtime/src/domains/packages.ts:1095` has honoured 「缺省 = 保持,有旗 = 设置」 since PR objectstack-ai#19291 landed: ``` const requestedEnabled = wrapped ? body?.enableOnInstall : undefined; ``` `true` calls `enablePackage`, `false` calls `disablePackage`, and an **absent** key makes no lifecycle call at all — so a package an operator disabled stays disabled across an upgrade. Verified unchanged on this branch; the runtime is not touched by this PR. The published declarations said something else. `z.boolean().default(true)` resolves absence **at parse time**, so a request that omitted the key came out of the parse byte-identical to one that set `true`. The third state did not exist on the published surface while the door went on acting on it — a declared default the runtime deliberately stops applying, on a contract this repo does not own both ends of. ## What changed All three declarations now spell `z.boolean().optional()`, with the semantics on the field in **both** the `describe` and the docblock — absent = keep the row's current lifecycle state; explicit `true` / `false` unchanged; a fresh install lands enabled: | declaration | file | | :--- | :--- | | `api/PackageInstallRequest` — the authority | `packages/spec/src/api/package-api.zod.ts` | | `kernel/InstallPackageRequest` — the copy | `packages/spec/src/kernel/package-registry.zod.ts` | | `marketplace/MarketplaceInstallRequest` — a different party's key | `packages/spec/src/marketplace/marketplace.zod.ts` | ### The executable criterion, both directions Read off the **built** package (`packages/spec/dist`), not `src/`, at head `f5b094a96`: ``` api/PackageInstallRequest | absent => undefined (key in parse output: false) | true => true | false => false kernel|api/InstallPackageReq | absent => undefined (key in parse output: false) | true => true | false => false marketplace/MarketplaceInst. | absent => undefined (key in parse output: false) | true => true | false => false ``` The `true` and `false` arms are **re-read after the change on all three declarations, never assumed** — the card's control in the other direction: a fix that makes absence visible by making the key mean nothing would be worse than the defect. The two refusal cells are unmoved: a string `'false'` and `null` are still refused by name. ### PR objectstack-ai#19130's consistency pin — flipped with its trigger registered, ⛔ not patched green `packages/spec/src/api/package-install-one-authority.test.ts` asserted `true` on every 缺省 reading. Only the 缺省 cell moves; the `false`, `true`, string and `null` cells are untouched, and the authority/copy agreement is still judged cell by cell. The **flip-trigger phrase registered in the test** is: ``` 缺省 = 保持,有旗 = 设置 ``` It is a named `FLIP_TRIGGER` const with its own docblock explaining that while the declarations spelled `.default(true)` the 缺省 reading was living on borrowed time — the phrase says absence is a state the door ACTS ON, and a `.default()` resolves absence at parse time so that state cannot survive to the published surface. It is quoted into the 缺省 cell's name so a test run prints it, and into the two flipped assertion titles. The file's header docblock carries a section stating that the cell FLIPPED, that this was expected on the day the pin landed, and that reading the red as "the pin needs updating" and writing the new value in silently is the failure the const exists to prevent. ##⚠️ DECLARED file-surface expansion, with the mechanism that forces it Beyond the three declarations, their tests and the changeset, four more paths are in this diff. Each is mechanically forced; none is a discretionary edit. 1. **`packages/spec/scripts/lib/default-changes.ts`** (+101). `check:authorable-surface` **refuses the build** on an undeclared move of an authorable key's default, and prints the copy-pasteable block naming each key and both fingerprints. The build exits 1 until the entries exist. Four entries are required, not three: `InstallPackageRequestSchema` is re-exported through `src/api/protocol.zod.ts`, so one declaration publishes under **two** def keys (`kernel/InstallPackageRequest` and `api/InstallPackageRequest`, byte-identical but for the `$id`) — the `CreateImportJobRequest` / `ImportRequest` shape already in that table. The ratchet names keys, not schemas, so dropping either row leaves that def unauthorised and the gate red. 2. **`packages/spec/authorable-defaults/{api,kernel,marketplace}.json`** (-4 lines total). Generated. `pnpm --filter @objectstack/spec build` writes them; exactly the four `… = true` entries are removed and nothing else moves. 3. **`content/docs/references/{api/package-api,api/protocol,kernel/package-registry,marketplace/marketplace}.mdx`** (+5 / -5). Generated by `gen:docs`, run via `check:generated --fix`, which regenerated **only** the one artefact it proved stale. The four projected rows lose their `(default: true)` cell and gain the three-state prose. No other row moves. `authorable-surface/*.json` and `authorable-surface.base.json` are **not** in this diff: the keys stay authorable, and the base anchor is only ever written by the explicit `gen:authorable-surface-base`, never by a build. ## Verification Reconciliation line, verbatim, derived and run at head `f5b094a96`: ``` Run reconciliation — 108 derived, 108 run, 0 NOT-MEASURED, 0 UNRUN. ``` `✓ dispatch-gates --ran: 108 derived famil(ies) accounted for — 108 run, 0 NOT-MEASURED (a DERIVED zero — all 108 recorded an exit code and none of them is 3).` Every command's exit code was captured **before any pipe**; no command answered `exit 3`, so nothing in the derived set measured nothing. Everything below ran in the foreground; each heavy run went through `scripts/pm/os-verify-lock.sh` with `OS_VERIFY_LOCK_SLOT=issue-19273`, and each verdict is that wrapper's own `VERDICT command-exit` line, never a bare shell status. | run | verdict | | :--- | :--- | | `pnpm --filter @objectstack/spec test` | `VERDICT command-exit 0` — 512 files, 14955 passed, 1 todo | | `pnpm --filter @objectstack/rest test` | `VERDICT command-exit 0` — 194 files, 3265 passed, 1 skipped | | `pnpm --filter @objectstack/runtime test` | `VERDICT command-exit 0` — 272 files, 3799 passed, 1 skipped | | `pnpm exec turbo run typecheck` | `VERDICT command-exit 0` — 143 tasks successful | | `pnpm build` | `VERDICT command-exit 0` — 73 tasks successful | | `pnpm --filter @objectstack/spec check:generated` | `VERDICT command-exit 0` — all 15 generated artifacts up to date | | `pnpm lint` | **exit 0**, run WHOLE (`eslint . --no-inline-config`), not narrowed — so no narrowing evidence is owed | `origin/main` was merged and the build state refreshed before the final push; the generated re-check and the union above were both taken **after** that merge, on the head this PR carries. ##⚠️ The open reading the ruling hands the dev, reported as a zero WITH its radius **Zero consumers found that parse an install request through the published schema.** The instrument's reachable radius, stated because a zero without one is not a reading: - **Reached:** `objectstack-ai/objectstack` at `f5b094a96` — `packages/**`, `apps/**`, `examples/**`, `scripts/**`, `content/**`, `docs/**`, `skills/**`, excluding `node_modules`. And `objectstack-ai/objectui` at `0cf2d66`, the only sibling checkout in this container, excluding `node_modules`. - **objectui reading, with a positive control:** `enableOnInstall` — **0** hits. `PackageInstall` (the schema name) — **0** hits. Control that proves the instrument reads that tree: `packages.install` / `/api/v1/packages` — **52** hits. So objectui calls the install route and never names the key, never parses through the published schema. - **⛔ NOT reached, and so NOT established in either direction:** `objectstack-ai/cloud` (no checkout exists in this container) and any third-party consumer of the published `@objectstack/spec`. The changeset body and all four `DEFAULT_CHANGES_BY_MAJOR` reasons are written for exactly that unreachable consumer — the caller who validates before sending — because they are the only channel that reaches them. ## Changeset grade **`minor`** for `@objectstack/spec`, ⛔ not the `patch` ruling objectstack-ai#157 item 5 wrote. Ruling objectstack-ai#210 item 4 overrode it and the override is measured: `check-changeset-no-major.mjs`'s `judgeLevel` verdict `enforce` refuses a clause-②-carrying diff whose moved packages are graded `patch` with none at `minor` or above. Judged against `packages/spec/package.json`'s `files[]` after a build as usual — `dist/` and `json-schema/` both ship, and both move here — so the floor and the measurement agree. `node scripts/check-changeset-no-major.mjs --base origin/main` and `node scripts/check-adr-0087-registration.mjs --base origin/main` both exit 0 on this head. ## ⛔ Fences honoured - **Not the engine half.** `packages/runtime/src/domains/packages.ts:1095` verified to still read `const requestedEnabled = wrapped ? body?.enableOnInstall : undefined;`. The runtime is not in this diff. - **The door does not sniff the raw body around the schema.** Nothing in this PR adds a parse on the serving path. - **No label writes of any kind**, and **no new issues filed** — findings go back to the dispatching seat. ## Acceptance notes None. Nothing outside this card's scope was surfaced that meets the filing bar. ## 维护者速读(草稿) **改了什么** — 三处 `enableOnInstall` 声明从「默认 true」改成「可缺省」。安装接口的实际行为半年前就被裁决改成了「不写这个键 = 保持这个包当前的启用/停用状态」,但对外发布的协议声明一直还写着「不写 = 启用」。这次让声明跟上已经生效的行为。 **为什么改** — 声明与实际不一致,受伤的是仓库外面的调用方。一个会先按协议校验请求再发送的客户端,会从「默认 true」里自动补出一个 `enableOnInstall: true` 发过来;而这个显式的 true 的含义是「强制启用」。结果就是:同样一个请求体,先校验的那一方会在每次升级时把运维手动停用的包悄悄重新打开,不校验的那一方则正常保持停用。两边行为相反,差别只在于有没有先校验。 **风险与代价(含回滚)** — 本仓内运行时行为零变化:安装接口读的是原始请求体,没有任何服务路径经过这几个 schema 解析,接受集也一个字节没动(缺省、true、false 照收,字符串和 null 照拒)。真正受影响的是仓外那位会校验的调用方,处方已写进 changeset 和四条默认值台账记录里:想要每次都强制启用,就把 `enableOnInstall: true` 显式写出来。回滚代价低——三处声明改回 `.default(true)`、撤掉四条台账记录、重跑生成即可,但回滚会把「声明 ≠ 实际」这个问题原样退回去。 **席位意见** — **你要做的** — 确认一件事就够了:仓外(尤其 cloud 侧和第三方)有没有会先按发布的 schema 校验安装请求、再把校验后的对象发出去的调用方。本次探测半径只到本仓和 objectui 两棵树,读数为零且带正控(objectui 会调安装接口但从不提这个键);cloud 在本容器里没有检出,所以那边是**未测**,不是「没有」。若那边确实有这样的调用方,它就是这次改动唯一会碰到的对象,而 changeset 里的处方正是写给它的。 --- _Generated by [Claude Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…nencrypted, in English (objectstack-ai#19689) Fixes objectstack-ai#19571 Implements ruling batch objectstack-ai#210 item 5 (letter B/B, maintainer 「210 同意」), which governs over the issue body. Upstream ruling: objectstack-ai#19489 item ④. PR objectstack-ai#19534 is landed and is **not** reverted here — this is its follow-up. Clause-②: no ## D1 — eligibility decides WHICH sentence, never WHETHER one is emitted `packages/plugins/plugin-auth/src/auth-plugin.ts#registerOidcDiscoveryRoutes`. The structural defect, verified on `origin/main` before writing code: the three AS discovery routes (`/.well-known/oauth-authorization-server`, `/.well-known/openid-configuration` and the RFC 8414 §3.1 path-insertion variant) are mounted unconditionally, while the only line that mentioned a refused transport — `MCP server is enabled but the OAuth track is NOT live` — sits **inside** `if (readMcpServerEnabledEnv() && ...)`. A public plain-HTTP boot with `OS_MCP_SERVER_ENABLED=false` therefore emitted **no warning at all**, while publishing its authorization-server metadata in the clear. The loudest-needed configuration was the quietest. The plain-HTTP branch now has a sibling `else if`, beside it and **not** inside the MCP block: - accepted origin (loopback / private / link-local) — unchanged line, minus its CJK prefix: `OAuth is served UNENCRYPTED: ...` - refused origin (public host) — new, distinct line: `OAuth discovery is served over PUBLIC plain HTTP: ...`, naming (a) the discovery documents and the issuer, (b) that the MCP OAuth track is DISABLED, (c) that TLS is the remedy. Both fire once at mount, neither under TLS, and no configuration key or environment variable gates either. **⛔ No admission decision changes.** `isOAuthEligibleBaseUrl` and every transport-rule predicate are byte-unchanged; the discovery routes are mounted exactly where and when they were. ⛔ No new exported symbol (`servedOverPlainHttp` is a local const) — the `Clause-②: no` declaration stands, and the mechanical floor at `references/contract-review.md:12` is not tripped. ## D2 — the emitted string is English; the ruled sentence moves to the comment The `'OAuth 未加密:仅限可信内网 — '` prefix is out of the executable string. The maintainer's sentence is kept verbatim in the code comment beside the call, cited to the objectstack-ai#19489 ruling and to batch objectstack-ai#210 item 5, and read as the line's meaning rather than its literal encoding. ⛔ No CJK executable string remains in either file's `src` tree; ⛔ no bilingual line. `AGENTS.md` is untouched. The comment block above the branch previously argued the **opposite** — that the Chinese belongs in the emitted string "not only in this comment". That paragraph is falsified by this ruling and has been rewritten to state the current rule with its citation. ## Carriers moved with it 1. `packages/plugins/plugin-auth/src/auth-plugin.ts` — the two branches and the comment. 2. `packages/plugins/plugin-auth/src/mcp-oauth-plaintext-notice.test.ts` — `NOTICE_MARKER` re-anchored to the English line; `PUBLIC_NOTICE_MARKER` added; the "does NOT fire on a PUBLIC plain-HTTP deployment" leg rewritten to assert the sentence **swaps** rather than vanishes; new coverage for the MCP-surface-OFF boot, MCP-independence, once-per-mount, warn level, mutual exclusivity and the no-env-var floor. 15 tests, all green. 3. `docs/qa/platform-checklist/areas/ai.json` — `ai.mcp-oauth-private-host-transport` revision 1 → 2 with its history entry. The grep anchors move to the two English lines; case (b) moves from asserting **0** occurrences to asserting **exactly 1** of the new line; step text, `clause`, `verify`, the `negative` conflation entry, the `source` line and the title moved together. 4. `.changeset/19489-oauth-private-host-transport-rule.md` — still unreleased on `origin/main` (the file is present in `.changeset/`, so `changeset version` has not consumed it). Its last bullet asserted the startup line carries the Chinese and that a public plain-HTTP boot gets no such line; both become false when this lands, so the sentence is corrected. The Chinese is kept only as a quoted ruling citation. Plus a new changeset, `.changeset/19571-plaintext-oauth-public-host-notice.md` (`@objectstack/plugin-auth: patch`). ##⚠️ One gate is red BY DESIGN and needs a human word `node scripts/check-empty-changeset.mjs --base origin/main` — **exit 1**, on carrier 4: > `.changeset/19489-oauth-private-host-transport-rule.md` present on the merge base and CHANGED by this PR This is the gate's **DELIBERATE CORRECTION** class, not the COLLISION class. Its own text says the remedy is *not* to restore the file — restoring it from the base republishes a sentence this PR makes false — and that correcting a pending release note "is a decision about a release rather than a refactor — say so on the PR, naming the note and what changed under it, and get it confirmed". Naming it here: the note is `19489-oauth-private-host-transport-rule.md`, and what changed under it is the D1 branch above. The gate stays red until a person confirms. ## Verification | check | result | |:---|:---| | `pnpm --filter @objectstack/plugin-auth test` | 114 files / **2439 passed** | | `pnpm --filter @objectstack/plugin-auth run typecheck` | exit 0 (after building the package; `tsconfig.examples.json` resolves through `dist`) | | `pnpm lint` (repo-wide `eslint . --no-inline-config`) | exit 0 | | `pnpm --filter '@objectstack/plugin-auth^...' build` | exit 0 | | derived gate families (`scripts/pm/dispatch-gates.mjs --commands`), 66 commands | 63 green · 1 red by design (above) · 2 `PREREQUISITE NOT MET` | `pnpm check:platform-checklist`, `check:nul-bytes`, `check:doc-authoring`, `check:test-source-alias`, `check:cross-package-test-inputs`, `check:published-files`, `check:type-check-coverage`, `check:adr-0087-registration`, `check:changeset-no-major` all exit 0. NOT MEASURED (each prints `PREREQUISITE NOT MET` — explicitly "not a pass and not a finding" — and needs a full workspace build, which is CI's): `pnpm check:dual-build-cjs-loads` (exit 3), `pnpm check:type-check-debt` (exit 3). This diff changes no `exports`, no `package.json` and no build shape. ### Reverse verification (both legs from the committed state; restored byte-identically) Run through `scripts/ablation-replace.mjs`, which proves the mutation landed on disk by blob hash and proves the restore by `git diff HEAD` being empty. The subject resolves from `src` through a relative import, so no `dist` leg applies. 1. **The new branch can fail.** `} else if (servedOverPlainHttp) {` → `} else if (servedOverPlainHttp && false) {`. Blob `17bd405104b2` → `eb7d11ae9d01`. Result: **9 failed | 6 passed (15)** — every public-face assertion red. Restored: blob back to `17bd405104b2`, `git diff HEAD` 0 bytes. 2. **The D2 pin can fail.** The CJK prefix re-added to the accepted line. Blob `17bd405104b2` → `c7aba397fff2`. Result: **1 failed | 14 passed (15)** — `fires on a private-address deployment` red on the no-CJK assertion. Restored: `ok restored: blob == HEAD (17bd405) and git diff HEAD is empty`. Predicted direction was RED for both, and RED is what was observed. ## Acceptance notes - The comment now states explicitly that whether the `.well-known` discovery routes should be mounted at all on a refused origin is pre-existing `main` behaviour neither ruling touched, and is not decided at that call site — per the ruling, that would be a card on its own evidence, ⛔ not folded here. --- _Generated by [Claude Code](https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…upgrade / resolve-dependencies / upload) (objectstack-ai#19937) Fixes objectstack-ai#19116 Clause-②: no Executes ruling 5793374037 on objectstack-ai#19116 (batch objectstack-ai#217 item 4, letter A, maintainer 「217 同意」): the three `PackageApiContracts` entries that named paths nothing mounts leave the contract map. ## What changed - **`packages/spec/src/api/package-api.zod.ts`** - `PackageApiContracts` loses `upgradePackage` (`POST /api/v1/packages/upgrade`), `resolveDependencies` (`POST /api/v1/packages/resolve-dependencies`) and `uploadArtifact` (`POST /api/v1/packages/upload`). A comment at the removal site records why, and carries ruling item 3: a package upgrade / dependency-resolution / upload entry is declared only in the same change that mounts its route. - The file-header `Endpoints` list loses the same three lines (this is the text the generated reference page prints). - Docblock-only corrections under the dispatch's Zone 2 exception, no shape change, no `.describe()` touched: 1. section header `// 5. Upgrade Package (POST /api/v1/packages/upgrade)` now reads `(request/response shapes — bound to no route, see §11)`; 2. `PackageUpgradeRequestSchema` JSDoc: the `@example POST /api/v1/packages/upgrade` request line is dropped (the example body stays) and one sentence says no route accepts it; 3. section header 6 (`resolve-dependencies`), same edit as 1; 4. `ResolveDependenciesRequestSchema` JSDoc, same edit as 2; 5. section header 7 (`upload`), same edit as 1; 6. `UploadArtifactRequestSchema` JSDoc, same edit as 2, and the `Content-Type: multipart/form-data` line goes with the request line, because it described the HTTP request of the unmounted route. - **`packages/spec/src/api/package-api.test.ts`**: the three `toBeDefined` / method / path pins on the removed keys are flipped into a new block that asserts the new fact: none of the three keys is present; no entry under any key is bound to any of the three paths (the whole map is swept, so a phantom cannot return under another key); the map holds exactly the four serving entries; and the six per-route schemas still resolve from the `api` namespace (scope of the ruling, see Acceptance notes). - **ADR-0087**: D3 semantic entry `package-api-contracts-unmounted-entries-retired` (`packages/spec/src/migrations/entries/semantic/18.package-api-contracts-unmounted-entries-retired.ts`), concatenated into `registry.ts` by `gen:migration-registry`. A contract-map entry is not metadata, so there is no source a D2 conversion could rewrite. - **Generated**: `content/docs/references/api/package-api.mdx` regenerated by `check:generated --fix` (only `check:docs` was proven stale): the three lines are gone. ⛔ Not hand-edited. `spec-changes.json` / `docs/protocol-upgrade-guide.md` were NOT stale (step-18 semantic entries are not projected yet), measured by `check:spec-changes` / `check:upgrade-guide` green. - **Changeset** `.changeset/19116-package-api-unmounted-entries-retired.md`: `@objectstack/spec` **minor** with a **BREAKING** banner, a FROM → TO table and the one-line fix, and the ADR-0087 disposition `registered package-api-contracts-unmounted-entries-retired`. Level chosen by the launch-window convention: `scripts/check-changeset-no-major.mjs` refuses `major` outside pre-mode (`.changeset/pre.json` absent), and ruling 5770445652 on objectstack-ai#19611 (batch objectstack-ai#210 item 3) ratified `minor` + BREAKING banner + ADR-0087 marker as the carrier for a breaking spec retirement. Rule text read: AGENTS.md Post-Task Checklist step 3 (breaking changesets carry FROM → TO + one-line fix; exactly one ADR-0087 marker; the changeset carries the PR's `Clause-②` line). Runtime behaviour is unchanged: nothing mounted the three paths or built a route from the entries. ## Premise re-taken at `fdeeea0cc9` (this branch's base) - The three path strings occur in exactly 3 files (`package-api.zod.ts`, `package-api.test.ts`, `content/docs/references/api/package-api.mdx`), `.map` and `CHANGELOG.md` excluded; control `/api/v1/packages/publish`: 27 files. - `upgradePackage` occurs only in the two spec files. `resolveDependencies` / `uploadArtifact` also occur in `packages/core` kernels and `packages/spec/src/contracts/package-service.ts`: those are the kernel's plugin sort and the `IPackageService` methods, a different surface, not readers of the map. - No reader of `PackageApiContracts` outside `package-api.test.ts` (non-generated, non-changeset); no route table, client method, SDK/codegen, MCP tool or doc generator iterates it. `handlePackagesRequest` (`packages/runtime/src/domains/packages.ts`) has no single-segment `POST` branch, so the three paths fall through (static read; the dynamic `handled=false` reading is the one recorded on objectstack-ai#18604). - objectui at the pin `62597c58` (the sibling checkout's HEAD equals `.objectui-sha`): 0 files for `PackageApiContracts`, each of the three keys, each of the three paths; controls `GetMetaItemLayeredResponseSchema` 14 files, `/api/v1/packages` 33 files. ## Verification (all at head `3c4da3412`) - `pnpm --filter @objectstack/spec build` (under the verify lock) → exit 0. - `pnpm --filter @objectstack/spec test` → 527 files, 15507 passed / 1 todo, exit 0. `test:repo` → 35 files, 602 passed, exit 0. `typecheck` (tsc + scripts + test-typecheck) → exit 0. - Cross-package suites whose turbo inputs cover the diff: `@objectstack/core` / `types` / `runtime` / `objectql` / `rest` `test:repo` → 3/1/2/1/1 files, all passed (runtime and objectql after building their dependency closure; the first runtime attempt refused on unbuilt dependencies and is not counted). - `@objectstack/downstream-contract` `test` → 2 files passed, 1 file NOT MEASURED: `consumer-specifier-ledger.test.ts` refuses because `@objectstack/cli` is not built; it measures published export specifiers, which this diff does not move. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` → 111 commands, each run with its exit code recorded; `--ran` reconciliation: **111 derived, 109 run, 2 NOT MEASURED, 0 UNRUN**. - NOT MEASURED, reason PREREQUISITE NOT MET (exit 3, a whole-tree build): `pnpm check:dual-build-cjs-loads`, `pnpm check:type-check-debt`. Both are in CI's required jobs. - Three families first exited 3 (`@objectstack/lint` `check:doc-formula-expressions` / `check:doc-security-posture`, `@objectstack/spec` `check:skill-examples`); after building their named prerequisites (`turbo run build` for formula, lint, client-react, 34 tasks) all three exited 0. - `check:adr-0087-registration`: `1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … registered package-api-contracts-unmounted-entries-retired (new here …)`. - The 11 declared wide-population families (CI-owned) were also run, because they walk `packages/**/*.ts` and this diff adds prose to `registry.ts`: all exit 0. - Lint, narrowed and proven: `eslint --no-inline-config --format json` over the 4 changed `.ts` files → 4 files, 0 errors, 0 warnings. Population read from eslint's own config: `--print-config` resolves for all four; the changeset `.md` and the `.mdx` are outside it (`File ignored because no matching configuration was supplied`). Invariance: `eslint.config.mjs` enables no type-aware linting (the resolved `parserOptions` carry only `ecmaVersion` / `sourceType`, no `project` / `projectService`), so this diff cannot move a verdict on an untouched file. - Reverse verification against the rebuilt `dist/api/index.d.mts`: a probe reading `PackageApiContracts.upgradePackage` / `.resolveDependencies` / `.uploadArtifact` → `TS2339` on each (tsc exit 2); the control leg reading `.installPackage.path` alone → exit 0. Direction observed: red, as expected. - Ablation of the new pins (`scripts/ablation-replace.mjs`, wrap mode, under the verify lock): re-planting an `upgradePackage` entry (anchor 1 → 0, blob `d2f65040` → `9173ef90`) turned 3 of the 4 new tests red (`expected true to be false`, `expected [ 'upgradePackage' ] to deeply equal []`, `expected [ Array(5) ] to deeply equal [ Array(4) ]`); the schemas-still-published test stayed green, as it should. Restore proven: blob back to HEAD `d2f65040`, `git diff HEAD` empty. The restored leg: `package-api.test.ts` 78/78 passed.⚠️ The first ablation attempt was a no-op (its replacement contained its anchor, so the tool refused before running anything and restored); it is not counted. ## Acceptance notes - **Per-route schemas (sections 5–7) are NOT deleted** — the ruling names the map entries only. Consumers measured at `fdeeea0cc9`, tests counted separately: | export | non-test consumers (this repo) | tests | objectui pin | | --- | --- | --- | --- | | `PackageUpgradeRequestSchema` / `PackageUpgradeResponseSchema` | 0 | 1 (`package-api.test.ts`) each | 0 | | `ResolveDependenciesRequestSchema` / `ResolveDependenciesResponseSchema` | 0 | 1 each | 0 | | `UploadArtifactRequestSchema` / `UploadArtifactResponseSchema` | 0 | 1 each | 0 | | the 12 matching `…Request` / `…Response` / `…Parsed` types | 0 | 0 | 0 | "Non-test consumers" excludes the generated artefacts that merely record the exports (`api-surface/`, `export-origins/`, `declaration-map/`, `authorable-surface/`, `json-schema.manifest/`, `dropped-refinements.baseline.json`, `content/docs/references/**`) and comment mentions in two retired-`mergeStrategy` migration entries. Imports that only these sections use in this file: `UpgradePlanSchema` (other consumers: its own `kernel/package-upgrade.zod.ts` and test) and `PackageArtifactSchema` (also used by `system/environment-artifact.zod.ts`). A follow-up can decide their fate on these numbers. - The `PackageApiContracts` docblock still says the map is "Used for generating SDKs, documentation, and route registration"; nothing in this repo registers routes or generates an SDK from it (0 readers outside its own tests). Noted, not filed; carrier: whoever takes the sections 5–7 decision. - `PackageApiErrorCode` still lists `upgrade_failed` and `upload_failed`; no emitter outside `package-api.zod.ts` and its test. Noted, not filed; same carrier. - The weight-carrying pin is package-local (the map's keys and paths). The page's absence of the three routes is held by `check:docs`, because the page is a generated projection of the file-header docblock this PR edits. A tree-scoped absence pin would need `scripts/cross-package-test-inputs.mjs` and `turbo.json` edits outside the claimed file surface, so it was not added. - No labels were written: the dispatch's write budget names none, and `skip-changeset` does not apply (a changeset is present). ## Files `.changeset/19116-package-api-unmounted-entries-retired.md` · `content/docs/references/api/package-api.mdx` · `packages/spec/src/api/package-api.test.ts` · `packages/spec/src/api/package-api.zod.ts` · `packages/spec/src/migrations/entries/semantic/18.package-api-contracts-unmounted-entries-retired.ts` · `packages/spec/src/migrations/registry.ts` — +208 / −43. Dispatched by PM seat `domain:spec#4` (session `session_019c3Hi6ZMU1p6m6aA6Bz45d`), claim comment 5804927566. --- _Generated by [Claude Code](https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…only at both application doors (objectstack-ai#19620) (objectstack-ai#19945) Fixes objectstack-ai#19620 Clause-②: no ## What is ruled, and what landed Ruling-ref `5770445203` — batch objectstack-ai#210 item 2, letter **B**, maintainer 「210 同意」: `settings` leaves `TranslationItemSchema` together with the singular alias `setting: 'settings'`; the item door refuses it at parse with the platform-only prescription; the `settings` liveness row retires; the D2 conversion `translation-per-app-settings-removed` learns the item shape; the ADR-0087 semantic entry is extended. Step ① (the production reading) was waived by the maintainer (records `5796717943`, `5797554787`), so this PR runs step ② and then step ③, and the D2 item-shape conversion takes the **migrate** branch with a loud notice. Nothing is folded into PR objectstack-ai#19600. ## Measured first: stored rows (dispatch assumption 3) The question was whether teaching the conversion the item shape removes a stored item's `settings` before the runtime reader merges it. **It does not, and not for one reason but two.** Probes (scratch scripts, not committed), read against this worktree: ```text # at origin/main fdeeea0, spec dist built from it [A] applyConversionsToStoredItem(translation, item-with-settings): settings survives = true ; notices = [] [B] readAuthoredTranslationLayer(raw row) -> layer[zh-CN].settings = {"mail":{"title":"我的邮件",...}} # after the conversion learned the item shape (411513f), spec dist rebuilt SINGULAR_TO_PLURAL.translation = undefined ; PLURAL_TO_SINGULAR.translations = undefined [stored seam] settings survives = true notices = [] [chain over translations collection] settings survives = false notices = ["translation-per-app-settings-removed"] ``` 1. `authored-translation-sync` reads `sys_metadata` itself and deep-merged the RAW stored payload; it never called the conversion chain. 2. Even the seams that DO call `applyConversionsToStoredItem` (the metadata protocol's stored reads, `DatabaseLoader.rowToData`, `os migrate meta --stored`) returned every `translation` row untouched: the stored pass wraps a row in its stack collection, and the manifest-collection maps carry no `translations` spelling. So no seam had ever replayed ANY translation conversion over a stored row. And the override is real: both i18n adapters read the runtime-authored layer OVER the static bundles (`deepMerge(static, authored)` in `packages/core/src/fallbacks/memory-i18n.ts` and `packages/services/service-i18n/src/file-i18n-adapter.ts`), so a stored item's `settings` beat the platform's own copy — the card's confidence gap 2 is closed, and the core test below pins it end to end. So the stored-row half lands in two places: the stored pass reaches `translation` rows (spec, `conversions/stored.ts`), and the runtime sync becomes a rehydration seam that calls it before merging (core, `authored-translation-sync.ts` — the claim's conditional surface). ## Step ② — conversion, semantic entry, stored rows - `packages/spec/src/conversions/registry.ts` — `translation-per-app-settings-removed` walks the bare item shape too: an entry carrying `locale`, or one with a declared translation group at its top level (a row written before `locale` was required, which the sync still reads by its name). Only the item's own top-level `settings` is stripped; an object literally named `settings` under `objects` stays. Surface, summary and docblock say both doors; the fixture gains the item and a locale-less control. - `packages/spec/src/migrations/entries/semantic/18.translation-per-app-settings-platform-only.ts` — extended to both doors: the item OVERRODE the platform copy (a bundle entry only filled gaps), so overridden keys go back to the platform string and filled gaps to the manifest literal; `acceptanceCriteria` no longer says the item is unchanged. `migrations/registry.ts` regenerated with `gen:migration-registry`; the hand-written step-18 rationale sentence that said the conversion never touches a `translation` item is rewritten. - `packages/spec/src/conversions/stored.ts` — `STORED_ONLY_COLLECTIONS` maps a stored `translation` (and the legacy plural `translations`) row to the `translations` collection. Kept out of the shared maps, which `check:stack-collection-maps` holds to the stack schema. - `packages/core/src/fallbacks/authored-translation-sync.ts` — each row replays the full chain through `applyConversionsToStoredItem` before the merge (the same policy as every other stored-read seam, PD objectstack-ai#12), and each conversion is logged at `warn` once per row per wiring, naming the row, the group (`'settings' → '(removed)'`), the conversion id, and `os migrate meta --stored --apply`. - Liveness: the `settings` row of `packages/spec/liveness/translation.json` is deleted (strict-delete route: the key left the walked shape, a surviving row would be an ORPHAN). Its `_note` and the README's `translation` row say what the deletion does NOT mean: the row's `live` evidence read the SERVED tree, which the platform bundle feeds, so the platform capability is untouched. `check:liveness` then named `translation/settings` a stale row of the shrink-only `undrilled-containers.baseline.json`; it is deleted. `state-counts.md` regenerated. ## Step ③ — the schema, the alias, the pins - `packages/spec/src/system/translation.zod.ts` — `TranslationItemSchema` spreads `appTranslationDataShape()` only (the per-app face, ten groups); `setting: 'settings'` leaves its alias table; `settings` and `setting` are answered by `ITEM_TRANSLATION_KEY_GUIDANCE` with the item's own `ITEM_SETTINGS_PLATFORM_ONLY`, because the bundle door's sentence ("the platform overwrote it anyway") is false for an item. `settingsCommon` stays on both faces. - `packages/spec/authorable-surface/system.json` — `system/TranslationItem:settings` deleted deliberately (the check (a) tripwire the strict-delete route owes). The build's check (c) adjudicated it by proof 4: ```text 1 baseline deletion(s) since fdeeea0 carry their own proof (objectstack-ai#4650): - system/TranslationItem:settings — def reachable from the metadata-type roots; writing 'settings' on it is REFUSED as an unrecognized key and the refusal carries the prescription its `strictObject` declaration owes it ... ``` - Pins flipped (`packages/spec/src/system/translation.test.ts`): "still accepts every declared group together" now asserts the item refuses exactly one key, `[['unrecognized_keys', ['settings']]]`; "still accepts it on the platform face" keeps the platform assertions and drops the item one; a new block refuses `settings` and `setting` on the item (issue path, keys, `PLATFORM group`, `PlatformTranslationData`, no rename suggestion), refuses through `defineTranslation`, with a control. - Door-level pin (`packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts`, section 4): saving a `translation` item carrying `settings` / `setting` answers `code: 'INVALID_METADATA'`, `status: 422`, the issue names the key and says `PLATFORM group`, and nothing is stored; a control saves the same item without it. It rides that file's already-pinned engine double, so the engine-double ledger does not move. - Stored-row pins: `packages/spec/src/conversions/stored.test.ts` (both row spellings drop `settings` with exactly one notice; a canonical row passes through by reference) and the new `packages/core/src/fallbacks/authored-translation-sync.test.ts` (dropped before the merge, rest of the item kept, one warning naming row/group/conversion, once per wiring, canonical-row control, and end to end over `createMemoryI18n`: the platform's `邮件投递` renders, not the stored override). - Published prose made false by this change: `content/docs/ui/translations.mdx` (said the item still declares it), `content/docs/protocol/kernel/i18n-standard.mdx` (adds the item door), `docs/qa/platform-checklist/areas/i18n.json` (anchor prose); `content/docs/references/system/translation.mdx` regenerated with `gen:docs`. - `.changeset/19620-translation-item-settings-platform-only.md` — `@objectstack/spec` and `@objectstack/core` `minor` (launch-window convention), BREAKING banner, FROM → TO table, one-line fix, the stored-row behaviour, ADR-0087 disposition `not-required (already-registered …)` because both entries existed and are extended here. ## PR objectstack-ai#19600's acceptance note is superseded PR objectstack-ai#19600 (merged) records "`TranslationItemSchema` is UNCHANGED and still declares `settings`" and, as its first acceptance note, "The `translation` metadata-type door is untouched and still accepts `settings`." **Both are superseded by this PR**: the item door refuses `settings` with the platform-only prescription, and rows stored before are converted at every stored seam. The seat carries this sentence to objectstack-ai#19600 as a comment. ##⚠️ One red gate by design — a pending release note corrected, confirmation requested `.changeset/15178-translation-bundle-split-settings-platform-only.md` is unreleased and its "Unchanged" section said the registered `translation` item still declares `settings` — false once this PR lands in the same release. It is **corrected, not restored** (one sentence: not changed by THAT entry, superseded in the same release by this PR's changeset). `node scripts/check-empty-changeset.mjs --base origin/main` therefore exits 1 in its DELIBERATE CORRECTION class, whose own text says the remedy is to say so on the PR and get it confirmed. **Please confirm this correction**; the alternative, restoring the file from base, republishes the false sentence. No `skip-changeset` is involved. ## Declared file-surface deviations The claim declared `translation.zod.ts` + tests, `liveness/translation.json`, the conversion + semantic entry + registries, regenerated artefacts, `.changeset/`, and conditionally `authored-translation-sync.ts` (taken: measured necessary above). Outside it, each forced rather than chosen: 1. `packages/spec/src/conversions/stored.ts` + `stored.test.ts` — the stored pass returned `translation` rows untouched (measured above); without it the migrate branch the ruling orders reaches no stored row. Same defect class, a one-entry map; no open PR on it was checked (not measured — ordinary concurrency). 2. `packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts` — the ruling's `code` + `status` pin lives at the metadata door, not in the schema package. 3. `packages/spec/scripts/liveness/undrilled-containers.baseline.json` — the shrink-only row `check:liveness` named stale once the key left the shape. 4. `content/docs/ui/translations.mdx`, `content/docs/protocol/kernel/i18n-standard.mdx`, `docs/qa/platform-checklist/areas/i18n.json`, `packages/spec/liveness/README.md` — published claims this change makes false. 5. `.changeset/15178-…` — the correction above. ## Verification Commits `411513f09` (step ②), `7ba3d25d9` (step ③), `d94e300e4` (regenerated artefacts), `889861a04` (stored pass, door pins, changeset), `b0f2b0ef0` (the changeset's ADR-0087 marker line only). **Tests** (targeted, through `scripts/pm/os-verify-lock.sh`; run at `889861a04` — `b0f2b0ef0` changes only the changeset marker, which no test reads; the 422 file re-run at `b0f2b0ef0`): | Package | Scope | Result | | --- | --- | --- | | `@objectstack/spec` | `translation`, `i18n-resolver`, `conversions/*`, `migrations/*`, `retired-key-migrate-sentence`, `alias-integrity`, `type-alias-convention.pin`, `metadata-plugin` | 14 files / 912 tests pass | | `@objectstack/core` | `fallbacks/authored-translation-sync.test.ts` (new), `fallbacks/fallbacks.test.ts` | 2 files / 66 tests pass | | `@objectstack/metadata-protocol` | whole package (dependency closure built first) | 188 files pass, 3 skipped / 2676 tests pass, 19 skipped | | `@objectstack/metadata-protocol` | the 422 file, verbose, at `b0f2b0ef0` | 11 / 11, the three new cases named | | `@objectstack/service-i18n` | whole package, against the rebuilt core `dist` | 5 files / 74 tests pass | **Typecheck:** `@objectstack/spec` (`tsc --noEmit` + scripts + test layer), `@objectstack/core`, `@objectstack/metadata-protocol` — all exit 0. **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 116 commands on the actual diff (21 paths vs merge base `fdeeea0cc`); all 116 run at `b0f2b0ef0` with the exit code recorded, and `--ran` reconciles: **116 derived, 116 run, 0 NOT-MEASURED, 0 UNRUN**. 115 exit 0; the one exit 1 is `check-empty-changeset` (above). The whole workspace was built first (`turbo run build --filter='./packages/*' --filter='./packages/*/*'`, 72 tasks) so the five built-output gates (`check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n-walk-parity`, `check:lean-entry-closure`, `check:type-check-debt`) measured instead of exiting 3. `check:type-check-debt`: "4 ledger entr(ies) re-measured, 53 raw tsc error(s) total, none above its recorded number." **Lint, narrowed and proven:** ESLint over the 10 changed `.ts` files, `--no-inline-config --format json`: 10 files linted, 0 errors, 0 warnings. Population: the config's own globs (`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`) take all 10 of the diff's script files. Invariance: `eslint.config.mjs` never enables type-aware linting (its own comment: "no `parserOptions.project`, no typed `@typescript-eslint` rules"), so this diff cannot move a verdict on an untouched file. The repo-wide `pnpm lint` is CI's. **Ablations** (each through `scripts/ablation-replace.mjs`, after the fix was committed; anchor hit 1 → 0 and a changed blob proved the mutation on disk; each restore proved blob == HEAD and `git diff HEAD` empty; the tests read `src/` by relative import, so no `dist/` was involved). Direction predicted before running: | Mutation | Predicted | Observed | | --- | --- | --- | | A — `...platformSettingsShape()` back on the item shape | the `settings` pins red; the singular `setting` pin stays green (its guidance still refuses it) | 3 red (declared-groups, `settings` refusal, `defineTranslation`); `setting` green | | B — stored pass without `STORED_ONLY_COLLECTIONS` | both stored-row cases red, control green | 2 red, control green | | C — sync merges without the chain replay | 4 core cases red, canonical control green | 4 red, 1 green | | D — conversion's item branch returns the entry | fixture replay + stored cases red | 4 red across `conversions`, `stored`, `migrations` | **Reverse verification of the rebuilt `.d.ts`:** a probe file in `packages/core/src` typed `const rejected: TranslationItem = { locale: 'en', settings: … }` beside an `apps` control; `tsc --noEmit -p packages/core` answered `TS2353 … 'settings' does not exist in type …` on the `settings` line only; the probe was removed by a trap and the tree read clean. ## Acceptance notes - **Same-class correction riding the stored-pass change.** The docblocks of `translation-validation-messages-removed` and `translation-component-submit-label-removed` already claimed stored `translation` rows replay through them; until this PR none did. Both strip keys no resolver reads, so the only observable change for them is a one-time stored-row warning when an old row is read. - **What an operator sees.** Reading an old row that still carries `settings` through the metadata API now serves it without the group and logs the protocol's stored-row warning; the runtime sync logs its own warning once. A Studio re-save or `os migrate meta --stored --apply` persists the canonical row. - **`setting` (singular) is not converted.** An alias only ever suggested a rename in the rejection; the item door never accepted `setting`, so no stored row can carry it. - **The sync's warning is conversion-agnostic.** It names the row, the dropped group and the conversion; the platform-only reason is carried by the item-door refusal and by the D3 semantic entry (which `os migrate meta --from 17` reports as a semantic TODO, per that command's own docblock — not run here), not restated per conversion in the consumer. - **Pinned sibling (objectui `62597c588`)** — `TranslationPreview.tsx` still lists a `settings` group and `clientValidation.ts` prose counts "19 keys". Neither breaks: the binding imports `TranslationItemSchema` itself and its parity test compares the schema with itself; the preview group simply never renders now. Stale prose / dead UI in the sibling; carrier: none; not filed. - Size: 21 files, +714 / −173 (887 changed lines), under the 5,000-line human-merge threshold. No governed surface is touched. --- _Generated by [Claude Code](https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 9, 2026
…nce, and the loopback OAuth notice at info (objectstack-ai#22097) Fixes objectstack-ai#22073 Clause-②: no ## What changes Three boot-output changes, as the triage ruling on the card (comment `6038562452`) directs. Log text and log levels only: no accept set, answer or public signature moves. 1. **One line per warning class.** `printAutomationSummary` (`packages/cli/src/utils/format.ts`) groups the engine's binding audit by (trigger type, reason) and prints one line per class with its flows listed. The class's short text is the reason's **first sentence**, derived by `leadSentence()`. It is never a second, hand-written wording: the engine owns every reason sentence (`describeUnboundReason`, and `scheduledWorkDisabledReason` for the deployment policy). A reason that is already one sentence (a missing trigger, a binding failure, a declined subflow) prints whole, remedy included. Distinct reasons stay distinct lines. 2. **Each warning prints once.** `printAutomationSummary` now returns the identifying text of every logger record it restated, at the moment it prints its own line. `printBootDiagnostics` withholds exactly those and counts them in its header: `⚠ Boot diagnostics — 3 warnings logged during startup (2 more already listed above):`. The shadowed-flow bootstrap restatement is covered by the same rule. The pull-time `Flow name collision` record still replays, because it says which rule armed the body. 3. **Loopback OAuth notice at `info`.** In `packages/plugins/plugin-auth/src/auth-plugin.ts`, `OAuth is served UNENCRYPTED: …` logs at `info` when the issuer's host is loopback and stays `warn` for private and link-local hosts. The sentence is the same and is still emitted on every accepted plain-HTTP boot, so D1 of ruling batch objectstack-ai#210 item 5 ("eligibility decides WHICH sentence, never WHETHER") holds across both levels. Where the long text now prints: at `--log-level debug` (or `info`) the boot-quiet window never opens. The live stream then carries `@objectstack/service-automation`'s per-flow `[Automation] flow 'NAME' declares a 'TYPE' trigger but is NOT bound — it will never auto-launch. REASON` warning, with the whole reason. When any class line was shortened, one dim line under the classes says so. ### Measured, before → after (H1) `examples/app-todo` has 2 package-authored `schedule` flows. Booted with `os dev --fresh --no-watch`, scheduled work off (the default), default log level: | | before (`3d918850`) | after (this branch) | | --- | --- | --- | | lines carrying the schedule `NOT bound` sentence | **4** (2 banner + 2 *Boot diagnostics*) | **1** | | `OAuth is served UNENCRYPTED` lines (localhost) | 1 (`WARN`, in *Boot diagnostics*) | 0 | | *Boot diagnostics* records shown | 6 | 3 (+2 counted as listed above) | The after banner line: ```text ⚠ 2 flows declare a 'schedule' trigger but are NOT bound — disabled by deployment policy — package-authored scheduled work is off on this deployment (OS_AUTOMATION_SCHEDULED_WORK_ENABLED is unset or not truthy), so no time trigger arms and no packaged `defineJob` is scheduled: task_reminder, overdue_escalation reasons cut to their first sentence — --log-level debug prints each flow's full reason ``` The second producer was the one the PM named: the banner's `a.unbound` loop, plus *Boot diagnostics* replaying `service-automation`'s `kernel:bootstrapped` audit warning (`plugin.ts:1533`). There was no third copy. By construction, the hotcrm shape (8 flows) goes from 16 lines to 1 class line plus 1 hint line. hotcrm itself was not booted here. ### How a new boot warning stays single (H7) *Boot diagnostics* withholds only a record that a banner section **handed back as restated when it printed its own line**. Today the only such section is the automation summary, and only for the service-automation audit records it summarises. - A new `logger.warn`, such as the branding-asset warning objectstack-ai#22071 adds, is restated by no banner section. It is never matched, so it replays once. - A banner line that is not also a logger record has nothing to withhold, so it prints once. - The match can fail only toward printing. A reworded producer or an escaped character in a JSON-format record is printed, never dropped, so the failure mode is a doubled line, never a lost diagnostic. - A banner section that starts restating a logger record must hand that record back, or it doubles. That contract is documented on `BootDiagnosticsReplayOptions.restatedAbove`. - The failed-boot and migrate-and-exit paths print no banner and pass nothing, so they replay every record. ## PM readings, measured - **H1**: reproduced with the counts above. The two producers are the ones named. - **H2**: the CLI does both jobs, and `service-automation` is untouched. Hosts that boot the kernel without the CLI banner read the service's warning as their only notice: `packages/runtime/src/domains/automation.ts` (the runtime composition every adapter host uses), `packages/verify/src/harness.ts`, and the plugin's own comment at `plugin.ts:1490`–`:1492` ("The warn matters for embedded hosts and tests"). Lowering that warning, or grouping it, would change what those hosts see. No `@objectstack/service-automation` changeset. - **H3**: classes are keyed on the whole reason, not its short form. The deployment-policy reason is one class per trigger type. A real binding failure, a missing trigger and a declined subflow each keep their own line. The short-text derivation and the long-text location are described above. - **H4**: no other published origin feeds the decision. `getAuthIssuer()`, `getMcpResourceUrl()` and `isMcpOAuthEnabled()` all derive from the one `getCanonicalOrigin()`, so "every published origin is loopback" reads as "the issuer's host is loopback". **Partly falsified:** the transport rule has no separable loopback predicate to reuse. `isOAuthEligibleBaseUrl` checks `localhost` / `*.localhost` inline (`auth-manager.ts:558`), and `isPrivateOrLoopbackHostLiteral` checks `127.0.0.0/8` and `::1` in the same function as the private ranges. Extracting one means editing `auth-manager.ts`, which is outside this card's declared file surface. So the notice's module-local `isLoopbackIssuer()` composes the rule's own pieces: the same `localhost` / `*.localhost` names, the `127.0.0.0/8` block judged by the same ADR-0069 D5 matcher (`ipMatchesRange`, already exported), and `[::1]`. It adds no new host regex, and it reads the WHATWG-canonical hostname as the rule does. It is asked only of an issuer the rule already accepted. Agreement is pinned by the loopback / private table below. Extracting one shared predicate is left as an open question in the report. - **H5**: two assertions in `mcp-oauth-plaintext-notice.test.ts` flip, both for the ruling's reason. `fires on a loopback deployment too — plain HTTP is plain HTTP` (localhost: 1 `warn`) is replaced by `logs the sentence at info, never warn, on a LOOPBACK deployment`, over `localhost`, `app.localhost`, `127.0.0.1`, `127.0.0.2`, `127.1` and `[::1]`, each 0 `warn` and 1 `info` naming the issuer. `every plain-HTTP boot gets exactly one of the two sentences` now counts across both levels; its localhost row would otherwise read 0. The control stays `warn`: `is emitted at warn on a private address` (`172.16.0.1`), plus a new `keeps the warning on a PRIVATE or LINK-LOCAL deployment` over `10.0.0.5`, `172.16.0.1`, `192.168.1.10`, `169.254.10.20`, `[fd00::1]` and `[fe80::1]`. - **H6**: `packages/cli/src/commands/doctor.ts:289` is unchanged. Text that quoted the old lines moved with them, outside `content/docs/**`: - `docs/qa/platform-checklist/areas/platform-core.json`: the `platform-core.boot-health` acceptance `verify` quoted the per-flow banner line. Revision 6 → 7, with a history entry. - `docs/qa/platform-checklist/areas/ai.json`: `ai.mcp-oauth-private-host-transport` expected the accepted-transport line "exactly 1 on (c)" (loopback) at the default level. It now names the level per boot, and boot (c) runs at `--log-level info`. Revision 2 → 3, with a history entry. `pnpm check:platform-checklist` is green. - No `content/docs/**` page needed an edit. `automation/flows.mdx:2380` and `deployment/environment-variables.mdx:91` say such flows are "listed by … the CLI startup summary as disabled by deployment policy", which is still true. `releases/v17/17-5.mdx` is release-owned and untouched. `packages/spec/scripts/publish-smoke-boot-failure.test.ts` holds verbatim historical specimens of the old header with no restated records, which are still valid. - **H7**: covered in the section above. ## Tests (at `de6b4a08`) - New `packages/cli/src/utils/format.boot-warning-classes.test.ts`, unit tier, 11 tests. The first block boots the **real** `AutomationServicePlugin` on a `LiteKernel` with 8 `schedule` flows and scheduled work off. It captures stdout through the same `BootLogCapture` `serve` uses, collects through `collectAutomationSummary`, and prints the real banner. Pins: - exactly **one** line carries `a 'schedule' trigger`, and it names all 8 flows; - every flow is named on exactly **one** line, so no warning appears twice; - the long explanation is absent at the default level. The premise is asserted first: the producer warned once per flow into the capture. Formatter legs cover distinct classes, the first-sentence cut, the hint only when shortened, a JSON-format record, a record the banner did not restate (never withheld), the shadowed restatement withheld while the collision record is kept, and the no-banner path replaying everything. - `pnpm --filter @objectstack/cli exec vitest run --project unit`: **260 files, 3797 passed**. `pnpm --filter @objectstack/cli typecheck`: exit 0. The new file is in `tsconfig.json`'s program (`--listFilesOnly`), and `check:test-typecheck` is OK. The `integration` tier is declared to CI: the diff touches no spawn entry, no integration-tier file and no driver. - `pnpm --filter @objectstack/plugin-auth test`: **126 files, 2623 passed, 10 skipped**. `pnpm --filter @objectstack/plugin-auth typecheck`: exit 0. - **Ablations.** Each mutation went through `scripts/ablation-replace.mjs` against the committed fix, with the anchor hit verified on disk and the restore proven as blob == HEAD with an empty `git diff HEAD`. The subjects resolve from source, so no rebuild was needed. - **A**: drop the unbound claim. 5 red: both real-boot pins, plus the three formatter print-once legs that withhold unbound records. - **B**: key classes by flow. 2 red: the one-schedule-line pin and the classes leg. - **C**: never log at `info`. 6 red: every loopback spelling. - **D**: treat every host as loopback. 10 red: every private and link-local control. - **Gates.** `node scripts/pm/dispatch-gates.mjs --ran` reports **72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN** at `de6b4a08`. The derivation added `pnpm --filter @objectstack/lint run check:doc-formula-expressions` and `pnpm check:platform-checklist` to the PM's list. `check:dual-build-cjs-loads` and `check:i18n-coverage` first exited 3 (PREREQUISITE NOT MET, unbuilt workspace packages). After `pnpm turbo run build`, they and the other dist-reading gates were re-run, all exit 0. Full `pnpm lint` exits 0 at `de6b4a08`. Narrowed eslint (`--no-inline-config --format json`) over the 4 changed TS files: 0 errors, 0 warnings. The other 3 changed files are outside eslint's population ("no matching configuration"), and the config enables no type-aware linting (`eslint.config.mjs:327`). ## Acceptance notes (not filed) - *Boot diagnostics* still pairs the ready line `⚠ Server is ready — DEGRADED: missing core services: …` with the kernel's `System started with degraded capabilities …` record. This is deliberately left: it is the ready-line status, not a banner-list entry, and `serve-ready-degraded-boot.e2e.test.ts` asserts the kernel sentence in the same output as its premise. - A flow declined onto a disabled packaged subflow also has a bind-time engine `warn` (`Flow 'NAME' is registered but NOT armed on trigger 'TYPE' — …`). That is a different record from the bootstrap audit, so it still replays beside the banner's class line. The case is rare, and the record carries the subflow detail. - The service's own bootstrap sentence reads "is NOT bound — it will never auto-launch. disabled by deployment policy — … This is not a binding failure" for an embedded host. That is a wording observation, unchanged here (H2). --- _Generated by [Claude Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CI build failing on
meta.jsonvalidation - fumadocs-mdx expects flat string arrays inpages, not nested objects.Changes
pagesarray in/content/docs/concepts/meta.jsonandmeta.cn.jsonpages[3]Before:
{ "pages": [ "manifesto", "architecture", { "title": "Protocol Namespaces", "pages": ["protocol-data", "protocol-driver", ...] }, "terminology" ] }After:
{ "pages": [ "manifesto", "architecture", "protocol-data", "protocol-driver", ..., "terminology" ] }Sidebar navigation will show protocol pages in sequence rather than nested under a group heading.
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.