Repository navigation
fix(plugin-security): explain credits read depth, not sharing, for a row only the depth admits - #21754
Conversation
…w only depth admits Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
…ry published decidedBy member Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
…' titles Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d877a6329f587539b2d77885def6cac503b12e22 && git checkout d877a6329f587539b2d77885def6cac503b12e22
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ea7ff394b6d802de9ad0448130bef86d4407760d 8998a647a0fe86ceadb4bbbea7ab68d1646eec74 && git checkout -B drift-repro ea7ff394b6d802de9ad0448130bef86d4407760d && git merge --no-ff 8998a647a0fe86ceadb4bbbea7ab68d1646eec74
node scripts/docs-audit/affected-docs.mjs --json ea7ff394b6d802de9ad0448130bef86d4407760d
|
…cision in words instead of a tracker number (stage 13) (objectstack-ai#21763) Part of objectstack-ai#20749 Clause-②: no Stage 13 of this card, and the fourth area of class (e): the test strings shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513. This stage takes two whole directories, `contracts/` and `conversions/`. Their 97 test-title and test-string literals carried 108 tracker ids citing 78 records. 107 ids in 96 literals now either state what their record decided, in words (form D), or are dropped where the title already says it. One id stays, for the reason given below. Text only: no assertion, identifier, test count or code comment changes. ## Census at the base (`866b4393d0`, the claim's base) Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`), `census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`) and `census-wide.cjs` (md5 `c98410a19529c439adb0afbfb00026a2`), byte-identical to the copies stages 10 to 12 used. A literal counts as a test title when its folded message is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` / `.only` chains included. Everything else is an "other" string. Both instruments read **1509 messages / 1606 ids in 348 files at the base**, which is stage 12's head reading exactly. `contracts/` reads 63 / 74 and `conversions/` 34 / 34, also stage 12's figures. | directory | files | messages / ids | titles | other | |:--|--:|--:|--:|--:| | `data/` | 95 | 468 / 501 | 445 / 475 | 23 / 26 | | `ui/` | 81 | 392 / 415 | 374 / 397 | 18 / 18 | | `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 | | `system/` | 34 | 154 / 165 | 128 / 138 | 26 / 27 | | (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 | | **`contracts/`** (this PR) | 25 | **63 / 74** | 59 / 70 | 4 / 4 | | **`conversions/`** (this PR) | 9 | **34 / 34** | 34 / 34 | 0 | | `security/` | 8 | 28 / 28 | 28 / 28 | 0 | | `ai/` | 9 | 18 / 20 | 13 / 15 | 5 / 5 | | `identity/` | 6 | 15 / 15 | 14 / 14 | 1 / 1 | | `integration/` | 4 | 14 / 14 | 13 / 13 | 1 / 1 | | `migrations/` | 2 | 9 / 12 | 9 / 12 | 0 | | `marketplace/`, `meta-spelling/`, `studio/` | 5 | 7 / 7 | 7 / 7 | 0 | | **total** | **348** | **1509 / 1606** | **1422 / 1515** | **87 / 91** | - **Controls.** Lit, a title: `contracts/data-engine.test.ts:575` reads one message with four ids. Lit, an `expect` message: `contracts/metadata-service-roundtrip-conformance.test.ts:142` reads one message. Dark: the comment at `contracts/core-service-contracts.test.ts:3` ("[objectstack-ai#4127] The map claims a binding per slot") reads 0. Planted in a scratch copy: an id put back into a title reads 1 / 1, and an id in an added comment reads 0. - **A wider pattern** (any `#` plus digits) reads 63 / 75 under `contracts/` and 35 / 36 under `conversions/` test files at the base. The extras are `(batch objectstack-ai#76)` in the `resume-failure-report.pin.test.ts:137` title, and `PD objectstack-ai#12` (Prime Directive 12, contract-first) in two `conversions.test.ts` titles, `:180` and `:785`. None matches the gate's 3-to-5-digit pattern. - **At the head:** 1413 messages / 1499 ids in 315 files. `contracts/` reads 1 / 1 (the needle below), `conversions/` 0 / 0. Nothing else moved. The wider pattern reads that needle and the two `PD objectstack-ai#12` titles, and nothing else. ## How the area was chosen Stage 10's rule: rank whole first-level directories by ids, and take the busiest within about 10% of the ~100-id bound. `data/` (501), `ui/` (415), `api/` (201) and `system/` (165) each exceed it alone, and the files directly in `src/` (120) are 20% over. No single remaining directory fits except smaller ones, and `contracts/` with `conversions/` reads exactly 108, within 10% of the bound. That is the pairing the stage-10 ACCEPT named, so the rule needed no second pass. **Named for the next stages:** `data/` (about five stages, by subdirectory or file group; `data/driver/` alone is 52), `ui/` (about four), `api/` (two), `system/` (two), the files directly in `src/` (one, 120), and `security/`, `ai/`, `identity/`, `integration/`, `migrations/`, `marketplace/`, `meta-spelling/` and `studio/` together (one, 96). ## What each id became 32 ids in 21 literals now state a decision in words. 75 ids in 75 literals are dropped where the title already says what the record decided. Every cited record was read with its comments through REST: 71 answer 200, and 7 answer 404 (objectstack-ai#6345, objectstack-ai#6523, objectstack-ai#11741, objectstack-ai#12010, objectstack-ai#12248, objectstack-ai#16559, objectstack-ai#16786). For those seven the decision was read from what landed: the landing commit and the CHANGELOG entry. | record(s) | literal | now reads | |:--|:--|:--| | objectstack-ai#16293 | `action-confirmation-contract.pin.test.ts:63` | "action-confirmation contract — an unconfirmed gated action is refused". The ruling: a gated action without an explicit confirmation is refused loudly, with the way to confirm. | | objectstack-ai#10331 | `approval-service.test.ts:23` | "approval rows declare the organization_id they are stamped with". The finding's first reading landed: both row types declare it, optional and nullable. | | objectstack-ai#19846 | `automation-context-caller-param-keys.pin.test.ts:49` | "AutomationContext.callerParamKeys — the keys the caller supplied". The ruling replaced the headless-screen inference with this explicit signal. | | objectstack-ai#18235 | `automation-service.test.ts:422` | "FlowRuntimeState — carries the reason a flow is unbound", so a policy-disabled flow reads differently from a broken binding. | | objectstack-ai#15937 | `confirmed-blueprint-identity-contract.pin.test.ts:52` | "confirmedBlueprintIdentity — declared on the protocol ToolExecutionContext". The maintainer chose option 1: declare it in the protocol, not only on cloud's augmentation. | | objectstack-ai#11493 (2) | `data-driver.test.ts:290`, `data-engine.test.ts:455` | "introspectSchema — an optional driver member at the spec shape" and "introspectDatasource — answers the spec introspection shape". Both ruled steps. | | objectstack-ai#12248, objectstack-ai#11833 | `data-engine.test.ts:514` | "datasource resolution members — declared, and optional". Fork 1 of the objectstack-ai#11833 ruling, option A, landed as `8425c17cc`. | | objectstack-ai#12248, objectstack-ai#12010, objectstack-ai#12805, objectstack-ai#11833 | `data-engine.test.ts:575` | "datasource lifecycle members — declared at the shape the engine keeps". Item 4 of the objectstack-ai#11833 ruling put `ConnectionEngineLike`'s members on the contract (`8425c17cc`, `77b91bd`), and objectstack-ai#12805 caught the declared def up to what the engine retains. | | objectstack-ai#12482, objectstack-ai#12010, objectstack-ai#11833 | `data-engine.test.ts:679` | "syncObjectSchema — declared, since two services already call it". | | objectstack-ai#5040 | `http-server.test.ts:183` | "optional setFallbackHandler — a not-found hook, not a wildcard route". The design's option C, so a declared endpoint never shadows a registered route. | | objectstack-ai#9835 | `http-server.test.ts:316` | "optional afterResponse — a transport-agnostic response observer". | | objectstack-ai#6617 | `job-service.test.ts:126` | "JobHandler degraded-outcome channel — optional and additive". | | objectstack-ai#14766, objectstack-ai#14501 | `job-service.test.ts:263` | "IJobService.replay force option — a succeeded window replays only when forced". The A + a2 ruling. | | objectstack-ai#4127 | `notification-service.test.ts:153` | "inbox — declared on the contract, and optional". | | objectstack-ai#5928 | `objectql-engine-hook-scope.test.ts:41` | "IObjectQLEngine.registerHook scope faces — global minus excluded objects". The ruled A shape, `excludeObjects`. | | objectstack-ai#12248, objectstack-ai#11833 | `objectql-engine.test.ts:29` | "getObject return contract — a structured answer, not `unknown`". Fork 3. | | objectstack-ai#12481, objectstack-ai#12248, objectstack-ai#11833 | `objectql-engine.test.ts:106` | "getSchema return contract — the same answer as its alias getObject". Fork 3, one member over. | | objectstack-ai#20157, objectstack-ai#19995 | `objectql-engine.test.ts:163` | "judgeFilter contract — the engine judges a filter without running it". Ruling C. | | objectstack-ai#4539 | `sharing-service.test.ts:35` | "recipient vocabularies, each under its own name". The same-name, different-form exports were split by renaming. | | objectstack-ai#5858 | `sharing-service.test.ts:194` | "HierarchyScopeContext tenancy authority — organizationId is authoritative". | **Dropped only (75 ids):** objectstack-ai#3903, objectstack-ai#4045, objectstack-ai#4127 (2), objectstack-ai#4158, objectstack-ai#4251, objectstack-ai#4343, objectstack-ai#4347 (2), objectstack-ai#4401, objectstack-ai#4456 (2), objectstack-ai#4538 (2), objectstack-ai#4827, objectstack-ai#4829, objectstack-ai#4923 (5), objectstack-ai#5011, objectstack-ai#5122, objectstack-ai#5125, objectstack-ai#5126, objectstack-ai#5493 (3), objectstack-ai#5777, objectstack-ai#5817, objectstack-ai#5945 (4), objectstack-ai#6345 (2), objectstack-ai#6428, objectstack-ai#6430, objectstack-ai#6523, objectstack-ai#6775 (2), objectstack-ai#6776, objectstack-ai#7378 (3), objectstack-ai#7616 (2), objectstack-ai#8321, objectstack-ai#11122 (2), objectstack-ai#11741, objectstack-ai#11832, objectstack-ai#13700, objectstack-ai#13937, objectstack-ai#14103, objectstack-ai#14244, objectstack-ai#14384, objectstack-ai#14945, objectstack-ai#14969, objectstack-ai#15389, objectstack-ai#15429, objectstack-ai#16231, objectstack-ai#16495, objectstack-ai#16559, objectstack-ai#16693, objectstack-ai#16786, objectstack-ai#19620, objectstack-ai#20323, objectstack-ai#20390, objectstack-ai#20740, objectstack-ai#20935, objectstack-ai#20940, objectstack-ai#21005, objectstack-ai#21220, objectstack-ai#21458. - Each title already states the pinned decision. In `conversions/` most titles are the conversion entry's own id ("action-aria-removed", "app-hidden-to-unpublished"), which is the decision that landed. - **Qualified references** were read before dropping: "objectstack-ai#13937 shape 4" (keep the consume order and add an operator exit verb; the title already names the repairable run), "objectstack-ai#4923 house rule" (equal values dedupe, different values keep both; the title already says "keeps BOTH"), "objectstack-ai#4127 batch 3" (the ledger extends past the enum; the title says "beyond the enum"), "objectstack-ai#4251 B3" (`IObjectQLEngine` widens `IDataEngine`; the title says it), "objectstack-ai#5122 shape" (a wrapper that forwards only required members; the title says it), and "objectstack-ai#7378 row 1" / "row 3" (both refusal messages already state their row's ruling). - **The 404 records:** objectstack-ai#6345 (`e2798fa`: one driver vocabulary, `mongo` converged to `mongodb`), objectstack-ai#6523 (`aa4b90d`: enforcement takes the full `ExecutionContext`), objectstack-ai#11741 (`b706af9`: an optional `organizationId` on both email inputs), objectstack-ai#16559 (`c7aca0dce`: the resume failure declared once, carried by a success answer), objectstack-ai#16786 (`6059b29`: `updateById` declares the record or `null`). Each title already carries what landed. - **`(batch objectstack-ai#76)`** in `resume-failure-report.pin.test.ts:137` goes with `objectstack-ai#16559` in the same literal. It names the decision batch whose ruling the title already states ("the resume failure a success answer carries"). It is outside the gate's pattern, and it is declared here. - **`PD objectstack-ai#12`** stays in `conversions.test.ts:785`. It is a Prime Directive reference in AGENTS.md, not a tracker number, and the untouched sibling title at `:180` spells it the same way. ## The one id that stays `contracts/approval-service.test.ts:274` is `expect(doc).toContain('objectstack-ai#16495')`. It is not a title: it is the expected value of an assertion that reads the `continueRestoredRun` docblock in `contracts/approval-service.ts` and pins that the docblock names the sibling it was ruled to copy. Changing it needs a code comment and assertion logic, which this claim excludes. It moves with that docblock when the comment lane rewrites `approval-service.ts:999`. ## Readers - **Test-name filters:** none. A tracked-tree search for `-t` and `--testNamePattern` finds only `packages/qa/dogfood/README.md:142` (`-t "owner-scoped"`), which is unrelated. - **Snapshots:** none. Neither directory has `__snapshots__`, and no `.snap` file is tracked under `packages/spec`. - **Projects:** none of the 34 files is listed in `packages/spec/vitest.repo-tests.json`, so all run in the `local` project. - **By substring:** every old literal, plus a window around each id (241 needles), was searched across the tracked tree outside its own file. No gate, doc, filter, snapshot or `scripts/check-*.mjs` self-test reads one. The 17 needle hits land on 11 lines: - Sibling test strings in other lanes' packages: `packages/objectql/src/metadata-service-roundtrip-conformance.test.ts:232` (the objectql driver of the same table, `register must REFUSE this write (objectstack-ai#7378)`); `packages/plugins/plugin-security/src/get-queryable-fields.test.ts:136` and `:165` (`[objectstack-ai#20935]`), `resolve-permission-sets-for-context.pin.test.ts:103` (`[objectstack-ai#7616]`), `authored-row-write-verdict.test.ts:350` (`[objectstack-ai#5493]`); and `packages/metadata-core/src/artifact-forward-conversion.test.ts:277` (`(ADR-0113, objectstack-ai#16693)`). - Code comments: `packages/spec/src/contracts/automation-service.ts:1061` (`objectstack-ai#13937 shape 4`), `packages/spec/src/contracts/index.ts:44` (`(objectstack-ai#4127)`), `packages/cli/src/utils/view-container-names.ts:11` and `packages/objectql/src/view-container-name-refusal.ts:13` (`objectstack-ai#7378 row 1`). ## Text-only proof Stage 10's scratch tool (`textonly10.cjs`, md5 `d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file on three legs: 1. **Skeleton:** the full AST, with string pieces masked. It must be identical. 2. **Comments:** every comment, byte-equal. 3. **Strings:** each changed string leaf must sit in a test-call title position or on a declared line, must carry a tracker id before, and must carry no `#` plus digits after. The declared lines are the three in `metadata-service-roundtrip-conformance.test.ts`: the reference double's two refusal messages (`:68`, `:75`) and the `expect` message at `:142`. No test asserts on the dropped text: the refusal checks assert `code`, `status` and the write's coordinates. - **Result:** 33 of 34 files SAME on all three legs. `conversions.test.ts` passes the skeleton and comment legs and is flagged on one string, `:785`, because its rewritten title keeps `PD objectstack-ai#12`. That was predicted in writing before the run. With `PD objectstack-ai#12` spelled `PD-12` in both the base and head copies of that one line, the file reads SAME with 19 changed titles. - **Totals:** 96 changed literals, 93 titles and 3 declared. The diff's `+` lines are exactly the 96 planned lines, and every file keeps its line count. - **Controls (10 of 10 as predicted, on scratch copies, each anchor hit once):** identifier rename DIFF; numeric literal DIFF; comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a rewritten title given a new id VIOLATION; a title that was id-free at base edited VIOLATION; one title reverted to base SAME; a declared string keeping an id VIOLATION; an undeclared `expect` message changed VIOLATION; a title re-split into a `+` chain DIFF. **Test counts:** the 34 files were run at the base (in a separate base worktree at `866b4393d0`) and at the head, in the `local` project. Both sides read 595 / 595 passed, with the same count and status sequence per file in 34 of 34. 354 full test names change, and each equals the base name with the planned replacements applied. One full name repeats on both sides: two `sqlite` rows of the `stored.test.ts` `it.each` table share the `%s` name. That predates this PR. ## Changeset: `skip-changeset` Measured, not assumed: - `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the 34 touched files are in it, and no `*.test.ts` at all. The controls `src/shared/expression.zod.ts` and `dist/contracts/index.js` are in it. - In `dist/`, four new phrases and three old ones each read in 0 files. The control `Unrecognized key(s) on` reads in 42. So this PR publishes nothing, and no changeset is added. ## Verification (at `72513933ee`) - `pnpm turbo run build` over all packages: 71 / 71. - `@objectstack/spec`: - `vitest run --project local`: 614 files, 18285 passed, 1 todo. - `typecheck` exit 0, including `check:test-typecheck`. Its program holds all 34 touched files, counted with `tsc --listFilesOnly`. - **Gates:** `dispatch-gates --commands` derived 79 families (stage 12's 80 without `check:future-spec-major`, which no touched file feeds), and all 79 exit 0. `--ran` reconciles: 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN. - The five roster families whose rosters sit under a touched directory were also run, and each exits 0: `check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing` and `check:filter-alias-parity`. - **ESLint, a proven narrowing:** `--no-inline-config` over the 34 files, 0 errors and 0 warnings. The population comes from ESLint's own config: 34 configured, 0 ignored. No `parserOptions.project` or `projectService`, so no untouched file's verdict can move. - `check-governed-merges --test`: NOT governed, 192 changed lines. ## Acceptance notes - **The `objectstack-ai#16495` needle** at `approval-service.test.ts:274` stays, with the `approval-service.ts:999` docblock it pins. The comment lane owns both. - **Code comments still carry ids** in these 34 files and in the two directories' sources. Comments are not this card's share and are untouched here. - **Sibling test strings in other packages** repeat ids this PR dropped: the objectql driver of the round-trip table, three `plugin-security` test files and one `metadata-core` test (listed under Readers). Each is its own lane's test-string stage. - **`origin/main` moved** five commits past the base before this PR opened (objectstack-ai#21752, objectstack-ai#21754, objectstack-ai#21753, objectstack-ai#21751, objectstack-ai#21743). None touches `packages/spec`, so nothing was merged. objectstack-ai#21756, which also edits `contracts/security-service.test.ts`, has no PR yet; whichever lands later merges `origin/main`. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21726
Clause-②: no
The explain answer now produces
decidedBy: 'depth', a member the publisheddecidedByenum (packages/spec/src/security/explain.zod.ts, about:331-:341) already declares. No access decision changes.What changed
All runtime changes are in
packages/plugins/plugin-security/src/explain-engine.ts, in the record-grained pass (applyRecordAttribution).decidedBy: 'depth'. The decision tree ranksdepthwithsharing: after a decisive bypass, after a share, and beforerls. These are the two layers that widen a private baseline past its owner.depthlayer carries arecordblock on record-grained requests. It isadmittedwhen the read depth admits the row, and it names the depth and, beloworg, the owner it reached. It isexcludedwhen the owner is outside the depth. It isnot_evaluatedwith a reason when the depth adds nothing (own, a non-private baseline, no record), when the depth cannot be separated (a share admits the row too, beloworg), or when the depth is not in play (an on-behalf-of read, a write).admittedonly when a share or rule matched. For a row that only the depth admits, it isnot_evaluated, and its detail says that no share grants the row and that read depth already admits it.visiblecomes from the same disjunction as before.depthAdmitsRowis the part of the oldsharingMatches === trueadmission that the sharing layer no longer claims, sosharingAdmitted || depthAdmitsRowequals the oldsharingAdmitted.How the depth admission is judged (measured, not re-derived)
The sharing read filter is one predicate with two grants: the owner-match, which read depth widens, OR-ed with the ids of the caller's own shares. Measured in plugin-sharing's
buildReadFilter: atorgdepth it returnsnulland composes no owner-match at all. Beloworgit returns the owner-match over the depth's owners, OR-ed withid IN (granted ids)when the caller holds shares.The engine does not parse that shape and does not call the filter a second time. It reads facts the pass already holds:
depthlayer published (depthScope);Two cases are deliberately not credited to the depth. On an on-behalf-of read,
security-plugin.tshands the filter the delegated context with no depth stamped. Beloworg, anullfilter means the sharing service imposes no owner-match on that object.Precedence when a share and the depth both admit
This case is not named on the card.
decidedBy's published contract decides it: "the layer whoserecord.outcomeexcluded it, or the last one to admit it" (explain.zod.tsabout:390, and the.describeat:342). In pipeline order, depth comes before sharing, so a row a share admits keepsdecidedBy: 'sharing'. That is also the answer before this change. The hand-written explain page (content/docs/permissions/explain.mdx) does not describe record verdict precedence, so no docs sentence became false.Tests
Tests were added or updated in
plugin-security:explain-engine.test.ts, unit harness:depth, with a depth record block;sharingwith exactly "1 share(s) attached; access is granted for this record.";explain-engine.test.ts, enumeration:decidedByis read fromExplainDecisionSchemain@objectstack/spec/security. The test fails by name for any member that no scenario produces and no ledger entry explains. Todayprincipalandflsare ledgered with reasons, and every other member is produced by a scenario that drives its branch.explain-read-verdict-inputs.test.ts, real SecurityPlugin plus real SharingService:The existing test that asserted the sharing layer
admittedan org-depth row was re-judged and replaced: it pinned the behaviour this change removes. Each cell still asserts explainvisibleequals the find's outcome.explain-dependency-fault.test.ts: the "a null filter is an answer, not a fault" test now assertsdecidedBy: 'depth', with the filter published and judged. A faulted layer publishes neither.Runs at head
8998a647:pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2(full package): 164 files, 3562 passed, 45 skipped, 0 failed.pnpm --filter @objectstack/plugin-security typecheck: exit 0. This includescheck:test-typecheckovertsconfig.test.json, which compiles the test layer.Ablations (each red, then restored and proven: blob equals HEAD,
git diff HEADempty)ff29410e): 15 failed out of 161 in the three explain files. Examples:expected 'sharing' to be 'depth', andexpected '0 share(s) attached; access is grante…' to contain ….depthAdmitsRowforced false: 9 red. These are every depth decider cell and the'depth' is producedenumeration case.flsledger entry renamed: 1 red, withdecidedBy member 'fls' is produced by no branch and carries no ledger reason.The subject resolves from
src(relative imports), so no dist rebuild was involved.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 66 commands from this branch, and all 66 ran at8998a647with exit 0.check:i18nandcheck:dual-build-cjs-loadsfirst answered PREREQUISITE NOT MET (exit 3). Each was re-run green after its stated build (turbocache hits). The--ranreconciliation: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN.Lint was narrowed to the 4 touched
.tsfiles. ① They are in the population ofeslint.config.mjs'spackages/**objects (--print-configresolves for each). ②--format jsonreports 4 files, 0 errors, 0 warnings. ③ The config enables no type-aware linting (noparserOptions.project, no typed rules), so this diff cannot move any untouched file's verdict.Acceptance notes
update/delete, the row verdict is the sharing service's per-record gate, which judges write depth, ownership and shares together. A write admitted by write depth still readsdecidedBy: 'sharing', with the gate detail naming ownership or a share. The newdepthrecord block on a write says that this report does not separate the write depth. Carrier: none named.owd_baseline, which was already the case.Generated by Claude Code