Skip to content

Commit b4a3529

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14279-adr-path-rot-repair
2 parents 73998d9 + 5c58423 commit b4a3529

55 files changed

Lines changed: 2026 additions & 93 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): `Field.valueDomain` — one closed standard-domain vocabulary and one membership predicate shared by settings specifiers and object fields (maintainer ruling 2026-09-02 on #14168, spec half)
6+
7+
<!-- adr-0087: not-required (accept-set expansion) One new CLOSED optional key
8+
on an existing shape (`FieldSchema.valueDomain`) and one new member of the
9+
closed field-level error catalog (`value_domain`); nothing authorable is
10+
renamed, retired or tombstoned, so there is no conversion to register.
11+
`SpecifierValueDomainSchema` keeps its export name and its exact three
12+
members — it is now an alias of the shared schema, not a second declaration. -->
13+
14+
An object field can now declare that its written value must be a member of a
15+
published standard, with the SAME closed vocabulary a settings specifier's
16+
`valueDomain` already uses — `iana_time_zone` · `iso_4217_currency` ·
17+
`iso_3166_alpha2` — and the same definition of membership. The vocabulary does
18+
not widen.
19+
20+
- `Field.valueDomain` (`@objectstack/spec/data`): authorable on `text` only —
21+
the one type whose stored value is a single plain string naming the member.
22+
On any other type the declaration is refused at parse with a located issue
23+
naming the type (the same applicability door `maxLength` / `minLength` use).
24+
A domain outside the vocabulary (`iso_8601_date`) is refused by name.
25+
- `ValueDomainSchema` / `ValueDomain` / `isValueDomainMember(domain, value)` /
26+
`ISO_3166_ALPHA2_CODES` (`@objectstack/spec/shared`): the vocabulary and its
27+
ONE membership predicate, declared once. `iana_time_zone` is the
28+
`Intl.DateTimeFormat` probe (`UTC`, `Asia/Kolkata`, `Europe/Kyiv` are
29+
members; `Europe/Munich` is not — never the `Intl.supportedValuesOf`
30+
enumeration, which omits `UTC`); `iso_4217_currency` is the key set of the
31+
package's checked-in CLDR snapshot (162 codes, exact uppercase);
32+
`iso_3166_alpha2` is the explicit list of the 249 officially assigned
33+
codes (exact uppercase).
34+
- `SpecifierValueDomainSchema` / `SpecifierValueDomain`
35+
(`@objectstack/spec/system`): unchanged name, unchanged members, now an
36+
alias of `ValueDomainSchema` — nothing that imports them moves.
37+
- `FieldErrorCode` gains `value_domain` (ADR-0114 D1: the code is the
38+
property's own name, like `max_length`), with message templates in the
39+
four platform locales (`value_domain`, plus one finer variant per domain).
40+
41+
What this release does NOT yet do: refuse a non-member on the record write
42+
path. The record validator does not read `Field.valueDomain` yet; that
43+
enforcement and the settings door's re-point onto the shared predicate are the
44+
engine and services halves of the same ruling and ship in their own releases.
45+
Until the engine half lands, a domain declared on a `text` field is accepted
46+
at parse and describes the contract the write path will enforce.

.changeset/repo-execute-elevated-context.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ Before this change, the handler's `ctx` carried `params`, `userId`, `tenantId` a
88

99
`ctx.api` is now a real `ScopedContext` bound to `{ ...callerContext, isSystem: true }` — the caller's own envelope, elevated — the same `sudo()`-shaped formula `buildActionExecutionContext` and `recomputeSummaries`'s `systemCtx` already use, so `userId`/`tenantId` still stamp the write and an open transaction still joins rather than escapes. `ctx.executionContext` carries the same elevated envelope, matching the REST/MCP shape exactly.
1010

11-
**What widens**: a `readonly: true` field a handler writes through `ctx.api.object(x).update(y)` when reached via `repo.execute()` now lands instead of being silently stripped, matching REST `/actions` and MCP `run_action`. A repo-wide census (production + test, `examples/` and `apps/` included) found no existing caller of `ObjectRepository.execute()` — every hit in the tree was prose describing the shape, never an invocation — so no shipped write changes behaviour.
11+
**What widens**: `ctx.api` inside a `repo.execute()`-dispatched action handler now carries `isSystem: true`, and ObjectQL's registered security middleware reads that as a **total**, unconditional bypass (`plugin-security/src/security-plugin.ts:1614-1616`, "System operations bypass security" — `return next()` before every other gate in the middleware runs) — not only the static `readonly` strip named in earlier drafts of this note. Every `find`/`insert`/`update`/`delete` the handler drives through this `ctx.api` now also skips, in the same stroke: RLS read scoping (`security-plugin.ts:4344`) and field-level security (`:4495`); the CRUD/export permission checks in the same middleware (`:1616`, `canExport` at `:4573`); the ADR-0103 engine-owned/append-only write guard (`system-write-guard.ts:96,120`, called at `security-plugin.ts:1736`); the package-managed / system-row / curated-capability-name / audience-anchor write gates (`security-plugin.ts:1690-1724`); the referential-integrity check (`engine.ts:5892`) and the tenant-audit mute (`engine.ts:3773`); and the static `readonly`/runtime-owned strip on **both** UPDATE paths (`engine.ts:11290`, `:11473`) and the INSERT path (`:10025`), not the single call site named earlier. This matches the platform's own documented posture — `content/docs/permissions/system-context.mdx`: "Elevation is total, and it is not granular" — and REST `/actions` / MCP `run_action` already carry the identical exposure, so this widens an existing bypass to a third dispatch path rather than introducing a new one.
12+
13+
Bounded on two sides: metadata-plane schema masking (`metadata-core/object-schema-fls.ts:228`) is a separate REST/GraphQL schema-serving dispatch path that this `ctx.api` surface (`find`/`insert`/`update`/`delete`/`count`/`aggregate`/`execute`) never calls into, so it is not reached here; and `plugin-sharing/rule-hooks.ts`'s insert/update `isSystem` materialisation skip was already retired by the maintainer's 2026-08-31 ruling on #13533 — a system write materialises sharing grants exactly as a user write does today, so nothing changes there either.
14+
15+
A repo-wide census (production + test, `examples/` and `apps/` included) found no existing caller of `ObjectRepository.execute()` — every hit in the tree was prose describing the shape, never an invocation — so no shipped write changes behaviour today: the widening is total in kind, empty in measured blast radius.

.claude/hooks/guard-governed-enqueue.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
# 1. "is this diff governed?" → `check-governed-merges.mjs --test --json`,
5252
# the very predicate a seat runs before flipping ready. Exit 3 = governed,
5353
# exit 0 = clear. Its register (`GOVERNED_SURFACES`) is repo-agnostic, so
54-
# the same call answers for all four governed repos.
54+
# the same call answers for every repo configured in `GOVERNED_REPOS`.
5555
# 2. "is it approved, pinned?" → `pinnedApprovalVerdict` +
5656
# `GOVERNED_APPROVERS` imported from `check-governed-queue-guard.mjs` —
5757
# the same function the queue build decides on, applied to the same review

.claude/skills/pm-dispatch/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ dev**:先试 SendMessage 复活,不可用才走接手协议(四条增量见 runb
720720
**ACCEPT 之后的路径分叉(动手之前先分,不是事后对照)。** 翻 ready / 挂 auto-merge / 入队前,
721721
先取一次 PR 的路径面(`get_files`,⛔ 不看报告自述)。governed 面统一定义(2026-08-18 裁定):
722722
`docs/adr/**` + `.claude/**`(全量,含 agents/hooks/settings)+ `skills/**` + `AGENTS.md` +
723-
`CLAUDE.md`;agent 指令文件按此跨仓同判 —— objectui、cloud、objectos 一并在内(2026-08-18 裁定)
723+
`CLAUDE.md`;agent 指令文件按此跨仓同判 —— 仓集读 `GOVERNED_REPOS`,此处不列(2026-08-18 裁定)
724724
(objectos 指令面 PR 照样 draft/人工合并)。路径面**一条命中** ⇒ ACCEPT 换终局四件套:
725725
① 复核结论照常写在 issue
726726
上(不能合 ≠ 不复核;技能面 PR 的复核席须跑在契约复审档位,档位单源见条款②闸门);② PR

.github/workflows/lint.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2972,6 +2972,42 @@ jobs:
29722972
node scripts/check-merge-queue-triage-outcome.mjs --self-test
29732973
node scripts/check-merge-queue-triage-outcome.mjs
29742974
2975+
# Merged-branch reaper classifier contract (#13503). The third harness of
2976+
# this shape, and the one with the sharpest consequence: the classifier in
2977+
# merged-branch-reaper.yml decides which branches a deletion pass would
2978+
# take, and the maintainer's ruling of 2026-09-03 turns its dry-run list
2979+
# into an action a human releases. The workflow's own token grant
2980+
# (`contents: read`) stops it deleting anything; nothing stopped the LIST
2981+
# from being wrong.
2982+
#
2983+
# The ruling added a base-ref guard — `merged_at` says a PR landed, not
2984+
# WHERE, and 48 of the 575 merged `copilot/*` branches in the #13503
2985+
# census merged into another branch, one of them (based on a PR that
2986+
# closed unmerged) with no confirmed path to `main` at all. Read alone,
2987+
# the classifier's diff looks the same whether an edit narrows what gets
2988+
# reaped or widens it, and a dry run over a real repository prints a list,
2989+
# which is not a contract. So the contract is asserted: the shipped script
2990+
# is extracted from the YAML with a real parser (never retyped) and run
2991+
# under doubles the way actions/github-script runs it, as one
2992+
# AsyncFunction body, holding `reaped ⇒ that branch has a merged PR based
2993+
# on main` over every scenario.
2994+
#
2995+
# Its --self-test deletes the guard, inverts it, renames the base it
2996+
# compares against, flips its fail-closed direction, and unrecords and
2997+
# unrenders its bucket — and requires the battery to redden for each,
2998+
# naming the scenario that catches it. It also mutates the decisions the
2999+
# guard sits beside (open-PR precedence, the grace window, the defensive
3000+
# head-ref filter, the protected short-circuit), because a guard that cost
3001+
# something already there is not a narrowing.
3002+
#
3003+
# Invoked as `node` rather than through a `pnpm check:*` alias, same
3004+
# reason as the two steps above: see the GATE INVOCATION IDIOM note at the
3005+
# top of this file.
3006+
- name: Merged-branch reaper classifier contract
3007+
run: |
3008+
node scripts/check-merged-branch-reaper-outcome.mjs --self-test
3009+
node scripts/check-merged-branch-reaper-outcome.mjs
3010+
29753011
# Shard positive-attestation gate (#6082). ci.yml's two aggregate gates
29763012
# used to decide from one `needs.<matrix>.result` word, which cannot carry
29773013
# three shards' verdicts: run 31120902911 read the undocumented

0 commit comments

Comments
 (0)