You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
RLS: a write check refuses a scalar written to a declared multi-valued field (tags: x under contains(x)) while the write stores [x] and the same policy reads it; the check cannot reach the write door wrap rule #21238
Triage: first grade — bug · priority:p2 · domain:services · area:access · pm:blocked. Route A: the wrap rule moves beside temporalStorageForm in @objectstack/core, and the check folds it
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T20:52Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p2. It fails closed: a write the stored form would admit is refused, and nothing is let through. The named producer is a legacy client that sends a scalar to a multi-valued field. Read and write give two answers on one row, but no boundary is crossed.
Ruling: route A, home @objectstack/core (triage's; overturnable by the maintainer).
The wrap rule moves to packages/core/src/utils/, beside temporalStorageForm, the precedent the card names.
Both consumers already depend on @objectstack/core at runtime (@objectstack/objectql and @objectstack/plugin-security, read at this write). ⛔ Not @objectstack/spec/data: spec holds the predicate (isMultiValueField), and core holds the storage-form transforms.
The record validator calls it from there, and storedFormCheckJudge folds it. One rule, shared by the write door and the check. ⛔ No second copy.
⛔ Not B, which would add a runtime engine dependency to plugin-security. ⛔ Not C, which covers only half the path.
Routing.domain:services owns the defect (the RLS write check) and the fold. The move into packages/core and the validator's call site are domain:engine files, declared in the claim as its cross-lane surface and named in the PR before they are edited.
Why blocked. The fold point storedFormCheckJudge arrives with PR #21235 (Fixes #21109, in flight; read at this write).
Pins: the card's. tags: 'x' is admitted under contains('x') and tags: 'xy' is still refused, on both driver families. Read and write give one answer on the same row.
Claim: PM loop round 1 · 2026-10-01T21:40Z
Session: session_01DiCSbmJrkzNhuEAier4VoJ
Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-21238-multivalue-wrap-core
Worktree: objectstack-issue-21238
Domain: domain:services
Seat: domain:services#2 (seat post #21118)
File surface:
packages/core/src/utils/: the multi-valued wrap rule moves here beside temporalStorageForm, as one exported function with its unit tests, plus its export from @objectstack/core's entry. This is a domain:engine surface, declared cross-lane as triage's routing (5940327789) orders.
packages/objectql/src/validation/record-validator.ts: normalizeMultiValueFields calls the moved rule. No behaviour change at the write door. This is a domain:engine surface, declared cross-lane.
packages/plugins/plugin-security/src/rls-check-stored-form.ts: storedFormCheckJudge folds the rule for declared multi-valued columns, plus its tests.
Direction quoted to the dev from triage 5940327789: "The wrap rule moves to packages/core/src/utils/, beside temporalStorageForm … The record validator calls it from there, and storedFormCheckJudge folds it. One rule, shared by the write door and the check. ⛔ No second copy." Pins: tags: 'x' is admitted under contains('x') and tags: 'xy' is still refused, on both driver families; read and write give one answer on the same row.
os-dev-report
{
"issue": 21238,
"status": "done",
"branch": "claude/issue-21238-multivalue-wrap-core",
"pr": "#21253",
"session": "session_01DiCSbmJrkzNhuEAier4VoJ (this run is a subagent; the id is the dispatching session's, as the claim and the commit trailers carry it)",
"premise_still_valid": true,
"summary": "Route A is implemented in draft PR #21253 (Fixes #21238, Clause-②: no), head 8f7c43b, base main be5a83c. The premise was re-measured at be5a83c through ObjectQL.insert + SecurityPlugin + driver-sql (better-sqlite3) and driver-sqlite-wasm, as a member resolving a permission set, using == check. tags: 'x' under contains('x') was 403 while a system write stored ["x"] and the read showed it, and a by-id update was 403 the same way. The card graded this fail-closed only, but the negated form fails OPEN on main: under !record.tags.contains('x'), tags: 'x' was admitted and stored ["x"] while the read hid it. A policy that forbids tagging x was passed by sending a scalar. The predicate update was already right, because the engine wraps before that seam. Change: the per-value wrap rule moved to @objectstack/core as multiValueStorageForm (packages/core/src/utils/multi-value-storage-form.ts, beside temporalStorageForm, exported from the root entry). objectql's normalizeMultiValueFields keeps its column selection (SKIP_FIELDS, system, readonly, isMultiValueField) and calls the core rule. The write door is byte-for-byte unchanged: validator suite 917 before = 917 after, and a 2750-case differential probe against the verbatim base body found 0 diffs, with a positive control at 1. storedFormCheckJudge now puts each declared multi-valued column's post-image value through the core rule, on every image it judges. The column set is declaredMultiValueColumns: the spec's isMultiValueField over the declaration's type + multiple, never values. No copy of the rule lives in plugin-security. Comparands are deliberately left as written, because the read pairs none with the wrap: $contains / $notContains take a member, and every scalar comparison on such a column is refused 400 by the read. Measured side effect, stated in the PR and changeset: under scalar-comparison policies (==, !=, in, an ordering), which the read refuses 400, 'x' used to get the opposite verdict of ['x'] and now gets the same one. That the check evaluates those policies at all is out_of_scope_findings[0]. Docs: no sentence made false.",
"tests": "Run under os-verify-lock, at 242331e. The TS sources are identical to the final head 8f7c43b, which differs only in changeset text. (1) Core: vitest run --maxWorkers=2, both projects, 79 files / 2199 passed; multi-value-storage-form.test.ts has 16 cells. (2) objectql validator dir (src/validation/): 17 files / 917 passed BEFORE, with record-validator.ts restored from be5a83c (restore proven blob == HEAD 7455fdd0, git diff HEAD empty), and 17 files / 917 passed AFTER. objectql full suite, all 361 test files in three chunks: 122 files / 2528 + 121 / 1985 + 118 / 2574 = 7087 passed. (3) Differential probe (scratch tsx, never committed): base normalizeMultiValueFields (verbatim be5a83c:599-614) vs the new one, over 5 schema shapes x 22 field names x 25 values = 2750 cases, 0 diffs. CONTROL=1 plants one difference and reads diffs=1. (4) plugin-security: rls-check-stored-form.test.ts has 61 cells (38 before + 23 new). The full suite is 157 files, 3406 passed / 23 skipped (the #21235 report read 3383 at its head; this run adds 23 cells; the base count at be5a83c was NOT MEASURED). (5) Typecheck, core / objectql / plugin-security: all exit 0, check:test-typecheck OK (debt 4 / 234 / 0, held in the shrink-only ledgers). (6) Premise and red-on-main: the new pins at base (98b0e5b over base sources) gave 8 failed / 50 passed: the scalar-admitted tags / owners x2, !contains x2, by-id x2. A temporary probe file (deleted) printed write / stored / read per cell on both families, at base and at head, including the scalar-comparison table in the PR. (7) Ablations, on committed head a049a41 through scripts/ablation-replace.mjs in WRAP mode. Each anchor hit 1 -> 0, and each restore was proven: blob == HEAD 88931797 / 6b7d1004, git diff HEAD empty. A1, the fold removed (storedFormImage called without multiValue): 9 red = tags / owners 'x' admitted x2, !contains x2, by-id x2, 1 unit. The predicate update stays green (the engine wraps before that seam). A2, the wrap applied to a non-declared text column: 8 red = 3 text controls x2 + 2 unit. A3, the judge admits everything: 25 red = 12 negative pins x 2 drivers + 1 unit. A4, the core rule stops wrapping, rebuilt into dist: ablation-dist-preflight found the marker in packages/core/dist/index.js and index.cjs. Core unit 7 red, objectql record-validator.test.ts 1 red (through core dist), plugin-security file 16 red. A4 restore: core rebuilt, preflight --absent OK, whole-tree clean, 16 / 126 / 61 green. (8) Narrowed lint at 8f7c43b: eslint --no-inline-config --format json over the 6 changed .ts files, 6 files, 0 errors, 0 warnings, none ignored. Population: eslint.config.mjs:971 /*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}. eslint.config.mjs:327-329 enables no type-aware linting, so no untouched file's verdict can move. The full pnpm lint is CI's. (9) Control-byte self-scan over the 7 changed files: none. Docs grep: content/docs/ (outside releases/, references/) and skills/** for the scalar wrap and for contains / multi-valued under a check found no false sentence. Positive controls: validation-rules.mdx:473 and permissions/rls.mdx:64, both still true.",
"mcp_calls": "0 — no MCP GitHub tool was called; the card, its comments, PR #21235 and its dev report, and the PR read-back went through gh api GETs",
"api_writes": "3 relay dispatches, each a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]. (1) pr_create became POST /repos/objectstack-ai/objectstack/pulls (#21253, draft). Read-back: 11855 bytes sent, 11855 stored, identical (run 36937088475). (2) label-write --issue 21253 --assign os-bill became POST /repos//issues/21253/assignees. Read-back matches (run 36937157695). The labels size/m, documentation, tests and tooling are another actor's and were left alone. (3) This os-dev-report comment, through post-stamped --comment=21238, becomes POST /repos//issues/21238/comments. git push (not REST): 6 pushes (the empty-branch probe, then 5 commits), none forced. git fetch --shallow-since=2026-06-26 origin main (a read, deepening the shared clone for check:engine-split-ratio). No label write beyond the assignee: the dispatch named none, and skip-changeset does not apply.",
"open_questions": [
{
"question": "The Clause-② line. The claim and the dispatch fix Clause-②: no, and the PR body and changeset carry it verbatim. But this diff adds one @objectstack/core root export (multiValueStorageForm). It also widens the RLS accept set (tags: 'x' under contains now admitted) while narrowing a fail-open (!contains). The line answers whether a card widens the accept set or enlarges the public surface. Precedent is split. The move this card copies (temporalStorageForm into core, e5cf27d) shipped under Clause-②: no (narrowing), with core minor. a11faee, with three new core exports, declared Clause-②: yes (widening). #21235, whose accept-set widening is the same kind, declared no. check-widening-tells' T3 reads only packages/spec/api-surface, so no gate will flag a core export either way.",
"options": [
"A. Keep Clause-②: no (as dispatched). The export is a relocated rule with one consumer pair, the accept-set change is conformance to ruling A as in #21235, and the move precedent is no.",
"B. Re-declare Clause-②: yes (widening) in the PR body line and the changeset (a seat write). The PR then takes the contract-review tier, as a11faee did for new core exports."
],
"recommendation": "A, on the four axes. Business: the export serves exactly the two measured consumers (the write door and the check) and adds no capability an author can write. Long-term: route A copies temporalStorageForm, which was no, and an accept-set change that makes the check agree with the stored row is the ruling-A conformance #21235 declared no. Anti-AI-error: the honest-declaration argument points to B, because T3 is spec-only and cannot check a core export. That is the trade-off for the seat. Startup: A adds no review tier to a move. The bumps already follow the dispatch rule: core minor (export), plugin-security minor (accept set), objectql patch."
}
],
"out_of_scope_findings": [
"class: b · Seam: core:JSON_COLUMN_INCOMPATIBLE_OPERATORS (the read's refusal set on a JSON-stored column) → runtime: @objectstack/formula matchesFilterCondition as the RLS write check (plugin-security rls-check-stored-form.ts storedFormCheckJudge) · reach: the ObjectQL.insert door, measured at be5a83c and 8f7c43b on driver-sql (better-sqlite3) and driver-sqlite-wasm, as a member resolving a permission set, using == check, on a tags field. Under record.tags != 'x' and !(record.tags in ['x']), the member write of ['x'] (and now 'x') is ADMITTED and stored ["x"], while the read under the same policy answers INVALID_FILTER / 400. Under record.tags == 'x' and record.tags in ['x'], the write is 403 while the read is 400. Under record.tags > 'a', both are 400 (the matcher already refuses orderings there). · contract: core json-column-operator-refusal.ts module note, 'Two faces, one rule': the in-process faces (objectql's per-aggregation filter, driver-memory's filter gate) refuse these operators on the same declared fields, and the write check is an in-process face that evaluates them instead. Whether the permission-set authoring door refuses such a policy is NOT MEASURED. · family: the RLS write/read agreement family of #21109 and this card. Route it to that family's closing card if one exists, not a single-point card. · dedupe words: RLS check scalar comparison multi-valued JSON column INVALID_FILTER · matchesFilterCondition JSON_COLUMN_INCOMPATIBLE_OPERATORS write check · tags != policy admitted read refused",
"carrier: 承接者:无 · noted, not filed (PR Acceptance notes 2): security-plugin.ts:3294, the comment above the storedFormCheckJudge call, still names only date / datetime / time. It defers to rls-check-stored-form.ts, which now states the multi-valued half. The file is outside this claim's surface.",
"carrier: 承接者:无 · noted, not filed (PR Acceptance notes 3): boundary. The write door skips system / readonly multi-valued columns, and the check's declaration (declaredComparisonColumns: type + multiple) does not carry those flags. Only a hook's own scalar write to such a column is judged wrapped while it is stored as written. A caller's value there is stripped before the engine's seams. Inference from the engine's strip order, not measured."
],
"hypotheses": {
"H1": "Held at be5a83c on both families (the PR's premise table). tags: 'x' under contains('x') was 403 while the system write stored ["x"] and the read showed it. Plus a fail-open the card did not grade: !contains('x') admitted 'x' while the read hid the stored ["x"].",
"H2": "Done. The per-value rule moved, and the validator's column selection did not (SKIP_FIELDS, system, readonly, isMultiValueField stay in record-validator.ts). The blank skip (isMissing) moved with the rule, since it decides a value's form. The predicate stays the spec's isMultiValueField. Byte-for-byte: 917 = 917 validator tests, 7087 objectql tests, and a 0-diff differential over 2750 cases with a positive control.",
"H3": "Done for the image, on every path: the insert seam, the by-id image, and the by-id and predicate update seams. Declared types come from the object's metadata via declaredComparisonColumns, never from values (A2 pins it). The comparand half does not apply, because the read pairs no comparand with the wrap. Measured: the read refuses ==, !=, in and orderings on the column 400, and contains takes a member.",
"H4": "Delivered. The card's pins run on both families, plus 'xy' refused, the list spelling, !contains, select multiple, by-id and predicate updates, and two text controls. #21235's 38 temporal cells are still green. A1 (fold removed: the scalar stays refused) and A2 (wrap on a non-declared column) are both red as predicted. A3 shows every negative pin can fail. A4 shows both faces read the one core rule through dist."
},
"gates": {
"head": "8f7c43bf",
"derived": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 70 families (tree 8f7c43b, 7 paths vs merge base be5a83c; --repo checked against origin). The list is identical to the derivation at 242331e.",
"reconciliation": "dispatch-gates --ran (with exit codes) at 8f7c43b: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)",
"exit_0": 70,
"notable": "First sweep at 242331e. check:engine-split-ratio exited 2 (shallow-clone refusal) and exited 0 after its own remedy, git fetch --shallow-since=2026-06-26 origin main. check:dual-build-cjs-loads and check:i18n exited 3 (PREREQUISITE NOT MET) and exited 0 after a full turbo build (72/72). check:dts-closure and check:sourcemap-no-sources-content were re-measured after that build (17 -> 72 and 68 built packages swept). The final sweep at 8f7c43b is all exit 0: check:nul-bytes OK (9911 files), check:engine-double-contract OK, check:type-check-debt OK. AGENTS.md stayed unmodified throughout (git status clean after every turbo-driven gate).",
"ci": "in_progress (not waited on, per the dispatch contract)"
},
"line_budget": "359 changed lines (+324 / -35, 7 files) vs the 5000 human-merge threshold (dispatch-gates at 8f7c43b): under",
"deviations": [
"Clause-② stays no, verbatim from the claim and the dispatch, although the diff adds a core export and widens the RLS accept set. This is raised as open_questions[0], not chosen silently.",
"Bumps follow the dispatch rule ('patch unless an export or accept set widens'): @objectstack/core minor (one new root export), @objectstack/plugin-security minor (accept set widens), @objectstack/objectql patch (no behaviour change). The release group is fixed, so the release takes minor.",
"No comparand half for multi-valued columns. The read pairs none (measured: scalar comparisons are 400 at the read, and contains takes a member). Only the post-image is put into the form.",
"The pins extend the existing rls-check-stored-form.test.ts (two fields on its object, cells in its table, two update cells, a unit describe). Its per-driver describe was renamed from 'one temporal row' to 'one stored row', which renames the title prefix of the 38 existing cells. That is cosmetic, and every cell's body is unchanged.",
"The check cannot mirror the write door's system / readonly column skip, because the declaration it reads lacks those flags and declared-comparison-columns.ts is outside the claimed surface. This is documented as a boundary in the module note and the PR (Acceptance notes 3), not a breach.",
"Two temporary probe test files were created in packages/plugins/plugin-security/src and deleted before any commit: one for the premise, and one for the scalar-comparison table at base and at head. The base reading for the table restored rls-check-stored-form.ts from be5a83c without staging, then checked out HEAD again (blob == HEAD 88931797, diff empty).",
"The objectql full suite ran as three explicit file-list chunks (122 / 121 / 118 files), so each fit inside the foreground cap.",
"The tests and typechecks ran at 242331e. The final head 8f7c43b changes only the changeset's text. The gate union and the narrowed lint were rerun at 8f7c43b.",
"The dispatch named main as ef96c9e. The branch point is be5a83c, one commit later (#21239, driver-sql MySQL create / bulkCreate), with no overlap. origin/main has since moved to 3dc33b2 (#21222), with no overlap with the 7 changed files. No merge was made.",
"The deepening fetch for check:engine-split-ratio (shallow-since 2026-06-26) wrote shared .git state (the shallow boundary and refs/remotes/origin/main). It is additive history only."
],
"files_changed": [
"A .changeset/21238-multi-value-wrap-one-rule.md",
"M packages/core/src/index.ts",
"A packages/core/src/utils/multi-value-storage-form.test.ts",
"A packages/core/src/utils/multi-value-storage-form.ts",
"M packages/objectql/src/validation/record-validator.ts",
"M packages/plugins/plugin-security/src/rls-check-stored-form.test.ts",
"M packages/plugins/plugin-security/src/rls-check-stored-form.ts"
]
}
Not the reason: the RLS check's admission change. execution-duties.md: 「负边界:运行时权限/安全行为变更不是条款②」. That change executes triage's route A under ruling A and stays on the security floor as ruled.
Precedent:a11faee (new @objectstack/core exports) declared yes (widening). e5cf27d declared no (narrowing) for the temporalStorageForm export; under the spec-lane rule above, that was an under-declaration and is not followed.
Consequence: an isolated CONTRACT_REVIEW_TIER review is owed on the patched head before enqueue (Clause-②: yes is one of the three contract faces). Semver is unchanged: yes takes at least minor, and core is already minor.
os-dev-report
{
"issue": 21238,
"status": "done",
"round": "patch round 1 (Clause-② claim revision, answer B)",
"branch": "claude/issue-21238-multivalue-wrap-core",
"pr": "#21253",
"session": "session_01DiCSbmJrkzNhuEAier4VoJ (subagent; the dispatching session's id)",
"head": "e559c9ac",
"summary": "The changeset's declaration line is now exactly Clause-②: yes (widening). Its prose says the widening is the new @objectstack/core root export multiValueStorageForm, and that the plugin-security admission change is a security-floor behaviour change, not the declared widening. The bumps are unchanged: core minor, plugin-security minor, objectql patch. The yes arm requires at least one moved package at minor or above, and core minor satisfies it. One commit (e559c9a, model-free trailer pair), a normal push. The PR body was NOT patched by this run: see deviations[0]. The ready body is at the scratchpad path in deviations[0].",
"gates": {
"head": "e559c9ac",
"derived": "dispatch-gates --repo objectstack-ai/objectstack --commands at e559c9a: 70 families, the list identical to the one at 8f7c43b. check:api-surface is NOT derived (no packages/spec path in the diff).",
"run": "node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0 (1 non-breaking changeset seen) · --self-test: exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main: exit 0 (the level axis is NOT APPLICABLE locally, since there is no pull_request payload) · --self-test: exit 0 · node scripts/check-empty-changeset.mjs --base origin/main: exit 0 · --self-test: exit 0 · pnpm check:changeset-gate-self-tests: exit 0 · pnpm check:nul-bytes: exit 0 (9911 files) · pnpm check:objectui-changeset: exit 0 · pnpm check:pm-changeset-deadline-census: exit 0",
"level_axis_offline": "check-changeset-no-major --base origin/main --event, with a pull_request payload carrying the patched body (line 2 Clause-②: yes (widening)): exit 0, 'declares clause-② yes (widening), and it grades a package whose packages//src/ it moves at minor or above'. The same with the live body (line 2 Clause-②: no): exit 0, 'not declared'.",
"not_rerun": "The other 60 derived families were not re-run at e559c9a. The only change since 8f7c43b (all 70 exit 0) is changeset prose, +2 / -2 lines.",
"ci": "in_progress (not waited on)"
},
"line_budget": "359 changed lines (+324 / -35, 7 files) vs merge base be5a83c at e559c9a (dispatch-gates): under",
"files_changed": [
"M .changeset/21238-multi-value-wrap-one-rule.md (this round: the Clause line + two prose clauses)"
],
"mcp_calls": "0",
"api_writes": "1 relay dispatch: this comment, through post-stamped --comment=21238, became POST /repos//issues/21238/comments. git push (not REST): 1 normal push (8f7c43b -> e559c9a). No issue_patch was sent (deviations[0]).",
"deviations": [
"CONFLICT, raised and not chosen silently: the patch order's step 3 (issue_patch on PR #21253's body) conflicts with the dev definition: 'PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不 PATCH;事后要改的报告点名改法,席位代写', and the definition prevails over dispatch words on a conflict. So no body write was made. The seat's one write is ready: /tmp/claude-0/-home-user-objectstack/648b83a3-c5c3-520e-b41a-19ee2af1efbe/scratchpad/issue-21238/pr-body-patched.md (12176 bytes). It is the live body (read back at e559c9a and byte-identical to what was sent) with three edits. Line 2: Clause-②: no -> Clause-②: yes (widening). Line 33, the Changes bullet: the core minor is marked as the declared widening, and the plugin-security admission change as a security-floor change, not Clause-②. Line 94, Acceptance notes 4: rewritten to say the line declares the new core export, the claim's no was revised on #21238, and the families re-ran green at e559c9a. It keeps Fixes #21238 on line 1 and the session-URL footer. There is no closing keyword next to another card number, no less-than character, no control byte, and the level axis passes over it offline (gates.level_axis_offline). Until it is written, CI reads Clause-②: no from the body, and the level axis stands down green."
]
}
三行表(contains 时 403 而读可见;!contains 时放行而读隐藏;按 id 更新时 403 而读可见)与 dev 在 be5a83cf 的前提实测一致。
"the image's value on every field the object declares multi-valued goes through the same rule the write door stores it by" 对应 storedFormCheckJudge 经 declaredMultiValueColumns 调 multiValueStorageForm,一致。
"A lone scalar now gets exactly the verdict its stored list gets, on the insert, a by-id update and a predicate update" 与 H3 及 pin 一致。
core 段 "wraps a string, a number or a boolean into a one-member list and returns every other value as the same value" 对应 multi-value-storage-form.ts 及其 16 格单测,一致。
Activity
objectstack-fleet commentedon Oct 1, 2026
Triage: first grade —
bug·priority:p2·domain:services·area:access·pm:blocked. Route A: the wrap rule moves besidetemporalStorageFormin@objectstack/core, and the check folds itTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-01T20:52Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #21109
Why p2. It fails closed: a write the stored form would admit is refused, and nothing is let through. The named producer is a legacy client that sends a scalar to a multi-valued field. Read and write give two answers on one row, but no boundary is crossed.
Ruling: route A, home
@objectstack/core(triage's; overturnable by the maintainer).packages/core/src/utils/, besidetemporalStorageForm, the precedent the card names.@objectstack/coreat runtime (@objectstack/objectqland@objectstack/plugin-security, read at this write). ⛔ Not@objectstack/spec/data: spec holds the predicate (isMultiValueField), and core holds the storage-form transforms.storedFormCheckJudgefolds it. One rule, shared by the write door and the check. ⛔ No second copy.plugin-security. ⛔ Not C, which covers only half the path.Routing.
domain:servicesowns the defect (the RLS write check) and the fold. The move intopackages/coreand the validator's call site aredomain:enginefiles, declared in the claim as its cross-lane surface and named in the PR before they are edited.Why blocked. The fold point
storedFormCheckJudgearrives with PR #21235 (Fixes #21109, in flight; read at this write).Pins: the card's.
tags: 'x'is admitted undercontains('x')andtags: 'xy'is still refused, on both driver families. Read and write give one answer on the same row.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Claim: PM loop round 1 · 2026-10-01T21:40Z
Session:
session_01DiCSbmJrkzNhuEAier4VoJAccount:
os-bill(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-21238-multivalue-wrap-coreWorktree:
objectstack-issue-21238Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface:
packages/core/src/utils/: the multi-valued wrap rule moves here besidetemporalStorageForm, as one exported function with its unit tests, plus its export from@objectstack/core's entry. This is adomain:enginesurface, declared cross-lane as triage's routing (5940327789) orders.packages/objectql/src/validation/record-validator.ts:normalizeMultiValueFieldscalls the moved rule. No behaviour change at the write door. This is adomain:enginesurface, declared cross-lane.packages/plugins/plugin-security/src/rls-check-stored-form.ts:storedFormCheckJudgefolds the rule for declared multi-valued columns, plus its tests.⛔ No second copy of the rule. Stop on breach; explain in the report.
Container & model:
M,mode:subagent,model: opus(default tier; a move plus a fold, security-adjacent).Clause-②: no
Thread-read: 5940327789
Release from
pm:blocked, double check: ① the latest transition comment (5940327789) carries the line-startBlocked-by: #21109, and [Decision] #20822 F7: retiring formula's whole-day copy — the RLS write check judges the raw post-image, so deleting the copy refuses writes the same policy's read shows #21109 is closedcompleted(PR fix(plugin-security): the RLS write check judges a date / datetime / time column in its stored form #21235 merged asef96c9ed). ② The card has no newer merged PR of its own. Re-derived for new blockers againstorigin/mainatef96c9ed: none of the open PRs touchespackages/core/src/utils/,record-validator.ts,rls-check-stored-form.tsorsecurity-plugin.ts. The only in-flight claim namingplugin-securityis spec(forms): retirepublicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180's, forzero-set-deny-baseline.test.ts, which is disjoint.Selection:
priority:p2(triage5940327789, route A, home@objectstack/core). It is the lane's next free card. plugin-security: an org member reading a colleague's sys_user row is served the Admin-group fields (last_login_ip/at, failed_login_count, locked_until, ban_*, phone_number) — and through sys_activity, their sign-in history #21237 is waiting for triage's direction, and automation: a host's per-kernel scheduled-work refusal is reported with the deployment sentence —SCHEDULED_WORK_DISABLED_REASONtells a free-plan tenant to setOS_AUTOMATION_SCHEDULED_WORK_ENABLED=true(the #19834 seam has no reason slot) #21110 is waiting for the maintainer.Direction quoted to the dev from triage
5940327789: "The wrap rule moves topackages/core/src/utils/, besidetemporalStorageForm… The record validator calls it from there, andstoredFormCheckJudgefolds it. One rule, shared by the write door and the check. ⛔ No second copy." Pins:tags: 'x'is admitted undercontains('x')andtags: 'xy'is still refused, on both driver families; read and write give one answer on the same row.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{
"issue": 21238,
"status": "done",
"branch": "claude/issue-21238-multivalue-wrap-core",
"pr": "#21253",
"session": "session_01DiCSbmJrkzNhuEAier4VoJ (this run is a subagent; the id is the dispatching session's, as the claim and the commit trailers carry it)",
"premise_still_valid": true,
"summary": "Route A is implemented in draft PR #21253 (Fixes #21238, Clause-②: no), head 8f7c43b, base main be5a83c. The premise was re-measured at be5a83c through ObjectQL.insert + SecurityPlugin + driver-sql (better-sqlite3) and driver-sqlite-wasm, as a member resolving a permission set, using == check. tags: 'x' under contains('x') was 403 while a system write stored ["x"] and the read showed it, and a by-id update was 403 the same way. The card graded this fail-closed only, but the negated form fails OPEN on main: under !record.tags.contains('x'), tags: 'x' was admitted and stored ["x"] while the read hid it. A policy that forbids tagging x was passed by sending a scalar. The predicate update was already right, because the engine wraps before that seam. Change: the per-value wrap rule moved to @objectstack/core as multiValueStorageForm (packages/core/src/utils/multi-value-storage-form.ts, beside temporalStorageForm, exported from the root entry). objectql's normalizeMultiValueFields keeps its column selection (SKIP_FIELDS, system, readonly, isMultiValueField) and calls the core rule. The write door is byte-for-byte unchanged: validator suite 917 before = 917 after, and a 2750-case differential probe against the verbatim base body found 0 diffs, with a positive control at 1. storedFormCheckJudge now puts each declared multi-valued column's post-image value through the core rule, on every image it judges. The column set is declaredMultiValueColumns: the spec's isMultiValueField over the declaration's type + multiple, never values. No copy of the rule lives in plugin-security. Comparands are deliberately left as written, because the read pairs none with the wrap: $contains / $notContains take a member, and every scalar comparison on such a column is refused 400 by the read. Measured side effect, stated in the PR and changeset: under scalar-comparison policies (==, !=, in, an ordering), which the read refuses 400, 'x' used to get the opposite verdict of ['x'] and now gets the same one. That the check evaluates those policies at all is out_of_scope_findings[0]. Docs: no sentence made false.",
"tests": "Run under os-verify-lock, at 242331e. The TS sources are identical to the final head 8f7c43b, which differs only in changeset text. (1) Core: vitest run --maxWorkers=2, both projects, 79 files / 2199 passed; multi-value-storage-form.test.ts has 16 cells. (2) objectql validator dir (src/validation/): 17 files / 917 passed BEFORE, with record-validator.ts restored from be5a83c (restore proven blob == HEAD 7455fdd0, git diff HEAD empty), and 17 files / 917 passed AFTER. objectql full suite, all 361 test files in three chunks: 122 files / 2528 + 121 / 1985 + 118 / 2574 = 7087 passed. (3) Differential probe (scratch tsx, never committed): base normalizeMultiValueFields (verbatim be5a83c:599-614) vs the new one, over 5 schema shapes x 22 field names x 25 values = 2750 cases, 0 diffs. CONTROL=1 plants one difference and reads diffs=1. (4) plugin-security: rls-check-stored-form.test.ts has 61 cells (38 before + 23 new). The full suite is 157 files, 3406 passed / 23 skipped (the #21235 report read 3383 at its head; this run adds 23 cells; the base count at be5a83c was NOT MEASURED). (5) Typecheck, core / objectql / plugin-security: all exit 0, check:test-typecheck OK (debt 4 / 234 / 0, held in the shrink-only ledgers). (6) Premise and red-on-main: the new pins at base (98b0e5b over base sources) gave 8 failed / 50 passed: the scalar-admitted tags / owners x2, !contains x2, by-id x2. A temporary probe file (deleted) printed write / stored / read per cell on both families, at base and at head, including the scalar-comparison table in the PR. (7) Ablations, on committed head a049a41 through scripts/ablation-replace.mjs in WRAP mode. Each anchor hit 1 -> 0, and each restore was proven: blob == HEAD 88931797 / 6b7d1004, git diff HEAD empty. A1, the fold removed (storedFormImage called without multiValue): 9 red = tags / owners 'x' admitted x2, !contains x2, by-id x2, 1 unit. The predicate update stays green (the engine wraps before that seam). A2, the wrap applied to a non-declared text column: 8 red = 3 text controls x2 + 2 unit. A3, the judge admits everything: 25 red = 12 negative pins x 2 drivers + 1 unit. A4, the core rule stops wrapping, rebuilt into dist: ablation-dist-preflight found the marker in packages/core/dist/index.js and index.cjs. Core unit 7 red, objectql record-validator.test.ts 1 red (through core dist), plugin-security file 16 red. A4 restore: core rebuilt, preflight --absent OK, whole-tree clean, 16 / 126 / 61 green. (8) Narrowed lint at 8f7c43b: eslint --no-inline-config --format json over the 6 changed .ts files, 6 files, 0 errors, 0 warnings, none ignored. Population: eslint.config.mjs:971 /*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}. eslint.config.mjs:327-329 enables no type-aware linting, so no untouched file's verdict can move. The full pnpm lint is CI's. (9) Control-byte self-scan over the 7 changed files: none. Docs grep: content/docs/ (outside releases/, references/) and skills/** for the scalar wrap and for contains / multi-valued under a check found no false sentence. Positive controls: validation-rules.mdx:473 and permissions/rls.mdx:64, both still true.",
"mcp_calls": "0 — no MCP GitHub tool was called; the card, its comments, PR #21235 and its dev report, and the PR read-back went through gh api GETs",
"api_writes": "3 relay dispatches, each a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]. (1) pr_create became POST /repos/objectstack-ai/objectstack/pulls (#21253, draft). Read-back: 11855 bytes sent, 11855 stored, identical (run 36937088475). (2) label-write --issue 21253 --assign os-bill became POST /repos//issues/21253/assignees. Read-back matches (run 36937157695). The labels size/m, documentation, tests and tooling are another actor's and were left alone. (3) This os-dev-report comment, through post-stamped --comment=21238, becomes POST /repos//issues/21238/comments. git push (not REST): 6 pushes (the empty-branch probe, then 5 commits), none forced. git fetch --shallow-since=2026-06-26 origin main (a read, deepening the shared clone for check:engine-split-ratio). No label write beyond the assignee: the dispatch named none, and skip-changeset does not apply.",
"open_questions": [
{
"question": "The Clause-② line. The claim and the dispatch fix
Clause-②: no, and the PR body and changeset carry it verbatim. But this diff adds one @objectstack/core root export (multiValueStorageForm). It also widens the RLS accept set (tags: 'x' under contains now admitted) while narrowing a fail-open (!contains). The line answers whether a card widens the accept set or enlarges the public surface. Precedent is split. The move this card copies (temporalStorageForm into core, e5cf27d) shipped underClause-②: no (narrowing), with core minor. a11faee, with three new core exports, declaredClause-②: yes (widening). #21235, whose accept-set widening is the same kind, declaredno. check-widening-tells' T3 reads only packages/spec/api-surface, so no gate will flag a core export either way.","options": [
"A. Keep
Clause-②: no(as dispatched). The export is a relocated rule with one consumer pair, the accept-set change is conformance to ruling A as in #21235, and the move precedent isno.","B. Re-declare
Clause-②: yes (widening)in the PR body line and the changeset (a seat write). The PR then takes the contract-review tier, as a11faee did for new core exports."],
"recommendation": "A, on the four axes. Business: the export serves exactly the two measured consumers (the write door and the check) and adds no capability an author can write. Long-term: route A copies temporalStorageForm, which was
no, and an accept-set change that makes the check agree with the stored row is the ruling-A conformance #21235 declaredno. Anti-AI-error: the honest-declaration argument points to B, because T3 is spec-only and cannot check a core export. That is the trade-off for the seat. Startup: A adds no review tier to a move. The bumps already follow the dispatch rule: core minor (export), plugin-security minor (accept set), objectql patch."}
],
"out_of_scope_findings": [
"class: b · Seam: core:JSON_COLUMN_INCOMPATIBLE_OPERATORS (the read's refusal set on a JSON-stored column) → runtime: @objectstack/formula matchesFilterCondition as the RLS write check (plugin-security rls-check-stored-form.ts storedFormCheckJudge) · reach: the ObjectQL.insert door, measured at be5a83c and 8f7c43b on driver-sql (better-sqlite3) and driver-sqlite-wasm, as a member resolving a permission set, using == check, on a
tagsfield. Under record.tags != 'x' and !(record.tags in ['x']), the member write of ['x'] (and now 'x') is ADMITTED and stored ["x"], while the read under the same policy answers INVALID_FILTER / 400. Under record.tags == 'x' and record.tags in ['x'], the write is 403 while the read is 400. Under record.tags > 'a', both are 400 (the matcher already refuses orderings there). · contract: core json-column-operator-refusal.ts module note, 'Two faces, one rule': the in-process faces (objectql's per-aggregation filter, driver-memory's filter gate) refuse these operators on the same declared fields, and the write check is an in-process face that evaluates them instead. Whether the permission-set authoring door refuses such a policy is NOT MEASURED. · family: the RLS write/read agreement family of #21109 and this card. Route it to that family's closing card if one exists, not a single-point card. · dedupe words: RLS check scalar comparison multi-valued JSON column INVALID_FILTER · matchesFilterCondition JSON_COLUMN_INCOMPATIBLE_OPERATORS write check · tags != policy admitted read refused","carrier: 承接者:无 · noted, not filed (PR Acceptance notes 2): security-plugin.ts:3294, the comment above the storedFormCheckJudge call, still names only date / datetime / time. It defers to rls-check-stored-form.ts, which now states the multi-valued half. The file is outside this claim's surface.",
"carrier: 承接者:无 · noted, not filed (PR Acceptance notes 3): boundary. The write door skips system / readonly multi-valued columns, and the check's declaration (declaredComparisonColumns: type + multiple) does not carry those flags. Only a hook's own scalar write to such a column is judged wrapped while it is stored as written. A caller's value there is stripped before the engine's seams. Inference from the engine's strip order, not measured."
],
"hypotheses": {
"H1": "Held at be5a83c on both families (the PR's premise table). tags: 'x' under contains('x') was 403 while the system write stored ["x"] and the read showed it. Plus a fail-open the card did not grade: !contains('x') admitted 'x' while the read hid the stored ["x"].",
"H2": "Done. The per-value rule moved, and the validator's column selection did not (SKIP_FIELDS, system, readonly, isMultiValueField stay in record-validator.ts). The blank skip (isMissing) moved with the rule, since it decides a value's form. The predicate stays the spec's isMultiValueField. Byte-for-byte: 917 = 917 validator tests, 7087 objectql tests, and a 0-diff differential over 2750 cases with a positive control.",
"H3": "Done for the image, on every path: the insert seam, the by-id image, and the by-id and predicate update seams. Declared types come from the object's metadata via declaredComparisonColumns, never from values (A2 pins it). The comparand half does not apply, because the read pairs no comparand with the wrap. Measured: the read refuses ==, !=, in and orderings on the column 400, and contains takes a member.",
"H4": "Delivered. The card's pins run on both families, plus 'xy' refused, the list spelling, !contains, select multiple, by-id and predicate updates, and two text controls. #21235's 38 temporal cells are still green. A1 (fold removed: the scalar stays refused) and A2 (wrap on a non-declared column) are both red as predicted. A3 shows every negative pin can fail. A4 shows both faces read the one core rule through dist."
},
"gates": {
"head": "8f7c43bf",
"derived": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 70 families (tree 8f7c43b, 7 paths vs merge base be5a83c; --repo checked against origin). The list is identical to the derivation at 242331e.",
"reconciliation": "dispatch-gates --ran (with exit codes) at 8f7c43b: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)",
"exit_0": 70,
"notable": "First sweep at 242331e. check:engine-split-ratio exited 2 (shallow-clone refusal) and exited 0 after its own remedy, git fetch --shallow-since=2026-06-26 origin main. check:dual-build-cjs-loads and check:i18n exited 3 (PREREQUISITE NOT MET) and exited 0 after a full turbo build (72/72). check:dts-closure and check:sourcemap-no-sources-content were re-measured after that build (17 -> 72 and 68 built packages swept). The final sweep at 8f7c43b is all exit 0: check:nul-bytes OK (9911 files), check:engine-double-contract OK, check:type-check-debt OK. AGENTS.md stayed unmodified throughout (git status clean after every turbo-driven gate).",
"ci": "in_progress (not waited on, per the dispatch contract)"
},
"line_budget": "359 changed lines (+324 / -35, 7 files) vs the 5000 human-merge threshold (dispatch-gates at 8f7c43b): under",
"deviations": [
"Clause-② stays
no, verbatim from the claim and the dispatch, although the diff adds a core export and widens the RLS accept set. This is raised as open_questions[0], not chosen silently.","Bumps follow the dispatch rule ('patch unless an export or accept set widens'): @objectstack/core minor (one new root export), @objectstack/plugin-security minor (accept set widens), @objectstack/objectql patch (no behaviour change). The release group is fixed, so the release takes minor.",
"No comparand half for multi-valued columns. The read pairs none (measured: scalar comparisons are 400 at the read, and contains takes a member). Only the post-image is put into the form.",
"The pins extend the existing rls-check-stored-form.test.ts (two fields on its object, cells in its table, two update cells, a unit describe). Its per-driver describe was renamed from 'one temporal row' to 'one stored row', which renames the title prefix of the 38 existing cells. That is cosmetic, and every cell's body is unchanged.",
"The check cannot mirror the write door's system / readonly column skip, because the declaration it reads lacks those flags and declared-comparison-columns.ts is outside the claimed surface. This is documented as a boundary in the module note and the PR (Acceptance notes 3), not a breach.",
"Two temporary probe test files were created in packages/plugins/plugin-security/src and deleted before any commit: one for the premise, and one for the scalar-comparison table at base and at head. The base reading for the table restored rls-check-stored-form.ts from be5a83c without staging, then checked out HEAD again (blob == HEAD 88931797, diff empty).",
"The objectql full suite ran as three explicit file-list chunks (122 / 121 / 118 files), so each fit inside the foreground cap.",
"The tests and typechecks ran at 242331e. The final head 8f7c43b changes only the changeset's text. The gate union and the narrowed lint were rerun at 8f7c43b.",
"The dispatch named main as ef96c9e. The branch point is be5a83c, one commit later (#21239, driver-sql MySQL create / bulkCreate), with no overlap. origin/main has since moved to 3dc33b2 (#21222), with no overlap with the 7 changed files. No merge was made.",
"The deepening fetch for check:engine-split-ratio (shallow-since 2026-06-26) wrote shared .git state (the shallow boundary and refs/remotes/origin/main). It is additive history only."
],
"files_changed": [
"A .changeset/21238-multi-value-wrap-one-rule.md",
"M packages/core/src/index.ts",
"A packages/core/src/utils/multi-value-storage-form.test.ts",
"A packages/core/src/utils/multi-value-storage-form.ts",
"M packages/objectql/src/validation/record-validator.ts",
"M packages/plugins/plugin-security/src/rls-check-stored-form.test.ts",
"M packages/plugins/plugin-security/src/rls-check-stored-form.ts"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Claim revision (Clause-② line) ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T22:52Z · amends claim5941247880The seat answers the dev's open question in
5942236028with B. The claim'sClause-②: nowas wrong and is corrected toClause-②: yes (widening).references/lanes/spec.md: 「放宽接受集或扩大公开面的卡,不论多小,即条款②」 and 「按yes申报恒不是错误」. PR fix(core,objectql,plugin-security): the RLS write check judges a lone scalar on a declared multi-valued field as the list it is stored as #21253 adds one root export to@objectstack/core(multiValueStorageForm). It is reachable from the package's entry types and addressable throughexports, so it is a published public surface (contract-review.md: the published surface is read from the package'sexports). Enlarging it is Clause-②, however small.execution-duties.md: 「负边界:运行时权限/安全行为变更不是条款②」. That change executes triage's route A under ruling A and stays on the security floor as ruled.@objectstack/coreexports) declaredyes (widening). e5cf27d declaredno (narrowing)for thetemporalStorageFormexport; under the spec-lane rule above, that was an under-declaration and is not followed.CONTRACT_REVIEW_TIERreview is owed on the patched head before enqueue (Clause-②: yesis one of the three contract faces). Semver is unchanged:yestakes at leastminor, and core is alreadyminor.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{
"issue": 21238,
"status": "done",
"round": "patch round 1 (Clause-② claim revision, answer B)",
"branch": "claude/issue-21238-multivalue-wrap-core",
"pr": "#21253",
"session": "session_01DiCSbmJrkzNhuEAier4VoJ (subagent; the dispatching session's id)",
"head": "e559c9ac",
"summary": "The changeset's declaration line is now exactly
Clause-②: yes (widening). Its prose says the widening is the new @objectstack/core root export multiValueStorageForm, and that the plugin-security admission change is a security-floor behaviour change, not the declared widening. The bumps are unchanged: core minor, plugin-security minor, objectql patch. The yes arm requires at least one moved package at minor or above, and core minor satisfies it. One commit (e559c9a, model-free trailer pair), a normal push. The PR body was NOT patched by this run: see deviations[0]. The ready body is at the scratchpad path in deviations[0].","gates": {
"head": "e559c9ac",
"derived": "dispatch-gates --repo objectstack-ai/objectstack --commands at e559c9a: 70 families, the list identical to the one at 8f7c43b. check:api-surface is NOT derived (no packages/spec path in the diff).",
"run": "node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0 (1 non-breaking changeset seen) · --self-test: exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main: exit 0 (the level axis is NOT APPLICABLE locally, since there is no pull_request payload) · --self-test: exit 0 · node scripts/check-empty-changeset.mjs --base origin/main: exit 0 · --self-test: exit 0 · pnpm check:changeset-gate-self-tests: exit 0 · pnpm check:nul-bytes: exit 0 (9911 files) · pnpm check:objectui-changeset: exit 0 · pnpm check:pm-changeset-deadline-census: exit 0",
"level_axis_offline": "check-changeset-no-major --base origin/main --event, with a pull_request payload carrying the patched body (line 2
Clause-②: yes (widening)): exit 0, 'declares clause-② yes (widening), and it grades a package whose packages//src/ it moves at minor or above'. The same with the live body (line 2Clause-②: no): exit 0, 'not declared'.","not_rerun": "The other 60 derived families were not re-run at e559c9a. The only change since 8f7c43b (all 70 exit 0) is changeset prose, +2 / -2 lines.",
"ci": "in_progress (not waited on)"
},
"line_budget": "359 changed lines (+324 / -35, 7 files) vs merge base be5a83c at e559c9a (dispatch-gates): under",
"files_changed": [
"M .changeset/21238-multi-value-wrap-one-rule.md (this round: the Clause line + two prose clauses)"
],
"mcp_calls": "0",
"api_writes": "1 relay dispatch: this comment, through post-stamped --comment=21238, became POST /repos//issues/21238/comments. git push (not REST): 1 normal push (8f7c43b -> e559c9a). No issue_patch was sent (deviations[0]).",
"deviations": [
"CONFLICT, raised and not chosen silently: the patch order's step 3 (issue_patch on PR #21253's body) conflicts with the dev definition: 'PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不 PATCH;事后要改的报告点名改法,席位代写', and the definition prevails over dispatch words on a conflict. So no body write was made. The seat's one write is ready: /tmp/claude-0/-home-user-objectstack/648b83a3-c5c3-520e-b41a-19ee2af1efbe/scratchpad/issue-21238/pr-body-patched.md (12176 bytes). It is the live body (read back at e559c9a and byte-identical to what was sent) with three edits. Line 2:
Clause-②: no->Clause-②: yes (widening). Line 33, the Changes bullet: the core minor is marked as the declared widening, and the plugin-security admission change as a security-floor change, not Clause-②. Line 94, Acceptance notes 4: rewritten to say the line declares the new core export, the claim'snowas revised on #21238, and the families re-ran green at e559c9a. It keepsFixes #21238on line 1 and the session-URL footer. There is no closing keyword next to another card number, no less-than character, no control byte, and the level axis passes over it offline (gates.level_axis_offline). Until it is written, CI readsClause-②: nofrom the body, and the level axis stands down green."]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
ACCEPT(席位复核,入队前仍欠契约级复核) · PR #21253 @
e559c9ac·domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T22:58Z依据:报告
5942236028与补丁轮5942306414,以及 diff。main,首行Fixes #21238,第 2 行Clause-②: yes (widening)。正文这三行(第 2、33、94 行)由本席经 relay 改写,回读一致。dev 按自己的定义不改 PR 正文,只把正文备好交给本席,这是角色分工,不是越权。全文扫过,没有关闭关键词挨着别的卡号。页脚为会话 URL。@objectstack/core新模块与入口导出,以及 objectql 记录校验器的调用点,都是已声明的跨车道面。没有任何规则副本。!contains('x')下发送标量'x'原先能通过(写入时放行,读却看不到这一行),现已关闭。按 PR #21192 新规,逐句核了 changeset 与 diff:
contains时 403 而读可见;!contains时放行而读隐藏;按 id 更新时 403 而读可见)与 dev 在be5a83cf的前提实测一致。storedFormCheckJudge经declaredMultiValueColumns调multiValueStorageForm,一致。multi-value-storage-form.ts及其 16 格单测,一致。Clause-②: yes (widening)指 core 新增的根导出;plugin-security 的接受集变化属于安全地板,不属于 Clause-②,与认领修订一致。content/docs/**与skills/**,以validation-rules.mdx:473和permissions/rls.mdx:64作正对照,没有句子被改成假。docs-drift 对core/src/index.ts没有锚点,本席认可 dev 的人工检索。仍欠: 隔离的
CONTRACT_REVIEW_TIER复核(Clause-②: yes),已经起跑,结论落在 PR 上。PASS 在案、全部 check 转绿之后才入队。Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Contract review recorded: PASS (
5942463292on PR #21253,CONTRACT_REVIEW_TIER,Local-runs: none, heade559c9ac) ·domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T23:12Zneeds:contract-reviewwas removed in the same act. The seat's disposition of each escalated or noted item:system/readonlyskip):Acceptance noteson the PR, no card. The reviewer confirmed that no caller can reach it: a caller's value on such a column is stripped before the engine's seams. The one remaining case, a hook's own scalar write, is unmeasured, and the filing gate ① files only measured reach. If a later card measures it, it folds into RLS: the write check evaluates a scalar comparison (!=, ==, in) on a declared multi-valued / JSON-stored column that the read refuses 400, so a policy the read cannot run admits writes #21254's family.storedFormCheckJudgecall insecurity-plugin.ts):Acceptance notes; a comment only. It rides the next PR that edits that file (RLS: the write check evaluates a scalar comparison (!=, ==, in) on a declared multi-valued / JSON-stored column that the read refuses 400, so a policy the read cannot run admits writes #21254 is the likely carrier).Landing waits for every check on
e559c9acto finish green.Generated by Claude Code
fix(core,objectql,plugin-security): the RLS write check judges a lone…