feat: brain-config classifyFilePath стемы нормативки config-driven — greenfield #3 observer

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-16 12:23:40 +03:00
parent c19941aea0
commit 318add8fa0
4 changed files with 128 additions and 11 deletions
+18 -8
View File
@@ -357,7 +357,16 @@ function collectToolResultText(turn) {
// Pass 3 — path-pattern classifier (project-brain-factor-analysis-4passes).
// Returns one of: test / config / spec / norm / data / src / other.
// Priority order matters (test before src, norm before src, etc).
export function classifyFilePath(path) {
//
// Greenfield #3-observer: project-normative stems are config-driven (design
// 2026-06-16-greenfield-regex-names-config-design §5). Default = the three Лидерра
// stems → byte-identical to the previous hardcoded patterns; a greenfield project
// supplies its own via loadConfig().normative_files → docStem (wired in observer-stop-hook).
export const DEFAULT_NORMATIVE_STEMS = Object.freeze(['Pravila_raboty_Claude', 'Plugin_stack_rules', 'Tooling']);
const _escRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export function classifyFilePath(path, normativeStems = DEFAULT_NORMATIVE_STEMS) {
if (!path) return 'other';
const p = String(path).replace(/\\/g, '/');
const base = p.split('/').pop() || p;
@@ -366,11 +375,12 @@ export function classifyFilePath(path) {
if (/\.(?:test|spec)\.[a-z0-9]+$/i.test(base)) return 'test';
if (/(?:^|\/)(?:tests?|spec)\//i.test(p)) return 'test';
// 2. normative documents (CLAUDE.md / Pravila / PSR / Tooling / Открытые_вопросы / memory store).
// 2. normative documents. Universal docs (CLAUDE.md / Открытые_вопросы / MEMORY.md / memory
// store) stay hardcoded; project-normative docs match config stems (default = Лидерра quintet).
if (/(?:^|\/)CLAUDE\.md$/i.test(p)) return 'norm';
if (/(?:^|\/)Pravila_raboty_Claude[^/]*\.md$/i.test(p)) return 'norm';
if (/(?:^|\/)Plugin_stack_rules[^/]*\.md$/i.test(p)) return 'norm';
if (/(?:^|\/)Tooling[^/]*\.md$/i.test(p)) return 'norm';
for (const stem of (normativeStems || [])) {
if (stem && new RegExp(`(?:^|/)${_escRe(stem)}[^/]*\\.md$`, 'i').test(p)) return 'norm';
}
if (/(?:^|\/)Открытые_вопросы[^/]*\.md$/i.test(p)) return 'norm';
if (/(?:^|\/)MEMORY\.md$/i.test(p)) return 'norm';
if (/\/memory\/[^/]+\.md$/i.test(p)) return 'norm';
@@ -395,10 +405,10 @@ export function classifyFilePath(path) {
const FILE_TYPE_CATEGORIES = ['src', 'test', 'config', 'spec', 'norm', 'data', 'other'];
export function extractFileTypeDistribution(files) {
export function extractFileTypeDistribution(files, normativeStems = DEFAULT_NORMATIVE_STEMS) {
const dist = Object.fromEntries(FILE_TYPE_CATEGORIES.map((c) => [c, 0]));
for (const f of files || []) {
dist[classifyFilePath(f)] += 1;
dist[classifyFilePath(f, normativeStems)] += 1;
}
return dist;
}
@@ -933,7 +943,7 @@ export function parseTranscript(transcriptText, fallbackSessionId = null, option
return {
prompt_length_chars: typeof prompt === 'string' ? prompt.length : 0,
mcp_servers_used: extractMcpServers(turn),
file_type_distribution: extractFileTypeDistribution(ts.files),
file_type_distribution: extractFileTypeDistribution(ts.files, options.normativeStems),
};
})(),
classifier_output: _classifierOutput,