context-engineering-cli 2.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. context_engineering/__init__.py +3 -0
  2. context_engineering/__main__.py +2 -0
  3. context_engineering/analysis/__init__.py +1 -0
  4. context_engineering/analysis/backfill.py +1064 -0
  5. context_engineering/analysis/context_check.py +253 -0
  6. context_engineering/analysis/context_layout.py +111 -0
  7. context_engineering/analysis/context_review.py +224 -0
  8. context_engineering/analysis/cross_cutting/__init__.py +6 -0
  9. context_engineering/analysis/cross_cutting/authors.py +57 -0
  10. context_engineering/analysis/cross_cutting/buckets.py +40 -0
  11. context_engineering/analysis/cross_cutting/co_change.py +47 -0
  12. context_engineering/analysis/cross_cutting/discover.py +75 -0
  13. context_engineering/analysis/cross_cutting/imports.py +61 -0
  14. context_engineering/analysis/cross_cutting/pair.py +118 -0
  15. context_engineering/analysis/impact.py +77 -0
  16. context_engineering/analysis/sessions.py +27 -0
  17. context_engineering/analysis/staleness.py +179 -0
  18. context_engineering/analysis/tier.py +91 -0
  19. context_engineering/checks/__init__.py +1 -0
  20. context_engineering/checks/antipatterns/__init__.py +5 -0
  21. context_engineering/checks/antipatterns/context.py +23 -0
  22. context_engineering/checks/antipatterns/density.py +72 -0
  23. context_engineering/checks/antipatterns/line_limits.py +52 -0
  24. context_engineering/checks/antipatterns/runner.py +137 -0
  25. context_engineering/checks/antipatterns/splitting.py +97 -0
  26. context_engineering/checks/antipatterns/volatile.py +38 -0
  27. context_engineering/checks/antipatterns/watermark.py +113 -0
  28. context_engineering/checks/contracts.py +456 -0
  29. context_engineering/checks/depth.py +82 -0
  30. context_engineering/checks/frontmatter.py +125 -0
  31. context_engineering/checks/references.py +325 -0
  32. context_engineering/checks/skill_structure.py +124 -0
  33. context_engineering/cli/__init__.py +3 -0
  34. context_engineering/cli/dispatch.py +90 -0
  35. context_engineering/cli/registry.py +33 -0
  36. context_engineering/cli/render.py +92 -0
  37. context_engineering/cli/subcommands.py +587 -0
  38. context_engineering/domain/__init__.py +0 -0
  39. context_engineering/domain/commit.py +19 -0
  40. context_engineering/domain/evidence.py +57 -0
  41. context_engineering/domain/finding.py +37 -0
  42. context_engineering/domain/result.py +59 -0
  43. context_engineering/infra/__init__.py +13 -0
  44. context_engineering/infra/filesystem.py +22 -0
  45. context_engineering/infra/git.py +153 -0
  46. context_engineering/infra/git_evidence.py +357 -0
  47. context_engineering/infra/git_tree.py +139 -0
  48. context_engineering/infra/markdown.py +58 -0
  49. context_engineering/infra/yaml_frontmatter.py +70 -0
  50. context_engineering_cli-2.6.0.dist-info/METADATA +27 -0
  51. context_engineering_cli-2.6.0.dist-info/RECORD +55 -0
  52. context_engineering_cli-2.6.0.dist-info/WHEEL +4 -0
  53. context_engineering_cli-2.6.0.dist-info/entry_points.txt +2 -0
  54. context_engineering_cli-2.6.0.dist-info/licenses/LICENSE +21 -0
  55. provenance.json +1 -0
