sourcecode 4.14.0__py3-none-any.whl → 4.15.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 CHANGED
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "4.14.0"
7
+ __version__ = "4.15.0"
sourcecode/cli.py CHANGED
@@ -1261,16 +1261,23 @@ def _analysis_budget(command: str) -> dict[str, Any]:
1261
1261
  command, ("repo-wide", 600, "run this command in a supervised or nightly job")
1262
1262
  )
1263
1263
  configured: Optional[float] = None
1264
+ source: Optional[str] = None
1264
1265
  raw = os.environ.get("ASK_MAX_ANALYSIS_SECONDS")
1265
1266
  if raw:
1266
1267
  try:
1267
1268
  configured = float(raw)
1268
1269
  except ValueError:
1269
1270
  configured = None
1271
+ else:
1272
+ # C3-83: a budget is inherited from a shell or a CI runner, so the
1273
+ # person reading `total_defects` is often not the person who set it.
1274
+ # Where it came from travels with it, from here to `_partial`.
1275
+ source = f"ASK_MAX_ANALYSIS_SECONDS={raw} (process environment)"
1270
1276
  return {
1271
1277
  "class": cls,
1272
1278
  "recommended_min_seconds": recommended,
1273
1279
  "configured_max_seconds": configured,
1280
+ "configured_source": source,
1274
1281
  "fallback": fallback,
1275
1282
  }
1276
1283
 
@@ -1318,9 +1325,23 @@ def _enforce_analysis_budget(
1318
1325
  """
1319
1326
  configured = budget.get("configured_max_seconds")
1320
1327
  recommended = float(budget.get("recommended_min_seconds") or 0)
1321
- if configured is None or float(configured) >= recommended:
1328
+ if configured is None:
1329
+ return
1330
+ source = budget.get("configured_source")
1331
+ from sourcecode.phased_run import budget_provenance, budget_warning
1332
+
1333
+ if float(configured) >= recommended:
1334
+ # C3-83: over the floor there is no advice to give, and there is still a
1335
+ # fact the reader of the answer needs — a limit set in an environment is
1336
+ # invisible to whoever reads `total_defects` afterwards.
1337
+ if not source:
1338
+ return
1339
+ try:
1340
+ sys.stderr.write(budget_provenance(command, float(configured), source))
1341
+ sys.stderr.flush()
1342
+ except Exception:
1343
+ pass
1322
1344
  return
1323
- from sourcecode.phased_run import budget_warning
1324
1345
 
1325
1346
  _anchors, _scope = _budget_evidence(command, path)
1326
1347
  try:
@@ -1328,7 +1349,7 @@ def _enforce_analysis_budget(
1328
1349
  budget_warning(
1329
1350
  command, str(budget.get("class") or "repo-wide"),
1330
1351
  float(configured), recommended,
1331
- anchors=_anchors, scope=_scope,
1352
+ anchors=_anchors, scope=_scope, source=source,
1332
1353
  )
1333
1354
  )
1334
1355
  sys.stderr.flush()
@@ -1960,12 +1981,14 @@ def _output_option(help: "Optional[str]" = None) -> Any:
1960
1981
  COMMANDS_WITHOUT_OUTPUT: "frozenset[str]" = frozenset()
1961
1982
 
1962
1983
 
1963
- #: The one help string for `--jobs` (F-AQ).
1984
+ #: The one help string for `--jobs` (F-AQ; the scope sentence is C3-84/CL-18).
1964
1985
  JOBS_OPTION_HELP = (
1965
1986
  "Worker processes used to parse files (default: CPU count − 1). "
1966
1987
  "`--jobs 1` parses in this process. Parallelism only warms the "
1967
1988
  "content-addressed parse cache — the answer is byte-identical at any value. "
1968
- "Env: ASK_JOBS."
1989
+ "It does NOT parallelise the rule pass, which is where a repository-wide "
1990
+ "audit spends most of its time, and on a warm cache there is nothing left "
1991
+ "to parse, so raising it buys nothing there. Env: ASK_JOBS."
1969
1992
  )
1970
1993
 
1971
1994
 
@@ -1980,6 +2003,18 @@ def _jobs_option() -> Any:
1980
2003
  repository — so it is recorded in :mod:`sourcecode.parallel` rather than
1981
2004
  threaded through every analysis signature. It is published only on commands
1982
2005
  that parse a repository; `ASK_JOBS` covers the rest.
2006
+
2007
+ **C3-84 / CL-18.** The fifth evaluation read this help, sampled the process
2008
+ and corrected four rounds of its own diagnosis: parallelism exists and covers
2009
+ the phase that is not the cost. `threads=4` for the first seconds (parsing),
2010
+ then `threads=1` with CPU/wall **0,97** for the remaining ~99 % — the rule
2011
+ pass, applying families over a built IR — and on a warm cache, which is the
2012
+ normal case, there is nothing to parse at all. The old sentence was true
2013
+ about what the flag *does* and silent about what it does not, which is the
2014
+ half that decides whether setting it is worth anything. Saying so costs a
2015
+ line; letting an operator raise it and measure no change costs trust. The
2016
+ capability that makes the rule pass parallel is F-AT, and no help text is a
2017
+ substitute for it.
1983
2018
  """
1984
2019
  return typer.Option(None, "--jobs", "-j", min=1, help=JOBS_OPTION_HELP)
1985
2020
 
@@ -8181,6 +8216,8 @@ def spring_audit_cmd(
8181
8216
  phases=_phases,
8182
8217
  output_path=output_path,
8183
8218
  budget_seconds=_analysis_budget("spring-audit").get("configured_max_seconds"),
8219
+ # C3-83: the budget's origin travels with the budget, into `_partial`.
8220
+ budget_source=_analysis_budget("spring-audit").get("configured_source"),
8184
8221
  writer=_safe_write_file,
8185
8222
  )
8186
8223
  _stopped_early: Optional[str] = None
@@ -8899,6 +8936,8 @@ def risk_cmd(
8899
8936
  phases=["audit", "compose"],
8900
8937
  output_path=output_path,
8901
8938
  budget_seconds=_analysis_budget("risk").get("configured_max_seconds"),
8939
+ # C3-83: the budget's origin travels with the budget, into `_partial`.
8940
+ budget_source=_analysis_budget("risk").get("configured_source"),
8902
8941
  writer=_safe_write_file,
8903
8942
  )
8904
8943
  with _expensive_analysis_scope("risk", path, phase):
sourcecode/phased_run.py CHANGED
@@ -88,6 +88,11 @@ class PhasedRun:
88
88
  phases: "list[str]"
89
89
  output_path: Optional[Path] = None
90
90
  budget_seconds: Optional[float] = None
91
+ #: Where `budget_seconds` came from, verbatim (C3-83) — e.g.
92
+ #: `ASK_MAX_ANALYSIS_SECONDS=420 (process environment)`. A truncated answer
93
+ #: declares everything about *being* truncated and could not say what decided
94
+ #: it, and the variable is inherited from a shell or a runner.
95
+ budget_source: Optional[str] = None
91
96
  #: injected by the CLI so a checkpoint uses the same durable write as the
92
97
  #: final answer (fsync + atomic rename), rather than a second, worse one.
93
98
  writer: Optional[Callable[[Path, str], None]] = None
@@ -140,6 +145,8 @@ class PhasedRun:
140
145
  }
