You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2026/07/15 12:25:40 UTC
(tinkerpop) branch master updated: Grade tinker-review changes as NONE/FORMATTING/BEHAVIORAL/STRUCTURAL
This is an automated email from the ASF dual-hosted git repository.
spmallette pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/master by this push:
new 10a2306de6 Grade tinker-review changes as NONE/FORMATTING/BEHAVIORAL/STRUCTURAL
10a2306de6 is described below
commit 10a2306de6e29db6d56d219e1cb8bf221410e901
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Wed Jul 15 08:22:54 2026 -0400
Grade tinker-review changes as NONE/FORMATTING/BEHAVIORAL/STRUCTURAL
Replace the boolean `changed` flag with a `changeLevel` graded by diffing each
changed file's base version against PR-head. An untouched helper in a changed
file now grades NONE instead of polluting the risk checks; centrality,
blast-radius, and coverage-gaps default to the meaningful tiers (BEHAVIORAL,
STRUCTURAL) with an opt-in minChangeLevel. listFunctions gains --changeLevel;
--changed stays as an alias for "not NONE".
Assisted-by: Claude Code:claude-opus-4-8
---
.skills/tinker-review/references/enrichment-cli.md | 6 +-
.skills/tinker-review/references/interfaces.md | 38 +++--
.skills/tinker-review/references/schema.md | 52 ++++--
.skills/tinker-review/scripts/enrichment/api.js | 42 +++--
.skills/tinker-review/scripts/enrichment/cli.js | 4 +-
.../scripts/extraction/tree-sitter.js | 156 ++++++++++++++++--
.../tinker-review/scripts/graph/change-levels.js | 177 +++++++++++++++++++++
.skills/tinker-review/scripts/graph/populate.js | Bin 15295 -> 15392 bytes
.skills/tinker-review/scripts/graph/references.js | 2 +-
.../tinker-review/scripts/patterns/architecture.js | 14 +-
.../tinker-review/scripts/patterns/blast-radius.js | 27 +++-
.../tinker-review/scripts/patterns/centrality.js | 25 ++-
.../scripts/patterns/cluster-analysis.js | 4 +-
.../scripts/patterns/community-detection.js | 8 +-
.../scripts/patterns/coverage-gaps.js | 8 +-
.skills/tinker-review/scripts/patterns/orphans.js | 6 +-
.skills/tinker-review/scripts/renderer/render.js | 16 +-
.../tinker-review/scripts/renderer/template.html | 1 +
.skills/tinker-review/scripts/review.js | 29 +++-
.skills/tinker-review/test/extraction.test.js | 20 +--
.skills/tinker-review/test/fingerprint.test.js | 136 ++++++++++++++++
21 files changed, 671 insertions(+), 100 deletions(-)
diff --git a/.skills/tinker-review/references/enrichment-cli.md b/.skills/tinker-review/references/enrichment-cli.md
index 044108451e..e42247c603 100644
--- a/.skills/tinker-review/references/enrichment-cli.md
+++ b/.skills/tinker-review/references/enrichment-cli.md
@@ -55,8 +55,10 @@ before enriching and to pull the verification worklist.
### listFunctions
Lists functions the graph knows about. **Your first orientation read.**
-`--changed true` narrows to just what the PR touched; `--visibility public`
-narrows to the API surface. Returns each function's signature and line span so
+`--changeLevel STRUCTURAL` narrows to signature-level changes (also `BEHAVIORAL`,
+`FORMATTING`, `NONE`); `--changed true` is a back-compat alias for "anything but
+NONE" (and `--changed false` for NONE only); `--visibility public` narrows to the
+API surface. Each result carries its `changeLevel`, signature, and line span so
you can open it in the worktree. Reach for it at the top of almost any playbook.
### listTypes
diff --git a/.skills/tinker-review/references/interfaces.md b/.skills/tinker-review/references/interfaces.md
index 93f290c542..282e06adf1 100644
--- a/.skills/tinker-review/references/interfaces.md
+++ b/.skills/tinker-review/references/interfaces.md
@@ -21,20 +21,24 @@ interface ExtractionResult {
imports: ImportInfo[];
}
+// How much the PR moved a vertex, graded by diffing base against PR-head.
+// Vocabulary/helpers: scripts/graph/change-levels.js.
+type ChangeLevel = "NONE" | "FORMATTING" | "BEHAVIORAL" | "STRUCTURAL";
+
interface FileInfo {
- path: string; // relative to worktree root
- language: string; // e.g., "dart", "java", "go"
- changed: boolean; // true if modified in this PR
+ path: string; // relative to worktree root
+ language: string; // e.g., "dart", "java", "go"
+ changeLevel: ChangeLevel; // rollup of the file's members + import/export delta
}
interface FunctionInfo {
name: string;
- signature: string; // full signature as string
+ signature: string; // full signature as string
visibility: "public" | "private" | "protected" | "internal";
- filePath: string; // which file this lives in
+ filePath: string; // which file this lives in
linesStart: number;
linesEnd: number;
- changed: boolean; // true if modified in this PR
+ changeLevel: ChangeLevel; // graded per function (untouched helper = NONE)
}
interface TypeInfo {
@@ -42,6 +46,7 @@ interface TypeInfo {
kind: "class" | "interface" | "struct" | "enum";
visibility: "public" | "private" | "protected" | "internal";
filePath: string;
+ changeLevel: ChangeLevel; // graded over the type's declaration surface
}
interface CallInfo {
@@ -159,6 +164,9 @@ interface ReportPackage extends Evidence {
* @param {string} language - Primary language to parse (e.g., "dart")
* @param {object} options
* @param {string[]} [options.changedFiles] - List of files changed in PR (relative paths)
+ * @param {Object<string,string>} [options.baseContents] - Base-version source of
+ * each changed file, keyed by path, so each member's `changeLevel` can be graded
+ * by diffing base against head. A missing entry = a file the PR added (STRUCTURAL).
* @returns {Promise<ExtractionResult>}
*/
export async function extract(directory, language, options = {}) {}
@@ -167,8 +175,8 @@ export async function extract(directory, language, options = {}) {}
**Responsibilities:**
- Load the appropriate tree-sitter grammar for the language
- Walk the directory, parse each source file
-- Run queries to extract functions, types, call sites, imports
-- Mark `changed: true` on files/functions that appear in `options.changedFiles`
+- Grade each file/function/type's `changeLevel` against `options.baseContents`
+ (context / hierarchy-neighborhood files, absent from `changedFiles`, grade `NONE`)
- Return the structured `ExtractionResult`
**Does NOT:**
@@ -232,9 +240,9 @@ export async function populate(g, extraction) {}
| Extraction data | Graph vertex | Key properties |
|---|---|---|
-| `files[]` | `File` | path, language, changed |
-| `functions[]` | `Function` | name, signature, visibility, lines_start, lines_end, changed |
-| `types[]` | `Type` | name, kind, visibility |
+| `files[]` | `File` | path, language, changeLevel |
+| `functions[]` | `Function` | name, signature, visibility, lines_start, lines_end, changeLevel |
+| `types[]` | `Type` | name, kind, visibility, changeLevel |
| Extraction data | Graph edge | From → To |
|---|---|---|
@@ -283,7 +291,8 @@ export async function completeness(g, params) {}
*
* @param {object} g - gremlin-js GraphTraversalSource
* @param {object} params
- * @param {boolean} [params.changedOnly] - Only check functions with changed=true (default: true)
+ * @param {boolean} [params.changedOnly] - Only check meaningfully-changed functions
+ * — BEHAVIORAL or STRUCTURAL (default: true)
* @returns {Promise<CoverageGapResult>}
*/
export async function coverageGaps(g, params = {}) {}
@@ -337,10 +346,11 @@ export async function review(params) {}
**Orchestration steps:**
1. `git fetch origin pull/${pr}/head:pr-review/${pr}`
2. `git worktree add /tmp/pr-review-${pr} pr-review/${pr}`
-3. Determine changed files via `git diff --name-only ${base}...pr-review/${pr}`
+3. Determine changed files via `git diff --name-only ${base}...pr-review/${pr}`, and
+ their base-version source via `git show ${base}:${path}` (for `changeLevel` grading)
4. `startServer()`
5. Connect gremlin-js to `handle.url`
-6. `extract(worktreePath, language, { changedFiles })`
+6. `extract(worktreePath, language, { changedFiles, baseContents })`
7. `populate(g, extraction)`
8. `completeness(g, { ... })`
9. `coverageGaps(g, { ... })`
diff --git a/.skills/tinker-review/references/schema.md b/.skills/tinker-review/references/schema.md
index 47df6e17a3..f80dbef8b4 100644
--- a/.skills/tinker-review/references/schema.md
+++ b/.skills/tinker-review/references/schema.md
@@ -4,24 +4,28 @@
### Code structure
-**File** `{ path, language, changed }`
-A source file in the PR. `changed: true` if modified in this PR.
-
-*Stub Files* `{ path, language, changed: true, parsed: false, deleted }` are
-markers for changed files the extractor didn't parse, so the PR's `modifies`
-edge still lands. `deleted: true` means the PR removed the file (absent from the
+**File** `{ path, language, changeLevel }`
+A source file in the PR. `changeLevel` grades how much the PR moved it (see
+**Change levels** below); a file the PR touched is anything but `NONE`.
+
+*Stub Files* `{ path, language, changeLevel: "STRUCTURAL", parsed: false, deleted }`
+are markers for changed files the extractor didn't parse, so the PR's `modifies`
+edge still lands. Unparsed, so no fingerprint is possible — graded `STRUCTURAL`
+conservatively. `deleted: true` means the PR removed the file (absent from the
PR-head worktree); `deleted: false` means it's present but an unparsed type
(non-code, or a non-primary language). Real Files have no `parsed` property, so
select materialized files with `.hasNot("parsed")` and markers with
`.has("parsed", false)`.
-**Function** `{ name, signature, visibility, filePath, language, lines_start, lines_end, changed }`
-A function or method. The primary unit of analysis. `language` is the source
+**Function** `{ name, signature, visibility, filePath, language, lines_start, lines_end, changeLevel }`
+A function or method. The primary unit of analysis. `changeLevel` is graded per
+function by diffing the base against the PR-head version — so an untouched helper
+in a changed file grades `NONE`, not "changed". `language` is the source
language (java, python, javascript, go, csharp, dart); by-name edges (`calls`,
`tests`) resolve only within a language, so a multi-language PR never invents
cross-language edges.
-*External stub Functions* `{ name, external: true, resolved: false, changed: false, origin?, definedIn? }`
+*External stub Functions* `{ name, external: true, resolved: false, changeLevel: "NONE", origin?, definedIn? }`
are markers created when a `calls`/`tests` edge targets a function by name that
wasn't extracted (a library/JDK call, or a function in a file this PR didn't
change). They keep the edge from vanishing and let blast-radius/centrality see
@@ -35,10 +39,12 @@ name — noise), `project` (a repo source declares a type with this name;
`origin: library` calls from out-degree so ubiquitous accessor calls don't
inflate hotspots.
-**Type** `{ name, kind, visibility, filePath, language, changed }`
+**Type** `{ name, kind, visibility, filePath, language, changeLevel }`
A class, interface, struct, or enum. `kind` is one of: class, interface, struct,
-enum. `changed: true` if the PR modified the file it's declared in. `language`
-scopes `extends`/`implements` resolution so hierarchies never cross languages.
+enum. `changeLevel` grades the type's declaration surface: a change to its
+kind/visibility/supertypes/member-set is `STRUCTURAL`; a change to method bodies
+in the declaration leaves it `BEHAVIORAL`/`FORMATTING`. `language` scopes
+`extends`/`implements` resolution so hierarchies never cross languages.
*External stub Types* `{ name, external: true, resolved: false }` are markers
created when an `extends`/`implements` edge names a supertype that wasn't
@@ -48,6 +54,28 @@ expansion (which pulls a changed type's ancestors and descendants in as context)
so stubs mostly stand for third-party types. They lack `kind`/`filePath`; filter
them with `.has("external", false)` or `.hasNot("external")`.
+#### Change levels
+
+`changeLevel` (on File, Function, Type) grades how much the PR moved a vertex,
+computed by diffing the base version of each changed file against PR-head. It
+replaces the old boolean `changed` — because that was stamped per file, it could
+not tell an untouched helper in a changed file from the code the PR rewrote.
+
+| Level | Meaning |
+|-------|---------|
+| `NONE` | byte-identical to base (untouched; also context / hierarchy-neighborhood files and external stubs) |
+| `FORMATTING` | only comments or whitespace moved; the tokens are identical |
+| `BEHAVIORAL` | the body changed for real, but the signature is stable |
+| `STRUCTURAL` | the signature/declaration changed, or the member/file was added or removed, or imports/exports changed |
+
+Query the two useful sets directly: **any real change** is
+`.has("changeLevel", within("FORMATTING","BEHAVIORAL","STRUCTURAL"))` (inclusion-
+oriented checks — orphans, cluster-analysis, architecture); **meaningful change**
+is `.has("changeLevel", within("BEHAVIORAL","STRUCTURAL"))` (risk checks —
+centrality, blast-radius, coverage-gaps, which discount NONE and FORMATTING by
+default). The vocabulary and predicate helpers live in
+`scripts/graph/change-levels.js`.
+
*Analysis-written property* — `community: number` is stamped onto Function, Type,
File and Test vertices by community detection (`communityDetection`, Louvain
modularity over the code subgraph). Vertices sharing a value are one densely-tied
diff --git a/.skills/tinker-review/scripts/enrichment/api.js b/.skills/tinker-review/scripts/enrichment/api.js
index c9b6c446d0..3f21cdf042 100644
--- a/.skills/tinker-review/scripts/enrichment/api.js
+++ b/.skills/tinker-review/scripts/enrichment/api.js
@@ -21,6 +21,7 @@ import { readFile } from "node:fs/promises";
import { join } from "node:path";
import gremlin from "gremlin";
import { CONFIDENCE, normalizeConfidence, isValidConfidence } from "../graph/confidence.js";
+import { changedAny, normalizeChangeLevel } from "../graph/change-levels.js";
import { symbolFromPath, createReferenceEdge } from "../graph/references.js";
const { process: { statics: __ } } = gremlin;
@@ -31,29 +32,40 @@ let cachedSteps = null;
/**
* List Function vertices, optionally filtered. The go-to orientation read:
- * `--changed true` shows just the functions this PR touched; `--visibility
- * public` narrows to the API surface. Returns signature and line span so you can
- * jump to source in the worktree.
+ * `--changeLevel STRUCTURAL` shows just the signature-level changes;
+ * `--changed true` (alias for "any level but NONE") shows everything the PR
+ * touched; `--visibility public` narrows to the API surface. Returns signature
+ * and line span so you can jump to source in the worktree.
*
* @param {object} g - gremlin-js GraphTraversalSource (already connected)
- * @param {object} [filter] - { changed?: boolean, visibility?: string, filePath?: string }
- * @returns {Promise<{name, signature, filePath, visibility, changed, linesStart, linesEnd}[]>}
+ * @param {object} [filter] - { changeLevel?: string, changed?: boolean, visibility?: string, filePath?: string }
+ * @returns {Promise<{name, signature, filePath, visibility, changeLevel, changed, linesStart, linesEnd}[]>}
*/
export async function listFunctions(g, filter = {}) {
let t = g.V().hasLabel("Function");
- if (filter.changed !== undefined) t = t.has("changed", filter.changed);
+ if (filter.changeLevel) {
+ // Exact-grade filter.
+ t = t.has("changeLevel", normalizeChangeLevel(filter.changeLevel, filter.changeLevel));
+ } else if (filter.changed !== undefined) {
+ // Back-compat alias: changed=true → anything but NONE; changed=false → NONE.
+ t = filter.changed ? t.has("changeLevel", changedAny()) : t.has("changeLevel", "NONE");
+ }
if (filter.visibility) t = t.has("visibility", filter.visibility);
if (filter.filePath) t = t.has("filePath", filter.filePath);
const results = await t.elementMap().toList();
- return results.map(m => ({
- name: m.get("name"),
- signature: m.get("signature"),
- filePath: m.get("filePath"),
- visibility: m.get("visibility"),
- changed: m.get("changed"),
- linesStart: m.get("lines_start"),
- linesEnd: m.get("lines_end"),
- }));
+ return results.map(m => {
+ const changeLevel = m.get("changeLevel");
+ return {
+ name: m.get("name"),
+ signature: m.get("signature"),
+ filePath: m.get("filePath"),
+ visibility: m.get("visibility"),
+ changeLevel,
+ changed: changeLevel !== undefined && changeLevel !== "NONE",
+ linesStart: m.get("lines_start"),
+ linesEnd: m.get("lines_end"),
+ };
+ });
}
/**
diff --git a/.skills/tinker-review/scripts/enrichment/cli.js b/.skills/tinker-review/scripts/enrichment/cli.js
index 989454d189..cac84dcc07 100644
--- a/.skills/tinker-review/scripts/enrichment/cli.js
+++ b/.skills/tinker-review/scripts/enrichment/cli.js
@@ -92,7 +92,7 @@ async function main() {
console.log(" node cli.js <command> --workDir /tmp/pr-review-3448 [--key value ...]");
console.log("");
console.log("Commands:");
- console.log(" listFunctions [--changed true] [--visibility public]");
+ console.log(" listFunctions [--changeLevel STRUCTURAL] [--changed true] [--visibility public]");
console.log(" listTypes [--kind class]");
console.log(" getCallsFrom --function <name> --file <path>");
console.log(" getCanonicalSteps");
@@ -150,7 +150,7 @@ async function main() {
switch (command) {
case "listFunctions":
- result = await fn(g, { changed: args.changed, visibility: args.visibility, filePath: args.file });
+ result = await fn(g, { changeLevel: args.changeLevel, changed: args.changed, visibility: args.visibility, filePath: args.file });
break;
case "listTypes":
result = await fn(g, { kind: args.kind, filePath: args.file });
diff --git a/.skills/tinker-review/scripts/extraction/tree-sitter.js b/.skills/tinker-review/scripts/extraction/tree-sitter.js
index 065f642020..8d425cdee9 100644
--- a/.skills/tinker-review/scripts/extraction/tree-sitter.js
+++ b/.skills/tinker-review/scripts/extraction/tree-sitter.js
@@ -22,9 +22,11 @@ const require = createRequire(import.meta.url);
const TreeSitter = require("web-tree-sitter");
import { readdir } from "node:fs/promises";
import { readFileSync } from "node:fs";
+import { createHash } from "node:crypto";
import { join, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
+import { CHANGE_LEVEL, gradeMember, gradeFile } from "../graph/change-levels.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -248,7 +250,34 @@ function extractSignature(node, language) {
return `${name}${params}`;
}
-function extractFunctionsFromTree(tree, filePath, language, fileChanged) {
+function sha256(text) {
+ return createHash("sha256").update(text).digest("hex");
+}
+
+// Hash of a node's exact source text — distinguishes NONE (identical) from any
+// edit, including a pure reformat.
+function rawHash(node) {
+ return sha256(node.text);
+}
+
+// Hash of a node's leaf tokens with comment nodes skipped. tree-sitter does not
+// emit whitespace as tokens, so this is "code, minus formatting and comments":
+// two bodies with the same normHash differ only in whitespace/comments
+// (FORMATTING); differing normHash means the tokens themselves moved (BEHAVIORAL).
+function normHash(node) {
+ const tokens = [];
+ (function walk(n) {
+ if (n.type.includes("comment")) return;
+ if (n.childCount === 0) {
+ tokens.push(n.text);
+ return;
+ }
+ for (let i = 0; i < n.childCount; i++) walk(n.child(i));
+ })(node);
+ return sha256(tokens.join("