From a34469972b6cfb9dedf24b4931f3286af10edcdb Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Tue, 7 Jul 2026 00:19:45 -0400 Subject: [PATCH] fix: emit real byte offsets on L3 body nodes so span.bytes is sliceable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GraphNode carried only line/column, so spanOf() in schema/v2/dataflow.ts hardcoded bytes:[0,0] for every L3 entry/exit/param/statement node — module.source.slice(...) on any L3 body node returned "". Add start_offset/end_offset (UTF-16 char offsets, matching the L1 convention) to GraphNode, populate them at the single construction site (dataflow/extract.ts's emitNode, from the owning ts-morph node's getStart()/getEnd() — the whole callable for entry/exit, the param decl for param nodes), and have spanOf() use them. --- src/dataflow/extract.ts | 11 ++++++++++- src/schema/graphs.ts | 3 +++ src/schema/v2/dataflow.ts | Bin 12941 -> 12965 bytes test/schema-v2.test.ts | 10 ++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/dataflow/extract.ts b/src/dataflow/extract.ts index 751ad53..d9a02f9 100644 --- a/src/dataflow/extract.ts +++ b/src/dataflow/extract.ts @@ -81,7 +81,16 @@ function emitNode(id: number, kind: GraphNode["kind"], ast: Node | null, build: const target = ast ?? build.fn; // ENTRY/EXIT carry the whole callable's span const s = build.sf.getLineAndColumnAtPos(target.getStart()); const e = build.sf.getLineAndColumnAtPos(target.getEnd()); - return { id, kind, start_line: s.line, start_column: s.column, end_line: e.line, end_column: e.column }; + return { + id, + kind, + start_line: s.line, + start_column: s.column, + end_line: e.line, + end_column: e.column, + start_offset: target.getStart(), + end_offset: target.getEnd(), + }; } function hasRestParam(build: FunctionCfgBuild): boolean { diff --git a/src/schema/graphs.ts b/src/schema/graphs.ts index 801eb5c..27e02a8 100644 --- a/src/schema/graphs.ts +++ b/src/schema/graphs.ts @@ -34,6 +34,9 @@ export interface GraphNode { start_column: number; end_line: number; end_column: number; + /** UTF-16 char offsets into the owning module's `source` (same convention as L1 `span.bytes`). */ + start_offset: number; + end_offset: number; } // ---------------------------------------------------------------------------------------------- diff --git a/src/schema/v2/dataflow.ts b/src/schema/v2/dataflow.ts index e5f7a22a36d99541326dbe109c7e1b26dee5c1ec..f664df9af9dda26d80c20595473825f4074f5713 100644 GIT binary patch delta 41 rcmeB8U7EVVhfgL?uec51fXw(RW diff --git a/test/schema-v2.test.ts b/test/schema-v2.test.ts index ada1509..a33584e 100644 --- a/test/schema-v2.test.ts +++ b/test/schema-v2.test.ts @@ -344,6 +344,16 @@ describe("schema v2 — L3 intraprocedural dataflow", () => { test("no dangling endpoints at L3", () => { expect(danglingCount(dfL3)).toBe(0); }); + + test("a sampled L3 statement node is source-sliceable (span.bytes reproduces its text)", () => { + const mod = dfL3.application.symbol_table["src/flow.ts"]; + const classify = mod.functions.classify as V2Callable; + const stmt = classify.body["4:3"]; + expect(stmt?.kind).toBe("statement"); + const [s, e] = (stmt as { span: { bytes: [number, number] } }).span.bytes; + expect(e).toBeGreaterThan(s); + expect(mod.source.slice(s, e)).toBe('let label = "none";'); + }); }); describe("schema v2 — L4 interprocedural SDG", () => {