Skip to content

deps: Bump the dev-dependencies group with 3 updates#5

Closed
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/npm_and_yarn/dev-dependencies-66b71395a7
Closed

deps: Bump the dev-dependencies group with 3 updates#5
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/npm_and_yarn/dev-dependencies-66b71395a7

Conversation

@dependabot
Copy link
Copy Markdown
Contributor

@dependabot dependabot Bot commented on behalf of github May 12, 2026

Bumps the dev-dependencies group with 3 updates: @eslint/js, eslint and typescript.

Updates @eslint/js from 9.39.4 to 10.0.1

Release notes

Sourced from @​eslint/js's releases.

v10.0.1

Bug Fixes

  • c87d5bd fix: update eslint (#20531) (renovate[bot])
  • d841001 fix: update minimatch to 10.2.1 to address security vulnerabilities (#20519) (루밀LuMir)
  • 04c2147 fix: update error message for unused suppressions (#20496) (fnx)
  • 38b089c fix: update dependency @​eslint/config-array to ^0.23.1 (#20484) (renovate[bot])

Documentation

  • 5b3dbce docs: add AI acknowledgement section to templates (#20431) (루밀LuMir)
  • 6f23076 docs: toggle nav in no-JS mode (#20476) (Tanuj Kanti)
  • b69cfb3 docs: Update README (GitHub Actions Bot)

Chores

  • e5c281f chore: updates for v9.39.3 release (Jenkins)
  • 8c3832a chore: update @​typescript-eslint/parser to ^8.56.0 (#20514) (Milos Djermanovic)
  • 8330d23 test: add tests for config-api (#20493) (Milos Djermanovic)
  • 37d6e91 chore: remove eslint v10 prereleases from eslint-config-eslint deps (#20494) (Milos Djermanovic)
  • da7cd0e refactor: cleanup error message templates (#20479) (Francesco Trotta)
  • 84fb885 chore: package.json update for @​eslint/js release (Jenkins)
  • 1f66734 chore: add eslint to peerDependencies of @eslint/js (#20467) (Milos Djermanovic)

v10.0.0

Breaking Changes

  • f9e54f4 feat!: estimate rule-tester failure location (#20420) (ST-DDT)
  • a176319 feat!: replace chalk with styleText and add color to ResultsMeta (#20227) (루밀LuMir)
  • c7046e6 feat!: enable JSX reference tracking (#20152) (Pixel998)
  • fa31a60 feat!: add name to configs (#20015) (Kirk Waiblinger)
  • 3383e7e fix!: remove deprecated SourceCode methods (#20137) (Pixel998)
  • 501abd0 feat!: update dependency minimatch to v10 (#20246) (renovate[bot])
  • ca4d3b4 fix!: stricter rule tester assertions for valid test cases (#20125) (唯然)
  • 96512a6 fix!: Remove deprecated rule context methods (#20086) (Nicholas C. Zakas)
  • c69fdac feat!: remove eslintrc support (#20037) (Francesco Trotta)
  • 208b5cc feat!: Use ScopeManager#addGlobals() (#20132) (Milos Djermanovic)
  • a2ee188 fix!: add uniqueItems: true in no-invalid-regexp option (#20155) (Tanuj Kanti)
  • a89059d feat!: Program range span entire source text (#20133) (Pixel998)
  • 39a6424 fix!: assert 'text' is a string across all RuleFixer methods (#20082) (Pixel998)
  • f28fbf8 fix!: Deprecate "always" and "as-needed" options of the radix rule (#20223) (Milos Djermanovic)
  • aa3fb2b fix!: tighten func-names schema (#20119) (Pixel998)
  • f6c0ed0 feat!: report eslint-env comments as errors (#20128) (Francesco Trotta)
  • 4bf739f fix!: remove deprecated LintMessage#nodeType and TestCaseError#type (#20096) (Pixel998)
  • 523c076 feat!: drop support for jiti < 2.2.0 (#20016) (michael faith)
  • 454a292 feat!: update eslint:recommended configuration (#20210) (Pixel998)
  • 4f880ee feat!: remove v10_* and inactive unstable_* flags (#20225) (sethamus)
  • f18115c feat!: no-shadow-restricted-names report globalThis by default (#20027) (sethamus)
  • c6358c3 feat!: Require Node.js ^20.19.0 || ^22.13.0 || >=24 (#20160) (Milos Djermanovic)

Features

  • bff9091 feat: handle Array.fromAsync in array-callback-return (#20457) (Francesco Trotta)
  • 290c594 feat: add self to no-implied-eval rule (#20468) (sethamus)
  • 43677de feat: fix handling of function and class expression names in no-shadow (#20432) (Milos Djermanovic)

... (truncated)

Commits
  • 84fb885 chore: package.json update for @​eslint/js release
  • 1f66734 chore: add eslint to peerDependencies of @eslint/js (#20467)
  • f3fbc2f chore: set @eslint/js version to 10.0.0 to skip releasing it (#20466)
  • b4b3127 chore: package.json update for @​eslint/js release
  • 0b14059 chore: package.json update for @​eslint/js release
  • fa31a60 feat!: add name to configs (#20015)
  • 1e2cad5 chore: package.json update for @​eslint/js release
  • 454a292 feat!: update eslint:recommended configuration (#20210)
  • c6358c3 feat!: Require Node.js ^20.19.0 || ^22.13.0 || >=24 (#20160)
  • See full diff in compare view

Updates eslint from 9.39.4 to 10.3.0

Release notes

Sourced from eslint's releases.

v10.3.0

Features

  • 379571a feat: add suggestions for no-unused-private-class-members (#20773) (sethamus)

Bug Fixes

  • b6ae5cf fix: handle unavailable require cache (#20812) (Simon Podlipsky)
  • 6fb3685 fix: rule suggestions cause continuation in class body (#20787) (Milos Djermanovic)

Documentation

  • 32cc7ab docs: fix typos in docs and comments (#20809) (Tanuj Kanti)
  • 7f47937 docs: Update README (GitHub Actions Bot)

Chores

  • d32235e ci: use pnpm in eslint-flat-config-utils type integration test (#20826) (Francesco Trotta)
  • 3ffb14e chore: clean up typos in comments and JSDoc (#20821) (Pixel998)
  • 22eb58a chore: add missing continue-on-error to ecosystem-tests.yml (#20818) (Josh Goldberg ✨)
  • 88bf002 ci: bump pnpm/action-setup from 6.0.1 to 6.0.3 (#20815) (dependabot[bot])
  • 97c8c33 chore: update ilshidur/action-discord action to v0.4.0 (#20811) (renovate[bot])
  • 2f58136 chore: pin peter-evans/create-pull-request action to 5f6978f (#20810) (renovate[bot])
  • 77add7f chore: add initial ecosystem plugin tests workflow (#19643) (Josh Goldberg ✨)
  • 4023b55 test: Add unit tests for SuppressionsService.prune() (#20797) (kuldeep kumar)
  • 54080da test: add unit tests for ForkContext (#20778) (kuldeep kumar)
  • f0e2bcc test: add unit tests for SuppressionsService.suppress() method (#20765) (kuldeep kumar)
  • a7f0b94 chore: update dependency prettier to v3.8.3 (#20782) (renovate[bot])
  • 7bf93d9 chore: update TypeScript to v6 (#20677) (sethamus)
  • b42dd72 ci: bump pnpm/action-setup from 6.0.0 to 6.0.1 (#20781) (dependabot[bot])
  • 2b252be test: add unit tests for IdGenerator (#20775) (kuldeep kumar)

v10.2.1

Bug Fixes

  • 14be92b fix: model generator yield resumption paths in code path analysis (#20665) (sethamus)
  • 84a19d2 fix: no-async-promise-executor false positives for shadowed Promise (#20740) (xbinaryx)
  • af764af fix: clarify language and processor validation errors (#20729) (Pixel998)
  • e251b89 fix: update eslint (#20715) (renovate[bot])

Documentation

  • ca92ca0 docs: reuse markdown-it instance for markdown filter (#20768) (Amaresh S M)
  • 57d2ee2 docs: Enable Eleventy incremental mode for watch (#20767) (Amaresh S M)
  • c1621b9 docs: fix typos in code-path-analyzer.js (#20700) (Ayush Shukla)
  • 1418d52 docs: Update README (GitHub Actions Bot)
  • 39771e6 docs: Update README (GitHub Actions Bot)
  • 71e0469 docs: fix incomplete JSDoc param description in no-shadow rule (#20728) (kuldeep kumar)
  • 22119ce docs: clarify scope of for-direction rule with dead code examples (#20723) (Amaresh S M)
  • 8f3fb77 docs: document meta.docs.dialects (#20718) (Pixel998)

Chores

  • 7ddfea9 chore: update dependency prettier to v3.8.2 (#20770) (renovate[bot])
  • fac40e1 ci: bump pnpm/action-setup from 5.0.0 to 6.0.0 (#20763) (dependabot[bot])
  • 7246f92 test: add tests for SuppressionsService.load() error handling (#20734) (kuldeep kumar)
  • 4f34b1e chore: update pnpm/action-setup action to v5 (#20762) (renovate[bot])

... (truncated)

Commits
  • 7889204 10.3.0
  • 5b69b4f Build: changelog update for 10.3.0
  • d32235e ci: use pnpm in eslint-flat-config-utils type integration test (#20826)
  • b6ae5cf fix: handle unavailable require cache (#20812)
  • 3ffb14e chore: clean up typos in comments and JSDoc (#20821)
  • 6fb3685 fix: rule suggestions cause continuation in class body (#20787)
  • 22eb58a chore: add missing continue-on-error to ecosystem-tests.yml (#20818)
  • 88bf002 ci: bump pnpm/action-setup from 6.0.1 to 6.0.3 (#20815)
  • 379571a feat: add suggestions for no-unused-private-class-members (#20773)
  • 97c8c33 chore: update ilshidur/action-discord action to v0.4.0 (#20811)
  • Additional commits viewable in compare view

Updates typescript from 5.9.3 to 6.0.3

Release notes

Sourced from typescript's releases.

TypeScript 6.0.3

For release notes, check out the release announcement blog post.

Downloads are available on:

TypeScript 6.0

For release notes, check out the release announcement blog post.

Downloads are available on:

TypeScript 6.0 Beta

For release notes, check out the release announcement.

Downloads are available on:

Commits
  • 050880c Bump version to 6.0.3 and LKG
  • eeae9dd 🤖 Pick PR #63401 (Also check package name validity in...) into release-6.0 (#...
  • ad1c695 🤖 Pick PR #63368 (Harden ATA package name filtering) into release-6.0 (#63372)
  • 0725fb4 🤖 Pick PR #63310 (Mark class property initializers as...) into release-6.0 (#...
  • 607a22a Bump version to 6.0.2 and LKG
  • 9e72ab7 🤖 Pick PR #63239 (Fix missing lib files in reused pro...) into release-6.0 (#...
  • 35ff23d 🤖 Pick PR #63163 (Port anyFunctionType subtype fix an...) into release-6.0 (#...
  • e175b69 Bump version to 6.0.1-rc and LKG
  • af4caac Update LKG
  • 8efd7e8 Merge remote-tracking branch 'origin/main' into release-6.0
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore <dependency name> major version will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
  • @dependabot ignore <dependency name> minor version will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
  • @dependabot ignore <dependency name> will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
  • @dependabot unignore <dependency name> will remove all of the ignore conditions of the specified dependency
  • @dependabot unignore <dependency name> <ignore condition> will remove the ignore condition of the specified dependency and ignore conditions

Bumps the dev-dependencies group with 3 updates: [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js), [eslint](https://github.com/eslint/eslint) and [typescript](https://github.com/microsoft/TypeScript).


Updates `@eslint/js` from 9.39.4 to 10.0.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js)

Updates `eslint` from 9.39.4 to 10.3.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](eslint/eslint@v9.39.4...v10.3.0)

Updates `typescript` from 5.9.3 to 6.0.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](microsoft/TypeScript@v5.9.3...v6.0.3)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: eslint
  dependency-version: 10.3.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: typescript
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels May 12, 2026
@ComBba
Copy link
Copy Markdown
Contributor

ComBba commented May 12, 2026

CI fails on this group: bumping @eslint/js to 10.x is incompatible with the pinned eslint 9.x line (eslint v10 isn't GA). @eslint/js is intentionally pinned to ^9 in package.json to track ESLint's major. Will revisit when eslint 10 ships and the trio can move together.

@ComBba ComBba closed this May 12, 2026
@dependabot @github
Copy link
Copy Markdown
Contributor Author

dependabot Bot commented on behalf of github May 12, 2026

This pull request was built based on a group rule. Closing it will not ignore any of these versions in future pull requests.

To ignore these dependencies, configure ignore rules in dependabot.yml

@dependabot dependabot Bot deleted the dependabot/npm_and_yarn/dev-dependencies-66b71395a7 branch May 12, 2026 08:13
ComBba pushed a commit that referenced this pull request May 14, 2026
External code-review pointed out 5 valid issues. All addressed in this commit
on the same branch — PR #44 picks them up automatically.

## Gemini code-assist

### #1 — Manage menu: parallelize per-rule dry-run reads
The previous serial `for` loop multiplied Redis latency by the draft count.
With up to 50 drafts and ~10ms per round-trip the difference is ~500ms vs
~10ms wall time. Switched to `Promise.allSettled([...])` so a single failed
fetch doesn't stop the others.

### #2 — applyManageActions: enforce the 50-rule cap on apply
pause / activate / unpause moves can push a bundle over the 50-rule cap
even when no compose-rule call is made. Added an explicit check before
the dual-write txn — the moderator gets a "Rule cap exceeded (active=N,
draft=N, max=50). Delete a rule first" toast and the bundles stay
untouched. New test in routes-manage.test.ts.

### #3 — Use the configured openaiModel as the fallback (not the literal "manage")
The previous `'manage'` fallback for `bundle.llmModel` broke
`estimateTokenCost()` because 'manage' isn't a key in
`OPENAI_PRICING_USD_PER_TOKEN`. The dashboard shows "(~$0 on manage)" until
a real compile lands. Now we read the configured `openaiModel` setting
through `readOpenaiModel()` (already SELECTION-array-safe per PR #39).

### #4 — Atomic dual-write via WATCH/MULTI/EXEC (the previous PR description's claim was wrong)
The previous `Promise.all([redis.set(activeKey, ...), redis.set(draftKey, ...)])`
was NOT atomic — if the second `set` failed (e.g. plugin RPC blip), pause
could leave a rule absent from active without showing up in draft.
Switched to Devvit's `redis.watch(...).multi().set(...).set(...).exec()`
pattern (same shape executor.ts already uses for audit writes), and check
`exec()`'s null return for the WATCH-aborted case so two moderators
managing rules at the same time get an actionable "Another moderator
changed the rules at the same time — re-open Manage rules" toast.

## CodeRabbit

### #5 — humanizeRule: cap value-stringification at 100 chars
A rule that uses `op: in` against a long allowlist (e.g. 50 banned domains)
would otherwise render thousands of characters into the confirm form's
`compiledSummary` field, blowing past Devvit's modal description budget
and obscuring the actual rule structure. Truncate with `...` after 100
chars in the predicate-tree leaf renderer.

## Test infrastructure
test/devvit-testkit.ts: fakeRedis.watch().exec() now returns `[] | null`
(matching real Devvit Redis MULTI/EXEC semantics) instead of `void`.
Without this, the new WATCH-abort detection in applyManageActions would
fire on every test. Updated `FakeTxn.exec` type accordingly.

## Verification
- `npm run check` 4/4 gates green
- 210 tests pass (1 skipped) — added 1 cap-overflow test in routes-manage.test.ts
ComBba pushed a commit that referenced this pull request May 14, 2026
5 review issues from Gemini code-assist on PR #45 (refactor/split-server-routes).

## HIGH severity

### #1 Non-ASCII regex made unambiguous
helpers/openai.ts: the `replace(/[<U+0080>-<U+FFFF>]/g, ...)` literal contained
the U+0080 control character invisibly, which made review tools render it
as `/[-<box>]/` and flag it as "matches only `-`". Behaviour was always
correct, but the explicit `/[�-ï¿¿]/g` form removes any doubt.

### #2 WATCH ordering in applyManageActions
routes/manage.ts: the previous shape did GET → modify → WATCH → MULTI →
EXEC, so the optimistic lock didn't actually cover the data we were
about to mutate. Restructured into a CAS retry loop:
  1. WATCH first
  2. GET (under WATCH — uses redis.get because Devvit's `txn.get` queues
     into the transaction and returns a chainable, not the value)
  3. modify in memory
  4. MULTI + SET + EXEC (null exec → another mod's call landed first
     between WATCH and now → loop and retry up to MAX_CAS_ATTEMPTS = 3).
After 3 contended attempts the moderator gets a clear "another mod
changed the rules — re-open and try again" toast.

### #3 Sequential audit-entry hGetAll in undo handler
routes/undo.ts: 100 sequential hGetAll calls could blow the menu
handler's deadline. Switched to `Promise.allSettled([...])` and scan
the resolved array in order so the "most recent first" break-on-found
behaviour is preserved.

## MEDIUM severity

### #4 Sequential audit-entry hGetAll in dashboard
routes/dashboard.ts: same parallelization for the 20-entry recent-actions
fetch and the per-draft-rule dry-run fetch.

### #5 Outdated comment on /internal/trigger/on-app-install
routes/triggers.ts: the comment claimed the submit triggers seed
in-band, but they actually fail-safe by returning ok when no bundle
exists. Updated to describe the real flow: starter rules are seeded
by the deferred /internal/scheduler/seed-on-install task, registered
in devvit.json's scheduler.tasks block.

## Verification
- `npm run check` 4/4 gates green: typecheck + lint + Prettier + 211 tests
  (1 skipped) + 3 @devvit/test cases + acceptance G1-G4
- All PR #45 tests still pass with no test-side modifications
ComBba pushed a commit that referenced this pull request May 14, 2026
5 review issues from Gemini code-assist + CodeRabbit on PR #49.

## Major

### CodeRabbit #4 — pending consumption now atomic (WATCH/MULTI/DEL/EXEC)
Two concurrent submits with the same `pendingId` (back-button + re-submit,
double-click on Save, multi-tab moderator) could both read the entry
before either deleted it, doubling the bundle write + dry-run schedule.
Replaced the plain GET-then-DEL with a WATCH/MULTI/DEL/EXEC round-trip.
Whichever caller commits first wins; the loser sees `exec() == null` and
gets an actionable "Another submission consumed this confirmation" toast.

Updated FakeTxn to match (added `del()` method on the txn object — the
production Devvit Redis client supports it, the fake didn't).

### CodeRabbit #5 — daily compile counter now bumps at compile time
Quota was previously incremented inside `persistRuleAndStartDryRun()`,
which only runs on Save. A moderator could repeatedly hit Compile +
Cancel and burn through OpenAI tokens without the per-day counter ever
moving. Moved the increment to immediately after a successful OpenAI
return in compose-rule-submit. The token cost is real either way, so
the quota now reflects it either way.

## Medium

### Gemini #1 — dashboard cancelLabel no longer says "Don't show intro again"
Devvit's Cancel button does NOT trigger the form submit handler, so
clicking the misnamed button could not actually persist
`dismissOnboarding` — it just closed the modal. Reverted the label to
"Cancel"; the dismissOnboarding boolean toggle + Close (acceptLabel) is
the real opt-out path that submits the form values.

## Minor

### CodeRabbit #2 — verify script tolerates non-int SLOW_MO
`int(os.environ.get("SLOW_MO", "0"))` crashed if a user passed e.g.
`SLOW_MO=fast`. Added `_safe_int` helper that warns + falls back to the
default rather than ending the recording session before it starts.

### CodeRabbit #3 — atomic SET + TTL via the `expiration` option
Previous shape was `redis.set(key, value)` then `redis.expire(key, ttl)`,
which could leak a TTL-less pending key if expire failed after set
succeeded. Switched to `redis.set(key, value, { expiration: new Date(...) })`
which is the same single round-trip the rollback writer in executor.ts
already uses.

## Verification
- `npm run check` 4/4 gates green (211 unit/integration tests)
- All compose tests still pass with the new pending-id flow
- Race condition smoke: a second compose-confirm-submit call against the
  same pendingId now returns the "another submission consumed" toast
  instead of double-persisting (previously: double bundle write).
ComBba pushed a commit that referenced this pull request May 15, 2026
…ng (Phase 1.7b Tier 1+2+3)

Best-practice UX rework for the 5 deferred audit findings (#2 #3 #5 #7 #10) plus
4 new items (#A empty states, #B delete confirm, #C onboarding, #D token cost).
Design + tier rationale: claudedocs/2026-05-14-ux-best-practices-plan.md.

## Tier 1 — compose flow (audit #2 #5 #7)

### #2 Compile preview confirmation form (composeConfirmForm)
Compile no longer persists straight to draft. After validation, the moderator
sees a confirm form with the rule rendered in plain English (humanizeRule()),
the original sentence, and the per-compile token cost (~$0.0006 on
gpt-5.4-mini). Tick "Edit instead of save" to re-open compose with the
original NL pre-filled. Save persists draft + schedules dry-run via the new
persistRuleAndStartDryRun() helper. Defence-in-depth: serialized rule is
re-validated before write.

New endpoint: /internal/form/compose-confirm-submit
New form: composeConfirmForm

### #5 Clarification turn limit (3 rounds)
Server-side counter shipped as a disabled paragraph field. After round 3, an
oscillating LLM gets refused with an actionable toast ("Try rephrasing more
concretely"), instead of opening yet another modal. Each clarify modal
description prefixed with "(Round X of 3)" so the moderator sees how many
tries remain.

### #7 Editable original rule in clarify modal
Removed `disabled: true` on the Original rule field. helpText now says
"Re-compile uses this text plus your answer below."

## Tier 2 — Manage rules menu + dashboard hardening (audit #3 #10 #A #B #D)

### #3 + #10 Per-rule control surface (Manage rules)
New "vibe-mod: Manage rules" menu (subreddit-level, mod-only). Renders one
form-group per rule with a `select` of available actions:
- Drafts: Keep / Activate (shadow 24h) / Activate immediately / Delete
- Active shadow rules: Keep / Promote shadow → live / Pause / Delete
- Active live rules: Keep / Pause / Delete
Submit applies all non-destructive actions atomically (one redis.set per
bundle) via the new applyManageActions() helper. Dashboard becomes
read-only — its old "Activate N drafts" boolean removed.

New endpoints: /internal/menu/manage-rules,
               /internal/form/manage-rules-submit,
               /internal/form/manage-delete-confirm
New forms: manageRulesForm, manageDeleteConfirmForm

### #B Delete confirmation
Any `delete` action in the Manage submit forwards to a confirm form listing
the rules about to disappear, with an explicit "I understand this is
permanent" boolean. The pending action map round-trips through a disabled
paragraph carrier so the second-step submit knows what to apply.

### #A Empty states
- Manage menu emits a guided toast when there are zero rules at all.
- Dashboard description shows "No rules yet — open ⋯ → Compose" when both
  rules count and recent actions are zero.

### #D Token cost transparency
- Dashboard description: "Tokens used (lifetime): N in / N out (~$X on
  gpt-5.4-mini)" via the new estimateTokenCost() helper.
- Compose confirm form shows per-compile cost so the moderator sees the
  unit economics before saving.

## Tier 3 — Onboarding (audit #C)

Dashboard shows a 3-step welcome card on first visit (per-sub Redis flag
`${sub}:onboarding:dismissed`). Cancel-label switches to "Don't show intro
again" and ticks dismissOnboarding to persist the flag. New key in
src/shared/redis-keys.ts.

## Verification
- `npm run check` 4/4 gates green
- 209 unit + integration tests (1 skipped):
  - 33 compose tests (8 updated for new 2-step flow + 4 new for #2/#5/#7)
  - 13 dashboard tests (4 updated for read-only + 4 new for onboarding/empty)
  - 17 manage tests (NEW file routes-manage.test.ts: menu render, all 5 actions, delete confirm round-trip)
  - existing trigger / scheduler / settings / undo / evaluator / executor unchanged

## Why a single PR
Tier 1+2+3 all touch overlapping files (index.ts, devvit.json, the dashboard form
shape). Splitting would force back-and-forth conflict resolution. Logical
commits inside this PR (split when applicable in follow-up).

Refs: claudedocs/2026-05-14-ux-best-practices-plan.md (Phase 1.7a),
      claudedocs/2026-05-14-compose-flow-audit.md (audit baseline),
      docs/demo-scenario.md (downstream dependency)
ComBba pushed a commit that referenced this pull request May 15, 2026
External code-review pointed out 5 valid issues. All addressed in this commit
on the same branch — PR #44 picks them up automatically.

## Gemini code-assist

### #1 — Manage menu: parallelize per-rule dry-run reads
The previous serial `for` loop multiplied Redis latency by the draft count.
With up to 50 drafts and ~10ms per round-trip the difference is ~500ms vs
~10ms wall time. Switched to `Promise.allSettled([...])` so a single failed
fetch doesn't stop the others.

### #2 — applyManageActions: enforce the 50-rule cap on apply
pause / activate / unpause moves can push a bundle over the 50-rule cap
even when no compose-rule call is made. Added an explicit check before
the dual-write txn — the moderator gets a "Rule cap exceeded (active=N,
draft=N, max=50). Delete a rule first" toast and the bundles stay
untouched. New test in routes-manage.test.ts.

### #3 — Use the configured openaiModel as the fallback (not the literal "manage")
The previous `'manage'` fallback for `bundle.llmModel` broke
`estimateTokenCost()` because 'manage' isn't a key in
`OPENAI_PRICING_USD_PER_TOKEN`. The dashboard shows "(~$0 on manage)" until
a real compile lands. Now we read the configured `openaiModel` setting
through `readOpenaiModel()` (already SELECTION-array-safe per PR #39).

### #4 — Atomic dual-write via WATCH/MULTI/EXEC (the previous PR description's claim was wrong)
The previous `Promise.all([redis.set(activeKey, ...), redis.set(draftKey, ...)])`
was NOT atomic — if the second `set` failed (e.g. plugin RPC blip), pause
could leave a rule absent from active without showing up in draft.
Switched to Devvit's `redis.watch(...).multi().set(...).set(...).exec()`
pattern (same shape executor.ts already uses for audit writes), and check
`exec()`'s null return for the WATCH-aborted case so two moderators
managing rules at the same time get an actionable "Another moderator
changed the rules at the same time — re-open Manage rules" toast.

## CodeRabbit

### #5 — humanizeRule: cap value-stringification at 100 chars
A rule that uses `op: in` against a long allowlist (e.g. 50 banned domains)
would otherwise render thousands of characters into the confirm form's
`compiledSummary` field, blowing past Devvit's modal description budget
and obscuring the actual rule structure. Truncate with `...` after 100
chars in the predicate-tree leaf renderer.

## Test infrastructure
test/devvit-testkit.ts: fakeRedis.watch().exec() now returns `[] | null`
(matching real Devvit Redis MULTI/EXEC semantics) instead of `void`.
Without this, the new WATCH-abort detection in applyManageActions would
fire on every test. Updated `FakeTxn.exec` type accordingly.

## Verification
- `npm run check` 4/4 gates green
- 210 tests pass (1 skipped) — added 1 cap-overflow test in routes-manage.test.ts
ComBba pushed a commit that referenced this pull request May 15, 2026
5 review issues from Gemini code-assist on PR #45 (refactor/split-server-routes).

## HIGH severity

### #1 Non-ASCII regex made unambiguous
helpers/openai.ts: the `replace(/[<U+0080>-<U+FFFF>]/g, ...)` literal contained
the U+0080 control character invisibly, which made review tools render it
as `/[-<box>]/` and flag it as "matches only `-`". Behaviour was always
correct, but the explicit `/[�-ï¿¿]/g` form removes any doubt.

### #2 WATCH ordering in applyManageActions
routes/manage.ts: the previous shape did GET → modify → WATCH → MULTI →
EXEC, so the optimistic lock didn't actually cover the data we were
about to mutate. Restructured into a CAS retry loop:
  1. WATCH first
  2. GET (under WATCH — uses redis.get because Devvit's `txn.get` queues
     into the transaction and returns a chainable, not the value)
  3. modify in memory
  4. MULTI + SET + EXEC (null exec → another mod's call landed first
     between WATCH and now → loop and retry up to MAX_CAS_ATTEMPTS = 3).
After 3 contended attempts the moderator gets a clear "another mod
changed the rules — re-open and try again" toast.

### #3 Sequential audit-entry hGetAll in undo handler
routes/undo.ts: 100 sequential hGetAll calls could blow the menu
handler's deadline. Switched to `Promise.allSettled([...])` and scan
the resolved array in order so the "most recent first" break-on-found
behaviour is preserved.

## MEDIUM severity

### #4 Sequential audit-entry hGetAll in dashboard
routes/dashboard.ts: same parallelization for the 20-entry recent-actions
fetch and the per-draft-rule dry-run fetch.

### #5 Outdated comment on /internal/trigger/on-app-install
routes/triggers.ts: the comment claimed the submit triggers seed
in-band, but they actually fail-safe by returning ok when no bundle
exists. Updated to describe the real flow: starter rules are seeded
by the deferred /internal/scheduler/seed-on-install task, registered
in devvit.json's scheduler.tasks block.

## Verification
- `npm run check` 4/4 gates green: typecheck + lint + Prettier + 211 tests
  (1 skipped) + 3 @devvit/test cases + acceptance G1-G4
- All PR #45 tests still pass with no test-side modifications
ComBba pushed a commit that referenced this pull request May 15, 2026
5 review issues from Gemini code-assist + CodeRabbit on PR #49.

## Major

### CodeRabbit #4 — pending consumption now atomic (WATCH/MULTI/DEL/EXEC)
Two concurrent submits with the same `pendingId` (back-button + re-submit,
double-click on Save, multi-tab moderator) could both read the entry
before either deleted it, doubling the bundle write + dry-run schedule.
Replaced the plain GET-then-DEL with a WATCH/MULTI/DEL/EXEC round-trip.
Whichever caller commits first wins; the loser sees `exec() == null` and
gets an actionable "Another submission consumed this confirmation" toast.

Updated FakeTxn to match (added `del()` method on the txn object — the
production Devvit Redis client supports it, the fake didn't).

### CodeRabbit #5 — daily compile counter now bumps at compile time
Quota was previously incremented inside `persistRuleAndStartDryRun()`,
which only runs on Save. A moderator could repeatedly hit Compile +
Cancel and burn through OpenAI tokens without the per-day counter ever
moving. Moved the increment to immediately after a successful OpenAI
return in compose-rule-submit. The token cost is real either way, so
the quota now reflects it either way.

## Medium

### Gemini #1 — dashboard cancelLabel no longer says "Don't show intro again"
Devvit's Cancel button does NOT trigger the form submit handler, so
clicking the misnamed button could not actually persist
`dismissOnboarding` — it just closed the modal. Reverted the label to
"Cancel"; the dismissOnboarding boolean toggle + Close (acceptLabel) is
the real opt-out path that submits the form values.

## Minor

### CodeRabbit #2 — verify script tolerates non-int SLOW_MO
`int(os.environ.get("SLOW_MO", "0"))` crashed if a user passed e.g.
`SLOW_MO=fast`. Added `_safe_int` helper that warns + falls back to the
default rather than ending the recording session before it starts.

### CodeRabbit #3 — atomic SET + TTL via the `expiration` option
Previous shape was `redis.set(key, value)` then `redis.expire(key, ttl)`,
which could leak a TTL-less pending key if expire failed after set
succeeded. Switched to `redis.set(key, value, { expiration: new Date(...) })`
which is the same single round-trip the rollback writer in executor.ts
already uses.

## Verification
- `npm run check` 4/4 gates green (211 unit/integration tests)
- All compose tests still pass with the new pending-id flow
- Race condition smoke: a second compose-confirm-submit call against the
  same pendingId now returns the "another submission consumed" toast
  instead of double-persisting (previously: double bundle write).
ComBba pushed a commit that referenced this pull request May 15, 2026
…versible rollback (P1)

Security-engineer agent strongly recommended GUARDED (not SAFE) classification:
- CRITICAL RULE #5 in system-prompt forces LLM to require explicit verbs for
  remove ('remove'/'delete'/'take down') but natural-language positive phrases
  ('trust regulars', 'let them through', 'whitelist') have a wider ambiguous
  path to approve emission.
- Asymmetric failure mode: a wrong approve waves spam through (Reddit ToS,
  reputation damage), while a wrong remove rolls back cleanly.
- shadow-mode does NOT mitigate approve risk (no visible harm in shadow ↔
  no mod warning signal).

Implementation:
- GUARDED_ACTIONS = [...existing, 'approve'] — mod must tick 'Allow
  ban/mute/approve' checkbox on compose for the rule to be storable.
- Discriminated-union arm: params { reason?: string<=200 } (optional —
  the Reddit API call itself takes no params).
- executor.applyAction reuses existing target.approve() path (already
  battle-tested in rollback flow for remove undo).
- rollbackAction explicitly refuses approve via the existing 'not reversible'
  branch — preserves audit trail but blocks one-click undo (mod must
  manually re-remove if the auto-approve was wrong).

UI sync:
- 'Allow this rule to ban/mute' label → 'ban/mute/approve' (3 sites)
- ALLOW_GUARDED_HELP copy expanded with approve rationale
- 'This rule would ban/mute users' toast → '...or auto-approve content'

Tests: +2 cases (live + shadow), whitelist snapshot updated.
217 passing (was 215).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant