Skip to content

chore(ci)(deps): bump github/codeql-action from 3 to 4 - #24

Merged
hotlong merged 1 commit into
mainfrom
dependabot/github_actions/github/codeql-action-4
Feb 1, 2026
Merged

hotlong merged 1 commit into
mainfrom
dependabot/github_actions/github/codeql-action-4

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Jan 19, 2026

Copy link
Copy Markdown
Contributor

Bumps github/codeql-action from 3 to 4.

Release notes

Sourced from github/codeql-action's releases.

v3.31.10

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

3.31.10 - 12 Jan 2026

  • Update default CodeQL bundle version to 2.23.9. #3393

See the full CHANGELOG.md for more information.

v3.31.9

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

3.31.9 - 16 Dec 2025

No user facing changes.

See the full CHANGELOG.md for more information.

v3.31.8

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

3.31.8 - 11 Dec 2025

  • Update default CodeQL bundle version to 2.23.8. #3354

See the full CHANGELOG.md for more information.

v3.31.7

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

3.31.7 - 05 Dec 2025

  • Update default CodeQL bundle version to 2.23.7. #3343

See the full CHANGELOG.md for more information.

v3.31.6

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

3.31.6 - 01 Dec 2025

... (truncated)

Changelog

Sourced from github/codeql-action's changelog.

4.31.5 - 24 Nov 2025

  • Update default CodeQL bundle version to 2.23.6. #3321

4.31.4 - 18 Nov 2025

No user facing changes.

Commits
  • 44e589b Fix feature flag lookup when uploading DB
  • 0d648eb Merge pull request #3380 from github/dependabot/github_actions/dot-github/wor...
  • 3fd7db8 Merge pull request #3379 from github/dependabot/npm_and_yarn/npm-minor-1607f6...
  • 6b11018 Rebuild
  • d0d445f Bump ruby/setup-ruby
  • 60b2ba3 Rebuild
  • 709d6de Bump the npm-minor group with 4 updates
  • See full diff in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot merge will merge this PR after your CI passes on it
  • @dependabot squash and merge will squash and merge this PR after your CI passes on it
  • @dependabot cancel merge will cancel a previously requested merge and block automerging
  • @dependabot reopen will reopen this PR if it is closed
  • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file github_actions labels Jan 19, 2026
@vercel

vercel Bot commented Jan 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
spec Ready Ready Preview, Comment Jan 19, 2026 0:54am

Request Review

@hotlong
hotlong merged commit 8f15694 into main Feb 1, 2026
22 checks passed
@dependabot
dependabot Bot deleted the dependabot/github_actions/github/codeql-action-4 branch February 1, 2026 02:48
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…ed posture (objectstack-ai#15051)

* fix(objectql): elevate ObjectRepository.execute() to REST/MCP's trusted posture

Director ruling 决裁批 objectstack-ai#24 (2026-09-01), clause 2: the census (repo +
examples/ + apps/, production + test) found ZERO real callers of
ObjectRepository.execute() anywhere — every hit was prose describing the
shape, never an invocation — so the premise (callers are internal / test /
single-digit) holds in its strongest form, and the elevation is implemented.

ObjectRepository.execute() now hands the action handler a ScopedContext
bound to { ...callerContext, isSystem: true } (ctx.api) and the same
elevated envelope as ctx.executionContext — the identical sudo()-shaped
formula buildActionExecutionContext (REST /actions, MCP run_action) and
recomputeSummaries's systemCtx already use. Before this, the handler got
neither api nor executionContext: a handler composing a sibling write via
ctx.api.object(x).update(y) got ctx.api === undefined, and the sandbox's
own last-resort fallback ran that write as a non-system caller, so the
engine's static readonly strip applied to this path and not to REST
/actions or MCP run_action.

Fixes objectstack-ai#13866

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

* fix(docs): re-anchor system-context census row after objectstack-ai#13866's insertion

Pure line rot: content/docs/permissions/system-context.mdx:183 pointed at
engine.ts:14463, which the elevation fix's inserted JSDoc pushed down to
14496 (the ScopedContext.isSystem getter itself is unchanged). Repaired
via `node scripts/check-system-context-census.mjs --fix`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

* fix(objectql): make the elevation pin's fake find() honor limit

check:objectql-double-limit flagged the fake driver's find() in
engine-repo-execute-elevation.test.ts as a new limit-blind ObjectQL double.
Apply the caller's bound after the filter, by presence — the gate's own
suggested fix — so the baseline stays at zero new limit-blind doubles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

* fix(objectql): make the elevation pin's fake where-matcher refuse combinators

check:where-matcher flagged the fake driver's matches() in
engine-repo-execute-elevation.test.ts as a new silently-wrong WHERE matcher
(a $-prefixed combinator key would be read as a literal field name instead
of being rejected). Refuse it loudly instead, matching the exact idiom
engine-readonly-strip-caller-values.test.ts's own fake driver already uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

---------

Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 18, 2026
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…d an 'end' node (objectstack-ai#18688)

Part of objectstack-ai#15646

Clause-②: yes

The flow accept set shrinks for five node types inside region bodies —
shapes the runtime never honoured. Ruling D clause 4 states it verbatim.

⚠️ **The runtime half is NOT in this PR.** A region-contained node that
*durably suspends* must fail the run with a named error — only the run
can know that — and ruling D assigns it to a separate `domain:services`
card. ⇒ this PR lands with `Part of`, ⛔ not `Fixes`; **objectstack-ai#15646 stays open
until the runtime half lands.**

---

## ⚠️ SEAT BANNER — ruling **D** superseded the route this body argues
for

Everything below the horizontal rule was written when the card was ruled
**C**, and it argues for **route A** ("Recommendation: A, as
implemented"). ⛔ **That is no longer what this PR does.** It is kept
unedited as the record of how the decision was reached — ⛔ deleting it
would erase the evidence the later ruling was made on.

**What this PR does NOW**, per ruling D (batch objectstack-ai#153 item 1, comment
`5724940095`, maintainer 「其他同意」):

- Inside `loop` / `parallel` branch / `try_catch` (try **and** catch)
bodies at any depth, `FlowSchema.superRefine` refuses **five** node
types: `screen`, `wait`, `approval`, `approval_revise`, and `end`.
- ⛔ **`map` and `subflow` are NOT refused by type.** They pause exactly
when the child flow their `config.flowName` names pauses — a *different
metadata record*, not in hand at parse. Refusing them by type would also
refuse `loop { map(synchronous child) }`, which runs correctly today.
- `packages/spec` keeps its published identifier
`FLOW_PAUSE_CAPABLE_NODE_TYPES`; only its **contents** narrow.
- **`packages/services` is untouched by this round** — measured, zero
paths. The 5-tests-in-3-files cost the section below describes **does
not occur**: the whole package runs **138 files / 1652 tests, all
passing**, with a false-green control proving the test read the rebuilt
artifact and not a stale `dist`.

⚠️ **Corrected by the seat, and it is a DECLARED DEVIATION from ruling
D's letter — ⛔ not a clean pass.** 「zero paths」 is true of **this
round's commits** and ⛔ NOT of the PR's cumulative diff:
`packages/services/service-automation/src/end-node-refused-outcome.test.ts`
(+57/−35) is in the diff, from the earlier round's commit `87973cab8d1`.
Ruling D clause 1 says 「`packages/services` untouched; **the 5 tests**
and objectstack-ai#15616's suite stand」 — and objectstack-ai#15788's region-`end` case **was one of
those 5**. ⇒ the ruling's premise 「B breaks nothing」 was **false for
that one case**: clause 1 itself orders `end` refused at parse, and
`registerFlow` parses, so the old run-time assertion is unreachable by
construction. What was done: the fixture is **byte-identical**, case
count **12 → 12**, and the assertion is **strengthened** (region path,
message text, and that nothing registered) so it fails again the day the
shape becomes declarable. ⛔ No test deleted, skipped or quarantined; ⛔
no engine source moved; objectstack-ai#15616's suite and the other three files are
untouched and green. The at-tier review measured all of this and ruled
it non-blocking — but it is a deviation and it is stated here rather
than buried.

**CI on `6de9d662f6df`: 32 success, 3 skipped, 0 failure, 0 pending.**

### ⛔ Two corrections the `domain:spec` seat owes on its own record

1. ⭐ **The seat ruled "keep the published name" on a premise that is
FALSE.** It told the round that renaming `FLOW_PAUSE_CAPABLE_NODE_TYPES`
removes a *published* export and therefore forces a major. Measured
since: main's `packages/spec/api-surface/automation.json` greps **0**
for that name (lit controls `FLOW_BUILTIN_NODE_TYPES` and
`FLOW_STRUCTURAL_NODE_TYPES` = 1 each; dark control = 0), and the branch
greps 1. ⇒ **the constant is introduced by this PR and is on no
consumer's import path**; the gate's 「1 breaking (removed)」 was computed
against the branch's own earlier snapshot. The *decision* stands and
costs nothing — a second name would be cost without benefit — but ⛔ the
record must not carry 「a removed published export」 as a fact about
consumers. The round measured this and told the seat; the seat
re-measured and confirms it.
2. ⛔ **THIS CORRECTION WAS ITSELF WRONG, and the seat withdraws it.** It
claimed `--pair 18688` re-measured 「exit 0」 at this head. **That reading
came from a STALE INSTRUMENT.** The shared checkout's
`check-clause2-carriers.mjs` is blob `ccd5ad7c9a00` and contains **0**
occurrences of rule **C8**; `origin/main`'s and this head's is blob
`3a270ef2eb5f` and contains **18** (lit control `C1`: 50 vs 51, so the
reader works). C8 landed on `main` at 01:41Z via objectstack-ai#18859 and the shared
checkout never had it. ⇒ every `--pair` reading this seat took today was
taken with a script that cannot see C8. Re-taken with `origin/main`'s
script: **objectstack-ai#18688 exit 4 on C8** — this seat held **two live `Claim:`
comments** on objectstack-ai#15646 (`5722016855`, `5728277407`), which the protocol
forbids. Repaired as C8 prescribes: `Release:` (`5729634742`) then ONE
fresh `Claim:` (`5729639847`). **`--pair 18688` now exits 0** —
`claim.selected` 1, `claim.rejected` 2. The at-tier review caught this;
the seat re-measured and confirms it.

---

Route **C**, as ruled. Director seat, summon objectstack-ai#24, batch objectstack-ai#145 item 5 —
objectstack-ai#15646 (comment)
(maintainer 「同意,其他也同意」), with the batch objectstack-ai#146 scope addition —
objectstack-ai#15646 (comment)
(maintainer 「146 同意」), which attached objectstack-ai#3267's 禁 ruling and absorbed
objectstack-ai#18112 into this card. One PR, one changeset, two refusals in one rule
family.

## 🛑 Read this first — this PR is NOT ready to land, and the reason is a
measured decision, not a bug

`packages/spec` is green end to end. **5 tests in 3
`packages/services/service-automation` files now fail**, and every one
of them fails for the same reason: the fixture can no longer be
REGISTERED, because `AutomationEngine.registerFlow` parses through
`FlowSchema.parse` (`engine.ts:3941`) and this rule refuses the shape.

⚠️ **Corrected by the `domain:spec` seat after a classification round —
the table below replaces one that named 5 tests in 3 files.** That
earlier count was taken by running **three named files**; CI runs `pnpm
--filter @objectstack/service-automation test`, the whole package, and a
named-file subset cannot see this class of breakage. `os-dev.md:56`
reserves this body to the PR-open write, so the round named the wording
and the seat writes it.

| File | Failing | Card | What it pins |
| --- | --- | --- | --- |
| `src/builtin/contained-failure-rollup.test.ts` | **7** | objectstack-ai#16314 | the
contained-failure rollup fold over `loop { subflow }` — ⚠️ **absent from
the earlier table entirely**; it predates this branch's base (`git
merge-base --is-ancestor` exit 0), so this is a measurement gap, ⛔ not
drift |
| `src/builtin/map-in-loop-iteration-state.test.ts` | 3 | objectstack-ai#15616 | `loop
{ body: [ map, probe ] }` over a **non-pausing** child: 5 iterations x 2
items ⇒ 10 child runs, `failed = 0` either way, a fresh result set per
iteration |
| `src/builtin/contained-failure-visibility.test.ts` | 1 | objectstack-ai#14456 | a
parent run's row identity does not leak into a `subflow` child; the
region shape is the **vehicle**, not the subject |
| `src/end-node-refused-outcome.test.ts` | 0 (was 1) | objectstack-ai#15788 | ⭐
**fixed on this branch** — see below |

**Measured with the package suite:** at `e10b395cee`, **12 failed / 1627
passed (1639)** across **4 files**. After the fix below, at
`87973cab8d1`: **11 failed / 1628 passed**.

⭐ **One of the twelve was never blocked on the open question, and it is
repaired here.** objectstack-ai#15788's region-`end` case sits in **both** candidate
populations — this body defines route B as the unconditionally pausing
types **plus `end`** — so no answer to the question below moves it. It
is re-homed to the registration refusal: the fixture is unchanged byte
for byte, and the case now asserts the ZodError's located path, its
message and prescription, and that **nothing registered**. ⛔ Not a
deletion — it fails again the day the shape becomes declarable.

**The 11 are mutually exclusive with route A, and that is measured
rather than argued.** Ablating `FLOW_PAUSE_CAPABLE_NODE_TYPES` to route
B's definition turns **all 11 green with nothing else moving**; route A
on the same four files is 11 red. ⚠️ Method note that is load-bearing:
`service-automation` resolves `@objectstack/spec` through **`dist`**, so
the ablation was rebuilt and verified present in 18 built artifacts
before anything was read — an unrebuilt ablation would have gone green
and proved nothing. Restored afterwards, verified absent from all 216
artifacts, whole-tree porcelain empty.

⭐ **The 11 are NOT one cost.** 3 of them (objectstack-ai#15616) are free: under route
A the shape becomes undeclarable, so the defect is unreachable and the
regression suite converts to a refusal pin — mechanically, the same
conversion performed above for objectstack-ai#15788; that file's second describe (a
TOP-LEVEL pausing map) is untouched and green, so the durable-pause half
keeps its coverage. The other 8 (objectstack-ai#16314, objectstack-ai#14456) are a genuine re-home
onto a top-level delegating node, and `loop { subflow }` over five rows
with one failing is the shape objectstack-ai#15617's ruling **named**, so any re-home
must record that the measurement no longer runs on it.

⛔ **Not repaired here.** The dispatch fences `packages/services` ("the
engine's runtime refusal stays exactly as it is") — ⚠️ and note
precisely what that fence claims: it is true of the **diff**, which
touches no `packages/services` file. Read as a claim about **effect** it
is false, because the parse refusal changes what those suites can
register. The changeset carries the same correction, and two of these
three are other cards' regression suites: deleting or re-homing objectstack-ai#15616's
and objectstack-ai#15788's coverage is a decision, not a fixture edit. **Two of them
are also evidence about the rule itself**, which is the open question
below.

### The open question: does the narrowing take a shape that WORKS with
it?

The ruling's population is "a node that **can durably pause** (`map` /
`subflow` **with a pausing child**, approval-class nodes)". Measured:
`map` and `subflow` pause **exactly when the child flow they NAME
pauses** — a different metadata record — so "with a pausing child" is
**not decidable at parse**. Only two spellings are:

- **A — judge the node TYPE** (what this PR implements). Closes this
card's own reproduction, covers all three region kinds, and is the only
reading under which C is the *complete* fix the ruling's own reasoning
requires. **Cost, measured:** it also refuses `loop { map(synchronous
child) }` — a shape that runs correctly today and was deliberately fixed
12 days ago by objectstack-ai#15616 / PR objectstack-ai#15648, whose regression suite is 3 of the 5
failures above.
- **B — judge only the UNCONDITIONALLY pausing types** (`screen` /
`wait` / `approval` / `approval_revise`) plus `end`. Refuses nothing
that works today, and the 3 `map` failures disappear. **Cost:** this
card's own reproduction — `loop { try_catch { map(pausing child) } }` —
stays declarable and stays silently green, so the card is not closed.

There is no third reading available to a parse. **Recommendation: A, as
implemented** — objectstack-ai#3267 is ruled 禁 ("structured regions do not support
durable pause"), and a shape whose legality lives in a record the author
is not editing, revocable by editing that record, is not a contract.
Under A the five tests are re-homed (a top-level `map`, a top-level
`end`) or retired with a statement, in this PR or a follow-up, once the
seat says the coverage may move.

## Step Zero — the ruling's precondition, answered before any code was
written

> **First step, before writing**: prove the nesting is statically
decidable at parse/validate time.

**Answer: YES for the nesting and for the node vocabulary this rule
judges, with two boundaries that are declared rather than discovered.**
What was measured, on this branch's base `7f7b8557df`:

1. **The nesting is decidable, and a refusing layer already exists.**
`collectFlowGraphs` (`packages/spec/src/automation/control-flow.zod.ts`)
yields the top-level graph plus every region body, depth first, with a
`scope` label and a `path` that anchors a Zod issue where the author
wrote the node. `FlowSchema`'s `superRefine` already walks exactly that
and refuses on it — the objectstack-ai#16134 one-node-id-space rule. The PM seat's
clue held: there is no *refusing* layer for this shape, but the walk and
the refusal machinery are both live and in the same file.
2. **The pausing vocabulary is statically declared for the built-in
set.** Derived by reading the shipped `defineActionDescriptor` literals,
not by recall: `supportsPause: true` appears on `screen` / `wait` /
`subflow` / `map` (`packages/services/service-automation/src/builtin/`)
and `approval` / `approval_revise`
(`packages/plugins/plugin-approvals/src/`) — six, the same six the
ADR-0044 `resumeAuthority` default-flip migration entry names in its own
prose. They are published here as `FLOW_PAUSE_CAPABLE_NODE_TYPES`.
3. **`end` is fully static** — `FLOW_STRUCTURAL_NODE_TYPES`, a node type
the engine handles with no executor at all.

**What is NOT decidable, and what this rule does about it.** Whether a
given node *will* pause is not decidable at parse, in two different
ways, and both are stated in the docblock, in the changeset and in the
ADR-0087 entry:

- **`map` / `subflow` pause exactly when the child flow they NAME
pauses** (`map.config.flowName`, an opaque reference to another metadata
record). So the rule judges the node **TYPE**, not the run. That is
wider than the runs that actually broke — a region-nested `map` over a
synchronous child parsed green before and is refused now — and it is
deliberate: the old shape's legality lived in a record the author is not
editing and could be revoked by editing that record. "Legal until
somebody adds a `wait` to the child flow" is not a contract.
- **A plugin-registered pausing type is invisible to a parse.** ADR-0018
left the node-type namespace open (`FlowNodeSchema.type` is a validated
`string`), and a parse has no registry. Pinned as a boundary test so it
moves deliberately.
- **`MAX_REGION_DEPTH` (32).** The walk stops there. ⚠️ Unlike objectstack-ai#16134's
duplicate-id rule, there is **no second spec refusal behind the
ceiling** for this rule — `analyzeRegion` says nothing about pausing
nodes — so past depth 32 the engine's run-time refusal is the only one.
Measured and pinned at nesting 32 (refused) / 33 (not judged), and
stated in the changeset rather than left for an author to find.

## What changed

`FlowSchema.superRefine` gains one walk over `collectFlowGraphs`,
skipping the flow's own graph, that raises a `custom` issue anchored at
`[...regionPath, 'nodes', i, 'type']` for:

- **a pause-capable node** in a region body — the message names the
node, the region scope (`loop 'sweep' body → try_catch 'guard' try`),
why a region body cannot host it, and the fix;
- **an `end` node** in a region body, whatever its `outcome` — an `end`
there was a no-op, and a refusing one was converted into a region error
at the same boundary (objectstack-ai#15788). The ruled prescription is the message: a
region body cannot end the run; put the `end` on the top-level graph.

`FLOW_PAUSE_CAPABLE_NODE_TYPES` is the new export (`api-surface` /
`export-origins` regenerated). The two approval entries are the declared
constants `APPROVAL_NODE_TYPE` / `APPROVAL_REVISE_NODE_TYPE`, so a
rename cannot desynchronise them.

⛔ `packages/services` is untouched — this is authoring-time enforcement
only. ⛔ No engine rollback seam (route A, no card filed, per the
ruling). ⛔ No runtime detection in `map` (route B, refused). ⛔ objectstack-ai#15617's
`failed` fold is not addressed.

## Tests

New file
`packages/spec/src/automation/flow-region-pause-and-end.test.ts` — every
case fails without the rule:

- both refusals × all three region kinds: `loop` body, `try_catch` try
**and** catch, `parallel` branch. A rule covering `loop` only is route B
wearing C's clothes; the `try_catch` catch arm and the `parallel` branch
arm are the two route B could never see, and each has its own case.
- all six pause-capable types, table-driven off the exported constant.
- the card's own reproduction, `loop { try_catch { map } }`, refused
with the chained region path.
- **negative tests, the over-reach guard**: every pause-capable type and
an `end` still parse on the **top-level graph**; every non-pausing type
still parses inside a region; a node merely *named* `end` or `wait` in a
region still parses (the rule judges `type`, not `id`).
- both declared boundaries pinned: the plugin-contributed pausing type,
and the depth-32/33 seam.
- `defineFlow` and `formatZodError` renderings.

Two existing cases pinned the behaviour this rule replaces and were
**replaced rather than re-spelled**, each saying so in its own comment:
`end-node-outcome.test.ts`'s region-nested `end` (its subject — an
`end`-in-region whose *config* is judged one door later — no longer
exists) and `flow.test.ts`'s BPMN `waitEventConfig` region case (now
asserts the earlier refusal *and* keeps the region-contract half it
actually exists to measure). The `requireTypeScopedConfig` docblock that
asserted a nested block-less `wait` parses green was corrected in the
same edit.

## Verification

Measured on `e10b395cee`. Heavy runs go through
`scripts/pm/os-verify-lock.sh`; every exit code below is read from the
wrapper's own `VERDICT command-exit` line or captured into a variable
**before** any pipe — never `$?` after one.

| Command | Verdict |
| --- | --- |
| `pnpm --filter @objectstack/spec build` | `command-exit 0` |
| `pnpm --filter @objectstack/spec test` (whole package) | `command-exit
0` — **486 files, 13895 tests, 0 failed** |
| `pnpm --filter @objectstack/spec typecheck` (`tsc --noEmit` +
`check:scripts-typecheck` + `check:test-typecheck`) | `command-exit 0` |
| `pnpm --filter @objectstack/spec check:generated` | `command-exit 0` —
all 15 artifacts up to date |
| `pnpm lint` (whole repo, `eslint . --no-inline-config`) | `exit 0` —
run in full, so nothing here is a narrowing |
| `dispatch-gates.mjs --ran` reconciliation | `exit 0` — **85 derived,
81 run, 4 NOT-MEASURED, 0 UNRUN** |
| `@objectstack/service-automation` — the 3 files whose fixtures feed
this rule | `exit 1` — **5 failed / 24 passed**, see the section at the
top |

**Reverse verification (one-shot, restored).** The rule's own early-exit
was mutated (`graph.path.length === 0` → `>= 0`), and the mutation was
proved on disk before anything was read from the run — anchor grep 1 →
0, marker grep 0 → 1, blob `044bbbba` → `56a1c350`:

- **ablated** — `flow-region-pause-and-end.test.ts`: **20 failed / 7
passed**. The 20 are exactly the refusal assertions; the 7 that survive
are the over-reach guards and the two boundary pins, which must stay
green with the rule absent. That split is itself the reading: a rule
that also broke the negative cases would be refusing too much.
- **restored** — `git checkout HEAD --` the file, blob back to
`044bbbba`, `git diff HEAD` clean, `git status` empty, same file **27/27
passed**.

No `dist` preflight applies: the test imports `./flow.zod` by relative
source path, so the subject never resolves through `packages/spec/dist`.
A restore `trap` was armed for the whole window.

**The four NOT-MEASURED gates** are `check:doc-formula-expressions`,
`check:dual-build-cjs-loads`, `check:lean-entry-closure` and
`check:type-check-debt` — each exited **3, PREREQUISITE NOT MET**,
printing in its own words that nothing was measured. All four read built
output across packages this diff does not touch and need a repo-wide
build; CI's `Build Core` and `Lint & Repo Gates` are where they run. ⛔
Not green, not red — unrun.

**Not measured, stated:** CI convergence on this PR (the report is filed
at the end of local verification); the branch has not been merged
forward since `7f7b8557df`, so `main`'s newer commits are tested by CI
and the queue rather than here.

**Review-gate reading, not an action.**
`scripts/pm/check-clause2-carriers.mjs --pair 18688` exits **4** on two
rows, both belonging to the claiming seat and ⛔ neither touched here:
**C1** — card objectstack-ai#15646 carries `needs:contract-review` while this PR does
not (the gate is a dual carrier); **C2** — no comment on the card's
thread is a machine-legible claim comment (none has a first line
beginning `Claim:` carrying the `Clause-②:` line), so the declaration
limb has nothing to read. The declaration itself is at the top of this
body and in the changeset.

## Acceptance notes

Observations from this card's reading, recorded here and **not** filed —
none is a reproducible defect, a contract violation, or an authoring
trap:

- The ruling's own parenthetical, "`map` / `subflow` **with a pausing
child**", describes the defect population rather than a decidable rule
population, and its "a shape the runtime never honoured" is exact for
`end`, `screen`, `wait` and the approval pair but not for a `map` over a
synchronous child, which runs today. The PR takes the capability reading
— the only one that makes C the complete fix the ruling's own reasoning
requires — and the changeset states the cost in the author's own terms.
Noted so a reviewer reads the widening deliberately rather than
discovering it.
- The card body and the batch objectstack-ai#145 ruling both say objectstack-ai#15617 is open; it is
**closed / completed**. Nothing here depends on it.
- `engine.ts:9937` in the ruling reads `engine.ts:9970` on this tree —
line numbers are clues, and this one was re-read rather than trusted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…escribes and remove the carve-out (objectstack-ai#19035)

Fixes objectstack-ai#18075

Executes **batch objectstack-ai#158 item 5 · letter A** (director seat summon objectstack-ai#24,
maintainer 「同意」 2026-09-18T11:14Z). The ruling's verbatim scope:

> the agreement shape — a unit in the key name, the same unit in the
JSDoc, no unit in `.describe()` — **IS an offence**: the carve-out
`!site.jsdocUnits.some((u) => site.keyUnits.includes(u))` is
**removed**, the two live rows get a `.describe()` that **names the
unit**, the two DEFERRED self-tests **turn positive**, and the header's
**shape (b) base refusal is restored**

Clause-②: no

## The four deliverables

| # | Deliverable | Where |
|:--|:--|:--|
| 1 | The two live rows name their unit in the published channel |
`packages/spec/src/ai/usage.zod.ts` `latencyMs` (had **no**
`.describe()` at all) and `packages/spec/src/system/tenant.zod.ts`
`frequencyHours` (`'Backup frequency'`, unit-silent) |
| 2 | The carve-out is removed | the divergence guard is now
`site.keyUnits.length > 0 && site.jsdocUnits.length > 0` |
| 3 | The two DEFERRED self-tests turn positive | both now assert
`unit-in-jsdoc-not-in-describe`, relabelled `REFUSED (agreement): …` |
| 4 | The header's shape (b) base refusal is restored | the "TWO shapes
this branch used to refuse" block now names ONE cost — (a), the retired
name list — and records (b) as restored |

**Landing order is the ruling's:** commit 1 adds the two describes,
commit 2 removes the carve-out. `main` is never red in between, and
neither is any intermediate commit on this branch — the gate was re-run
green after commit 1 alone.

Every `DEFERRED to objectstack-ai#18075` marker is gone from the checker: five
occurrences, **two cases** and three prose passages. The count
difference was the card's own warning and it held.

## The class-(a) sibling rides this PR, as ruled

> The class-(a) sibling the dev found (the `instant` exemption branch
reads `proseUnits` only, never `jsdocUnits`, while `durationType` reads
all three) rides the same PR — zero live rows today, a fixture proves
it, no separate card.

The `EpochMs` instant exemption now reads the JSDoc channel too, under
the **same predicate shape** the describe half already used (`length > 0
&& !includes('ms')`) — a channel added, not a predicate widened. Two
fixtures pin it: an `EpochMs` key whose JSDoc says seconds is refused,
and one whose JSDoc says ms is not.

**Live rows: 0.** The population run is unchanged at zero offenders with
the channel added.

## One extra edit, and why it is not scope creep

The finding message told every offender *"the only unit the reader can
see is the one the JSDoc disagrees with"* — written when this branch
only ever fired on a **contradiction**. The moment agreement became a
refusal that sentence was false for half the class, in a file whose own
header says *"A gate that cannot see a channel writes falsehoods about
it."* The message now names the silent published channel as the harm and
keeps the contradiction reading as the conditional half it always was.

## The card's recorded knock-on is discharged, not reworded

The card recorded that objectstack-ai#15939's changeset over-claims — it says the gate
refuses a JSDoc unit the describe does not name *"(or there is no
describe at all)"*, which was untrue of exactly these two rows. **That
sentence is now true of the gate.** Nothing was edited in place to make
it true; the two rows are remediated and the carve-out is gone. The new
changeset says so explicitly.

## Changeset: `patch`, measured — not `skip-changeset`

`.describe()` text was measured into the published tarball, not assumed.
Both new strings appear under paths in `packages/spec`'s `files[]`, with
a pre-existing describe (`'Computed cost in USD'`) as the positive
control landing the same way:

```
Wall-clock latency in milliseconds -> dist/ai/index.{js,mjs}, json-schema/ai/AIUsageRecord.json, json-schema/objectstack.json
Backup frequency in hours         -> dist/{browser/,}system/index.{js,mjs}, json-schema/system/{DatabaseLevelIsolationStrategy,TenantIsolationConfig}.json
Computed cost in USD (control)    -> dist/ai/index.{js,mjs}, json-schema/ai/AIUsageRecord.json
```

A shipped JSON Schema `description` moves, so a released package
publishes a change. `patch`, `Clause-②: no`.

The reader-facing half regenerated with it —
`content/docs/references/system/tenant.mdx` stops printing
`frequencyHours | integer | Backup frequency`.

## Verification

All readings on the merged head `ce75c01bb9`, exit codes captured before
any pipe.

**The gate, final tree** — exit 0: `203 unit-declaring numeric key(s)
across 2522 source file(s) … zero offenders, no baseline` · self-test
`106 case(s) across 13 batteries, every battery at or above its pinned
floor`.

**The floor did not move.** 104 cases before, 106 after: the two
DEFERRED cases turned positive without changing the count, and the two
new instant fixtures grew a battery **above** its floor, which the
roster documents as ordinary work. `SELF_TEST_BATTERY_FLOOR` and every
entry in `SELF_TEST_BATTERIES` are untouched.

**Three ablation legs**, each mutated and restored through
`scripts/ablation-replace.mjs` (anchor must hit; restore proven by blob
hash against HEAD, never by exit code), each run from a committed state:

| leg | mutation | result | restore |
|:--|:--|:--|:--|
| A | re-add the carve-out to the guard | self-test exit 1, **exactly**
the 2 agreement cases red, 106 still registered | blob back to
`d00c46dfe66b` = HEAD, `git diff HEAD` empty |
| B1 | revert `latencyMs`'s describe | gate exit 1, **exactly 1**
offender: `[unit-in-jsdoc-not-in-describe] …/usage.zod.ts:52 latencyMs`
| blob back to `d241245cd5ae` = HEAD |
| B2 | revert `frequencyHours`'s describe | gate exit 1, **exactly 1**
offender: `[unit-in-jsdoc-not-in-describe] …/tenant.zod.ts:603
frequencyHours` | blob back to `77b5db008946` = HEAD |
| C | delete the instant branch's JSDoc channel | self-test exit 1,
exactly the 1 new positive control red | blob back to `ec726de17d8f` =
HEAD |

Leg B is the one that matters for the ruling: it shows the widened guard
catching the ruled shape **on live source**, not only on fixtures.

**Package obligations** — `pnpm --filter @objectstack/spec typecheck`
exit 0; `pnpm --filter @objectstack/spec test` exit 0, **489 files /
14229 tests passed**; `check:generated` all **16** artefacts up to date
after the merge.

**Gate families** — `scripts/pm/dispatch-gates.mjs` derived **103** for
these paths from its own change set (never a hand-fed list). **102 ran,
all exit 0**, reconciled back through `--ran` with each exit code
recorded: `103 derived, 102 run, 0 NOT-MEASURED, 1 UNRUN`.

- **NOT MEASURED: `pnpm check:pm-dispatch-gates`** — it exceeds this
environment's ~10-minute foreground cap (killed at 560s with 1840 lines
of passing self-test cases and no verdict). It is a whole-tree-declared
family that grades `scripts/pm/dispatch-gates.mjs`, which this diff does
not touch. CI runs it. ⛔ Not reported as green.
- Four families first returned `PREREQUISITE NOT MET` (unbuilt workspace
packages) and one returned exit 3 on a shallow-clone fixture. Each was
cleared by building the named package / fetching the pinned commit and
**re-run to a real verdict** — ⛔ none is reported from the instrument
that did not run.

**Repo-wide lint** — `pnpm lint` (`eslint . --no-inline-config`) over
the **whole** population at `ce75c01bb9`: exit 0, no narrowing claimed
and none needed.

**Merge** — `origin/main` moved 10 commits into `packages/spec` while
this was in flight, including a breaking spec change. Merged through
`scripts/pm/os-regen-merge.sh`, reinstalled, rebuilt, and every reading
above re-taken on the merged head. Both describes, both reference pages
and the removed carve-out were verified present after the merge.

## Acceptance notes

Noted, not filed — recorded so the next reader does not re-open them:

- The instant branch and the `durationType` branch still differ in
**predicate shape**, not in channel coverage: instant refuses a channel
only when it names a unit and **none** of them is `ms`, while
`durationType` refuses **each** non-matching unit. A describe naming
both `ms` and another unit is therefore tolerated on an instant and
refused on a duration type. Zero live rows either way, and the tolerant
reading is arguably right for a sentence with an incidental second unit.
Deliberately left alone: the ruling authorized a channel, not a
predicate. Handler: whichever PR next touches this file.
- `packages/spec/src/system/tenant.zod.ts:600-608` carries trailing
whitespace on its blank separator lines. No gate reads it; not touched,
because this PR's edit there is one line and a whitespace sweep would
bury it. Handler: none needed.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…t does not exist, and the capabilities docs say explain is an API (objectstack-ai#19209)

Fixes objectstack-ai#18253

Clause-②: yes

Ruling implemented: director summon objectstack-ai#24, batch objectstack-ai#147 item 3, **letter B**
(`issuecomment-5715643277`), confirmed at `issuecomment-5715763202` —
the UI half stays parked, the two stability items proceed.

## What changed

**R2 — loud unknown object (the stability half).** `explain` on an
object name that does not exist now answers `404` with `error.code:
"OBJECT_NOT_FOUND"` instead of `200 { allowed: false, object_crud:
"denies" }`.

**R1 — docs made true.** The capabilities wording now states that
explain is an API capability, with a one-line example; no
product-surface promise remains in that prose.

**R3 — UI parked.** No `.tsx`, no panel, no drawer, no affordance. Zero
files outside the surface below.

## The measurements, taken before the fix

**A1 — reproduced first.** A one-off probe (written, run, then deleted —
a one-time proof, not a permanent test) drove `explainAccess` with the
security meta the plugin really produces for an undeclared object,
beside a genuine denial as the control:

```
TYPO   : {"allowed":false,"object_crud":"denies","detail":"The security posture of 'leave_requst' could not be resolved …"}
GENUINE: {"allowed":false,"object_crud":"denies","detail":"No resolved permission set grants read on 'invoice'."}
```

The card's claim holds, with one honest refinement worth recording:
every **machine-readable** channel was identical — same `allowed`, same
layer verdict, same HTTP 200 — while the layer **prose** did differ
(objectstack-ai#10401 / objectstack-ai#10424 wrote that prose). No client branches on prose, so a
typo still arrived as a permission decision.

**A2 — the envelope was read, not chosen.** Two facts, both read off
this tree:

- The route family's envelope is the ADR-0112 D5 nested body emitted by
its ONE refusal emitter, `respondError` → `sendEnvelopeError`, at
`packages/rest/src/rest-server.ts:11588` (the objectstack-ai#8073 convergence). Every
arm of `/security/explain` and `/security/my-delegable-scope` goes
through it.
- The pair this platform already uses for a missing OBJECT is `404` +
`OBJECT_NOT_FOUND`, at `packages/rest/src/error-response.ts:1238-1244`
(`mapDataError`: `if (error?.code === 'OBJECT_NOT_FOUND' …)` → status
404, `code: 'OBJECT_NOT_FOUND'`), and `OBJECT_NOT_FOUND` is a member of
the standard catalog at `packages/spec/src/api/errors.zod.ts:88`.

So the code is registered already and `packages/spec` is untouched —
**no new error code was minted**, and `ERROR_CODE_LEDGER` /
`StandardErrorCode` needed no row (the ledger's own convention:
standard-catalog members "need no row").

**A3 — which layer answers, and why.** The **engine** decides, the
**door** maps.

`explainAccess` throws `ExplainObjectNotFoundError`; the REST handler
turns it into the status through the family's existing emitter. The
judgement belongs in the engine because `ISecurityService.explain` has
callers other than this route — a door-level check would have been loud
over HTTP and silent for every in-process caller, which is exactly the
failure mode named in the dispatch. The door owns the transport mapping
and nothing else, and it matches on the declared `code` (the objectstack-ai#8016
thrown-shape contract) rather than importing plugin-security, which is
not a dependency of `@objectstack/rest`.

**One cause of three moved, deliberately.** `getObjectSecurityMeta`
already classifies an unresolvable posture as `unpublished_draft` /
`metadata_unavailable` / `unknown`. Only `unknown` — "neither the live
schema nor the metadata service returned a declaration" — becomes the
404. A draft declaration EXISTS (its remedy is "publish it"), and a
degraded metadata read never established absence; claiming either is
missing would manufacture a fact, which is the same error this fix
removes, pointed the other way. Both keep today's `denies` explanation,
each pinned.

## Reverse verification (one-time, both legs restored byte-identical)

Run from the committed state via `scripts/ablation-replace.mjs`, which
proves the mutation reached disk (anchor count and blob hash
before/after) and proves the restore (`blob == HEAD`, `git diff HEAD`
empty).

| ablation | result |
| :-- | :-- |
| delete the engine throw | `5 failed \| 100 passed` — `AssertionError:
promise resolved "{ allowed: false, …(5) }" instead of rejecting` |
| delete the route's 404 arm | `2 failed \| 13 passed` — `expected 404,
got 500 with body {"error":{"code":"EXPLAIN_FAILED", …}}` |

Direction predicted before running, and observed: **turns red**.

## Tests

Measured at `7dbd164d4`.

- `pnpm --filter @objectstack/plugin-security --filter @objectstack/rest
run test` → **exit 0**: plugin-security `114 files / 2228 tests passed`;
rest `194 files / 3247 passed, 1 skipped`.
- `pnpm --filter @objectstack/plugin-security --filter @objectstack/rest
run typecheck` → **exit 0** (both `tsc --noEmit` and both
`check:test-typecheck` ledgers at `0 file(s) / 0 error(s)`).
- Gate families: `dispatch-gates.mjs --ran` reconciles **92 derived, 92
run, 0 NOT-MEASURED, 0 UNRUN**, every one recording an exit code and
none of them 3. Four answered a PREREQUISITE (`check:skill-examples`,
`check:dual-build-cjs-loads`, `check:i18n`, `check:type-check-debt`);
the prerequisite was cleared with a full build and all four then
measured **exit 0**. `check:i18n-coverage` and `check:i18n-walk-parity`
were run on top of the derivation (the docs edit touches a
locale-bearing surface) — both 0 — as were the three roster gates whose
roster sits under a directory this diff is in (`check-changeset-fixed`,
`check:error-code-casing`, `check:filter-alias-parity`) — all 0.
- ESLint, as a **measured narrowing**: the repo's single
`eslint.config.mjs` lints `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus
`NEVER_LINTED` (no `.mdx` glob, so the two docs files are outside the
population entirely); `--format json` counted **7 files, 0 errors, 0
warnings**; and that config **never enables type-aware linting for ANY
file** — "no `parserOptions.project`, no typed `@typescript-eslint`
rules", its own words at `eslint.config.mjs:327-329` — so this diff
cannot move the verdict of a file it does not contain.
- Control characters: `grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'`
over every changed path found none (exit 1), beside a green
`check:nul-bytes`.

## Clause ②, re-declared from the actual diff

**`Clause-②: yes`, minor** — measured, not copied. `GET/POST
/api/v1/security/explain` is a published REST surface, and a request
that answered `200` with a decision body now answers `404` with a
refusal body for one input class. `ISecurityService.explain` likewise
gains a throw for that class. That is a published behaviour change on
the wire, so it is `yes` whatever its size; it is *minor* because the
code and status are ones this platform already emits, the envelope is
the one the route family already sends, no schema and no vocabulary
moved, and a resolvable object's report is byte-identical. The changeset
is `minor` on both packages for the same reason.

⛔ Not flipped to ready and not enqueued: the contract review at
`CONTRACT_REVIEW_TIER` is the PM seat's to arrange.

## Scope

Files: `packages/plugins/plugin-security/src/{errors.ts,
explain-engine.ts}` + three test files,
`packages/rest/src/{rest-server.ts, security-explain-envelope.test.ts}`,
`content/docs/capabilities/{index.mdx, permissions.mdx}`, one changeset.
Zero `packages/spec`, zero `content/docs/releases/`, zero `.tsx`.

⚠️ **One declared deviation from the dispatched file surface.** The
order named `content/docs/capabilities/index.mdx`. The literal phrase
「audit and explain」 is there, but the **product-surface promise** the
ruling says must not remain in prose is one file over —
`content/docs/capabilities/permissions.mdx:24`, "**Explain** answers
'why can this person see this record?' layer by layer", sitting beside
an **Audit** bullet that describes a real on-record timeline. That
sentence is also the only place a one-line example fits, which R1
requires. Both files are edited, both inside
`content/docs/capabilities/`. Fixing only the card description would
have left the promise itself standing, which R1 forbids.

## H17 on-hold trigger-file index

The actual diff touches **neither** held trigger file — not
`packages/rest/src/rest-route-ledger.ts` (objectstack-ai#13776) and not
`packages/plugins/plugin-security/src/security-plugin.ts` (objectstack-ai#7401,
objectstack-ai#13542). No route was added or moved, so the ledger is unchanged; the
plugin file supplies the facts this fix reads (`getObjectSecurityMeta`'s
`unresolvedCause`) and needed no edit to do so. None of the three cards
is worked, relabelled, closed or discharged here: objectstack-ai#13776 is not
addressed by this PR, and objectstack-ai#7401 and objectstack-ai#13542 remain open and held.

## Acceptance notes

- Noted, not filed: `unresolvedPostureExplainDetail`'s `'unknown'`
branch is no longer reached from the explain engine (the other two
causes still read the module, and the branch remains the module's
documented fail-safe and its `default:` arm). It is read by a pin rather
than deleted. Successor who would touch it: whoever next revisits the
objectstack-ai#10401 wording module.
- Noted, not filed: `packages/rest/src/rest-server.ts:8427` answers a
missing object with the generic `NOT_FOUND` where `mapDataError` uses
`OBJECT_NOT_FOUND` for the same condition. Untouched here — an
observation about a different route, not this card's defect. Successor:
whoever next works that meta route.

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

---------

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

Fixes objectstack-ai#18877

Clause-②: no

Maintainer decision batch objectstack-ai#157 item 5, letter C (ruling comment
`5727164909`, director seat summon objectstack-ai#24): the install contract becomes
「缺省 = 保持,有旗 = 设置」.

## The defect

`SchemaRegistry.initialDisabledPackageIds` is a **boot hydration input**
— filled once, before any registration, from the durable disable file,
and never updated by `enablePackage` / `disablePackage`. It was
nevertheless consulted by **every** `installPackage` call, so once an id
was in the boot seed set, every re-install within that boot re-landed it
disabled whatever the operator had most recently done. Since PR objectstack-ai#18752
made the durable write follow the row the door returns, that stopped
being memory-only:

```text
boot 1   operator disables the package                → disk lists the id
boot 2   seeded from disk; the package installs disabled
         PATCH /packages/:id/enable                   → 200, registry true, disk CLEARED
         install(m, { overwrite: true })   (no flag)  → the seed still listed the id
                                                      → row disabled, disk written DISABLED
boot 3   the operator's enable is gone, with no error anywhere
```

Reachable with nothing exotic: disable → restart → enable in Studio → an
SDK upgrade with `overwrite`.

## What changed — the ruling's items 1, 2 and 3

**Item 1 · `packages/objectql/src/registry.ts` · `installPackage`.** The
row is read **first**: an existing row keeps its own `enabled`, `status`
and `statusChangedAt`, and the boot seed decides only for an id that has
no row yet (boot hydration and a genuinely fresh install). A fresh id
the seed never named still lands enabled, the declared default.
`statusChangedAt` is carried, never restamped — a re-install is not a
lifecycle move.

**Item 2 · `packages/runtime/src/domains/packages.ts` · the install
door.** `enableOnInstall: true` ⇒ `enablePackage`, `false` ⇒
`disablePackage`, **absent ⇒ no lifecycle call at all**. Only `false`
was read before; the `true` case was carried by the re-install
restamping every row enabled, so under item 1 it would have silently
stopped working — the same «declared ≠ enforced» defect card objectstack-ai#18058 was
about, pointing the other way. The bare (unwrapped) body form still
honours nothing: no schema declares the key there, so it is always 「缺省」.

**Item 3 · `DELETE /packages/:id`.** The id leaves the boot seed set
with its row (inside `uninstallPackage`, downstream of the ADR-0029
refusal point, so a refused uninstall still removes nothing at all) and
its durable disable entry is cleared. Previously that record was
immortal: a delete left behind a disable naming a package that no longer
existed.

**Item 5 is not here.** It measured TRUE and rides on spec sub-card
objectstack-ai#19273; this PR does not touch `packages/spec/**` at all, which is why
it declares `Clause-②: no`.

## Item 4 — the pins that were re-ruled

Every flip is an expectation change on an existing pin, ⛔ never a
deletion. In
`packages/runtime/src/domains/packages-install-enable-on-install.test.ts`:

| pin | asserted before | asserts now | authority |
|---|---|---|---|
| `re-installing with the flag ABSENT …` | clears the durable disable —
row / registry / disk all `true` | **preserves** it — row / registry /
disk all `false`, plus `status` carried | ruling item 4, by name |
| `a BARE re-install …` | clears it too, because that form cannot ask
for `false` | **preserves** it — a body that cannot ask for a lifecycle
change is 「缺省」 | ruling items 1 and 2 |
| `a seeded id asked for enableOnInstall: true …` | SELF-CONSISTENT only
— 「the seed wins」, no claim about which state it lands in | **ENABLED**
— the flag outranks the seed; the self-consistency assertions are kept
underneath the new one | ruling item 2 |

**F1b stands unchanged**, exactly as the ruling says: every one of its
cases is a seeded id whose install asks for nothing, which still lands —
and stays — disabled. The `objectstack-ai#18058 MEASURED` matrix (`disk.has(id) ===
(row.enabled === false)`) is untouched and still holds on all eleven
arms.

New pins:
`packages/runtime/src/domains/packages-install-preserves-lifecycle.test.ts`
(the card's own scenario end to end — row, registry, disk **and** a
restart — plus both 「有旗」 arms and both halves of item 3) and
`packages/objectql/src/registry-install-preserves-lifecycle.test.ts`
(the registry's own answer, which the door's pins structurally cannot
see: on every door arm 「the registry preserved the row」 and 「the door
re-applied the same state」 produce the same three records).

## Verification

Full local suites of both affected packages, plus the derived gate set,
at the final commit.

| run | result |
|---|---|
| `pnpm --filter @objectstack/objectql test` | 301 files / 5018 tests
passed |
| `pnpm --filter @objectstack/objectql typecheck` | exit 0 |
| `pnpm --filter @objectstack/runtime test` | 269 files / 3714 passed, 1
skipped |
| `pnpm --filter @objectstack/runtime run test:repo` | 2 files / 69
tests passed |
| `pnpm --filter @objectstack/runtime typecheck` | exit 0 |
| `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` | 63 families derived, 63 run, **all exit 0** |
| `--ran` reconciliation | 63 derived, 63 accounted, 0 UNRUN, 0
NOT-MEASURED |
| `pnpm lint` (repo-wide `eslint . --no-inline-config`) | exit 0 — run
whole, so no narrowing to justify |
| four roster gates whose roster sits under these paths |
`check:authz-resolver`, `check:error-code-casing`,
`check:filter-alias-parity`, `check:route-ledger-census` — all exit 0 |

`pnpm check:dual-build-cjs-loads` first answered **exit 3 — PREREQUISITE
NOT MET** (no `dist/` for 37 packages). That is not a pass and was not
recorded as one: a full `pnpm build` (73/73 tasks) was run and the gate
re-run to exit 0.

### Reverse verification — direction predicted before running, both legs
proven on disk

Both ablations went through `scripts/ablation-replace.mjs` (anchor must
hit; the write is verified against the disk and the blob hash, never
against an exit code) and `scripts/ablation-dist-preflight.mjs` (the
mutation really reached the `dist/` the consuming suite reads, and
really left it again).

**A · revert item 1** — plant `ablation18877RevertsPreserve` so
`installPackage` never sees the existing row. Marker present in 4 built
files of `@objectstack/objectql`. Predicted red, observed red: 3 of 9
registry pins and 6 of 34 door pins failed, including the card's own
end-to-end scenario and both flipped ABSENT pins. Restored: blob
`f75d202b2b98` == HEAD, `git diff HEAD` empty, marker absent from all 14
built files, whole-tree `git status --porcelain` clean.

**B · drop item 2's `true` arm** — plant `ablation18877DropsTrueArm` to
restore the pre-ruling door (only `false` moves the registry). Marker
present in 2 built files of `@objectstack/runtime`. Predicted red,
observed red: exactly 3 pins failed, all of them `true`-arm ones — and
one of them is objectstack-ai#18058's own pre-existing `re-installing with
enableOnInstall: true clears the durable disable`, which is the
measurement showing the new arm is load-bearing rather than decoration.
Restored: blob `010a542447e8` == HEAD, `git diff HEAD` empty, marker
absent from all 6 built files, tree clean.

Both suites green again afterwards at `e40efa41b` (9 + 34 passed), which
is the commit every number above was taken at.

## Acceptance notes

- **Declared file-surface breach.** The dispatch claim named
`packages/objectql/src/` as the file surface. The ruling's items 2 and 3
land in `packages/runtime/src/domains/packages.ts`, and the pins item 4
re-rules live in
`packages/runtime/src/domains/packages-install-enable-on-install.test.ts`
— so **no arm of this card is implementable inside the declared
surface**, including a registry-only slice, which would have left
`enableOnInstall: true` silently unhonoured on an existing row. The
breach was de-risked first, not assumed: all 24 open PRs were enumerated
and **none** touches `packages/objectql/src/registry.ts`,
`packages/runtime/src/domains/packages.ts`,
`packages/runtime/src/package-state-store.ts` or the `objectstack-ai#18058` pin file.
PR objectstack-ai#18319 touches two other test files in the same directory and is
file-disjoint.
- **H17 rider objectstack-ai#12789 — SKIPPED, deliberately.** The two `[Registry]
Collision` warnings in `registry.ts` do quote the package id
inconsistently (one double-quoted at the `sys_metadata`-shadow branch,
one single-quoted at its sibling), so the defect is real and it is one
line. It is skipped for three reasons: objectstack-ai#12789 is `pm:on-hold` and its
card was not read, so which spelling it rules canonical is unknown; the
text is runtime output of a published package, so changing it grows the
changeset story this card was told not to grow; and it is unrelated to
the install contract.
- **noted, not filed:** `setPackageDisabled` failures are logged at
`warn` at all four call sites in this door (three pre-existing, one
added by item 3 in the same words). By the degradation-log rule that is
arguably a durability seam — the write claims to persist, the response
stays a clean 200/201, and the loss surfaces one restart later — but
`setPackageDisabled` is deliberately absent from
`DURABILITY_CRITICAL_CALLEES`, so escalating it would red all four sites
at once and is a vocabulary decision, not this card's. Carrier: the next
card that touches this door's durable half.
- **noted, not filed:** nothing parses an install request through
`PackageInstallRequestSchema` on the serving path —
`PackageApiContracts` has no runtime consumer and the handler reads the
raw body — so the schema's `.default(true)` is inert there today. That
is what makes «absent» visible to the door at all, and it is the
mechanism the PM's own assumption 1 predicted by a different route. It
is not filed as a defect because objectstack-ai#19273 already owns the declaration
half; recorded here so the next reader does not conclude the door is
parsing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…estTimeoutMs at the one platform fetch site (objectstack-ai#19388)

Fixes objectstack-ai#18975

Clause-②: yes

Ruling of record: comment `5729479418` — director seat summon objectstack-ai#24, batch
objectstack-ai#159 item 5, maintainer 「同意」 2026-09-18T11:42Z, letter **实现**. Not
re-adjudicated here. **The spec declarations do not move**: the
connector schema keeps every key, every bound and every default it had.

## STEP ZERO — where the platform-owned connector fetch actually lives

Measured before anything was written, on `origin/main` = `be7382d77e`
(2026-09-20T14:05Z), re-confirmed after the merge to `ada70122`.

**The ruling's wrapper already exists.**
`packages/spec/src/shared/resilient-fetch.ts` — exported as
`resilientFetch` from `@objectstack/spec/shared` — is the platform's
outbound-HTTP call: it already gave every attempt a 30s timeout and a
bounded exponential backoff with jitter and `Retry-After` handling. So
"land the wrapper there once, no gateway, no new subsystem" was
satisfiable without building anything new.

What the connectors do with it, measured per package:

| package | the one call its handler makes | before this PR |
|---|---|---|
| `connector-rest` | `rest-connector.ts` `request()` |
`resilientFetch(...)` |
| `connector-slack` | `slack-connector.ts` `callSlack()` |
`resilientFetch(...)` |
| `connector-openapi` | `openapi-connector.ts` `request()` | a naked
`fetch` — unbounded, never retried |
| `connector-mcp` | handlers to `client.callTool` | no `fetch` at all;
the MCP SDK owns the transport, with a hardcoded 30s `timeout` |

So the fetch site is **one shared wrapper plus one bypass to fold in**,
not several independent paths and not something that needed
restructuring. The gap was never "there is no wrapper" — it was that the
wrapper could not express the declared policy, and that no authored
value could reach it: `ConnectorProviderContext` carried none of the
three keys.

Holder check at claim time: across all 30 open PRs, zero touch any file
matching `connector`, `resilient-fetch` or `service-automation` (lit
control on the same scan: `packages/spec` hits 9, 22 and 9 files on PRs
objectstack-ai#19364 / objectstack-ai#19363 / objectstack-ai#19335).

## What landed

**One wrapper, extended by exactly what was missing.**
`ResilientFetchOptions` gains `strategy`, `backoffMultiplier`,
`maxDelayMs`, `jitter` and `retryOnNetworkError`. **Each defaults to the
behaviour the wrapper already had**, so a caller that passes none is
byte-identical to before.

**One mapping.** `connectorFetchOptions()`
(`packages/spec/src/integration/connector-fetch-policy.ts`) is the
single place a connector's declared policy becomes wrapper options — one
execution site, not one per connector package.

**One contract widening.** `ConnectorProviderContext` gains
`retryConfig`, `connectionTimeoutMs` and `requestTimeoutMs`, read-only
and resolved: the materializer parses `retryConfig` through
`RetryConfigSchema`, so a factory reads real values instead of
re-deriving the schema's defaults. The policy also joins the instance
signature, so editing it re-materializes the connector instead of
leaving the old policy serving until restart.

**The built-in HTTP providers honour it by construction.** `rest` and
`openapi` pass the context's policy into their connector builders.

✅ **RESOLVED at 2026-09-20T17:02:28Z — the work is on the branch; the
push simply lagged the report by about two minutes.** Kept in full
rather than deleted, because the sequence is worth more than the tidy
version. At **17:00:04Z** the remote tip was `4432f967e3` with an
18-file diff and ⛔ none of the three files below; the dev's report
already described them at head `5911c8cf`. The seat held the review and
struck this paragraph. At **17:02:28Z** `git ls-remote` reports the tip
as **`5911c8cf664f534823d598c801f852c346be8075`** — `5911c8cf
docs(spec): the connector header and SYNC_ARCHITECTURE describe the
implemented behaviour` sitting on top of `4432f967` — **21 files**, all
three present. ⇒ the 17:00Z reading was **true when taken** and is now
superseded; the report was accurate about content and early about the
push. ⭐ The rule that survives, and it is not 「the check was wasted」:
**`git ls-remote` is the authority and the PR object is not** — while
this was being checked the PR object was still serving the stale 18-file
count. ⛔ A conclusion drawn from a summary face has a shelf life; one
drawn from the ref does not.

**The teaching text objectstack-ai#18794 narrowed is corrected to describe the
implemented behaviour** — `packages/spec/docs/SYNC_ARCHITECTURE.md` in
**five** places, plus the `connector.zod.ts` header TSDoc it renders
from (`content/docs/references/integration/connector.mdx` follows by
`gen:docs`, ⛔ never hand-edited). Those passages asserted the keys were
「declared but currently unimplemented」 and that
`ConnectorProviderContext` could never carry them; **both are now
false**. This is the ruling's third bullet, ⛔ not an absorption of
objectstack-ai#18794. ⭐ `health.circuitBreaker` and `connectionTimeoutMs` are
explicitly kept named as **still inert** in every corrected passage.

⚠️ ⭐ **Found by hand, ⛔ not by the drift bot — and it is the bot's own
declared blind spot doing exactly what it warns about.**
`SYNC_ARCHITECTURE.md` states the rule by its **inputs**, so it shares
no identifier with the emitter this diff changed and ⛔ no run could ever
have listed it. The bot's three named pages were each hand-verified and
**two were ACCURATE and left untouched** — `error-catalog.mdx`'s
`no_retry` is the API error-envelope enum from `api/errors.zod.ts`, a
different enum this diff never touches, and `jobs.mdx` is
`job.retryPolicy` from `shared/retry-policy.zod.ts`, likewise untouched.
The third was accurate too, and it is the one that falsified the code.

**Two interpretive calls, both stated rather than assumed:**

- 🔴 **`maxAttempts` counts TOTAL calls, the first included** — the
contrast `content/docs/automation/flows.mdx` already draws against
`maxRetries`, and it is what **corrected this implementation**. ⚠️
**Replaced by the seat 2026-09-20T17:00Z.** This bullet previously read
「counts **retries**, not total calls」, reasoning from `min(0)` and a
`shared/retry-policy.zod.ts` comment the dev has since said it
**over-read** (that comment is about opt-in vs opt-out defaults, ⛔ not
the counting base). The first reading reached a pushed commit; it was
falsified by a **documentation page**, and the implementation was
changed to match the page — ⛔ not the other way round. New pin:
`maxAttempts: 3` must make **three** calls, ⛔ not four, the case that
tells the two readings apart. Ablation: restoring `+ 1` turns 3 mapping
tests red.
- `maxDelayMs` is applied **after** jitter. Jitter is additive, so
capping first would let a delay land up to 99ms above the declared
ceiling.

## The seat's assumption 4 is falsified, and that is the one thing the
ruling asked me to report rather than invent

`AbortSignal.timeout` **is** available (Node 22 or newer, which the root
`engines` field pins; already used at
`packages/drivers/driver-turso/src/turso-driver.ts`). The
connection-vs-request distinction is **not**.

A connector's call is a WHATWG `fetch`, whose only cancellation surface
is one `AbortSignal` over the whole operation; nothing in that interface
observes the connection phase separately. Bounding time-to-response with
`connectionTimeoutMs` would kill a slow-but-connected upstream that the
author meant to allow with a large `requestTimeoutMs` — breaking the
very promise the key makes. Node's undici exposes `connectTimeout`
through a custom dispatcher, which is Node-only and a new subsystem
underneath every connector: the same ruling forbids it.

So `connectionTimeoutMs` is **carried** onto `ConnectorProviderContext`
(a custom provider on a transport that can separate the phases may
honour it) and **not enforced** by the platform.
`packages/spec/liveness/connector.json` keeps that one row `dead`, with
the measurement written into it, and a pin in
`connector-fetch-policy.test.ts` goes red if anyone aliases it onto
`timeoutMs`. **Nine of the ten rows flip, not ten.** The tenth is owed a
second, narrower ADR-0049 decision — see the acceptance notes.

## Verification

Full pipeline at the final commit `956fdb10`.

**Gate family**, re-derived in this worktree from the real changed paths
(`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`), every command run with its exit code
captured before any pipe, then reconciled with `--ran`:

```
dispatch-gates --ran: 86 derived family(ies) accounted for — 83 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3).
```

The three NOT MEASURED are `PREREQUISITE NOT MET`, not findings:
`check:dual-build-cjs-loads` and `check:type-check-debt` both need a
whole-repo build closure (CI builds it before those steps), and
`check-plugin-teardown-shape.mjs --self-test` cannot reach its
commit-pinned positive control on a shallow clone. All three are CI's to
run.

**Tests** (all on the merged tree):

```
@objectstack/spec                503 files / 14711 tests  passed
@objectstack/service-automation  140 files /  1676 tests  passed
@objectstack/connector-openapi     4 files /    34 tests  passed
@objectstack/connector-rest        4 files /    24 tests  passed
@objectstack/connector-mcp         3 files /    23 tests  passed
@objectstack/connector-slack       3 files /    10 tests  passed
```

`typecheck` green for all six. `pnpm --filter @objectstack/spec
check:generated`: 15 of 15 artifacts up to date (`api-surface/` and
`export-origins/` regenerated after a real build — the two new exports
plus the `ResilientFetchOptions` re-export).

**Lint — measured whole, not narrowed.** `eslint . --no-inline-config
--format json` over the repo root: **6939 files, 0 errors, 0 warnings**,
exit 0.

**Ablation — both pins proved able to fail**, through
`scripts/ablation-replace.mjs` (anchor must hit, blob hash must move,
restore proved against `HEAD`):

| mutation | result |
|---|---|
| `connector-fetch-policy.ts`: invert the early return so a declared
`retryConfig` is never mapped | 9 of 10 mapping tests RED, restored blob
== HEAD |
| `rest-provider.ts`: stop passing `ctx.retryConfig` into the connector
| 4 of 5 provider retry pins RED, restored blob == HEAD |

The pins assert **call counts and delay sequences**, not the presence of
a field: a pin on the def's `retryConfig` could not have failed here,
because the key was already storable and served back before any of this
landed. The sharpest one is the narrowing case — an authored
`retryableStatusCodes: [429]` must leave a 500 unretried, which only
passes if the authored list is the one executed (500 is retryable under
both the wrapper's own default and the schema default).

## Acceptance notes

**To file (class (c), an authoring trap that survives this PR):**
`connector.connectionTimeoutMs` still parses, still stores, is still
served back by `/meta/connector`, and is enforced by nothing — for the
measured reason above, which is a property of `fetch`, not an omission
here. It now needs a decision this card's ruling did not answer: retire
it, or re-describe it as something the platform can enforce (its sibling
`requestTimeoutMs` already is). Reproduction: declare a `connectors:`
entry with `provider: 'rest'` and `connectionTimeoutMs: 1000`, point
`providerConfig.baseUrl` at an endpoint that takes 5s, and dispatch the
`request` action — it completes normally. Dedupe words:
`connectionTimeoutMs declared unenforced` · `connector connect timeout
AbortSignal fetch` · `ADR-0049 connectionTimeoutMs second decision` ·
`connector.json connectionTimeoutMs dead row` · `retire or redescribe
connect timeout`.

**Fixed in place, declared here rather than filed:**
`connector-openapi`'s generated actions went through a naked `fetch` —
unbounded, never retried, and the one built-in HTTP path an authored
policy could never reach. It is the same defect on the same measured
site as this card's, the fix is mechanical and its shape was already
pinned by two sibling connectors, no other open PR holds the file, and
it adds no new gate family. Those actions now go through the same
wrapper as `connector-rest` and `connector-slack`. Evidence:
`openapi-connector.ts` `createOpenApiConnector` — `doFetch(url, init)`
became `resilientFetch(url, init, fetchOptions)`;
`@objectstack/connector-openapi` 34 tests still pass.

**Noted, not filed:**

- `ConnectorProviderContext.icon` and `.type` are set by the
materializer and read by none of the three shipped provider factories,
so an authored `icon:` or `type:` on a declarative instance never
reaches `GET /api/v1/automation/connectors`. Already recorded per-row in
`packages/spec/liveness/connector.json`, with what is owed already
stated there. Next toucher: whoever adds or changes a provider factory.
- `connector-slack` ships no provider factory, so nothing authored can
reach it — only the plugin door, hand-wired by its host. Not silent: an
unknown `provider` is a loud, named boot failure that lists the
installed ones. Next toucher: whoever adds a `slack` provider key.
- The `connector-rate-limit-config-removed` comment in
`packages/spec/src/conversions/registry.ts` says `retryConfig` and the
timeouts "are live". It was wrong when written (the ledger's
`retryConfig.strategy` row corrects it by name), and this PR makes nine
tenths of it accidentally true. A stale comment, no behaviour. Next
toucher: whoever edits that conversion entry.
- `health.circuitBreaker`'s sub-keys are `dead` on the same schema and
the same ADR-0049 worklist. Out of this card's scope by the card's own
words ("本卡只管这三个"), and its teaching text was already stanched by objectstack-ai#18983.
Next toucher: the next ADR-0049 connector sweep.
---

## Round 2 — both at-tier FAIL items fixed, at head `4a9b3480f2`

Review record `5751411253` FAILed this PR on two items. Both are fixed,
pinned and ablated; ⛔ nothing else was widened, and the two optional
notes the review offered (the `.describe('Maximum retry attempts')`
counting-base wording, and the pre-existing
`Retry-After`-on-any-retryable-status and unbounded-body-read
observations) were **deliberately not acted on**.

### FAIL 1 — the openapi routing is now pinned

The review's ablation proved this PR's own justification false: 「shape
already pinned by two sibling connectors」 did not hold for **this file**
— restoring the naked fetch left **34/34** openapi tests green.

Two cases added in `openapi-provider.test.ts`, through the factory with
`retryConfig` on ctx, mirroring `rest-provider.test.ts`: scripted fetch
`[503, 200]`, `{strategy: 'fixed_delay', maxAttempts: 2, initialDelayMs:
100, retryableStatusCodes: [503], jitter: false}`, asserting **exactly
2** upstream calls and a 200. The review's own ablation reproduces on
the same blobs (`a0172843ccb9` → `22b1470c9170`) and now turns the retry
pin **RED** where it measured 34/34 green; restore proved blob == HEAD.

⚠️ **Precision, stated rather than glossed: only 1 of the 2 new cases
discriminates.** The narrowing case cannot — with a naked fetch nothing
retries, so 「1 call」 is what **both** trees produce. It is kept for what
it pins, ⛔ not as a revert detector.

### FAIL 2 — `maxDelayMs` is now a maximum. Route (a), and the reason

The review offered two routes. **Route (a)** was taken: a `Retry-After`
longer than `maxDelayMs` now **ends the retry loop and returns the
response**.

⭐ **Why (a) and not (b):** this card exists to make a declaration equal
its enforcement, so making 「Maximum retry delay in ms」 **true** beats
documenting an exception to it. Route (b) would have left a key whose
*name* says maximum with an upstream-controlled way past it — which is
the exact shape **objectstack-ai#19410** was filed for earlier today.

The three alternatives, and why returning wins: sleeping it out makes
the key **not a maximum**; retrying sooner than asked is the abuse
`Retry-After` exists to prevent; **returning** hands the caller the real
status and its header. Only a `Retry-After` can reach that branch,
because `backoffMs` caps its own output — so the review's jitter-cap lit
control is untouched.

**Pinned** at `maxDelayMs: 1000` + `retry-after: 3600` → 1 call, the 429
returned, `sleep` **never called**, with a control that a `Retry-After`
**within** the ceiling is still honoured and still retried.
**Ablation**: deleting the guard (`bab28fcb1bde` → `9564b3126ef7`) turns
it RED; restore proved blob == HEAD.

⇒ the `retryConfig.maxDelayMs` ledger note **and** the changeset
sentence were both corrected, so ⛔ no artefact still claims a ceiling
the code ignores.

### Verification at the pushed head

Gate family re-derived on the pushed tip **and again** on the fix commit
— **identical 109 families** both times: **107 green / 2 NOT-MEASURED /
0 red / 0 unrun**. The two NOT-MEASURED are the shallow-clone self-test
and `check:dual-build-cjs-loads` needing the full build closure — ⛔ exit
3 is a prerequisite, ⛔ not a red. `check:generated` 15/15 with **no
regeneration owed** (route (a) moved no `.describe()`, so
`connector.mdx` did not move). Tests: openapi **36** (was 34), rest 26,
slack 10, spec wrapper+mapping 29. Full-repo lint 6,945 files, **0
errors, 0 warnings**.

⏹ ⚠️ **Overtaken and corrected 2026-09-20T20:37Z — the merge WAS
taken.** The paragraph below was true when written and is spent; kept
struck rather than deleted, because the reason it gave is the reason
round 3 exists.

> ~~`origin/main` has moved 12 commits since this branch's single merge
of record (`a88a9733`). It was **deliberately not re-merged**: the
at-tier record is keyed to a head sha, and a fresh merge creates a head
the record does not name. The merge is taken when the review is clear, ⛔
not while it is in flight.~~

---

## Round 3 — `origin/main` merged, the head re-reviewed, and the
base-drift cost paid

Once round 2 cleared, the base was **21** commits stale and the landing
needed a fresh CI run, so `origin/main` `576d5df6` was merged once as
**`13987f1b`**. ⛔ No rebase, ⛔ no amend, ⛔ no force-push, ⛔ no empty
commit.

**The merge is provably automatic**: `13987f1b` has exactly two parents
(`4a9b3480`, `576d5df6`), and `git merge-tree --write-tree 4a9b348
576d5df` yields tree `8350d10d`, which **equals** `13987f1b^{tree}` ⇒
no hand resolution existed. The two sides are disjoint — this PR 22
files, main 83, intersection **0** (lit control: both lists non-empty).

**This PR's own delta did not move**: 22 files, +1197/−132, the same
file list as before the merge.

**Neither guard was undone.** Both blobs are byte-identical to round 2,
and both ablations still give **exactly 1 red** — the openapi routing
pin (35 passed) and `maxDelayMs bounds a Retry-After by STOPPING` (18
passed, the within-ceiling and jitter-cap controls green by name).
Restores proved blob-equal to HEAD.

**Gates: 110 derived / 109 green / 1 NOT-MEASURED / 0 red / 0 unrun.**
One family *appeared* with the incoming commits
(`check:issue-citations`, wired in by `5a5e710f`); a true set comparison
against a round-2 derivation gives only-in-r3 = that one, only-in-r2 =
none. The NOT-MEASURED is `check-plugin-teardown-shape --self-test` at
exit 3 — a shallow-clone prerequisite, ⛔ not a red. `check:generated`
15/15 with no regeneration owed, the migration registry included after
main deleted ten entry files. Whole-repo lint 6,943 files, 0 errors, 0
warnings.

⭐ **Honest cost, stated because the reviewer found it and the dev's
number alone would have hidden it**: the first gate reading on a
turbo-cache-restored closure was **108 green + 2 exit 3**, not 109 + 1 —
`check:type-check-debt` refused on a dist whose mtimes predated its
sources, and reached exit 0 only after a real rebuild. Same conclusion,
named with what it cost.

**CI on `13987f1b`: 35 names, 33 success + 2 skipped, 0 failing, 0
pending** — all six `Test Core` shards green, including the 2/6 shard
that was red before. ⚠️ A green re-run **corroborates** that the earlier
red was not this PR's; it ⛔ does not prove it. The load-bearing evidence
is still the mechanism filed as **objectstack-ai#19424** (`80 × 0.25 s = 20 s` against
an observed 20,999 ms, the same titled assertion passing in 299 ms in
the same run).

⭐ **And one thing this PR previously could not establish, now
established from a different door**: `GET /branches/main/protection`
answers **403**, but `GET /rules/branches/main` answers **200** and
lists seven required contexts — `Test Core` among them, all seven
`success` here. **A 403 on one endpoint is a fact about that endpoint, ⛔
not about the question.**

**Round-3 at-tier review: PASS** — record `5752480809`, keyed to this
head. `check-clause2-carriers --pair 19388` exits **0** with zero ✗
rows, run *after* the record existed.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — b37bba44 Deployed Jan 19, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/xs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant