Skip to content

Commit 9136327

Browse files
docs(spec): align GroupingConfig.fields and NotifyConfig sourceObject/sourceId describes with the measured acceptance face (#7111)
- GroupingConfigSchema.fields: drop the '(supports up to 3 levels)' claim — the gate is .min(1) with no upper bound and the grid renderer recurses over all configured levels; state the shape instead (array order = nesting order, first entry outermost, at least one field). Fixes #7084. - NotifyConfigSchema.sourceObject/sourceId: replace 'Requires ...' with the module JSDoc's recorded tolerance — the pair only takes effect together; a half-specified click-through target is dropped at execute time, so the inbox never renders a dead link. Fixes #7085. - Pin tests for both describes (non-empty arm first so the negative arms are non-vacuous); regenerated the two reference pages; changeset (patch). Acceptance face unchanged: check:authorable-surface and check:api-surface green with zero diff on authorable-surface/**, json-schema.manifest/**, authorable-defaults/, authorable-surface.base.json, api-surface/**. Claude-Session: https://claude.ai/code/session_018ffcE95NaMJcL9XJ9VDYgk Co-authored-by: Claude <noreply@anthropic.com>
1 parent 424316b commit 9136327

7 files changed

Lines changed: 73 additions & 6 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Align two schema `.describe()` strings with their measured acceptance faces (docs-only; no acceptance change — every previously-valid input is judged byte-identically):
6+
7+
- `GroupingConfigSchema.fields` no longer claims "(supports up to 3 levels)". The gate is `.min(1)` with no upper bound, nothing downstream enforces a cap, and the grid renderer recurses over all configured levels — the describe now states the shape instead: array order is nesting order (first entry outermost), at least one field. (#7084)
8+
- `NotifyConfigSchema.sourceObject` / `sourceId` no longer say "Requires sourceId." / "Requires sourceObject.". The schema deliberately accepts the half-specified pair — the executor drops it at execute time so the inbox never renders a dead link (the module JSDoc's recorded contract) — and the describes now state that tolerance. (#7085)

content/docs/references/automation/io-node-config.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ const result = HttpConfigSchema.parse(data);
101101
| **channels** | `string \| string[]` | optional | Channels to fan out to (default: inbox) |
102102
| **topic** | `string` | optional | Event topic (default: "notify") |
103103
| **severity** | `string` | optional | info \| warning \| critical |
104-
| **sourceObject** | `string` | optional | Object name of the record the notification links to (writes sys_notification.source_object). Requires sourceId. |
105-
| **sourceId** | `string` | optional | Record id the notification links to (writes sys_notification.source_id). Requires sourceObject. |
104+
| **sourceObject** | `string` | optional | Object name of the record the notification links to (writes sys_notification.source_object). Only takes effect together with sourceId — a half-specified click-through target is dropped at execute time, so the inbox never renders a dead link. |
105+
| **sourceId** | `string` | optional | Record id the notification links to (writes sys_notification.source_id). Only takes effect together with sourceObject — a half-specified click-through target is dropped at execute time, so the inbox never renders a dead link. |
106106
| **actorId** | `string` | optional | User id that caused the event (writes sys_notification.actor_id) |
107107
| **actionUrl** | `string` | optional | Explicit click-through URL; overrides the link synthesized from sourceObject/sourceId |
108108
| **payload** | `Record<string, any>` | optional | Extra template inputs merged into the notification payload |

content/docs/references/ui/view.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ Record grouping configuration
340340

341341
| Property | Type | Required | Description |
342342
| :--- | :--- | :--- | :--- |
343-
| **fields** | `{ field: string; order: Enum<'asc' \| 'desc'>; collapsed: boolean }[]` || Fields to group by (supports up to 3 levels) |
343+
| **fields** | `{ field: string; order: Enum<'asc' \| 'desc'>; collapsed: boolean }[]` || Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field) |
344344

345345

346346
---

packages/spec/src/automation/io-node-config.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,40 @@ describe('NotifyConfigSchema — strict as of #4001 批 9', () => {
102102
.error?.issues.some((i) => i.code === 'unrecognized_keys')).not.toBe(true);
103103
}
104104
});
105+
106+
it('sourceObject/sourceId describes state the documented pair tolerance, not a phantom requirement (#7085)', () => {
107+
const shape = (NotifyConfigSchema as unknown as { shape: Record<string, { description?: string }> }).shape;
108+
for (const [key, partner] of [
109+
['sourceObject', 'sourceId'],
110+
['sourceId', 'sourceObject'],
111+
] as const) {
112+
const doc = shape[key]!.description ?? '';
113+
114+
// Non-empty arm FIRST — the negative arm below passes vacuously on ''
115+
// (the #6918 demonstration), so this arm is what gives it teeth.
116+
expect(doc.length, `${key} .describe() must not be empty`).toBeGreaterThan(0);
117+
118+
// Substance, by idiom borrowed from the module JSDoc (#6881 — no third
119+
// spelling): the pair only takes effect together, and a half-specified
120+
// click-through target is DROPPED at execute time rather than rejected
121+
// at the gate.
122+
expect(doc).toMatch(/only takes effect together/i);
123+
expect(doc).toContain(partner);
124+
expect(doc).toMatch(/dropped at execute time/i);
125+
126+
// The #7085 defect: "Requires <partner>." read as gate-enforced
127+
// requiredness, while the schema deliberately keeps both keys optional
128+
// (module JSDoc: the executor tolerates/drops the half pair). The
129+
// phantom-requirement wording must not return in any casing or tense.
130+
expect(doc).not.toMatch(/\brequire[sd]?\b/i);
131+
}
132+
133+
// The tolerance the describes now document, proven live on the same
134+
// schema — this is the acceptance face this change must NOT move: each
135+
// half pair still parses green.
136+
expect(NotifyConfigSchema.safeParse({ recipients: 'u1', title: 't', sourceObject: 'showcase_task' }).success).toBe(true);
137+
expect(NotifyConfigSchema.safeParse({ recipients: 'u1', title: 't', sourceId: 'r1' }).success).toBe(true);
138+
});
105139
});
106140

107141
describe('HttpConfigSchema — strict as of #4001 批 9', () => {

packages/spec/src/automation/io-node-config.zod.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ export const NotifyConfigSchema = lazySchema(() => strictObject({
160160
severity: z.string().optional().describe('info | warning | critical'),
161161
/** Click-through target object — only effective together with `sourceId` (#2675). */
162162
sourceObject: z.string().optional()
163-
.describe('Object name of the record the notification links to (writes sys_notification.source_object). Requires sourceId.'),
163+
.describe('Object name of the record the notification links to (writes sys_notification.source_object). Only takes effect together with sourceId — a half-specified click-through target is dropped at execute time, so the inbox never renders a dead link.'),
164164
/** Click-through target record id — only effective together with `sourceObject`. */
165165
sourceId: z.string().optional()
166-
.describe('Record id the notification links to (writes sys_notification.source_id). Requires sourceObject.'),
166+
.describe('Record id the notification links to (writes sys_notification.source_id). Only takes effect together with sourceObject — a half-specified click-through target is dropped at execute time, so the inbox never renders a dead link.'),
167167
/** User id that caused the event. */
168168
actorId: z.string().optional().describe('User id that caused the event (writes sys_notification.actor_id)'),
169169
/** Explicit click-through URL; overrides the sourceObject/sourceId link. */

packages/spec/src/ui/view.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,6 +1511,31 @@ describe('GroupingConfigSchema', () => {
15111511

15121512
expect(() => GroupingConfigSchema.parse(grouping)).toThrow();
15131513
});
1514+
1515+
it('fields .describe() states shape semantics without a fixed level cap (#7084)', () => {
1516+
const shape = (GroupingConfigSchema as unknown as { shape: Record<string, { description?: string }> }).shape;
1517+
const doc = shape.fields!.description ?? '';
1518+
1519+
// Non-empty arm FIRST — the negative arms below pass vacuously on '',
1520+
// so this arm is what makes them non-vacuous (the #6918 demonstration).
1521+
expect(doc.length, 'fields .describe() must not be empty').toBeGreaterThan(0);
1522+
1523+
// Substance, by idiom not verbatim: array order IS nesting order, and the
1524+
// gate's real lower bound (`.min(1)`) is stated.
1525+
expect(doc).toMatch(/nesting order/i);
1526+
expect(doc).toMatch(/outermost/i);
1527+
expect(doc).toMatch(/at least one/i);
1528+
1529+
// The #7084 defect must not return under a new number: the gate is
1530+
// `.min(1)` with NO upper bound, and nothing downstream enforces one
1531+
// either (objectui useGroupedData's buildLevel recurses over ALL
1532+
// configured levels — its only stop is `depth >= fields.length`). So any
1533+
// fixed-count support envelope here is prose the acceptance face does not
1534+
// have; house rule E17 says "up to N" is the same defect as "up to 3".
1535+
expect(doc).not.toMatch(/\bup to \d+\b/i);
1536+
expect(doc).not.toMatch(/\b\d+\s+levels?\b/i);
1537+
expect(doc).not.toMatch(/\bmax(?:imum)?(?:\s+of)?\s+\d+\b/i);
1538+
});
15141539
});
15151540

15161541
describe('GroupingFieldSchema', () => {

packages/spec/src/ui/view.zod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ export const GroupingConfigSchema = lazySchema(() => strictObject({
559559
surface: 'this grouping configuration',
560560
history: VIEW_HISTORY,
561561
}, {
562-
fields: z.array(GroupingFieldSchema).min(1).describe('Fields to group by (supports up to 3 levels)'),
562+
fields: z.array(GroupingFieldSchema).min(1).describe('Fields to group by, in nesting order — the first entry is the outermost group and each later entry nests one level deeper (at least one field)'),
563563
}).describe('Record grouping configuration'));
564564

565565
/**

0 commit comments

Comments
 (0)