fix(objectql,runtime,rest): store a serializable manifest projection in the package registry - #14499

Merged
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest
Sep 2, 2026
Merged

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry#14499
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest

Conversation

@hotlong

@hotlonghotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14309

SchemaRegistry.installPackage now stores a serializable projection of the
manifest instead of the caller's live defineStack() object, and the /packages
read doors project a registry entry onto its declared record fields instead of
spreading it whole.

The measured defect

Reproduced on this branch's base, showcase on a random high port
(objectstack dev --seed-admin --fresh -p 39117), signed in as the seeded admin:

routebeforeafter
GET /api/v1/packages500INTERNAL_ERROR200
GET /api/v1/packages/com.example.showcase500INTERNAL_ERROR200
GET /api/v1/meta/package/com.example.showcase500200
GET /api/v1/meta/package/com.objectstack.setup200200

Error body of the first two:

Converting circular structure to JSON
--> starting at object with constructor '_ObjectQL'
| property 'actionActivation' -> object with constructor 'ActionActivationProjection'
| property 'store' -> object with constructor 'ObjectStoreActionActivationStore'
--- property 'engine' closes the circle

Which key actually reached the engine

Rather than reason about it, the running server was instrumented at
SchemaRegistry.getPackage (temporary, env-gated, reverted — the file is
byte-identical to its committed state, git hash-object verified) to classify
every top-level manifest key of every installed package by whether it
survives JSON.stringify. Across the 26 packages a showcase boot installs,
exactly one key on one package failed:

