From 17e0d3384ec2118a5d323d9c095d0d28589688a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 23:49:27 +0000 Subject: [PATCH 1/2] fix(client): normalizeSessionResponse's JSDoc records the image gap as closed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The block's job is to say what the lift compensates for, so it names the cards that opened and closed each compensation. One clause was still in the present tense: `data.user.image` served `null` against a declared `string | undefined`, with its card marked "tracked separately". Both halves went false when `SessionUserSchema.image` widened to `z.string().nullish()` and that card closed — the sentence pointed the next reader at a closed card as if it were somewhere to go look. Rewritten in the past tense, naming the widening that closed it and the residue list in `auth-login-register-envelope.test.ts` that is pinned empty. Nothing else in the block moves. A pin beside it holds the relation rather than the wording: for each card on a declared closed roster, the block may cite it but may not carry an open-tracking phrase beside that citation. Located by symbol, never by line. Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude --- packages/client/src/index.ts | 12 +- ...rmalize-session-response-jsdoc.pin.test.ts | 189 ++++++++++++++++++ 2 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 packages/client/src/normalize-session-response-jsdoc.pin.test.ts diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 102b25a4dab..4f96d1e085b 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1523,11 +1523,13 @@ const SET_AUTH_TOKEN_HEADER = 'set-auth-token'; * reads the row `internalAdapter.createSession` already committed, back by * the response's OWN token — the same seam `/get-session` uses — and attaches * it, rather than this lift inventing one. So `login` and `register` now - * parse as the full declared `SessionResponse`, with one gap that is NOT - * this: `data.user.image` served `null` against a declared - * `string | undefined` (#17235, tracked separately). This does not touch the - * `data.token` rule above: `session.token` is the SAME unsigned string the - * body's own `token` already carried, not a second credential, and + * parse as the full declared `SessionResponse`. The one gap that remained + * when this was written — `data.user.image` served `null` against a declared + * `string | undefined` — closed with #17235, which widened that declaration + * to `z.string().nullish()`; the residue list those two routes are pinned + * against (`auth-login-register-envelope.test.ts` ②) is now empty. This does + * not touch the `data.token` rule above: `session.token` is the SAME unsigned + * string the body's own `token` already carried, not a second credential, and * `data.token` is still never synthesized FROM a session. * * The `!body` guard no longer carries the anonymous answer — since #17881 that diff --git a/packages/client/src/normalize-session-response-jsdoc.pin.test.ts b/packages/client/src/normalize-session-response-jsdoc.pin.test.ts new file mode 100644 index 00000000000..3716e7f528e --- /dev/null +++ b/packages/client/src/normalize-session-response-jsdoc.pin.test.ts @@ -0,0 +1,189 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#18510] `normalizeSessionResponse`'s JSDoc must not cite a CLOSED card as if + * it were still being tracked. + * + * That block's job is to say what the lift compensates for, so it names the + * cards that opened and closed each compensation. A card cited beside a phrase + * like "tracked separately" reads to the next person as 「still open, go look」 + * — and the moment the card closes, the sentence is false while still reading + * present-tense. #18510 is exactly that: `data.user.image` was described as a + * live gap against a declared `string | undefined` with `#17235` marked + * "tracked separately", and PR #18501 both widened the declaration to + * `z.string().nullish()` and closed #17235. + * + * ⛔ This is NOT a pin on the block's wording. It pins one relation: for each + * card on `CLOSED_CARDS`, the block may cite it (past tense is the point of + * citing it at all) but may NOT carry an `OPEN_TRACKING_PHRASES` entry beside + * that citation. A future card that really is open can be tracked here in + * those words; a closed one cannot. Closing a card therefore has a mechanical + * consequence in this file — add its row and this suite says which sentence + * has to move. + * + * Three cases, because two of them exist to keep the third honest: + * + * - `① the locator` — the extractor finds THIS block and discriminates. A + * locator that silently returned `''` would make ② pass while measuring + * nothing, so ① asserts the block's own anchors are present AND that a + * symbol which does not exist yields no block. + * - `② the pin` — the relation above, on the real source. Red before #18510's + * fix, green after. + * - `③ the instrument can still fail` — the same predicate over the sentence + * as it stood before the fix, copied verbatim. Without it a green ② could + * equally mean the predicate stopped looking. + * + * ⭐ Located BY SYMBOL, never by line number — the card that filed this carried + * `:1514-1517`, which had already drifted by the time it was dispatched. + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = dirname(fileURLToPath(import.meta.url)); + +/** + * The JSDoc block immediately above `const = (`, located by symbol. + * Returns `''` when either the declaration or a block above it is absent — + * case ① is what stops that empty answer from reading as a pass. + */ +const jsdocAboveConst = (src: string, symbol: string): string => { + const decl = src.indexOf(`\nconst ${symbol} = (`); + if (decl === -1) return ''; + const close = src.lastIndexOf('*/', decl); + if (close === -1) return ''; + const open = src.lastIndexOf('/**', close); + if (open === -1) return ''; + return src.slice(open, close + 2); +}; + +/** + * Cards this block cites that are CLOSED. Each row is a reading, with the + * state that was read — a row is added when a card closes, not when someone + * guesses it has. + */ +const CLOSED_CARDS: ReadonlyArray = [ + ['#17234', 'closed/completed 2026-09-13T05:55:30Z'], + ['#17235', 'closed/completed 2026-09-16T17:44:08Z — PR #18501'], + ['#17238', 'closed/completed 2026-09-12T19:42:55Z'], + ['#17881', 'landed 2026-09-12T19:42:54Z'], +]; + +/** Phrases that make a card citation beside them read as "still open". */ +const OPEN_TRACKING_PHRASES: readonly string[] = [ + 'tracked separately', + 'tracked elsewhere', + 'still open', + 'still tracked', + 'not yet fixed', + 'open card', +]; + +/** + * How near a phrase has to be to count as describing that citation. Generous + * on purpose: the clause #18510 removed sat two characters away, and a rule + * that only caught the tightest spelling would be evaded by a comma. + */ +const ADJACENCY = 160; + +/** Strip the ` * ` decoration so a claim split across lines reads as one. */ +const flatten = (block: string): string => + block + .split('\n') + .map((line) => line.replace(/^\s*\*\/?/, '').replace(/^\s*\/\*\*/, '')) + .join(' ') + .replace(/\s+/g, ' '); + +/** Every closed card cited within `ADJACENCY` of an open-tracking phrase. */ +const closedCardsReadingAsOpen = (block: string): string[] => { + const text = flatten(block); + const flagged: string[] = []; + for (const [card] of CLOSED_CARDS) { + for (let at = text.indexOf(card); at !== -1; at = text.indexOf(card, at + 1)) { + const window = text.slice( + Math.max(0, at - ADJACENCY), + at + card.length + ADJACENCY, + ); + if (OPEN_TRACKING_PHRASES.some((phrase) => window.includes(phrase))) { + flagged.push(card); + break; + } + } + } + return flagged; +}; + +const SRC = readFileSync(join(HERE, 'index.ts'), 'utf8'); +const BLOCK = jsdocAboveConst(SRC, 'normalizeSessionResponse'); + +describe('#18510 — `normalizeSessionResponse`s JSDoc cites no closed card as open', () => { + describe('① the locator found THIS block, and can fail to find one', () => { + it('the block carries the anchors that identify it', () => { + // Three anchors the block owns and its neighbours do not. If the + // extractor ever grabs a different block, or the whole file, these move. + expect(BLOCK).toContain('#17234'); + expect(BLOCK).toContain('data.token'); + expect(BLOCK).toContain('SessionResponseSchema'); + // And it really is a single JSDoc block, not a slice of the file. + expect(BLOCK.startsWith('/**')).toBe(true); + expect(BLOCK.endsWith('*/')).toBe(true); + expect(BLOCK.slice(3, -2)).not.toContain('*/'); + }); + + it('a symbol this file does not declare yields no block', () => { + expect(jsdocAboveConst(SRC, 'normalizeSessionResponseThatDoesNotExist')).toBe(''); + }); + }); + + describe('② the pin', () => { + it('no closed card is cited beside an open-tracking phrase', () => { + const flagged = closedCardsReadingAsOpen(BLOCK); + expect( + flagged, + `these cards are closed but the JSDoc still reads as tracking them: ${flagged + .map((card) => `${card} (${CLOSED_CARDS.find(([c]) => c === card)?.[1]})`) + .join(', ')}`, + ).toEqual([]); + }); + }); + + describe('③ the instrument can still fail — negative control', () => { + it('the sentence as it stood before #18510 is reported', () => { + // Verbatim from `packages/client/src/index.ts` at a84da6096a, the base + // this fix was cut from. Not a paraphrase: a predicate that only catches + // a rewritten example is a predicate that would have missed the defect. + const beforeTheFix = [ + '/**', + ' * So `login` and `register` now', + ' * parse as the full declared `SessionResponse`, with one gap that is NOT', + ' * this: `data.user.image` served `null` against a declared', + ' * `string | undefined` (#17235, tracked separately). This does not touch the', + ' * `data.token` rule above.', + ' */', + ].join('\n'); + + expect(closedCardsReadingAsOpen(beforeTheFix)).toEqual(['#17235']); + }); + + it('and a citation with no such phrase beside it is NOT reported', () => { + // The other half: the predicate must not flag the past-tense form, or ② + // would be unsatisfiable and the block would lose the citation entirely. + // + // ⚠️ This half earned its keep on the first run: the past-tense sentence + // drafted for #18510 opened "the one gap that was still open when this + // was written", and `still open` is on the vocabulary above — the + // rewrite would have tripped the pin it was written to satisfy. The + // shipped sentence says "remained" for that reason. + const pastTense = [ + '/**', + ' * The one gap that remained when this was written closed with', + ' * #17235, which widened that declaration to `z.string().nullish()`.', + ' */', + ].join('\n'); + + expect(closedCardsReadingAsOpen(pastTense)).toEqual([]); + }); + }); +}); From 5b19c70af629bfc931c5d0cb3a7408a7ecb0a812 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 23:59:13 +0000 Subject: [PATCH 2/2] chore(client): the JSDoc fix takes a patch changeset, measured on the published dist `@objectstack/client` ships `dist`; the four code/type artifacts come out byte-identical across this change, but both source maps move, so the published tarball's bytes move and `skip-changeset` would be a false declaration. Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude --- ...alize-session-response-jsdoc-past-tense.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .changeset/18510-normalize-session-response-jsdoc-past-tense.md diff --git a/.changeset/18510-normalize-session-response-jsdoc-past-tense.md b/.changeset/18510-normalize-session-response-jsdoc-past-tense.md new file mode 100644 index 00000000000..8b7463ffec6 --- /dev/null +++ b/.changeset/18510-normalize-session-response-jsdoc-past-tense.md @@ -0,0 +1,47 @@ +--- +"@objectstack/client": patch +--- + +fix(client): `normalizeSessionResponse`'s JSDoc records the `data.user.image` gap as closed, not as tracked (#18510) + +Clause-②: no + +No declaration, accept set, export or runtime behaviour moves. What moves is one +sentence of developer commentary and the pin that now holds it honest. + +The block above `normalizeSessionResponse` says what the lift compensates for, +so it names the cards that opened and closed each compensation. One clause was +still in the present tense: + +> … with one gap that is NOT this: `data.user.image` served `null` against a +> declared `string | undefined` (#17235, tracked separately). + +Both halves went false when `SessionUserSchema.image` widened to +`z.string().nullish()` and #17235 closed — the sentence described a live gap +that no longer existed and pointed the next reader at a closed card as somewhere +to go look. It now reads in the past tense, naming the widening that closed it +and the residue list in `auth-login-register-envelope.test.ts` that is pinned +empty. Nothing else in the block moves. + +**Why this is a `patch` and not `skip-changeset`, measured rather than +assumed.** "Only comments changed" is not "nothing published moves", and on this +package the two answers differ. `@objectstack/client` ships `dist`, `README.md` +and `CHANGELOG.md`; `dist` is six files (`index.js`, `index.mjs`, `index.d.ts`, +`index.d.mts` and a `.map` beside each of the two bundles — this package emits no +`*.cjs` and no `*.d.cts`). Built from the same tree before and after the change: + +- the comment text reaches **none** of the six (`no longer residue`, + `data.user.image` and `tracked separately` each 0 hits), while the positive + controls land — `{@link normalizeSessionResponse}` appears 3× in each bundle + and 3× in each `.d.ts`, carried there by the JSDoc of the **exported** + `auth.login` / `auth.register` / `auth.me` that link to it, and `set-auth-token` + 4× / 3×. So comment text from this file does reach the published types; this + block's own text does not, because the function it documents is not exported; +- `index.js`, `index.mjs`, `index.d.ts` and `index.d.mts` are **byte-identical** + across the change (sha256, same build, reproducibility control re-run); +- both `.map` files **differ**. Neither carries `sourcesContent`, so no comment + text ships inside them either — the position table shifts because the rewritten + comment is two lines longer than the one it replaced. + +So the published tarball's bytes do move, and a released package whose shipped +bytes move takes a changeset.