Skip to content

feat(skills): typecheck the skill code fences; export the missing B24HelperManager - #404

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

feat(skills): typecheck the skill code fences; export the missing B24HelperManager#404
IgorShevchik merged 3 commits into
mainfrom
claude/text-tools-docs-refactor-mb9dj7

Conversation

@IgorShevchik

@IgorShevchik IgorShevchik commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Closes #402.

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 a public export — a deliberate decision, not a side effect

B24HelperManager is now exported from the package root. Calling this out here rather than leaving it in a source comment, because it is the one change with permanent API-surface consequences and it lands while 3.0.0 scope is being frozen.

The situation the gate exposed: the class already had its own documentation page (50.helper.md), the helpers skill already taught import { B24HelperManager } from '@bitrix24/b24jssdk' for backend code — and the import resolved to nothing. Confirmed undefined in the built bundle.

So there were two ways to make the tree consistent:

  1. Export it — what this PR does.
  2. Stop advertising it: correct the skill and delete the documentation page, routing everyone through useB24Helper().

Option 1 was chosen because the class is deliberately shaped as public API — a real constructor taking TypeB24, setLogger, getters that throw named errors when accessed before init — and because option 2 means reversing documentation that predates this PR and was written on purpose. The cost is explicit: 3.0.0 now has one more class it must treat as already-public, and removing or reshaping it later is a breaking change. If that trade is unwanted, option 2 is still available and this is the moment to say so.

What the first run found

On files that have been reviewed by hand many times:

Finding Why it survived
B24HelperManager not exported see above
helper.license / helper.payment do not exist they are licenseInfo / paymentInfo; the skill's own frontmatter advertises "license, payment"
destroyB24Helper imported from the package root it is a member of useB24Helper()
Text.toB24Format(...) resolving to the DOM Text two files; the fence never imported the SDK's Text, and since the name exists globally nothing complained until a method was called on it
selectAccess({}) where the parameter is string[]; CRM ids 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 ordinary type errors, invisible to prose review
Fences that were not TypeScript at all filter: { … } as a statement; a try/catch returning outside any function

Fragments were made complete, not ignored — const params = { filter: { … } } compiles and shows the reader where filter actually lives. The 24 missing imports were inserted by a throwaway script driven by the gate's own output rather than by hand.

Shared engine, not a second copy

Extraction, // @check-ignore, the mapping of a tsc diagnostic back to file:line:col, and the GitHub annotation escaping now live once in scripts/_typecheck-blocks.mjs. docs-typecheck still reports 155 blocks, 0 errors — the refactor is behaviour-preserving.

One deliberate behaviour change: zero blocks found is now an ERROR (exit 1), where the old docs-typecheck logged "no TS blocks found" and exited 0. A glob that stops matching would otherwise report "0 errors" and read as healthy. The count is across the whole sweep, so a single prose-only SKILL.md does not trip it.

.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 that snippet needs the line, and an ambient would make the gate certify code that does not run.

A test that could not fail

docs-typecheck.test.mjs carried an inline re-implementation of extractTsBlocks, commented as "mirrors the production logic exactly". 16 tests were passing against a copy, so a bug in the real extractor could not redden any of them. They bind to it now — verified by mutation: breaking the real extractor fails 9, and previously failed 0.

docs-lint now agrees with CI

Twice this PR series passed docs-lint --strict locally and failed it in CI on the same change. The freshness check read only committed history, so a cited source modified-but-not-yet-committed still reported its old date. An uncommitted edit now counts as modified-now; a gitignored cited source, invisible to both git status and git log, no longer gets skipped in silence; and the repo status is read once per run instead of once per link.

The branch that mattered — a dirty tracked file — initially had no test: both probe tests exited through the untracked fallback, so disabling it left the suite green. All three branches now fail under mutation.