[OS_DEBUG_PKG_SERIALIZE] id=com.example.showcase keys=[…40 keys…]
UNSERIALIZABLE=["plugins :: Array<ConnectorOpenApiPlugin|ConnectorMcpPlugin|ConnectorRestPlugin|
ConnectorSlackPlugin|MarketplaceProxyPlugin|MarketplaceInstallLocalPlugin|CloudConnectionPlugin|
RuntimeConfigPlugin> :: Converting circular structure to JSON"]

And only after plugin init. The same manifest was probed 40 times during
boot with UNSERIALIZABLE=[] and 381 times afterwards with the plugins
failure — the transition is a single clean split in the log. That rules out a
whole family of "fixes": an install-time JSON.stringify probe, or any check
that runs while the record is being written, would have called this record
healthy. The projection therefore drops by shape, not by trial
serialisation, and does not depend on when it is asked.

The repair, at the producer

installPackage stored the argument verbatim as pkg.manifest. The registry
item is a record, not the runtime, so it now holds only the declarative
half. The rule is structural rather than a plugins denylist, because the fault
is not "the key is called plugins" — it is "a live object reached the record":

  • primitives, plain objects, arrays and Date are data and survive;
  • functions / symbols / bigints are dropped (a function was already invisible to
    JSON.stringify; a bigint would have thrown);
  • class instances, Map, Set and other exotic objects are dropped;
  • a reference cycle among plain data is dropped at the back-edge, so a
    self-referencing manifest degrades to a missing field instead of throwing.

Nothing downstream loses a member it was using: the kernel keeps the live object
in ObjectQL.manifests, and the only reader of manifest.plugins[]
registerApp's nested-plugin seam — reads its own parameter, never the
record (grepped, and pinned). The caller's manifest is copied, never stripped in
place, which the pins assert both ways.

⛔ Not a consumer-side tolerance (AGENTS.md Prime Directive #12): no reader is
taught to survive an unserializable record — the record is never unserializable.

Premise correction: which handler answered the 500

The card attributes the list/detail 500 to packages/rest/src/package-routes.ts
{ ...item, source: 'registry' }. Measured, that is not the handler that
answered in the showcase composition — packages/runtime/src/domains/packages.ts
did. Two independent probes agree:

  • the 404 wording. GET /api/v1/packages/no.such.package returned
    Package 'no.such.package' not found (the runtime domain's spelling); the
    REST twin says Package "no.such.package" was not found.
  • the response shape. GET /api/v1/packages/com.objectstack.setup returned the
    installed-package record directly under data (deps.success(pkg)); the
    REST twin wraps it as data.package and stamps source.

So the route-level defence is applied at both twins, and
packages/runtime/src/domains/packages.ts is a declared addition to this card's
file surface (announced on the issue before the work landed). Applying it only
where the card pointed would have shipped a defence with a hole exactly where the
defect was observed.

The doors now project onto the declared InstalledPackageSchema fields, so an
undeclared member appearing on the registry item — a live handle, a
back-reference — degrades to a field the response never mentions, instead of one
bad member on one package failing the whole list for every caller. The REST
merge's database half is deliberately left spreading: its shape belongs to
PackageService, and narrowing it would be a contract decision this card does
not carry. On the REST side _diagnostics is carried explicitly, because
decorateMetadataItem grafts it onto every item leaving getMetaItems and it is
measured to be the only thing that decoration adds for type: 'package'.

No response field is added or renamed; responses that already served fine are
byte-identical.

Verification

git rev-parse --short HEAD at the time of the runs below: 5b8b76e06.

Live routes, same showcase boot, after the fix — the table above; GET /packages
returns all 26 packages, the showcase manifest keeps its 40 declarative keys
(24 objects, 29 flows, 1 app), plugins serves as [] and the function-valued
onEnable is gone. JSON.stringify of the whole listing succeeds.

Package suites (all green, on the commit named above):

@objectstack/objectql Test Files 255 passed (255) Tests 4400 passed (4400)
@objectstack/rest Test Files 168 passed (168) Tests 2808 passed (2808)
@objectstack/runtime Test Files 208 passed (208) Tests 3066 passed (3066)

New pins — 20 cases over three files:
packages/objectql/src/registry-package-manifest-serializable.test.ts (9),
packages/runtime/src/domains/packages-serializable-response.test.ts (5),
packages/rest/src/package-registry-item-projection.test.ts (6).

Typecheck: @objectstack/objectql, @objectstack/runtime, @objectstack/rest
all exit 0. ⚠️ Measured rather than assumed: each package's tsc --noEmit
program excludes **/*.test.ts, so --listFiles reports 0 hits for all three
new test files there. The test layer is covered separately — objectql and rest
run check:test-typecheck over tsconfig.test.json, whose --listFilesdoes
name the new files (1 hit each, 0 errors attributed to them). packages/runtime
has no test-typecheck wiring at all, so its new test file is covered by no gate;
it was type-checked standalone (tsc --noEmit --ignoreConfig --strict, exit 0, 0
errors) and that gap is reported to the PM rather than papered over.

Gate families — 62 derived by node scripts/pm/dispatch-gates.mjs --commands
(no paths passed; it takes its own change set from the merge base). 60 ran green
locally. The remaining two exited 3, which each script's own verdict text
declares is NOT MEASURED and explicitly "not a red": check-test-completeness
needs a saved turbo run test log it was not handed, and
scripts/pm/check-half-states.mjs needs a per-card comment fetch that never
happened.

check:system-context-census was the one family this diff genuinely moved: the
+50-line helper in packages/runtime/src/domains/packages.ts shifted three
elevation-read anchors (:95 -> :145, :128 -> :178, :246 -> :296). Repaired
mechanically with the gate's own --fix, which re-anchored
content/docs/permissions/system-context.mdx; the gate then reports
OK — 109 elevation read sites in 20 packages across 45 files, all anchored.
That docs edit pulled 24 further doc families into the derivation, all of which
were run and are green.

Ablation (the pin fails without the fix). manifest: toRecordManifest(manifest)
was reverted to manifest, in installPackage; the mutation was confirmed on
disk before running (anchor text 1 -> 0, injected text 1, blob hash differing
from HEAD), the restore leg was git checkout HEAD -- <abs path> under an
EXIT/INT/TERM trap, and restoration was proved by an empty git diff HEAD plus
git hash-object equality with the HEAD blob. No rebuild was needed: the pin
imports ./registry from source, not through dist. Result:

Tests 7 failed | 2 passed (9) ← ablated
Tests 9 passed (9) ← restored
AssertionError: expected [Function] to not throw an error but
'TypeError: Converting circular struct…' was thrown

Declared narrowing — verification ran UNLOCKED.scripts/pm/os-verify-lock.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not ship it), so the commands were run directly, without the lock — a declared
narrowing, not a silent one. No serialization guarantee held for these runs, nor
for any sibling agent in this container while they ran.

Declared narrowing — repo-wide scans left to CI.pnpm lint and the rest of
the 62-family farm were not run locally; CI runs the farm exactly once on this
PR. What ran locally is the targeted set and the 60 gate families
named above.

Generated by Claude Code

hotlongand others added 4 commits September 2, 2026 10:39
…e registry
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the door projections
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…packages-domain projection
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
hotlongand others added 2 commits September 3, 2026 00:19
…ckages-serializable-manifest
# Conflicts:
#	packages/runtime/src/domains/packages.ts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/rest, @objectstack/runtime, touching 25 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 37 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 50d9fd8153b16a9dd058730a4e02f89ac304578d — the merge of head 6961d6ec9bc74651928d56022bc909a7f7cc1a81 into base 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 50d9fd8153b16a9dd058730a4e02f89ac304578d && git checkout 50d9fd8153b16a9dd058730a4e02f89ac304578d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 6961d6ec9bc74651928d56022bc909a7f7cc1a81 && git checkout -B drift-repro 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 && git merge --no-ff 6961d6ec9bc74651928d56022bc909a7f7cc1a81
node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@hotlong
hotlong marked this pull request as ready for review September 2, 2026 16:38
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
os-zhuang pushed a commit that referenced this pull request Sep 2, 2026
…d land it on sendError
Re-homed under the domain seat's serial fence: `packages/rest/src/package-routes.ts`
is held by the open PR #14499, so this no longer edits that file. The REST
direct-mount doors are covered from the producer side instead.
`sendError` (`@objectstack/types`) is the single writer for every
nested-envelope error in the repo, and every catch in the package registrar
ends there — so wiring the rule at that one exit covers those doors with no
per-door call, and covers any door added later by construction. That also puts
the helper in the same package as `resolveThrownHttpError`, on the same
argument: a rule two doors must agree on cannot live inside one of them.
`@objectstack/observability` and `packages/rest` are back to origin/main
byte-for-byte, as is the system-context census page (its line rot was caused
by the package-routes import this drops).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33663028874 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@hotlong

Copy link
Copy Markdown
ContributorAuthor

Queue-eject triage (PM session session_62849a16-0144-4728-8942-9f60bb3a73f1), read against the signature ledger rather than judged on the spot:

Per the four-branch rule: known flaky ⇒ re-queue once, as-is, no new commit; if it ejects again on the same signature it stays out until #14679 is resolved (no reflexive re-arm). Re-arming now.

Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8ce628aSep 2, 2026
36 checks passed
@hotlong
hotlong deleted the claude/issue-14309-packages-serializable-manifest branch September 2, 2026 20:21
os-sales pushed a commit that referenced this pull request Sep 2, 2026
…stem-write-sharing-materialization
Resolves the single CONFLICT (content) in
content/docs/permissions/system-context.mdx (the os-regen driver flags
this file as MIXED — generated anchors plus hand-written prose — so it
text-merges instead of taking one side whole).
Row 37 (share-link policy `enabled` check, isSystem) diverged: this
branch's copy already carried the #14033 prose ("resolution is not
bypassed since #14033 ... a link minted this way does not resolve
until the block is enabled"), added when 13bf05d landed on both
branches. main's copy had reverted to the pre-#14033 wording via an
unrelated commit (8ce628a, "store a serializable manifest projection
in the package registry (#14499)") whose own docs-regen step appears
to have run against a stale pre-#14033 checkout of this file.
Kept this branch's row 37 text: packages/plugins/plugin-sharing/src/
share-link-service.ts:652-697 (the `[#14033]` docblock and the
`if (!policy.enabled)` redemption-time check it documents, citing the
maintainer's 2026-09-01 ruling and this exact ledger row by number)
confirms it is what the current source does, not the reverted main
copy. Row 36 differed only in its generated anchor line number
(1088 vs 1077) — anchor numbers are recomputed by
`pnpm gen:system-context-census` below, so either side was equivalent;
kept this branch's.
Anchors re-derived on the merged tree with
`pnpm gen:system-context-census` in a follow-up commit, per the
os-regen driver's documented recovery for this file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry - #14499

Merged
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest
Sep 2, 2026
Merged

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry#14499
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest

Conversation

@hotlong

@hotlonghotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14309

SchemaRegistry.installPackage now stores a serializable projection of the
manifest instead of the caller's live defineStack() object, and the /packages
read doors project a registry entry onto its declared record fields instead of
spreading it whole.

The measured defect

Reproduced on this branch's base, showcase on a random high port
(objectstack dev --seed-admin --fresh -p 39117), signed in as the seeded admin:

routebeforeafter
GET /api/v1/packages500INTERNAL_ERROR200
GET /api/v1/packages/com.example.showcase500INTERNAL_ERROR200
GET /api/v1/meta/package/com.example.showcase500200
GET /api/v1/meta/package/com.objectstack.setup200200

Error body of the first two:

Converting circular structure to JSON
--> starting at object with constructor '_ObjectQL'
| property 'actionActivation' -> object with constructor 'ActionActivationProjection'
| property 'store' -> object with constructor 'ObjectStoreActionActivationStore'
--- property 'engine' closes the circle

Which key actually reached the engine

Rather than reason about it, the running server was instrumented at
SchemaRegistry.getPackage (temporary, env-gated, reverted — the file is
byte-identical to its committed state, git hash-object verified) to classify
every top-level manifest key of every installed package by whether it
survives JSON.stringify. Across the 26 packages a showcase boot installs,
exactly one key on one package failed:

[OS_DEBUG_PKG_SERIALIZE] id=com.example.showcase keys=[…40 keys…]
UNSERIALIZABLE=["plugins :: Array<ConnectorOpenApiPlugin|ConnectorMcpPlugin|ConnectorRestPlugin|
ConnectorSlackPlugin|MarketplaceProxyPlugin|MarketplaceInstallLocalPlugin|CloudConnectionPlugin|
RuntimeConfigPlugin> :: Converting circular structure to JSON"]

And only after plugin init. The same manifest was probed 40 times during
boot with UNSERIALIZABLE=[] and 381 times afterwards with the plugins
failure — the transition is a single clean split in the log. That rules out a
whole family of "fixes": an install-time JSON.stringify probe, or any check
that runs while the record is being written, would have called this record
healthy. The projection therefore drops by shape, not by trial
serialisation, and does not depend on when it is asked.

The repair, at the producer

installPackage stored the argument verbatim as pkg.manifest. The registry
item is a record, not the runtime, so it now holds only the declarative
half. The rule is structural rather than a plugins denylist, because the fault
is not "the key is called plugins" — it is "a live object reached the record":

  • primitives, plain objects, arrays and Date are data and survive;
  • functions / symbols / bigints are dropped (a function was already invisible to
    JSON.stringify; a bigint would have thrown);
  • class instances, Map, Set and other exotic objects are dropped;
  • a reference cycle among plain data is dropped at the back-edge, so a
    self-referencing manifest degrades to a missing field instead of throwing.

Nothing downstream loses a member it was using: the kernel keeps the live object
in ObjectQL.manifests, and the only reader of manifest.plugins[]
registerApp's nested-plugin seam — reads its own parameter, never the
record (grepped, and pinned). The caller's manifest is copied, never stripped in
place, which the pins assert both ways.

⛔ Not a consumer-side tolerance (AGENTS.md Prime Directive #12): no reader is
taught to survive an unserializable record — the record is never unserializable.

Premise correction: which handler answered the 500

The card attributes the list/detail 500 to packages/rest/src/package-routes.ts
{ ...item, source: 'registry' }. Measured, that is not the handler that
answered in the showcase composition — packages/runtime/src/domains/packages.ts
did. Two independent probes agree:

  • the 404 wording. GET /api/v1/packages/no.such.package returned
    Package 'no.such.package' not found (the runtime domain's spelling); the
    REST twin says Package "no.such.package" was not found.
  • the response shape. GET /api/v1/packages/com.objectstack.setup returned the
    installed-package record directly under data (deps.success(pkg)); the
    REST twin wraps it as data.package and stamps source.

So the route-level defence is applied at both twins, and
packages/runtime/src/domains/packages.ts is a declared addition to this card's
file surface (announced on the issue before the work landed). Applying it only
where the card pointed would have shipped a defence with a hole exactly where the
defect was observed.

The doors now project onto the declared InstalledPackageSchema fields, so an
undeclared member appearing on the registry item — a live handle, a
back-reference — degrades to a field the response never mentions, instead of one
bad member on one package failing the whole list for every caller. The REST
merge's database half is deliberately left spreading: its shape belongs to
PackageService, and narrowing it would be a contract decision this card does
not carry. On the REST side _diagnostics is carried explicitly, because
decorateMetadataItem grafts it onto every item leaving getMetaItems and it is
measured to be the only thing that decoration adds for type: 'package'.

No response field is added or renamed; responses that already served fine are
byte-identical.

Verification

git rev-parse --short HEAD at the time of the runs below: 5b8b76e06.

Live routes, same showcase boot, after the fix — the table above; GET /packages
returns all 26 packages, the showcase manifest keeps its 40 declarative keys
(24 objects, 29 flows, 1 app), plugins serves as [] and the function-valued
onEnable is gone. JSON.stringify of the whole listing succeeds.

Package suites (all green, on the commit named above):

@objectstack/objectql Test Files 255 passed (255) Tests 4400 passed (4400)
@objectstack/rest Test Files 168 passed (168) Tests 2808 passed (2808)
@objectstack/runtime Test Files 208 passed (208) Tests 3066 passed (3066)

New pins — 20 cases over three files:
packages/objectql/src/registry-package-manifest-serializable.test.ts (9),
packages/runtime/src/domains/packages-serializable-response.test.ts (5),
packages/rest/src/package-registry-item-projection.test.ts (6).

Typecheck: @objectstack/objectql, @objectstack/runtime, @objectstack/rest
all exit 0. ⚠️ Measured rather than assumed: each package's tsc --noEmit
program excludes **/*.test.ts, so --listFiles reports 0 hits for all three
new test files there. The test layer is covered separately — objectql and rest
run check:test-typecheck over tsconfig.test.json, whose --listFilesdoes
name the new files (1 hit each, 0 errors attributed to them). packages/runtime
has no test-typecheck wiring at all, so its new test file is covered by no gate;
it was type-checked standalone (tsc --noEmit --ignoreConfig --strict, exit 0, 0
errors) and that gap is reported to the PM rather than papered over.

Gate families — 62 derived by node scripts/pm/dispatch-gates.mjs --commands
(no paths passed; it takes its own change set from the merge base). 60 ran green
locally. The remaining two exited 3, which each script's own verdict text
declares is NOT MEASURED and explicitly "not a red": check-test-completeness
needs a saved turbo run test log it was not handed, and
scripts/pm/check-half-states.mjs needs a per-card comment fetch that never
happened.

check:system-context-census was the one family this diff genuinely moved: the
+50-line helper in packages/runtime/src/domains/packages.ts shifted three
elevation-read anchors (:95 -> :145, :128 -> :178, :246 -> :296). Repaired
mechanically with the gate's own --fix, which re-anchored
content/docs/permissions/system-context.mdx; the gate then reports
OK — 109 elevation read sites in 20 packages across 45 files, all anchored.
That docs edit pulled 24 further doc families into the derivation, all of which
were run and are green.

Ablation (the pin fails without the fix). manifest: toRecordManifest(manifest)
was reverted to manifest, in installPackage; the mutation was confirmed on
disk before running (anchor text 1 -> 0, injected text 1, blob hash differing
from HEAD), the restore leg was git checkout HEAD -- <abs path> under an
EXIT/INT/TERM trap, and restoration was proved by an empty git diff HEAD plus
git hash-object equality with the HEAD blob. No rebuild was needed: the pin
imports ./registry from source, not through dist. Result:

Tests 7 failed | 2 passed (9) ← ablated
Tests 9 passed (9) ← restored
AssertionError: expected [Function] to not throw an error but
'TypeError: Converting circular struct…' was thrown

Declared narrowing — verification ran UNLOCKED.scripts/pm/os-verify-lock.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not ship it), so the commands were run directly, without the lock — a declared
narrowing, not a silent one. No serialization guarantee held for these runs, nor
for any sibling agent in this container while they ran.

Declared narrowing — repo-wide scans left to CI.pnpm lint and the rest of
the 62-family farm were not run locally; CI runs the farm exactly once on this
PR. What ran locally is the targeted set and the 60 gate families
named above.

Generated by Claude Code

hotlongand others added 4 commits September 2, 2026 10:39
…e registry
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the door projections
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…packages-domain projection
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
hotlongand others added 2 commits September 3, 2026 00:19
…ckages-serializable-manifest
# Conflicts:
#	packages/runtime/src/domains/packages.ts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/rest, @objectstack/runtime, touching 25 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 37 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 50d9fd8153b16a9dd058730a4e02f89ac304578d — the merge of head 6961d6ec9bc74651928d56022bc909a7f7cc1a81 into base 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 50d9fd8153b16a9dd058730a4e02f89ac304578d && git checkout 50d9fd8153b16a9dd058730a4e02f89ac304578d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 6961d6ec9bc74651928d56022bc909a7f7cc1a81 && git checkout -B drift-repro 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 && git merge --no-ff 6961d6ec9bc74651928d56022bc909a7f7cc1a81
node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@hotlong
hotlong marked this pull request as ready for review September 2, 2026 16:38
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
os-zhuang pushed a commit that referenced this pull request Sep 2, 2026
…d land it on sendError
Re-homed under the domain seat's serial fence: `packages/rest/src/package-routes.ts`
is held by the open PR #14499, so this no longer edits that file. The REST
direct-mount doors are covered from the producer side instead.
`sendError` (`@objectstack/types`) is the single writer for every
nested-envelope error in the repo, and every catch in the package registrar
ends there — so wiring the rule at that one exit covers those doors with no
per-door call, and covers any door added later by construction. That also puts
the helper in the same package as `resolveThrownHttpError`, on the same
argument: a rule two doors must agree on cannot live inside one of them.
`@objectstack/observability` and `packages/rest` are back to origin/main
byte-for-byte, as is the system-context census page (its line rot was caused
by the package-routes import this drops).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33663028874 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@hotlong

Copy link
Copy Markdown
ContributorAuthor

Queue-eject triage (PM session session_62849a16-0144-4728-8942-9f60bb3a73f1), read against the signature ledger rather than judged on the spot:

Per the four-branch rule: known flaky ⇒ re-queue once, as-is, no new commit; if it ejects again on the same signature it stays out until #14679 is resolved (no reflexive re-arm). Re-arming now.

Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8ce628aSep 2, 2026
36 checks passed
@hotlong
hotlong deleted the claude/issue-14309-packages-serializable-manifest branch September 2, 2026 20:21
os-sales pushed a commit that referenced this pull request Sep 2, 2026
…stem-write-sharing-materialization
Resolves the single CONFLICT (content) in
content/docs/permissions/system-context.mdx (the os-regen driver flags
this file as MIXED — generated anchors plus hand-written prose — so it
text-merges instead of taking one side whole).
Row 37 (share-link policy `enabled` check, isSystem) diverged: this
branch's copy already carried the #14033 prose ("resolution is not
bypassed since #14033 ... a link minted this way does not resolve
until the block is enabled"), added when 13bf05d landed on both
branches. main's copy had reverted to the pre-#14033 wording via an
unrelated commit (8ce628a, "store a serializable manifest projection
in the package registry (#14499)") whose own docs-regen step appears
to have run against a stale pre-#14033 checkout of this file.
Kept this branch's row 37 text: packages/plugins/plugin-sharing/src/
share-link-service.ts:652-697 (the `[#14033]` docblock and the
`if (!policy.enabled)` redemption-time check it documents, citing the
maintainer's 2026-09-01 ruling and this exact ledger row by number)
confirms it is what the current source does, not the reverted main
copy. Row 36 differed only in its generated anchor line number
(1088 vs 1077) — anchor numbers are recomputed by
`pnpm gen:system-context-census` below, so either side was equivalent;
kept this branch's.
Anchors re-derived on the merged tree with
`pnpm gen:system-context-census` in a follow-up commit, per the
os-regen driver's documented recovery for this file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry - #14499

Merged
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest
Sep 2, 2026
Merged

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry#14499
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest

Conversation

@hotlong

@hotlonghotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14309

SchemaRegistry.installPackage now stores a serializable projection of the
manifest instead of the caller's live defineStack() object, and the /packages
read doors project a registry entry onto its declared record fields instead of
spreading it whole.

The measured defect

Reproduced on this branch's base, showcase on a random high port
(objectstack dev --seed-admin --fresh -p 39117), signed in as the seeded admin:

routebeforeafter
GET /api/v1/packages500INTERNAL_ERROR200
GET /api/v1/packages/com.example.showcase500INTERNAL_ERROR200
GET /api/v1/meta/package/com.example.showcase500200
GET /api/v1/meta/package/com.objectstack.setup200200

Error body of the first two:

Converting circular structure to JSON
--> starting at object with constructor '_ObjectQL'
| property 'actionActivation' -> object with constructor 'ActionActivationProjection'
| property 'store' -> object with constructor 'ObjectStoreActionActivationStore'
--- property 'engine' closes the circle

Which key actually reached the engine

Rather than reason about it, the running server was instrumented at
SchemaRegistry.getPackage (temporary, env-gated, reverted — the file is
byte-identical to its committed state, git hash-object verified) to classify
every top-level manifest key of every installed package by whether it
survives JSON.stringify. Across the 26 packages a showcase boot installs,
exactly one key on one package failed:

[OS_DEBUG_PKG_SERIALIZE] id=com.example.showcase keys=[…40 keys…]
UNSERIALIZABLE=["plugins :: Array<ConnectorOpenApiPlugin|ConnectorMcpPlugin|ConnectorRestPlugin|
ConnectorSlackPlugin|MarketplaceProxyPlugin|MarketplaceInstallLocalPlugin|CloudConnectionPlugin|
RuntimeConfigPlugin> :: Converting circular structure to JSON"]

And only after plugin init. The same manifest was probed 40 times during
boot with UNSERIALIZABLE=[] and 381 times afterwards with the plugins
failure — the transition is a single clean split in the log. That rules out a
whole family of "fixes": an install-time JSON.stringify probe, or any check
that runs while the record is being written, would have called this record
healthy. The projection therefore drops by shape, not by trial
serialisation, and does not depend on when it is asked.

The repair, at the producer

installPackage stored the argument verbatim as pkg.manifest. The registry
item is a record, not the runtime, so it now holds only the declarative
half. The rule is structural rather than a plugins denylist, because the fault
is not "the key is called plugins" — it is "a live object reached the record":

  • primitives, plain objects, arrays and Date are data and survive;
  • functions / symbols / bigints are dropped (a function was already invisible to
    JSON.stringify; a bigint would have thrown);
  • class instances, Map, Set and other exotic objects are dropped;
  • a reference cycle among plain data is dropped at the back-edge, so a
    self-referencing manifest degrades to a missing field instead of throwing.

Nothing downstream loses a member it was using: the kernel keeps the live object
in ObjectQL.manifests, and the only reader of manifest.plugins[]
registerApp's nested-plugin seam — reads its own parameter, never the
record (grepped, and pinned). The caller's manifest is copied, never stripped in
place, which the pins assert both ways.

⛔ Not a consumer-side tolerance (AGENTS.md Prime Directive #12): no reader is
taught to survive an unserializable record — the record is never unserializable.

Premise correction: which handler answered the 500

The card attributes the list/detail 500 to packages/rest/src/package-routes.ts
{ ...item, source: 'registry' }. Measured, that is not the handler that
answered in the showcase composition — packages/runtime/src/domains/packages.ts
did. Two independent probes agree:

  • the 404 wording. GET /api/v1/packages/no.such.package returned
    Package 'no.such.package' not found (the runtime domain's spelling); the
    REST twin says Package "no.such.package" was not found.
  • the response shape. GET /api/v1/packages/com.objectstack.setup returned the
    installed-package record directly under data (deps.success(pkg)); the
    REST twin wraps it as data.package and stamps source.

So the route-level defence is applied at both twins, and
packages/runtime/src/domains/packages.ts is a declared addition to this card's
file surface (announced on the issue before the work landed). Applying it only
where the card pointed would have shipped a defence with a hole exactly where the
defect was observed.

The doors now project onto the declared InstalledPackageSchema fields, so an
undeclared member appearing on the registry item — a live handle, a
back-reference — degrades to a field the response never mentions, instead of one
bad member on one package failing the whole list for every caller. The REST
merge's database half is deliberately left spreading: its shape belongs to
PackageService, and narrowing it would be a contract decision this card does
not carry. On the REST side _diagnostics is carried explicitly, because
decorateMetadataItem grafts it onto every item leaving getMetaItems and it is
measured to be the only thing that decoration adds for type: 'package'.

No response field is added or renamed; responses that already served fine are
byte-identical.

Verification

git rev-parse --short HEAD at the time of the runs below: 5b8b76e06.

Live routes, same showcase boot, after the fix — the table above; GET /packages
returns all 26 packages, the showcase manifest keeps its 40 declarative keys
(24 objects, 29 flows, 1 app), plugins serves as [] and the function-valued
onEnable is gone. JSON.stringify of the whole listing succeeds.

Package suites (all green, on the commit named above):

@objectstack/objectql Test Files 255 passed (255) Tests 4400 passed (4400)
@objectstack/rest Test Files 168 passed (168) Tests 2808 passed (2808)
@objectstack/runtime Test Files 208 passed (208) Tests 3066 passed (3066)

New pins — 20 cases over three files:
packages/objectql/src/registry-package-manifest-serializable.test.ts (9),
packages/runtime/src/domains/packages-serializable-response.test.ts (5),
packages/rest/src/package-registry-item-projection.test.ts (6).

Typecheck: @objectstack/objectql, @objectstack/runtime, @objectstack/rest
all exit 0. ⚠️ Measured rather than assumed: each package's tsc --noEmit
program excludes **/*.test.ts, so --listFiles reports 0 hits for all three
new test files there. The test layer is covered separately — objectql and rest
run check:test-typecheck over tsconfig.test.json, whose --listFilesdoes
name the new files (1 hit each, 0 errors attributed to them). packages/runtime
has no test-typecheck wiring at all, so its new test file is covered by no gate;
it was type-checked standalone (tsc --noEmit --ignoreConfig --strict, exit 0, 0
errors) and that gap is reported to the PM rather than papered over.

Gate families — 62 derived by node scripts/pm/dispatch-gates.mjs --commands
(no paths passed; it takes its own change set from the merge base). 60 ran green
locally. The remaining two exited 3, which each script's own verdict text
declares is NOT MEASURED and explicitly "not a red": check-test-completeness
needs a saved turbo run test log it was not handed, and
scripts/pm/check-half-states.mjs needs a per-card comment fetch that never
happened.

check:system-context-census was the one family this diff genuinely moved: the
+50-line helper in packages/runtime/src/domains/packages.ts shifted three
elevation-read anchors (:95 -> :145, :128 -> :178, :246 -> :296). Repaired
mechanically with the gate's own --fix, which re-anchored
content/docs/permissions/system-context.mdx; the gate then reports
OK — 109 elevation read sites in 20 packages across 45 files, all anchored.
That docs edit pulled 24 further doc families into the derivation, all of which
were run and are green.

Ablation (the pin fails without the fix). manifest: toRecordManifest(manifest)
was reverted to manifest, in installPackage; the mutation was confirmed on
disk before running (anchor text 1 -> 0, injected text 1, blob hash differing
from HEAD), the restore leg was git checkout HEAD -- <abs path> under an
EXIT/INT/TERM trap, and restoration was proved by an empty git diff HEAD plus
git hash-object equality with the HEAD blob. No rebuild was needed: the pin
imports ./registry from source, not through dist. Result:

Tests 7 failed | 2 passed (9) ← ablated
Tests 9 passed (9) ← restored
AssertionError: expected [Function] to not throw an error but
'TypeError: Converting circular struct…' was thrown

Declared narrowing — verification ran UNLOCKED.scripts/pm/os-verify-lock.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not ship it), so the commands were run directly, without the lock — a declared
narrowing, not a silent one. No serialization guarantee held for these runs, nor
for any sibling agent in this container while they ran.

Declared narrowing — repo-wide scans left to CI.pnpm lint and the rest of
the 62-family farm were not run locally; CI runs the farm exactly once on this
PR. What ran locally is the targeted set and the 60 gate families
named above.

Generated by Claude Code

hotlongand others added 4 commits September 2, 2026 10:39
…e registry
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the door projections
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…packages-domain projection
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
hotlongand others added 2 commits September 3, 2026 00:19
…ckages-serializable-manifest
# Conflicts:
#	packages/runtime/src/domains/packages.ts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/rest, @objectstack/runtime, touching 25 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 37 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 50d9fd8153b16a9dd058730a4e02f89ac304578d — the merge of head 6961d6ec9bc74651928d56022bc909a7f7cc1a81 into base 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 50d9fd8153b16a9dd058730a4e02f89ac304578d && git checkout 50d9fd8153b16a9dd058730a4e02f89ac304578d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 6961d6ec9bc74651928d56022bc909a7f7cc1a81 && git checkout -B drift-repro 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 && git merge --no-ff 6961d6ec9bc74651928d56022bc909a7f7cc1a81
node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@hotlong
hotlong marked this pull request as ready for review September 2, 2026 16:38
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
os-zhuang pushed a commit that referenced this pull request Sep 2, 2026
…d land it on sendError
Re-homed under the domain seat's serial fence: `packages/rest/src/package-routes.ts`
is held by the open PR #14499, so this no longer edits that file. The REST
direct-mount doors are covered from the producer side instead.
`sendError` (`@objectstack/types`) is the single writer for every
nested-envelope error in the repo, and every catch in the package registrar
ends there — so wiring the rule at that one exit covers those doors with no
per-door call, and covers any door added later by construction. That also puts
the helper in the same package as `resolveThrownHttpError`, on the same
argument: a rule two doors must agree on cannot live inside one of them.
`@objectstack/observability` and `packages/rest` are back to origin/main
byte-for-byte, as is the system-context census page (its line rot was caused
by the package-routes import this drops).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33663028874 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@hotlong

Copy link
Copy Markdown
ContributorAuthor

Queue-eject triage (PM session session_62849a16-0144-4728-8942-9f60bb3a73f1), read against the signature ledger rather than judged on the spot:

Per the four-branch rule: known flaky ⇒ re-queue once, as-is, no new commit; if it ejects again on the same signature it stays out until #14679 is resolved (no reflexive re-arm). Re-arming now.

Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8ce628aSep 2, 2026
36 checks passed
@hotlong
hotlong deleted the claude/issue-14309-packages-serializable-manifest branch September 2, 2026 20:21
os-sales pushed a commit that referenced this pull request Sep 2, 2026
…stem-write-sharing-materialization
Resolves the single CONFLICT (content) in
content/docs/permissions/system-context.mdx (the os-regen driver flags
this file as MIXED — generated anchors plus hand-written prose — so it
text-merges instead of taking one side whole).
Row 37 (share-link policy `enabled` check, isSystem) diverged: this
branch's copy already carried the #14033 prose ("resolution is not
bypassed since #14033 ... a link minted this way does not resolve
until the block is enabled"), added when 13bf05d landed on both
branches. main's copy had reverted to the pre-#14033 wording via an
unrelated commit (8ce628a, "store a serializable manifest projection
in the package registry (#14499)") whose own docs-regen step appears
to have run against a stale pre-#14033 checkout of this file.
Kept this branch's row 37 text: packages/plugins/plugin-sharing/src/
share-link-service.ts:652-697 (the `[#14033]` docblock and the
`if (!policy.enabled)` redemption-time check it documents, citing the
maintainer's 2026-09-01 ruling and this exact ledger row by number)
confirms it is what the current source does, not the reverted main
copy. Row 36 differed only in its generated anchor line number
(1088 vs 1077) — anchor numbers are recomputed by
`pnpm gen:system-context-census` below, so either side was equivalent;
kept this branch's.
Anchors re-derived on the merged tree with
`pnpm gen:system-context-census` in a follow-up commit, per the
os-regen driver's documented recovery for this file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry - #14499

Merged
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest
Sep 2, 2026
Merged

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry#14499
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest

Conversation

@hotlong

@hotlonghotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14309

SchemaRegistry.installPackage now stores a serializable projection of the
manifest instead of the caller's live defineStack() object, and the /packages
read doors project a registry entry onto its declared record fields instead of
spreading it whole.

The measured defect

Reproduced on this branch's base, showcase on a random high port
(objectstack dev --seed-admin --fresh -p 39117), signed in as the seeded admin:

routebeforeafter
GET /api/v1/packages500INTERNAL_ERROR200
GET /api/v1/packages/com.example.showcase500INTERNAL_ERROR200
GET /api/v1/meta/package/com.example.showcase500200
GET /api/v1/meta/package/com.objectstack.setup200200

Error body of the first two:

Converting circular structure to JSON
--> starting at object with constructor '_ObjectQL'
| property 'actionActivation' -> object with constructor 'ActionActivationProjection'
| property 'store' -> object with constructor 'ObjectStoreActionActivationStore'
--- property 'engine' closes the circle

Which key actually reached the engine

Rather than reason about it, the running server was instrumented at
SchemaRegistry.getPackage (temporary, env-gated, reverted — the file is
byte-identical to its committed state, git hash-object verified) to classify
every top-level manifest key of every installed package by whether it
survives JSON.stringify. Across the 26 packages a showcase boot installs,
exactly one key on one package failed:

[OS_DEBUG_PKG_SERIALIZE] id=com.example.showcase keys=[…40 keys…]
UNSERIALIZABLE=["plugins :: Array<ConnectorOpenApiPlugin|ConnectorMcpPlugin|ConnectorRestPlugin|
ConnectorSlackPlugin|MarketplaceProxyPlugin|MarketplaceInstallLocalPlugin|CloudConnectionPlugin|
RuntimeConfigPlugin> :: Converting circular structure to JSON"]

And only after plugin init. The same manifest was probed 40 times during
boot with UNSERIALIZABLE=[] and 381 times afterwards with the plugins
failure — the transition is a single clean split in the log. That rules out a
whole family of "fixes": an install-time JSON.stringify probe, or any check
that runs while the record is being written, would have called this record
healthy. The projection therefore drops by shape, not by trial
serialisation, and does not depend on when it is asked.

The repair, at the producer

installPackage stored the argument verbatim as pkg.manifest. The registry
item is a record, not the runtime, so it now holds only the declarative
half. The rule is structural rather than a plugins denylist, because the fault
is not "the key is called plugins" — it is "a live object reached the record":

  • primitives, plain objects, arrays and Date are data and survive;
  • functions / symbols / bigints are dropped (a function was already invisible to
    JSON.stringify; a bigint would have thrown);
  • class instances, Map, Set and other exotic objects are dropped;
  • a reference cycle among plain data is dropped at the back-edge, so a
    self-referencing manifest degrades to a missing field instead of throwing.

Nothing downstream loses a member it was using: the kernel keeps the live object
in ObjectQL.manifests, and the only reader of manifest.plugins[]
registerApp's nested-plugin seam — reads its own parameter, never the
record (grepped, and pinned). The caller's manifest is copied, never stripped in
place, which the pins assert both ways.

⛔ Not a consumer-side tolerance (AGENTS.md Prime Directive #12): no reader is
taught to survive an unserializable record — the record is never unserializable.

Premise correction: which handler answered the 500

The card attributes the list/detail 500 to packages/rest/src/package-routes.ts
{ ...item, source: 'registry' }. Measured, that is not the handler that
answered in the showcase composition — packages/runtime/src/domains/packages.ts
did. Two independent probes agree:

  • the 404 wording. GET /api/v1/packages/no.such.package returned
    Package 'no.such.package' not found (the runtime domain's spelling); the
    REST twin says Package "no.such.package" was not found.
  • the response shape. GET /api/v1/packages/com.objectstack.setup returned the
    installed-package record directly under data (deps.success(pkg)); the
    REST twin wraps it as data.package and stamps source.

So the route-level defence is applied at both twins, and
packages/runtime/src/domains/packages.ts is a declared addition to this card's
file surface (announced on the issue before the work landed). Applying it only
where the card pointed would have shipped a defence with a hole exactly where the
defect was observed.

The doors now project onto the declared InstalledPackageSchema fields, so an
undeclared member appearing on the registry item — a live handle, a
back-reference — degrades to a field the response never mentions, instead of one
bad member on one package failing the whole list for every caller. The REST
merge's database half is deliberately left spreading: its shape belongs to
PackageService, and narrowing it would be a contract decision this card does
not carry. On the REST side _diagnostics is carried explicitly, because
decorateMetadataItem grafts it onto every item leaving getMetaItems and it is
measured to be the only thing that decoration adds for type: 'package'.

No response field is added or renamed; responses that already served fine are
byte-identical.

Verification

git rev-parse --short HEAD at the time of the runs below: 5b8b76e06.

Live routes, same showcase boot, after the fix — the table above; GET /packages
returns all 26 packages, the showcase manifest keeps its 40 declarative keys
(24 objects, 29 flows, 1 app), plugins serves as [] and the function-valued
onEnable is gone. JSON.stringify of the whole listing succeeds.

Package suites (all green, on the commit named above):

@objectstack/objectql Test Files 255 passed (255) Tests 4400 passed (4400)
@objectstack/rest Test Files 168 passed (168) Tests 2808 passed (2808)
@objectstack/runtime Test Files 208 passed (208) Tests 3066 passed (3066)

New pins — 20 cases over three files:
packages/objectql/src/registry-package-manifest-serializable.test.ts (9),
packages/runtime/src/domains/packages-serializable-response.test.ts (5),
packages/rest/src/package-registry-item-projection.test.ts (6).

Typecheck: @objectstack/objectql, @objectstack/runtime, @objectstack/rest
all exit 0. ⚠️ Measured rather than assumed: each package's tsc --noEmit
program excludes **/*.test.ts, so --listFiles reports 0 hits for all three
new test files there. The test layer is covered separately — objectql and rest
run check:test-typecheck over tsconfig.test.json, whose --listFilesdoes
name the new files (1 hit each, 0 errors attributed to them). packages/runtime
has no test-typecheck wiring at all, so its new test file is covered by no gate;
it was type-checked standalone (tsc --noEmit --ignoreConfig --strict, exit 0, 0
errors) and that gap is reported to the PM rather than papered over.

Gate families — 62 derived by node scripts/pm/dispatch-gates.mjs --commands
(no paths passed; it takes its own change set from the merge base). 60 ran green
locally. The remaining two exited 3, which each script's own verdict text
declares is NOT MEASURED and explicitly "not a red": check-test-completeness
needs a saved turbo run test log it was not handed, and
scripts/pm/check-half-states.mjs needs a per-card comment fetch that never
happened.

check:system-context-census was the one family this diff genuinely moved: the
+50-line helper in packages/runtime/src/domains/packages.ts shifted three
elevation-read anchors (:95 -> :145, :128 -> :178, :246 -> :296). Repaired
mechanically with the gate's own --fix, which re-anchored
content/docs/permissions/system-context.mdx; the gate then reports
OK — 109 elevation read sites in 20 packages across 45 files, all anchored.
That docs edit pulled 24 further doc families into the derivation, all of which
were run and are green.

Ablation (the pin fails without the fix). manifest: toRecordManifest(manifest)
was reverted to manifest, in installPackage; the mutation was confirmed on
disk before running (anchor text 1 -> 0, injected text 1, blob hash differing
from HEAD), the restore leg was git checkout HEAD -- <abs path> under an
EXIT/INT/TERM trap, and restoration was proved by an empty git diff HEAD plus
git hash-object equality with the HEAD blob. No rebuild was needed: the pin
imports ./registry from source, not through dist. Result:

Tests 7 failed | 2 passed (9) ← ablated
Tests 9 passed (9) ← restored
AssertionError: expected [Function] to not throw an error but
'TypeError: Converting circular struct…' was thrown

Declared narrowing — verification ran UNLOCKED.scripts/pm/os-verify-lock.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not ship it), so the commands were run directly, without the lock — a declared
narrowing, not a silent one. No serialization guarantee held for these runs, nor
for any sibling agent in this container while they ran.

Declared narrowing — repo-wide scans left to CI.pnpm lint and the rest of
the 62-family farm were not run locally; CI runs the farm exactly once on this
PR. What ran locally is the targeted set and the 60 gate families
named above.

Generated by Claude Code

hotlongand others added 4 commits September 2, 2026 10:39
…e registry
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the door projections
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…packages-domain projection
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
hotlongand others added 2 commits September 3, 2026 00:19
…ckages-serializable-manifest
# Conflicts:
#	packages/runtime/src/domains/packages.ts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/rest, @objectstack/runtime, touching 25 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 37 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 50d9fd8153b16a9dd058730a4e02f89ac304578d — the merge of head 6961d6ec9bc74651928d56022bc909a7f7cc1a81 into base 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 50d9fd8153b16a9dd058730a4e02f89ac304578d && git checkout 50d9fd8153b16a9dd058730a4e02f89ac304578d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 6961d6ec9bc74651928d56022bc909a7f7cc1a81 && git checkout -B drift-repro 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 && git merge --no-ff 6961d6ec9bc74651928d56022bc909a7f7cc1a81
node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@hotlong
hotlong marked this pull request as ready for review September 2, 2026 16:38
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
os-zhuang pushed a commit that referenced this pull request Sep 2, 2026
…d land it on sendError
Re-homed under the domain seat's serial fence: `packages/rest/src/package-routes.ts`
is held by the open PR #14499, so this no longer edits that file. The REST
direct-mount doors are covered from the producer side instead.
`sendError` (`@objectstack/types`) is the single writer for every
nested-envelope error in the repo, and every catch in the package registrar
ends there — so wiring the rule at that one exit covers those doors with no
per-door call, and covers any door added later by construction. That also puts
the helper in the same package as `resolveThrownHttpError`, on the same
argument: a rule two doors must agree on cannot live inside one of them.
`@objectstack/observability` and `packages/rest` are back to origin/main
byte-for-byte, as is the system-context census page (its line rot was caused
by the package-routes import this drops).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33663028874 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@hotlong

Copy link
Copy Markdown
ContributorAuthor

Queue-eject triage (PM session session_62849a16-0144-4728-8942-9f60bb3a73f1), read against the signature ledger rather than judged on the spot:

Per the four-branch rule: known flaky ⇒ re-queue once, as-is, no new commit; if it ejects again on the same signature it stays out until #14679 is resolved (no reflexive re-arm). Re-arming now.

Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8ce628aSep 2, 2026
36 checks passed
@hotlong
hotlong deleted the claude/issue-14309-packages-serializable-manifest branch September 2, 2026 20:21
os-sales pushed a commit that referenced this pull request Sep 2, 2026
…stem-write-sharing-materialization
Resolves the single CONFLICT (content) in
content/docs/permissions/system-context.mdx (the os-regen driver flags
this file as MIXED — generated anchors plus hand-written prose — so it
text-merges instead of taking one side whole).
Row 37 (share-link policy `enabled` check, isSystem) diverged: this
branch's copy already carried the #14033 prose ("resolution is not
bypassed since #14033 ... a link minted this way does not resolve
until the block is enabled"), added when 13bf05d landed on both
branches. main's copy had reverted to the pre-#14033 wording via an
unrelated commit (8ce628a, "store a serializable manifest projection
in the package registry (#14499)") whose own docs-regen step appears
to have run against a stale pre-#14033 checkout of this file.
Kept this branch's row 37 text: packages/plugins/plugin-sharing/src/
share-link-service.ts:652-697 (the `[#14033]` docblock and the
`if (!policy.enabled)` redemption-time check it documents, citing the
maintainer's 2026-09-01 ruling and this exact ledger row by number)
confirms it is what the current source does, not the reverted main
copy. Row 36 differed only in its generated anchor line number
(1088 vs 1077) — anchor numbers are recomputed by
`pnpm gen:system-context-census` below, so either side was equivalent;
kept this branch's.
Anchors re-derived on the merged tree with
`pnpm gen:system-context-census` in a follow-up commit, per the
os-regen driver's documented recovery for this file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry - #14499

Merged
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest
Sep 2, 2026
Merged

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry#14499
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest

Conversation

@hotlong

@hotlonghotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14309

SchemaRegistry.installPackage now stores a serializable projection of the
manifest instead of the caller's live defineStack() object, and the /packages
read doors project a registry entry onto its declared record fields instead of
spreading it whole.

The measured defect

Reproduced on this branch's base, showcase on a random high port
(objectstack dev --seed-admin --fresh -p 39117), signed in as the seeded admin:

routebeforeafter
GET /api/v1/packages500INTERNAL_ERROR200
GET /api/v1/packages/com.example.showcase500INTERNAL_ERROR200
GET /api/v1/meta/package/com.example.showcase500200
GET /api/v1/meta/package/com.objectstack.setup200200

Error body of the first two:

Converting circular structure to JSON
--> starting at object with constructor '_ObjectQL'
| property 'actionActivation' -> object with constructor 'ActionActivationProjection'
| property 'store' -> object with constructor 'ObjectStoreActionActivationStore'
--- property 'engine' closes the circle

Which key actually reached the engine

Rather than reason about it, the running server was instrumented at
SchemaRegistry.getPackage (temporary, env-gated, reverted — the file is
byte-identical to its committed state, git hash-object verified) to classify
every top-level manifest key of every installed package by whether it
survives JSON.stringify. Across the 26 packages a showcase boot installs,
exactly one key on one package failed:

[OS_DEBUG_PKG_SERIALIZE] id=com.example.showcase keys=[…40 keys…]
UNSERIALIZABLE=["plugins :: Array<ConnectorOpenApiPlugin|ConnectorMcpPlugin|ConnectorRestPlugin|
ConnectorSlackPlugin|MarketplaceProxyPlugin|MarketplaceInstallLocalPlugin|CloudConnectionPlugin|
RuntimeConfigPlugin> :: Converting circular structure to JSON"]

And only after plugin init. The same manifest was probed 40 times during
boot with UNSERIALIZABLE=[] and 381 times afterwards with the plugins
failure — the transition is a single clean split in the log. That rules out a
whole family of "fixes": an install-time JSON.stringify probe, or any check
that runs while the record is being written, would have called this record
healthy. The projection therefore drops by shape, not by trial
serialisation, and does not depend on when it is asked.

The repair, at the producer

installPackage stored the argument verbatim as pkg.manifest. The registry
item is a record, not the runtime, so it now holds only the declarative
half. The rule is structural rather than a plugins denylist, because the fault
is not "the key is called plugins" — it is "a live object reached the record":

  • primitives, plain objects, arrays and Date are data and survive;
  • functions / symbols / bigints are dropped (a function was already invisible to
    JSON.stringify; a bigint would have thrown);
  • class instances, Map, Set and other exotic objects are dropped;
  • a reference cycle among plain data is dropped at the back-edge, so a
    self-referencing manifest degrades to a missing field instead of throwing.

Nothing downstream loses a member it was using: the kernel keeps the live object
in ObjectQL.manifests, and the only reader of manifest.plugins[]
registerApp's nested-plugin seam — reads its own parameter, never the
record (grepped, and pinned). The caller's manifest is copied, never stripped in
place, which the pins assert both ways.

⛔ Not a consumer-side tolerance (AGENTS.md Prime Directive #12): no reader is
taught to survive an unserializable record — the record is never unserializable.

Premise correction: which handler answered the 500

The card attributes the list/detail 500 to packages/rest/src/package-routes.ts
{ ...item, source: 'registry' }. Measured, that is not the handler that
answered in the showcase composition — packages/runtime/src/domains/packages.ts
did. Two independent probes agree:

  • the 404 wording. GET /api/v1/packages/no.such.package returned
    Package 'no.such.package' not found (the runtime domain's spelling); the
    REST twin says Package "no.such.package" was not found.
  • the response shape. GET /api/v1/packages/com.objectstack.setup returned the
    installed-package record directly under data (deps.success(pkg)); the
    REST twin wraps it as data.package and stamps source.

So the route-level defence is applied at both twins, and
packages/runtime/src/domains/packages.ts is a declared addition to this card's
file surface (announced on the issue before the work landed). Applying it only
where the card pointed would have shipped a defence with a hole exactly where the
defect was observed.

The doors now project onto the declared InstalledPackageSchema fields, so an
undeclared member appearing on the registry item — a live handle, a
back-reference — degrades to a field the response never mentions, instead of one
bad member on one package failing the whole list for every caller. The REST
merge's database half is deliberately left spreading: its shape belongs to
PackageService, and narrowing it would be a contract decision this card does
not carry. On the REST side _diagnostics is carried explicitly, because
decorateMetadataItem grafts it onto every item leaving getMetaItems and it is
measured to be the only thing that decoration adds for type: 'package'.

No response field is added or renamed; responses that already served fine are
byte-identical.

Verification

git rev-parse --short HEAD at the time of the runs below: 5b8b76e06.

Live routes, same showcase boot, after the fix — the table above; GET /packages
returns all 26 packages, the showcase manifest keeps its 40 declarative keys
(24 objects, 29 flows, 1 app), plugins serves as [] and the function-valued
onEnable is gone. JSON.stringify of the whole listing succeeds.

Package suites (all green, on the commit named above):

@objectstack/objectql Test Files 255 passed (255) Tests 4400 passed (4400)
@objectstack/rest Test Files 168 passed (168) Tests 2808 passed (2808)
@objectstack/runtime Test Files 208 passed (208) Tests 3066 passed (3066)

New pins — 20 cases over three files:
packages/objectql/src/registry-package-manifest-serializable.test.ts (9),
packages/runtime/src/domains/packages-serializable-response.test.ts (5),
packages/rest/src/package-registry-item-projection.test.ts (6).

Typecheck: @objectstack/objectql, @objectstack/runtime, @objectstack/rest
all exit 0. ⚠️ Measured rather than assumed: each package's tsc --noEmit
program excludes **/*.test.ts, so --listFiles reports 0 hits for all three
new test files there. The test layer is covered separately — objectql and rest
run check:test-typecheck over tsconfig.test.json, whose --listFilesdoes
name the new files (1 hit each, 0 errors attributed to them). packages/runtime
has no test-typecheck wiring at all, so its new test file is covered by no gate;
it was type-checked standalone (tsc --noEmit --ignoreConfig --strict, exit 0, 0
errors) and that gap is reported to the PM rather than papered over.

Gate families — 62 derived by node scripts/pm/dispatch-gates.mjs --commands
(no paths passed; it takes its own change set from the merge base). 60 ran green
locally. The remaining two exited 3, which each script's own verdict text
declares is NOT MEASURED and explicitly "not a red": check-test-completeness
needs a saved turbo run test log it was not handed, and
scripts/pm/check-half-states.mjs needs a per-card comment fetch that never
happened.

check:system-context-census was the one family this diff genuinely moved: the
+50-line helper in packages/runtime/src/domains/packages.ts shifted three
elevation-read anchors (:95 -> :145, :128 -> :178, :246 -> :296). Repaired
mechanically with the gate's own --fix, which re-anchored
content/docs/permissions/system-context.mdx; the gate then reports
OK — 109 elevation read sites in 20 packages across 45 files, all anchored.
That docs edit pulled 24 further doc families into the derivation, all of which
were run and are green.

Ablation (the pin fails without the fix). manifest: toRecordManifest(manifest)
was reverted to manifest, in installPackage; the mutation was confirmed on
disk before running (anchor text 1 -> 0, injected text 1, blob hash differing
from HEAD), the restore leg was git checkout HEAD -- <abs path> under an
EXIT/INT/TERM trap, and restoration was proved by an empty git diff HEAD plus
git hash-object equality with the HEAD blob. No rebuild was needed: the pin
imports ./registry from source, not through dist. Result:

Tests 7 failed | 2 passed (9) ← ablated
Tests 9 passed (9) ← restored
AssertionError: expected [Function] to not throw an error but
'TypeError: Converting circular struct…' was thrown

Declared narrowing — verification ran UNLOCKED.scripts/pm/os-verify-lock.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not ship it), so the commands were run directly, without the lock — a declared
narrowing, not a silent one. No serialization guarantee held for these runs, nor
for any sibling agent in this container while they ran.

Declared narrowing — repo-wide scans left to CI.pnpm lint and the rest of
the 62-family farm were not run locally; CI runs the farm exactly once on this
PR. What ran locally is the targeted set and the 60 gate families
named above.

Generated by Claude Code

hotlongand others added 4 commits September 2, 2026 10:39
…e registry
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the door projections
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…packages-domain projection
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
hotlongand others added 2 commits September 3, 2026 00:19
…ckages-serializable-manifest
# Conflicts:
#	packages/runtime/src/domains/packages.ts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/rest, @objectstack/runtime, touching 25 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 37 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 50d9fd8153b16a9dd058730a4e02f89ac304578d — the merge of head 6961d6ec9bc74651928d56022bc909a7f7cc1a81 into base 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 50d9fd8153b16a9dd058730a4e02f89ac304578d && git checkout 50d9fd8153b16a9dd058730a4e02f89ac304578d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 6961d6ec9bc74651928d56022bc909a7f7cc1a81 && git checkout -B drift-repro 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 && git merge --no-ff 6961d6ec9bc74651928d56022bc909a7f7cc1a81
node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@hotlong
hotlong marked this pull request as ready for review September 2, 2026 16:38
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
os-zhuang pushed a commit that referenced this pull request Sep 2, 2026
…d land it on sendError
Re-homed under the domain seat's serial fence: `packages/rest/src/package-routes.ts`
is held by the open PR #14499, so this no longer edits that file. The REST
direct-mount doors are covered from the producer side instead.
`sendError` (`@objectstack/types`) is the single writer for every
nested-envelope error in the repo, and every catch in the package registrar
ends there — so wiring the rule at that one exit covers those doors with no
per-door call, and covers any door added later by construction. That also puts
the helper in the same package as `resolveThrownHttpError`, on the same
argument: a rule two doors must agree on cannot live inside one of them.
`@objectstack/observability` and `packages/rest` are back to origin/main
byte-for-byte, as is the system-context census page (its line rot was caused
by the package-routes import this drops).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33663028874 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@hotlong

Copy link
Copy Markdown
ContributorAuthor

Queue-eject triage (PM session session_62849a16-0144-4728-8942-9f60bb3a73f1), read against the signature ledger rather than judged on the spot:

Per the four-branch rule: known flaky ⇒ re-queue once, as-is, no new commit; if it ejects again on the same signature it stays out until #14679 is resolved (no reflexive re-arm). Re-arming now.

Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8ce628aSep 2, 2026
36 checks passed
@hotlong
hotlong deleted the claude/issue-14309-packages-serializable-manifest branch September 2, 2026 20:21
os-sales pushed a commit that referenced this pull request Sep 2, 2026
…stem-write-sharing-materialization
Resolves the single CONFLICT (content) in
content/docs/permissions/system-context.mdx (the os-regen driver flags
this file as MIXED — generated anchors plus hand-written prose — so it
text-merges instead of taking one side whole).
Row 37 (share-link policy `enabled` check, isSystem) diverged: this
branch's copy already carried the #14033 prose ("resolution is not
bypassed since #14033 ... a link minted this way does not resolve
until the block is enabled"), added when 13bf05d landed on both
branches. main's copy had reverted to the pre-#14033 wording via an
unrelated commit (8ce628a, "store a serializable manifest projection
in the package registry (#14499)") whose own docs-regen step appears
to have run against a stale pre-#14033 checkout of this file.
Kept this branch's row 37 text: packages/plugins/plugin-sharing/src/
share-link-service.ts:652-697 (the `[#14033]` docblock and the
`if (!policy.enabled)` redemption-time check it documents, citing the
maintainer's 2026-09-01 ruling and this exact ledger row by number)
confirms it is what the current source does, not the reverted main
copy. Row 36 differed only in its generated anchor line number
(1088 vs 1077) — anchor numbers are recomputed by
`pnpm gen:system-context-census` below, so either side was equivalent;
kept this branch's.
Anchors re-derived on the merged tree with
`pnpm gen:system-context-census` in a follow-up commit, per the
os-regen driver's documented recovery for this file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry - #14499

Merged
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest
Sep 2, 2026
Merged

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry#14499
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest

Conversation

@hotlong

@hotlonghotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14309

SchemaRegistry.installPackage now stores a serializable projection of the
manifest instead of the caller's live defineStack() object, and the /packages
read doors project a registry entry onto its declared record fields instead of
spreading it whole.

The measured defect

Reproduced on this branch's base, showcase on a random high port
(objectstack dev --seed-admin --fresh -p 39117), signed in as the seeded admin:

routebeforeafter
GET /api/v1/packages500INTERNAL_ERROR200
GET /api/v1/packages/com.example.showcase500INTERNAL_ERROR200
GET /api/v1/meta/package/com.example.showcase500200
GET /api/v1/meta/package/com.objectstack.setup200200

Error body of the first two:

Converting circular structure to JSON
--> starting at object with constructor '_ObjectQL'
| property 'actionActivation' -> object with constructor 'ActionActivationProjection'
| property 'store' -> object with constructor 'ObjectStoreActionActivationStore'
--- property 'engine' closes the circle

Which key actually reached the engine

Rather than reason about it, the running server was instrumented at
SchemaRegistry.getPackage (temporary, env-gated, reverted — the file is
byte-identical to its committed state, git hash-object verified) to classify
every top-level manifest key of every installed package by whether it
survives JSON.stringify. Across the 26 packages a showcase boot installs,
exactly one key on one package failed:

[OS_DEBUG_PKG_SERIALIZE] id=com.example.showcase keys=[…40 keys…]
UNSERIALIZABLE=["plugins :: Array<ConnectorOpenApiPlugin|ConnectorMcpPlugin|ConnectorRestPlugin|
ConnectorSlackPlugin|MarketplaceProxyPlugin|MarketplaceInstallLocalPlugin|CloudConnectionPlugin|
RuntimeConfigPlugin> :: Converting circular structure to JSON"]

And only after plugin init. The same manifest was probed 40 times during
boot with UNSERIALIZABLE=[] and 381 times afterwards with the plugins
failure — the transition is a single clean split in the log. That rules out a
whole family of "fixes": an install-time JSON.stringify probe, or any check
that runs while the record is being written, would have called this record
healthy. The projection therefore drops by shape, not by trial
serialisation, and does not depend on when it is asked.

The repair, at the producer

installPackage stored the argument verbatim as pkg.manifest. The registry
item is a record, not the runtime, so it now holds only the declarative
half. The rule is structural rather than a plugins denylist, because the fault
is not "the key is called plugins" — it is "a live object reached the record":

  • primitives, plain objects, arrays and Date are data and survive;
  • functions / symbols / bigints are dropped (a function was already invisible to
    JSON.stringify; a bigint would have thrown);
  • class instances, Map, Set and other exotic objects are dropped;
  • a reference cycle among plain data is dropped at the back-edge, so a
    self-referencing manifest degrades to a missing field instead of throwing.

Nothing downstream loses a member it was using: the kernel keeps the live object
in ObjectQL.manifests, and the only reader of manifest.plugins[]
registerApp's nested-plugin seam — reads its own parameter, never the
record (grepped, and pinned). The caller's manifest is copied, never stripped in
place, which the pins assert both ways.

⛔ Not a consumer-side tolerance (AGENTS.md Prime Directive #12): no reader is
taught to survive an unserializable record — the record is never unserializable.

Premise correction: which handler answered the 500

The card attributes the list/detail 500 to packages/rest/src/package-routes.ts
{ ...item, source: 'registry' }. Measured, that is not the handler that
answered in the showcase composition — packages/runtime/src/domains/packages.ts
did. Two independent probes agree:

  • the 404 wording. GET /api/v1/packages/no.such.package returned
    Package 'no.such.package' not found (the runtime domain's spelling); the
    REST twin says Package "no.such.package" was not found.
  • the response shape. GET /api/v1/packages/com.objectstack.setup returned the
    installed-package record directly under data (deps.success(pkg)); the
    REST twin wraps it as data.package and stamps source.

So the route-level defence is applied at both twins, and
packages/runtime/src/domains/packages.ts is a declared addition to this card's
file surface (announced on the issue before the work landed). Applying it only
where the card pointed would have shipped a defence with a hole exactly where the
defect was observed.

The doors now project onto the declared InstalledPackageSchema fields, so an
undeclared member appearing on the registry item — a live handle, a
back-reference — degrades to a field the response never mentions, instead of one
bad member on one package failing the whole list for every caller. The REST
merge's database half is deliberately left spreading: its shape belongs to
PackageService, and narrowing it would be a contract decision this card does
not carry. On the REST side _diagnostics is carried explicitly, because
decorateMetadataItem grafts it onto every item leaving getMetaItems and it is
measured to be the only thing that decoration adds for type: 'package'.

No response field is added or renamed; responses that already served fine are
byte-identical.

Verification

git rev-parse --short HEAD at the time of the runs below: 5b8b76e06.

Live routes, same showcase boot, after the fix — the table above; GET /packages
returns all 26 packages, the showcase manifest keeps its 40 declarative keys
(24 objects, 29 flows, 1 app), plugins serves as [] and the function-valued
onEnable is gone. JSON.stringify of the whole listing succeeds.

Package suites (all green, on the commit named above):

@objectstack/objectql Test Files 255 passed (255) Tests 4400 passed (4400)
@objectstack/rest Test Files 168 passed (168) Tests 2808 passed (2808)
@objectstack/runtime Test Files 208 passed (208) Tests 3066 passed (3066)

New pins — 20 cases over three files:
packages/objectql/src/registry-package-manifest-serializable.test.ts (9),
packages/runtime/src/domains/packages-serializable-response.test.ts (5),
packages/rest/src/package-registry-item-projection.test.ts (6).

Typecheck: @objectstack/objectql, @objectstack/runtime, @objectstack/rest
all exit 0. ⚠️ Measured rather than assumed: each package's tsc --noEmit
program excludes **/*.test.ts, so --listFiles reports 0 hits for all three
new test files there. The test layer is covered separately — objectql and rest
run check:test-typecheck over tsconfig.test.json, whose --listFilesdoes
name the new files (1 hit each, 0 errors attributed to them). packages/runtime
has no test-typecheck wiring at all, so its new test file is covered by no gate;
it was type-checked standalone (tsc --noEmit --ignoreConfig --strict, exit 0, 0
errors) and that gap is reported to the PM rather than papered over.

Gate families — 62 derived by node scripts/pm/dispatch-gates.mjs --commands
(no paths passed; it takes its own change set from the merge base). 60 ran green
locally. The remaining two exited 3, which each script's own verdict text
declares is NOT MEASURED and explicitly "not a red": check-test-completeness
needs a saved turbo run test log it was not handed, and
scripts/pm/check-half-states.mjs needs a per-card comment fetch that never
happened.

check:system-context-census was the one family this diff genuinely moved: the
+50-line helper in packages/runtime/src/domains/packages.ts shifted three
elevation-read anchors (:95 -> :145, :128 -> :178, :246 -> :296). Repaired
mechanically with the gate's own --fix, which re-anchored
content/docs/permissions/system-context.mdx; the gate then reports
OK — 109 elevation read sites in 20 packages across 45 files, all anchored.
That docs edit pulled 24 further doc families into the derivation, all of which
were run and are green.

Ablation (the pin fails without the fix). manifest: toRecordManifest(manifest)
was reverted to manifest, in installPackage; the mutation was confirmed on
disk before running (anchor text 1 -> 0, injected text 1, blob hash differing
from HEAD), the restore leg was git checkout HEAD -- <abs path> under an
EXIT/INT/TERM trap, and restoration was proved by an empty git diff HEAD plus
git hash-object equality with the HEAD blob. No rebuild was needed: the pin
imports ./registry from source, not through dist. Result:

Tests 7 failed | 2 passed (9) ← ablated
Tests 9 passed (9) ← restored
AssertionError: expected [Function] to not throw an error but
'TypeError: Converting circular struct…' was thrown

Declared narrowing — verification ran UNLOCKED.scripts/pm/os-verify-lock.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not ship it), so the commands were run directly, without the lock — a declared
narrowing, not a silent one. No serialization guarantee held for these runs, nor
for any sibling agent in this container while they ran.

Declared narrowing — repo-wide scans left to CI.pnpm lint and the rest of
the 62-family farm were not run locally; CI runs the farm exactly once on this
PR. What ran locally is the targeted set and the 60 gate families
named above.

Generated by Claude Code

hotlongand others added 4 commits September 2, 2026 10:39
…e registry
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the door projections
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…packages-domain projection
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
hotlongand others added 2 commits September 3, 2026 00:19
…ckages-serializable-manifest
# Conflicts:
#	packages/runtime/src/domains/packages.ts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/rest, @objectstack/runtime, touching 25 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 37 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 50d9fd8153b16a9dd058730a4e02f89ac304578d — the merge of head 6961d6ec9bc74651928d56022bc909a7f7cc1a81 into base 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 50d9fd8153b16a9dd058730a4e02f89ac304578d && git checkout 50d9fd8153b16a9dd058730a4e02f89ac304578d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 6961d6ec9bc74651928d56022bc909a7f7cc1a81 && git checkout -B drift-repro 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 && git merge --no-ff 6961d6ec9bc74651928d56022bc909a7f7cc1a81
node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@hotlong
hotlong marked this pull request as ready for review September 2, 2026 16:38
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
os-zhuang pushed a commit that referenced this pull request Sep 2, 2026
…d land it on sendError
Re-homed under the domain seat's serial fence: `packages/rest/src/package-routes.ts`
is held by the open PR #14499, so this no longer edits that file. The REST
direct-mount doors are covered from the producer side instead.
`sendError` (`@objectstack/types`) is the single writer for every
nested-envelope error in the repo, and every catch in the package registrar
ends there — so wiring the rule at that one exit covers those doors with no
per-door call, and covers any door added later by construction. That also puts
the helper in the same package as `resolveThrownHttpError`, on the same
argument: a rule two doors must agree on cannot live inside one of them.
`@objectstack/observability` and `packages/rest` are back to origin/main
byte-for-byte, as is the system-context census page (its line rot was caused
by the package-routes import this drops).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33663028874 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@hotlong

Copy link
Copy Markdown
ContributorAuthor

Queue-eject triage (PM session session_62849a16-0144-4728-8942-9f60bb3a73f1), read against the signature ledger rather than judged on the spot:

Per the four-branch rule: known flaky ⇒ re-queue once, as-is, no new commit; if it ejects again on the same signature it stays out until #14679 is resolved (no reflexive re-arm). Re-arming now.

Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8ce628aSep 2, 2026
36 checks passed
@hotlong
hotlong deleted the claude/issue-14309-packages-serializable-manifest branch September 2, 2026 20:21
os-sales pushed a commit that referenced this pull request Sep 2, 2026
…stem-write-sharing-materialization
Resolves the single CONFLICT (content) in
content/docs/permissions/system-context.mdx (the os-regen driver flags
this file as MIXED — generated anchors plus hand-written prose — so it
text-merges instead of taking one side whole).
Row 37 (share-link policy `enabled` check, isSystem) diverged: this
branch's copy already carried the #14033 prose ("resolution is not
bypassed since #14033 ... a link minted this way does not resolve
until the block is enabled"), added when 13bf05d landed on both
branches. main's copy had reverted to the pre-#14033 wording via an
unrelated commit (8ce628a, "store a serializable manifest projection
in the package registry (#14499)") whose own docs-regen step appears
to have run against a stale pre-#14033 checkout of this file.
Kept this branch's row 37 text: packages/plugins/plugin-sharing/src/
share-link-service.ts:652-697 (the `[#14033]` docblock and the
`if (!policy.enabled)` redemption-time check it documents, citing the
maintainer's 2026-09-01 ruling and this exact ledger row by number)
confirms it is what the current source does, not the reverted main
copy. Row 36 differed only in its generated anchor line number
(1088 vs 1077) — anchor numbers are recomputed by
`pnpm gen:system-context-census` below, so either side was equivalent;
kept this branch's.
Anchors re-derived on the merged tree with
`pnpm gen:system-context-census` in a follow-up commit, per the
os-regen driver's documented recovery for this file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry - #14499

Merged
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest
Sep 2, 2026
Merged

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry#14499
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest

Conversation

@hotlong

@hotlonghotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14309

SchemaRegistry.installPackage now stores a serializable projection of the
manifest instead of the caller's live defineStack() object, and the /packages
read doors project a registry entry onto its declared record fields instead of
spreading it whole.

The measured defect

Reproduced on this branch's base, showcase on a random high port
(objectstack dev --seed-admin --fresh -p 39117), signed in as the seeded admin:

routebeforeafter
GET /api/v1/packages500INTERNAL_ERROR200
GET /api/v1/packages/com.example.showcase500INTERNAL_ERROR200
GET /api/v1/meta/package/com.example.showcase500200
GET /api/v1/meta/package/com.objectstack.setup200200

Error body of the first two:

Converting circular structure to JSON
--> starting at object with constructor '_ObjectQL'
| property 'actionActivation' -> object with constructor 'ActionActivationProjection'
| property 'store' -> object with constructor 'ObjectStoreActionActivationStore'
--- property 'engine' closes the circle

Which key actually reached the engine

Rather than reason about it, the running server was instrumented at
SchemaRegistry.getPackage (temporary, env-gated, reverted — the file is
byte-identical to its committed state, git hash-object verified) to classify
every top-level manifest key of every installed package by whether it
survives JSON.stringify. Across the 26 packages a showcase boot installs,
exactly one key on one package failed:

[OS_DEBUG_PKG_SERIALIZE] id=com.example.showcase keys=[…40 keys…]
UNSERIALIZABLE=["plugins :: Array<ConnectorOpenApiPlugin|ConnectorMcpPlugin|ConnectorRestPlugin|
ConnectorSlackPlugin|MarketplaceProxyPlugin|MarketplaceInstallLocalPlugin|CloudConnectionPlugin|
RuntimeConfigPlugin> :: Converting circular structure to JSON"]

And only after plugin init. The same manifest was probed 40 times during
boot with UNSERIALIZABLE=[] and 381 times afterwards with the plugins
failure — the transition is a single clean split in the log. That rules out a
whole family of "fixes": an install-time JSON.stringify probe, or any check
that runs while the record is being written, would have called this record
healthy. The projection therefore drops by shape, not by trial
serialisation, and does not depend on when it is asked.

The repair, at the producer

installPackage stored the argument verbatim as pkg.manifest. The registry
item is a record, not the runtime, so it now holds only the declarative
half. The rule is structural rather than a plugins denylist, because the fault
is not "the key is called plugins" — it is "a live object reached the record":

  • primitives, plain objects, arrays and Date are data and survive;
  • functions / symbols / bigints are dropped (a function was already invisible to
    JSON.stringify; a bigint would have thrown);
  • class instances, Map, Set and other exotic objects are dropped;
  • a reference cycle among plain data is dropped at the back-edge, so a
    self-referencing manifest degrades to a missing field instead of throwing.

Nothing downstream loses a member it was using: the kernel keeps the live object
in ObjectQL.manifests, and the only reader of manifest.plugins[]
registerApp's nested-plugin seam — reads its own parameter, never the
record (grepped, and pinned). The caller's manifest is copied, never stripped in
place, which the pins assert both ways.

⛔ Not a consumer-side tolerance (AGENTS.md Prime Directive #12): no reader is
taught to survive an unserializable record — the record is never unserializable.

Premise correction: which handler answered the 500

The card attributes the list/detail 500 to packages/rest/src/package-routes.ts
{ ...item, source: 'registry' }. Measured, that is not the handler that
answered in the showcase composition — packages/runtime/src/domains/packages.ts
did. Two independent probes agree:

  • the 404 wording. GET /api/v1/packages/no.such.package returned
    Package 'no.such.package' not found (the runtime domain's spelling); the
    REST twin says Package "no.such.package" was not found.
  • the response shape. GET /api/v1/packages/com.objectstack.setup returned the
    installed-package record directly under data (deps.success(pkg)); the
    REST twin wraps it as data.package and stamps source.

So the route-level defence is applied at both twins, and
packages/runtime/src/domains/packages.ts is a declared addition to this card's
file surface (announced on the issue before the work landed). Applying it only
where the card pointed would have shipped a defence with a hole exactly where the
defect was observed.

The doors now project onto the declared InstalledPackageSchema fields, so an
undeclared member appearing on the registry item — a live handle, a
back-reference — degrades to a field the response never mentions, instead of one
bad member on one package failing the whole list for every caller. The REST
merge's database half is deliberately left spreading: its shape belongs to
PackageService, and narrowing it would be a contract decision this card does
not carry. On the REST side _diagnostics is carried explicitly, because
decorateMetadataItem grafts it onto every item leaving getMetaItems and it is
measured to be the only thing that decoration adds for type: 'package'.

No response field is added or renamed; responses that already served fine are
byte-identical.

Verification

git rev-parse --short HEAD at the time of the runs below: 5b8b76e06.

Live routes, same showcase boot, after the fix — the table above; GET /packages
returns all 26 packages, the showcase manifest keeps its 40 declarative keys
(24 objects, 29 flows, 1 app), plugins serves as [] and the function-valued
onEnable is gone. JSON.stringify of the whole listing succeeds.

Package suites (all green, on the commit named above):

@objectstack/objectql Test Files 255 passed (255) Tests 4400 passed (4400)
@objectstack/rest Test Files 168 passed (168) Tests 2808 passed (2808)
@objectstack/runtime Test Files 208 passed (208) Tests 3066 passed (3066)

New pins — 20 cases over three files:
packages/objectql/src/registry-package-manifest-serializable.test.ts (9),
packages/runtime/src/domains/packages-serializable-response.test.ts (5),
packages/rest/src/package-registry-item-projection.test.ts (6).

Typecheck: @objectstack/objectql, @objectstack/runtime, @objectstack/rest
all exit 0. ⚠️ Measured rather than assumed: each package's tsc --noEmit
program excludes **/*.test.ts, so --listFiles reports 0 hits for all three
new test files there. The test layer is covered separately — objectql and rest
run check:test-typecheck over tsconfig.test.json, whose --listFilesdoes
name the new files (1 hit each, 0 errors attributed to them). packages/runtime
has no test-typecheck wiring at all, so its new test file is covered by no gate;
it was type-checked standalone (tsc --noEmit --ignoreConfig --strict, exit 0, 0
errors) and that gap is reported to the PM rather than papered over.

Gate families — 62 derived by node scripts/pm/dispatch-gates.mjs --commands
(no paths passed; it takes its own change set from the merge base). 60 ran green
locally. The remaining two exited 3, which each script's own verdict text
declares is NOT MEASURED and explicitly "not a red": check-test-completeness
needs a saved turbo run test log it was not handed, and
scripts/pm/check-half-states.mjs needs a per-card comment fetch that never
happened.

check:system-context-census was the one family this diff genuinely moved: the
+50-line helper in packages/runtime/src/domains/packages.ts shifted three
elevation-read anchors (:95 -> :145, :128 -> :178, :246 -> :296). Repaired
mechanically with the gate's own --fix, which re-anchored
content/docs/permissions/system-context.mdx; the gate then reports
OK — 109 elevation read sites in 20 packages across 45 files, all anchored.
That docs edit pulled 24 further doc families into the derivation, all of which
were run and are green.

Ablation (the pin fails without the fix). manifest: toRecordManifest(manifest)
was reverted to manifest, in installPackage; the mutation was confirmed on
disk before running (anchor text 1 -> 0, injected text 1, blob hash differing
from HEAD), the restore leg was git checkout HEAD -- <abs path> under an
EXIT/INT/TERM trap, and restoration was proved by an empty git diff HEAD plus
git hash-object equality with the HEAD blob. No rebuild was needed: the pin
imports ./registry from source, not through dist. Result:

Tests 7 failed | 2 passed (9) ← ablated
Tests 9 passed (9) ← restored
AssertionError: expected [Function] to not throw an error but
'TypeError: Converting circular struct…' was thrown

Declared narrowing — verification ran UNLOCKED.scripts/pm/os-verify-lock.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not ship it), so the commands were run directly, without the lock — a declared
narrowing, not a silent one. No serialization guarantee held for these runs, nor
for any sibling agent in this container while they ran.

Declared narrowing — repo-wide scans left to CI.pnpm lint and the rest of
the 62-family farm were not run locally; CI runs the farm exactly once on this
PR. What ran locally is the targeted set and the 60 gate families
named above.

Generated by Claude Code

hotlongand others added 4 commits September 2, 2026 10:39
…e registry
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the door projections
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…packages-domain projection
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
hotlongand others added 2 commits September 3, 2026 00:19
…ckages-serializable-manifest
# Conflicts:
#	packages/runtime/src/domains/packages.ts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/rest, @objectstack/runtime, touching 25 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 37 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 50d9fd8153b16a9dd058730a4e02f89ac304578d — the merge of head 6961d6ec9bc74651928d56022bc909a7f7cc1a81 into base 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 50d9fd8153b16a9dd058730a4e02f89ac304578d && git checkout 50d9fd8153b16a9dd058730a4e02f89ac304578d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 6961d6ec9bc74651928d56022bc909a7f7cc1a81 && git checkout -B drift-repro 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 && git merge --no-ff 6961d6ec9bc74651928d56022bc909a7f7cc1a81
node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@hotlong
hotlong marked this pull request as ready for review September 2, 2026 16:38
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
os-zhuang pushed a commit that referenced this pull request Sep 2, 2026
…d land it on sendError
Re-homed under the domain seat's serial fence: `packages/rest/src/package-routes.ts`
is held by the open PR #14499, so this no longer edits that file. The REST
direct-mount doors are covered from the producer side instead.
`sendError` (`@objectstack/types`) is the single writer for every
nested-envelope error in the repo, and every catch in the package registrar
ends there — so wiring the rule at that one exit covers those doors with no
per-door call, and covers any door added later by construction. That also puts
the helper in the same package as `resolveThrownHttpError`, on the same
argument: a rule two doors must agree on cannot live inside one of them.
`@objectstack/observability` and `packages/rest` are back to origin/main
byte-for-byte, as is the system-context census page (its line rot was caused
by the package-routes import this drops).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33663028874 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@hotlong

Copy link
Copy Markdown
ContributorAuthor

Queue-eject triage (PM session session_62849a16-0144-4728-8942-9f60bb3a73f1), read against the signature ledger rather than judged on the spot:

Per the four-branch rule: known flaky ⇒ re-queue once, as-is, no new commit; if it ejects again on the same signature it stays out until #14679 is resolved (no reflexive re-arm). Re-arming now.

Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8ce628aSep 2, 2026
36 checks passed
@hotlong
hotlong deleted the claude/issue-14309-packages-serializable-manifest branch September 2, 2026 20:21
os-sales pushed a commit that referenced this pull request Sep 2, 2026
…stem-write-sharing-materialization
Resolves the single CONFLICT (content) in
content/docs/permissions/system-context.mdx (the os-regen driver flags
this file as MIXED — generated anchors plus hand-written prose — so it
text-merges instead of taking one side whole).
Row 37 (share-link policy `enabled` check, isSystem) diverged: this
branch's copy already carried the #14033 prose ("resolution is not
bypassed since #14033 ... a link minted this way does not resolve
until the block is enabled"), added when 13bf05d landed on both
branches. main's copy had reverted to the pre-#14033 wording via an
unrelated commit (8ce628a, "store a serializable manifest projection
in the package registry (#14499)") whose own docs-regen step appears
to have run against a stale pre-#14033 checkout of this file.
Kept this branch's row 37 text: packages/plugins/plugin-sharing/src/
share-link-service.ts:652-697 (the `[#14033]` docblock and the
`if (!policy.enabled)` redemption-time check it documents, citing the
maintainer's 2026-09-01 ruling and this exact ledger row by number)
confirms it is what the current source does, not the reverted main
copy. Row 36 differed only in its generated anchor line number
(1088 vs 1077) — anchor numbers are recomputed by
`pnpm gen:system-context-census` below, so either side was equivalent;
kept this branch's.
Anchors re-derived on the merged tree with
`pnpm gen:system-context-census` in a follow-up commit, per the
os-regen driver's documented recovery for this file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry - #14499

Merged
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest
Sep 2, 2026
Merged

fix(objectql,runtime,rest): store a serializable manifest projection in the package registry#14499
hotlong merged 6 commits into
mainfrom
claude/issue-14309-packages-serializable-manifest

Conversation

@hotlong

@hotlonghotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14309

SchemaRegistry.installPackage now stores a serializable projection of the
manifest instead of the caller's live defineStack() object, and the /packages
read doors project a registry entry onto its declared record fields instead of
spreading it whole.

The measured defect

Reproduced on this branch's base, showcase on a random high port
(objectstack dev --seed-admin --fresh -p 39117), signed in as the seeded admin:

routebeforeafter
GET /api/v1/packages500INTERNAL_ERROR200
GET /api/v1/packages/com.example.showcase500INTERNAL_ERROR200
GET /api/v1/meta/package/com.example.showcase500200
GET /api/v1/meta/package/com.objectstack.setup200200

Error body of the first two:

Converting circular structure to JSON
--> starting at object with constructor '_ObjectQL'
| property 'actionActivation' -> object with constructor 'ActionActivationProjection'
| property 'store' -> object with constructor 'ObjectStoreActionActivationStore'
--- property 'engine' closes the circle

Which key actually reached the engine

Rather than reason about it, the running server was instrumented at
SchemaRegistry.getPackage (temporary, env-gated, reverted — the file is
byte-identical to its committed state, git hash-object verified) to classify
every top-level manifest key of every installed package by whether it
survives JSON.stringify. Across the 26 packages a showcase boot installs,
exactly one key on one package failed:

[OS_DEBUG_PKG_SERIALIZE] id=com.example.showcase keys=[…40 keys…]
UNSERIALIZABLE=["plugins :: Array<ConnectorOpenApiPlugin|ConnectorMcpPlugin|ConnectorRestPlugin|
ConnectorSlackPlugin|MarketplaceProxyPlugin|MarketplaceInstallLocalPlugin|CloudConnectionPlugin|
RuntimeConfigPlugin> :: Converting circular structure to JSON"]

And only after plugin init. The same manifest was probed 40 times during
boot with UNSERIALIZABLE=[] and 381 times afterwards with the plugins
failure — the transition is a single clean split in the log. That rules out a
whole family of "fixes": an install-time JSON.stringify probe, or any check
that runs while the record is being written, would have called this record
healthy. The projection therefore drops by shape, not by trial
serialisation, and does not depend on when it is asked.

The repair, at the producer

installPackage stored the argument verbatim as pkg.manifest. The registry
item is a record, not the runtime, so it now holds only the declarative
half. The rule is structural rather than a plugins denylist, because the fault
is not "the key is called plugins" — it is "a live object reached the record":

  • primitives, plain objects, arrays and Date are data and survive;
  • functions / symbols / bigints are dropped (a function was already invisible to
    JSON.stringify; a bigint would have thrown);
  • class instances, Map, Set and other exotic objects are dropped;
  • a reference cycle among plain data is dropped at the back-edge, so a
    self-referencing manifest degrades to a missing field instead of throwing.

Nothing downstream loses a member it was using: the kernel keeps the live object
in ObjectQL.manifests, and the only reader of manifest.plugins[]
registerApp's nested-plugin seam — reads its own parameter, never the
record (grepped, and pinned). The caller's manifest is copied, never stripped in
place, which the pins assert both ways.

⛔ Not a consumer-side tolerance (AGENTS.md Prime Directive #12): no reader is
taught to survive an unserializable record — the record is never unserializable.

Premise correction: which handler answered the 500

The card attributes the list/detail 500 to packages/rest/src/package-routes.ts
{ ...item, source: 'registry' }. Measured, that is not the handler that
answered in the showcase composition — packages/runtime/src/domains/packages.ts
did. Two independent probes agree:

  • the 404 wording. GET /api/v1/packages/no.such.package returned
    Package 'no.such.package' not found (the runtime domain's spelling); the
    REST twin says Package "no.such.package" was not found.
  • the response shape. GET /api/v1/packages/com.objectstack.setup returned the
    installed-package record directly under data (deps.success(pkg)); the
    REST twin wraps it as data.package and stamps source.

So the route-level defence is applied at both twins, and
packages/runtime/src/domains/packages.ts is a declared addition to this card's
file surface (announced on the issue before the work landed). Applying it only
where the card pointed would have shipped a defence with a hole exactly where the
defect was observed.

The doors now project onto the declared InstalledPackageSchema fields, so an
undeclared member appearing on the registry item — a live handle, a
back-reference — degrades to a field the response never mentions, instead of one
bad member on one package failing the whole list for every caller. The REST
merge's database half is deliberately left spreading: its shape belongs to
PackageService, and narrowing it would be a contract decision this card does
not carry. On the REST side _diagnostics is carried explicitly, because
decorateMetadataItem grafts it onto every item leaving getMetaItems and it is
measured to be the only thing that decoration adds for type: 'package'.

No response field is added or renamed; responses that already served fine are
byte-identical.

Verification

git rev-parse --short HEAD at the time of the runs below: 5b8b76e06.

Live routes, same showcase boot, after the fix — the table above; GET /packages
returns all 26 packages, the showcase manifest keeps its 40 declarative keys
(24 objects, 29 flows, 1 app), plugins serves as [] and the function-valued
onEnable is gone. JSON.stringify of the whole listing succeeds.

Package suites (all green, on the commit named above):

@objectstack/objectql Test Files 255 passed (255) Tests 4400 passed (4400)
@objectstack/rest Test Files 168 passed (168) Tests 2808 passed (2808)
@objectstack/runtime Test Files 208 passed (208) Tests 3066 passed (3066)

New pins — 20 cases over three files:
packages/objectql/src/registry-package-manifest-serializable.test.ts (9),
packages/runtime/src/domains/packages-serializable-response.test.ts (5),
packages/rest/src/package-registry-item-projection.test.ts (6).

Typecheck: @objectstack/objectql, @objectstack/runtime, @objectstack/rest
all exit 0. ⚠️ Measured rather than assumed: each package's tsc --noEmit
program excludes **/*.test.ts, so --listFiles reports 0 hits for all three
new test files there. The test layer is covered separately — objectql and rest
run check:test-typecheck over tsconfig.test.json, whose --listFilesdoes
name the new files (1 hit each, 0 errors attributed to them). packages/runtime
has no test-typecheck wiring at all, so its new test file is covered by no gate;
it was type-checked standalone (tsc --noEmit --ignoreConfig --strict, exit 0, 0
errors) and that gap is reported to the PM rather than papered over.

Gate families — 62 derived by node scripts/pm/dispatch-gates.mjs --commands
(no paths passed; it takes its own change set from the merge base). 60 ran green
locally. The remaining two exited 3, which each script's own verdict text
declares is NOT MEASURED and explicitly "not a red": check-test-completeness
needs a saved turbo run test log it was not handed, and
scripts/pm/check-half-states.mjs needs a per-card comment fetch that never
happened.

check:system-context-census was the one family this diff genuinely moved: the
+50-line helper in packages/runtime/src/domains/packages.ts shifted three
elevation-read anchors (:95 -> :145, :128 -> :178, :246 -> :296). Repaired
mechanically with the gate's own --fix, which re-anchored
content/docs/permissions/system-context.mdx; the gate then reports
OK — 109 elevation read sites in 20 packages across 45 files, all anchored.
That docs edit pulled 24 further doc families into the derivation, all of which
were run and are green.

Ablation (the pin fails without the fix). manifest: toRecordManifest(manifest)
was reverted to manifest, in installPackage; the mutation was confirmed on
disk before running (anchor text 1 -> 0, injected text 1, blob hash differing
from HEAD), the restore leg was git checkout HEAD -- <abs path> under an
EXIT/INT/TERM trap, and restoration was proved by an empty git diff HEAD plus
git hash-object equality with the HEAD blob. No rebuild was needed: the pin
imports ./registry from source, not through dist. Result:

Tests 7 failed | 2 passed (9) ← ablated
Tests 9 passed (9) ← restored
AssertionError: expected [Function] to not throw an error but
'TypeError: Converting circular struct…' was thrown

Declared narrowing — verification ran UNLOCKED.scripts/pm/os-verify-lock.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not ship it), so the commands were run directly, without the lock — a declared
narrowing, not a silent one. No serialization guarantee held for these runs, nor
for any sibling agent in this container while they ran.

Declared narrowing — repo-wide scans left to CI.pnpm lint and the rest of
the 62-family farm were not run locally; CI runs the farm exactly once on this
PR. What ran locally is the targeted set and the 60 gate families
named above.

Generated by Claude Code

hotlongand others added 4 commits September 2, 2026 10:39
…e registry
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the door projections
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…packages-domain projection
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
hotlongand others added 2 commits September 3, 2026 00:19
…ckages-serializable-manifest
# Conflicts:
#	packages/runtime/src/domains/packages.ts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/rest, @objectstack/runtime, touching 25 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 37 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 50d9fd8153b16a9dd058730a4e02f89ac304578d — the merge of head 6961d6ec9bc74651928d56022bc909a7f7cc1a81 into base 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 50d9fd8153b16a9dd058730a4e02f89ac304578d && git checkout 50d9fd8153b16a9dd058730a4e02f89ac304578d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 6961d6ec9bc74651928d56022bc909a7f7cc1a81 && git checkout -B drift-repro 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 && git merge --no-ff 6961d6ec9bc74651928d56022bc909a7f7cc1a81
node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@hotlong
hotlong marked this pull request as ready for review September 2, 2026 16:38
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
os-zhuang pushed a commit that referenced this pull request Sep 2, 2026
…d land it on sendError
Re-homed under the domain seat's serial fence: `packages/rest/src/package-routes.ts`
is held by the open PR #14499, so this no longer edits that file. The REST
direct-mount doors are covered from the producer side instead.
`sendError` (`@objectstack/types`) is the single writer for every
nested-envelope error in the repo, and every catch in the package registrar
ends there — so wiring the rule at that one exit covers those doors with no
per-door call, and covers any door added later by construction. That also puts
the helper in the same package as `resolveThrownHttpError`, on the same
argument: a rule two doors must agree on cannot live inside one of them.
`@objectstack/observability` and `packages/rest` are back to origin/main
byte-for-byte, as is the system-context census page (its line rot was caused
by the package-routes import this drops).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33663028874 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@hotlong

Copy link
Copy Markdown
ContributorAuthor

Queue-eject triage (PM session session_62849a16-0144-4728-8942-9f60bb3a73f1), read against the signature ledger rather than judged on the spot:

Per the four-branch rule: known flaky ⇒ re-queue once, as-is, no new commit; if it ejects again on the same signature it stays out until #14679 is resolved (no reflexive re-arm). Re-arming now.

Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8ce628aSep 2, 2026
36 checks passed
@hotlong
hotlong deleted the claude/issue-14309-packages-serializable-manifest branch September 2, 2026 20:21
os-sales pushed a commit that referenced this pull request Sep 2, 2026
…stem-write-sharing-materialization
Resolves the single CONFLICT (content) in
content/docs/permissions/system-context.mdx (the os-regen driver flags
this file as MIXED — generated anchors plus hand-written prose — so it
text-merges instead of taking one side whole).
Row 37 (share-link policy `enabled` check, isSystem) diverged: this
branch's copy already carried the #14033 prose ("resolution is not
bypassed since #14033 ... a link minted this way does not resolve
until the block is enabled"), added when 13bf05d landed on both
branches. main's copy had reverted to the pre-#14033 wording via an
unrelated commit (8ce628a, "store a serializable manifest projection
in the package registry (#14499)") whose own docs-regen step appears
to have run against a stale pre-#14033 checkout of this file.
Kept this branch's row 37 text: packages/plugins/plugin-sharing/src/
share-link-service.ts:652-697 (the `[#14033]` docblock and the
`if (!policy.enabled)` redemption-time check it documents, citing the
maintainer's 2026-09-01 ruling and this exact ledger row by number)
confirms it is what the current source does, not the reverted main
copy. Row 36 differed only in its generated anchor line number
(1088 vs 1077) — anchor numbers are recomputed by
`pnpm gen:system-context-census` below, so either side was equivalent;
kept this branch's.
Anchors re-derived on the merged tree with
`pnpm gen:system-context-census` in a follow-up commit, per the
os-regen driver's documented recovery for this file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

1 participant

@hotlong