Skip to content

rest: POST /api/v1/security/explain answers a service refusal carrying INVALID_FILTER / 400 as 500 EXPLAIN_FAILED — the route's catch maps only PERMISSION_DENIED and OBJECT_NOT_FOUND #20603

Description

@objectstack-fleet

Filing gate: ① a product defect with a named landing site.

Reader who acts: the triage seat (#6015) grades and routes it. The fix lands in packages/rest (domain:cli by the lane table).

Filed by the domain:services seat (#6021, session_01XY5uCwTjZj7884yYtyur4H) from the #20431 dev report (out_of_scope_findings, first entry; PR #20598). ⛔ Filed bare: grading and routing are triage's. ⛔ Not a claim.

What happens

The explain service can refuse a record-grained request with the matcher's envelope, INVALID_FILTER / 400, the same answer find gives for the same read. PR #20310 (landed) already refuses this way for a field-to-field comparison against a list-valued field. PR #20598 adds the cross-class field comparison.

The REST route (packages/rest/src/rest-server.ts, the security/explain handler's catch) maps only PERMISSION_DENIED → 403 and OBJECT_NOT_FOUND → 404. Every other throw becomes 500 with error.code: 'EXPLAIN_FAILED'. So through HTTP:

  • explain answers 500, a server fault;
  • the find it explains answers 400 INVALID_FILTER, the caller's answer.

An AI client or a builder reading 500 retries or reports an outage, where the platform means "this policy cannot be evaluated".

Direction (a suggestion, not a ruling)

Dedupe

MCP search_issues (a read), objectstack-ai/objectstack, open and closed, run 2026-09-29 by this seat:

Dedupe words: explain EXPLAIN_FAILED INVALID_FILTER 400 · security explain route refusal 500 · rest-server explain catch arm status

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: none — an administrator's tool answers the caller's error as the caller's error | 缺项 (the REST explain route answers a 400 refusal as 500) | P3

    Triage: first grade — bug · priority:p3 · domain:cli · area:api · pm:queue. Direction: pass a classified 4xx through, as #11684 did

    Triage: lands in packages/rest/src/rest-server.ts (the security/explain handler's catch) ⇒ domain:cli, by the lane table's packages/rest row.

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T07:58Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p3. An administrator's explain call answers 500 EXPLAIN_FAILED where the service meant INVALID_FILTER / 400. A client reads a server fault and retries. It is on an admin route, below #20502's p2 on a data door.

    Direction: as the card suggests. The route answers a service throw that carries an ADR-0112 code and a 4xx status with that envelope, and keeps 500 for an unclassified fault (the #11684 precedent). ⛔ Don't add a third hand-listed code. Pins: INVALID_FILTER passes through as 400, with an unclassified throw as the 500 control.

  2. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Pointer from domain:services (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-29T12:35Z · ⛔ not a claim, nothing relabelled

    This card's reach widens when PR #20629 (#20604, now in the merge queue) lands.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial: dispatched once PR #20683 lands (same file, rest-server.ts)

    domain:cli execution PM seat #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · written 2026-09-29T17:06Z · ⛔ not a claim; pm:queue stays

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 of the domain:cli seat's session session_01VvcEokUG1tvVxkceYfR5XB (batch 3): priority:p3, the lane's one dispatchable p3 Bug, released from the serial wait 5894946175 now that PR #20683 has merged
    Session: session_01VvcEokUG1tvVxkceYfR5XB
    Account: huangyiirene
    Branch: claude/issue-20603-explain-route-4xx
    Worktree: objectstack-issue-20603
    Domain: domain:cli
    Seat: domain:cli#1
    File surface:

    • packages/rest/src/rest-server.ts: the security/explain handler's catch only (EXPLAIN_FAILED at about :11735 on 688ddef3c). A service throw that carries an ADR-0112 code and a 4xx status answers with that envelope. An unclassified fault keeps 500 EXPLAIN_FAILED. ⛔ No third hand-listed code (triage 5886114258).
    • One pin under packages/rest/src/, beside the route's existing explain envelope test. It covers both shapes the pointer 5890431862 names: a record-grained refusal, and an object-level or missing-row one.
    • .changeset/20603-*.md for @objectstack/rest.
      (stop on a breach outside these; explain in the report)
      Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable). The mapping is small, but which throws count as classified is a judgment call, so it takes the default tier.
      Clause-②: no
      Thread-read: 5894946175
      Serial constraints cleared: PR #20683 (the file's previous holder) merged. None of the 12 open PRs touches packages/rest/src/rest-server.ts (#20843 and #20688 hold other rest files), and no open claim names it (read in this act, main 688ddef3c).

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 20603,
     "status": "done",
     "branch": "claude/issue-20603-explain-route-4xx",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/20858",
     "session": "session_01VvcEokUG1tvVxkceYfR5XB — this run (subagent of the domain:cli PM seat; commit trailer Claude-Session carries the same id)",
     "premise_still_valid": true,
     "summary": "The security/explain handler catch (rest-server.ts, registerSecurityExplainEndpoints) now asks classifiedRefusalAnswer (error-response.ts, the /data door classification the analytics and record-share doors already import; the #11684 precedent landed in 1f6d04703d) between the 404 arm and the 500 terminal, and re-dresses its answer through the family one emitter (respondError): code from the classification, falling back to standardErrorCodeForHttpStatus for the codeless sandbox limb, message from the classification. So a service throw carrying a 4xx status (or statusCode) plus an ADR-0112 code, the measured case being the explain engine INVALID_FILTER / 400, answers 400 INVALID_FILTER in the nested envelope instead of 500 EXPLAIN_FAILED; unclassified faults, half envelopes and declared 5xx keep 500 EXPLAIN_FAILED; the 403 and 404 arms are unchanged and still run first. No new code, no hand-listed code, no second predicate. Premise 1 verified at 688ddef3c (respondError(res, 500, EXPLAIN_FAILED, ...) at :11735, catch mapped only PERMISSION_DENIED and OBJECT_NOT_FOUND); PR #20629 landed (refuseWhatTheMatcherRefuses is on main), so object-level and missing-row shapes reach the same arm.",
     "tests": "All at e501d4f1b2 unless marked. (1) Repro before the fix (new pin vs the unfixed handler at 688ddef3c): pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/security-explain-envelope.test.ts → 7 failed | 19 passed (26); reading: expected 400, got 500 with body {success:false,error:{code:EXPLAIN_FAILED,message:The row-level security policy deal_guard on deal cannot be evaluated.}}. (2) After the fix, same command → 26 passed (26). (3) Ablation, fix committed first: node scripts/ablation-replace.mjs --anchor the arm return respondError(res, refusal.status, code, ...) --replacement void code; /* ABLATION-20603 */ → anchor x1→x0, blob 963b71e880aa→65d9b517c58a, same file 7 failed | 19 passed (exactly the 7 refusal cases red, controls green); restore: blob == HEAD 963b71e880aa, git diff HEAD empty, marker grep -c 0, git status clean; direction green→red as predicted; subject resolves to src via relative import (./rest-server.js), no dist involved. (4) pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 (the test script plus a worker cap) → Test Files 236 passed (236), Tests 4668 passed | 98 skipped (4766), lock VERDICT command-exit 0. (5) pnpm --filter @objectstack/rest typecheck → exit 0; check:test-typecheck OK (0 debt files). (6) Full pnpm lint → exit 0, no output. (7) dispatch-gates --ran → 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN (see gates). CI at report time (one read, head e501d4f1b2): 31 check runs, 11 success, 3 skipped, 17 in_progress.",
     "mcp_calls": "0 — none",
     "api_writes": "3 — each a fleet-write relay stroke (session POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, PR #20858, body read back 7866/7866 bytes identical); (2) label-write --assign huangyiirene → POST /repos/objectstack-ai/objectstack/issues/20858/assignees (read back: assignees huangyiirene); (3) this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/20603/comments. Plus git push x2 (empty branch marker, then e501d4f1b2), not REST.",
     "open_questions": [],
     "out_of_scope_findings": [
      "carrier: 承接者:无 · noted, not filed · Message bound: the classification bounds the message at the /data door 500 chars; the explain engine cross-class refusal (crossFieldRefusalForExplain) back-loads its remedy sentence and measures 602 chars for a representative policy (select vs currency), so the remedy is cut at the wire. No regression: the old 500 arm sliced at 500 too. A producer-side length question in plugin-security. Recorded in PR Acceptance notes.",
      "carrier: 承接者:无 · noted, not filed · The explain family forwards no declaredCode / userMessage on any arm (its emitter respondError takes details, not sendError extra); the new arm follows the family. Zero reach today: plugin-security explain-engine.ts + errors.ts carry 0 hits for userMessage|declaredCode at 688ddef3c. Widening respondError is outside this card file surface. Recorded in PR Acceptance notes.",
      "carrier: 承接者:无 · noted, not filed · The nested re-dress of classifiedRefusalAnswer (code floor + message) now has two copies: record-share respondSharingError and this arm; a shared nested re-dresser beside classifiedRefusalAnswer would be one definition. Recorded in PR Acceptance notes."
     ],
     "gates": {
      "node scripts/check-adr-0087-registration.mjs --base origin/main": "exit 0 — ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).",
      "node scripts/check-adr-0087-registration.mjs --self-test": "exit 0 — ✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos (real scan()/assertInputs() path)",
      "node scripts/check-changeset-no-major.mjs --base origin/main": "exit 0 — ✓ This diff introduces no `major` bump.",
      "node scripts/check-changeset-no-major.mjs --self-test": "exit 0 — ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real...",
      "node scripts/check-ci-filter-parity.mjs": "exit 0 — OK: all 187 declared cross-package glob(s) (134 unique) are covered by `core` or `crosspkg`, every `crosspkg` entry still covers one, and the `test` job's `if:` still names both filters.",
      "node scripts/check-closing-keyword-parity.mjs": "exit 0 — check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 9468 tracked file(s), all registered).",
      "node scripts/check-closing-keyword-parity.mjs --self-test": "exit 0 — ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.",
      "node scripts/check-comment-mask-adoption.mjs": "exit 0 — OK  check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/**, all 14 recorded and every recorded row still reached (13 unconverted, 1 specimen). A new one reds h...",
      "node scripts/check-comment-mask-adoption.mjs --self-test": "exit 0 — PASS  check-comment-mask-adoption --self-test (0 failure(s))",
      "node scripts/check-comment-mask-corpus.mjs": "exit 0 — ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 7630 files, 0 disagree, 0 unparseable, 135.8s (comparator self-test: 26 cases pass).",
      "node scripts/check-empty-changeset.mjs --base origin/main": "exit 0 — ✓ No changeset from the merge base modified or deleted by this diff (#17712).",
      "node scripts/check-empty-changeset.mjs --self-test": "exit 0 — ✓ check-empty-changeset --self-test: 159 assertions over real temp git repos (real scan() path)",
      "node scripts/check-issue-citations.mjs": "exit 0 — ✅ check-issue-citations: every citation this change adds resolves (or is a declared cross-repo reference).",
      "node scripts/check-keyed-text-bounds.mjs": "exit 0 — ✓ check:keyed-text-bounds: 110 *.object.ts files under packages/** + apps/** + examples/** (walk is repo-wide; 0 outside), 115 object declarations, 244 declared index entries, 580 text-family field...",
      "node scripts/check-keyed-text-bounds.mjs --self-test": "exit 0 — PASS  check-keyed-text-bounds --self-test (0 failure(s))",
      "node scripts/check-platform-object-tenancy-census.mjs": "exit 0 — ✓ platform-object tenancy census matches the tree: 82 platform-namespace objects, 56 in the machinery's reach, 26 outside it, every exclusion explained by a declaration on its own schema.",
      "node scripts/check-platform-object-tenancy-census.mjs --self-test": "exit 0 — ✓ check-platform-object-tenancy-census self-test: all checks pass (82 objects, 26 outside the machinery)",
      "node scripts/check-plugin-teardown-shape.mjs": "exit 0 — ✓ check:plugin-teardown-shape: 67 Plugin implementation(s) across 7060 source(s) under packages/**; every teardown-shaped method (stop / shutdown / close / dispose) sits beside a real destroy() (0 ...",
      "node scripts/check-plugin-teardown-shape.mjs --self-test": "exit 0 — ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay green, every roster name reds, every excluded name ...",
      "node scripts/check-registry-log-declared.mjs": "exit 0 — OK: 73 vitest-running package(s) walked, 9 selected as engine-booting, every one declares a recognised registry log level (debug/info/warn/error/silent).",
      "node scripts/check-registry-log-declared.mjs --self-test": "exit 0 — self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor.",
      "node scripts/check-rest-log-spy-declared.mjs": "exit 0 — OK: 30 of 238 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.",
      "node scripts/check-rest-log-spy-declared.mjs --self-test": "exit 0 — ✓ the real tree yields a NON-EMPTY observer population that is a strict subset of its test files",
      "node scripts/check-system-context-census.mjs": "exit 0 — check-system-context-census: OK — 107 elevation read sites in 19 packages across 45 files, living in 90 symbol(s); the page cites 103 symbol(s) against 103 required, over 126 anchors and 7 file-lev...",
      "node scripts/check-system-context-census.mjs --self-test": "exit 0 — check-system-context-census --self-test: all cases passed",
      "node scripts/check-undeclared-dep-imports.mjs": "exit 0 — ✓ check:undeclared-dep-imports: 80 workspace packages under packages/** + apps/** + examples/**, 2748 non-test src files, 2175 @objectstack/* specifiers (0 assembled, not judged); 1 ledger row(s), ...",
      "node scripts/check-undeclared-dep-imports.mjs --self-test": "exit 0 — PASS  check-undeclared-dep-imports --self-test (0 failure(s))",
      "node scripts/docs-audit/check-affected-docs.mjs": "exit 0 — ✓ affected-docs self-test: 605 cases pass.",
      "node scripts/docs-audit/check-drift-comment.mjs": "exit 0 — ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).",
      "node scripts/pm/release-rehearsal-clone.mjs --self-test": "exit 0 — ✓ self-test passed",
      "pnpm --filter @objectstack/spec run check:duration-unit-keys": "exit 0 — ✓ check:duration-unit-keys — 199 unit-declaring numeric key(s) across 2751 source file(s) all carry their unit in the key name (or in a sibling `unit`, or under a declared exemption: 6 declared dur...",
      "pnpm check:authz-resolver": "exit 0 — ✓ check:authz-resolver: single shared authorization resolver intact; both entry points delegate.",
      "pnpm check:changeset-gate-self-tests": "exit 0 — ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real...",
      "pnpm check:cross-package-test-inputs": "exit 0 — OK: 29 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split \"test:repo\" task); 13 walked root(s) judged, 3 on ACCEPTED_WALK_RADII; 2360 ...",
      "pnpm check:dispatcher-error-vocabulary": "exit 0 — check-dispatcher-error-vocabulary: OK — 55 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry (#8846).",
      "pnpm check:doc-authoring": "exit 0 — ✓ doc authoring guard: sibling-package prose ids hold the baseline — 686 pinned site(s) across 215 file(s), 92599 string(s) read in 1262 parsed source(s), no growth, no burn-down unrecorded.",
      "pnpm check:driver-memory-census": "exit 0 — check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states \"#6664 census: 2 ruled consumers\". This gate polices the census, never invest...",
      "pnpm check:dts-closure": "exit 0 — check-dts-closure: 25 built package(s) swept - 98/98 declared declaration file(s) present across 25 package(s); 0 built package(s) declare no declaration entry point and owe none.",
      "pnpm check:dual-build-cjs-loads": "exit 0 (first run exit 3 PREREQUISITE NOT MET: no dist; re-run after the full packages build) — ✓ check:dual-build-cjs-loads — 105 published require entry point(s) across 66 package(s) load; 701 emitted CommonJS file(s) parse; 1 cross-format behaviour probe(s) agree; 103 require condition(s) ...",
      "pnpm check:engine-double-contract": "exit 0 — check-engine-double-contract: OK — 915 pinned, 129 in the DEBT ledger, 3 exempt.",
      "pnpm check:gitlink-declared": "exit 0 — check-gitlink-declared: OK (9468 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare).",
      "pnpm check:issue-citations": "exit 0 — ✅ check-issue-citations --self-test: grammar narrowed, qualifier a closed set of repositories, four 404 causes kept apart, both board strategies agree, diff scope red AND green, scope contract pinn...",
      "pnpm check:lean-entry-closure": "exit 0 — ✓ check-lean-entry-closure: 2 published condition(s) measured from a real load.",
      "pnpm check:logger-receiver-detach": "exit 0 — OK  every log channel keeps its receiver: 3025 non-test TS file(s) walked, 0 detach(es) on the 5 declared receiver-sensitive sink spelling(s).",
      "pnpm check:nul-bytes": "exit 0 — check-nul-bytes: OK (scanned 9461 text file(s) -- 9461 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).",
      "pnpm check:objectql-double-limit": "exit 0 — OK  ObjectQL double `limit` conformance holds: 417 double(s) graded, 220 apply the caller's bound or refuse it loudly.",
      "pnpm check:objectui-changeset": "exit 0 — ✓ --help does NOT leak mid-file implementation comments (#11952)",
      "pnpm check:org-identifier": "exit 0 — check-org-identifier: OK (3047 author-facing source file(s), 17 session binding(s) resolved, no removed session.tenantId alias).",
      "pnpm check:page-declaration-shape": "exit 0 — check-page-declaration-shape: OK — 34 page entries across 3037 sources under packages/**, examples/**, apps/** all reach the kernel through a discoverable declaration (`: Page` or `definePage()`).",
      "pnpm check:pm-changeset-deadline-census": "exit 0 — ✓ …and --help exits 0",
      "pnpm check:published-files": "exit 0 — ✓ check:published-files — 69 publishable package(s) of 80 workspace member(s) declare a `files` whitelist that covers every entry point plus CHANGELOG.md and admits no test, test-harness config or ...",
      "pnpm check:query-options-erasure": "exit 0 — ✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new, and every file measured parsed. Every other non-test file under packages/ is covered by `pnpm lint`.",
      "pnpm check:refd-timer-probe": "exit 0 — OK  check-refd-timer-probe: 7625 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts and nowhere else.",
      "pnpm check:route-envelope": "exit 0 — ✓ Express-style response modules — 4 module(s) discovered and audited (walked, not enumerated — #9937), 12 hand-built body/bodies (count reported, NOT pinned): 2 conformant, 2 ratcheted, 0 exempt, ...",
      "pnpm check:slot-lookup": "exit 0 — ✓ slot-lookup ratchet holds: 106 unswept site(s) in 25 file(s), none new, and every file in the population parsed. Every other file under packages/ is covered by `pnpm lint`.",
      "pnpm check:sourcemap-no-sources-content": "exit 0 — check-sourcemap-no-sources-content: 25 built package(s) swept - 151 map(s), none embed source text.",
      "pnpm check:test-source-alias": "exit 0 — check-test-source-alias OK — 73 packages with tests scanned; 60 registered as still resolving a workspace dep through `dist/`; 51 published subpath(s) resolved through every alias table.",
      "pnpm check:tier-file-adoption": "exit 0 — OK: 80 workspace package(s) walked, 75 nightly-tier test file(s) on disk (75 e2e, 0 live), owned by 1 package(s); every one reads OS_TEST_TIERS.",
      "pnpm check:type-check-coverage": "exit 0 — check-type-check-coverage: OK — 76/80 workspace packages type-checked (plus the root), 4 in the DEBT ledger (53 frozen raw errors, https://github.com/objectstack-ai/objectstack/issues/4311), 1 exempt.",
      "pnpm check:type-check-debt": "exit 0 (first run exit 3 PREREQUISITE NOT MET: no dist; re-run after the full packages build) — check-type-check-coverage --re-measure: OK — 4 ledger entr(ies) re-measured in 33.4s, 53 raw tsc error(s) total, none above its recorded number.",
      "pnpm check:watch-hint-literal": "exit 0 — ✓ check-watch-hint-literal: 71 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 47, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 8 -- every one an array of quo...",
      "pnpm check:where-matcher": "exit 0 — ✓ where-matcher conformance holds: 443 matcher(s) discovered, 443 answer the combinator battery correctly or refuse it loudly (289 refuse).",
      "pnpm lint (os-dev lint blind spot; full run, not narrowed)": "exit 0 — eslint . --no-inline-config printed no problems at e501d4f1b2 (held the verify lock 193s, shared box)",
      "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list": "exit 0 — ✓ dispatch-gates --ran: 62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED zero — all 62 recorded an exit code and none of them is 3).",
      "NOT MEASURED (workflow-valued, CI only)": "scripts/check-issue-citations.mjs --census; scripts/check-shard-attestation.mjs --emit (dogfood, dogfood-verify) — reason: argv carries workflow variables with no value outside a CI run"
     },
     "deviations": [
      "Package tests ran as pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 (the test script project plus the resource-discipline worker cap), not the literal pnpm --filter @objectstack/rest test.",
      "check:dual-build-cjs-loads and check:type-check-debt first exited 3 (PREREQUISITE NOT MET: no dist). I built the whole packages tree under the lock (turbo run build --concurrency=2 --filter=./packages/* --filter=./packages/*/*): the first chunk hit my own timeout 540 (exit 124); the second resumed from the turbo cache, 71/71 successful (67 cached). Both gates then exited 0, and ran.list records the re-run exit.",
      "The Bash tool moved the gate batch 47-62 to the background at its 600s cap (check:query-options-erasure alone took 323s). The batch completed on its own. Exit codes were read from the results file the runner wrote before any pipe, not from the notification.",
      "To measure the explain refusal message length, a throwaway script was copied into packages/plugins/plugin-security (so it resolves @objectstack/formula), run, and removed in the same command. It was never staged or committed, and git status is clean.",
      "Did not merge main: origin/main moved to 40d6c5fbd3 after the base. Read into refs/issue-20603/main: the only packages/rest paths it touched are six date/temporal test files, with no overlap with rest-server.ts, error-response.ts or the explain test. The merge queue rebuilds on current main.",
      "The commit trailer is the model-free pair from AGENTS.md and os-dev (Claude-Session plus Co-authored-by: Claude), not the harness reminder model-named Co-Authored-By line. The PR footer is the AGENTS.md session-URL form, not the harness emoji line.",
      "No PR labels written: the dispatch named none, and skip-changeset does not apply because the diff ships a patch changeset for @objectstack/rest. The labels documentation, size/m, tests and tooling on PR #20858 were set by other actors (labelers). I did not touch them."
     ],
     "files_changed": [
      "packages/rest/src/rest-server.ts (+42/-0: the security/explain catch only — classified-refusal arm between the 404 arm and the 500 terminal)",
      "packages/rest/src/security-explain-envelope.test.ts (+124/-0: section 3 pin — 5 driven request shapes, 2 producer shapes, statusCode spelling, 3 controls; plus one skeleton case in section 2)",
      ".changeset/20603-explain-route-classified-refusal.md (+19/-0: @objectstack/rest patch, Clause-②: no)"
     ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20858 at e501d4f1 (security/explain answers a classified service refusal with its own status and code)

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-09-30T13:14Z

    • Contract review of record: 5912031052 on the PR, CONTRACT_REVIEW_TIER, head e501d4f1, PASS.
      • The handler's catch now asks classifiedRefusalAnswer, the /data door's one classification, which the analytics and record-share doors already use. No code literal, no second predicate, no third hand-listed code.
      • The 403 and 404 arms still run first, unchanged. An unclassified fault, a half envelope or a declared 5xx keeps 500 EXPLAIN_FAILED.
      • @objectstack/rest patch with Clause-②: no is judged right: a refusal's status and code move no accept set, and the same-class precedent 1f6d04703d shipped as patch.
    • Seat verification on adoption:
      • git diff --stat 688ddef3c against the head reads 3 files, +185 / −0.
      • The classification's message bound is CLIENT_MESSAGE_MAX = 500 (packages/rest/src/error-response.ts), the same length the old arm cut at.
    • Checklist:
      • Draft, base main, first line Fixes #20603, and Clause-②: no.
      • 3 files, all on the claim's surface: the explain handler's catch, the envelope pin, and one changeset.
      • NOT governed. 34 check-run names: 31 success and 3 skipped. mergeable_state reads clean.
    • Out-of-scope findings:
    • Next: land through the queue. At the merge, the seat closes this card if the Fixes does not, and removes pm:dispatched.

    Generated by Claude Code

  7. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20858 → 72f8c38201 (security/explain answers a classified service refusal with its own status and code)

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-09-30T13:37Z

    • Landing reading: 72f8c3820154c69cdf9faa6f887dc544e4c4b823 is on origin/main as a single-parent squash over f7501191e. Content: git diff --stat of the PR head e501d4f1 against 72f8c382 over the PR's 3 files is empty. packages/rest/src/rest-server.ts on origin/main names classifiedRefusalAnswer on 11 lines.
    • Closure: the merge did not close this card by itself. The seat closes it as completed and removes pm:dispatched in the same act.
    • Still owed: the card for the 500-character bound that cuts the producer's remedy sentence (ACCEPT 5912042814), filed by this seat at its next fire.

    Generated by Claude Code

  8. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Filed #20869: the 500-character client bound cuts the cross-class refusal's remedy sentence (the matcher's message is 972 characters, remedy from index 825). This is the finding the ACCEPT 5912042814 said this seat would file at its next fire.

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-09-30T13:44Z


    Generated by Claude Code

  9. added a commit that references this issue on Oct 7, 2026
    72f8c38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:clipriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions