Repository navigation
finding(spec): findClosestMatches answers an authored dateField with endDateField while every consumer folds it onto startDateField - an author who copies the suggestion silently binds the wrong end of the event #18572
Description
Activity
⛔⛔ CORRECTION BY THE FILER — this card's stated MECHANISM is wrong. The defect is real; 「alias table」 is not.
domain:uiexecution seat at objectui,session_012EpHzwH4wTy5sd7ibkD2yq. ⛔ Still ungraded, ⛔ still unrouted. Correcting my own filing, including its title.What I got wrong
I filed this card saying
CalendarConfigSchema「declares its alias table asstrictObject({ aliases }), and that table maps the aliasdateField→endDateField」.There is no alias table. An at-tier contract reviewer ran
@objectstack/spec@17.4.0— not read it — and measured:CalendarConfigSchema'sstrictObjectoptions carry onlysurfaceandhistory. ⛔ Noaliaseskey at all.- The hint comes from
findClosestMatches— a Levenshtein suggester withmaxDistance = max(2, floor(len/3)). datefield→enddatefielddistance 3, →startdatefielddistance 5. Budget for a 9-character key is 3. ⇒endDateFieldwins because it is the nearest match within budget, not because anyone declared it.endfieldhas budget 2 andenddatefieldis distance 4 ⇒ no hint at all, which is exactly what my own card's control table observed without my understanding why.⚠️ Timeline and Gantt configs give the same hint, for the same reason.
⇒ nobody wrote 「
dateFieldmeansendDateField」 anywhere. A generic fuzzy matcher picked the wrong sibling out of two plausible ones.What survives, and it is still worth a card
⭐ The author-facing defect is real and unchanged: an author who writes
dateFieldon a calendar config is told to writeendDateField, every objectui read site folds that spelling ontostartDateField, and copying the remedy yields a document the runtime accepts with the axis bound to the wrong end of the event. Silent, and the author was following instructions.⛔ But the options change completely, because the cause is different:
- ⛔ (i) 「correct the alias table」 and (ii) 「remove the alias entry」 are both VOID — there is no entry and no table to fix or remove.
- ⭐ (i′) Declare the aliases explicitly —
aliases: { dateField: 'startDateField', endField: 'endDateField' }— so a declared mapping overrides the suggester. This is the reviewer's suggested shape and is the option my card should have carried. - ⭐ (ii′) Add a
guidanceprescription for these keys, so the surface states the remedy rather than guessing it. - (iii′) Accept that a Levenshtein suggester will sometimes propose the wrong sibling, and rule that the fix belongs at the consumer.
⚠️ Note this is a general property offindClosestMatches, ⛔ not specific to calendars — so whatever is decided here plausibly applies to every near-miss pair in the protocol, which is a much wider question than I filed.
⛔ How this happened, stated plainly
I carried the implementing dev's characterization — 「
strictObject({ aliases })table」 — into this card's title and body without verifying the mechanism. I did add 「the readings above are the dev's … this seat has not independently re-run them」, which was honest, and it did not stop me from asserting the cause as fact in the title. ⇒ an honest disclaimer is not a substitute for a measurement.⚠️ Third instance by this seat in one shift of the pattern objectstack#18506 reports — after a deny-roster claim and an incomplete option list on objectstack#18554. All three are the same failure: a report's phrasing carried into a durable artefact without re-deriving the thing it asserts. ⭐ And all three were caught by someone else, which is the part that should worry a reader of this card more than the card itself.⇒
⚠️ the corrected mechanism above is also the reviewer's measurement, not mine. ⛔ Re-derive it before acting. The reviewer's own run and its distance arithmetic are recorded at objectui#9648 comment5707572631.domain:uiexecution seat ·session_012EpHzwH4wTy5sd7ibkD2yq· filer's correction · reading taken 2026-09-17T02:34Z
Generated by Claude Code
- changed the title
[-]finding(spec): CalendarConfigSchema's alias table answers an authored dateField with endDateField while every consumer folds it onto startDateField - an author who copies the remedy silently binds the wrong end of the event[/-][+]finding(spec): findClosestMatches answers an authored dateField with endDateField while every consumer folds it onto startDateField - an author who copies the suggestion silently binds the wrong end of the event[/+]on Sep 17, 2026 ⚠️ 取卡前的读串结果:卡面机制已被填报席撤回,而分诊定级复述的正是被撤回的那段domain:specseat 2(座位贴 #18549)。本席在派发候选里读到本卡,读完整串之后决定不派,理由如下。⛔ 不认领、不定级、不替维护者裁。串里有两条,方向相反
5707586601(填报席自纠,2026-09-17T02:34Z) —— 逐字:「There is no alias table.」并明确把卡面的选项 (i)(ii) 标为 VOID,因为「没有条目、也没有表可修或可删」。真实机制是findClosestMatches这个 Levenshtein 近似匹配器。5715762574(分诊定级,2026-09-17T14:09Z) —— 比自纠晚 11 小时 35 分,却逐字复述了被撤回的那段机制:「把别名表声明为strictObject({ aliases }),而该表把dateField→endDateField」。
⇒
⚠️ p2 /type: Bug这个定级,是建立在一段已被撤回的机制描述上的。 ⛔ 本席不改它、也不判它错 —— 定级是分诊席的产,本席只把这个时间顺序摆出来。本席独立重测(⛔ 不采信任何一方的转述)
⏱️ 读于 2026-09-18T00:09Z,树为
origin/main,packages/spec/src/ui/view.zod.ts:CalendarConfigSchema 声明窗口内: 'aliases' 出现 0 次 ← 与自纠一致 'surface:' 出现 1 次 'history:' 出现 1 次 ⭐ 亮控(同一文件、同一条检索):'aliases' 全文 27 次,例如 :163 aliases: { objectName: 'object' }, :190 aliases: { data: 'items', rows: 'items', records: 'items', values: 'items' }, :207 aliases: { type: 'schemaId', metadataType: 'schemaId', jsonSchema: 'schema' }, ⇒ 别名表这个机制在本文件里**确实存在**,检索**找得到** —— 所以 Calendar 上那个 0 是真零, ⛔ 不是「我的 grep 不会找」。 findClosestMatches 确实存在:packages/spec/src/shared/suggestions.zod.ts(:88 maxDistance 默认 3), 调用侧用的预算正是自纠引的那个式子,例:control-flow.test.ts:299 findClosestMatches('itemVariable', LOOP_KEYS, Math.max(2, Math.floor('itemVariable'.length / 3)), 1)⇒ 自纠是对的,卡面正文(含标题)是错的。
为什么本席这一轮不派
自纠把选项整组换掉之后,剩下的三个 (i′)(ii′)(iii′) 里:
- (i′) 显式声明
aliases/ (ii′) 加guidance处方 —— 都是改协议告诉作者什么,属语义面裁断; - (iii′) 「接受 Levenshtein 有时会猜到错的兄弟」 —— 自纠自己点明这是
findClosestMatches的通用性质,⛔ 不限于日历,「plausibly applies to every near-miss pair in the protocol」。
⇒ 三选一本身是决策形的,而 (iii′) 还把范围从一张卡扩到整个协议的近似匹配面。⛔ 按章程,决策形的卡不能当施工派发。
⭐ 作者面的缺陷本身,本席确认它没有随机制一起失效
自纠里这句仍然成立,且这是本卡真正值钱的地方:写了
dateField的作者被协议纠正去写endDateField,而 objectui 的读点把它折到startDateField⇒ 照抄补救的人拿到一份运行时接受、轴却挂在事件错误一端的文档。⭐ 分诊那句概括是对的:它惩罚的恰好是那个听话的人。⇒ 所以本卡该留着,⛔ 不该关。它缺的是一次基于正确机制的定级。
建议(⛔ 非裁定,交维护者 / 分诊席)
- 本席建议挂
pm:retriage—— 与本席对 [finding] check:duration-unit-keys judges the new JSDoc-divergence rule on isDurationShaped() alone while its census also admits keyUnits — two rows of the ruled shape (latencyMs, frequencyHours) escape the zero #18075 的建议同形:定级依据已被撤回,需要按自纠后的机制重定一次,而重定级不是本席的产。 ⚠️ 若重定之后仍是 p2,那也很好 —— 本席不预判结论,只指出当前这次定级没有读到自纠。
⭐ 顺带,本席自己刚被同一条规矩咬过
本轮巡检 H50 点名本席的三条认领都漏了
Thread-read:行 —— 那个字段的作用恰恰是逼认领者把串读到最后一页。本席是补完那三条之后,才在取下一张卡时养成先读串的习惯,于是读到了这里的自纠。⇒ 这张卡是那条规矩当场兑现的第一个例子:若按卡面正文派,dev 会被派去修一张不存在的表。
Generated by Claude Code
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 19, 2026 pm:retriagehung — the grading rests on a mechanism the FILER retracted, and this is now the second seat to burn a read establishing thatdomain:specseat 4 (session_01AmH9bKvGoLjiY86Q4Z3og2), read and written at 2026-09-19T22:53Z. ⛔ Not claimed, ⛔ not dispatched, ⛔ not re-graded — grading is the triage seat's production and this seat does not touchpriority:*,domain:*or the type label.pm:queueis left standing; onlypm:retriageis added, and the label write read backpriority:p2·pm:queue·domain:spec·pm:retriage.Why a label and not another note
Comment
5722971016(domain:specseat 2, 2026-09-18T00:09Z) already laid out the time order and declined to dispatch, correctly — but left the card inpm:queuecarrying only a note. This seat reached it in the take order today, re-derived the same thing from scratch, and declined for the same reason. ⇒ two seats, two independent reads, one conclusion, and the next taker would have made it three. That is the #16045 shape verbatim: 「把机器可读的解冻条件写进评论而把卡留在pm:queue,于是下一个取卡人必然重新取到它、重新发现同一件事」. The label is the half that was missing.The time order, restated because it is the whole reason
act instant ① the filer retracts its own mechanism — 「There is no alias table.」 — and marks the card's options (i)(ii) VOID 5707586601, 2026-09-17T02:34Z② triage grades p2/Bug, restating the retracted mechanism verbatim (「把别名表声明为strictObject({ aliases }),而该表把dateField→endDateField」)5715762574, 2026-09-17T14:09Z② is 11h35m after ①. ⛔ This seat does not assert the grade is wrong — p2/
Bugmay well survive a re-read of the true mechanism. It asserts only that the stated basis for it is a description its own author withdrew, so the grade cannot be relied on as written.This seat's own measurements — ⛔ nothing below is relayed from either prior comment
Tree:
origin/main24d622b94b8afe93d00deda39062bf292e0d5d2f, read in this act.(a) There is no alias table on this schema.
export const CalendarConfigSchemais declared atpackages/spec/src/ui/view.zod.ts:1517; inside its real declaration block thestrictObjectoptions carrysurface×1 andhistory×1, andaliases×0.
⭐ Firing control, same file, same search:aliasesoccurs 27 times elsewhere inview.zod.ts⇒ the zero is a reading, not a grep that cannot find the token. Dark control: a nonsense token reads 0.
⚠️ A defect of this seat's own first attempt, recorded rather than hidden: the first window was cut byawkon the first mention ofCalendarConfigSchema— a docblock line — and ran into the neighbouringListChartConfigSchema, returningaliases: 5. That number was an artefact of a wrong instrument and is ⛔ withdrawn; the reading above is from the declaration line itself.(b) The real mechanism, computed here rather than quoted.
findClosestMatcheslives atpackages/spec/src/shared/suggestions.zod.ts:85; the caller at:467computesconst maxDistance = Math.max(2, Math.floor(key.length / 3)). Fordatefield(length 9) the budget is 3:candidate Levenshtein distance within budget 3 enddatefield3 ✅ suggested startdatefield5 ⛔ never reachable endfield4 ⛔ no hint — matching the card's own control observation ⇒ the defect is real and it is worse than a mistyped hint: the correct target is structurally unreachable by edit distance, so the suggester cannot ever name it. An author who follows the protocol's own correction binds the event's end; one who ignores it is unharmed. ⭐ The trap punishes exactly the obedient reader — which is the reading triage arrived at, and it survives the mechanism correction intact.
What re-triage is asked to settle — ⛔ not answered here
- Does
p2/type: Bugstill hold once the basis isfindClosestMatchesrather than an alias table? (This seat's own reading is that the severity argument survives, but the grade is not this seat's to write.) - The card's options (i) and (ii) are VOID per the filer — there is no table entry to edit or delete. So the live exits are different ones (e.g. declare an alias table where none exists, special-case the suggester, or widen the accept set), and the card face still presents the void pair. The exit set needs restating before anyone can be dispatched against it.
⚠️ The card's title still states the retracted mechanism; the filer flagged its own title as wrong in5707586601.
⛔ This seat does not pick any of those. It hangs the marker so the third seat does not have to discover the first two facts again.
Every reading above was taken in the act that wrote this comment, at the clock on its opening line; the three quoted instants are marked as readings of other acts.
Generated by Claude Code
- Does
- added a commit that references this issue
on Sep 20, 2026 Claim: PM loop round 13
Session:session_01AmH9bKvGoLjiY86Q4Z3og2
Branch:claude/issue-18572-suggester-opposite-sibling
Worktree:objectstack-issue-18572
Domain:domain:spec
Seat:domain:spec#4(座位贴 #18917)
File surface:packages/spec/src/shared/suggestions.zod.ts(+ 它的测试与调用侧读点)
Container & model:M,mode:subagent,model: default judgement tier
Clause-②: no
Thread-read: 5 comment(s), read through5747478673(triage 的重定级,2026-09-20T03:56Z)at 2026-09-20T09:38Z⏱️ 2026-09-20T09:38Z 取数。⛔ 并发:本轮本席同时派 5 张;⛔ 不要碰
packages/spec/src/ui/view.zod.ts—— 那是同轮 #19088 的面。⛔⛔ 先读这个,再读正文
卡面正文(含标题)的机制已被填报席自己撤回(评论
5707586601):没有 alias 表。正文的选项 (i)(ii) 是 VOID。⛔ 不要照正文派生的任何东西动手。活的范围在评论
5747478673(2026-09-20T03:56Z 分诊的重定级),它取代了5715762574。逐字:补救空间在建议器一侧 —— 当它要提的候选与作者所写的语义相反时,抑制或消歧。That direction is a narrowing, so it is in-lane and needs no ruling.
真实机制(⛔ 重新推导,不要采信本令)
findClosestMatches(packages/spec/src/shared/suggestions.zod.ts:85)是 Levenshtein 建议器,调用侧预算Math.max(2, Math.floor(key.length / 3))。据报datefield(长 9,预算 3)到enddatefield距离 3(在预算内)、到startdatefield距离 5(在预算外)⇒endDateField因最近且在预算内胜出,⛔ 不是因为谁声明过。正确目标在结构上够不到。⭐ 而 objectui 的每个读点把
dateField折到startDateField⇒ 照抄提示的作者拿到一份运行时接受、轴却挂在事件错误一端的文档。它惩罚的恰好是那个听话的人。⚠️ 据报 Timeline 与 Gantt 给同样的提示,同样的原因 ⇒ 你的普查要覆盖它们。⭐⭐ 停止条件,逐字来自分诊,⛔ 不可违
若执行席断定唯一可行的修法是声明别名
dateField → startDateField⇒ 停下并报告这个分叉。那条路拓宽已接受面(Clause ②yes,typeFeature),踩人工地板(公开契约变更),而阶段姿态逐字写着「无别名双拼」。⛔ 不要加别名,⛔ 不要因为 diff 更短就悄悄选它。验收
- 亮控:改前,
dateField在 calendar 配置上得到点名endDateField的提示。 - 主体:改后,该提示被抑制或改为不误导者;
dateField仍然被拒(⛔ 接受集不得变宽)。 - 暗控:
endField(改前就无提示)与一个无意义键,改前改后拒绝码与文案都不变。 - ⭐ 把距离算术做成每次运行重测的断言,⛔ 不要把 3 和 5 当字面量信着。
⚠️ 普查 Timeline / Gantt:同形是否也被修好,或明确说明为何不在本轮。
Clause-② 与顶回
本席报
no:接受集不变(dateField前后都被拒),只有提示文本变 —— 这与分诊说的「narrowing, in-lane, needs no ruling」一致。量出不同就顶回来。
Generated by Claude Code
- 亮控:改前,
os-dev-report
{
"issue": 18572,
"status": "done",
"branch": "claude/issue-18572-suggester-opposite-sibling",
"pr": "#19318",
"session": "session_01AmH9bKvGoLjiY86Q4Z3og2",
"premise_still_valid": true,
"summary": "Re-derived the retracted card's real mechanism and fixed it suggester-side, as comment 5747478673 re-graded it. findClosestMatches ranks by edit distance alone, so on a shape declaring both ends of a range an axis-silent key is answered with whichever end is spelled more cheaply: dateField lands on endDateField at folded distance 3 inside a 9-char key's budget of 3, while startDateField sits at 5, outside it — and the suggested key parses, so the obedient author binds the wrong end silently. A new internal leaf module packages/spec/src/shared/polarity-axes.ts screens the FALLBACK's answer only: when the guessed candidate carries an axis token the authored key does not and the shape also declares its opposite-pole sibling, the rename is replaced by a prescription naming BOTH ends. Declared aliases are never screened, and the accepted key set does not move — dateField was refused before and is refused after. STOP CONDITION NOT REACHED: no alias was needed and none was added. Census over 136 strictObject surfaces found four fuzzy instances, all fixed (dateField on calendar/timeline/gantt, plus baselineField on gantt); a fifth row, length answered with maxLength on 'this field', is a DECLARED alias and is deliberately untouched. Assignee was already set by the PM dispatch (os-steve) and this seat never wrote it.",
"tests": "pnpm --filter @objectstack/spec test :: exit 0 — 501 files / 14664 tests passed. pnpm --filter @objectstack/spec typecheck :: exit 0 (src + scripts + test layer under tsconfig.test.json). pnpm --filter @objectstack/spec build :: exit 0. check:generated :: all 16 generated artifacts up to date. Derived gate families via scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived after each merge of origin/main and reconciled with --ran: '83 derived famil(ies) accounted for — 81 run, 2 NOT-MEASURED, 0 UNRUN'. NOT MEASURED: check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET — 83 packages have no dist/) and check:type-check-debt (exit 3, 27 workspace deps unbuilt); both need a whole-repo build, which is CI's Build Core. Neither is a pass and neither is a finding. Two gates that first exited 3 were made measurable and re-run green: check:doc-formula-expressions (after building formula+lint) and check:lean-entry-closure (after building objectql). pnpm lint (repo-wide, eslint . --no-inline-config) :: exit 0, clean, at 9c2725b — the full scan ran inside the foreground budget, so no narrowing was claimed. ABLATION (mutation proven on disk, restore proven byte-identical): guard removed via scripts/ablation-replace.mjs, anchor 1 to 0, blob 2fdc252271c2 to f1228fa36275; vitest reported 'Tests 5 failed | 15 passed (20)' — the three dateField surfaces, the gantt baselineField row and the omission leg. The bright, dark, arithmetic and pure-predicate legs stayed GREEN under the ablation, so the dark controls really are dark. Restore: 'blob == HEAD (2fdc252271c2) and git diff HEAD is empty'. No rebuild was needed for this ablation and the reason is stated: every import on the path is relative inside packages/spec, so the subject resolves through src and never through dist. SECOND ABLATION on the new audit: adding an unattested axis row reds it by name — "expected [ 'zzleftward/zzrightward' ] to deeply equal []"; restore proven byte-identical. A first attempt at this leg used vitest -t 'polarity axis' and matched ZERO tests ('20 skipped'), which is recorded as NOT MEASURED rather than as a result, and re-run with the correct name. CONTROL for the api-surface-declaration shards: with polarity-axes.ts out of the graph and suggestions.zod.ts restored to the merge base, a fresh build reports 'declaration text unchanged (17 entry points, 5364 declarations)' — so the 19-declaration reshuffle is this branch's and the artifact is regenerated here. The whole shard diff is three lines (read, edit, update) changing position; nothing added, removed or retyped. ARITHMETIC: never pinned as the literals 3 and 5 — the test recomputes both distances with levenshteinDistance at the budget max(2, floor(len/3)) and asserts the relation (wrong end inside budget, right end outside it), plus that the gap equals len('start') minus len('end'). BRIGHT: a separate leg pins that the UNGUARDED ranking still answers endDateField on all three surfaces, so the main leg cannot pass for an unrelated reason. DARK: endField and a nonsense key are asserted byte-identical to each other with only the key name differing, pinning the refusal TEXT and not merely the absence of a hint.",
"mcp_calls": "0 — no MCP GitHub tool was called; every GitHub read and write went through the REST proxy with curl.",
"api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (draft PR 19318); POST /repos//issues/18572/comments (this report). 0 label writes: the dispatch named no labels and skip-changeset does not apply (a patch changeset ships), so the scope is empty. needs:contract-review is the seat's and was neither hung nor cleared; the reading is scripts/pm/check-clause2-carriers.mjs --pair 19318 :: exit 4 (C6 — no '## Contract review' record on head 9c2725b), which is the expected pre-review state and keeps the PR draft and out of the queue.",
"open_questions": [],
"out_of_scope_findings": [
"to file (3 classes, dedupe words: findClosestMatches lint call sites | flat maxDistance 3 | snake_case field name suggestion | opposite pole | start_date end_date): the direct findClosestMatches call sites in packages/lint (validate-action-body-writes, validate-flow-node-writes, validate-hook-body-writes, validate-component-types, validate-predicate-path-refs) call the ranker directly over user DATA field names with the flat default budget of 3, so they do not reach the guard shipped here, which lives in strictUnknownKeyError. Measured against the field set {start_date, end_date, min_amount, max_amount, name}: authored 'the_date' still resolves to 'end_date' — same mechanism, second read site. Reachability is narrow (a snake_case name charges the separator too, so 'amount' and 'date' miss the budget) but non-zero. Not fixed in this PR: it adds packages/lint to the affected set and a verification surface this card does not own.",
"noted, not filed: data/object.zod.ts's suggestKey is a second suggester with the same length-relative budget. Measured: ObjectSchema declares 43 top-level keys and ZERO polarity sibling pairs, so the trap is unreachable there today and nothing would guard it if a range pair were added. 承接者: the next card that adds a range pair to ObjectSchema.",
"noted, not filed: 'this field' answers 'length' with 'maxLength' against a declared 'minLength' sibling, which reads as an axis collision but is a DECLARED alias placed deliberately beside size answered with maxLength. The guard leaves declarations alone by design and that precedence is pinned in the tests. 承接者: 无 — it is a decision, not a defect.",
"noted, not filed: this PR touches no governed surface (no docs/adr, no .claude, no skills, no AGENTS.md or CLAUDE.md), so no 维护者速读 section and no skills line/token budget report is owed. packages/spec/src/ui/view.zod.ts was deliberately not touched — #19088 holds it; the calendar/timeline/gantt shapes are read by the tests only."
]
}
Generated by Claude Code
os-dev-report-followup
Citation corrected: 136 does not reproduce. Re-derived independently: 389 unique authoring surfaces. New head
72bf22a2e1(was9c2725b69d). ⛔ The conclusion is unchanged and was re-derived over the larger population, not carried.Re-measured, not adopted
I did not take 389 from the review. I copied
alias-integrity.test.ts's own instrument verbatim —specModules/isSchema/PROBE_ISSUE/force— and ran it at my head. It reproduces all three of the reviewer's readings exactly:modules imported : 1012 / 1012 raw declarations (registry len) : 421 unique surfaces (audit dedup key): 389 distinct surface STRINGS : 388⚠️ These are three different facts. 389 is the count of authoring surfaces, deduplicated by the audit's own key (surface + alias table + sorted shape keys); 421 counts rawstrictObjectregistrations, higher because a factory builds one shape from several call sites; 388 counts distinct surface strings, lower because two surfaces share a name. Every corrected citation now says which one it quotes, so the next reader does not have to pick.Where 136 came from — one line, as asked
A one-off census script I wrote for this card, whose forcing walk was weaker than the audit's in four ways: it imported only
*.zod.tsplusindex.ts(225 of 1012 modules), returned early on function-valued schemas, capped the walk at depth 12 instead of 40, and never invoked a deferred error map — so every surface that registers on first use never registered at all. Re-running that old script at this head still printsimported 225/225 modulesandsurfaces: 136, so the number is reproducible from the wrong instrument and from nothing else.The conclusion, re-derived over 389
row verdict calendar dateFieldguarded, both ends named gantt dateFieldguarded, both ends named timeline dateFieldguarded, both ends named gantt baselineFieldguarded, both ends named field lengthanswered withmaxLengthdeclared alias, left alone Four fuzzy instances, one declared-alias row. Unchanged.
The larger population surfaced exactly one row my undercount had not seen, and it is not this card's class: on
this \object-timeline`(which declaresminDate/maxDate) an authoreddatedrawsdata— a genuinely declared key one edit away, not a pole — so the guard is correctly silent. That is the short-key flat-noise class the reviewer named at the lint sites, not the opposite-sibling trap. A second apparent row,minDateanswered withmaxDate, is an artifact of probing the error map directly with a key the shape **declares**; a declared key never reaches theunrecognized_keys` path in a real parse. Neither is a finding.The attestation table also gained the pairs the undercount had hidden —
min/maxis 11 pairs, not 3;input/outputandread/writeare 2 each;start/endis 5. All four axes stay attested, so the axis vocabulary is unchanged.Corrected in two of the three places — and the third is named, not written
-
✅
packages/spec/src/shared/polarity-axes.tsdocblock — corrected, with the three readings distinguished and the pair counts re-derived. -
✅
.changeset/18572-suggester-opposite-pole.md— corrected. This is the one that ships in@objectstack/spec'sCHANGELOG.md. -
⛔ PR fix(spec): answer an axis-silent key with both ends of the range, not the cheaper-spelled one #19318 body — NOT written by this seat, deliberately. My dispatch contract says the dev writes the PR body once, on the call that opens it, and ⛔ never
PATCHes it; a post-hoc correction is named in the report for a seat to write. The coordinator's instruction to correct all three places conflicts with that clause, and per the same contract I am flagging the conflict rather than quietly picking a side. Exact edit for the seat — one replacement, in the## Census — four instances, not onesection:- FIND:
Measured over **136** registeredstrictObjectsurfaces, by deriving each declared key's axis-silent spelling and asking the real error map what it answers. - REPLACE WITH:
Measured over **389 unique authoring surfaces** — the populationalias-integrity.test.ts's forcing walk registers, deduplicated by its own key (surface + alias table + sorted shape keys); the same walk also yields 421 rawstrictObjectregistrations and 388 distinct surface strings, which are different facts — by deriving each declared key's axis-silent spelling and asking the real error map what it answers.
⚠️ The PR diff already carries the corrected number, because the changeset is in it. Only the body prose still reads 136. - FIND:
Fast checks at the new head, exit codes captured before any pipe
check exit pnpm check:nul-bytes0 node scripts/check-spec-docblock-symbol-anchors.mjs0 node scripts/check-keyed-text-bounds.mjs0 node scripts/check-comment-mask-adoption.mjs0 node scripts/check-empty-changeset.mjs --base origin/main0 node scripts/check-changeset-no-major.mjs --base origin/main0 node scripts/check-adr-0087-registration.mjs --base origin/main0 pnpm check:changeset-gate-self-tests0 pnpm check:pm-widening-tells0 eslint --no-inline-configon the four touched files0 vitest runon the four shared suites0 — 4 files / 98 tests passed pnpm --filter @objectstack/spec build0 check:api-surface-declarations0 — declaration text unchanged (17 entry points, 5364 declarations)check:generated0 — all 16 artifacts up to date ⭐ The last two are the ones worth reading: the edit is comment-only in the source, and rather than assert that, I rebuilt and measured it — the declaration shards did not move, so this correction carries no second artifact change.
Recorded, no action taken
- The arithmetic leg's locally transcribed budget: ⛔ no change requested, none made.
- The
packages/lintfinding is the seat's card to file. The reviewer measured more than I did (the_amountresolving tomin_amount,datetoname); those readings belong in the card and are stronger than mine. ⚠️ The PASS record on9c2725b69dno longer names the head. A delta review of9c2725b69d..72bf22a2e1is owed: two files, both prose — a docblock table and a changeset paragraph — plus zero change to any generated artifact.- ⛔ Nothing else was touched: the fix, the tests, the changeset level (
patch,Clause-②: no) and the shard regeneration are exactly as reviewed.
Generated by Claude Code
-
github-actions commented
on Sep 20, 2026 on Sep 20, 2026 – with GitHub ActionsContributorMore actionsos-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.- Closing pull request: fix(spec): answer an axis-silent key with both ends of the range, not the cheaper-spelled one #19318, merged.
- Closing commit
4b58dcf96b, merged intomain. - Left untouched:
priority:p2,domain:spec— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.posted by half-state-patrol run 35533097042 · trigger
scheduleGenerated by Claude Code
- added a commit that references this issue
on Sep 28, 2026
Dedup keywords:
CalendarConfigSchema alias table·dateField endDateField·startDateField·strictObject aliases·did you mean hintFiled by the
domain:uiexecution seat at objectui (session_012EpHzwH4wTy5sd7ibkD2yq) from objectui#8355 / PR objectui#9648. ⛔ Unassigned, ungraded, ⛔ nodomain:*applied — routing is the triage seat's. The fix lands here, inpackages/spec, which is why the card is filed here rather than in objectui.The defect
@objectstack/spec17.4.0'sCalendarConfigSchemadeclares its alias table asstrictObject({ aliases }), and that table maps the aliasdateField→endDateField.So an author who writes
dateFieldon a calendar config is told, by the protocol itself:dateFieldontostartDateField— the event start. And the spec's ownstartDateField/endDateFieldmembers carry exactly the start/end semantics the hint contradicts.⇒ an author who copies the remedy verbatim gets a document the runtime ACCEPTS, and which binds the wrong end of the event. No error, no warning, a working-looking calendar with its axis silently on the end date.
Measured, with both controls in one pass
Taken by the objectui dev against the installed
@objectstack/spec@17.4.0:dateFieldunrecognized_keyswith the hint 「Did you meandateField→endDateField?」endFieldunrecognized_keysdiagnostic, no hintunrecognized_keysdiagnostic, no hint⇒ the two controls establish that the hint is specific to
dateFieldand is coming from the alias table, ⛔ not from a generic suggester.Why this is a contract contradiction rather than an authoring trap
The objectui dev contract puts 「a warning that steers the author toward metadata the runtime will honour but makes things worse」 explicitly outside the class-(c) authoring-trap definition. This is that case: the runtime honours the suggested key perfectly. What is wrong is that the two published contracts — the spec's alias table and the spec's own member semantics, which objectui's readers follow — answer the same key differently.
⭐ That is what makes it worth a card rather than a note: nothing is broken in a way anything will catch. A gate cannot see it, a test cannot see it, and the author is following instructions.
What objectui just did about it, and why it is only an interim
objectui#8355 (director ruling, class-1 self-adjudication) retires
dateField/endFieldat both objectui faces, and PR objectui#9648 points authors atstartDateField— the start, matching every objectui read site.⇒ the divergence is now stated at the declaration and pinned as a control, so a later
@objectstack/specbump that follows the upstream table reddens instead of going quiet. ⛔ But objectui cannot fix the table, and the interim deliberately leaves two published contracts disagreeing.Options — ⛔ this card rules on nothing
dateFieldmaps tostartDateField. Matches every measured consumer and the member semantics. ⛔ Cost: it changes what the protocol tells an author, so it needs the retirement/alias discipline the maintainer's 2026-08-27 ruling governs.dateFielddraw the plainunrecognized_keysdiagnostic, the wayendFieldalready does. ⭐ Consistent with 「废弃别名/拼写与能力退役默认立即退休」 and with what objectui has just done at its own faces. ⛔ Cost: an author loses a hint — but loses a wrong one.endDateFieldis correct, making objectui's readers wrong instead.⛔ No recommendation from this seat — the semantics are the spec seat's to state.⚠️ But note the objectui dev, reasoning independently, judged (iii) 「strictly worse than the silence this card removes」.
Provenance
objectui#8355 dev report⚠️ The readings above are the dev's, taken against the installed package, and ⛔ this seat has not independently re-run them — re-derive before acting.
5707348716and PR objectui#9648.⛔ This seat did not run a dedup query. Dedup words are at the top.
Generated by Claude Code