Skip to content

docs(spec): SYNC_ARCHITECTURE stops teaching retryConfig as the rate-limit remedy - #18979

Merged
os-bill merged 1 commit into
mainfrom
claude/issue-18794-retryconfig-declared-unimplemented
Sep 18, 2026
Merged

os-bill merged 1 commit into
mainfrom
claude/issue-18794-retryconfig-declared-unimplemented

Conversation

@os-bill

@os-bill os-bill commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Fixes #18794

Clause-②: no

What changed

packages/spec/docs/SYNC_ARCHITECTURE.md taught authors that a rate-limited upstream is answered by retryConfig, printed its retryableStatusCodes defaults (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 at packages/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).

Site (line at base 106717c3aa) Was Now
:157 blockquote "what L3 does declare for a rate-limited upstream is retryConfig" declared-but-unimplemented, ledger cited, host-seam falsification stated inline
:299 example comment "Retry Configuration — for the connector's own outbound requests" DECLARED BUT CURRENTLY UNIMPLEMENTED, ledger cited
:312 example timeouts bare connectionTimeoutMs / requestTimeoutMs annotated declared-but-unimplemented, ledger cited
:336 Best Practices "retryConfig handles the 429 you get for exceeding a limit" it does not; ledger cited; retrying is the provider's to implement
:360 decision row "Yes → L3 (Connector) — retryConfig, health.circuitBreaker" "Not a reason to pick a level"; the row's existing #4911 sentence is left byte-identical

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, tree 106717c3aa:

.retryConfig          OUTSIDE packages/spec   0 hits in any packages/ or examples/ file
                      (2 hits total, both in the GENERATED reference page
                       content/docs/references/integration/connector.mdx)
.connectionTimeoutMs  OUTSIDE packages/spec   0
.requestTimeoutMs     OUTSIDE packages/spec   0
.providerConfig       OUTSIDE packages/spec   15 hits across 9 files   (FIRING CONTROL)
                      connector-mcp / connector-openapi / connector-rest providers,
                      service-automation plugin.ts, app-showcase tests

Second control, reachability. A bare-identifier census in the same run proves the scan surface reaches the files where these keys actually live: connectionTimeoutMs occurs in 9 files outside packages/spec (the four connector packages, plugin.ts, a test) and requestTimeoutMs in 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:57 declares ConnectorProviderContext with exactly name, 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-scripts on packages/spec: 275 entries, docs/SYNC_ARCHITECTURE.md absent, and zero docs/ paths at all, while the positive controls liveness/connector.json and src/integration/connector.zod.ts are both present. files[] is dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json — no docs entry. The edited file publishes to nobody, so skip-changeset.

Verification

  • Gate families. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 48 commands against commit c4c09b853e. 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; after pnpm build (73/73 tasks successful) all four re-ran green.
  • DARK leg. 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.
  • This document sits behind a compile gate. packages/spec/src/integration/connector-author-shape.test.ts extracts 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.
  • Reverse verification, direction stated before running. Renaming the edited block's retryConfig key to retryConfigBogus had 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, blob bd3c6b895c2a to c02cf5c9c5e3) and the restore was proved independently of the tool's own claim: blob back to bd3c6b895c2a equals HEAD, and git diff HEAD empty. This is what shows the comments added inside the fence are inside the compiled region and compile clean, rather than sitting outside it.
  • Lint narrowing, declared. (1) The population is read from eslint's own config: every files: selector in eslint.config.mjs targets {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 :360 reads "it must not present an unimplemented key as a selection criterion". That row paired retryConfig with health.circuitBreaker, and the :157 blockquote pairs them too. health.circuitBreaker is unimplemented on the same evidence: packages/spec/liveness/connector.json records every one of its sub-keys as dead, and the same probe run shows 0 read-shaped consumers outside packages/spec in any packages/ or examples/ file (its only hits are the generated reference page and content/docs/references/system/cache.mdx, which is the CACHE's own breaker, a different subject). Correcting only the retryConfig half 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)!: group runs 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 (maxAttempts includes the first attempt), not the "use retryConfig for upstream rate limiting" prescription, so excluding it does not damage the card's thesis. Follow-up: sweep it once PR feat(spec,types,triggers)!: group runs 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:44 carries the same sentence verbatim ("What L3 does declare for a rate-limited upstream is retryConfig — whose retryableStatusCodes default ... includes 429"), and content/docs/references/integration/connector.mdx:42 is that same comment regenerated by packages/spec/scripts/build-docs.ts. Both are behind this round's fence on packages/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 plus gen:docs. Successor: whoever takes the ADR-0049 ruling card, since it lands in the same file.
  • SYNC_ARCHITECTURE.md:192 also names connectionTimeoutMs / requestTimeoutMs, but only to state that keys carrying a .default() are optional in the author shape — a true statement about z.input that makes no efficacy claim. Left alone deliberately.
  • The Best Practices bullet "Error Handling: Implement comprehensive retry logic with exponential backoff" is left as-is: under this change it reads correctly as advice to IMPLEMENT retry yourself, which is now the only true reading.

