diff --git a/.changeset/10150-author-shape-header-ruling.md b/.changeset/10150-author-shape-header-ruling.md new file mode 100644 index 0000000000..599e585381 --- /dev/null +++ b/.changeset/10150-author-shape-header-ruling.md @@ -0,0 +1,6 @@ +--- +--- + +Comment-only in `@object-ui/app-shell`: the docblock above `clientValidation`'s `AUTHOR_SHAPE_ONLY_TYPES` ended on a plan, to switch the `sharing_rule` edit gate on once the `_diagnostics` ingress was closed. That condition was met (`extractDraftBody` strips the read decorations through the spec's `stripReadDecorations`), the switch was put to the maintainer on objectui#7612, and the ruling was option A: the edit door stays with the server. The docblock now records that ruling beside the condition, and keeps the measurement above it. A source-scan pin requires the docblock to name the ruling's card and the function that closed the ingress. + +Declared as releasing nothing because the emit was measured rather than assumed: `AUTHOR_SHAPE_ONLY_TYPES` is not exported, so neither it nor its docblock reaches the emitted `clientValidation.d.ts`; the emitted `.js` carries the comment, and its comment-stripped emit is identical to the base. No published behaviour changes (objectui#10150). diff --git a/packages/app-shell/src/views/metadata-admin/clientValidation.authorShapeRuling-10150.test.ts b/packages/app-shell/src/views/metadata-admin/clientValidation.authorShapeRuling-10150.test.ts new file mode 100644 index 0000000000..b872da54a7 --- /dev/null +++ b/packages/app-shell/src/views/metadata-admin/clientValidation.authorShapeRuling-10150.test.ts @@ -0,0 +1,101 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * objectui#10150 — the `AUTHOR_SHAPE_ONLY_TYPES` header carries the ruling that + * keeps `sharing_rule` in the set, at the place its reader meets the condition. + * + * ## The trap this keeps shut + * + * The header in `clientValidation.ts` used to end on a plan: switch the + * `sharing_rule` edit gate on once the `_diagnostics` ingress is closed. The + * ingress was closed (objectui#7603, hoisted by objectui#8181), the switch was + * put to the maintainer on objectui#7612 with that already measured, and the + * ruling was option A: the edit door stays with the server. A header that still + * reads as the plan sends its next reader, who checks the condition and finds + * it met, straight back to re-filing objectui#7612. + * + * ## What this pins + * + * NAMED SUBJECTS inside ONE docblock, the one directly above the set: the + * ruling's card, and the function that closed the ingress. Inside that docblock + * and not merely somewhere in the file, because a citation moved to another + * comment is one the reader of the condition never meets. + * + * ## What it cannot pin, said here so it is not assumed + * + * - ⛔ Not the ruling's wording. The header quotes it verbatim; nothing parses + * that quotation, so it is not pinned as text. + * - ⛔ Not prose intent. A header that cites objectui#7612 AND re-states the + * old plan passes here. Catching that is review's job. + * - ⛔ Not whether the ingress is still closed. That is behaviour, and + * `ResourceEditPage.readDecorationStrip.test.tsx` is its instrument. + * + * The first case is the lit CONTROL. The same reader finds the measurement the + * header must keep, and finds it only between the docblock's own bounds, so an + * empty or misplaced read cannot let the second case pass as a scan of nothing. + * + * If a later ruling takes `sharing_rule` out of the set, the header changes in + * that pull request, and this file changes with it. + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const SOURCE = readFileSync(path.join(here, 'clientValidation.ts'), 'utf8'); + +/** The declaration the header documents. */ +const ANCHOR = '\nconst AUTHOR_SHAPE_ONLY_TYPES = '; + +/** + * The docblock that ends directly above `ANCHOR`. A lookup, not a comment mask: + * nothing is removed from the source, the block is only located. Answers '' when + * the declaration is gone or is not directly preceded by a docblock, which every + * case below reads as a failure. + */ +function headerAboveSet(src: string): string { + const at = src.indexOf(ANCHOR); + if (at < 0) return ''; + const before = src.slice(0, at); + const close = before.lastIndexOf('*/'); + if (close < 0 || before.slice(close + 2).trim() !== '') return ''; + const open = before.lastIndexOf('/**', close); + return open < 0 ? '' : before.slice(open, close + 2); +} + +const HEADER = headerAboveSet(SOURCE); + +describe('objectui#10150 — the AUTHOR_SHAPE_ONLY_TYPES header records the objectui#7612 ruling', () => { + it('CONTROL — the reader returns that header, bounded, and it still carries the measurement', () => { + expect(HEADER, 'no docblock directly above `const AUTHOR_SHAPE_ONLY_TYPES`').not.toBe(''); + + // The measurement is why the type was listed, and the card that added the + // ruling kept it on purpose: the envelope family, the read decoration, and + // the inversion both would cause. + for (const subject of ['ADR-0010', '_diagnostics', 'objectstack#5316']) { + expect(HEADER, `the header no longer names ${subject}`).toContain(subject); + } + + // Bounded on both sides: the code just above the docblock and the comment + // just below the set are in the file, and neither is in what was read. + for (const outside of ['function expandViewIssues', 'Map metadata-type name']) { + expect(SOURCE).toContain(outside); + expect(HEADER).not.toContain(outside); + } + }); + + it('cites the ruling and the closed ingress by name, in the header itself', () => { + for (const subject of ['objectui#7612', 'extractDraftBody']) { + expect( + HEADER, + `the header above AUTHOR_SHAPE_ONLY_TYPES must name ${subject}: the ` + + '`_diagnostics` ingress it describes was closed, the edit-door switch ' + + 'was put to the maintainer on objectui#7612, and the ruling kept that ' + + 'door with the server. Without it the header reads as a plan whose ' + + 'condition is met (objectui#10150).', + ).toContain(subject); + } + }); +}); diff --git a/packages/app-shell/src/views/metadata-admin/clientValidation.ts b/packages/app-shell/src/views/metadata-admin/clientValidation.ts index d7ba23ed78..4181dbb619 100644 --- a/packages/app-shell/src/views/metadata-admin/clientValidation.ts +++ b/packages/app-shell/src/views/metadata-admin/clientValidation.ts @@ -562,11 +562,33 @@ function expandViewIssues( * where a permissive match-all sharing condition gets written — and deliberately * not on `edit`. This is NOT a tolerant fallback: nothing is coerced and no * draft is waved through; one door has a client gate and the other keeps the - * server's. Turning this gate on is gated on the `_diagnostics` ingress being - * closed first (the strip belongs where the draft is assembled, not here — + * server's. + * + * ── The ingress condition was MET, and the answer was still no (objectui#7612) ── + * + * This block used to end on a plan: switch the edit gate on once the + * `_diagnostics` ingress is closed where the draft is assembled (not here — * reconstructing the decoration list in this file would be a second de-facto - * contract). Until then a gate here would refuse legitimate author input, which - * is worse than the defense-in-depth gap it closes. + * contract), and not before, since until then a gate here would refuse + * legitimate author input. That condition was met. objectui#7603 stripped the + * read decorations off the pending draft, and objectui#8181 hoisted the strip + * into `extractDraftBody`, which removes the spec's own list through the spec's + * `stripReadDecorations` before the pending draft is merged. So the + * `getDraft().item` assembly quoted above is the edit path as objectui#6982 + * measured it, not as it stands; for the live answer read + * `ResourceEditPage.readDecorationStrip.test.tsx`, not this paragraph. + * + * The switch was then put to the maintainer on objectui#7612, with the met + * condition measured on that card and a finished implementation beside it + * (PR objectui#10054). The maintainer ruled option A on 2026-09-20 (restated on + * objectui#10150), verbatim 「7612 只需要服务端校验」 ("7612 needs server-side + * validation only"): the edit door stays ungated, because the server is + * authoritative on it and that is enough. The PR closed unmerged. The create + * door was not in question and keeps its client gate. + * + * ⇒ A closed ingress is NOT a reason to take `sharing_rule` out of this set: + * that question was asked with the condition already met, and answered. Only a + * new maintainer ruling re-opens it, never a re-reading of the ingress. */ const AUTHOR_SHAPE_ONLY_TYPES = new Set(['sharing_rule']);