Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .changeset/sharing-sweep-logger-required-warn.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
---
'@objectstack/plugin-sharing': minor
---

⚠️ **BREAKING (published parameter tightened):** `sweepOrphanedRowsByRecordExistence` —
publicly exported from `@objectstack/plugin-sharing` — now types its optional `logger`
parameter as `{ info?, warn }` with real signatures (`(msg: any, ...rest: any[]) => void`)
and a **required** `warn`, replacing the old `{ info?: Function, warn?: Function }`. A host
that passed this function a logger without a `warn` member (for example an
`{ info, error }`-only literal) compiles today and stops compiling after this release. The
one-line fix: add a `warn` callback to that logger object — or omit the `logger` argument
entirely, since the parameter itself stays optional. Nothing about the sweep's runtime
behaviour changes, and no call site in this repo moved: both in-package callers forward the
owning services' `logger` options, whose `warn` is already required since the producer
tightening (`SharingServiceOptions` / `ShareLinkServiceOptions` /
`SharingRuleServiceOptions`) shipped in the previous release.

Why: every report this sweep emits lands on `warn` — the "could not check whether records
still exist", "stopped early" and "revoked N rows" lines — so a logger without a
guaranteed `warn` is one the sweep can lose its ONLY output into. That is #9754's
permit-silence shape, one module downstream of the producers #10556 tightened, and the
maintainer ruled (#10692, 2026-08-25) that this package's logger contracts refuse it
loudly at compile time rather than keep it. The bare `Function` members were also their
own defect: they documented no call shape and caught no arity mistake. The refusal is
pinned at compile time in `logger-required-warn.pin.ts`, which also pins the three
publicly exported options types above.

Breaking ships as `minor` per the launch-window convention
(`scripts/check-changeset-no-major.mjs`).

<!-- adr-0087: not-required (no-migration-prescription) A TypeScript parameter type on one published function tightens; no metadata surface is involved — no Zod schema, no `packages/spec` declaration, no authorable key, no stored representation — so `objectstack migrate meta` has nothing to visit and no conversion-layer entry could replay anything. The affected caller is host CODE, and the compiler names the exact argument at the exact call line on upgrade, which is more precise than a ledger entry; the repair is adding a `warn` callback to that logger object. The sibling host repo objectui was grepped at claim time (ref 194fae18): zero imports of this package and zero warn-less logger literals, so no known caller has code to rewrite. -->
105 changes: 105 additions & 0 deletions packages/plugins/plugin-sharing/src/logger-required-warn.pin.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #10692 — compile-time pins for the REQUIRED `warn` channel on this package's
* logger sinks, and for their members being real signatures rather than bare
* `Function`.
*
* WHY A PIN AT ALL. A test that passes a full logger cannot tell a required
* member from an optional one — every runtime suite in this package stays
* green if some `warn:` quietly regresses to `warn?:` or to `Function`. The
* contract IS the compile-time refusal of a `{ info, error }`-only host
* logger (#9754: an optional `error` beside an optional `warn` is a contract
* that permits silence; the maintainer ruled on 2026-08-25 that these
* publicly exported types refuse it loudly instead of baselining it). So the
* pin is the refusal itself: each `@ts-expect-error` below is red exactly
* when the refusal stops happening.
*
* WHY A `.pin.ts` AND NOT A `*.test.ts`: this package's `tsconfig.json`
* excludes `**\/*.test.ts` (measured TEST_DEBT), so a pin written in a test
* file is read by NO tsc program the `typecheck` script runs — a phantom
* check that stays green however the contract is broken. This file IS in the
* program, is imported by nothing, and is never bundled (tsup entry is
* `src/index.ts`), exactly like `exec-context-annotation.pin.ts`.
*
* ⚠️ SHAPE DISCIPLINE for this file: it deliberately declares NO interface,
* no type-literal alias and no inline type literal carrying channel-named
* members. `check:optional-error-sink`'s census reads exactly those three
* node kinds, and a pin file must observe the population, never join it. The
* sinks are named below through indexed-access and `Parameters<>` types only.
*
* The four pinned sinks:
* - `SharingServiceOptions['logger']` (public, tightened by PR #11856)
* - `ShareLinkServiceOptions['logger']` (public, tightened by PR #11856)
* - `SharingRuleServiceOptions['logger']` (public, tightened by PR #11856)
* - `sweepOrphanedRowsByRecordExistence`'s `logger` parameter
* (`record-orphan-cleanup.ts`'s module-local `MinimalLogger`, tightened
* by #10692 — the consumer that was blocked behind the three producers)
*/

import type { SharingServiceOptions } from './sharing-service.js';
import type { ShareLinkServiceOptions } from './share-link-service.js';
import type { SharingRuleServiceOptions } from './sharing-rule-service.js';
import type { sweepOrphanedRowsByRecordExistence } from './record-orphan-cleanup.js';

type SharingSink = NonNullable<SharingServiceOptions['logger']>;
type ShareLinkSink = NonNullable<ShareLinkServiceOptions['logger']>;
type RuleSink = NonNullable<SharingRuleServiceOptions['logger']>;
type SweepSink = NonNullable<Parameters<typeof sweepOrphanedRowsByRecordExistence>[3]>;

/**
* Never called — every line below is a type-level assertion evaluated by
* `tsc --noEmit`. `noop` and `anyCallable` exist only to give the literals
* members to carry.
*/
export function __pinLoggerWarnIsRequiredAndReal(
noop: (msg: any, ...rest: any[]) => void,
anyCallable: Function,
): void {
// ── NEGATIVE: a `{ info, error }`-only host logger must NOT compile. ─────
// This is the break the changeset declares, pinned as a refusal. The three
// producers declare `error?`, so the ONLY error each literal can produce is
// the missing required `warn` (TS2741) — nothing else can satisfy the
// directive. The sweep's sink declares no `error` member at all, so its
// refusal literal spells only `info` (an `error` key would trip the
// excess-property check instead and let a `warn?` regression hide).
// @ts-expect-error 'warn' is required on SharingServiceOptions['logger'] (#9754/#10692)
const sharingRefusesSilence: SharingSink = { info: noop, error: noop };
// @ts-expect-error 'warn' is required on ShareLinkServiceOptions['logger'] (#9754/#10692)
const shareLinkRefusesSilence: ShareLinkSink = { info: noop, error: noop };
// @ts-expect-error 'warn' is required on SharingRuleServiceOptions['logger'] (#9754/#10692)
const ruleRefusesSilence: RuleSink = { info: noop, error: noop };
// @ts-expect-error 'warn' is required on the orphan sweep's logger (#10692)
const sweepRefusesWarnless: SweepSink = { info: noop };

// ── POSITIVE control: a bare `{ warn }` stub still compiles everywhere. ──
// Keeps the negatives honest (the aliases resolve, the literals are not red
// for some unrelated reason) and pins the documented promise that a minimal
// test stub stays a legal host logger.
const sharingAcceptsWarnOnly: SharingSink = { warn: noop };
const shareLinkAcceptsWarnOnly: ShareLinkSink = { warn: noop };
const ruleAcceptsWarnOnly: RuleSink = { warn: noop };
const sweepAcceptsWarnOnly: SweepSink = { warn: noop };

// ── NEGATIVE: bare `Function` no longer satisfies the members. ───────────
// `Function` is assignable to a concrete signature in neither direction, so
// this line is red exactly while the member carries a REAL signature — and
// goes green (failing the directive) if anyone loosens it back to
// `warn?: Function` or `warn: any` (#11069 taught the sink census to read
// bare `Function`; the members must not regress to it).
// @ts-expect-error a value of type Function does not satisfy SharingSink's real-signature 'warn'
const sharingRefusesBareFunction: SharingSink = { warn: anyCallable };
// @ts-expect-error a value of type Function does not satisfy SweepSink's real-signature 'warn'
const sweepRefusesBareFunction: SweepSink = { warn: anyCallable };

void sharingRefusesSilence;
void shareLinkRefusesSilence;
void ruleRefusesSilence;
void sweepRefusesWarnless;
void sharingAcceptsWarnOnly;
void shareLinkAcceptsWarnOnly;
void ruleAcceptsWarnOnly;
void sweepAcceptsWarnOnly;
void sharingRefusesBareFunction;
void sweepRefusesBareFunction;
}
18 changes: 10 additions & 8 deletions packages/plugins/plugin-sharing/src/logger-shapes.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,14 +58,16 @@
* is required because they FORWARD into `bulk-recompute.ts`'s guaranteed sink;
* a `warn?` there re-opens the silence one module downstream of where #10556
* closed it.
* - `record-orphan-cleanup.ts` — `{ info?: Function, warn?: Function }`. Bare
* `Function` documents nothing, but it cannot be tightened to this shape:
* `Function` is NOT assignable to a concrete signature ("Type 'Function'
* provides no match for the signature"), and the two loggers handed to it —
* `SharingServiceOptions['logger']` and `ShareLinkServiceOptions['logger']` —
* are themselves spelled with bare `Function`. Tightening it requires
* tightening those producers first, which is a gate-population change, not a
* refactor. It stays open on #10692 rather than being done quietly here.
* - `record-orphan-cleanup.ts` — `{ info?, warn }`. Its `warn` is required
* because the sweep's every report lands there (#10692, ruled 2026-08-25);
* this type must stay optional-`warn` because its consumers are
* fire-and-forget projections whose reports are advisory. The bare-`Function`
* spelling it once carried could not tighten until the two producers feeding
* it — `SharingServiceOptions['logger']` and `ShareLinkServiceOptions
* ['logger']` — dropped bare `Function` (#10556 limb (c), PR #11856;
* `Function` is assignable to no concrete signature). Requiredness of every
* required `warn` in this package's public options types and in the sweep is
* pinned by `logger-required-warn.pin.ts`.
*/
export interface OptionalSharingLogger {
info?: (msg: string, meta?: Record<string, any>) => void;
Expand Down
25 changes: 21 additions & 4 deletions packages/plugins/plugin-sharing/src/record-orphan-cleanup.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,13 +86,30 @@ export interface OrphanShareSweepResult {
}

/**
* Structural and loose on purpose — it has to accept both owning services'
* option shapes (`SharingServiceOptions['logger']`, `ShareLinkServiceOptions`)
* Structural on purpose — it accepts both owning services' option shapes
* (`SharingServiceOptions['logger']`, `ShareLinkServiceOptions['logger']`)
* and a bare `{ warn }` stub in a test.
*
* `warn` is REQUIRED, and the members carry the same real signatures as the
* producers that feed this parameter (#10692, ruled 2026-08-25). Every report
* this module emits lands on `warn` — the "could not check", "stopped early"
* and "revoked N rows" lines — so a logger without a guaranteed `warn` is one
* this sweep can lose its ONLY output into: #9754's silence rule, one module
* downstream of the producers #10556 tightened. Both owning services' `logger`
* members require `warn` since PR #11856, so they stay assignable; what stops
* compiling is a logger with no `warn`, which was never a useful argument
* here. The bare-`Function` spelling this replaces documented no call shape
* and could not tighten until those producers dropped bare `Function`
* (`Function` is assignable to no concrete signature).
*
* Deliberately NO `error` member — adding one would enrol this sink in
* `check:optional-error-sink`'s population (see `logger-shapes.ts`, "Why this
* shape declares no `error`"). Requiredness is pinned in
* `logger-required-warn.pin.ts`.
*/
interface MinimalLogger {
info?: Function;
warn?: Function;
info?: (msg: any, ...rest: any[]) => void;
warn: (msg: any, ...rest: any[]) => void;
}

/** How one caller's rows are named in this module's log lines. */
Expand Down
Loading