diff --git a/src/build/neo4j/project.ts b/src/build/neo4j/project.ts index a779c6d..ac36f77 100644 --- a/src/build/neo4j/project.ts +++ b/src/build/neo4j/project.ts @@ -233,10 +233,10 @@ function projectField(b: RowBuilder, f: TSField, owner: NodeRef, fileKey: string // ---------------------------------------------------------------------------------------------- function moduleProps(mod: TSModule, fileKey: string): Props { - // The wire strips the internal fields (module_name, content_hash) — the graph name is the - // file key, exactly as the historical projection of the stripped tree produced. + // `name` is the file key. `content_hash` is what the incremental push diffs against on the next + // run (bolt.ts) -- schema.ts has always declared it on :TSModule, but nothing wrote it (#118). return prune({ - id: mod.id, kind: "module", name: fileKey, + id: mod.id, kind: "module", name: fileKey, content_hash: mod.content_hash ?? null, is_tsx: mod.is_tsx, is_declaration_file: mod.is_declaration_file, ...span(mod), _module: fileKey, }); diff --git a/src/schema/emit.ts b/src/schema/emit.ts index 6a38883..dc35091 100644 --- a/src/schema/emit.ts +++ b/src/schema/emit.ts @@ -58,7 +58,10 @@ function stripInternal(root: TSApplication): void { for (const nt of Object.values((t["types"] as Record>) ?? {})) stripType(nt); }; for (const mod of Object.values(root.symbol_table) as unknown as Record[]) { - delete mod["content_hash"]; + // `content_hash` STAYS on the wire (#118). The Neo4j projection is built from this envelope + // and writes it onto :TSModule, where the incremental push reads it back to find changed + // modules -- stripping it here made that diff compare against NULL forever, so every push was + // a full re-upsert. codeanalyzer-python keeps it for the same reason (schema/py_schema.py). delete mod["last_modified"]; delete mod["file_size"]; for (const fn of Object.values((mod["functions"] as Record>) ?? {})) stripCallable(fn); diff --git a/test/neo4j-schema.test.ts b/test/neo4j-schema.test.ts index 8872c94..5aed008 100644 --- a/test/neo4j-schema.test.ts +++ b/test/neo4j-schema.test.ts @@ -122,6 +122,22 @@ describe("neo4j schema conformance", () => { // The JSON envelope advertises `analyzer{name,version}` (#29); the Neo4j :Application node is the // co-primary projection of the same envelope and must not diverge on analyzer identity. +// #118: the incremental push diffs each module's stored `content_hash` to find what changed. The +// field was stripped from the wire and never projected, so the diff compared against NULL forever +// and every push was a full re-upsert. schema.ts declared the property, bolt.ts read it, and +// project.ts never wrote it -- three parts of the system disagreeing. The old bolt test seeded the +// value by hand, so it exercised the diff against data the projection could not produce. +describe("module content_hash reaches the graph (#118)", () => { + test("every projected :TSModule carries a non-null content_hash", () => { + const modules = rows.nodes.filter((n) => n.labels.includes("TSModule")); + expect(modules.length).toBeGreaterThan(0); + for (const m of modules) { + expect(m.props["content_hash"], `no content_hash on ${m.value}`).toBeDefined(); + expect(typeof m.props["content_hash"]).toBe("string"); + } + }); +}); + describe(":Application node carries analyzer identity (issue #43)", () => { test("version matches package.json (the same source the JSON envelope's analyzer.version uses)", () => { const appNode = rows.nodes.find((n) => n.labels.includes("Application"));