Skip to content

fix(skills): stop teaching the removed LoggerBrowser, and fix the calls it left behind - #401

Merged
IgorShevchik merged 3 commits into
mainfrom
claude/text-tools-docs-refactor-mb9dj7
Aug 26, 2026
Merged

fix(skills): stop teaching the removed LoggerBrowser, and fix the calls it left behind#401
IgorShevchik merged 3 commits into
mainfrom
claude/text-tools-docs-refactor-mb9dj7

Conversation

@IgorShevchik

Copy link
Copy Markdown
Collaborator

Found during the #277 analysis. Non-breaking, ships in 2.x, and shrinks the blast radius before the 3.0.0 removal PR is written.

The problem

skills/b24jssdk-core/SKILL.md (3 sites) and skills/b24jssdk-recipes/SKILL.md (2 sites) presented LoggerBrowser.build(...) as the way to obtain a logger. That class is tagged @removed 3.0.0.

These files are what an AI agent reads before writing code. So our own instructions were emitting a deprecated API today, and producing code that breaks at the next major — the opposite of what a skill file is for.

Swapping the constructor is not enough

This is the part worth reading twice, and the reason this PR is larger than "five find-and-replaces".

LoggerBrowser (removed) LoggerInterface (current)
signature info(...params: any[]) info(message: string, context?: Record<string, any>)
warn level warn() warning()

A mechanical rename leaves logger.info('Hello,', name) compiling — an array or an Error satisfies Record<string, any> structurally — while the second value is silently reshaped or dropped.

Two shipped recipes already had exactly that

So this is a bug fix, not only a deprecation sweep:

  • 12-oauth-install.ts logged logger.error('install failed', e). An Error's message and stack are not own enumerable properties, so the context serialised as {} and the failure vanished from the log — in the handler for a Bitrix24 install callback, where losing the reason is expensive.
  • 10-error-handling.ts passed res.getErrorMessages() as the context, landing the array as { '0': …, '1': … }.

Neither is caught by skills:typecheck, which is why they had survived.

Node and browser now get different guidance

Previously one answer, wrong for one of the two:

  • Browser / frameLoggerFactory.createForBrowser('App', isDev), the documented migration target.
  • Node / backend / recipesLogger.create(name) plus an explicit ConsoleV2Handler(level, { useStyles: false }), because the browser factory emits CSS styling that a terminal prints as literal noise. This is also what recipe 01-crm-analytics.ts already did correctly, so the skills now match a working example rather than contradicting it.

Also $b24.setLogger(logger) rather than setLogger?.(logger) — it is a required member of TypeB24, and the optional call implied it might be absent.

Guards

Three table-driven checks across both SKILL files, b24jssdk-rest/SKILL.md, and every recipe: no LoggerBrowser/LoggerType, no logger.warn(, and every logger call's second argument is an object literal. Each verified by mutation — reintroducing the old constructor, the old level name, and the original logger.error('install failed', e) each fail the suite.

The third guard needed a scanner, and the first two attempts were wrong

Worth recording, because it is the same defect twice:

  1. logger.<level>\(\s*[^,)]+, treats the first comma it meets as the argument separator — but most of these calls pass a template literal whose text contains commas. Ten files failed on their own log messages.
  2. A quote-aware scanner with a single "in a string" flag then broke on `(${it.TYPE}${it.SIZE ? `, ${it.SIZE} bytes` : ''})` in 05-disk-files.ts — a template literal holding an interpolation holding another template literal holding a comma. The flat flag closed the outer template on the inner backtick.

It now tracks quote and interpolation state with a stack. That is precisely what #139 item 2 reported about the docs code transform — counting delimiters without tracking whether you are inside a string — found again while writing the guard against it.

Checks

  • pnpm run typecheck — all eight passes, 0 errors
  • skills:typecheck, skills:unit (184 tests), jsSdk:unit + jsSdk:types
  • lint (1 pre-existing unrelated warning in docs/server/api/ai.post.ts), lint:md, docs-lint --strict, check-v3-method-refs, md-internal-links

Refs #277

🤖 Generated with Claude Code

https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr


Generated by Claude Code

…ls it left behind

`skills/b24jssdk-core/SKILL.md` and `skills/b24jssdk-recipes/SKILL.md` presented
`LoggerBrowser.build(...)` as the way to obtain a logger. That class is tagged
`@removed 3.0.0` (#277). These files are what an agent reads before writing
code, so our own instructions were producing deprecated calls today and code
that breaks at the next major.

Swapping the constructor is not enough, which is the part worth reading twice.
`LoggerBrowser` took variadic arguments (`info(...params: any[])`);
`LoggerInterface` takes `(message: string, context?: Record<string, any>)`, and
has `warning`, not `warn`. A mechanical rename leaves `logger.info('Hello,',
name)` compiling — an array or an Error satisfies `Record<string, any>`
structurally — while the second value is silently reshaped or dropped.

Two shipped recipes already had exactly that, so this is a bug fix and not only
a deprecation sweep:

- `12-oauth-install.ts` logged `logger.error('install failed', e)`. An Error's
  message and stack are not own enumerable properties, so the context serialised
  as `{}` and the failure vanished from the log — in the handler for a Bitrix24
  install callback, where losing the reason is expensive.
- `10-error-handling.ts` passed `res.getErrorMessages()` as the context, landing
  the array as `{ '0': …, '1': … }`.

Node and browser now get different guidance rather than one wrong answer:
`LoggerFactory.createForBrowser` in the frame example, and an explicit
`ConsoleV2Handler(..., { useStyles: false })` for the backend and recipe
snippets, because the browser factory emits CSS styling a terminal prints as
literal noise. Also `$b24.setLogger(logger)` rather than `setLogger?.()` — it is
a required member of `TypeB24`, so the optional call implied it might be absent.

Guarded by three table-driven checks across both SKILL files and every recipe:
no `LoggerBrowser`/`LoggerType`, no `logger.warn(`, and every logger call's
second argument is an object literal. Each verified by mutation.

The third guard needed a scanner rather than a regex, and the first attempt is
worth recording: matching `logger.<level>(\s*[^,)]+,` treats the first comma it
meets as the argument separator, but most of these calls pass a template literal
whose text contains commas — ten files failed on their own log messages. The
second attempt then broke on a template literal nested inside `${}` in
`05-disk-files.ts`. It now tracks quote and interpolation state with a stack.
That is the same defect #139 item 2 reported in the docs code transform, found
again while writing the guard against it.

Refs #277

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
claude added 2 commits August 26, 2026 09:00
…scanner to an ESLint rule

The review was right that the scanner was the wrong layer. `eslint-rules/`
already holds two hand-written AST rules for logger callsites
(`no-credential-in-logger`, `require-catch-on-logger-call`), and this is a third
of the same kind. Working on the AST also removes a class of defect the scanner
demonstrably had: a regex literal containing a comma or a quote broke argument
splitting, silently, and no amount of care in a hand-written lexer makes that
impossible.

The rule is scoped to `skills/b24jssdk-recipes/**` only. It demands an object
literal at the callsite, which is a teaching convention rather than a
correctness rule — hoisting the context into a variable is perfectly correct
code, and the SDK's own deprecated `LoggerBrowser` passthroughs are written that
way. It earns its place in files shipped for an agent to copy, where the literal
is what makes the parameter's meaning visible at the point of copying. Enabling
it repo-wide reported twelve false positives in `logger/browser.ts` alone.

The two remaining guards stay in the spec, because ESLint cannot see inside a
Markdown fence and `SKILL.md` is where the deprecated logger was being taught.
They now DISCOVER the skill files instead of naming three by hand: the tree has
seven `SKILL.md` and four of them were unguarded, so a new skill could have
taught `LoggerBrowser` and passed. Verified by planting one in
`b24jssdk-helpers`, which the old list did not cover and the new sweep fails on.

Also from the review:

- Both SKILL files now say why `requestInfo` is safe to log — `AjaxError`'s
  constructor redacts it, so the safety is an SDK contract and not caller
  discipline. Without that, an agent may rebuild the same context by hand from
  the original params and put a live credential in a log.
- `12-oauth-install.ts` records that logging `message`/`stack` there is safe only
  while the credential store throws plain filesystem errors.
- Refreshed the `audited:` stamp on `79.security.md`, which cites the recipe this
  PR modifies.

Filed #402 for the gap both mechanisms leave: a wrong call SHAPE inside a
`SKILL.md` fence is caught by neither, and compiling skill fences the way
`docs:typecheck-blocks` compiles the docs site would close it for every
deprecated symbol at once rather than one substring guard at a time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
…e guards

The rule shipped without a spec while both of its siblings in `eslint-rules/`
have one — and its own doc comment carefully enumerates edge cases that nothing
was pinning. Those distinctions were exercised only by whichever recipe happened
to hit each branch, so the coverage would erode silently. Now the same shape as
the sibling specs: the real rule module through ESLint's `Linter` over known-bad
and known-good snippets, plus an assertion that `eslint.config.mjs` actually
wires it — including that it is NOT applied to the SDK source, which is a
deliberate scope decision rather than an omission.

Two gaps found by review, both latent rather than live:

Skill discovery skipped a symlinked directory. `Dirent.isDirectory()` reports
the dirent's own type, so it is false for a symlink even when the link points at
a directory — a symlinked skill could have taught `LoggerBrowser` and passed,
which is precisely the blind spot the discovery replaced. Verified: `linked
isDirectory=false isSymlink=true`.

The rule exempted an explicit `undefined` as "deliberately no context". It
cannot tell that from a context the author meant to build and forgot, and
`logger.info('m')` already says "no context" unambiguously — so the exemption
was both unnecessary and inconsistent with rejecting a known-good identifier.
Removed.

Also softened the discovery count from a hard floor of seven to non-empty.
Pinning today's headcount would fail on a legitimate skill removal for a reason
unrelated to logger hygiene; discovery already makes a stale list impossible, so
the check only needs to rule out an empty sweep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
@IgorShevchik
IgorShevchik merged commit 50a0e19 into main Aug 26, 2026
10 checks passed
IgorShevchik pushed a commit that referenced this pull request Aug 27, 2026
…at found

`docs:typecheck-blocks` has compiled the fences under `docs/content/**` since
#109 — 155 blocks, a CI gate. Nothing equivalent covered `skills/*/SKILL.md`,
though those are what an AI agent reads BEFORE writing code: a broken snippet
there is not a page someone might misread, it is a template that gets
reproduced. #401 is the proof — the skills taught `LoggerBrowser`, removed in
3.0.0, for months, and hand review never caught it.

This adds `skills:typecheck-blocks` over 68 fences, sharing the engine with the
docs gate rather than copying it: extraction, `// @check-ignore`, the mapping of
a tsc diagnostic back to `file:line:col`, and the GitHub annotation escaping now
live once in `_typecheck-blocks.mjs`. `docs-typecheck` still reports 155 blocks,
0 errors — behaviour unchanged.

It also generalises where the current guards do not. The substring checks in
`recipe-hygiene.unit.spec.ts` catch one named symbol each, so every deprecation
needs a new guard; a compiler covers the whole surface at once, which matters
for #277 and its 22-symbol removal list.

WHAT THE FIRST RUN FOUND, on files reviewed by hand many times:

- `B24HelperManager` is not exported. The documentation gives it its own page,
  the helpers skill teaches constructing it directly for backend code, and
  `import { B24HelperManager } from '@bitrix24/b24jssdk'` resolved to nothing —
  confirmed `undefined` in the built bundle. Now exported (additive; the #383
  reference gate correctly demanded the row, 96 -> 97).
- `helper.license` / `helper.payment` do not exist — they are `licenseInfo` /
  `paymentInfo`. The skill's own frontmatter advertises license and payment.
- `destroyB24Helper` imported from the package root; it is a member of
  `useB24Helper()`.
- `Text.toB24Format(...)` resolving to the DOM `Text`, in two files, because the
  fence never imported the SDK's `Text`. Silent: the name exists, so nothing
  complained until a method was called on it.
- `selectAccess({})` where the parameter is `string[]`; CRM ids written as
  `'D_42'` where the type is `number[]`; `getJsonObject<T>()`, which takes no
  type argument; a `call.make()` with no type argument reading `.item` off
  `unknown`.
- Fragments that were not TypeScript at all — `filter: { … }` as a statement, a
  try/catch `return`ing outside any function. Made complete rather than ignored:
  `const params = { filter: { … } }` also shows the reader where `filter` lives.

The 24 missing imports were inserted by a throwaway script driven by the gate's
own output, not by hand.

Two things about the harness worth knowing:

`.skills-typecheck/globals.d.ts` never declares a real SDK export. If a fence
uses `Text` or `AjaxResult`, it must import it — an agent copying the snippet
needs that line, and an ambient would make the gate certify code that does not
run. Placeholders belonging to the reader's own application, and framework
globals that really are auto-imported, are declared there. The header says so.

An empty sweep is an ERROR, not a pass. A glob that stops matching would
otherwise report "0 errors" and read as healthy.

Also fixes a test that could not fail: `docs-typecheck.test.mjs` carried an
inline re-implementation of `extractTsBlocks`, with a comment claiming it
"mirrors the production logic exactly". Sixteen tests were therefore passing
against a copy, so a bug in the real extractor could not redden any of them. It
is exported now, and the tests bind to it — verified by breaking the real
extractor, which fails 9 of them and previously failed none.

Closes #402

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
IgorShevchik added a commit that referenced this pull request Aug 27, 2026
…HelperManager (#404)

Added: `skills:typecheck-blocks`, a CI gate compiling every ```ts fence in
`skills/*/SKILL.md` — 68 blocks — the way `docs:typecheck-blocks` has guarded the
documentation site since #109. Skill files are what an AI agent reads before
writing code, so a broken snippet there is not a page someone might misread, it
is a template that gets reproduced; #401 found them teaching a class removed in
3.0.0, for months, past repeated hand review.

Added: `B24HelperManager` is now exported from the package root. It already had
its own documentation page and the helpers skill already taught importing it,
but the import resolved to `undefined` in the built bundle. Exporting it rather
than deleting the documentation was a deliberate call — the class is shaped as
public API (a constructor taking `TypeB24`, `setLogger`, getters that throw
named errors before init) — and it commits 3.0.0 to treating one more class as
already-public.

Fixed: the inaccuracies that first run found, on files reviewed by hand many
times. `helper.license` / `helper.payment` do not exist — the getters are
`licenseInfo` / `paymentInfo`, and the skill's own frontmatter advertised the
wrong names. `destroyB24Helper` imported from the package root when it is a
member of `useB24Helper()`. `Text.toB24Format(...)` silently resolving to the DOM
`Text` in two files, because the fence never imported the SDK's. `selectAccess({})`
where the parameter is a `string[]`; CRM ids written as `'D_42'` where the type
is `number[]`; a type argument `getJsonObject` does not accept; a `call.make()`
with no type argument reading `.item` off `unknown`. Plus fences that were not
valid TypeScript at all, made complete rather than ignored.

Changed: `docs-lint` treats an uncommitted — or gitignored — cited source as
modified now, instead of reading committed history alone. The old behaviour
passed locally and then failed in CI on the same change, twice, because a source
modified but not yet committed still reported its old date. The repository status
is now read once per run rather than once per cited link.

Also: `docs-typecheck.test.mjs` carried an inline re-implementation of the block
extractor, so sixteen tests were passing against a copy and a bug in the real one
could not redden any of them. The extractor is shared and exported now, and the
tests bind to it.

Closes #402
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants