Skip to content

fix(plugin-sharing): report a refused resolveToken usage stamp once as a durability degradation — #12981 batch 9 - #14383

Merged
os-sales merged 3 commits into
mainfrom
claude/issue-12981-batch9-share-link-durability
Sep 2, 2026
Merged

fix(plugin-sharing): report a refused resolveToken usage stamp once as a durability degradation — #12981 batch 9#14383
os-sales merged 3 commits into
mainfrom
claude/issue-12981-batch9-share-link-durability

Conversation

@claude

@claude claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Part of #12981 — batch 9 of the DARK worklist: the resolveToken usage-stamp swallow in packages/plugins/plugin-sharing/src/share-link-service.ts. Batch 10 (widening DURABILITY_CRITICAL_CALLEES together with declaring keys.ts::handleKeysRequest in FAILURE_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/main at 5c7cbe37 (this branch's base; the file last landed via #14171 67192ce0), not on batch 8's line numbers: resolveToken begins at share-link-service.ts:600; the use_count / last_used_at update is :702-711; the swallowing catch whose 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-258 declares use_count as "Incremented by resolveToken on every successful resolution" and :247-251 declares last_used_at as "Stamped by resolveToken; used by the dashboard to highlight active links", both readonly: true — which is exactly why the write goes out under SYSTEM_CTX (readonly-strict-errors.ts:130: isSystem exempts statically readonly fields). And the shipped active_links grid (: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

  • The catch now binds the error and calls a private reportUsageStampRefusal(row, err). The resolution itself is unchanged: the holder is still served, redactFields is unchanged, resolveToken never throws for a refused stamp.
  • Say it ONCE. A private usageStampRefusalReported latch, per service instance, never reset. resolveToken runs 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.
  • The one line carries the consequence (both counters are not being persisted; links keep resolving so nothing looks broken; the active_links grid 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 through index.ts:29, with warn required under #9754 / #10556 and pinned by logger-required-warn.pin.ts; the plugin injects ctx.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 declare error — is FALSE: it declares error?, and it is the ruling's own option-C terminal shape ({ info?, warn, error? }, the bulk-recompute.ts shape). The ruling's option-B harm (growing error? onto a published type, enrolling every module on it into check:optional-error-sink) does not arise because nothing is widened. Spelled the outbox-sweep.ts way — if (this.logger?.error) this.logger.error(...) else this.logger?.warn?.(...) — because a conditional error?.(...) call against a host sink without error emits nothing. pnpm check:optional-error-sink and pnpm check:durability-log-level both print their pass lines on this head.

Clause-②: no, on both limbs, from the actual diff

  • Path limb: no packages/spec/src/** path in the change set (node scripts/pm/dispatch-gates.mjs lists exactly four paths: the service, its test, one changeset, and the census script).
  • Content limb: no accept/reject behaviour moves, no export is added or changed, no constructor/option member is added or widened. Evidence: git diff -U0 5c7cbe37..51de322d | grep export prints nothing. The new method and field are private.

Changeset: @objectstack/plugin-sharing patch (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:

  • Before (base tree): self-test green, 56 members / 37 files, DARK 5 / 5, share-link-service.ts listed.
  • After the repair: 55 / 36, DARK 4 / 4, LOUD 34 → 35. The site moved to the LOUD bucket because conjunct 1 (silent catch) is exactly what the repair changes — isLogCall reads this.logger.error(...) through the same-file helper reportUsageStampRefusal (walkWithHelpers). Comments are AST-trivial; a log call is not.
  • ⚠️ The self-test went RED after the repair, because its tier-1 DARK positive control WAS this site — chosen after batch 2 destroyed the previous control, on batch 1's "usage telemetry" reading that batch 8 reversed. The control's own text prescribes the remedy: "⛔ If a later card ever does repair it, repoint this control at another member ruled OUT rather than at a member merely not repaired YET."

So the second commit (51de322d) repoints that one control to packages/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, drop 51de322d — the repair commit 3186953c stands 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.ts is named by the control, not edited).

Verification (all on 51de322d unless stated; exit codes captured before any pipe)

Suite. pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.tsTest 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:

  1. positive — the link still resolves, and the refusal is reported at error naming both counters (and the counters genuinely did not move);
  2. say it ONCE — five consecutive refused stamps produce exactly one report (N = 5 ≥ 3);
  3. reverse control — five stamps that LAND produce zero output at every level, and use_count reaches 5 with last_used_at a parseable timestamp (the success-path invariance);
  4. the first degradation after healthy stamps is reported, once, and later refusals stay silent;
  5. falls back to the guaranteed warn channel when the host sink declares no error;
  6. a host with no logger at all is served exactly as before.

Typecheck. pnpm --filter @objectstack/plugin-sharing typecheck → exit 0. ⚠️ That program excludes **/*.test.ts, so it says nothing about the new tests (tsc --noEmit --listFiles lists the test file 0 times). Measured separately: a scratch tsconfig extending the package's, including only the test file, --listFiles lists it once, exit 0, no error TS.

Ablation (commit first; trap-restored; no dist leg because the suite imports ./share-link-service.js from the same package, which vitest resolves to source): the reporter call in the catch replaced by a comment. On-disk proof: marker count 1 → 0, injected marker 0 → 1, blob 501a02dc → 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 hash 501a02dc == HEAD: blob, git diff HEAD empty, git status clean.

Gates. node scripts/pm/dispatch-gates.mjs (no paths; change set from the merge base 5c7cbe37, 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), plus check:nul-bytes, check:durability-log-level and check:optional-error-sink on top. All 47 were run on 51de322d (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 3 PREREQUISITE NOT MET (grades a saved turbo run test log; CI tees one).
  • pnpm check:dual-build-cjs-loads → exit 3 PREREQUISITE NOT MET (needs every package's dist/; a workspace-wide build is CI's). Targeted equivalent on the built @objectstack/plugin-sharing: dist/index.js carries the change and require() of it loads, ShareLinkService.prototype.resolveToken is a function.
  • pnpm check:type-check-debt → exit 3 PREREQUISITE NOT MET (--re-measure needs 41 more built type entry points; CI builds ./packages/* + ./packages/*/* first). The only ledger entry this diff can move is plugin-sharing's TEST_DEBT, and the new test file type-checks clean as measured above.
    pnpm check:i18n was 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.mjs itself: the TS/JS objects match **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} and packages/**/*.{ts,tsx,mts,cts} minus NEVER_LINTED, so the service, its test and the census .mjs are in it and the changeset .md is not ("File ignored because no matching configuration was supplied"). Count from --format json: 4 files handed in, 3 linted, 0 errors / 0 warnings on each. Invariance: the config sets no parserOptions.project and no typed @typescript-eslint rules (its own comment at eslint.config.mjs:328), so this diff cannot move any untouched file's verdict; the repo-wide pnpm lint is CI's.

Census. node scripts/measure-durability-swallow-family.mjs --self-test green 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.mdx moved by +6 (434→440, 488→494, 492→498, 565→571, 595→601) because this PR inserted lines above the isSystem read sites in share-link-service.ts. Gate family check-system-context-census; repaired with its own --fix, population unchanged (109 sites / 20 packages / 45 files), --self-test green. 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

…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
@github-actions github-actions Bot added the size/m label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e8088909585e57a78389c002bf40545a01b01428packageMentionDocs.

Which tree this was computed on

This run read content/docs from ace813fa28780f188a00ac158a2e2d35765141a1 — the merge of head 8d79c623f4e306cf9d7b079d2626102c3d9a329a into base e8088909585e57a78389c002bf40545a01b01428, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

os-sales commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

CI status note (PM seat, session_01AUF1NoViznQK32gqpK8wS8): Lint & Repo Gates is red on 51de322d at one gate, check-system-context-census — a pure anchor shift: content/docs/permissions/system-context.mdx cites five context.isSystem read sites in packages/plugins/plugin-sharing/src/share-link-service.ts at their old lines (434, 488, 492, 565, 595), and this PR's insertions moved them by +6 (440, 494, 498, 571, 601). It is this PR's to fix; the fix is in flight (the delivering dev runs the gate's own --fix and pushes one docs-anchor commit, declared on the body as an adjacent mechanical fix). No re-run is owed — the failure is deterministic on this head. The ACCEPT recorded on #12981 covers the batch-9 increment; the seat verifies the next head differs from 51de322d by that one docs file only before marking ready.


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

os-sales commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Landing provenance — ready + auto-merge at head 8d79c623


Generated by Claude Code

Merged via the queue into main with commit 953a81f Sep 2, 2026
37 of 39 checks passed
@os-sales
os-sales deleted the claude/issue-12981-batch9-share-link-durability branch September 2, 2026 09:49
os-sales pushed a commit that referenced this pull request Sep 2, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants