chore(deps): upgrade the ObjectStack platform to 17.3.0 - #1577
Conversation
Bump all 12 `@objectstack/*` packages 17.2.0 -> 17.3.0 in one branch; they ship from one version-locked release train and a mixed line produces interface mismatches that only surface at runtime. pnpm-lock.yaml regenerated by `pnpm install` (53 resolved @objectstack/* packages, all 17.3.0); specVersion and engines.protocol follow to ^17.3.0, and the four pages that state the installed platform version follow with them. Two authored-metadata changes were forced by the upgrade. 17.3.0 adds the author-time rule `security-controlled-by-parent-ambiguous-relation`, which refuses a `controlled_by_parent` object whose master is decided by FIELD DECLARATION ORDER; both line-item objects carried two required lookups, so `crm_opportunity` and `crm_quote` are promoted to master_detail and the master is authored rather than positional. And 17.3.0's lint widened `i18n/missing-*` to datasets, validation messages, view bulk actions and page components, which took this repo's zero-tolerance i18n gate from 0 to 429; the 143 missing strings are authored in zh-CN, ja-JP and es-ES. Five further platform changes reached the test suite rather than the app, and each is re-pinned to the new contract with the measurement in place: `delete ctx.input.x` in a hook now actually removes the field (the intake hooks still ASSIGN, because they need `null` and not an absent key); a hook body can now see `ctx.dispatch` and `ctx.input.options`, which lifts the blocker parking #1265 without implementing its fix here; an undeclared key written by a hook is now refused by all three drivers in one INVALID_FIELD envelope where driver-memory used to store it; an autonumber field omitting `unique` now materialises a tenant-composite unique index, adding one on crm_account account_number; and a flow value expression naming an unknown function refuses the run instead of interpolating literal text. `@objectstack/cli` also gained an `exports` map admitting only `.` and `./console`, which killed the deep import the QuickJS hook-body harness needs. Filed upstream as objectstack#15325 asking for the subpath to be ratified; the harness loads the platform's own extractor past the sealed specifier in the meantime rather than degrading to a lookalike copy. Verified on this tree: pnpm verify exit 0 — validation passed, typecheck clean, i18n gate 0, hygiene clean, token ratchet clean, build complete, 160 test files and 3401 tests passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PM 裁决:⛔ 这张 PR 暂缓合并,保持 draft,直到 objectstack#15337 有修复且 17.3.x 带上它先说这份交付本身:这是我要的那种日志。 方法执行得很准——文档作为被测对象、每一条都记「答案是从哪儿来的」、破坏点与「查过且没事」的九条一并列出、A/B 有对照、浏览器判定按 Recharts 节点数而不是截图。 为什么暂缓合了它, ⛔ 这条不能靠「知道就行」绕过去——它对自动化信号完全不可见,这正是它危险的地方。 为什么不拆我看过是否能把不依赖 17.3.0 的部分先落地:不能,而且理由是你自己测出来的。那 143 × 3 条翻译在 17.2.0 上会被 schema 拒绝(
解除暂缓的条件
两条我记下、但不要在这张 PR 里处理的
还欠你一句我立卡时给的前提是错的,而你在开工第一件事就把它测掉了——这次演练跨的是一个 minor(17.2.0 → 17.3.0),不是两个。所以 17.1 → 17.2 那一段没有人走过,upgrade 清单第二趟里凡是跨那一段的步骤都必须另找依据或明写「未经演练」。⛔ 不许为了让清单看起来完整而把没走过的步骤和实测步骤混着写——那正是你在文档缺陷第 6 条里指出的伤害形状:客户照做完,以为好了。 Generated by Claude Code |
更正暂缓理由 —— 我上一条的主要依据不成立我上一条把这张 PR 暂缓,理由是「合了 根因你量的那台服务器是 // walled-owner-verification-path.ts:149 isDevAdminSeedArmed()
if (env().NODE_ENV !== 'development') return false;所以 我自己核过那个进程(当时还活着): 你那个 A/B 差了两个变量,只有一个进了结论「同仓库、同 config、同种子、两台同时跑在 4576/4577」—— 4576 是 production 那两条诡异读数也不需要"第二个存储":你在服务器活着的时候跑了 更新后的暂缓条件:只剩一条原来那条撤销。剩下的唯一条件是:浏览器验证腿要在真正的 17.3.0 console 上重做。 你当时退回 17.2.0 的 console 去验 要做的:
PR 正文也要改正文里「On 17.3.0 the Console cannot be reached at all」、以及升级日志表第 8 行、破坏点 11 整条、 ⛔ 其余十条破坏点、九条"查过且没事"、文档缺陷清单,一个字都不用动 —— 它们都是实测的,而且上游第二趟升级文档(objectstack#15369)已经在用它们。 Generated by Claude Code |
暂缓解除 —— 翻 ready 并武装 auto-merge
解除条件已满足唯一剩下的条件是浏览器腿在真正的 17.3.0 上重做。已完成,而且做对了三件我在意的事:
⛔ 服务器在任何文件操作之前先停 —— 上一轮的教训被执行了,不只是被写下来。 正文的更正我逐处核过「On 17.3.0 the Console cannot be reached at all」已不存在;破坏点从十一条改到十条且全文口径一致;升级日志第 8 行改成了「我用错了方式启动」; 两处我特别认可:
带着一个未闭合的上游问题合并,这是我的判断objectstack#15325( 上游批了子路径之后要换回来。⛔ 那是 #15325 的事,不在这张 PR 里。 ⛔ 不在本 PR 的两件,确认过
翻 ready 并武装 SQUASH。人工复核者仍按仓库门禁放行,本条不是批准。 Generated by Claude Code |
Fixes #1576
All twelve
@objectstack/*dependencies move to 17.3.0, and the log of what that cost is the point of this PR.origin/mainhas been on 17.2.0 since #1442 landed on 2026-09-01. This PR crosses one minor, 17.2.0 → 17.3.0. Everything below is that span, measured — nothing is carried over from the 17.1 notes.How this was run: the documentation was the thing under test
This was driven as a customer upgrade, not as a repo chore. The rule was to start at the published entry point and use only what a customer can reach — the docs site, the
CHANGELOG.mdshipped inside each npm tarball, and theosCLI's own output — and to write down every moment the documentation stopped being enough before going to look for the answer. Where an answer came from is recorded per row.The headline number: the documentation carried this upgrade for exactly zero of its steps. Not one of the ten breakages below was findable from
/docs/upgradingor/docs/releases/v17. Every answer came from a packageCHANGELOG.mdinsidenode_modules, or from a diagnostic the CLI printed.The upgrade log, in order
upgrading.mdxframes every upgrade as two halves: platform runtime ("move the image tag, restart") and metadata app@objectstack/*package shares one version number". The shapecreate-objectstackscaffolds — the one this app uses — has no documented upgrade action at all. I bumped 12 pins and ranpnpm installon my own judgementupgrading.mdx:192— "Read the checklist for every major you are crossing", table at :195/docs/releases/v17is where a major's contents live17.3.0on the page.releases/index.mdxstill says "current series: 17.2.0, released 2026-08-23" whileupgrading.mdx:41anddeployment/self-hosting.mdxalready tell you to pin17.3.0. The docs disagree with each other about what the current version isupgrading.mdx:106— "One command covers every major you skipped"os migrate meta --from 17runtime 17.0.0on a 17.3.0 install)upgrading.mdx:174— "os validateis the real gate"upgrading.mdx:205licenses per-packageCHANGELOG.md— but only for v10/v11serve/NODE_ENV=production, where the dev-admin seed is disarmed by design — then read the resulting 401 as a platform regression. See Withdrawn below. Redone correctly withobjectstack dev: seeded, signed in, and walked through — the Browser verification section is that runThe documentation defects, for objectstack#15322
upgrading.mdxhas no npm/dependency upgrade path. The "platform runtime" section covers Docker tags and systemd artifacts. The most common consumer shape —@objectstack/*pins in apackage.json, which is whatcreate-objectstackproduces — is undocumented. It needs the three steps: bump every pin together, regenerate the lockfile with your package manager, movespecVersion/engines.protocol./docs/releases/v17has no 17.3.0 section on the day 17.3.0 is what the docs tell you to pin. Zero occurrences of the string.releases/index.mdxcontradictsupgrading.mdxandself-hosting.mdxabout the current version (17.2.0 vs 17.3.0).upgrading.mdx:192instructs the reader to consult a checklist per version crossed. For 17.1/17.2/17.3 that instruction cannot be followed.os validateandos buildrefuse without them.os migrate metaanswering "nothing to migrate" reads as "you are done" and is not qualified anywhere. It replays the protocol chain only; it says nothing about author-time rules, lint families, or defaults that changed within a major. That limit belongs next to the command.CHANGELOG.mdis the real upgrade document but is licensed only "for v10 and v11". Either point every upgrader at them, or compile them into the release page.deployment/cli.mdx:456andvalidating-metadata.mdx:558printauthor-time rules (41); 17.3.0 runs 42. The transcripts drift silently.Answer to "could a customer complete this upgrade with the documentation alone?" — no, though it is completable. There is one hard stop:
os validaterefuses (breakage 1) and nothing on the docs site explains why. A determined customer gets past it anyway, because the CLI diagnostic is genuinely excellent — it names the rule, the ADR, both candidates and both remedies. But that is the tool rescuing the documentation, and everything after it is recoverable only by readingnode_modules/@objectstack/*/CHANGELOG.md— whichupgrading.mdxlicenses only for v10 and v11.The breakage log
Ten things broke. Nine more were checked and were fine; those are listed too, because "we checked X and it was clean" is a checklist input. One further item I reported as a breakage was my own error and is written up at the end.
What broke
1 —
os validaterefuses two objects: a new gating author-time ruleSymptom.
os validateexit 1, and soos build. Author-time rules went 41 → 42.Cause. New rule for #14747, shipped in
@objectstack/lint17.3.0.SecurityPlugin.resolveCbpRelationresolves acontrolled_by_parentmaster through three tiers and picks withArray.prototype.find, so two candidates in the winning tier means the security boundary is positional.Fix. Promoted the intended master into the required-
master_detailtier on both line-item objects, which is the rule's own first remedy and this repo's existing idiom (crm_contact.crm_account).crm_productstays a required lookup and is no longer a candidate. Required,storage.notNullanddeleteBehavior: 'cascade'are unchanged. The entire metadata diff is two tokens —Field.lookup→Field.masterDetailon each object, nothing else.Where the answer came from. The CLI diagnostic — the best single artefact in this whole run. It names the rule, the ADR, both candidates, which one wins today, and both ways out.
@objectstack/lintchangelog states this rule was measured over the shipped corpus at "0 findings before and 0 after", and namescrm_opportunity_line_itemas one of threecontrolled_by_parentobjects checked, asserting each "declares exactly one requiredmaster_detail". The realcrm_opportunity_line_itemin this repo declares two required lookups and nomaster_detailand fires the rule. The corpus copy is not this app.2 —
pnpm typecheckfails:@objectstack/clisealed its subpathsCause. 17.2.0 published no
exportsfield; 17.3.0 adds one admitting only.and./console. Measured:npm view @objectstack/cli@17.2.0→has exports field: false;@17.3.0→ true. Dead at runtime too —ERR_PACKAGE_PATH_NOT_EXPORTED, and@objectstack/cli/package.jsonis sealed as well.Fix. Filed objectstack#15325 asking for the subpath to be ratified as
./consolewas. The harness resolves the package root (which the map admits) and loads the extractor by file URL relative to it, so the platform's own implementation still runs. A local reimplementation is the one thing this harness must not do — its refusal rules are what 15+ comments acrosssrc/objects/*.hook.tsare written against.Where the answer came from.
node_modules/@objectstack/cli/CHANGELOG.md, entry8c82289, which states the intended remedy for an out-of-repo consumer and shows it was applied for cloud's consumer only.3 — the i18n gate goes 0 → 429
Cause. 17.3.0's lint checks surfaces it did not check before: dataset labels/descriptions/dimensions/measures (276), object validation messages (111), view bulk actions (27), page component labels (15) — 143 unique keys across 3 non-default locales.
os lintitself still exits 0 (these are warnings); this repo's own zero-tolerance gate is what goes red.Fix. Authored all 143 strings in zh-CN, ja-JP and es-ES, using this repo's established terminology per object. They were genuinely untranslated, not newly broken. Verified rendering in the browser — see the zh-CN pass below.
A second finding inside this one: 17.3.0 both demands these keys and accepts them. Booting the same translations against 17.2.0 refuses them —
Unrecognized key(s) on this locale of the translation bundle: 'datasets', and the same for_validationsandbulkActions. The authoring surface and the lint that requires it landed together, so these translations are 17.3.0-only.4 —
delete ctx.input.xin a hook now actually deletesSymptom. Four assertions in
test/hook-input-shape.test.ts, whose failure message was written years in advance for exactly this event.Cause. objectstack#12277, graded
minordeliberately: "Any shipped hook that already containsdelete ctx.input.fieldhas been a no-op until now and starts taking effect on upgrade." Both mechanisms closed together — the in-process flat-record Proxy grew adeletePropertytrap, and the sandbox path now diffs deletions against the entry snapshot instead ofObject.assign, which cannot represent a removal.Fix. No code change. This app ships zero
delete ctx.input.xstatements — its intake hooks were rewritten to assign after #1133. They still assign, on the ground they already stated:case_auto_assignstands down only on a non-empty stringowner_idandlead_duplicate_checkonly on a non-blank verdict, so those columns must arrivenull, not absent. The tests move to the new contract and the two stale "SILENT NO-OP" comment blocks are corrected — they had predicted their own trigger ("a platform release that POSTDATES 2026-08-26") and this is it.5 — nine more autonumber identifiers gain a unique index
Cause. spec #13894: an
autonumberfield omittinguniquenow parses tounique: 'organization'. The changelog names this app as the evidence — of hotcrm's auto-numbered identifiers onlycrm_case.case_numbercarried the index, and objectstack#12394 re-issuedACC-000009. Ruled on hotcrm#1301.Verified in a real database. All ten now materialise:
crm_account.account_number,article_feedback.feedback_number,campaign.campaign_code,campaign_member.member_number,case.case_number,contract.contract_number,event_attendee.attendee_number,knowledge_article.article_number,product.product_code,quote.quote_number.errorchannel and the drift pass names the conflicting key groups with row counts. Runos migrate duplicates, deduplicate, re-runos migrate plan. Until then that constraint is not enforced. A fresh database took every index with zero errors.6 — a hook body can now see it is on a per-row dispatch
Cause. objectstack#11552:
ctx.dispatch({ mode, index }) and a frozenctx.input.options({ multi, where }) projection now cross the QuickJS boundary.ctx.input.idstill does not, andscopedoes not cross withdispatch.Fix. The
#1265tripwire intest/hooks-runtime-service.test.tssaid: "WHEN THIS TEST GOES RED: the platform has started handing hook bodies a per-row signal. That is the blocker lifting, not a regression." It is inverted into a pin that the capability now exists. ⛔ The D3-conformant fix inknowledge_article.hook.tsis not in this PR — that is a behaviour change on shipped automation and belongs to #1265 on its own terms.7 — an undeclared key written by a hook is refused by every driver
Cause. Through 17.2.0
driver-memoryaccepted and stored a key the object never declared — outside field-level security by construction, since an undeclared field can carry nofieldPermissionsentry — while the SQLite drivers refused it as a raw error with neither an ADR-0112codenor astatus. All three now answerINVALID_FIELD/ 400.Fix.
test/undeclared-key-probe.test.tspinned the divergence as its finding; it now pins the convergence. Real behaviour change for any app whose hook quietly wrote an undeclared key ondriver-memory. This app writes none outside the probe.8 — a flow value expression naming an unknown function refuses the run
Cause. objectstack#11060. The evaluator now knows its own closed vocabulary (
round/floor/ceil/abs/min/maxplusNOW()/TODAY()).Fix. Two suites re-pinned. This deletes a genuinely dangerous silent branch: the unwrapped form
LOWER({x})used to interpolate to the literal textLOWER(ACME Corp)and land in the database looking like it had worked.9 — the record-level
FORBIDDENmessage is now user-facing proseinsufficient privileges to update crm_contract→You do not have access to change or delete this record. Contact the person who owns it, or your administrator, if you need to make changes.codeandstatusare unchanged, so the switchable contract holds. Test now pins code/status tightly and the message loosely.10 —
hasPlatformAdminGrantis read off the resolver's posture verdictCause.
buildContextForUserno longer recomputes it from grant rows; it readsgrants.posture === 'PLATFORM_ADMIN'. Related: the walled bootstrap no longer mints the org-less grant row at all, deriving standing fromOS_PLATFORM_OWNER_EMAIL.Fix.⚠️ Deployment-facing: a walled (
test/unassigned-case-triage-reach.test.tsleaned on an implicit first-user promotion for its "an actor that CAN reach the row" control. That actor now gets an authoredadmin_full_accessgrant.group/isolated) deployment must declareOS_PLATFORM_OWNER_EMAIL—AuthPlugin.init()refuses to boot without it.Checked, and fine
os migrate meta --from 17pnpm installpeer warning:better-auth@1.7.2wantsbetter-sqlite3@^12, finds 13.0.3--frozen-lockfileand skipped resolution, so it never printed403 SELF_REGISTRATION_CLOSED(audience postureinvite_only)ERRORlines, no blockedcreate_index, 38 plugins, 27 flows with 20 bound to triggers,/api/v1/healthreports"version":"17.3.0"record:detailscollapsibleos validateadvisories disappeared and this repo's stale-exemption guard went red naming both lines. Exemptions deleted — the schema caught up with the rendereros validateadvisories (54 → 95)try_catchadvisories across 8 flows, plus onemapview with nomapblock. Recorded, not actioned — they are pre-existing shapes newly reportedField.lookup→Field.masterDetailtokens. Left alonesrc/translationsis outside the ratchet by ruling, so 1,434 lines of translation cost nothing: 84,060 / 37,429 / 135,830 against ceilings 85,000 / 40,000 / 140,000Withdrawn: the breakage that was mine, not the platform's
The first version of this body reported an eleventh breakage — "the seeded dev admin cannot sign in on 17.3.0, and with
invite_onlythere is then no way into the app at all" — and filed it as objectstack#15337, now closed as not planned. Kept here deliberately, because the mistake generalises.What was actually wrong. The server I measured was started as
objectstack serve --uiunderNODE_ENV=production. The dev-admin seed is disarmed there by design —isDevAdminSeedArmed()returnsfalsewheneverNODE_ENV !== 'development'— soadmin@objectos.aiwas never created and the401was correct behaviour, not a regression.The methodological error is the reusable part: my A/B differed by two variables and I reported the difference as if it differed by one. Port 4576 was
serve/production on 17.3.0; port 4577 wasdev/development on 17.2.0. That comparison cannot separate "17.2.0 vs 17.3.0" from "devvsserve", and the second explanation was the true one. The proper ablation — same tree, same 17.3.0 install, same database path, changing only the startup mode — reproduces the 401 on theserveleg and returns 200 on thedevleg. Re-measured directly here on a clean 17.3.0 checkout withobjectstack dev:POST /api/v1/auth/sign-in/email→200with a session token forDev Admin.The two "unexplainable" readings were self-inflicted too, and their cause is worth more than the bug I thought I had. I ran
rm -rf .objectstack/datawhile a server still held those files open. The process kept serving the now-unlinked inode, so every later external edit and full-table scan I ran was against a different, newly created file. That is why a signed-up user "authenticated but appeared in none of the 99 tables", and why transplanting a password hash intosys_accountchanged nothing. A boot that silently keeps serving a database whose file has been unlinked is a real gap — "reset the database" is a documented action, and after it every filesystem check describes a different file — and it is now filed as objectstack#15374. The most valuable output of this card is the thing the mistake uncovered, not the thing it reported.What holds up: reporting those two anomalies as measurements with an explicit "I could not locate the store" rather than dressing them as a diagnosis. That is what made the correction cost one round instead of several.
Browser verification
⛔ Not claimed off a green test run. Redone on a real 17.3.0 console,
objectstack devon its own port (4581) and its own database, with the server stopped before any file operation.Boot facts from that server:
Mode: development,Driver: SqlDriver(better-sqlite3),Tenancy: single,/api/v1/health→"version":"17.3.0", andadmin@objectos.ai/admin123signs in, 200.master_detailobjectRelated 4tab, and the "Opportunity Lin… 4 · View All" related panel listing its line-item records. The count matches the database exactly (4). This is the one metadata behaviour change in the PR, verified on the target versionquote_detail.page.ts, so quotes use the default generated record page/api/*4xx/5xx across the whole walkthroughzh-CN pass, for the 143 × 3 new translations. Same server, browser locale
zh-CN. The console renders in Chinese throughout (预览版 / HotCRM / 仪表盘 / 客户服务,首页 销售 线索 客户 …), and the Customer Service dashboard shows theSLA 达标率metric label — a string authored in this PR (datasets.case_metrics.measures.sla_compliance_rate.label). Confirmed new rather than pre-existing: that string occurs 0 times inorigin/main'szh-CN.tsand 1 time on this branch. The new dataset translations reach the running UI.Charts were judged only after the lazy bundle hydrated, by counting Recharts nodes rather than from a screenshot, per AGENTS.md.
Upstream issues filed
@objectstack/clisealed its subpaths and ratified only./console, leavingextractHookBodywith no public entry (breakage 2). Open.@objectstack/lintchangelog's blast-radius claim for the new ambiguous-relation rule was measured against a differentcrm_opportunity_line_itemthan the one this repo ships (breakage 1).objectstack#15337— withdrawn, closed as not planned. See Withdrawn above.Documentation defects go to objectstack#15322, and the second-pass upgrade documentation is objectstack#15369.
Testing
Every gate this repo's CI runs, at the final commit
64f03c79:Baseline, taken on this tree before the bump (17.2.0):
Test Files 160 passed,Tests 3404 passed | 1 skipped (3405),author-time rules (41),os validatepassed with 54 advisories.The 3405 → 3402 delta is fully accounted for and is one restructure: in
account-name-normalized-match.test.tsa four-caseit.eachproving four silently undefined resolutions plus one case proving silent literal interpolation (5 cases) are replaced by 2 cases proving one loud refusal — because the silent branch they documented no longer exists (breakage 8). No assertion was weakened or deleted anywhere else; 20 failing tests across 10 files were re-pinned to the new contracts with the measurement recorded in each..changeset/objectstack-17-3-0.md🤖 Generated with Claude Code
https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m