fix(observer): hook-resolver — split combined matchers (Edit|Write)

Final-review followup. .claude/settings.json uses regex-style combined
matchers like "Edit|Write"; transcript writes per-tool PreToolUse:Edit.
Split on | when building map so per-tool counts resolve. Also sync
spec doc loadHookMap -> buildHookMap (impl name).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-23 13:49:42 +03:00
parent ed535b1626
commit 5222e109e0
4 changed files with 41 additions and 7 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Brain Status (auto-generated)
Last updated: 2026-05-23T10:39:02.076Z
Last updated: 2026-05-23T10:45:47.776Z
| Контролёр | Состояние | Детали |
|---|---|---|
@@ -23,7 +23,7 @@
**`tools/observer-hook-resolver.mjs`** (новый, ~80 LoC, pure)
```js
export function loadHookMap({ projectSettings, userSettings } = {})
export function buildHookMap({ projectSettings, userSettings } = {})
// Map<matcher, string[]>
// matcher: "PreToolUse:Bash" | "UserPromptSubmit" | "SessionStart:startup" | ...
// value: ["tools/observer-stop-hook.mjs", "inline:claude-md-guard-7f3a", ...]
@@ -117,7 +117,7 @@ Stop-hook → parser.parseTranscript(transcriptText)
├─ collectToolUse → skills[], counts, errors
├─ extractProcessEvents
│ ├─ hookCounts (matcher) ← из attachment.hookName
│ ├─ resolveScriptCounts(hookCounts, hookResolver.loadHookMap())
│ ├─ resolveScriptCounts(hookCounts, hookResolver.buildHookMap())
│ └─ event hook_fired = { counts, scripts, errors }
└─ primary_rationale
└─ recommended_node = skills.length === 0
+11 -4
View File
@@ -57,14 +57,21 @@ export function buildHookMap(projectSettings = {}, userSettings = {}) {
if (!Array.isArray(entries)) continue;
for (const entry of entries) {
if (!entry || typeof entry !== 'object') continue;
const matcher = entry.matcher ? `${event}:${entry.matcher}` : event;
const scripts = Array.isArray(entry.hooks) ? entry.hooks : [];
const existing = map.get(matcher) || [];
const scriptNames = [];
for (const h of scripts) {
if (!h || h.type !== 'command') continue;
existing.push(extractScriptName(h.command));
scriptNames.push(extractScriptName(h.command));
}
if (scriptNames.length === 0) continue;
const matcherKeys = entry.matcher
? String(entry.matcher).split('|').map((t) => `${event}:${t.trim()}`).filter(Boolean)
: [event];
for (const matcher of matcherKeys) {
const existing = map.get(matcher) || [];
existing.push(...scriptNames);
map.set(matcher, existing);
}
map.set(matcher, existing);
}
}
}
+27
View File
@@ -97,6 +97,33 @@ describe('buildHookMap', () => {
const map = buildHookMap(project, user);
expect(map.get('PreToolUse:Bash')).toEqual(['tools/a.mjs', 'tools/b.mjs']);
});
it('splits combined matcher "Edit|Write" into two map entries', () => {
const settings = {
hooks: {
PreToolUse: [
{ matcher: 'Edit|Write', hooks: [{ type: 'command', command: 'node tools/guard.mjs' }] },
],
},
};
const map = buildHookMap(settings);
expect(map.get('PreToolUse:Edit')).toEqual(['tools/guard.mjs']);
expect(map.get('PreToolUse:Write')).toEqual(['tools/guard.mjs']);
expect(map.get('PreToolUse:Edit|Write')).toBeUndefined();
});
it('trims whitespace around matchers split on |', () => {
const settings = {
hooks: {
PreToolUse: [
{ matcher: 'Edit | Write', hooks: [{ type: 'command', command: 'node tools/g.mjs' }] },
],
},
};
const map = buildHookMap(settings);
expect(map.get('PreToolUse:Edit')).toEqual(['tools/g.mjs']);
expect(map.get('PreToolUse:Write')).toEqual(['tools/g.mjs']);
});
});
describe('resolveScriptCounts', () => {