Add metamodel interfaces for ObjectQL/ObjectUI contract - #1
Merged
Merged
Conversation
Co-authored-by: huangyiirene <[email protected]>
Copilot
AI
changed the title
[WIP] Add Metamodel interfaces for backend and frontend
Add metamodel interfaces for ObjectQL/ObjectUI contract
Jan 18, 2026
huangyiirene
approved these changes
Jan 18, 2026
huangyiirene
marked this pull request as ready for review
January 18, 2026 09:09
hotlong
added a commit
that referenced
this pull request
May 20, 2026
Closes gap #1 from the production-readiness review: the client-side ObjectForm / inline-grid masker (shipped earlier) was only a UX layer — a hand-crafted POST or direct ObjectQL call could still target any field. This commit closes the loop by enforcing field-level write permissions in the SecurityPlugin middleware. Behavior: on every insert/update, after the existing CRUD check and before the tenant/owner auto-injection, the middleware now scans the caller's payload against the merged field permissions for the target object. If the payload references any field the caller is not permitted to edit, the engine throws PermissionDeniedError (HTTP 403) with the offending field names exposed via details.forbiddenFields. Design choices: - **Fail-closed via throw, not silent strip.** Silent strip hides the boundary from honest clients (partial-save confusion: 'why didn't my change save?') AND gives probing clients no signal that the field exists. Throwing makes the boundary observable in both directions — legitimate UIs get an actionable error; probing clients learn nothing they could not already infer. - **Allow-list semantics.** Only fields explicitly enumerated in a permission set's 'fields' map are constrained. Fields without a rule pass through untouched. - **Bulk inserts checked row-by-row.** Arrays are scanned in full; a single offender in any row rejects the entire batch atomically. - **Runs BEFORE auto-injection.** The tenant/owner auto-fill (org_id, owner_id) is system-supplied from ExecutionContext, not from the caller's payload, so it is not subject to the user's edit permissions even when the user has no rule for those fields. - **System operations bypass entirely.** ExecutionContext.isSystem short-circuits the whole security middleware including this check. API additions: - FieldMasker.detectForbiddenWrites(data, fieldPermissions): string[] — exported helper for adapters that want to perform the check out-of-band (e.g., strip-then-warn instead of fail-closed). Documentation: - content/docs/guides/security.mdx — new 'Server-side enforcement (fail-closed)' subsection under Field-Level Security with the 403 response shape, the why-throw-vs-strip rationale, allow-list semantics, and the bulk/system bypass rules. - .changeset/security-fls-write-enforcement.md — minor bump. Tests: 7 unit tests for FieldMasker.detectForbiddenWrites + 8 integration tests via the existing security middleware harness covering insert/update/bulk/system-bypass/no-rule passthrough. 53 plugin-security tests pass. Co-authored-by: Copilot <[email protected]>
xuyushun441-sys
pushed a commit
that referenced
this pull request
May 22, 2026
Vendor-neutral observability primitives, extracted as a standalone
package so deployment-target code (cloud, self-hosted, ...) can depend
on the contracts without pulling in the whole runtime.
Owns:
- Contracts: MetricsRegistry, ErrorReporter, MetricSample, CapturedError
(Logger is re-exported from @objectstack/spec/contracts).
- Semantic conventions (SEMCONV): canonical Prometheus-style metric
names emitted by the framework, plus the back-compat RUNTIME_METRICS
alias.
- Metric exporters: Noop, InMemory (with totalCounter/histogramValues/
lastGauge helpers), Console, and OtlpHttp (buffered JSON exporter,
flush()-on-demand so it works on Workers as well as Node).
- Error reporters: Noop, InMemory, Console (structured JSON to stderr).
- Loggers: Noop, Console, Json (production-ready structured logging
that satisfies the existing @objectstack/spec Logger contract).
Backwards compatibility:
- @objectstack/runtime now depends on @objectstack/observability and
its src/observability/{metrics,error-reporter}.ts files are thin
re-export shims, so existing internal imports (and the public
runtime/index.ts surface) are unchanged.
Tests: 34 new tests covering all exporters; @objectstack/runtime test
suite still passes (the 2 pre-existing app-plugin.test.ts failures
around i18n service warnings are not affected by this change — they
were already failing on main).
Co-authored-by: Copilot <[email protected]>
xuyushun441-sys
pushed a commit
that referenced
this pull request
May 22, 2026
Introduces an opt-in path in ObjectStackProtocolImplementation.saveMetaItem that writes overlay metadata through SysMetadataRepository.put instead of the raw engine, so writes append to the change-log and emit HMR seq events. Behavioural changes (all behind options.useRepositoryWritePath / OBJECTSTACK_USE_REPOSITORY_WRITE_PATH=1): - saveMetaItem request gained optional parentVersion (If-Match) and actor fields. ConflictError -> 409 metadata_conflict. - Plural type aliases (views, dashboards, ...) normalized to singular before the repo's overlay-allowlist gate (rubber-duck #5). - Object-registry mutation moved AFTER successful put() so a conflict does not leave the in-memory registry stale (rubber-duck #3 invariant test added). Repo/test-fake fixes uncovered by rubber-duck review: - SysMetadataRepository.put/delete now update/delete by row id because the engine's strict .update requires id or multi:true (rubber-duck #1). - sys_metadata.checksum column widened from 64 -> 71 chars to hold the sha256: prefix produced by hashSpec() (rubber-duck #2). - Three test fake engines extended to support both overlay-tuple and id-based where lookups. 333/333 objectql tests pass. Deferred to PR-10d.4: REST plumbing for parentVersion/actor (rubber-duck #6), race-window retry for omitted parentVersion (rubber-duck #4), default flag flip + legacy path removal. Co-authored-by: Copilot <[email protected]>
xuyushun441-sys
pushed a commit
that referenced
this pull request
May 23, 2026
Walking through Studio as a low-code developer surfaced a fundamental gap: it is a beautiful metadata BROWSER but offers no authoring affordances. The #1 reflex of every Airtable / Power Apps user — add a field — has no entry point in our UI. This change adds two authoring touchpoints to the Object Hub > Fields panel that respect Prime Directive #6 (no temporary workarounds) and stay true to metadata-as-code: 1. + Add field button A primary CTA in the toolbar opens a guided dialog (AddFieldDialog) with a type picker (18 supported field types, each with icon + one-line semantics), a derived snake_case machine-name preview, and a live snippet preview. Two actions: • Copy snippet — pastes a defineField-style literal into the clipboard, ready to drop into the fields: { … } block. • Open .object.ts in VS Code — vscode:// deep-link via the existing vscode-objectstack extension. Filesystem writes from the browser are intentionally avoided. When the runtime overlay write-path matures (ADR-0005), the dialog can swap the snippet flow for a real persist call without changing its contract. 2. Click any field row to open a detail drawer Rows are now cursor-pointer and trigger a side Sheet (FieldDetailDrawer) showing the full normalised field spec — all properties, options enumerated, references, formula, validation — plus the same VS Code deep-link and a per-field Copy snippet that emits just this field's literal. The drawer is read-only; users who want to edit follow the VS Code link. The previous behaviour (clicking a row did nothing) was the single biggest dead-end during the persona walkthrough. The drawer is the minimum viable acknowledgement that a field is an interactive object, not a static row of text. Plumbing changes - ObjectSchemaInspector preserves every property of the field spec (spread over the cherry-picked subset) so the drawer has access to schema properties beyond the table columns. - Added a ChevronRight column on the right edge of every row, group-hover translate-x for the same drill-in affordance used on MetadataListPage compact rows. - CopyButton stops propagation so the row click does not fire when copying the field name. Build / tests pnpm --filter @objectstack/studio build — clean. pnpm --filter @objectstack/studio test — 69/69 tests pass; same 2 pre-existing @object-ui/core/dist/evaluator/ExpressionEvaluator module resolution failures in playground-plugins / plugin-system suites, unrelated to this work. Files - apps/studio/src/components/FieldDetailDrawer.tsx (new, ~160 lines) - apps/studio/src/components/AddFieldDialog.tsx (new, ~280 lines) - apps/studio/src/components/ObjectSchemaInspector.tsx · Imports FieldDetailDrawer, AddFieldDialog, Plus, ChevronRight · State for selectedField + addOpen · Preserves full field spec via spread in fieldEntries · Toolbar: + Add field primary CTA · TableRow: cursor-pointer, onClick → setSelectedField · New chevron column on right; colSpan bumped to 7 · Drawer + dialog mounted at end of component · CopyButton stops click propagation Co-authored-by: Copilot <[email protected]>
This was referenced May 25, 2026
Closed
Closed
xuyushun441-sys
added a commit
that referenced
this pull request
May 31, 2026
…nector-rest (ADR-0018) (#1416) Promote `connector_action` to a built-in baseline node — the generic-dispatch counterpart to `http_request`: where http_request calls any raw URL, connector_action invokes any registered connector's declared action. - engine: connector registry (registerConnector / unregisterConnector / resolveConnectorAction / getRegisteredConnectors) + ConnectorActionHandler / ConnectorActionContext / RegisteredConnector types. registerConnector validates via ConnectorSchema and asserts every declared action has a handler. - builtin/connector-nodes.ts: connector_action executor (source:'builtin', category:'io', all three paradigms), wired into installBuiltinNodes() — the core plugin now seeds 11 baseline node types. Missing connector fails the step (not flow registration) with a clear error. - packages/connectors/connector-rest (@objectstack/connector-rest): the reference concrete connector. createRestConnector + ConnectorRestPlugin, `request` action, static auth (none/api-key/basic/bearer), no OAuth2 refresh (enterprise tier). - New packages/connectors/ workspace category (alongside plugins/services/adapters). - ADR-0018 §Addendum: records the decision, resolves Open-question #1, supersedes M2's "connector_action dropped from baseline". Tests: service-automation 87/87, connector-rest 10/10 (incl. end-to-end kernel boot: both plugins -> connector_action flow -> REST handler). Co-authored-by: Jack Zhuang <[email protected]>
This was referenced Sep 8, 2026
os-project-manager
added a commit
that referenced
this pull request
Sep 8, 2026
…r write `bin/run-dev.js` explained #14858's crash with "oclif's `displayWarnings()` makes the first write". Re-traced with a `--import` observer that wraps `process.stderr.write` and logs the call site of the first EPIPE-ing call: node's OWN default `warning` handler (`internal/process/warning.js`: `onWarning` -> `writeOut` -> `console.error`) makes write #1, and `displayWarnings()` makes writes #2 and #3 of the same warning. Every write on that path is a `console.error`, and the reason that is fatal here while `bin/run.js` measured it harmless is not payload size. Console's `ignoreErrors` keep-alive is installed by the write CALLBACK and only `if (stream.listenerCount('error') === 0)`. `tsx` registers an off-thread module-customization hook, so node pipes the hooks worker's stderr into `process.stderr` and `Stream.prototype.pipe` prepends an `onerror` there; the count is 1, the keep-alive never installs, `onerror` takes the first EPIPE and re-emits it with nothing listening. Controls, node 22.22.2, read end destroyed, one variable between the legs: `console.error` alone 0/3, `module.register()` of a no-op hook plus the same `console.error` 3/3, raw `process.stderr.write` 3/3. The shim as shipped is 0/3 (exit 2); with the #14858 listener ablated it is 3/3 (exit 1). Comment text only. No behaviour changes, the listener stays exactly as it is, and the three `displayWarnings()` sites that state listener TIMING rather than authorship are untouched. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
This was referenced Sep 8, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 9, 2026
…26-09-07 is the acceptance act (objectstack-ai#16647) Ruling A on this card's last outstanding record, recorded by the director seat 2026-09-07 (comment 5572010837, decision batch objectstack-ai#1 of summon objectstack-ai#17, maintainer's verbatim reply 「同意」): the Status line becomes Accepted (2026-09-07) — accepted by the maintainer's reply of 2026-09-07 (objectstack#15453, decision batch objectstack-ai#1 of director summon objectstack-ai#17) Dated to the ruling, not to the 2026-08-28 landing: that landing PR (objectstack-ai#12839, commit bbf88be) was merged by the seat account os-sales, and the earlier ruling A of 2026-09-05 (5548576472) explicitly does not cover a seat merge — so "the merge that landed it on main" is NOT the acceptance clause here, and the sibling records' (ADR-0130, ADR-0131) merge clause is deliberately absent. The whole Status field is replaced, not only its state sentence, following the ADR-0130 (objectstack-ai#15704) and ADR-0131 (objectstack-ai#16590) flights: the field carries the state and the act and nothing else. The tail this drops is flagged in the PR body as a judgment call a reviewer can reject. Claude-Session: https://claude.ai/code/session_018dxq7YqsLDMeZDZ5AzsgJX Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 9, 2026
…ases — data / type are the only spellings (objectstack-ai#14791) (objectstack-ai#16777) * feat(spec)!: retire the ListView objectName / viewType react-tier aliases — data / type are the only spellings Maintainer ruling on objectstack-ai#14791 (2026-09-07, director seat summon objectstack-ai#17, decision batch objectstack-ai#1, option B): the two overlay props objectstack-ai#11284 had deprecated are removed from the ListView block with no deprecation window, now that the consumer fold ships in the pinned console (objectui normalizeListViewSchema at a472b071). - react-blocks.ts: objectName / viewType gone; `data` restated as the required binding (ledgered in REACT_OVERLAY_SHADOWS); REACT_RETIRED_OVERLAY_PROPS is the tombstone ledger; the record:related_list alternative writes the canonical spelling. - lint: boundObjectName reads data.provider === 'object' for ListView (the canonical read step 1 deferred); a retired spelling is a react-prop-retired error carrying the prescription; the step-1 unfolded-deprecation scaffolding is deleted. - showcase pages, the published objectstack-ui skill, the react-pages and validating-metadata guides and one recognizer fixture write the canonical spelling. - ADR-0087: semantic entry ui-react-list-view-binding-aliases-retired under protocol major 18; changeset minor with the BREAKING banner. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x * chore(spec): regenerate the react-blocks contract, api-surface, export-origins and the migration registry; pay the pages.md token ratchet; keep the tracker id out of the lint message Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x * chore(spec): accept the ListView objectName / viewType registry-only inputs in the declaration-parity baseline, with their discharge condition The gate's own --update path (MANIFEST=sdui.manifest.json check:react-declaration-parity --baseline react-declaration-parity.baseline.json --update), then the hand-maintained _acceptedReasons block re-added as the baseline's _note prescribes, with two new entries that state the expiry: accepted only until objectui#8510 removes the two designer inputs from objectui's list-view registration. This moves a ratchet as the mechanical consequence of the objectstack-ai#14791 ruling (option B, no deprecation window); declaring the props back in spec or on the overlay would undo that ruling and is not an exit. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x * test(lint): restore the ObjectForm half of the parse-gate mixed-spelling fixture The `parseable` array in `validate-react-page-props.test.ts` is the FALSE-POSITIVE CONTROL for the syntax gate: every entry asserts only `not.toContain(REACT_PAGE_SOURCE_UNPARSEABLE)`, so it grades the PARSE and nothing else. One entry carries an `ObjectForm` and a `ListView` in a single fragment. Retiring the `ListView` binding aliases re-spelled BOTH halves to `data={{ provider: "object", object: "a" }}`, but only the `ListView` half is in that retirement's scope: `ObjectForm` binds by its own props and carries the shared `OBJECT_NAME` overlay (`packages/spec/src/ui/react-blocks.ts`, the `REACT_BLOCKS` entry for `ObjectForm`), which is `objectName`, required. It has no `data` prop at all — neither in its `interactions` nor in its `dataProps`. The fixture therefore spelled a prop the contract does not carry. Because the array grades parseability only, both spellings parse and CI stayed green: no gate in the repo could see it. Restore the `ObjectForm` half to `objectName="a"` and keep the `ListView` half canonical, which is what the entry was — a genuine MIXED-SPELLING fragment, and a stronger parse fixture than either uniform spelling. Measured over whole file text (never line-oriented, so a hard-wrapped occurrence cannot hide), across the full diff versus the merge base: the `objectName=` prop sites attributed to `ObjectForm` are 16 -> 16 and to `ObjectChart` 36 -> 36 — both unchanged — and `ListView` is the only tag that gains the canonical `data` spelling. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x --------- Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 9, 2026
…r write (objectstack-ai#16971) `bin/run-dev.js` explained objectstack-ai#14858's crash with "oclif's `displayWarnings()` makes the first write". Re-traced with a `--import` observer that wraps `process.stderr.write` and logs the call site of the first EPIPE-ing call: node's OWN default `warning` handler (`internal/process/warning.js`: `onWarning` -> `writeOut` -> `console.error`) makes write objectstack-ai#1, and `displayWarnings()` makes writes objectstack-ai#2 and objectstack-ai#3 of the same warning. Every write on that path is a `console.error`, and the reason that is fatal here while `bin/run.js` measured it harmless is not payload size. Console's `ignoreErrors` keep-alive is installed by the write CALLBACK and only `if (stream.listenerCount('error') === 0)`. `tsx` registers an off-thread module-customization hook, so node pipes the hooks worker's stderr into `process.stderr` and `Stream.prototype.pipe` prepends an `onerror` there; the count is 1, the keep-alive never installs, `onerror` takes the first EPIPE and re-emits it with nothing listening. Controls, node 22.22.2, read end destroyed, one variable between the legs: `console.error` alone 0/3, `module.register()` of a no-op hook plus the same `console.error` 3/3, raw `process.stderr.write` 3/3. The shim as shipped is 0/3 (exit 2); with the objectstack-ai#14858 listener ablated it is 3/3 (exit 1). Comment text only. No behaviour changes, the listener stays exactly as it is, and the three `displayWarnings()` sites that state listener TIMING rather than authorship are untouched. Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 Co-authored-by: os-dev <[email protected]> Co-authored-by: Claude Opus 5 <[email protected]>
This was referenced Sep 10, 2026
github-merge-queue Bot
pushed a commit
that referenced
this pull request
Sep 12, 2026
) Fixes #17366 ## The defect: a carrier a seat could write into and not get out of The clause-② declaration limb had one carrier — the `Clause-②:` line inside the card's governing `Claim:` comment — and repairing a line already written there was not an act every seat can perform. The MCP GitHub tool set carries no edit-an-issue-comment call; the claim protocol forbids a second `Claim:`; and this gate's own refusal forbids the checker filling the line in ("the declaration IS the judgement"). Three closed doors, and a seat that wrote the line as prose was left waiting for somebody outside the repository to retype it. That was measured five times in one shift across two roles, and the fifth instance was still holding a PR with 38 checks and 0 failures out of the queue when the card was filed. ## The fix: one correction comment, read by the same reader A dedicated comment whose FIRST line is a fixed key naming, in digits, the claim comment it corrects: ```text Clause-②-correction: 5642248126 Clause-②: no Session: `session_01MCLBsUgfykL74aU716rzVK` ``` The newest correction naming the card's governing claim supersedes that claim's declaration, in both directions — so a wrong VALUE is repaired by the same act as an unreadable one. The declaration inside it is read by the SAME `CLAUSE2_KEY_LINE` through the SAME `readClause2Line`, so what moved is WHICH COMMENT may carry the declaration, never what counts as an answer. That is the move #16304 already made when it asked a sibling CARD. Attribution is the `Session:` line the claim protocol already makes mandatory (SKILL.md 〈模板与表〉, 「session ID 不可省」, and 「`mode:subagent` 的 dev 与 PM 同会话同 ID」 — so the session is exactly the granularity of "the claiming seat"). A correction is attributed when its `Session:` equals the governing claim's. That is a DECLARED identity, never a verified one: the value is copyable text and this fleet writes under one GitHub login, so the comparison is on what the comments SAY — the same ceiling C4 already works at. ⛔ No branch of it may become a new one-way door, which is the defect being removed. A correction naming another comment, declaring a different session, carrying no `Session:` line, or carrying a prose declaration is IGNORED WITH A PRINTED REASON that names an act the claiming seat can perform. And a governing claim that carries no `Session:` line at all leaves nothing to compare: the correction APPLIES, with a note saying attribution could not be verified and why. Refusing there would have rebuilt the door one room over. The C2 remedy text was rewritten to name WHO can act and HOW, in three parts: the claim template in SKILL.md 〈模板与表〉 that already carries the literal `Clause-②: yes | no` line and should be copied rather than composed; the fact that an already-posted claim comment is not editable from every seat; and the one comment that repairs it. It replaces "add the line to that claim comment", which named an act the claiming seat may have no tool for. ## What deliberately did not move - **The accept set.** `CLAUSE2_KEY_LINE`, `readValueToken` and `CLAUSE2_VALUES` are untouched. The card's five measured prose spellings are pinned as negatives in the self-test. - **The exit register.** The correction is a new INPUT to C2, not a new verdict family. 0/1/2/3/4 keep their meanings and their numbers. - **`check-half-states.mjs`.** `CLAIM_COMMENT_MARKER` is still imported, not restated, and not widened. A correction is not a claim comment and never enters the claim pool. - **SKILL.md.** The template already carries the fixed line; this PR points at it and does not restate it. - **The writes.** This script still writes nothing, hangs no label, and reads no verdict word. ## Acceptance, all four from the card 1. **A seat writing per the template gets a machine-readable declaration.** Pinned by importing the reader over the template's own key with each value substituted, plus the new remedy text that sends the seat to the template rather than to a regex. 2. **The negative control holds.** All five measured spellings from the card's table are pinned as not-declared, from the line reader and from a claim comment. The `#1` spelling is reported as a SPELLING near miss; the `#2`–`#5` spelling reaches no pattern at all, because 条款② carries no `Clause` token — pinned as a measured fact. 3. **Self-solvability is pinned.** A card whose claim declaration is unreadable earns a C2 finding; adding ONE correction comment clears it, with the claim comment byte-identical across the two threads, no second `Claim:`, and the governing claim unmoved. Demonstrated end to end through the offline `--pair-json` path: `--pair` exit 4 with the broken claim, exit 0 with the one comment added. 4. **Ablation.** Deleting the correction reading reds 22 of 465 self-test cases (every criterion-3 case); deleting the template pointer and the who-can-act remedy reds 7 (criterion 1's new half). Both legs were mutated on disk, proved to have landed, then restored to a blob hash equal to HEAD's. ## Acceptance notes -⚠️ Measured and pinned as a CONTROL, not endorsed and not fixed here: the template line copied UNFILLED — `Clause-②: yes | no` — reads as a declared `yes`, because `readValueToken` takes the first token after the colon and treats the rest as the seat's argument. That is an instance of the population #17098 is already open against (a key-INITIAL DESCRIBING line read as a declaration), so it is ⛔ not filed again here. The case carries a pre-registered FLIP TRIGGER: when #17098 lands, the expectation becomes `kind !== 'declared'` and the case flips with it in that PR. ⛔ It is not to be deleted and its green today is not an endorsement. - The PR-body carrier of `Clause-②:` is read by `scripts/check-changeset-no-major.mjs`, not by this gate: `--pair` reads the PR side for LABELS (C1) only, and no `readClause2Line` call here takes a PR body. The card-side correction shape therefore has no PR-side counterpart to add, and the PR-body carrier is editable by the seat anyway — which is exactly the asymmetry the card names. - `check-scripts-symbol-anchors`, `check-self-test-wired` and `check-self-test-workflow-commands` all pass unchanged: the `check:pm-clause2-carriers` step in `lint.yml` already runs the self-test, and no second step was added. ## Gates, on head `dbbfbb596` `node scripts/pm/dispatch-gates.mjs --commands` derives 35 families for this one-path diff; all 35 ran and all 35 exited 0 (`--ran` reconciliation: "35 derived famil(ies) accounted for — 35 run, 0 NOT-MEASURED (a DERIVED zero — all 35 recorded an exit code and none of them is 3)"). Lint is the declared narrowing rather than the repo-wide run CI owns: the receiving population is `eslint.config.mjs`'s base block (`files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`), `--format json` reports 1 file linted with 0 errors and 0 warnings, and the config "never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file" (`eslint.config.mjs`, its own words) — so this diff cannot move the verdict on any file it does not touch. `node scripts/pm/check-governed-merges.mjs --test scripts/pm/check-clause2-carriers.mjs` prints "✅ NOT governed — ordinary queue landing applies". `--pair 17738` exits 0. --- _Generated by [Claude Code](https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <[email protected]>
baozhoutao
pushed a commit
that referenced
this pull request
Sep 14, 2026
…records that mean them
`ADR-0071` names two unrelated decisions from this repo's point of view. The
record under `docs/adr/0071-*` is *Dataset semantic-layer depth — multi-hop
joins*; the identity/SCIM citations mean the enterprise-identity decision taken
in `objectstack-ai/cloud`, whose open mechanism half is now mirrored here as
ADR-0134 (landed 2026-09-07). Every identity citation therefore resolved to a
real page about the wrong subject.
Re-points 44 bare identity-meaning citations, per director ruling B as amended:
- 43 -> `ADR-0134` — the open mechanism half (SCIM forces the admin plugin on,
`active:false` -> ban, the env-side Service Provider, the seven stable SCIM
models). ADR-0134 is a local record with anchors into exactly these files.
- 1 -> `cloud ADR-0071` — `auth-manager.ts`'s "the paid Identity lifecycle",
which names the commercial half that stays in the cloud record.
Untouched, deliberately: the 22 dataset-meaning citations (they match the local
record), the 6 CHANGELOGs (historical archive), `docs/adr/**` (governed), and
`auth-plugin.ts`'s already-qualified `cloud ADR-0071 verification #1`.
Bare `ADR-0071` still resolves exactly as before — the qualifier only adds
precision, it does not weaken the gate.
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Co-authored-by: Claude <[email protected]>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…records that mean them (objectstack-ai#18098) Part of objectstack-ai#14361 — ⛔ this PR deliberately does NOT close it; see *"What this PR leaves open on the same card"* below. Clause-②: no Director ruling **B — the repo-qualified spelling** (comment `5507409725`), **as amended by the same director seat in comment `5507573601`**, which is the operative form of the ruling and is independently mandated by AGENTS.md Prime Directive objectstack-ai#13. See *"The one place this PR departs from the dispatch brief"* below — please read it before reviewing the diff. ## What was wrong From this repository's point of view `ADR-0071` named two unrelated decisions and only one of them had a record here. - `docs/adr/0071-dataset-semantic-layer-depth.md` is **ADR-0071: Dataset semantic-layer depth — multi-hop joins**. - The identity / SCIM citations mean something else entirely: the enterprise-identity decision taken in `objectstack-ai/cloud`, whose **open mechanism half** has been mirrored into this repo since 2026-09-07 as [ADR-0134](../blob/main/docs/adr/0134-env-side-scim-provisioning.md). `check:adr-anchors` was green over all of them, because the number *resolves* — it just resolves to a real page about the wrong subject, which is worse than a dangling id: a plausible record invites belief instead of a second question. ## The count, re-derived from the tree The card said 39, measured on `e854a531a`. Re-derived on this branch's merge base `66e34d14d`, per line rather than per file: | bucket | lines | disposition | |---|---|---| | identity meaning, bare | **44** | re-pointed (43 → `ADR-0134`, 1 → `cloud ADR-0071`) | | identity meaning, already repo-qualified | 1 | untouched — `auth-plugin.ts:1210` `cloud ADR-0071 verification objectstack-ai#1` | | dataset meaning | 22 | ⛔ untouched — they match the local record | | `docs/adr/**` (the 0071 record, ADR-0134, ADR-0135) | 18 | ⛔ untouched — governed surface | | six package CHANGELOGs | 15 | ⛔ untouched — historical archive | | generated `content/docs/references/system/auth-config.mdx` | 2 | regenerated from its producer, never hand-edited | | **total `ADR-0071` lines in tracked files** (`pnpm-lock.yaml` excluded) | **102** | over 46 files | **How 44 differs from 39.** The card counted *files* (43, of which 39 identity); this counts *citation lines*, and several files carry more than one (`auth-manager.ts` 8, `auth-manager.test.ts` 9, `auth-config.zod.ts` 4). The surface also grew between `e854a531a` and today: the two mirror ADRs landed, `auth-manager.test.ts` and `last-admin-guard.{ts,test.ts}` gained citations, and the two changesets the card counted have since been released into CHANGELOGs (`.changeset/` carries no `ADR-0071` today). 24 source files are touched; the 39 in the card is neither the file count nor the line count of what actually needed moving. ## How each citation was classified — per site, by meaning, never by path The split is not a judgement call this PR invented. **AGENTS.md Prime Directive objectstack-ai#13** states it directly: > Decisions that draw the open/closed or commercial boundary live in `objectstack-ai/cloud` and are cited from this repo as `cloud ADR-NNNN` — ⛔ never as a bare number […] When a cloud decision's **mechanism half** governs open code here, this repo carries its own ADR — own number […] the commercial half left in cloud. So the question asked at every site was: *which half of the cloud decision is this sentence about?* **(b) the open mechanism half → `ADR-0134`, 43 sites.** Every one of them is describing code that lives in this repository, and ADR-0134's own Consumers list names those exact files: - `plugin-auth` (14) — effective SCIM forces the better-auth `admin` plugin on; the construction-time refusal when `plugins.admin: false` sits beside effective SCIM; `active:false` → ban + session revocation; `@better-auth/scim` accepting no `schema` option; the SCIM/SSO adapter model map. - `plugin-auth` tests (10) — the four pins on the refusal message, re-judged in place (see below). - `spec` (7) — the `admin` flag's docblock and `.describe()` text, `public-auth-features.ts`'s `notes`, and the v17 `default-changes.ts` upgrade note. - `platform-objects` (9) — the `protection.reason` on the eight `sys_scim_*` objects and the `sys_user` action note. - `qa/dogfood` (3), `pnpm-workspace.yaml` (1, the rc.2 seven-model migration), `docs/qa/platform-checklist/areas/identity-auth.json` (1). **(a) the commercial / boundary half → `cloud ADR-0071`, 1 site.** `auth-manager.ts:3682`, *"the paid Identity lifecycle"* — that is D6 of the cloud record, the paid-Enterprise-Identity framing which ADR-0134 §*What stays in the cloud record* explicitly refuses to restate. It stays cited to cloud. **Dataset meaning → untouched, 22 sites.** Each was read, not inferred: all 22 are about multi-hop `include` chains, the 3-hop limit, join allowlists and Cube joins — `service-analytics` (15), `spec/src/ui/dataset.zod.ts` (3), `lint/validate-dataset-references.{ts,test.ts}` (2), `analytics.mdx` (1), `query-syntax.mdx` (1). ## Positive control against over-rewriting The discrimination rule is not a regex over the id — it is an explicit per-file allowlist of identity files, with a qualifier-aware substitution inside them. Two independent proofs that nothing on the dataset side moved: ``` $ git diff --stat origin/main -- packages/services/service-analytics packages/lint \ packages/spec/src/ui/dataset.zod.ts content/docs/data-modeling/analytics.mdx \ content/docs/protocol/objectql/query-syntax.mdx docs/adr/ (empty) ``` and, in the other direction, the already-qualified `cloud ADR-0071` at `auth-plugin.ts:1210` survived a pass over its own file untouched — the substitution skips any id already carrying a `CROSS_REPO_QUALIFIERS` word. ## Bare `ADR-0071` still resolves — the gate is not weakened Identical summary lines, before and after: ``` main check-adr-anchors: OK (53 anchored file(s) … 35477 citation(s) across 4521 file(s) resolve; 1022 decision-letter citation(s) …) HEAD check-adr-anchors: OK (53 anchored file(s) … 35477 citation(s) across 4521 file(s) resolve; 1022 decision-letter citation(s) …) ``` 22 bare `ADR-0071` citations remain in the tree and the gate resolves every one of them. **Ablation**, to show that is a measurement and not a vacuous pass — one bare dataset citation mutated to an id with no record: ``` on-disk proof : ADR-0071 3 -> 2 ; ADR-9071 injected = 1 ABLATION check-adr-anchors exit = 1 • ADR-9071 is cited by 1 file(s) but names no record under docs/adr/ — restored blob : 5c90e77 RESTORE SETTLED: blob matches HEAD and 'git diff HEAD' is empty ``` The restore was settled by blob hash plus an empty `git diff HEAD`, ⛔ never by an exit code — `trap` is unreliable in this container (objectstack-ai#17875). ## Pins re-judged in place, ⛔ none deleted `auth-manager.test.ts` carries four `toThrow(/…ADR-0071…/)` pins on the operator-facing refusal message and one test name. They pin the message's *content*, and the content moved, so the pins move with it — the assertions still pin exactly what they pinned before: that the refusal names the ADR that explains the coupling. Reverse-verified that they are live rather than decorative: with the shipped message mutated back to `ADR-0071`, the two message pins fail loudly. ``` MUTATED vitest exit = 1 AssertionError: expected [Function] to throw error matching /plugins\.admin[\s\S]*ADR-0134[\s\S]*p…/ AssertionError: expected [Function] to throw error matching /OS_SCIM_ENABLED[\s\S]*ADR-0134/ restored : f9b5c2a RESTORE SETTLED: blob == HEAD and 'git diff HEAD' empty ``` ## Generated docs `content/docs/references/system/auth-config.mdx` lines 105 and 215 follow their producer (`auth-config.zod.ts`'s `.describe()`), regenerated with `pnpm --filter @objectstack/spec gen:schema && … gen:docs`. Exactly two lines drifted; nothing else in the 222 generated files moved. `check:docs`, `check:generated` and `check:authorable-surface` are green. ## Changeset — measured, not defaulted `patch` for `@objectstack/plugin-auth`, `@objectstack/platform-objects`, `@objectstack/spec`. Published bytes really do move, measured against each package's `files[]` after a build: - `@objectstack/spec` — `files[]` lists `src/**/*.zod.ts`, so the changed `.describe()` ships verbatim; the generated `json-schema/` bundle (also in `files[]`) carries it too. - `@objectstack/plugin-auth` — `dist/index.mjs` carries 4 `ADR-0134`, including the operator-facing refusal string (positive control: a shipped literal greps at 1; negative control: a comment-only marker greps at 0). - `@objectstack/platform-objects` — `dist/index.mjs` carries the 9 `protection.reason` strings. `patch` and not `minor`: no export, no schema shape, and no accept/refuse face moves. The refusal fires on exactly the condition it fired on before; only the ADR number inside its sentence changes. The changeset says so, because a deployment grepping that message for `ADR-0071` is the one consumer this can surprise. ## Reverse-read — which existing sentence does this make false? Three, all of them in `docs/adr/**`, which this lane ⛔ must not touch (governed surface, PD objectstack-ai#14). None is falsified in substance; each goes tense-stale: 1. `docs/adr/0134-env-side-scim-provisioning.md:36` — *"Identity code that writes a bare `ADR-0071` today therefore cites, by this repo's own convention, the wrong document."* After this lands, no identity code does. The sentence's headline claim — that `ADR-0071` is an ambiguous string in this repo — stays **true**: the local 0071 is still the dataset record and cloud's 0071 still exists. 2. `docs/adr/0134:38` and `:294` — *"Re-pointing the existing bare citations is objectstack-ai#14361's work and is deliberately ⛔ not done by this file."* Still true about the file; the pointer becomes past tense. 3. `docs/adr/0135-identity-and-access-architecture.md:59` — *"Re-pointing the identity surface's existing bare citations at this record is [objectstack-ai#14361]"* — same class, and see the scope note below, because the `ADR-0024` half of that sentence is **still outstanding and still true**. **Reverse direction — a sentence this makes true rather than false:** `docs/adr/0134:37`, *"**Always write `cloud ADR-0071` for the SCIM record**, and `ADR-0134` for this one."* That instruction was correct and simply unobeyed by the tree; this PR is the tree obeying it. **Zero** other sentences in the tree assert a present-tense count or claim about these citations — the only `39 files` strings in the repo belong to `packages/cli` and are about something else entirely. ## The one place this PR departs from the dispatch brief The dispatch brief asks for **every** identity citation to read `cloud ADR-0071`, citing ruling `5507409725`. Three sources on `main` say otherwise, and they agree with each other: 1. **The ruling's own amendment**, comment `5507573601`, same director seat, 14 minutes later, self-titled *"Ruling amended"*: *"**Target for the (b) set** (citations that mean the cloud decision's *open mechanism half*): the **new local ADR numbers** that objectstack-ai#14506 / objectstack-ai#14507 / objectstack-ai#14508 land — not `cloud ADR-NNNN`"*, and *"the operator-facing refusal text in `auth-manager.ts:359` re-pointed to the new local number"*. 2. **The release comment** `5594577301` that unblocked this card restates it: *"the (b) citation targets are the new local ADR numbers those cards land"*, naming ADR-0134 as the mirror of cloud 0071. 3. **AGENTS.md Prime Directive objectstack-ai#13**, quoted above, which is binding regardless of any comment and says mechanism-half → this repo's own number. Under the unamended reading, ruling B is still satisfied by this diff (every identity citation is now unambiguous and the gate discriminates), but 43 of the 44 would read `cloud ADR-0071` instead of `ADR-0134`. If the reviewer prefers that reading it is a one-command flip on this branch; I did not pick a side silently, which is why this section exists. ## What this PR leaves open on the same card The amendment also folds the sibling collision class into this card — bare `ADR-0024` and bare `ADR-0081`, the mirrors of which landed as ADR-0135 and ADR-0133 — *"one card over the identity surface (0071 + 0024 + 0081) … so it is not paid twice"*. The dispatch brief scopes this lane to `ADR-0071` only, and the sibling surface is large: **151** bare `ADR-0024` lines and **86** bare `ADR-0081` lines, most of which legitimately mean this repo's own `0024-mcp-connectors` and `0081-trusted-react-page-tier` records and must **not** move. `docs/adr/0135:260` says so in as many words — *"Whether any individual bare `ADR-0024` citation should move … cannot be a search-and-replace"*. That is a real per-site pass with its own budget, and `docs/adr/0135:260` says so in as many words. `objectql-adapter.ts:58` now reads `See ADR-0024 / ADR-0134.`, where the `ADR-0024` half is knowingly left for it. ⭐ **This PR therefore delivers the `ADR-0071` third of objectstack-ai#14361 — the 44 sites above — and nothing else.** The per-site judgement over bare `ADR-0024` and bare `ADR-0081` is the remaining work of **the same card**, not a new one: the amendment's Scope line reads *"the identity surface's **whole collision class, one pass**: bare `ADR-0071`, bare `ADR-0024`, bare `ADR-0081` — every citation read for its meaning"*, and `docs/adr/0135:260` calls the `ADR-0024` half *"objectstack-ai#14361's per-site call"*. That is why the first line of this body says `Part of objectstack-ai#14361` and not `Closes`: merging this while two thirds of the ruled scope is unwritten would close the card on a third of its work. The card stays open for the next round. ## Verification | what | result | |---|---| | `pnpm check:adr-anchors` | green, byte-identical summary to `main` | | `pnpm --filter @objectstack/{spec,plugin-auth,platform-objects} test` | 476 + 111 + 40 files, **16 517** tests, all pass | | same three, `typecheck` | green (incl. `check:test-typecheck` ledgers, unmoved) | | `check:authorable-surface` · `check:docs` · `check:generated` | green | | `check:platform-checklist` · `check:doc-anchors` · `check:docs-single-h1` · `check:doc-authoring` · `check:docs-spec-enumerations` | green | | `check:nul-bytes` · `check:published-files` · `check:corpus-claim-drift` · `check:quick-reference-counts` · `check:comment-mask-{adoption,corpus}` · `check:spec-docblock-symbol-anchors` | green | | changeset gates (`check-changeset-fixed`, `check-changeset-no-major`, `check-empty-changeset`, `check:changeset-gate-self-tests`) | green | | `check:doc-frontmatter` · `check:docs-section-name` · `check:keyed-text-bounds` · `check:platform-object-tenancy-census` · `check:reference-carrier-shape` · `check:adr-0087-registration` | green | | `eslint . --no-inline-config` — the **whole repo**, not a narrowing | **6743** files, 0 errors, 0 warnings, at `8d2dfb3` | 39 gate commands, every one `exit 0`, captured before any pipe. **Declared narrowing.** `node scripts/pm/dispatch-gates.mjs --ran` derives 123 commands from this change set and accounts 39 of them. I ran the families this card names plus every one I could see implicated, and — since it turned out to fit the foreground budget — the repo-wide lint union rather than a narrowing of it. The remaining derived families are CI's farm, not this lane's run. `packages/qa/dogfood`'s three edits are comment-only — no dogfood boot was run locally, and that layer is declared to CI. --- _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.
Defines the type system shared between backend (ObjectQL) parser and frontend (ObjectUI) renderer.
Core Interfaces
FieldType- 16 data types covering text, numeric, date/time, lookup, selection, and special fields (file, image, json)ObjectField- Field metadata with validation rules, constraints, lookup configuration, and display propertiesObjectEntity- Complete entity definition including fields array, keys, UI hints, audit/soft-delete flags, and search configurationObjectView- View configuration supporting 10 presentation types (list, form, detail, card, kanban, calendar, chart, map, timeline, custom) with columns, filters, sorting, and layoutsUsage Example
Implementation Notes
metadatafields without core schema changesisFieldType()for runtime validationOriginal prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.