Skip to content

chore(typecheck): burn down advisory typecheck debt to zero (1438 → 0)#232

Open
jsugg wants to merge 36 commits into
mainfrom
chore/typecheck-debt-burndown
Open

chore(typecheck): burn down advisory typecheck debt to zero (1438 → 0)#232
jsugg wants to merge 36 commits into
mainfrom
chore/typecheck-debt-burndown

Conversation

@jsugg

@jsugg jsugg commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

Burns the advisory strict-JSDoc typecheck gate (npm run typecheck
tsconfig.checkjs.json, checkJs + strict, TypeScript 6) down to zero
across src/, config/, and scripts/. Measured baseline was 1438 errors;
npm run typecheck now exits 0.

Every commit is comment-only JSDoc typing plus type-only casts — no runtime
change
, and the committed strict profile is never weakened. Work landed
area by area, highest error counts first, each batch verified independently
(tsc clean for the area + the related Jest suites + ESLint) before commit.

Method

  • Annotate params; @typedef external-JSON / duck shapes; cast once at
    validation boundaries (double-cast through unknown only where TS reports
    insufficient overlap).
  • Under-specified or stale annotations were corrected at the source (e.g. a
    narrow @returns, a snake_case output map mislabeled camelCase), never by
    loosening an exported type.
  • Second-order errors were fixed at the consumer, never by widening a new
    export. Two were caught and folded in-flight (a fixture-app cast into the
    perf smoke; an impl-seam typing that surfaced an under-specified artifact
    @returns).

Verification

  • npm run typecheck → 0 errors (exit 0).
  • Each area's related unit/integration suites pass; ESLint clean on every
    touched file; the one file with no test coverage (scripts/doctor-tls.js,
    a manual CLI) checked with node --check.
  • Full burn-down log and per-area rationale in docs/typecheck-debt.md.

Follow-up (not in this PR)

Promotion of the gate from advisory to required is staged separately and is
sequenced after this merges (so main is green first): remove
continue-on-error from the CI typecheck job and add the typecheck
context to config/github/required-checks.json, then a maintainer applies the
live branch-protection / ruleset change per docs/required-checks.md.

