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",
+  );
+});


Reply via email to