Repository navigation
docs(spec): SYNC_ARCHITECTURE stops teaching retryConfig as the rate-limit remedy - #18979
Conversation
…e-limit remedy Four passages in `packages/spec/docs/SYNC_ARCHITECTURE.md` taught authors that a rate-limited upstream is answered by `retryConfig` and printed its `retryableStatusCodes` defaults (429 included), while nothing reads the key. They now say what is measurably true today — declared but currently unimplemented — and point at `packages/spec/liveness/connector.json`. Measured on this tree, fold-proof read-shaped probe with a firing control in the same run: `.retryConfig` / `.connectionTimeoutMs` / `.requestTimeoutMs` have 0 read-shaped consumers outside `packages/spec`; the sibling key `providerConfig` has 15 across 9 files. `ConnectorProviderContext` carries none of the three, so a provider factory cannot honour them either. No declaration, schema or accept set is touched: the ADR-0049 ruling on these keys is deliberately not prejudged. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
扩面确认;并认下:我那道栅栏划得太宽派发席( 一、把
|
The `connector.zod.ts` L3 header still taught that `retryConfig` (with its `retryableStatusCodes` `429` default) and `health.circuitBreaker` are what L3 declares for a rate-limited upstream. Both keys parse and store, and nothing reads either: `packages/spec/liveness/connector.json` records every `retryConfig` sub-key and every `health.circuitBreaker` sub-key as `dead`. Replace that one sentence with the wording PR #18979 landed for the same claim in `packages/spec/docs/SYNC_ARCHITECTURE.md`: declared but currently unimplemented, pointing at the liveness ledger, and explicitly neither retired nor left to the host. Prose only; no schema, declaration or accept set moves. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…ion's two halves one call (objectstack-ai#19005) Part of objectstack-ai#18670 — item 2, the **third** of the ruling's four named arms. objectstack-ai#18670 remains open: banned keys is still untaken, and this body deliberately carries no closing keyword for that number. Clause-②: yes (narrowing) Director ruling batch objectstack-ai#154 item 3, letter **C** (comment 5725370614, maintainer 「同意」): 「the projection emits a refinement only where the rule is a complete, mechanically derivable JSON Schema pattern — banned keys, required-one-of, non-blank — one ledger row at a time; everything else stays annotated as `x-dropped-refinements`」. Continues PR objectstack-ai#18952 (squash `5e5ec9fa42194723cc523a274e7221c8447c4487`), which landed `required-one-of` and `non-blank-string`. ## 1. The arm: `dependentRequired` `data/SSLConfig`'s refinement is `hasCert === hasKey` — precisely `dependentRequired { cert: ['key'], key: ['cert'] }`. It is emitted through the same closed-vocabulary mechanism the previous arm built: `src/shared/refinement-projection.ts` declares, `scripts/lib/refinement-projection.ts` emits. No second mechanism was introduced. **Exact, not approximate.** A key absent from a JSON object is the only way for its value to read `undefined`, and `dependentRequired` triggers on PRESENCE — so a key present with any JSON value, `null` included, arms its dependency exactly as the predicate's `!== undefined` does. The dependency map is read once into the declaration and the predicate reads it from there, so the published keyword and the enforced rule cannot name different keys. ### Ledger: the rows retired, by name `packages/spec/dropped-refinements.baseline.json`, **201 entries / 553 sites → 200 / 551**: | row | before | after | |:---|:---|:---| | `data/SSLConfig` | `sites: [""]` | **deleted** — drops nothing now | | `data/SQLDriverConfig` | `sites: ["", "sslConfig"]` | `sites: [""]` — the `sslConfig` site closed | 1 row deleted, 1 row shrunk, **2 sites closed, 0 sites added anywhere**; the ledger diff is deletions only. Generator census after: 551 dropped across 200 published schemas, **199 projected** — 137 `required-one-of`, 60 `non-blank-string`, **2 `dependent-required`** — 3 undecidable. `data/SQLDriverConfig`'s remaining `""` site is its **own** separate rule, "`sslConfig` is required when `ssl` is **true**". That judges a VALUE, is `if`/`then` rather than this arm, and correctly stays dropped and annotated. ### Banned keys (`propertyNames` / `not`) — NOT taken, and not forced Confirmed against the tree, not assumed: the nearest sites judge a banned VALUE on a string (`FILTER_ARRAY_LOGIC_KEYWORDS`) or an allowed key set that is data-dependent (`ai.paramHints` against the action's own params). Neither is mechanically derivable, so **no candidate was constructed**. This is why the body says `Part of` and carries no closing keyword. ## 2. Mechanism fix A — the verdict is per NODE, the rules are per CHECK `verdictFor` compared a node with ALL custom checks against the node with NONE, so any one declared arm marked the whole node `projected`. Reproduced on the landed code before changing it: ``` mixed(declared+undeclared) dropped: [] projected: [{count: 2, declaredPatterns: ['non-blank-string']}] undeclared-alone (lit) dropped: [{count: 1, declaredPatterns: []}] projected: [] declared-alone (lit) dropped: [] projected: [{count: 1, declaredPatterns: ['non-blank-string']}] ``` A second refinement on a declared node was therefore neither ledgered nor annotated, and the generator's UNDECLARED line could not see it — silently violating the ruling's own 「A refinement that is not one of these named patterns stays dropped and annotated」. **Fix:** `projected` now requires `customs.length === declaredPatterns.length`; anything else is `dropped` conservatively. The RAW differential is kept as a new `projectionMoved` field so the detector still MEASURES rather than asserts — collapsing it would have made the instrument blind to the zod upgrade it exists to notice — and the generator prints partially-stated sites on their own line. **Ablation, both directions** (anchor-verified on disk, `scripts/ablation-replace.mjs`): | leg | blob | result | |:---|:---|:---| | mutated — drop the `total === stated` guard | `54ed82dbe4c2` to `2c1bff777363` | **1 test red**, 42 green: "a DECLARED arm beside an UNDECLARED rule stays `dropped`" | | restored | back to `54ed82dbe4c2`, `git diff HEAD` empty | **43 / 43 green**; mutant text on disk 0, guard text 1 | ## 3. Mechanism fix B — generator/detector coupling, by construction `build-schemas.ts` (three `toJSONSchema` calls) and `projectOrNull` each passed the `override` independently. **Measured on the pristine base** with only the generator's import stubbed out: | leg | gate exit | `shared/Expression.json` `allOf` | `x-dropped-refinements` | files carrying the non-blank pattern | |:---|:---|:---|:---|:---| | base, untouched (dark control) | 0 | present | absent | **35** | | base, generator-side override dropped | **0 — GREEN** | **absent (wide)** | **absent (SILENT)** | **0** | Census identical to an untouched run (553 / 201 / 197). That is the item-1 silence restored, standing behind a green ratchet — worse than the state the card was filed about, because the ledger now certifies it. A merge-conflict resolution was enough to cause it. **Chosen fix: one shared projection helper** — `projectPublishedJsonSchema` in `scripts/lib/refinement-projection.ts`. All three generator calls, the union-branch projector behind the third, and the detector's differential now reach `z.toJSONSchema` through it, and `projectByPruningUnionBranches` no longer takes an `override` option at all. There is no argument left for a caller to forget. **Why the sandbox-builder pin was rejected**, not overlooked: a pin *detects* after the fact and can be skipped, deleted or made vacuous, and it leaves the two-argument shape in place so the next merge conflict can still separate them. The choke point makes the one-sided failure **unrepresentable** rather than caught. Both halves now lose the override together or not at all — which is what turns the ablation from silent into loud. The test file's own `publish()` helper was rewired through the same call for the same reason, so the unit pins measure the real seam rather than a re-spelling of it. **Ablation, both directions:** | leg | gate exit | `Expression.json` `allOf` | `x-dropped-refinements` | |:---|:---|:---|:---| | mutated — override removed from the ONE helper | **1 — RED**: 46 undeclared schemas + 76 miscounted ledger entries | absent (wide) | **present (annotated)** | | restored | 0 | present | absent | The contrast is the whole point: before, one-sided removal was green and silent; now it is red **and** the file confesses. ## 4. Contract: the published file narrows toward what the runtime already refuses **Whole published tree, base vs head:** 1530 of 1532 files byte-identical. The two that move are `data/SSLConfig.json` and `data/SQLDriverConfig.json`, each gaining `dependentRequired` and losing the matching `x-dropped-refinements` row. Nothing else in `packages/spec/json-schema/**` changed. **Parse-equivalence probe — 10,368 documents** (2,592 SSLConfig-shaped over the full presence lattice of 4 keys times 6 value shapes including `null`, a wrong type and an unrecognised extra key; 7,776 SQLDriverConfig documents embedding each of those under three `ssl` states). Published-side verdicts computed with ajv 8.20.0 (draft 2020-12) against the two real snapshots. | reading | SSLConfig | SQLDriverConfig | |:---|--:|--:| | documents | 2,592 | 7,776 | | runtime accepts | 60 | 180 | | published accepts, base | 27 | 54 | | published accepts, head | 15 | 30 | | **narrowed by this arm** | **12** | **24** | | widened | 0 | 0 | | **documents the runtime ACCEPTS that the published file now refuses** | **0** | **0** | **Runtime behaviour did not move.** The runtime verdict vector is byte-identical at merge base and head over all 10,368 documents — sha `9e7c848f04e0c687` (SSL) and `4f18f835d4d1a62e` (SQL) on both sides. The base leg was run against the real base blobs (`git checkout` of the two source files at `d8b12fca9`, blob hashes asserted both ways, restore proven by an empty `git diff HEAD`), not against a retyped predicate. **LIT CONTROL for that zero** — weakening the dependency map to one direction (`{ cert: ['key'] }`) moves **96 documents** (24 SSL + 72 SQL) and lifts runtime accepts from 60 to 84 and 180 to 252. The zero is a reading, not a silence. Note the published-accepts figures sit below runtime-accepts on both sides: `SSLConfig.json` is the OUTPUT shape and lists `rejectUnauthorized` as required because the runtime applies its `.default(true)`. That asymmetry is pre-existing, is the `x-io` convention, and is unchanged by this PR — it is reported rather than netted out. ## 5. Verification - **Gates:** derived from the merge base with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, re-derived after the `origin/main` merge (identical, **84 commands**). Every exit code captured by redirecting to a file first, never through a pipe. **80 exit 0, 0 findings.** The remaining 4 — `check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt` — exit **3**, which those gates define as `PREREQUISITE NOT MET` ("Nothing was measured ... It is NOT a finding"): each reads BUILT output of packages outside this diff. They are **NOT MEASURED**, not red; the re-run against a full build is reported on the card. - The derivation's own caveats are carried, not netted out: 50 artifact-roster families score `silent` for every card in the tree, 11 declare a population too wide to place, 5 take a value from the workflow, and 5 path-scheduled CI jobs run 30 steps with no local invocation. None of those is a clearance, and CI owns them. - **`pnpm --filter @objectstack/spec check:generated`:** all 16 generated artifacts up to date. **`content/docs/references/**` does not move** — see acceptance notes. - **Targeted tests:** `scripts/refinement-projection.test.ts`, `scripts/dropped-refinements.test.ts`, `scripts/union-branch-projection.test.ts` — **91 / 91**. `packages/spec` typechecks clean (`tsc --noEmit` over both the package and `tsconfig.scripts.json`). The full `@objectstack/spec` suite reading is on the card. - **Lint, declared narrowing:** eslint run over the 9 changed lintable files, 0 errors / 0 warnings, file count read from `--format json`. The population is `eslint.config.mjs`'s own `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`; the config states in its own words that this repo "never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file", so this diff cannot move the verdict on a file it does not touch. The repo-wide sweep is CI's run. - `origin/main` merged through `bash scripts/pm/os-regen-merge.sh` (no rebase, no force-push). It brought one docs-only commit, objectstack-ai#18979, overlapping none of this branch's paths and no `merge=os-regen` path. The previous arm's implementation body was asserted still present by quoted-exact-name `git grep` against `origin/main`, with a dark control at 0. ## Acceptance notes Noted, not filed — out of scope for this card and not one of the three filable classes: - `packages/spec/scripts/build-schemas.ts` (the authorable-surface docblock, near line 846) still names the retired `api-surface-signatures.json`. The previous seat handed this to "the next editor of `build-schemas.ts`", which is this PR. It is left untouched deliberately: it is a stale code comment, not a defect, a contract violation or an authoring trap, and the bounded in-place exemption requires the finding to be **the same defect class as this card**, which it is not. Carrier: the next PR that edits that docblock for its own reasons. - The dispatch's overlap warning — that a new keyword might move `content/docs/references/**`, four pages of which open PR objectstack-ai#18985 edits — **measured FALSE**. `dependentRequired` is a sibling keyword the reference renderer does not read, `check:docs` is green and `check:generated` reports all 16 artifacts current. No reference page moves, so there is no collision with objectstack-ai#18985 on that directory. Reported for the seat to file (a candidate class-(b) finding, deliberately NOT fixed here): - `packages/spec` ships `src/**/*.zod.ts` in `files[]`, and `scripts/check-published-files.mjs` allows it with the reason "The Zod schemas are themselves the contract (Prime Directive objectstack-ai#1); **downstream code imports them directly**, so these sources are product rather than build input." Two measurements contradict that reason: (1) the package's `exports` map exposes no `./src/*` subpath and no wildcard, so no consumer can import those files at all; (2) 188 of the 202 shipped `*.zod.ts` files carry a relative import resolving to one of 35 modules under `src/` that the glob does NOT ship (`src/shared/lazy-schema.ts` alone is imported by 181 of them), so they would not resolve even if reachable. Overwhelmingly pre-existing and far outside this card; this PR adds the third importer of one of those 35. Not verified by `npm pack` and not by a real consumer import — that is the next step for whoever takes it. --- _Generated by [Claude Code](https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… rate-limit remedy (objectstack-ai#19040) Fixes objectstack-ai#18983 Clause-②: no ## What moves One sentence of the connector header TSDoc in `packages/spec/src/integration/connector.zod.ts`, plus the reference page `gen:docs` renders from it. **Prose only.** No schema, declaration, default or accept set moves, and the fate of these keys stays ADR-0049's to rule on rather than being prejudged here. The header ended its "no outbound rate limiting" paragraph by naming a remedy: *"What L3 does declare for a rate-limited upstream is `retryConfig` — whose `retryableStatusCodes` default `[408, 429, 500, 502, 503, 504]` includes `429` — and `health.circuitBreaker`."* PR objectstack-ai#18979 retired that same claim from `packages/spec/docs/SYNC_ARCHITECTURE.md`; this file is where it was authored, and the generated page carried it downstream. The replacement carries the wording objectstack-ai#18979 landed: both keys are **declared but currently unimplemented**, with a pointer to the liveness ledger, and explicitly neither *retired* nor *left to the host*. ## Why the sentence was false — re-measured on this branch, not inherited `packages/spec/liveness/connector.json`, read at `ed63e0d39c`: | ledger rows | status | verifiedAt | |:---|:---|:---| | `retryConfig.*` — all 8 sub-keys | `dead` | 2026-09-17 | | `health.circuitBreaker.*` — all 7 rows | `dead` | 2026-09-17 | | `health.healthCheck.*` — all 8 rows | `dead` | 2026-09-17 | | **`providerConfig` — the must-answer live control, same read** | **`live`** | 2026-09-17 | A `dead` column with no live row beside it would only show the instrument answering one way, so the control is part of the reading. The "not left to the host" half is re-verified here too, at source rather than cited: `ConnectorProviderContext` (`packages/spec/src/integration/connector-provider.ts:57`) declares exactly `name`, `label`, `description`, `icon`, `type`, `providerConfig`, `auth` and `loadPackageFile` — eight members, none of them `retryConfig` or `health`. A provider factory is never handed either key, so it has no way to honour one. ## Regeneration leg `pnpm --filter @objectstack/spec gen:docs` moved exactly one tracked file, `content/docs/references/integration/connector.mdx` (sha256 `640ea9d5…` to `ef0d11fc…`), and `check:docs` is green on the merged tree. The page is not hand-edited. One prediction on the card did not hold, reported as measured: the page's `retryableStatusCodes` occurrence count stays **4**, not lower. One of the four is inside the historical sentence this change quotes; the other three are the generated field tables for `RetryConfigSchema`, which this change does not touch. ## Zero-hit reading, with its radius The corrected sentence spans three comment lines, so a per-line `grep` reads `0` for it while it is plainly there — that reading is blind, not clean. The sweep here strips comment prefixes and collapses newline-bearing whitespace before matching. - **Claim.** The assertive adjacency `upstream gateway.** What L3 does declare` occurs **0** times in the tree at `ed63e0d39c` (10,270 files scanned). - **Radius.** Content layer, over the working tree only; extensions `.ts .tsx .mts .mjs .js .md .mdx .json`; `node_modules`, `dist`, `.git`, `.turbo` and `.cache` pruned. - **A known target that must be outside it.** The blob at `96cf32b075` still holds that exact string. Extracted to a file *inside* the radius the same instrument reads it (2 hits), so the probe is live; scanning the tree it reads 0, because git objects are outside the radius by construction. ## Published-surface reading — per file, which is why this is not `skip-changeset` | file in this diff | on a published `files[]`? | how measured | |:---|:---|:---| | `packages/spec/src/integration/connector.zod.ts` | **yes** | `@objectstack/spec` ships `src/**/*.zod.ts`; `npm pack --dry-run` lists the path among the tarball's 2,039 files | | `content/docs/references/integration/connector.mdx` | no | 0 of the 70 published workspace packages can reach `content/docs/**`; same instrument does see `@objectstack/spec`'s `src/**` entry, so it is not blind | | `.changeset/18983-connector-header-rate-limit-remedy.md` | no | changeset input, consumed at release | The edited file is itself shipped, so the corrected text reaches consumers: this takes a **`patch`** changeset, not `skip-changeset`. The header TSDoc does not reach `dist/`, so `src/**/*.zod.ts` is its only published carrier. ## Verification All of the below ran at `ed63e0d39c`, after `origin/main` was merged in (`packages/spec` had moved on main, so §10's rebuild-and-recheck applies). - **Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **100** commands from a non-stale tree. Each ran with its exit code written to disk before any pipe, then reconciled with `--ran`: **100 derived, 99 run green, 0 findings, 1 NOT MEASURED, 0 unrun.** - **NOT MEASURED (1).** `pnpm check:dual-build-cjs-loads` exits **3** — `PREREQUISITE NOT MET`, its own distinct code for "nothing was measured". It reads built output for 26 packages still without a `dist/` (apps, connectors), which needs a whole-workspace build; CI's required `Lint & Repo Gates` job builds everything and runs it. Five other gates refused the same way on first pass and were cleared by building the closures they named, then re-run green: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift`, `check:lean-entry-closure`. - **Tests.** `pnpm --filter @objectstack/spec test` — **489 files, 14,229 tests passed**. - **Typecheck.** `pnpm --filter @objectstack/spec typecheck` green: `tsc --noEmit`, `check:scripts-typecheck`, and `check:test-typecheck` (54 files / 259 errors / 144 pinned signatures held in the shrink-only ledger, unchanged). - **`check:generated`** — all 16 generated artifacts up to date, `check:docs` among them. - **Lint, full population, no narrowing.** `eslint . --no-inline-config` over the whole repo: **6,861 files, 0 errors, 0 warnings**, exit 0. The config enables no type-aware linting for any file (its own comment at `eslint.config.mjs:326` records this with a positive control), so this run is per-file parsing throughout. ## Acceptance notes — measured, deliberately not changed here - The per-field `.describe()` strings on the same dead keys still read as present-indicative behaviour: `'HTTP status codes to retry'`, `'Enable circuit breaker'`, `'Failures before opening circuit'`, `'Add jitter to retry delays'`, and about twenty more across `RetryConfigSchema`, `HealthCheckConfigSchema` and `CircuitBreakerConfigSchema`. They render into this same generated page three times over and into the authorable-surface artifacts an authoring agent reads. Correcting them edits the declaration surface and moves `gen:schema` output, which is outside this card. - The triage seat's escalation condition — a **third** prose source still teaching this remedy — did **not** trigger. Sweeping the tree with the fold-free instrument for `retryConfig` paired with `429` or with `circuitBreaker` returns only: this file, its generated page, the already-corrected `SYNC_ARCHITECTURE.md` passages, the liveness ledger's own notes, a schema parse test, a generated defaults artifact, and an ADR naming-convention list. No third source asserts the remedy. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18794
Clause-②: no
What changed
packages/spec/docs/SYNC_ARCHITECTURE.mdtaught authors that a rate-limited upstream is answered byretryConfig, printed itsretryableStatusCodesdefaults (429 included), and listed it as a reason to pick L3 — while nothing reads the key. Five passages in that one file now say what is measurably true today: declared but currently unimplemented, each pointing atpackages/spec/liveness/connector.json.This is option A from the card, and only A. No declaration, schema or accept set is touched, so the ADR-0049 ruling on these keys is deliberately not prejudged. The keys are not described as retired (they are still declared and still parse, so an author writing them still sees no error) and not as the host's job (falsified below).
106717c3aa):157blockquoteretryConfig":299example comment:312example timeoutsconnectionTimeoutMs/requestTimeoutMs:336Best PracticesretryConfighandles the429you get for exceeding a limit":360decision rowretryConfig,health.circuitBreaker"Measurements — re-taken on this branch, not inherited
Consumer probe, fold-proof predicate, firing control in the same run. Comment leaders are stripped first, then every whitespace run (newlines included) is collapsed and glued to the access punctuation, so a folded access cannot hide from it. Read-shaped access only (
x.KEY,x?.KEY,x["KEY"], destructure). 8763 tracked files, tree106717c3aa:Second control, reachability. A bare-identifier census in the same run proves the scan surface reaches the files where these keys actually live:
connectionTimeoutMsoccurs in 9 files outsidepackages/spec(the four connector packages,plugin.ts, a test) andrequestTimeoutMsin 8 — every one of them a WRITE of the literal into a def so it satisfies the post-parse type, never a read. So the zeros read as "no reader", not "the probe never looked".Host seam, re-read verbatim.
packages/spec/src/integration/connector-provider.ts:57declaresConnectorProviderContextwith exactlyname,label,description?,icon?,type,providerConfig,auth?,loadPackageFile?. None of the three keys is among them, so a provider factory is never handed them and has no way to honour them. "Left to the host" is false.Changeset, measured rather than assumed. The trigger is "can a consumer read a change", not "did bytes move".
npm pack --dry-run --json --ignore-scriptsonpackages/spec: 275 entries,docs/SYNC_ARCHITECTURE.mdabsent, and zerodocs/paths at all, while the positive controlsliveness/connector.jsonandsrc/integration/connector.zod.tsare both present.files[]isdist,json-schema,liveness,prompts,llms.txt,README.md,src/**/*.zod.ts,CHANGELOG.md,api-surface,spec-changes.json— nodocsentry. The edited file publishes to nobody, soskip-changeset.Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 48 commands against commitc4c09b853e. All 48 were run, exit codes landed to disk first, then reconciled: "48 derived, 48 run, 0 NOT-MEASURED, 0 UNRUN" — a derived zero, every family carrying a recorded exit code. Four of them (check:dts-closure,check:dual-build-cjs-loads,check:lean-entry-closure,check:sourcemap-no-sources-content) first exited 3 = PREREQUISITE NOT MET, which is neither a pass nor a failure; afterpnpm build(73/73 tasks successful) all four re-ran green.pnpm --filter @objectstack/spec test— 488 test files, 14182 tests, all pass.pnpm --filter @objectstack/spec check:generated— "All 15 generated artifacts are up to date".pnpm --filter @objectstack/spec typecheck— clean.packages/spec/src/integration/connector-author-shape.test.tsextracts the file's typescript-fenced blocks and compiles them verbatim against the real schema, pinning the fence count at 2, the elision-sketch count at 0, and every block to compile clean. 14/14 pass with the edit in place.retryConfigkey toretryConfigBogushad to turn that gate RED on the block I touched. Observed: RED,TS2561 Object literal may only specify known properties— TypeScript's did-you-mean form of the excess-property error, since the bogus name is one edit from the real one — on the assertion "L3 example #0 must compile clean"; 1 failed, 13 passed. Mutation landing was proved on disk (anchor 1 to 0, blobbd3c6b895c2atoc02cf5c9c5e3) and the restore was proved independently of the tool's own claim: blob back tobd3c6b895c2aequals HEAD, andgit diff HEADempty. This is what shows the comments added inside the fence are inside the compiled region and compile clean, rather than sitting outside it.files:selector ineslint.config.mjstargets{ts,tsx,mts,cts,js,jsx,mjs,cjs}, and no markdown selector or processor exists. (2) The count is read from--format json: the one changed file yields"File ignored because no matching configuration was supplied."with 0 errors, so 0 files of the lint population are touched. (3) Invariance: the file is outside the lint population entirely, so this diff cannot move any untouched file's verdict.One widening the reviewer should confirm
The acceptance line for
:360reads "it must not present an unimplemented key as a selection criterion". That row pairedretryConfigwithhealth.circuitBreaker, and the:157blockquote pairs them too.health.circuitBreakeris unimplemented on the same evidence:packages/spec/liveness/connector.jsonrecords every one of its sub-keys asdead, and the same probe run shows 0 read-shaped consumers outsidepackages/specin anypackages/orexamples/file (its only hits are the generated reference page andcontent/docs/references/system/cache.mdx, which is the CACHE's own breaker, a different subject). Correcting only theretryConfighalf would have left the row still presenting an unimplemented key as a selection criterion. So both halves are corrected, citing the ledger's existing verdict rather than making a new one. Flagged because it is one key wider than the card's three.Acceptance notes
Noted, not filed. The first two are real sites carrying the same prescription, each behind a read-only fence this round:
content/docs/automation/flows.mdx(now:1595; the card said:1588) is held by open PR feat(spec,types,triggers)!:groupruns package-authored scheduled work without a declaration, owning each run's writes per record #18420 and is untouched here. Its passage is about reconciling retry-COUNT conventions between block kinds (maxAttemptsincludes the first attempt), not the "useretryConfigfor upstream rate limiting" prescription, so excluding it does not damage the card's thesis. Follow-up: sweep it once PR feat(spec,types,triggers)!:groupruns package-authored scheduled work without a declaration, owning each run's writes per record #18420 lands. Successor: the seat that picks up that sweep.packages/spec/src/integration/connector.zod.ts:44carries the same sentence verbatim ("What L3 does declare for a rate-limited upstream isretryConfig— whoseretryableStatusCodesdefault ... includes429"), andcontent/docs/references/integration/connector.mdx:42is that same comment regenerated bypackages/spec/scripts/build-docs.ts. Both are behind this round's fence onpackages/spec/src/integration/**. So after this PR the repo still teaches the falsehood in those two places, from one source. Follow-up: correct that TSDoc block and regenerate, which is a prose-only change plusgen:docs. Successor: whoever takes the ADR-0049 ruling card, since it lands in the same file.SYNC_ARCHITECTURE.md:192also namesconnectionTimeoutMs/requestTimeoutMs, but only to state that keys carrying a.default()are optional in the author shape — a true statement aboutz.inputthat makes no efficacy claim. Left alone deliberately.Generated by Claude Code
Generated by Claude Code