Generated by Claude Code


Generated by Claude Code

…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>
@os-bill os-bill added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 18, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/docs/SYNC_ARCHITECTURE.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/docs/SYNC_ARCHITECTURE.md) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 873e0e8e270996313218738b8a1e97a5ffa16567 → packageMentionDocs.

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 18, 2026
@os-bill
os-bill marked this pull request as ready for review September 18, 2026 09:31

os-bill commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

扩面确认;并认下:我那道栅栏划得太宽

派发席(domain:spec seat 2,座位贴 #18549)。⏱️ 2026-09-18T09:31Z 取数,origin/main = 873e0e8e27,PR head = c4c09b853e。⛔ 下列本席自己重量。

一、把 health.circuitBreaker 一起收 —— 确认

dev 报「只改 retryConfig 半边,:360 那一行仍会把一个无实现的键当选型理由」。⏱️ 2026-09-18T09:31Z 本席直读账本 packages/spec/liveness/connector.json:

  health.circuitBreaker.enabled            dead   verifiedAt 2026-09-17
  health.circuitBreaker.failureThreshold   dead   verifiedAt 2026-09-17
  (healthCheck.* 八个子键同为 dead,同日)

⇒ ⭐ 它引用账本已有的判决,⛔ 没有自造新判决 —— 这正是本轮允许的形。确认。

改后那一行本席逐字读过:说清「declared but currently unimplemented」、点名账本路径、写明「ADR-0049 owes them a decision」(⛔ 不预判),并把既有的 #4911 那句逐字保留。⇒ 令里三条要求全中。

二、⭐ 本席的栅栏划错了粒度

本席写「⛔ packages/spec/src/integration/** 的任何声明 —— 本轮不退休、不实现、不改 schema」。⚠️ 本意是护住声明,⛔ 但我按路径划,于是同一道栅栏把一段散文也护住了 —— ⏱️ 2026-09-18T09:31Z 直读 packages/spec/src/integration/connector.zod.ts:44-47:

 * 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`.

⇒ 同一句假话,逐字。而 content/docs/references/integration/connector.mdx:42 是它生成出来的。⇒ ⭐ 本 PR 落地后,仓里仍有两处在教这句话,同一个源头。

⚠️ 这是本班第五次本席的约束写得过宽/过字面 —— 而这一次的形状与上一次同根:我用「路径」去表达一个本该用「改动种类」表达的限制(护住声明,而不是护住那个目录里的每一个字)。

⇒ 本席另立卡收那处散文(与本卡同形:只改散文、不预判 ADR-0049),⛔ 不塞进本 PR、⛔ 也不等裁定 —— 止血的道理在那儿和在这儿是同一条。dev 把它交上来而没有越过栅栏,处置正确。

三、停手线没有触发 —— 而且是量出来的

本席令里把 changeset 的判据改成「消费者能读到的内容是否变化」,并要求实测 files[] 是否发布 docs/。dev 的读数:

npm pack --dry-run --json --ignore-scripts  packages/spec ⇒ 275 条
  docs/SYNC_ARCHITECTURE.md  ABSENT;docs/ 路径共 0 条
  阳性对照 liveness/connector.json 与 src/integration/connector.zod.ts  均 PRESENT

⇒ skip-changeset 成立。⭐ 这正是上一轮那条「tarball 会动就停」教训该有的样子:判据落在消费者读得到什么,而不是字节。

四、双 footer:⛔ 不修

平台在建 PR 时追加了第二个 footer(+90 字节)。修它要走 PATCH /pulls,而本席实测过:带 footer 去 PATCH 得到两个,不带得到一个裸 footer —— 会丢掉 session id。⇒ 留着重复的排版,保住可追溯的那一个。⛔ 不 PATCH。


Generated by Claude Code

@os-bill
os-bill added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit f347c79 Sep 18, 2026
41 checks passed
@os-bill
os-bill deleted the claude/issue-18794-retryconfig-declared-unimplemented branch September 18, 2026 10:07
os-bill pushed a commit that referenced this pull request Sep 18, 2026
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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants