Repository navigation
fix: array field types persist + Field.time accepts time-of-day - #2004
Merged
Merged
Conversation
Two field-type runtime gaps found driving the showcase field-zoo (which had
no seed data, so neither was ever exercised at runtime).
1. Array/object fields broke every write (driver-sql). multiselect/checkboxes/
tags/repeater/vector were missing from the SQL driver's JSON-field
classification, so their arrays reached the better-sqlite3 binder
un-serialized → "SQLite3 can only bind numbers, strings, bigints, buffers,
and null" 500 on insert/update (incl. task.labels on a normal object). The
DDL column-type switch and isJsonField had drifted into two lists; unified
into one JSON_COLUMN_TYPES source covering the array/object types, plus a
formatInput safety net that serializes any stray array/object so an
unclassified field degrades to a stored string instead of crashing.
2. Field.time rejected every valid value (objectql). The validator reused the
date/datetime Date.parse branch, which is NaN for any bare time string, so
`time` could never accept 14:30 / 09:05:30. Now validates HH:MM / HH:MM:SS
(optional fractional seconds + Z/offset), still accepts a full ISO datetime;
date/datetime unchanged. (Date.parse('14:60') returns a bogus number, so the
fallback requires a real date component — malformed times are rejected.)
Verified live on app-showcase: full field-zoo specimen now persists +
round-trips. Tests: driver-sql array round-trip (155 green) + time validator
(objectql 652 green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 19 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
added a commit
that referenced
this pull request
Jun 17, 2026
…ook (#2005) The Field Zoo declared all 48 field types but shipped NO seed data, so no field type was ever exercised with a real value — which is exactly why the array-serialization and Field.time bugs (fixed in #2004) went unnoticed. - Seed two field-zoo specimens ("Full" + "Minimal") covering every input-able field type with real values: arrays (multiselect/checkboxes/tags/repeater/ vector), objects (composite/address/location/json/record), time (14:30 / 09:05:30), relational (lookup→Account, master_detail→Project by externalId), and computed (formula=315, autonumber). `f_secret` is intentionally omitted — the seed path has no CryptoProvider so a secret value is refused fail-closed. This doubles as a runtime regression guard: a field type that can't persist now fails the boot seed instead of shipping silently. - Guard the showcase_warn_over_budget hook condition with has() — a partial rollup update (task_count recompute) fires afterUpdate with a record lacking spent/budget, and CEL throws "No such key" on a bare record.spent. has() is the missing-key-safe macro; the noisy warning is gone. Verified live: both specimens seed (37 populated f_* fields on Full); coverage test 20 green; warn_over_budget "No such key" warnings = 0 after a task update. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 17, 2026
os-zhuang
added a commit
that referenced
this pull request
Jun 18, 2026
…thoring) (#2022) Extends the dogfood gate from "pin one example app" toward the platform's real risk: third parties have an AI author arbitrary metadata, so every authorable primitive needs a runtime proof — not a static shape check. `showcase_field_zoo` carries one field of (almost) every protocol FieldType but was only static-checked (the bundle registers it). This adds the first capability-matrix block: write one record covering many field types over the real REST data API, read it back, assert each round-trips with type fidelity. - Headliner coverage = the #2004 regressions: array-typed fields (multiselect/checkboxes/tags) and Field.time (time-of-day) — all proven to round-trip end-to-end on the in-memory (WASM) driver path `objectstack dev` uses for :memory:. - On first run it surfaced three real type-fidelity gaps — rating/slider/toggle persist but read back as strings ('4'/'25'/'1') while number/currency/percent/ boolean round-trip correctly. They are quarantined with `it.fails` (passes while broken, turns RED when fixed) rather than hidden by coercion, and filed as a separate task. Private package — no changeset. 21 passed + 3 expected-fail. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang
added a commit
that referenced
this pull request
Jun 25, 2026
fix(console): route org-less users into guided org creation on multi-org (#2004) objectui@9130b33ec35ac79bcd0da2618be4806e49ac2475
os-zhuang
added a commit
that referenced
this pull request
Jun 26, 2026
…ad (SQLite) (#2347) Field.time is a tz-naive time-of-day, not an instant (#2004). A defaultValue:'NOW()' time column historically took the full SQLite CURRENT_TIMESTAMP default, so a defaulted/legacy row read back a full 'YYYY-MM-DD HH:MM:SS' timestamp instead of a time-of-day. formatOutput now repairs a Field.time value to just its time portion (toTimeOnly): a legacy full timestamp (or a full ISO value that leaked into the column) is sliced to HH:MM[:SS[.fff]], while a value already stored as a bare time-of-day is left untouched. Deliberately NARROW + read-only (no write/filter counterpart) so it introduces no write/read asymmetry and preserves exact round-trips for bare time-of-day values (the field-zoo f_time guard). Runs for every dialect (a native TIME column already returns a time-of-day → no-op). Adds a timeFields registry + registration mirroring dateFields/datetimeFields. Completes the temporal read normalization alongside #2346: datetime -> ISO-8601-Z, date -> YYYY-MM-DD, time -> wall-clock time-of-day. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang
pushed a commit
that referenced
this pull request
Aug 1, 2026
…synthetic ids (#4441) `field-zoo-roundtrip` seeded its three relational fields with ids that exist in no row: { field: 'f_lookup', write: 'acc_synthetic_0001' } { field: 'f_master_detail', write: 'proj_synthetic_0001' } { field: 'f_tree', write: 'cat_synthetic_0001' } under a comment reading "FK enforcement is off in this harness". That comment described a HOLE — precisely the one #4441 closes — so the fixture depended on the defect the platform now prevents, and the create started failing with `reference_not_found` for all three fields at once. This is the fixture being wrong, not the check being strict, so the data is what changes. The suite now creates a real row in each target object and substitutes its id. What the file actually proves is unchanged — an id string must round-trip as the same id string, the #2004 type-fidelity guard — and it now proves it over references that resolve, which is strictly stronger. The matrix keeps a `REFERENCE_PLACEHOLDER` symbol rather than a magic string, so a future edit cannot typo its way back into writing a literal id, and the placeholder can never reach the wire: substitution is keyed on identity. `REFERENCE_TARGETS` is ORDERED with a body factory because the targets reference each other — `showcase_project` declares a REQUIRED lookup to `showcase_account`, so the account must exist first and the project must be given its real id. Seeding them in the wrong order now fails loudly instead of writing a project that points at nothing, which is a small proof of #4441 in its own right. `status: 'planned'` is the state machine's declared initial state; `active` is refused with `invalid_initial_state`. Verified: `field-zoo-roundtrip.dogfood.test.ts` 46/46 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 2, 2026
…i#4441) (objectstack-ai#4511) * wip: rescue in-progress work before container restart loss * fix(objectql): a lookup id that resolves to nothing is refused on write (objectstack-ai#4441) A `lookup` accepted an id that exists in no row of the object it declares, on both platform and application objects: POST /data/sys_position_permission_set {"position_id":"…","permission_set_id":"ps_does_not_exist_at_all"} → 200 POST /data/showcase_task {"title":"…","project":"proj_does_not_exist","status":"backlog"} → 200 The field metadata is unambiguous (`type: lookup`, `required: true`, `reference: 'sys_permission_set'`) and `deleteBehavior: 'set_null'` shows the platform already reasons about this edge on the DELETE side. Only the insert side never checked. On the RBAC link tables that is a security-surface record that resolves to nothing: an administrator auditing permissions sees a binding whose target cannot be inspected, and the audience-anchor gate must resolve that very set to evaluate the grant — so the row is an unevaluable gate input, not just untidy. Enforced in the engine, so every write path inherits it (REST, flows, actions), with the refusal carried in the `fields[]` envelope a `required` violation already uses: `{ field, code: 'reference_not_found', value, message }` → `400 VALIDATION_FAILED`. `reference_not_found` was already a catalogued `FieldErrorCode` with no emitter; it has one now, plus its message in the four platform locales. Scope kept deliberately narrow: - **Caller-supplied keys only.** `owner_id` / `organization_id` / `created_by` / `updated_by` are lookups too, written by hooks and middleware; re-validating them would turn a platform stamp into a caller-facing rejection. - **Non-system writes only**, like every other write-path guard here (`stripReadonlyFields`, `stripReadonlyForInsert`). Seed replay, package install and boot provisioning legitimately write in an order that resolves only once the batch completes; failing them closed turns an ordering detail into a boot failure. The residual — an `isSystem` caller can still write a dangling reference — is recorded rather than silently accepted. - **Empty is not a reference.** `null` / `undefined` / `''` mean "no link" — exactly what `deleteBehavior: 'set_null'` produces. - **Fails OPEN when the target cannot be checked** (unregistered object, no driver, probe throws). An integrity check that cannot run must not invent a rejection; the alternative converts a connectivity problem into data loss. - **The probe is unscoped.** Existence is a fact about the database, not about the caller's visibility — the same distinction objectstack-ai#4435's probe turns on. A scoped probe would refuse a link to a permission set the caller cannot READ, which is ordinary under RLS and would break the platform's own admin flows. Whether the caller may create the binding is the RBAC/RLS layer's decision. Wired at BOTH update call sites, single-id and bulk — PD objectstack-ai#10's own worked example (objectstack-ai#3106) is a guard that reached only the single-id path. No new authorable spec key and no protocol change: the issue's "optional dangling references should be opt-in" would need one, so it is NOT implemented here and is left as a decision for the maintainers. Tests: `engine-lookup-referential-integrity.test.ts` (10) — the RBAC link table and an ordinary object, insert / update / bulk update, multi-value elements, clearing a lookup, the system-context exemption, server-stamped lookups, and the fail-open target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD * fix(objectql): the audit anchor is engine-owned — a declared created_at cannot loosen it (objectstack-ai#4447) `created_at` accepted a client-supplied value on an ordinary REST write and PERSISTED it: PATCH /api/v1/data/showcase_task/<id> {"progress":42,"created_at":"1999-01-01T00:00:00Z"} → 200, and the row reads back 1999-01-01 from then on Any authenticated caller who could edit a record could forge the anchor of the audit timeline — the field historical import deliberately preserves, dashboards bucket on, and an auditor reads to establish when a record came into existence — silently, in the same request as a legitimate edit, with no diagnostic. Its two siblings only LOOKED protected. The audit hook force-advances `updated_at` / `updated_by` on every update, so a forged value there is overwritten rather than refused. `created_at` is insert-only, so nothing overwrote it: one field out of the trio was genuinely unguarded, which is why this read as "one field, not a posture". ## Root cause Not a missing definition — `AUDIT_FIELD_DEFS.created_at` has carried `readonly: true, system: true` all along. The hole is that it never applied: `applySystemFields` injects an audit field only when the object does not already have one, and the merge (`{...additions, ...schema.fields}`) lets a declared field WIN. Correct for an authored business field; wrong for a family whose whole point is that the engine owns it. `showcase_task` declares no `created_at` in source, yet the built artifact ships one: "created_at": {"label":"Created At","type":"datetime","readonly":false, …} — a materialized field carrying only FieldSchema DEFAULTS. It shadowed the engine-owned definition, so `stripReadonlyFields` had nothing to key off and the forged value went straight through. No `droppedFields` either, because from the platform's point of view nothing was dropped — which is also why the objectstack-ai#3794 contract had no live producer on this axis to test against. ## Fix The audit family's GOVERNANCE is not authorable. A declared audit field now keeps everything presentational (label, description, hidden, group, ordering …) and has `type` / `readonly` / `system` / `reference` forced to the platform's values, derived from `AUDIT_FIELD_DEFS` rather than restated so the two cannot drift. Defensive by design: it closes the class for any producer of a bogus audit field — artifact, stored metadata, AI-authored object, hand-written YAML — not just the one that surfaced it. Deliberate back-dating is untouched: `preserveAudit` (objectstack-ai#3479/objectstack-ai#3493) and `isSystem` writes still reinstate the original timeline, because both are checked downstream of `readonly`, not by it. Forcing `readonly`/`system` also only ever RELAXES validation — `validateRecord` skips system/readonly fields — so no previously-accepted write starts failing. ## Verified on a real boot `pnpm dev -- --fresh -p 38106`, showcase, ordinary admin session, the issue's own curl: before {"created_at":"2026-06-22T00:00:00.000Z","progress":55} PATCH {"progress":42,"created_at":"1999-01-01T00:00:00Z"} → 200 droppedFields: [{"fields":["created_at"],"reason":"readonly"}] after {"created_at":"2026-06-22T00:00:00.000Z","progress":42} The legitimate half of the request still lands, the anchor does not move, and the drop is now REPORTED — the `droppedFields` contract's first live producer here. Tests: `engine-audit-anchor-write.test.ts` (10) — the update strip, the `droppedFields` report, the whole trio behaving alike, the bulk call site, the `preserveAudit` and `isSystem` escape hatches, and the shadowing root cause reproduced with the artifact's field verbatim. Verified failing on the three root-cause cases before the fix. Known follow-up, NOT fixed here: `/api/v1/meta/objects/showcase_task` still reports `readonly: false` for `created_at` — that surface reads the artifact rather than the registry, so it now disagrees with the enforcement. Filed separately; a machine-readable surface must not lie (AGENTS.md, Route & surface ownership objectstack-ai#4). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD * fix(objectql): check only the reference the CALLER named, and narrow the audit override (objectstack-ai#4441, objectstack-ai#4447) Two corrections found by running the full objectql suite (1554 tests). ## The reference check validated platform-derived ids (objectstack-ai#4441) Filtering by supplied KEY was wrong. A form serializes an unpicked control as an explicit `null`, and `applyFieldDefaults` then fills it from `defaultValue` — including the `current_user` token (objectstack-ai#2706). The key IS in the payload, but the id that lands is the platform's, so the check reported a server-derived value as the caller's bad reference and rejected an ordinary insert whenever the acting principal had no row in the target object. The value is still read from the post-normalization payload (multi-value strings are already split by then), but WHETHER to check is now decided by the caller's own RAW value being non-empty. `engine.test.ts`'s objectstack-ai#2706 case was the one that caught it. ## The audit override forced more than the defect required (objectstack-ai#4447) Forcing `type`/`reference` broke `registry.test.ts`'s "does NOT overwrite author-declared audit fields", and that test is right: an external/federated object legitimately maps its audit column to a differently-typed remote column, and objectstack-ai#4447 is about WRITABILITY, not storage shape. The override now carries only `readonly` and `system` — the keys that decide who may write it — so the author keeps `type`, `label`, `description`, `hidden`, `group` and the rest. The pre-existing test passes unchanged. Suite green: objectql 95 files / 1554 tests. New case pinned in `engine-lookup-referential-integrity.test.ts` (11) for the derived-value direction, both for an explicit `null` and an omitted key, plus the proof that a value the caller DID name is still checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD * docs: correct the two pages this change makes untrue (objectstack-ai#4441) Checked the three hand-written pages the docs-drift report flagged as actually describing behaviour these fixes change (the other ~107 are indirect `@objectstack/spec` references). **`api/error-catalog.mdx` — was wrong.** It documented `INVALID_REFERENCE` as what a `lookup`/`master_detail` pointing at a missing record answers with. That code has ZERO producers in source, and now that dangling references are actually refused, the real answer is `400 VALIDATION_FAILED` with `fields[].code === 'reference_not_found'`. The entry now says so, tells callers which code to branch on, and a callout carries the real envelope plus the three deliberate non-rejections (empty value, `isSystem` write, uncheckable target). `INVALID_REFERENCE` itself is left in place — it is a `StandardErrorCode` member, and removing it is a spec change, not a docs fix. **`data-modeling/fields.mdx` — was silent.** The lookup section documented `reference` and `deleteBehavior` without saying whether the target has to exist; it does now, so authors need it. Added a short "Referential integrity" note with the rejection shape, the bulk-update coverage, and the fact that clearing a relationship is not a dangling reference — plus the distinction from `deleteBehavior`, which governs the other half of the same contract. **`protocol/objectql/schema.mdx` — accurate as-is, unchanged.** It documents the `reference` PROPERTY in a property table, not write-time semantics, so it makes no claim this change falsifies. Duplicating the note there would be two places to keep in sync. Also checked `droppedFields`, since objectstack-ai#4447 gives that contract its first live producer on the audit axis: the only non-generated mention is in `protocol/kernel/http-protocol.mdx`, about the CORS-exposed `x-objectstack-dropped-fields` header, and it is correct. `releases/v17.mdx` mentions it too but is deliberately untouched — release notes are written centrally at release time (CLAUDE.md), never as a rider on a code PR. `content/docs/references/` is auto-generated and was not touched. `pnpm check:doc-authoring` green (215 files). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD * fix(objectql): a readonly lookup is not the caller's reference to answer for (objectstack-ai#4441) The dogfood gate rejected ordinary metadata authoring — package create, publish and clone all failed with: ValidationError: Recorded By: no sys_user record has id "system" `sys_metadata_history.recorded_by` is `Field.lookup('sys_user', { readonly: true })`, and the metadata repository fills it with `actor ?? 'system'` — a SENTINEL STRING, not a user id — on a write that does not carry `isSystem`. So the PR body's claim that "non-system writes are caller writes" was simply not true of the running platform, and only the real gate could show that: the unit suite's fakes never write a sentinel into a lookup. The narrowing follows from the check's OWN stated scope rather than being an exemption bolted on to go green. `stripReadonlyFields` removes a non-system caller's value from a readonly field before the write, and the create ingress does the same (`stripReadonlyForInsert`, objectstack-ai#3043). So a value still sitting in a readonly field at this point was written by the PLATFORM by construction — "the reference the caller named" was never going to include it. This does not weaken the fix. The two fields objectstack-ai#4441 names — `sys_position_permission_set.permission_set_id` and `showcase_task.project` — are ordinary author-facing lookups with no `readonly`, and both stay enforced; a new case asserts exactly that, so a future widening of this skip cannot quietly swallow them. The sentinel-in-a-lookup is a genuine modelling wart — a lookup column holding a non-id — and is the same class objectstack-ai#4441 is about, written by the platform rather than a caller. It is filed separately; rejecting the platform's own write is not the way to report it, and changing what `recorded_by` stores is not this change's call. objectql: 23 tests across the two new suites green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD * test(dogfood): the field-zoo matrix round-trips REAL references, not synthetic ids (objectstack-ai#4441) `field-zoo-roundtrip` seeded its three relational fields with ids that exist in no row: { field: 'f_lookup', write: 'acc_synthetic_0001' } { field: 'f_master_detail', write: 'proj_synthetic_0001' } { field: 'f_tree', write: 'cat_synthetic_0001' } under a comment reading "FK enforcement is off in this harness". That comment described a HOLE — precisely the one objectstack-ai#4441 closes — so the fixture depended on the defect the platform now prevents, and the create started failing with `reference_not_found` for all three fields at once. This is the fixture being wrong, not the check being strict, so the data is what changes. The suite now creates a real row in each target object and substitutes its id. What the file actually proves is unchanged — an id string must round-trip as the same id string, the objectstack-ai#2004 type-fidelity guard — and it now proves it over references that resolve, which is strictly stronger. The matrix keeps a `REFERENCE_PLACEHOLDER` symbol rather than a magic string, so a future edit cannot typo its way back into writing a literal id, and the placeholder can never reach the wire: substitution is keyed on identity. `REFERENCE_TARGETS` is ORDERED with a body factory because the targets reference each other — `showcase_project` declares a REQUIRED lookup to `showcase_account`, so the account must exist first and the project must be given its real id. Seeding them in the wrong order now fails loudly instead of writing a project that points at nothing, which is a small proof of objectstack-ai#4441 in its own right. `status: 'planned'` is the state machine's declared initial state; `active` is refused with `invalid_initial_state`. Verified: `field-zoo-roundtrip.dogfood.test.ts` 46/46 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD * fix(dogfood): the matrix placeholder must satisfy BOTH consumers, not just one (objectstack-ai#4441) Fixes the shard-1 regression 7bcd40d introduced. `field-zoo.matrix.ts` has TWO consumers, and I only checked one: - `field-zoo-roundtrip.dogfood.test.ts` — drives the vectors over real HTTP and (as of 7bcd40d) substitutes a seeded reference id; - `field-zoo-value-shape.test.ts` — the ADR-0104 contract⇔oracle interlock, which parses every `write` vector against `valueSchemaFor(type, 'stored')` WITHOUT booting a stack, and therefore never substitutes. The `Symbol` placeholder satisfied the first and broke the second with `expected string, received symbol`. Because the two live in different FILES and dogfood shards by file, that surfaced as "shard 2 fixed, shard 1 regressed" — one root cause wearing two masks, not two problems. The placeholder is now a STRING, which is what a reference's stored form actually is, so the contract test parses it like any other id vector. And substitution is keyed on the field appearing in `REFERENCE_TARGETS` — the authoritative list — rather than on comparing against the placeholder value, so the placeholder is documentation rather than a control signal and cannot leak to the wire even if someone edits it. Verified together this time, in one tree: - both matrix consumers in one run: 91/91 - dogfood shard 1/2: 38 files passed - dogfood shard 2/2: 37 passed, 1 skipped Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD --------- Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 30, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…gfood's files outside src to the commits and ADR that decided them (objectstack-ai#20898) Part of objectstack-ai#20594 Clause-②: no ## What changed This is stage 15 of the `domain:cli` lane's dead-citation sweep: **`packages/qa/dogfood`'s dead citations in files outside `src/**`**, the largest package left after stage 14 (claim `5914651671`). The package has no `src/` at all, so the surface is its `test/**`, `README.md`, `tsconfig.json` and `vitest.config.ts`. Every comment site on that surface whose tracker number answers 404 now cites the object that decided what the line describes, in ruling C+D's form C (comment `5749154545` on objectstack-ai#19123): the ADR clause when one records the ruling, otherwise the commit in this repository's history. Stages 1 to 14 of this card are the precedents; the latest is PR objectstack-ai#20883. - **94 sites on 93 lines in 27 files**, covering **24 numbers**, now cite **24 distinct commits and one ADR clause** (ADR-0029 D9.2a). - The 27 files are `vitest.config.ts` and 26 under `test/**`: 21 test files, 4 helper modules and 1 fixture. - **Comments only.** 93 lines out and 93 in, and every file keeps its line count. A token guard (below) shows zero non-comment tokens changed. - **String literals, `describe` / `it` titles and messages are untouched.** They belong to objectstack-ai#20752 (form D, fold `5911936313`); 28 such sites stay on this file list, listed in the acceptance notes. - **No tracker number is added.** The live numbers that share a changed line stay as they were: objectstack-ai#4757, objectstack-ai#6915, objectstack-ai#7987, objectstack-ai#8074, objectstack-ai#8284, objectstack-ai#8408, objectstack-ai#9719, objectstack-ai#11373 and objectstack-ai#17978 all answer 200. - **Sites left without a deciding commit: none.** Every one of the 24 numbers has one. - Where an earlier stage anchored a number and the line here describes the same decision, the same anchor is reused: 16 of the 24 numbers. The other 8 anchors are new (marked below). - A trailing box rule (`── … ─────`) is shortened by the characters its heading gained, never below one dash. Three of the five section headers keep their width; `admin-route-nonadmin-refusal:339` grows by 2 characters and `two-doors-permission:81`, which already ended in one dash, by 11. ## Census **Instrument.** The card's gate does not read these files: its declared surface is `packages/**/src/**`, and `surfaceFor` answers null for all 27 touched paths (the control `packages/cli/src/commands/init.ts` answers `package-docblocks`). So the census is taken by hand, with the gate's own grammar, as stage 14 took it: - **Files:** every tracked file of `packages/qa/dogfood` outside `src/**`, with `CHANGELOG.md` (claim) and `package.json` (not on the claim's list) left out. That is 178 files. - **Extraction:** `extractCitations` from `scripts/check-issue-citations.mjs`, with its comment-prose projection for code and JSONC files and the whole file for `README.md`. The whole-file extraction minus the comment projection gives the string sites. - **Numbers kept:** only those naming this repository (`namesThisRepository`). - **Probe:** each distinct number, `GET /repos/objectstack-ai/objectstack/issues/N`; 404 means dead. | | tree | probe window (UTC) | numbers | answer 200 | answer 404 | dead comment sites | comment sites | dead string sites | |---|---|---|---|---|---|---|---|---| | before | base `4edb61449b` | 2026-09-30 15:46:43 to 15:49:29 | 386 | 359 | 27 | **94** (24 numbers, 27 files) | 1,145 | 28 (13 files) | | after | head `57ffb83b00` | 2026-09-30 15:59:37 to 16:02:15 | 375 | 359 | 16 | **0** | 1,051 | 28 (13 files) | - The before count matches stage 14's census at `660a9b247`: 94 comment sites and 28 string sites in 13 files. - No number present in both probes changed its answer. The 11 numbers that left the census were only on the rewritten lines. - Comment sites fell by exactly 94, the rewritten sites. String sites did not move. - `README.md` cites objectstack-ai#2018 and objectstack-ai#1994, and `package.json`'s description cites objectstack-ai#2018, objectstack-ai#1994 and objectstack-ai#2004. All answer 200. ## Per-number anchors Each anchor was checked by blame on the site, and in the anchor's own message or diff. "Reused" names an earlier stage that gave the number the same anchor. | number | sites | anchor | what it decided | | |---|---|---|---|---| | `objectstack-ai#6293` | 9 | `c39a911ae` | the build stand-in: `build-shaped-artifact.ts` runs the real lowering, and no published surface grows (its subject names the card) | reused (cli `src`) | | `objectstack-ai#6483` | 15 | `ee58392e1` | the ADR-0005 rollback of `permission` (and eight more types) to `allowOrgOverride: false`; `allowRuntimeCreate` stays open | reused (spec, plugin-security, runtime) | | `objectstack-ai#8460` | 4 | ADR-0029 D9.2a | the ruling itself, 2026-08-13, option A "tenant wins"; implemented in `01a7337fc`, which amends the ADR | reused (spec stage 6) | | `objectstack-ai#8676` | 9 | `d6e80b28b` | flags `sys_account.password` and `previous_password_hashes` `internal` | reused (plugin-auth) | | `objectstack-ai#8711` | 3 | `2ce1eb41b` | half (2): the ruled narrowing of the matrix's completeness claim to routes (its message records the maintainer ruling) | **new** | | `objectstack-ai#8711` | 1 | `60ade586e` | half (1): the two `active` rows with no `covers`, and why (`matrix.ts:393`) | **new** | | `objectstack-ai#8811` | 1 | `d6e793507` | adds the `grant-validity-window` matrix row (its subject names the card) | **new** | | `objectstack-ai#8839` | 7 | `c25b2d52a` | comment moderation stops being dead behind the delete floor; ruling of 2026-08-15, reading 1 | reused (plugin-security) | | `objectstack-ai#8919` | 1 | `b5378550e` | gates `/meta` publish and rollback on `manage_metadata`, with the enumeration pin | reused (rest, runtime, cloud-connection) | | `objectstack-ai#9797` | 7 | `1258dcaee` | the PR itself: the opt-in whole-operation dispatch that restores the unscoped multi-delete refusal | **new** | | `objectstack-ai#9934` | 2 | `79c46da90` | the producer-side `userMessage` marking, including the QuickJS side-channel | reused (types, rest, client, runtime, plugin-hono-server, spec) | | `objectstack-ai#10243` | 1 | `266436a7f` | the toggle ruling: `POST /automation/:name/toggle` joins the `manage_metadata` write set (`automation-toggle-tenant-scope:4`) | reused (runtime, service-automation) | | `objectstack-ai#10243` | 1 | `02b41232d` | the measured cross-organization toggle leak ADR-0126 §7.2 retires (`packaged-activation-ledger-reach:291`) | reused (runtime, service-automation) | | `objectstack-ai#10943` | 1 | `46d34ab7c` | `fallbackImport` becomes a caller-supplied parameter | reused (types, cli, verify) | | `objectstack-ai#10996` | 1 | `02b41232d` | the PR itself, the first landing of this file as a measurement | **new** | | `objectstack-ai#11477` | 7 | `6dd3e6968` | `/admin/remove-user` authorizes before the break-glass guard (its message records "Ruled option A on" the card) | reused (plugin-auth, verify) | | `objectstack-ai#11530` | 1 | `033a34c7c` | the PR itself: retires the `set_user_role` console action | **new** | | `objectstack-ai#11686` | 1 | `7131f12bf` | the PR itself: `hasPlatformAdminStanding` as the one authority | **new** | | `objectstack-ai#12176` | 3 | `7986d973f` | stage 3 of the ruled retirement: un-mounts the compound arities, `PUT /meta/:type/:section/:name` among them. The ruling's `D3` ordinal is kept beside it | reused (rest, client, runtime, spec) | | `objectstack-ai#12194` | 1 | `311433f6b` | declares the metadata item-name grammar | reused (rest, client, runtime, spec) | | `objectstack-ai#13214` | 4 | `cc837dbfe` | the ruled ownership gate at `GET /ui/view/:object/:type`; its diff writes these census lines and its message the 2026-08-30 ruling | reused (rest) | | `objectstack-ai#16589` | 4 | `555a89cbd` | `driver-memory` refuses a tenant-scoped call; its diff names the card at every seam | reused (trigger-schedule) | | `objectstack-ai#16659` | 6 | `ecdfc9411` | a time-triggered flow declares its acting organization; its diff adds these pins and the fixture. The `F2` ordinal is kept, as the trigger-schedule stage kept it | reused (trigger-schedule, service-automation, spec, lint) | | `objectstack-ai#16687` | 1 | `779710213` | the PR itself; its squash carries the contract-review patch round the line describes | **new** | | `objectstack-ai#17853` | 1 | `08f5f0e5a` | a vitest filter that selects nothing says so (the same sentence stage 14 rewrote in cli's config) | reused (qa, cli) | | `objectstack-ai#19306` | 2 | `f9e16d856` | a packaged permission set's DELETE stops reporting a deletion; its diff adds both pins | **new** | **ADR and ruling records.** A grep of `docs/adr` and `scripts/adr-anchors` for the 24 numbers finds three: objectstack-ai#8460, objectstack-ai#6483 and objectstack-ai#10243. ADR-0029 D9.2a is the ruling for objectstack-ai#8460 (it carries the number in its heading), so it is cited. The objectstack-ai#6483 hits (ADR-0086, ADR-0094, ADR-0126 and two adr-anchors entries) and the objectstack-ai#10243 hits (ADR-0126, ADR-0131) mention those landings as history; none records the ruling itself, so those two stay on the commit every earlier stage anchored them to. The control number `7329` finds 1 file in the same tree. **Anchor checks:** - **Each sha is unambiguous:** `git rev-parse --disambiguate` gives count 1 for each of the 24. - **Each is a plain commit** with one parent. - **Each is on the base:** `merge-base --is-ancestor` against `4edb61449b` exits 0 for all 24. - **The history is complete:** the checkout is not shallow. Control leg: `255588bd36`, the parent of the oldest anchor `ee58392e1` (2026-08-09), exits 0. Negative control: the base as an ancestor of `ee58392e1` exits 1. ## Verification All at head `57ffb83b00`. Heavy runs went through `scripts/pm/os-verify-lock.sh` with `OS_VERIFY_LOCK_SLOT=issue-20594-dogfood`. - **Build (dependency closure):** `pnpm exec turbo run build --filter='@objectstack/dogfood^...' --concurrency=2` → 63 successful, 63 total (32 cached), lock verdict exit 0. - **Every touched test file ran, by name.** Dogfood has two vitest projects (`shared-showcase` and `isolated`) and no tier split, so one run named all 21 touched test files plus the unit tests of the two touched helpers no touched test imports (`authz-probe-blind-spot.test.ts`, the census module's only importer, and `enterprise-organizations.test.ts`): `pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 FILES` → **Test Files 23 passed (23), Tests 317 passed (317)**, lock verdict command-exit 0. `vitest.config.ts` is loaded by that run. - **Typecheck:** `pnpm --filter @objectstack/dogfood typecheck` (`tsc --noEmit`) → lock verdict command-exit 0. `--listFilesOnly` shows 26 of the 27 touched files in the program; the 27th is `vitest.config.ts`. - **Token guard** (TypeScript leaf tokens via `getChildren`, JSDoc nodes skipped), base `4edb61449b` against head, 27 files, 52,502 base tokens: **0 files differ**. Controls, in memory only: a comment inserted into each file, 0 of 27 differ; a statement appended, 27 of 27; one character flipped inside each file's first `StringLiteral`, 27 of 27. - **Control bytes:** a scan of the 27 files finds 0 (positive control, a scratch file holding U+0001: 1). `pnpm check:nul-bytes` exits 0. - **Derived gates and lint:** listed under "Gates". ## Gates All at head `57ffb83b00`, exit codes captured before any pipe. - **Derivation:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, no paths, derives **53 commands** for this change set (27 paths against merge base `4edb61449b`). Re-derived after two fetches of `origin/main` (to `00a92e18da`, then `33b6e8bece`): the same 53, and none of the upstream commits touches a derivation input or a file here. - **All 53 exit 0.** One needed a second run for a reason outside the diff: `pnpm check:dual-build-cjs-loads` first exited 3 (`PREREQUISITE NOT MET`, no `dist/` for 8 packages outside dogfood's build closure); after `pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2` (71 of 71, all cached) it exits 0. - **Reconciliation:** `dispatch-gates --ran` over the recorded `COMMAND :: exit CODE` list → "53 derived, 53 run, 0 NOT-MEASURED, 0 UNRUN", exit 0. - **`pnpm lint`** (repo-wide `eslint . --no-inline-config`, the family the derivation does not name) → exit 0, 2026-09-30T16:13:49Z to 16:18:00Z. - `node scripts/check-issue-citations.mjs` (diff mode, in the 53) reads 0 files, because none of these paths is on its declared surface; the hand census above is the measurement for this surface. - A local `git merge-tree --write-tree` of this head against `origin/main` `33b6e8bece` is clean. **The 53 derived commands:** - `node scripts/check-ci-filter-parity.mjs` - `node scripts/check-closing-keyword-parity.mjs` - `node scripts/check-closing-keyword-parity.mjs --self-test` - `node scripts/check-comment-mask-adoption.mjs` - `node scripts/check-comment-mask-adoption.mjs --self-test` - `node scripts/check-comment-mask-corpus.mjs` - `node scripts/check-issue-citations.mjs` - `node scripts/check-keyed-text-bounds.mjs` - `node scripts/check-keyed-text-bounds.mjs --self-test` - `node scripts/check-platform-object-tenancy-census.mjs` - `node scripts/check-platform-object-tenancy-census.mjs --self-test` - `node scripts/check-plugin-teardown-shape.mjs` - `node scripts/check-plugin-teardown-shape.mjs --self-test` - `node scripts/check-registry-log-declared.mjs` - `node scripts/check-registry-log-declared.mjs --self-test` - `node scripts/check-rest-log-spy-declared.mjs` - `node scripts/check-rest-log-spy-declared.mjs --self-test` - `node scripts/check-system-context-census.mjs` - `node scripts/check-system-context-census.mjs --self-test` - `node scripts/check-undeclared-dep-imports.mjs` - `node scripts/check-undeclared-dep-imports.mjs --self-test` - `node scripts/docs-audit/check-affected-docs.mjs` - `node scripts/docs-audit/check-drift-comment.mjs` - `pnpm --filter @objectstack/spec run check:empty-state` - `pnpm --filter @objectstack/spec run check:liveness` - `pnpm --filter @objectstack/spec run check:strictness-ledger` - `pnpm --filter @objectstack/spec run check:variant-docs` - `pnpm check:cross-package-test-inputs` - `pnpm check:dispatcher-error-vocabulary` - `pnpm check:doc-authoring` - `pnpm check:driver-memory-census` - `pnpm check:dts-closure` - `pnpm check:dual-build-cjs-loads` - `pnpm check:engine-double-contract` - `pnpm check:gitlink-declared` - `pnpm check:issue-citations` - `pnpm check:lean-entry-closure` - `pnpm check:logger-receiver-detach` - `pnpm check:nul-bytes` - `pnpm check:objectql-double-limit` - `pnpm check:org-identifier` - `pnpm check:page-declaration-shape` - `pnpm check:published-files` - `pnpm check:query-options-erasure` - `pnpm check:refd-timer-probe` - `pnpm check:slot-lookup` - `pnpm check:sourcemap-no-sources-content` - `pnpm check:test-source-alias` - `pnpm check:tier-file-adoption` - `pnpm check:type-check-coverage` - `pnpm check:type-check-debt` - `pnpm check:watch-hint-literal` - `pnpm check:where-matcher` ## Acceptance notes - **Changeset:** none. `@objectstack/dogfood` is `private: true`, so nothing here publishes; the PR carries `skip-changeset`. - **28 dead string sites stay on this file list** (16 numbers in 13 files: objectstack-ai#6293, objectstack-ai#6483, objectstack-ai#8676, objectstack-ai#8710, objectstack-ai#8711, objectstack-ai#8811, objectstack-ai#9934, objectstack-ai#10243, objectstack-ai#11477, objectstack-ai#11530, objectstack-ai#11686, objectstack-ai#11757, objectstack-ai#12176, objectstack-ai#13260, objectstack-ai#16589, objectstack-ai#16659). They are `describe` / `it` titles and string values such as the matrix rows' `note:` text. They belong to objectstack-ai#20752 (form D). - **What remains on this card after this stage** (stage 14's census at `660a9b247`, report `5914100460`): 17 comment sites in the other lane packages outside `src/**` (`plugin-hono-server` 4; `plugin-dev`, `client` and `qa/vitest-filter-preflight` 2 each; `cloud-connection`, `mcp`, `qa/downstream-contract`, `rest`, `runtime`, `types` and `verify` 1 each), the five `cli` sites with no deciding commit, and the 55 off-list sites stage 14 listed. None of them is touched here. - **No open PR touches `packages/qa/dogfood`** (all 10 open PRs' file lists read at 2026-09-30T15:56:00Z). The in-flight branch for objectstack-ai#20862 adds one new file there, `automation-authoring-doors-durable.dogfood.test.ts`, which is not among these 27. --- _Generated by [Claude Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…d decision in words instead of a tracker number (stage 16) (objectstack-ai#21831) Part of objectstack-ai#20749 Clause-②: no Stage 16 of this card: the next area of class (e), the test strings shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513. This stage takes the second name-ordered file group directly under `packages/spec/src/data/`: the 20 test files from `default-value-shape.test.ts` to `filter-comparand-shape.test.ts`. They carried 94 messages and 100 tracker ids, citing 59 records plus one decision-batch number. Every one of those ids now either states what its record decided, in words (form D), or is dropped where the title already says it. Text only: no assertion, identifier, test count or code comment changes. ## Census at the base (`a3ffc4512d`) Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`), `census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs` (md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5 `dda605c54745b4a60cc14c9a686e4eff`). They are byte-identical to the copies stages 10 to 15 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 **1231 messages / 1304 ids in 262 files**, the seat's reading at `a3ffc4512d` (stage 15's head). | directory | files | messages / ids | titles | other | |:--|--:|--:|--:|--:| | `ui/` | 81 | 393 / 416 | 375 / 398 | 18 / 18 | | `data/` (this PR: the second 20 files) | 75 | 374 / 399 | 353 / 376 | 21 / 23 | | `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 | | `ai/` | 1 | 2 / 2 | 0 | 2 / 2 | | `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 | | **total** | **262** | **1231 / 1304** | **1154 / 1224** | **77 / 80** | The group reads **94 messages / 100 ids in 20 files**, the seat's figures, file for file: | file (under `data/`) | messages / ids | titles | other | |:--|--:|--:|--:| | `default-value-shape.test.ts` | 6 / 6 | 6 / 6 | 0 | | `default-value-tokens.test.ts` | 1 / 1 | 1 / 1 | 0 | | `document.test.ts` | 2 / 2 | 2 / 2 | 0 | | `driver-nosql.test.ts` | 1 / 1 | 1 / 1 | 0 | | `driver.test.ts` | 5 / 5 | 5 / 5 | 0 | | `esignature-deadline-keys-retirement.test.ts` | 4 / 4 | 4 / 4 | 0 | | `external-lookup-retirement.test.ts` | 3 / 3 | 1 / 1 | 2 / 2 | | `feed.test.ts` | 1 / 1 | 1 / 1 | 0 | | `field-autonumber-default-format.test.ts` | 1 / 1 | 1 / 1 | 0 | | `field-autonumber-default-unique.test.ts` | 2 / 2 | 2 / 2 | 0 | | `field-autonumber-readonly.test.ts` | 2 / 3 | 2 / 3 | 0 | | `field-currency-scale-refused.test.ts` | 3 / 3 | 3 / 3 | 0 | | `field-default-value.test.ts` | 4 / 4 | 1 / 1 | 3 / 3 | | `field-rows-option-description.test.ts` | 4 / 4 | 4 / 4 | 0 | | `field-value.test.ts` | 5 / 5 | 5 / 5 | 0 | | `field.test.ts` | 32 / 36 | 32 / 36 | 0 | | `filter-array-declaration.test.ts` | 2 / 2 | 1 / 1 | 1 / 1 | | `filter-ascii-fold.test.ts` | 1 / 1 | 1 / 1 | 0 | | `filter-boolean-comparand-declared-type.test.ts` | 11 / 11 | 11 / 11 | 0 | | `filter-comparand-shape.test.ts` | 4 / 5 | 4 / 5 | 0 | | **20 files** | **94 / 100** | **88 / 94** | **6 / 6** | - **Controls.** Lit, a title: `data/filter-comparand-type.test.ts`, outside the group, reads 9 / 10 at the head as at the base. Lit, "other" strings: the twin expect messages in `system/message-queue-retirement.test.ts` (`:87`, `:127`) still read at the head. Dark: `data/filter-array-declaration.test.ts` reads 0 / 0 at the head while 9 of its comment lines still carry a number. Planted in a scratch copy of the head `data/filter-ascii-fold.test.ts`: an id put into a title reads 1 / 1, and an id put into a comment reads 0. - **A wider pattern** (any `#` plus digits) reads the same totals in 19 of the 20 files. In `field.test.ts` it reads four more, at both base and head: the hex colours `#00FF00` / `#00AA00` in option fixtures (`:859`, `:1461`, `:1476`, `:1484`), which are not tracker ids. - **At the head:** 1137 messages / 1204 ids in 242 files. The 20 files read 0 / 0 on the gate pattern, and no other file moved. ## How the area was chosen `data/` has no subdirectory to split by, so its stages take name-ordered file groups near the ~100-id bound. Stage 15's re-cut named this group at exactly 100, and this census reads 100, so the rule needed no re-cut. **Named for the next stages** (re-cut from the head census, 1137 / 1204; `data/` 280 / 299 left, the same cut stage 15 named): - `data/` in three more stages, name-ordered: 1. `filter-comparand-type.test.ts` to `filter-view-operator-parity.test.ts`: 20 files, 95 messages / 99 ids; 2. `filter.test.ts` to `object.test.ts`: 17 files, 103 / 114. `object.test.ts` alone carries 42, so no cut lands nearer the bound; 3. `query-transport.test.ts` to `validation.test.ts` (11 files, 34 / 34) with `data/driver/` (7 files, 48 / 52): 86 ids. - `ui/` 416, about four stages. `api/` 201, two. `system/` 165, two. The files directly in `src/`, 120, one. - The three docblock needles (`ai/build-progress.test.ts:236`, `:237`, `contracts/approval-service.test.ts:274`), one stage with their docblocks. ## What each id became 31 literals (36 ids) now state a decision in words. 2 literals (2 ids) get their subject back in words where the number stood in for it. 61 literals (62 ids) drop a number the title already explains. Every cited record was read with its comments through REST: 56 answer 200, the four `objectui` records among them. objectstack-ai#8656, objectstack-ai#12868 and objectstack-ai#16867 answer 404, and their decisions were read from what landed: `94f7ef8` (a `Field.time` builder that produces the literal shape an author could already write), `c459da6bc` (the form-view option shape is the object-field option shape minus `default`) and `0ee32ed` (the flat `notNull` spellings prescribe `storage.notNull`, not `required`), each with its CHANGELOG entry. | record(s) | literal (under `data/`) | now reads | |:--|:--|:--| | objectstack-ai#4560 | `default-value-tokens.test.ts:15` | "defaultValue runtime tokens — one vocabulary the engine and the DDL both read". The token set moved to one source that the engine's default stamping and the SQL driver's column defaults both read, so a runtime token is never written as a literal column DEFAULT. | | objectstack-ai#14477 (2) | `document.test.ts:238`, `:255` | "… a retiredKey() tombstone, since nothing ever read it (ADR-0049)". The pair had no reader and no roadmap, so it retired with the other deadline families. | | objectstack-ai#4484 | `driver.test.ts:112` | "the streaming prescription names the removed findStream and the paged-find fix". `findStream` had no caller and was retired; the `streaming` bit described it. | | objectstack-ai#14478 | `driver.test.ts:399` | "DriverOptions.timeout → DriverOptions.timeoutMs: the key carries its unit". | | objectstack-ai#18063 | `driver.test.ts:428` | "driverSupportsTransactions — a driver that cannot honour transactions says so". Decision batch 148, item 3, letter B: a transport may declare that it has no transactions, and the engine refuses transactional use loudly. | | objectstack-ai#8075 (2) | `external-lookup-retirement.test.ts:89`, `:130` | Two declared expect messages: "… must have zero holders after the external-lookup retirement" and "… being undone — the family had no consumer and accepted inline credentials". The measurement found no door reaching either schema, and both carried inline secrets, so both families were removed whole. | | objectstack-ai#11807 | `feed.test.ts:22` | "SYS_ACTIVITY_BUILTIN_TYPES — the built-in activity types, published by the spec". Ruling A. | | objectstack-ai#5628, objectstack-ai#5503 | `field-autonumber-readonly.test.ts:82` | "RUNTIME_OWNED_FIELD_TYPES is the protocol vocabulary — the types whose value the runtime owns". The engine strips a client's autonumber value, and the builder declares the field read-only. | | objectstack-ai#13388 | `field-default-value.test.ts:98` | A declared case label: "(`postal_code`, as the showcase seed once wrote it)". The seed's key was corrected to `postalCode`. | | objectstack-ai#16077 (2) | `field-default-value.test.ts:116`, `:123` | Two declared case labels: "… (the rename is named over the missing-member error that sorts ahead)" and "… (the rename is named over the type error)". The literal gate prefers the rename over a positional member error. | | objectui#6140 | `field-rows-option-description.test.ts:52` | "FieldSchema accepts `rows` on the multiline editor types — declared because the editor widget reads it". Option A: validation and the running widget agree. | | objectui#6153 | `field-rows-option-description.test.ts:162` | "SelectOptionSchema accepts `description` — declared because the lookup widget reads it", the inherited ruling for a consumed key. | | objectstack-ai#12868 | `field-rows-option-description.test.ts:192` | "… by construction (that face is the option shape minus `default`)". | | objectstack-ai#8163 | `field.test.ts:197` | "FieldSchema — field-level `currency` key guidance, naming `currencyConfig.defaultCurrency`". The refusal gained a prescription; nothing it accepts moved. | | objectstack-ai#9784, objectstack-ai#4447 | `field.test.ts:997` | "keeps non-reference types ACCEPTING the key (installed-base artifact shape) — materialization moved to the reference-types block below". `deleteBehavior` materializes only on reference types. objectstack-ai#4447 is dropped: "installed-base artifact shape" already says why the key must stay legal. | | objectstack-ai#8793 | `field.test.ts:1174` | "relatedListFilter is judged by the schema door that refuses a bare date preset as a comparand", the ruled half that refuses the preset vocabulary as a bare temporal comparand at publish time. | | objectstack-ai#8656 | `field.test.ts:1434` | "should create time field — the same literal shape an author could already write". | | objectstack-ai#11437, objectui#4015 | `field.test.ts:2105` | "FieldSchema — authored `radio` + `multiple: true` is REFUSED (ruled: at the entrance, never rendered as checkboxes)". Option C; option A was not taken. | | objectstack-ai#17469 | `field.test.ts:2191` | "FieldSchema — authored `multiple: true` on a NON-MULTI-CAPABLE type is REFUSED (ruled: one definition of multi-value, the radio rule generalised)". Option 1′ of decision batch 128, item 5. That batch number went with the id. | | objectstack-ai#9019, objectui#4676 | `field.test.ts:2320` | "FieldSchema — `placeholder` is a DECLARED key (ruled: the renderers already apply it)". Option C: the producer moves. | | objectstack-ai#8993 | `field.test.ts:2393` | "FieldSchema — `maskingRule` is a DECLARED key (ruled: partial masking, enforced at runtime)". Option A: partial masking as a runtime-first capability, declared only with its enforcement. | | objectstack-ai#11339 | `field.test.ts:2476` | "Polymorphic pointer pair — referenceVia names the sibling column that holds the object (ADR-0052 §5)", the declared pointer-pair carrier. | | objectstack-ai#5158 | `filter-array-declaration.test.ts:142` | A declared expect message: "… — the array form is input-only sugar, lowered before any driver sees it". Ruling C: one lowering seam. | | objectstack-ai#5286 | `filter-array-declaration.test.ts:233` | "FilterArray type-level declaration (type-checked since the test layer got its own tsc program — see above)". | | objectstack-ai#4706 | `filter-ascii-fold.test.ts:32` | "leaves non-ASCII letters ALONE — the case-insensitive operators fold ASCII only", the record's first question, answered A. | | objectstack-ai#21333 | `filter-boolean-comparand-declared-type.test.ts:138` | "booleanComparandDoorVerdict — an accepted spelling narrows to its boolean, any other is refused". | | objectstack-ai#5423 (2) | `filter-comparand-shape.test.ts:779`, `:1068` | "the whole refusal fits under the 500-char client bound the REST layer truncates at". The REST layer truncates a long 4xx message rather than replacing it. | **Subject restored (2 ids):** objectstack-ai#16077 at `default-value-shape.test.ts:172` ("the rename preference is a NO-OP for a class that cannot emit `unrecognized_keys`"; without its number the title would have read as if `checkLiteralDefaultValue` itself were the no-op) and objectstack-ai#21382 at `filter-boolean-comparand-declared-type.test.ts:195` ("the accepted set is unchanged — the non-string widening adds refusals only"). **Dropped only (62 ids):** objectstack-ai#2004, objectstack-ai#3855 (2), objectstack-ai#4001 (3), objectstack-ai#4455, objectstack-ai#4634 (2), objectstack-ai#5628, objectstack-ai#5869, objectstack-ai#6555, objectstack-ai#7127 (3), objectstack-ai#7768, objectstack-ai#8075, objectstack-ai#8321 (2), objectstack-ai#8371, objectstack-ai#9228, objectstack-ai#9689 (4), objectstack-ai#9784, objectstack-ai#11566, objectstack-ai#11949, objectstack-ai#13053, objectstack-ai#13671, objectstack-ai#13802, objectstack-ai#13894, objectstack-ai#14168, objectstack-ai#14477 (4), objectstack-ai#15680, objectstack-ai#16077 (3), objectstack-ai#16867, objectstack-ai#18972, objectstack-ai#19629 (3), objectstack-ai#19992 (5), objectstack-ai#20740, objectstack-ai#21333 (5), objectstack-ai#21382 (4), objectstack-ai#21448. - Each of these titles already states the decision it pins: for example "deleteBehavior: set_null on master_detail is a parse-time rejection" for objectstack-ai#9689, "the list-comparand shape door runs inside parseFilterAST" for objectstack-ai#5869 and objectstack-ai#9228, or "refuses `precision` at EVERY value, in range or not: the key was removed, not re-bounded" for objectstack-ai#19992. - **`driver-nosql.test.ts:375`** now reads "NoSQLQueryOptions.timeout carries its unit", the same text stage 14 gave `ai/conversation.test.ts`, so the two read alike. - **`filter-boolean-comparand-declared-type.test.ts`:** its six describes and five `it`s carried the two records as a file-wide tag. The verdict's describe now states the decision (above). The others name the contract part they pin (the accepted spellings, the judged fields, the refusal message, the fixture, the case table), and their child titles state what each part does. The file's header comment still names both records; comments are not touched here. - **The `(the objectstack-ai#9689 class)` tails** on three idempotency titles go: each title already says "parse(parse(x)) is byte-identical" or "re-parses cleanly", which is what that class names. ## Readers - **Test-name filters:** none. A tracked-tree search for `--testNamePattern` and `vitest … -t` finds only the option name in `packages/qa/vitest-filter-preflight/src/project-cli-override-preflight.ts:212`, which filters nothing. - **Snapshots:** none. No `.snap` file or `__snapshots__` directory is tracked under `packages/spec`. - **Projects:** none of the 20 files is listed in `packages/spec/vitest.repo-tests.json`; all 20 run in the `local` project. - **By substring:** every old literal, plus a window around each id (287 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 33 needle hits fall on 18 lines: - **same-text strings in this card's later stages:** `data/driver/memory.test.ts:548` and `data/driver/turso.test.ts:172` (the last `data/` group), `ui/dashboard.test.ts:717` ("carries its unit"); `system/message-queue-retirement.test.ts:87` and `:127`, the twin expect messages of the two `external-lookup-retirement.test.ts` strings; `system/translation.test.ts:767` ("unknown-key strictness"); `ui/action.test.ts:1245` ("alias is REMOVED"); - **sibling titles in other lanes:** `objectql` `engine-aggregate-flag-comparand-refusal.test.ts:187`, `service-analytics` `where-boolean-flag-refusal.test.ts:197` and `where-empty-operator.test.ts:130` (all `[objectstack-ai#21448] a LIST …`); - **comments and CHANGELOG:** `field.zod.ts:2264`, `driver-sql` `sql-driver.ts:20176`, `cli` `generate.ts:30`, `filter-comparand-shape.ts:735` and `:802`, `filter-comparand-type.ts:116` and `:228`, and one `packages/spec/CHANGELOG.md` entry. None reads a test title. - **Same-text titles named in stage 14's and 15's ACCEPTs:** `driver-nosql.test.ts:375` (`(objectstack-ai#15680)`) is the one in this group, rewritten as above. ## 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 `external-lookup-retirement.test.ts:89` and `:130`, `field-default-value.test.ts:98`, `:116` and `:123`, and `filter-array-declaration.test.ts:142`. - **Result:** 20 of 20 files SAME on all three legs, with the per-file counts predicted in writing before the run. - **Totals:** 94 changed literals, 88 titles and 6 declared. The diff's `+` and `-` lines are exactly the 94 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 given a new id VIOLATION; an undeclared `expect` message changed VIOLATION; a title re-split into a `+` chain DIFF. **Test counts:** the 20 files were run at the base, in a separate base worktree, and at the head, with `--project local --project repo`. Both sides read 766 tests (765 passed, 1 todo), with the same count and status sequence per file in 20 of 20. 503 full test names change, and each equals the base name with the planned replacements applied (0 mismatches). No full name repeats on either side. ## `main` since the base Re-fetched just before this PR opened, `origin/main` was six commits past the base (`088428fb42`). None of them touches any of the 20 files, so `main` was not merged. Two of them touch `packages/spec` source (`ui/report.form.ts` and `data/object.zod.ts`); this diff changes only test-title text, so it cannot combine with them into a joint failure, and the merge queue re-runs the suite on the merged generation. ## Changeset: `skip-changeset` Measured, not assumed: - `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the 20 touched files are in it, and no `*.test.ts` at all. The controls `src/data/field.zod.ts`, `src/data/filter.zod.ts` and `dist/data/index.js` are in it. - In the built `dist/`, five new phrases and four old literals 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 `95baf08ba9`) - `pnpm turbo run build` over all packages: 71 / 71. - `@objectstack/spec`: - `vitest run --project local`: 615 files, 18360 passed, 1 todo. - `typecheck` exit 0, including `check:test-typecheck` (52 files / 246 errors / 135 pinned signatures held). Its program holds all 20 touched files, counted with `tsc --listFilesOnly -p tsconfig.test.json`. - `check:generated`: all 15 generated artifacts up to date. - **Gates:** `dispatch-gates --commands` derived 79 families, the same set as stages 13 to 15, and all 79 exit 0. `--ran` reconciles: 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded. - The derivation printed STALE TREE: `main` moved past the base during verification and `scripts/engine-double-contract.pinned.json` changed there. `check:engine-double-contract` exits 0 on this tree, this diff touches no engine double, and the queue re-runs the family on the merged generation. - 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 20 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 20 configured, 0 ignored. No file sets `parserOptions.project` or `projectService`, so no untouched file's verdict can move. - `check-governed-merges --test`: NOT governed, 188 changed lines. ## Acceptance notes - **No needle in this group.** Every id was a title or a declared string; no expected value of an assertion over a source docblock was found. The three known needles are untouched. - **Same-id test titles in this card's later stages** go with those stages: 46 lines in `packages/spec/src` (14 in later `data/` groups, 9 in `system/`, 23 in `ui/`). Among them, `ui/view.test.ts:1893` ("leaves the malformed-declaration refusals (objectstack-ai#8321/objectstack-ai#12174) reading exactly as before") and `ui/action-param-default-value.test.ts:154` (`objectstack-ai#16077 carries the rename …`) are near-twins of titles rewritten here. - **Same-id test titles in other packages** are their lanes' test-string shares. A search of `describe` / `it` / `test` lines outside `packages/spec` finds 111 lines in 15 packages citing ids this PR handled: `objectql` 47 (22 files), `driver-sql` 18 (13), `rest` 15 (12), `driver-mongodb` 6 (3), `lint` 5 (4), `plugin-security` 5 (2), `cli` 3 (2), `core` 2, `driver-memory` 2, `metadata-protocol` 2, `service-analytics` 2, and one each in `driver-turso`, `plugin-audit`, `qa/dogfood` and `service-automation`. - **Two spec test files outside `src/`** carry same-id titles: `packages/spec/scripts/check-generated-ledger.test.ts:100` and `packages/spec/scripts/file-description.test.ts:66`. They are outside class (e) as ruled ("the test strings shipped under `src/`"). - **Code comments still carry ids** in these files and their sources, for example the header of `filter-boolean-comparand-declared-type.test.ts`, the block above `filter-array-declaration.test.ts:233`, and the `field.zod.ts` docblocks naming the shadowing mechanism. Comments are not this card's share, and none is touched here. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ Co-authored-by: Claude <noreply@anthropic.com>
This branch was successfully deployed
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.
Two field-type runtime gaps found driving
app-showcase's field-zoo end-to-end. The showcase's coverage test is green because it asserts every field type is declared — but field-zoo has no seed data, so no field type was ever exercised with a real value. Both bugs are pure "passes build / silent at runtime."1. Array/object field types broke every write (
driver-sql)multiselect/checkboxes/tags/repeater/vectorwere missing from the SQL driver's JSON-field classification. Their array values reached the better-sqlite3 binder un-serialized:This hit common field types on insert and update — even
task.labelson an ordinary object (labels: ['red','blue']→ 500). Root cause: the DDL column-type switch andisJsonFieldhad drifted into two separate type lists; the array types were in neither.Fix: one shared
JSON_COLUMN_TYPESsource used by both the DDL switch andisJsonField, now covering the array/object types (so the columns are created as JSON and round-trip as arrays/objects). Plus aformatInputsafety net that serializes any stray array/object value, so an unclassified field degrades to a stored string instead of a hard 500.2.
Field.timerejected every valid value (objectql)The validator reused the
date/datetimebranch (Date.parse), which isNaNfor any bare time string — so atimefield could never accept14:30or09:05:30.Fix:
timenow validates a time-of-day (HH:MM/HH:MM:SS, optional fractional seconds +Z/offset) and still accepts a full ISO datetime;date/datetimeunchanged. (Date.parse('14:60')returns a bogus number in Node, so the lenient fallback requires a real date component — malformed times like14:60/25:00are correctly rejected.)Verification
multiselect/tags/checkboxes/repeater/vectorcome back as arrays/objects,timeas14:30.task.labelsarray persists.sql-driver-array-fields.test.ts(round-trip + update + empty-array) →@objectstack/driver-sql155 green; time-validator cases (accept HH:MM/HH:MM:SS/ISO, reject malformed, no date/datetime regression) →@objectstack/objectql652 green.Follow-ups (separate, showcase-side)
showcase_warn_over_budgethook references a nonexistentrecord.spentfield (fires a "no such key" warning). (Next PR.)🤖 Generated with Claude Code