Skip to content

Add metamodel interfaces for ObjectQL/ObjectUI contract - #1

Merged
huangyiirene merged 2 commits into
mainfrom
copilot/create-meta-model-interfaces
Jan 18, 2026
Merged

huangyiirene merged 2 commits into
mainfrom
copilot/create-meta-model-interfaces

Conversation

Copilot AI commented Jan 18, 2026

Copy link
Copy Markdown
Contributor

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 properties

ObjectEntity - Complete entity definition including fields array, keys, UI hints, audit/soft-delete flags, and search configuration

ObjectView - View configuration supporting 10 presentation types (list, form, detail, card, kanban, calendar, chart, map, timeline, custom) with columns, filters, sorting, and layouts

Usage Example

import { ObjectEntity } from '@objectstack/spec';

const userEntity: ObjectEntity = {
  name: 'User',
  label: 'User',
  pluralLabel: 'Users',
  fields: [
    { name: 'email', label: 'Email', type: 'email', required: true, unique: true },
    { name: 'role', label: 'Role', type: 'select', options: [
      { value: 'admin', label: 'Administrator' },
      { value: 'user', label: 'User' }
    ]},
    { name: 'manager', label: 'Manager', type: 'lookup', 
      lookupEntity: 'User', lookupDisplayField: 'name' }
  ],
  primaryKey: 'id',
  displayField: 'email'
};

Implementation Notes

  • TSDoc comments on all properties for IntelliSense
  • Extensibility via metadata fields without core schema changes
  • Pure TypeScript, platform-agnostic (Node.js/Browser/Electron compatible)
  • Type guard isFieldType() for runtime validation
Original prompt

Please create a src/types/meta directory.
Define the Metamodel interfaces: ObjectEntity, ObjectField, and ObjectView.
These interfaces will be the contract shared between the backend (ObjectQL) parser and the frontend (ObjectUI) renderer.
Include specific types for FieldType (e.g., text, number, lookup)."


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Copilot AI changed the title [WIP] Add Metamodel interfaces for backend and frontend Add metamodel interfaces for ObjectQL/ObjectUI contract Jan 18, 2026
Copilot AI requested a review from huangyiirene January 18, 2026 08:50
@huangyiirene
huangyiirene marked this pull request as ready for review January 18, 2026 09:09
@huangyiirene
huangyiirene merged commit b45c7f2 into main Jan 18, 2026
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 <223556219+Copilot@users.noreply.github.com>
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 <223556219+Copilot@users.noreply.github.com>
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 <223556219+Copilot@users.noreply.github.com>
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 <223556219+Copilot@users.noreply.github.com>
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 <277994282+os-zhuang@users.noreply.github.com>
zhuangjianguo pushed a commit that referenced this pull request Sep 8, 2026
…ases — data / type are the only spellings

Maintainer ruling on #14791 (2026-09-07, director seat summon #17, decision
batch #1, option B): the two overlay props #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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x

---------

Co-authored-by: Claude <noreply@anthropic.com>
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 <pm@objectstack.ai>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants