Repository navigation
chore(ci)(deps): bump github/codeql-action from 3 to 4 - #24
Merged
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
dependabot
Bot
deleted the
dependabot/github_actions/github/codeql-action-4
branch
February 1, 2026 02:48
3 tasks
This was referenced Sep 1, 2026
This was referenced Sep 3, 2026
This was referenced Sep 4, 2026
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 16, 2026
This was referenced Sep 18, 2026
This was referenced Sep 18, 2026
This was referenced Sep 19, 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
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.
Bumps github/codeql-action from 3 to 4.
Release notes
Sourced from github/codeql-action's releases.
... (truncated)
Changelog
Sourced from github/codeql-action's changelog.
Commits
44e589bFix feature flag lookup when uploading DB0d648ebMerge pull request #3380 from github/dependabot/github_actions/dot-github/wor...3fd7db8Merge pull request #3379 from github/dependabot/npm_and_yarn/npm-minor-1607f6...6b11018Rebuildd0d445fBump ruby/setup-ruby60b2ba3Rebuild709d6deBump the npm-minor group with 4 updatesDependabot 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 rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot mergewill merge this PR after your CI passes on it@dependabot squash and mergewill squash and merge this PR after your CI passes on it@dependabot cancel mergewill cancel a previously requested merge and block automerging@dependabot reopenwill reopen this PR if it is closed@dependabot closewill close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill 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 versionwill 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 dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)