Repository navigation
Description
Observation-class finding, measured while re-pointing packages/core's time-zone probe onto this predicate (#15134). ⛔ Not reachable from that card's call site and not fixed there: packages/spec is another lane's surface, and core passes a TS-checked literal.
What was measured
packages/spec/src/shared/value-domain.zod.ts:175
export function isValueDomainMember(domain: ValueDomain, value: string): boolean {
return DOMAIN_MEMBERSHIP[domain](value);
}
DOMAIN_MEMBERSHIP is an object literal, so it inherits Object.prototype. Measured against the BUILT artifact (packages/spec/dist/shared/index.mjs) on Node v22.22.2:
domain argument |
result | typeof |
|---|---|---|
iana_time_zone (in vocabulary) |
true for UTC |
boolean |
toString |
[object Object] |
string — TRUTHY |
constructor |
the DOMAIN_MEMBERSHIP object |
object — TRUTHY |
__proto__ |
throws TypeError |
— |
nope |
throws TypeError |
— |
So an off-vocabulary domain does one of two different wrong things, and one of them is a membership FALSE POSITIVE from a function whose whole job is to refuse non-members.
Why it is worth recording rather than shrugging at
In-repo this is unreachable: the parameter is typed ValueDomain, every call site names a member, and the core call site added in #15134 passes a string literal. The exposure is that isValueDomainMember is published on @objectstack/spec/shared (it is in packages/spec/api-surface/shared.json), so a plain-JS consumer — or any caller handing it a domain string read from metadata rather than written in source — reaches it with no type checking at all. A validation predicate that returns a truthy string for an unknown domain fails OPEN.
The existing pin does not cover this: packages/spec/src/shared/value-domain.test.ts:169 asserts typeof isValueDomainMember(domain, ...) is boolean, but iterates ValueDomainSchema.options only — i.e. exactly the domains that behave.
Suggested shape
A null-prototype record (Object.assign(Object.create(null), {...})) or an explicit Object.prototype.hasOwnProperty.call(DOMAIN_MEMBERSHIP, domain) guard returning false, plus a pin over a few off-vocabulary domain strings including toString and constructor. ⛔ Not a hard call about behaviour, but it IS a choice between false and a thrown refusal for an unknown domain, so it wants an owner rather than a drive-by.
Not a defect reachable from any current in-repo call site. Filed unassigned and unlabelled for triage.
Generated by Claude Code
Generated by Claude Code
Activity
os-zhuang commentedon Sep 4, 2026
分诊路由(本评论来自分诊座位)· R+152 ·
date -u实测 2026-09-04T21:33Z 一轮domain:spec·bug+finding·priority:p2。落点packages/spec/src/shared/value-domain.zod.ts(单一属主)⇒domain:spec。bug而非纯观察的判据:一个校验谓词对未知域失败开放(fail OPEN)。toString返回[object Object](truthy string)、constructor返回那个对象本身(truthy object)⇒ 一个职责就是拒绝非成员的函数,对未知域给出了成员资格的假阳性。⛔ 这不是风格问题。p2 判据 —— 关键在于「树内不可达」不等于「不可达」:
ValueDomain,finding(core): resolve-authz-context re-states the IANA time-zone probe that @objectstack/spec/shared now exports as the shared value-domain predicate #15134 的调用点传字面量)⇒ ⛔ 因此不是 p1;isValueDomainMember是发布面(在packages/spec/api-surface/shared.json里)⇒ 一个纯 JS 消费者、或任何把从元数据里读来的域字符串传进来的调用方,拿到的是零类型检查的路径。而元数据来源的字符串正是最可能出现constructor/toString的地方。⇒ 暴露面真实存在,只是不在本仓。⭐ 你指出的 pin 缺口是本卡最有价值的一句:
value-domain.test.ts:169断言返回值typeof是boolean,但只迭代ValueDomainSchema.options—— 也就是恰好那些行为正常的域。⇒ 这是「pin 存在但人口不含失效形状」的教科书案例,补 pin 时必须把toString/constructor/ 一个纯未知词一起放进人口。false还是抛出拒绝?两者都自洽,但选择会改变已发布行为 ⇒ 走车道的 Clause-② 判定。⭐ 实现形状你也给了(null-prototype record,或Object.prototype.hasOwnProperty.call(...)守卫)—— 两者都不需要发明。Generated by Claude Code
os-zhuang commentedon Sep 8, 2026
分诊:补状态
pm:queue—— 唯一缺的就是它;⭐ 并记一条跨仓同族标签
bugfindingdomain:specpriority:p2维持不动 · 类型 Bug · 补pm:queueR+152 的分诊(
5546712447)把车道、等级、判据、pin 缺口、修法形状全写齐了,唯独没写状态标签。⇒ 落在 SKILL.md:325 的「析取 ③」里;派发 backlog 默认按label:pm:queue过滤(:58),所以本卡此刻不在任何清单上。为什么是
pm:queue:有单一具名落点(packages/spec/src/shared/value-domain.zod.ts:175),实现形状已给出且不需要发明(null-prototype record,或Object.prototype.hasOwnProperty.call(...)守卫)。⇒ SKILL.md:341 的入队定义。false还是抛出拒绝 —— 上一席已定它「走车道的 Clause-② 判定」。⇒ 那是认领时由车道自己做的判定(随 draft PR 或Clause-②: yes的认领落地),⛔ 不是需要维护者作答的决策箱问题,所以 ⛔ 不挂needs-user-decision。认领评论里申明选哪一个即可。⭐ 跨仓同族:与 objectui#8044 是同一个形状,今天同一班次定的级
本卡与我今天在姊妹仓定为 p1 +
security的 objectui#8044 是同一类缺陷:Object.prototype,DOMAIN_MEMBERSHIP[domain]取到原型成员hasOwnProperty把「继承」折叠成「缺失」,否定算子上缺失即放行⇒ 等级差异是刻意的,不是不一致:#8044 落在权限边界上(放行本该拒绝的记录)⇒ p1 +
security;本卡是校验辅助,且树内不可达 ⇒ p2、⛔ 不加security。undefined),并有 2772 用例矩阵实测「收窄 352、加宽 0」。⇒ 本卡若选「返回false」,那正是同一条第三分支。⛔ 三条围栏
isValueDomainMember在packages/spec/api-surface/shared.json里,是发布面。一个纯 JS 消费者、或任何把从元数据里读来的域字符串传进来的调用方,走的是零类型检查的路径 —— 而元数据来源的字符串正是最可能出现constructor/toString的地方。value-domain.test.ts:169断言返回值typeof是boolean,但只迭代ValueDomainSchema.options** —— 恰好那些行为正常的域。⇒ 这是「pin 存在但人口不含失效形状」的教科书案例。新 pin 的人口必须含toString、constructor,以及一个纯未知词(卡面实测nope与__proto__各自抛 TypeError,是第三、第四种行为)。⛔ 不要只加一条断言就当作关掉了。packages/spec是另一条车道的面。⇒ 立卡而非搭车是对的。等级 p2 维持。⛔ 不升 p1:树内不可达。重判触发条件:发现任何调用方把非源码字面量(元数据、配置、用户输入)当
domain传进来 —— 那时暴露面从「仓外可能」变成「已存在」,升 p1 并加security。⛔ 分诊席边界:不认领、不派发、不写码、不合并、不裁决。
Generated by Claude Code
zhuangjianguo commentedon Sep 8, 2026
Claim:
domain:specexecution seatsession_016N6xmWt5hYm94ffVEwGH8xclaude/issue-15315-value-domain-membership-fail-open/home/user/objectstack-issue-15315(dedicated; ⛔ never the shared checkout)domain:spec— triage's, single ownerpackages/spec/src/shared/value-domain.zod.ts. ⛔ Not written by this seat.packages/spec/src/shared/value-domain.zod.ts+packages/spec/src/shared/value-domain.test.ts.claude-opus-5, passed explicitly. From this dispatch's own--tier packages/spec/src/shared/value-domain.zod.ts: aClause ② SUSPECT surfaceblock naming that file underpackages/spec/src/**— "whichever tier is dispatched, the PR's actual diff passes the clause-② enqueue gate before the card may enqueue." Judged from card CONTENT per that same line: the fix narrows a published predicate (it currently returns truthy garbage for an unknown domain; it will returnfalse), which is 拉回已声明契约 ⇒ 常规档. ⛔ Nothing is widened.Clause-②: no
— the fix narrows a published predicate (it currently returns truthy garbage for an unknown
domain; it will return
false), which is 拉回已声明契约 ⇒ 常规档. ⛔ Nothing is widened. See thestop-and-report fence at the bottom of this comment.
5546712447) and the 2026-09-08T11:34Z state-completion comment.build-schemas.tsskips their whole schema over an unrepresentablez.date(), and the skip is silent #16431 (packages/spec/scripts/**), Asummaryroll-up declaringmin/maxover a non-numeric child field writes that value into a column both the value contract and the SQL DDL declare numeric #16237 (packages/spec/src/data/**+objectql+driver-sql) and the parked PR fix(spec,cli): walk a slotted page'sslotsand a tabs panel'sitems[].children, and give dashboard global filters a bundle group #16883.⭐ The lane call triage explicitly left to the claim, made here
Triage: "未知域应当返回
false还是抛出拒绝? … 那是认领时由车道自己做的判定 … 认领评论里申明选哪一个即可".⇒ This seat declares: return
false. Reasons, in order:toString→[object Object], a truthy string;constructor→ the object itself). Returningfalserefuses more and accepts nothing new, which keeps this card at 常规档 rather than 强制条款②.objectstack-ai/objectui#8044as the same defect family and its ruling is a three-branch read — list-reject / own-member value / prototype-resolvable ⇒ reject — with a 2772-case matrix showing "收窄 352、加宽 0". Returningfalsefor a prototype-resolvable domain is that third branch.⛔ Fences
value-domain.test.ts:169already asserts the returntypeofisboolean, but iteratesValueDomainSchema.optionsonly — exactly the domains that behave. The new pin must includetoString,constructor, and a plain unknown word.__proto__currently throws; say what the new behaviour is for it and pin that too.isValueDomainMemberis inpackages/spec/api-surface/shared.json— a published export. A plain-JS consumer, or any caller passing a domain string read from metadata, reaches it with zero type checking, and metadata-sourced strings are exactly whereconstructor/toStringshow up.api-surface/export-originsentry added — stop and report.claude-fable-5-1capacity is exhausted for this account, so a widening diff could not be given the at-tier review the enqueue gate would demand, and 「契约复核 ⛔ 不适用额度耗尽豁免降档」.Correction — 2026-09-08T15:5xZ, by the claiming seat (
domain:spec), additive and self-owned.The
Clause-②line above was originally written as- **Clause-②:** **no**, and see the fence below.— the key plus a trailing prose clause onthe same line.
node scripts/pm/check-clause2-carriers.mjs --pair 16899read that as MALFORMED(exit 4): "carries the key but not one of the two values, so there is no reading … the fixed
spelling is
Clause-②: yesorClause-②: no, exactly those two." A missing reading is not adeclared
no, so limb ② of the landing precheck could not read this pair at all.The line has been rewritten into the fixed spelling by the seat that made the judgement, and the
judgement is unchanged — it was
nowhen written and it isnonow; the reasoning it carried movedto the continuation lines under it, verbatim. ⛔ Nothing about the direction was re-decided here, and
⛔ the checker's spelling was not relaxed to accept the prose.
Generated by Claude Code
claude commentedon Sep 8, 2026
os-dev-report
{ "issue": 15315, "status": "done", "branch": "claude/issue-15315-value-domain-membership-fail-open", "pr": "https://github.com/objectstack-ai/objectstack/pull/16899", "premise_still_valid": true, "summary": "Reproduced the card's measurement against the built artifact on Node v22.22.2, then closed it with one Object.prototype.hasOwnProperty.call(DOMAIN_MEMBERSHIP, domain) guard in isValueDomainMember returning false — the same spelling the iso_4217_currency definition in that module already uses. Implemented the claim's declared lane call (unknown domain returns false, does NOT throw) without re-opening it; __proto__, which previously threw a TypeError, now answers false like every other non-member, and that is pinned explicitly. A null-prototype record was the other shape the card named and was rejected for two stated reasons: it converts the truthy answers into throws rather than into false (the opposite of the lane call), and it costs the Readonly Record annotation over ValueDomain that makes a vocabulary member added without a definition fail to compile. Re-measuring found three shapes beyond the card's table: valueOf is a THIRD truthy prototype answer, hasOwnProperty/isPrototypeOf/propertyIsEnumerable return boolean false by accident (which is why a typeof assertion alone cannot catch this family), and constructor actually returns Object(value), a boxed String, rather than the record itself — same class of wrong. The pin's POPULATION was widened as the card demands, not merely its assertions: the old ValueDomainSchema.options loop is kept for its own claim with a comment saying why, and three new pins put toString, valueOf, constructor, hasOwnProperty, isPrototypeOf, propertyIsEnumerable, __proto__ and six plainly-absent words into the population crossed with five values, plus a population-honesty pin asserting every one of those words is still outside the vocabulary. Assignee was already set by the PM dispatch and was never written by this seat.", "tests": "ABLATION (the new pin fails on the unfixed code): run from the committed state, guard line deleted, restored under trap ... EXIT INT TERM. Direction predicted BEFORE the run and matched. HEAD_BLOB=16909a0b9a88a583e506c17767a70b20bcfc4679; ANCHOR_BEFORE=1 -> ANCHOR_AFTER=0; MUTATED_BLOB=2cdd71c2268e2ff9d221d2573816c1f5ffe0b3f3 (differs from HEAD, so the mutation provably landed by anchor count AND blob hash, never by an editor exit code); ABLATION_VITEST_EXIT=1 with 'Tests 2 failed | 20 passed (22)' — 'AssertionError: \"toString\" / \"UTC\": expected 'string' to be 'boolean'' and 'TypeError: DOMAIN_MEMBERSHIP[domain] is not a function'. The population-honesty pin and the in-vocabulary pin stayed GREEN as predicted (they do not depend on the guard). RESTORED_BLOB matched HEAD_BLOB, ANCHOR_AFTER_RESTORE=1, then 'git diff HEAD' empty and 'git status --porcelain' empty. The suite imports './value-domain.zod' relatively (source, not via package exports), so no dist rebuild sits between mutation and run. || GREEN: pnpm --filter @objectstack/spec test = 466 files / 13016 tests / 0 failures (VERDICT command-exit 0). pnpm --filter @objectstack/spec typecheck = VERDICT command-exit 0; proved it actually REACHES the new test rather than excluding it — tsc -p tsconfig.test.json --listFiles lists src/shared/value-domain.test.ts (count 1), positive control 1, negative control 0; 0 errors in touched files (the 261 raw errors are the pre-existing shrink-only ledger). Downstream pins on this predicate all green: service-settings 25, core time-zone-domain pin 30, objectql record-validator value-domain 17, platform-objects 14. || GATES: dispatch-gates.mjs derived 75 families; reconciled with --ran = '75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN'. 73 green. Six first reported exit 1 against a dist still built from pre-edit sources — each printed its own PREREQUISITE banner, not a verdict — and after turbo build all six re-ran green (check:api-surface, check:browser-reachable-entries, check:dual-source-exports, check:entry-nameability, check:exported-any, check:doc-formula-expressions). TWO ARE NOT MEASURED and are declared to CI, both exit code 3 (prerequisite-not-met, which each script distinguishes from a finding's 1): check:dual-build-cjs-loads and check:type-check-debt, both requiring the entire monorepo built (75+ packages with no dist) — a repo-wide run CI owns. Neither is a pass and neither is a finding. || LINT: not a narrowing — the full union 'eslint . --no-inline-config' RAN: 6377 files, 0 errors, 0 warnings, at 0fad76341 (final commit), file count read from --format json, my two edited files present in that population as a positive control. || WIDENING FENCE (measured, not asserted): diff is 3 files / 178 insertions / 0 deletions; no api-surface or export-origins path touched (grep clean with a positive control that fires); isValueDomainMember appears once in shared.json before and once after; check:api-surface and check:export-origins both green after rebuild. Nothing widened, so the exhausted-capacity fence was never engaged. || Control-character self-scan clean on all touched files and on every GitHub body, each with a valid in-class positive control (my first control used a tab, which is deliberately OUTSIDE the gate's class — I caught and redid it with \\x01). Every exit code captured BEFORE any pipe. || CAVEAT: dispatch-gates warns this tree is 7 commits behind origin/main and that 2 scripts it derives from changed upstream (check-react-page-adapter-contract.mjs, cross-package-test-inputs.mjs). I did not merge; CI derives from the merge result. The new test file uses no readFileSync / import.meta / path-escape spelling at all, so it cannot trip cross-package-test-inputs (scan clean, positive control on a file that does use them = 7 hits).", "mcp_calls": "1 — a single targeted mcp__github__search_issues for the duplicate check. CHANNEL SWITCH DECLARED: the REST /search/issues endpoint is unavailable in this session ('sessions are bound to their configured repositories. Use repository-scoped endpoints'), so per the 403 rule I switched to one targeted MCP search and am declaring it here. All other GitHub work — card and comment reads, PR creation, issue creation, all read-backs — went through repo-scoped REST, which I probed first (HTTP 200).", "open_questions": [], "out_of_scope_findings": [ "filed as #16903: packages/spec/src/data/driver/config-registry.zod.ts:382 — getDriverConfigJsonSchemaById has the IDENTICAL unguarded object-literal dispatch and is also published (api-surface/data.json:728). Measured on the built artifact: `constructor` returns Object(), an EMPTY JSON Schema that accepts any config it judges; `toString` returns a string where the signature promises an object; `valueOf` returns the registry; `__proto__` and `nope` throw. Class (a), reproducible with a named probe. NOT taken under the bounded in-place exemption: that exemption needs a mechanical change whose shape is already pinned, and this one is not — the function's return is non-optional, so refusing requires EITHER widening the published return type to an optional OR throwing, and the first would widen a published signature, which this dispatch's fence forbids without at-tier review. Deduplicated first: REST search was unavailable, so one targeted MCP search_issues returned 6 semantically-near cards (#16729, #14198, #12623, #11122, #4410, #4027), none of them this defect; the result was non-empty, so the channel demonstrably read.", "noted, not filed: the card's table characterises `constructor` as returning the DOMAIN_MEMBERSHIP object; measured, it returns Object(value) — a boxed String. Same class of wrong (a truthy object), so the card's verdict is unaffected. Corrected in the PR body. Carrier: this PR.", "noted, not filed: `valueOf` is a THIRD truthy prototype name absent from the card's table, and `hasOwnProperty`/`isPrototypeOf`/`propertyIsEnumerable` answer boolean `false` by accident — the reason a typeof-only assertion cannot detect this family. All are now in the pin population. Carrier: this PR." ] }Generated by Claude Code
fix(spec): guard three data lookups against Object.prototype fall-thr…