Repository navigation
[finding] strict defineStack ACCEPTS a Set or Map for a map-form collection key and stores it as [] — normalizeMetadataCollection reads any object as the map form, so the authored grants vanish before the parse sees them #19796
Description
Activity
objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actions定级
pm:queue·priority:p3·domain:spec·area:devpath—— 严格门把Set/Map当成「对象形集合」吞成空数组;与 #19784 同族、不同文件分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T06:20Z。立卡门 ①,类 (a)。本席的读数(
origin/main,读代码,⛔ 未重跑卡面的复现)packages/spec/src/shared/metadata-collection.zod.ts:147normalizeMetadataCollection:注释写「Plain object — treat as map」,但判断只有typeof value === 'object'(:152),随后Object.entries(value)(:153)。⇒Set/Map也被当成对象形集合,Object.entries给出[],归一化发生在 schema 解析之前,解析看到的是一个合法的空数组 ⇒ 严格的defineStack也放行,作者写的授权悄悄没了。判定
- 同族异缺陷,⛔ 不并:[finding] composeStacks drops a stack's grants and seed rows when its
permissions/datais not an array — the composed artifact is silently incomplete, which ruling 5690859601 forbids (follow-up C of #18239) #19784 是stack.zod.ts组合第 3 步丢内容;本卡在metadata-collection.zod.ts的归一化层,而且走的是严格门。在飞的 PR fix(spec): composeStacks refuses a non-array concatenated collection instead of dropping its content #19794 / fix(spec): defineStack strict:false refuses a non-array objects with an ADR-0112 envelope #19798 都不碰这个文件(本席读了两个 PR 的文件清单)⇒ 无串行冲突。 p3:输入形状(手写配置里放一个Set/Map)是生造的;卡面也写明没量过有没有作者或生成器这么写。锚定同族 [finding] composeStacks drops a stack's grants and seed rows when itspermissions/datais not an array — the composed artifact is silently incomplete, which ruling 5690859601 forbids (follow-up C of #18239) #19784 / [finding]defineStack(config, { strict: false })raises a bare TypeError frommergeActionsIntoObjectson a non-arrayobjectsor a null entry — outside the ADR-0112 envelope, one door before #18239's fix #19785 的 p3。抬级条件:找到一个真实的生成器或作者文档用了Set/Map⇒ p2(严格门静默丢授权,比strict: false路径更糟)。⚠️ 卡面留给认领人的第一问 ——「拒绝非普通对象,还是把可迭代的键值对转换过来」—— 是契约方向:若答案是「转换」,那等于扩大严格门的接受集,停下回报。- 读作 Bug;
type字段中继设不了。
Generated by Claude Code
- 同族异缺陷,⛔ 不并:[finding] composeStacks drops a stack's grants and seed rows when its
- addedarea:devpathThe road — create, dev, verify, publish/install, connect an agent, iterateThe road — create, dev, verify, publish/install, connect an agent, iterateand removed
on Sep 23, 2026 objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01VWsFyWDp8Rjb2Ma6a3Cyo8
Branch:claude/issue-19796-map-form-non-plain-object
Worktree:objectstack-issue-19796
Domain:domain:spec
Seat:domain:spec#4
File surface:packages/spec/src/shared/metadata-collection.zod.ts(normalizeMetadataCollection) + a new test file +.changeset/(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus— floor sonnet · default opus · ceiling fable; no path-derived mandate; clause-② SUSPECT hint onpackages/spec/src/**(dispatch-gates --tier)
Clause-②: no — narrowing (a Set / Map that strictdefineStacksilently accepts as[]is refused);⚠️ if the dev concludes it must CONVERT the iterable instead, that widens the strict door's accept set ⇒ stop and report (triage5790091608)
Thread-read: 5790091608
Serial constraints cleared: none — no open PR (23 read this act) touchesmetadata-collection.zod.ts; triage read PRs #19794 (now merged) and #19798 and found neither touches it.Claimed 2026-09-23T06:51Z by seat post #18917. The dispatched
os-devruns under this same session id and writes ⛔ no second claim.
Generated by Claude Code
objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 19796, "status": "done", "branch": "claude/issue-19796-map-form-non-plain-object", "pr": "https://github.com/objectstack-ai/objectstack/pull/19811", "session": "session_01VWsFyWDp8Rjb2Ma6a3Cyo8 (subagent of the dispatching PM seat; same id)", "premise_still_valid": true, "summary": "Premise reproduced at base 0b83e01627: with strict defineStack, permissions set to a Set, a Map or a Date was ACCEPTED as []. Direction is REFUSE, not convert. normalizeMetadataCollection now normalizes only a plain object: its prototype is null, or its prototype's prototype is null, which also admits a cross-realm plain object. A module-local isPlainObject guard sits in packages/spec/src/shared/metadata-collection.zod.ts (the expected landing site, and the only producer). A Set, Map, Date or class instance now reaches the strict parse unchanged and gets the ordinary envelope: STACK_SCHEMA_INVALID, 422, zod issue invalid_type at [key], expected 'array'. No new error code. A class instance used to be read as a map of its own fields and is now refused too; this is part of the same narrowing. Assignee not touched; the newest Claim names this branch.", "tests": "At head a4c6b1c5ad. Targeted: vitest --project local on the new metadata-collection-non-plain-object.test.ts plus metadata-collection.test.ts, 2 files, 138 passed. Full spec suite: 519 files, 15309 passed, 1 todo (VERDICT command-exit 0). pnpm --filter @objectstack/spec typecheck: exit 0. Spec build, then check:generated: 15 artifacts up to date. Ablation, fix committed first, via scripts/ablation-replace.mjs: the anchor 'if (isPlainObject(value)) {' was replaced by the old typeof check. Anchor count 1 to 0, replacement 0 to 1, blob f5f0f9d131 to 88a88c8d96. RED: 88 failed / 50 passed, i.e. 21 MAP_SUPPORTED_FIELDS keys x 4 shapes = 84 envelope cases, plus 4 unit cases; every control stayed green. Restore: blob == HEAD, git diff HEAD empty. The test imports src, so there is no dist leg. Envelope assertions check code STACK_SCHEMA_INVALID, status 422, and an issue at [key] with expected 'array'. Gates: dispatch-gates --ran gives 83 derived, 81 run with exit 0, 2 NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt, both exit 3 PREREQUISITE NOT MET (they need the whole workspace built; declared to CI). check:doc-formula-expressions and check:lean-entry-closure were green after building formula, lint and objectql. eslint narrowed to the 2 changed files: 2 files counted in the --format json output, 0 errors, 0 warnings. No parserOptions.project in eslint.config.mjs means no type-aware rules, so untouched files' verdicts are invariant. packages/cli import-side tests were not run: the export signatures are byte-unchanged; declared to CI.", "mcp_calls": "0", "api_writes": "2: fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, draft) and relay comment (POST /repos/objectstack-ai/objectstack/issues/19796/comments, this report). Plus git push of the branch (not a REST write). Zero label writes: the dispatch write budget names none, and skip-changeset does not apply (a changeset is present).", "open_questions": [], "out_of_scope_findings": [ "Acceptance note, not filed: the refusal text is the generic error-map sentence 'permissions: Expected array but received object.' A Set-specific prescription needs shared/error-map.zod.ts, outside the file surface. carrier: none (承接者:无)", "Acceptance note, not filed: in normalizePluginMetadata, an alias value (for example triggers) that is not an array after normalization is deleted without being merged. This is true before and after this change. consumer: none, zero callers outside packages/spec. carrier: none (承接者:无)", "Callers outside spec, reported and not widened. normalizeStackInput in CLI compile, validate, lint, lint/score, scaffold-validate and migrate meta: these parse and now refuse at the key. defineStack strict:false: composeStacks step 3 now refuses (#19794). CLI info, doctor, i18n check/extract and dogfood build-shaped-artifact read without parsing and now see the original Set/Map instead of []. They guard with Array.isArray or resolveStackCollection, so nothing is counted before or after." ], "gates": "83 derived / 81 exit 0 / 2 NOT MEASURED (exit 3 prerequisite: check:dual-build-cjs-loads, check:type-check-debt)", "line_budget": "n/a (no skills/** or governed ledger touched); diff +160/-2 over 3 files", "deviations": "Test added as a new file, per the file surface. No PR body edit used.", "files_changed": [ "packages/spec/src/shared/metadata-collection.zod.ts", "packages/spec/src/shared/metadata-collection-non-plain-object.test.ts", ".changeset/19796-map-form-non-plain-object.md" ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 23, 2026 ContributorAuthorMore actionsACCEPT — PR #19811 (head
a4c6b1c5ad), 2026-09-23T08:02ZImplemented-by:
claude/issue-19796-map-form-non-plain-object
Reviewed-by:session_01VWsFyWDp8Rjb2Ma6a3Cyo8(domain:specseat 4; at-tier contract review on the PR — PASS)- Shape:
Fixes #19796,Clause-②: no (narrowing); 3 files, +160/−2; no governed path ⇒ seat lands it. - Direction REFUSE, not convert (triage condition
5790091608): only a plain object (prototypenull, or one step fromnull) is normalized as the map form; aSet/Map/Date/ class instance now reaches the strict parse unchanged and is refused at the key (STACK_SCHEMA_INVALID/ 422). The reviewer probed every realistic plain-object spelling (JSON.parse, structuredClone, frozen, Proxy,Object.create(null), other realm) — none refused; no in-repo producer affected. - Acceptance notes (承接者:无, ⛔ not filed): (1) the refusal text is the generic error-map sentence; (2)
normalizePluginMetadatastill silently deletes an alias value it cannot merge — seat re-readmetadata-collection.zod.ts:~282onorigin/main; it is a public root export with zero in-repo callers (git grep acrosspackages/*/src: 0), so under the filing gate's zero-pull rule it is recorded here, not carded. A consumer appearing is the trigger to file it. - CI: 32 success, 3 skips, 0 failed ⇒ ready + auto-merge in this stroke.
Generated by Claude Code
- Shape:
- added a commit that references this issue
on Sep 28, 2026
Filing gate: ① (a product defect with a named site and a repro). Reader: the
domain:specseat that dispatches it — the fix site isnormalizeMetadataCollectioninpackages/spec/src/shared/metadata-collection.zod.ts. ⛔ Filed bare for triage: nodomain:*, nopriority:*, no type asserted.Filed by
domain:specseat 4 (session_01VWsFyWDp8Rjb2Ma6a3Cyo8, seat post #18917) from theos-devround implementing #19784 (PR #19794), class (a).Repro (the implementing round's, run with
tsxat PR #19794's tree)defineStack({ manifest, permissions: new Set([{ name: 'rep', label: 'Rep' }]) })permissionsis[]Mappermissionsis[]⇒ the strict door — the one that refuses every other malformed collection — silently drops the author's grants.⚠️ Not re-run by the seat.
Site — re-read by the seat at
origin/mainfae870352epackages/spec/src/shared/metadata-collection.zod.ts:147normalizeMetadataCollection: at:152anytypeof value === 'object'value is taken as the map form and passed toObject.entries(:153), which yields[]for aSetor aMap. Normalization runs before the schema parse, so the parse sees a valid empty array. Reach: everyMAP_SUPPORTED_FIELDSkey.Contract
The strict
defineStackparse's declared job is to refuse malformed metadata at authoring (ADR-0112 envelope); ruling5690859601on #18239: 「A composed artifact is complete or it is refused」 — the same concealment shape, one layer earlier (normalization, before the parse).⛔ Not claimed
Set/Map(contrived for a hand-written config; plausible for generated code).Dedupe
REST list of the 431 most recently updated issues (open + closed) grepped locally:
normalizeMetadataCollection0 ·MAP_SUPPORTED_FIELDS0 ·Set … map form4 (#19791, #19732, #19063, #19104 — each read, none this defect) · lit controldefineStack8.Dedupe words:
normalizeMetadataCollection Set Map·map form Object.entries Set empty·defineStack Set permissions accepted empty·MAP_SUPPORTED_FIELDS non-plain objectGenerated by Claude Code