sourcecode 4.2.0__py3-none-any.whl → 4.3.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.
- sourcecode/__init__.py +1 -1
- sourcecode/architectural_baseline.py +19 -0
- sourcecode/baseline_autocapture.py +375 -0
- sourcecode/cache_model.py +19 -0
- sourcecode/cli.py +552 -23
- sourcecode/client_calls.py +305 -0
- sourcecode/context_graph.py +11 -0
- sourcecode/data_exposure.py +353 -0
- sourcecode/data_labels.py +200 -0
- sourcecode/dynamic_argument_surface.py +12 -5
- sourcecode/format_contract.py +1 -0
- sourcecode/non_coverage.py +45 -0
- sourcecode/openrewrite_recipe.py +278 -0
- sourcecode/posture.py +32 -0
- sourcecode/repository_ir.py +24 -3
- sourcecode/risk.py +453 -92
- sourcecode/sarif.py +618 -0
- sourcecode/validation_inference.py +33 -4
- sourcecode/verify_repo.py +17 -2
- {sourcecode-4.2.0.dist-info → sourcecode-4.3.0.dist-info}/METADATA +27 -8
- {sourcecode-4.2.0.dist-info → sourcecode-4.3.0.dist-info}/RECORD +24 -18
- {sourcecode-4.2.0.dist-info → sourcecode-4.3.0.dist-info}/WHEEL +0 -0
- {sourcecode-4.2.0.dist-info → sourcecode-4.3.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-4.2.0.dist-info → sourcecode-4.3.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -312,6 +312,25 @@ def load_baseline(path: Path) -> dict:
|
|
|
312
312
|
return data
|
|
313
313
|
|
|
314
314
|
|
|
315
|
+
def latest_baseline_path(directory: Path) -> "Path | None":
|
|
316
|
+
"""The most recently captured baseline in `directory`, or None if there is none.
|
|
317
|
+
|
|
318
|
+
Ordered exactly as `load_baselines_dir` orders its series (capture time, commit
|
|
319
|
+
as the tie-break), so "the last point in the trend" and "the base a diff uses by
|
|
320
|
+
default" can never disagree about which artifact that is.
|
|
321
|
+
"""
|
|
322
|
+
dated: list[tuple[str, str, Path]] = []
|
|
323
|
+
for p in sorted(Path(directory).glob("*.json")):
|
|
324
|
+
try:
|
|
325
|
+
b = load_baseline(p)
|
|
326
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
327
|
+
continue
|
|
328
|
+
dated.append((str(b.get("captured_at", "")), str(b.get("commit", "")), p))
|
|
329
|
+
if not dated:
|
|
330
|
+
return None
|
|
331
|
+
return max(dated, key=lambda t: (t[0], t[1]))[2]
|
|
332
|
+
|
|
333
|
+
|
|
315
334
|
def load_baselines_dir(directory: Path) -> list[dict]:
|
|
316
335
|
"""Load every architectural baseline in `directory`, sorted by capture time."""
|
|
317
336
|
out: list[dict] = []
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""baseline_autocapture.py — the half of the architectural history that is not a command.
|
|
2
|
+
|
|
3
|
+
`ask baseline capture` has shipped since D5 and produces a history exactly as long as
|
|
4
|
+
the number of times a human remembered to type it. Field audit #8 named that plainly:
|
|
5
|
+
the artifact a competitor cannot backfill is *three years of a specific monolith's
|
|
6
|
+
architectural baselines*, and **a history that depends on someone remembering to invoke
|
|
7
|
+
it will not exist**. So capture has to be a by-product of a run that was going to happen
|
|
8
|
+
anyway — the CI gate, which already builds the same IR the baseline is extracted from.
|
|
9
|
+
|
|
10
|
+
This module is the single authority for the question *"should this run leave a baseline
|
|
11
|
+
behind, and what happened when it tried?"*. It is deliberately small and conservative,
|
|
12
|
+
because every branch here is a side effect inside somebody else's repository:
|
|
13
|
+
|
|
14
|
+
* **It never starts a history nobody asked for.** A piggybacked run captures only where
|
|
15
|
+
a history already exists (`.ask/baselines/`) or where the operator opted in explicitly
|
|
16
|
+
(`--auto`, `ASK_BASELINE_AUTOCAPTURE=1`). The read-only complaint that `--dir` closed
|
|
17
|
+
is not going to be re-opened by a command that writes on its own initiative.
|
|
18
|
+
* **One commit, one baseline.** The history is keyed by commit; re-capturing a commit
|
|
19
|
+
would silently relabel a point already in the series (and, after a tool upgrade, change
|
|
20
|
+
its metrics under the same key — the comparability contract exists precisely so that
|
|
21
|
+
cannot pass unnoticed). An already-captured commit is a skip, not an overwrite.
|
|
22
|
+
* **A dirty tree is not a commit.** Writing `<commit>.json` from a modified working tree
|
|
23
|
+
attributes measurements to a state that commit never had. Skip and say so.
|
|
24
|
+
* **It never fails the run it rode in on.** A read-only checkout, a full disk, a `.ask`
|
|
25
|
+
owned by another user: the host command's verdict is about the host command's question.
|
|
26
|
+
The capture reports what it could not do; it does not change an exit code.
|
|
27
|
+
|
|
28
|
+
Every outcome is published (`action`, `reason`, `statement`) rather than being silent —
|
|
29
|
+
I-8: a run declares the effect of what it did and did not do, and "no history was written"
|
|
30
|
+
is exactly the kind of non-event that is invisible until someone needs the series.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
import subprocess
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import TYPE_CHECKING, Iterable, Optional
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
42
|
+
from sourcecode.canonical_ir import CanonicalRepositoryIR
|
|
43
|
+
|
|
44
|
+
#: Where a history lives when nobody said otherwise — inside the repository, so it
|
|
45
|
+
#: travels with the code (same default as `ask baseline capture`).
|
|
46
|
+
HISTORY_DIRNAME: Path = Path(".ask") / "baselines"
|
|
47
|
+
|
|
48
|
+
#: Explicit operator switch. "1"/"true"/"yes"/"on" enables piggybacked capture even
|
|
49
|
+
#: without an existing history; "0"/"false"/"no"/"off" disables it even with one.
|
|
50
|
+
ENV_FLAG: str = "ASK_BASELINE_AUTOCAPTURE"
|
|
51
|
+
|
|
52
|
+
_TRUE = frozenset({"1", "true", "yes", "on"})
|
|
53
|
+
_FALSE = frozenset({"0", "false", "no", "off"})
|
|
54
|
+
|
|
55
|
+
# Actions.
|
|
56
|
+
CAPTURED: str = "captured"
|
|
57
|
+
SKIPPED: str = "skipped"
|
|
58
|
+
|
|
59
|
+
# Reasons to capture.
|
|
60
|
+
HISTORY_PRESENT: str = "history_present"
|
|
61
|
+
EXPLICITLY_ENABLED: str = "explicitly_enabled"
|
|
62
|
+
|
|
63
|
+
# Reasons to skip.
|
|
64
|
+
NO_HISTORY: str = "no_history"
|
|
65
|
+
DISABLED: str = "disabled"
|
|
66
|
+
NO_COMMIT_IDENTITY: str = "no_commit_identity"
|
|
67
|
+
WORKTREE_DIRTY: str = "worktree_dirty"
|
|
68
|
+
WORKTREE_STATE_UNKNOWN: str = "worktree_state_unknown"
|
|
69
|
+
COMMIT_ALREADY_CAPTURED: str = "commit_already_captured"
|
|
70
|
+
WRITE_FAILED: str = "write_failed"
|
|
71
|
+
NO_ANALYSIS: str = "no_analysis"
|
|
72
|
+
|
|
73
|
+
_STATEMENTS: dict[str, str] = {
|
|
74
|
+
HISTORY_PRESENT: (
|
|
75
|
+
"This repository already keeps an architectural history, so this run added "
|
|
76
|
+
"the current commit to it."
|
|
77
|
+
),
|
|
78
|
+
EXPLICITLY_ENABLED: (
|
|
79
|
+
"Automatic capture was requested, so this run added the current commit to the "
|
|
80
|
+
"architectural history."
|
|
81
|
+
),
|
|
82
|
+
NO_HISTORY: (
|
|
83
|
+
"No architectural history exists here and none was requested, so nothing was "
|
|
84
|
+
"written. A series cannot be reconstructed later: start one with "
|
|
85
|
+
"`ask baseline capture <path>` (or `--auto` in the pipeline that already runs)."
|
|
86
|
+
),
|
|
87
|
+
DISABLED: (
|
|
88
|
+
f"Automatic capture is switched off ({ENV_FLAG}), so this run added nothing to "
|
|
89
|
+
"the architectural history."
|
|
90
|
+
),
|
|
91
|
+
NO_COMMIT_IDENTITY: (
|
|
92
|
+
"The path has no resolvable git commit, so a captured baseline could not be "
|
|
93
|
+
"joined to anything later. Nothing was written."
|
|
94
|
+
),
|
|
95
|
+
WORKTREE_DIRTY: (
|
|
96
|
+
"The working tree carries uncommitted changes, so measurements taken now do not "
|
|
97
|
+
"describe the commit they would be filed under. Nothing was written."
|
|
98
|
+
),
|
|
99
|
+
WORKTREE_STATE_UNKNOWN: (
|
|
100
|
+
"Whether the working tree is clean could not be determined, so the measurements "
|
|
101
|
+
"could not be attributed to the commit. Nothing was written."
|
|
102
|
+
),
|
|
103
|
+
COMMIT_ALREADY_CAPTURED: (
|
|
104
|
+
"This commit is already in the architectural history; the stored baseline was "
|
|
105
|
+
"kept as captured (re-capturing would relabel a point in the series)."
|
|
106
|
+
),
|
|
107
|
+
WRITE_FAILED: (
|
|
108
|
+
"The architectural history could not be written (see `error`). The verdict of "
|
|
109
|
+
"this run is unaffected — capture never changes what the command reports."
|
|
110
|
+
),
|
|
111
|
+
NO_ANALYSIS: (
|
|
112
|
+
"This run did not analyse the repository, so there were no measurements to add "
|
|
113
|
+
"to the architectural history. Nothing was written."
|
|
114
|
+
),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class CaptureDecision:
|
|
120
|
+
"""What a run should do about the architectural history, and why."""
|
|
121
|
+
|
|
122
|
+
action: str # CAPTURED (intent, before execution) | SKIPPED
|
|
123
|
+
reason: str
|
|
124
|
+
history_dir: Optional[str] = None
|
|
125
|
+
commit: Optional[str] = None
|
|
126
|
+
baseline_file: Optional[str] = None
|
|
127
|
+
history_size: Optional[int] = None
|
|
128
|
+
error: Optional[str] = None
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def statement(self) -> str:
|
|
132
|
+
return _STATEMENTS.get(self.reason, "")
|
|
133
|
+
|
|
134
|
+
def to_dict(self) -> dict:
|
|
135
|
+
out = {
|
|
136
|
+
"action": self.action,
|
|
137
|
+
"reason": self.reason,
|
|
138
|
+
"statement": self.statement,
|
|
139
|
+
"history_dir": self.history_dir,
|
|
140
|
+
"commit": self.commit,
|
|
141
|
+
"baseline_file": self.baseline_file,
|
|
142
|
+
"history_size": self.history_size,
|
|
143
|
+
}
|
|
144
|
+
if self.error is not None:
|
|
145
|
+
out["error"] = self.error
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def env_opt_in(environ: "Optional[dict[str, str]]" = None) -> Optional[bool]:
|
|
150
|
+
"""Tri-state read of the operator switch: True / False / None (never asked).
|
|
151
|
+
|
|
152
|
+
None is a distinct answer on purpose — the same rule the telemetry consent and
|
|
153
|
+
the fact registry follow. "Nobody configured this" is not "somebody said no".
|
|
154
|
+
"""
|
|
155
|
+
raw = (environ if environ is not None else os.environ).get(ENV_FLAG)
|
|
156
|
+
if raw is None:
|
|
157
|
+
return None
|
|
158
|
+
value = raw.strip().lower()
|
|
159
|
+
if value in _TRUE:
|
|
160
|
+
return True
|
|
161
|
+
if value in _FALSE:
|
|
162
|
+
return False
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def history_dir_for(root: Path, out_dir: "Optional[Path]" = None) -> Path:
|
|
167
|
+
"""The directory a repository's baselines live in."""
|
|
168
|
+
return Path(out_dir).resolve() if out_dir is not None else Path(root) / HISTORY_DIRNAME
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _porcelain_path(line: str) -> str:
|
|
172
|
+
"""The path a `git status --porcelain` line refers to (the destination on renames)."""
|
|
173
|
+
body = line[3:] if len(line) > 3 else ""
|
|
174
|
+
if " -> " in body: # rename/copy: `R old -> new`
|
|
175
|
+
body = body.split(" -> ", 1)[1]
|
|
176
|
+
return body.strip().strip('"')
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def worktree_dirty(
|
|
180
|
+
root: Path, ignore: "Optional[Path | Iterable[Path]]" = None
|
|
181
|
+
) -> Optional[bool]:
|
|
182
|
+
"""True/False for a git worktree, None when the state could not be determined.
|
|
183
|
+
|
|
184
|
+
`git status --porcelain` honours `.gitignore`, so build output does not count as
|
|
185
|
+
a modification — but an untracked `.java` file does, because it is in the IR the
|
|
186
|
+
baseline measures.
|
|
187
|
+
|
|
188
|
+
`ignore` drops directories from the answer, and callers pass exactly one thing:
|
|
189
|
+
**this tool's own footprint** (`.ask/`, and the history directory when `--dir`
|
|
190
|
+
put it elsewhere). Two measured reasons, not a convenience:
|
|
191
|
+
|
|
192
|
+
* the first capture leaves an untracked file inside the repository, so without
|
|
193
|
+
this the history would report a dirty tree from its second run onward — a
|
|
194
|
+
series of exactly one;
|
|
195
|
+
* `.ask/contracts.yml` is what a gate run is *for*, and in a repository that has
|
|
196
|
+
not committed it yet, counting it as a modification of the code would mean the
|
|
197
|
+
history never starts.
|
|
198
|
+
|
|
199
|
+
Nothing else is excused, because everything else is what the baseline measures.
|
|
200
|
+
"""
|
|
201
|
+
try:
|
|
202
|
+
out = subprocess.run(
|
|
203
|
+
# `-uall` because git collapses an untracked directory to one entry
|
|
204
|
+
# (`?? .ask/`), and the whole point here is to tell OUR untracked file
|
|
205
|
+
# apart from the source file next to it.
|
|
206
|
+
["git", "-C", str(root), "status", "--porcelain", "-uall"],
|
|
207
|
+
capture_output=True, text=True, timeout=30,
|
|
208
|
+
)
|
|
209
|
+
except (OSError, subprocess.SubprocessError):
|
|
210
|
+
return None
|
|
211
|
+
if out.returncode != 0:
|
|
212
|
+
return None
|
|
213
|
+
lines = [ln for ln in out.stdout.splitlines() if ln.strip()]
|
|
214
|
+
if ignore is None:
|
|
215
|
+
return bool(lines)
|
|
216
|
+
candidates = [ignore] if isinstance(ignore, (str, Path)) else list(ignore)
|
|
217
|
+
prefixes: list[str] = []
|
|
218
|
+
for candidate in candidates:
|
|
219
|
+
try:
|
|
220
|
+
rel = Path(candidate).resolve().relative_to(Path(root).resolve()).as_posix()
|
|
221
|
+
except ValueError: # outside the repo (`--dir`): nothing of it to drop
|
|
222
|
+
continue
|
|
223
|
+
prefixes.append(rel.rstrip("/") + "/")
|
|
224
|
+
remaining = [
|
|
225
|
+
ln for ln in lines
|
|
226
|
+
if not any(
|
|
227
|
+
(_porcelain_path(ln).rstrip("/") + "/").startswith(p) for p in prefixes
|
|
228
|
+
)
|
|
229
|
+
]
|
|
230
|
+
return bool(remaining)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def history_size(history_dir: Path) -> Optional[int]:
|
|
234
|
+
"""How many baselines the history holds, or None when there is no history."""
|
|
235
|
+
try:
|
|
236
|
+
if not history_dir.is_dir():
|
|
237
|
+
return None
|
|
238
|
+
return sum(1 for _ in history_dir.glob("*.json"))
|
|
239
|
+
except OSError:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def decide(
|
|
244
|
+
*,
|
|
245
|
+
history_dir: Path,
|
|
246
|
+
history_exists: bool,
|
|
247
|
+
opt_in: Optional[bool],
|
|
248
|
+
commit: Optional[str],
|
|
249
|
+
dirty: Optional[bool],
|
|
250
|
+
already_captured: Optional[Path],
|
|
251
|
+
size: Optional[int] = None,
|
|
252
|
+
) -> CaptureDecision:
|
|
253
|
+
"""Pure policy: given the facts, capture or skip — with the reason named.
|
|
254
|
+
|
|
255
|
+
Split from execution so the policy is testable without a repository, and so the
|
|
256
|
+
order of the checks is visible in one place: consent first, then identity, then
|
|
257
|
+
attribution, then idempotency.
|
|
258
|
+
"""
|
|
259
|
+
common = {
|
|
260
|
+
"history_dir": str(history_dir),
|
|
261
|
+
"commit": commit,
|
|
262
|
+
"history_size": size,
|
|
263
|
+
}
|
|
264
|
+
if opt_in is False:
|
|
265
|
+
return CaptureDecision(SKIPPED, DISABLED, **common)
|
|
266
|
+
if opt_in is not True and not history_exists:
|
|
267
|
+
return CaptureDecision(SKIPPED, NO_HISTORY, **common)
|
|
268
|
+
if not commit:
|
|
269
|
+
return CaptureDecision(SKIPPED, NO_COMMIT_IDENTITY, **common)
|
|
270
|
+
if dirty is None:
|
|
271
|
+
return CaptureDecision(SKIPPED, WORKTREE_STATE_UNKNOWN, **common)
|
|
272
|
+
if dirty:
|
|
273
|
+
return CaptureDecision(SKIPPED, WORKTREE_DIRTY, **common)
|
|
274
|
+
if already_captured is not None:
|
|
275
|
+
return CaptureDecision(
|
|
276
|
+
SKIPPED, COMMIT_ALREADY_CAPTURED,
|
|
277
|
+
baseline_file=str(already_captured), **common
|
|
278
|
+
)
|
|
279
|
+
return CaptureDecision(
|
|
280
|
+
CAPTURED, HISTORY_PRESENT if history_exists else EXPLICITLY_ENABLED, **common
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _existing_baseline(history_dir: Path, commit: str) -> Optional[Path]:
|
|
285
|
+
candidate = history_dir / f"{commit}.json"
|
|
286
|
+
try:
|
|
287
|
+
return candidate if candidate.is_file() else None
|
|
288
|
+
except OSError:
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def plan(
|
|
293
|
+
root: Path,
|
|
294
|
+
*,
|
|
295
|
+
out_dir: "Optional[Path]" = None,
|
|
296
|
+
explicit: bool = False,
|
|
297
|
+
environ: "Optional[dict[str, str]]" = None,
|
|
298
|
+
) -> CaptureDecision:
|
|
299
|
+
"""Gather the repository facts and apply `decide` to them."""
|
|
300
|
+
root = Path(root)
|
|
301
|
+
from sourcecode.architectural_baseline import git_commit
|
|
302
|
+
|
|
303
|
+
hist = history_dir_for(root, out_dir)
|
|
304
|
+
exists = hist.is_dir()
|
|
305
|
+
opt_in = True if explicit else env_opt_in(environ)
|
|
306
|
+
|
|
307
|
+
# Consent is the cheapest question and the most common answer, so it is asked
|
|
308
|
+
# first: a command that piggybacks on every run must cost nothing at all in a
|
|
309
|
+
# repository that keeps no history. `git status -uall` on a monolith is not
|
|
310
|
+
# nothing.
|
|
311
|
+
if opt_in is False or (opt_in is not True and not exists):
|
|
312
|
+
return decide(
|
|
313
|
+
history_dir=hist, history_exists=exists, opt_in=opt_in,
|
|
314
|
+
commit=None, dirty=None, already_captured=None, size=history_size(hist),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
commit = git_commit(root)
|
|
318
|
+
# The tool's own footprint, and only that.
|
|
319
|
+
own = (hist, Path(root) / HISTORY_DIRNAME.parts[0])
|
|
320
|
+
dirty = worktree_dirty(root, ignore=own) if commit else None
|
|
321
|
+
already = _existing_baseline(hist, commit) if commit else None
|
|
322
|
+
return decide(
|
|
323
|
+
history_dir=hist,
|
|
324
|
+
history_exists=exists,
|
|
325
|
+
opt_in=opt_in,
|
|
326
|
+
commit=commit,
|
|
327
|
+
dirty=dirty,
|
|
328
|
+
already_captured=already,
|
|
329
|
+
size=history_size(hist),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def run(
|
|
334
|
+
root: Path,
|
|
335
|
+
cir: "Optional[CanonicalRepositoryIR]" = None,
|
|
336
|
+
*,
|
|
337
|
+
out_dir: "Optional[Path]" = None,
|
|
338
|
+
explicit: bool = False,
|
|
339
|
+
ref: "Optional[str]" = None,
|
|
340
|
+
environ: "Optional[dict[str, str]]" = None,
|
|
341
|
+
decision: "Optional[CaptureDecision]" = None,
|
|
342
|
+
) -> CaptureDecision:
|
|
343
|
+
"""Decide, and when the decision is to capture, write the baseline.
|
|
344
|
+
|
|
345
|
+
`cir` is the IR the host command already built — capture adds a metric extraction
|
|
346
|
+
and a file write, never a second analysis. Passing None with a capture decision is
|
|
347
|
+
a caller bug, reported as a skip rather than raised: this function's contract is
|
|
348
|
+
that it cannot fail the run that called it.
|
|
349
|
+
"""
|
|
350
|
+
plan_ = decision if decision is not None else plan(
|
|
351
|
+
root, out_dir=out_dir, explicit=explicit, environ=environ
|
|
352
|
+
)
|
|
353
|
+
if plan_.action != CAPTURED:
|
|
354
|
+
return plan_
|
|
355
|
+
|
|
356
|
+
from sourcecode.architectural_baseline import build_baseline, write_baseline
|
|
357
|
+
|
|
358
|
+
hist = history_dir_for(Path(root), out_dir)
|
|
359
|
+
try:
|
|
360
|
+
if cir is None:
|
|
361
|
+
raise ValueError("no IR was available to capture from")
|
|
362
|
+
baseline = build_baseline(cir, ref=ref, commit=plan_.commit)
|
|
363
|
+
written = write_baseline(baseline, hist)
|
|
364
|
+
except Exception as exc: # a side effect never decides the host command's verdict
|
|
365
|
+
return CaptureDecision(
|
|
366
|
+
SKIPPED, WRITE_FAILED,
|
|
367
|
+
history_dir=str(hist), commit=plan_.commit,
|
|
368
|
+
history_size=plan_.history_size, error=f"{type(exc).__name__}: {exc}",
|
|
369
|
+
)
|
|
370
|
+
return CaptureDecision(
|
|
371
|
+
CAPTURED, plan_.reason,
|
|
372
|
+
history_dir=str(hist), commit=plan_.commit,
|
|
373
|
+
baseline_file=str(written),
|
|
374
|
+
history_size=(plan_.history_size or 0) + 1,
|
|
375
|
+
)
|
sourcecode/cache_model.py
CHANGED
|
@@ -138,6 +138,25 @@ COMMANDS: tuple[CommandCache, ...] = (
|
|
|
138
138
|
"within the run.",
|
|
139
139
|
"not measured on the battery yet — the composition is bounded by the "
|
|
140
140
|
"`spring-audit` + `impact-chain` costs listed here, not by new analysis"),
|
|
141
|
+
CommandCache("enrich", ("cir", "parse"), "shared", False,
|
|
142
|
+
"Runs the same composition as `risk` over the repository, then joins a SARIF "
|
|
143
|
+
"log to it. Reading the log is negligible; everything a warm helps with is the "
|
|
144
|
+
"repository side, so what it buys is what it buys `risk`.",
|
|
145
|
+
"not measured on the battery yet — bounded by the `risk` composition, plus "
|
|
146
|
+
"reading one JSON file"),
|
|
147
|
+
CommandCache("migrate-recipe", ("parse",), "shared", False,
|
|
148
|
+
"Runs the same scan as `migrate-check` and projects its findings into an "
|
|
149
|
+
"OpenRewrite recipe, so it buys exactly what a warm buys `migrate-check`: "
|
|
150
|
+
"the parse, not the rule pass.",
|
|
151
|
+
"not measured on the battery yet — bounded by `migrate-check` on the same "
|
|
152
|
+
"repository (openmrs-core ~2 s)"),
|
|
153
|
+
CommandCache("data-exposure", ("cir", "parse"), "shared", False,
|
|
154
|
+
"Walks the same call reach as `impact-chain` once per declared type and "
|
|
155
|
+
"reads the endpoint security surface, both over the shared CIR a warm "
|
|
156
|
+
"builds. Cost scales with the number of declared types, not with the "
|
|
157
|
+
"size of the label.",
|
|
158
|
+
"not measured on the battery yet — one `impact-chain` traversal per "
|
|
159
|
+
"declared seed type over a CIR the warm already paid for"),
|
|
141
160
|
CommandCache("endpoints", ("ris", "parse"), "shared", False,
|
|
142
161
|
"Recomputes the endpoint surface on every run, over a parse a warm has already "
|
|
143
162
|
"paid for. Until 3.7.0 the extractor parsed every file itself instead of reading "
|