gh-security: unused-code triage & GitHub security visibility (2026-06-21)
Re-filed S504 (ruling R9), from reference/: dated triage (2026-06-21), point-in-time.
import { Aside } from “@astrojs/starlight/components”;
1. GitHub security visibility (unsandboxed gh, scope repo)
Section titled “1. GitHub security visibility (unsandboxed gh, scope repo)”| Surface | State | API-visible? |
|---|---|---|
| Code scanning — CodeQL security | 2 open, 18 fixed | yes (/code-scanning/alerts) |
| Dependabot | 0 open | yes |
| Secret scanning | 0 open | yes |
| CodeQL code-quality (maintainability/reliability) + AI = the 128/23/10 | as seen in UI | NO — dashboard-only |
- Default setup =
query_suite: default,threat_model: remote→ security queries only; every REST alert (all states) is a security rule, zero quality rules. - 2 open:
js/incomplete-sanitization(lib/supabase/escape.ts),js/incomplete-multi-character-sanitization(a test file). - “Security wave” PRs #18–#43 were CodeQL Autofix; #43 reverted the breakages.
- Code Quality = separate product, public preview now, GA 2026‑07‑20 (paid). Findings live only in the “Code quality” tab (Standard + AI sections), not the REST API. Enable via repo Settings → Security → Code quality (UI only; needs Actions; consumes Actions minutes).
- Local tooling under-reports the maintainability axis: repo eslint finds 4 unused-vars total (sets
no-unused-vars: warn, ignoresscripts/**+supabase/**, skips_-prefixed). knip tracks unused exports (different axis). So the 70-imports/21-vars are not reproducible from current local config without tightening it.
2. Triage of 184 knip-flagged unused exports/types
Section titled “2. Triage of 184 knip-flagged unused exports/types”124 non-vendored symbols classified by a 31-agent fan-out (+60 auto-kept: shadcn/ui, tools/ast-dataflow type-barrel — relocated from lib/ast-dataflow in S393 bl-360).
| Class | Count |
|---|---|
| KEEP_API (live, reached structurally) | 45 |
| BUILT_NOT_WIRED (finished, no consumer yet) | 65 |
| DEAD (genuinely unused) | 14 |
⚠️ MANDATORY pre-pivot second pass
Section titled “⚠️ MANDATORY pre-pivot second pass”The classifier anchored “built-not-wired” on in-code task IDs, some of which may pre-date the procurement/sector-intelligence pivot and be superseded, not deferred. Before wiring or backlogging any cluster, check each task ID against the live roadmap / state-of-the-product: on-roadmap → keep; superseded → reclassify DEAD.
Built-not-wired clusters (each needs the pre-pivot check)
Section titled “Built-not-wired clusters (each needs the pre-pivot check)”| Cluster | n | In-code anchor | Pre-pivot risk |
|---|---|---|---|
A — API response-validation rollout (*ResponseSchema + defineRoute) | 52 | OPS-T1 R-WP21 | validate — large parked batch |
B — MCP union output schemas (lib/mcp/formatters/search) | 5 | B-INV-37/M37 | low — externally blocked (SDK z.union) |
C — feed flag-analyser + starter-packs (lib/intelligence) | 4 | S155 WP3 | validate — pivot-sensitive |
D — edit-intent write-back (lib/edit-intent) | 2 | ID-59 {59.8/9/13} | low — Task 59 active |
E — Q&A corpus promotion (lib/q-a-pairs) | 2 | Task 59 {59.22–25} | low — hot, landing now |
F — dedup gate (lib/dedup/content-dedup.ts) | 2 | OPS-3/s183 | validate — if cancelled → DEAD |
G — workspaces icons/badge (components/workspace) | 4 | T4 (TODO(T4)) | validate |
H — company-profile detail page (useCompanyProfile) | 1 | none (route exists) | decision: build page or delete hook+route |
| I — drafting / cost / quality-actions | 3 | none (“Phase 3”) | highest rot risk — no owner |
J — pipeline-run polling (lib/query/fetchers) | 1 | none | low — wire or delete pair |
3. Shipped (PR #44)
Section titled “3. Shipped (PR #44)”DELETED (0 refs, tsc-green): getContentTypeIcon, LAYER_VALUES, hasEditorialNotes(+EDITORIAL_NOTE_PATTERNS). DEMOTED (dropped export, used in-file): requestContextStorage, HEADING_PATTERNS, HTML_CONTENT_TYPES, hasGarbledKeywords, contentTypeSortKey, UploadFileSuggestedLayer. knip exports 111→103, types 73→72; .knip-baseline.json re-based. Verified: tsc green · 56 tests · detect_changes risk=low/0 processes.
EXCLUDED (deferred): extractFromUrl ({112.13}), computeGapSummary (P1-29), the HighlightCategory/CitationsData type-chains (knip flags one member but sibling Transcript*/ProcurementResponseMetadata are exported+0-ref yet unflagged — needs knip.config types/ review), coverage-guide-card (NOT orphaned — CoverageGuideCard live via coverage-guide-tab ← coverage-tabs.tsx; agent over-reached, caught by tsc).
4. S387 follow-up resolutions (pre-pivot 2nd pass + H/I/J + type-chain + CodeQL quality)
Section titled “4. S387 follow-up resolutions (pre-pivot 2nd pass + H/I/J + type-chain + CodeQL quality)”Executed the S384 plan (session ran as “S387”). 4 commits on gh-security, pushed + tagged s387-knip-cleanup; full suite green (13948 pass / 0 fail).
| Commit | Thread | What |
|---|---|---|
5d71f7d1 | 1+4 | knip cleanup, 9 symbols (2 deleted, 7 demoted); exports 103→100, types 74→68 |
9187b304 | 3-I | retired dead quality-actions module (+test) + formatCostUSD; types 68→66 |
6f2d8bb0 | 3-H | built app/intelligence/profiles/[id] detail/edit page; wired useCompanyProfile; exports 100→99 |
93d8cec2 | 2 | fixed CodeQL smart-quote regex bug (degraded to ASCII dupes) |
Cluster dispositions (pre-pivot 2nd pass)
Section titled “Cluster dispositions (pre-pivot 2nd pass)”| Cluster | Verdict | Action |
|---|---|---|
| C intelligence-feed (S155 WP3) | KEEP_DEFERRED — on-roadmap | none; 4 zero-ref contract types stay deferred |
| F dedup (OPS-3/s183) | triage WAS WRONG → keep-live | dedup is SHIPPED; every export live via dynamic await import('@/lib/dedup'). Demoted 1 internal const (DEDUP_MIN_CONTENT_LENGTH) |
| G workspace (T4) | MIXED | DELETED 2 true orphans (WorkspaceBadge, getWorkspaceIcon); kept-live WorkspaceSelector; 8-symbol T4 island kept-deferred → new backlog item (keep-vs-retire pending T4 redesign vs per-type-launcher pivot) |
| H company-profile | BUILT | detail/edit page; wired orphaned useCompanyProfile |
| I drafting/cost/quality-actions | RETIRED (partial) | deleted quality-actions.ts+test + formatCostUSD; estimateQuestionCost + non-streaming draftResponse are LIVE — kept (2 more over-reaches) |
| J pipeline-run polling | DEFER (not wire, not retire) | IS the cocoindex single-run progress poller but host-less (EP2 markdown-batch UI removed). Reuse fetchPipelineRun when non-blocking /api/upload is built → ID-56 {56.12} |
type-chain (content.ts/procurement-metadata.ts) | premise false (rg false-zero) | demoted HighlightCategory/CitationsData + cluster-E Skip*/PromotionFailure* in-file; no knip.config change |
⚠️ Meta-finding — static-analysis over-reach (5 caught this session)
Section titled “⚠️ Meta-finding — static-analysis over-reach (5 caught this session)”The static-0-ref signal produced 5 false “dead” calls, all caught by adversarial re-validation before deletion: coverage-guide-card + WorkspaceSelector (live via JSX chains), cluster-F dedup entirely (live via dynamic await import()), estimateQuestionCost + draftResponse (live via indirect callers). Root cause: static analysis is blind to (1) dynamic imports, (2) JSX render-chains, (3) sandboxed-rg false-zeros on read-denied files. Trust tsc (includes test files) + read the actual call sites before any deletion. Validates the mandatory 2nd pass.
Thread 2 — CodeQL Code Quality reproduced locally
Section titled “Thread 2 — CodeQL Code Quality reproduced locally”Dashboard-only findings reproduced via CodeQL CLI → SARIF (javascript-code-quality-extended.qls): 128 maintainability = EXACT match (note-level); 8/23 reliability (GitHub folds extra server-side queries); 10 “AI” not CLI-reproducible. 119/136 cosmetic; no empty-catch findings (silent-failure policing holds). 1 genuine bug fixed (smart-quote regex). Outstanding (S387): the eslint-tightening decision (drop scripts/+supabase/ ignores, add unused-imports, raise to error) to make these locally enforceable. Resolved S391 — see §5.
5. S391 closeouts — clusters G + J retired, eslint-tightening adopted
Section titled “5. S391 closeouts — clusters G + J retired, eslint-tightening adopted”S391 (theme-14 / ID-121 stream-2, merged via PR #47 → main on 22/06/2026, branch s391-security-closeouts) lands the three remaining decisions from §4 — both deferred clusters and the outstanding recurrence-prevention layer. The CodeQL classification work itself does not change; this section records the dispositions and the ledger movement.
| Commit | Decision | Outcome |
|---|---|---|
a58879f5 + 9bb4fd88 | Cluster G — T4 workspace island RETIRED | 8 symbols / 4 src + 2 test files / −1043 LOC. Removed: WorkspaceCreateDialog, WorkspaceCard, WorkspaceWithCounts, WorkspaceColourPicker, WorkspaceIconPicker, WORKSPACE_ICONS, WorkspaceIconName, ICON_MAP. The live /workspaces route renders the per-type launcher (useLauncherTypes); WorkspaceSelector is kept (live via organise-section). Closes bl-351. |
37ebe7fa | Cluster J — item-J pipeline-run poller RETIRED | Removed GET /api/pipeline-runs/[id] + its test, fetchPipelineRun, and pipelineRuns.detail. ID-56 {56.12} shipped a separate folder-drop status route, so the generic poller never gained a consumer. The pipeline_runs table + server-side writes are preserved. Carries a note on bl-61 (EP3 retrofit) that the retired endpoint must not be assumed by the EP3 progress UI — rebuild against the folder-drop status pattern instead. |
2d0e7b81 | Thread 2 outstanding — eslint unused-imports gate ADOPTED | Un-ignore scripts/ + supabase/; promote unused-imports/no-unused-imports + unused-imports/no-unused-vars to error (^_ exempt). 13 imports auto-fixed, 6 vars/args handled, 5 scope-creep sites inline-disabled with reasons. The 18 unused-code findings are now locally enforceable; the recurrence-prevention layer is in place. |
knip impact. Exports 99 → 97 (a DROP — never trips the baseline gate per /runbooks/ci/ §6, which fails only on EXCEED). .knip-baseline.json is safe to leave at 99 or rebase opportunistically to 97 per §6.3.
Hosted security alerts. 3 → 0 open at HEAD (the three false-positive / by-design findings dismissed with documented reasons via PATCH …/code-scanning/alerts/{n}). Verified per the ID-121 {121.1} RESEARCH §8 audit trail.
What remains for ID-121. The genuine-fix core from §5 (above — remote-property-injection ×4, the 2 useless-comparison + 1 unreachable + 1 unused-state warnings, 22 commented-out blocks), the cosmetic bucket (110 note-level — return-style + TODOs, staff-engineer assessment in hand), and the AI-suggestions / posture waves. Tracked as ID-121 resolution waves A–E in /specs/id-121-security-quality-baseline/plan/.