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 752c5b4edb tinker-review: single-source the enrichment CLI docs
752c5b4edb is described below
commit 752c5b4edbc820251ea3eda02d8a788efc928dc4
Author: Stephen Mallette <[email protected]>
AuthorDate: Fri Jul 3 19:05:14 2026 -0400
tinker-review: single-source the enrichment CLI docs
Command semantics now live in one narrative catalog,
references/enrichment-cli.md
(what each does / when to reach for it), backed by JSDoc on every api.js
export.
The cli.js COMMANDS registry gains a `facing` flag (agent vs internal
Phase-1)
and is the single source of truth for what exists; --help groups
accordingly.
SKILL.md drops its duplicated command listing and links to the catalog
instead.
A node --test guard (test/cli-docs.test.js) fails if the surfaces drift:
every
registered command must have a doc section and a --help line, and SKILL.md
must
keep the catalog link. Fixed the package.json test script to actually run.
Assisted-by: Claude Code:claude-opus-4-8
---
.skills/tinker-review/SKILL.md | 57 +++----
.skills/tinker-review/package.json | 2 +-
.skills/tinker-review/references/enrichment-cli.md | 169 +++++++++++++++++++++
.skills/tinker-review/scripts/enrichment/api.js | 118 ++++++++++++++
.skills/tinker-review/scripts/enrichment/cli.js | 67 +++++---
.skills/tinker-review/test/cli-docs.test.js | 119 +++++++++++++++
6 files changed, 471 insertions(+), 61 deletions(-)
diff --git a/.skills/tinker-review/SKILL.md b/.skills/tinker-review/SKILL.md
index 9926c22e90..1ea01dac48 100644
--- a/.skills/tinker-review/SKILL.md
+++ b/.skills/tinker-review/SKILL.md
@@ -25,6 +25,7 @@ metadata:
- Read [references/schema.md](references/schema.md) when you need to
understand what vertices, edges, or properties exist in the knowledge graph
(typically during enrichment or when writing raw Gremlin)
- Read [references/interfaces.md](references/interfaces.md) when you need the
exact function signatures or data type definitions for a module
+- Read [references/enrichment-cli.md](references/enrichment-cli.md) when you
need to know what an enrichment CLI command *does* and when to reach for it
(the command names below are terse; this is where their meaning lives)
## Execution Sequence
@@ -93,49 +94,29 @@ to the per-check `@typedef`s in `scripts/patterns/*.js`.
The Gremlin Server is still running. Use `scripts/enrichment/cli.js` to
read from and write to the knowledge graph. Do NOT write your own connection
-code or import gremlin-js directly — the CLI handles connections for you.
+code or import gremlin-js directly — the CLI handles connections for you. For
+what each command does and when to use it, see
+[references/enrichment-cli.md](references/enrichment-cli.md).
-**Usage:**
+Every command takes `--workDir /tmp/pr-review-<pr>`, connects using that dir's
+`session.json`, prints its JSON result to stdout, and disconnects:
```bash
node scripts/enrichment/cli.js <command> --workDir /tmp/pr-review-<pr>
[--options...]
```
-**Read commands:**
-```bash
-node scripts/enrichment/cli.js listFunctions --workDir /tmp/pr-review-<pr>
--changed true --visibility public
-node scripts/enrichment/cli.js listTypes --workDir /tmp/pr-review-<pr> --kind
class
-node scripts/enrichment/cli.js getCallsFrom --workDir /tmp/pr-review-<pr>
--function <name> --file <path>
-node scripts/enrichment/cli.js getCanonicalSteps --workDir /tmp/pr-review-<pr>
-node scripts/enrichment/cli.js auditConfidence --workDir /tmp/pr-review-<pr>
-node scripts/enrichment/cli.js listInferred --workDir /tmp/pr-review-<pr>
--relation implements_step
-node scripts/enrichment/cli.js listDeleted --workDir /tmp/pr-review-<pr>
# removal PRs: files the PR deleted + their symbols
-node scripts/enrichment/cli.js listExternalRefs --workDir /tmp/pr-review-<pr>
# unresolved callees; flags any matching a deleted symbol
-```
-
-`auditConfidence` returns the edge confidence distribution and the list of
-AMBIGUOUS edges. Re-run it after enrichment to refresh the audit with the edges
-you added, then reflect any remaining AMBIGUOUS links in `openQuestions`.
-
-`listInferred` is your **verification worklist** — the name-resolved /
agent-mapped
-edges worth a source check (optionally narrowed with `--relation`). After
reading
-the source, use `setEdgeConfidence` (below) to promote a confirmed edge to
-`EXTRACTED` or downgrade a wrong resolution to `AMBIGUOUS`.
-
-**Write commands** (edges you create default to `INFERRED`; pass
-`--confidence AMBIGUOUS` for a guess you want flagged, or `EXTRACTED` when the
-source states it directly):
-```bash
-node scripts/enrichment/cli.js mapStep --workDir /tmp/pr-review-<pr>
--function <name> --file <path> --step <canonicalName> [--confidence
INFERRED|AMBIGUOUS|EXTRACTED]
-node scripts/enrichment/cli.js setEdgeConfidence --workDir /tmp/pr-review-<pr>
--relation <label> --fromName <name> [--fromFile <path>] [--toName <name>]
--confidence <EXTRACTED|INFERRED|AMBIGUOUS>
-node scripts/enrichment/cli.js addReference --workDir /tmp/pr-review-<pr>
--fromPath <survivingFile> --toPath <deletedFile> --symbol <name> [--location
<where>] [--confidence ...]
-node scripts/enrichment/cli.js linkDiscussion --workDir /tmp/pr-review-<pr>
--url <url> --source jira --title <title> [--confidence ...]
-node scripts/enrichment/cli.js linkDoc --workDir /tmp/pr-review-<pr> --entity
Step --name <name> --doc <path> [--confidence ...]
-node scripts/enrichment/cli.js addGrammarRule --workDir /tmp/pr-review-<pr>
--name <name>
-node scripts/enrichment/cli.js annotate --workDir /tmp/pr-review-<pr> --label
Function --name <name> --key <key> --value <value>
-```
-
-Each command connects, executes, prints JSON result to stdout, and disconnects.
-The connection info is read from `/tmp/pr-review-<pr>/session.json`
automatically.
+The command catalog — what each does and when to reach for it — lives in
+[references/enrichment-cli.md](references/enrichment-cli.md); run
+`node scripts/enrichment/cli.js --help` for exact flags. In brief: **read
+commands** (`listFunctions`, `listDeleted`, `listExternalRefs`, …) orient you
and
+feed the playbooks; **write commands** (`mapStep`, `addReference`,
+`setEdgeConfidence`, …) add the semantic edges. Edges you create default to
+`INFERRED` — pass `--confidence AMBIGUOUS` for a flagged guess or `EXTRACTED`
+when the source states it directly.
+
+The confidence loop is the backbone of enrichment: `auditConfidence` →
+`listInferred` (your verification worklist) → read the source →
`setEdgeConfidence`
+to promote or downgrade, then re-run `auditConfidence` and reflect anything
still
+`AMBIGUOUS` in `openQuestions`. See `general.md`'s **Verify confidence**
section.
**Read source files:** worktree at `/tmp/pr-review-<pr>/src/`
diff --git a/.skills/tinker-review/package.json
b/.skills/tinker-review/package.json
index 27dd211819..a76f6ef8f8 100644
--- a/.skills/tinker-review/package.json
+++ b/.skills/tinker-review/package.json
@@ -8,7 +8,7 @@
"node": ">=20.0.0"
},
"scripts": {
- "test": "node --test test/",
+ "test": "node --test \"test/**/*.test.js\"",
"review": "node review.js"
},
"dependencies": {
diff --git a/.skills/tinker-review/references/enrichment-cli.md
b/.skills/tinker-review/references/enrichment-cli.md
new file mode 100644
index 0000000000..53ae52de5c
--- /dev/null
+++ b/.skills/tinker-review/references/enrichment-cli.md
@@ -0,0 +1,169 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with this
+work for additional information regarding copyright ownership. The ASF
+licenses this file to You under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+-->
+
+# Enrichment CLI — Command Reference
+
+This is the human-readable reference for `scripts/enrichment/cli.js`: what each
+command *does* and *when you'd reach for it* while writing a playbook or
running
+an enrichment pass. The command names are terse and not always self-descriptive
+— this file is where the meaning lives.
+
+**Three sources, three jobs (keep them in sync):**
+
+| Source | Job |
+|--------|-----|
+| `scripts/enrichment/cli.js` — the `COMMANDS` registry | Single source of
truth for **what commands exist** and whether each is `agent`- or
`internal`-facing. |
+| `scripts/enrichment/api.js` — JSDoc on each function | Canonical
**semantics** (params, return shape, edge confidence), co-located with the
implementation. |
+| **this file** | The **narrative** — plain-language "what/when/gotchas" for a
playbook author. |
+
+`test/cli-docs.test.js` fails the build if these drift: every registry command
+must have a `##` section here and a line in `cli.js --help`; agent-facing
+commands must also appear in `SKILL.md`. Add a command → document it in all
+three or CI goes red.
+
+## Invocation
+
+```bash
+node scripts/enrichment/cli.js <command> --workDir /tmp/pr-review-<pr> [--key
value ...]
+```
+
+Every command connects using `/tmp/pr-review-<pr>/session.json`, prints a JSON
+result to stdout, and disconnects. Edges you create default to `INFERRED`
+confidence unless the command says otherwise; pass `--confidence AMBIGUOUS` for
+a flagged guess or `EXTRACTED` when the source states the fact directly.
+
+---
+
+## Agent-facing — read
+
+These answer "what's in the graph?" and never mutate it. Use them to orient
+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
+you can open it in the worktree. Reach for it at the top of almost any
playbook.
+
+### listTypes
+Lists types (classes, interfaces, enums), optionally filtered by `--kind` or
+`--file`. Use it to see the types a PR defines or touches before drilling into
+their functions.
+
+### getCallsFrom
+The direct callees of one function (its outgoing `calls` edges). **Keyed by
+`--function` AND `--file`** because names repeat across the codebase. Reach for
+it to trace what a changed function depends on — e.g. to confirm it no longer
+calls a symbol the PR removed.
+
+### getCanonicalSteps
+The canonical Gremlin step vocabulary, parsed straight from `Gremlin.g4`. This
+is the authoritative list of step names you map GLV methods onto with `mapStep`
+— validate against it so you never invent a step that doesn't exist. Reads the
+grammar file, not the graph. Central to the GLV playbook.
+
+### auditConfidence
+The edge-confidence distribution plus the list of `AMBIGUOUS` edges. **Run it
+twice**: once at the start of the confidence pass, and again after you enrich,
so
+the audit reflects edges you added. Anything still `AMBIGUOUS` afterward
belongs
+in the report's `openQuestions`, not asserted as fact.
+
+### listInferred
+Your **verification worklist**: the name-resolved / agent-mapped edges worth a
+source check, optionally narrowed with `--relation` (start with
+`implements_step`, then `calls`). After reading the source, promote or
downgrade
+each with `setEdgeConfidence`. This is how INFERRED becomes EXTRACTED.
+
+### listDeleted
+The files the PR deleted (graph stubs marked `deleted: true`), each paired with
+the symbol name it likely defined. **The entry point for the removal
playbook** —
+it gives you the symbols to grep the surviving tree for, and the valid
`--toPath`
+targets for `addReference`.
+
+### listExternalRefs
+Unresolved external callees — names the changed code calls that weren't defined
+in the changed set. Shows each stub's `origin` (library/project/unresolved)
and,
+crucially, **flags any whose name matches a deleted symbol**: a changed file
+still calling a just-removed name is a dangling reference the graph catches on
+its own. Sorted so the dangerous ones surface first.
+
+---
+
+## Agent-facing — write
+
+These mutate the graph. New edges default to `INFERRED`; grade honestly.
+
+### addReference
+**Manual escape hatch for the removal playbook.** The Phase-1 pass grepped for
+*code-symbol* references automatically; use this to record the ones it can't
see
+— config strings, doc mentions, build-file references to a removed symbol.
+`--toPath` must be a file `listDeleted` returned. Creates the same `references`
+edge the automatic pass does.
+
+### mapStep
+Records that a GLV/host-language function implements a canonical Gremlin step,
as
+an `implements_step` edge (creating the Step vertex on first use). **The core
+move of the GLV playbook.** Map only real traversal-step methods — never
+language boilerplate (`toString`, `equals`, `close`) or internal helpers.
+Validate `--step` against `getCanonicalSteps` first.
+
+### setEdgeConfidence
+Re-grades an existing edge after you verify it against source. Identify the
edge
+by `--relation` + `--fromName` (optionally pin `--fromFile`, narrow
`--toName`).
+**The other half of the `listInferred` loop**: promote a confirmed edge to
+`EXTRACTED`, or downgrade a wrong name-resolution to `AMBIGUOUS` so it surfaces
+in the report's review list.
+
+### linkDiscussion
+Attaches an external discussion you found (JIRA, dev-list thread, proposal) as
a
+Discussion vertex, linked from the PR discussion via `addresses`. Use it when
+enrichment turns up prior context the Phase-1 discovery pass missed, so the
+report can cite where the change was debated. `--source` is `jira | devlist |
+proposal`.
+
+### linkDoc
+Records that a documentation file documents a graph entity, via a `documents`
+edge from a Doc vertex to the named entity (`--entity` label + `--name`). Use
it
+to connect a step or feature to its reference docs / recipe, so the report can
+flag a change whose docs weren't updated.
+
+### addGrammarRule
+Adds a `GrammarRule` vertex for a rule the PR introduces. Reach for it in the
+grammar playbook when a `*.g4` change adds a production the graph should track
so
+later steps can link to it.
+
+### annotate
+Sets an arbitrary `--key`/`--value` property on a vertex identified by
`--label`
++ `--name`. The **general-purpose escape hatch** for a fact the schema has no
+dedicated edge for. Prefer a typed command when one fits; reach for this only
+when none does.
+
+---
+
+## Internal (Phase 1)
+
+`review.js` runs these itself while building the graph. They're exposed on the
+CLI only for manual re-runs during debugging — **you do not call them during
+enrichment**, and they intentionally do not appear in `SKILL.md`.
+
+### classifyExternals
+Tags each external callee stub with `origin` = `library | project |
unresolved`.
+`listExternalRefs` and the centrality check read that tag. Runs once in Phase
1.
+
+### createPrDiscussion
+Creates the root PR Discussion vertex that every other discussion links back to
+via `addresses`. Idempotent (a second call is a no-op). Runs once in Phase 1.
diff --git a/.skills/tinker-review/scripts/enrichment/api.js
b/.skills/tinker-review/scripts/enrichment/api.js
index b90b78757c..460b15619d 100644
--- a/.skills/tinker-review/scripts/enrichment/api.js
+++ b/.skills/tinker-review/scripts/enrichment/api.js
@@ -29,6 +29,16 @@ let cachedSteps = null;
// === Read operations ===
+/**
+ * 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.
+ *
+ * @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}[]>}
+ */
export async function listFunctions(g, filter = {}) {
let t = g.V().hasLabel("Function");
if (filter.changed !== undefined) t = t.has("changed", filter.changed);
@@ -46,6 +56,15 @@ export async function listFunctions(g, filter = {}) {
}));
}
+/**
+ * List Type vertices (classes, interfaces, enums), optionally by `kind` or
+ * `filePath`. Use it to see the types the PR defines or touches before
drilling
+ * into their functions.
+ *
+ * @param {object} g - gremlin-js GraphTraversalSource (already connected)
+ * @param {object} [filter] - { kind?: string, filePath?: string }
+ * @returns {Promise<{name, kind, visibility, filePath}[]>}
+ */
export async function listTypes(g, filter = {}) {
let t = g.V().hasLabel("Type");
if (filter.kind) t = t.has("kind", filter.kind);
@@ -59,6 +78,17 @@ export async function listTypes(g, filter = {}) {
}));
}
+/**
+ * The direct callees of one function — its outgoing `calls` edges. Use it to
+ * trace what a changed function depends on (e.g. does it still call a symbol
the
+ * PR removed?). The function is keyed by name AND file because names repeat
+ * across the codebase.
+ *
+ * @param {object} g - gremlin-js GraphTraversalSource (already connected)
+ * @param {string} functionName - Caller's name
+ * @param {string} filePath - Caller's file (disambiguates same-named
functions)
+ * @returns {Promise<{calleeName, filePath}[]>}
+ */
export async function getCallsFrom(g, functionName, filePath) {
const results = await g.V().hasLabel("Function")
.has("name", functionName)
@@ -72,6 +102,16 @@ export async function getCallsFrom(g, functionName,
filePath) {
}));
}
+/**
+ * The canonical Gremlin traversal-step vocabulary, parsed from `Gremlin.g4`
+ * (every `traversalMethod_<name>` rule). This is the authoritative set of step
+ * names you map GLV/host-language methods onto with `mapStep`. Reads the
grammar
+ * file, not the graph, so it needs the repo path rather than `g`; result is
+ * cached for the process.
+ *
+ * @param {string} repoPath - Path to the checked-out worktree
+ * @returns {Promise<string[]>} sorted, de-duplicated canonical step names
+ */
export async function getCanonicalSteps(repoPath) {
if (cachedSteps) return cachedSteps;
const g4Path = join(repoPath, "gremlin-language/src/main/antlr4/Gremlin.g4");
@@ -130,6 +170,20 @@ export async function listExternalRefs(g) {
// === Write operations ===
+/**
+ * Record that a GLV/host-language function implements a canonical Gremlin
step,
+ * as an `implements_step` edge (creating the Step vertex if it's the first
time
+ * that step is seen). The core enrichment move of the GLV playbook: map only
+ * real traversal-step methods, not language boilerplate. Validate the step
name
+ * against `getCanonicalSteps` first.
+ *
+ * @param {object} g - gremlin-js GraphTraversalSource (already connected)
+ * @param {string} functionName - The implementing function's name
+ * @param {string} filePath - The implementing function's file
+ * @param {string} canonicalStepName - A name from getCanonicalSteps
+ * @param {string} [confidence] - default INFERRED (a name-based mapping)
+ * @returns {Promise<object>}
+ */
export async function mapStep(g, functionName, filePath, canonicalStepName,
confidence) {
const conf = normalizeConfidence(confidence, CONFIDENCE.INFERRED);
const stepExists = await g.V().hasLabel("Step").has("name",
canonicalStepName).hasNext();
@@ -225,6 +279,21 @@ export async function addReference(g, params = {}) {
return { referenced: `${fromPath} -> ${toPath}`, symbol: symbol || "",
confidence: conf };
}
+/**
+ * Attach an external discussion you found (JIRA issue, dev-list thread,
proposal
+ * doc) to the graph as a Discussion vertex, linked from the PR's own
discussion
+ * via an `addresses` edge. Use it when enrichment turns up prior context the
+ * Phase-1 discovery pass missed, so the report can cite where the change was
+ * debated.
+ *
+ * @param {object} g - gremlin-js GraphTraversalSource (already connected)
+ * @param {string} url - Canonical link to the discussion
+ * @param {string} source - jira | devlist | proposal
+ * @param {string} title - Human-readable title
+ * @param {string} [body] - Optional excerpt/summary
+ * @param {string} [confidence] - default INFERRED
+ * @returns {Promise<object>}
+ */
export async function linkDiscussion(g, url, source, title, body, confidence) {
const conf = normalizeConfidence(confidence, CONFIDENCE.INFERRED);
await g.addV("Discussion")
@@ -246,6 +315,19 @@ export async function linkDiscussion(g, url, source,
title, body, confidence) {
return { linked: `${source}: ${title}`, confidence: conf };
}
+/**
+ * Set an arbitrary property on a vertex identified by label + name. The
+ * general-purpose escape hatch for recording a fact the schema has no
dedicated
+ * edge for (e.g. tagging a Function with a review note). Prefer a typed
command
+ * when one fits; reach for this only when none does.
+ *
+ * @param {object} g - gremlin-js GraphTraversalSource (already connected)
+ * @param {string} label - Vertex label (e.g. Function, Step)
+ * @param {string} name - Vertex name
+ * @param {string} key - Property key to set
+ * @param {string} value - Property value
+ * @returns {Promise<object>}
+ */
export async function annotate(g, label, name, key, value) {
await g.V().hasLabel(label).has("name", name)
.property(key, value)
@@ -254,6 +336,20 @@ export async function annotate(g, label, name, key, value)
{
return { annotated: `${label}:${name}.${key} = ${value}` };
}
+/**
+ * Record that a documentation file documents a graph entity, as a `documents`
+ * edge from a Doc vertex (created on first use) to the named entity. Use it to
+ * connect a step/feature to the reference docs or recipe that covers it, so
the
+ * report can flag a change whose docs weren't updated.
+ *
+ * @param {object} g - gremlin-js GraphTraversalSource (already connected)
+ * @param {string} entityLabel - Label of the documented entity (e.g. Step)
+ * @param {string} entityName - Name of the documented entity
+ * @param {string} docPath - Path to the doc file
+ * @param {string} [section] - Optional section/anchor within the doc
+ * @param {string} [confidence] - default INFERRED
+ * @returns {Promise<object>}
+ */
export async function linkDoc(g, entityLabel, entityName, docPath, section,
confidence) {
const conf = normalizeConfidence(confidence, CONFIDENCE.INFERRED);
const docExists = await g.V().hasLabel("Doc").has("path", docPath).hasNext();
@@ -273,6 +369,16 @@ export async function linkDoc(g, entityLabel, entityName,
docPath, section, conf
return { linked: `${docPath} documents ${entityLabel}:${entityName}`,
confidence: conf };
}
+/**
+ * Add a GrammarRule vertex for a rule the PR introduces to the grammar. Use it
+ * in the grammar playbook when a `*.g4` change adds a production the graph
+ * should track so later steps can link to it.
+ *
+ * @param {object} g - gremlin-js GraphTraversalSource (already connected)
+ * @param {string} name - Rule name
+ * @param {string} [production] - Optional production body
+ * @returns {Promise<object>}
+ */
export async function addGrammarRule(g, name, production) {
await g.addV("GrammarRule")
.property("name", name)
@@ -282,6 +388,18 @@ export async function addGrammarRule(g, name, production) {
return { added: `GrammarRule: ${name}` };
}
+/**
+ * INTERNAL (Phase 1). Create the root PR Discussion vertex that every other
+ * discussion links back to via `addresses`. review.js calls this once while
+ * building the graph; it's idempotent (a second call is a no-op). Exposed on
the
+ * CLI only for manual re-runs — reviewers don't call it during enrichment.
+ *
+ * @param {object} g - gremlin-js GraphTraversalSource (already connected)
+ * @param {number|string} pr - PR number (forms the GitHub URL)
+ * @param {string} [title] - Defaults to "PR #<pr>"
+ * @param {string} [body] - Optional description
+ * @returns {Promise<{created: boolean, pr?}>}
+ */
export async function createPrDiscussion(g, pr, title, body) {
const exists = await g.V().hasLabel("Discussion").has("source",
"pr").hasNext();
if (exists) return { created: false };
diff --git a/.skills/tinker-review/scripts/enrichment/cli.js
b/.skills/tinker-review/scripts/enrichment/cli.js
index 49d078e853..464f0ab214 100644
--- a/.skills/tinker-review/scripts/enrichment/cli.js
+++ b/.skills/tinker-review/scripts/enrichment/cli.js
@@ -19,6 +19,7 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
+import { pathToFileURL } from "node:url";
import gremlin from "gremlin";
import {
listFunctions, listTypes, getCallsFrom, getCanonicalSteps,
@@ -29,24 +30,34 @@ import {
import { confidenceAudit, listInferred } from
"../patterns/confidence-audit.js";
import { classifyExternals } from "../patterns/classify-externals.js";
+// Every command carries `facing`: "agent" commands are the Phase-2 enrichment
+// tools a reviewer (human or agent) invokes and that SKILL.md documents;
+// "internal" commands are Phase-1 pipeline steps review.js runs itself,
exposed
+// here only for manual re-runs. The --help text and the docs/consistency test
+// read `facing` from this registry, so this object is the single source of
truth
+// for which commands exist and who they are for. See
references/enrichment-cli.md
+// for what each one does.
const COMMANDS = {
- listFunctions: { fn: listFunctions, needsG: true },
- listTypes: { fn: listTypes, needsG: true },
- getCallsFrom: { fn: getCallsFrom, needsG: true },
- getCanonicalSteps: { fn: getCanonicalSteps, needsG: false },
- auditConfidence: { fn: confidenceAudit, needsG: true },
- listInferred: { fn: listInferred, needsG: true },
- listDeleted: { fn: listDeleted, needsG: true },
- listExternalRefs: { fn: listExternalRefs, needsG: true },
- classifyExternals: { fn: classifyExternals, needsG: true },
- addReference: { fn: addReference, needsG: true },
- mapStep: { fn: mapStep, needsG: true },
- setEdgeConfidence: { fn: setEdgeConfidence, needsG: true },
- linkDiscussion: { fn: linkDiscussion, needsG: true },
- linkDoc: { fn: linkDoc, needsG: true },
- addGrammarRule: { fn: addGrammarRule, needsG: true },
- annotate: { fn: annotate, needsG: true },
- createPrDiscussion: { fn: createPrDiscussion, needsG: true },
+ // Agent-facing (Phase 2 enrichment) — read
+ listFunctions: { fn: listFunctions, needsG: true, facing: "agent" },
+ listTypes: { fn: listTypes, needsG: true, facing: "agent" },
+ getCallsFrom: { fn: getCallsFrom, needsG: true, facing: "agent" },
+ getCanonicalSteps: { fn: getCanonicalSteps, needsG: false, facing: "agent" },
+ auditConfidence: { fn: confidenceAudit, needsG: true, facing: "agent" },
+ listInferred: { fn: listInferred, needsG: true, facing: "agent" },
+ listDeleted: { fn: listDeleted, needsG: true, facing: "agent" },
+ listExternalRefs: { fn: listExternalRefs, needsG: true, facing: "agent" },
+ // Agent-facing (Phase 2 enrichment) — write
+ addReference: { fn: addReference, needsG: true, facing: "agent" },
+ mapStep: { fn: mapStep, needsG: true, facing: "agent" },
+ setEdgeConfidence: { fn: setEdgeConfidence, needsG: true, facing: "agent" },
+ linkDiscussion: { fn: linkDiscussion, needsG: true, facing: "agent" },
+ linkDoc: { fn: linkDoc, needsG: true, facing: "agent" },
+ addGrammarRule: { fn: addGrammarRule, needsG: true, facing: "agent" },
+ annotate: { fn: annotate, needsG: true, facing: "agent" },
+ // Internal — run by review.js during Phase 1; exposed for manual re-runs
only
+ classifyExternals: { fn: classifyExternals, needsG: true, facing: "internal"
},
+ createPrDiscussion: { fn: createPrDiscussion, needsG: true, facing:
"internal" },
};
function parseArgs(argv) {
@@ -87,7 +98,6 @@ async function main() {
console.log(" listInferred [--relation implements_step|calls|...]
[--limit 100]");
console.log(" listDeleted");
console.log(" listExternalRefs");
- console.log(" classifyExternals (tags external stubs
origin=library|project|unresolved)");
console.log(" addReference --fromPath <path> --toPath <deletedPath>
[--symbol <name>] [--location <L42>] [--confidence ...]");
console.log(" mapStep --function <name> --file <path> --step
<canonicalName> [--confidence INFERRED|AMBIGUOUS|EXTRACTED]");
console.log(" setEdgeConfidence --relation <label> --fromName <name>
[--fromFile <path>] [--toName <name>] --confidence
<EXTRACTED|INFERRED|AMBIGUOUS>");
@@ -96,6 +106,12 @@ async function main() {
console.log(" addGrammarRule --name <name> [--production <production>]");
console.log(" annotate --label <label> --name <name> --key <key>
--value <value>");
console.log("");
+ console.log("Internal (normally run by review.js during Phase 1; here for
manual re-runs):");
+ console.log(" classifyExternals (tags external stubs
origin=library|project|unresolved)");
+ console.log(" createPrDiscussion --pr <number> --title <title> [--body
<body>]");
+ console.log("");
+ console.log("Full description of each command:
references/enrichment-cli.md");
+ console.log("");
console.log("Options:");
console.log(" --workDir Work directory (default: env
PR_REVIEW_WORKDIR or /tmp/pr-review-*)");
process.exit(0);
@@ -201,7 +217,14 @@ async function main() {
}
}
-main().catch((err) => {
- console.error(err.message || err);
- process.exit(1);
-});
+// Registry is exported so test/cli-docs.test.js can assert docs stay in sync.
+export { COMMANDS };
+
+// Only run when invoked directly (node cli.js ...), not when imported by a
test.
+const invokedDirectly = process.argv[1] && import.meta.url ===
pathToFileURL(process.argv[1]).href;
+if (invokedDirectly) {
+ main().catch((err) => {
+ console.error(err.message || err);
+ process.exit(1);
+ });
+}
diff --git a/.skills/tinker-review/test/cli-docs.test.js
b/.skills/tinker-review/test/cli-docs.test.js
new file mode 100644
index 0000000000..db94f9b633
--- /dev/null
+++ b/.skills/tinker-review/test/cli-docs.test.js
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Drift guard for the enrichment CLI. The COMMANDS registry in cli.js is the
+// single source of truth for which commands exist and who each is for; this
+// test fails if the human-facing surfaces fall out of sync with it:
+//
+// - references/enrichment-cli.md must have a `## <name>` section per command
+// - `cli.js --help` must list every command in the right group
+// - SKILL.md must mention every agent-facing command, and no internal one
+//
+// Add / rename / remove a command and this goes red until the docs follow.
+
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import { execFileSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
+
+import { COMMANDS } from "../scripts/enrichment/cli.js";
+
+const here = dirname(fileURLToPath(import.meta.url));
+const root = join(here, "..");
+
+const all = Object.keys(COMMANDS);
+const agent = all.filter((c) => COMMANDS[c].facing === "agent");
+const internal = all.filter((c) => COMMANDS[c].facing === "internal");
+
+async function read(rel) {
+ return readFile(join(root, rel), "utf-8");
+}
+
+test("every command has a facing of agent or internal", () => {
+ for (const c of all) {
+ assert.ok(
+ COMMANDS[c].facing === "agent" || COMMANDS[c].facing === "internal",
+ `command "${c}" is missing a valid facing ("agent" | "internal")`,
+ );
+ }
+});
+
+test("enrichment-cli.md documents exactly the registry's commands", async ()
=> {
+ const doc = await read("references/enrichment-cli.md");
+ const sections = [...doc.matchAll(/^###?\s+(\w+)\s*$/gm)].map((m) => m[1]);
+ const documented = new Set(sections);
+
+ for (const c of all) {
+ assert.ok(
+ documented.has(c),
+ `command "${c}" has no "## ${c}" section in
references/enrichment-cli.md`,
+ );
+ }
+ // Catch stale sections describing a command that no longer exists. Section
+ // headers that aren't command names (e.g. "Invocation") are ignored by only
+ // checking single-word headers against the registry's known non-command set.
+ const knownNonCommands = new Set(["Invocation"]);
+ for (const s of sections) {
+ if (knownNonCommands.has(s)) continue;
+ assert.ok(
+ COMMANDS[s],
+ `references/enrichment-cli.md documents "${s}", which is not a
registered command`,
+ );
+ }
+});
+
+test("cli.js --help lists every command in the correct group", () => {
+ const help = execFileSync(
+ process.execPath,
+ [join(root, "scripts/enrichment/cli.js"), "--help"],
+ { encoding: "utf-8" },
+ );
+ const [commandsPart, internalPart] = help.split(/^Internal .*$/m);
+ assert.ok(internalPart, "--help is missing the 'Internal' group header");
+
+ for (const c of agent) {
+ assert.match(
+ commandsPart,
+ new RegExp(`\\b${c}\\b`),
+ `agent command "${c}" is missing from the --help Commands group`,
+ );
+ }
+ for (const c of internal) {
+ assert.match(
+ internalPart,
+ new RegExp(`\\b${c}\\b`),
+ `internal command "${c}" is missing from the --help Internal group`,
+ );
+ }
+});
+
+test("SKILL.md points at enrichment-cli.md instead of duplicating the
catalog", async () => {
+ // enrichment-cli.md is the single command catalog; SKILL.md must link to it
+ // (and must not grow its own exhaustive listing again). We assert the link
+ // survives rather than that SKILL.md names every command — it deliberately
+ // names only a few pivotal ones as workflow narrative.
+ const skill = await read("SKILL.md");
+ assert.match(
+ skill,
+ /references\/enrichment-cli\.md/,
+ "SKILL.md no longer links to references/enrichment-cli.md — the command
catalog reference was dropped",
+ );
+});