Skip to content

[WIP] Create a new release version - #9

Closed
huangyiirene with Copilot wants to merge 1 commit into
mainfrom
copilot/create-new-release-version
Closed

huangyiirene with Copilot wants to merge 1 commit into
mainfrom
copilot/create-new-release-version

Conversation

Copilot AI commented Jan 18, 2026

Copy link
Copy Markdown
Contributor

Thanks for asking me to work on this. I will get started on it and keep this PR's description up to date as I form a plan and make progress.

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.

Copilot AI requested a review from huangyiirene January 18, 2026 14:59
Copilot stopped work on behalf of huangyiirene due to an error January 18, 2026 14:59
os-zhuang added a commit that referenced this pull request Jun 15, 2026
…ure-function pattern, #1870) (#1926)

Long-term design for flow functions (per the templates evaluation): a flow
`function` is a PURE compute step — inputs → return value — and ALL data I/O
stays declarative on the flow graph. This keeps the data layer visible, governed
(RLS/tenancy/transactions via the engine's data path), and build-checkable
(create/update_record nodes the build already validates) — rather than giving
functions a raw data API that hides writes and bypasses governance. (Data-
lifecycle side effects remain L2 hooks, which legitimately get ctx.api.)

Two enablers:
- `config.outputVariable` exposes the function's return value as a flow variable
  → a later update_record persists it (`fields: { ai_category: '{ai.ai_category}' }`).
- `config.inputs` are now interpolated against live flow variables, so a function
  can consume a prior node's output (`inputs: { ticketId: '{record.id}' }`).

Skill: automation pitfall #9 now teaches the pure-function pattern (function
returns → outputVariable → update_record; hooks for data side effects).
+1 end-to-end test (compute → outputVariable → downstream node interpolates it);
service-automation 202; check:skill-docs passes.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
xuyushun441-sys added a commit that referenced this pull request Jun 23, 2026
…mation) (#2240)

- spec: FieldSchema.requiredPermissions: string[] (passes through Field.* builders).
- plugin-security: a field whose requiredPermissions aren't all held by the caller's
  systemPermissions is masked on read and denied on write (AND-gate, strictest-wins
  over permission-set field grants). getObjectSecurityMeta now reads per-field
  requiredPermissions; foldFieldRequiredPermissions folds unmet ones into the
  FieldMasker map as {readable:false,editable:false}, reusing maskResults +
  detectForbiddenWrites — no masker signature change.
- liveness: classify field.requiredPermissions (live). api-surface unchanged.
- Tests: spec field (+3) + middleware read-mask/write-deny (+4).

D5 (package-seeded admin-maintainable per-object secure defaults) needs no new
code: stack.permissions already seeds sys_permission_set (incl. per-object +
per-field grants), admin-editable in Setup — it composes with D2 (private) + D1
(capability seeding). cloud sys_license is the worked example. Delegated admin (#9)
remains future per the ADR.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jun 30, 2026
Prime Directive #9 only mandated the `OS_` prefix; the *shape* was
undocumented, so new vars drifted (e.g. a bare `OS_PINYIN_SEARCH` for a
boolean flag). Codify the structure observed across the existing vars:

- `OS_{DOMAIN}_{FEATURE}[_QUALIFIER]` — domain-grouped (AUTH/CORS/SSO/…)
- boolean flags → `_ENABLED` suffix (OS_SSO_ENABLED, OS_SEARCH_PINYIN_ENABLED)
- config values → `OS_{DOMAIN}_{NAME}` (OS_CLOUD_URL, OS_LOG_LEVEL)
- escape hatches → `OS_ALLOW_{X}`; opt-outs → `OS_SKIP_/OS_DISABLE_`
- test-only → `OS_TEST_*`/`OS_EXPECT_*`

Pre-existing non-conforming vars (OS_METADATA_WRITABLE, OS_EAGER_SCHEMAS,
OS_SERVER_TIMING) flagged as debt, not precedent.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…fuse to load under NODE_ENV=production (#4093) (#4126)

#4058 step 1 classified plugin-dev's fakes honestly and step 2 made the
dispatcher gate on that classification. Neither is enough for three of them,
for the same reason honest labelling was not enough for the analytics shim
(#3891): the label was accurate while the behaviour still inverted the decision
it stood in for.

    security.permissions → checkObjectPermission() { return true; }   // allow-all
    security.rls         → compileFilter()        { return null; }    // no predicate
    security.fieldMasker → maskResults(r)         { return r; }       // unmasked

ADR-0076 D12's own line, learned from #3891, is that a fallback may degrade
features but NEVER security semantics — and the shim it retired merely dropped
the caller's RLS scoping, where these answered "allowed" outright.
`spec/src/contracts/security-service.ts` states it from the other side: these
three are plugin-security's implementation internals, and access-narrowing
answers must fail CLOSED. A fake registered under those names by a different
package is precisely the opposite.

Triggering it required nothing exotic: plugin-dev loads SecurityPlugin through
the same optional dynamic import as everything else, so `@objectstack/
plugin-security` merely not being installed swapped real RBAC/RLS/masking for
allow-all behind one `warn` line.

The slots now stay empty — what production has without SecurityPlugin, and what
the consumers already handle: enforcement lives inside the plugin's own
registered hooks, and the only reader of a slot (plugin-hono-server's
`/auth/me/permissions` and `/me/apps`) resolves it defensively and fails open on
presentation only, over data the read path already enforced. The boot log now
states plainly that RBAC, RLS and field masking are unenforced.

Second half, from the same finding: plugin-dev now REFUSES to initialize under
`NODE_ENV=production`. It is a published package that registers fakes for every
unclaimed core slot, including ones that report success for work they never
did, and it had no environment check of its own. Failing the boot is the right
response: such a process is misconfigured in a way no runtime behaviour can
make safe. `OS_ALLOW_DEV_PLUGIN=1` (the `OS_ALLOW_{X}` escape-hatch shape,
Prime Directive #9) overrides it for the deliberate cases.

Also: `/auth/me/permissions` resolves `security.permissions` and `metadata`
through the same guarded lookup its three sibling lookups already used — an
unregistered slot previously landed in the outer catch and was logged as
"/auth/me/permissions failed" on every console navigation instead of taking
the deliberate `!evaluator` branch.

Out of scope, filed as #4113 and blocking the `auth` half of #4093's A tier:
the dispatcher's `/auth` domain carries its OWN mock fallback which answers any
email + any password with 200 and a fabricated 24h session token, lives in
packages/runtime rather than an opt-in dev plugin, and gates on nothing but "no
auth service registered".

Verified: plugin-dev 12, plugin-hono-server 132, plugin-security 677, runtime
923, objectql 1183 pass; build 71/71; eslint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 30, 2026
… the most rounds (#4152)

Both written from one branch's lifetime, so every row is something actually hit
rather than something that might happen.

#9 — refresh a long-lived worktree's build state after merging main. Four stale
artefacts each fail AS IF your change broke something, naming other people's
exports, other packages' files, or config you never touched:

  packages/spec/dist          check:api-surface reports OTHER people's exports as
                              "N breaking"; check:i18n-coverage rejects an example
                              config for a value the spec allows
  node_modules                a package cannot resolve a dependency it declares
  runtime/.objectstack/       fixture rows accumulating across runs
  .cache/objectui-*           dozens of lint errors in files never opened

None is CI-visible — CI checks out fresh and installs clean — so the whole cost
lands on whoever is debugging, which is exactly why it is worth recognising in one
step instead of re-diagnosing per gate. Also records that OS_SKIP_DTS=1 leaves no
.d.ts, so gen:api-surface cannot run under it at all.

#10 — a clean merge is not a working merge. Git conflicts on overlapping lines and
says nothing when two changes are individually fine and jointly wrong. Both
examples are recent: a test pinning a response body's exact shape landed while
that shape was being changed elsewhere (merged clean, failed CI), and a domain
file was deleted while another agent's guard still declared it (caught only
because the guard existed). Hence pull main and re-run before opening a PR, and
again before merging.


Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…board it names (objectstack-ai#18223)

Part of objectstack-ai#17512

⚠️ Deliberately `Part of`, and the dispatch asked for a closing keyword.
One half of the card is undelivered and is the seat's to rule on — see
**The half this does NOT deliver**. A merge that closed the card here
would close it while nothing sweeps the surfaces the card is about. One
word flips it back if the seat disagrees.

Clause-②: no
Reason: this is a gate ADDITION and widens no acceptance set. The diff
is one new `scripts/check-issue-citations.mjs` plus one new
root-manifest `check:*` key; no existing gate's rule, threshold, ledger,
exemption list or population is touched, and nothing that used to be
refused is now admitted.

## What this adds

`scripts/check-issue-citations.mjs` — extracts `#N` citations from a
declared surface set and resolves each against this repository's board.

| mode | what it does | verdict |
|---|---|---|
| default (diff-scoped) | judges only the citations **this change
adds**, against the merge base | exit 2 on a finding |
| `--census` | the whole declared surface | report-only, always exit 0 |
| `--probe-cause` | adds the web-endpoint probe that separates
transferred from deleted | — |
| `--list` | extraction only, no network | — |
| `--self-test` | offline; 56 cases over 6 batteries with a per-battery
floor | exit 1 on a breach |
| — | the board could not be read | exit 3, never a quiet pass |

## Re-measured on today's tree, before anything was written

The card was measured 2026-09-10. Re-measured 2026-09-14 against
`2d3d1c969`:

| number | 2026-09-10 | 2026-09-14 | carrier today |
|---|---|---|---|
| 16785 | 404 | **404** | the `dataset-compiler.ts` scope note is GONE;
the number now survives only in a changeset that already records it as
resolving to nothing |
| 16685 | 404 | **404** | still cited from
`content/docs/releases/v17/17-4.mdx:408` (a published page),
`measure-result-type.ts:115` and `aggregate-field-type-compatibility.ts`
|
| 14313 · 14832 · 14366 | 404 | **404** | unchanged |

Controls, same call, same token: 16099, 16737, 17444, 17560, 17242,
15809 all **200**.

⭐ **The card's own control has partly decayed, and that is the sharpest
reading here.** The card cited 16783 / 16784 / 16786 / 16787 — the
immediate neighbours of 16785 — as all resolving, proving a hole in a
dense sequence. Today only **16784** answers 200; **16783, 16786 and
16787 answer 404**. Three numbers stopped resolving in four days, with
no change to this repository.

## The surface is ~500× the five instances

Enumerating the whole board (159 cursor pages) and judging every
citation offline against it:

```
allocation frontier   18,219      the highest number ever minted here
resolvable            15,834
holes                  2,385      13.1% of every number this repo ever minted
```

and through this gate's own `--census --json`:

```
33,750  citations judged across 2,357 files
28,359    resolve
 1,410    resolve as a PULL REQUEST, not an issue
 1,196    cross-repo — UNJUDGED, never a finding
 2,785    UNRESOLVABLE: 103 sites / 71 distinct on the release pages
                        2,682 sites / 439 distinct in package docblocks
```

A second instrument written before the gate existed (a scratch scan with
the same projection) answered 2,782 on the same tree minutes earlier;
the two agree to within the three citations the frontier moved by while
they ran.

**Those two readings together decide the gate's shape.** A tree-wide
blocking verdict is refused for two reasons, not one: 2,785 findings is
the permanently-red gate this repo retired, and — worse — the predicate
is **not a function of this tree**. 16783 / 16786 / 16787 prove a still
tree goes red because somebody else deleted an issue. So the default
verdict is **diff-scoped**: the half of the class an author owns, which
cannot red on a still tree and which stops the debt growing. The 2,785
standing sites are a `--census` reading, not a verdict.

## The four 404 causes, kept apart

⛔ The card's hardest constraint. Each arm has its own decision
procedure, and none of them is a guess.

| cause | how it is decided | measured live? |
|---|---|---|
| `cross-repo-unjudged` | the citation NAMES another repo
(`owner/repo#N` or `repo#N`) — never resolved, ⛔ never a finding | ✅
`objectui#4356` stayed silent in the live ablation |
| `never-issued` | the number is beyond the allocation frontier | ✅
`objectstack-ai#18888` against frontier 18221 |
| `transferred` | the web endpoint still redirects out of this repo (the
API does not keep that redirect) | ⛔ **NOT MEASURED** — no positive
specimen exists on this board; exercised only by the self-test stub |
| `deleted` | minted, absent from the board, and the web endpoint 404s
too — named as the residual class | ✅ 16785 under `--probe-cause` |

Without `--probe-cause` the last two do not separate and the finding
carries `allocated-but-absent`, which is a refusal to guess rather than
a third cause. Every unresolvable instance re-probed on 2026-09-14
(16785, 16783, 14366, plus twelve sampled at random from the census)
answered 404 on **both** endpoints, so no citation measured here was
transferred — and this gate's silence is ⛔ not evidence that none ever
was.

## Both directions, measured live on this branch

The ablation ran against the live board, on the committed
implementation, with the mutation proven on disk before the reading was
taken and the restore proven by blob hash afterwards.

```
leg 0  tree clean at HEAD, diff-scoped run           exit 0   "no issue citations added"
mutate one docblock line in a declared surface       blob f318190 -> bd2007413 (marker count 0 -> 1)
leg 1  diff-scoped run with --probe-cause            exit 2   1 deleted, 1 never-issued, 1 cross-repo-unjudged
       [deleted]      ...aggregate-field-type-compatibility.ts:8  objectstack-ai#16785
       [never-issued] ...aggregate-field-type-compatibility.ts:8  objectstack-ai#18888
leg 2  git checkout HEAD -- THE_PATH                   blob restored to f318190, git diff HEAD empty
       diff-scoped run                               exit 0   "no issue citations added"
```

⛔ A first attempt at this ablation wrote nothing to disk (a `perl -i`
invocation that swallowed its argument and still exited 0). The on-disk
marker count caught it and the run was declared void rather than
re-rolled quietly; the reading above is from the run that landed.

## The scope contract, written down

The card rules that the hard part is scope, so it is a table with a
reason per row rather than a habit.

**Declared** — start where the damage is measured:

- `content/docs/releases/**/*.mdx` — the published release pages;
16685's carrier.
- `packages/**/src/**/*.ts` and `.tsx`, **comment prose only** (through
`scripts/symbol-anchors.mjs#commentProse`, so a gate's own fixtures and
string literals are blanked and line numbers survive) — 16785's carrier.

**Deferred, and the table is applied as a hard EXCLUSION rather than
kept as prose** (the first draft declared it and swept the files anyway
— 5,102 sites instead of 2,785 — which is why `surfaceFor` now checks
the deferred globs first):

- `**/CHANGELOG.md` — 27,978 citations, generated release prose, and the
surface the card means by 「会被 changelog 引用淹没」.
- `scripts/**` — objectstack-ai#15809's lane. ⛔ Not folded; the card is explicit that
folding the siblings fails the fix.
- `docs/adr/**` — a governed surface with its own anchor corpus; a
citation finding there would force a governed-surface PR over a number
somebody else deleted.
- `.changeset/**` — consumed and deleted at release.
- `packages/**/*.test.ts` and siblings — the next candidate widening,
held back so the first installation is judged on the surfaces the card
measured damage on.

Three grammar narrowings, each measured rather than assumed: a two-digit
floor (all 133 one-digit `#N` tokens in the declared surfaces are
ordinals — `Prime Directive objectstack-ai#9`, `acceptance objectstack-ai#5` — and none names an
issue), a six-digit ceiling (zero such tokens exist, which keeps hex
colours out), and `NON_CITATION_HEADS` for numbering systems that are
not the board's (`Directive objectstack-ai#14`, `batch objectstack-ai#127`, `re-charter objectstack-ai#26` — 405
sites).

## The half this does NOT deliver — the seat's ruling, not an oversight

⛔ **No workflow invokes this file.** `.github/workflows/**` was out of
the dispatch's file surface, and a census of the root manifest's 160
`check:*` keys found **every one of them named by a workflow**, directly
or through its alias — there is no precedent here for a gate that CI
does not run. The manifest carries `check:issue-citations`, which runs
the `--self-test` and nothing else (the `check:pm-half-states` shape,
for the same reason: the live modes need a board and a credential).

So today this is a tool a seat runs, and
`scripts/pm/check-half-states.mjs` states what that is worth: 「an alarm
added to a script nobody runs is still silence」. The two homes it wants
are in **different lanes**, which is the substance of the ruling being
asked for:

1. **the diff-scoped verdict** belongs in `lint.yml`'s `Lint & Repo
Gates` job — per PR, already holds a `GITHUB_TOKEN`, and the verdict is
a fact about the diff;
2. **`--census`** belongs in the patrol lane (`half-state-patrol.yml`) —
scheduled and report-only, which is the only honest posture for a
reading a third party can change between runs.

Neither is installed. A follow-up card owning `.github/workflows/**` can
install both; this branch cannot.

## Tests

- `node scripts/check-issue-citations.mjs --self-test` — 56 cases, 6
batteries, per-battery floor plus a roster-size floor and a verdict
handshake. Batteries: `grammar` (14), `causes` (12), `transport` (7),
`scope-contract` (12), `diff-scope` (6), `live-corpus` (3). The
`diff-scope` battery builds a throwaway git repository and asserts the
green direction and the red direction on the same tree; `causes`
provokes all four 404 arms including the two the live tree has no
specimen for; `transport` asserts the two board strategies answer
board-identically over one stubbed transport.
- `node scripts/pm/dispatch-gates.mjs --commands` → 36 families, all
run, **36 run / 0 NOT-MEASURED / 0 UNRUN** (`--ran` reconciled with exit
codes). Four exited 3 (PREREQUISITE NOT MET, no `dist/`), were re-run
after a full build under the shared verify lock, and all four then
exited 0.
- `pnpm lint` (full repo, `eslint . --no-inline-config`) — exit 0, no
narrowing to declare. Run on `061ab1ac1`.
- `pnpm build` under `scripts/pm/os-verify-lock.sh` — 73/73 tasks
successful.

## changeset: none, measured

No publishable package's `files[]` can ship `scripts/` (0 of 70
publishable manifests carry an entry that could), and the root manifest
is `private: true`. Negative control: the new file's `CITATION_SURFACES`
appears in 0 built `dist/` trees. Positive control, same instrument:
`AGGREGATE_FIELD_TYPE_COMPATIBILITY` appears in 10. Nothing published
moves, so `skip-changeset`.

## Acceptance notes

Noted, not filed — out of scope here, and each names who would meet it:

- **The `#N` citation debt is a 2,785-site class, not a five-instance
one**, and this branch repairs none of it. The carriers of 16685 that
are still live (a published release page and two `packages/spec` /
`service-analytics` docblocks) are deliberately untouched: the
disposition pattern the card records is to strike or annotate in place
with the reason, never to guess a replacement number, and picking three
sites out of 2,785 by hand is arbitrary. Successor: whoever the seat
gives the wiring ruling to, since the census is the input to that
decision.
- **The `/issues` list endpoint refuses `page` beyond a depth** on this
repository ("Pagination with the page parameter is not supported for
large datasets") and GitHub's own `Link` header answers in the
`repositories/{id}/...` form, which this session's proxy refuses. Both
are handled here (`normalizeNextUrl`), and any future board enumerator
in this tree hits the same two walls. Successor: none today — no other
script in this tree enumerates the whole board.
- **`--census` costs 159 requests and grows with the board.** Fine on a
schedule, wrong per PR; that asymmetry is the reason the two lanes above
are named separately.

---
_Generated by [Claude
Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
os-support-ai pushed a commit that referenced this pull request Sep 18, 2026
…espace

`check:runner-env-posture` reddened on all three code positions of
`VITEST_PROJECT_CLI_OVERRIDES`. Reproduced at the PR head before touching
anything: exit 1, the same three lines.

The gate is RIGHT and this is not a false positive worth an exemption.
None of the three sites is an environment read — they are a re-export, an
`export const` and a `.includes()` call, and nothing in this package's
`src/` reads `process.env` at all. But the gate's first pass is a bare
IDENTIFIER scan over comment- and literal-masked source, deliberately: a
destructured read carries no `env.` prefix at its use site, so requiring
one would reopen the hole the gate exists to close. SCREAMING_SNAKE plus
`VITEST_` is the shape of a runner variable, so the gate could not tell
the constant from one — and neither can a human reader.

So the name was the defect. Renamed to `PROJECT_CLI_OVERRIDES`, which is
vitest's own local name for the array (`cliOverrides` in `resolveProjects`)
and therefore the truer transcription.

⛔ Not an allowlist entry — the gate forbids that spelling by name.
⛔ Not a gate edit: narrowing the matcher to `env.`-prefixed access is a
   regression for the whole repo, and exempting a `private: true` package's
   `src/` buys an exemption for a problem that is not an env read.
⛔ Not `OS_`-prefixed either: Prime Directive #9 reserves `OS_` for this
   repo's own environment variables, so that trades one env-var-shaped name
   for another in a namespace we own.

Measured before renaming, with a LIT control: vitest 4.1.11 names
`VITEST_PROJECT_CLI_OVERRIDES` nowhere in its distribution (0 hits, while
the same scan finds `VITEST_WORKER_ID`, `VITEST_POOL_ID`, `VITEST_MODE` and
nine more), so no mechanism depended on the name. The reason is recorded
next to the constant so the prefix is not helpfully re-added.

Also corrects an over-citation of #17978 in this file: that card is a
finding card and does not carry the one-copy reasoning in its text, so the
argument now cites the two headers in this package that do.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…and refuses a foreign one (objectstack-ai#19288)

Fixes objectstack-ai#19191

Clause-②: no

`scripts/pm/check-half-states.mjs` took three readings off the LOCAL
checkout with plain
`execFileSync` git calls and no `cwd`, so each inherited `process.cwd()`
while the board it reports on
is named by `PM_SWEEP_REPO`. The filer measured both directions on the
same board with the same script
(objectstack-ai#19191, comment 5740947564): with `cwd` in objectstack, H17's
trigger-file index validated objectui's
candidate paths against **objectstack's 8888** tracked files (objectui
had **7945**) and H57 read
NOTHING for the whole run — and the row it never read was hiding a
scheduled lane dead through five
consecutive fires (objectstack-ai/objectui#10010). Internally
consistent, externally wrong.

## 维护者速读(草稿)

**改了什么** —— 给这个巡检工具加了一个环境变量 `PM_SWEEP_CHECKOUT`,用来回答「哪个本地检出服务于这块
board」。设了它,三处本地 git 读取(H17 的 tracked-file 判据、H57 的 workflow
文件、`origin`)就都在那棵
树上读;那棵树的 `origin` 不是被扫的仓时,整轮巡检响亮拒绝(退出码 3),⛔ 绝不静默换一棵树读。不设它,
行为与今天逐字一致 —— 只多了一句:H17 页脚现在写明它到底在哪棵树上读的(路径 + `origin`)。

**为什么改** —— 这不是「跑错目录」的操作失误,而是一个没有症状的读数缺陷。跨仓巡检是常态(工具住在
objectstack,board 可以是 objectui),而 H17 的页脚正是在告诉派发席位「派发前请拿你的文件面和这张清单求
交集」。清单是拿另一个仓的文件列表验过的,只在另一个仓存在的触发文件被静默丢弃 —— 席位读到的是一份干净
清单,而那份干净是假的。H57 那半反而是诚实的:它明说自己什么都没读,代价是那一轮真有红行没人看见 ——
本 PR 的实测里那是两行,其中一行连卡都没提到。

**风险与代价(含回滚)** —— 风险面很窄:变量不设时是逐字旧行为(自测里有一条专门盯这件事),巡检
workflow 本身不设它,所以线上那条 lane 的行为不变。新增的唯一失败模式是「变量设错路径」,而那正好是本卡
要的那个响亮拒绝。回滚 = revert 这两个 commit,没有数据迁移、没有已发布面(`scripts/pm/**` 不随任何包
发布,故无 changeset)。

**席位意见** ——

**你要做的** —— 无需维护者动手。若希望巡检 workflow 把这棵树写明(目前不需要,因为 runner 的检出就是被扫
的仓),那是 `.github/workflows/half-state-patrol.yml` 的一行 env,已写在下面的
Acceptance notes 里,留给
单独一个由人合的改动 —— 本 PR ⛔ 不碰 workflow(objectstack-ai#19259、objectstack-ai#19225 正在改它)。

## What changed, mechanically

- **`PM_SWEEP_CHECKOUT`** — one knob, beside `PM_SWEEP_REPO` and
`PM_SWEEP_CLOSED_FLOOR` in `--help`,
carrying the PATH of the checkout that serves the board.
`resolveSweepCheckout(env)` resolves and
  trims it; whitespace is unset.
- **One git read site.** All three readers (`readTrackedFiles`,
`readRepoRoot`, `readOriginUrl`) now go
through a single `gitRead(args, extra)` helper that passes that path as
`cwd`. That is the only
`execFileSync` git call left in the file, and the self-test pins the
count at one — so a fourth
reader cannot be added later without the `cwd`. That is the card's
mechanism assumption turned into a
  measured property instead of a belief.
- **The refusal.** `checkoutPrerequisite(sweepRepo, env, originUrl)` is
a pure verdict in the shape
`reportPrerequisiteNotMet` prints: `null` when the knob is unset
(today's behaviour is not a
prerequisite) or when the named tree really serves the board, otherwise
the file's own exit-3
PREREQUISITE NOT MET, named. It is answered FIRST in `sweep()` — ahead
of the transport probe — so a
  foreign checkout costs zero requests and reads nothing.
- **`localCheckoutServes` resolves the same way**: the knob leg first
(it names the tree, so the
`origin` read IN that tree is the authority), then `GITHUB_REPOSITORY`
(which names the RUNNER's
tree, i.e. exactly not the knob's), then the checkout's `origin`. H57
therefore judges when the knob
  names the right tree, instead of refusing for the whole run.
- **The H17 footer names the tree** it read — path plus `origin` —
beside its oracle size, in BOTH
oracle states (read, and EMPTY BY FAILURE), knob set or not. A
wrong-tree reading is internally
consistent; the tree it names is the only thing that tells it from a
right one.

## Readings

| reading | before | after |
|:--|--:|--:|
| `scripts/pm/check-half-states.mjs` lines | 36,176 | 36,256 — net
**+80**, budget +80 |
| `--self-test` cases | 5,063 | 5,081 (+18), exit 0 |
| battery roster | 6 batteries, each above its pin | unchanged, each
above its pin |

Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at this
branch → 39 runnable commands, all run locally with the exit code
captured BEFORE any pipe, all 0. The
per-command verdicts are in this card's `os-dev-report` comment.
Control-character self-scan over the
changed file: no match. No package contains `scripts/pm/**`, so no
dependency-closure build and no
package test suite is owed; repo-wide scans stay CI's.

## The firing pair, live

Both legs ran at this branch's head against the objectui board, from the
objectstack tool.

**Knob = the tree that serves the board** —
`PM_SWEEP_REPO=objectstack-ai/objectui` with
`PM_SWEEP_CHECKOUT=/home/user/objectui`, detached under the shared
heavy-verify lock (held 398s, waited
0s — shared-box seconds, not idle-box figures): **exit 0**, 502
half-states over 374 open
pm-/p0-labeled issues. The H17 footer:

```text
(read on 75 of 75 open `pm:on-hold` card(s); 8142 tracked file(s) in the oracle.) Read in `/home/user/objectui` (`origin` `https://github.com/objectstack-ai/objectui`).
```

8142 is objectui's LIVE tracked count today (`git -C /home/user/objectui
ls-files | wc -l` = 8142); the
filer read 7945 on 2026-09-19 and the board has moved since. This
worktree reads 9031, so the two trees
are still 889 apart — the number that never used to appear is now the
one printed, with the tree it came
from beside it.

H57 is judged rather than UNRESOLVED: `10 workflow(s) on the swept repo
declare a schedule; 8 were
judged against their latest event=schedule run and 0 are UNJUDGED rather
than clean because that read
failed.` It filed **two** red rows, neither reachable by any wrong-cwd
sweep:

- `.github/workflows/changeset-release.yml` — latest scheduled run
`35493590744`, started
2026-09-20T06:11:39Z, concluded `failure`. This is the lane the card's
re-run traced to
  objectstack-ai/objectui#10010, still dead today.
- `.github/workflows/check-links.yml` — latest scheduled run
`35489310168`, started
2026-09-20T04:30:22Z, concluded `failure`. Not named anywhere in the
card.

**Knob = a foreign tree** — same board,
`PM_SWEEP_CHECKOUT=/home/user/objectstack`: **exit 3**, nothing
swept, nothing spent:

```text
check-half-states: PREREQUISITE NOT MET — PM_SWEEP_CHECKOUT="/home/user/objectstack" does not serve `objectstack-ai/objectui`

  this checkout's `origin` is `objectstack-ai/objectstack` while the sweep reads `objectstack-ai/objectui`.

  H17 validates every on-hold trigger path against that checkout and H57 classifies its workflow
  files there, so a foreign tree renders an index whose paths were checked against another repo.

  Fix:  point PM_SWEEP_CHECKOUT at the checkout whose `origin` IS the swept board,
        or unset it and run the sweep from inside that checkout.
```

## Reverse verification — both new pins can fail

Each ran from the COMMITTED state, mutated through
`scripts/ablation-replace.mjs` (which proves the
write landed on disk and restores byte-exactly), and each turned exactly
ONE case red out of 5,081.
The committed blob is `f49d566ed276`.

| ablation | mutation landed | case that went red | restore |
|:--|:--|:--|:--|
| strip the `cwd` from the one git-read site | anchor 1 to 0, blob
`f49d566ed276` to `994882b2fcfc` | `objectstack-ai#19191 knob: …and that one site
takes the knob as its cwd` — got false, want true | blob back to
`f49d566ed276` == HEAD, `git diff HEAD` empty |
| re-spell `readRepoRoot` with its own `execFileSync` git call — a
fourth unrouted reader | anchor 1 to 0, blob `f49d566ed276` to
`ecd24ab63593` | `objectstack-ai#19191 knob: ONE git read site in the file, so a
fourth reader cannot skip the cwd` — got 2, want 1 | same |

The first is the one that mattered most: before that pin existed,
stripping the `cwd` left the whole
5,080-case suite green, i.e. the fix's central wiring had no test that
could fail. The second is what
makes the card's "the three readers are the ONLY local-tree reads" a
property the suite enforces.

## Acceptance notes

- **Knob name**: `PM_SWEEP_CHECKOUT`. It joins the established 12-member
`PM_*` PM-tooling family
(`PM_SWEEP_REPO`, `PM_SWEEP_CLOSED_FLOOR`, `PM_STATE_LABELS`,
`PM_TOKEN`, …) rather than the
product's `OS_{DOMAIN}_{NAME}` runtime family of Prime Directive objectstack-ai#9 —
the card asked for a knob
"beside `PM_SWEEP_REPO`", and `--help` groups the three under one
heading where a reader looks.
Flagged because that directive's wording is categorical; a maintainer
who prefers
  `OS_PM_SWEEP_CHECKOUT` gets it for the price of one rename.
- **The patrol workflow needs no env line, and this PR writes none.** On
every real patrol fire the
runner's own checkout IS the swept board (`PM_SWEEP_REPO: ${{
github.repository }}`, per-repo
installs), so the knob would be a no-op there and `GITHUB_REPOSITORY`
already answers
`localCheckoutServes`. If it should be stated explicitly anyway, the
exact line for the sweep step's
  `env:` block in `.github/workflows/half-state-patrol.yml` is
`PM_SWEEP_CHECKOUT: ${{ github.workspace }}` — a separate, human-merged
change, since objectstack-ai#19259 and
  objectstack-ai#19225 are open on that file.
- **`origin/main` was NOT merged into this branch.** It moved to
`e6a03e6` while this ran and touched
`scripts/pm/check-half-states.mjs` in none of those commits (verified
against a fetch into a ref this
worktree owns, not against the shared `origin/main` pointer), so the
line budget's 36,176 baseline
still holds and no serial writer conflict exists. The merge queue
rebuilds the PR onto current `main`
and re-runs the required contexts there, which is where a jointly-wrong
merge would surface.
- **Noted, not filed**: `sweep()` and `sweepScheduledWorkflows` each
perform their own
`readOriginUrl()` / `readRepoRoot()` — two duplicate local git reads per
sweep (microseconds, zero
requests). Threading one reading through `sweepInto` would widen that
function's signature, which is
the kind of change this card was told to keep out of. Successor: whoever
lands objectstack-ai#19177 (H59) or objectstack-ai#19160
  (H52) is next on this writer.
- **Nothing else in the file was touched**: objectstack-ai#19230, objectstack-ai#19177, objectstack-ai#19160,
objectstack-ai#19203, objectstack-ai#19255 and objectstack-ai#19108 queue
  behind this card on the same writer.
- `scripts/pm/**` publishes nothing from any released package, so this
diff owes no changeset
  (`Clause-②: no`). No label writes.
- Tier S: this stops at the draft PR. The owning seat writes the `##
Contract review` record, reads
  `--pair`, and lands it.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ine, and every ref in one (objectstack-ai#19455)

Fixes objectstack-ai#19439

Clause-②: no

`check-half-states.mjs` read **one** target from a line stating several,
and for a directive whose remainder is prose it read whichever number
the ref walk reached first. Both readings under-report a block, and
H19's row is what the unlock scan's release act fires from.

## The two measured shapes, before and after (live bodies, read this
pass)

| shape | before | after |
|---|---|---|
| objectui#8347 body — NINE backticked `·`-separated directives on
**one** line | `directiveValues` 1 value, `blockedByTargets` **1**
target (`objectstack-ai#8345`) — H19 printed 「1 of 1 … CLOSED」 | 9 values, **9**
targets (`objectstack-ai#8345, objectstack-ai#8648 … objectstack-ai#8655`) |
| objectui#8093 body — **one** directive whose remainder is prose naming
three numbers | **1** target, `objectui#7434` — a possessive that was
never a blocker; both real blockers dropped | **3** targets,
`objectui#7434, objectstack-ai#8090, objectui#8065` — both real blockers read |
| DARK control — one directive | 1 target | 1 target |
| DARK control — prose naming `objectstack-ai#9` and `objectstack-ai#10` outside any directive | 0 |
0 |

⚠️ One correction to the reading this was dispatched on: the seat
measured 「nine directives on TWO lines → 2 targets」 from the card's
*quoted* (line-wrapped) rendering. objectui#8347's **live** body carries
all nine on ONE line, so the reading there is **1** target, not 2 —
which is exactly the 「1 of 1」 H19 published. The defect is one notch
worse than the claim stated.

## What lands

Two halves, in the two places they belong.
`scripts/pm/check-half-states.mjs` only, net **+79** lines (101 added /
22 removed), including 16 self-test cases.

1. **The shared decorated-directive reader (`directiveValues`) yields
EVERY directive on a line.** A line splits at every further key that
follows a **separator run** — decoration, spaces and one of `·•|,;、`,
and nothing else. ⛔ A key that follows a WORD is still prose, so the
anchoring contract that keeps 「seats park the `Blocked-by: objectstack-ai#1` line in
comments」 out of the index is unchanged. Every directive family reads
through this one reader — `Blocked-by`, `Restart-when` (H9),
`Unlock-action`, `Maintainer-action`, `Prior-rulings`, `Reviewed-by`,
`Seam` — so all of them gain the multiplicity, and the pin that they
invent nothing from prose is asserted on a NON-`Blocked-by:` key.
2. **The `Blocked-by:` walk takes EVERY ref in a directive's value**,
not the leading run.

## The prose-remainder decision, on the four axes

The card offered two readings for a directive whose remainder is prose:
every `#N` in the value is a target, or the value is reported UNJUDGED
with the line quoted. **Every `#N` is a target.**

- **实际业务需求** — measured, not assumed. The prose-remainder shape is live
on both boards (8 of the 116 blocked cards that carry a directive), and
in 4 of them the old reader extracted **zero** targets from a directive
a seat really wrote: objectstack#14570, objectstack#13458,
objectui#9273, objectui#5987. A stated block that the machine turns into
nothing is the same silence this item exists to end.
- **项目长远合理性** — one rule ("a directive's value names blockers; every
reference in it is one") instead of two ("the leading run, unless…").
The UNJUDGED reading needs a second verdict channel out of a parser that
returns `{repo, number}` refs; H19's `unresolved` state is produced by
the RESOLVER, so feeding it from the extractor means a new return shape
through `buildBlockingIndex`, `blockerTargetsFor` and
`blockerChannelKeys` — and the card ⛔ forbids changing H19's UNJUDGED
arm.
- **防 AI 写错** — decisive. The leading run was taken to keep a context
`#N` out of the index, and it does not buy that: on objectui#8093 it
filed the possessive as a dependent AND dropped both real blockers. So
it files a phantom either way; what it ADDS is **substitution**, and a
block whose real blockers are invisible reads as expired. Reading every
ref can only ADD a target, and an extra target can only WITHHOLD a
discharge (H19 counts it open, names it on the row, and says PARTIAL) —
⛔ never found a release. That is this file's standing posture on an
ambiguity (never resolve one in the direction of release), applied to
the value instead of to the resolution.
- **创业阶段不扩散** — no new surface, no new verdict, no new flag, no config.
The declared file surface only.

⚠️ **The price, measured rather than asserted**: a context `#N` in a
directive's prose becomes a target. On these boards that is 13 new
reverse-index edges (see below) and, where a comment narrates in a
line-initial `Blocked-by:` sentence, a row that says PARTIAL where it
used to say 「every target it names is closed」. The row still fires and
names the extra target, and — unlike the comment archive this file
refuses to found a verdict on — the line carrying it is rewritable by
the seat that wrote it.

## The census the card asks for as the first act

Population: **140** open `pm:blocked` cards on both boards (objectstack
68, objectui 72), **body AND comments** (140 comment sets fetched), read
2026-09-20T23:10Z. Both readers run over the same texts; the old one is
objectstack at `57ceb9d`.

- **116** of the 140 carry at least one `Blocked-by:` directive
(objectstack 62, objectui 54) — 167 directive values under the old
reader (89 + 78).
- **LINES carrying ≥2 directives: 3 — all on objectui, all on ONE card**
(objectstack-ai#8347: one body line, two comment lines). objectstack: **0**. ⇒ the
MULTIPLICITY shape is one card on two boards today.
- Texts whose extracted target set changes: **10**, on **9** cards
(objectstack 3, objectui 6). Nothing is lost on either board (−0 refs,
−0 index edges).
- Cards whose **JUDGED** set changes — H19's own count, body-first
carrier — **2 of 140**: objectui#8347 (1 → 9, the measured false-release
row) and objectui#9273 (0 → 1, a body-less card whose comment states its
blocker in prose). objectstack: **0 of 68**, because those narrating
lines are comment-borne beside body lines and the carrier rule keeps the
body authoritative.
- Reverse index (H14): objectstack 83 → 87 edges (+4, −0), objectui 30 →
39 (+9, −0). Of the 13 new edges, 8 name a CLOSED target (H14 judges
open cards, so no row), 1 names a card already carrying `pm:blocking`
(objectstack#13457), **3 are open `pm:blocked` cards that really are
objectstack-ai#8347's blockers** (objectui#8649, objectstack-ai#8652, objectstack-ai#8654 — a correct
missing-cache row that was invisible before), and 1 is
objectstack#11333, named as the parent in a triage audit comment — the
single edge whose blocker status is a judgement call.
- ⭐ **Lit control**: objectui#8347 is in the population and moves 1 → 9;
and 116 of 140 cards read as carrying a directive — a scan that read
nothing prints 0 for both. **Dark control**: prose naming `objectstack-ai#9`/`objectstack-ai#10`
outside a directive → 0 targets under BOTH readers; one directive → 1
under both.
- ⛔ **Radius, stated**: open `pm:blocked` cards only. objectui#8093 —
triage's substitution specimen — is **NOT** in this population: it
carries `pm:queue` today, released since that reading. So the
substitution class is under-counted here by construction, not absent.

## Verification

- `node scripts/pm/check-half-states.mjs --self-test` :: exit 0 — 5160
cases pass (16 added), every battery above its pin;
`SELF_TEST_BATTERY_FLOOR` untouched (8), no new battery opened.
- The derived union, taken at the final commit `da8c088`: `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths) → 42 commands, **42 run, every
one exit 0**, reconciled with `--ran` over a record carrying each exit
code: 「42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN … a DERIVED zero」.
`pnpm check:pm-dispatch-gates` took 962.5s on this box and is in that
42. Every exit code was captured before any pipe.
- New self-test cases: the two measured live shapes as fixtures (the
objectstack-ai#8347 line byte-identical to the live body), both dark controls, the
family pin on `Restart-when` plus its prose control, the URL-fragment
control, and the end-to-end count — a two-directive body line with one
closed and one open target reads 「1 of 2」, ⛔ never 「1 of 1」.
- The pin this overturns is rewritten in place, not deleted:
`Blocked-by: objectstack-ai#123 (see objectstack-ai#456 for the background)` now reads `123,456`,
with the reason beside it.

## 维护者速读(草稿)

- **改了什么**:巡检脚本读 `Blocked-by:` 的两处修正——一行上写了几条指令就读几条;一条指令的值里有几个 `#N`
就算几个阻塞目标。
- **为什么改**:H19 的行是「解锁放行」的触发器。它在 objectui#8347 上打印「1 of 1
已关闭」,而该卡九个阻塞里三个还开着、卡面自己写着「未裁决完不得派发」;另一张卡把句中的所有格 `objectui#7434's`
当成了阻塞方,两个真阻塞被丢掉。
- **风险与代价(含回滚)**:代价已实测——指令值里的「顺带提到的卡号」也会算作目标,两板共新增 13 条反向索引边(其中 3
条是本来就该有的真阻塞),H19 可能把「全部关闭」改口为「部分关闭」并点名多出来的那张卡。方向只会扣住放行、不会造成放行。回滚 =
revert 本 PR 的单文件改动。
- **席位意见**:(留空,待席位定稿)
- **你要做的**:确认「宁可扣住放行,也不接受读错后放行」这条取舍;若要改为「读不准就报 UNJUDGED」,那是另一张卡(需要改 H19
的第三态入口,本卡明令不动)。

## Acceptance notes

- `noted, not filed`: four of the changed texts are **narration** lines
in comments that begin with a decorated `Blocked-by:` key and then
describe a re-point in prose (「`Blocked-by:` re-pointed `objectstack-ai#14096` →
`objectstack-ai#15195`…」). The line-anchored reader counts them as directives — that
is pre-existing and deliberate (it models the literal grep the unlock
scan runs), and it is what makes their prose numbers reachable at all.
Narrowing it is a change to WHICH LINES are read, which this card ⛔ does
not authorise. Carrier: the next card that touches `directiveValues`'
anchoring.
- `noted, not filed`: a cross-repo qualifier separated from its `#` by
decoration (`objectui**objectstack-ai#7434**`) still reads as a LOCAL ref, before and
after — the ref grammar is unchanged in that respect. Carrier: none
today; no live specimen on either board in this census.

---
_Generated by [Claude
Code](https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…rence three exported constants (objectstack-ai#19478)

Part of objectstack-ai#18697 — construction round **R1 of 2 (phase 1 only)**. This
round does not close the card; R2 does.

Branch: `claude/issue-18697-version-grammar-canon-phase1`

Clause-②: yes (widening)
Reason: the criterion has two limbs and this round hits the second one.
The ACCEPT SET does not move — every regex written here is
byte-identical to the literal it replaces, verified per carrier by
sha256 over the extracted literal (G1 `0dc7272048754554`, G2
`f0503f4f0c703a40`, G3 `3e3cb2710d62909a`, each hash equal across its
whole group and equal to the constant), so nothing an author can write
changes. But the PUBLIC SURFACE grows: three new exported constants on
`@objectstack/spec/kernel`, landing as three rows in
`packages/spec/api-surface/kernel.json` at :157, :381 and :382. A new
exported symbol is `yes` on its own, and that second limb is what this
declares. The ruling's `Clause-②: yes` therefore already holds for R1;
R2 carries its own accept-set widening on top of it.

## What this does

Eight in-repo carriers of "the version of a package or plugin" each
spelled a version regex out as a literal of their own. Three accept sets
written eight times, still growing on their own: three of the eight were
published schema declarations with no parse caller at all. A ninth
carrier of the same concept, `PackageManifestSchema.version`, spelled no
regex at all — it is a bare `z.string()` and is deliberately left
unconstrained by this round. Each now references the constant carrying
the pattern it already enforced.

`@objectstack/spec/kernel` gains three exported patterns
(`packages/spec/src/kernel/version-grammar.ts`, a new file):

| constant | grammar |
|:--|:--|
| `MAJOR_MINOR_PATCH_VERSION_PATTERN` | three numeric segments and
nothing else |
| `SEMVER_SHAPED_VERSION_PATTERN` | plus an optional prerelease and an
optional build suffix, identifiers in either ASCII case |
| `SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN` | the same, suffix
identifiers restricted to lowercase ASCII |

10 carriers → 3 referenced declarations + 1 deliberately unconstrained.

## Per-carrier before → after, with the byte-identity assertion

Every "before" literal was extracted from the file BY LINE WITH A
REQUIRED MARKER on that line (a moved line throws rather than reading
the wrong thing) and hashed. Within each group every hash is equal, and
the constant's own literal hashes to the same value.

**G1 — `MAJOR_MINOR_PATCH_VERSION_PATTERN`, sha256/16
`0dc7272048754554`, 17 bytes**

```
/^\d+\.\d+\.\d+$/
```

| carrier | line at `fbc12be3` | line on this head | after |
|:--|:--|:--|:--|
| `packages/spec/src/kernel/manifest.zod.ts` `ManifestSchema.version` |
414 | **415** | `z.string().regex(MAJOR_MINOR_PATCH_VERSION_PATTERN)` |
| `packages/spec/src/kernel/metadata-plugin.zod.ts`
`MetadataPluginManifestSchema.version` | 649 | **650** | same |
| `packages/spec/src/kernel/plugin-registry.zod.ts`
`PluginRegistryEntrySchema.version` | 158 | **159** | same |
| `packages/spec/src/kernel/plugin-validator.zod.ts`
`PluginMetadataSchema.version` | 144 | **145** | same |
| `packages/runtime/src/domains/packages.ts` `PATCH
/api/v1/packages/:id` | 1680 | **1770** |
`!MAJOR_MINOR_PATCH_VERSION_PATTERN.test(patch.version)` |

**G2 — `SEMVER_SHAPED_VERSION_PATTERN`, sha256/16 `f0503f4f0c703a40`, 54
bytes**

```
/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/
```

| carrier | line at `fbc12be3` | line on this head | after |
|:--|:--|:--|:--|
| `packages/spec/src/kernel/plugin.zod.ts` `PluginSchema.version` | 212
| **216** | `z.string().regex(SEMVER_SHAPED_VERSION_PATTERN)` |
| `packages/core/src/plugin-loader.ts` `isSemverShapedVersion` | 501 |
**505** | `return SEMVER_SHAPED_VERSION_PATTERN.test(version)` |

**G3 — `SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN`, sha256/16
`3e3cb2710d62909a`, 48 bytes**

```
/^\d+\.\d+\.\d+(-[a-z0-9.-]+)?(\+[a-z0-9.-]+)?$/
```

| carrier | line at `fbc12be3` | line on this head | after |
|:--|:--|:--|:--|
| `packages/spec/src/marketplace/package-version.zod.ts`
`PackageVersionSchema.version` | 146 | **147** |
`.regex(SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN)` |

**G4 — untouched, on purpose.**
`packages/spec/src/marketplace/package-version.zod.ts:81` on this head
(`:80` at `fbc12be3`) — `PackageManifestSchema.version` — keeps its bare
`z.string()`. Giving it a grammar here would be an accept-set move and
would break this round's premise.

**Tenth carrier, out of reach from here.** `objectui`
`PackageFormDialog.tsx:39` / `:227` is a second repository and gets its
own card at this PR's ACCEPT.

## Why the fences held

- **No `.describe()` text, refusal string or JSON Schema `pattern`
moved.** The constant is the regex only; each carrier keeps its own
description. Moving the prose is phase 2's job, because the prose only
becomes true when the grammar moves.
- **Nothing was exported from the ROOT entry.** The constants are on
`./kernel`; `marketplace/package-version.zod.ts` references
`SEMVER_SHAPED_LOWERCASE_VERSION_PATTERN` cross-entry via
`../kernel/version-grammar`, the precedent at
`marketplace/marketplace.zod.ts:4`.
`packages/spec/api-surface/root.json` is untouched, so PR objectstack-ai#19373's hold
on that file is intact.
- **No `packages/spec/api-surface/*.json` was hand-edited**, and
`packages/spec/CHANGELOG.md` and `packages/spec/package.json` were never
opened.
- **The objectstack-ai#16365 record in `plugin.zod.ts` is intact.** One sentence in it
became false — the one saying this key's regex is
`PluginLoader.isSemverShapedVersion`'s spelling character for character
— and it is corrected minimally, in place: the two now reference one
declaration and can no longer drift apart. The same correction is made
to that method's own docblock in `plugin-loader.ts`, where "Change one
spelling and you must change both" no longer describes anything. Nothing
about phase 2 is announced in either.

## Regenerated artifacts — gains only, on the allow-listed shards

`pnpm --filter @objectstack/spec check:generated` named exactly two
stale artifacts out of fifteen; both were regenerated with their own
generators, never by hand.

```
 M packages/spec/api-surface/kernel.json      (+3, -0)
 M packages/spec/export-origins/kernel.json   (+3, -0)
```

That is the whole of `git status` after regeneration.
`api-surface/root.json`, every `automation.json`,
`dropped-refinements.baseline.json`, `declaration-map/*` and
`json-schema.manifest/*` all came back byte-unchanged — which is itself
the neutrality reading: a pure literal-to-constant collapse produces the
identical Zod schema, so a moved `dropped-refinements` line would have
been evidence the refactor was not accept-set neutral.
`check:authorable-surface` and `check:declaration-map` pass with no
regeneration at all.

The six added rows are the three constant names in the two kernel
shards, and nothing is removed.

## The neutrality proof, and the instrument R2 will move

**Not one existing test expectation was edited.**
`packages/spec/src/kernel/plugin.test.ts` (which pins eight
forbidden-by-SemVer forms as ACCEPTED),
`packages/spec/src/kernel/manifest.test.ts` and
`packages/core/src/plugin-loader.test.ts` all pass unchanged — that is
the proof this cannot move a verdict.

New: `packages/spec/src/kernel/version-grammar.test.ts`, describe block
**`version grammars — accept-set pin`** — a 3 × 12 matrix pinning each
constant's verdict on `1.2.3`, `2.0.0-beta.1`, `1.0.0-Beta.1`,
`1.0.0+20230101`, `01.1.1`, `1.0.0-0123`, `1.0.0-alpha..1`, `1.0.0+.`,
`v1.0.0`, `1.0`, `latest` and the empty string, plus a source-bytes pin
per constant, a no-stateful-flag assertion, and the strict-containment
ordering (G1 inside G3 inside G2).

**Two ablations, each mutated through an anchor that had to hit, each
proved on disk, each restored byte-identically (`blob == HEAD`, `git
diff HEAD` empty):**

1. Widening `MAJOR_MINOR_PATCH_VERSION_PATTERN` by an optional `-beta`
group turns `manifest.test.ts > ManifestSchema > Basic Properties >
should enforce semantic versioning` **red** (1 failed / 65 passed). That
is the carrier wiring proved live: `ManifestSchema.version` really reads
the constant.
2. Widening it by an optional `-ABLATED` group turns the new pin's
**source-bytes** assertion red while the 3 × 12 matrix stays green — the
witness strings do not cover that shape. Reported because it is the
honest reading of what each half of the pin buys: the matrix pins
verdicts, the bytes pin catches an equivalent-looking rewrite the
witnesses cannot see. Both halves earn their place.

Cross-package wiring is visible in the built output too:
`packages/core/dist/index.js` and `packages/runtime/dist/index.js` both
reference the imported constant.

## Verification

| what | command | result |
|:--|:--|:--|
| build closure | `turbo run build --filter=@objectstack/runtime...
--filter=@objectstack/core... --filter=@objectstack/spec...
--concurrency=2` | 30/30 tasks successful |
| spec suite | `pnpm --filter @objectstack/spec test` | 507 files, 14837
tests passed |
| core suite | `pnpm --filter @objectstack/core test` | 51 files, 1321
tests passed |
| runtime suite | `pnpm --filter @objectstack/runtime test` | 271 files,
3761 passed / 1 skipped |
| typecheck | `turbo run typecheck` for spec, core, runtime | 32/32
tasks successful |
| derived gates | `node scripts/pm/dispatch-gates.mjs --commands` then
`--ran` | **89 derived, 86 run green, 3 NOT MEASURED, 0 UNRUN** |

The three NOT MEASURED are `check:dual-build-cjs-loads`, `check:i18n`
and `check:type-check-debt`, each exiting **3 — PREREQUISITE NOT MET**,
all three for the same reason: they read built output for the whole
workspace and only the spec/core/runtime closure was built here. Neither
red nor green; nothing was measured. CI builds the full closure and will
answer them. A fourth, `check-plugin-teardown-shape.mjs --self-test`,
also exited 3 on first run — its positive-control fixture is pinned to a
commit this shallow clone could not reach; after `git fetch --depth=200`
of that object it re-ran at **exit 0, 48 cases pass**.

## Enqueue: C5 fired, it was correct, and the declaration was corrected

Measured, not predicted:

```
node scripts/pm/check-widening-tells.mjs --declaration no --diff PR.DIFF   ->  exit 4

✗ T3 packages/spec/api-surface/kernel.json:157 — a new row in a published entry point's export listing
✗ T3 packages/spec/api-surface/kernel.json:381 — a new row in a published entry point's export listing
✗ T3 packages/spec/api-surface/kernel.json:382 — a new row in a published entry point's export listing
```

This PR was first pushed declaring `Clause-②: no`, on the reading that
an accept-set-neutral collapse is not a clause-② change. That reading
answered one conjunct of two. T3 was right: the public surface really
does grow by three exports, and the lane's criterion is
「放宽接受集**或扩大公开面**的卡,不论多小,即条款②」 — a new exported symbol is `yes` on its
own, whatever the accept set does.

The author escalated rather than flipping the declaration, and the
`domain:spec` seat re-declared `Clause-②: yes (widening)` in correction
comment 5754511685, superseding its earlier correction. **The question
is closed.** The instrument was not weakened and no card was filed
against it — the literals this diff removed were inline regexes inside
schemas, never on the public surface, so T3 measured exactly what it is
defined to measure. The accept-set half of the claim is untouched by any
of this and is still proved above, byte for byte.

## Contract review of record (row C6)

`domain:spec` owes an at-tier contract review on **every** round it
delivers, `Clause-②: yes` **or** `no`, and until that record exists this
PR is not landable. The record is written by an isolated
`CONTRACT_REVIEW_TIER` reviewer, not by the author. Its
`Implemented-by:` names
`claude/issue-18697-version-grammar-canon-phase1`.

## Changeset

`.changeset/18697-version-grammar-canon-phase1.md` — `@objectstack/spec:
minor` (new exported public constants), `@objectstack/core: patch`,
`@objectstack/runtime: patch`. No BREAKING banner: that belongs to R2.

## Base

Branched from `origin/main` `fbc12be318de0713e82e1f38ab804b5b63478e64`;
the dispatch order read `72eeabd3`, which had already moved when the
worktree was created.

⚠️ **This head carries a base merge.** PR objectstack-ai#19473 landed on `origin/main`
and touched `packages/runtime/src/domains/packages.ts` (+96/−8) —
carrier objectstack-ai#9 of this diff — so the branch went `dirty` and `origin/main`
was merged in at `32b5831c4e48ac9b1626322cf185b286a09073aa`. ⇒ **the
base every line number in this body is measured against is `32b5831c`,
not `fbc12be3`**, ⚠️ **and every pointer moved, not only
`packages.ts`.** Each carrier file gained an `import … from
'./version-grammar'` line, so each carrier sits one line lower than it
did at `fbc12be3`; `plugin.zod.ts` and `plugin-loader.ts` moved +4
because their comment blocks grew as well, and `packages.ts` moved `1680
→ 1770` (the merge plus the resolved comment block). The tables above
now carry BOTH columns, every cell verified line-by-line against this
head with a required-substring check. An earlier revision of this body
put the `fbc12be3` numbers in a column headed `line` beside one headed
`after`, which resolved to the wrong line on the head that ships.

`origin/main` has since reached `8015dc85` (one commit, objectstack-ai#19471), which
is ⛔ **not** an ancestor of this head and touches none of the 14 files
here — `git diff --stat 32b5831 8015dc8` over the 14 paths is empty.
The queue rebuilds on current `main` regardless.

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

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