fix(rees): coerce non-string values in safeCodeSpan/promptText#3441
Conversation
safeCodeSpan and promptText are typed (value: string): string but sit at the analyzer-output boundary, receiving findings routed through `as never` casts (registry.ts render()) rather than values this module controls. The terminology descriptor's render template passes item.term/item.suggestion directly with no template-literal coercion, so a non-string term or suggestion reaching either helper threw `value.replace is not a function` straight out of the brief pipeline (GITTENSORY-15). Defensively coerce in both helpers, mirroring bytesLabel's existing `value: number | null` guard in the same file -- the established pattern for a render helper that must never throw into the pipeline.
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
Tip 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Gittensory review result - approve/merge recommendedReview updated: 2026-07-05 07:11:28 UTC
✅ Suggested Action - Approve/Merge
Review summary Nits — 4 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
Summary
TypeError: value.replace is not a functionatreview-enrichment/src/render-helpers.ts:29insafeCodeSpan, hit viaPOST /v1/enrich, tracing through the "terminology" analyzer descriptor's render template intorenderDescriptorSection→renderBrief→buildBrief→server.ts.scanTerminology/detectTerminologyare provably pure (term/suggestioncome only from a fixed, fully-typed lookup table — no config injection point exists today that could produce a non-string). The real gap is thatsafeCodeSpan/promptTextare typed(value: string): stringbut call.replace()directly with zero guard, despite receiving values fromrenderDescriptorSection→renderer(result as never, ...)— anas never/as unknownboundary that trusts loosely-typed analyzer/descriptor output. 54 call sites acrossrender.ts/registry.tspass values through this same unguarded path. The sibling helperbytesLabel(value: number | null)in the same file already narrows its signature for exactly this kind of less-trusted input —safeCodeSpan/promptTextwere the odd ones out.asRenderableStringcoercion and applied it inside bothsafeCodeSpanandpromptText, mirroringbytesLabel's existing defensive style. No blanket try/catch was added around the render pipeline — the actual unguarded boundary is fixed directly, for all 54 call sites at once, without masking any other error class.Scope
type(scope): short summaryConventional Commit format, for examplefix(api): restore profile access checks.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run build(review-enrichment's own typecheck-equivalent — clean)node --test --experimental-strip-types "test/**/*.test.ts"(run fromreview-enrichment/) — 993/993 passing, including the 6 new regression testsnode scripts/generate-analyzer-metadata.mjs --check— cleannpm run validate:sourcemaps(review-enrichment) — 58 source maps validatednpm run typecheck/test:workers/build:mcp/test:mcp-pack/ui:openapi:check/ui:build— not applicable;review-enrichment/is a standalone sub-package with its owntsconfig.jsonand test runner, not included in the root tsconfig'sincludelist, and this change doesn't touch the Worker/MCP/UI/OpenAPI surface.undefined,null, number, array, object-with-toString) for both helpers, and an end-to-endrenderBrieftest reproducing the exact Sentry stack trace (a malformed terminology finding withterm: undefined, suggestion: 42through the real "terminology" descriptor render path) that no longer throws.Safety
/v1/enrichrequest/response shape is unchanged; this only prevents a crash on malformed internal render input).UI Evidencesection below. — N/A, no visible UI change.Notes