meta(wiki): ingest concepts/interns-grep-audit-design in OpeItcLoc03/claude-skills
This commit is contained in:
190
.wiki/concepts/interns-grep-audit-design.md
Normal file
190
.wiki/concepts/interns-grep-audit-design.md
Normal file
@@ -0,0 +1,190 @@
|
||||
---
|
||||
date: '2026-05-22'
|
||||
status: design-approved
|
||||
parent: concepts/interns-design.md
|
||||
source_buffer: .workshop/.brainstorm/interns.md
|
||||
title: interns-grep-audit-design
|
||||
type: concept
|
||||
ingested_at: '2026-05-22T04:16:29.503Z'
|
||||
ingested_by: OpeItcLoc03@DESKTOP-NSEF0UK
|
||||
source_project: OpeItcLoc03/workshop
|
||||
---
|
||||
# Interns — `grep_audit` intern (v0.1.0)
|
||||
|
||||
Расширение каталога `interns-mcp`: добавляет интерн `grep_audit`, специализированный на структурированном grep по списку путей с матрицей паттернов. Особенность — **детерминированный**, **без LLM-вызова**: server применяет `re` локально, endpoint Ollama Cloud не вызывается ни в каком режиме. Нулевая цена, нулевая hallucination-граница.
|
||||
|
||||
## Context
|
||||
|
||||
Pain-point всплыл при аудите 13 CLAUDE.md на 6 канонических триггер-строк в bootstrap-rollout сессии 2026-05-07. Делалось через `bulk_text_read` с длинной формулировкой «вот колонки, вот формат, вот сортировка». Шаблонная задача — выдать матрицу N×M по бинарному условию contains/not-contains. Интерн со специализированной сигнатурой убирает 80% текста запроса.
|
||||
|
||||
Решение «без LLM вообще» зафиксировано 2026-05-21: семантический матч (если когда-нибудь возникнет pain-point) — отдельный путь через `bulk_text_read` с вопросом, а не режим `grep_audit`. Caller не держит в голове «иногда детерминированно, иногда нет» — граница проведена между интернами, не внутри одного.
|
||||
|
||||
Это первый **LLM-free** интерн в каталоге — паттерн для будущих детерминированных тулзов (потенциально `path_classify`, `json_extract`, если pain-point всплывёт; те отброшены в текущем раунде как дублирующие jq/grep_audit).
|
||||
|
||||
## Decisions
|
||||
|
||||
| # | Решение | Аргумент |
|
||||
|---|---|---|
|
||||
| 1 | Шейп — `grep_audit(paths, patterns, output, case_sensitive) → matrix` | Структурированный matrix-output, не Q&A. Симметрия с другими интернами нарушена сознательно — это аудит, не вопрос. |
|
||||
| 2 | Без LLM на back-end совсем | substring/regex детерминирован, `re` локально достаточен. Cheaper, точнее, нулевая hallucination. |
|
||||
| 3 | `patterns: list[str \| dict]` — либо substring, либо `{pattern, name, regex?}` | Простой случай (substring) — одна строка; сложный (named regex для читаемой матрицы) — dict. |
|
||||
| 4 | Always-ask политика единообразна для всех интернов | Несмотря на отсутствие endpoint-вызова, server открывает файл. Caller не должен различать «безопасный/небезопасный» интерн. |
|
||||
| 5 | Routing в `using-interns/SKILL.md` явно фиксирует «детерминированный, no LLM, zero cost, zero hallucination» | Каталог не-гомогенный (один интерн без LLM, остальные с) — это надо явно проговорить чтобы Claude не путался. |
|
||||
| 6 | `output: "table" \| "json"` — default `"table"` | Markdown-таблица для human-readable аудитов; JSON для programmatic consumption. |
|
||||
|
||||
## Сигнатура
|
||||
|
||||
```python
|
||||
grep_audit(
|
||||
paths: list[str], # absolute file paths
|
||||
patterns: list[str | dict], # str = substring; dict = {pattern, name, regex?}
|
||||
output: Literal["table", "json"] = "table",
|
||||
case_sensitive: bool = True,
|
||||
) -> InternResponse
|
||||
```
|
||||
|
||||
`InternResponse = {text: str, usage: {files_scanned, patterns_evaluated, matches_total}}`
|
||||
|
||||
Output shape:
|
||||
- `"table"`: markdown-таблица, rows = paths, cols = pattern names (или `pattern` если `name` не задан), ячейки ✅/❌.
|
||||
- `"json"`: `{"rows": [{path, matches: {<pattern_name>: bool}}]}`.
|
||||
|
||||
При файле-не-найден / unreadable — соответствующая ячейка `null` в JSON, `⚠️` в table; не abort всего вызова (аудит идёт по N путям, partial-result полезнее total fail).
|
||||
|
||||
## Реализация (`interns_mcp/interns/grep_audit.py`)
|
||||
|
||||
```python
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from .base import Intern, InternResponse
|
||||
from .. import safety
|
||||
|
||||
|
||||
class GrepAudit(Intern):
|
||||
id = "grep_audit"
|
||||
|
||||
def run(
|
||||
self,
|
||||
paths: list[str],
|
||||
patterns: list[str | dict],
|
||||
output: Literal["table", "json"] = "table",
|
||||
case_sensitive: bool = True,
|
||||
) -> InternResponse:
|
||||
safety.check_paths(paths) # raise if always-ask
|
||||
|
||||
compiled = _compile_patterns(patterns, case_sensitive)
|
||||
|
||||
rows = []
|
||||
matches_total = 0
|
||||
for p in paths:
|
||||
try:
|
||||
text = Path(p).read_text(encoding="utf-8", errors="replace")
|
||||
except (FileNotFoundError, PermissionError, IsADirectoryError) as exc:
|
||||
rows.append({"path": p, "matches": {c["name"]: None for c in compiled}, "error": str(exc)})
|
||||
continue
|
||||
row_matches = {}
|
||||
for c in compiled:
|
||||
hit = bool(c["matcher"](text))
|
||||
row_matches[c["name"]] = hit
|
||||
if hit:
|
||||
matches_total += 1
|
||||
rows.append({"path": p, "matches": row_matches})
|
||||
|
||||
rendered = (
|
||||
json.dumps({"rows": rows}, ensure_ascii=False, indent=2)
|
||||
if output == "json"
|
||||
else _render_table(rows, [c["name"] for c in compiled])
|
||||
)
|
||||
|
||||
return InternResponse(
|
||||
text=rendered,
|
||||
usage={
|
||||
"files_scanned": len(paths),
|
||||
"patterns_evaluated": len(compiled),
|
||||
"matches_total": matches_total,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _compile_patterns(patterns, case_sensitive):
|
||||
out = []
|
||||
for p in patterns:
|
||||
if isinstance(p, str):
|
||||
out.append({"name": p, "matcher": _substring(p, case_sensitive)})
|
||||
continue
|
||||
name = p.get("name", p["pattern"])
|
||||
if p.get("regex"):
|
||||
flags = 0 if case_sensitive else re.IGNORECASE
|
||||
out.append({"name": name, "matcher": re.compile(p["pattern"], flags).search})
|
||||
else:
|
||||
out.append({"name": name, "matcher": _substring(p["pattern"], case_sensitive)})
|
||||
return out
|
||||
|
||||
|
||||
def _substring(needle, case_sensitive):
|
||||
if case_sensitive:
|
||||
return lambda text: needle in text
|
||||
n = needle.lower()
|
||||
return lambda text: n in text.lower()
|
||||
|
||||
|
||||
def _render_table(rows, names):
|
||||
header = "| Path | " + " | ".join(names) + " |"
|
||||
sep = "|" + "---|" * (len(names) + 1)
|
||||
lines = [header, sep]
|
||||
for row in rows:
|
||||
cells = []
|
||||
for n in names:
|
||||
v = row["matches"][n]
|
||||
cells.append("⚠️" if v is None else ("✅" if v else "❌"))
|
||||
lines.append(f"| `{row['path']}` | " + " | ".join(cells) + " |")
|
||||
return "\n".join(lines)
|
||||
```
|
||||
|
||||
**NB по базовому классу:** `Intern` base class должен пропустить интерны без `endpoint`/`model` — либо `GrepAudit` overrides `__init__`/`__call__`, либо base поддерживает `endpoint=null` без инициализации LLM-client. Решение — в impl-таске; рекомендую второй вариант (открывает дорогу другим детерминированным интернам).
|
||||
|
||||
## Layer 1 — config (`.common/config/interns/config.yaml`)
|
||||
|
||||
```yaml
|
||||
interns:
|
||||
grep_audit:
|
||||
description: "Deterministic grep matrix over N paths × M patterns. No LLM call, no endpoint cost."
|
||||
endpoint: null # local, no LLM call
|
||||
# No model / max_tokens / temperature / system_prompt — этот интерн без LLM.
|
||||
```
|
||||
|
||||
## Layer 3 — skill update (`using-interns/SKILL.md`)
|
||||
|
||||
Routing-подсказки, добавить:
|
||||
|
||||
> - **`grep_audit`** — аудит N путей × M паттернов (substring или regex). Детерминированный, без LLM-вызова, zero cost, zero hallucination boundary. Использовать когда нужна матрица contains/not-contains: проверка набора CLAUDE.md / SKILL.md / frontmatter полей на присутствие канонических строк. Возвращает markdown-таблицу (default) или JSON.
|
||||
> - **`bulk_text_read` vs `grep_audit`:** первый — Q&A над несколькими файлами (LLM-summary); второй — детерминированная проверка contains/not-contains. Семантический матч — это `bulk_text_read` с вопросом, не `grep_audit`.
|
||||
> - Always-ask paths применяются единообразно с остальными интернами (server всё равно открывает файл, даже без LLM-вызова).
|
||||
|
||||
Bump `using-interns` MINOR (capability added — новый интерн в routing-таблице).
|
||||
|
||||
## Cross-platform
|
||||
|
||||
| Слой | Windows | Linux | macOS |
|
||||
|---|---|---|---|
|
||||
| `Path.read_text(encoding="utf-8", errors="replace")` | ✅ | ✅ | ✅ |
|
||||
| `re` substring/regex | ✅ | ✅ | ✅ |
|
||||
| Always-ask `PurePath.match` | ✅ POSIX-style globs работают везде | ✅ | ✅ |
|
||||
|
||||
Полностью pure-Python, без subprocess/CLI зависимостей.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Cap на size файла** (e.g., 5 MB)? Сейчас server читает целиком в память. Если bytecode/blob случайно попадёт в paths — RAM-spike. Добавить если pain-point всплывёт; пока YAGNI.
|
||||
- **Batch-mode** (несколько output forms за один вызов)? YAGNI.
|
||||
- **Counting matches** (не bool, а number-of-occurrences)? Сейчас shape — boolean matrix. Если понадобится counts — расширение `output: "counts"`. Решение откладывается.
|
||||
|
||||
## References
|
||||
|
||||
- `concepts/interns-design.md` — parent design (архитектура interns-mcp).
|
||||
- `concepts/interns-repo-read-design.md` — sibling intern (с LLM-вызовом, для сравнения паттерна).
|
||||
- `using-interns/SKILL.md` — target file для routing-добавки.
|
||||
- `.workshop/.archive/2026-05-22-grep-audit-extract.md` — process trace (extract из living-catalog).
|
||||
Reference in New Issue
Block a user