[WIP] Add query enhancements and advanced validation features - #35
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…vements, advanced validation, and theme configuration Co-authored-by: huangyiirene <[email protected]>
Copilot stopped work on behalf of
huangyiirene due to an error
January 20, 2026 05:17
huangyiirene
requested review from
Copilot
and removed request for
huangyiirene
January 20, 2026 05:29
Contributor
|
This PR is very large. Consider breaking it into smaller PRs for easier review. |
huangyiirene
approved these changes
Jan 20, 2026
huangyiirene
approved these changes
Jan 20, 2026
huangyiirene
marked this pull request as ready for review
January 20, 2026 05:33
3 tasks
xuyushun441-sys
pushed a commit
that referenced
this pull request
May 25, 2026
Adds entries 26-40 covering the gaps that make the helpdesk template 'pretty but not daily-usable' from an end-user perspective: P0 additions: - #26 No inline message composer on detail pages - #27 No external-user portal mechanism - #28 Attachment/file-list field UI not E2E P1 additions: - #29 No 'changed since last visit' indicator - #30 Bulk operations UI unverified (escalates #17) - #31 Rich-text editor scoped to comments only - #32 No first-class canned response / macro - #33 No collaboration presence indicators - #34 No keyboard-shortcut API - #35 No conditional SLA timer (pause on waiting_customer) - #36 Formula fields can't reference foreign object fields P2 additions: - #37 No chart drill-down - #38 No period-over-period analytics primitive - #39 No inbound-channel abstraction (email-to-ticket etc.) - #40 i18n translation namespace validation weak Includes 'user-pain → platform-gap' mapping table tracing each end-user complaint to a specific issue number. Co-authored-by: Copilot <[email protected]>
This was referenced Sep 4, 2026
This was referenced Sep 10, 2026
os-sam
pushed a commit
that referenced
this pull request
Sep 10, 2026
Graded `patch`: nothing is widened and no symbol is added. The contract already published these shapes; the implementation is coming back to a declaration it had already published. Checked against the recorded WHICH LEVEL ruling of 2026-09-04 (decision batch #35, on #15294), whose `minor` trigger is additive widening. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
This was referenced Sep 10, 2026
Merged
This was referenced Sep 11, 2026
os-bill
pushed a commit
that referenced
this pull request
Sep 11, 2026
…ged docs page (#15117) Contract review returned FAIL with two must-fixes. 1. The changeset is `minor`, not `patch`. The written rule (`.github/workflows/pr-automation.yml`, maintainer ruling 2026-09-04 batch #35) is that a purely additive widening of a published package's public surface takes at least `minor`, and a commit type may raise a bump but never lower it below what the act requires. The PR's own `Clause-②: yes` line says this widens the accept set, in those words. The `find` precedent it leaned on does not reach: that was a NARROWING, it landed the day the rule was ruled, and the rule disclaims pre-rule `patch` precedents. How the wrong level survived local verification is the more useful half: `check-changeset-no-major` reads the clause-② declaration from the event payload and nothing else, so a local run without `--event` cannot exercise the level axis at all. Its exit 0 was recorded as a reading when the instrument could not have come back the other way. 2. `content/docs/ui/actions.mdx` — the page the repo's own Docs Drift Check flagged on this PR — is re-verified against the rewritten example. Its handler snippet still annotated `ctx: ActionContext`, a type that file no longer declares; it now imports and annotates the published `ActionHandlerContext`. The same snippet also wrote `completed_date`, which is `readonly` on `todo_task` and stamped by the object's `beforeUpdate` hook: copying it made the action refuse itself against `completed_date_required`. Both facts are verified against `task.object.ts` and `task.hook.ts`. The page now states the `delete` convention beside where it already states `find`'s. Folded in: the example's comment attributed to the contract a request the contract does not make. The contract asks for `ActionHandler`; it says so, and says why a file of function declarations annotates `ActionHandlerContext` instead. Comment-only — 11 changed lines, all comments or blank. Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
os-sales
pushed a commit
that referenced
this pull request
Sep 11, 2026
…ive clause ② sets (#17625) `Check Changeset` failed the LEVEL AXIS on the previous head: a PR whose clause ② is declared affirmative must grade at least one package whose `packages/**/src/**` it moves at `minor` or above, and this changeset graded the only such package `patch`. The level is a mechanical floor, not an editorial reading of the act. The maintainer ruling of 2026-09-04 (decision batch #35, on #15294) is written out under "WHICH LEVEL" in the `Check Changeset` step: the commit type may raise a bump but never lower it below what the act requires. The act here re-admits an input class the merged tree refuses, on an authorisation surface, so the type stays `fix(runtime)` and only the level moves. The changeset now records that reasoning so a later reader does not re-grade it back down as a plain bug fix. ⛔ The declaration was not softened to fit the level, and neither the gate nor the workflow was touched. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
This was referenced Sep 11, 2026
This was referenced Sep 13, 2026
os-project-manager
pushed a commit
that referenced
this pull request
Sep 15, 2026
The diff adds MessagingService.registerChannelProvider to an already-published class. A purely additive widening of a published surface takes at least minor (maintainer ruling 2026-09-04, decision batch #35), and the PR declares clause two yes, so patch was a self-contradiction inside one PR. Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…on the caller NAMES, not whichever one the session has active (objectstack-ai#16761) * fix(client): getActiveMember addresses the organisation the caller names `organizations.getActiveMember(organizationId)` built `GET /organization/get-active-member?organizationId=...`, and better-auth 1.7.2's handler for that path reads `session.session.activeOrganizationId` and never looks at `ctx.query`. The query string was dead on arrival: a permission check for organisation B while A was active answered A's row, with a 200 and no diagnostic. The method now asks the question honestly, in two requests: `GET /get-session` for the caller's own user id, then `GET /organization/list-members?organizationId=...&filterField=userId&filterValue=<self>&limit=1`, unwrapping the one-entry page. `list-members` reads `ctx.query.organizationId` and its rows carry the identical shape, so the signature and the declared return type are unchanged. The `get-active-member` ledger row is rebooked `server-only`: no SDK method builds that URL any more, and `sdk` means "expressed by the SDK". Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 * chore(changeset): declare the getActiveMember addressing fix Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 * chore(plugin-auth): keep the tracker id out of the ledger note string check:doc-authoring — a runtime string reaches authors and generated surfaces, none of whom can resolve `#NNNN`; git history keeps the anchor. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 * chore(changeset): grade @objectstack/client minor, not patch Check Changeset: a PR declaring clause-② yes may not grade a package it grew `patch`. The maintainer's ruling of 2026-09-04 (decision batch objectstack-ai#35) holds that a change to a published package's public surface takes at least `minor`; a commit type may raise a bump, never lower it. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 * fix(client): refuse a falsy organizationId in getActiveMember better-auth resolves `ctx.query.organizationId || session.activeOrganizationId` on `list-members`, so an empty string fell through to session state and came back 200 carrying the ACTIVE organisation's row — the same silent substitution this method was fixed to stop making, surviving on one argument while the JSDoc says "the GIVEN organisation". The SDK now refuses it before the wire, in the shape `environment(id)` already uses. The pinned case asserts nothing reaches the wire at all, and drives `list-members` with an empty id through the same double to show the fallback the refusal prevents is real in the fixture, not assumed. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5 * docs(plugin-auth): the ledger notes name every SDK method that builds each URL `get-active-member` was rebooked `server-only` because a truth ledger must not ship a false statement; by the same standard two rows were left incomplete. `get-session` named only `auth.me` and `auth.refreshToken`, and `list-members` named only `organizations.listMembers`, while `organizations.getActiveMember` now builds both. The `invite-member` row is the precedent for exactly this. Also restores a by-name anchor for the method: after the rebooking it was pinned by URL through `client-url-conformance.test.ts` but by no `client:` or `note:` string anywhere in the ledger. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5 * chore(changeset): carry the breaking-ness and its ADR-0087 disposition The changeset now carries the `**BREAKING**` banner, one before/after pair per moved input, and an ADR-0087 `not-required (no-migration-prescription)` disposition. The level stays `minor`: under the launch-window convention the level cannot carry breaking-ness, so the banner and the disposition are the carriers. Four inputs move, each stated as the response it drew before and the response it draws now: an id other than the active organisation; an organisation the caller is not a member of; any id on a session with no active organisation; and an empty id, which this round refuses client-side. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5 * docs(client): correct the pre-fix answer stated for a non-member of the named organisation The changeset bullet and the `getActiveMember` docblock both said a caller who was not a member of the NAMED organisation used to draw `400 MEMBER_NOT_FOUND`. better-auth 1.7.2's `get-active-member` handler reads `session.session.activeOrganizationId` and never `ctx.query`, so the named organisation was never consulted at all: such a caller drew a 200 carrying the ACTIVE organisation's row, and `MEMBER_NOT_FOUND` fired only when the caller had no row in the active organisation either. The PR's own ablation agrees — case ⑤ went red as "expected undefined to be 'YOU_ARE_NOT_A_MEMBER…'", i.e. the old shape resolved rather than throwing. Both sentences now state that before-state. The `after` (403) was already right, and the neighbouring bullets already stated it for every other input. Prose only: the changeset body ships as CHANGELOG text and the docblock is a comment. No executable line, no test and no behaviour moves. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_017Js5kTpTtxieBjPyScgxJ3 --------- Co-authored-by: Claude <[email protected]> Co-authored-by: huangyiirene <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…s[].children`, and give dashboard global filters a bundle group (objectstack-ai#16883) * wip(i18n): widen the shared page walk to slots + items[].children; add dashboards.*.globalFilters Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x * wip(i18n): translate the newly addressable global-filter keys; fix test typing Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x * wip(i18n): regenerate spec artifacts; re-measure the platform-page boundary pin Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x * docs(i18n): re-measure the extractor-config note now that the walk reaches under slots; declare the platform-objects bundle rows The platform-objects extract config explained its missing `pages` key by the shared walk rooting at `regions[].components[]` only. The walk now also roots at `slots.<slot>`, so the reason moved: it reaches every component on the three shipped record pages, and none of them carries an `id`, so the extractor still offers the page label alone. The changeset gains `@objectstack/platform-objects` — its shipped Setup bundles carry the new `dashboards.system_overview.globalFilters.created_at.label` row. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x * chore(spec): regenerate the protocol reference on the merged tree The os-regen driver merged `content/docs/references/api/protocol.mdx` with exit 0 while keeping one side; regenerating from the merged sources restores both — main's `droppedFields` prose (objectstack-ai#16930) and this branch's `dashboards` row gaining its `globalFilters` continuation. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5 * fix(i18n): declare the walk's return-shape change and pin the dashboard filter emitter The changeset graded `@objectstack/cli` and `@objectstack/platform-objects` `patch` while the diff moves each package's `src/**` and the PR declares clause ②; the maintainer ruling of 2026-09-04 (decision batch objectstack-ai#35, on objectstack-ai#15294) puts a purely additive widening of a published surface at `minor` or above, so both are raised. `walkAddressedPageComponents` is published and its return value changed shape, so the changeset now carries the `**BREAKING**` banner and exactly one ADR-0087 disposition, which are the only breaking-ness carriers during the launch window the level number cannot express. `collectExpectedEntries` gained a `globalFilters` emitter with no test: `check:i18n-walk-parity` measures at top-level group granularity and `dashboards` was already walked, so the sub-group could drift green. The new pin holds the `name`-keyed and `field`-keyed spellings, the option keys, the unkeyed filter that is passed over, and the `optionsFrom` filter whose label stays addressable while its fetched rows do not. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5 * docs(i18n): correct the walk-contract docblocks the widened walk left stale Docblocks only — no behaviour change, no test change. F2 (`packages/cli/src/utils/i18n-extract.ts`, `emitPageComponentCopy`): the shared walk this function delegates to now roots at `regions[].components[]` AND `slots.<slot>` and descends `properties.children` AND a panel's `properties.items[].children`, so the three claims written against the old, narrower walk were false on this branch: the roots/descent "only" pair, the emission exception's REGION-LEVEL wording (it is any ROOT-LEVEL `page:header`, a region's entry or a slot's — the code already reads `!nested`), and the count of ways `@objectstack/lint`'s `walkPageComponents` is wider. Re-derived against `packages/lint/src/page-walk.ts`: lint roots at slots (`:166-173`), descends `items[].children` (`:126-133`), `children` (`:139-142`) and `body` / `footer` (`:144-145`); the resolver now shares the first three, so lint is wider in exactly two ways — `properties.body` and `properties.footer`. F3 (`packages/spec/src/system/i18n-resolver.ts`, `walkAddressedPageComponents`): the docblock said `children` and `items[].children` are "the slots the walk owns; everything else on the node is the visitor's". `walkComposition` is called with the ORIGINAL `component` (`:1851`) and rebuilds the WHOLE `items` array from that original's entries (`:1814-1822` — non-panel entries copied across verbatim), then `:1853` spreads it over `next.properties`, so a visitor's edit to any other `items[*]` key is overwritten. The behaviour is deliberate and no current visitor writes `items`; only the sentence was wrong, so only the sentence changed. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_017Js5kTpTtxieBjPyScgxJ3 --------- Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…pes it already published (objectstack-ai#17255) * fix(engine): ObjectRepository declares the findOne/update shapes it already publishes `IScopedObjectRepository.findOne` / `.update` declare `Record<string, any> | null` and `Record<string, any> | number | null`, and `IDataEngine` — the call each of these forwards to — declares the same. `ObjectRepository` sat between two narrow declarations and re-widened the value back to `Promise<any>` on the way out, which `implements IScopedObjectRepository` accepts (a wider return always satisfies a narrower one) while every call site reaching a repository through the CLASS kept reading `any`, `ObjectQL.createContext(…).object(n).findOne(…)` included. Census: one consumer, `engine-filter-alias.test.ts`, which read `.status` off a value that can be null. Repaired with the file's own `not.toBeNull()` / `!` idiom. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU * test(engine): pin that a class-typed `object(name)` hands back a declared repository Compiler-driven probes (`ts.createProgram`, the idiom `packages/spec/src/contracts/scoped-context.test.ts` uses) over the exported class doors — `ScopedContext`, `ObjectQL.createContext`, `sudo()` — asserting the diagnostic NAMES the declared shape, so neither a bare "it errored" nor an `any` that erased the type can satisfy it. Anti-vacuity: the legal spelling must compile clean and no probe may report TS2307. Probes go through the CLASS, not `HookContext`: `HookContext.api` was narrowed to `IScopedContext` by objectstack-ai#5945, so a `(ctx: HookContext)` probe is green on both sides of this fix and pins nothing. Measured, and recorded in the file header. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU * chore(engine): changeset for the declared repository return shapes Graded `patch`: nothing is widened and no symbol is added. The contract already published these shapes; the implementation is coming back to a declaration it had already published. Checked against the recorded WHICH LEVEL ruling of 2026-09-04 (decision batch objectstack-ai#35, on objectstack-ai#15294), whose `minor` trigger is additive widening. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU * chore(engine): re-grade the repository return declarations to minor + BREAKING Landed precedent PR objectstack-ai#15280 measured: `SqlDriver.update()` and the `TursoDriver.update()` override moved off an explicit `Promise<any>` onto the shape `IDataDriver` already declared -- no new exported symbol, `packages/spec` untouched -- and both changesets shipped `minor` with a **BREAKING** banner. That is this change's shape exactly, so the earlier `patch` reasoning ("the contract already published it, so nothing moved") is the very fact pattern that precedent grades `minor`: the emitted `.d.ts` read `any`, so no caller holding the class was ever asked to narrow. The ADR-0087 disposition is `no-migration-prescription`, as sibling PR objectstack-ai#16783 used for the same family. `type-surface-only` is semantically the right category but its predicate 4 cannot address either narrowed symbol; the marker records that measurement. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU --------- Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…e already accepts, and says which convention is the contract (objectstack-ai#17608) * fix(spec): declare ActionEngineFacade.delete's id array, and say which convention is the contract `ActionEngineFacade.delete` declared `id: string` while the runtime facade has always accepted `string | string[]`, iterating one `ql.delete` per id. The declaration is now `string | string[]`, and the member's doc comment states what the runtime comment used to record as an accident of two handler suites: both spellings are contract, the array form is a convenience over the same per-row path, and it is neither bulk nor atomic. Two consumers follow from the declaration: - `packages/runtime`'s "tolerant of both conventions" comment is retired (a comment correction only; the arm's behaviour is untouched). - `examples/app-todo/src/actions/task.handlers.ts` drops the hand-rolled `ActionContext` copy of the facade — which existed because the published type could not express its array call, and which had already drifted on `find` — for the published `ActionHandlerContext`. Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH * chore(changeset): patch for the ActionEngineFacade.delete declaration (objectstack-ai#15117) The level is measured, not assumed: nothing is removed and nothing narrows, no runtime behaviour changes, and the sibling re-declaration of the neighbouring member (`find`, objectstack-ai#14175) shipped as a patch from the same interface. What ships is a published type that finally describes behaviour that was already served. Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH * docs(spec): cite the runtime delete arm by symbol, not by line number (objectstack-ai#15117) `check:spec-docblock-symbol-anchors` refuses a line number as an anchor form, and the citation added with the widening (`:1471`) was one — a NEW finding, not one of the seven day-one residuals. It is now the symbol anchor `packages/runtime/src/action-execution.ts#buildActionEngineFacade`, which is also the more honest citation: this card exists partly because the line numbers the issue quoted had already drifted by three hundred lines. Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH * fix(spec): grade the delete widening as minor, and re-verify the flagged docs page (objectstack-ai#15117) Contract review returned FAIL with two must-fixes. 1. The changeset is `minor`, not `patch`. The written rule (`.github/workflows/pr-automation.yml`, maintainer ruling 2026-09-04 batch objectstack-ai#35) is that a purely additive widening of a published package's public surface takes at least `minor`, and a commit type may raise a bump but never lower it below what the act requires. The PR's own `Clause-②: yes` line says this widens the accept set, in those words. The `find` precedent it leaned on does not reach: that was a NARROWING, it landed the day the rule was ruled, and the rule disclaims pre-rule `patch` precedents. How the wrong level survived local verification is the more useful half: `check-changeset-no-major` reads the clause-② declaration from the event payload and nothing else, so a local run without `--event` cannot exercise the level axis at all. Its exit 0 was recorded as a reading when the instrument could not have come back the other way. 2. `content/docs/ui/actions.mdx` — the page the repo's own Docs Drift Check flagged on this PR — is re-verified against the rewritten example. Its handler snippet still annotated `ctx: ActionContext`, a type that file no longer declares; it now imports and annotates the published `ActionHandlerContext`. The same snippet also wrote `completed_date`, which is `readonly` on `todo_task` and stamped by the object's `beforeUpdate` hook: copying it made the action refuse itself against `completed_date_required`. Both facts are verified against `task.object.ts` and `task.hook.ts`. The page now states the `delete` convention beside where it already states `find`'s. Folded in: the example's comment attributed to the contract a request the contract does not make. The contract asks for `ActionHandler`; it says so, and says why a file of function declarations annotates `ActionHandlerContext` instead. Comment-only — 11 changed lines, all comments or blank. Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH * docs(ui): state the true reason a handler must not name a server-owned field (objectstack-ai#15117) The previous push removed `completed_date` from the handler snippet, which was right, and then explained it with a mechanism that is false. Re-measured: - A handler's `ctx.engine` runs ELEVATED — `buildActionExecutionContext` returns `{ ...base, isSystem: true }` (`packages/runtime/src/action-execution.ts`) — and the read-only strip is gated `if (!opCtx.context?.isSystem)` (`packages/objectql/src/engine.ts`). Nothing is stripped on that path, so "stripped from the payload before the record is validated" never happens. - The refusal does not exist either, for any caller. The hook's stamp is unconditional on the transition and its own docblock says that is precisely so a caller-supplied value is overwritten and survives the strip; a live test asserts it — "a caller that still sends `completed_date` is not punished for it — the hook value wins". The REJECTED row whose mechanism the page described is labelled "Measured before the fix". The true reason is close to the inverse, and is now what the page says: the write is not stripped, it LANDS, and on a write that is not a completion transition the hook does not stamp, so the handler's "now" silently replaces the real completion timestamp. I read the objectstack-ai#7036 history at the three sites I cited and wrote it in the present tense, without reading the four sites the conclusion depended on. A citation that exists is not a citation that entails. Co-authored-by: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH --------- Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
… ARITY case in the conformance kit, and the cube face stops dropping the window (objectstack-ai#17694) * feat(core): hold every analytics face to the dateRange array ARITY The shared conformance kit had exactly one array-arm case — a two-element window — so the arity itself was governed nowhere and each face was free to invent a reading for `['2026-01-01']`, `[]`, `[a, b, c]` and `[null, null]`. Adds `ANALYTICS_DATE_RANGE_NOT_A_WINDOW` and the case that holds every REGISTERED face to the rule PR objectstack-ai#17593 already landed on the service-analytics faces: a non-two-bound array is refused with the ADR-0112 ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400 envelope. No existing case is weakened — the two-element window case is this one's control. Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <[email protected]> * fix(driver-memory): refuse a dateRange array that is not a two-bound window MEASURED end to end on `49cd71548`, four rows spanning 2020…2099: the cube face emitted NO time predicate at all for `['2026-01-01']`, `[]` and `['2026-01-01','2026-01-31','2026-02-01']` — a pipeline byte-identical to one with no `dateRange` — so the query read ALL of history, and `[null, null]` compared against the string `'null'` and selected nothing. The array arm is now judged at the discriminator and yields two bounds or throws the shared ADR-0112 ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400 envelope, which is the rule the kit's new ARITY case holds every registered face to. The `if (range.length === 2)` guard — the line that dropped the window — is gone, and `ResolvedDateRange.bounds` is a tuple so it cannot come back. Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <[email protected]> * chore(changeset): dateRange array arity — one reading on every face Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <[email protected]> * refactor(core): the ARITY finding names which half of the contract broke Two bounds is necessary, not sufficient: calling `[null, null]` an arity problem sends the next reader to the wrong line. Surfaced by this change's own ablation, where a real window smuggled into the not-a-window population was reported as "every other arity". Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <[email protected]> * chore(changeset): grade @objectstack/core `minor` — it gains an export A purely additive widening of a published package's public surface takes at least `minor` (maintainer ruling 2026-09-04, decision batch objectstack-ai#35, on objectstack-ai#15294): `ANALYTICS_DATE_RANGE_NOT_A_WINDOW` is absent on `origin/main`, exported from `analytics-date-range-conformance.ts` and re-exported by the package entry. `@objectstack/driver-memory` stays `patch` and the changeset now says why: its public surface is byte-unchanged, and the behaviour it stops producing was a defect the contract never admitted. Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…ated bare-root request is not refused (objectstack-ai#17625) (objectstack-ai#17691) * fix(runtime): canonicalise the API root to the discovery route (objectstack-ai#17625) `dispatch()` strips one trailing slash, so both root spellings the dispatcher accepts — `${prefix}/` (arriving as `/`) and `${prefix}` (arriving as ``, the MSW/base-URL-stripped form) — collapsed onto the empty string. Only the discovery branch at the foot of the method knew that meant the API root; the ADR-0069 gate, which runs far above it, did not. That disagreement was invisible while `isAuthGateAllowlisted` exempted a falsy path, and became a 403 on the bare-root discovery request once the predicate went fail-closed. Normalising the root to `/` would relocate the 403 rather than remove it: a segment-less path matches no `ALLOW_ROUTES` entry, and the discovery branch tests `/discovery` or the empty string, neither of which `/` satisfies. The root is canonicalised to `/discovery` instead — the route it has always served — read from one constant by both sites so the two cannot drift again. `packages/core` is untouched and `ALLOW_ROUTES` is unchanged: the only input whose gate answer moves is the API root, which gains exactly the exemption `/discovery` already carried, and gains it by being that route. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c * docs(changeset): the API root is the discovery route (objectstack-ai#17625) Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c * fix(changeset): grade the runtime bump `minor`, the floor an affirmative clause ② sets (objectstack-ai#17625) `Check Changeset` failed the LEVEL AXIS on the previous head: a PR whose clause ② is declared affirmative must grade at least one package whose `packages/**/src/**` it moves at `minor` or above, and this changeset graded the only such package `patch`. The level is a mechanical floor, not an editorial reading of the act. The maintainer ruling of 2026-09-04 (decision batch objectstack-ai#35, on objectstack-ai#15294) is written out under "WHICH LEVEL" in the `Check Changeset` step: the commit type may raise a bump but never lower it below what the act requires. The act here re-admits an input class the merged tree refuses, on an authorisation surface, so the type stays `fix(runtime)` and only the level moves. The changeset now records that reasoning so a later reader does not re-grade it back down as a plain bug fix. ⛔ The declaration was not softened to fit the level, and neither the gate nor the workflow was touched. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c --------- Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…nal, matching the body schema of the route it posts to (objectstack-ai#17215) (objectstack-ai#17834) Fixes objectstack-ai#17215 Clause-②: yes Declared by the dispatching `domain:cli` seat (objectstack-ai#6024) and unchanged by the delivered diff: relaxing `redirect_uris` **enlarges a published SDK method's accepted set**, which is the widening direction and is graded conservatively regardless of the fact that it restores parity with the vendor. `needs:contract-review` is on the card; this PR does not enqueue until an at-tier contract review of record exists, which is the PM seat's half and is ⛔ not satisfied by green CI. ## ⭐ The falsifier, resolved FIRST Triage (`5621351376`) fenced this card: before writing the `?`, establish whether `redirect_uris`' required-ness is a **deliberate guard** rather than an oversight — RFC 7591 makes `redirect_uris` required for authorization-code clients, so a wrapper stricter than a permissive vendor may be correct on purpose. An assertion found ⇒ stop, write no code. **Verdict: no such assertion exists anywhere.** Every search below is reported with its count and a lit control, because a bare "found nothing" is not a result. | surface searched | how | hits | lit control (proves the matcher fired) | |---|---|---|---| | the declaration's own file, in context | `grep -n redirect_uris packages/client/src/index.ts`, **every hit read in context** | **8**, none an assertion | the same 8 hits are the control — the matcher fires and each was read, not counted | | repo-wide `redirect_uris` | `git grep -c` | **20 files**, none an assertion | `create-client` -> 8 files | | ADRs | `git grep` in `docs/adr/` | **0** files for `redirect_uris`, **0** hits for `7591` | `Status` -> **136** ADR files | | RFC 7591 anywhere in-tree | `git grep 7591` | **20 files** — none ties 7591 to this member's required-ness | `RFC ` -> **150** files | | guard-shaped prose (`deliberately/intentionally required`, `stricter than`, `guard rather than`, `on purpose`) | `git grep -niE` over client, plugin-auth, docs, content, changesets | **12 hits, zero about this member** | `deliberately` in `packages/client` -> 51 in CHANGELOG alone | | the pin test named by the docblock | read in full (253 lines) | pins the **key set**, never this member's optionality | it pins `client_name` and `scope` value types one screen up — so the idiom exists and was deliberately not applied here | | `oauth-provider-schema-parity.test.ts` | read in full | **0** mentions of `redirect_uris`, `create-client`, `body` | it is a DB-column parity gate; its own subject matter is the control | | PR review threads on the sibling PR objectstack-ai#17209 | REST `/pulls/17209/comments` and `/reviews` | **0** and **0** | repo-wide `/pulls/comments` -> **100** returned, so the channel is lit | | review threads on the commits that CREATED the required-ness | REST `/commits/{sha}/pulls` | **none — both are direct commits, no PR, so no review thread ever existed** | the same endpoint resolves PRs for other shas | | the sibling issue threads | `objectstack-ai/issues/15447/comments`, `objectstack-ai/issues/17210/comments` | **13** and **9** `redirect_uris` lines — every one calls it a *measured mismatch / divergence to report* | comment counts 10 and 7 returned | **What the history shows instead of a guard.** The member was required from the method's first commit, `170ecfa4c` ("feat: add OAuth application management features"), which declared `client_name: string` required too. Nine hours later `bc5bd719b` ("feat(auth): update OAuth application handling and consent flow with improved parameter management") re-pointed the method at `/oauth2/create-client`, **relaxed `client_name` to optional**, added the three bogus members objectstack-ai#15447 later removed — and left `redirect_uris` untouched as unchanged context, with no comment and a one-line commit message. Both are direct commits with no PR. Nothing, anywhere, ever stated a reason. ⇒ Per triage's own branch: **no assertion found ⇒ proceed; the `?` restores parity.** ###⚠️ Near-misses, so the next reader does not re-file this The dispatch order handed over two; reading every hit in context turned up **three more**. None speaks to whether the REQUEST member may be omitted: 1. `index.ts` "Only `client_id` and `redirect_uris` are always present … `redirect_uris` is unconditional" — the **response** projection's serialiser. 2. `index.ts` "⚠️ `redirect_uris` is always `[]` here" — the **public response** projection. 3. *(new)* `index.ts`, inside this very method's docblock: "the array the vendor schema requires … `redirect_uris` must arrive **pre-split**". About the **element shape** (array vs the Console textarea), not about presence. 4. *(new)* `auth-plugin.ts`: "better-auth's Zod body schema requires `redirect_uris: string[]`" — about the **type**, and it names its own failure mode, `expected array, received string`. It is the Console wrapper's split, in another package. 5. *(new)* objectstack-ai#17210 comment `5642689415` calls the request type "the vendor shape" while pointing at `redirect_uris: string[]` — an acceptance row asserting that a prose-only PR moved no type. ## Acceptance 2 — the vendor schema re-introspected at RUNTIME ⛔ Not read from a `.d.ts`. Method as the card prescribes: `createRequire` the package, `await import` the resolved entry, instantiate, walk `endpoints`, select the endpoint whose `path` contains `create-client`, read `options.body`.⚠️ **The package version has MOVED: the card measured `1.7.2`; the installed version is `1.7.3`** (`packages/plugins/plugin-auth/package.json` pins `1.7.3`, lockfile resolves `1.7.3`, and the package's own `package.json` read off the resolved entry's directory reports `1.7.3` at runtime). Re-measuring was therefore load-bearing, not ceremony.⚠️ A second difference at 1.7.3: a zero-argument `oauthProvider()` **throws** (`Cannot read properties of undefined (reading 'clientRegistrationAllowedScopes')`), so it was instantiated the way this repo instantiates it, `oauthProvider({ loginPage, consentPage })`. ``` endpoints walked : 33 create-client endpoint: key=createOAuthClient path=/oauth2/create-client options.body present : yes body constructor: ZodObject member count : 21 (card recorded 21 at 1.7.2 — no drift) ``` The row under test, read today, with optionality **driven** rather than inferred: | member | optionality (driven: `safeParse(undefined)`) | type | |---|---|---| | `redirect_uris` | **optional** | optional of array of string | ``` safeParse(undefined).success : true <- the member may be omitted safeParse([]).success : false <- an EMPTY array is refused safeParse(["https://a/cb"]) : true body.safeParse({ client_name: 'probe' }).success : true parsed keys: client_name unknown key => success: true | STRIPPED (no catchall — zod default strip, as the card recorded) ``` **All 21 members are optional**, and a body omitting `redirect_uris` entirely parses `ok`. The divergence the card describes is confirmed at 1.7.3.⚠️ Carried into the docblock and the changeset, because it is a trap in the other direction: optional does **not** mean `[]` will do — the vendor refuses an empty array, so omitting the member and passing `[]` are different requests and only the first is legal. ## Acceptance 3 — a DRIVEN reading that the call is now expressible ⛔ Not a type-level argument; compiled. `pnpm --filter @objectstack/client typecheck` -> **exit 0**, and that script is two halves — `tsc --noEmit && pnpm check:test-typecheck` — the second of which compiles the test layer under `tsconfig.test.json`. New pin `registerRedirectUrisOptionalPin17215`, compiled and never invoked, in the file the docblock already names: - an `expectTypeOf` on `RegisterRequest['redirect_uris']`, asserted equal to the union of `string[]` and `undefined` — the parity assertion, in the file's existing house idiom (spelled in words here because the body sanitiser eats angle-bracket fragments; the file carries the real generic syntax); - `register({ client_name: 'PROBE-17215-OMITTED' })` — **previously inexpressible**; - `register({})` — the emptiest legal call; - `register({ client_name, redirect_uris })` — still compiles, unchanged; - plus one runtime `it()` proving the SDK synthesises no default: the body sent is exactly `{"client_name":"PROBE-17215-OMITTED"}` with no `redirect_uris` key.⚠️ **The existing `keyof` key-set equality gives this change ZERO coverage** — `keyof` is insensitive to optionality, so it reads the same eleven names in both states. That is why a new assertion was owed rather than relying on the one already there, and it is stated in the new pin's docblock. ⛔ The equality itself is **untouched**: `git diff` on that test file has **0 removed lines** — the file is purely additive. ### Ablation, and a correction I am reporting rather than hiding⚠️ **My first ablation run was wrong and its green is void.** I predicted RED, mutated `?` back to required, ran `pnpm --filter @objectstack/client exec tsc --noEmit`, and got **exit 0**. The cause is not the pin: `packages/client/tsconfig.json` carries `"exclude": [… "**/*.test.ts"]`, so the bare `tsc` program never contains the pin file. Proved with `--listFiles`: **0** occurrences of the pin file and **0** `.test.ts` files in that program, against a lit control of **1** for `src/index.ts`. That run is **NOT MEASURED**, not a pass. Re-run against the program that actually compiles the test layer: ``` MUTATION PROVEN ON DISK (optional spelling 1 -> 0, required spelling 0 -> 1, blob differs from HEAD's) pnpm --filter @objectstack/client typecheck -> exit 1 check:test-typecheck: src/oauth-applications-register-request-members.test.ts: 4 type error(s) packages/client/src/oauth-applications-register-request-members.test.ts(189,66): error TS2344: Type 'string[] | undefined' does not satisfy the constraint 'string[]'. packages/client/src/oauth-applications-register-request-members.test.ts(194,50): error TS2345: Argument of type '{ client_name: string; }' is not assignable ... packages/client/src/oauth-applications-register-request-members.test.ts(197,50): error TS2345: Argument of type '{}' is not assignable ... packages/client/src/oauth-applications-register-request-members.test.ts(269,41): error TS2345: Argument of type '{ client_name: string; }' is not assignable ... ``` Direction RED as predicted; **count 4 where I predicted 3**, and the extra is nameable: the runtime `it()` I added also omits the member, so it stops compiling too. Both legs ran from the committed state under `trap … EXIT INT TERM` with absolute paths; restore proved by blob equality with `HEAD:packages/client/src/index.ts` **and** empty `git diff HEAD` **and** empty `git status --porcelain`. No dist preflight applies — the pin imports `./index`, a relative source specifier inside its own package, so no built artifact sits in the resolution path (`check:test-source-alias` exit 0). ## Acceptance 4 — nothing else in the signature moves ``` 1 file changed, 1 insertion(+), 1 deletion(-) (the type change itself) - redirect_uris: string[]; + redirect_uris?: string[]; ``` Member-by-member against the merge base, extracted from both revisions and compared: ``` members at BASE: 11 members at HEAD: 11 lines IDENTICAL between the two: 10 client_name? client_uri? contacts? grant_types? logo_uri? policy_uri? response_types? scope? token_endpoint_auth_method? tos_uri? the single differing line: redirect_uris: string[]; -> redirect_uris?: string[]; ``` ## Acceptance 5 — changeset `.changeset/17215-oauth-register-redirect-uris-optional.md`, **minor**: a published package's public surface moves, per the maintainer's ruling of 2026-09-04 (decision batch objectstack-ai#35) that such a change takes at least `minor` — the same grade the sibling objectstack-ai#15447 carried. ⛔ Not breaking in this direction: relaxing a required member to optional keeps every existing call valid, so the changeset carries no BREAKING banner and `check:adr-0087-registration` correctly reports no declared-breaking changeset (exit 0). ## Verification Exit codes captured by redirect-then-`$?`, ⛔ never through a pipe. Lock runs read their own `VERDICT command-exit` line. - `pnpm --filter @objectstack/client typecheck` — **exit 0** (test layer compiles under `tsconfig.test.json`; 0 files / 0 errors in `test-typecheck-debt.json`). - `pnpm --filter @objectstack/client test` — **exit 0**, **43 files / 519 tests passed**. - `pnpm --filter '@objectstack/client^...' build` — **exit 0**. Required first: on an unbuilt `dist` the typecheck reported 17 phantom `TS2307 Cannot find module '@objectstack/spec/*'` errors that have nothing to do with this diff. - Gate families derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (⛔ never a hand list) and reconciled: **`✓ 60 derived famil(ies) accounted for — 59 run, 1 NOT-MEASURED, 0 UNRUN`**. - The first derivation carried a **STALE TREE** warning naming `scripts/pm/check-half-states.mjs`; rather than run a list about a tree nobody is on, `origin/main` was fetched and merged (clean, no `os-regen` deferral, and neither incoming file overlaps this diff) and the families re-derived at `834e79059`. - `pnpm check:dual-build-cjs-loads` — **exit 3, `PREREQUISITE NOT MET`**: it reads built output and 32 packages have no `dist/`. ⛔ Declared NOT MEASURED, never counted as a pass; its own text says "This is NOT a pass: nothing was measured". CI checks out fresh and builds. This diff changes one type annotation and adds test code, so it emits no JavaScript for that gate to read. - Two others refused their prerequisites on the first pass and were remediated, ⛔ not counted as passes until re-run: `check:skill-examples` exit 1 (`packages/client-react/dist` held no declarations) -> after building `client-react` + `client`, **exit 0**, 258 prose examples type-check across 3 surfaces; `check:type-check-debt` exit 3 (the whole-repo re-measure OOMed under `--max-old-space-size=4096`) -> at 8192, **exit 0**, "5 ledger entr(ies) re-measured, 55 raw tsc error(s), none above its recorded number". - `pnpm lint` population, run in full rather than narrowed: `eslint . --no-inline-config` — **exit 0**, **6645 files checked, 0 errors, 0 warnings**, at `834e79059`. Type-aware linting is not enabled (`eslint.config.mjs` states "no `parserOptions.project`, no typed `@typescript-eslint` rules"), so per-file verdicts are independent. ## Acceptance notes - **The `[]` asymmetry, noted not filed.** The TYPE cannot express "non-empty array", so `register({ redirect_uris: [] })` compiles and the vendor refuses it at runtime. This is unchanged by this PR — it was equally true while the member was required, which is also why the old required-ness guarded nothing the vendor did not already guard better. Recorded in the docblock and in the new pin so the compiling call is not mistaken for a legal one. - **`return-type-precision.test.ts` passes `redirect_uris` at both its call sites (including `[]`) and both still compile** either way; no change was needed there. - The card's item 3 — checking the answer against the two other registration doors (`POST /oauth2/register` DCR, and the ObjectStack `sys-oauth-application/register` mount) — is ⛔ deliberately not taken here: neither door's shape moves, and objectstack-ai#17210 already ruled the mount stays `server-only`. --- _Generated by [Claude Code](https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c)_ --------- Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…on (objectstack-ai#18027) Fixes objectstack-ai#18003 Writes the **level half** of the pre-launch launch-window exemption into ADR-0087, per the maintainer's ruling on this card (recorded verbatim in `5653264942`): > **A,统一 minor,把规则写进 ADR-0087** One file, one insertion: 32 lines added to the ratified section `Ratified: the pre-launch launch-window exemption (majors 12–15)`. Nothing existing is rewritten — the verbatim sentence both rulings cited ("The exemption covers the *window* only — never the *chain*") is left byte-identical. ## What the measurement changed about the fix The card's premise was that the level rule "lives **only in prose**" — a declared-but-unenforced rule. **Half of that is false, and it changed the shape of this PR.** - ✅ **ADR-0087 is silent on the level** — verified across all 928 lines, not just the cited section. `minor` occurs exactly **once** in the whole document (line 718), describing four `@objectstack/client` type-surface narrowings — a different class, descriptively, not prescriptively. All **41** occurrences of `major` mean the **protocol** major. `patch` and `semver`: zero. - ❌ **But the rule is NOT unenforced.** `scripts/check-changeset-no-major.mjs` refuses any PR that introduces a `major` changeset. It runs per-PR in `pr-automation.yml`'s `Check Changeset` job, and it is **armed right now** — the RC exemption that stands it down needs `.changeset/pre.json` in `"mode": "pre"`, and that file does not exist on this branch. The level was also already ruled by the maintainer on **2026-09-04** (decision batch objectstack-ai#35, on objectstack-ai#15294) and its prose sits in that workflow's `WHICH LEVEL` block. ⇒ So objectstack-ai#16885's `major` was not merely the wrong level — it was **unimplementable**: CI would have refused the changeset. And the actual defect is **discoverability, not enforcement**: neither ruling cited the gate, because nothing in the document they *did* read points at it. That is why the insertion **points at the gate as the authority instead of restating its rules**. A second copy of an enforced rule is the drift this repo forbids (AGENTS.md: "the rule is stated once here and the script's own header is the authority on detail"), and ADR-0087 already uses exactly this pattern for `check-adr-0087-registration.mjs`. ## Why the text is shaped this way The section could be read two opposite ways because **every `major` in this ADR means a *protocol* major**, while the level a changeset declares is the **npm** version of the lockstep `fixed` group. The two move independently, and nothing said so. The insertion makes that the load-bearing distinction, then states the three consequences: `minor` + `**BREAKING**` banner + ADR-0087 disposition; an npm major as a planned act at window close; and a tombstone naming the **npm release**, never the protocol major. That last line is the cheap in-scope half of objectstack-ai#18021 (the six tombstones dating a 17.x removal to "`@objectstack/spec` 18"). ⛔ Those six lines are **not** touched here — objectstack-ai#18021 remains open and is not addressed by this PR. ## 维护者速读(草稿) **改了什么** 往 ADR-0087 那条已批准的「launch-window 豁免」小节里补了一段:退役类改动发 npm 的哪一级。只加不改,原有那句被两条裁决同时援引的原文一个字节都没动。 **为什么改** 同一个上午,两条总监裁决援引同一个约定,给出相反的等级(objectstack-ai#16929 发 `minor`,objectstack-ai#16885 发 `major`),两条都带您的「同意」。根因不是谁读错了,是这份文档里的「major」从头到尾指的都是**协议大版本**,而 changeset 的等级是 **npm 发行号**——两者各走各的,文档从没说过这件事。补上这句区分,并把等级这一轴写明。 **一个和派发前判断不同的实测结果** 这条规则**并非只活在散文里**。`scripts/check-changeset-no-major.mjs` 已经在每个 PR 上拒收 `major`,而且现在是**武装状态**;等级本身您也已经在 **2026-09-04**(决策批次 objectstack-ai#35,objectstack-ai#15294)裁过了。也就是说 objectstack-ai#16885 那条 `major` 根本落不了地——CI 会直接拒收。真正缺的是**可发现性**:两条裁决读的那份文档里,没有任何一处指向这个门禁。所以这段文字是**指向门禁**,不是再抄一遍规则——再抄一份,就是这个仓库反复禁止的那种会各自漂移的第二份声明。 **风险与代价(含回滚)** 风险很低:纯文档,零运行时、零发布物、零 npm 包内容变动,因此无 changeset(`docs/adr/**` 不发布)。唯一的实质代价是这段文字要和门禁保持一致——已经用「门禁的 header 是细节权威」的写法把这个代价压到最小。回滚 = revert 这一个 commit,无任何后续清理,不影响任何在飞的轮次。 **席位意见** **你要做的** ① 确认这段文字的措辞确实排除了当初那两种读法;② 确认「墓碑写 npm 发行号、不写协议大版本」这句您认可(它是 objectstack-ai#18021 的便宜一半,那六行本身本 PR 没碰);③ 这是受管面,合并由您手动执行——本轮不翻 ready、不入队、不挂 auto-merge。另外下面 Acceptance notes 里有一条给席位的门禁提案,需要您或分诊席决定要不要立卡。 ## Acceptance notes ### Zone 3 answer — should a mechanical check compare a retirement changeset's declared level against this rule? **No new gate — because the gate already exists, and a second one would be the duplication this repo forbids.** `check-changeset-no-major.mjs` already refuses the exact failure this card is about (`major` on a retirement), per-PR, armed. The card's third axis diagnosed "declared but unenforced"; the measurement says **declared, enforced, and unreachable from the document that matters**. This PR fixes the reachability, which is the whole of the defect. **But there is a narrower residual, and it is a proposal to the seat — ⛔ not built here** (a gate would widen a governed-surface PR the maintainer must hand-merge): - **The gap:** nothing refuses a retirement graded **`patch`**. The no-major guard has no floor, and its level axis (objectstack-ai#16055) exits 0 with `not-declared` on any PR that does not declare Clause-②. This is not hypothetical — objectstack-ai#16885's own dev report (comment `5643769773`) proposed `'@objectstack/spec': patch` for this very retirement. So three different levels were live across one class. - **Where it would live:** inside `check-changeset-no-major.mjs`, as a third axis in the run that already parses every introduced changeset's bumps and body. ⛔ Not a new script — the parse, the merge-base discipline and the self-test harness all already exist there, and a second script would re-derive them and drift. - **What it would key on:** the `**BREAKING**` banner, which is already mandatory during the window and already read by `check-adr-0087-registration.mjs`. Predicate: a changeset whose body carries the banner must grade at least one published package `minor` or above. That keys on a declaration the author already makes, so it needs no new authoring surface and no taxonomy of "retirement-class". - **What its lit control would be:** objectstack-ai#16885's own `patch` changeset body plus a `**BREAKING**` banner — refused; and the same body with the banner absent — exits 0. Both as `--self-test` fixtures beside the existing ones, so the checker's own health is graded the way the other two axes already are. - **Its end condition is inherited, not new:** it disarms at GA with the guard around it, when the level becomes the carrier again. ### Out-of-scope, noted, not filed - `.changeset/pre.json` is absent on `origin/main`, so the no-major guard's RC exemption is inactive and the guard's enforcing half is live. Recorded because the guard's own header documents a window in which it stands down; a future reader checking "is this enforced today" needs the state, and today it is armed. Not a defect. **承接者:无** — no open PR or person is touching that file. - The `check-doc-formula-expressions` gate exits **3** (PREREQUISITE NOT MET) on a clean worktree until `@objectstack/formula` and `@objectstack/lint` are built; installing does not build them. Its own refusal text names the fix and is exemplary, so this is a note, not a finding. **承接者:无**. ### Gates 22 derived families from the actual changed path (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`), all 22 run, exit code captured on the command before any pipe, reconciled with `--ran`: ``` Run reconciliation — 22 derived, 22 run, 0 NOT-MEASURED, 0 UNRUN. ✓ 22 derived famil(ies) accounted for — a DERIVED zero: all 22 recorded an exit code and none of them is 3. ``` `check:doc-formula-expressions` exited **3** on first run (measured nothing — neither pass nor finding); its prerequisites were built under the shared verify lock and it was re-run to **exit 0**. Reconciliation ledger holds real exit codes, not bare "I ran it" lines. **`pnpm lint` — a measured narrowing, not a skip.** eslint's own config **ignores markdown**: `isPathIgnored('docs/adr/0087-…md')` returns `true`, and `lintFiles` on it returns 1 result with 0 errors and 1 warning (the "file ignored" warning itself). The single changed path is therefore outside eslint's covered set entirely. Invariance for untouched files: `eslint.config.mjs` configures no type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot move any untouched file's verdict. Read at `93ec068bf`. **Line budget:** none applies. The token/line ratchet is `check-skills-token-ratchet.mjs`, scoped to published `skills/**`; `docs/adr/**` is outside it. No lines were bought by re-wrapping — the insertion is 32 added lines and 0 modified lines (`git diff --stat`: `1 file changed, 32 insertions(+)`). **Changeset:** none. `docs/adr/**` publishes nothing from any released package, so `skip-changeset` applies. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01EfsizFDgAcEjpwv4oM3WGT --- _Generated by [Claude Code](https://claude.ai/code/session_01EfsizFDgAcEjpwv4oM3WGT)_ Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
… at kernel:ready (objectstack-ai#18272) Part of objectstack-ai#18050 Clause-②: yes This delivers the card's SECOND half — registration read as a one-shot verdict. The first half (the outbox fan-out writing a delivery row for a channel nobody registered) landed in objectstack-ai#18081 and is on `main` today, so it is not what this PR changes. See "Premise re-verified" below: that half of the card's stated premise has DRIFTED and no longer reproduces. ## Premise re-verified on today's `origin/main`, driven rather than read Re-derived by symbol (every line number on the card is a stale anchor), at merge base `e592b4b6b`: | the card says | measured today | |---|---| | inline P0 `fanOut` checks registration | holds — `getChannel(channelId)`, refusing one line down | | outbox P1 `enqueueDeliveries` has NO check | **DRIFTED** — objectstack-ai#18081 added one; the loop refuses and counts | | an unregistered channel gets one row per recipient, dead-lettered on attempt one | **no longer reproduces** | | registration is a ONE-SHOT VERDICT (`if (getEmail())` at `kernel:ready`) | holds, unchanged — this PR's target | Both directions were driven end to end through a real `MemoryNotificationOutbox` and a real `NotificationDispatcher.tick()`, not concluded from the shape of the loops. A temporary probe emitted on `['inbox','email']` with `email` never registered: ``` leg A — today's main, guard in place rows_written_for_unregistered_channel=0 after_one_tick=[] emit_counts={"enqueued":2,"failed":2,"suppressed":[]} leg B — the same tree with the landed guard REMOVED (the shape the card describes) rows_written_for_unregistered_channel=2 after_one_tick=[{"status":"dead","attempts":1,"error":"channel 'email' not registered"}, {"status":"dead","attempts":1,"error":"channel 'email' not registered"}] emit_counts={"enqueued":4,"failed":0,"suppressed":[]} ``` So the card's reported symptom is real and is already cured on `main`; the probe file was deleted after both legs ran (restore proven by blob identity, see Evidence). ## What was wrong with the half this PR takes `messaging-service-plugin.ts` mounted the email and SMS channels inside a `kernel:ready` hook, behind `if (getEmail())` / `if (getSms())`. The comment above that guard reasoned "the dispatcher looks channels up dynamically, so registering after it is fine" — true of the dispatcher, and contradicted by the guard beneath it: the `if` ran exactly once and nothing revisited it. A transport that registered later in the same boot — a plugin ordered after this one registering from its own `kernel:ready` handler, `kernel:bootstrapped`, `kernel:listening`, or a runtime mount — never got its channel, and every `notify` naming it was refused as "not registered" for the life of the process, with the transport sitting in the registry. That is the three-part shape AGENTS.md's "Startup registry reads" section names: a read of a still-filling registry, a terminal conclusion from absence, and the conclusion recorded — here as a non-registration nothing revisits. `pnpm check:startup-registry-verdict` is green on it both before and after, because the AST rule reads a recorded verdict and a mount that never happens leaves nothing for it to read. ## The change `MessagingService.registerChannelProvider(id, resolve)` mounts a channel that is resolved on EVERY lookup; `getChannel` asks the provider, and `getRegisteredChannels` lists only what resolves right now. The plugin mounts both channels through it, memoising the channel OBJECT (so it keeps its identity and its template-store handle) and never the transport's PRESENCE — that re-read is the whole fix. This is AGENTS.md's first cure, in the shape `createLazyCounterStore` (plugin-auth) established: nothing is resolved at start, so plugin order decides nothing. The dispatcher reads the same lookup through `ChannelRegistry.getChannel`, so the old guard's claim is finally true end to end rather than contradicted by the code beneath it. ## What this deliberately does NOT move ⛔ An unmounted channel is still REFUSED, never suppressed. A composition that never registers the transport behaves exactly as it does today: the channel is not mounted, both fan-out paths refuse it with the same failed `DeliveryOutcome`, no `sys_notification_delivery` row is written, and nothing is recorded in `sys_notification.suppressed_channels` — an absent mount is a COMPOSITION fact, identical for every tenant in the process, and that column answers "why can this TENANT not send" (objectstack-ai#18041's settlement). Whether a MOUNTED channel can send is the separate question `isAvailable` answers, and this PR does not touch it. `channel-availability.test.ts` and `unregistered-channel.test.ts` are unchanged and green: the boundary they pin was held, not rewritten to make room. Both existing plugin pins ("registers the sms channel when an sms service is present" / "does NOT register the sms channel when no sms service exists") are also unchanged and green — this PR changes WHEN the mount is decided, not what it decides. The alternative fork — mounting both channels unconditionally and letting `isAvailable` answer `transport_not_configured` — was rejected for exactly that reason: it would move a deployment-wide composition fact into the per-tenant suppression column, re-opening what objectstack-ai#18041 settled, and (SMS implements no `isAvailable`) would turn an absent SMS transport into a delivery row recorded as SUCCESS with nothing sent. ## Evidence **Suite** (affected package, full): `pnpm --filter @objectstack/service-messaging test` — `Test Files 44 passed (44) · Tests 467 passed (467)`, VERDICT command-exit 0. `pnpm --filter @objectstack/service-messaging typecheck` — VERDICT command-exit 0, and `tsc -p tsconfig.json --listFiles` puts both edited test files in the program (1 hit each), so the green is attributable. **Ablations** — one-off, direction predicted RED and observed RED, each proven to reach disk by anchor counts and restored by STATE (not by an exit code). No `dist` leg is owed: the suite is in-package and vitest resolves these modules from `src`. | mutation | anchor before/after | result | restore | |---|---|---|---| | M1 — the plugin decides the mount ONCE again (`presentOnce`) | 1/0, `presentOnce` 0/2 | `Tests 2 failed, 7 passed` — "expected [ 'inbox' ] to include 'sms'" | on-disk == HEAD blob `4e177e236`, `git diff HEAD` empty, 9 passed after | | M2 — the service MEMOISES the provider answer (the verdict, moved) | 1/0, `resolvedOnce` 0/3 | `Tests 1 failed, 4 passed` — "expected [ 'inbox', 'email' ] to not include 'email'" | on-disk == HEAD blob `428fc9e93`, 5 passed after | | premise probe leg B — the landed objectstack-ai#18081 guard removed | 1/0 | 2 rows written, both `dead` on attempt 1 | on-disk == HEAD blob `428fc9e93`, anchor back to 1 | **Gates — derived 64 / run 20 of them / NOT MEASURED 1 / UNRUN 44** (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, re-derived after the changeset existed; exit codes captured before any pipe, verdicts read from each gate's own output). 14 of the 15 exit 0: `check:nul-bytes`, `check:test-source-alias`, `check:cross-package-test-inputs`, `check:engine-double-contract`, `check:type-check-coverage`, `check-registry-log-declared`, `check-plugin-teardown-shape`, `check-undeclared-dep-imports`, `check-empty-changeset --base origin/main`, `check-changeset-no-major --base origin/main`, `check-adr-0087-registration --base origin/main`, `check-closing-keyword-parity`, `check:published-files`, `check-comment-mask-adoption`. Plus `pnpm check:startup-registry-verdict` (exit 0), run because it owns this defect shape although the derivation does not name it. ⊘ NOT MEASURED: `pnpm check:type-check-debt` exited **3 — PREREQUISITE NOT MET** (it wants the whole-workspace build closure lint.yml builds first). Exit 3 is not a pass and not a finding; nothing was measured. The 44 UNRUN are the Lint and Repo Gates farm, left to CI rather than enumerated locally — see the follow-up section: one of them turned out to be a real finding about this diff, and the count above already includes the gates that round added. **Control characters**: a self-scan over the diff's files matched nothing. ## Clause-②: yes — re-derived from the DELIVERED diff, with controls ⛔ Not from the word `export` and ⛔ not from a bundle grep. Probed through the published entry (`exports["."]` to `dist/index.js` / `dist/index.d.ts`) after building the package: | probe | reading | |---|---| | added `export` lines across the whole diff | **0** — and that is the trap, not the answer | | `src/index.ts` (barrel re-export list) | **unchanged**, 0 lines | | positive control — `MessagingService.prototype.registerChannelProvider` | **function**, and declared on the exported class in `dist/index.d.ts` | | positive control — an already-published member (`registerChannel`) | function — the probe is not new-blind | | negative control — `lazyChannelMount` (plugin-local helper) | `undefined`, and absent from the d.ts | | negative control — the `ChannelProvider` type | not importable (not in the export list) | | negative control — a name that does not exist | `undefined` — the probe can answer no | ⇒ the published surface of an already-published entry grows by one callable member, so the direction is a WIDENING and the honest declaration is `yes`.⚠️ The card's claim comment declares `Clause-②: no` as a prediction; that prediction is falsified by this diff, and correcting the card's declaration and carrying the label on the card side are the seat's acts, not this branch's. `needs:contract-review` is on this PR. ## Not in scope, recorded `email-channel.ts` and `sms-channel.ts` still answer `{ ok: true }` when their transport is absent, so a direct `send()` from the dispatcher can record a delivery as success with nothing sent. The card's body already records it as not this card's fix, and this PR narrows rather than widens its reach: with the mount tracking the transport, fan-out cannot reach an unmounted channel at all. ## Follow-up after review feedback (head `6166e303c`) Two reds were reported on head `661c9b611` and both were mine. Neither was answered by loosening anything. **1. `Check Changeset` — the changeset grade was the side that was wrong.** The PR declares clause ② `yes`, and `.changeset/lazy-messaging-channel-mounts.md` graded `patch`. Under the maintainer ruling of 2026-09-04 (decision batch objectstack-ai#35, on objectstack-ai#15294) a purely additive widening of a published package's public surface takes at least `minor`, and my own Clause-② derivation above is the argument for it: a new callable member on an already-exported class is exactly that widening. Regraded to `minor`, and the entry now names the added API so an upgrading consumer reads what is new. `node scripts/check-changeset-no-major.mjs --base origin/main` driven offline against this PR's own event payload: "✓ LEVEL AXIS: this PR declares clause-② `yes`, and no package whose `packages/**/src/**` it moves is graded `patch`", exit 0. The lockstep cost is real and named rather than dodged: every publishable package is in the `fixed` group, so this minor moves the group's minor — that is the price the ruling sets for the widening, not a reason to under-grade it. **2. `Lint & Repo Gates` / `Slot-lookup ratchet` — a real finding about this diff.** The ratchet is right and my tests were wrong: both plugin tests this PR added wrote `const messaging: any = ctx.getService('messaging')`, which is the erasure shape `slot-lookup/no-any-assignment` bans, and `messaging-service-plugin.test.ts` is grandfathered for its EXISTING sites only — so the file's count grew 2 to 4. Fixed at the source with the spelling this same file already uses for its other lookups (`as MessagingService`). ⛔ The two grandfathered sites were left alone and `scripts/slot-lookup-baseline.json` is untouched: sweeping them is the batch work that owns that ledger, and re-baselining to clear my own red would be the one move this gate exists to prevent. Verdict line after the fix: "✓ slot-lookup ratchet holds: 106 unswept site(s) in 25 file(s), none new… baseline key set verified against e592b4b: no files added", exit 0 (captured before any pipe).⚠️ Worth recording against my own gate report: this red came out of the 49 I had left to CI. The reading was contract-legal, but a diff that adds test code doing service lookups owes `check:slot-lookup` locally, and I did not run it. **Re-verified at `6166e303c`**, after both remedies: closure build, `pnpm --filter @objectstack/service-messaging test` -> `Test Files 44 passed (44) · Tests 467 passed (467)`, and `typecheck` — one chained run, VERDICT command-exit 0. Both ablations re-run at this head because the ratchet fix touched the very file M1's red is read from: M1 still `Tests 2 failed | 7 passed` ("expected [ 'inbox' ] to include 'sms'"), M2 still `Tests 1 failed | 4 passed`, each restored to its HEAD blob with `git diff HEAD` empty. Authored by Claude Code, session `session_01URLHobLUJB9K1ABV6ofdjj`. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <[email protected]>
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.
Implementation Plan: High Priority Features
📊 Query Enhancements (query.zod.ts)
✅ Advanced Validation (validation.zod.ts)
🎨 Theme Configuration (theme.zod.ts - new)
📝 Enhanced Field Types (field.zod.ts)
🔍 Final Validation
Summary of Changes
Enhanced Field Types (7 new types)
Added 7 new field types to support richer UI components:
location- GPS coordinates with map display supportaddress- Structured address with format optionsrichtext- WYSIWYG editor supportcode- Syntax highlighting with language selectioncolor- Color picker with format optionsrating- Star rating with configurable maxsignature- Digital signature captureQuery Enhancements
Significantly expanded query capabilities for complex analytics:
Advanced Validation
Added 4 new validation types for richer data quality controls:
Theme Configuration (New Module)
Complete theming system for brand customization:
Test Coverage
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.