Skip to content

fix(plugin-reports): stop DELETE /reports/schedules/:scheduleId revealing whether a schedule id exists (#7603) - #7688

Merged
os-help merged 1 commit into
mainfrom
claude/issue-7603-schedule-delete-enumeration-oracle
Aug 11, 2026
Merged

fix(plugin-reports): stop DELETE /reports/schedules/:scheduleId revealing whether a schedule id exists (#7603)#7688
os-help merged 1 commit into
mainfrom
claude/issue-7603-schedule-delete-enumeration-oracle

Conversation

@os-help

@os-help os-help commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7603

Root cause

DELETE /api/v1/reports/schedules/:scheduleId discriminated on existence:

Target Before After
Another owner's schedule id 404 REPORT_NOT_FOUND 404 REPORT_NOT_FOUND (unchanged)
A schedule id that does not exist 204 No Content 404 REPORT_NOT_FOUND
A schedule whose report row is gone 404 REPORT_NOT_FOUND 404 REPORT_NOT_FOUND (unchanged)
Your own schedule 204 No Content 204 No Content (unchanged)

The caller can delete neither of the first two, yet still read which one they hit straight off the status code — an enumeration oracle over other owners' report schedules.

The leak was in the service, not the route. packages/plugins/plugin-reports/src/report-service.ts:602 (pre-fix):

const schedule = await this.loadScheduleRow(scheduleId);
if (!schedule) return; // idempotent — nothing to drop (mirrors deleteReport)
// A schedule is owned through its report (#2980): ... Others get a not-found so
// the delete neither fires nor reveals the schedule's existence
const report = await this.loadReportRow(schedule.report_id);
if (!this.canAccessReport(report, context)) {
  throw new Error(`REPORT_NOT_FOUND: ${scheduleId}`);
}

The intent was written down directly beneath the line that broke it. packages/rest/src/rest-server.ts:9536 maps the early return to 204 and the throw to 404 via handleValidation. Idempotence is only harmless where every caller may see the row; with a cross-owner arm that throws, resolving quietly is the tell.

Why it was read as correct: this route was cited by #7523's own investigation as proof of the right shape, because it does route its catch through handleValidation — which is why the cross-owner arm is a clean 404 and not a 500. QA run #7515 exercised only that arm. The unknown-id arm was never probed, and packages/rest/src/rest.test.ts:1651 pinned its 204 green.

The fix

Both deny arms are now one decision, taken before the delete fires, by the predicate already blind to the difference between them — canAccessReport is false for a schedule that does not exist, for one whose report is gone, and for one owned by somebody else alike:

const schedule = await this.loadScheduleRow(scheduleId);
const report = schedule ? await this.loadReportRow(schedule.report_id) : null;
if (!this.canAccessReport(report, context)) {
  throw new Error(`REPORT_NOT_FOUND: ${scheduleId}`);
}

A single throw site means a single message, so the route's single handleValidation call emits a single response — status and body cannot drift apart.

The precedent's mechanism does not hold on this surface — as the card asked me to verify

#7523 leans on getReport() being blind to the existence/ownership difference (#2980), which lets the route pre-empt both arms. That is unavailable here: the caller presents a scheduleId, and IReportService exposes no by-id schedule read at alllistSchedules is keyed by reportId, and there is no getSchedule. There is nothing in the route for the route to be blind with.

So the blinding has to live in the service, and IReportService.unscheduleReport now states it as a contract obligation (packages/spec/src/contracts/report-service.ts, JSDoc only — no logic in spec) rather than leaving each implementation to rediscover it. The route keeps its half of the composition and documents it: one emitter for whatever the service throws.

Layers, and what pins each:

Layer Obligation Pinned by
IReportService contract both deny arms → same REPORT_NOT_FOUND: <scheduleId>, before the delete JSDoc (the enforcement channel available)
ReportService (real impl) conforms report-service.test.ts — "the unknown-id and cross-owner deny arms are indistinguishable"
REST route given a conforming service, both arms → one identical response schedule-delete-enumeration-oracle.test.ts — whole-response equality

Behaviour changes beyond the deny arms

  • Deleting a schedule you own still answers 204.
  • Deleting one you cannot see is now 404 instead of a silent 204 — the cost of closing the oracle, and in line with the cross-owner GET / run / upsert-overwrite / delete arms, which all already answer 404.
  • A system/dispatcher context deleting an id with no row now gets REPORT_NOT_FOUND where it previously resolved. The route is the only production caller of unscheduleReport (verified by grep across the repo); nothing relies on the old behaviour.

Superseded pins — called out by name

Both are replaced in place, same input, opposite assertion, with a comment in the diff explaining why the old expectation was wrong. Neither was deleted quietly.

1. packages/rest/src/rest.test.ts:1651DELETE /reports/schedules/:scheduleId returns 204 (the pin the issue names).

Drove { scheduleId: 'rsch_1' } against a service whose unscheduleReport resolved, and asserted 204 as the route's answer for any schedule id. Resolving quietly was precisely what unscheduleReport did for an id that does not exist, so the pinned 204 was one arm of the oracle. Now split into two tests on that same input: an id the caller cannot see → 404 (REPORT_NOT_FOUND), and a resolving unscheduleReport204, which post-fix means only "the caller owned it and it is gone".

2. packages/plugins/plugin-reports/src/report-service.test.ts:457unscheduleReport: an unknown schedule id is idempotent, not a leak.

Not named in the issue, but the deeper pin: same input ('rsch_nope' as a stranger), asserting resolves.toBeUndefined(). Its title stated the conclusion backwards — that resolution was the leak. Now asserts rejects.toThrow(/REPORT_NOT_FOUND/), renamed to an unknown schedule id is denied as not-found, not silently idempotent.

Acceptance target — whole-response equality, and the mutation table

Per the card, the tests assert the two deny arms' whole responses are equal (transcript of every status()/json()/end() call with arguments, driven with the same id against two worlds differing only in whether the schedule exists — nothing normalised away), not each arm's status separately.

Directions predicted before running. Each mutation applied to a clean tree, then reverted.

# Mutation report-service.test.ts schedule-delete-enumeration-oracle.test.ts rest.test.ts
M1 Restore if (!schedule) return; in ReportService.unscheduleReport (the exact pre-fix line) 🔴 2 failed — "denied as not-found…", "deny arms are indistinguishable" ⚪ n/a (own double) ⚪ n/a
M2 Same early return in the route test's declared port — the pre-fix service as the route sees it ⚪ n/a 🔴 3 failed — the two equality tests + the orphaned-report arm 🟢 pass
M3 Drop if (handleValidation(res, error)) return; from the route (remove the single emitter) ⚪ n/a 🔴 4 failed — incl. VALIDATION_FAILED → 400 🔴 1 failed — the new 404 test
M4 The half-fix. Both deny arms throw REPORT_NOT_FOUND → both answer 404, but the unknown arm carries a different body 🔴 1 failed — "deny arms are indistinguishable" 🔴 3 failed — all three equality tests 🟢 pass

M4 is the result the card asked for. Both arms answer 404, so every per-arm status assertion — including the one this PR just wrote into rest.test.ts — stays green, while the bodies differ and the response still discriminates on existence. Only the whole-response equality assertions catch it. That is the concrete demonstration that a per-arm assertion cannot fail on a half-fix.

M3 is worth stating precisely, and cuts the other way. Removing the emitter makes both arms 500 with an identical body, so the equality assertions alone would have passed. What catches it is the companion assertion that the response they agree on is the right one, pinned in full:

expect(whenItExists).toEqual(whenItDoesNot);
expect(whenItExists).toEqual([
  ['status', [404]],
  ['json', [{ code: 'REPORT_NOT_FOUND', error: 'REPORT_NOT_FOUND: rsch_owned_by_a' }]],
]);

Equality and the pinned transcript are each insufficient alone and are both required; M3 and M4 are the two mutations that prove it.

Also covered, so the fix is not bought by breaking the feature or over-reaching: the owner still deletes their own schedule (204), a genuine fault is still 500 SCHEDULE_DELETE_FAILED, VALIDATION_FAILED is still 400, and both deny arms do identical work (one call, no delete — the shape a timing side channel would take).

Sibling-handler sweep — "does any other idempotent-early-return handler on this surface have the same split?"

Enumerated every 204-answering DELETE in rest-server.ts (grep -n "status(204)" → 4 hits) and read each one's service method:

Route Service Unknown id Cross-owner / unauthorised Verdict
DELETE /reports/schedules/:scheduleId unscheduleReport silent 204 → 404 404 this PR
DELETE /reports/:id deleteReport 404 404 fixed by #7523 (af5918b2b)
DELETE /data/:object/:id/shares/:shareId SharingService.revoke throws NOT_FOUND NOT_FOUND for a record you cannot see; PERMISSION_DENIED only for a record you can already see but may not manage clean — no early return, and the split is behind a visibility gate that reveals nothing new
DELETE /sharing/rules/:idOrName deleteRule if (!row) return; → 204 clean, but for a different reason: getRule is gated only by the global assertCanManageRules(context) and is not per-rule ownership-filtered, so there is no cross-owner arm for the early return to split against. The early return exists; it has nothing to discriminate from. Worth re-checking if per-rule visibility is ever introduced.

Nothing new to file, so no separate issues opened. The reports surface itself has exactly the two handlers (deleteReport, unscheduleReport) — the first was #7523's, the second is this one.

Verification

packages/rest — targeted tests (232 passed)
$ npx vitest run src/schedule-delete-enumeration-oracle.test.ts src/rest.test.ts

 RUN  v4.1.10 /home/user/objectstack-7603/packages/rest

 Test Files  2 passed (2)
      Tests  232 passed (232)
   Start at  10:02:39
   Duration  5.10s (transform 6.72s, setup 0ms, import 9.18s, tests 344ms, environment 0ms)
packages/plugins/plugin-reports — full suite (70 passed)
$ npx vitest run

 Test Files  3 passed (3)
      Tests  70 passed (70)
   Start at  10:02:47
   Duration  6.81s (transform 9.25s, setup 0ms, import 11.85s, tests 328ms, environment 0ms)
eslint — clean on changed files
$ npx eslint --no-inline-config \
    packages/rest/src/rest-server.ts \
    packages/rest/src/rest.test.ts \
    packages/rest/src/schedule-delete-enumeration-oracle.test.ts \
    packages/plugins/plugin-reports/src/report-service.ts \
    packages/plugins/plugin-reports/src/report-service.test.ts \
    packages/spec/src/contracts/report-service.ts
exit=0

(--no-inline-config matches the root lint script. Without it, rest-server.ts:4265 reports a pre-existing Definition for rule '@typescript-eslint/ban-ts-comment' was not found from an inline disable comment on an untouched line.)

pnpm typecheck — clean on all three changed packages
$ pnpm --filter @objectstack/spec --filter @objectstack/rest --filter @objectstack/plugin-reports typecheck

Scope: 3 of 78 workspace projects
packages/spec typecheck$ tsc --noEmit && pnpm check:scripts-typecheck && pnpm check:test-typecheck
packages/spec typecheck: ✓ check:test-typecheck --self-test — 8 semantic case(s) + the parser hold.
packages/spec typecheck: check:test-typecheck: OK — @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json; 57 file(s) / 265 error(s) held in test-typecheck-debt.json (shrink-only)
packages/spec typecheck: Done
packages/rest typecheck$ tsc --noEmit
packages/plugins/plugin-reports typecheck$ tsc --noEmit
packages/plugins/plugin-reports typecheck: Done
packages/rest typecheck: Done
pnpm check:type-check-debt — the ratchet #7562 tripped, run after a full pnpm build

The new test file imports ./rest-server.js with its explicit extension, which is what #7562's follow-up commit had to fix after @objectstack/rest's TEST_DEBT rose from 155 to 156. No entry rose here:

$ pnpm build          # 71 successful, 71 total — required, the ratchet measures through built d.ts
$ pnpm check:type-check-debt

✓ check:type-check-coverage --self-test — 23 semantic + 16 observation + 15 re-measure + 12 built-closure + 9 auto-lowering case(s) hold.
check-type-check-coverage: OK — 63/77 workspace packages type-checked (plus the root), 14 in the DEBT ledger (455 frozen raw errors), 1 exempt.
check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 193.3s, 1786 raw tsc error(s) total, none above its recorded number.

@objectstack/rest is not among the entries the run flags as lowerable, i.e. it re-measures at exactly its recorded number.

Scope fences

Untouched, as instructed: the /meta route registration block (#7584), the meta app-list handler (#7566), mapDataError / the /api/v1/data error mapping (#7575), and the package registrar / package-routes.ts (#7563). The only edit in rest-server.ts is inside the reports-schedule delete handler — a comment block; its control flow is unchanged. content/docs/releases/ and docs/adr/** untouched; a changeset is the release-notes input.

Files

File Change
packages/plugins/plugin-reports/src/report-service.ts the fix — both deny arms collapsed onto one canAccessReport decision
packages/spec/src/contracts/report-service.ts JSDoc only — states the obligation the route cannot enforce
packages/rest/src/rest-server.ts comment only — records why the blinding cannot live here
packages/rest/src/schedule-delete-enumeration-oracle.test.ts new — whole-response equality at the route (7 tests)
packages/rest/src/rest.test.ts superseded pin #1
packages/plugins/plugin-reports/src/report-service.test.ts superseded pin #2 + the equality test + the don't-break-the-feature test
.changeset/schedule-delete-enumeration-oracle.md new — patch for all three packages

Generated by Claude Code

…ealing whether a schedule id exists (#7603)

`DELETE /api/v1/reports/schedules/:scheduleId` answered `404 REPORT_NOT_FOUND`
for another owner's schedule but `204 No Content` for a schedule id that does not
exist. The caller can delete neither, yet still reads which of the two they hit
straight off the status code — an enumeration oracle over other owners' report
schedules.

This is the defect #7523 closed on the sibling `DELETE /reports/:id`, in the
costume that card explicitly warned about: there the split was 500-vs-204 and
loud, here 404-vs-204 and quiet. The route was in fact cited by #7523's
investigation as the example of the RIGHT shape, because it does route its catch
through `handleValidation` — which is why the cross-owner arm is a clean 404
rather than a 500. Only that arm was ever probed (QA run #7515); the unknown-id
arm was not, so the surviving half went unseen and `rest.test.ts:1651` pinned its
204 green.

`unscheduleReport()` carried the intent — "others get a not-found so the delete
neither fires nor reveals the schedule's existence" — and a hole one line wide
above it: `if (!schedule) return; // idempotent`. Idempotence is only harmless
where every caller may see the row; with a cross-owner arm that throws, resolving
quietly IS the tell.

Both deny arms are now one decision, taken before the delete fires, by the
predicate already blind to the difference between them: `canAccessReport` is
false for a schedule that does not exist, for one whose report is gone, and for
one owned by somebody else alike. A single throw site means a single message, so
the route's single `handleValidation` call emits a single response — status and
body cannot drift apart.

Unlike `deleteReport`, this could NOT be pre-empted in the route. That one
collapses its arms with `getReport()`, already blind to the same difference
(#2980); the caller here presents a scheduleId and `IReportService` exposes no
by-id schedule read to be blind with (`listSchedules` is keyed by reportId). The
blinding therefore lives in the service, and `IReportService.unscheduleReport`
now states it as a contract obligation rather than leaving each implementation to
rediscover it. The route keeps its half of the composition: ONE emitter for
whatever the service throws.

Deleting a schedule you own still answers 204. Deleting one you cannot see is now
404 instead of a silent 204 — the cost of closing the oracle, and in line with
the cross-owner GET / run / upsert-overwrite / delete arms, which all already
answer 404. A system context deleting an id with no row now gets
REPORT_NOT_FOUND too; the route is the only production caller.

Two pins asserted the leaking arm and are superseded IN PLACE, same input,
opposite assertion — `rest.test.ts`'s "DELETE /reports/schedules/:scheduleId
returns 204" and `report-service.test.ts`'s "an unknown schedule id is
idempotent, not a leak", whose title stated the conclusion backwards.

Tests assert the two deny arms' whole responses are EQUAL rather than pinning
each arm's status separately: a mutation answering both arms 404 with different
bodies leaves every per-arm status assertion green and turns the equality
assertions red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4Zy4fY7Kj8t27QAEzr9G1
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 10:15am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/plugin-reports, @objectstack/rest, @objectstack/spec.

108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/plugin-reports, @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/rest, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-reports, @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

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.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 11, 2026
@os-help
os-help marked this pull request as ready for review August 11, 2026 11:27
@os-help
os-help added this pull request to the merge queue Aug 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

  • Test Core (1/3) — 失败步骤: Publish this shard's attestation(日志不可读,点进 job 看)

历史信号:

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

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

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

Merged via the queue into main with commit ea936f3 Aug 11, 2026
27 checks passed
@os-help
os-help deleted the claude/issue-7603-schedule-delete-enumeration-oracle branch August 11, 2026 11:45
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/m tests tooling

Projects

None yet

2 participants