jsugg added 30 commits July 2, 2026 20:03
JSDoc @param/@typedef annotations plus type-only boundary casts at the
JSON/gh-API edges. No runtime change, no `any`. Strict typecheck errors
1438 -> 1423; adds burn-down log + pattern to docs/typecheck-debt.md.
Add DOM to tsconfig.checkjs lib so Node's global fetch/Response/Headers
resolve (34 'Cannot find name fetch' errors, repo-wide), and type the
parseArgs arg-objects + JSON/boundary casts in four scripts/github
pages/reporting scripts (14 errors). Strict typecheck 1423 -> 1372.
No runtime change except compose-pages-site publishPath defaulting to
'' instead of null (safer). 32 related unit tests green.
@param {unknown} on the config coercion + provider-override helpers,
import('joi').Root + env callback types, and type-only boundary casts
on the parsed-YAML object. Strict typecheck 1372 -> 1360. No runtime
change; providerOverrides unit tests green.
@typedef the GitHub dependency-graph compare API model
(DependencyChange/Vulnerability/VulnerableChange) plus the CLI
ParsedArgs/ValidatedArgs shapes, annotate every helper's params and
returns, and cast once at the parseArgs validation boundary. Strict
typecheck 1360 -> 1311 (scripts/github/review-dependencies.js 49 -> 0).
No runtime change; 23 related unit tests green.
@typedef the CLI ParsedArgs/ValidatedArgs and the App API
InstallationLookupResponse/InstallationTokenResponse; fetchGitHubJson
now returns unknown with each caller casting at the boundary; the header
map is Record<string,string> for the conditional Content-Type. Strict
typecheck 1311 -> 1277 (create-github-app-installation-token.js 34 -> 0).
No runtime change; 12 related unit tests green.
@typedef the duck-typed collaborators (Describer/AsyncDescriber/
ScraperService/ImageDescriberFactory) and domain shapes (ImageDescription/
ProviderJob/SettledDescription); make supportsAsyncJobs a type predicate so
the async-only describer methods narrow; @template the concurrency mapper;
type-only cast of this.constructor to preserve dynamic dispatch. Strict
typecheck 1277 -> 1230 (PageDescriptionService.js 47 -> 0). No runtime
change; 8 related unit tests green.
@typedef the job model (Job/JobSeed/JobError/ProviderJob), collaborators
(Describer/JobStore/ImageDescriberFactory), and the ResolveDescriptionResult
union; type the now/sleep constructor deps; cast this.constructor static
access (replace_all across the buildExpirationIso calls) and job.providerJobId
once at the refresh boundary. Strict typecheck 1230 -> 1188
(DescriptionJobService.js 42 -> 0). No runtime change; 7 unit tests green.
@typedef collaborators (Logger/HttpClient/ProviderConfig/RequestOptions/
ImageAsset) and axios-style HttpError; error helpers take unknown and cast
to HttpError internally so catch-vars flow without caller changes; document
the sleep constructor dep; type-only this.constructor cast; cast Set.has
args where the value is optional/null. Strict typecheck 1188 -> 1146
(OpenAiCompatibleVisionDescriberService.js 42 -> 0). No runtime change;
6 related unit tests green.
@typedef Logger/ProviderConfig/RequestOptions/Prediction/ReplicateClient/
ProviderJob; type the SDK-shaped predictions client; replace_all the
this.constructor.normalizePrediction static-access casts; boundary-cast the
unknown error message. Strict typecheck 1146 -> 1104
(ReplicateDescriberService.js 42 -> 0). No runtime change; 4 unit tests green.
Sibling of DescriptionJobService: @typedef PageJob/DescriptionJob/JobStore/
Logger/PageDescriptionServiceLike/DescriptionJobServiceLike/PageResolveResult;
cast the optional descriptionJobService once at the resolver boundary; type
the inner recursive waitForTerminalDescriptionJob arrow; replace_all the
this.constructor static casts; Error & { code } casts in the job-error
builders; buildFailedJob takes unknown + internal cast. Strict typecheck
1104 -> 1039 (PageDescriptionJobService.js 65 -> 0). No runtime change;
13 related unit tests green.
Azure/Ollama/Scraper describer+scraper services: per-file Logger/HttpClient/
ProviderConfig/RequestOptions typedefs, AzureError shape cast for the
axios-error skip-check, type-only this.constructor casts, URLSearchParams.set/
new URL arg casts (constructor-guaranteed endpoint), Scraper images-accumulator
cast + typed outbound-policy. Strict typecheck 1039 -> 1004; src/services area
now 0 (from 273 baseline). No runtime change; 31 related unit tests green.
The concrete JobStore (in-memory + Redis): @typedef StoredJob/RedisClient/
RedisTransaction/DescriptionJobsConfig; annotate the module helpers and both
stores' methods; double-cast the real redis client to a loose RedisClient at
the boundary (avoids fighting the generic redis types); type the initialize
options object so the destructuring-default stops hiding config/logger. Strict
typecheck 1004 -> 972 (src/infrastructure/descriptionJobStore.js 32 -> 0).
No runtime change; 6 related unit tests green.
…rs to zero

