fix(plugin-sharing): report a refused resolveToken usage stamp once as a durability degradation — #12981 batch 9 - #14383
Conversation
…s a durability degradation
`ShareLinkService.resolveToken` stamps `use_count` / `last_used_at` on
`sys_share_link` after a successful resolution; the stamp's `catch` was empty,
so a storage refusal froze both counters while the link kept resolving and the
shipped `active_links` grid kept asserting them. The refusal is now reported
through the service's existing `{ info?, warn, error? }` logger at `error`
(guaranteed `warn` fallback), once per service instance, with the resolution
itself unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…red share-link stamp to harness.ts The census self-test pinned `share-link-service.ts`'s usage stamp as its tier-1 DARK positive control, with the instruction to repoint at another member ruled OUT if a later card repaired it. Batch 9 repaired it, so the control now names `packages/verify/src/harness.ts` (`inviteForAudienceGate`), the member batch 8 judged out of the programme on the merits and annotated in place. No reading changes: 55/36, DARK 4/4 before and after the repoint. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ace813fa28780f188a00ac158a2e2d35765141a1 && git checkout ace813fa28780f188a00ac158a2e2d35765141a1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e8088909585e57a78389c002bf40545a01b01428 8d79c623f4e306cf9d7b079d2626102c3d9a329a && git checkout -B drift-repro e8088909585e57a78389c002bf40545a01b01428 && git merge --no-ff 8d79c623f4e306cf9d7b079d2626102c3d9a329a
node scripts/docs-audit/affected-docs.mjs --json e8088909585e57a78389c002bf40545a01b01428 |
|
CI status note (PM seat, session_01AUF1NoViznQK32gqpK8wS8): Generated by Claude Code |
…-link-service.ts (#12981 batch 9) The batch-9 repair inserted lines above the five `isSystem` read sites in `share-link-service.ts`, so the census anchors on `system-context.mdx` row 37 rotted by +6 (434→440, 488→494, 492→498, 565→571, 595→601). Re-anchored by the gate's own `--fix`; population unchanged (109 sites, 20 packages, 45 files). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Landing provenance — ready + auto-merge at head
|
…ain merge Patch round 1. `content/docs/permissions/system-context.mdx` is os-regen-driven and both sides moved it again, so the merge stopped there and neither side could simply win: main's #14383 (#12981 batch 9) moved row 37's `share-link-service.ts` anchors by +6, while this branch moved row 36's `sharing-plugin.ts` anchor by +11 and rewrote row 30 plus rough edge 2. The resolution takes row 37 from main and row 36 from this branch — the two sides moved different rows, so there was no real overlap, only the driver's refusal to text-merge a generated page. Verified by re-deriving rather than by inspection: `check:system-context-census` reports "OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Part of #12981 — batch 9 of the DARK worklist: the
resolveTokenusage-stamp swallow inpackages/plugins/plugin-sharing/src/share-link-service.ts. Batch 10 (wideningDURABILITY_CRITICAL_CALLEEStogether with declaringkeys.ts::handleKeysRequestinFAILURE_PROPAGATION_SITES) remains on the card, so this PR does not close it.Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8 (durable copy of the attribution; the footer is create-only).
The site, relocated on the current tree
Measured on
origin/mainat5c7cbe37(this branch's base; the file last landed via #1417167192ce0), not on batch 8's line numbers:resolveTokenbegins atshare-link-service.ts:600; theuse_count/last_used_atupdate is:702-711; the swallowingcatchwhose whole body was the comment "best-effort — usage telemetry is a nice-to-have" is:712-714.Why it is a durability site (batch 8's ruling, not re-litigated)
The persistence claim is not made by the resolve response — that carries neither counter, and both public HTTP callers project a nine-field whitelist (
share-link-routes.ts:279-289) that excludes both. It is made by the declarations:sys-share-link.object.ts:254-258declaresuse_countas "Incremented by resolveToken on every successful resolution" and:247-251declareslast_used_atas "Stamped by resolveToken; used by the dashboard to highlight active links", bothreadonly: true— which is exactly why the write goes out underSYSTEM_CTX(readonly-strict-errors.ts:130:isSystemexempts statically readonly fields). And the shippedactive_linksgrid (:55-60) lists both columns. After a swallowed refusal: HTTP 200 to the holder, and an admin grid asserting a count the system's own declaration defines — wrong, with no signal anywhere. Neither legal alternative applies: the failure is handed to no caller, and a write was genuinely issued.The fix
catchnow binds the error and calls a privatereportUsageStampRefusal(row, err). The resolution itself is unchanged: the holder is still served,redactFieldsis unchanged,resolveTokennever throws for a refused stamp.usageStampRefusalReportedlatch, per service instance, never reset.resolveTokenruns on every public share-link request, so a line per refused stamp is the mirror-image failure the rule names. The latch deliberately does not reset on a later successful stamp: a resetting latch prints on every other request under flapping storage, i.e. the per-request flood again. Later refusals are silent by design, and the one line says so.active_linksgrid under-counts), the fix (resolve the storage refusal named as the cause; refused stamps are not replayed), and the cause, plus{ link, object, record, reason }metadata.The sink and the #13398 class ruling — one correction to the order's premise
The order's section 三.2 said the service has no logger today. On this tree it does:
ShareLinkServiceOptions.logger(share-link-service.ts:365-386), shape{ info?, warn, error?, debug? }, publicly exported throughindex.ts:29, withwarnrequired under #9754 / #10556 and pinned bylogger-required-warn.pin.ts; the plugin injectsctx.logger(sharing-plugin.ts:535). So no new sink is introduced. Judged against that sink, the ruling's conjunction — published AND the sink type does not declareerror— is FALSE: it declareserror?, and it is the ruling's own option-C terminal shape ({ info?, warn, error? }, thebulk-recompute.tsshape). The ruling's option-B harm (growingerror?onto a published type, enrolling every module on it intocheck:optional-error-sink) does not arise because nothing is widened. Spelled theoutbox-sweep.tsway —if (this.logger?.error) this.logger.error(...) else this.logger?.warn?.(...)— because a conditionalerror?.(...)call against a host sink withouterroremits nothing.pnpm check:optional-error-sinkandpnpm check:durability-log-levelboth print their pass lines on this head.Clause-②: no, on both limbs, from the actual diff
packages/spec/src/**path in the change set (node scripts/pm/dispatch-gates.mjslists exactly four paths: the service, its test, one changeset, and the census script).git diff -U0 5c7cbe37..51de322d | grep exportprints nothing. The new method and field areprivate.Changeset:
@objectstack/plugin-sharingpatch (no new export or key).Census: the site LEAVES tier-1 DARK, and the self-test needed one control repointed — flagged for the PM
Section 五 expected the site to stay in DARK. Measured instead, self-test first each time:
56 members / 37 files, DARK5 / 5,share-link-service.tslisted.55 / 36, DARK4 / 4, LOUD34 → 35. The site moved to the LOUD bucket because conjunct 1 (silent catch) is exactly what the repair changes —isLogCallreadsthis.logger.error(...)through the same-file helperreportUsageStampRefusal(walkWithHelpers). Comments are AST-trivial; a log call is not.So the second commit (
51de322d) repoints that one control topackages/verify/src/harness.ts(inviteForAudienceGate,:695,write=insert@699,silent/dark) — the member batch 8 judged OUT of the programme on the merits and annotated inside the catch itself (harness.ts:720-742): nothing claims to have persisted, and the loss is answered one line later loudly. Nothing else in the script changes — no matcher, vocabulary or bucket logic. Self-test after the repoint: green (4 positive / 3 negative / 2 regression / 6 resolution controls, 55 members); the census reading is byte-identical before and after the repoint.This is outside the three files the claim declared, taken as a bounded in-place fix and declared on the card in the same round (
12981#issuecomment-5503850410): same instrument as the card, form pinned by the instrument's own instruction plus batch 8's ruled-OUT list, no other open PR holds the file (all 21 open PRs' file lists read, zero holders of the census script, the service or its test), and the self-test is the verification section 五 already requires (it is not wired into CI). If the PM prefers a separate card for the repoint, drop51de322d— the repair commit3186953cstands alone, with the self-test red as the recorded consequence.Not touched, as ordered:
DURABILITY_CRITICAL_CALLEES,FAILURE_PROPAGATION_SITES,scripts/durability-degradation.baseline.json,content/docs/releases/, and the four remaining DARK sites (harness.tsis named by the control, not edited).Verification (all on
51de322dunless stated; exit codes captured before any pipe)Suite.
pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.ts→Test Files 1 passed (1) · Tests 32 passed (32)— 26 pre-existing plus six new under[#12981] a refused usage stamp is reported ONCE as a durability degradation:errornaming both counters (and the counters genuinely did not move);use_countreaches 5 withlast_used_ata parseable timestamp (the success-path invariance);warnchannel when the host sink declares noerror;Typecheck.⚠️ That program excludes
pnpm --filter @objectstack/plugin-sharing typecheck→ exit 0.**/*.test.ts, so it says nothing about the new tests (tsc --noEmit --listFileslists the test file 0 times). Measured separately: a scratch tsconfig extending the package's, including only the test file,--listFileslists it once, exit 0, noerror TS.Ablation (commit first; trap-restored; no dist leg because the suite imports
./share-link-service.jsfrom the same package, which vitest resolves to source): the reporter call in thecatchreplaced by a comment. On-disk proof: marker count1 → 0, injected marker0 → 1, blob501a02dc → ed84219a. Result:4 failed | 28 passed— the four red are tests 1, 2, 4 and 5 above; the reverse control (3), the no-logger case (6) and all 26 pre-existing stayed green. Restore proven by state:git checkout HEAD -- FILE, then blob hash501a02dc==HEAD:blob,git diff HEADempty,git statusclean.Gates.
node scripts/pm/dispatch-gates.mjs(no paths; change set from the merge base5c7cbe37, three-dot) derived 36 families on the first commit and 44 on the final head (the census-script edit adds 8:agent-test-spelling,bash32-floor,cli-command-ids,entry-guard,parse-guard,pnpm-filter-targets,ratchet-remedy-authority,watch-hint-literal), pluscheck:nul-bytes,check:durability-log-levelandcheck:optional-error-sinkon top. All 47 were run on51de322d(the runner records the sha at start and end of each batch). 44 exit 0 on their own pass lines. Three are NOT MEASURED by their own verdict text, none a red:node scripts/check-test-completeness.mjs→ exit 3PREREQUISITE NOT MET(grades a savedturbo run testlog; CI tees one).pnpm check:dual-build-cjs-loads→ exit 3PREREQUISITE NOT MET(needs every package'sdist/; a workspace-wide build is CI's). Targeted equivalent on the built@objectstack/plugin-sharing:dist/index.jscarries the change andrequire()of it loads,ShareLinkService.prototype.resolveTokenis a function.pnpm check:type-check-debt→ exit 3PREREQUISITE NOT MET(--re-measureneeds 41 more built type entry points; CI builds./packages/*+./packages/*/*first). The only ledger entry this diff can move isplugin-sharing's TEST_DEBT, and the new test file type-checks clean as measured above.pnpm check:i18nwas NOT MEASURED on the first pass (unbuilt CLI) and then measured green after building its declared 10-package closure:OK (9 package(s) — all bundles in sync).ESLint, narrowed with the three pieces of evidence. Population from
eslint.config.mjsitself: the TS/JS objects match**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}andpackages/**/*.{ts,tsx,mts,cts}minusNEVER_LINTED, so the service, its test and the census.mjsare in it and the changeset.mdis not ("File ignored because no matching configuration was supplied"). Count from--format json: 4 files handed in, 3 linted,0 errors / 0 warningson each. Invariance: the config sets noparserOptions.projectand no typed@typescript-eslintrules (its own comment ateslint.config.mjs:328), so this diff cannot move any untouched file's verdict; the repo-widepnpm lintis CI's.Census.
node scripts/measure-durability-swallow-family.mjs --self-testgreen before every reading that is cited above; readings quoted in the census section.Out of scope
Batch 10 stays on #12981. No other findings — nothing was filed.
Adjacent mechanical fix (patch round)
The five census anchors on row 37 of
content/docs/permissions/system-context.mdxmoved by +6 (434→440, 488→494, 492→498, 565→571, 595→601) because this PR inserted lines above theisSystemread sites inshare-link-service.ts. Gate familycheck-system-context-census; repaired with its own--fix, population unchanged (109 sites / 20 packages / 45 files),--self-testgreen. The file is outside the original claim surface and is declared here and on the card.Generated by Claude Code
Generated by Claude Code
Generated by Claude Code