Repository navigation
chore(turbo): opt out of the agent-guidance block in the root turbo.json - #21151
Merged
objectstack-fleet[bot] merged 1 commit intoOct 1, 2026
Merged
Conversation
turbo 2.11 (landed with the 2.11.5 bump) maintains a managed block in the repository-root AGENTS.md whenever it detects an AI agent, before repository-scoped commands. AGENTS.md is a governed surface and the block tells agents to commit it, so the default turns every agent PR that obeys into a human-approval PR. The root-only option agentGuidance set to false is the opt-out turbo's bundled docs name. Claude-Session: https://claude.ai/code/session_018gA1pE6eJtwHhqx72G8U9X Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Oct 1, 2026
This was referenced Oct 1, 2026
objectstack-fleet
Bot
deleted the
claude/issue-21146-turbo-agent-guidance
branch
October 1, 2026 11:10
This was referenced Oct 1, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…isplay field the caller may query (objectstack-ai#21136) Part of objectstack-ai#21062 Clause-②: yes (narrowing) Position 2 of objectstack-ai#21062 only. Position 1 (the public-form submit read-back, in `plugin-security`) is a separate PR on its own branch, so this PR does not close the card. ## What this changes `GET /forms/:slug/lookup/:field`, the public form's lookup picker, matches the visitor's search and orders its rows by one key. The handler took that key blindly from the first entry of `publicPicker.displayFields`. A field whose masking rule applies to the caller is served to it masked and is not queryable, and the engine refuses a search or an order on it with `403 PERMISSION_DENIED`. Since PR objectstack-ai#21051 that includes the caller who resolves no permission set. So a picker whose first display field declares a masking rule answered `403` to every caller the rule applies to, on every request. The key is now the first display field the caller may query on, by the security service's published answer (`ISecurityService.getQueryableFields`, objectstack-ai#20935). The handler reads it through the security service it already resolves (`resolveSecurityService`), for the context it already builds. No masking rule is derived in `rest`. - **No display field queryable:** the route answers the engine's refusal for those fields, in the same envelope (`403 PERMISSION_DENIED`, the flat body `mapDataError` already produces for this route) and with the engine's sentence, before any query runs. - **No security service:** nothing is masked on such a deployment, and the first display field stays the key, as before. - **A security service that cannot give the query answer** (the method absent, or it answered `undefined`): the fallback the contract prescribes on `getQueryableFields`. Every display field whose declaration carries a `maskingRule` is passed over, whoever the caller is. A declaration that cannot be read admits no display field. - The projection is unchanged. Every display field is still returned in each row, and the engine still masks the masked one. File surface: `packages/rest/src/rest-server.ts`, the picker handler's key selection only (the context moved above it, unchanged). Pins beside it, one dogfood pin, and `.changeset/21062-picker-queryable-key.md`. ## Per case, by class (measured on real boots) `bootStack` with the real security, REST, auth and SQL layers, before at `d9b2e34fac` (the pins commit, the route as on `main`) and after at `2fa3a86e9b`. Three deployments: no guest set registered; a guest set without the capability the rule names; a guest set holding it. The readings stay in the seat's private scratch. | Picker | Caller class | Before: status, key | After: status, key | |:--|:--|:--|:--| | First display field masked | visitor with no session, no guest set registered (resolves no set) | 403 `PERMISSION_DENIED` on every request; the masked field | 200; the next queryable display field; masked field served masked | | First display field masked | visitor, guest set without the capability | 403 `PERMISSION_DENIED` on every request; the masked field | 200; the next queryable display field; masked field served masked | | First display field masked | signed-in caller the rule applies to | 403, the same as the visitor | 200, the same as the visitor | | First display field masked | visitor, guest set holding the capability (rule lifted) | 200; the first display field; served stored | unchanged | | Masked field not first (the dogfood shape) | every class above | 200; the first display field; masked where the rule applies | unchanged | | No masked display field (control) | every class above | 200; the first display field | unchanged | | Only display field masked | the two classes the rule applies to | 403 `PERMISSION_DENIED`, from the engine | 403 `PERMISSION_DENIED`, from the route before the engine is asked; same refusal text | The signed-in caller reads the same as the visitor in every row, before and after: the door builds its own context and does not read the session. **What a search finds (H4).** For a picker whose first display field is masked, every search was refused before. After, a search matches the next queryable display field, so a caller finds rows by that field. A search for the masked field's value finds no row. In every other row of the table the search matches the same field as before, and what a caller finds is unchanged. ## Pins, red then green - `packages/rest/src/public-form-lookup-picker-queryable-key.test.ts` (new). The real `SecurityPlugin` over a real `ObjectQL` on a real `SqlDriver`, and the real protocol `findData`, so the request the route composes crosses the real ingress and the engine's own field guards. Two masked caller classes, each with: the premise (the security service answers the masked field readable and not queryable, and the engine refuses it); rows sorted on the next queryable display field with the masked field served masked; the search matching that field; no request the engine receives naming the masked field; and a picker with no queryable display field answering the engine's refusal (code, status and text equal to the engine's own for the same fields and context) with the engine never asked. Controls: a picker with no masked display field, and a caller the rule is lifted for. The fallback: a security service without the method, and one answering `undefined`, for a caller the rule applies to and for one it is lifted for. The declared narrowing (patch round 2): the same fallback service, a caller the rule is lifted for, and a picker whose display fields all declare a masking rule, refused `403 PERMISSION_DENIED` with the engine never asked; its control is the answering service, which serves that caller. The pickers are parsed by the spec's own `FormFieldPublicPickerSchema` first. - `packages/qa/dogfood/test/public-picker-queryable-key.dogfood.test.ts` (new). A real boot: the visitor with no session and a signed-in caller are served rows sorted, and searched, on the next queryable display field with the masked field served masked; a picker whose only display field is masked answers `403 PERMISSION_DENIED`. - Red on the pins commit `d9b2e34fac`: unit 12 failed, 6 passed (both masked classes on sorted, searched, never-a-key and the no-queryable refusal, and the four fallback cases; each failure on the defect: a 403 where 200 was expected, the masked field as the order key, the engine asked, the masked order). Dogfood 1 of 1 failed (403 where 200 was expected). - Green on the fix commit `2fa3a86e9b`: unit 18 of 18; the four sibling picker and public-form suites 69 of 69; `@objectstack/rest` rebuilt and the dogfood pin 1 of 1. ## Ablation On the fix commit `2fa3a86e9b`, through `scripts/ablation-replace.mjs` in wrap mode: the queryable branch of the key expression (the first display field in the security service's answer) is replaced by the first display field, so the key is `displayFields[0]` again on every path. Anchor 1 to 0, replacement 1 to 2, blob `e2f14974` to `d520d9ab`. - **Prediction**, written down before the run: exactly the 12 unit pins that were red on the pins commit go red and the other 6 stay green; the dogfood pin goes red. - **Unit pins:** 12 failed, 6 passed. They are the same 12 as the red run, compared by name. - **Rebuilt** `@objectstack/rest`; `ablation-dist-preflight --absent` passed (the marker is in none of the 6 built files; on the pristine build the same marker was in 2). - **Dogfood pin:** 1 of 1 failed, on a 403 where 200 was expected. - **Restore:** `git checkout HEAD --` on the absolute path, by the tool and again by the script's own `EXIT INT TERM` trap. Proven byte-identical: the blob equals HEAD's (`e2f14974`), `git diff HEAD` is empty and the status is clean. Rebuilt; the marker is back in 2 built files; unit pins 18 of 18 and the dogfood pin 1 of 1. - **A first attempt was a no-op and is not counted.** Its replacement text already sat inside its anchor, so the replacement count did not rise and `ablation-replace` refused to run anything. The anchor was narrowed and the ablation repeated; the readings above are the second attempt's. ## Local verification At the merged head `cdeed45868`. `origin/main` was merged twice (no rebase) before opening this PR, and both merges were clean. They brought three changes to `rest-server.ts`, all outside this handler: one import line (objectstack-ai#21084), the published-snapshot doors (objectstack-ai#21002) and the datasource metadata reads (objectstack-ai#21119). The changed packages' closure was rebuilt first. - Picker and public-form suites with the new pins: 5 files, 87 of 87. Dogfood, this pin and the objectstack-ai#20995 zero-set pin: 2 files, 3 of 3. - `@objectstack/rest` tests (`--project local`): 250 files, 4744 passed, 248 skipped. `@objectstack/rest` typecheck passed, the test layer included (`check:test-typecheck` OK, 0 errors). - `@objectstack/dogfood` typecheck passed. Its tsconfig includes `test/**/*`, so the new pin is compiled. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 66 commands, plus the three roster families it marks under a directory this diff touches (`check-changeset-fixed`, `check:error-code-casing`, `check:filter-alias-parity`) and `check-partof-closing-keyword` fed this body. All 77 ran in one locked sequential script and every one exited 0. `--ran` reconciliation: 66 derived, 66 run, 0 not measured. - `eslint --no-inline-config --format json` on the 3 touched TypeScript files: 3 files, 0 errors, 0 warnings. The population is `eslint.config.mjs`'s TypeScript and JavaScript glob, and all 3 files are in it. The config enables no type-aware linting, so this diff cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is left to CI. ## Acceptance notes - **Clause-②: `yes (narrowing)`, per the contract review** (`5929549767`, which ruled the earlier `no` wrong). Widening: a picker whose first display field is masked for its caller was refused and is now served. Narrowing: for a security service that lacks `getQueryableFields`, a caller the masking rule is lifted for, and a picker whose display fields all declare a masking rule, the request was served and is now refused `403 PERMISSION_DENIED`, by the fallback the service contract prescribes. The only security service in the tree (`plugin-security`) implements the method, so no in-tree composition reaches the narrowing; the contract names that composition as valid, so it is declared. Patch round 2 carries the `minor` changeset and its pin. - **The spec describe on `FormFieldPublicPickerSchema.displayFields`** says the visitor's search matches the first entry. For a caller whose masking rule applies to the first entry it now matches the first entry that caller may query; before this PR that search was refused. `packages/spec/src/**` is outside this PR's claimed surface, so the describe is reported to the seat for the spec owner and is not edited here. - **A declared `publicPicker.filter` row that names a masked field** is still refused `403 PERMISSION_DENIED` for every caller the rule applies to. It is the author's filter, not the route's key, so this PR leaves it alone, and it was measured unchanged. Reported to the seat, by class. - **The refusal text is the engine's predicate-guard sentence, carried into the route** so that a caller reads the same words whichever side refuses. The unit pin asserts it equal to the engine's live refusal for the same fields and context, so the copy cannot drift silently. - **Not measured:** a security service without the method on a real boot (the tree has none); the published-snapshot door and the form-submit route, which are outside this surface; the Console's picker client, which lives in another repository. CI's whole-root and workflow-valued families (10 and 6), the 5 path-scheduled CI jobs and the 4 type-check lanes the derivation names run on this PR. ## Patch round 1 CI's `Test Core (3/6)` was red on this PR's own change: `@objectstack/rest#test:repo` failed in `packages/rest/src/rest-server-canonical-query-ast.test.ts`. That test's by-name presence pin (section 1, "names them") still asserted the picker's query literal ordered by `displayFields[0]`. The local runs had used `vitest --project local`, which does not run the repo-test project. - **Reproduced first** at `cdeed45868`, under the verify lock: `test:repo` 1 failed, 178 passed, 1 skipped (5 files). The one failure is that pin. - **The pin's purpose is kept.** It is a backstop for the census: it proves the server-built literals are still present, in the canonical QueryAST spelling. It now names the picker's literal as it is, `orderBy` on the handler's one key. It also names the line that derives that key from the security service's queryable answer, so no other `key` in the file can satisfy it. `rest-server.ts` is untouched in this round (`d815dd449a`). - **Full `@objectstack/rest` surface** at the merged head `6dacc4c51b` (`origin/main` merged, no rebase, no conflicts): every project together, 256 files (251 local and 5 repo, which is the whole tracked population), 4925 passed and 249 skipped; `test:repo` on its own, 5 files, 179 passed and 1 skipped; typecheck OK, the test layer included. The dogfood picker and zero-set pins: 3 of 3. Dogfood typecheck OK. - **Gates:** the 66 derived commands, the three roster families and `check-partof-closing-keyword`, in one locked script; every exit 0. `--ran` reconciliation: 66 of 66, 0 not measured. eslint on the 4 touched TypeScript files: 0 errors, 0 warnings. - **CI on `6dacc4c51b`:** 32 success, 3 skipped, 0 failed, all six `Test Core` shards included. - **Tooling note, outside this diff:** the `turbo` version `main` now pins writes an agent-guidance block into `AGENTS.md` when it runs in an agent session. It happened twice here: during the closure build, and from a type-check gate that spawns `turbo`. Both times `AGENTS.md` was restored from `HEAD` with the blob proven equal, and the gates the write could have reached were re-run on the clean tree. `AGENTS.md` is not in this PR. Already filed as objectstack-ai#21146, with the fix in flight as PR objectstack-ai#21151. ## Patch round 2 The contract review (`5929549767`) ruled the earlier `Clause-②: no` wrong. The value is `yes (narrowing)`, at least `minor`. This round carries its items 2 and 3 and nothing else. `rest-server.ts` is not touched. - **Changeset** `.changeset/21062-picker-queryable-key.md`: `'@objectstack/rest': minor`; the summary is `fix(rest)!:`; a `**BREAKING**` banner names the narrowing by class (with a security service that lacks `getQueryableFields` or answers no answer, every display field declaring a `maskingRule` is passed over for every caller, so a picker whose display fields all declare one is refused `403 PERMISSION_DENIED` without the engine being asked); the body's declaration line reads `Clause-②: yes (narrowing)`; exactly one `adr-0087:` marker, `not-required (no-migration-prescription)`, in the shape of objectstack-ai#20986's changeset, closing the other four categories on facts. The widening text is kept. The fallback, which round 0 listed under "Unchanged", moved to the banner, and a "What to do after upgrading" paragraph addresses a deployer whose security service predates `getQueryableFields`. `scripts/pm/clause2-line.mjs` reads the line as declared `yes`, arm `narrowing`. - **Pin on the declared narrowing**, appended to `packages/rest/src/public-form-lookup-picker-queryable-key.test.ts`, with the existing synthetic fixtures. A caller the rule is lifted for is served by the answering service (the control). With the method absent, and with it answering no answer, the same caller is refused `403` with code `PERMISSION_DENIED`, and the engine's call count is unchanged. - **Ablation:** the fallback stops excluding fields that declare a masking rule, through `scripts/ablation-replace.mjs` in wrap mode (anchor 1 to 0, replacement 0 to 1). The prediction was written down before the run: the new pin and the four round-0 fallback pins go red, because they drive the same branch; the other 14 stay green. Observed: 5 failed, 14 passed, those five by name; the new pin failed on a 200 where the 403 was expected. The restore was proven by the tool (blob equal to `HEAD`'s, `git diff HEAD` empty), and the pin file is green again, 19 of 19. - **Merged** `origin/main` first (no rebase, clean; no incoming change to `rest-server.ts`). Head `207eb98a97`. - **`@objectstack/rest`:** all vitest projects, 257 files (the whole tracked population, local and repo), 4931 passed and 254 skipped; `test:repo` on its own, 5 files, 179 passed and 1 skipped; typecheck OK, the test layer included. Dogfood picker and zero-set pins 3 of 3; dogfood typecheck OK. - **Gates:** in one locked script, the changeset families against the new declaration ran first: `check-adr-0087-registration` reads `[BREAKING+bang+clause-②-narrowing]` with `not-required (no-migration-prescription)`; `check-changeset-no-major`, `check-empty-changeset` and `check-changeset-fixed` passed. Then the 66 derived commands, the three roster families, `check-partof-closing-keyword` fed the live body, and eslint on the 4 touched TypeScript files (0 errors, 0 warnings). All 81 commands exited 0. `--ran` reconciliation: 66 of 66, 0 not measured. Locally `check-changeset-no-major`'s level axis has no PR payload to read and says so; CI's `Check Changeset` reads it, and it passed. - **`turbo` and `AGENTS.md`:** `turbo` wrote its block into `AGENTS.md` during the closure build and during `check:type-check-debt`. The script restored it from `HEAD` after each command and proved the blob, so every later command ran on the clean tree. `AGENTS.md` is not in this PR. - **CI on `207eb98a97`:** 35 check runs, 32 success, 3 skipped (path and opt-in filters), 0 failed. Every required context is success, `Check Changeset` and all six `Test Core` shards included. _The seat appended this section from the dev's patch-round report (`5930061042` on objectstack-ai#21062), and extended the pin-file line above with the narrowing pin._ --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…om the shared persistent sequence (objectstack-ai#21113) (objectstack-ai#21160) Fixes objectstack-ai#21113 Clause-②: yes (widening) ## Patch round 1 (review 5930100091) The one blocking item was `@objectstack/driver-sql` at `patch` beside a root `type` re-export (`AutoNumberReservation`). Branch taken: the re-export is dropped and the two re-seed overrides in `turso-driver.ts` are typed `Parameters` indexed over `SqlDriver`'s `autoNumberValueExists` (its second parameter) and `resyncSequenceToDataMax` (its first) — the premise (the protected member's parameter type is reachable from the subclass without the export) measured true: `driver-sql` typecheck exit 0, `driver-turso` typecheck exit 0, no structural copy. `driver-sql`'s export list is byte-identical to `main`, so `patch` stands; the changeset sentence that named the export now reads "No export is added". `origin/main` merged again (`fde553c50`, which also lands the `agentGuidance: false` opt-out of objectstack-ai#21151); re-run on the merged tree: closure build exit 0, both typechecks exit 0, `driver-turso` `82 passed (82)` files / `2218 passed | 33 skipped` (83 files / 2219 with the throwaway probe present), `driver-sql` autonumber suites `66 passed | 2 skipped`, and the changeset, ADR-0087, nul-bytes and published-files gates all exit 0. Finding (b) was measured on the remote face as the review asked and reproduces; it is in the round-1 report on objectstack-ai#21113 for the seat to file. ## What this does Disposition A of the remote-autonumber question, executed: a `create`, `bulkCreate` or `upsert` on the Turso REMOTE transport that leaves an `auto_number` slot empty now gets a generated value, issued atomically in the database from the same persistent `_objectstack_sequences` counter the local and embedded-replica faces draw from. `supports.autonumber` stays `true` on the remote face and is now honoured. The `NOT_IMPLEMENTED` / 501 refusal is gone; its suite is converted into the generation suite. Why now: the appetite door the refusal left shut (「for want of measured demand」) was met by objectstack-ai/cloud#2531 — `POST /api/v1/data/crm_account` answers `501` on a hosted tenant, whose database is on this transport, so no object declaring an `auto_number` field could get a new record on the hosted product. That card carries the cloud half, `Blocked-by:` this one. ## How, and what is shared rather than copied **Where the slots are filled.** On the driver, one layer above the statement builder, exactly where the refusal was raised: `RemoteTransport.create(object, data)` takes no schema and caches none (pinned), while `TursoDriver` holds `autoNumberFields` from remote schema sync. The remote `create` / `upsert` / `bulkCreate` call the inherited `SqlDriver.fillAutoNumberFields` on a copy of the caller's row, then hand the filled row to the transport, which builds the same `INSERT` it built for a caller-supplied number. **One semantics (H3).** The rules that decide WHICH counter a value is drawn from and WHERE a cold counter starts are `SqlDriver`'s and are called, never copied. Lifted into `protected` members in the sequence region of `sql-driver.ts` (`:7532`–`:8190` on this head; nothing else in that file): `resolveSequenceTenantId` (replaces three inline copies), `defineSequencesTable` (the table's one definition — the remote face compiles it to text with the connection-less Knex it already holds and sends that), `maxAutonumberCounter` + `escapeLikePrefix` (the bootstrap reading, suffix included, split from the Knex statement that fetches the values), `sequencesTableName`, `autoNumberCollisionRetries`. The two re-seed overrides take their parameter types through `Parameters` indexed over `SqlDriver`'s protected members, so `driver-sql`'s export list is unchanged. The format rendering and precedence were already shared (`resolveAutonumberFormat` at registration, `renderAutonumber` / `missingFieldValues` in `fillAutoNumberFields`), and are reached by calling that method. **What is this face's own: how the counter moves (H2).** `TursoDriver.getNextSequenceValue` routes by transport. Local and replica keep the inherited Knex transaction. Remote moves the counter in ONE statement: ``` warm: UPDATE _objectstack_sequences SET last_value = last_value + 1 WHERE key_hash = ? RETURNING last_value cold: scan the data table's MAX in JS by the shared reading, then INSERT … VALUES (…, max + 1) ON CONFLICT (key_hash) DO UPDATE SET last_value = last_value + 1 RETURNING last_value ``` Each is a single SQLite statement serialised by the database's write lock across connections and processes; no in-process state takes part (`remoteSequencesTableEnsured` remembers only that the TABLE exists). Two cold writers both scan and both INSERT; one wins the row, the other's `ON CONFLICT` arm increments it. H2 falsified on one point, reported rather than followed: the PM expected the seed folded into the statement as a SQL `MAX(…)`. It is not, because the bootstrap reading strips a declared suffix and reads the digit run after the prefix (`readAutonumberCounter`, objectstack-ai#6468), which a dialect expression would be a second copy of — the exact collision H3 forbids. A Hrana batch was not needed either: the JS reading has to sit between the scan and the insert, and both insert arms are atomic on their own. **The legs (H4).** `create`, `bulkCreate` and `upsert` all generate. `bulkCreate` on the remote face now runs through the driver's own `create` per row, which is what `RemoteTransport.bulkCreate` always did underneath (it loops the transport's `create`: one INSERT and one read-back per row, never one statement — so the old comment's 「all-or-nothing on this transport too」 was not true, and the statements sent and the mid-batch failure state are unchanged). `upsert` reserves before the statement, as `SqlDriver.upsert` does, and names the autonumber columns to the transport as insert-only (`RemoteTransport.upsert` gains an optional fifth argument, schema-free: WHICH columns, not WHY), so a merge keeps the number already in the row (objectstack-ai#7011) and the insert leg — the one objectstack-ai#7099 recorded as writing NULL — gets its number. The reservation a merge does not use is a gap, never a renumbering, the local face's rule byte for byte (pinned: seeded 42, merge, next create is 44). `objectstack-ai#7099`'s post-write warning is removed with the leg it reported. **Legacy shape and collision re-seed (H5), decided from the code.** The pre-`key_hash` table: `SqlDriver.ensureSequencesKeyHashShape` rebuilds it through a live Knex connection, which this face has none of, and a raw-SQL rewrite would be a second copy of a migration; keying by the legacy `(object, tenant_id, field)` rule would be a second keying rule beside the shared one. So it is **refused**, `DATABASE_ERROR` / 500 with the remedy in the message (open the database once through the local or replica face), not cached as a verdict, caller-supplied numbers still written. It cannot arise on a database this face created. The objectstack-ai#5495 re-seed is **needed** on this face — the bypass paths that create a stale counter (`isSystem` seed replay, `preserveAudit` import) are exactly the seed/import paths, and `bulkCreate` is their common door — and it is carried: `autoNumberValueExists` and `resyncSequenceToDataMax` are overridden for the remote face (the forward-only move is one atomic UPDATE guarded by `last_value` being below the observed MAX), and `collidingAutoNumberReservations` is the inherited discriminator, so a duplicate on a value the caller typed stays the caller's 409 (pinned). **H6.** `Clause-②: yes (widening)` holds as measured: a create refused 501 today is accepted; no spec key moves; no export is added (the six lifted members are `protected`, and the overrides are typed through indexed types rather than a root re-export); `RemoteTransport.upsert` gains an optional parameter. Changeset: `@objectstack/driver-turso` minor, `@objectstack/driver-sql` patch. ## Tests Head for every number below: `52ab7ad76` (the merge of `origin/main` `1bd14c984`), unless stated. - **Converted, not deleted:** `turso-remote-autonumber-refusal.test.ts` → `turso-remote-autonumber-generation.test.ts` (`git mv` then rewritten; git's rename detection does not fire at this similarity, so the diff reads D+A — the three-face structure, the controls, the `update()` / plain-object / caller-supplied cases and the layer pins are the refusal suite's own). 27 cases: generation on every leg incl. the objectstack-ai#7099 leg; caller-supplied numbers, merge, update, plain object untouched; the two-face block (local Knex face and remote native-libsql face on ONE file: numbers interleave 1,2,3,4 on one `key_hash` row equal to the local face's `sequenceKeyHash`; per-tenant buckets; the objectstack-ai#6468 suffix seed read as 5 not 52026; the `{field}` refusal is the same sentence on both faces and writes nothing); the objectstack-ai#5495 re-seed on `create` and `bulkCreate`; the caller's own 409; the legacy-shape refusal with `code` + `status`; local and replica still issue; `RemoteTransport.prototype` still has no autonumber member and `create.length === 2`; `supports.autonumber === true`. - **The two boundary pins rewritten, not deleted** (`turso-autonumber-resync.test.ts`, `turso-autonumber-batch-resync.test.ts`): the transport-surface probe verbatim, and the refusal half replaced by generation + re-seed on the remote face. - **Cross-process pin** `turso-remote-autonumber-concurrency.test.ts`: two child processes (`tsx` over the package source, as `packages/spec`'s process-boundary pins do; `tsx` added as a devDependency of `driver-turso`), each with its own `@libsql/client` native `file:` connection to one database file and its own remote-mode `TursoDriver`, released together by a file barrier; 2 × 200 creates per round → 400 distinct, exactly 1..400, strictly increasing per writer, no failed write, the table and the counter row agreeing. Those are the hard pins, asserted on every round. The overlap EVIDENCE (the writer changes at least twice in the global order) ends the loop on the first round that shows it; after three rounds without one the test is skipped with a note that says NOT MEASURED — because whether two processes overlap is the scheduler's decision, measured here once as a strictly serial run (1 change) under another seat's full-package run on this shared box. Final-head runs: 3/3 green standalone, measuring `400 writes · 400 distinct` with 31, 21 and 23 writer changes; inside the full package run, 9. **Said plainly:** no `sqld` runs in this container; what is measured is two processes on libSQL's engine under its write lock, not an HTTP server. - Driver-conformance ledger (lane commitment): identical before and after — `50 covered cell(s), 0 in the DEBT ledger, 0 exempt`; the matrix has no autonumber cell for any driver, so none moved. Runs, all under `scripts/pm/os-verify-lock.sh` after the merge: `pnpm --filter '@objectstack/driver-turso^...' build` exit 0 · `driver-sql` typecheck exit 0 · `driver-turso` typecheck exit 0 · `driver-turso` `vitest run`: `82 passed (82)` files, `2218 passed | 33 skipped (2251)` · `driver-sql` `vitest run` in two halves of 108 files: `103 passed | 5 skipped`, `1389 passed | 100 skipped`; `102 passed | 6 skipped`, `1914 passed | 88 skipped` (every file; the autonumber-named nine also run on their own: `66 passed | 2 skipped`). ## Reverse verification (predicted before each run; mutation and restore proven on disk by `scripts/ablation-replace.mjs`, blob == HEAD and `git diff HEAD` empty after each leg) 1. **Merge-set exclusion dropped** (`RemoteTransport.upsert` ignores `insertOnlyColumns`). Predicted: exactly the two `[objectstack-ai#7011]` merge pins red. Measured: `2 failed | 25 passed`, both merge pins, `expected 'CASE-00043' to be 'CASE-00042'` and `expected 'CASE-00099' to be 'CASE-00042'` — the renumbering the exclusion exists to prevent. 2. **Warm path made non-atomic** (SELECT `last_value`, then UPDATE to `+1`). Predicted: the distinct count in the concurrency pin drops below N; the stub-backed generation suite stays green. Measured — **direction reversed, kept rather than tidied**: the generation suite stayed green (`27 passed`) as predicted, but the concurrency pin went red on its 「no write failed」 assertion, not on the distinct count: the fixture's tenant-scoped unique index refused the second row carrying a duplicated number, and the forward-only re-seed could not outrun a writer that kept reading the same stale value, so writes failed with `SQLITE_CONSTRAINT: UNIQUE constraint failed: index 'uniq_crm_case_organization_id_case_number'` — 2 of 3 runs with no gap between the two statements, 3 of 3 with a 1 ms gap (the order of one HTTP round trip). On a declared-unique column a counter that hands out a number twice surfaces as refused writes before duplicate rows; recorded in the pin's docblock. 3. **Shared bootstrap reading zeroed** in `driver-sql` (`maxAutonumberCounter`, reached through `dist/`: rebuilt, `ablation-dist-preflight` present in 2 built files, exit 0). Predicted: the bootstrap and re-seed pins red on BOTH faces. Measured: `10 failed | 25 passed` across the three suites — generation: explicit-then-continues-above, merge-then-44, objectstack-ai#6468 suffix, re-seed `create`, re-seed `bulkCreate`; resync: LOCAL and REMOTE re-seed; batch-resync: LOCAL `bulkCreate`, LOCAL `upsert`, REMOTE batch. Restore: rebuilt, preflight `--absent` exit 0. A first attempt of this ablation was a null operation (the tool refused because the replacement restated the anchor; the preflight correctly reported the marker absent and that green run is void) and was redone with a different anchor. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `52ab7ad76` (no paths; 73 commands). Run on the final tree (`2c9d2985a`, every file identical to the commit): the 73 derived commands — **72 exited 0**, incl. `check:driver-conformance`, `check:query-options-erasure` (holds: 67 unswept non-test sites, none new — my first head had added one `as any` on a `find()` query in the new concurrency pin, retyped as `DriverQuery`), `check:test-source-alias`, `check:undeclared-dep-imports`, `check:doc-authoring`, `check:nul-bytes`, `check:lean-entry-closure` (measured after building `@objectstack/objectql`: 15 packages, 201/199 modules, admitted set held), `check:type-check-debt` (399.5 s re-measure, none above record), `check:pm-skill-ratchet` (0 with AGENTS.md at the merge-base blob — the turbo 2.11.5 agent-rules block, objectstack-ai#21146, had rewritten it once in this worktree and is restored). `check:dts-closure` exited 1 on its first final-tree run naming `plugin-email` and `plugin-security` (neither in this diff nor in its closure) with `dist/index.d.ts` absent at that moment; a forced rebuild of the two and a rerun exits 0 (71 packages, 167/167 declarations) — concurrent turbo activity in the same tree, not this change. **NOT MEASURED (1):** `check:dual-build-cjs-loads` exits 3 PREREQUISITE NOT MET locally (78 packages without `dist/` in this worktree) — CI's. `dispatch-gates --ran`: 73 derived, 72 run, 1 NOT-MEASURED declared, 0 UNRUN (exit 0). Seven further families (`agent-test-spelling`, `docs-audit-scope`, `pm-governed-merges`, `pm-governed-prose`, `pm-skill-id-lint`, `pm-skill-ratchet`, `required-contexts`) were derived only while the turbo block sat in AGENTS.md; they were run anyway and all exit 0. The derivation itself still lists what no local run covers: the 53 artifact-roster families, 11 wide-population families, 6 path-scheduled CI jobs and the type-check lanes. `pnpm lint` narrowed, as a measurement: ① population = the lint script's own `eslint . --no-inline-config`, under one `eslint.config.mjs`; ② `--format json` on the changed source files: 9 files, 0 errors, 0 warnings; ③ invariance: the config enables no type-aware linting for any file (`eslint.config.mjs` states it, no `parserOptions.project`), so this diff moves no untouched file's verdict. ## Acceptance notes - Both faces: SQLite's `LIKE` has no escape character unless `ESCAPE` is declared, and the shared `escapeLikePrefix` writes backslashes. The remote statement declares `ESCAPE '\'`; the local Knex builder in `scanMaxNumericTail` does not (knex emits `like ?` with the escaped binding and no `ESCAPE`, measured on the sqlite and pg dialects), so on the local SQLite faces a format whose prefix contains `_` or `%` seeds from 0 (measured: `SO\_%` matches nothing on SQLite without `ESCAPE`). Pre-existing, outside this card's class; filed in the report as a finding, not fixed here. - `RemoteTransport.upsert` with caller `conflictKeys` leaves `id` in the merge set (`"id" = excluded."id"`) — the objectstack-ai#8622 shape the local face closed. Not touched; noted for the lane. - The README's remote refusal table loses its 「Record numbers」 row; `content/docs/**` carries no statement of the remote refusal (grepped). ## 维护者速读(草稿) - **改了什么:** Turso 远程传输面上,`create` / `bulkCreate` / `upsert` 留空的 `auto_number` 字段现在会生成编号,编号来自与本地面相同的持久计数表,在数据库里原子递增;原先的 501 拒绝改为生成。 - **为什么改:** 托管产品的租户库全部走远程传输,任何声明了 `auto_number` 字段的对象都无法新建记录(cloud#2531 实测:HotCRM 建客户答 501)。这正是 objectstack-ai#6944 当初留下的「等实测需求」的门,现在门开了。 - **风险与代价(含回滚):** 多进程并发只靠数据库写锁串行,无进程内计数;两个面共用一张计数表、一套键规则,已在同一数据库文件上对拍。遗留形状的计数表在远程面上明确拒绝、不迁移。回滚即还原本 PR;已生成的编号留在数据里,计数表形状与本地面一致。 - **席位意见:** (留空) - **你要做的:** 批准后 cloud#2531 推进 pin 并在托管环境复验建客户。 --- _Generated by [Claude Code](https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG)_ --------- Co-authored-by: Claude Code <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…and lints again — the `live` ledger row carries no author warning (objectstack-ai#21127) (objectstack-ai#21176) Fixes objectstack-ai#21127 Clause-②: no ## What this changes `os validate --json` and `os lint --json` exited 1 on any stack whose `mappings[]` entry authors `connectorSource`. The whole answer was the liveness lint's integrity sentinel: `lintLivenessProperties: ledger entry has unrecognised status "live"`. The ledger row `mapping.connectorSource` had been re-graded `live` and kept `authorWarn: true`. `describe()` in `packages/lint/src/lint-liveness-properties.ts` throws on a warned `live` row by design, and that throw stays as it is. - **The row** (`packages/spec/liveness/mapping.json`): `connectorSource` stays `live` and drops `authorWarn` / `authorHint`. Its `note` records why. - **The caveat moves to where an author reads it** (`packages/spec/src/data/mapping.zod.ts`). The `connectorSource` `.describe()` now reads: "Pulled when a job drives it; nothing schedules it yet, so the binding alone moves no rows — schedule the pull with a `job` once a job can drive one". The docblock no longer says the ledger keeps `authorWarn`. - **The existing integrity check refuses the combination, inside its current gate** (`check:liveness`, `packages/spec/scripts/liveness/check-liveness.mts`). No new gate or script was added. The graded walk never reads a container row that drills into `children`, and `connectorSource` is such a row, so the new rule walks the raw ledger rows at every depth. It fails on `status: "live"` with `authorWarn: true`, prints a prescription, and prints a census line on every run: `author warnings: 2 ledger row(s) opt into authorWarn, at any depth (planned 2); 0 on a live row.` - **Regenerated:** `content/docs/references/data/mapping.mdx` and `integration/connector.mdx` (`gen:docs`), and `packages/spec/src/migrations/registry.ts` (`gen:migration-registry`). ## Files beyond the claim's listed surface (each one is text this change made false) The claim lists the ledger row, the `connectorSource` describe, the existing integrity check, the regenerated artefacts, the pins and one changeset. These files are outside that list. Each one said that authoring the binding warns, and that stopped being true with the row fix: - `packages/spec/src/integration/connector.zod.ts`: the retired `connector.syncConfig` prescription, which an author sees in the refusal. - `packages/spec/src/migrations/entries/semantic/18.connector-sync-keys-retired.ts`: the upgrade entry's acceptance text. `registry.ts` is regenerated from it. - `packages/spec/src/integration/connector-sync-retirement.test.ts`: a one-line pin on that prescription text. - `packages/spec/docs/SYNC_ARCHITECTURE.md`: "authoring `connectorSource` still warns". - `packages/spec/liveness/README.md`: the mapping row's note, plus a third `authorWarn` rule (a `live` row carries none). - `packages/lint/src/authoring-rules.ts`: comments only. - `packages/lint/src/runtime-gate.inert-type-writes.test.ts`: a comment, and a pin. A `mapping` write that authors `connectorSource` used to get an `authoring-rule-threw` advisory at the runtime door, and now gets none. - `packages/cli/test/validate-lint-mapping-connector-source.test.ts`: the door pin, a new file. - `.changeset/20919-spec-connector-source-live.md`: see the next section. ##⚠️ One deliberate correction of a pending release note (needs confirming) `.changeset/20919-spec-connector-source-live.md` belongs to PR objectstack-ai#21084. It has not been released yet, and it said "`connectorSource` rows are `live` and keep `authorWarn`". This PR makes that false, so the sentence now reads "rows are `live`, with no author warning: that nothing schedules a pull yet is said on the key's description". `node scripts/check-empty-changeset.mjs --base origin/main` is therefore red by design. It says: "This PR changes a changeset it did not add … DELIBERATE CORRECTION -- … do NOT restore it -- say so on the PR and get it confirmed". This section is that statement, and `skip-changeset` is not applied. `.changeset/20281-connector-sync-moved-to-mapping.md` carried a similar claim. PR objectstack-ai#21150 has since rewritten it on `main` without the warn claim, and this PR does not touch it. ## Premise check - **H1, the crash** (measured at the dispatch base `665cab338f`, which contains PR objectstack-ai#21092 `b616c0a63d`). The card's fixture is one object `fx_account` with `sharingModel: 'private'`, plus one mapping with `sourceFormat: 'json'`, `targetObject: 'fx_account'`, `mode: 'upsert'`, a `fieldMapping` and `connectorSource: { connector: 'crm_api', action: 'request' }`. - At base, `os validate --json` and `os lint --json` both exited 1, and the only error was the sentinel. - After the fix, on the merged head `b6d58dd04`: `validate` exits 0 with `"valid": true`. `lint` exits 0 with `"passed": true` and one unrelated warning (`protocol/missing-engines-range`). Neither output names `connectorSource` or contains the sentinel. - The crash had been hiding a real finding. Without `sharingModel`, the same object gets `security-owd-unset`, so the fixture declares it. - **H2, the integrity check.** Yes, inside `check:liveness`. Run against the unfixed ledger, the gate exits 1 with exactly one ✗ block: "1 `live` ledger row(s) opt into `authorWarn` — the author-side lint throws on them: mapping/connectorSource". - **H3, census** (re-run on the merged head). Across all 41 governed ledgers, 924 rows were walked at every depth. Two carry `authorWarn: true`, both `planned` (`object.externalSharingModel`, `translation.flows`). None is `live`. Before the fix, `mapping.connectorSource` was the only `live` one, so no other row has this defect. - **H4, describe text.** Done as above. Author-facing text cites no tracker number. ## Pins - `packages/spec/src/data/mapping-connector-source.test.ts`: every key of the binding is `live`, and no row of it, at any depth, carries `authorWarn` / `authorHint`. The description still says nothing schedules a pull. The old assertion `authorWarn === true` pinned the defect. - `packages/spec/scripts/liveness/check-liveness.test.ts` runs the real gate via `--ledger-root`: - green on the shipped ledgers, with the census line showing warned `planned` rows and none on a `live` row (the control); - red when a `live` container row with `children` opts in (the regression's shape); - red when a drilled `live` child opts in. - The carriers are picked from the shipped ledgers by shape, not by name. - `packages/lint/src/lint-liveness-properties.test.ts` runs against the real ledger. The card's fixture lints without throwing and draws no `connectorSource` finding. Control: `object.externalSharingModel` (`planned` + `authorWarn`) still warns as `liveness-planned-property`. - `packages/lint/src/runtime-gate.inert-type-writes.test.ts`: a `mapping` write that authors `connectorSource` gets no errors and no advisories at the runtime door. - `packages/cli/test/validate-lint-mapping-connector-source.test.ts` is in the integration tier and spawns the CLI: - the card's fixture: `os validate --json` exits 0 with `valid: true`, and `os lint --json` exits 0 with `passed: true`; - control: adding `externalSharingModel` produces a `liveness-planned-property` warning, still at exit 0. ## Reverse verification (one-time, not a permanent file) This was run twice on the committed fix, with identical results: once at `7bc7c5c26`, and again after the `main` merge at `b6d58dd04`. `node scripts/ablation-replace.mjs` put `"authorWarn": true` back on the `connectorSource` row. The anchor count went 1 → 0, and the blob changed `49cc72e394b8` → `65665b90b21d`. With the defect back, every leg went red: - door: `validate` exit 1 and `lint` exit 1, with the sentinel in both; - `check:liveness` exit 1 (`1 on a live row — FORBIDDEN`); - spec pins: 2 failed; - lint pins: 3 failed; - CLI door pin: 3 failed. The tool restored the file and proved it: the blob equals HEAD and `git diff HEAD` is empty. The ledger is read from `packages/spec/liveness/` at runtime, not from `dist/`, so no rebuild was involved. ## Verification (all at `b6d58dd04`, the PR head) `origin/main` `fde553c50` was merged in with `bash scripts/pm/os-regen-merge.sh`. The merge was clean. `registry.ts` auto-merged as text, and `check:migration-registry` then confirmed it. Main had not touched the two reference pages, so they kept the branch's bytes. No regeneration commit was needed: after a fresh spec build, `check:generated` reports all 15 generated artefacts up to date. The delta against `main` is unchanged: 19 files, +538 / −42. | what | command | result | |---|---|---| | spec build | `pnpm --filter @objectstack/spec build` | exit 0 | | generated artefacts | `pnpm --filter @objectstack/spec run check:generated` | exit 0, "All 15 generated artifacts are up to date" | | CLI closure build | `pnpm exec turbo run build --filter=@objectstack/cli... --concurrency=2` | exit 0, 59/59 tasks | | the card's fixture | `os validate --json` / `os lint --json` | exit 0 / exit 0, sentinel 0 times, `connectorSource` 0 times | | ledger integrity | `pnpm --filter @objectstack/spec run check:liveness` | exit 0, "0 on a `live` row" | | strictness ledger | `pnpm --filter @objectstack/spec run check:strictness-ledger` | exit 0 | | spec tests | `vitest run scripts/liveness src/data src/integration/connector-sync-retirement.test.ts src/migrations` | exit 0, 130 files, 4269 passed, 1 todo | | spec typecheck | `pnpm --filter @objectstack/spec typecheck` | exit 0 | | lint, whole package | `pnpm --filter @objectstack/lint exec vitest run` + `typecheck` | exit 0, 118 files, 5489 passed; typecheck exit 0 | | CLI unit tier | `vitest run --project unit test/lint test/validate src/commands/validate src/commands/lint` | exit 0, 11 files, 125 passed | | CLI door pin | `vitest run --project integration test/validate-lint-mapping-connector-source.test.ts` | exit 0, 3/3 | | eslint, narrowed | `pnpm exec eslint --no-inline-config --format json` on all 19 changed paths | exit 0. eslint linted 12 and reported 7 (`.md` / `.mdx` / `.json`) as "no matching configuration"; 0 errors, 0 warnings | | derived gates | `dispatch-gates.mjs --commands` (117), each run, then `--ran` | 117 run: 116 exit 0, and 1 exit 1. That one is `check-empty-changeset --base origin/main`, the deliberate correction above. `--ran`: "117 run, 0 NOT-MEASURED, 0 UNRUN". `check:pm-dispatch-gates` was killed by a timeout at 400s and then at 540s. It was then run to completion and exited 0; its battery took 1432s on this box | **The eslint narrowing is a measurement, not a skip:** - The population comes from eslint's own answer for each changed path: 12 linted, and 7 that `eslint.config.mjs` has no configuration for. - The file count comes from the `--format json` output. - The config enables no type-aware linting (no `parserOptions.project`), so this diff cannot move the verdict on any file it did not touch. ## Acceptance notes - **turbo 2.11.5 wrote into `AGENTS.md` before the merge.** It appended its managed `turborepo-agent-rules` block on every AI-attributed turbo run in this worktree: 3 builds, and once during `check:type-check-debt`. Each time the file was restored with `git checkout HEAD -- AGENTS.md`, and `git diff HEAD` was confirmed empty. Files were staged by path only, so the block was never staged and is not in this PR. PR objectstack-ai#21151 (`agentGuidance: false` in `turbo.json`) came in with the merge. Since then, `AGENTS.md` stayed unmodified across the 59-task CLI closure build and all 117 gates. - `@objectstack/lint` gets no changeset. Its source change is comments only. Measured: tsup keeps comments, so the comment text does reach `dist/*.js` bytes. The seat can override this. - objectstack-ai#21127 is cited only where agents and reviewers read: the `authoring-rules.ts` comment, and the ledger's `note` and README prose. Author-facing text (`.describe()`, the retired-key prescription, the upgrade entry) cites no tracker. --- _Generated by [Claude Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…lared JSON-stored field, in the SQL family's words (objectstack-ai#21066) (objectstack-ai#21159) Fixes objectstack-ai#21066 Clause-②: yes (narrowing) On a field the object declares JSON-stored (a `multiple: true` field, `tags` / `multiselect` / `checkboxes`, or a structured-JSON type such as `json`), `driver-memory` now refuses the scalar-comparison family that `driver-sql`'s `where` refuses: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$in`, `$nin` and implicit equality, whatever the comparand, at any depth. The answer is `INVALID_FILTER` / 400 with the same message. The operator set and the sentence are read from `@objectstack/core` (`JSON_COLUMN_INCOMPATIBLE_OPERATORS`, `jsonColumnOperatorRefusalText`, homed by PR objectstack-ai#21097). There is no third copy. `$contains` / `$notContains` (membership), `$null`, `$exists` and `$empty` keep answering. ## What was wrong (H1, measured at `origin/main` `670680e93` through `engine.find`) A real `ObjectQL` over `InMemoryDriver`, objectstack-ai#21004's six rows (`owners` is a `multiple: true` lookup, `tags` is a `tags` field). Every row reproduces the card: | `where` | before | now | |:--|:--|:--| | `owners` `$eq 'u1'` | `d1`, `d3` (per element) | 400 `INVALID_FILTER` | | `owners` `$in ['u1','u9']` | `d1`, `d3` | 400 | | `owners` `$nin ['u1','u9']` | `d2`, `d4`, `d5`, `d6` | 400 | | `owners` `$gt 'u1'` | `d1`, `d2`, `d3`, `d5` | 400 | | `tags` `$gt 'red'` | `d3` | 400 | | also: bare `{ owners: 'u1' }`, `$ne`, `$gte`, `$lt`, `$lte`, `$between`, `tags $eq`, `{ owners: null }`, `$eq null`, `$ne null`, `$in []`, `$nin []` | rows, per element | 400 | | controls: `owners $contains 'u1'` / `$notContains` / `$null` / `$exists` / `$empty`, `title $in` | rows | unchanged rows | The engine hands the driver the operators as written, except `$ne` / `$nin`. Those arrive inside the spec's null-safe lowering (`$and` of `$or` of `$null: true` and the operator). The gate walks `$and` / `$or` / `$not`, so that shape is refused too. The analytics face (`MemoryAnalyticsService`) answered the same per-element rows. Its SQL echo rendered `owners = 'u1'`, which matches no row over the JSON text the SQL family stores. It now refuses in `query()` and `generateSql()` alike. ## What changed - `filter-refusal.ts`: the shape gate (`assertFilterConditionShape`) takes an optional `FilterFieldDeclarations` (`isJsonStoredField`, `reportWithheld`). It has two arms. Implicit equality on a declared JSON-stored field is refused as `=`, bare. Any operator in the shared set is refused AFTER the existing comparand-shape rules, which is `driver-sql`'s order (comparand gate, then column-type gate). So an array under `$eq` or a one-element `$between` still gets its own refusal first. `jsonStoredFieldOperatorError` builds the error from the shared text: this package's `unsupportedFilterError` envelope, with the withheld diagnostic handed to `reportWithheld` (prefixed `At PATH:`) before the throw. - `memory-driver.ts`: `convertToMongoQuery` passes `this.filterFieldDeclarations(object)`. The population is `isJsonStoredField`, the predicate `$contains` already forks on (`STRUCTURED_JSON_TYPES` or `isMultiValueField`). So the fields where `$contains` asks membership are exactly the fields where the family is refused. The diagnostic goes to the driver's logger at `warn`, the level `driver-sql` uses for its withheld filter diagnostics. That keeps the message's "the full diagnostic is in the server log" true here. - `memory-analytics.ts`: `normalizeFilters` takes the cube. It judges a `where` key (a cube member) by the field it maps to on the cube's table, the same (table, field path) pair `filterContainsTest` reads. Its diagnostic goes to the analytics service's own logger. - `.changeset/21066-memory-json-column-family-refusal.md`: `@objectstack/driver-memory` `minor`, BREAKING banner, `Clause-②: yes (narrowing)`, one ADR-0087 marker `not-required (no-migration-prescription)`. No registered id covers a filter operator on a JSON-stored column. The one migration-registry entry that mentions json columns (`cel-predicate-one-value-comparand-refused`) is the CEL list-comparand surface, not this one. ## Hypotheses, measured - **H1** holds: the table above. - **H2.** The shared home is `@objectstack/core`'s `json-column-operator-refusal.ts`, and both names are read. Before this change `driver-memory` had NO withheld-diagnostic seam: every refusal it raises (the `$null` / `$exists` non-boolean refusals included) names the field in the message, and nothing in the package logged a diagnostic. This change keeps the shared posture: the message names neither field nor operator, and the diagnostic goes to the server log. - **H3.** `driver-sql` decides a JSON column from `jsonFields`, filled from `JSON_COLUMN_TYPES.has(type) or isMultiValueField(field)`. `JSON_COLUMN_TYPES` is `STRUCTURED_JSON_TYPES` plus `MULTI_OPTION_TYPES` plus the driver-internal `object` / `array` aliases. Memory's population is the same predicate less those aliases and less a single-value media field on an unmoved deployment (both recorded on `isJsonStoredField`). On a schemaless direct call (an object never passed through `syncSchema`), nothing is judged. Every operator answers per element as before, as `SqlDriver.isJsonColumn` answers `false` for a table it was never told about. Pinned. A field declared SCALAR (`text`) that holds an array is not judged either. - **H4.** `@objectstack/formula`'s `ORDERING_OPERATORS` docblock does NOT declare a per-element reading for the query plane. It records a non-alignment ("driver-memory's read, a frozen test driver, compares a stored list element by element and keeps returning those rows ... declared on objectstack-ai#15104"). objectstack-ai#15104 is the `$field` cross-field reference card, shut as `not_planned` under the driver-memory investment freeze. It rules nothing about the equality or ordering family on a stored list. So this is a formula-plane record of observed behaviour, not a query-plane contract, and no contract conflict stops the card. That docblock sentence goes stale on declared fields once this lands (see Acceptance notes). - **H5.** objectstack-ai#21009 widens the same shared set to the text operators. Both gates here read the set live, and the new suite iterates `JSON_COLUMN_INCOMPATIBLE_OPERATORS` intersected with this driver's vocabulary, with a floor of the nine `$`-spellings. So once both land, memory refuses `$startsWith` / `$endsWith` / `$icontains` on these fields with no edit here, and the suite pins them. Whichever of the two lands second merges `main` and checks the other's members on its face. The suite's `$contains` control is outside objectstack-ai#21009's scope. ## Pin sweep - The ONE per-element pin the package carried on a declared field flipped: `memory-20444-empty-operator.test.ts` had `{ tags: { $empty: true, $ne: null } }` giving `r2`. It is now a refusal pin (`code` + `status` + the shared message). The composition (`$empty` beside a has-a-value sibling on one multi-value field) is kept through `$null: false`, which answers `r2`. `driver-sql`/SQLite answers that row too, and refuses the `$ne: null` spelling with the same body (measured on the built driver). - `memory-matcher-scalar-comparand-array-value.test.ts` pins per-element answers on a column declared `text`. Those cells still hold, and a header note now says the population there is a scalar-declared column. - Repo-wide: only two tests outside this package bind the real driver (`packages/runtime`'s two ruled consumers). Neither filters a JSON-stored field. No other `INVALID_FILTER` pin moves. ## Tests (final head `13407b76f`) - `pnpm --filter @objectstack/driver-memory exec vitest run --maxWorkers=2`: **70 files, 1703 passed**. The first run after the implementation, before any test edit: 69 files, 1 red of 1613, the per-element pin flipped above. - `pnpm --filter @objectstack/driver-memory run typecheck`: exit 0. `tsc --listFiles` includes both edited test files. - New `memory-21066-json-column-family-refusal.test.ts` (89 tests): - the card's five rows; - every family member on `owners` / `tags` / `meta` (`json`); - ten shapes per field (bare, bare null, `$eq null`, `$ne null`, the engine's `$ne` lowering, `$in []`, `$nin []`, under `$not`, an `$or` branch after a holding one, `$eq` beside `$contains`); - `count` / `findOne` / `updateMany` / `deleteMany` refusing with the table untouched; - the withheld message plus the logged `At filter.$or[1].owners.$gte:` diagnostic; - comparand-first ordering; - eleven answered controls; - the declaration boundary (undeclared object, scalar-declared column, the gate with and without declarations); - the analytics face, `query()` and `generateSql()`, including the `cube.member` spelling, plus its log line and a `$contains` control. - Ablations (`node scripts/ablation-replace.mjs`, wrap mode, run at `de1fef341`; the two later merges touched no file in this package; each restore proven blob == HEAD with `git diff HEAD` empty). The subjects are this package's `src`, imported relatively, so no `dist` is involved: - **A** `filterFieldDeclarations`' predicate forced to `() => false`: **73 red / 37 green** of 110 across the new file and 20444. The 17 green in the new file are exactly the answered controls, the declaration-boundary trio, the premise, the comparand-first case and the analytics `$contains` control. - **B** the implicit-equality arm disabled: **7 red**, exactly the six bare cases and the analytics bare case. - **C** the operator arm disabled: **67 red**, every operator-based refusal pin, the direct-gate test and the 20444 flip. - Gate union, derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths; 7 changed paths, working tree clean) at `13407b76f`: **60 derived, 60 run, every one exit 0**. Reconciled with `--ran`: "60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED (a DERIVED zero — all 60 recorded an exit code and none of them is 3)". The same 60 also ran all-zero at the previous merge head `5b75fe461`. At `de1fef341`, `check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET, no dist yet); it measured on both later heads. - Driver conformance ledger (`node scripts/check-driver-conformance.mjs`), before and after: byte-identical. 50 covered cells, 0 DEBT, 0 exempt. The shared matrix has no JSON-column or multi-value case-set, so this invariant is held by the per-package pins, not the matrix. ## Acceptance notes - **The class gains one method.** `InMemoryDriver.filterFieldDeclarations` is tagged `@internal`. It is not private only because the analytics face is another class. `FilterFieldDeclarations` is not exported from the package root, but the method does appear in the published `.d.ts`. objectstack-ai#20984 graded the analogous public `filterContainsTest` as a surface widening (`Clause-②: yes (widening)`). The seat graded it so (5929927010): the line is `yes (narrowing)`, with the semver (`minor`) and the ADR-0087 marker unchanged. - **Surface beyond the claim's list.** `memory-driver.ts` and `memory-analytics.ts` are edited. The gate cannot see a declaration on its own, so the plumbing is the minimum the direction needs, and the analytics face calls the same gate. No open PR touched either file when read before the first edit. - **The AST comparison-node door** (`{ type: 'comparison', field: 'owners', operator: '=', value: 'u1' }`) still answers per element on a declared field: `d1`, `d3`, measured on the built driver. No seam emits that form (the engine and the protocol hand a driver a FilterCondition), so it is reachable only by a direct driver call. Left alone. - **The shared sentence's mechanism clause** ("a field this driver stores as a JSON TEXT column", "$in/$eq matched nothing") is `driver-sql`'s, and is literally untrue of this driver and of the engine's per-aggregation face. The prescription (`$contains`, an `$or` of `$contains`) is right on all three. Inherited as objectstack-ai#21007 shipped it. objectstack-ai#21009 is the PR that next edits the shared home. - `@objectstack/formula`'s `ORDERING_OPERATORS` docblock ("driver-memory's read ... keeps returning those rows") is now true only of undeclared objects. It is a comment, and no claim holds that file. - **Tooling.** With the `turbo` 2.10.10 to 2.11.5 bump now on `main`, every repo-scoped turbo run in an agent session appends a managed "turborepo-agent-rules" block (an HTML-comment-delimited section) to `AGENTS.md`. These include `pnpm exec turbo run build`, `pnpm check:type-check-debt`, `check:query-options-erasure` and `check:slot-lookup`. It happened repeatedly in this worktree and was restored each time, and every gate derivation above was taken on a clean tree; this PR does not touch `AGENTS.md`. Tracked as objectstack-ai#21146 (PR objectstack-ai#21151). --- _Generated by [Claude Code](https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #21146
Clause-②: no
Adds
"agentGuidance": falseto the rootturbo.json, and nothing else (1 file, +1 line).Why
turbo 2.11 (landed with the 2.11.5 bump) defaults the root option
agentGuidancetotrue: when it detects an AI agent it maintains a managed block, the BEGIN/ENDturborepo-agent-rulespair, in the repository-rootAGENTS.mdbefore repository-scoped commands, and the block tells the agent to keep it committed.AGENTS.mdis a governed surface, so every agent worktree that obeys turns its PR into a human-approval PR. The opt-out is the root-only key turbo's own bundleddocs/reference/configuration.mdxnames. No other file is in the surface;AGENTS.mdwas never staged (see Evidence).Evidence
All commands run in this worktree at base e952cff with
AI_AGENTset in the environment,turbo2.11.5 (node_modules/.bin/turbo --version).AGENTS.mdsha1 before every leg:99e45344e3582f8b5e9c9e9987fc3c8375c1999d.turbo.jsonwith no opt-out:turbo lsexit 0, thengit diff --exit-code -- AGENTS.mdexit 1,1 file changed, 11 insertions(+). The 11 lines are the managed block appended after the last line ofAGENTS.md. Restoring withgit restore --source=HEAD --staged --worktree AGENTS.mdand runningturbo lsagain re-added the same block (exit 1 again), so the write is repeatable and not a one-off.turbo lsexit 0,git diff --exit-code -- AGENTS.mdexit 0,git status --shortlists onlyturbo.json,AGENTS.mdsha1 unchanged.turbo run build --dry=json --filter=@objectstack/typesexit 0,git diff --exit-code -- AGENTS.mdexit 0 again.git diff --cached --name-onlybefore the commit liststurbo.jsononly (zero lines matchAGENTS.md);git show HEAD --name-onlyliststurbo.jsononly.Readers of
turbo.jsonNone pins the top-level key set, so nothing is loosened. Readers measured at HEAD 3e3b4c9 (tree identical to HEAD,
git diff HEAD --quietexit 0), each exit code taken before any pipe:dispatch-gates.mjs --commands(9 commands, derivation identical before and after the commit):check-closing-keyword-parity.mjs(plus--self-test),check-comment-mask-corpus.mjs,check:driver-memory-census,check:gitlink-declared,check:nul-bytes,check:refd-timer-probe,check:turbo-task-graph,check:watch-hint-literal: all exit 0.--ranreconciliation: 9 derived, 9 run, 0 NOT-MEASURED, 0 UNRUN, every family carrying an exit code.turbo.jsonread points, run beyond the derivation:check-ci-filter-parity.mjs(and--self-test),partition-test-shards.mjs --self-test,check:cross-package-test-inputs,check:examples-live-imports,check:type-check-coverage: all exit 0. They readtasks,globalDependenciesand per-taskinputs; none iterates the top-level keys.pnpm check:dev-prereqs(exit 1, "The workspace is not built": 67 of 67 packages have nodist/, a prerequisite and not a verdict; this diff touches no package, so no build was owed). Its--self-testis exit 0, and its only read ofturbo.jsonisglobalDependenciesthroughscripts/build-input-hash.mjs, which this change does not move.pnpm lintand the rest of thecheck:*farm.scripts/ci/select-gate-families.shclassifiesturbo.jsonas root config, so CI runs every gate family on this PR by design.Changeset
None owed: the root
turbo.jsonpublishes nothing (root config, private root package)..github/workflows/pr-automation.yml'sCheck Changesetjob counts changesets this PR adds and has no path exemption, only theskip-changesetlabel, so the PR carries that label.Acceptance notes
git restore --source=HEAD --staged --worktree AGENTS.md; never commit the block.turbo.jsonis not on the governed-surface register, so this PR is not a Tier H or Tier S landing.Generated by Claude Code