OpenAiCompatibleProviderOptions typedef for the options bag; type the inner
buildEnvSchema/buildConfig/isConfiguredInConfig/createRuntime arrows; cast the
runtime constructor arg via ConstructorParameters<typeof Service>[0]. Root-caused
the never[] errors to helpers.js's dependentEnvNames = [] default and typed
validateApiKeyBackedProviderEnv + the shared helpers. Strict typecheck 972 -> 931
(buildOpenAiCompatibleProvider 33 -> 0, helpers 8 -> 0). No runtime change;
51 provider unit tests green.
Express controller: minimal duck-typed collaborators (factory + 3 service-likes)
and ControllerRequest/ControllerResponse typedefs (query/params as
Record<string,string>, chainable res); @param appended to each handler's
existing @Swagger block; replace_all the two uniform catch-error casts
(error.message -> (Error), cause: error -> (object)). Strict typecheck
931 -> 898 (descriptionController.js 33 -> 0). No runtime change;
24 controller unit tests green.
Bare `npx jest` used the root config, which never loaded
tests/setup/jest.setup.js, so the QE-009 env restore only ran in lane
configs. Any suite that mutates process.env leaked between tests when
run directly (validateEnvVars: 12 false failures).
Typedef the external Newman report shapes (report/execution/assertion/
failure) and the internal aggregate model; replace bare-object JSDoc
params, type the Map indexes and reduce seed, cast at the JSON
boundaries. scripts/postman/newman-summary.js 60 -> 0; total 823 -> 750
(includes prior second-order batch). Ledger rows added for both. No
runtime change.
Model the Postman collection JSON (PostmanCollection/CollectionItem/
CollectionEvent/CollectionHeader) once in collection-utils and reuse it
across lint-collection and newman-runner; type the provider-validation
scope resolution against a ProviderValidationInfo shape cast at the
providerCatalog boundary; type the app-server env bag, port allocator
results, and newman-runner failure plumbing. Drops the dead
desiredScope !== 'all' guard (unreachable after the early return).
scripts/postman 116 -> 0 (area from 185 baseline); total 750 -> 609
including second-order wins in importers. No runtime change; 106 tests
across 13 suites green.
Type the server lifecycle plumbing: generic configureServer over
net.Server, RequestListener app params, Promise resolve hints, duck-
typed logger/cluster/process collaborators (ClusterLike, FatalLogger,
ShutdownHandler), and injectable-dependency shapes for the runtime
bootstrap. src/server 82 -> 0; total 609 -> 529. No runtime change;
23 tests across 6 server suites green.
Type the provider definitions (Joi/env/config arrow params, createRuntime
deps, ConstructorParameters ctor casts, replicate ESM-shaped-types double
cast), the shared caption/image helpers (unknown params with one cast
after narrowing), and the runtime registry (RuntimeProvider typedef).
src/providers 70 -> 0; total 529 -> 465. No runtime change; 51 provider
suites green.
Type the final 54 src/api errors (the tail of the 87 baseline, after
descriptionController's 33). Express ships no bundled types, so add an
ambient types/express.d.ts shim (declare module 'express') wired into the
tsconfig include: this keeps the express boundary `any` while local JSDoc
typedefs carry the handler/request/response shapes, and clears the 5 TS7016
require sites on its own.

- healthController/scraperController: duck-typed HttpRequest/HttpResponse
  (chainable status/json/send), ScraperServiceLike, Error cast on the catch.
- access-control/error-handler: AccessRequest (get/path), ErrorRequest/
  ErrorResponse; asyncHandler and createAccessControlMiddleware use an
  explicit function-type @returns so the returned handler's params are
  contextually typed (no inline casts).
- rate-limiters: express-rate-limit is ESM-shaped for a CJS require, so
  double-cast to (options: object) => Function (same shape as pino/replicate);
  a RateLimiterOptions bag with optional skip clears the status-limiter TS2345.
- request-filter/routes: Filter* (headers/log/router) and route controller
  typedefs; loggers typed (...args: any[]) => void so pino's generic
  Logger<{...}> stays assignable (an unknown[] shape fails the contravariant
  param check — this fixed a 2-error createApp regression).

Type-only; no runtime change. src/api area 0; total 465 -> 407.
Verified (verifier-lite): tsc src/api empty, total 407, related
controller/middleware unit tests green, eslint clean.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
…Asset

The src/providers fetchImageAsset typing (04c53f3) gave its httpClient param
a concrete Record<string, PolicyRequestFn> shape (2-arg (url, options?)
calls). The Azure/Ollama/OpenAiCompatibleVision describer services genuinely
call httpClient.post(url, body, config) (3-arg) internally, so their local
HttpClient type cannot structurally be that Record — an irreconcilable arity
mismatch. Fixed at the consumer (never loosen the new export):

- Cast this.httpClient to `any` once at each fetchImageAsset call site so
  the internal .post(url, body, config) typing stays intact.
- Tighten each outboundUrlPolicy dep from opaque Function to
  (value: any) => Promise<URL> (matches fetchImageAsset + ScraperService).

Type-only; no runtime change. src/services area back to 0; total 407 -> 401.
Verified: tsc src/services empty; 16 tests across the 3 service suites green.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
Finishes src/ (utils + composition root). All type-only.

src/utils:
- helmet & swagger-ui-express are ESM-shaped for a CJS require: helmet
  double-cast to its ['default']; swagger-ui-express gets an ambient
  declare-module shim (docs UI is lazy-required behind /api-docs).
- express.Router() is `any` under the express shim, so the router helpers
  cast the router to a local typed shape (ValidationRouter/SwaggerRouter)
  whose handler signature contextually types the (req, res[, next]) params.
- getUpstreamErrorSummary: @param {unknown} on the axios-error/header
  helpers, UpstreamErrorLike + HeaderBag duck typedefs cast once past the
  guards, Record<string, unknown> reduce seed; lazy swagger lets typed any.

src/createApp.js (composition root):
- CreateAppOptions typedef makes every injectable dep optional (clears the
  non-defaulted destructure errors) and duck-types them `any` — except
  config, kept precise as typeof import('../config'). This also clears the 3
  pino Logger<{...}>-vs-service-Logger assignability errors at logger:
  appLogger. Return type left inferred so app.js keeps resolving .app.

src/app.js:
- Cast the node cluster module to `any` at the runtimeBootstrap boundary
  (TS2559: the minimal { isPrimary? } param shares no properties with the
  module type), and double-cast the startRuntimeFn arrow to
  () => Promise<void> through unknown (its Promise<object> is consumed as void).

No runtime change. src/ tree now 0; total 401 -> 349.
Verified (verifier-lite): tsc src/ empty, total 349, createRouter +
getUpstreamErrorSummary + createApp unit suites and the api integration
suite green.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
With src/ already at zero, the whole non-scripts tree now typechecks.
All type-only.

- swagger-jsdoc: ambient declare-module shim (dev-only spec-regen fallback).
- providerCatalog.js: a ProviderDefinition typedef cast once onto
  getProviderCatalog().slice() types every .forEach/.filter/.map/.flatMap
  callback; validateEnv -> string[] and getStartupWarnings ->
  {provider?,message?}[] so validateEnvVars keeps resolving; a
  ProviderValidationDefinition intersection narrows the validation-scope
  filter so .providerValidation.scopeKey is non-optional; ProviderOverrides
  record; Record<string, object> sections seed.
- descriptionJobStore.js / rateLimitStore.js: the env / loosely-parsed-value
  bags are typed Record<string, any> / any at the parse boundary — this
  clears the {}-index and Set.has/Array.includes literal-union errors while
  preserving the existing inferred return shapes (no consumer churn);
  redisTopology typed string clears the 'external'-literal TS2367; the kind
  resolvers get { mode?, redisUrl? } option bags.

No runtime change. config area 0 (src also 0); total 349 -> 302.
Verified: tsc config+src empty; 39 tests across 6 config suites green;
eslint clean.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
scripts/openapi area 63 -> 0; total 302 -> 239. The whole area is
comment-only JSDoc typing - no runtime change.

- spec-utils.js: export shared external-JSON typedefs OpenApiNode
  (Record<string, any> - the document is external JSON, nodes stay
  loose) and OpenApiSpec (only the top-level sections the gates walk
  are named: openapi/info/paths/components/security, & OpenApiNode).
  loadSpec returns OpenApiSpec; listOperations yields
  operation: OpenApiNode; canonicalize reduce seed cast to
  Record<string, any>; resolveRef pointer-walk seed bridged to any.
- validate-spec.js: import OpenApiSpec; Violation typedef for the
  rule reports; every bare-object spec @param -> OpenApiSpec (clears
  the TS2339-on-object property reads); writeReport stats/violations
  typed; Map reduce seed cast to Map<string, Violation[]>;
  isNonEmptyString takes unknown; two catch-site (Error) casts.
- diff-contract.js: import OpenApiSpec/OpenApiNode; ContractChange
  typedef; pathsOf/schemasOf return Record<string, OpenApiNode>;
  methodsOf takes unknown (guards narrow it); @type on the parseArgs
  literal so base: null widens to string|null; Map seed cast; two
  catch casts.
- check-freshness.js: DriftSummary/FreshnessResult typedefs;
  keysOf(unknown)/difference(string[]); driftLines' pair table cast
  to [string, string[]][] so the destructured items keep their array
  type; one DriftSummary boundary cast at the non-fresh call site;
  two catch casts.

Exposed 2 pre-existing baseline TS2352s in
scripts/postman/provider-validation-scope.js (second-order from the
providerCatalog typing in eae331e, missed by the config-only grep) -
next batch.

Verified (verifier-lite): tsc scripts/openapi grep empty, total 239;
46 tests across the 3 openapi gate suites green; eslint clean on all
4 files.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
Total 239 -> 237. The two boundary casts onto the local
ProviderValidationProvider duck stopped "sufficiently overlapping"
once eae331e gave getProviderValidationProviders()/
getProviderValidationByScope() their rich ProviderValidationDefinition
types (providerValidation: { scopeKey } & Record<string, any> vs the
duck's required typed fields). Fixed at the consumer: bridge both
casts through unknown, keeping the local precise duck as the source
of truth. Comment-only - no runtime change.

Verified (verifier-lite): tsc scripts/postman/ grep empty, total 237;
24 providerValidationScope tests green; eslint clean.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
Area 45 -> 0; total 237 -> 192 (exactly -45, no second-order).
Comment-only JSDoc typing - no runtime change.

- RESOLVED_ENV_VALUE_BUILDERS: one @type
  Record<string, (input: { host, ports: Record<string, number> })
  => string | null> on the const contextually types all 12 builder
  arrows (23 TS7031s) and the [entry.key] lookup.
- runNewman: hoisted the inline options type into a RunNewmanOptions
  typedef with envPath required (always supplied by the wrapper;
  clears the (string|undefined)[] args TS2322); destructure default
  cast to it (TS2739); runHarnessNewman wrapper takes
  Partial<RunNewmanOptions>.
- @type annotations: fixedPorts Record<string, number>; parsed
  Newman env clone { values?: { key, value? }[] } & Record<string,
  any>; childDiagnosticLogs; authAppServer ChildProcess | undefined;
  the three provider-credential lets string | null | undefined
  (real-provider mode assigns process.env reads).
- Boundary casts: buildAppServerEnv results -> Record<string,
  string> at both spawnLogged sites; frozen LOW_COST scopes ->
  string[] (TS4104); signal-handler pair -> [string, () => void]
  tuple (TS2345 on process.off); catch (Error) cast at
  emitHarnessFailureDiagnostics.
- (value?: unknown) => void resolve hint clears the TS2810
  new Promise(); Uint8Array[] chunks accumulator (Buffer[] trips the
  TS6 Uint8Array<ArrayBufferLike> generic on Buffer.concat).

Verified (verifier-lite + direct): tsc harness grep empty, total
192; 8 runPostmanHarness tests green; eslint clean.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
Area 8 -> 0; total 192 -> 184 (exactly -8, no second-order).
Comment-only JSDoc typing - no runtime change.

run-postman-deploy.js:
- sleep: @param {number} + @returns {Promise<void>} (the return
  annotation contextually types the executor, so Promise<unknown>
  never leaks into waitForStableDeploy's sleepFn default).
- Frozen LOW_COST_PROVIDER_VALIDATION_SCOPES -> string[] cast
  (TS4104, same pattern as the harness).
- requestDeployProbe catch: error -> (Error) cast (TS2322 unknown).
- collectDeployStabilizationIssues input: the read jsonBody widened
  unknown -> any (optional-chained .code read on external JSON).
- JSDoc on the recursive attempt arrow's destructured input
  ({ attemptCount, consecutiveSuccesses, lastIssues }).

scripts/postman/live-provider-validation.js:
- ensureReportsDir's `= null` default inferred `null | undefined`,
  rejecting the `string | null` dir every caller passes; inline
  @type {string | null | undefined} on the param corrects the
  under-annotation at the source (clears run-postman-live's one
  error).

Verified (verifier-lite): tsc deploy/live/live-provider grep empty,
total 184; 34 tests across runPostmanDeploy + runPostmanLive green;
eslint clean on all 3 files.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
jsugg added 5 commits July 5, 2026 03:33
Area 29 -> 0; total 184 -> 156 (-29 here, +1 second-order in
scripts/perf/run-perf-smoke.js from the now-typed createFixtureApp -
folded into the upcoming perf batch). Comment-only JSDoc typing - no
runtime change.

- Express is `any` under the ambient shim: cast the app once to a
  local FixtureApp duck whose get/post handler signature
  (FixtureHandler over FixtureRequest/FixtureResponse) contextually
  types all 12 (req, res) pairs - same pattern as the src routers.
- createOpenAiCompatibleStubHandler returns FixtureHandler instead
  of the shim-less import('express').RequestHandler (TS2694).
- TS6 Buffer gotcha: the JSDoc-resolved Buffer type does not
  sufficiently overlap Uint8Array<ArrayBufferLike> (Buffer.compare's
  parameter type), so the three comparison fixtures
  (ASSET_{A,B,PROVIDER_FAILURE}_PNG) and the raw-body accumulator
  are typed Uint8Array once at their declarations via
  unknown-bridged double-casts; captionForBuffer takes
  @param {Uint8Array}.
- Replicate stub body param widened Record<string, unknown> ->
  Record<string, any> (optional-chained .input.image read on
  external JSON).

Verified (verifier-lite + direct): tsc fixture-server grep empty,
total 156; 4 postmanFixtureServer tests green; eslint clean.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
Area 45 -> 0; total 156 -> 111 (-45: 44 in the two perf files + the
+1 second-order in run-perf-smoke.js folded from the fixture-server
batch, now that createFixtureApp is typed). Comment-only JSDoc typing
plus one ambient shim - no runtime change.

- supertest ships no bundled types and no @types package is
  installed, so a `declare module 'supertest'` shim in
  types/supertest.d.ts (same approach as the express shim) clears
  the TS7016 and keeps the request/agent boundary `any` while local
  JSDoc types the callers.
- performanceBudget.js gains a PerformanceSample typedef (the input
  duck) and an EvaluatedSample typedef (the enriched result),
  threaded through evaluatePerformanceSample, summarizePerformanceReport,
  and formatSampleLine.
- run-perf-smoke.js: standard param annotations (toNumber unknown,
  resolveAcceptedBudgets string|undefined, median number[], the
  FastStubDescriber methods string). @returns {Promise<void>} on
  listen/closeServer contextually types their executors, clearing
  the no-arg resolve() TS2810 and the close-callback implicit-any
  error; reservePort @returns {Promise<number>} with server.address()
  cast to import('node:net').AddressInfo for the .port read.
- The createApp option bag is `any`, so requestLogger's (req, res,
  next) take inline @type {any} and the app-taking helpers
  (secureGet / settlePageDescription / measurePageFanOut /
  measureDocsSteadyState) take @param {any} app. The two
  this.constructor.buildResult calls cast this.constructor to
  typeof FastStubDescriber. The folded second-order is
  http.createServer(/** @type {any} */ (createFixtureApp(...))).

Verified: tsc perf grep empty, total 111; 8 performanceBudget tests
green; eslint clean.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
Area 42 -> 0; total 111 -> 69 (exactly -42, no second-order). The four
production-ref / audit CLIs. Comment-only JSDoc typing - no runtime
change.

Three of them (promote-to-production, rollback-production,
update-deployment-status) share one root cause: each parseArgs seeds a
partial `const args = { ...defaults }` and fills the rest in the arg
loop, but the literal's inferred type held only the seeded keys, so
every `args.repo = ...` write, every later read, and the `return args`
failed against the full @returns shape. Fix per file:

- Hoist the inline @returns object into a named typedef (PromoteArgs /
  RollbackArgs / DeploymentStatusArgs).
- Annotate the accumulator /** @type {Partial<T>} */ so the
  incremental writes are legal.
- Assert the return /** @type {T} */ (args) after the required-field
  guard already proves completeness - the guard enforced it at runtime
  before this change.

parse-security-audit-report.js was the same parseArgs shape (typedef
AuditArgs + Partial + return assert) plus missing @params (argv
string[], reportFile string, appendOutput's outputFile/key/value). Its
report parsing got an AuditReport / AuditVulnerabilities external-JSON
duck typedef so the parseError catch-branch property and the
vulnerabilities.{critical,high,low,moderate} reads resolve, and the two
catch (error) blocks narrow `error instanceof Error ? error.message :
...`.

Verified: tsc github grep empty, total 69; 35 tests across the four
suites green; eslint clean.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
Area 31 -> 0; total 69 -> 38 (exactly -31, no second-order). The manual
TLS-trust diagnostic CLI. Comment-only JSDoc typing plus two type-only
casts - no runtime change.

Every error was TS7006 implicit-any on a param; no structural issues.

- Added three duck typedefs: CertName (string | { CN?: string } -
  X509Certificate yields a string subject/issuer, the synthesized
  trust-anchor candidate uses a { CN } object), InspectedCertificate
  (the chain-item shape), and DoctorTlsArgs.
- parseArgs seeds targetUrl: undefined then assigns the positional arg,
  so the accumulator is annotated { ..., targetUrl: string | undefined }
  and the return asserted DoctorTlsArgs (targetUrl guaranteed by the
  usage guard) so main sees a plain string.
- splitPemCertificates typed @returns {string[]}, which contextually
  types the four downstream .map/.find/.forEach callback params
  (fingerprint/chain building) with no annotation of their own.
- probeUrl / inspectCertificateChain got param-only annotations: their
  new Promise return type is inferred from the resolved values, not the
  params, so main's probe-result and chain usage is unchanged.
- printChain's two defensive .CN fallbacks (dead for string subjects
  but needed for the { CN } form) read through a local
  /** @type {any} */ cast.

Verified: tsc doctor-tls grep empty, total 38; no test references this
manual CLI, so node --check + eslint clean.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
Areas 29 + 9 -> 0; total 38 -> 0. npm run typecheck now exits 0 across
src/, config/, and scripts/ - the ledger reaches zero. Comment-only
JSDoc typing plus type-only casts - no runtime change.

scripts/reporting (29 -> 0), the Allure history/metadata scripts:

- Three accumulators seeded with a subset of keys then extended with
  optional string props (environmentProperties, executor, manifest) are
  annotated with their Record<string, string> /
  Record<string, string | number> return type so the conditional writes
  are legal.
- resolve-allure-history-policy's two policy builders had stale
  camelCase @returns typedefs while the code actually returns snake_case
  GitHub-Actions output keys (history_artifact_name, ...); corrected
  both to @returns {Record<string, string>} (how toOutputLines and the
  outer resolver already consume them), clearing 8 TS2561s. The
  resolveAllureHistoryPolicy tests confirm the runtime snake_case keys
  are unchanged.
- Two required-prop option bags with = {} defaults cast the default
  = /** @type {T} */ ({}).
- restore: typed the inner collectPage accumulator to the rich
  GitHub-API artifact shape (matching listArtifactsForName's @returns -
  a narrow first guess triggered a second-order TS2322 at the
  selectNewestArtifact consumer), completed the options typedef with the
  three missing test-seam impl overrides (downloadArtifactArchiveImpl /
  extractArchiveImpl / listArtifactsForNameImpl - typing these properly
  is what surfaced the artifact-shape mismatch), and the TS6
  Buffer->ArrayBufferView gotcha at fs.writeFile reads through an
  unknown-bridged Uint8Array cast.
- fetch: result.content (flat, non-discriminated @returns so always
  string | undefined) cast string at the restored-branch write.

scripts/docs (9 -> 0), the Markdown validator: TS7006/TS7031
implicit-any params/binding-elements plus one unknown catch. @param
annotations (filePath/dirPath string, files string[], argv string[]), a
DocViolation typedef threaded through validateMarkdownFile's @returns and
writeReport's destructured { files, violations } param (which
contextually types the inner forEach violation), and the main catch
narrows error instanceof Error ? error.message : error.

Verified: tsc grep empty, total 0 (npm run typecheck exit 0); 43
reporting tests + 6 validateDocs tests green; eslint clean.

Claude-Session: https://claude.ai/code/session_01C2NqWYBoePEb1yP8PmcRT9
Comment thread scripts/reporting/restore-allure-history-from-artifact.js Fixed
@jsugg jsugg changed the title chore(typecheck): burn down advisory typecheck debt (1438 → 1372) chore(typecheck): burn down advisory typecheck debt to zero (1438 → 0) Jul 6, 2026
The Allure-history restore path wrote a downloaded artifact straight to
disk (`fs.writeFile(dest, Buffer.from(await res.arrayBuffer()))`). Those
bytes are untrusted network input, which CodeQL flags as
js/http-to-file-access ("network data written to file").

Add explicit validation before the write:

- cap the archive at 100 MiB, checked against both the Content-Length
  header (fail fast, pre-buffer) and the actual byte length, so a
  corrupted or hostile response cannot exhaust the disk;
- require the ZIP "PK" magic bytes so only a recognized archive format
  can reach the filesystem and the downstream unzip;
- reject empty payloads.

assertArchivePayload returns a real Uint8Array, which also drops the
unknown-bridge cast the write previously needed to type-check.

Covered by new unit tests for assertArchivePayload (valid/empty/
oversized/non-ZIP) and downloadArtifactArchive (writes a valid ZIP,
refuses oversized Content-Length, refuses non-ZIP).
}

const archiveBytes = assertArchivePayload(await archiveResponse.arrayBuffer());
await fs.writeFile(destinationPath, archiveBytes);
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