141
146
  if self.budget_seconds:
142
147
  out["budget_seconds"] = float(self.budget_seconds)
148
+ if self.budget_source:
149
+ out["budget_source"] = self.budget_source
143
150
  return out
144
151
 
145
152
  # -- checkpoints -------------------------------------------------------
@@ -210,6 +217,7 @@ def budget_warning(
210
217
  *,
211
218
  anchors: Optional[str] = None,
212
219
  scope: Optional[str] = None,
220
+ source: Optional[str] = None,
213
221
  ) -> str:
214
222
  """What the operator is told when the budget is under the class floor.
215
223
 
@@ -238,6 +246,10 @@ def budget_warning(
238
246
  f"[ask] budget: {command} is a {cls} analysis and the configured budget "
239
247
  f"is {configured:g}s."
240
248
  ]
249
+ if source:
250
+ # C3-83: name the origin, because a variable inherited from a shell or a
251
+ # runner is the one setting nobody in the conversation remembers choosing.
252
+ lines.append(f" Budget read from {source}.")
241
253
  if anchors:
242
254
  lines.append(f" Measured for this command: {anchors}.")
243
255
  else:
@@ -259,3 +271,24 @@ def budget_warning(
259
271
  "the ones that did not. It is a floor, not a verdict."
260
272
  )
261
273
  return "\n".join(lines) + "\n"
274
+
275
+
276
+ def budget_provenance(command: str, configured: float, source: str) -> str:
277
+ """One line naming where a budget over the class floor came from (C3-83).
278
+
279
+ Above the floor there is no advice to give and there is still a fact worth
280
+ stating. The field measured what its absence costs: the same build answers
281
+ **395 findings / 98 defects** without a budget and **267 / 11** with
282
+ `ASK_MAX_ANALYSIS_SECONDS=420`, and everything about the truncation is
283
+ declared in the payload — `partial`, `counts_are_floor`, a capped confidence
284
+ — while the *reason* the run was cut lives in an environment variable the
285
+ reader of that payload never saw. So it is named where the run happens, once,
286
+ on stderr, next to the command it is about.
287
+ """
288
+ return (
289
+ f"[ask] budget: {command} will stop after {configured:g}s. Budget read "
290
+ f"from {source}.\n"
291
+ " A run that hits it publishes a FLOOR: `summary.partial`, "
292
+ "`summary.counts_are_floor` and `_partial` say so. Unset the variable "
293
+ "for a complete audit.\n"
294
+ )
@@ -27,7 +27,7 @@ from typing import Any, Optional
27
27
  #: The date `__version__` was released, from that version's CHANGELOG entry.
28
28
  #: `tests/test_release_info.py` fails the build if the two disagree, so this is
29
29
  #: a copy of a fact rather than a second authority for it.
30
- RELEASE_DATE = "2026-08-09"
30
+ RELEASE_DATE = "2026-08-10"
31
31
 
32
32
  #: Releases per day, measured over the 30 releases before this one (the same
33
33
  #: computation the test re-runs against the CHANGELOG). Stable across window
sourcecode/rule_pass.py CHANGED
@@ -155,20 +155,37 @@ class RulePassProgress:
155
155
  from the family's duration to a batch's, and the cost of the check is a
156
156
  modulo per unit.
157
157
 
158
+ **C3-82.** The sampling constant belongs to the *clock* and not to the
159
+ *counter*, and until this the two shared it. The field measured what that
160
+ costs: `1856/3342 java files` at 10,0 s, then **13,6 minutes with no line
161
+ at all**, then `3328/3342` — because a report that lands between two batch
162
+ boundaries is never produced, and a stretch of expensive units (or one
163
+ pathological file) is invisible for as long as it lasts. So the count is
164
+ reported on **every** unit and the clock is still read once per batch: the
165
+ emitter throttles itself on an interval, so a per-unit report costs one
166
+ clock read and produces a line only when one is due, while a per-unit
167
+ budget check would buy no resolution the deadline can use.
168
+
158
169
  Never raises: an unreadable clock is not evidence the budget is spent, and
159
170
  a reporting fault is not a reason to stop analysing.
160
171
  """
