Skip to content

Commit 74b4362

Browse files
fix(plugin-approvals): sys_approval_request.record_id declares referenceDeletedWhen on cancel_reason = record_deleted (#23798)
Fixes #23736 Clause-②: no ## What One key on `sys_approval_request.record_id`, beside its `referenceVia: 'object_name'`: referenceDeletedWhen: { field: 'cancel_reason', value: 'record_deleted' } A request the platform cancelled because its record was deleted (`cancel_reason: record_deleted`) now says, in the served object definition, that the pointer names a record that no longer exists, so a renderer can draw the sibling option's label (`Related record deleted`) instead of a link. This is the services half of the objectui#12489 ruling, letter A (batch #352 item 4); the spec key is #23735 (`8776119bdc`). No page edit and nothing in objectui: the served page's Details tab draws the pair through `record:details`, and the renderer reads the key. Landing place: `packages/plugins/plugin-approvals/src/sys-approval-request.object.ts`, as the card expected. The producer is there; no other package needed a change. Files: the object (3 lines: the key and a 2-line comment), the pin test beside it (`sys-approval-request-pointer-pair.test.ts`, +28 lines, one new describe with two tests), and `.changeset/23736-record-id-deleted-when.md`. Line budget: 43 additions, 0 deletions. ## Measured assumptions 1. **The line parses.** Measured on `origin/main` d3df3ba (the card cited 0b1179f): the key is `field.zod.ts:1494`, its field-half refusal `:2294` (no `referenceVia` is refused), and the sibling half is `refuseDishonestReferenceDeletedWhen` in `object.zod.ts:1862` (the sibling must be a declared single-value `select` with inline `options` holding `value`). `record_id` declares `referenceVia: 'object_name'`; `cancel_reason` is `Field.select(APPROVAL_CANCEL_REASONS.map(...))` holding `record_deleted`. `ObjectSchema.create` parses the object at import and the pin re-parses the exported object with `ObjectSchema.safeParse`: success. 2. **Clause-② no holds: the emitted declaration closure of `@objectstack/plugin-approvals` is byte-identical.** Built the package before (on the base) and after (on this head), `dist/index.d.ts` and `dist/index.d.mts`, the only two declaration files it emits: before 8d6525557b9d3cc681e0cf1e06c8dac6f163ebe535b4f36f99d1a87690d88b1e (both files) after 8d6525557b9d3cc681e0cf1e06c8dac6f163ebe535b4f36f99d1a87690d88b1e (both files) Positive control: after the second build `dist/index.js` and `dist/index.mjs` each carry the `referenceDeletedWhen` line (1 hit each), so that build did pick the change up. The key is already in the declarations (45 hits in both builds, from the spec's field type), which is why they do not move. No export, type or accept set changes. ## The pin, and its ablations The pin: `fields.record_id.referenceDeletedWhen` equals `{ field: 'cancel_reason', value: 'record_deleted' }`; `cancel_reason` is a `select` declaring the `record_deleted` option; the exported object parses through `ObjectSchema`. Both ablations ran through `scripts/ablation-replace.mjs` (anchor hit 1 time, blob changed, restore proven: blob equals HEAD and `git diff HEAD` empty), after the fix was committed. The pin's subject is this package's source (`./sys-approval-request.object.js`), so no rebuild is in the path. Direction in both: red. - **Delete the line** -> the presence test is red (1 failed, 5 passed): AssertionError: expected undefined to deeply equal { field: 'cancel_reason', ...(1) } - Expected: { "field": "cancel_reason", "value": "record_deleted" } + Received: undefined - **Point `value` at an option that does not exist** (`record_gone`) -> the spec refuses the parse at import, so the whole file is red (0 tests run) with the located issue: ZodError: code custom, path fields.record_id.referenceDeletedWhen.value Field `record_id` on object `sys_approval_request` declares `referenceDeletedWhen.value: 'record_gone'`, but `select` field `cancel_reason` declares no option `record_gone`. Its option values: `record_deleted`. Restored tree: the pin file passes 6 of 6 (4 existing pointer-pair tests and the 2 new ones); the whole `@objectstack/plugin-approvals` suite passes 78 files, 1082 tests (`vitest run --maxWorkers=2`, under the shared verify lock, exit 0). ## Gates (all at head 194e9ef) - `pnpm --filter @objectstack/plugin-approvals typecheck`: exit 0 (the test-layer debt ledger holds at 8 files, 324 errors, 27 signatures: unchanged). - `eslint --no-inline-config` on the two changed source files: exit 0. Control-byte scan of all three changed files: no hits. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 65 commands from the diff; every one was run with its exit code recorded before any pipe; `--ran` reconciles: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN. Two families first exited 3 (PREREQUISITE NOT MET: `check:i18n` and `check:dual-build-cjs-loads` read built output of packages not yet built). Their prerequisites were built (`turbo run build`, 61 and 75 tasks) and both were re-run: exit 0 (`check:i18n`: 10 packages in sync; `check:dual-build-cjs-loads`: 110 entry points load). The four other dist-reading families (`check:dts-closure`, `check:published-files`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`) were re-run on the fully built tree too: exit 0. - `origin/main` moved to 46ca793 during the run (a version-packages commit and two others; under `packages/plugins/plugin-approvals` it touches only `CHANGELOG.md` and `package.json`, the version bump, and nothing under `packages/spec/src/data`). The three changeset gates were re-run against it: exit 0. No merge of main into this branch. - Everything CI runs beyond that set (the full `Lint & Repo Gates` farm, `Test Core`, `Build Core`) is left to the PR's check-runs; none was re-run locally. ## Acceptance notes - `packages/spec/liveness/field.json` keeps `referenceDeletedWhen` as `planned`: its own note says it flips to `live` on the objectui#12489 step 4 renderer landing, citing this declaration as `producer`. That flip belongs to the renderer's landing, not to this PR; not edited here. - The key's field-level-security rule is #23735's: a caller who cannot read `cancel_reason` is served `record_id` without the predicate. Nothing to do here; recorded so the renderer card reads it. - No labels were written (the dispatch budget names none; this PR carries a changeset, so `skip-changeset` does not apply). --- _Generated by [Claude Code](https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 60f9baf commit 74b4362

3 files changed

Lines changed: 43 additions & 0 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
fix(plugin-approvals): `sys_approval_request.record_id` declares that the record it names was deleted when `cancel_reason` is `record_deleted`
6+
7+
Clause-②: no
8+
9+
- **The pointer carries its own tombstone predicate.** `record_id` declares `referenceDeletedWhen: { field: 'cancel_reason', value: 'record_deleted' }` beside its `referenceVia: 'object_name'`. A request the platform cancelled because its record was deleted (`status: cancelled`, `cancel_reason: record_deleted`) therefore says, in the served object definition, that the pointer names a record that no longer exists. A renderer draws that option's label (`Related record deleted`) in place of a link, and reads nothing.
10+
- **Nothing is accepted or answered differently.** The sibling is the existing `cancel_reason` select, which already holds `record_deleted`, so the spec's parse-time refusals of the key are satisfied by the shipped object. No export, type, route, stored value or request answer moves.
11+
12+
A patch: one key on one platform object's served field definition.

‎packages/plugins/plugin-approvals/src/sys-approval-request-pointer-pair.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { describe, it, expect } from 'vitest';
4+
import { ObjectSchema } from '@objectstack/spec/data';
45
import { SysApprovalRequest } from './sys-approval-request.object.js';
56

67
/**
@@ -49,3 +50,30 @@ describe('sys_approval_request — declared pointer pair (#11386)', () => {
4950
expect(fields.object_name.required).toBe(true);
5051
});
5152
});
53+
54+
/**
55+
* The pointer's tombstone predicate — objectui#12489 ruling, letter A.
56+
*
57+
* When the platform voids a `pending` request because its record was deleted
58+
* (`cancelled` + `cancel_reason: 'record_deleted'`), `record_id` names a record
59+
* that no longer exists. `referenceDeletedWhen` is the object's own assertion of
60+
* that, so a renderer draws the sibling option's label instead of a link to
61+
* nothing. The spec refuses the declaration at parse unless the sibling is a
62+
* single-value `select` holding the option, so the parse is half of the pin.
63+
*/
64+
describe('sys_approval_request — record_id tombstone predicate', () => {
65+
const fields = SysApprovalRequest.fields as Record<string, Record<string, unknown>>;
66+
67+
it('declares record_id as deleted when cancel_reason holds record_deleted', () => {
68+
expect(fields.record_id.referenceDeletedWhen).toEqual({ field: 'cancel_reason', value: 'record_deleted' });
69+
});
70+
71+
it('names a sibling select that declares that option, and the object parses through ObjectSchema', () => {
72+
expect(fields.cancel_reason.type).toBe('select');
73+
expect(fields.cancel_reason.options).toEqual(
74+
expect.arrayContaining([expect.objectContaining({ value: 'record_deleted' })]),
75+
);
76+
const parsed = ObjectSchema.safeParse(SysApprovalRequest);
77+
expect(parsed.success, parsed.success ? '' : JSON.stringify(parsed.error.issues)).toBe(true);
78+
});
79+
});

‎packages/plugins/plugin-approvals/src/sys-approval-request.object.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ export const SysApprovalRequest = ObjectSchema.create({
218218
// dropped-deferral error, and `success: false` — and since commit 1cba33f16 the
219219
// loader also WARNS at load time, before the engine rejects the row.
220220
referenceVia: 'object_name',
221+
// A `cancelled` request whose `cancel_reason` is `record_deleted` names a
222+
// record the platform deleted: a renderer draws that option's label, not a link.
223+
referenceDeletedWhen: { field: 'cancel_reason', value: 'record_deleted' },
221224
}),
222225

223226
submitter_id: Field.lookup('sys_user', {

0 commit comments

Comments
 (0)