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.
- context_engineering/__init__.py +3 -0
- context_engineering/__main__.py +2 -0
- context_engineering/analysis/__init__.py +1 -0
- context_engineering/analysis/backfill.py +1064 -0
- context_engineering/analysis/context_check.py +253 -0
- context_engineering/analysis/context_layout.py +111 -0
- context_engineering/analysis/context_review.py +224 -0
- context_engineering/analysis/cross_cutting/__init__.py +6 -0
- context_engineering/analysis/cross_cutting/authors.py +57 -0
- context_engineering/analysis/cross_cutting/buckets.py +40 -0
- context_engineering/analysis/cross_cutting/co_change.py +47 -0
- context_engineering/analysis/cross_cutting/discover.py +75 -0
- context_engineering/analysis/cross_cutting/imports.py +61 -0
- context_engineering/analysis/cross_cutting/pair.py +118 -0
- context_engineering/analysis/impact.py +77 -0
- context_engineering/analysis/sessions.py +27 -0
- context_engineering/analysis/staleness.py +179 -0
- context_engineering/analysis/tier.py +91 -0
- context_engineering/checks/__init__.py +1 -0
- context_engineering/checks/antipatterns/__init__.py +5 -0
- context_engineering/checks/antipatterns/context.py +23 -0
- context_engineering/checks/antipatterns/density.py +72 -0
- context_engineering/checks/antipatterns/line_limits.py +52 -0
- context_engineering/checks/antipatterns/runner.py +137 -0
- context_engineering/checks/antipatterns/splitting.py +97 -0
- context_engineering/checks/antipatterns/volatile.py +38 -0
- context_engineering/checks/antipatterns/watermark.py +113 -0
- context_engineering/checks/contracts.py +456 -0
- context_engineering/checks/depth.py +82 -0
- context_engineering/checks/frontmatter.py +125 -0
- context_engineering/checks/references.py +325 -0
- context_engineering/checks/skill_structure.py +124 -0
- context_engineering/cli/__init__.py +3 -0
- context_engineering/cli/dispatch.py +90 -0
- context_engineering/cli/registry.py +33 -0
- context_engineering/cli/render.py +92 -0
- context_engineering/cli/subcommands.py +587 -0
- context_engineering/domain/__init__.py +0 -0
- context_engineering/domain/commit.py +19 -0
- context_engineering/domain/evidence.py +57 -0
- context_engineering/domain/finding.py +37 -0
- context_engineering/domain/result.py +59 -0
- context_engineering/infra/__init__.py +13 -0
- context_engineering/infra/filesystem.py +22 -0
- context_engineering/infra/git.py +153 -0
- context_engineering/infra/git_evidence.py +357 -0
- context_engineering/infra/git_tree.py +139 -0
- context_engineering/infra/markdown.py +58 -0
- context_engineering/infra/yaml_frontmatter.py +70 -0
- context_engineering_cli-2.6.0.dist-info/METADATA +27 -0
- context_engineering_cli-2.6.0.dist-info/RECORD +55 -0
- context_engineering_cli-2.6.0.dist-info/WHEEL +4 -0
- context_engineering_cli-2.6.0.dist-info/entry_points.txt +2 -0
- context_engineering_cli-2.6.0.dist-info/licenses/LICENSE +21 -0
- provenance.json +1 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
"""The subcommands — each a tiny configure() + run() pair.
|
|
2
|
+
|
|
3
|
+
This is the whole CLI surface. Adding a subcommand means adding one entry
|
|
4
|
+
here, not editing a 350-line dispatcher.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..analysis import backfill as backfill_mod
|
|
13
|
+
from ..analysis import context_check as context_check_mod
|
|
14
|
+
from ..analysis import context_review as context_review_mod
|
|
15
|
+
from ..analysis import sessions as sessions_mod
|
|
16
|
+
from ..analysis import staleness as staleness_mod
|
|
17
|
+
from ..analysis import tier as tier_mod
|
|
18
|
+
from ..analysis.cross_cutting import discover as cc_discover
|
|
19
|
+
from ..analysis.cross_cutting import pair as cc_pair
|
|
20
|
+
from ..checks import contracts as contracts_mod
|
|
21
|
+
from ..checks import depth as depth_mod
|
|
22
|
+
from ..checks import frontmatter as frontmatter_mod
|
|
23
|
+
from ..checks import references as references_mod
|
|
24
|
+
from ..checks import skill_structure as skill_mod
|
|
25
|
+
from ..checks.antipatterns import lint as antipatterns_lint
|
|
26
|
+
from ..infra.git import git_root
|
|
27
|
+
from .registry import Subcommand
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _add_target(parser: argparse.ArgumentParser, help_text: str) -> None:
|
|
31
|
+
parser.add_argument("path", help=help_text)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _resolve(path_str: str) -> Path | None:
|
|
35
|
+
p = Path(path_str).expanduser().resolve()
|
|
36
|
+
return p if p.exists() else None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _resolve_git_scope(path_str: str) -> Path | None:
|
|
40
|
+
path = Path(path_str).expanduser().resolve()
|
|
41
|
+
if path.exists() and not path.is_dir():
|
|
42
|
+
return None
|
|
43
|
+
return path if git_root(path) is not None else None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# tier
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _configure_tier(p: argparse.ArgumentParser) -> None:
|
|
52
|
+
_add_target(p, "module path to classify (e.g., services/payments)")
|
|
53
|
+
p.add_argument(
|
|
54
|
+
"--lookback-days",
|
|
55
|
+
type=int,
|
|
56
|
+
default=90,
|
|
57
|
+
metavar="DAYS",
|
|
58
|
+
help="window of recent commits to consider (default: %(default)s)",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _run_tier(args: argparse.Namespace):
|
|
63
|
+
target = _resolve(args.path)
|
|
64
|
+
if target is None:
|
|
65
|
+
return None
|
|
66
|
+
return tier_mod.analyze(target, lookback_days=args.lookback_days)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
TIER = Subcommand(
|
|
70
|
+
name="tier",
|
|
71
|
+
help="classify a module by unique author count (opinion-density tier)",
|
|
72
|
+
description="Classify a module's opinion-density tier from recent unique authors.",
|
|
73
|
+
examples=(
|
|
74
|
+
"context-engineering tier services/payments",
|
|
75
|
+
"context-engineering tier packages/api --lookback-days 180 --json",
|
|
76
|
+
),
|
|
77
|
+
configure=_configure_tier,
|
|
78
|
+
run=_run_tier,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# antipatterns
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _configure_antipatterns(p: argparse.ArgumentParser) -> None:
|
|
88
|
+
_add_target(p, "directory to scan (e.g., services/payments)")
|
|
89
|
+
p.add_argument(
|
|
90
|
+
"--modified",
|
|
91
|
+
action="store_true",
|
|
92
|
+
help="only scan files in git status (also checks watermark staleness)",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run_antipatterns(args: argparse.Namespace):
|
|
97
|
+
target = _resolve(args.path)
|
|
98
|
+
if target is None or not target.is_dir():
|
|
99
|
+
# Must be a directory — the runner discovers files under `root`.
|
|
100
|
+
# Returning None here routes through dispatch's "invalid target path"
|
|
101
|
+
# diagnostic, matching the old script's explicit sys.exit(1) guard.
|
|
102
|
+
return None
|
|
103
|
+
return antipatterns_lint(target, modified_only=args.modified)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
ANTIPATTERNS = Subcommand(
|
|
107
|
+
name="antipatterns",
|
|
108
|
+
help="scan AGENTS.md / docs/ files for content antipatterns",
|
|
109
|
+
description="Scan for volatile content, watermark drift, size limits, splitting candidates.",
|
|
110
|
+
examples=(
|
|
111
|
+
"context-engineering antipatterns services/payments",
|
|
112
|
+
"context-engineering antipatterns modules/help-desk --modified --json",
|
|
113
|
+
),
|
|
114
|
+
configure=_configure_antipatterns,
|
|
115
|
+
run=_run_antipatterns,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# depth
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _configure_depth(p: argparse.ArgumentParser) -> None:
|
|
125
|
+
_add_target(p, "AGENTS.md file or directory")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _run_depth(args: argparse.Namespace):
|
|
129
|
+
target = _resolve(args.path)
|
|
130
|
+
if target is None:
|
|
131
|
+
return None
|
|
132
|
+
return depth_mod.lint(target)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
DEPTH = Subcommand(
|
|
136
|
+
name="depth",
|
|
137
|
+
help="compute AGENTS.md depth + P3 stringency warnings",
|
|
138
|
+
description="Compute depth and run depth-appropriate stringency checks.",
|
|
139
|
+
examples=(
|
|
140
|
+
"context-engineering depth services/payments/AGENTS.md",
|
|
141
|
+
"context-engineering depth packages/api --json",
|
|
142
|
+
),
|
|
143
|
+
configure=_configure_depth,
|
|
144
|
+
run=_run_depth,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# staleness
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _configure_staleness(p: argparse.ArgumentParser) -> None:
|
|
154
|
+
_add_target(p, "path to AGENTS.md")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _run_staleness(args: argparse.Namespace):
|
|
158
|
+
# Staleness accepts non-existent AGENTS.md paths — returns "full-rebuild"
|
|
159
|
+
# as the recommendation, which is the correct answer for "should this
|
|
160
|
+
# not-yet-created AGENTS.md be generated?". Don't gate on path existence.
|
|
161
|
+
return staleness_mod.analyze(Path(args.path).expanduser().resolve())
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
STALENESS = Subcommand(
|
|
165
|
+
name="staleness",
|
|
166
|
+
help="check staleness of an AGENTS.md relative to git activity",
|
|
167
|
+
description="Categorize changes since last update and emit an update recommendation.",
|
|
168
|
+
examples=(
|
|
169
|
+
"context-engineering staleness services/payments/AGENTS.md",
|
|
170
|
+
"context-engineering staleness modules/help-desk/AGENTS.md --json",
|
|
171
|
+
),
|
|
172
|
+
configure=_configure_staleness,
|
|
173
|
+
run=_run_staleness,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# sessions
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _configure_sessions(p: argparse.ArgumentParser) -> None:
|
|
183
|
+
_add_target(p, "path to a project directory")
|
|
184
|
+
p.add_argument("--max", type=int, default=20, metavar="N", help="maximum artifacts to return")
|
|
185
|
+
p.add_argument("--sessions-dir", help="optional directory of exported JSONL session artifacts; absent is an explicit portable fallback")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _run_sessions(args: argparse.Namespace):
|
|
189
|
+
target = _resolve(args.path)
|
|
190
|
+
if target is None:
|
|
191
|
+
return None
|
|
192
|
+
return sessions_mod.analyze(str(target), max_results=args.max, sessions_dir=args.sessions_dir)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
SESSIONS = Subcommand(
|
|
196
|
+
name="sessions",
|
|
197
|
+
help="discover Claude Code sessions relevant to a path",
|
|
198
|
+
description="Rank explicitly configured exported JSONL artifacts by mentions of the target.",
|
|
199
|
+
examples=(
|
|
200
|
+
"context-engineering sessions /abs/path/to/services/payments --max 5",
|
|
201
|
+
"context-engineering sessions /abs/path/to/modules/help-desk --json",
|
|
202
|
+
),
|
|
203
|
+
configure=_configure_sessions,
|
|
204
|
+
run=_run_sessions,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# cross-cutting
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _configure_cross_cutting(p: argparse.ArgumentParser) -> None:
|
|
214
|
+
p.add_argument(
|
|
215
|
+
"paths",
|
|
216
|
+
nargs="+",
|
|
217
|
+
help="one path (with --discover) or several for pair mode",
|
|
218
|
+
)
|
|
219
|
+
p.add_argument(
|
|
220
|
+
"--discover",
|
|
221
|
+
action="store_true",
|
|
222
|
+
help="discover mode: auto-find modules coupled to a single target "
|
|
223
|
+
"(requires exactly one path)",
|
|
224
|
+
)
|
|
225
|
+
p.add_argument(
|
|
226
|
+
"--lookback-days",
|
|
227
|
+
type=int,
|
|
228
|
+
default=90,
|
|
229
|
+
metavar="DAYS",
|
|
230
|
+
help="git history window for co-change/author signals (default: %(default)s)",
|
|
231
|
+
)
|
|
232
|
+
p.add_argument(
|
|
233
|
+
"--min-edge",
|
|
234
|
+
type=int,
|
|
235
|
+
default=3,
|
|
236
|
+
metavar="N",
|
|
237
|
+
help="drop edges weaker than N from the output (default: %(default)s)",
|
|
238
|
+
)
|
|
239
|
+
p.add_argument(
|
|
240
|
+
"--top",
|
|
241
|
+
type=int,
|
|
242
|
+
default=10,
|
|
243
|
+
metavar="N",
|
|
244
|
+
help="cap discover results at top N per signal (default: %(default)s)",
|
|
245
|
+
)
|
|
246
|
+
p.add_argument(
|
|
247
|
+
"--signals",
|
|
248
|
+
default="imports,co-change,authors",
|
|
249
|
+
metavar="CSV",
|
|
250
|
+
help=("comma-separated subset of `imports`, `co-change`, `authors` (default: %(default)s)"),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _run_cross_cutting(args: argparse.Namespace):
|
|
255
|
+
resolved = [_resolve(p) for p in args.paths]
|
|
256
|
+
if any(p is None for p in resolved):
|
|
257
|
+
return None
|
|
258
|
+
paths = [p for p in resolved if p is not None]
|
|
259
|
+
signals = frozenset(s.strip() for s in args.signals.split(","))
|
|
260
|
+
if args.discover:
|
|
261
|
+
if len(paths) != 1:
|
|
262
|
+
return None
|
|
263
|
+
return cc_discover(
|
|
264
|
+
target_path=paths[0],
|
|
265
|
+
lookback_days=args.lookback_days,
|
|
266
|
+
min_edge=args.min_edge,
|
|
267
|
+
top_n=args.top,
|
|
268
|
+
signals=signals,
|
|
269
|
+
)
|
|
270
|
+
return cc_pair(
|
|
271
|
+
module_paths=paths,
|
|
272
|
+
lookback_days=args.lookback_days,
|
|
273
|
+
min_edge=args.min_edge,
|
|
274
|
+
signals=signals,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
CROSS_CUTTING = Subcommand(
|
|
279
|
+
name="cross-cutting",
|
|
280
|
+
help="imports/co-change/author-overlap between modules",
|
|
281
|
+
description="Surface cross-cutting concerns via three signals.",
|
|
282
|
+
examples=(
|
|
283
|
+
"context-engineering cross-cutting services/payments --discover",
|
|
284
|
+
"context-engineering cross-cutting packages/api services/payments --min-edge 5 --json",
|
|
285
|
+
),
|
|
286
|
+
configure=_configure_cross_cutting,
|
|
287
|
+
run=_run_cross_cutting,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# frontmatter
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _configure_frontmatter(p: argparse.ArgumentParser) -> None:
|
|
297
|
+
_add_target(p, "directory containing docs/*.md files")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _run_frontmatter(args: argparse.Namespace):
|
|
301
|
+
target = _resolve(args.path)
|
|
302
|
+
if target is None:
|
|
303
|
+
return None
|
|
304
|
+
return frontmatter_mod.lint(target)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
FRONTMATTER = Subcommand(
|
|
308
|
+
name="frontmatter",
|
|
309
|
+
help="validate docs YAML frontmatter when the target establishes that convention",
|
|
310
|
+
description=("Validate id/title/description/index[] consistency without imposing frontmatter."),
|
|
311
|
+
examples=(
|
|
312
|
+
"context-engineering frontmatter services/payments",
|
|
313
|
+
"context-engineering frontmatter managed configuration/modules/context-engineering --json",
|
|
314
|
+
),
|
|
315
|
+
configure=_configure_frontmatter,
|
|
316
|
+
run=_run_frontmatter,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# references
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _configure_references(p: argparse.ArgumentParser) -> None:
|
|
326
|
+
_add_target(p, "directory to scan")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _run_references(args: argparse.Namespace):
|
|
330
|
+
target = _resolve(args.path)
|
|
331
|
+
if target is None:
|
|
332
|
+
return None
|
|
333
|
+
return references_mod.lint(target)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
REFERENCES = Subcommand(
|
|
337
|
+
name="references",
|
|
338
|
+
help="verify backtick paths, CLAUDE.md symlinks, docs indexes",
|
|
339
|
+
description="Check that references resolve and CLAUDE.md files are valid symlinks.",
|
|
340
|
+
examples=(
|
|
341
|
+
"context-engineering references services/payments",
|
|
342
|
+
"context-engineering references modules/help-desk --json",
|
|
343
|
+
),
|
|
344
|
+
configure=_configure_references,
|
|
345
|
+
run=_run_references,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
# skills
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _configure_skills(p: argparse.ArgumentParser) -> None:
|
|
355
|
+
_add_target(p, "skill directory (or parent with --all)")
|
|
356
|
+
p.add_argument(
|
|
357
|
+
"--all",
|
|
358
|
+
action="store_true",
|
|
359
|
+
dest="recurse",
|
|
360
|
+
help="validate every skill directory under <path>",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _run_skills(args: argparse.Namespace):
|
|
365
|
+
target = _resolve(args.path)
|
|
366
|
+
if target is None:
|
|
367
|
+
return None
|
|
368
|
+
return skill_mod.lint(target, recurse=args.recurse)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
SKILLS = Subcommand(
|
|
372
|
+
name="skills",
|
|
373
|
+
help="validate skill directory structure",
|
|
374
|
+
description="Validate one skill directory (or --all skills under a parent).",
|
|
375
|
+
examples=(
|
|
376
|
+
"context-engineering skills ./skills/context-agents-md",
|
|
377
|
+
"context-engineering skills ./skills --all --json",
|
|
378
|
+
),
|
|
379
|
+
configure=_configure_skills,
|
|
380
|
+
run=_run_skills,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# ---------------------------------------------------------------------------
|
|
385
|
+
# contracts / review / backfill
|
|
386
|
+
# ---------------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _configure_contracts(p: argparse.ArgumentParser) -> None:
|
|
390
|
+
_add_target(p, "repository or module root containing optional SPEC.md and ADRs")
|
|
391
|
+
p.add_argument("--require-spec", action="store_true", help="error when SPEC.md is absent")
|
|
392
|
+
p.add_argument(
|
|
393
|
+
"--require-adrs",
|
|
394
|
+
action="store_true",
|
|
395
|
+
help="error when no valid ADR record exists",
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _run_contracts(args: argparse.Namespace):
|
|
400
|
+
target = _resolve(args.path)
|
|
401
|
+
if target is None or not target.is_dir():
|
|
402
|
+
return None
|
|
403
|
+
return contracts_mod.analyze(
|
|
404
|
+
target,
|
|
405
|
+
require_spec=args.require_spec,
|
|
406
|
+
require_adrs=args.require_adrs,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
CONTRACTS = Subcommand(
|
|
411
|
+
name="contracts",
|
|
412
|
+
help="discover and validate optional SPEC.md and ADR contracts",
|
|
413
|
+
description="Discover existing contract conventions and validate deterministic SPEC/ADR rules.",
|
|
414
|
+
examples=(
|
|
415
|
+
"context-engineering contracts .",
|
|
416
|
+
"context-engineering contracts packages/api --require-spec --json",
|
|
417
|
+
),
|
|
418
|
+
configure=_configure_contracts,
|
|
419
|
+
run=_run_contracts,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _configure_review(p: argparse.ArgumentParser) -> None:
|
|
424
|
+
_add_target(p, "repository or module root to scope the diff")
|
|
425
|
+
p.add_argument(
|
|
426
|
+
"--base",
|
|
427
|
+
required=True,
|
|
428
|
+
metavar="REF",
|
|
429
|
+
help="base Git ref for the three-dot diff",
|
|
430
|
+
)
|
|
431
|
+
p.add_argument(
|
|
432
|
+
"--head",
|
|
433
|
+
default="HEAD",
|
|
434
|
+
metavar="REF",
|
|
435
|
+
help="head Git ref (default: %(default)s)",
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _run_review(args: argparse.Namespace):
|
|
440
|
+
target = _resolve_git_scope(args.path)
|
|
441
|
+
if target is None:
|
|
442
|
+
return None
|
|
443
|
+
return context_review_mod.analyze(target, base=args.base, head=args.head)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
REVIEW = Subcommand(
|
|
447
|
+
name="review",
|
|
448
|
+
help="inspect a diff for advisory context update routes",
|
|
449
|
+
description=(
|
|
450
|
+
"Preserve raw Git evidence and suggest advisory context owners without writing files."
|
|
451
|
+
),
|
|
452
|
+
examples=(
|
|
453
|
+
"context-engineering review . --base origin/main",
|
|
454
|
+
"context-engineering review packages/api --base v1.2.0 --head HEAD --json",
|
|
455
|
+
),
|
|
456
|
+
configure=_configure_review,
|
|
457
|
+
run=_run_review,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _configure_check(p: argparse.ArgumentParser) -> None:
|
|
462
|
+
_add_target(p, "repository or module root to validate at the exact head tree")
|
|
463
|
+
p.add_argument(
|
|
464
|
+
"--base",
|
|
465
|
+
metavar="REF",
|
|
466
|
+
help="base Git ref for changed-owner validation; omit with --all",
|
|
467
|
+
)
|
|
468
|
+
p.add_argument(
|
|
469
|
+
"--head", default="HEAD", metavar="REF", help="head Git ref (default: %(default)s)"
|
|
470
|
+
)
|
|
471
|
+
p.add_argument(
|
|
472
|
+
"--all",
|
|
473
|
+
dest="all_context",
|
|
474
|
+
action="store_true",
|
|
475
|
+
help="validate every discovered context owner, not only affected owners",
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _run_check(args: argparse.Namespace):
|
|
480
|
+
target = _resolve_git_scope(args.path)
|
|
481
|
+
if target is None:
|
|
482
|
+
return None
|
|
483
|
+
all_context = args.all_context or args.base is None
|
|
484
|
+
return context_check_mod.analyze(
|
|
485
|
+
target,
|
|
486
|
+
base=args.base or args.head,
|
|
487
|
+
head=args.head,
|
|
488
|
+
all_context=all_context,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
CHECK = Subcommand(
|
|
493
|
+
name="check",
|
|
494
|
+
help="run the composed deterministic PR-time context check",
|
|
495
|
+
description=(
|
|
496
|
+
"Validate affected context owners at the exact head tree and report semantic routes "
|
|
497
|
+
"separately as advisory evidence."
|
|
498
|
+
),
|
|
499
|
+
examples=(
|
|
500
|
+
"context-engineering check . --base origin/main",
|
|
501
|
+
"context-engineering check . --all --json",
|
|
502
|
+
),
|
|
503
|
+
configure=_configure_check,
|
|
504
|
+
run=_run_check,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _configure_backfill(p: argparse.ArgumentParser) -> None:
|
|
509
|
+
_add_target(p, "repository or module root whose local Git history should be extracted")
|
|
510
|
+
p.add_argument(
|
|
511
|
+
"--publish-safe",
|
|
512
|
+
action="store_true",
|
|
513
|
+
help="omit raw historical subjects and paths from the rendered response",
|
|
514
|
+
)
|
|
515
|
+
p.add_argument(
|
|
516
|
+
"--cutoff",
|
|
517
|
+
default="HEAD",
|
|
518
|
+
metavar="REF",
|
|
519
|
+
help="latest Git ref visible to reconstruction (default: %(default)s)",
|
|
520
|
+
)
|
|
521
|
+
p.add_argument(
|
|
522
|
+
"--quick",
|
|
523
|
+
action="store_true",
|
|
524
|
+
help=(
|
|
525
|
+
"use a bounded current-state reconstruction instead of the default "
|
|
526
|
+
"complete-picture genesis-through-cutoff history"
|
|
527
|
+
),
|
|
528
|
+
)
|
|
529
|
+
p.add_argument(
|
|
530
|
+
"--max-commits",
|
|
531
|
+
type=int,
|
|
532
|
+
default=None,
|
|
533
|
+
metavar="N",
|
|
534
|
+
help=(
|
|
535
|
+
"maximum local commits in --quick mode (default with --quick: 100); "
|
|
536
|
+
"rejected without --quick"
|
|
537
|
+
),
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _run_backfill(args: argparse.Namespace):
|
|
542
|
+
target = _resolve(args.path)
|
|
543
|
+
if target is None or not target.is_dir():
|
|
544
|
+
return None
|
|
545
|
+
return backfill_mod.analyze(
|
|
546
|
+
target,
|
|
547
|
+
cutoff=args.cutoff,
|
|
548
|
+
max_commits=args.max_commits,
|
|
549
|
+
quick=args.quick,
|
|
550
|
+
publish_safe=args.publish_safe,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
BACKFILL = Subcommand(
|
|
555
|
+
name="backfill",
|
|
556
|
+
help="extract provenance-preserving local Git history for contract backfill",
|
|
557
|
+
description=(
|
|
558
|
+
"Return deterministic read-only historical evidence for agent-owned synthesis: "
|
|
559
|
+
"complete-picture by default, or bounded current-state reconstruction with --quick. "
|
|
560
|
+
"The CLI never writes files and does not author claims, SPEC content, ADR decisions, "
|
|
561
|
+
"or documentation."
|
|
562
|
+
),
|
|
563
|
+
examples=(
|
|
564
|
+
"context-engineering backfill . --cutoff HEAD",
|
|
565
|
+
"context-engineering backfill . --cutoff HEAD --json",
|
|
566
|
+
"context-engineering backfill packages/api --cutoff v1.2.0 --quick --max-commits 50 --json",
|
|
567
|
+
),
|
|
568
|
+
configure=_configure_backfill,
|
|
569
|
+
run=_run_backfill,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
ALL: tuple[Subcommand, ...] = (
|
|
574
|
+
TIER,
|
|
575
|
+
ANTIPATTERNS,
|
|
576
|
+
DEPTH,
|
|
577
|
+
STALENESS,
|
|
578
|
+
SESSIONS,
|
|
579
|
+
CROSS_CUTTING,
|
|
580
|
+
FRONTMATTER,
|
|
581
|
+
REFERENCES,
|
|
582
|
+
SKILLS,
|
|
583
|
+
CONTRACTS,
|
|
584
|
+
REVIEW,
|
|
585
|
+
CHECK,
|
|
586
|
+
BACKFILL,
|
|
587
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Commit — git-log row with exact-segment path matching."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Commit:
|
|
11
|
+
sha: str
|
|
12
|
+
author: str
|
|
13
|
+
date: datetime.datetime
|
|
14
|
+
files: tuple[str, ...]
|
|
15
|
+
|
|
16
|
+
def touches(self, prefix: str) -> bool:
|
|
17
|
+
"""True if any file is exactly `prefix` or lives under `prefix/`."""
|
|
18
|
+
needle = prefix.rstrip("/")
|
|
19
|
+
return any(f == needle or f.startswith(needle + "/") for f in self.files)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Exact evidence-boundary facts for historical reconstruction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EvidenceCompleteness(StrEnum):
|
|
10
|
+
"""How much of the declared evidence scope was deterministically established."""
|
|
11
|
+
|
|
12
|
+
COMPLETE = "complete"
|
|
13
|
+
BOUNDED = "bounded"
|
|
14
|
+
INCOMPLETE = "incomplete"
|
|
15
|
+
UNKNOWN = "unknown"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EvidenceScopeKind(StrEnum):
|
|
19
|
+
"""The historical scope whose completeness is being described."""
|
|
20
|
+
|
|
21
|
+
REPOSITORY = "repository"
|
|
22
|
+
MODULE = "module"
|
|
23
|
+
RECENT_HISTORY = "recent-history"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class EvidenceLimitation:
|
|
28
|
+
"""One exact reason an evidence scope is not complete."""
|
|
29
|
+
|
|
30
|
+
code: str
|
|
31
|
+
message: str
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> dict[str, str]:
|
|
34
|
+
return {"code": self.code, "message": self.message}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class EvidenceBoundary:
|
|
39
|
+
"""Machine-verifiable boundary for a backfill evidence result."""
|
|
40
|
+
|
|
41
|
+
completeness: EvidenceCompleteness
|
|
42
|
+
scope_kind: EvidenceScopeKind
|
|
43
|
+
scope_path: str
|
|
44
|
+
cutoff_sha: str
|
|
45
|
+
cutoff_tree: str | None
|
|
46
|
+
repository_roots: tuple[str, ...] = ()
|
|
47
|
+
limitations: tuple[EvidenceLimitation, ...] = ()
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> dict[str, object]:
|
|
50
|
+
return {
|
|
51
|
+
"completeness": self.completeness.value,
|
|
52
|
+
"scope": {"kind": self.scope_kind.value, "path": self.scope_path},
|
|
53
|
+
"cutoff_sha": self.cutoff_sha,
|
|
54
|
+
"cutoff_tree": self.cutoff_tree,
|
|
55
|
+
"repository_roots": list(self.repository_roots),
|
|
56
|
+
"limitations": [limitation.to_dict() for limitation in self.limitations],
|
|
57
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Finding — the one warning/error type every linter emits."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Severity(StrEnum):
|
|
10
|
+
INFO = "info"
|
|
11
|
+
WARNING = "warning"
|
|
12
|
+
ERROR = "error"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, order=False)
|
|
16
|
+
class Finding:
|
|
17
|
+
file: str
|
|
18
|
+
line: int
|
|
19
|
+
severity: Severity
|
|
20
|
+
code: str
|
|
21
|
+
message: str
|
|
22
|
+
hint: str | None = field(default=None)
|
|
23
|
+
|
|
24
|
+
def __lt__(self, other: Finding) -> bool:
|
|
25
|
+
return (self.file, self.line, self.code) < (other.file, other.line, other.code)
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict:
|
|
28
|
+
payload = {
|
|
29
|
+
"file": self.file,
|
|
30
|
+
"line": self.line,
|
|
31
|
+
"severity": self.severity.value,
|
|
32
|
+
"code": self.code,
|
|
33
|
+
"message": self.message,
|
|
34
|
+
}
|
|
35
|
+
if self.hint is not None:
|
|
36
|
+
payload["hint"] = self.hint
|
|
37
|
+
return payload
|