feat(rul,cml): durability findings — config lost after compaction (v5.5.0 / A)
Per the official "what survives compaction" model (context-window.md): only the project-root CLAUDE.md (+ unscoped rules) is re-injected after a context compaction. Two additive, structural findings (low severity): - RUL: a large (>50-line) PATH-SCOPED rule — reloads only on a matching file read, and is not re-injected after compaction, so must-hold rules can silently drop mid-session. - CML: a NESTED (subdirectory) CLAUDE.md — not re-injected after compaction (only the project root is). Additive (no new scanner, count stays 13); one humanizer entry each; hermetic temp-fixture tests (positive + negative). Suite 957 -> 961, SC-5 byte-stable, self-audit A/A. Known limitation (pre-existing, broader than A): the lightweight frontmatter parser reads inline `paths:` but not YAML block sequences, so block-sequence-scoped rules are still seen as unscoped. Deferred. Part of v5.5.0 "steering-model I". Foundation (active-config-reader enumeration) deferred to v5.6 with B. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
This commit is contained in:
parent
f75ed5655c
commit
f3aadb5183
6 changed files with 149 additions and 5 deletions
|
|
@ -10,6 +10,7 @@ import { SEVERITY } from './lib/severity.mjs';
|
|||
import { parseFrontmatter, extractSections, findImports } from './lib/yaml-parser.mjs';
|
||||
import { lineCount, truncate } from './lib/string-utils.mjs';
|
||||
import { LARGE_CONTEXT_WINDOW, LARGE_CONTEXT_SCALE, withCommas } from './lib/context-window.mjs';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
const SCANNER = 'CML';
|
||||
const MAX_RECOMMENDED_LINES = 200;
|
||||
|
|
@ -68,6 +69,23 @@ export async function scan(targetPath, discovery) {
|
|||
const sections = extractSections(body);
|
||||
const imports = findImports(content);
|
||||
|
||||
// A nested (subdirectory) CLAUDE.md is NOT re-injected after a context
|
||||
// compaction — only the project-root CLAUDE.md is (context-window.md). Its
|
||||
// instructions silently drop until a file in that directory is read again.
|
||||
const relDir = dirname(file.relPath);
|
||||
if (file.scope === 'project' && relDir !== '.' && relDir !== '.claude' && lines > 5) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.low,
|
||||
title: 'Nested CLAUDE.md is not re-injected after compaction',
|
||||
description: `${file.relPath} is a nested (subdirectory) CLAUDE.md. It loads when Claude reads a file in that directory, but after a context compaction it is not re-injected (only the project-root CLAUDE.md is) — its instructions silently drop until a file in that directory is read again.`,
|
||||
file: file.absPath,
|
||||
evidence: `${lines} lines, nested (scope=project, dir="${relDir}")`,
|
||||
recommendation: 'If these instructions must always apply, move the must-hold parts to the project-root CLAUDE.md (re-injected after compaction). Keep nested CLAUDE.md for guidance only needed when working in that directory.',
|
||||
autoFixable: false,
|
||||
}));
|
||||
}
|
||||
|
||||
// --- Length checks ---
|
||||
// Raw line count is no longer an absolute adherence threshold: CC 2.1.169
|
||||
// scales the "too long" warning by context window, and cache-prefix
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ export const TRANSLATIONS = {
|
|||
description: 'Without `CLAUDE.md` at your project root, Claude has to work out your conventions from scratch every conversation. Project-specific guidance is the single highest-impact thing you can add.',
|
||||
recommendation: 'Create a file called `CLAUDE.md` in your project root. Start with a one-paragraph project overview, common commands, and any quirks Claude should know about.',
|
||||
},
|
||||
'Nested CLAUDE.md is not re-injected after compaction': {
|
||||
title: 'A `CLAUDE.md` in a subfolder can quietly drop out mid-session',
|
||||
description: 'Only the main project `CLAUDE.md` is restored when Claude trims older history. A `CLAUDE.md` in a subfolder loads when you open a file there, but it does not come back after a trim until you open one again.',
|
||||
recommendation: 'If its guidance must always apply, move that part into the main `CLAUDE.md`. Keep the subfolder file for things only needed when working in that folder.',
|
||||
},
|
||||
'CLAUDE.md is nearly empty': {
|
||||
title: 'Your `CLAUDE.md` is mostly empty',
|
||||
description: 'An empty instructions file gives Claude no project-specific context, so behavior falls back to defaults.',
|
||||
|
|
@ -254,6 +259,11 @@ export const TRANSLATIONS = {
|
|||
description: 'Without scoping, the rule loads on every conversation regardless of which files you\'re working with.',
|
||||
recommendation: 'Add a scoping block at the top of the file to limit when the rule loads (see the details).',
|
||||
},
|
||||
'Large path-scoped rule is lost after compaction': {
|
||||
title: 'A large scoped rule can quietly drop out mid-session',
|
||||
description: 'Scoped rules load only when you open a matching file, and they fall out of context when Claude trims older history — they do not come back until you open a matching file again.',
|
||||
recommendation: 'If part of it must always apply, move that part into the main `CLAUDE.md`, which is restored automatically.',
|
||||
},
|
||||
'Rule uses "globs" instead of documented "paths"': {
|
||||
title: 'A rule uses an unrecognized scoping field',
|
||||
description: 'Claude Code\'s docs use `paths:` to scope a rule; `globs:` is not the documented field, so the rule may not scope the way you intend.',
|
||||
|
|
|
|||
|
|
@ -122,6 +122,23 @@ export async function scan(targetPath, discovery) {
|
|||
}));
|
||||
}
|
||||
|
||||
// A large PATH-SCOPED rule follows best practice, but path-scoped rules are
|
||||
// NOT re-injected after a context compaction — they reload only when a
|
||||
// matching file is read again (context-window.md). A big one carrying
|
||||
// must-always-hold instructions can silently drop out mid-session.
|
||||
if (frontmatter?.paths && lines > 50) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.low,
|
||||
title: 'Large path-scoped rule is lost after compaction',
|
||||
description: `${file.relPath} is path-scoped (${lines} lines). Path-scoped rules load only when a matching file is read, and after a context compaction they are not re-injected until a matching file is read again — so a large scoped rule carrying must-always-hold instructions can silently drop out mid-session.`,
|
||||
file: file.absPath,
|
||||
evidence: `${lines} lines, path-scoped`,
|
||||
recommendation: 'If parts of this rule must always apply, move them to the project-root CLAUDE.md (re-injected after compaction). Keep path-scoped rules for context only needed when those files are open.',
|
||||
autoFixable: false,
|
||||
}));
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
if (!file.absPath.endsWith('.md')) {
|
||||
findings.push(finding({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue