feat(skills): typecheck the skill code fences; export the missing B24HelperManager - #404
Merged
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #402.
docs:typecheck-blockshas compiled the fences underdocs/content/**since #109 — 155 blocks, a CI gate. Nothing equivalent coveredskills/*/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 taughtLoggerBrowser, removed in 3.0.0, for months, and hand review never caught it.B24HelperManageris 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 taughtimport { B24HelperManager } from '@bitrix24/b24jssdk'for backend code — and the import resolved to nothing. Confirmedundefinedin the built bundle.So there were two ways to make the tree consistent:
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:
B24HelperManagernot exportedhelper.license/helper.paymentdo not existlicenseInfo/paymentInfo; the skill's own frontmatter advertises "license, payment"destroyB24Helperimported from the package rootuseB24Helper()Text.toB24Format(...)resolving to the DOMTextText, and since the name exists globally nothing complained until a method was called on itselectAccess({})where the parameter isstring[]; CRM ids as'D_42'where the type isnumber[];getJsonObject<T>(), which takes no type argument; acall.make()with no type argument reading.itemoffunknownfilter: { … }as a statement; a try/catchreturning outside any functionFragments were made complete, not ignored —
const params = { filter: { … } }compiles and shows the reader wherefilteractually 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 atscdiagnostic back tofile:line:col, and the GitHub annotation escaping now live once inscripts/_typecheck-blocks.mjs.docs-typecheckstill 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-typechecklogged "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-onlySKILL.mddoes not trip it..skills-typecheck/globals.d.tsnever declares a real SDK export. If a fence usesTextorAjaxResult, 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.mjscarried an inline re-implementation ofextractTsBlocks, 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-lintnow agrees with CITwice this PR series passed
docs-lint --strictlocally 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 bothgit statusandgit 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/0jsSdk:unit+jsSdk:types+skills:unit— 729 testsnode --test scripts/__tests__/**— 74 testslint(1 pre-existing unrelated warning indocs/server/api/ai.post.ts),lint:md,docs-lint --strict,md-internal-links,check-api-reference-index(96 → 97),check-v3-method-refsSuggested squash body
Why before the #277 removal PR
The substring guards in
recipe-hygiene.unit.spec.tscatch 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