Skip to content

docs(spec): re-derive two stale contract statements in object.zod.ts against the behaviour that landed (#10526, #10336) - #10733

Merged
os-elon merged 4 commits into
mainfrom
claude/issue-10526-lifecycle-prose-family
Aug 21, 2026
Merged

docs(spec): re-derive two stale contract statements in object.zod.ts against the behaviour that landed (#10526, #10336)#10733
os-elon merged 4 commits into
mainfrom
claude/issue-10526-lifecycle-prose-family

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#10526
Fixes#10336

Two prose-accuracy members on one file, packages/spec/src/data/object.zod.ts. Both bring author-facing contract text up to behaviour that landed in an earlier card; neither changes what parses. One commit per member.

Member 1 — #10526: the ttl.onlyWhen × archive refine's stated reason (commit 1)

Scope is piece 1 only per the triage comment: the refusal itself stays, only its stated rationale changes. Piece 2 (whether the refusal should survive at all) is untouched.

The wording was re-derived against the merged Archiver on this merge base (dd8172ee2), not against the card's prediction. Reading packages/objectql/src/lifecycle/lifecycle-service.ts:

claim in the old textstill true?evidence on this base
"the ttl sweep never runs"yesreapObject returns into archiveObject at if (lc.archive) { return this.archiveObject(...) } — before the if (lc.ttl) reap branch that is the only caller passing lc.ttl.onlyWhen into reap()
"the Archiver moves rows by age alone"no — retired by #10347const dueField = lc.ttl ? lc.ttl.field : 'created_at' / dueWindow = lc.ttl ? lc.ttl.expireAfter : archive.after

So the refusal is still correct, on a narrower reason than it gave: what carries over to the Archiver is the ttl window, never the filter. Its candidate read is hot.find(object, { where: { [dueField]: { $lt: cutoff } }, limit }) — the cutoff and nothing else, with no onlyWhen spread of the kind reap() does into its scope. Rows the filter names would still be copied and hot-deleted. The comment and the rejection message now say that.

The newly-landed #10643 refine one block above is accurate and was not touched.

Member 2 — #10336: the enable.files / enable.feeds describe strings (commit 2)

Both strings scoped the flag to creation. Since #10170 (PR #10332) plugin-audit registers each gate on beforeUpdate as well as beforeInsert (packages/plugins/plugin-audit/src/audit-writers.ts), so each refuses any write that makes a row target the walled object — a create and an update that re-points a sys_attachment / re-threads a sys_comment alike, 403 FILES_DISABLED / FEEDS_DISABLED. The strings now state that, matching the docblocks directly above them.

enable.activities is genuinely a mirror-on-write switch and is untouched, as the card requires.

Bounded in-place extension, declared (commit 3)

The clause-② probe below surfaced a third copy of member 1's retired claim in the same file: ttl.onlyWhen's own .describe() ended with "Incompatible with rotation storage and archive, which act on whole shards / age alone." By construction that sentence only applies when ttlis declared — exactly the case where the Archiver now selects by the ttl cutoff. Same defect class, same file, same fix already derived; shipping member 1 without it would have left the refine message and the describe string giving contradictory reasons for one refusal.

retention.onlyWhen's matching clause is not touched and is still accurate: with retention and no ttl the Archiver moves rows by created_at age, and when the triple is declared #10643's refine pins ttl to restate created_at/retention.maxAge, so the ttl cutoff coincides with the age bound in every shape that parses.

Clause-② proof: no — mechanically demonstrated

Diff-shape proof. Every added/removed line in object.zod.ts classified mechanically:

  • member 1 — comment_lines=16 message_lines=2 other=0
  • member 2 — describe_lines=4 other=0 (the .default(false) / .default(true) on those same lines unchanged)
  • commit 3 — describe_string_lines=2 other=0

No predicate, key, type, or default line is touched in any commit.

Acceptance-probe proof. A 16-case probe over the shapes this file's tests exercise (enable combinations from object.test.ts, plus the onlyWhen refusals, the accepted ttl/archive pairs, and #10643's triple) digests each case as ACCEPT/REFUSE plus the resolved enable object plus every issue's code@path — deliberately not the messages, which are what member 1 rewrites. Run against pristine origin/main and against this branch: byte-identical output.

The probe is falsifiable, verified in both directions rather than assumed:

  • neutering the ttl.onlyWhen && archive refine flips that case to ACCEPT and drops one issue from the combined case — digest turns red
  • flipping files.default(false) to true changes the resolved enable in the defaults case — digest turns red (this second check is why the digest carries resolved values at all: an accept-only digest was blind to it, and .describe() sits on the same line as .default())

Each mutation was confirmed on disk by counting the injected and removed anchor text, and each mutated copy was restored through a shell trap.

Verification

Merge base dd8172ee2; all results below are from final commit fcaf1fdc0.

  • pnpm --filter @objectstack/spec testTest Files 416 passed (416) / Tests 11107 passed (11107)
  • pnpm --filter @objectstack/spec typecheck — clean (tsc --noEmit + check:scripts-typecheck + check:test-typecheck)
  • pnpm --filter @objectstack/spec check:generated✓ All 14 generated artifacts are up to date. (re-run on fcaf1fdc0 after the final commit)
  • pnpm --filter @objectstack/spec check:docs✅ 229 generated files in sync with packages/spec
  • The 26 gate families re-derived from the actual diff by node scripts/pm/dispatch-gates.mjs (no hand-supplied path list): all green.

content/docs/references/data/object.mdx was regenerated wholesale via pnpm --filter @objectstack/spec gen:docs, never hand-edited; its diff is exactly the two describe strings, with the default: columns unchanged.

Two gates initially failed for a reason that is not this diff — an unbuilt workspace in the container:

  • node scripts/check-dev-prereqs.mjs — attribution proven by running it on a pristine merge-base worktree, where it fails 67/67 packages against 66/67 here (this branch is strictly better; the one built package is spec). Environment state, not the diff.
  • check:doc-formula-expressions — failed on a missing @objectstack/formula/dist. Building that package resolved it: ✓ 24 self-test cases passed, 22 record-scoped formula example(s) across 416 files / 1443 TS blocks judged clean, 9 @example(s) clean across 972 packages/spec/src files.

Changeset: patch on @objectstack/spec — describe-string text ships in published output.

No file under skills/ is touched, so the published-skills line budget does not apply.


Generated by Claude Code

…son against the post-#10347 Archiver (#10526)
The refine's rationale comment and its author-facing message both justified
the refusal with a runtime fact that #10347 (PR #10541) retired: "the ttl
sweep never runs ... the Archiver moves rows by age alone". Re-derived
against the merged Archiver on this base:
- `reapObject` still returns into `archiveObject` before the ttl reap branch
(lifecycle-service.ts), so the ttl sweep genuinely never runs under
`archive` — that half stands.
- `archiveObject` now selects candidates by the declared ttl cutoff
(`dueField = lc.ttl ? lc.ttl.field : 'created_at'`), so "moves rows by age
alone" is false whenever `ttl` is declared — that half is replaced.
- Its candidate read is `where: { [dueField]: { $lt: cutoff } }` and nothing
else: the WINDOW carries over to the Archiver, the `onlyWhen` FILTER does
not. That is the reason today, and the refusal stands on it.
Text only: the refusal itself, and every accepted/refused shape, is
unchanged (piece 2 of #10526 — whether the refusal should survive at all —
is explicitly out of scope and untouched).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw
…e.feeds describe strings (#10336)
Both `.describe()` strings said the flag rejects *creation*. Since #10170
(PR #10332) `plugin-audit` registers each capability gate on `beforeUpdate`
as well as `beforeInsert` (audit-writers.ts), so both refuse any write that
makes a row TARGET the walled object — a create and an update that re-points
a `sys_attachment` / re-threads a `sys_comment` alike, 403 FILES_DISABLED /
FEEDS_DISABLED. The strings now say that, which is what the docblocks
directly above them already said.
These strings are not inert prose: `.describe()` reaches the generated schema
output and the authoring surfaces built on it, so an author (or an LLM
authoring metadata) reading "otherwise creation is rejected" would conclude a
re-point is outside the flag's reach. It is not.
`enable.activities` is genuinely a mirror-on-write switch and is untouched.
Text only: no default, type, or key changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw
…rom ttl.onlyWhen's own describe string (#10526)
Bounded in-place extension of this PR's #10526 member, declared in the PR
body. The refine comment and rejection message were not the only places the
pre-#10347 rationale was written down: `ttl.onlyWhen`'s `.describe()` closed
with "Incompatible with rotation storage and archive, which act on whole
shards / age alone" — and by construction that sentence only ever applies
when `ttl` IS declared, which is exactly the case where the Archiver now
selects by the ttl cutoff rather than by age.
Same defect class, same file, same fix already derived for the message; the
correct wording is pinned by the merged Archiver read this PR's first commit
cites. Leaving it would have shipped a PR whose refine message and whose
describe string give contradictory reasons for one refusal.
`retention.onlyWhen`'s matching clause is NOT touched and is still accurate:
with `retention` and no `ttl` the Archiver moves rows by `created_at` age,
and when the triple is declared #10643's refine pins `ttl` to restate
`created_at`/`retention.maxAge`, so the ttl cutoff coincides with the age
bound in every shape that parses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw
… prose family (#10526, #10336)
`content/docs/references/data/object.mdx` regenerated wholesale via
`pnpm --filter @objectstack/spec gen:docs` (never hand-edited). The projection
diff is exactly the two `enable` describe strings; the `default:` columns and
every other row are untouched, which is the generated-side echo of the
clause-(2) probe result.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/getting-started/quick-reference.mdx(via ObjectCapabilities (symbol))
  • content/docs/protocol/objectql/security.mdx(via ObjectCapabilities (symbol))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via ObjectCapabilities (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 126 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 8f04d9a4ff8d50f84783a20c70b73a291be9c0c7packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3970ccb26966e8db0bf57b9a56d0fb3a335573f7 — the merge of head fcaf1fdc04183db4f3cac5744e4d3c9ff0c23fa1 into base 8f04d9a4ff8d50f84783a20c70b73a291be9c0c7, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3970ccb26966e8db0bf57b9a56d0fb3a335573f7 && git checkout 3970ccb26966e8db0bf57b9a56d0fb3a335573f7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8f04d9a4ff8d50f84783a20c70b73a291be9c0c7 fcaf1fdc04183db4f3cac5744e4d3c9ff0c23fa1 && git checkout -B drift-repro 8f04d9a4ff8d50f84783a20c70b73a291be9c0c7 && git merge --no-ff fcaf1fdc04183db4f3cac5744e4d3c9ff0c23fa1
node scripts/docs-audit/affected-docs.mjs --json 8f04d9a4ff8d50f84783a20c70b73a291be9c0c7

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 8f04d9a4ff8d50f84783a20c70b73a291be9c0c7 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling protocol:data labels Aug 21, 2026
@os-elon
os-elon marked this pull request as ready for review August 21, 2026 12:05
@os-elon
os-elon added this pull request to the merge queueAug 21, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32481624229 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (4/6) — 失败步骤: Run this shard's tests

    × no package records a verdict the boot can still contradict 9144ms
    FAIL src/lint-startup-registry-verdict.corpus.test.ts > startup open-vocabulary verdicts across packages/ (#4776) > no package records a verdict the boot can still contradict
    ↳ 失败原因: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/lint-startup-registry-verdict.corpus.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@os-elon
os-elon added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 52db1d1Aug 21, 2026
33 checks passed
@os-elon
os-elon deleted the claude/issue-10526-lifecycle-prose-family branch August 21, 2026 12:58
os-zhuang pushed a commit that referenced this pull request Aug 21, 2026
… hook budget
The corpus read + scan (1880 files / 28.15 MB) ran twice, once inside each of
the two corpus cases, sharing nothing — and both cases sat under vitest's
default 5000ms per-test timeout. On a loaded merge-queue shard the first case
measured 9144ms and ejected PR #10733, which never touched packages/lint, from
the queue; every entry behind it rebuilt.
Hoist the sweep into one beforeAll and give it an explicit, commented budget:
- Work removed: file `tests` total 1441ms -> 816ms locally; the largest thing
measured against the per-test budget drops from 972ms to 2ms.
- The budget is a hook timeout, deliberately loose (60s ~= 6.5x the worst
wall-clock ever observed for this sweep). It is a liveness backstop, not a
performance tripwire — a snug budget is what ejected an unrelated PR, and the
work grows with the repo (1872 files at 12:23Z, 1880 four hours later) while
the wall-clock varies with shard load.
Sharing is only sound because neither case mutates what the other reads, so the
findings array and every finding in it are frozen, and the shared value starts
`undefined` rather than `[]` behind a `corpusFindings()` accessor that throws.
That last part is a third false green the file now refuses: a case reading a
sweep that never ran would otherwise print as a clean audit over nothing.
Both gate cases still fire individually — proven by ablation, see the PR body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/stooling

Projects

None yet

2 participants

@os-elon@claude