Commit graph

23 commits

Author SHA1 Message Date
Marco Beretta
0c637aa10a
🧪 test: Spec the First-Sentence Diagnostic Comparison in the Suppression Runner (#17165)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Client build recovery regression (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/chat (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
* test: Spec the First-Sentence Diagnostic Comparison in the Suppression Runner

* test: Copy the Diagnostics Module into the Suppression Spec Synthetic Roots

* test: Copy the Diagnostic Comparison Spec into the Suppression Synthetic Roots
2026-10-16 22:03:54 +02:00
Marco Beretta
1e23b3545b
📝 refactor: Move Text-Like Message Parts Into @librechat/chat (#17087)
Some checks failed
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Client build recovery regression (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/chat (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Main Promotion Policy Tests / Main promotion denial and fast-forward tests (push) Has been cancelled
* refactor: Move text-like message parts into @librechat/chat

Text, EmptyText, CollapsibleText, Summary and AgentUpdate move into the package's components entry with their unit specs and read the app only through MessagePartsHost, which gains renderMarkdown, useSmoothStreaming, useAgent and renderAgentIcon. The app's markdown and agent-icon renderers register from the Parts barrel, so the host module keeps its import graph.

The design-suppression runner compares a moved file's diagnostics by their first sentence: the remedy that follows names app-only compositions for client paths, so the same violation read as new after a move.

* test: Cover text-like parts rendered from @librechat/chat in the browser
2026-10-14 18:05:48 +02:00
Marco Beretta
a30b11b4c7
🧩 refactor: Route Message Parts Through a @librechat/chat Part Dispatcher and Host (#16976) 2026-10-14 11:10:28 +02:00
Marco Beretta
e1b4e09d7c
🥟 ci: Type-Check Workspaces with bun check (#17009)
Some checks failed
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Client build recovery regression (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/chat (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Main Promotion Policy Tests / Main promotion denial and fast-forward tests (push) Has been cancelled
* ci: Type-Check Workspaces with bun check

Make every workspace tsconfig valid under TypeScript 7 semantics while
staying clean on the installed tsc 5.9: explicit types arrays (TS 7 no
longer auto-includes node_modules/@types), es2017 target for
data-provider, rootDir for api specs that import client contracts, and
noUncheckedSideEffectImports off for the CSS and matchMedia mock imports.

Fix the four client type errors newer DOM and Intl libs expose, and drop
the fetch.preconnect copy in useRum that only existed to satisfy the
leaked @types/bun globals.

Switch the client typecheck script and the CI type-check steps to
bun check (Bun 1.4.3); tsc --noEmit stays available as typecheck:tsc.

* chore: Fall Back to tsc When bun check Is Unavailable

Add scripts/typecheck.mts, which runs bun check when the installed Bun
provides it and the repository's tsc otherwise, so contributors without
Bun can still type-check. The client typecheck script and the
static-checks TypeScript gate both resolve their checker through it,
keeping local runs on the same checker CI uses whenever Bun is present.

* ci: Type-Check the Chat Package Through the Shared Checker

* ci: Run the Type Check When Only the Shared Checker Changes

A change to scripts/typecheck.mts alone started neither frontend review
nor the static-checks TypeScript gate, so a broken resolver could merge
unexercised. Add it to the frontend-review triggers and to every
typecheck project's paths.

The suppression-baseline scenarios copy static-checks.mts into synthetic
roots; copy its new typecheck.mts import alongside i18n.mts so the runner
loads there. Add scenarios for the checker choice and its fallback.
2026-10-12 06:40:07 -04:00
Marco Beretta
8e60a4185a
🧱 refactor: Judge Chat Package Imports on the Resolved Module (#16975)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Client build recovery regression (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/chat (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Main Promotion Policy Tests / Main promotion denial and fast-forward tests (push) Waiting to run
* refactor: Judge chat imports on the resolved target with one boundary rule

scripts/eslint/chat.mjs replaces the import patterns and load selectors: every
specifier is normalized (extension, trailing slash, /index, package name)
before it is judged, across static imports, re-exports, import(), require(),
import-equals and import types, so any spelling of the /react or /components
entry or a UI package is rejected from core. A RuleTester table and a
table-driven scenario cover the spellings.

* fix: Anchor the chat boundary root to the config directory

* fix: Keep bare app paths and bare core entries behind the chat boundary

* fix: Treat aliased react paths in core as the /react entry
2026-10-11 01:06:07 +02:00
Marco Beretta
c00c776e0e
📦 feat: Scaffold the @librechat/chat Package (#16964)
* feat: Scaffold the @librechat/chat package

Adds packages/chat with core, react and components entries, a tsdown build,
a node-environment jest config, turbo and root build wiring, import boundary
rules, design-lint scope, and core types for the chat transport, stream,
host and status. No code moves yet.

* test: Cover the @librechat/chat boundaries as scenarios

* fix: Run the chat suite in test:turbo and build it for the mock lane

test:turbo now selects @librechat/chat, the mock e2e job builds the package the
chat-package scenarios load, and the synthetic repository in the suppression
scenarios carries the packages/chat/src root lint:design:record names.

* chore: Build the chat package in frontend:ci too

* chore: Run the chat suite in test:all

* fix: Hold core TSX to the chat boundary and scan the package for cycles

* fix: Apply the chat import boundary to JavaScript sources

* fix: Keep nested core files off the components entry

* fix: Close the remaining chat package gates and fence aborts by generation

Blocks imports through the @librechat/frontend workspace name, adds the chat
sources to the i18n scan, the CSS color gate and the client Tailwind content,
and lets ChatAbortRequest carry the generation it means to stop.

* fix: Keep JSX out of the chat core and carry abort and reconnect outcomes

Rejects JSX under the core, copies the chat manifest before the Docker
install, keys the client app build caches on the chat sources, fences
reconnects by generation, and types the abort result the transport returns.

* test: Give the CSS gate's scratch tree the chat root it now scans

* fix: Close the remaining @librechat/chat gates and wiring

Rejects import() and require() across the package boundaries, holds chat
components to the literal-copy rule, compiles TSX in the package's jest
transform, builds the package in Frontend Unit Tests, requires its test job for
main promotion, adds it to the unused-dependency scan, and lists it wherever a
sibling workspace is listed: Dockerfile.multi, the docker smoke paths, the
remaining node_modules caches, the reinstall helper, the bun build chain and
AGENTS.md.

* fix: Tighten the chat boundaries and carry steering and Assistants aborts

Template-literal import() and require() and self-imports through the package
name are rejected, user-facing JSX attributes need localized text, the cycle
scan resolves self-imports, Dockerfile.multi builds the package for the client
stage, and the transport carries steer, cancelSteer, armSteer and the
Assistants abort key.

* feat: Match the chat transport and host contracts to the client's

The transport now reports settled and replaced starts, the stream's protocol
version and resumed flag, reconnect replay, generation status, paused-run
resume with approvals or answers, abort and steer protocol versions, and the
request error shape. The host config and auth carry what the client's
ChatSettings now supplies: rename-while-running, model specs, retention mode
and the user.

* fix: Keep lazy React-entry loads out of the core and type the Assistants stop

Relative import() and require() of the /react entry from core are rejected,
and an abortKey stop resolves to the terminal conversation and run messages the
Assistants route returns.

* fix: Fence start controls by protocol and close extension-bearing entry loads

Settled and replaced start results carry the protocol version, core loads of
the /react or /components entry with a file extension are rejected, and
test:packages:chat builds data-provider first.
2026-10-10 21:00:10 +02:00
Marco Beretta
adb04422bf
🚦 feat: Lint Disabled Dims That Compose No Disabled Recipe (#16907)
* feat: Lint Disabled Dims That Compose No Disabled Recipe

* fix: Catch Bracketed Disabled Variants and Read Recipes From References

* fix: Skip Enabled-State Branches and False-Valued Disabled States

* fix: Read Full-Opacity Recipes, Primitive Recipes and Compound Conditions

* fix: Keep the Suppression Error Wording the Baseline Spec Reads

* test: Add a lint scenario for the disabled-recipe rule

* fix: Leave negated disabled selectors and conditions alone in the disabled-recipe rule

* feat: Judge disabled recipes per branch, by import and by the element they restore

* fix: Trust recipes only from the theme module and only through defaulted cva variants

* fix: Resolve namespaced recipes through their import

* fix: Read cva disabled variants, guarded recipes and trailing important in the disabled-recipe rule

* fix: Read variant dims a disabled condition picks and recipes behind the same ternary

* fix: Follow nested disabled guards and real cva defaults in the disabled-recipe rule

* fix: Trust recipes only through class helpers and string concatenation

* fix: Read cva compound variants only under compoundVariants and guard key lookups

* fix: Require the recipe that can win for the faded element in the disabled-recipe rule

* fix: Judge stacked disabled variants by their most distant element

* fix: Match recipe markers, read || polarity, compound arrays, constant class strings and helper bindings

* fix: Accept spelled recipes, default-imported primitives, inverted props and disabled-guarded recipes

* fix: Check every chosen dim, trusted helper sources, data-disabled, recipe markers and guard identity

* chore: Re-record the disabled-recipe backlog after rebasing onto dev
2026-10-10 01:47:47 +02:00
Marco Beretta
6c8f659d54
🧮 fix: Read Math Function Channels in CSS Custom Property Triplets (#16924)
* fix: Read Math Function Channels in CSS Custom Property Triplets

* fix: Collapse Nested Numeric Math Before the Triplet Check

* fix: Read Exponents and Math Constants in Triplet Channels

* fix: Read round() Strategies in Triplet Math Channels
2026-10-09 18:39:30 +02:00
Marco Beretta
945a885394
🎚️ feat: Promote Stylesheet Colour Roles to Registry Colours (#16864) 2026-10-08 13:22:23 +02:00
Marco Beretta
3ca084a6cf
🎨 refactor: Route Stylesheet Colours Through Theme Variables (#16823)
* refactor: Route Stylesheet Colours Through Theme Variables and Gate CSS Literals

* fix: Run the Colour Check Through Its Own Script

* fix: Read Numeric Fallbacks Inside Colour Function var() in the Colour Literal Check
2026-10-07 16:02:41 +02:00
Lia
ad99250a35 fix: Verify Sidebar Status and Project Name Loading 2026-10-03 17:56:52 -04:00
Danny Avila
abab0d3c6d
🪟 fix: Read the Static Checks Workflow With CRLF Line Endings (#16669)
workflowSelectsWhatItFilters() split .github/workflows/static-checks.yml
with $-anchored multiline patterns, so on a Windows checkout with
core.autocrlf the trailing \r kept every pattern from matching and the
Design-rule suppressions gate failed every commit.

Parse through workflowFilters(), which normalizes line endings first, and
check in-script that the CRLF rendering reads the same lists, so CI on an
LF checkout catches a parser that only reads LF.
2026-10-02 09:26:17 -04:00
Marco Beretta
5961f767fd 🔤 fix: Ship the Theme Font Faces With the Package (#16485)
* fix: ship the Inter, Roboto Mono and Inconsolata faces with the package

Move the woff2 files from client/public/fonts into packages/client/src/theme/fonts
and every @font-face rule (the app's Inter and Roboto Mono faces and
client/src/fonts.css) into theme/fonts.css, which tokens.css imports and the build
copies into dist with the files. Each url() names the file by a new
@librechat/client/fonts/* export: a relative path breaks under Vite with
@tailwindcss/postcss, because Vite inlines the import before Tailwind runs and
Tailwind reports every emitted declaration as coming from the root stylesheet.

The app loads the same files through the same import and still emits them to
assets/fonts with their licences, now read from the package; the $fonts alias is
gone. Inter and Roboto Mono gain their OFL texts beside Inconsolata's. The README's
consumer stylesheet moves the package stylesheet import above @config, where
Vite's inliner no longer skips it. The static checks' bundle freshness test reads a
subpath pattern export as the directory it maps into.

* docs: describe how the theme fonts resolve through the package export

* fix: leave subpath pattern exports out of the bundle freshness check

A pattern such as ./dist/fonts/* names no entry the design rules load, and
reading it as its directory made the freshness check depend on a directory the
synthetic build in suppression-baseline.spec.ts never dates. The scenario's
mirror of the manifest reading skips patterns the same way.

(cherry picked from commit 681515c9e5)

Original-PR: https://github.com/LibreChat-AI/LibreChat/pull/16485
2026-10-01 03:46:18 +00:00
Marco Beretta
339a323ff1 🎨 chore: Enforce Design-System Rules With @shadcn/lint (#15981)
* 🎨 chore: Enforce Design-System Rules With @shadcn/lint

Registers @shadcn/lint for client/src and packages/client/src, resolving\n@librechat/client as the design system so a className that overrides what a\nprimitive owns is reported with the variants and sizes to use instead.\n\nFive rules run at error. no-unknown-classes stays off: it needs Tailwind v4.\nContracts record what callers legitimately own (text typography, skeleton\nsilhouette); eslint-suppressions.json records the 3,141 violations the tree\nalready carried, so the rules gate new code without a tree-wide migration.\n

* 🩹 fix: Close the Gaps Review Found in the Design-Rule Gate

Five findings against this pull request, and one that killed its CI:

- The lockfile entries this change added resolved to a private mirror, so every
  job died in `npm ci`. They now resolve from registry.npmjs.org, with each
  integrity hash checked against the tarball npmjs.org serves.
- The pre-commit hook ran ESLint twice and the first run had no
  `--pass-on-unpruned-suppressions`, so it exited 2 on the suppression the fix
  had just made unused and lint-staged never reached the command that tolerates
  it. One invocation now does both; ESLint applies suppressions after fixes, so
  it reports the post-fix state.
- `scripts/static-checks.mts` omitted that same flag, so `npm run static-checks`
  failed locally on a diff the Static Checks lane passes.
- `lint:design:suppress` recorded a moved file's new path and left the old one
  behind, because only `--prune-suppressions` removes it and ESLint refuses both
  flags in one run. The script now chains the prune, and CLAUDE.md documents the
  scoped recipe that does not re-baseline the whole surface.
- The design rules matched only `.ts`/`.tsx`, so the client's `.jsx` entry points
  bypassed all five. Both globs now cover `.js`/`.jsx`; the one violation that
  exposed — an inline `display: none` on App.jsx's silent-audio iframe — is fixed
  at the source rather than recorded.
- A suppressions-only diff started no workflow and nothing read the baseline. It
  is now a trigger path and its own filter group, and a new `suppressions` check
  rejects a shape ESLint cannot load, a count no run can reach, a rule the plugin
  does not define, and a path that no longer exists.

The count-neutral swap two reviewers raised is not closed: ESLint's baseline
format records a count, not violation identities, so replacing one suppressed
violation with another of the same rule in the same file is invisible here.
CLAUDE.md now says that instead of implying detection.

Four `@scenario`-tagged specs cover the developer-facing behaviour: the report a
caller gets on a primitive override, a `.jsx` file being checked, the fixing
commit staying green (with the counter-proof that it exits 2 without the flag),
the move recipe dropping the old path, and every locked tarball being fetchable
from the public registry.

* 🧪 fix: Close the Dormant-Entry and Gate-Trigger Gaps

The suppressions gate did not run when the file that implements it changed, a
count recorded against a path no design-rule lint reaches passed validation, the
documented re-record chain could not run under cmd.exe, and the lockfile
scenario failed a legitimate SheetJS upgrade. The trigger scenarios also ran
four full-tree sweeps inside one test that declared a 180s deadline for work
measured at 100-280s per sweep.

- scripts/static-checks.mts: `scripts/static-checks.mts` joins the suppressions
  filter and the whole-record trigger, and a recorded path ESLint never reports
  on is rejected as dormant.
- package.json/CLAUDE.md: `|| node -e ""` in place of `|| true`, which cmd.exe
  does not define.
- e2e: the whole-record triggers are a matrix over a miniature repository (2-6s
  each instead of 100-280s), the oversized scenario is split into five
  behaviour-shaped ones, scratch baselines live in os.tmpdir(), and the lint
  gate runs in one project because it has no viewport.

* 📏 chore: Re-Record the Counts Dev Moved Under the Backlog

Rebasing onto dev brought #16067, which restyled three files the recorded
backlog speaks for: MarketplaceSidebar.tsx gained one arbitrary value and two
`no-restyle` violations, ToolsMarketplaceDialog.tsx gained one, and SkillsDialog
lost one. Counts above the file fail the lint for whoever edits it next; the one
below leaves capacity a later violation would spend. Scoped re-record and prune
of those three paths, per the recipe in CLAUDE.md.

* 🧭 test: Run the Copied Runner From the Tree It Is Testing

The trigger matrix pointed the copied static-checks runner at a miniature
repository but invoked it from the real one. The runner derives its repository
from its own location and relativizes file arguments against the working
directory, so `eslint.config.mjs` resolved to a path outside the miniature tree,
the suppressions group read as "not affected by this diff", and the scenario
passed a check that never ran (verify recorded it as a failure: exit 0 where a
rejection was expected). `run` now takes a `cwd`, and both synthetic-root probes
use it.

* 🔇 fix: Reject a Disable Comment With Nothing Behind It Yet

Comparing the two ESLint runs finds a directive only while it is silencing
something. A `/* eslint-disable shadcn/no-raw-colors */` over a file with no raw
colour left both runs agreeing, so it landed unseen and then silenced the first
violation anyone added to that file. The configured run now also asks for unused
directives, which is how ESLint names a disable with nothing behind it: a
dormant design-rule disable and a dormant blanket disable are rejected, a
dormant directive for another rule is not. Neither root carries one today (0
reports across 2,241 files), so what this rejects is new ones.

The same pass also has to see the primitives' variants, so `validateSuppressions`
builds `packages/client/dist` before the directive check rather than inside the
capacity scan that follows it; without the metadata both runs report strictly
fewer design diagnostics and a comment silencing a `no-restyle` violation is
invisible to the comparison.

* 🧱 fix: Count the Library's Build Inputs as Sources

`designMetadataIsFresh` compared `packages/client/dist` with `packages/client/src`
alone, so a commit that changed only the library's manifest or its tsdown config
left every source older than the build and the runner skipped the rebuild. The
design rules then classified against output emitted under the old contract —
locally only, since CI always builds before validating, which is the shape of
divergence nobody sees until the lane disagrees with the commit that passed.

Both files already count as design metadata everywhere else (the suppressions
filter, the whole-record trigger); they now count in the freshness comparison
too. The scenario runs the miniature repository with an observable
`build:client-package`: a current build is not rebuilt, and touching either
input asks for one.

* ⏱️ test: Set the Times the Freshness Rule Compares

The rebuild scenario wrote the build output and then the manifest, and node
wrote both inside the same millisecond, so the manifest was not newer than the
build and the runner correctly declined to rebuild — the assertion failed on the
filesystem clock rather than on the rule. The scenario now sets each mtime it
depends on with `utimesSync`, which is what it meant to say.

* 🩹 fix: Restore the Write the Import Edit Dropped

Adding \ to the import list replaced \ instead of joining it, and nothing local caught it: no tsconfig covers e2e/, and the flat config leaves \ to TypeScript there. The mock harness did — every scenario in the file failed with ReferenceError — which is the point of running it. Typechecked the four scenario files directly this time.

* 🚫 fix: Reject Every Inline Escape, Not Only the Ones in Use

Codex found three more ways past the gate, one shape each. A blanket
\ busy silencing an unrelated rule is used, so ESLint's
unused-directive report never names it, and the configured-versus-ignored
comparison sees no design delta — it waits for the file's first design
violation and takes it. Inline \ over a
clean file is invisible the same way. And a nested baseline that drops an entry
for an unchanged source was checked against its remaining keys only, so the
violation it used to speak for went unread.

The directive check now has two halves: ESLint still answers what a comment is
silencing today, and the comments themselves are read out of the parser — not
the text, so a string that reads like a directive is not one — for a disable
naming a design rule, a blanket disable, or inline configuration naming a
design rule, whether or not anything is behind it yet. That supersedes
\, which could only see the unused ones.
Neither design root carries any of the three today.

\ unions the entries a diff removes into a nested baseline's
file list, and \ joins the design metadata: it
carries the \ mapping, the entry emit and the include/exclude that decide
what reaches the bundle the rules read, which is why the repository's own build
caches key on \.

* 🔐 fix: Make the Hook Select What the Lane Selects, and Distrust a Half-Built Bundle

Two more shapes of the same mistake — trusting a proxy for the thing itself.

The pre-commit group listed individual files while the Static Checks lane's
filter also names both design roots, so staging a primitive ran ESLint locally
and the whole-record sweep only in CI: the commit passed and the lane failed on
callers nobody staged. The hook's group is now the lane's filter pattern for
pattern, and the count-edit scenario reads both files and fails when the two
lists differ, so the next path added to one has to reach the other.

Freshness took the newest timestamp under \ as proof of a
finished build. The build empties that directory before it writes, so a build
that failed halfway leaves a recent directory over a bundle with no entry
point, and every later run skipped rebuilding it. The check now also requires
what \ promises inside \ — its entry fields
and every string leaf of \ — to exist.

* 🧾 fix: Give the CDN Exception to xlsx, Not to the Host

The lockfile rule allowed \ for anything and then compared
versions against the base branch, so regenerating the lockfile against that CDN
could move any package off the public registry at the same version and pass.
The exception now belongs to the package — \, at any version, nested or
not — and every other entry has to come from the registry, which drops the
version comparison and the shallow-clone caveat with it.

The rule is a function the scenario applies to the repository's lockfile and to
lockfiles written for the purpose: an xlsx upgrade on its CDN passes, lodash
rewritten onto the same CDN at the same version fails, a new package from a
private mirror fails, and a nested xlsx is still xlsx.

* ↩️ fix: One Local Entry Point, Asked of the Runner

The pre-commit group I added for the backlog and the design metadata was
redundant and slower: \ already ends by running the runner
over the staged diff with \, and that run
selects its groups from the same filters — including both design roots, and
including deletions, which lint-staged never hands to a task. The group made
the whole-record sweep run twice for one commit. It is gone.

The scenario now asserts the property instead of a copy of the list: the hook
still runs the runner over the staged diff, and for every path the lane's
suppressions paths-filter names, the runner reports that check as selected
(\), while a path outside both roots is not affected.

* 🗺️ fix: Assert Where the Rules Are Asked, Not Only What They Found

A report cannot say where the rules were asked. Narrowing the design block over
a subtree that happens to be clean today — \, say —
produces no new diagnostic, no changed count, and no baseline entry to inspect,
so every gate stays green while the styling written there next goes unread.

The new scenario asks the resolved flat configuration instead: for one
representative path of each source extension in every directory under both
roots, all five design rules must be at error in the app and at the library's
own profile inside \ (overriding a primitive is off there;
colour and inline geometry still hold), and server code must carry none of
them. 1,184 paths, answered through ESLint's API in one process in under a
second — \ would have been eleven minutes of subprocesses.

* 🎯 fix: Spend the Budget Once — Reject a Swap Inside the Allowance

The recorded count is a budget per file and rule, and until now it could be
respent: removing one raw colour while adding a different one keeps the total
equal, so ESLint suppresses the new violation inside the old allowance and the
count checks see nothing move. The backlog was documented as unable to tell
that apart, and it was the one hole Codex kept finding its way back to.

What the totals cannot distinguish the diagnostics can. For every design source
a diff touches, the runner now lints that file's version at the base — through
ESLint's API, one process for the whole diff — and reports a design-rule
message the head has that the base did not, naming it. A violation that only
moved keeps its message and is not reported; a file with no version at the base
has no allowance to hide in and the count checks already answer for it.

Measured on the checkout: swapping one `fill="#AFC1FF"` for `fill="#BEEF11"`
in a file recording five raw colours is rejected in 4.9s naming the new one,
and the unchanged file passes. The scenario proves all three states — recorded,
moved, swapped — in a miniature repository with a history of its own, and
CLAUDE.md no longer claims the swap is undetectable.

* 🧰 fix: The Root Manifests Are Build Inputs Too

\ is defined in the root manifest and runs with the
toolchain the root lockfile pins, and both already select the suppressions
check — but neither counted as an input to bundle freshness, so a commit that
changed the build definition could validate locally against output made by the
previous one while CI rebuilt. They count now, beside the library's own
manifest, build config and compiler options, and the rebuild scenario drives
the lockfile case through the miniature repository's observable build.

* 🧭 fix: Read the Base From the Merge Base, and Give a New File No Allowance

Two holes in the swap check, both about what "the base" means.

It read each changed file's prior version from the base branch's tip, while the
changed-file set comes from \. With \ moving under a branch,
that calls a violation dev added since the branch diverged this change's doing,
and forgives one this change added that dev happens to carry. It resolves the
merge base once and reads from there.

And a file with no version at the base was skipped, so adding a source with a
violation and recording it in the same commit passed as "already recorded".
A file the change adds has no allowance to inherit: every design violation in
it is new. Renames are followed (\), so the
documented re-record of a moved path stays green — its violations came with the
file.

The scenario now walks recorded, moved, swapped, added-with-its-own-entry and
renamed through one miniature repository with a history of its own.

* 🔗 fix: One Base Commit, Asked Once

\ resolved the merge base; \ still read
the baseline from the base branch's tip, so the two checks in the same run
disagreed about what "before" meant — and an entry the base branch edited after
the branch diverged entered \ as though this change had edited it.

\ resolves it once and both ask it.

* 🔤 fix: A Suppressed Message Carries Its Text

\ omitted \, and the swap check reads it
— the union of messages and suppressed messages is what a file's diagnostics
are, and what the baseline silences is exactly what that check has to see.
Nothing local caught it: node strips types without checking them and no
tsconfig covers scripts/, so the runner is now typechecked directly
(                                                                               
                This is not the tsc command you are looking for                
                                                                               

To get access to the TypeScript compiler, tsc, from the command line either:

- Use npm install typescript to first add TypeScript to your project before using npx
- Use yarn to avoid accidentally running code from un-installed packages, clean).

* ⚖️ fix: Let the Record Grow, Out Loud

The swap check reported every violation a change added, which also forbade the
one path CLAUDE.md sanctions: add the violation and raise the count, so the
debt arrives as a line in the diff a reviewer reads. What separates that from a
swap is not the violation, it is whether the record said so — a swap keeps the
count still.

The check now charges each new diagnostic against how much that entry's count
grew between the merge base and here, and reports only what the growth does not
cover. Measured: a swap at an unchanged count fails, the same violation with
the count raised passes, and raising nothing fails naming the class. CLAUDE.md
says which of the three is which.

* 🚧 fix: A File the Change Adds Brings No Allowance

Letting the record grow to cover a new violation is right for a file that
already existed — the raised count is the line a reviewer reads — but it also
let a brand-new source arrive with its own entry, which is the backlog
recording what the tree never owed. Growth applies only where there is
something to grow: a file present at the merge base, renamed or not. A file the
change adds owes everything in it, and the message says so.

* ✏️ test: Assert the Wording the Added-File Rule Uses

The scenario still expected "is new here" for a file the change adds, which now
gets its own message — the backlog records what the tree owed, not what a new
file brings. The harness caught it; the assertion follows the rule.

* 🧮 fix: Ask Git What Existed, and Where Its Entry Lived

Three faults in the new swap check, one cause: it inferred from content what
only git can answer.

A tracked empty file at the base reads as absent, so an existing file was
treated as newly added and its recorded growth refused. A renamed file's
growth was measured at the new key, where the base has no entry at all, so the
move itself looked like room to spend. And a base commit missing from the clone
made every changed file look new, silently.

Existence is now \ succeeding, the allowance is read under the name
the entry had (the rename map already resolved the source; it resolves the
entry too), and a base commit that is not in the clone is reported rather than
guessed at. Measured: a rename carrying a new violation with its count raised
at the new key fails, a plain rename passes, a swap at an equal count fails.

* 🗃️ fix: A Deleted Baseline Is a Deleted Baseline, Nested or Not

The root baseline's deletion failed the check; a nested one was quietly dropped
from the target list by the same \ filter that was meant to skip
paths never present. What a nested baseline silences goes back to being
reported — or stops being — and nothing else reads it, so its deletion is the
same failure and now says which file is missing.

* 📚 fix: A Baseline in the Diff Is Read in Full

The comment said a baseline named in the diff is checked in full; the code
checked only the entries the diff moved, because \ made
\ non-empty and the full-record branch required it to be empty. So a
count edit never revalidated the rest of the record — dormant paths and stale
counts in unedited entries went unread. Editing the baseline now reads every
path it records.

And the rename map keyed on git's raw line, so a CRLF checkout left a \ on
the new path and the rename never matched — the file read as added and its
inherited allowance was refused. The lines are trimmed.

* 🧷 fix: Lint the Base Under the Name It Had, and Compare a Commit With Its Parent

Two last seams in the swap check.

A renamed file's base contents were linted under the new path, so moving a
component out of \ — where overriding a primitive is the
library's own business — into \ made the destination's stricter
rules apply to both sides: the diagnostics the move itself introduces looked
inherited, and a matching entry would have hidden them. The base is linted as
the path it had.

And \, the documented single-commit mode, resolved its
comparison to \ — the commit against itself, which forgives everything.
It resolves to the commit's parent.

* 🔖 fix: Quotes Do Not Hide an Inline Rule Configuration

ESLint reads \ exactly as it reads
the unquoted key, but the comment scan required the colon to follow the rule id
directly, so the quoted form passed over a file that does not violate the rule
yet — and then silences the first violation a primitive or config fan-out
creates there. Over a violating file the two-run comparison always caught it;
the dormant case is the one that slipped. The scan accepts either quoting, and
the scenario carries the quoted spelling beside the rest.

* 🧪 perf: A Spec Is Not Component Metadata

Both roots the design rules police are full of specs — every primitive in
`packages/client/src/components` has one — and editing any of them ran the
two-root suppression sweep, because the metadata predicate asked only whether
the path was a lintable source under a metadata root. A spec is neither: the
flat config turns all five design rules off inside it and the library's
tsconfig keeps it out of the bundle the caller rules resolve primitives
through, so it carries no `cva` variant and can report no design diagnostic of
its own.

`packages/client/src/components/Button.spec.tsx` through the runner:
59.7s before, 0.0s after.

The same `DESIGN_TEST_SOURCE` test is applied everywhere the invariant holds:
the freshness input no longer treats a spec as something `dist` is built from
(directory mtimes stay, so an added or deleted source still forces a rebuild),
the directive gate no longer fails a commit over a comment in a file where the
design rules are already off, and the base-vs-head lint no longer fetches a
spec's base version to ask a question with one possible answer.

* 🔎 fix: A Build That Reports Success Is Not a Build

Two gates trusted a signal instead of the condition it stands for.

`buildClientPackage` returned on the build's exit code. A build that exits 0
while leaving `packages/client/dist` incomplete or older than its sources left
`designMetadataIsFresh()`'s answer exactly as it found it, and the suppression
lint that follows then classified callers against a bundle describing something
else — reporting fewer violations than CI, which is the failure the rebuild
exists to prevent, reached through the branch meant to fix it. The freshness
question is now asked again after a successful build, and a bundle still stale
is an error naming `packages/client/dist`.

The workflow's `on.pull_request.paths` named only the root
`eslint-suppressions.json` while the inner `suppressions` filter names
`**/eslint-suppressions.json`. A diff touching only a nested baseline outside
`api|client|config|packages|scripts` — `e2e/fixtures`, say — started no
workflow at all, so `validateSuppressions()` never ran over exactly the nested
baselines it advertises. The trigger carries both spellings now.

The miniature repository's `build:client-package` writes the bundle its
manifest promises instead of only a marker, so the rebuild rows assert a
finished build rather than a claim of one, and a build that writes nothing is
its own row.

* 🧭 fix: Ask the Tool, Don't Reconstruct Its Answer

Four findings on the last head, and three of them were the next variant of a
family this branch had already patched once. The shared cause is one habit:
the gate re-derived a fact from a cheaper proxy instead of asking whatever owns
it, and every round found another case the proxy got wrong.

An inline `eslint` configuration comment was matched by the spelling of its
rule key, so `shadcn/x`, `"shadcn/x"`, `"shadcn\/x"` and `"shadcn\u002fx"` —
one key, four spellings, all of which ESLint honours — needed four regexes.
The key is now decoded before it is compared, which ends the family rather
than adding the fourth.

Bundle freshness reduced a set of outputs to the newest file anywhere under
`dist`. A resolver loads one entry, not a directory, so a build that rewrote
the ESM bundle and left the CJS one or the type declarations behind passed
while `@shadcn/lint` read the stale one. Every declared output is compared
now, which is the same reason their existence was already checked one by one.

Target discovery read `git diff --name-only` with rename detection, which
names one side. `git mv eslint-suppressions.json backlog.json` therefore
selected no group that owns the source, and the baseline's disappearance was
reported by nothing. `--no-renames` at the single funnel makes a rename two
paths; the one caller that wants the pairing asks for it separately.

The fourth was the parity assertion added last round, sitting in the mock
Playwright lane — whose own trigger excludes `.github/workflows` apart from its
own file, so a workflow-only diff never ran the assertion guarding it. The
claim moved into the runner, where the file that can break it already
activates the group, and the scenario now exercises the runner both ways over
a copy of the real workflow instead of reimplementing its parser.

* 📝 docs: Say Which Swaps the Ratchet Misses

The ratchet paragraph claimed a swap arrives as a failure, full stop. Two do
not, and a reader deciding whether to trust the gate needs both.

A swap whose diagnostic message equals the one it replaced is invisible,
because the comparison keys on the message and some rules name the property
without the value — `Inline style sets display.` is one message for `none` and
for `flex`. And a swap the diff itself causes, by changing a primitive or the
config, is invisible because both versions are linted under the head's design
inputs.

Tracked at berry-13/LibreChat#70 and berry-13/LibreChat#69.

* 📏 chore: Re-Record the Count Dev Moved Under the Backlog

`dev` fixed one `shadcn/no-restyle` violation in
`client/src/components/SharePoint/SharePointPickerDialog.tsx`, so the rebased
branch records 8 where the file now has 7. An entry with slack is exactly what
this change added a check for: the unused 1 would silence the next violation
anyone writes there, and the gate says so rather than letting it through.

`npm run lint:design:prune` drops the one count; the trailing newline it strips
is restored by prettier, the way the pre-commit hook's `*.json` group does.

* 📝 docs: Drop a Recorded Count That Dev Keeps Moving

The backlog total moves with every rebase that picks up new violations from
`dev`, so a number written into CLAUDE.md is stale by the next re-record — it
read 3,230 against a baseline holding 3,232. The recipe does not need the
figure; `eslint-suppressions.json` is the record.

* 📏 chore: Re-Record the Counts Canary Moved Under the Backlog

Retargeting onto canary changes the tree the design rules read, so five
files carry different violation counts than the recorded backlog claims:
Wakeup and SubagentConversation shed no-restyle violations and picked up
a require-static-classes one, and Ledger, UnifiedSidebar and ChatRoute
each gained one. Re-recorded those entries and pruned; nothing else in
the baseline moved.

* 🧱 fix: Build the Primitives Before Recording What They Own

`lint:design:record` was the one exposed design script that linted without
building `packages/client/dist` first. The rules read each primitive's `cva`
variants out of that bundle, so a fresh checkout or a primitive change left
the recording run classifying against a library that does not exist yet and
writing counts lower than the ones CI reports.

Moved the build onto the record script, where the other three already have
it, and dropped it from the `lint:design:suppress` wrapper so the wrapper
still pays for exactly two builds. A build failure inside the wrapper is
still loud: the record step cannot rewrite the baseline when the build it is
chained to fails, and the prune that follows does not swallow its own.

* 📏 chore: Re-Record the Counts Canary Moved Under the Backlog

Rebasing onto canary picks up PR #15798, which rewrote the agent marketplace
components, so eighteen recorded entries no longer describe the files they
name: AgentDetail is gone, AgentCard traded its restyle and arbitrary-value
violations for inline styles, AgentDetailContent grew to eighteen restyles,
and CopyLink, GridSkeleton, Marketplace, MineFilterToggle, SortDropdown and
LoadingDots arrived with violations nothing had recorded.

Re-recorded and pruned; every entry that moved belongs to a file that commit
touched, and nothing else in the baseline changed.

* 🧪 test: Assert the Record Asks for a Build Before It Writes

The build-freshness scenario covers the runner. Nothing covered the other
entry point: `npm run lint:design:record`, which writes the budget every
later change is measured against and, until now, linted whatever bundle
happened to be on disk.

The miniature repository's `build:client-package` appends to a log, so the
scenario can ask whether the record built before it wrote, and whether a
build that fails still stops the wrapper now that the wrapper no longer
carries a build of its own.

---------

Co-authored-by: berry-13 <berry-13@users.noreply.github.com>
(cherry picked from commit 8d5e1a75ee)

Original-PR: https://github.com/LibreChat-AI/LibreChat/pull/15981
2026-10-01 03:46:16 +00:00
lia-by-librechat[bot]
76dbb931a1
🧤 docs: Define Safe Backend Error Contracts in AGENTS.md (#16443)
Co-authored-by: Lia <lia@librechat.ai>
2026-09-28 08:25:26 -04:00
Danny Avila
d369c649ed
🪥 chore: Run CI's Static Checks on Each Commit's Diff (#15303)
* 🪝 chore: Run Static Checks on Every Commit

Adds `scripts/static-checks.mts`, a local port of the Static Checks CI job
(.github/workflows/static-checks.yml) scoped to the files in a diff. It
resolves the changed-file list, applies the same `dorny/paths-filter` groups
the job uses, and runs whichever checks those paths activate — ESLint,
Prettier, import order, ESLint config validation, package.json validation, and
(behind `--full`) config migration tests, unused i18n keys and depcheck. Like
the job, every selected check runs even after one fails and the failures are
summarized at the end.

The pre-commit hook keeps lint-staged for the per-file layer, which verifies
the exact staged content of partially staged files, then runs the script for
everything lint-staged cannot cover. lint-staged now uses the job's ESLint
invocation, so warnings fail locally the way they fail CI.

The slow gates stay opt-in (`npm run static-checks:full`, or
`STATIC_CHECKS_FULL=1`) to keep commit latency unchanged.

Hooks were never installed: `config/prepare.js` existed but no `prepare` script
called it, so the hook only ran where `core.hooksPath` had been set by hand.
Replaces it with an inline `prepare` — both Dockerfiles run `npm ci` after
copying only the manifests, so a `node config/prepare.js` step would fail the
image build, and husky is absent from `--omit=dev` installs.

The i18n scan is a single pass over the source identifiers rather than one grep
per key, verified to flag exactly the same keys as the CI loop (including the
substring and dynamic-key cases) in 0.5s instead of 14s.

* 🩹 fix: Address Codex Round 1 on the Static Checks Runner

Activate gates from the unfiltered changed-path list. `dorny/paths-filter`
matches deleted paths too, so gating on the `--diff-filter=ACMRTUXB` list the
per-file steps use let a delete-only commit — the last reference to a
translation key, say — slip past the i18n and depcheck gates. The two lists are
now derived separately, the way CI derives them.

Pass `-m` to `git diff-tree` in `--commit` mode. Without it a merge commit
emits no paths at all, so `--commit <merge-sha>` reported "Nothing to check";
a real merge in this repo's history goes from 0 to 97 files.

Build the workspaces the config suite imports instead of skipping when `dist`
is absent. The three `dist` directories are gitignored and `npm ci` does not
produce them, so a fresh checkout reported a pass for a gate that never ran —
and an existing `dist` could be stale. Each is a sub-second tsdown build.

Resolve a global depcheck through a shell on Windows, where npm exposes it as
`depcheck.cmd` and `spawnSync` cannot see the shim.

Skip dot directories when walking for imports. `.claude/worktrees/` can hold a
full checkout per branch — 117 on this machine — and the root-wide scan behind
the depcheck gate walked every one of them.

Records the remaining boundary in the header: the per-file checks see exact
staged content via lint-staged, while the tree-wide gates read the working
tree, as running them by hand would.

* 🩹 fix: Address Codex Round 2 on the Static Checks Runner

Treat an unresolvable checker as a failure. ESLint and Prettier missing meant
the runner printed "All affected static checks passed" without having linted
anything; only depcheck, which CI installs globally and this documents as
optional, may still skip.

Catch per-check exceptions. The runner promises that every selected check runs
even after a failure, but a throw — a malformed translation JSON, say —
escaped and cancelled the checks after it. Each is now recorded as that
check's failure; verified that depcheck still runs after i18n throws.

Restrict `--commit` to the checked-out commit. Paths came from the named
commit while contents came from the working tree, so an older revision was
scored against the wrong file contents: a file added then deleted vanished,
and one modified since was read at its newer contents. It now fails with a
pointer to `--against`.

Reject unknown options. `--ful` silently ran the fast tier and `--commmit HEAD`
treated `HEAD` as a file path, both exiting 0 and implying gates had run.

Cover the runner in CI. `scripts/**` was absent from the workflow's trigger
paths, so a PR touching only the script the pre-commit hook now depends on got
no Static Checks run — and ESLint has no flat-config match for
`scripts/**/*.mts`, so nothing else loads it either. Adds the trigger path, a
`runner` filter group and a step that runs the script against the PR's own diff.

* 🔗 feat: Add Circular Dependency and TypeScript Gates

Both already run in CI as jobs of the Backend Unit Tests workflow; this brings
them to the local runner so they land before a push rather than after.

Circular dependencies (`node config/circular-deps.mjs`) is fast enough at 0.9s
to sit in the per-commit tier, gated on the same paths that trigger the CI job.

TypeScript stays opt-in behind `--full`: the five projects cost between 2.3s
and 20.9s each, which is too much per commit. Each project declares the paths
that can affect it — its own sources plus its upstream packages — so an edit to
data-provider still typechecks data-schemas, api, packages/client and client,
while an `api/**`-only change runs none of them, since no typechecked project
includes that directory. The builds a project's imports resolve through are
made first, mirroring the CI jobs' dependency on the build artifacts, through a
helper the config suite now shares.

Also addresses codex round 3:

Reject conflicting target selectors. `--against origin/dev package.json`
silently checked only the file, and `--against <bad-ref> --commit HEAD` never
resolved the bad base, so a caller could believe a range had been checked.

Require a clean worktree in `--commit` mode. The HEAD-only restriction was not
enough: contents still come from the working tree, so an uncommitted edit was
scored against the named commit — an invalid uncommitted package.json failing a
valid HEAD, or an uncommitted fix masking a defect in it.

The summary now names how many checks were skipped rather than reporting a
bare pass, and a typecheck failure carries the stale-workspace-build hint —
inside a git worktree `librechat-data-provider` resolves to the main checkout,
whose dist can predate the branch and shows up as missing properties.

* 🩹 fix: Address Codex Round 4 on the Static Checks Runner

Diff `--against` from the merge base. A two-dot diff reports the base branch's
own commits in reverse once it advances, so `--against origin/dev` scored 64
files for a branch that changed 7, activating gates for files the branch never
touched. Three dots makes the documented PR-style command mean what it says.

Typecheck on root manifest changes. Both review workflows trigger their
TypeScript jobs on package.json and package-lock.json, because a dependency or
@types bump breaks compilation on its own; the local filter ignored them, so
`static-checks:full` passed where CI would fail.

Validate every workspace manifest. The list mirrored the four the CI step
happens to name, so a malformed packages/api, data-provider or data-schemas
manifest passed validation in the revision modes, which have no lint-staged
pass behind them. Both lists now cover all seven.

Make the runner smoke execute a check. `--list` never runs one, and a
script-only PR activates no group, so the CI coverage added for exactly that
case could pass with the execution path untouched. It now runs against an
explicit target.

* 🩹 fix: Address Codex Round 5 on the Static Checks Runner

Activate the JSON gate for every manifest it validates. Round 4 added the four
workspace manifests to the validation list but not to the filter that turns the
gate on, so a malformed packages/data-provider or data-schemas manifest still
passed when it was the only changed file — the list grew and the trigger did
not. The same two entries also feed the unused-package calculation, reached
through api/package.json's @librechat/data-schemas dependency.

Include the owning workflows in the imported gates' filters. Circular
dependencies and TypeScript come from the review workflows, both of which list
their own YAML in `on.paths` and therefore rerun those jobs when the workflow
changes; locally the gates stayed inactive, so a change to how they are built
or invoked could bypass the local equivalent. Added to the group filters and to
the per-project predicates, since a workflow-only change would otherwise
activate the group and then select no project.

Bound command batches by characters rather than file count. Windows caps a
command line at 32767 characters, far below POSIX ARG_MAX, and a count does not
bound that: 400 of this repository's longer paths already come to 30176
characters before the executable and fixed arguments. Verified that a list
spanning several batches still reports a defect in its final file.
2026-08-28 10:08:37 -04:00
Danny Avila
c77e6a5ad2
🌉 fix: Preserve Missing Locize Translations (#14917) 2026-08-17 02:20:25 -04:00
Danny Avila
5ca667c258
🛡️ fix: Validate Translation Contracts Against English (#14914) 2026-08-16 22:55:05 -04:00
Danny Avila
6755544cee
🌍 ci: Harden Locize Translation Sync (#14784) 2026-08-13 07:29:29 -04:00
Danny Avila
b807292997
📉 perf: Bound Early Event Buffering for Detached Generations (#14612)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
* 📉 perf: Bound Early Event Buffering for Detached Generations

A generation streaming with no attached subscriber re-entered buffering
mode on every disconnect and retained each emitted event in
earlyEventBuffer for its remaining duration. A single 26-minute detached
run (~58,800 tool-argument deltas) grew the heap past 2 GiB with GC cost
climbing alongside it, while client reconnects always resume from
durable state and discard that local buffer anyway.

- Close the early buffer after the first attachment drains it in Redis
  mode; the durable chunk log and pub/sub own recovery from then on,
  matching how cross-replica subscribers already attach.
- Enforce hard bounds (5,000 events / 8 MB estimated) in both modes; on
  overflow the buffer is discarded and closed, with recovery falling back
  to the durable chunk log (Redis) or resume snapshot (in-memory).
- Add a generation_stream_early_buffer_overflows_total counter and
  earlyBufferedEvents/Bytes gauges on getRuntimeStats() for visibility.
- Add incident-shaped regression tests and update specs that pinned the
  old post-disconnect re-buffering contract.

* fix: redirect post-overflow first attachments to resume recovery

A buffer discarded by the overflow guard left the initial non-resume
SSE attachment with nothing to replay, silently omitting pre-attach
output until the final event. Track the overflow on the runtime and
close such attachments with the existing reconnect signal instead: the
client already re-attaches with resume=true on transport failure and
its sync frame reconstructs the discarded output from durable/snapshot
state. Adds no per-event work; the check is one boolean per attachment.

* fix: enforce buffer bounds when restoring canceled resume captures

Captured emissions restored by a resume canceled before activation
bypassed the early-buffer hard cap, so one oversized restoration could
persist past the limits with no later emission to trip the guard.
Restoration now applies the same overflow-and-close behavior through a
shared helper, and the restore-cap spec fails before this change
(5 events / ~10MB retained) and passes after.

* chore: add Redis management scripts and update package.json for Redis commands
2026-08-03 19:03:37 -04:00
Danny Avila
ad4ed67070
🟦 chore: Convert Activity-Label Eval Harness to TypeScript (#14530) 2026-07-31 09:58:55 -04:00
Danny Avila
a07c0e4ae8
🧪 chore: Add the Activity-Label Prose Eval Harness (#14527)
Grades fast-model activity-label headers against a fixed corpus so
instruction changes are measured rather than eyeballed on one
conversation. This existed untracked while the continuity work was
developed; committing it because it is the only reproducible record of
WHY `ACTIVITY_INSTRUCTION` is ordered and capped the way it is.

- captured.json: 9 real production payloads pulled verbatim from Langfuse
  with the headers that shipped. Irreplaceable — traces age out.
- corpus.js: 17 cases / 28 steps. The captured run replays as one
  sequence, plus synthetic cases for the modes it never exercised
  (all-failed, partial, parallel batches, rapid near-duplicates, entry
  overflow, truncated output, error-shaped success). Multi-step cases
  chain each generated label into the next step's context, which is what
  makes cross-batch redundancy measurable at all.
- prompt.js: faithful port of the SDK's buildActivityLabelPrompt so
  synthetic cases render the bytes production sends, plus a
  previousLabelCap knob for continuity-window experiments.
- variants.js: single-factor instruction variants. The baseline is read
  from the BUILT package (workspace resolution, then dist, then
  LABEL_EVAL_DIST) so a variant can never be graded against a stale copy
  of the shipped instruction.
- checks.js: length/punctuation/markdown/tool-echo/count-echo, plus
  overlap split into `restate` (adds nothing over an earlier header) vs
  `template` (same frame, new payload — often fine).
- run.js / rescore.js: live runner on the production wire shape
  (max_tokens 256) and an offline re-grader, so metric fixes never
  require re-spending on the API.

Results are gitignored — regenerable, and 292K of the 364K. A full sweep
is ~$0.03 per variant and ~45s.

Findings are recorded in the README, two of them counter-intuitive:
enumerating acceptable opening verbs ANCHORED the model rather than
diversifying it (Confirmed 18→23, opener diversity halved), and diverse
examples alone changed nothing. Sentence order is load-bearing, so a
tidying reshuffle of ACTIVITY_INSTRUCTION regresses real output.
2026-07-30 09:22:34 -04:00
Danny Avila
bfb6b224d2
🔧 chore: Update ESLint config, Import Sorting script, Test Sharding, Bump @librechat/agents (#13552)
* 🔧 chore: Update ESLint config, add import sorting script, Test Sharding, Bump `@librechat/agents`

* Change 'no-nested-ternary' rule from 'warn' to 'error' in ESLint config
* Add new scripts for sorting imports in the project
* Update lint-staged configuration to include import sorting
* Modify GitHub Actions workflows to support sharding for unit tests

* chore: remove nested ternary expressions

* refactor: Extract scale multiplier logic into a separate function in CircleRender component
* refactor: Simplify auto-refill rendering logic in Balance component for better readability
* refactor: Improve width style handling in DataTable components for clarity and maintainability

* chore: remove CircleRender component

* delete: Remove CircleRender component as it is no longer needed in the project

* chore: Bump @librechat/agents to version 3.2.31 and update Node.js engine requirement

* Update @librechat/agents dependency from 3.2.2 to 3.2.31 in package-lock.json, api/package.json, and packages/api/package.json
* Change Node.js engine requirement from >=20.0.0 to >=24.0.0 in @librechat/agents

* chore: Add import sorting check to ESLint CI workflow

* Implement a new job in the GitHub Actions workflow to verify import ordering on changed files.
* The job checks for changes in specific file types and reports any import order drift, providing instructions for local fixes.
2026-06-06 12:31:55 -04:00