161
- if done % self.sample_every and done != total:
162
- return False
163
172
  if self.detail_sink is not None:
164
173
  try:
165
174
  self.detail_sink(done, total, label, unit)
166
175
  except Exception:
167
176
  pass
177
+ if done % self.sample_every and done != total:
178
+ return False
168
179
  return self.should_stop()
169
180
 
170
181
  def record_partial(
171
- self, rule_ids: "list[str]", done: int, total: int, *, unit: str = "files"
182
+ self,
183
+ rule_ids: "list[str]",
184
+ done: int,
185
+ total: int,
186
+ *,
187
+ unit: str = "files",
188
+ stage: str = "",
172
189
  ) -> None:
173
190
  """Record a family the budget cut *in flight* (C3-79).
174
191
 
@@ -176,12 +193,29 @@ class RulePassProgress:
176
193
  calling it not-run discards findings that were measured and are published.
177
194
  It gets its own state, with the count it reached, because *how far* is the
178
195
  thing a reader needs in order to decide whether to re-run with more time.
196
+
197
+ **C3-81.** The count is the *stage's*, not the family's, and until this
198
+ seam said so the entry read as its own contradiction. The field measured
199
+ it: `units_done: 3342, units_total: 3342` in a list meaning *"this family
200
+ did not finish"*, on a family that walks four populations in sequence
201
+ (Java sources, then configuration files, then descriptors, then mappers)
202
+ and was cut at the end of the first. Six `DEAD-001` findings really were
203
+ missing, and nothing in the entry could say where from. So the stage is
204
+ named, and `units_basis` states what the two numbers count — which is the
205
+ difference between a reader concluding *"it finished"* and a reader
206
+ concluding *"three populations after this one never started"*.
179
207
  """
180
208
  self.partially_run.append({
181
209
  "rule_ids": list(rule_ids),
182
210
  "units_done": done,
183
211
  "units_total": total,
184
212
  "unit": unit,
213
+ "stage_stopped_in": stage or unit,
214
+ "units_basis": (
215
+ "units_done/units_total count the stage named in "
216
+ "stage_stopped_in, not the whole family: a family cut at the end "
217
+ "of one stage did not run the stages after it."
218
+ ),
185
219
  })
186
220
 
187
221
  def partial_rule_ids(self) -> "list[str]":
@@ -223,14 +257,21 @@ class FamilyBudget:
223
257
  label: str
224
258
  stopped: bool = False
225
259
 
226
- def tick(self, done: int, total: int, *, unit: str = "files") -> bool:
227
- """True when this family must stop. Idempotent once it has said so."""
260
+ def tick(
261
+ self, done: int, total: int, *, unit: str = "files", stage: str = ""
262
+ ) -> bool:
263
+ """True when this family must stop. Idempotent once it has said so.
264
+
265
+ `done` is what the loop has **finished**, so a stage in flight can never
266
+ report its own population as complete (C3-81); `stage` names the walk the
267
+ count belongs to, because a family may walk several in sequence.
268
+ """
228
269
  if self.stopped:
229
270
  return True
230
271
  if self.progress.tick(done, total, label=self.label, unit=unit):
231
272
  self.stopped = True
232
273
  self.progress.record_partial(
233
- list(self.rule_ids), done, total, unit=unit
274
+ list(self.rule_ids), done, total, unit=unit, stage=stage
234
275
  )
235
276
  return True
236
277
  return False
@@ -612,8 +612,13 @@ def scan_security_configuration(
612
612
 
613
613
  out: "list[SecurityConfigObservation]" = []
614
614
  _java_total = len(java_files)
615
- for _seen, rel in enumerate(java_files, start=1):
616
- if budget is not None and budget.tick(_seen, _java_total, unit="java files"):
615
+ for _seen, rel in enumerate(java_files):
616
+ # C3-81: `_seen` is what this walk has *finished*, so the stage cannot
617
+ # report a complete population while it is still being cut, and the stage
618
+ # is named because three more populations follow this one.
619
+ if budget is not None and budget.tick(
620
+ _seen, _java_total, unit="java files", stage="scanning java sources"
621
+ ):
617
622
  return sorted(
618
623
  _shared_across_environments(out),
619
624
  key=lambda o: (o.rule, o.file, o.line),
@@ -656,9 +661,10 @@ def scan_security_configuration(
656
661
  if config_files is None:
657
662
  config_files = _walked_config
658
663
  _cfg = sorted(set(config_files))
659
- for _seen, rel in enumerate(_cfg, start=1):
664
+ for _seen, rel in enumerate(_cfg):
660
665
  if budget is not None and budget.tick(
661
- _seen, len(_cfg), unit="configuration files"
666
+ _seen, len(_cfg), unit="configuration files",
667
+ stage="scanning configuration files",
662
668
  ):
663
669
  break
664
670
  try:
@@ -673,9 +679,10 @@ def scan_security_configuration(
673
679
  # deployment descriptors, and a per-environment overlay tree is covered
674
680
  # because the walk finds the file wherever the build put it.
675
681
  _desc = sorted(set(descriptors))
676
- for _seen, rel in enumerate(_desc, start=1):
682
+ for _seen, rel in enumerate(_desc):
677
683
  if budget is not None and budget.tick(
678
- _seen, len(_desc), unit="deployment descriptors"
684
+ _seen, len(_desc), unit="deployment descriptors",
685
+ stage="scanning deployment descriptors",
679
686
  ):
680
687
  break
681
688
  try:
@@ -686,9 +693,9 @@ def scan_security_configuration(
686
693
  out.extend(_scan_descriptor(blank_xml_comments(text), rel))
687
694
 
688
695
  _maps = sorted(set(mappers) - set(descriptors))
689
- for _seen, rel in enumerate(_maps, start=1):
696
+ for _seen, rel in enumerate(_maps):
690
697
  if budget is not None and budget.tick(
691
- _seen, len(_maps), unit="mapper files"
698
+ _seen, len(_maps), unit="mapper files", stage="scanning mapper files",
692
699
  ):
693
700
  break
694
701
  try:
@@ -110,10 +110,13 @@ class _SEC001UnsecuredEndpoint:
110
110
  findings: list[SpringFinding] = []
111
111
 
112
112
  _total = len(cir.endpoints)
113
- for _seen, ep in enumerate(cir.endpoints, start=1):
113
+ for _seen, ep in enumerate(cir.endpoints):
114
114
  # C3-79: the population is the endpoints, so that is what the deadline
115
115
  # is observed on. What was measured before the cut is returned.
116
- if budget is not None and budget.tick(_seen, _total, unit="endpoints"):
116
+ # C3-81: `_seen` counts what is finished, and the walk is named.
117
+ if budget is not None and budget.tick(
118
+ _seen, _total, unit="endpoints", stage="walking endpoints"
119
+ ):
117
120
  break
118
121
  if ep.security is not None and ep.security.policy != "none_detected":
119
122
  continue
@@ -635,12 +638,13 @@ class _GATE00XCustomGateAudit:
635
638
  sources = _read_java_sources(Path(root), cir)
636
639
  findings: list[SpringFinding] = []
637
640
  _token_total = len(tokens)
638
- for _token_seen, token in enumerate(tokens, start=1):
641
+ for _token_seen, token in enumerate(tokens):
639
642
  # C3-79: the gate tokens are this family's population. Each one costs
640
643
  # three passes over the admissible sources, so the boundary between two
641
644
  # tokens is where a deadline can be honoured without a half-built pass.
642
645
  if budget is not None and budget.tick(
643
- _token_seen, _token_total, unit="gate annotations"
646
+ _token_seen, _token_total, unit="gate annotations",
647
+ stage="resolving gate annotations",
644
648
  ):
645
649
  break
646
650
  # C3-61, second measurement. The `token not in source` guard removed
@@ -1098,8 +1102,9 @@ def run_security_audit(
1098
1102
  _rules_partial = list(getattr(progress, "partially_run", []) or [])
1099
1103
  if _rules_partial:
1100
1104
  _cut = ", ".join(
1101
- f"{'/'.join(entry['rule_ids'])} after {entry['units_done']} of "
1102
- f"{entry['units_total']} {entry['unit']}"
1105
+ f"{'/'.join(entry['rule_ids'])} while {entry['stage_stopped_in']}, "
1106
+ f"after {entry['units_done']} of {entry['units_total']} "
1107
+ f"{entry['unit']}"
1103
1108
  for entry in _rules_partial
1104
1109
  )
1105
1110
  _sec_limitations.append(
@@ -189,10 +189,12 @@ class _TX001ProxyBypass:
189
189
  _seen_ids: set[str] = set()
190
190
  _declared = tx_index.all_declared
191
191
  _total = len(_declared)
192
- for _seen, boundary in enumerate(_declared, start=1):
192
+ for _seen, boundary in enumerate(_declared):
193
193
  # C3-79: the declared boundaries are this family's population.
194
+ # C3-81: counted as finished, and the walk is named.
194
195
  if budget is not None and budget.tick(
195
- _seen, _total, unit="declared boundaries"
196
+ _seen, _total, unit="declared boundaries",
197
+ stage="walking declared boundaries",
196
198
  ):
197
199
  break
198
200
  if boundary.scope != "method":
@@ -793,9 +795,12 @@ class _TX006SelfInvocation:
793
795
  _seen_ids: set[str] = set()
794
796
 
795
797
  _total = len(body_facts)
796
- for _seen, (caller_fqn, atoms) in enumerate(body_facts.items(), start=1):
798
+ for _seen, (caller_fqn, atoms) in enumerate(body_facts.items()):
797
799
  # C3-79: one method body per unit — the population this walk iterates.
798
- if budget is not None and budget.tick(_seen, _total, unit="method bodies"):
800
+ # C3-81: counted as finished, and the walk is named.
801
+ if budget is not None and budget.tick(
802
+ _seen, _total, unit="method bodies", stage="walking method bodies"
803
+ ):
799
804
  break
800
805
  if not isinstance(atoms, list):
801
806
  continue
@@ -1046,8 +1051,9 @@ def run_tx_audit(
1046
1051
  _rules_partial = list(getattr(progress, "partially_run", []) or [])
1047
1052
  if _rules_partial:
1048
1053
  _cut = ", ".join(
1049
- f"{'/'.join(entry['rule_ids'])} after {entry['units_done']} of "
1050
- f"{entry['units_total']} {entry['unit']}"
1054
+ f"{'/'.join(entry['rule_ids'])} while {entry['stage_stopped_in']}, "
1055
+ f"after {entry['units_done']} of {entry['units_total']} "
1056
+ f"{entry['unit']}"
1051
1057
  for entry in _rules_partial
1052
1058
  )
1053
1059
  _tx_limitations.append(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 4.14.0
3
+ Version: 4.15.0
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
 
43
43
  **Context · Impact · Migration · Architecture · Review — everything from one structural model.**
44
44
 
45
- ![Version](https://img.shields.io/badge/version-4.14.0-blue)
45
+ ![Version](https://img.shields.io/badge/version-4.15.0-blue)
46
46
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
47
47
 
48
48
  > **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
@@ -76,13 +76,31 @@ With the right command class, ASK Engine becomes **constant infrastructure** ins
76
76
  **Large-repo controls.** Pass `--progress stderr` (or `--progress file:run.log`)
77
77
  for line heartbeats in a non-TTY supervised run — with the file being read so
78
78
  far and an ETA for the stage being measured. `ASK_PROGRESS` sets the same thing
79
- in the environment. Long repo-wide/deep runs also write durable records under
80
- `~/.sourcecode/runs/<repo-hash>/` by default, not inside the audited repository;
81
- set `ASK_RUNS_IN_REPO=1` only when repo-local `.ask/runs` records are intentional.
82
- Set `ASK_MAX_ANALYSIS_SECONDS=<seconds>` to fail before starting a repo-wide/deep
83
- command whose configured budget is below its safe floor. Use
84
- `ask audit-report --from-risk risk.json` to package an existing risk run instead
85
- of recomputing it.
79
+ in the environment, and the heartbeat is emitted by the analysis itself, not only
80
+ by a timer thread, so a CPU-bound stretch is never silent. `--jobs N` / `-j`
81
+ (env `ASK_JOBS`) sets the worker processes that parse files — default CPU count
82
+ − 1; parallelism only warms the content-addressed parse cache, so the answer is
83
+ byte-identical at any value. It does **not** parallelise the rule pass, which is
84
+ where a repository-wide audit spends most of its time, and a warm cache leaves it
85
+ nothing to parse — so raising it there buys nothing. Long repo-wide/deep runs also write durable records
86
+ under `~/.sourcecode/runs/<repo-hash>/` by default, not inside the audited
87
+ repository; set `ASK_RUNS_IN_REPO=1` only when repo-local `.ask/runs` records are
88
+ intentional. Use `ask audit-report --from-risk risk.json` to package an existing
89
+ risk run instead of recomputing it.
90
+
91
+ **A budget is honoured, never a veto.** `ASK_MAX_ANALYSIS_SECONDS=<seconds>`
92
+ bounds a repo-wide/deep command. A value below the class floor prints what that
93
+ class typically needs and **runs anyway** — refusing to answer is the opposite of
94
+ this product's doctrine. A run that spends its budget stops on the loop that is
95
+ spending the time, not only between phases: a rule family cut in flight is
96
+ published as `rules_partially_run` with how far it reached, families that never
97
+ started are named in `rules_not_run`, `_partial` names the phases that completed
98
+ and the ones that did not, and the findings measured before the cut are kept.
99
+ The answer marks itself where the numbers are — `summary.partial`,
100
+ `counts_are_floor`, `counts_basis` — and `confidence_level` is capped at `low`,
101
+ because the confidence of the findings that exist says nothing about the family
102
+ that did not run. Measured on BroadleafCommerce (2 766 Java files): a 4.5s budget
103
+ stops at 1 408 files and ends at 5.1s.
86
104
 
87
105
  **What a warm actually covers.** `ask cache warm` runs the compact analysis: it rebuilds the
88
106
  shared structural layers (L1/L2 + the Repository Intelligence Snapshot + the shared Canonical
@@ -108,7 +126,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
108
126
  # pip / pipx
109
127
  pipx install sourcecode # or: pip install sourcecode
110
128
 
111
- ask version # ask 4.14.0 — and, on a build that has aged,
129
+ ask version # ask 4.15.0 — and, on a build that has aged,
112
130
  # how many releases have probably shipped since
113
131
  ```
114
132
 
@@ -204,6 +222,9 @@ ask verify /path/to/repo # then: only new violations block
204
222
  > A repository that starts declaring contracts already violates them somewhere; `ask verify`
205
223
  > is **baseline-relative by default** (`--fail-on new`) so the gate survives contact with
206
224
  > reality instead of being switched off on day one.
225
+ > A pipeline wired before the contracts exist exits 2 (`unverified`, with
226
+ > `unverified_reason` naming which state it is); `--allow-unverified` exits 0 while the
227
+ > verdict stays `unverified` — the declared form of `|| true`, and violations still block.
207
228
  > `ask baseline capture|diff|trend` is a different thing: versioned architectural metrics over
208
229
  > time, for trend reporting rather than blocking.
209
230
 
@@ -324,7 +345,7 @@ from it.
324
345
  | `migrate-recipe` | experimental | the migration report as the OpenRewrite recipe that applies it | only recipes a finding named; the manual remainder published beside them; writes nothing without `--write`, and never a runnable command for an empty recipe list |
325
346
  | `data-exposure` | experimental | which routes can carry the data you labelled, and who reaches them | labels declared in `sourcecode.config.json` — never inferred from a name; `signature` and `call_reach` evidence published apart, field-level flow out of scope (NC-008). With nothing declared, `ask data-exposure /path/to/repo` answers `answered: false` and hands back the file, the key and an example to declare — it never reports zero exposed routes |
326
347
  | `endpoints` | core | every REST endpoint, effective path, security policy, confidence | Spring MVC + JAX-RS (~65 % recall on JAX-RS sub-resource locators). `--compact` answers the exposure census without the rows; `--servlets` lists the servlet-mounted surface as its own population; `--client-usage` says which routes the TS/JS client in this repository actually calls |
327
- | `spring-audit` | core | transactional anomalies + security configuration/exposure + validation gaps | `--ci`, `-f github-comment`; `--table --rule SEC-008 --top-n 20` for human-scale review |
348
+ | `spring-audit` | core | transactional anomalies + security configuration/exposure + validation gaps | `--ci`, `-f github-comment`; `--table --rule SEC-008 --top-n 20` for human-scale review. Every run inventories its own rules — `rules_run`, `rules_not_run`, `rules_partially_run`, `rules_run_count`/`rules_total` — so "all of them" is a fact, not an assumption |
328
349
  | `migrate-check` | core | Boot 2→3 readiness + Java LTS/licensing inventory: located blockers, per-dimension score, effort, detected Java 8/11/17/21/25 evidence and explicit licensing-review signals | `--target-jdk 25` sets the target LTS route; `--blast-radius` orders the re-test plan; `--table --rule MIG-001 --band critical --top-n 20` for blocker triage |
329
350
  | `impact` / `impact-chain` | core | blast radius of a change, to the endpoints it reaches | target the **interface**, not the `Impl` |
330
351
  | `pr-impact` | core | the same, scoped to a PR diff | gating command: `--fail-on`, exit codes |
@@ -499,6 +520,25 @@ range, and errors: **no source code, paths, secrets, or output**. Turn it off ag
499
520
  > default you must remember to disable is the wrong default for third-party code. An explicit
500
521
  > choice you made before is unchanged.
501
522
 
523
+ **A question is not a write — `--no-write` / `ASK_READONLY=1`.** One contract over
524
+ every command: nothing is created inside the analysed repository, and a command
525
+ that cannot answer without writing says so instead of writing. A refusal is never
526
+ silent — each one names the path that was not created, on stderr. The scope is the
527
+ tree you pointed at; the shared caches under `~/.sourcecode` and an `--output` path
528
+ elsewhere on the machine are not refused, because refusing them would make the mode
529
+ unusable without making it safer.
530
+
531
+ Most commands read only, and the few that create anything inside the repository do
532
+ it under `.ask/` and only behind an explicit flag (`baseline`, `migrate-check
533
+ --history`, `verify --update-baseline`, `verify-edit --install-hook`, and `.ask/runs`
534
+ when `ASK_RUNS_IN_REPO=1`). `ask --help` prints that list in full, generated from the
535
+ one authority the battery checks — a write seam added without a row there fails the
536
+ suite.
537
+
538
+ ```bash
539
+ ask spring-audit /path/to/client-repo --no-write -o audit.json
540
+ ```
541
+
502
542
  **Custom security annotations.** Teach `endpoints`, `spring-audit`, and `explain` about
503
543
  project-specific authorization annotations via an optional `sourcecode.config.json` at the
504
544
  repo root (otherwise they report `policy: "none_detected"`):
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=qNowObAMVSm2IHYY_tkZt5xIhyBvBeJ4bDCcnIyty9Q,309
1
+ sourcecode/__init__.py,sha256=Ub5mdlZZyGdFTJZvvfzvTcZgbEJ8EyeNf62l-V-LOP0,309
2
2
  sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
3
3
  sourcecode/archetype.py,sha256=CZvRLpkHot_D8D3JFQVorr-EHDJyh0BbS7RnSTqBigM,40499
4
4
  sourcecode/architectural_baseline.py,sha256=4GiMVBLJVHvRKSWQGf2ZVurI1-R0qk5pmaevgvGSgVA,26325
@@ -18,7 +18,7 @@ sourcecode/chain_rules.py,sha256=Bi6UHfgd-GxWswmnHRcPz5jdbAuqka3Zkz_P-MTvqhw,127
18
18
  sourcecode/change_plan.py,sha256=aX1mp2XnlDN-8R2BcTiu8HlkDwQ_4fN1ZLckj0VMXHM,9945
19
19
  sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
20
20
  sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
21
- sourcecode/cli.py,sha256=eiUg6TKullRAlAdRnHTaCXfq3t_oLFRCXp9BTqOTvnc,595227
21
+ sourcecode/cli.py,sha256=lUGvP4xPxLvTlKqMyqJhF-Lomls1L_qYijD6BpxcaQ4,597537
22
22
  sourcecode/client_calls.py,sha256=daRTgbXNUOfkzGXJpVb6A737R_Thhka8vw1_YgxCaLg,13548
23
23
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
24
24
  sourcecode/compare.py,sha256=2GdDy0qkDSgmkkpgvoCHitoLj3mt30EeFgJ8PGEXJng,14712
@@ -81,7 +81,7 @@ sourcecode/parse_cache.py,sha256=fhXnoaOgVWwp_8q0ngJ76UZ09qJ6uxDd0jaX8jdezvo,208
81
81
  sourcecode/path_admission.py,sha256=OGNSoluhVh8YYOBZBnACUrkmH4Tp_yxjGLifkcsxQpo,5838
82
82
  sourcecode/path_filters.py,sha256=LmTYq735orssKTIuxTX1mE1FHAXF7dVEJEse35xE1RA,12371
83
83
  sourcecode/perf.py,sha256=vZDEj1wS905peXmREdOniZrWCikElOyPTw-E32zVZAQ,25847
84
- sourcecode/phased_run.py,sha256=-rO31RnNc26oWTxwloURW6PydkMb-UW6jFO-wuUVN6w,10970
84
+ sourcecode/phased_run.py,sha256=yrTV7qtGDwgOPw0wYEioLMCKwReAZPtml567n4SYED4,12750
85
85
  sourcecode/pipe_contract.py,sha256=PML0Er5d8uDyec4OrdUXuyqHbGPUYndjeaKUjkFz2u4,8369
86
86
  sourcecode/posture.py,sha256=cZ3By8LR-1bHfzyVpCDo2HHSAsCvePOmUc37E9GXYDw,76206
87
87
  sourcecode/pr_comment_renderer.py,sha256=239PmJdf95av_ZW236C7_tvq_ahECeuF9AZxrheJ4OQ,15573
@@ -95,7 +95,7 @@ sourcecode/readonly.py,sha256=dqGiNVvpfi4Ww8VzwUs6E3SdbqcHL1LFHItIK7IjrM0,5830
95
95
  sourcecode/reconciliation.py,sha256=GU-1PTcVr8zcbtC7BASfpHcZndP9AdboXPNQiBc0fzo,34251
96
96
  sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
97
97
  sourcecode/reference_facts.py,sha256=Ns495c6eTmq2SrqPkcUrJUJru4_wj_yRWm4YDeJwves,13440
98
- sourcecode/release_info.py,sha256=dcCjnLmWzkI0LubNKqisdUxm6vwv9V3KP3zR7xloLZQ,5378
98
+ sourcecode/release_info.py,sha256=4Jfym0xPvJpauhXDWLY2An8yIzae5OlAYZeUr24URsc,5378
99
99
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
100
100
  sourcecode/remedies.py,sha256=9eAuP9ndw78YvuUtOi2VvoeNnVCMh1q0uxfnPIkvx7c,7838
101
101
  sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
@@ -104,14 +104,14 @@ sourcecode/repository_ir.py,sha256=t1JNnk5g3ksfjkkrxaXZUKMyszw9zWEb6HQd-vlE3SE,3
104
104
  sourcecode/ris.py,sha256=SjjDNMcc9Zr9us9G8ikBVkoPXKK3BpM-dBodSGMh_g4,24783
105
105
  sourcecode/risk.py,sha256=jfhCIhxiAqHE0CUO3FZK_-m1bwu-o-yEZfQZrhKnvVs,58449
106
106
  sourcecode/rule_catalog.py,sha256=pTkgkQZ1u7atB3fkQJ9BPWo8lNiamvEuRN7MtRgle0c,5263
107
- sourcecode/rule_pass.py,sha256=qDH-FAc1uJvAN9Ud_7-utHR1oMmR1bpHN0wYzYzWcrA,11121
107
+ sourcecode/rule_pass.py,sha256=lHk2Y9hp73th2Rh6yeJdOBQyVz9NQS3rTZKOiW7Ky1Q,13374
108
108
  sourcecode/runs.py,sha256=zIxEPz9DBVVCrX958bZR6bODyfH56A6srAyc8LFMs8I,5303
109
109
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
110
110
  sourcecode/sarif.py,sha256=3hQEegUxIZbojFdY59oB-yKsK-rnafHdTNmYsEP2--A,25984
111
111
  sourcecode/scanner.py,sha256=z3CV0rcGunu0Y8mpNgp07wI7nxT0pxw1BkXRRtI0Rpo,9609
112
112
  sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
113
113
  sourcecode/security_config.py,sha256=KblMEoRiEjrIE68YsPaUAFebxFp8UM7MS7lAk5CGD8U,3531
114
- sourcecode/security_config_scan.py,sha256=aH71o6pVju1NJRg_fvz-5vwRzU1nDRLOxYo97xYNWd8,29311
114
+ sourcecode/security_config_scan.py,sha256=dO_z9pquH3pypaZU5gL3eHuWx4rpLEUp5jQVUDZ6u6Y,29693
115
115
  sourcecode/security_posture.py,sha256=JH3v8HKBT4oBKaDCrxNOyrlYSleLlxQw_UgRJcvjX4c,51677
116
116
  sourcecode/semantic_analyzer.py,sha256=bpgdC6m0_ftVtRf3rSdwhbhWjnZnGxRXaZVcfe4BbcQ,95414
117
117
  sourcecode/semantic_impact_engine.py,sha256=t09IirGC3JjQDy33JZd1_WKzQVKXkoNl3-XEUr5kjis,20563
@@ -126,9 +126,9 @@ sourcecode/spring_impact.py,sha256=GW77k7KMn13QmzP5520BbVcXSCYlTFeD1bFibhbmzJM,8
126
126
  sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
127
127
  sourcecode/spring_profiles.py,sha256=-kwrCK0O-MRjrCp6SA1d31t--o4Tg87iqPVrTwxoIUs,19605
128
128
  sourcecode/spring_properties.py,sha256=kPTk5qAJxdbHc1hclhHAP0O-hCIzBn8PTh3lzl-I4a4,8400
129
- sourcecode/spring_security_audit.py,sha256=5hIZUk5-0rFnLEc3nCB3gDCLLHoxxZoRl3PIAhyBXEs,55299
129
+ sourcecode/spring_security_audit.py,sha256=0cS73BXaoFByJk4cMl3YqItPQ2xRUMu4XChURbKLCQo,55519
130
130
  sourcecode/spring_semantic.py,sha256=UrmLg_4gmBVmMqvSnUMgSReVEERVGv3X7LYDBEs6k_o,16598
131
- sourcecode/spring_tx_analyzer.py,sha256=ivKeXFTYAxa5sDoUrjGVf6MeGqKGPHjAOt__w3g0kpw,49134
131
+ sourcecode/spring_tx_analyzer.py,sha256=4pUTaqqlsKRTBUr6tIwYP9oLamHz_IVKmLq3kFcH7Ww,49412
132
132
  sourcecode/summarizer.py,sha256=0aD4x3vgPngqBCEBKGuES1J2Vk5f7mqCm_ZWErwm3js,27025
133
133
  sourcecode/target_admission.py,sha256=wFZ4pzlxhiF6Q6s2lEAZEzcCfj1Y6xNvujjt8MdO0Qo,7154
134
134
  sourcecode/test_gap_ranking.py,sha256=hl-tTyQUXZGJycG1b6npbLeE_DaVHaOsvPwG9qqC5y8,15711
@@ -203,8 +203,8 @@ sourcecode/telemetry/consent.py,sha256=pQdl-QeLl6Gcibn0eWHSKZrm-HYSsjpVqOnjrgFp8
203
203
  sourcecode/telemetry/events.py,sha256=4_yeO58U-Cwc1Qb27VB0_EjhmroY0k91n3_VGxeALB8,2776
204
204
  sourcecode/telemetry/filters.py,sha256=RzxauTz8HliO4BllQnXEXc7zTeqdCZi5MgqGEDuW7OQ,6570
205
205
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
206
- sourcecode-4.14.0.dist-info/METADATA,sha256=Wjh_U4ayRHzad0wUYf4R8zI6jsW12wn-diDUcqw5jTM,39250
207
- sourcecode-4.14.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
208
- sourcecode-4.14.0.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
209
- sourcecode-4.14.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
210
- sourcecode-4.14.0.dist-info/RECORD,,
206
+ sourcecode-4.15.0.dist-info/METADATA,sha256=Pej25eqhwMzbD468yWla-_e00Xg2fwKH5gRJmgBoMW8,42089
207
+ sourcecode-4.15.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
208
+ sourcecode-4.15.0.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
209
+ sourcecode-4.15.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
210
+ sourcecode-4.15.0.dist-info/RECORD,,