|
120 | 120 | # Knowledge MCPs arm unconditionally (handled separately in accumulator.py). |
121 | 121 | CI_CONTEXT_SUBCATS = frozenset({"project", "data", "design"}) |
122 | 122 |
|
| 123 | +# ---- Ordered-planning file taxonomy (C1/C2/C3) ----------------------------- |
| 124 | +# classify_change_target/is_plan_file_target back the ordered-planning eligibility |
| 125 | +# and plan-detection redesign: a write's file TYPE decides whether it counts toward |
| 126 | +# change-session eligibility (C2) and whether it counts as a plan artifact (C3). |
| 127 | +_LOCKFILE_NAMES = frozenset({ |
| 128 | + "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "npm-shrinkwrap.json", |
| 129 | + "cargo.lock", "poetry.lock", "pipfile.lock", "gemfile.lock", "go.sum", |
| 130 | + "composer.lock", "mix.lock", "flake.lock", "packages.lock.json", |
| 131 | +}) |
| 132 | +_CODE_EXTS = frozenset({ |
| 133 | + ".py", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".go", ".rb", ".java", |
| 134 | + ".kt", ".kts", ".swift", ".c", ".h", ".cpp", ".cc", ".hpp", ".rs", ".php", |
| 135 | + ".cs", ".scala", ".m", ".mm", ".vue", ".svelte", ".dart", ".ex", ".exs", |
| 136 | + ".sh", ".bash", ".zsh", ".sql", ".lua", ".r", ".pl", ".clj", ".elm", |
| 137 | +}) |
| 138 | +_TEST_NAME_RX = re.compile( |
| 139 | + r'(^|[/_.-])tests?([/_.-]|$)|\.test\.[a-z]+$|\.spec\.[a-z]+$|_test\.[a-z]+$|' |
| 140 | + r'_spec\.[a-z]+$|^test_|(^|/)__tests__(/|$)|(^|/)spec(/|$)', |
| 141 | + re.I, |
| 142 | +) |
| 143 | +_DOC_EXTS = frozenset({".md", ".mdx", ".rst", ".txt", ".adoc"}) |
| 144 | +_DOC_NAMES = frozenset({"readme", "changelog", "license", "contributing", "authors"}) |
| 145 | +_CONFIG_EXTS = frozenset({ |
| 146 | + ".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf", ".env", |
| 147 | + ".properties", ".xml", |
| 148 | +}) |
| 149 | +_CONFIG_NAMES = frozenset({ |
| 150 | + "dockerfile", "makefile", "vagrantfile", "procfile", "gemfile", |
| 151 | + ".gitignore", ".dockerignore", ".editorconfig", ".eslintrc", |
| 152 | + ".eslintrc.json", ".eslintrc.js", ".prettierrc", |
| 153 | +}) |
| 154 | + |
| 155 | + |
| 156 | +def classify_change_target(path): |
| 157 | + """Classify a write target into code/test/doc/config/lockfile/other for |
| 158 | + change-session eligibility (C2) and file-type semantics (P1/P2 fixes). |
| 159 | + Order matters: lockfile and test checks run before the generic extension/ |
| 160 | + name maps, since e.g. package-lock.json is a .json (config-looking) file |
| 161 | + and foo.test.ts has a code extension.""" |
| 162 | + if not path: |
| 163 | + return "other" |
| 164 | + name = str(path).rsplit("/", 1)[-1] |
| 165 | + low = name.lower() |
| 166 | + if low in _LOCKFILE_NAMES: |
| 167 | + return "lockfile" |
| 168 | + ext = "" |
| 169 | + if "." in name: |
| 170 | + ext = "." + name.rsplit(".", 1)[-1].lower() |
| 171 | + if _TEST_NAME_RX.search(str(path)): |
| 172 | + return "test" |
| 173 | + if ext in _CODE_EXTS: |
| 174 | + return "code" |
| 175 | + if ext in _DOC_EXTS or low.split(".")[0] in _DOC_NAMES: |
| 176 | + return "doc" |
| 177 | + if ext in _CONFIG_EXTS or low in _CONFIG_NAMES: |
| 178 | + return "config" |
| 179 | + return "other" |
| 180 | + |
| 181 | + |
| 182 | +_PLAN_FILE_RX = re.compile( |
| 183 | + r'(^|/)\.claude/plans/[^/]*\.md$|(^|/)\.cursor/plans/|(^|/)\.context/[^/]*plan[^/]*$|' |
| 184 | + r'(^|/)plans/[^/]+\.(md|mdx|txt)$', |
| 185 | + re.I, |
| 186 | +) |
| 187 | + |
| 188 | + |
| 189 | +def is_plan_file_target(path): |
| 190 | + """True when a write target is a durable plan artifact on disk (C3/C4): |
| 191 | + `.claude/plans/*.md`, `.cursor/plans/`, `.context/*plan*`, or any |
| 192 | + `.md`/`.mdx`/`.txt` file directly inside a `plans/` directory at any depth |
| 193 | + (e.g. the superpowers `docs/**/plans/<n>-<name>.md` convention), regardless |
| 194 | + of filename — matched by taxonomy so cross-session credit (C4) can recognize |
| 195 | + hand-off plan files regardless of source CLI.""" |
| 196 | + if not path: |
| 197 | + return False |
| 198 | + return bool(_PLAN_FILE_RX.search(str(path))) |
| 199 | + |
123 | 200 |
|
124 | 201 | def classify_mcp_subcategory(server_name, tool_name=""): |
125 | 202 | low = server_name.lower() |
|
0 commit comments