Checks

  • pnpm run typecheck — nine passes, 0 errors · docs-typecheck: 155/0 · skills-typecheck: 68/0
  • jsSdk:unit + jsSdk:types + skills:unit — 729 tests
  • node --test scripts/__tests__/** — 74 tests
  • lint (1 pre-existing unrelated warning in docs/server/api/ai.post.ts), lint:md, docs-lint --strict, md-internal-links, check-api-reference-index (96 → 97), check-v3-method-refs

Suggested squash body

Added: skills:typecheck-blocks, a CI gate compiling every ```ts fence in
skills/*/SKILL.md; B24HelperManager exported from the package root — previously
documented and taught, but not exported.

Fixed: ~24 inaccuracies in skill files found by the new gate — helper.license /
helper.payment (the getters are licenseInfo / paymentInfo), the selectAccess and
selectCRM signatures, a type argument getJsonObject does not accept, missing
imports, and fences that were not valid TypeScript.

Changed: docs-lint treats an uncommitted or gitignored cited source as modified
now, instead of reading committed history alone.

Why before the #277 removal PR

The substring guards 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. Landing this first means that PR gets verified rather than merely accompanied.

🤖 Generated with Claude Code

https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr

claude added 3 commits August 27, 2026 07:59
…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
…with CI

Twice now `docs-lint --strict` passed locally and then failed in CI on the same
change. The mechanism: the freshness check asks `git log -1` for a cited
source's last modification, so a source that is modified but not yet committed
still reports its OLD date — the local run reports on a state that no longer
exists, and the warning appears the moment the commit lands.

The second occurrence came immediately after writing "re-run it AFTER
committing" into a commit message, which is the evidence that the rule does not
work as a rule. It is now unnecessary: an uncommitted edit to a cited source
counts as modified now, so the local run agrees with CI whatever the commit
state.

Verified both directions — dirtying a cited source produces the warning with
today's date, restoring it clears it — and the regression test drives the real
`gitLastCommitDate` rather than the injected seam, so the git plumbing is what
is under test. Removing the new branch fails it.

Also refreshes the `audited:` stamps on the two pages citing `index.ts`, which
exporting `B24HelperManager` invalidated. Checked, not rubber-stamped: the API
reference page gained its row in the same change, and the types index lists
types rather than value exports, so a new class does not alter what it claims.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
…op spawning git per link

Review found three things, and my own mutation testing found a fourth that
mattered more than any of them.

One `git status` for the run, not one per cited link. There are ~84 audited
links; a spawn each doubled the script's process count for information a single
call already holds. Parsing is now a pure exported function, so the rename and
quoted-path cases are tested directly rather than by making the repository
actually contain them.

A gitignored cited source is no longer skipped in silence. `git status
--porcelain` does not list an ignored path and `git log` has no commit for it,
so both signals were blind and the page's audit was never checked at all — the
least useful of the three possible outcomes. It now counts as modified.

The test wrote to a real tracked source. It appended a probe line to
`packages/jssdk/src/types/payloads.ts` and restored it in a `finally`, which
loses the race against a killed runner and leaves a corrupted source file.
Rewritten to use a throwaway path, now also gitignored so a killed run leaves
nothing behind at all.

And the one I would have shipped: the dirty-TRACKED-file branch — the whole
point of the change — had NO test. Both probe tests exit through the
untracked/ignored fallback, so disabling that branch left the suite green. It is
now covered at the function that owns it, with `isDirty` injected rather than by
dirtying a real source. All three branches fail under mutation; before this, one
of them failed under none.

The earlier version of this test was also wrong in a way that passed review:
it set `audited` in the FUTURE so that committed history would look fresh, but
"modified now" cannot exceed a future date either, so the assertion could not
have fired for the right reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
@IgorShevchik IgorShevchik changed the title feat(skills): compile the code fences in skill files, and fix what that found feat(skills): typecheck the skill code fences; export the missing B24HelperManager Aug 27, 2026
@IgorShevchik
IgorShevchik merged commit 2d75bee into main Aug 27, 2026
10 checks passed
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.

Type-check the code fences in skill files, the way docs:typecheck-blocks does for the docs site

2 participants