Repository navigation
[WIP] Create a new release version - #9
Closed
huangyiirene with Copilot wants to merge 1 commit into
Closed
huangyiirene with Copilot wants to merge 1 commit into
huangyiirene with Copilot wants to merge 1 commit into
Conversation
Copilot stopped work on behalf of
huangyiirene due to an error
January 18, 2026 14:59
This was referenced May 25, 2026
Closed
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>
15 of 18 tasks
This was referenced Jul 19, 2026
This was referenced Jul 27, 2026
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>
This was referenced Aug 31, 2026
This was referenced Sep 2, 2026
This was referenced Sep 2, 2026
This was referenced Sep 16, 2026
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>
This was referenced Oct 2, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.