From 5a596f9c7227756b42c64badadf43a16d5fd49e6 Mon Sep 17 00:00:00 2001 From: Test Date: Sat, 18 Apr 2026 13:44:54 +0200 Subject: [PATCH] fix: wire a11y phase into pipeline (extraction was not processed) jsxElements were extracted by the parse worker but never passed through the rule engine or persisted as A11ySignal nodes. This commit: - adds allJsxElements accumulation in parse-impl and ParseOutput - creates a11y.ts pipeline phase that runs runA11yRules + processA11ySignals - registers a11yPhase in index.ts and pipeline.ts (after orm, before crossFile) Co-Authored-By: Claude Sonnet 4.6 --- .../core/ingestion/pipeline-phases/a11y.ts | 62 +++++++++++++++++++ .../core/ingestion/pipeline-phases/index.ts | 1 + .../ingestion/pipeline-phases/parse-impl.ts | 7 +++ .../core/ingestion/pipeline-phases/parse.ts | 2 + gitnexus/src/core/ingestion/pipeline.ts | 2 + 5 files changed, 74 insertions(+) create mode 100644 gitnexus/src/core/ingestion/pipeline-phases/a11y.ts diff --git a/gitnexus/src/core/ingestion/pipeline-phases/a11y.ts b/gitnexus/src/core/ingestion/pipeline-phases/a11y.ts new file mode 100644 index 000000000..a8fbe471c --- /dev/null +++ b/gitnexus/src/core/ingestion/pipeline-phases/a11y.ts @@ -0,0 +1,62 @@ +/** + * Phase: a11y + * + * Runs WCAG rule engine over JSX elements extracted during parse and + * creates A11ySignal nodes + HAS_A11Y_SIGNAL edges in the graph. + * + * @deps parse + * @reads allJsxElements (from parse) + * @writes graph (A11ySignal nodes, HAS_A11Y_SIGNAL edges) + */ + +import type { PipelinePhase, PipelineContext, PhaseResult } from './types.js'; +import { getPhaseOutput } from './types.js'; +import type { ParseOutput } from './parse.js'; +import { runA11yRules } from '../a11y-rules/index.js'; +import { processA11ySignals } from '../a11y-processor.js'; + +export interface A11yOutput { + signalsCreated: number; + edgesCreated: number; + violations: number; + needsReview: number; +} + +export const a11yPhase: PipelinePhase = { + name: 'a11y', + deps: ['parse'], + + async execute( + ctx: PipelineContext, + deps: ReadonlyMap>, + ): Promise { + const { allJsxElements } = getPhaseOutput(deps, 'parse'); + + if (allJsxElements.length === 0) { + return { signalsCreated: 0, edgesCreated: 0, violations: 0, needsReview: 0 }; + } + + // Group elements by file so each rule gets the correct filePath context + const byFile = new Map(); + for (const el of allJsxElements) { + const bucket = byFile.get(el.filePath); + if (bucket) { + bucket.push(el); + } else { + byFile.set(el.filePath, [el]); + } + } + + const allSignals = []; + for (const [filePath, elements] of byFile) { + const signals = runA11yRules(elements, filePath); + for (const s of signals) allSignals.push(s); + } + + if (allSignals.length === 0) { + return { signalsCreated: 0, edgesCreated: 0, violations: 0, needsReview: 0 }; + } + + return processA11ySignals(allSignals, ctx.graph); + }, +}; diff --git a/gitnexus/src/core/ingestion/pipeline-phases/index.ts b/gitnexus/src/core/ingestion/pipeline-phases/index.ts index c05264de1..9a15700d5 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/index.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/index.ts @@ -15,6 +15,7 @@ export { parsePhase, type ParseOutput } from './parse.js'; export { routesPhase, type RoutesOutput, type RouteEntry } from './routes.js'; export { toolsPhase, type ToolsOutput, type ToolDef } from './tools.js'; export { ormPhase, type ORMOutput } from './orm.js'; +export { a11yPhase, type A11yOutput } from './a11y.js'; export { crossFilePhase, type CrossFileOutput } from './cross-file.js'; export { mroPhase, type MROOutput } from './mro.js'; export { communitiesPhase, type CommunitiesOutput } from './communities.js'; diff --git a/gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts b/gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts index f8e0d3b53..7322ea54a 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts @@ -57,6 +57,7 @@ import type { ExtractedToolDef, FileConstructorBindings, } from '../workers/parse-worker.js'; +import type { ExtractedJSXElement } from '../a11y-rules/types.js'; import type { ExtractedHeritage } from '../model/heritage-map.js'; import type { KnowledgeGraph } from '../../graph/types.js'; import type { PipelineOptions } from '../pipeline.js'; @@ -106,6 +107,7 @@ export async function runChunkedParseAndResolve( allDecoratorRoutes: ExtractedDecoratorRoute[]; allToolDefs: ExtractedToolDef[]; allORMQueries: ExtractedORMQuery[]; + allJsxElements: ExtractedJSXElement[]; bindingAccumulator: BindingAccumulator; resolutionContext: ReturnType; usedWorkerPool: boolean; @@ -248,6 +250,7 @@ export async function runChunkedParseAndResolve( const allDecoratorRoutes: ExtractedDecoratorRoute[] = []; const allToolDefs: ExtractedToolDef[] = []; const allORMQueries: ExtractedORMQuery[] = []; + const allJsxElements: ExtractedJSXElement[] = []; const deferredWorkerCalls: ExtractedCall[] = []; const deferredWorkerHeritage: ExtractedHeritage[] = []; const deferredConstructorBindings: FileConstructorBindings[] = []; @@ -393,6 +396,9 @@ export async function runChunkedParseAndResolve( if (chunkWorkerData.ormQueries?.length) { for (const item of chunkWorkerData.ormQueries) allORMQueries.push(item); } + if (chunkWorkerData.jsxElements?.length) { + for (const item of chunkWorkerData.jsxElements) allJsxElements.push(item); + } } else { await processImports(graph, chunkFiles, astCache, ctx, undefined, repoPath, allPaths); sequentialChunkPaths.push(chunkPaths); @@ -589,6 +595,7 @@ export async function runChunkedParseAndResolve( allDecoratorRoutes, allToolDefs, allORMQueries, + allJsxElements, bindingAccumulator, resolutionContext: ctx, // Whether a worker pool was actually live for this run. False means the diff --git a/gitnexus/src/core/ingestion/pipeline-phases/parse.ts b/gitnexus/src/core/ingestion/pipeline-phases/parse.ts index 6415cb6e5..4c31c153b 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/parse.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/parse.ts @@ -27,6 +27,7 @@ import type { ExtractedToolDef, ExtractedORMQuery, } from '../workers/parse-worker.js'; +import type { ExtractedJSXElement } from '../a11y-rules/types.js'; import type { createResolutionContext } from '../model/resolution-context.js'; import { runChunkedParseAndResolve } from './parse-impl.js'; @@ -47,6 +48,7 @@ export interface ParseOutput { readonly allDecoratorRoutes: readonly ExtractedDecoratorRoute[]; readonly allToolDefs: readonly ExtractedToolDef[]; readonly allORMQueries: readonly ExtractedORMQuery[]; + readonly allJsxElements: readonly ExtractedJSXElement[]; bindingAccumulator: BindingAccumulator; /** Resolution context from the parse phase — carries importMap, namedImportMap, etc. */ resolutionContext: ReturnType; diff --git a/gitnexus/src/core/ingestion/pipeline.ts b/gitnexus/src/core/ingestion/pipeline.ts index e1f226289..491c98972 100644 --- a/gitnexus/src/core/ingestion/pipeline.ts +++ b/gitnexus/src/core/ingestion/pipeline.ts @@ -29,6 +29,7 @@ import { routesPhase, toolsPhase, ormPhase, + a11yPhase, crossFilePhase, mroPhase, communitiesPhase, @@ -79,6 +80,7 @@ function buildPhaseList(options?: PipelineOptions): PipelinePhase[] { routesPhase, toolsPhase, ormPhase, + a11yPhase, crossFilePhase, ];