Repository navigation
Add formula function library documentation - #87
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Add documentation for formula function library
Add formula function library documentation
Jan 23, 2026
xuyushun441-sys
requested review from
Copilot
and removed request for
xuyushun441-sys
January 23, 2026 11:05
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive documentation for the formula function library, addressing the gap where formula fields existed (expression: z.string()) but lacked reference documentation about available functions.
Changes:
- Created
/packages/spec/docs/formula-functions.mdwith detailed documentation for 20 formula functions across 4 categories (Text, Math, Date, and Logical) - Added usage examples, parameter specifications, return types, and real-world use cases for each function
- Included type compatibility matrix and best practices guidance
xuyushun441-sys
marked this pull request as ready for review
January 23, 2026 11:05
This was referenced Aug 18, 2026
This was referenced Sep 8, 2026
This was referenced Sep 17, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…ubmission moves to ctx.submitted (objectstack-ai#16344) (objectstack-ai#17195) * wip(objectql): hide caller-forged readonly values from beforeUpdate (objectstack-ai#16344) Checkpoint before the first heavy verify run, so the working tree is not the only copy of the work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU * fix(objectql)!: beforeUpdate receives the persist image; the caller submission moves to ctx.submitted (objectstack-ai#16344) The update-side leak: a value sent for a `readonly: true` field was correctly not persisted, and was still handed to `beforeUpdate`. A hook deriving columns from the incoming record derived them from a value the row would never contain, and those derived writes persisted — a row whose own audit trail cites values it does not hold, with no error, no warning and a 200. Ruled by the maintainer (decision batch objectstack-ai#87, 2026-09-08), option B, in two halves that ship together: 1. Caller-forged static `readonly` values are HIDDEN from the hooks' view of `ctx.input.data` and handed back at the post-hook confluence, so every engine-owned consumer below — `onFieldsDropped`, the readonly WARN, `strictReadonlyWrites`, the declared-field door, validation — reads the payload it read before and says the identical thing about it. 2. The caller's submission as sent travels on `HookContext.submitted` (`packages/spec`), frozen, diagnostics only. plugin-auth's ADR-0092 identity write guard is migrated onto it in this change, so its 403 and its security warn keep naming the non-whitelisted field. The ENFORCEMENT pass does not move: it stays after the hooks, where it is the only point that can tell a hook's stamp from a caller's forgery (objectstack-ai#5591 / objectstack-ai#14088). `beforeInsert` is untouched (ruling C, objectstack-ai#14147) and `readonlyWhen` stays hook-writable (objectstack-ai#9107). The objectstack-ai#5591 docblock in engine-readonly-strip-caller-values.test.ts is superseded in writing rather than deleted, and the case that pinned the old diagnostic channel is re-pinned on the new one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU * chore: regenerate the two artifacts the new HookContext member moves (objectstack-ai#16344) Both produced by the repo's own generators, neither hand-edited: pnpm gen:system-context-census content/docs/permissions/system-context.mdx — six declared counts, 108 -> 109 elevation read sites. The +1 is this change's own `opCtx.context?.isSystem` gate on the pre-hook hide pass. The census is green on symbols without a new row: the read lives in `ObjectQL.update`, already cited. pnpm --filter @objectstack/spec gen:schema && ... gen:docs content/docs/references/data/hook.mdx — one generated row for `HookContextSchema.submitted`. `pnpm --filter @objectstack/spec check:generated` now reports all 15 generated artifacts up to date. ⛔ content/docs/releases/ untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU * test(objectql): re-pin the one suite the new hook input moves, and honour the caller's bound in the new double (objectstack-ai#16344) Two reds reproduced locally at e8359df and fixed at their cause. 1. `engine-readonly-strip-signal.test.ts` — `[objectstack-ai#5591] a hook OVERWRITING a key the caller supplied` reached its subject through a guard, `if (ctx.input.data.work_duration !== undefined)`, which silently made the case depend on a SECOND fact: that the caller's forged read-only value is visible to the hook. It no longer is. The objectstack-ai#5591 verdict itself is unchanged and is re-pinned with an unconditional hook write; the fact the guard was quietly carrying gets its own case, asserting what this card ships — a hook that GATES on seeing the caller's forgery does not fire, and the column keeps its stored value. ⛔ Neither case is skipped, weakened or deleted. 2. `check:objectql-double-limit` — the `find` double in the new `engine-readonly-hook-input.test.ts` was limit-blind. It now applies the caller's bound after the filter, by presence, exactly as the gate prescribes. Local: exit 1 naming line 72 BLIND, then exit 0, "baseline key set verified against fd5cff2: no files added". Local readings after both: objectql 4869/4869 in 289 files, plugin-auth 2245/2245 in 106, spec 13198/13198 in 470; all three typechecks OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU * test(runtime): re-route objectstack-ai#14760's write-THROUGH control off the caller, and pin what objectstack-ai#16344 does to its old path (objectstack-ai#16344) `Test Core (3/6)` was a THIRD failure, in a package I had not run: @objectstack/runtime, src/sandbox/hook-input-writeback-readonly-provenance .integration.test.ts. My local scope was the three packages I edited, so a downstream consumer of the update path went unmeasured. Recorded as the miss it was, not as a surprise. objectstack-ai#14760's write-THROUGH control asks one thing: can leg 2 of the sandbox write-back carry a mutation made THROUGH an object-valued readonly key, which leg 1 (the `set` trap on `ctx.input`) structurally cannot see? It reached that question by having the CALLER put the object on the payload. Since objectstack-ai#16344 a caller cannot: the value is hidden from `beforeUpdate`, so `ctx.input.locked_meta` is `undefined` and the body faults on the dereference — measuring the hide, not the write-back. The object now arrives the way the platform is still allowed to put it there: a code hook's own write ahead of the body (objectstack-ai#5591/objectstack-ai#14088 semantics, which this card did not move). The control is strictly SHARPER for it — the value under test is unambiguously hook-authored, so a pass can no longer be explained by a caller value leaking through — and leg 1 still cannot see the body's in-place mutation, so leg 2 is still the only thing that can carry it. Asserting `who: 'hook'` against a pre-hook `who: 'platform'` is what keeps it non-vacuous: a write-back gone silent leaves the pre-hook value standing and fails here. ⛔ The old path is not deleted. It is pinned as its own case with the verdict objectstack-ai#16344 gives it, and that verdict is the sharpest edge in this change: a body that reaches through a caller-supplied readonly key now throws, and a body's default `onError` is `abort`, so the caller's WHOLE write is refused where it used to succeed. What it used to do was persist a value derived from the caller's forgery, so refusing is the right direction — but the author sees a raw `TypeError`, which names nothing actionable. Now recorded in the changeset's "Who is affected" alongside the remedy: a body reads `ctx.previous`, since `ctx.submitted` is deliberately not marshalled onto the sandbox face. One mechanism worth the next reader's time, measured the hard way: the pre-hook must be registered AFTER `bindHooksToEngine` and under a DIFFERENT packageId. The binder is hot-reload friendly and opens by calling `unregisterHooksByPackage(opts.packageId)`, so a pre-hook registered before it under the same id is silently dropped — which presents exactly as the body faulting on an absent key. Local: runtime 9/9 on that file; shard 3/6 13/13 tasks; shards 2+4 83/83; shard 5 72/72; shard 6 69/69. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU * fix(objectql): set-to-undefined of an engine-hidden readonly key is a no-op, not a hook write (objectstack-ai#16344) Contract-review finding F1. A `beforeUpdate` hook that assigns a hidden key from the payload it was shown (`data.x = data.x`) reads `undefined` and re-creates the key holding it. Three mechanisms then agreed the wrong way: the recorder's `set` trap counted it as a hook write, the hand-back skipped the key because `k in target`, and the static strip kept it on that record — so a driver was handed `{ x: undefined }`. On the memory driver that ERASES the stored read-only value; on a knex-backed one `formatInput` does not drop `undefined` and `builder.update(payload)` hands knex an undefined binding — a bare compile-time Error outside the ADR-0112 envelope. Neither is "the record the engine intends to persist". At the confluence the key is now deleted, dropped from the sealed record, and the ordinary hand-back puts the caller's value back for the strip to judge. The write reads exactly as it would with no hook at all: stripped, reported on `onFieldsDropped`, warned, refused under `strictReadonlyWrites`. Dropping the key from `hookWrittenKeys` is load-bearing, not tidiness: handing the caller's value back over a key the record still calls hook-owned would credit the forgery with hook provenance. The narrowing reaches only keys this card's pass hid, and only the one value no driver can store, so objectstack-ai#14088's deliberate blindness to VALUE is unchanged for every key a hook can see. The pin moves in both directions: the stored value now STANDS (`completed_at === STAMPED`, the seed), and the FORGED negative stays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU * docs(objectql): carry the whitelist+readonly boundary and the persist-image rule where authors read them (objectstack-ai#16344) Contract-review findings F2, F3, and F4/F5 riding along. F2 — the changeset now carries the ADR-0092 boundary the round report claimed was already in it. An UPDATE-whitelisted field that is ALSO declared `readonly` now answers 403 where it answered 200-having-written-nothing, and the refusal reads `(—)` because a whitelisted key is excluded from the guard's refused list by design. No in-repo object is on that boundary; `patch` for plugin-auth stands. The self-assignment row is also corrected to the verdict that now holds: a no-op, stored value standing, not a persisted `undefined`. F3 — `content/docs/protocol/objectql/security.mdx` is the hand-authored authority for the update-side strip and said nothing about this card. It gains a fifth rule ("hooks are shown the persist image, not the submission") and a migration callout naming `ctx.submitted`, `ctx.previous`, the self-assignment no-op and the sandbox `body` exclusion. Rule 2's trailing paragraph is corrected while there: "cannot rescue one the caller supplied" has been false since objectstack-ai#5591/objectstack-ai#14088 — a hook that ASSIGNS a caller-sent key owns the value and the strip keeps that write. `content/docs/automation/hooks.mdx` takes the one-line cross-reference from "Mutate the incoming record". F4 — the `submitted` TSDoc said "FROZEN by the producer" without qualifying depth. It is a shallow spread shallow-frozen, so a nested object reached through a key here is the caller's own mutable reference. Not a laundering route, but not a deep guarantee either, and now it says so. F5 — the hidden set is the update strip's own subject set: author-declared `readonly: true` AND runtime-owned types (`autonumber`, objectstack-ai#5503), not "statically readonly" alone. Stated in the changeset and in the `input` contract note. No generated artifact moves: the edited prose is TSDoc, and only the `.describe()` string reaches `authorable-surface/data.json` and `references/data/hook.mdx`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU --------- Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 27, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
… key its five item-titled siblings already carry (objectstack-ai#18561) Fixes objectstack-ai#16894 Clause-②: yes (widening) `KanbanConfigSchema` now declares `titleField` as optional `z.string()`, executing the director seat's decision batch objectstack-ai#87 (recorded at objectstack-ai/objectui#8367 comment `5582071618`, confirmed by the maintainer verbatim 「批 objectstack-ai#87 同意」). The direction was settled there; this PR is the implementation only. ## What moved | file | change | |---|---| | `packages/spec/src/ui/view.zod.ts` | `titleField: z.string().optional()` on `KanbanConfigSchema`, plus the docblock that records why it is optional | | `packages/spec/src/ui/view.test.ts` | the card's four-leg probe (both controls firing on the same call shape) and the optionality leg | | `packages/spec/authorable-surface/ui.json` | regenerated — one added row, `ui/KanbanConfig:titleField` | | `content/docs/references/ui/view.mdx`, `content/docs/references/api/protocol.mdx`, `content/docs/references/data/object.mdx` | regenerated by `gen:docs` | | `.changeset/16894-kanban-config-titlefield.md` | `@objectstack/spec: minor` | Regenerated, never hand-edited: `check:authorable-surface` wrote the first and `pnpm --filter @objectstack/spec gen:docs` the rest. `check:generated` reports all 15 artifacts up to date. ## Premise re-derivation — the card's sibling table does NOT match the tree Re-derived on this worktree at `origin/main` `79a046f8c` (the dispatch's own derivation ran one commit behind, at `582d3e5`). `git grep -n 'titleField' -- packages/spec/src/ui/view.zod.ts` returns six carriers; mapping each to the schema whose member list encloses it: | line (pre-change) | owning schema | arity | |---|---|---| | `:1057` | `GalleryConfigSchema` (declared `:1050`) | optional | | `:1071` | `TimelineConfigSchema` (declared `:1065`) | **required** | | `:1407` | `CalendarConfigSchema` (declared `:1401`) | optional — the ADR-0079 fallback docblock the ruling names | | `:1490` | `GanttConfigSchema` (declared `:1484`) | required | | `:1638` | `ListMapConfigSchema` (declared `:1631`) | optional | | — | `KanbanConfigSchema` (declared `:1346`) | **absent — the defect** | Two corrections to the card body, neither of which disturbs the ruling: - The card's table calls **Timeline optional**. It is **required** on the tree. - The card's table lists **five** schemas. There are **six** carriers: `ListMapConfigSchema` also declares `titleField`, optional, and the card does not mention it. The ruling is unaffected: it prescribes the arity directly (`optional z.string()`) and names `CalendarConfigSchema`'s docblock as the reference, which is optional at `:1407`. So the shape landed here is the ruled one, and the card's optional/required split was simply not re-measured when it was written. The defect itself re-derives exactly as filed: `KanbanConfigSchema` is a `strictObject` and refused `titleField` by name. ## Evidence | leg | command | verdict | |---|---|---| | build | `pnpm --filter @objectstack/spec build` | `VERDICT command-exit 0` | | ② typecheck | `pnpm --filter @objectstack/spec typecheck` | `TYPECHECK_EXIT=0` — `check:test-typecheck: OK` | | ② tests | `pnpm --filter @objectstack/spec test` | `TEST_EXIT=0` — `Test Files 483 passed (483)` / `Tests 13775 passed (13775)` | | targeted | `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui/view.test.ts` | `Test Files 1 passed (1)` / `Tests 376 passed (376)` | | artifacts | `pnpm --filter @objectstack/spec check:generated` | 15 of 15 up to date | ① is empty by construction: `packages/spec` has no workspace dependencies, so `--filter '@objectstack/spec^...' build` has an empty closure. The package itself was built before any gate that reads `dist/`. ③ Gate families were derived on this worktree, not inherited: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` over the real 7-path change set yields **108 families**. All 108 were run with each exit code landed to disk before being read, and reconciled with `--ran`: ``` dispatch-gates --ran: 108 derived family(ies) accounted for — 105 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3) ``` **103 green.** The five non-zero results, none of them a finding against this diff: - `pnpm check:dual-build-cjs-loads`, `pnpm check:lean-entry-closure`, `pnpm check:type-check-debt` — **exit 3, PREREQUISITE NOT MET**, all three refusing because the whole monorepo is not built on this worktree. NOT MEASURED, declared to CI, which checks out and builds fresh. - `node scripts/check-plugin-teardown-shape.mjs --self-test` — **exit 1, but a prerequisite refusal in its own words**: "cannot read the positive control at `621a487607881c66b2899b7e3477115229a156b4` … Deepen the clone". This container's checkout is shallow. NOT MEASURED. The gate itself (without `--self-test`) ran **green**. - `pnpm check:cross-package-test-inputs` — **exit 1, pre-existing on the base tree**, proven by control rather than asserted. With all seven changed paths restored to `79a046f8c` (and the changeset removed), the gate fails identically; the finding it prints names `packages/cli/test/init-created-files-summary.e2e.test.ts` descending `packages/spec/dist/`, and this diff touches neither that test, nor `turbo.json`, nor any declaration table. Restore was proven by `git diff HEAD` empty plus a `git hash-object` match against every HEAD blob. **Lint, as a proven narrowing rather than a repo-wide sweep.** `pnpm exec eslint --no-inline-config --format json` over the two changed TypeScript files: **exit 0, 2 files, 0 errors, 0 warnings**, the file count read from the JSON output's own length. The population it narrows from is **6798** files — computed from ESLint's own resolved config via `ESLint#isPathIgnored` over `git ls-files`, not estimated. The narrowing excludes nothing, and that is a property of this repository's config rather than a hope: `eslint.config.mjs` states it in its own comment — this repo "runs one `eslint.config.mjs`, which never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file, test or not." With no rule reading types across a file boundary, a diff confined to these two files cannot move the verdict on any of the other 6796. The repo-wide `pnpm lint` run remains CI's. Every reading above was taken at `6d01b4b`, this branch's final commit, on a tree whose `git status --porcelain` is empty. `origin/main` had not moved from the branch point (`79a046f8c`) when the PR was opened, so no merge was owed. ## Acceptance notes - **`packages/lint` does not field-check the key this PR declares.** `POSITIONS.kanban` in `packages/lint/src/validate-list-view-field-refs.ts` lists `groupByField` (error), `summarizeField` (warning) and `columns` (warning) — and no `titleField`. Every sibling face lists it: `calendar.titleField` warns, `gantt.titleField` and `timeline.titleField` error. So from this release a stale or misspelled `kanban.titleField` names nothing and is reported by nothing, while the identical typo one block away is caught. Read-only on this card by dispatch, so it is reported and not edited; nothing in this PR turns it red, and no gate reconciles that table against the spec's member lists. Worth its own card. - **The node-level half (objectstack-ai/objectui#7742) has no home here, and the card's conditional resolves to "no".** `ObjectKanbanPropsSchema` in `packages/spec/src/ui/component.zod.ts` **already declares** `titleField: z.string().optional()`, described as "Legacy fallback for `cardTitle` (the board reads `cardTitle || titleField`). Prefer `cardTitle`". That is a different semantic on a different schema in a different file — a deprecated alias for `cardTitle`, not a view-config field binding — so one declaration cannot serve both faces and this card is not widened. That file is also held by objectstack-ai#18305 / PR objectstack-ai#18403; it was read, never touched. - Noted, not filed: `KanbanConfigSchema` carries no object-level `.describe()`, where `GalleryConfigSchema`, `TimelineConfigSchema`, `CalendarConfigSchema` and `ListMapConfigSchema` all do, so its nested-shape heading in the generated reference renders with no description sentence. Cosmetic, generated-docs only, no accept set involved — left for whichever card next edits this schema. - The issue body was checked for sanitizer truncation: none found. It is 6732 characters through the REST read and terminates on its own `Refs:` line. ## File surface Two files beyond the dispatch's declared surface, both **generated by the mandated `gen:docs` run** and neither hand-edited: `content/docs/references/api/protocol.mdx` and `content/docs/references/data/object.mdx`. The kanban config shape is inlined on those two reference pages as well as on `ui/view.mdx`, so each picks up `titleField?: string` in its `kanban` row. Omitting them would leave `check:docs` red. Every hunk in all three files is this one key and nothing else. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…n names no sys_position row (objectstack-ai#16712) (objectstack-ai#20292) Fixes objectstack-ai#16712 Clause-②: yes Fixes objectstack-ai#20297 Executes the maintainer-confirmed ruling on objectstack-ai#16712 (batch objectstack-ai#87, option A): a `sys_user_position` write whose `position` names **no `sys_position` row at all** is refused, instead of answering 201 over an assignment that resolves to nothing. Dispatch: PM session `session_01TEah6PeJGjxJfbHaySJjLQ`, claim comment 5858098043, branch `claude/issue-16712-position-catalog-refusal`. ## What changes A new engine middleware, `packages/plugins/plugin-security/src/position-catalog-refusal.ts`, registered by `SecurityPlugin.start()` right AFTER its security middleware (so it runs INSIDE it, after the delegated-admin gate and the CRUD check, the same placement the ADR-0094 permission-set data door uses). - **Predicate:** exactly "no catalog row carries this name". A deactivated position (`active: false`) is still a catalog row, so an assignment naming it is accepted and, as before, grants nothing (ADR-0049 shape E). The catalog is the WRITER's: it is read under `{ ...context, isSystem: true }` (the engine lookup probe's spelling), so the reach is the writer's organization plus organization-less positions. A name only another organization carries is refused exactly like a name no organization carries (objectstack-ai#20297; see the patch-round section below). - **Envelope:** `400 VALIDATION_FAILED`, one `fields[]` entry per offending value: `field: 'position'`, `code: 'reference_not_found'`, `constraint: { target: 'sys_position', targetField: 'name' }`. This is the envelope the row's sibling lookup columns (`user_id`, `organization_id`, ...) already answer with. **No new error code**: `VALIDATION_FAILED` is a registered ADR-0112 code (built with `validationFailure` from `@objectstack/types`, which both HTTP doors map to 400), and `reference_not_found` is a member of the closed ADR-0114 field catalog. Nothing in `packages/spec` changes. - **Message:** names the value, says the column takes the catalog NAME, and names the fix. When the value is the record id of a position the writer's own organization can see (the hotclm id-spelling trap), it names that position and says to write its name. - **Writes judged:** every non-system insert (one row or a batch; the batch is refused whole), a non-system update by id that CHANGES `position`, and a predicate update (`multi: true`) that sets it. - A `position` that is not a string is judged too, by its string form: a number, bigint or boolean by `String(value)`, an object or array by its JSON text. The engine's `text` validation stores every one of these with 201, except an operator object (next bullet). - Left to the engine, and only these: `null` and a blank string (`required`), a value whose `String()` form is longer than the column (`max_length`), and an operator object. An operator object is a plain object with a declared filter operator as an own key, such as `{ $in: [...] }`; the engine refuses it as `invalid_type` (objectstack-ai#5922). - An update that edits another column, or echoes the stored `position` back unchanged, is not judged, so an existing row whose name has left the catalog stays editable. The comparison reads string forms, so `123` echoed over a stored `'123'` counts as unchanged. - **Writes NOT judged, deliberately:** every `isSystem` write, the same stand-down the engine's own lookup check (`assertReferencesResolve`) takes. Measured reason for the seed door: on a fresh single-posture boot `AppPlugin.start()` writes `stack.data` inline (`packages/runtime/src/app-plugin.ts`, the `seedLoader.load` branch), while the declared position catalog is seeded on `kernel:ready` (`bootstrapDeclaredPositions`, `security-plugin.ts`), so a refusal at the seed door would fail every authored assignment seed on the first boot and pass it on the second. Also not judged: invitation acceptance (`invitation-placement.ts` `apply`, system context) and the platform bootstraps. This was a code reading, not a live boot of a seed-carrying stack. - **Fails open** (with a `warn`) when the catalog cannot be read, the engine lookup probe's stance. Two consequences outside `plugin-security/src`, both required by derived gates: - `content/docs/permissions/system-context.mdx`: new row 23b for the refusal's `isSystem` stand-down, and the census counts regenerated with `node scripts/check-system-context-census.mjs --fix` (105 to 106 sites). `check:system-context-census` requires an anchor for every elevation read site. - `.changeset/16712-position-catalog-refusal.md`: `@objectstack/plugin-security` `minor`, a `**BREAKING**` banner, and the ADR-0087 disposition `not-required (no-migration-prescription)`. `check:adr-0087-registration` reads it as `[BREAKING+bang] not-required (no-migration-prescription)`. The level follows the WHICH LEVEL rule (decision batch objectstack-ai#35): `Clause-②: yes` takes at least `minor`, and breaking-ness is carried by the banner, not the level. ## The two pre-enumerations (the ruling's gate), in-repo half, tree `4d7e740d3b` The out-of-repo halves were measured EMPTY on the card (hotcrm `2f7b2326`, hotclm `14c899fc`, triage comment 5857658468). **Precondition 2 (a catalog-less or membership-derived name written into `sys_user_position.position`): EMPTY.** Every non-test writer: | writer | context | values written | catalog-less? | |:--|:--|:--|:--| | `packages/plugins/plugin-security/src/invitation-placement.ts` `apply` | `isSystem`, on invitation acceptance | `intent.positions`, the names the issuer put on the invitation (gate-checked at issuance). No literal. | no | | `examples/app-showcase/src/security/seed-approval-demo.ts` `assignPositions` | `isSystem`, on `kernel:bootstrapped` (after the catalog bootstrap) | `manager`, `finance`, `legal`, `exec`, `auditor`. All five are declared in `examples/app-showcase/src/security/positions.ts` `allPositions`. | no | | `packages/verify/src/rls.ts` `provisionRlsPositionPersona` | `isSystem`, post-boot | `declaredPositionNames(config)`, the stack's own declared names | no | - Search: the write verbs (`insert`/`create`/`upsert`/`update`/`*Many`) naming `sys_user_position` returned 11 hits, all in `*.test.ts`. The non-test write calls in every file naming the object add the three rows above. - Positive control: the same verb pattern finds the `sys_user_permission_set` writer in non-test code. - `org_member`, `everyone` and the other membership-derived or anchor names are resolver PROJECTIONS (`resolve-authz-context.ts`, the `mapMembershipRole` loop and the implicit `everyone` push), never stored rows. A `position:` value spelling any of the eight anchor or built-in names has 0 non-test hits, and 12 in tests (control). **Precondition 1 (in-repo half), re-confirmed on today's tree: EMPTY.** No shipped `stack.data` seed carries `sys_user_position`. The only `object: 'sys_user_position'` in non-test code is the gate dry-run literal in `invitation-placement.ts`. Seed files for other objects are found by the same search (control). ## Evidence (first round, head `cbdd0e70`; the patch-round section below is the evidence for the current head) **Live HTTP, real composition** (fresh showcase, `pnpm dev -- --fresh -p` on a random port, account created through `POST /api/v1/auth/admin/create-user`; the server was stopped by its recorded process group): | request | answer | |:--|:--| | `POST /data/sys_user_position` with `position` = the `auditor` row's id | **400** `VALIDATION_FAILED`, `fields[0]` = `position` / `reference_not_found`, message says to write `auditor` | | same, `position: 'totally_not_a_position_zzz'` | **400** `VALIDATION_FAILED` / `reference_not_found`, generic remedy | | same, `position: 'auditor'` | **201** | | that holder reads `showcase_inquiry` | **200, 3 rows** (the name resolves) | | `PATCH` that row, `position` = the auditor id | **400** | | `PATCH` that row, `reason` edited, `position` echoed unchanged | **200** | | `POST /data/sys_user_position/createMany`, `[finance, fin4nce_typo]` | **400**, names only `fin4nce_typo`; nothing stored | **Unit and integration** (`position-catalog-refusal.test.ts`: 14 cases at `cbdd0e70`; each patch-round section below gives the count for its head): a real `ObjectQL` over SQLite, with the real `SecurityPlugin` registered the way a kernel composition registers it. Every refusal pin asserts `code` and `status` through `resolveThrownHttpError`, never a bare throw. - **The ruling's control leg:** an id-spelled assignment is refused, and the same account reads 0 rows. A direct `sys_user_permission_set` grant of the same set to the SAME account then reads 3 rows. - **Both accepted halves:** a catalog name resolves (the holder reads 3 rows). A deactivated position's name is accepted and stored, and grants nothing. - **Authorization first:** a plain member, and an `organization_admin`-shaped caller (wildcard `modifyAllRecords` plus an explicit per-table deny), get `403 PERMISSION_DENIED` for a bogus name and for a real name alike. - **Walled posture, two organizations:** - a name no organization carries is refused; - a name only another organization carries: pinned ACCEPTED at `cbdd0e70`, under the literal predicate. Since objectstack-ai#20297 it is REFUSED, and that pin is reversed (patch round below); - the id hint never names another organization's position; - **the org boundary for a VALID foreign organization** (the leg the card carried as NOT MEASURED): an `org_a` writer stamping `organization_id: 'org_b'` gets `403` at the tenant wall, identically for all three names. Measured at the engine layer with the `isolated` posture, not over HTTP. **Ablations** (each through `scripts/ablation-replace.mjs` in WRAP mode, plus a shell `trap` restoring the absolute path from `HEAD`; every restore proven by blob hash == `HEAD` and an empty `git diff HEAD`). The subject resolves through relative imports to `src/`, so no rebuild or `dist/` preflight applies: | ablation | mutation | result | |:--|:--|:--| | A1 | the refusal never fires | **7 red** (every refusal pin: "expected the write to be refused, but it succeeded"), 7 green | | A2 | the catalog judged at the TOP of the security middleware, before authorization | **2 red**: authz-first (`expected 'VALIDATION_FAILED' to be 'PERMISSION_DENIED'`) and the foreign-org wall. The first attempt was a no-op: the tool refused it because the replacement re-contained the anchor. It is reported, not counted. | | A3 | an unchanged `position` judged too | **1 red** (the fossil-edit pin) | | A5 | the id hint drops its organization filter (at `cbdd0e70`; the patch round removes that filter, and the hint read is scoped instead, see C1 below) | **1 red** (the hint named `qa_b_only`) | **Local verification at head `cbdd0e70`:** - `pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2`: **141 files / 2910 tests passed**. - `pnpm --filter @objectstack/plugin-security typecheck`: exit 0. The test layer compiles, with 0 debt. - `node scripts/pm/dispatch-gates.mjs --commands` over the actual diff: **94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN**, all exit 0 (`--ran` reconciliation green). This includes `check:dual-build-cjs-loads`, `check:i18n` and `check:type-check-debt` after a full workspace build. - Lint, narrowed and proven: - (1) The population read from eslint's own config (`--print-config`): the three changed `.ts` files are in it, and the `.mdx` and `.md` files are not. - (2) `eslint --no-inline-config --format json` counted 3 files, 0 errors and 0 warnings. - (3) No `parserOptions.project` is set anywhere (type-aware linting is not enabled), so this diff cannot move a verdict on an untouched file. - Declared to CI, not run locally: the `packages/qa/dogfood` suites. The three that write `sys_user_position` over HTTP (`delegation-of-duty`, `showcase-permission-zoo`, `delegated-admin-invite`) all write catalog names. ## The predicate's tenancy scope: decided on objectstack-ai#20297 (B) The first round applied the ruled predicate literally and read the catalog unscoped. On a walled posture that accepted a name only another organization carries, and it raised the scope as an open question. - Triage routed that question as precedent-decided (objectstack-ai#20297, comment 5859496956): the read is the writer's organization plus organization-less rows, in the engine lookup probe's `{ ...context, isSystem: true }` (objectstack-ai#19808, objectstack-ai#19819, objectstack-ai#19860). - The patch round below executes it. The objectstack-ai#16712 ruling itself is unchanged: a deactivated position is still a catalog row. ## Acceptance notes - **`security/explain` naming an unresolvable entry** (hotclm evidence, 5611199715): out of scope by dispatch. Noted, not filed. - **Lint as the authoring-time second carrier**: `packages/lint/src/validate-security-posture.ts` lints `sys_user_position` seed rows but does not check that `position` names a declared position. It is the natural carrier for the seed door this refusal stands down on. Out of scope by dispatch; noted, not filed. - **Invitation issuance is not judged by this refusal**: `assertIssuable` dry-runs only the delegated-admin gate, and acceptance writes under a system context. An invitation naming a catalog-less position is still accepted at issuance, and the placement lands silently at acceptance. This is a door this change does not cover. Carrier: whoever next touches `invitation-placement.ts`; no carrier is named today. - The refusal message is English only. Localizing it needs a message-catalog key in `packages/spec`, another lane's. - The write preview (`ObjectQL.validate`) runs no middleware, so it does not report this refusal, the same as it does not report the engine's lookup check today. - The claim's surface listed `plugin-security/src` plus the changeset. The dispatch's file-surface clause names `packages/plugins/plugin-security/src/` as the landing, so the middleware registration in `security-plugin.ts` (one `registerMiddleware` call, with its import and comment) sits inside it, away from the `writeCheckPolicies` docblock. The `system-context.mdx` row is the one addition outside it, owed to `check:system-context-census`. <sub>Seat's append (domain:services objectstack-ai#6021, `session_01TEah6PeJGjxJfbHaySJjLQ`): the objectstack-ai#20297 dev's patch-round section, verbatim from its report 5860074760. The seat also replaced the `Held-for:` line in the head with the closing line for that card.</sub> ## Patch round (objectstack-ai#20297) Executes the triage routing on objectstack-ai#20297 (comment 5859496956), option **B**: the predicate the objectstack-ai#16712 ruling fixed now reads the WRITER's catalog, meaning the writer's organization plus organization-less rows, in the engine lookup probe's spelling `{ ...context, isSystem: true }` (`assertReferencesResolve`, objectstack-ai#19808). The ruling is not re-opened: a deactivated position is still a catalog row, so shape E stays accepted. Dispatch: PM session `session_01TEah6PeJGjxJfbHaySJjLQ`, claim 5859557977. **What changed** (head `cb1d5be9`) - `namesWithoutCatalogRow(deps, names, context)` and `idSpellingHints(deps, values, context)` now take the writer's context as a required parameter. They read `sys_position` under `catalogReadContext(context)`, which is `{ ...context, isSystem: true }`, and `assertPositionNamesCatalogRow` passes `opCtx.context`. `security-plugin.ts` is unchanged in this round. - The id hint's organization post-filter is removed. The hint now reads exactly what the check reads (see the C1 ablation below). - The by-id pre-image read stays a bare `{ isSystem: true }`. It is measured unreachable for a foreign row id, and that is pinned (below). - Module docblock: a new "Whose catalog" section states B and cites objectstack-ai#20297 with the engine precedent (objectstack-ai#19808, objectstack-ai#19819, objectstack-ai#19860). "Where it runs" no longer says authorization-first is what protects an unscoped read. Authorization-first itself stays (P1 below). - Changeset: the "read across every organization" line is gone. A "Whose catalog" paragraph says the catalog is the writer's organization's positions plus the organization-less ones, and that a name only another organization carries is refused like any unknown name. `minor`, `**BREAKING**`, `!`, `Clause-②: yes` and the ADR-0087 disposition are kept; `check:adr-0087-registration` reads `[BREAKING+bang] not-required (no-migration-prescription)`. - `system-context.mdx` row 23b now describes the scoped read. - `origin/main` `7b1e4a48` is merged in (merge commit `2f036209`). The only conflict was the census file-count row. `pnpm gen:system-context-census` then re-derived symbols 88 → 89 (`75804ad6`). The PR now reads `mergeable_state: clean`. **Mechanism readings.** These use the walled two-organization fixture: a real `ObjectQL` over SQLite, with the real `SecurityPlugin`. The "before" column was measured at `75804ad6`, whose refusal module is byte-identical to `cbdd0e70`'s; the "after" column is the pins at `cb1d5be9`. | write, from an `org_a` admin | before | after | | --- | --- | --- | | insert `position: 'qa_b_only'` (only `org_b` carries it) | 201 | 400 `VALIDATION_FAILED` / `reference_not_found`, message identical to the exists-nowhere one | | insert `position: 'nope_position'` (no organization carries it) | 400 | 400 | | update by id of an `org_b` row, unknown name | 403 `PERMISSION_DENIED` | 403 | | update by id of an id that exists nowhere, unknown name | 403 `PERMISSION_DENIED`, the same message | 403 | - **The pre-image read is never reached for a foreign id.** The security middleware answers a foreign id and a nonexistent id identically, before the refusal runs, so the pre-image read is left bare. - **Scoped vs bare lookup by name.** Looking up `sys_position` by name under `{ ...orgAdminContext, isSystem: true }` finds `qa_b_only` 0 times and `qa_a_own` once. Under a bare context, or a context with no tenant, each is found once. - **A writer with no organization never reaches the refusal on the isolated posture.** The security middleware answers `403` ("no active organization") first, for an insert and for a predicate update. So the "reads every organization" behaviour is pinned directly on `namesWithoutCatalogRow`. - **`single` posture.** The fixture seeds its catalog the way a `single` deployment seeds its declared catalog, with no organization, so both rows read a null `organization_id`. That is not true of every row on a `single` deployment: a position created through the data door by a session with an active organization is stamped with it. On a deployment holding one organization, the scoped read (`organization_id = tenant OR organization_id IS NULL`) still covers the whole catalog. A `single` deployment holding more than one organization still boots; `TenancyService` reports that state at `error` (objectstack-ai#17010). In that state each writer reads its active organization's positions plus the organization-less ones. The existing single-posture tests carry no tenant, so they never exercise the organization-less term. A new pin writes with `tenantId: 'org_a'`: `qa_auditor` is accepted and an unknown name is refused. **Tests** (`position-catalog-refusal.test.ts`, 14 → 18 cases) - **Reversed, not deleted.** A name only another organization carries is now refused `400 VALIDATION_FAILED`, `reference_not_found` at `position`. Its message is byte-identical to `positionNotInCatalogMessage('qa_b_only')`, and its envelope matches the exists-nowhere case key for key. A predicate update that sets that name is refused the same way. - **New.** The writer's own organization's name is accepted (the control). The scoped and unscoped readings are pinned on the function. A foreign row id is pinned to answer like a nonexistent id. An organization-bound writer on the single posture is pinned. - **Unchanged and green.** The control leg, shape E, the batch / multi / echo legs, the `isSystem` stand-down, 403-first, the foreign-organization wall and the id hint. **Ablations** (at `cb1d5be9`). Each ran through `scripts/ablation-replace.mjs` in WRAP mode plus a shell trap, and every restore was proven by blob == `HEAD` and an empty `git diff HEAD`. The subject resolves through relative `src/` imports, so no rebuild or `dist/` preflight applies. | ablation | mutation | result | | --- | --- | --- | | B1 | the catalog name read reverted to the bare `SYSTEM_CTX` | 2 red: the reversed pin ("expected the write to be refused, but it succeeded") and the organization-bound leg of the function pin | | C1 | the id-hint read reverted to the bare `SYSTEM_CTX` (post-filter already removed) | 1 red: the hint names `qa_b_only` | | P1 | the catalog judged at the top of the security middleware, before authorization | 3 red: 403-first, the foreign-row-id pin (`nope_position` answers 400 instead of 403), and the foreign-organization wall | C1 doubles as the post-filter measurement. With the scoped read and no post-filter, the full suite is green, and the hint pin fails only when the read itself is unscoped. So the post-filter is not kept. **Local verification at `cb1d5be9`** - `pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2`: 141 files / 2914 tests passed. - `pnpm --filter @objectstack/plugin-security typecheck`: exit 0. The refusal test is in the `tsconfig.test.json` program (checked with `--listFiles`). - `node scripts/pm/dispatch-gates.mjs --commands` over the actual diff: - 94 derived, 94 run, all exit 0; - `--ran` reconciliation: "94 run, 0 NOT-MEASURED (a DERIVED zero)"; - this includes `check:query-options-erasure`, back to 236 sites: the pre-image pin's read had first been cast to `any`, and `cb1d5be9` types it. - Also run: the four artifact-roster gates flagged for this diff, and the diff-scoped `check-issue-citations.mjs` (6 citations, 6 resolve). - Lint, narrowed and proven: - `eslint --print-config` puts the three `.ts` files in the population and the `.md` / `.mdx` files outside it; - `eslint --no-inline-config --format json` counted 3 files, 0 errors and 0 warnings; - `eslint.config.mjs` sets no `parserOptions.project`, so this diff cannot change a verdict on an untouched file. **Acceptance notes added this round** - **A context carrying only `organizationId`** (no `tenantId`) is not tenant-scoped by the engine's driver options. It reads every organization here (measured on the fixture) and, by the same driver-options reading, in the engine's own lookup probe (code reading). It cannot reach this refusal on the isolated posture, where it gets 403 first. Noted, not filed. - **Under the `group` posture** the scoped read follows the engine's membership union, so a name from another organization the writer belongs to is accepted. This is a code reading, not measured. Noted, not filed. <sub>Seat's append: the round-4 section, from the objectstack-ai#20297 dev's report 5860889632, with "stored text" corrected to "string form" where the SQLite measurement shows the two differ (`123` is stored as `'123.0'`).</sub> ## Patch round 4 (contract re-review 5860712690) **Measured first.** On head `ebf00fd8`, before any code change, I ran non-system inserts and by-id updates as an admin, on both the single and the walled fixture, with the same result on each: | `position` written | answer | stored as | | --- | --- | --- | | `123` | 201 | `'123.0'` | | `true` | 201 | `'1.0'` | | `{}` | 201 | `'{}'` | | `['x']` | 201 | `'["x"]'` | | `[]` | 201 | `'[]'` | | `NaN` | 201 | `NULL` | | `10n` | 201 | `'10'` | | by-id update to `123` or `true` | 200 | `'123.0'` / `'1.0'` | | `''`, `' '` or `null` | 400 `VALIDATION_FAILED`, `required` | nothing | | a 101-character string | 400 `VALIDATION_FAILED`, `max_length` | nothing | So the reviewer's premise held: the engine refuses only `null`, blank strings and over-long values, and stores everything else. Branch (a) applies. **What changed** (head `6e1ef6aa`) - `judgedName(value)` in `position-catalog-refusal.ts` now judges every value except those the engine answers itself. - A number, bigint or boolean is judged by `String(value)`. - An object or array is judged by its JSON text (in the measurement, SQLite stored `{}` and `['x']` in exactly that form). It is never judged by `String(['x'])`, which reads `'x'` and would accept an array naming a real position that then resolves nothing. - The engine answers `null`, a blank string, and any value whose `String()` form is longer than the column (its `max_length` check reads `String(value)`). Patch round 6 below adds the one refusal this missed: an operator object, `invalid_type` (objectstack-ai#5922). - The refusal is the usual one: `400 VALIDATION_FAILED`, `reference_not_found` at `position`, with the value's string form as `value` and in the message. - The by-id unchanged-value check compares string forms. - The docblock's stand-down sentence now names only the refusals the engine really makes. - The changeset and `system-context.mdx` are unchanged; their "Which writes" and "Such a write is now refused" text is true as written. **Tests** (21 cases, up from 18). Every refusal pin asserts `code` and `status`. - **Insert:** `123`, `true`, `{}` and `['qa_auditor']` are each refused, with `value` equal to `'123'`, `'true'`, `'{}'` and `'["qa_auditor"]'`, and nothing is stored. - **By-id update:** `123` and `true` are refused, and the stored row is untouched. - **Echo:** `123` over a stored `'123'` is not judged. - **Id hint:** now also asserts `VALIDATION_FAILED` / 400, plus `field` and `code` on `fields[0]`, for both refusals. **Ablations** (at `6e1ef6aa`, through `scripts/ablation-replace.mjs` in WRAP mode plus a shell trap; every restore was proven by blob == `HEAD` and an empty `git diff HEAD`): | ablation | mutation | result | | --- | --- | --- | | N1 | only strings are judged | 2 red: the insert and update pins ("expected the write to be refused, but it succeeded") | | N2 | the echo compares raw values | 1 red: the echo pin (`123` is refused as `'123'`) | | N3 | no JSON form, so objects fall back to `String()` | 1 red: the insert pin (`{}` is judged as `'[object Object]'`) | **Verification at `6e1ef6aa`** - plugin-security: 141 files / 2917 tests passed; typecheck exit 0. - `dispatch-gates --commands`: 94 derived, 94 run, all exit 0. The `--ran` reconciliation reports a derived zero. - `check:query-options-erasure` holds at 236, since no new `find` was added. - `check:adr-0087-registration` reads `[BREAKING+bang] not-required (no-migration-prescription)`. - The diff-scoped issue-citation check: 7 citations, all resolve. **Acceptance note.** On SQLite a non-string is stored as the driver's text (`'123.0'`, `'1.0'`), not as `String(value)`. So a non-string whose `String()` form happens to equal a catalog name, for example `true` where a position is named `true`, is accepted and stored in a form that resolves nothing. `sys_position.name` has no pattern that rules such names out. The root cause is the engine's `text` leniency for non-string input, which is outside this card; it is measured at the engine layer only. <sub>Seat's append: the round-6 section, verbatim from the objectstack-ai#20297 dev's report.</sub> ## Patch round 6 (contract review 3, 5861153477) **Measured first** on head `0e5f4c79`, on the single fixture, with the refusal in place and then with it ablated (`ablation-replace` WRAP; the restore was proven by blob == `HEAD`): | `position` | with the refusal | engine alone | | --- | --- | --- | | `{ $in: ['x'] }`, `{ $in: [], a: 1 }`, `{ $regex: 'x' }`, `{ $or: [] }` | 400 `invalid_type` | 400 `invalid_type` | | by-id update to `{ $in: ['x'] }` | 400 `invalid_type`, row untouched | 400 `invalid_type` | | `{ a: 1 }`, `{ $foo: 1 }` | **201, stored** | 201 | | `'{nope_tok}'` | **201, stored** | 201 | | `'{current_user_id}'`, `'{today}'` | 400 `reference_not_found` | 201 | | `{}`, `[{ $in: 1 }]` | 400 `reference_not_found` | 201 | The reviewer's predicted outcome (`reference_not_found` pre-empting the engine) did not occur, but its cause is real. `invalid_type` came through only because the refusal failed open. - **The mechanism.** The catalog read put the value's text in `where`. A fully-wrapped `{…}` comparand is a filter placeholder (`classifyFilterToken` in `@objectstack/spec`, applied by `@objectstack/core`'s `resolveFilterTokens`). - An unknown one throws `FILTER_TOKEN_UNKNOWN`. The refusal then logged "catalog could not be read" and let the write through. - A known one is replaced by its value, so the lookup judged a different name. - **The consequence.** Every object without a declared operator but with at least one key, and every brace-wrapped string, bypassed the refusal. **What changed** (head `c282e608`, `position-catalog-refusal.ts` only): - **Operator objects are left to the engine.** `judgedName` stands down on them using `isFilterOperatorObject`, a narrow mirror of the engine's module-private `filterOperatorKeysIn` built from the same spec inputs (`isPlainRecord`, no `Date`, an own key in `ALL_OPERATORS` or the retired operators). It is not a `$`-prefix test: `{ $foo: 1 }` is judged. - **Placeholder-shaped names are looked up literally.** `catalogCarries` handles every name `classifyFilterToken` would treat as a placeholder: it reads the catalog names that share its first character (`$startsWith`) and compares in code, under the same scoped context. Such a name is therefore judged, never resolved and never failed open. - **The id hint** skips placeholder-shaped values. - **Docblock.** The stand-down bullet now names the engine's three refusals exactly: `required`, `max_length`, and `invalid_type` for an operator object. The `stringForm` doc now says a bigint is `String(value)` and only null, undefined, a symbol, a function or an unserialisable object gives `undefined`. "Fails open" says a placeholder-shaped name never falls open. - **The changeset is unchanged**; its "Which writes" is still true as written. **Tests** (24 cases, up from 21). Every refusal pin asserts `code` and `status`. - An insert of `{ $in: ['x'] }` or `{ $in: [], a: 1 }` gets the engine's `VALIDATION_FAILED` / 400 with `fields[0]` `{ field: 'position', code: 'invalid_type' }`, and nothing is stored. - `{ a: 1 }` and `{ $foo: 1 }` are refused `reference_not_found`, with value `'{"a":1}'` / `'{"$foo":1}'`, and no "could not be read" warning is logged. - `'{nope_tok}'` and `'{current_user_id}'` are refused `reference_not_found` as themselves. A catalog row really named `'{lit_pos}'` is found, and the assignment naming it is accepted. **Ablations** at `c282e608`; every restore was proven by blob == `HEAD` and an empty `git diff HEAD`: | ablation | mutation | result | | --- | --- | --- | | O1 | operator stand-down removed | 1 red: `{"$in":["x"]}` answered `reference_not_found` instead of `invalid_type` | | O2 | literal lookup removed | 2 red: `{ a: 1 }` and `'{nope_tok}'` were accepted ("expected the write to be refused, but it succeeded") | | B1 / C1 / P1 / N1 / N2 / N3 re-run | as before | 2 / 1 / 3 / 3 / 1 / 2 red | **Verification at `c282e608`** - plugin-security: 141 files / 2917 → 2920 tests passed; typecheck exit 0. - `dispatch-gates --commands`: 94 derived, 94 run, all exit 0. The `--ran` reconciliation reports a derived zero. - `check:query-options-erasure` holds at 236. - The diff-scoped issue-citation check: 11 citations, all resolve. --- _Generated by [Claude Code](https://claude.ai/code/session_01TEah6PeJGjxJfbHaySJjLQ)_ --------- 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.
Formula fields existed with
expression: z.string()but lacked function reference documentation, making them unusable without implementation knowledge.Changes
Created
/packages/spec/docs/formula-functions.mddocumenting 20 formula functions across 4 categories:UPPER,LOWER,CONCATENATE,TEXT,LENSUM,AVERAGE,ROUND,CEILING,FLOORTODAY,NOW,YEAR,MONTH,DAY,ADDDAYSIF,AND,OR,NOT,ISBLANKEach function includes:
Usage
Aligns with industry standards (Salesforce: 100+ functions, ServiceNow: 50+ functions).
Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.