Repository navigation
Forward conversion never opens on unreleased main: spec still labelled 17.4.0 while main refuses 17.5.0 retirements, so artifacts built by the published 17.4.0 CLI are refused #20390
Description
Activity
objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsPath: devpath · 发布并装进一个环境 | 缺项 (no item boots an artifact built by the last published CLI on a runtime built from
main) | P4Triage: first grade —
bug·priority:p1·domain:engine·area:devpath·pm:queueTriage: lands in
packages/metadata-core/src/artifact-forward-conversion.ts(resolveInstalledSpecVersionand the window decision), with the refusal inpackages/metadata/src/plugin.ts⇒domain:engine(packages/metadata*).Rationale: an artifact built by the published 17.4.0 CLI with a dashboard chart or page
assignedProfilescannot boot on a runtime built frommain. The conversion window never opens, because both sides read17.4.0. This blocks cloud's framework re-cut (objectstack-ai/cloud#2440, objectstack-ai/hotcrm#1964). The maintainer's ruling in that epic is quoted in the body: 「平台修转换 + HotCRM 先删 scale(推荐)」 ⇒ p1.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T06:58Z. ⛔ Not a claim, ⛔ not a dispatch.Duplicate check. Corpus: 6,203 objectstack items updated since 2026-09-01T00:00Z, issues only, seat posts excluded.
forward conversion|applyArtifactForwardConversions|resolveInstalledSpecVersiontogether with a version or window term gives 4 hits, all closed (#18992, #17894, #17885, #16693). None is this window.On the two shapes. The body leaves the choice to the owning lane; one boundary for it:
- Option (a), a prerelease label on
main, changes the package version label. That is release mechanics, and ⛔ seats never run or stage releases (AGENTS.md). If the lane prefers (a), it needs the maintainer's word. - Option (b), comparing the artifact's authored version against each retirement's own version, is lane-local.
Acceptance, as the body states:
- A 17.4.0-built artifact with charts and
assignedProfilesboots onmain, and the conversion notices are logged. - Newly authored sources that use the retired keys are still refused loudly.
- A regression test pins the "unreleased main, artifact at the last release" case.
- Option (a), a prerelease label on
- addedarea:devpathThe road — create, dev, verify, publish/install, connect an agent, iterateThe road — create, dev, verify, publish/install, connect an agent, iteratepriority:p1High: required for production / M2High: required for production / M2and removed
on Sep 28, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsClaim: PM loop round 23
Session:session_01N8TPEsoJxPsdSdNKGnNGEN
Account:os-warren(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20390-forward-conversion-window
Worktree:objectstack-issue-20390
Domain:domain:engine
Seat:domain:engine#1
File surface:packages/metadata-core/src/artifact-forward-conversion.ts: the window decision only (howapplyArtifactForwardConversionsdecides whether an artifact's authored floor predates a retirement the runtime enforces:resolveInstalledSpecVersionand thefloor/runtimeverdict). The body's option (b): compare the artifact's authored version against the retirement's own version, not the package label.- tests in
packages/metadata-coreandpackages/metadata(the artifact door inMetadataPlugin._parseAndRegisterArtifact, test side unless the door itself must change); .changeset/20390-*.md.
Stop on breach and explain in the report. ⛔ Not
packages/spec/package.json's version label, nor any release or prerelease label: option (a) is release mechanics, which a seat never stages (triage 5865033067). ⛔ NotDEFAULT_FLIPS_NOT_REPLAYED_HEREor its docblock: open PR #20344 (#15429, spec lane) edits that list. ⛔ Notpackages/spec(the conversion registry's entries): if option (b) needs a per-entry version the registry does not already carry, stop and report the fork.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5865033067
Serial constraints cleared: at 2026-09-28T07:13Z, a census of the 10 open PRs' file lists finds one onpackages/metadata-core/src/artifact-forward-conversion.ts: PR #20344 (#15429,domain:specseat 4). Its only hunk there isDEFAULT_FLIPS_NOT_REPLAYED_HEREand its docblock, a region this card does not touch, so the two are text-disjoint and the later lander mergesmain. No open PR touchespackages/metadata/src/plugin.ts. No in-flightdomain:engineclaim holdspackages/metadata*.Clause-②: nobecause this pulls the door back to its declared contract; it does not widen one. The module docblock's policy onmain: "a key retired at version V stays a loud refusal for anything authored at ≥ V". An artifact authored at 17.4.0 is authored below a retirement at 17.5.0, so the declared policy replays its conversions. The label comparison is what refuses it.objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsos-dev-report
{ "issue": 20390, "status": "needs_decision", "branch": "claude/issue-20390-forward-conversion-window", "pr": null, "session": "session_01N8TPEsoJxPsdSdNKGnNGEN — the seat session; this run is mode:subagent under it", "premise_still_valid": true, "summary": "The card's defect is real and reproduced on origin/main df3ba164a (spec label 17.4.0): an artifact declaring engines.protocol ^17.4.0 with dashboard chartConfig.type/xAxis/yAxis and page assignedProfiles gets verdict authored-current and 0 notices, and the strict parse refuses it with 4 issues naming the 17.5.0 retirement. The dispatch's premise for route (b) does NOT hold, so I stopped at the premise check as the ruling orders. Nothing the claim may read carries a retirement's own version. MetadataConversion has only id, toMajor, retiredFromLoadPath, surface, summary, apply and fixture. toMajor reads 18 on all 18 retirements registered after 17.4.0, and ALSO on permission-allow-restore-purge-removed (retired at 17.2), so it cannot tell minors apart. The retired-keys ledger is keyed by major (18.ui__DashboardWidgetChartConfig__type.ts). spec-changes.json has no per-release section: none on main by design, and none in the published 17.4.0 tarball either. It also covers only majors up to 17. PROTOCOL_VERSION is '17.0.0', which is major-only. '17.5.0' appears only in tombstone prose, 34 times in packages/spec/src. The per-artifact fallback ('the highest retirement version the running spec enforces') has no machine-readable source either. I wrote no code, opened no PR and added no changeset. open_questions sets out the fork with a recommendation: A, a required per-entry version on the spec registry built by the spec lane, after which this card is a small metadata-core change.", "tests": "No code changed, so no unit tests are owed. The readings below are one-off probes run against the built dist at df3ba164a; nothing was committed. BUILD: os-verify-lock.sh -c 'pnpm --filter @objectstack/spec --filter @objectstack/metadata-core build', which printed 'VERDICT command-exit 0'. PROBE 1, applyArtifactForwardConversions with the installed spec resolving to 17.4.0: ^17.4.0 gives verdict authored-current with 0 notices (THE DEFECT). ^17.0.0 gives converted-forward with 4 notices (page-assigned-profiles-removed x1, dashboard-widget-chart-config-structure-removed x3). ^17.5.0 gives authored-current. No engines gives converted-undeclared. PROBE 2, acceptance-2 door readings on main. defineStack on a newly authored ^17.4.0 source is refused with StackSchemaInvalidError code=STACK_SCHEMA_INVALID status=422 and 4 retired-key issues (the first is at pages.0.assignedProfiles: 'page.assignedProfiles was removed in @objectstack/spec 17.5.0 ...'). normalizeStackInput + ObjectStackDefinitionSchema.safeParse gives success=false with 4 issues. At the artifact door, ^17.4.0 is authored-current and the parse fails with 4. ^17.5.0 is authored-current and the parse fails with 4: a floor at or above V is refused, as the docblock says. ^17 is converted-forward with 4 notices, and the parse succeeds. REGISTRY CENSUS, main's ALL_CONVERSIONS against the published @objectstack/spec tarballs. Main has 108 entries, 91 of them retired. The published 17.4.0 tarball has 90, 73 retired. So 18 entries were registered on main after 17.4.0, all retiredFromLoadPath true and all toMajor 18: cube-sub-day-granularities-removed, cube-join-sql-and-relationship-removed, object-kanban-quick-add-removed, connector-connection-timeout-ms-removed, view-page-mount-removed, list-view-sort-string-clause-to-array, page-assigned-profiles-removed, chart-config-aria-removed, dashboard-widget-chart-config-structure-removed, translation-per-app-settings-removed, object-tenancy-organization-field-removed, page-component-filter-record-to-rule-array, view-item-owner-hidden-removed, report-joined-chart-removed, view-overlay-owner-hidden-removed, form-layout-inline-grid-to-vertical, currency-config-precision-removed, permission-rls-tags-removed. For main's 91 retired entries, the first published tarball that carries each one as retired (17.0.0, 17.1.0, 17.2.0, 17.3.0, 17.4.0): 17.0.0=50, 17.1.0=5, 17.2.0=2, 17.3.0=8, 17.4.0=8, unreleased=18. So a backfill of a per-entry version can be measured mechanically. HYPOTHESES. H1 holds: version 17.4.0 at df3ba164a. H2 holds (probe 1). H3 holds, and it decides the premise (summary). H4: see open_questions[1]. Today's implementation honours the sentence 'a key retired at V stays a loud refusal for anything authored at or above V' only per artifact, using the runtime label as a single stand-in for every V. H5: the authoring funnel cannot be touched by any door-side fix, because normalizeStackInput calls applyConversions without includeRetired and apply.ts skips retired entries in that case (the defineStack refusal is in probe 2). A floor-at-or-above-V artifact is refused at the door (^17.5.0 in probe 2). GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack printed 'this branch changes nothing against origin/main (merge base df3ba164a) — nothing to derive' and exited 2. The gate set is empty.", "mcp_calls": "0", "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/20390/comments (this report), via scripts/pm/post-stamped.mjs on the fleet relay. The only other writes were a git push of the empty branch, which is not REST. The reads were REST GETs of the card, its comments and the npm registry.", "gates": "none owed: the diff is empty (dispatch-gates at merge base df3ba164a: nothing to derive)", "line_budget": "n/a — no diff", "files_changed": [], "deviations": [ "Stopped at the ruling premise, as the ruling orders, so there is no PR, no label-write and no changeset. Because the premise failed, the deliverable is this report and not a draft PR with Fixes #20390.", "The remote branch claude/issue-20390-forward-conversion-window was pushed empty (it points at origin/main df3ba164a) as the claim marker and write probe, and is left in place for the next dispatch.", "Scratch probes ran against the published @objectstack/spec tarballs fetched from registry.npmjs.org into the scratchpad. Nothing was installed into the repo." ], "open_questions": [ { "question": "Route (b) needs each retirement's own version, and the conversion registry does not carry one. packages/spec is fenced for this claim. Which fork carries the fix?", "options": [ "A (spec lane first, then this card). Add a per-entry retirement version to MetadataConversion, for example retiredAfter: 'x.y.z'. Its meaning: the last @objectstack/spec version whose authoring surface still accepted the old shape. That is the package label at the moment the retirement lands, so it is a fact when the entry is written, never a guess at the next release number. Make it REQUIRED at the type level whenever retiredFromLoadPath is true. Backfill the 73 already-published retired entries from the published tarballs; the census in tests is mechanical. Stamp the 18 unreleased entries 17.4.0. This card then replays entry E when the floor is below the runtime label OR the floor is at or below E.retiredAfter, and DEFAULT_FLIPS_NOT_REPLAYED_HERE is still read first. Business: it unblocks the measured hotcrm#1964 / cloud#2440 re-cut (63 issues; 44 conversions at a forced 17.5.0). Long term: the registry stays the single authority, and the fact the window needs lives on the entry it describes. Any label-derived window reopens this gap every release cycle, because main's label always lags its content. AI-proofing: tsc forces the field onto every future retirement, the authoring-funnel tombstones are untouched, and a floor at or above V is still refused. Scope: one field, no new gate, and a one-time mechanical backfill. Cost: 91 registry entries plus gen:api-surface in packages/spec, on a spec-lane card. Then about 30 lines plus tests here, and this card becomes Blocked-by that card.", "A' (spec lane, smaller). The same field, but optional, and set only on the 18 unreleased entries; where it is absent, today's label rule applies. The cost is 18 entries. It is weaker on AI-proofing: the next retirement that omits the field silently brings this defect back for that entry.", "B (spec lane). One spec-level constant naming the highest version whose retirements this build enforces, and the window compares the floor against max(label, constant) per artifact. It is the cheapest spec change, but the decision is per artifact, not per entry, so it cannot honour 'at or above V stays refused' entry by entry. It is also a second version label beside package.json that every retirement must remember to bump: release-adjacent and easy to forget.", "C. Option (a), a prerelease label on main. That is release mechanics and needs the maintainer's word (triage 5865033067). The ruling excludes it for this seat.", "D (lane-local). A table in metadata-core mapping each conversion id to its retirement version. It can be built inside the claim, but it is the second conversion table the module docblock rules out ('Not a second conversion table'), and a consumer-side copy of a spec fact (PD #12). It drifts silently the day the spec lane lands a retirement without touching metadata-core, which is exactly the failure this card reports.", "E (lane-local, one line). Open the window when the floor equals the runtime label, by switching the comparison from strictly-below to at-or-below. It fixes main today but breaks the declared policy at V equal to the runtime. Once the M2 batch brings back allowRestore/allowPurge, permission-allow-restore-purge-removed would strip them from an artifact authored exactly at the return version: the hazard the module docblock names. A floor-17.5.0 artifact on a 17.5.0 runtime would also be converted silently instead of refused." ], "recommendation": "A. It is the only per-entry option, which is what the ruling's option (b) means. It keeps the spec registry the single authority, the type system enforces it rather than memory or a new gate, and no release act is involved. Choose A' only if the spec lane wants the smallest change and accepts the risk that a later retirement forgets the field. The metadata-core half is small, and ready to build as soon as the field exists on main." }, { "question": "Once the per-entry version exists, which rule should the window use? The union rule replays E when the floor is below the runtime label OR the floor is at or below E.retiredAfter. The strict per-entry rule replays E only when the floor is at or below E.retiredAfter.", "options": [ "Union. It opens exactly the unreleased gap: on main, a floor-17.4.0 artifact gets the 18 post-17.4.0 entries and nothing more, and after 17.5.0 ships the rule reduces to today's. It narrows nothing (Clause-② no). A today-scaffolded app stamps ^17 (floor 17.0.0) and is ALREADY converted-forward on this runtime: 107 of 108 entries are eligible (90 of 91 retired; app-hidden-to-unpublished is excluded). So for that app the union does not widen the retired-key class. Axes: it serves the measured case; the long-term cost is that it keeps today's per-artifact stand-in for released retirements; AI-proofing is unchanged from today; scope is minimal.", "Strict. It implements the docblock sentence ('a key retired at V stays a loud refusal for anything authored at or above V') entry by entry, which today's implementation does NOT do for released retirements. Today the runtime label serves as the one V for every entry, so an artifact at floor 17.3.0 carrying a key retired at 17.2.0 is replayed. But strict narrows. The 50 entries retired at or before 17.0.0 would stop replaying for every ^17 scaffold artifact. More generally, every entry retired at a V at or below the label would stop replaying for floors from V up to the label. That is a Clause-② narrowing (breaking), and the affected population is unmeasured. Axes: it is the tighter contract and more AI-proof, but it has no named business pull, and it is a narrowing with no measured population." ], "recommendation": "Union for this card: it is the fix the card asks for and nothing else. Strict is a separate narrowing decision, to be taken only on named evidence of artifacts that need it." } ], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed. The door notice for an already-retired entry says 'retires from the load path in protocol 19'. packages/spec/src/conversions/apply.ts:193 derives retiresIn = toMajor + 1, and toMajor is 18 on all 18 post-17.4.0 entries, yet those entries already carry retiredFromLoadPath true on main and the authoring funnel refuses them. This is wording only, not class a/b/c. Dedupe words: retiresIn, retires from the load path, conversion notice, toMajor." ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorMore actions决策卡:转换窗口按「每条退役自己的版本」判断,需要在 spec 的转换登记表上加字段。要不要加、谁来做(p1,挡住 cloud 重切)
domain:engine#1·session_01N8TPEsoJxPsdSdNKGnNGEN(os-warren) · written 2026-09-28T07:36Z. 线程读至 dev 报告(本卡上的os-dev-report,status: needs_decision)。一句话问题:用已发布的 17.4.0 CLI 构建的应用包(带仪表盘图表或页面
assignedProfiles),在main构建的运行时上启动失败。平台本应自动把旧写法转换成新写法,却没有触发转换;要永久修好,需要给 spec 的公开类型加一个字段。背景与已测事实
dev 在
origin/maindf3ba164a上复现了缺陷:^17.4.0的 artifact 判为authored-current,0 条转换,严格解析报 4 个退役键。路线 (b) 的前提被证伪,所以 dev 按派发令停手,没写代码、没开 PR。
- 登记表
MetadataConversion只有id / toMajor / retiredFromLoadPath / surface / summary / apply / fixture。 - 17.4.0 之后新增的 18 条退役
toMajor全是 18;17.2 退役的permission-allow-restore-purge-removed也是 18。所以这个字段分不出次版本。 17.5.0只出现在墓碑文案里(34 处),不是机器可读的数据。- 按已发布 tarball 做普查,main 上 91 条已退役条目的首次退役版本分别是:17.0.0 有 50 条,17.1.0 有 5 条,17.2.0 有 2 条,17.3.0 有 8 条,17.4.0 有 8 条,尚未发布的 18 条。所以回填这个字段可以机械完成。
Governing text
- ADR-0087 D2:"each conversion is applied by the loader for one major … then retired from the load path in N+1"。
packages/metadata-core/src/artifact-forward-conversion.ts模块文档:"a key retired at version V stays a loud refusal for anything authored at ≥ V";以及 "Not a second conversion table: the ADR-0087 registry in@objectstack/specstays the single authority on what converts"。- AGENTS.md:席位永不执行或暂存发布动作(triage 5865033067 据此把选项 (a) 划给维护者)。
协议声明:
MetadataConversion从@objectstack/spec包根导出(src/index.ts:224export * from './conversions/index.js'),是已发布类型。选项 A / A′ / B 都会改公开契约的形状,属于 spec 车道的Clause-②: yes。前提(每条附 re-check 命令)
- P1:
main上 spec 仍标 17.4.0。git show origin/main:packages/spec/package.json | grep -m1 '"version"'→"version": "17.4.0"。 - P2:转换类型上没有任何版本字段。
git grep -cE 'retiredAfter|sinceVersion|removedIn' origin/main -- packages/spec/src/conversions/types.ts→ 0;同文件对照git grep -c toMajor→ 3。 - P3:npm
latest是 17.4.0。curl -sS https://registry.npmjs.org/@objectstack/spec/latest→17.4.0。
选项 × 代价
选项 做什么 客户可感知的后果 A spec 车道给已退役条目加一个必填字段 retiredAfter(最后一个仍接受旧写法的版本):已发布的 73 条从 tarball 机械回填,未发布的 18 条填 17.4.0。然后 metadata-core 按条目判断:地板低于运行时标签,或地板 ≤ 该条目的retiredAfter,就回放这条转换17.4.0 构建的应用包在 main上能启动,并记录转换通知;以后每次退役都由类型系统强制写明版本,同一问题不会每个发布周期复发A′ 同 A,但字段可选,只标未发布的 18 条 眼下修好;下一次退役忘了填字段,就对那一条静默复发 B spec 加一个常量:「本构建执行到哪个版本的退役」,窗口改为按 max(标签, 常量) 整包判断 修好,但等于多了第二个版本标签,每次退役都得记得改;按整包判断,做不到逐条「≥V 仍拒」 C 发布 17.5.0,或给 main挂预发布标签(维护者动作)最快:零代码。发布后 main标签变 17.5.0,17.4.0 构建的包立刻能开窗转换。但下个周期main执行 17.6 退役时同样的缺口会复发D metadata-core 自己维护一张「转换 id → 版本」表 模块文档已明令排除(第二张转换表);spec 一加退役就静默漂移,正是本卡报告的那种失败 E 把比较从 <改成≤一行修好 main,但违反已声明的政策:地板恰为 17.5.0 的包在 17.5.0 运行时上会被静默转换而不是拒绝业务含义直译:
- A:给每条退役规则写上生效日期,系统自己查表。
- A′:只给新规则写日期,旧规则靠记性。
- B:在墙上贴一张「今天执行到哪条」的告示。
- C:先把版本号往前拨,今天就能过,下个月再碰一次。
- D:另抄一本账。
- E:把门槛放宽一格,恰好压线的也放行。
四维分析(业务立场)
- 长远合理性:A 把「何时退役」这个事实放在它描述的那条登记上,登记表仍是唯一权威。只要窗口还靠标签推算,
main的标签永远落后于内容,这个缺口每个发布周期都会复发。 - 实际业务需求:实测拉动是 hotcrm#1964 / cloud#2440 框架重切被挡(63 个问题;强制 17.5.0 时 44 条转换生效)。
- 防 AI 犯错:A 的必填字段让
tsc强制以后每条退役写明版本,写漏在编译期就会被响亮拒绝;E 会把应当拒绝的包静默转换(静默泄露)。 - 创业阶段不扩散:A 是一个字段加一次机械回填,不新增门禁;B 多一个需要人记着改的标签;D 多一张表。
四棱块
os-decision-facets
- ① 项目长远合理性:A 缩小特例(退役版本由唯一权威携带,不再用标签推算);B / D 各增生一个并行事实源。
- ② 实际业务拉动:今天 cloud 框架重切被挡(hotcrm#1964,63 个问题实测);p1。
- ③ 防 AI 犯错:A 必填字段让编译器响亮拒绝漏写;E 静默转换;A′ 漏写即静默复发。
- ④ 创业阶段不扩散:A 增一个字段(机械回填,零新门禁);B 增第二版本标签;D 增第二张表。
Prior rulings read: forward conversion,retiredafter,conversion window,tomajor,resolveinstalledspecversion → 1 hits; ADR-0120 D2 (false positive: matched
toMajor, rules on unique scope); thread: none; repo: objectstack-ai/objectstack推荐:A,窗口规则取 dev 的「并集」(地板低于运行时标签,或地板 ≤ 该条目的
retiredAfter,即回放该条)。回退项:A′。两个可以同时做的动作:
- C(发布 17.5.0)是维护者动作,可以作为当天解锁 cloud 的独立快速通道,与 A 并行。
- 它不替代 A:下个周期缺口会复发。
只看①选 A;②③④ 是否翻转:否(② 是 C 的时效理由,只影响分期,不翻字母)。
置信缺口:
MetadataConversion是已发布类型,加必填字段对仓外自行实现该接口的人是破坏性变更。仓外有没有这样的实现者,没有实测(推测为零)。- 按 tarball 回填 17.0.0 批次(50 条)的准确度,由 spec 席的测试验证。
裁后执行
- 裁 A 或 A′:本卡按
Seam:形态交分诊改道 spec 席(spec 字段 + 回填 + metadata-core 窗口,纵向一个 PR),p1 保留;dev 报告里已写好 metadata-core 那一半。 - 同时裁 C:由维护者执行发布;本卡照常等 A 落地。
- 裁 B / D / E:本席重新派发,按所裁形状施工。
Release: claim 5865235478 · session
session_01N8TPEsoJxPsdSdNKGnNGEN(os-warren) · cause: premise falsified (route (b) needs a per-entry retirement version the registry does not carry, andpackages/specis fenced for this lane) · destination:needs-user-decision(this comment). Branchclaude/issue-20390-forward-conversion-windowis empty atdf3ba164aand free for the next taker.- 登记表
15 remaining items
objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsos-dev-report
{ "issue": 20390, "status": "done", "branch": "claude/issue-20390-forward-conversion-window", "pr": "https://github.com/objectstack-ai/objectstack/pull/20435", "session": "session_01ARcDurZ5j34RdqsGgc4jgH — the dispatching seat's session; this run is mode:subagent under it (the Claude-Session line on every commit of the branch)", "premise_still_valid": true, "summary": "Ruling 5865890672 (letter A) is implemented in one vertical PR, draft #20435, head 87da6b88. Spec: MetadataConversion is now a live-or-retired union, so retiredFromLoadPath true requires retiredAfter, typed as a stable x.y.z template-literal string, and tsc refuses an unstamped retirement (TS2322). All 94 retired entries are stamped. The 73 published ones come from the published tarballs: 15.1.0=5 (15.0.0), 17.0.0=45 (16.1.0), 17.1.0=5, 17.2.0=2, 17.3.0=8, 17.4.0=8. The 21 unpublished ones carry the label 17.4.0, including flow-decision-mode-inclusive-explicit from #20344, merged in. A committed census (retired-after.census.json) and an offline census test pin every value. scripts/build-retired-after-census.ts re-derives the census from npm; its --check passes today. gen:api-surface and gen:export-origins are regenerated: MetadataConversion (interface) becomes (type). metadata-core: applyArtifactForwardConversions replays entry E when floor is below the runtime label OR floor is at or below E.retiredAfter. DEFAULT_FLIPS_NOT_REPLAYED_HERE is still read first, and its membership is untouched. The new verdict is 'converted-retired-after'. Pins (1) to (4) are green on a REAL artifact built by the published @objectstack/cli 17.4.0. Label and npm latest were both 17.4.0 at the final check, so the census is against 17.4.0.", "tests": "HEAD 87da6b88 (the union ran after the final commit). GATES: dispatch-gates --commands derived 88 commands and all 88 exit 0 on 87da6b88. `dispatch-gates --ran` with per-line exit codes printed 'Run reconciliation — 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN' and '✓ ... 0 NOT-MEASURED (a DERIVED zero — all 88 recorded an exit code and none of them is 3)'. Four gates first answered PREREQUISITE NOT MET (exit 3): lint check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt. After a full packages build (turbo, 71/71 successful, under the verify lock) all four were re-run, and the whole union was re-run on 87da6b88. TESTS, run at b9a07ea3; the last merge, 87da6b88, has an empty diff under packages/spec, packages/metadata-core and packages/metadata. spec `vitest run --project local`: 'Test Files 564 passed (564) / Tests 16604 passed | 1 todo (16605)'. spec `--project repo`: 'Test Files 36 passed (36) / Tests 655 passed (655)'. metadata-core: 'Test Files 16 passed (16) / Tests 295 passed (295)'. metadata: 'Test Files 55 passed (55) / Tests 825 passed (825)'. Typecheck: spec, metadata-core and metadata each exit 0 (spec: tsc + scripts + test-typecheck 'OK ... held'). `--listFiles` counts each new test file once in its package's type program. check:generated: 'All 15 generated artifacts are up to date'. eslint --no-inline-config --format json on the 8 changed source files: 8 linted, 0 errors, 0 warnings; the config enables no type-aware linting. ABLATION, from committed state, through scripts/ablation-replace.mjs in WRAP mode, with a bash trap restoring from HEAD. The anchor 'const closed = idsTheFloorPostdates(floor);' became \"const closed = floor && 'ABLATION-20390-LABEL-ONLY' ? null : idsTheFloorPostdates(floor);\" (the old label-only verdict). On disk: anchor 1 → 0, blob 5daaa45c → eed68e51. metadata-core was rebuilt, and ablation-dist-preflight read '✓ dist/: marker present in 2 built files'. Mutated: metadata-core 'Tests 3 failed | 32 passed (35)' — pin (4), the older-retirement companion and the default-flip per-entry companion are red; pin (3), the premise and the post-release companion are green. metadata file 'Tests 2 failed | 2 passed (4)' — pin (1) boot and pin (1) notices are red; pin (2) and the premise guard are green. Restore: blob after restore 5daaa45c equals HEAD, `git diff HEAD` is empty, porcelain has 0 lines, the rebuilt dist reads 'marker absent from all 12 built files' and 'working tree clean against HEAD', metadata-core is 35/35 and metadata 4/4. REVERSE VERIFICATION: with retiredAfter removed from page-assigned-profiles-removed, spec `tsc --noEmit` exits 2 with exactly 1 'error TS2322' on that entry. The first attempt was a no-op that ablation-replace refused (the replacement was a prefix of the anchor, so its count rose by 0), and it restored; the retry used a distinct replacement. The census test, mutated: published object-compactLayout-to-highlightFields stamped '14.8.0' reds the PUBLISHED test ('the tarballs say 15.0.0'); unpublished page-assigned-profiles-removed stamped '17.3.0' reds the UNPUBLISHED test. `build-retired-after-census.ts --check` exits 0 against npm, and exits 1 with 'census drift' when precedingRelease is tampered. Every mutation was restored to the HEAD blob with 0 porcelain lines. CI on 87da6b88 at report time: in_progress (32 check runs: 17 in_progress, 12 success, 3 skipped, 0 failed).", "mcp_calls": "0", "api_writes": "3 — each one POST /repos/objectstack-ai/objectstack/dispatches through the fleet relay, executed as objectstack-fleet[bot]: (1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls, draft #20435, relay run 36418356626; (2) label-write --assign os-warren, which became POST /repos/objectstack-ai/objectstack/issues/20435/assignees, relay run 36418507779, read back as a match; (3) this os-dev-report comment via post-stamped, which becomes POST /repos/objectstack-ai/objectstack/issues/20390/comments. git push is not REST. The reads were REST GETs of the card, its comments, the PR and its check runs, plus npm registry GETs.", "gates": "88/88 derived gates exit 0 on HEAD 87da6b88; dispatch-gates --ran: 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero, since every line carries an exit code). CI on the PR: in_progress at report time.", "line_budget": "n/a — no skills/** or other line-ratcheted file touched. The diff is 13 files, +1480/−49 (dispatch-gates at 87da6b88), under the 5000-line human-merge threshold.", "files_changed": [ ".changeset/20390-conversion-retired-after-window.md", "packages/metadata-core/src/artifact-forward-conversion.test.ts", "packages/metadata-core/src/artifact-forward-conversion.ts", "packages/metadata/src/__fixtures__/forward-probe-17.4-built.artifact.json", "packages/metadata/src/plugin-artifact-forward-conversion-retired-after.test.ts", "packages/spec/api-surface/root.json", "packages/spec/export-origins/root.json", "packages/spec/scripts/build-retired-after-census.ts", "packages/spec/src/conversions/apply.ts", "packages/spec/src/conversions/registry.ts", "packages/spec/src/conversions/retired-after.census.json", "packages/spec/src/conversions/retired-after.census.test.ts", "packages/spec/src/conversions/types.ts" ], "deviations": [ "Unpublished-entry rule, one tolerance. Read literally, 'every entry absent from the last published tarball carries the current package.json label' turns the Version Packages PR red: that PR bumps the label to 17.5.0 before 17.5.0 is published, while the 17.5.0 cohort correctly carries 17.4.0. So while the label is ahead of the census's last release, the test accepts any value from that release up to the label, and it demands the exact label whenever the two are equal (true today). The tolerance closes once the census records the new tarball.", "Network half. PM mechanism assumption 5 is falsified: the pin against the tarballs is scripts/build-retired-after-census.ts (write / --check), not a repo-tier test. vitest.repo-tests.json is held EQUAL to the set of tests that read outside the package by check:cross-package-test-inputs ('lists file(s) that do not read outside the package' reds), so a network-only test cannot be listed there. Reading the tarballs also means about 11 downloads totalling over 250 MB per run. CI pins the committed census offline. The script is loud when offline and never skips. It is not a package.json script and not wired into CI, so no gate is added.", "Census counts differ from the ruling's, and the ruling's principle decides them: 94 retired = 73 published + 21 unpublished. The ruling's 17.0.0 bucket of 50 is 45 + 5: five entries (object-compactLayout-to-highlightFields, stack-roles-to-positions, owd-legacy-read-aliases, sharing-recipient-role-to-position, book-audience-profile-to-permission-set) are already retired in the 15.1.0, 15.1.1, 16.0.0 and 16.1.0 tarballs, so they carry 15.0.0. The engine seat's census had started at 17.0.0.", "Stable releases only: the census and the rule skip the -rc versions, because a caret floor never names a prerelease and the door compares x.y.z triples.", "File surface: packages/spec/src/conversions/apply.ts had one docblock sentence edited so it stays true (the door opens its window per entry with retiredAfter). The real 17.4.0-built fixture uses manifest id com.example.forward-probe, because main now refuses an underscore in manifest.id (#17534, a documented narrowing), which is unrelated to this window.", "Changeset: @objectstack/spec minor and @objectstack/metadata-core minor. metadata-core earns minor because a published union type gains a verdict member. The changeset carries a BREAKING banner, 'Clause-②: yes' and the ADR-0087 disposition not-required (runtime-interface-only packages/spec/src/conversions/types.ts#MetadataConversion), which check-adr-0087-registration verified.", "Main was merged three times through scripts/pm/os-regen-merge.sh. The #20344 merge had one conflict, in the metadata-core test file, where both sides added a describe block; both were kept. The regeneration discharge came after, and check:generated then reported all 15 up to date." ], "open_questions": [ { "question": "Nothing forces a census refresh after a stable publish. Until the refresh, the unpublished-entry rule stays in its release-pending tolerance, so an entry stamped low but not below the last censused release passes. How is the refresh triggered?", "options": [ "A. One line in the release checklist (docs/releases-maintenance.md): after a stable @objectstack/spec publish, run build-retired-after-census.ts and commit. No gate is added.", "B. Nothing. Accept the tolerance and refresh only when someone notices.", "C. A scheduled network --check workflow. This is a new gate, and the startup-stage axis defaults it to no without the maintainer naming it." ], "recommendation": "A, on the four axes. Real need: the window is only exact when the census is current, and a low stamp recreates this card's defect for that entry. Long-term soundness: the refresh is a mechanical publish-time step beside the one the release workflow already runs against the previous tarball. AI-proofing: a refreshed census turns a low stamp into a red test. Startup scope: one checklist line and no new gate, where C would add one." } ], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed. packages/metadata/src/plugin.ts: the door's warn line under the new 'converted-retired-after' verdict reads 'predates this runtime's spec (authored engines.protocol floor 17.4.0, runtime spec 17.4.0)'. It is accurate, since the runtime's content is newer than its label, but it can confuse an operator. Wording only. Dedupe words: predates this runtime's spec, converted-retired-after, artifact conversion warn.", "carrier: 承接者:无 · noted, not filed. ADR-0087 D2 (docs/adr/0087, the retiredFromLoadPath paragraph) does not describe retiredAfter or the per-entry artifact window. An ADR-0087 note may be owed, but docs/adr/** is fenced for this PR. Dedupe words: retiredAfter, ADR-0087 D2, artifact forward conversion window, per-entry.", "carrier: 承接者:无 · noted, not filed. .claude/skills/spec-property-retirement/SKILL.md lists 'retiredFromLoadPath: true' in its retirement kit without retiredAfter. tsc now refuses the omission, so the playbook is only behind. It is a governed surface (Tier S). Dedupe words: spec-property-retirement, retiredAfter, retirement kit." ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsREWORK round 1 (from the at-tier record on the PR: FAIL) — PR #20435 at head
87da6b88·domain:specseat 4 (session_01ARcDurZ5j34RdqsGgc4jgH) · 2026-09-28T12:14ZThe at-tier contract review FAILED at this head. Its record is on the PR as
5869622844. It passes every execution parameter of ruling5865890672, (i)–(vii), and it cross-checks all 94 stamps against the committed census with zero mismatches. It fails on one in-tree consumer the new verdict left behind. The seat re-read that consumer on the branch:packages/metadata/src/plugin.ts:830still readsif (result.verdict !== 'converted-forward' && result.verdict !== 'converted-undeclared') return;. The finding is accepted.Owed in this round (new commits only; ⛔ no rebase, amend or force-push):
MetadataPlugin._warnUnboundFormPredicateRoots(plugin.ts~825-830) admits'converted-retired-after'. Either add the arm, or invert the guard to return only on'authored-current'and'runtime-version-unknown'; pick one and say why. Its docblock's 「read off that pass's own verdict rather than recomputed, so the two can never disagree」 must be true again: the 17.1-era artifacts carrying bare-root form-view predicates silently degrade on 17.2: unbound root faults open, conditionally-hidden fields render and theirrequired: truedead-ends console record creation — no ADR-0087 conversion covers the predicate-root move #12915 scope-C notice must not depend on the label.- One pin beside pin (1) in
plugin-artifact-forward-conversion-retired-after.test.ts: the^17.4.0fixture, carrying one bare-root form predicate, on the 17.4.0 runtime, logs the 17.1-era artifacts carrying bare-root form-view predicates silently degrade on 17.2: unbound root faults open, conditionally-hidden fields render and theirrequired: truedead-ends console record creation — no ADR-0087 conversion covers the predicate-root move #12915 line exactly once. Show it red with the old guard (ablation from committed state) and green with the new one. - The changeset and the PR body drop 「The door's operator log lines are unchanged」 and name the consumer arm added.
- In the same
plugin.tstouch, amend the warn line under'converted-retired-after'so it names what is replayed: the retirements this runtime enforces past the artifact's floor. The current wording, 「predates this runtime's spec … runtime spec 17.4.0」, reads as a contradiction. - The seat's answer to your
open_questionsentry: A. Add one line todocs/releases-maintenance.md, in the GA release flow (the Version Packages PR section): after a stable@objectstack/specpublish, runscripts/build-retired-after-census.tsand commit the refreshed census. No workflow and no gate (option C is refused on the startup-stage axis, and B leaves the defect this card fixes open for the next retirement). This is a verification-strategy step, a non-escalation class. The file is not governed. Add it to this PR's file surface.
The seat's readings on the two deviations (the record judged both to keep the ruling's intent):
- (a) The release-pending tolerance is confirmed. The ruling's literal rule would turn the maintainer's Version Packages PR red. The tolerance is exactly
[last censused release, label]and is open only while the label is ahead. The refresh step in item 5 closes the residual. - (b) The network half,
build-retired-after-census.ts --checkplus the committed census, is accepted as the ruling's 「census test … against the tarballs」.
Not owed here (the seat files them at landing, as governed-surface follow-ups): ADR-0087's #12772 addendum sentence 「floor at or above runtime replays nothing」, and
.claude/skills/spec-property-retirement/SKILL.md's retirement kit.Verification: re-run the
dispatch-gates --commandsunion on the final head and reconcile it with--ran. Run spec (local + repo),@objectstack/metadata-coreand@objectstack/metadata. Mergemainwithbash scripts/pm/os-regen-merge.shif it has moved. Writes:git push, oneos-dev-reportcomment on this card, and one PR-body update viascripts/pm/(relaypr_patchor the tool the relay offers). No label or assignee write. No MCP write.domain:specseat 4 · #18917 ·session_01ARcDurZ5j34RdqsGgc4jgH
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsos-dev-report
{ "issue": 20390, "status": "done", "branch": "claude/issue-20390-forward-conversion-window", "pr": "https://github.com/objectstack-ai/objectstack/pull/20435", "session": "session_01ARcDurZ5j34RdqsGgc4jgH — the dispatching seat's session; this run is mode:subagent under it (the Claude-Session line on every commit of the branch)", "premise_still_valid": true, "summary": "REWORK round 1 per order 5869635456 (at-tier record 5869622844, FAIL on one consumer). All five items are done in new commits: 874a4e74 and a1dc7d33, plus two os-regen merges, df46d5b8 and 2c537b7e. There was no rebase, amend or force-push. (1) plugin.ts: the verdicts that open the window are now one total table, FORWARD_WINDOW_OPENED, a Readonly Record keyed by every ArtifactForwardConversionVerdict member and valued boolean, with 'converted-retired-after' on the open side. _warnUnboundFormPredicateRoots returns on !FORWARD_WINDOW_OPENED[result.verdict]. The form chosen is 'add the arm', spelled as a total table: the old hand-written guard defaults a future verdict to closed, which is how this defect arose; the inverted guard defaults it to open and also admits 'not-an-object'; a total Record has no default, so tsc refuses the next verdict until it is placed (reverse-verified with TS2741). The docblock sentence 'read off that pass's own verdict … never disagree' holds again, and the #12915 notice no longer depends on the label. (2) A new pin beside pin (1): the ^17.4.0 fixture with one bare-root form predicate, on the 17.4.0 runtime, logs the #12915 line exactly once, across two ingestions. It is red under the old guard and green now. (3) The changeset drops 'The door's operator log lines are unchanged' and names the arm added; it also gains '@objectstack/metadata': patch, because plugin.ts now changes. The PR body names it too. (4) Under 'converted-retired-after', the conversion summary names the retirement this runtime enforces past the artifact's floor, with the release that last accepted the shape. That fact comes from a new metadata-core result field, replayedRetirements (element type ArtifactReplayedRetirement), so the door does not recompute it. The #12915 notice opens with the same verdict-aware clause, and every other verdict keeps its old wording. (5) docs/releases-maintenance.md, GA Version Packages PR flow: after a stable @objectstack/spec publish, run scripts/build-retired-after-census.ts and commit the refreshed census. One consequence: the existing 'says NOTHING about an artifact authored against the current surface' pin had derived that surface as a caret range on the installed label, which is exactly the label-dependence removed here. It now derives the first x.y.z past both the label and every retiredAfter. Label and npm latest are still 17.4.0, and no new retired entry arrived with the merges (94 stamped).", "tests": "Final HEAD 2c537b7e. GATES: dispatch-gates --commands derived 90 commands at 2c537b7e (16 files, +1667/−72) and all 90 exit 0. dispatch-gates --ran with per-line exit codes: 'Run reconciliation — 90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN' and '✓ … 0 NOT-MEASURED (a DERIVED zero — all 90 recorded an exit code and none of them is 3)'. A full packages build ran first (turbo, 71/71 successful, under the verify lock). TESTS at 2c537b7e: spec --project local 'Test Files 565 passed (565) / Tests 16645 passed | 1 todo (16646)'. spec --project repo: one full run hit the 570s timeout (exit 124) under box contention and was rerun as two halves of vitest.repo-tests.json, 'Test Files 18 passed (18) / Tests 460 passed (460)' and 'Test Files 18 passed (18) / Tests 195 passed (195)', together 36 files and 655 tests. metadata-core 'Test Files 16 passed (16) / Tests 295 passed (295)'. metadata 'Test Files 55 passed (55) / Tests 826 passed (826)'. Typecheck: metadata-core exit 0. metadata first exited 2 (TS2550, Array.at in the new test helper under the package lib); a1dc7d33 fixed it and it then exits 0. eslint --no-inline-config --format json on the 10 changed source files: 10 linted, 0 errors, 0 warnings. ABLATION, from committed state a1dc7d33, through scripts/ablation-replace.mjs in WRAP mode, with a bash trap restoring from HEAD by absolute path. Anchor 'if (!FORWARD_WINDOW_OPENED[result.verdict]) return;' became \"if (result.verdict !== 'converted-forward' && result.verdict !== 'converted-undeclared') return; // ABLATION-20390-OLD-GUARD\". On disk: marker 1, new guard 0, blob 8f43972c → b64fd125. Mutated, over the three door suites (plugin-artifact-forward-conversion-retired-after, plugin-unbound-form-predicate-roots, plugin-artifact-forward-conversion): 'Tests 1 failed | 20 passed (21)'; the one red is 'announces a bare-root form predicate once — the #12915 notice follows the per-entry window', and every other test, including the updated current-surface silence pin, stays green. Restore: blob after restore 8f43972c equals HEAD, `git diff HEAD` has 0 lines, porcelain has 0 lines, marker 0 and new guard 1, 'Test Files 3 passed (3) / Tests 21 passed (21)'. The suites import plugin.ts from source, so no build sits between the mutation and the run. REVERSE VERIFICATION: with the \"'converted-retired-after': true\" row dropped from FORWARD_WINDOW_OPENED, packages/metadata `tsc --noEmit` exits 2 with 'error TS2741: Property '\"converted-retired-after\"' is missing …'. Restored to the HEAD blob, 0 porcelain lines. The round-0 evidence stands: window ablation, the TS2322 on an unstamped retirement, the census negatives and the census --check against npm. CI on 2c537b7e at report time: 42 check runs, 33 success, 5 skipped, 4 in_progress, 0 failed.", "mcp_calls": "0", "api_writes": "2 this round — each one POST /repos/objectstack-ai/objectstack/dispatches through the fleet relay, executed as objectstack-fleet[bot]: (1) issue_patch on #20435, which became PATCH /repos/objectstack-ai/objectstack/issues/20435 (the body), relay run 36428801663; the stored body was read back byte-identical, 14838 chars, and was confirmed unchanged since my read before the write; (2) this os-dev-report comment via post-stamped, which becomes POST /repos/objectstack-ai/objectstack/issues/20390/comments. No label or assignee write. git push is not REST. The reads were REST GETs of the order, the record, the PR and its check runs, plus npm registry GETs.", "gates": "90/90 derived gates exit 0 on HEAD 2c537b7e; dispatch-gates --ran: 90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero, since every line carries an exit code). CI on the PR: 4 checks in_progress at report time, 0 failed.", "line_budget": "n/a — no skills/** or other line-ratcheted file touched. The PR diff is 16 files, +1667/−72 (dispatch-gates at 2c537b7e), under the 5000-line human-merge threshold.", "files_changed": [ ".changeset/20390-conversion-retired-after-window.md", "docs/releases-maintenance.md", "packages/metadata-core/src/artifact-forward-conversion.test.ts", "packages/metadata-core/src/artifact-forward-conversion.ts", "packages/metadata/src/__fixtures__/forward-probe-17.4-built.artifact.json", "packages/metadata/src/plugin-artifact-forward-conversion-retired-after.test.ts", "packages/metadata/src/plugin-unbound-form-predicate-roots.test.ts", "packages/metadata/src/plugin.ts", "packages/spec/api-surface/root.json", "packages/spec/export-origins/root.json", "packages/spec/scripts/build-retired-after-census.ts", "packages/spec/src/conversions/apply.ts", "packages/spec/src/conversions/registry.ts", "packages/spec/src/conversions/retired-after.census.json", "packages/spec/src/conversions/retired-after.census.test.ts", "packages/spec/src/conversions/types.ts" ], "deviations": [ "Item 1: 'add the arm', spelled as a total Record over the verdict union (FORWARD_WINDOW_OPENED) rather than a longer hand-written disjunction. The order offered two forms, and this is the arm-adding one, made total so tsc places the next verdict. It is reverse-verified: dropping the row gives TS2741.", "Item 4 needed a fact the door did not have (each replayed entry's retiredAfter), so metadata-core's result gains replayedRetirements, with the exported element type ArtifactReplayedRetirement. It stays inside the metadata-core minor. Reading it off the pass keeps the door from recomputing the window.", "@objectstack/metadata now carries a patch changeset entry, because plugin.ts is changed. The at-tier record's 'no changeset owed' for packages/metadata predates this round.", "The existing #12915 silence pin ('says NOTHING about an artifact authored against the current surface') had to change: it derived 'current' as a caret range on the installed label, which after item 1 names an artifact built BY the last release. It now derives the first x.y.z past both the label and every retiredAfter. Its intent (an artifact on the surface this runtime enforces gets no notice) is unchanged, and it stays green under both guards.", "spec test:repo ran as two halves of vitest.repo-tests.json (18 + 18 files). A single run exceeded the 570s foreground budget under box contention (exit 124, 0 results read). It is not a narrowing: all 36 files ran.", "Main was merged twice this round, via scripts/pm/os-regen-merge.sh (df46d5b8, 2c537b7e), with no conflicts; the script kept the branch's bytes of api-surface/export-origins root.json, and check:generated plus check:api-surface are green on the final head." ], "open_questions": [], "out_of_scope_findings": [ "carrier: the seat at landing (per order 5869635456) · noted, not filed by me. ADR-0087's #12772 addendum sentence that a floor at or above the runtime 'replays nothing' becomes false when this lands (Tier H, docs/adr/**). Dedupe words: ADR-0087 addendum, replays nothing, retiredAfter, per-entry window.", "carrier: the seat at landing (per order 5869635456) · noted, not filed by me. The retirement kit in .claude/skills/spec-property-retirement/SKILL.md does not list retiredAfter; tsc refuses the omission (Tier S). Dedupe words: spec-property-retirement, retiredAfter, retirement kit." ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsos-dev-report
{ "issue": 20390, "status": "done", "branch": "claude/issue-20390-forward-conversion-window", "pr": "https://github.com/objectstack-ai/objectstack/pull/20435", "session": "session_01ARcDurZ5j34RdqsGgc4jgH — the dispatching seat's session; this run is mode:subagent under it", "premise_still_valid": true, "summary": "Stamp lap before round-2 review. New head 59687e3f. origin/main was merged with scripts/pm/os-regen-merge.sh as 9cfc3122 (main 3cf64493, which carries 6e3e5462 #20357, 7fa3e3e0, 8cdbe0c6 and aeb0557f); the merge was clean, and the script kept the branch's api-surface/export-origins root.json bytes. On the merged tree exactly one retired entry lacked retiredAfter: view-list-tabs-removed (#20357). 7fa3e3e0, 8cdbe0c6, aeb0557f and 3cf64493 add no retired entry. Commit 59687e3f stamps it retiredAfter '17.4.0': it is in no published tarball, and the label and npm latest were both re-checked at 17.4.0. Census on 59687e3f: 95 retired = 73 published + 22 unpublished, all 22 at 17.4.0. retired-after.census.json is unchanged, because it holds tarball facts only and no new stable release exists; build-retired-after-census.ts --check against npm passes ('matches the published tarballs (11 releases)'). No round-1 work changed. main has since moved to b2855081 (#20414: a new data filter module plus the api-surface/export-origins data.json shards). It adds no retired entry, and a driverless merge-tree probe (bare --shared clone, no os-regen driver) of 59687e3f with b2855081 is clean (exit 0). I did not merge it, to avoid another full lap; the queue's merge ref carries it.", "tests": "HEAD 59687e3f, after a full packages build under os-verify-lock (turbo 71/71 successful, 0 cached). spec --project local: 'Test Files 565 passed (565) / Tests 16644 passed | 1 todo (16645)'. spec --project repo, in two halves of vitest.repo-tests.json, which now lists 37 files: 'Test Files 18 passed (18) / Tests 460 passed (460)' + 'Test Files 19 passed (19) / Tests 224 passed (224)', so all 37 files and 684 tests. metadata-core: 'Test Files 16 passed (16) / Tests 295 passed (295)'. metadata: 'Test Files 55 passed (55) / Tests 826 passed (826)'. spec typecheck: exit 0 with 0 'error TS' lines (the merged tree's TS2322 on view-list-tabs-removed is gone); test-typecheck 'OK'. check:generated: 'All 15 generated artifacts are up to date'. check:api-surface: 'public API surface + factory signatures unchanged ✓'. GATES: dispatch-gates --commands derived 90 at 59687e3f (16 files, +1668/−72) and all 90 exit 0; --ran with per-line exit codes: 'Run reconciliation — 90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN' and '✓ … a DERIVED zero'. CI on 59687e3f at report time: 35 check runs, 31 success, 3 skipped, 1 in_progress, 0 failed.", "mcp_calls": "0", "api_writes": "1 — this os-dev-report comment via scripts/pm/post-stamped.mjs on the fleet relay, which becomes POST /repos/objectstack-ai/objectstack/issues/20390/comments. No PR body, label or assignee write. git push is not REST. The reads were REST GETs of check runs and npm registry GETs.", "gates": "90/90 derived gates exit 0 on HEAD 59687e3f; dispatch-gates --ran: 90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero).", "line_budget": "n/a — no line-ratcheted file touched. The PR diff is 16 files, +1668/−72.", "files_changed": [ "packages/spec/src/conversions/registry.ts (this lap: +1 line, view-list-tabs-removed retiredAfter '17.4.0')" ], "deviations": [ "main moved to b2855081 after my merge at 3cf64493. I did not merge it: it adds no retired entry and touches none of this PR's files (its api-surface/export-origins changes are in the data.json shards; this PR moves root.json), and a driverless merge-tree probe of 59687e3f with b2855081 is clean." ], "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsReview: ACCEPT · PR #20435 at head
59687e3f2fbd435cbbab9b2df698cc3f88c6fc59· 2026-09-28T15:10Zdomain:specseat 4 (session_01ARcDurZ5j34RdqsGgc4jgH), reviewer of record under claim5867115315. The readings below were taken on GitHub or on the tree by this seat.check reading Ruling 5865890672(letter A; the maintainer's record is5865873150). Every retired conversion entry carries a REQUIREDretiredAfter, and the artifact window decides per entry: replay E when the floor is below the label OR at or belowE.retiredAfter.DEFAULT_FLIPS_NOT_REPLAYED_HEREis still read first. All seven execution parameters and the four pins are judged right on this head.Shape Draft, base main. Body line 1Fixes #20390;Clause-②: yesstands alone.Scope 16 files, +1668/−72, across packages/spec(the type, the 95 stamps, the census file, test and script,api-surface/export-originsroot rows),packages/metadata-core(the window, a new verdict'converted-retired-after',replayedRetirements),packages/metadata(the door'sFORWARD_WINDOW_OPENEDtable, the #12915 pin, the 17.4.0-built fixture) and one line indocs/releases-maintenance.md.Contract review Round 0: FAIL 5869622844on87da6b88. One consumer,_warnUnboundFormPredicateRoots, was left behind; the seat's rework order was5869635456. Round 2: PASS5872777111, same head. It covers the five rework items, the new metadata-core surface, the re-derived silence pin, and the stamp lap. Census cross-check: 95 retired = 73 published + 22 unpublished, every one stamped, zero mismatches.Seat answers The release-pending tolerance is confirmed. The network half ( build-retired-after-census.ts --checkplus the committed census) is accepted as the ruling's census test. The census-refresh question is answered A: one line in the GA release flow, no gate.Changeset @objectstack/specminorwith a BREAKING banner (a published type gains a required member);@objectstack/metadata-coreminor(a verdict member, a result member and its type);@objectstack/metadatapatch. ADR-0087 dispositionnot-required (runtime-interface-only …).CI at this head 32 success, 3 skipped, all on the roster ( check-expected-skips.mjs --pr 20435, exit 0).mergeable_state:clean.origin/mainfbeb56e4adds no retired entry since the last merge (0 newretiredFromLoadPath: truelines), andgit merge-treeis clean.Governed / size Not governed ( check-governed-merges.mjs --pr 20435: 0 of 16 paths), 1740 changed lines.Acceptance notes / follow-ups:
- The changeset and PR body still count 「94 retired … 21 unpublished」, the
2c537b7esnapshot; at landing it is 95 / 22. This is prose only: no level, pin or accept set rests on it, and the census file and its test are the authority, a count every later retirement moves → Acceptance notes, carrier: none. - ADR-0087's Artifacts built by released 17.x tooling are REFUSED by the 17.2 runtime: retired-key tombstones fire at artifact parse, and no artifact-ingestion door runs the ADR-0087 conversion that exists for exactly this #12772 addendum sentence (a floor at or above the runtime 「replays nothing」) becomes false when this lands (Tier H), and
.claude/skills/spec-property-retirement/SKILL.md's retirement kit omitsretiredAfter(Tier S;tscrefuses the omission loudly). → The seat files one governed follow-up card at landing, as5869635456said. - Cross-seat: after landing, any open PR that adds a retired entry fails typecheck until it stamps
retiredAfter. That is the designed loud direction; feat(spec)!: retire the list view's owntabskey; named presets arelistViewsentries #20357 was the first case, and this branch absorbed it.
Landing next: ready + auto-merge through the relay. At MERGED, this card closes by
Fixes. Downstream: objectstack-ai/cloud#2332 (the framework re-cut behind objectstack-ai/cloud#2423) was waiting on this.domain:specseat 4 · #18917 ·session_01ARcDurZ5j34RdqsGgc4jgH
Generated by Claude Code
- The changeset and PR body still count 「94 retired … 21 unpublished」, the
objectstack-fleet commented
on Sep 28, 2026 ContributorMore actionsLanded: PR #20435 →
maine956924e17b8407ff443b12ccdeefa64d3c5de06· 2026-09-28T15:47Zdomain:specseat 4 (session_01ARcDurZ5j34RdqsGgc4jgH), landing record for claim5867115315.Review trail and queue path:
- ACCEPT
5872830280, resting on the at-tier round-2 PASS5872777111. - Round 0 was FAIL
5869622844; the rework order was5869635456. - The stamp lap absorbed feat(spec)!: retire the list view's own
tabskey; named presets arelistViewsentries #20357'sview-list-tabs-removed. - Ready and auto-merge at 15:12Z,
added_to_merge_queueat 15:21Z, merged at 15:46Z.
Verified on
origin/main:e956924ehas one parent,dc0ab6a2(feat(spec): curated fieldGroups and indexes repeaters on the object form (#19332, flight G2a) #20449, which went through the queue just ahead), and is an ancestor oforigin/main.- Its diff against that parent is 16 files, +1668/−72, the same as the PR.
- Content control:
'converted-retired-after'is inpackages/metadata-core/src/artifact-forward-conversion.tsatorigin/mainand absent at the parent. - Stamp completeness on
origin/main: 95retiredFromLoadPath: trueentries and 95retiredAfterinpackages/spec/src/conversions/registry.ts. - No queue branch for feat(spec,metadata-core)!: every retired ADR-0087 conversion carries retiredAfter; the artifact door opens its window per entry (#20390) #20435 remains.
Close-out:
- This card was closed
completedby the PR'sFixesline.pm:dispatchedcomes off in this act. - Ruling
5865890672is executed: an artifact authored at the last published release (17.4.0) with retired shapes now boots onmainwith the conversions logged. - Downstream: objectstack-ai/cloud#2332 (the framework re-cut behind objectstack-ai/cloud#2423) was waiting on this.
- From now on, every new retirement stamps
retiredAfter, andtscrefuses one that does not. After each stable publish, the census refresh step indocs/releases-maintenance.mdapplies.
domain:specseat 4 · #18917 ·session_01ARcDurZ5j34RdqsGgc4jgH
Generated by Claude Code
- ACCEPT
- added 4 commits that reference this issue
on Oct 7, 2026
Ruled: 5865890672 · letter A · 2026-09-28T08:02Z
Source
This surfaced on objectstack-ai/hotcrm#1964 (floor 1 of the cloud framework re-cut, objectstack-ai/cloud#2332 / PR objectstack-ai/cloud#2423) and is tracked under epic objectstack-ai/cloud#2440.
Maintainer ruling in the epic session, 2026-09-28 (verbatim): 「平台修转换 + HotCRM 先删 scale(推荐)」. The platform fixes the forward conversion. hotcrm only removes what no release can convert.
What breaks
objectstack
main(measured at862b6ce8) already refuses keys that the retirement ledger marks "removed in @objectstack/spec 17.5.0". Butpackages/speconmainstill carries the version label17.4.0, and 17.5.x is not published (npmlatest= 17.4.0).applyArtifactForwardConversions(packages/metadata-core/src/artifact-forward-conversion.ts~301,resolveInstalledSpecVersion) therefore reads runtime17.4.0== the artifact's authored floor^17.4.0and returns authored-current. The retired conversions are then not replayed, andMetadataPlugin._parseAndRegisterArtifact(packages/metadata/src/plugin.ts~894) refuses the artifact.Consequence: any artifact built by the published
@objectstack/cli17.4.0 that contains a dashboard chart cannot boot on a runtime that runs unreleasedmain. At 17.4.0,chartConfig.typeis required, andmainrefuses it. Such a runtime is the EE composed boot throughOS_COMPOSED_ARTIFACT_URL, as cloud does when it pins a framework SHA. No source spelling ofchartConfigis legal on both versions short of deleting the block.Measured (hotcrm#1964 dev report, 2026-09-28)
Probe: the same
ObjectStackDefinitionSchema.parse(applyArtifactForwardConversions(...))from objectstack862b6cedist, run on hotcrmmain's artifact:17.4.0(actual label on main)chartConfig.type, 9xAxis, 9yAxis, 23objects..scale, 6pages..assignedProfiles17.5.0dashboard-widget-chart-config-structure-removed×34,page-assigned-profiles-removed×6,page-component-filter-record-to-rule-array×4scaleissues (no conversion exists; hotcrm is removing them)Direct EE boot of the same artifact on
main: exit 1. With all three key families stripped: serves (HTTP 200).Direction (for triage and the owning lane)
Make the conversion window open for artifacts authored at the last published release whenever the runtime already enforces a later retirement. The dev named two shapes; picking one is the owning lane's design call:
maincarries a prerelease label (e.g.17.5.0-next) as soon as it contains a 17.5 retirement.Acceptance
assignedProfilesboots onmain, and the conversion notices are logged.