@@ -0,0 +1,23 @@
1
+ """Context passed to each checker — depth, modified flag, shared git metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class CheckContext:
11
+ root: Path
12
+ git_root: Path | None
13
+ modified_only: bool
14
+
15
+ def depth(self, path: Path) -> int:
16
+ """Depth of `path.parent` relative to git root. Falls back to 2 (leaf default)."""
17
+ if self.git_root is None:
18
+ return 2
19
+ try:
20
+ rel = path.parent.resolve().relative_to(self.git_root.resolve())
21
+ return len([p for p in rel.parts if p != "."])
22
+ except ValueError:
23
+ return 2
@@ -0,0 +1,72 @@
1
+ """P2: section density — long prose with no file refs or DO/NOT/BECAUSE bullets.
2
+
3
+ Heuristic: an H2 section with 5+ prose lines and no file:line references and no
4
+ DO/NOT/BECAUSE markers is drifting architecture prose that belongs in docs/.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from collections.abc import Iterator
11
+ from pathlib import Path
12
+
13
+ from ...domain.finding import Finding, Severity
14
+ from ...infra.filesystem import read_text_safe
15
+ from .context import CheckContext
16
+
17
+ _FILE_REF_RE = re.compile(r"`[^`]+:\d+`|`[^`]+\.[a-z]+`")
18
+ _MARKER_RE = re.compile(r"\*\*DO\*\*|\*\*NOT\*\*|\*\*BECAUSE\*\*")
19
+ _MIN_PROSE = 5
20
+
21
+
22
+ def _is_prose(line: str) -> bool:
23
+ s = line.strip()
24
+ return bool(s) and not s.startswith(("#", "-", "*", "|", ">", "```"))
25
+
26
+
27
+ def _evaluate_section(
28
+ path: Path, section: str, start_line: int, body: list[str]
29
+ ) -> Iterator[Finding]:
30
+ prose = [ln for ln in body if _is_prose(ln)]
31
+ if len(prose) <= _MIN_PROSE:
32
+ return
33
+ if any(_FILE_REF_RE.search(ln) for ln in body):
34
+ return
35
+ if any(_MARKER_RE.search(ln) for ln in body):
36
+ return
37
+ yield Finding(
38
+ file=str(path),
39
+ line=start_line,
40
+ severity=Severity.INFO,
41
+ code="P2-low-density",
42
+ message=(
43
+ f"Section '{section}' has {len(prose)} prose lines with no "
44
+ "file:line references or DO/NOT/BECAUSE bullets"
45
+ ),
46
+ hint="Consider cutting, adding specific references, or moving to docs/",
47
+ )
48
+
49
+
50
+ def check(path: Path, _ctx: CheckContext) -> Iterator[Finding]:
51
+ if path.name not in ("AGENTS.md", "CLAUDE.md"):
52
+ return
53
+ content = read_text_safe(path)
54
+ if content is None:
55
+ return
56
+
57
+ current: str | None = None
58
+ start = 0
59
+ body: list[str] = []
60
+
61
+ for i, line in enumerate(content.splitlines(), 1):
62
+ if line.startswith("## "):
63
+ if current is not None:
64
+ yield from _evaluate_section(path, current, start, body)
65
+ current = line.lstrip("# ").strip()
66
+ start = i
67
+ body = []
68
+ elif current is not None:
69
+ body.append(line)
70
+
71
+ if current is not None:
72
+ yield from _evaluate_section(path, current, start, body)
@@ -0,0 +1,52 @@
1
+ """P11: depth-tiered line limits for AGENTS.md and docs/ files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from pathlib import Path
7
+
8
+ from ...domain.finding import Finding, Severity
9
+ from ...infra.filesystem import read_text_safe
10
+ from .context import CheckContext
11
+
12
+ LIMITS: dict[str, tuple[int, int]] = {
13
+ "depth_0_agents": (80, 120),
14
+ "depth_1_agents": (150, 200),
15
+ "depth_2plus_agents": (250, 300),
16
+ "docs": (300, 500),
17
+ }
18
+
19
+
20
+ def _limits_for(path: Path, depth: int) -> tuple[tuple[int, int], str]:
21
+ is_docs = "docs" in path.parts and path.name not in ("AGENTS.md", "README.md")
22
+ if is_docs:
23
+ return LIMITS["docs"], "docs/ file"
24
+ if depth == 0:
25
+ return LIMITS["depth_0_agents"], "root AGENTS.md (depth 0)"
26
+ if depth == 1:
27
+ return LIMITS["depth_1_agents"], f"module AGENTS.md (depth {depth})"
28
+ return LIMITS["depth_2plus_agents"], f"leaf AGENTS.md (depth {depth})"
29
+
30
+
31
+ def check(path: Path, ctx: CheckContext) -> Iterator[Finding]:
32
+ content = read_text_safe(path)
33
+ if content is None:
34
+ return
35
+ line_count = len(content.splitlines())
36
+ depth = ctx.depth(path)
37
+ (target, hard_stop), label = _limits_for(path, depth)
38
+
39
+ if line_count > hard_stop:
40
+ yield Finding(
41
+ file=str(path), line=line_count, severity=Severity.WARNING,
42
+ code="P11-size-hard-stop",
43
+ message=f"{label} exceeds hard stop: {line_count}L > {hard_stop}L",
44
+ hint=f"target: {target}, hard stop: {hard_stop}",
45
+ )
46
+ elif line_count > target:
47
+ yield Finding(
48
+ file=str(path), line=line_count, severity=Severity.INFO,
49
+ code="P11-size-warning",
50
+ message=f"{label} exceeds target: {line_count}L > {target}L (hard stop: {hard_stop})",
51
+ hint=f"target: {target}, hard stop: {hard_stop}",
52
+ )
@@ -0,0 +1,137 @@
1
+ """Compose the 5 antipattern checkers over a discovered file set."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from collections.abc import Callable, Iterable, Iterator
7
+ from pathlib import Path
8
+
9
+ from ...domain.finding import Finding
10
+ from ...domain.result import LintResult
11
+ from ...infra.filesystem import EXCLUDED_DIRS
12
+ from ...infra.git import git_root
13
+ from . import density, line_limits, splitting, volatile, watermark
14
+ from .context import CheckContext
15
+
16
+ # Checker signature: (path, ctx) -> Iterator[Finding]
17
+ Checker = Callable[[Path, CheckContext], Iterator[Finding]]
18
+
19
+ # Universal checkers — run on every candidate file.
20
+ UNIVERSAL: tuple[Checker, ...] = (
21
+ volatile.check,
22
+ watermark.check_presence,
23
+ watermark.check_staleness,
24
+ density.check,
25
+ )
26
+
27
+ # Checkers that apply only to AGENTS.md / CLAUDE.md
28
+ AGENTS_ONLY: tuple[Checker, ...] = (
29
+ line_limits.check,
30
+ splitting.check,
31
+ )
32
+
33
+ # Checkers that apply only to docs/*.md (non-AGENTS, non-README)
34
+ DOCS_ONLY: tuple[Checker, ...] = (
35
+ line_limits.check,
36
+ )
37
+
38
+
39
+ def _is_candidate(path: Path) -> bool:
40
+ if any(p in EXCLUDED_DIRS for p in path.parts):
41
+ return False
42
+ return (
43
+ path.name == "AGENTS.md"
44
+ or path.name == "CLAUDE.md"
45
+ or "docs" in path.parts
46
+ )
47
+
48
+
49
+ def _git_tracked_md(root: Path) -> list[Path]:
50
+ try:
51
+ result = subprocess.run(
52
+ ["git", "ls-files", "--cached", "--others", "--exclude-standard", "*.md"],
53
+ capture_output=True, text=True, cwd=root,
54
+ )
55
+ except FileNotFoundError:
56
+ return []
57
+ if result.returncode != 0:
58
+ return []
59
+ return [root / p for p in result.stdout.strip().splitlines() if p]
60
+
61
+
62
+ def _git_modified_md(root: Path, gr: Path) -> list[Path]:
63
+ try:
64
+ result = subprocess.run(
65
+ ["git", "status", "--porcelain", "-u"],
66
+ capture_output=True, text=True, cwd=root,
67
+ )
68
+ except FileNotFoundError:
69
+ return []
70
+ if result.returncode != 0:
71
+ return []
72
+ files: list[Path] = []
73
+ for line in result.stdout.strip().splitlines():
74
+ if len(line) < 4:
75
+ continue
76
+ path_str = line[3:].strip()
77
+ if " -> " in path_str:
78
+ path_str = path_str.split(" -> ")[-1]
79
+ if not path_str.endswith(".md"):
80
+ continue
81
+ full = (gr / path_str).resolve()
82
+ try:
83
+ full.relative_to(root.resolve())
84
+ files.append(full)
85
+ except ValueError:
86
+ continue
87
+ return files
88
+
89
+
90
+ def _filesystem_fallback(root: Path) -> Iterable[Path]:
91
+ """Used outside git — targeted walk, not full rglob."""
92
+ if (root / "AGENTS.md").exists():
93
+ yield root / "AGENTS.md"
94
+ docs = root / "docs"
95
+ if docs.is_dir():
96
+ yield from sorted(docs.rglob("*.md"))
97
+ for child in sorted(root.iterdir()):
98
+ if child.is_dir() and not child.name.startswith("."):
99
+ nested = child / "AGENTS.md"
100
+ if nested.exists():
101
+ yield nested
102
+
103
+
104
+ def _discover(root: Path, gr: Path | None, modified_only: bool) -> list[Path]:
105
+ if modified_only and gr is not None:
106
+ return _git_modified_md(root, gr)
107
+ candidates = _git_tracked_md(root) if gr is not None else []
108
+ if not candidates and not modified_only:
109
+ candidates = list(_filesystem_fallback(root))
110
+ return candidates
111
+
112
+
113
+ def _checkers_for(path: Path) -> Iterable[Checker]:
114
+ yield from UNIVERSAL
115
+ if path.name in ("AGENTS.md", "CLAUDE.md"):
116
+ yield from AGENTS_ONLY
117
+ elif "docs" in path.parts and path.name != "README.md":
118
+ yield from DOCS_ONLY
119
+
120
+
121
+ def lint(root: Path, *, modified_only: bool = False) -> LintResult:
122
+ gr = git_root(root)
123
+ ctx = CheckContext(root=root, git_root=gr, modified_only=modified_only)
124
+
125
+ seen: set[Path] = set()
126
+ findings: list[Finding] = []
127
+ for path in _discover(root, gr, modified_only):
128
+ if not path.is_file():
129
+ continue
130
+ resolved = path.resolve()
131
+ if resolved in seen or not _is_candidate(path):
132
+ continue
133
+ seen.add(resolved)
134
+ for checker in _checkers_for(path):
135
+ findings.extend(checker(path, ctx))
136
+
137
+ return LintResult(target=str(root), findings=findings)
@@ -0,0 +1,97 @@
1
+ """P3: splitting-candidate — 3+ gotcha bullets clustering under a common path.
2
+
3
+ Signal that the AGENTS.md wants to be split — promote a nested
4
+ <subdir>/AGENTS.md rather than pack more into the current one.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from collections import Counter
11
+ from collections.abc import Iterator
12
+ from pathlib import Path
13
+
14
+ from ...domain.finding import Finding, Severity
15
+ from ...infra.filesystem import read_text_safe
16
+ from .context import CheckContext
17
+
18
+ _FILEREF_RE = re.compile(r"`([a-zA-Z0-9_./\-]+\.[a-zA-Z0-9_]+)(?::\d+(?:-\d+)?)?`")
19
+ _MIN_BULLETS = 3
20
+
21
+
22
+ def _parse_bullets(content: str) -> list[list[str]]:
23
+ """Return one list of referenced paths per DO bullet."""
24
+ bullets: list[list[str]] = []
25
+ current: list[str] = []
26
+ in_bullet = False
27
+
28
+ for line in content.splitlines():
29
+ stripped = line.lstrip()
30
+ if stripped.startswith("- **DO**"):
31
+ if in_bullet and current:
32
+ bullets.append(current)
33
+ current = []
34
+ in_bullet = True
35
+ elif not stripped and in_bullet:
36
+ if current:
37
+ bullets.append(current)
38
+ current = []
39
+ in_bullet = False
40
+
41
+ if in_bullet:
42
+ for m in _FILEREF_RE.finditer(line):
43
+ ref = m.group(1)
44
+ if "/" in ref:
45
+ current.append(ref)
46
+
47
+ if in_bullet and current:
48
+ bullets.append(current)
49
+ return bullets
50
+
51
+
52
+ def _count_prefixes(bullets: list[list[str]]) -> tuple[Counter, dict[str, list[str]]]:
53
+ prefix_bullets: Counter = Counter()
54
+ samples: dict[str, list[str]] = {}
55
+ for paths in bullets:
56
+ prefixes: set[str] = set()
57
+ for p in paths:
58
+ parts = p.split("/")
59
+ for n in (1, 2, 3):
60
+ if len(parts) > n:
61
+ prefixes.add("/".join(parts[:n]))
62
+ for prefix in prefixes:
63
+ prefix_bullets[prefix] += 1
64
+ samples.setdefault(prefix, []).extend(paths)
65
+ return prefix_bullets, samples
66
+
67
+
68
+ def check(path: Path, _ctx: CheckContext) -> Iterator[Finding]:
69
+ if path.name not in ("AGENTS.md", "CLAUDE.md"):
70
+ return
71
+ content = read_text_safe(path)
72
+ if content is None:
73
+ return
74
+
75
+ bullets = _parse_bullets(content)
76
+ prefix_bullets, samples = _count_prefixes(bullets)
77
+
78
+ for prefix, count in prefix_bullets.most_common():
79
+ if count < _MIN_BULLETS:
80
+ break
81
+ # Exact-segment match: `chat_bot` as file parent should NOT suppress
82
+ # a warning about prefix `bot` — compare leaf exactly, not endswith.
83
+ prefix_leaf = prefix.rsplit("/", 1)[-1]
84
+ if path.parent.name and prefix_leaf == path.parent.name:
85
+ continue
86
+ sample_refs = list(dict.fromkeys(samples[prefix]))[:3]
87
+ yield Finding(
88
+ file=str(path),
89
+ line=0,
90
+ severity=Severity.INFO,
91
+ code="splitting-candidate",
92
+ message=(
93
+ f"{count} gotcha bullets reference paths under `{prefix}/` — "
94
+ f"consider promoting to `{prefix}/AGENTS.md`"
95
+ ),
96
+ hint=f"sample refs: {', '.join(sample_refs)}",
97
+ )
@@ -0,0 +1,38 @@
1
+ """P4: flag content that goes stale quickly (counts, timestamps, versions)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Iterator
7
+ from pathlib import Path
8
+
9
+ from ...domain.finding import Finding, Severity
10
+ from ...infra.filesystem import read_text_safe
11
+ from .context import CheckContext
12
+
13
+ _PATTERNS: tuple[tuple[re.Pattern, str], ...] = (
14
+ (re.compile(
15
+ r"~?\d{2,}\s+(files?|dirs?|directories|modules?|models?|services?|"
16
+ r"tables?|tests?|endpoints?|routes?|components?|packages?)"
17
+ ), "volatile count"),
18
+ (re.compile(r"[Aa]ll\s+\d+\s+\w+"), "volatile count with 'all'"),
19
+ (re.compile(r"Updated:\s*\d{4}-\d{2}-\d{2}"), "diagram timestamp (stale quickly)"),
20
+ (re.compile(r"v\d+\.\d+\.\d+"), "version number (may go stale)"),
21
+ )
22
+
23
+
24
+ def check(path: Path, _ctx: CheckContext) -> Iterator[Finding]:
25
+ content = read_text_safe(path)
26
+ if content is None:
27
+ return
28
+ for i, line in enumerate(content.splitlines(), 1):
29
+ for pattern, message in _PATTERNS:
30
+ if pattern.search(line):
31
+ yield Finding(
32
+ file=str(path),
33
+ line=i,
34
+ severity=Severity.WARNING,
35
+ code="P4-volatile",
36
+ message=message,
37
+ hint=line.strip()[:100],
38
+ )
@@ -0,0 +1,113 @@
1
+ """Watermark presence + staleness checks.
2
+
3
+ Presence: AGENTS.md needs an HTML-comment watermark; docs/*.md need both
4
+ `generated-by` and `last-updated` frontmatter keys together.
5
+
6
+ Staleness: a modified file whose `last-updated` date matches HEAD was
7
+ "edited without updating the watermark" — a common oversight.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ import subprocess
14
+ from collections.abc import Iterator
15
+ from pathlib import Path
16
+
17
+ from ...domain.finding import Finding, Severity
18
+ from ...infra.filesystem import read_text_safe
19
+ from .context import CheckContext
20
+
21
+ _DATE_RE = re.compile(r"last-updated:\s*(\d{4}-\d{2}-\d{2})")
22
+ _HTML_WATERMARK_RE = re.compile(r"<!--\s*generated-by:")
23
+
24
+
25
+ def _last_updated(content: str) -> str | None:
26
+ m = _DATE_RE.search(content)
27
+ return m.group(1) if m else None
28
+
29
+
30
+ def _head_content(path: Path, git_root: Path | None) -> str | None:
31
+ if git_root is None:
32
+ return None
33
+ try:
34
+ rel = path.resolve().relative_to(git_root.resolve())
35
+ except ValueError:
36
+ return None
37
+ try:
38
+ result = subprocess.run(
39
+ ["git", "show", f"HEAD:{rel}"], capture_output=True, text=True, cwd=git_root,
40
+ )
41
+ except FileNotFoundError:
42
+ return None
43
+ return result.stdout if result.returncode == 0 else None
44
+
45
+
46
+ def check_presence(path: Path, _ctx: CheckContext) -> Iterator[Finding]:
47
+ content = read_text_safe(path)
48
+ if content is None:
49
+ return
50
+ lines = content.splitlines()
51
+
52
+ if path.name == "AGENTS.md" and content.startswith("---\n"):
53
+ yield Finding(
54
+ file=str(path), line=1, severity=Severity.WARNING,
55
+ code="structural", message="AGENTS.md should not have YAML frontmatter",
56
+ hint="---",
57
+ )
58
+
59
+ has_generated_by = any("generated-by:" in ln for ln in lines)
60
+ has_last_updated = any("last-updated:" in ln for ln in lines)
61
+
62
+ if path.name == "AGENTS.md":
63
+ if not _HTML_WATERMARK_RE.search(content):
64
+ yield Finding(
65
+ file=str(path), line=max(1, len(lines)), severity=Severity.WARNING,
66
+ code="watermark-missing",
67
+ message="AGENTS.md missing generated-by HTML comment watermark",
68
+ hint="<!-- generated-by: plugin@version | last-updated: YYYY-MM-DD -->",
69
+ )
70
+ return
71
+
72
+ if path.name == "README.md" or path.suffix != ".md":
73
+ return
74
+
75
+ if has_generated_by and not has_last_updated:
76
+ yield Finding(
77
+ file=str(path), line=1, severity=Severity.WARNING,
78
+ code="watermark-incomplete",
79
+ message="has generated-by but missing last-updated",
80
+ )
81
+ elif has_last_updated and not has_generated_by:
82
+ yield Finding(
83
+ file=str(path), line=1, severity=Severity.WARNING,
84
+ code="watermark-incomplete",
85
+ message="has last-updated but missing generated-by",
86
+ )
87
+
88
+
89
+ def check_staleness(path: Path, ctx: CheckContext) -> Iterator[Finding]:
90
+ if not ctx.modified_only:
91
+ return
92
+ content = read_text_safe(path)
93
+ if content is None:
94
+ return
95
+ current = _last_updated(content)
96
+ if current is None:
97
+ return
98
+ head = _head_content(path, ctx.git_root)
99
+ if head is None:
100
+ return
101
+ head_date = _last_updated(head)
102
+ if head_date is None or head_date != current:
103
+ return
104
+ line_num = next(
105
+ (i for i, ln in enumerate(content.splitlines(), 1) if "last-updated:" in ln),
106
+ 1,
107
+ )
108
+ yield Finding(
109
+ file=str(path), line=line_num, severity=Severity.WARNING,
110
+ code="watermark-stale",
111
+ message=f"file was modified but last-updated date unchanged ({current})",
112
+ hint=f"Update last-updated to today's date (currently: {current})",
113
+ )