Uh oh!
There was an error while loading. Please reload this page.
docs(spec): re-derive two stale contract statements in object.zod.ts against the behaviour that landed (#10526, #10336) - #10733
Conversation
…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
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32481624229 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Uh oh!
There was an error while loading. Please reload this page.
… 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
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×archiverefine'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. Readingpackages/objectql/src/lifecycle/lifecycle-service.ts:reapObjectreturns intoarchiveObjectatif (lc.archive) { return this.archiveObject(...) }— before theif (lc.ttl)reap branch that is the only caller passinglc.ttl.onlyWhenintoreap()const dueField = lc.ttl ? lc.ttl.field : 'created_at'/dueWindow = lc.ttl ? lc.ttl.expireAfter : archive.afterSo 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 noonlyWhenspread of the kindreap()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.feedsdescribe strings (commit 2)Both strings scoped the flag to creation. Since #10170 (PR #10332)
plugin-auditregisters each gate onbeforeUpdateas well asbeforeInsert(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 asys_attachment/ re-threads asys_commentalike, 403FILES_DISABLED/FEEDS_DISABLED. The strings now state that, matching the docblocks directly above them.enable.activitiesis 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 whenttlis 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: withretentionand nottlthe Archiver moves rows bycreated_atage, and when the triple is declared #10643's refine pinsttlto restatecreated_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.tsclassified mechanically:comment_lines=16 message_lines=2 other=0describe_lines=4 other=0(the.default(false)/.default(true)on those same lines unchanged)describe_string_lines=2 other=0No 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 (
enablecombinations fromobject.test.ts, plus theonlyWhenrefusals, the acceptedttl/archivepairs, and #10643's triple) digests each case as ACCEPT/REFUSE plus the resolvedenableobject plus every issue'scode@path— deliberately not the messages, which are what member 1 rewrites. Run against pristineorigin/mainand against this branch: byte-identical output.The probe is falsifiable, verified in both directions rather than assumed:
ttl.onlyWhen && archiverefine flips that case to ACCEPT and drops one issue from the combined case — digest turns redfiles.default(false)totruechanges the resolvedenablein 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 commitfcaf1fdc0.pnpm --filter @objectstack/spec test—Test 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 onfcaf1fdc0after the final commit)pnpm --filter @objectstack/spec check:docs—✅ 229 generated files in sync with packages/specnode scripts/pm/dispatch-gates.mjs(no hand-supplied path list): all green.content/docs/references/data/object.mdxwas regenerated wholesale viapnpm --filter @objectstack/spec gen:docs, never hand-edited; its diff is exactly the two describe strings, with thedefault: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 isspec). 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:
patchon@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