* test(#3839): docs hook tables must match surface registrations (failing first) * docs(#3839): hook tables say PreToolUse for validate-commit, SessionStart for session-state gsd-validate-commit.sh is registered PreToolUse (src/runtime-hooks-surface.cts; its exit-2 block IS the contract — a post-tool hook cannot prevent a commit) and gsd-session-state.sh is registered SessionStart (session orientation, not post-tool tracking). Both rows said PostToolUse in ARCHITECTURE.md and the three INVENTORY locales; the issue asked for a neighbouring-row scan, which is how the session-state row was found. All other rows in the four tables verify against the surface. * fix(#3839): review fold-ins — 10 more wrong rows in ko-KR/pt-BR/zh-CN, parser authority + drift pins Adversarial review found the same two wrong rows shipped in five more files the issue's table missed (ko-KR ARCHITECTURE+INVENTORY, pt-BR ARCHITECTURE+INVENTORY, zh-CN ARCHITECTURE) — all fixed; DOC_TABLES now covers all ten shipped tables. The parity parser unioned only the Kimi mirror list, silently exempting agent-isolation-guard (registered via the dynamic preToolEvent push): probes are now parsed too, with bare hook names resolved against hooks/ ground truth and dynamic event variables resolved to their canonical (non-Gemini) events; an exact-set pin replaces the loose size guard. allow-test-rule marker carries the issue ref; unverified-ceiling 280→281 (audited: the new marker is legitimate — the suite reads product docs whose text is the contract). * fix(#3839): register the hook-table parity suite in the docs-guard lane The new suite reads ten docs/ paths, so lint-docs-guard-registration requires it in the docs-guard registry — the first GREEN bench run caught the omission (the RED run's docs-guard failures were the same signal, previously misread as marker fallout). * chore(#3839): changeset fragment (pr number backfilled after PR creation) * chore(#3839): backfill changeset PR number (4041) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/jolly-badgers-cheer.md
Normal file
5
.changeset/jolly-badgers-cheer.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4041
|
||||
---
|
||||
the shipped hook tables documented gsd-validate-commit.sh as PostToolUse (it is registered PreToolUse — exit-2 blocking is its contract) and gsd-session-state.sh as PostToolUse (registered SessionStart); 18 wrong rows corrected across ARCHITECTURE.md and INVENTORY.md in en/ja-JP/zh-CN/ko-KR/pt-BR, with a new docs-vs-surface parity suite guarding all ten tables (#3839)
|
||||
@@ -295,8 +295,8 @@ Runtime hooks that integrate with the host AI agent:
|
||||
| `gsd-read-injection-scanner.js` | `PostToolUse` | Scans Read tool output for injected instructions in untrusted content |
|
||||
| `gsd-workflow-guard.js` | `PreToolUse` | Detects file edits outside GSD workflow context (advisory, opt-in via `hooks.workflow_guard`) |
|
||||
| `gsd-read-guard.js` | `PreToolUse` | Advisory guard preventing Edit/Write on files not yet read in the session |
|
||||
| `gsd-session-state.sh` | `PostToolUse` | Session state tracking for shell-based runtimes |
|
||||
| `gsd-validate-commit.sh` | `PostToolUse` | Commit validation for conventional commit enforcement |
|
||||
| `gsd-session-state.sh` | `SessionStart` | Session state tracking for shell-based runtimes |
|
||||
| `gsd-validate-commit.sh` | `PreToolUse` | Commit validation for conventional commit enforcement |
|
||||
| `gsd-phase-boundary.sh` | `PostToolUse` | Phase boundary detection for workflow transitions |
|
||||
|
||||
See [`docs/INVENTORY.md`](INVENTORY.md#hooks) for the authoritative hook roster.
|
||||
|
||||
@@ -712,8 +712,8 @@ Full listing: `hooks/`.
|
||||
| `gsd-write-guard.js` | `PreToolUse` | Hard-blocks a whole-file `Write` that catastrophically shrinks a curated `.planning/` artifact (ROADMAP.md, milestone roadmaps, STATE.md); override via the single-use sentinel `.planning/.gsd-allow-shrink` (workflow steps) or `GSD_ALLOW_PLANNING_SHRINK=1` (interactive) (#2255, fix 3 of #973) |
|
||||
| `gsd-config-reload.js` | `FileChanged` | Hot-reloads GSD config context when `.planning/config.json` changes mid-session (#770) |
|
||||
| `gsd-ensure-canonical-path.js` | `SessionStart` | Symlinks `~/.claude/gsd-core/{bin,contexts,references,templates,workflows}` to the plugin's bundled tree so `@~/.claude/gsd-core/...` includes resolve in marketplace plugin installs; no-op in classic installs, self-heals after `claude plugin update` (#997) |
|
||||
| `gsd-session-state.sh` | `PostToolUse` | Session-state tracking for shell-based runtimes |
|
||||
| `gsd-validate-commit.sh` | `PostToolUse` | Commit validation for conventional-commit enforcement |
|
||||
| `gsd-session-state.sh` | `SessionStart` | Session-state tracking for shell-based runtimes |
|
||||
| `gsd-validate-commit.sh` | `PreToolUse` | Commit validation for conventional-commit enforcement |
|
||||
| `gsd-phase-boundary.sh` | `PostToolUse` | Phase-boundary detection for workflow transitions |
|
||||
| `gsd-graphify-update.sh` | `PostToolUse` | Auto-rebuild knowledge graph after main HEAD advances (opt-in, default off — #3347) |
|
||||
| `gsd-node-runner.sh` | (helper) | Portable node resolver managed JS hook commands route through under `--portable-hooks`: install-time node path first, then `command -v node`, then well-known layouts — resolves at hook-fire time so a shared config root works in every environment (#3662) |
|
||||
|
||||
@@ -476,8 +476,8 @@
|
||||
| `gsd-worktree-path-guard.js` | `PreToolUse` | ワークツリールート外の絶対パスを持つ Edit/Write/MultiEdit をハードブロック(PR #579、#260) |
|
||||
| `gsd-agent-isolation-guard.js` | `PreToolUse` | プロジェクトの解決済みディスパッチ分離が `harness-worktree` の場合、ハーネス分離パラメータを欠く executor の `Agent()` ディスパッチをハードブロック(#3045) |
|
||||
| `gsd-write-guard.js` | `PreToolUse` | キュレーションされた `.planning/` アーティファクト(ROADMAP.md、マイルストーンロードマップ、STATE.md)を大幅に縮小するファイル全体の `Write` をハードブロック。使い捨てセンチネル `.planning/.gsd-allow-shrink`(ワークフローステップ)または `GSD_ALLOW_PLANNING_SHRINK=1`(対話時)でオーバーライド(#2255、#973 の修正 3) |
|
||||
| `gsd-session-state.sh` | `PostToolUse` | シェルベースランタイム向けのセッション状態追跡 |
|
||||
| `gsd-validate-commit.sh` | `PostToolUse` | Conventional Commit 適用のためのコミットバリデーション |
|
||||
| `gsd-session-state.sh` | `SessionStart` | シェルベースランタイム向けのセッション状態追跡 |
|
||||
| `gsd-validate-commit.sh` | `PreToolUse` | Conventional Commit 適用のためのコミットバリデーション |
|
||||
| `gsd-phase-boundary.sh` | `PostToolUse` | ワークフロー遷移のためのフェーズ境界検出 |
|
||||
| `gsd-graphify-update.sh` | `PostToolUse` | メイン HEAD が進んだ後にナレッジグラフを自動再ビルド(オプトイン、デフォルトオフ — #3347) |
|
||||
|
||||
|
||||
@@ -247,8 +247,8 @@ GSD 워크플로우에 thinking 클래스 모델(o3, o4-mini, Gemini 2.5 Pro)을
|
||||
| `gsd-read-injection-scanner.js` | `PostToolUse` | 신뢰할 수 없는 콘텐츠에서 주입된 지시 사항을 위한 Read 도구 출력 스캔 |
|
||||
| `gsd-workflow-guard.js` | `PreToolUse` | GSD 워크플로우 컨텍스트 외부의 파일 편집 감지 (자문적, `hooks.workflow_guard`를 통한 옵트인) |
|
||||
| `gsd-read-guard.js` | `PreToolUse` | 세션에서 아직 읽지 않은 파일에 Edit/Write를 방지하는 자문적 가드 |
|
||||
| `gsd-session-state.sh` | `PostToolUse` | 쉘 기반 런타임을 위한 세션 상태 추적 |
|
||||
| `gsd-validate-commit.sh` | `PostToolUse` | 컨벤셔널 커밋 시행을 위한 커밋 검증 |
|
||||
| `gsd-session-state.sh` | `SessionStart` | 쉘 기반 런타임을 위한 세션 상태 추적 |
|
||||
| `gsd-validate-commit.sh` | `PreToolUse` | 컨벤셔널 커밋 시행을 위한 커밋 검증 |
|
||||
| `gsd-phase-boundary.sh` | `PostToolUse` | 워크플로우 전환을 위한 단계 경계 감지 |
|
||||
|
||||
권위 있는 11개 훅 목록은 [`docs/INVENTORY.md`](INVENTORY.md#hooks-11-shipped)를 참조하라.
|
||||
|
||||
@@ -476,8 +476,8 @@
|
||||
| `gsd-worktree-path-guard.js` | `PreToolUse` | 워크트리 루트 외부의 절대 경로로 Edit/Write/MultiEdit를 하드 차단 (PR #579, #260) |
|
||||
| `gsd-agent-isolation-guard.js` | `PreToolUse` | 프로젝트의 해석된 디스패치 격리가 `harness-worktree`일 때 하네스 격리 매개변수가 누락된 executor `Agent()` 디스패치를 하드 차단 (#3045) |
|
||||
| `gsd-write-guard.js` | `PreToolUse` | 큐레이션된 `.planning/` 아티팩트(ROADMAP.md, 마일스톤 로드맵, STATE.md)를 치명적으로 축소하는 전체 파일 `Write`를 하드 차단. 일회용 센티널 `.planning/.gsd-allow-shrink`(워크플로 단계) 또는 `GSD_ALLOW_PLANNING_SHRINK=1`(대화형)로 우회 가능 (#2255, #973의 수정 3) |
|
||||
| `gsd-session-state.sh` | `PostToolUse` | 셸 기반 런타임을 위한 세션 상태 추적 |
|
||||
| `gsd-validate-commit.sh` | `PostToolUse` | 컨벤셔널 커밋 적용을 위한 커밋 검증 |
|
||||
| `gsd-session-state.sh` | `SessionStart` | 셸 기반 런타임을 위한 세션 상태 추적 |
|
||||
| `gsd-validate-commit.sh` | `PreToolUse` | 컨벤셔널 커밋 적용을 위한 커밋 검증 |
|
||||
| `gsd-phase-boundary.sh` | `PostToolUse` | 워크플로우 전환을 위한 단계 경계 감지 |
|
||||
| `gsd-graphify-update.sh` | `PostToolUse` | 메인 HEAD 진행 후 지식 그래프 자동 재빌드 (옵트인, 기본 비활성화 — #3347) |
|
||||
|
||||
|
||||
@@ -262,8 +262,8 @@ Hooks de runtime que se integram ao agente de IA anfitrião:
|
||||
| `gsd-read-injection-scanner.js` | `PostToolUse` | Escaneia saídas da ferramenta Read em busca de instruções injetadas em conteúdo não confiável |
|
||||
| `gsd-workflow-guard.js` | `PreToolUse` | Detecta edições de arquivos fora do contexto de workflow do GSD (consultivo, ativado via `hooks.workflow_guard`) |
|
||||
| `gsd-read-guard.js` | `PreToolUse` | Guarda consultivo que impede Edit/Write em arquivos ainda não lidos na sessão |
|
||||
| `gsd-session-state.sh` | `PostToolUse` | Rastreamento de estado de sessão para runtimes baseados em shell |
|
||||
| `gsd-validate-commit.sh` | `PostToolUse` | Validação de commit para aplicação de commits convencionais |
|
||||
| `gsd-session-state.sh` | `SessionStart` | Rastreamento de estado de sessão para runtimes baseados em shell |
|
||||
| `gsd-validate-commit.sh` | `PreToolUse` | Validação de commit para aplicação de commits convencionais |
|
||||
| `gsd-phase-boundary.sh` | `PostToolUse` | Detecção de limite de fase para transições de workflow |
|
||||
|
||||
Consulte [`docs/INVENTORY.md`](INVENTORY.md#hooks-11-shipped) para o roster oficial de 11 hooks.
|
||||
|
||||
@@ -476,8 +476,8 @@ Listagem completa: `hooks/`.
|
||||
| `gsd-worktree-path-guard.js` | `PreToolUse` | Bloqueia rigorosamente Edit/Write/MultiEdit com caminhos absolutos fora da raiz do worktree (PR #579, #260) |
|
||||
| `gsd-agent-isolation-guard.js` | `PreToolUse` | Bloqueia rigorosamente um dispatch `Agent()` de executor que não tenha o parâmetro de isolamento do harness quando o isolamento de dispatch resolvido do projeto é `harness-worktree` (#3045) |
|
||||
| `gsd-write-guard.js` | `PreToolUse` | Bloqueia rigorosamente um `Write` de arquivo inteiro que encolhe catastroficamente um artefato curado de `.planning/` (ROADMAP.md, roadmaps de milestone, STATE.md); override via o sentinela de uso único `.planning/.gsd-allow-shrink` (passos de workflow) ou `GSD_ALLOW_PLANNING_SHRINK=1` (interativo) (#2255, correção 3 de #973) |
|
||||
| `gsd-session-state.sh` | `PostToolUse` | Rastreamento de estado de sessão para runtimes baseados em shell |
|
||||
| `gsd-validate-commit.sh` | `PostToolUse` | Validação de commit para aplicação de conventional-commit |
|
||||
| `gsd-session-state.sh` | `SessionStart` | Rastreamento de estado de sessão para runtimes baseados em shell |
|
||||
| `gsd-validate-commit.sh` | `PreToolUse` | Validação de commit para aplicação de conventional-commit |
|
||||
| `gsd-phase-boundary.sh` | `PostToolUse` | Detecção de limite de fase para transições de workflow |
|
||||
| `gsd-graphify-update.sh` | `PostToolUse` | Reconstrução automática do grafo de conhecimento após o avanço do HEAD principal (opt-in, padrão desativado — #3347) |
|
||||
|
||||
|
||||
@@ -247,8 +247,8 @@ GSD Core 是一个**元提示框架**,位于用户与 AI 编码 Agent(Claude
|
||||
| `gsd-read-injection-scanner.js` | `PostToolUse` | 扫描 Read 工具输出中不受信任内容里的注入指令 |
|
||||
| `gsd-workflow-guard.js` | `PreToolUse` | 检测 GSD 工作流上下文之外的文件编辑(建议性,通过 `hooks.workflow_guard` 选择启用) |
|
||||
| `gsd-read-guard.js` | `PreToolUse` | 建议性防护,防止对本会话中尚未读取的文件执行 Edit/Write |
|
||||
| `gsd-session-state.sh` | `PostToolUse` | 基于 shell 的运行时的会话状态跟踪 |
|
||||
| `gsd-validate-commit.sh` | `PostToolUse` | 用于规范提交格式执行的提交验证 |
|
||||
| `gsd-session-state.sh` | `SessionStart` | 基于 shell 的运行时的会话状态跟踪 |
|
||||
| `gsd-validate-commit.sh` | `PreToolUse` | 用于规范提交格式执行的提交验证 |
|
||||
| `gsd-phase-boundary.sh` | `PostToolUse` | 工作流转换的阶段边界检测 |
|
||||
|
||||
请参阅 [`docs/INVENTORY.md`](INVENTORY.md#hooks-11-shipped) 获取权威的 11 个 hook 列表。
|
||||
|
||||
@@ -476,8 +476,8 @@
|
||||
| `gsd-worktree-path-guard.js` | `PreToolUse` | 硬性阻止对 worktree 根目录之外绝对路径执行 Edit/Write/MultiEdit(PR #579,#260) |
|
||||
| `gsd-agent-isolation-guard.js` | `PreToolUse` | 当项目解析出的调度隔离模式为 `harness-worktree` 时,硬性阻止缺少隔离参数的 executor `Agent()` 调度(#3045) |
|
||||
| `gsd-write-guard.js` | `PreToolUse` | 硬性阻止将精选的 `.planning/` 工件(ROADMAP.md、里程碑路线图、STATE.md)灾难性缩减的整文件 `Write`;可通过一次性哨兵文件 `.planning/.gsd-allow-shrink`(工作流步骤)或 `GSD_ALLOW_PLANNING_SHRINK=1`(交互式)覆盖(#2255,#973 的修复 3) |
|
||||
| `gsd-session-state.sh` | `PostToolUse` | 基于 shell 运行时的会话状态跟踪 |
|
||||
| `gsd-validate-commit.sh` | `PostToolUse` | 常规提交强制执行的提交验证 |
|
||||
| `gsd-session-state.sh` | `SessionStart` | 基于 shell 运行时的会话状态跟踪 |
|
||||
| `gsd-validate-commit.sh` | `PreToolUse` | 常规提交强制执行的提交验证 |
|
||||
| `gsd-phase-boundary.sh` | `PostToolUse` | 工作流过渡的阶段边界检测 |
|
||||
| `gsd-graphify-update.sh` | `PostToolUse` | 在主 HEAD 推进后自动重建知识图谱(可选启用,默认关闭 — #3347) |
|
||||
|
||||
|
||||
@@ -215,6 +215,20 @@ const DOCS_GUARD_TESTS = {
|
||||
// Walks docs/*.md and every docs/<locale>/*.md dir dynamically
|
||||
// (docs-parity-live-registry.test.cjs:42, 428) — deliberately generic.
|
||||
'tests/docs-parity-live-registry.test.cjs': ['*'],
|
||||
// #3839: pins every hook-table Event cell in the five locales' ARCHITECTURE
|
||||
// and INVENTORY files against src/runtime-hooks-surface.cts registrations.
|
||||
'tests/docs-hooks-table-parity.test.cjs': [
|
||||
'docs/ARCHITECTURE.md',
|
||||
'docs/INVENTORY.md',
|
||||
'docs/ja-JP/ARCHITECTURE.md',
|
||||
'docs/ja-JP/INVENTORY.md',
|
||||
'docs/zh-CN/ARCHITECTURE.md',
|
||||
'docs/zh-CN/INVENTORY.md',
|
||||
'docs/ko-KR/ARCHITECTURE.md',
|
||||
'docs/ko-KR/INVENTORY.md',
|
||||
'docs/pt-BR/ARCHITECTURE.md',
|
||||
'docs/pt-BR/INVENTORY.md',
|
||||
],
|
||||
'tests/docs-state-md-locale-parity.test.cjs': [
|
||||
'docs/reference/state-md.md',
|
||||
'docs/ja-JP/reference/state-md.md',
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"maxFiles": 280
|
||||
"maxFiles": 281
|
||||
}
|
||||
|
||||
@@ -245,11 +245,12 @@
|
||||
},
|
||||
"docs": {
|
||||
"files": [
|
||||
"docs-hooks-table-parity.test.cjs",
|
||||
"docs-parity-live-registry.test.cjs",
|
||||
"docs-state-md-locale-parity.test.cjs",
|
||||
"docs-update.test.cjs"
|
||||
],
|
||||
"issue": "3873",
|
||||
"issue": "3873 | #3839 \u2014 the hook-event parity suite cross-checks the four docs hook tables against src/runtime-hooks-surface.cts registrations; a generated-docs concern distinct from content freshness and registry parity",
|
||||
"justification": "ADR-3473 \u00a78.8 makes docs/reference/state-md.md and its four locale siblings generated-and-committed and adds the repo's first locale-parity check (verified: no locale-parity lint precedent exists here). That check is a distinct concern from docs-update (content freshness) and docs-parity-live-registry (registry parity); folding it into either would place an unrelated subject inside them purely to satisfy a count. The section it guards is absent from all four translations today and documents the status enum behind #3853."
|
||||
},
|
||||
"audit": {
|
||||
|
||||
170
tests/docs-hooks-table-parity.test.cjs
Normal file
170
tests/docs-hooks-table-parity.test.cjs
Normal file
@@ -0,0 +1,170 @@
|
||||
// allow-test-rule: source-text-is-the-product (#3839)
|
||||
// Reads the docs hook tables and the hook-surface source whose registrations
|
||||
// ARE the deployed contract — asserting the docs rows match the surface.
|
||||
|
||||
/**
|
||||
* Docs hook-table parity — docs-hooks-table-parity.test.cjs
|
||||
*
|
||||
* #3839: the shipped hook tables documented `gsd-validate-commit.sh` as
|
||||
* PostToolUse when it is registered PreToolUse (a PreToolUse hook BLOCKS a
|
||||
* commit via exit 2; a PostToolUse hook cannot — the documented event
|
||||
* misdescribes the hook's entire contract). The same scan found
|
||||
* `gsd-session-state.sh` documented PostToolUse while registered
|
||||
* SessionStart. Nine files carried the wrong rows (ARCHITECTURE.md +
|
||||
* INVENTORY.md × en, ja-JP, zh-CN, ko-KR, pt-BR).
|
||||
*
|
||||
* Truth source: the literal hook-spec array in `buildKimiHooksTomlBlock`
|
||||
* (src/runtime-hooks-surface.cts) — its own comment pins the invariant
|
||||
* "mirrors applySettingsJsonHooks' settings.json wiring 1:1" — unioned with
|
||||
* literal-event probe lines (`settings.hooks.<Event>.some(…
|
||||
* referencesHook(…, 'gsd-…'))`). Registrations written through the
|
||||
* runtime-resolved variables (`preToolEvent`/`postToolEvent`) are NOT
|
||||
* statically parseable and are covered only via the mirror invariant; hooks
|
||||
* registered on other surfaces (statusline, plugin-surface) are out of
|
||||
* scope. Docs rows are exempt when their Event cell is not exactly one
|
||||
* registered event (multi-event `A` / `B` cells, `statusLine`, `(helper)`,
|
||||
* host-native names) — note docs/how-to/install-on-your-runtime.md also
|
||||
* documents these mappings in transposed event-first tables, which this
|
||||
* hook-first row parser intentionally does not read.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const SURFACE_PATH = path.join(ROOT, 'src', 'runtime-hooks-surface.cts');
|
||||
|
||||
const DOC_TABLES = [
|
||||
'docs/ARCHITECTURE.md',
|
||||
'docs/INVENTORY.md',
|
||||
'docs/ja-JP/ARCHITECTURE.md',
|
||||
'docs/ja-JP/INVENTORY.md',
|
||||
'docs/zh-CN/ARCHITECTURE.md',
|
||||
'docs/zh-CN/INVENTORY.md',
|
||||
'docs/ko-KR/ARCHITECTURE.md',
|
||||
'docs/ko-KR/INVENTORY.md',
|
||||
'docs/pt-BR/ARCHITECTURE.md',
|
||||
'docs/pt-BR/INVENTORY.md',
|
||||
];
|
||||
|
||||
// The exact basename set the surface parser must resolve. If a registration
|
||||
// disappears or the parser drifts, this pin fails instead of the parity
|
||||
// checks silently narrowing to a subset.
|
||||
const EXPECTED_SURFACE_HOOKS = [
|
||||
'gsd-agent-isolation-guard.js',
|
||||
'gsd-check-update.js',
|
||||
'gsd-config-reload.js',
|
||||
'gsd-context-monitor.js',
|
||||
'gsd-graphify-update.sh',
|
||||
'gsd-phase-boundary.sh',
|
||||
'gsd-prompt-guard.js',
|
||||
'gsd-read-guard.js',
|
||||
'gsd-read-injection-scanner.js',
|
||||
'gsd-session-state.sh',
|
||||
'gsd-validate-commit.sh',
|
||||
'gsd-workflow-guard.js',
|
||||
'gsd-worktree-path-guard.js',
|
||||
'gsd-write-guard.js',
|
||||
];
|
||||
|
||||
/** hook basename → Set of events it is registered under on this surface. */
|
||||
function registeredHookEvents() {
|
||||
const src = fs.readFileSync(SURFACE_PATH, 'utf8');
|
||||
const map = new Map();
|
||||
// Probe lines name hooks WITHOUT file extensions (`'gsd-session-state'`).
|
||||
// Resolve a bare name against the actual hooks/ directory (the file's
|
||||
// extension is ground truth) so both spellings resolve to one entry.
|
||||
const resolveBase = (name) => {
|
||||
if (/\.(sh|js|cmd)$/.test(name)) return name;
|
||||
for (const ext of ['.js', '.sh', '.cmd']) {
|
||||
if (fs.existsSync(path.join(ROOT, 'hooks', `${name}${ext}`))) return `${name}${ext}`;
|
||||
}
|
||||
return name; // no file twin (e.g. a name never documented in docs tables)
|
||||
};
|
||||
const add = (base, event) => {
|
||||
const key = resolveBase(base);
|
||||
if (!map.has(key)) map.set(key, new Set());
|
||||
map.get(key).add(event);
|
||||
};
|
||||
let m;
|
||||
// Literal hook-spec array (the Kimi mirror of the settings.json wiring).
|
||||
const specRe = /event:\s*'([A-Za-z]+)',\s*command:\s*cmd\('([^']+)'\)/g;
|
||||
while ((m = specRe.exec(src)) !== null) add(path.basename(m[2]), m[1]);
|
||||
// Probe lines paired with a literal event:
|
||||
// settings.hooks.<Event>.some(… referencesHook(…, '<name>'))
|
||||
const probeRe = /settings\.hooks\.([A-Za-z]+)\.some\(\(entry: HookGroup\) =>\s*\n\s*entry\.hooks && entry\.hooks\.some\(\(h: HookEntry\) => referencesHook\(h as Record<string, unknown>, '([^']+)'\)/g;
|
||||
while ((m = probeRe.exec(src)) !== null) add(m[2], m[1]);
|
||||
// Probe lines paired with the runtime-resolved variables — statically
|
||||
// resolved to their non-Gemini canonical events (docs document the
|
||||
// canonical Claude/GS wiring; BeforeTool/AfterTool are the Gemini twins):
|
||||
// const preToolEvent = hookEvents === 'gemini' ? 'BeforeTool' : 'PreToolUse'
|
||||
const dynRe = /settings\.hooks\[(preToolEvent|postToolEvent)\]\.some\(\(entry: HookGroup\) =>\s*\n\s*entry\.hooks && entry\.hooks\.some\(\(h: HookEntry\) => referencesHook\(h as Record<string, unknown>, '([^']+)'\)/g;
|
||||
while ((m = dynRe.exec(src)) !== null) add(m[2], m[1] === 'preToolEvent' ? 'PreToolUse' : 'PostToolUse');
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Hook-table rows: [basename, eventCell] for `gsd-*` hooks. */
|
||||
function docHookRows(docPath) {
|
||||
const lines = fs.readFileSync(path.join(ROOT, docPath), 'utf8').split(/\r?\n/);
|
||||
const rows = [];
|
||||
for (const line of lines) {
|
||||
const m = line.match(/^\|\s*`?(gsd-[a-z0-9-]+\.(?:sh|js|cmd))`?\s*\|\s*`([^`]+)`\s*\|/);
|
||||
if (m) rows.push([m[1], m[2]]);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Cells that are not a single surface event: multi-event (`A` / `B`),
|
||||
// non-event identifiers (statusLine, host-native names), or placeholders.
|
||||
const SINGLE_EVENT_CELL = /^[A-Z][A-Za-z]+$/;
|
||||
|
||||
describe('docs hook tables match runtime-hooks-surface registrations', () => {
|
||||
const surface = registeredHookEvents();
|
||||
|
||||
test('surface parser resolves the exact expected hook set (no silent drift)', () => {
|
||||
const withExt = [...surface.keys()].filter((k) => /\.(sh|js|cmd)$/.test(k)).sort();
|
||||
assert.deepEqual(
|
||||
withExt,
|
||||
[...EXPECTED_SURFACE_HOOKS].sort(),
|
||||
'parser must resolve exactly the expected surface registrations'
|
||||
);
|
||||
});
|
||||
|
||||
for (const doc of DOC_TABLES) {
|
||||
test(`${doc}: every single-event hook row matches the surface`, () => {
|
||||
const rows = docHookRows(doc);
|
||||
assert.ok(rows.length >= 3, `${doc}: hook-table parser found rows (guard against silent table drift)`);
|
||||
const mismatches = [];
|
||||
for (const [base, cell] of rows) {
|
||||
const events = surface.get(base);
|
||||
if (!events) continue; // not registered on this surface (statusline, plugin-surface hooks…)
|
||||
if (!SINGLE_EVENT_CELL.test(cell)) continue; // multi-event or non-event cell
|
||||
if (!events.has(cell)) {
|
||||
mismatches.push(`${base}: docs say \`${cell}\`, surface registers ${[...events].join(', ')}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(mismatches, [], `docs rows must match src/runtime-hooks-surface.cts (#3839)`);
|
||||
});
|
||||
}
|
||||
|
||||
test('#3839 regression pin: the two misdocumented hooks are asserted directly', () => {
|
||||
assert.ok(surface.get('gsd-validate-commit.sh').has('PreToolUse'),
|
||||
'gsd-validate-commit.sh blocks commits — must stay PreToolUse on the surface');
|
||||
assert.ok(surface.get('gsd-session-state.sh').has('SessionStart'),
|
||||
'gsd-session-state.sh orients the session — must stay SessionStart on the surface');
|
||||
// ja-JP/ARCHITECTURE.md's hook table is a shorter translation that never
|
||||
// listed these two hooks — it stays in the parity loop above but not here.
|
||||
const PINNED = DOC_TABLES.filter((d) => d !== 'docs/ja-JP/ARCHITECTURE.md');
|
||||
for (const doc of PINNED) {
|
||||
const rows = docHookRows(doc);
|
||||
const vc = rows.find(([b]) => b === 'gsd-validate-commit.sh');
|
||||
assert.ok(vc, `${doc}: validate-commit row present`);
|
||||
assert.equal(vc[1], 'PreToolUse', `${doc}: validate-commit documented as PreToolUse`);
|
||||
const ss = rows.find(([b]) => b === 'gsd-session-state.sh');
|
||||
assert.ok(ss, `${doc}: session-state row present`);
|
||||
assert.equal(ss[1], 'SessionStart', `${doc}: session-state documented as SessionStart`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user