sourcecode 5.9.2__py3-none-any.whl → 5.9.4__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__ = "5.9.2"
7
+ __version__ = "5.9.4"
@@ -1,5 +1,5 @@
1
1
  """Generated at build time by hatch_build.py. Do not edit (AUD-592-D04)."""
2
2
 
3
- BUILD_COMMIT = 'cbecede0ebc0e7fe99d4c064d5fc052b1b37799e'
3
+ BUILD_COMMIT = 'ec6b285e69ecb8d5dc90f82b106af14c221d1856'
4
4
  CLOSURE_PROVENANCE = {'Window': {'commit': '', 'status': 'uncited'}, '`AUD-588-F05`': {'commit': '470e4a4', 'status': 'present'}, '`AUD-590-B01` / `B-01`, `F-01`': {'commit': '', 'status': 'uncited'}, '`AUD-590-B02` / `B-02`, `F-02`': {'commit': '', 'status': 'uncited'}, '`AUD-590-B03` / `B-03`, `F-03`': {'commit': '', 'status': 'uncited'}, '`AUD-590-B04` / `B-04`, `F-04`': {'commit': '', 'status': 'uncited'}, '`AUD-590-R01` / `R-01`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A01` / `A-1`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A02` / `A-2`, `A-4`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A03` / `A-3`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A05` / `A-5`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A06` / `A-6`, `A-7`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A08` / `A-8`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A09` / `A-9`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A10` / `A-10`': {'commit': '', 'status': 'uncited'}, '`AUD-592-A02` / `A-1` second half, `§12.5`': {'commit': '63f3d61', 'status': 'present'}, '`AUD-592-B01` / `B-8`': {'commit': '8329614', 'status': 'present'}, '`AUD-592-D01` / `D-1`': {'commit': '0f8e03f', 'status': 'present'}, '`AUD-592-D02` / `D-2`': {'commit': '1d3db4d', 'status': 'present'}, '`AUD-592-D03` / `D-3`': {'commit': 'f28fe40', 'status': 'present'}, '`AUD-592-R01` / `A-1`': {'commit': '216025f', 'status': 'present'}, '`AUD-592-R02` / `N-4`, reopens `AUD-513-N04`': {'commit': 'ec3050d', 'status': 'present'}, '`AUD-593-N01` / `N-01`': {'commit': 'dd6a706', 'status': 'present'}, '`AUD-593-N02` / `N-02`': {'commit': 'f539787', 'status': 'present'}, '`AUD-593-N03` / `N-03`': {'commit': '1dd0b1c', 'status': 'present'}, '`AUD-593-N05` / `N-05`': {'commit': 'aa89388', 'status': 'present'}, '`AUD-593-N06` / `N-06`': {'commit': 'aa584a6', 'status': 'present'}, '`AUD-594-N04` / `D-5`, disclosure half of `B-3`': {'commit': 'a0060b7', 'status': 'present'}, '`AUD-594-X01` / `A-1`, residual of `AUD-593-N03`': {'commit': 'dd222b9', 'status': 'present'}, '`AUD-594-X02` / `A-1` second half': {'commit': '1193937', 'status': 'present'}, '`AUD-594-X03` / `X-03`, residual of `AUD-593-N01`': {'commit': 'e89e8f0', 'status': 'present'}, '`AUD-594-X04`': {'commit': '05b8cc9', 'status': 'present'}, '`AUD-595-A02` / `N-5`, advisory half of `AUD-588-B11`': {'commit': '3e15227', 'status': 'present'}, '`AUD-595-A03` / `B-6` exit-code half': {'commit': 'ab7285f', 'status': 'present'}, '`AUD-595-A05`': {'commit': 'e0b3d10', 'status': 'present'}, '`AUD-595-A07`, narrow half of `AUD-592-A02`': {'commit': 'cc15177', 'status': 'present'}, '`AUD-595-B01` / §B': {'commit': 'b77a1ad', 'status': 'present'}, '`AUD-595-Q01`': {'commit': '91a6566', 'status': 'present'}, '`AUD-596-A01` / `ASK-DET-001`': {'commit': 'fa508b4', 'status': 'present'}, '`AUD-596-A02` / `ASK-SELF-001`': {'commit': '4dfadc8', 'status': 'present'}, '`AUD-596-A03` / `ASK-C1-001`': {'commit': 'bffa3a6', 'status': 'present'}, '`AUD-596-A04` / `ASK-UX-003` + `ASK-UX-004`': {'commit': 'f0aac3c', 'status': 'present'}, '`AUD-596-B01` / `B-01`, harder witness for `BUG-6`': {'commit': '567d09d', 'status': 'present'}, '`AUD-596-B02` / `B-02`, class residual of `R2`': {'commit': '', 'status': 'uncited'}, '`AUD-596-B03` / `B-03`': {'commit': 'a8cb814', 'status': 'present'}, '`AUD-596-B06` / `B-06`': {'commit': '8107927', 'status': 'present'}, '`AUD-596-B07` / `B-07`, class residual of `AUD-592-R02`': {'commit': 'e060485', 'status': 'present'}, '`AUD-596-B09` / `B-09`': {'commit': '5b8146c', 'status': 'present'}, '`AUD-596-B10` / `B-10`': {'commit': '5cb994d', 'status': 'present'}, '`AUD-596-B16` / `B-16`': {'commit': '1507f69', 'status': 'present'}, '`AUD-596-D01` / `B-13` + `B-14`': {'commit': '0b1bdf0', 'status': 'present'}, '`AUD-596-D02` / `B-08`, residual of `AUD-594-N04`': {'commit': 'e7f73c3', 'status': 'present'}, '`AUD-596-D03` / `ASK-AGT-001`': {'commit': '8c46967', 'status': 'present'}, '`AUD-596-D04` / `B-18`': {'commit': '040f027', 'status': 'present'}, '`AUD-596-D05` / `B-19`': {'commit': 'fd2bf68', 'status': 'present'}, '`AUD-596-D06` / `B-20`': {'commit': '784cd83', 'status': 'present'}, '`AUD-596-D07` / `B-17`, half refuted': {'commit': 'c3ff822', 'status': 'present'}, '`AUD-596-D08` / `B-22`, class residual of `AUD-591-A06`': {'commit': 'b396c6a', 'status': 'present'}, '`AUD-596-D09` / `B-23`, surviving half of `B26`': {'commit': 'fd3d63f', 'status': 'present'}, '`AUD-596-D10` / `B-29`': {'commit': '', 'status': 'uncited'}, '`AUD-596-D11` / `ASK-UX-005`': {'commit': 'f4b4f77', 'status': 'present'}, '`AUD-596-D12` — six hints that send the reader back to the error': {'commit': '498df44', 'status': 'present'}, '`AUD-596-D13` / `B-28`': {'commit': '1014c72', 'status': 'present'}, '`AUD-596-D14` / `B-15`, narrow half of `B20`': {'commit': '10f2827', 'status': 'present'}, '`AUD-596-R01` / `B-05`': {'commit': '', 'status': 'uncited'}, '`AUD-596-X01` / `ASK-GATE-001`, gating half of `B24`': {'commit': 'e59c655', 'status': 'present'}, '`AUD-596-X02` / `ASK-UX-001` + `ASK-UX-002` + `B-21`': {'commit': 'f6ebf50', 'status': 'present'}, '`AUD-597-A01` / `ASK-UX-001` + `N-01`': {'commit': 'f8cf061', 'status': 'present'}, '`AUD-597-A02` / `ASK-UX-008` + `ASK-DOC-002`': {'commit': '9d47a4c', 'status': 'present'}, '`AUD-597-A03` / `ASK-DET-001`': {'commit': '4c7622a', 'status': 'present'}, '`AUD-597-A04` / `ASK-LEDGER-001`': {'commit': '46fb870', 'status': 'present'}, '`AUD-597-D01` / `B-04`': {'commit': '1094e6a', 'status': 'present'}, '`AUD-597-D02` / `ASK-DOC-001`': {'commit': 'ecb1bb7', 'status': 'present'}, '`AUD-597-D03` / `N-06`': {'commit': '9784333', 'status': 'present'}, '`AUD-597-D04` / `N-05`': {'commit': 'e51c087', 'status': 'present'}, '`AUD-597-D05` / `N-04`, fourth appearance of `C4-27`': {'commit': '9cccc15', 'status': 'present'}, '`AUD-597-D06` / `ASK-CLI-001`': {'commit': '178aa6d', 'status': 'present'}, '`AUD-597-D07` / `N-03`': {'commit': '9245e35', 'status': 'present'}, '`AUD-597-R01` / `ASK-PERF-001`': {'commit': '6910054', 'status': 'present'}, '`AUD-597-R02` / `ASK-ENC-001`': {'commit': 'ce6dac1', 'status': 'present'}, '`AUD-597-R03` / `ASK-UX-002`': {'commit': 'c57e50b', 'status': 'present'}, '`AUD-597-X01` / `ASK-BUILD-001` + `ASK-META-001` / `B-12`': {'commit': '354338b', 'status': 'present'}, '`AUD-598-B01` / `ASK-META-001`': {'commit': 'b2f7c65', 'status': 'present'}, '`AUD-598-B02` / `ASK-UX-002`': {'commit': '56a4bf7', 'status': 'present'}, '`AUD-598-B03` / `B-11`': {'commit': 'c63cb24', 'status': 'present'}, '`AUD-598-B04` / `N-02`': {'commit': '187937f', 'status': 'present'}, '`AUD-598-R01` / `ASK-PERF-002`': {'commit': '', 'status': 'uncited'}, '`AUD-598-X01` / `N-01` + `B-13` + `B-14` + `B-29` + `N-06`': {'commit': '6f2f5a4', 'status': 'present'}, '`AUD-600-B01` / `P-01`': {'commit': '366ffed', 'status': 'present'}, '`AUD-600-B02` / `P-02` + `ASK-PACK-002`': {'commit': '2a814c3', 'status': 'present'}, '`AUD-600-B03` / `ASK-PACK-001`': {'commit': '452f2a0', 'status': 'present'}, '`AUD-600-D01`': {'commit': '1224e1b', 'status': 'present'}, '`AUD-600-D02`': {'commit': 'eab8172', 'status': 'present'}, '`AUD-600-R01` / `R-01`': {'commit': '88add64', 'status': 'present'}, '`AUD-601-B01` / `N-02`, 5th round': {'commit': '', 'status': 'uncited'}, '`AUD-601-B02` / `ASK-PACK-004`': {'commit': '', 'status': 'uncited'}, '`AUD-601-D01` / `N-01`, `ASK-UX-001`, 5th round': {'commit': '', 'status': 'uncited'}, '`AUD-601-D02` / `B-13`, `B-14`, 5th round': {'commit': '', 'status': 'uncited'}, '`AUD-601-M01` / `B-26`, `B-27` retracted': {'commit': '', 'status': 'uncited'}, '`AUD-601-R01` / `R-02`': {'commit': '', 'status': 'uncited'}, '`B-3`': {'commit': '773268d', 'status': 'present'}, '`B-6`': {'commit': '17c6e06', 'status': 'present'}, '`B-8`': {'commit': '8329614', 'status': 'present'}, '`BUG-3` / `443b345`': {'commit': '', 'status': 'uncited'}, '`BUG-4a` / `AUD-588-B11` residual': {'commit': '24b4185', 'status': 'present'}, '`BUG-4b` / `AUD-588-B11` residual': {'commit': '0376350', 'status': 'present'}, '`E-39`, class residual of `AUD-593-N06`': {'commit': '25fc11f', 'status': 'present'}, '`N-5`': {'commit': 'c68083f', 'status': 'present'}, '`N-7`': {'commit': '082e9de', 'status': 'present'}, '`R-1`': {'commit': '424fb1c', 'status': 'present'}, '`R-2`': {'commit': '5158b92', 'status': 'present'}, '`R-3`': {'commit': '88aec04', 'status': 'present'}, '`R-4`': {'commit': '04fd04f', 'status': 'present'}}
5
5
  CLOSURE_COVERAGE = {'closed_rows': 113, 'present': 88, 'absent': 0, 'unresolved': 0, 'uncited': 25, 'unknown': 0}
@@ -46,15 +46,30 @@ CLI commands — impact, endpoints, spring-audit, explain, … each a pro
46
46
  The key idea: the extraction is **content-addressed**. Commands reuse the parse cache and,
47
47
  where their analysed scope matches, the shared Canonical IR; `ask cache model` names what a
48
48
  warm buys for each command rather than implying that every projection costs the same. In
49
- 5.9.2, `validation` enters through that shared CIR and `data-exposure` reuses one semantic
49
+ 5.9.4, `validation` enters through that shared CIR and `data-exposure` reuses one semantic
50
50
  model across all declared label seeds. (The extraction and consumption contract is fixed in
51
51
  the architecture ADRs 0001–0004 under `docs/architecture/`.)
52
52
 
53
53
  The current external pack audit intake is tracked in
54
54
  [`AUDIT-2026-08-26-EXTERNAL-5.8.28-PACK.md`](AUDIT-2026-08-26-EXTERNAL-5.8.28-PACK.md);
55
- the two pack fixes are included in release 5.9.2, with release-artifact verification
55
+ the two pack fixes are included in release 5.9.4, with release-artifact verification
56
56
  remaining for detached execution and pack evidence.
57
57
 
58
+ ### Reliability contracts in 5.9.4
59
+
60
+ Invalid analysis inputs fail before expensive work and use the structured
61
+ `INVALID_INPUT` envelope: a missing `--consumer` repository, a malformed CVE
62
+ feed, or an unresolved `pack gate --since` ref is never treated as an empty or
63
+ `UNVERIFIED` result. `posture --diff` reports unknown profiles when the project
64
+ declares profiles. `baseline capture` does not replace a snapshot already stored
65
+ for the same commit; use `--auto` when a pipeline should skip it.
66
+
67
+ With `endpoints --compact`, `endpoints: []` means the rows were deliberately
68
+ omitted. Read `endpoints_note`, `endpoints_omitted` and `total`; an empty list is
69
+ not evidence that the API has no routes. `impact` and `impact-chain` return exit
70
+ code 1 when the target is unresolved, including when the selected repository
71
+ contains no Java sources.
72
+
58
73
  ---
59
74
 
60
75
  ## The semantic engine
@@ -221,7 +236,7 @@ pipx install sourcecode # isolated install, no venv needed
221
236
 
222
237
  # Verify
223
238
  ask version
224
- # ask 5.9.2
239
+ # ask 5.9.4
225
240
  ```
226
241
 
227
242
  Requires Python 3.9+.
@@ -1180,6 +1195,8 @@ the stored architecture or findings series (`--kind findings`) and publishes its
1180
1195
  boundaries, latest commit, comparability and explicit limits as `baseline-attestation-v1`.
1181
1196
  It is evidence continuity, not a legal or regulatory compliance verdict, and it returns
1182
1197
  `status: no_history` instead of manufacturing a clean zero when no capture exists.
1198
+ Unlike `audit-report --sign-key`, `baseline attest` is currently unsigned: its
1199
+ `content_id` supports deterministic comparison, not third-party tamper verification.
1183
1200
 
1184
1201
  For a multi-repository buyer, `ask auth workspace <repo>...` previews the aggregate Java
1185
1202
  population that a workspace-sized entitlement would see. The preview is advisory only: it
@@ -1187,6 +1204,10 @@ does not change the current per-repository gate or charge the workspace.
1187
1204
  For a named-buyer pilot, add `--enforce`: crossing the aggregate threshold then requires
1188
1205
  the existing Pro entitlement and exits with the standard license-required contract; below
1189
1206
  the threshold it passes. This flag is opt-in and does not change per-repository gates.
1207
+ Repository arguments are normalized before counting. If a parent and descendant are both
1208
+ supplied, the parent is counted once and the overlap is listed in `overlapping_paths`.
1209
+ Missing or non-directory paths fail with the standard structured `INVALID_INPUT` / `missing`
1210
+ contract; they are never treated as empty repositories.
1190
1211
 
1191
1212
  ---
1192
1213
 
@@ -445,11 +445,15 @@ def _announce_ask_dir(ask_dir: Path) -> None:
445
445
 
446
446
 
447
447
  def write_baseline(baseline: dict, out_dir: Path) -> Path:
448
- """Write `baseline` as deterministic JSON under `out_dir`; return the file path."""
448
+ """Write a new baseline without replacing an existing commit snapshot."""
449
449
  _readonly.guard(out_dir, what="baseline history directory")
450
450
  out_dir.mkdir(parents=True, exist_ok=True)
451
451
  protect_ask_dir(out_dir)
452
452
  path = out_dir / _baseline_filename(baseline)
453
+ if path.exists():
454
+ raise FileExistsError(
455
+ f"Baseline already exists for commit {baseline.get('commit') or '-'}: {path}"
456
+ )
453
457
  path.write_text(
454
458
  json.dumps(baseline, sort_keys=True, indent=2) + "\n", encoding="utf-8"
455
459
  )
sourcecode/cli.py CHANGED
@@ -4552,6 +4552,25 @@ def pack_gate_cmd(
4552
4552
  )
4553
4553
  raise typer.Exit(code=2)
4554
4554
  comparison_ref = since
4555
+ if since is not None:
4556
+ from sourcecode.git_checkout import RefError, repo_root, resolve_ref
4557
+
4558
+ try:
4559
+ resolve_ref(repo_root(root), since)
4560
+ except RefError as exc:
4561
+ # Test doubles and callers that provide a non-Git analysis root are
4562
+ # still handled by the existing pack component contracts. Only a
4563
+ # ref failure in an actual repository is an invalid comparison.
4564
+ if exc.code == "not_a_git_repository":
4565
+ pass
4566
+ else:
4567
+ _emit_error_json(
4568
+ INVALID_INPUT_CODE,
4569
+ str(exc),
4570
+ ref=exc.ref or since,
4571
+ hint="Use a commit, tag, or local branch that exists in the repository.",
4572
+ )
4573
+ raise typer.Exit(code=2)
4555
4574
  if pr is not None:
4556
4575
  try:
4557
4576
  from sourcecode.github_diff import pull_request_base_ref
@@ -8578,6 +8597,9 @@ def impact_cmd(
8578
8597
  not_found — no symbol and no substring. `risk_score` is null, never 0:
8579
8598
  nothing was measured, which is not measuring zero.
8580
8599
 
8600
+ An unresolved target exits with code 1, including when the selected repository
8601
+ contains no Java sources; the structured answer still names `not_found`.
8602
+
8581
8603
  \b
8582
8604
  NOTE: Free on repos up to the size limit; Pro unlocks enterprise-scale
8583
8605
  monoliths. Run 'ask license' for details.
@@ -8652,7 +8674,7 @@ def impact_cmd(
8652
8674
  format,
8653
8675
  )
8654
8676
  _emit_command_output(_nf_output, output_path, copy)
8655
- return
8677
+ _exit_on_unresolved_target("not_found")
8656
8678
 
8657
8679
  _prog = Progress()
8658
8680
  _prog.start(f"building CIR ({len(file_list)} files) for impact analysis")
@@ -8902,7 +8924,8 @@ def endpoints_cmd(
8902
8924
  False, "--compact",
8903
8925
  help="Answer how exposed the repository is without listing every endpoint: "
8904
8926
  "counts by method, by security policy and by controller. Every count is "
8905
- "measured over the full population.",
8927
+ "measured over the full population; an empty `endpoints` list means the "
8928
+ "rows were intentionally omitted, not that no endpoints exist.",
8906
8929
  ),
8907
8930
  no_cache: bool = typer.Option(
8908
8931
  False, "--no-cache",
@@ -8980,6 +9003,13 @@ def endpoints_cmd(
8980
9003
  _enforce_format("endpoints", format)
8981
9004
 
8982
9005
  target = _admit_path(path)
9006
+ if consumer:
9007
+ # A missing consumer must fail before the server scan. Treating it as an
9008
+ # empty consumer silently turns a requested join into an unfiltered one.
9009
+ consumer = [
9010
+ _admit_path(item, usage="endpoints --consumer")
9011
+ for item in consumer
9012
+ ]
8983
9013
 
8984
9014
  # AUD-591-A10: a prefix that is not a route selects nothing and reports it
8985
9015
  # as no exposure. Rejected before the analysis, so the shell rewrite is
@@ -9116,6 +9146,12 @@ def endpoints_cmd(
9116
9146
 
9117
9147
  _listed = len(data.get("endpoints", []))
9118
9148
  data["endpoints"] = []
9149
+ data["endpoints_note"] = (
9150
+ f"The endpoints list is intentionally omitted by --compact; "
9151
+ f"{_listed} endpoint(s) were measured. An empty `endpoints` list "
9152
+ "does not mean that the repository has no endpoints. Re-run without "
9153
+ "--compact or narrow the request to retrieve rows."
9154
+ )
9119
9155
  data["endpoints_omitted"] = {
9120
9156
  "total": _listed,
9121
9157
  "shown": 0,
@@ -10599,7 +10635,9 @@ def baseline_capture_cmd(
10599
10635
  ref: Optional[str] = typer.Option(
10600
10636
  None, "--ref",
10601
10637
  help="Human label for this baseline (e.g. a version or milestone). "
10602
- "The commit is recorded automatically when the path is a git repo.",
10638
+ "The commit is recorded automatically when the path is a git repo. "
10639
+ "A capture never overwrites an existing snapshot for the same commit; "
10640
+ "use --auto to skip an already captured commit.",
10603
10641
  ),
10604
10642
  kind: str = typer.Option(
10605
10643
  "architecture", "--kind",
@@ -10750,7 +10788,16 @@ def baseline_capture_cmd(
10750
10788
  baseline = build_baseline(
10751
10789
  _cir, ref=ref, commit=git_commit(target_dir), record_env=record_env,
10752
10790
  )
10753
- written = write_baseline(baseline, dest)
10791
+ try:
10792
+ written = write_baseline(baseline, dest)
10793
+ except FileExistsError as exc:
10794
+ _prog.finish()
10795
+ _emit_error_json(
10796
+ INVALID_INPUT_CODE,
10797
+ str(exc),
10798
+ hint="Use `--auto` to skip an already captured commit, or capture a new commit.",
10799
+ )
10800
+ raise typer.Exit(code=1)
10754
10801
  _prog.finish()
10755
10802
 
10756
10803
  t = baseline["totals"]
@@ -14519,7 +14566,9 @@ def posture_cmd(
14519
14566
  Two axes, and they are different questions. `--diff` holds the tree fixed
14520
14567
  and moves the profile set. `--diff-ref` (and `--since`) hold the profile set
14521
14568
  fixed and move the tree: *does this change what a client can reach without
14522
- authenticating?* That one takes `--fail-on` and is the per-PR gate.
14569
+ authenticating?* That one takes `--fail-on` and is the per-PR gate. When the
14570
+ repository declares profiles, `--diff` rejects a profile name outside that
14571
+ population instead of comparing a configuration that does not exist.
14523
14572
 
14524
14573
  \b
14525
14574
  Examples:
@@ -14743,10 +14792,28 @@ def posture_cmd(
14743
14792
  )
14744
14793
  raise typer.Exit(code=1)
14745
14794
  left_raw, right_raw = diff.split(":", 1)
14795
+ left_profiles = _parse_set(left_raw, flag="--diff (left side)")
14796
+ right_profiles = _parse_set(right_raw, flag="--diff (right side)")
14797
+ from sourcecode.spring_profiles import profile_population
14798
+
14799
+ declared_profiles = set(profile_population(path).names)
14800
+ missing_profiles = sorted(
14801
+ (left_profiles | right_profiles) - declared_profiles
14802
+ )
14803
+ if missing_profiles and declared_profiles:
14804
+ _emit_error_json(
14805
+ INVALID_INPUT_CODE,
14806
+ "--diff names profile(s) not declared by the repository: "
14807
+ + ", ".join(missing_profiles),
14808
+ profiles=missing_profiles,
14809
+ hint="Use profiles reported by `ask posture <repo>`; use a single "
14810
+ "posture run to inspect an undeclared profile explicitly.",
14811
+ )
14812
+ raise typer.Exit(code=1)
14746
14813
  data = diff_posture(
14747
14814
  path,
14748
- _parse_set(left_raw, flag="--diff (left side)"),
14749
- _parse_set(right_raw, flag="--diff (right side)"),
14815
+ left_profiles,
14816
+ right_profiles,
14750
14817
  overrides,
14751
14818
  )
14752
14819
  else:
@@ -15571,7 +15638,7 @@ def impact_chain_cmd(
15571
15638
  output = _serialize_dict(data, format)
15572
15639
  _emit_command_output(output, output_path, copy,
15573
15640
  success_msg=f"Impact chain written to {output_path}")
15574
- return
15641
+ _exit_on_unresolved_target("not_found")
15575
15642
 
15576
15643
  _prog = Progress()
15577
15644
  _prog.start(f"analyzing impact ({len(file_list)} files)")
@@ -17919,7 +17986,8 @@ def auth_workspace_cmd(
17919
17986
  "--enforce",
17920
17987
  help=(
17921
17988
  "Pilot gate: require the existing Pro entitlement when the aggregate "
17922
- "workspace threshold is exceeded. Advisory by default."
17989
+ "workspace threshold is exceeded. Advisory by default. Overlapping "
17990
+ "paths are counted once; missing paths are rejected."
17923
17991
  ),
17924
17992
  ),
17925
17993
  ) -> None:
@@ -17927,12 +17995,17 @@ def auth_workspace_cmd(
17927
17995
 
17928
17996
  Advisory mode is the default. Use ``--enforce`` only for the named-buyer
17929
17997
  pilot: it reuses the existing Pro gate when the aggregate threshold is
17930
- crossed and performs no billing.
17998
+ crossed and performs no billing. Repository paths are normalized, parent /
17999
+ descendant overlaps are counted once, and missing paths are rejected.
17931
18000
  """
17932
18001
  _enforce_format("auth-workspace", format)
17933
18002
  from sourcecode.license import require_pro, workspace_entitlement
17934
18003
 
17935
- data = workspace_entitlement([str(repo) for repo in repos], enforce=enforce)
18004
+ validated_repos = [
18005
+ _admit_path(repo, usage="auth workspace")
18006
+ for repo in repos
18007
+ ]
18008
+ data = workspace_entitlement([str(repo) for repo in validated_repos], enforce=enforce)
17936
18009
  if data["enforcement_decision"] == "pro_required":
17937
18010
  require_pro("workspace entitlement")
17938
18011
  _emit_command_output(
sourcecode/cve_feed.py CHANGED
@@ -258,9 +258,14 @@ def load_feed(feed_path: Path, *, now: Optional[datetime] = None) -> Feed:
258
258
  newest = stamp
259
259
 
260
260
  if unreadable:
261
+ feed.error = f"{unreadable} feed file(s) were not valid JSON"
261
262
  feed.limitations.append(
262
- f"{unreadable} feed file(s) were not valid JSON and were skipped"
263
+ f"{unreadable} feed file(s) were not valid JSON; the feed was rejected"
263
264
  )
265
+ # Partial advisory data is unsafe for a reachability gate: a skipped
266
+ # file can contain the one advisory that should have blocked the run.
267
+ feed.loaded = False
268
+ return feed
264
269
  feed.packages = len(feed.by_package)
265
270
  feed.loaded = True
266
271
 
sourcecode/license.py CHANGED
@@ -550,7 +550,33 @@ def workspace_entitlement(
550
550
  aggregate crosses the threshold. This function only publishes the decision;
551
551
  it never changes the per-repository rule or performs billing.
552
552
  """
553
- unique = sorted({str(Path(path).resolve()) for path in repo_paths})
553
+ normalized = sorted({str(Path(path).resolve()) for path in repo_paths})
554
+ invalid = [path for path in normalized if not Path(path).is_dir()]
555
+ if invalid:
556
+ raise FileNotFoundError(
557
+ "Workspace repository path does not exist or is not a directory: "
558
+ + invalid[0]
559
+ )
560
+ unique: list[str] = []
561
+ overlapping_paths: list[dict[str, str]] = []
562
+ for candidate in normalized:
563
+ parent = next(
564
+ (existing for existing in unique if Path(candidate) == Path(existing)
565
+ or Path(existing) in Path(candidate).parents),
566
+ None,
567
+ )
568
+ if parent is not None:
569
+ overlapping_paths.append({"parent": parent, "child": candidate})
570
+ continue
571
+ descendants = [
572
+ existing for existing in unique
573
+ if Path(candidate) in Path(existing).parents
574
+ ]
575
+ for descendant in descendants:
576
+ unique.remove(descendant)
577
+ overlapping_paths.append({"parent": candidate, "child": descendant})
578
+ unique.append(candidate)
579
+ unique.sort()
554
580
  repositories = [
555
581
  {"path": path, "java_files": count_java_files(path)}
556
582
  for path in unique
@@ -561,6 +587,7 @@ def workspace_entitlement(
561
587
  "status": "ok" if repositories else "no_repositories",
562
588
  "repository_count": len(repositories),
563
589
  "repositories": repositories,
590
+ "overlapping_paths": overlapping_paths,
564
591
  "workspace_java_files": total,
565
592
  "workspace_java_file_limit": _FREE_REPO_JAVA_FILE_LIMIT,
566
593
  "workspace_threshold_exceeded": total > _FREE_REPO_JAVA_FILE_LIMIT,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 5.9.2
3
+ Version: 5.9.4
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  Project-URL: Homepage, https://github.com/HarounDominique/sourcecode
6
6
  Project-URL: Repository, https://github.com/HarounDominique/sourcecode
@@ -59,7 +59,7 @@ Description-Content-Type: text/markdown
59
59
  > question. All analysis is local and deterministic; see [Privacy](#configuration--privacy)
60
60
  > before inspecting a repository whose telemetry policy you need to control.
61
61
 
62
- ![Version](https://img.shields.io/badge/version-5.9.2-blue)
62
+ ![Version](https://img.shields.io/badge/version-5.9.4-blue)
63
63
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
64
64
 
65
65
  Copyright (c) 2026 Dominique Haroun. All rights reserved. ASK Engine 5.7.0 and
@@ -196,7 +196,7 @@ set `ASK_MAX_RSS_MB=<megabytes>`; if the process exceeds it, ASK refuses the
196
196
  answer with `MEMORY_TOO_LARGE`, including the observed peak and limit. Leave it
197
197
  unset, or set it to `0`, to disable the ceiling explicitly.
198
198
 
199
- **Current performance status.** Release 5.9.2 retains the shared Canonical IR
199
+ **Current performance status.** Release 5.9.4 retains the shared Canonical IR
200
200
  path used by `validation` and now passes that same CIR into labelled `data-exposure`
201
201
  runs; the no-label path remains parse-free. These are implementation properties,
202
202
  not duration promises. A controlled external warm-path audit has opened performance
@@ -212,6 +212,20 @@ aggregate Java entitlement. `--enforce` is an opt-in named-buyer pilot that reus
212
212
  the existing Pro gate; advisory mode remains the default and no billing is
213
213
  performed by the CLI.
214
214
 
215
+ `auth workspace` normalizes its repository arguments before counting. If one
216
+ path contains another, the parent is counted once and the overlap is published
217
+ in `overlapping_paths`; a missing or non-directory path is rejected as structured
218
+ invalid input rather than treated as an empty repository. This prevents silent
219
+ entitlement errors in CI and does not change per-repository gates or billing.
220
+
221
+ Release 5.9.4 also hardens the boundary contracts: missing consumer repositories,
222
+ malformed CVE feeds and unresolved `pack gate --since` refs fail as structured
223
+ input errors; duplicate same-commit baselines are never overwritten. `posture
224
+ --diff` validates unknown profiles when the repository declares profiles. In
225
+ `endpoints --compact`, an empty `endpoints` list is explicitly marked as an
226
+ intentional omission, not an empty API; unresolved `impact` and `impact-chain`
227
+ queries exit non-zero, including on repositories without Java sources.
228
+
215
229
  **What a warm actually covers.** `ask cache warm` rebuilds the declared root views and their
216
230
  shared structural layers (L1/L2 + the Repository Intelligence Snapshot + the shared Canonical
217
231
  IR). Pass `--views compact` or `--views compact,agent` to warm only selected views; without
@@ -237,7 +251,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
237
251
  # pip / pipx
238
252
  pipx install sourcecode # or: pip install sourcecode
239
253
 
240
- ask version # ask 5.9.2 — and, on a build that has aged,
254
+ ask version # ask 5.9.4 — and, on a build that has aged,
241
255
  # how many releases have probably shipped since
242
256
  ```
243
257
 
@@ -1,12 +1,12 @@
1
- sourcecode/__init__.py,sha256=ut7y9JuYeIRLMJEe0f5IzU0SKAdtECMoi4vqjiB12d4,308
1
+ sourcecode/__init__.py,sha256=0qlYxElAIApXgtaqvLvB59Bcp23mslHDvyztYWrQZD0,308
2
2
  sourcecode/__main__.py,sha256=qDFVkobwsdZrX0B3stiRqsUNivVpybKO-egzAwCW4xw,165
3
- sourcecode/_build_commit.py,sha256=F8XcP7GedwxwV4VpzoMYIzsaNzln5x6bazw5rs5xHGw,8740
3
+ sourcecode/_build_commit.py,sha256=W0ohkcA2_TncmrWEsLzLPfZNmCxpl2xc1rVqQD4-i6o,8740
4
4
  sourcecode/access_drift.py,sha256=1R7_fiAPvKNOnxc9uxkH3JH-k8MiAuyE7FwWUX1r4Fg,8995
5
5
  sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
6
6
  sourcecode/analysis_budget.py,sha256=ZuNEboPF8A6GK-_V7WNt92ezgriga7_Rbr-vvSN4UEE,4183
7
7
  sourcecode/answer_bounds.py,sha256=_51_DCuYEUmI_xwUFxDMiprSavkNakkLiIBbPjevQ3M,9793
8
8
  sourcecode/archetype.py,sha256=V_s5grgpbanrXb8qGEQb73W5Cg5iKW9FQHWKgg5KM8Y,41201
9
- sourcecode/architectural_baseline.py,sha256=4GiMVBLJVHvRKSWQGf2ZVurI1-R0qk5pmaevgvGSgVA,26325
9
+ sourcecode/architectural_baseline.py,sha256=9ds-ZYw4dEJUKWZSdq_ufHq7d2AXnMbEHteSQlT7iu8,26468
10
10
  sourcecode/architectural_delta.py,sha256=JhZVmA0gYUryt4x4ieVuxPbcvoBygHgADZaYJ_o4gdk,15142
11
11
  sourcecode/architecture_analyzer.py,sha256=GFc4ek-s1IHWM7pl-0L32WahZ93AmDgrAMcOuBKA5Dk,61463
12
12
  sourcecode/architecture_summary.py,sha256=BVVRHd952cjRhjHnR6CPrvKgaa-tdM16l-pBi1yDCPs,32395
@@ -35,7 +35,7 @@ sourcecode/change_plan.py,sha256=drAoDsqFplutCLc6Y0xue7sYkTls0SpYh7aOXvttv3I,129
35
35
  sourcecode/ci_output.py,sha256=X4NRokWWMRF9Ex8OQrEO2g04GzJ00GDP5zYIoqI9YCc,4625
36
36
  sourcecode/cir_graphs.py,sha256=RytqSv1WF8kdlfXj2fPQz16LY4akS2kyThbdWL8beCQ,14835
37
37
  sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
38
- sourcecode/cli.py,sha256=W4OUlL5RIy81G2cByGjyQfA7HLurirL1sftErTxhkuE,932859
38
+ sourcecode/cli.py,sha256=_8CaYY1ye6GNc1VEYzOa4JbTkmUiGoxfnUffIi7i1Zo,936454
39
39
  sourcecode/client_calls.py,sha256=YOPnfRZzRyqmdHvXSVeiunW8cuiuCuWXacNIQRgKMCw,22640
40
40
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
41
41
  sourcecode/compare.py,sha256=Lnfmm9I3k0H1jP1tV6ZJ-R7znjQPLPLCe5TzV1Jb_PM,15323
@@ -52,7 +52,7 @@ sourcecode/contract_init.py,sha256=bXRCqWM9tvrDLeT8j7ea6IWf87jB4VJxnlSzGBsRRXo,1
52
52
  sourcecode/contract_model.py,sha256=nRxJKPMs1VHwFTa8AVXhGmaLjti3Lr2sjHDpWgv1bfE,3917
53
53
  sourcecode/contract_pipeline.py,sha256=4XE-xhab7ysdExXyUOBl2hAH4MABo7moKnT9V4XcEwQ,30243
54
54
  sourcecode/coverage_parser.py,sha256=X9scuUWxacahYTrTBGea2Ku-sf-WCVcnc9OEB35_3IU,19340
55
- sourcecode/cve_feed.py,sha256=t93Ofu0uxB3zyI8W9nUKmH4Ljbt4qZpGIg68IP_aTrY,10749
55
+ sourcecode/cve_feed.py,sha256=IBVpQaY04biXKEeNf1qKO_95GqEV23fwhj4oNk7s1-g,11028
56
56
  sourcecode/cve_reachability.py,sha256=aEHkG3IaeLxgekK79cz07QTcv9LZHEqEYqyLGjwjzSE,20409
57
57
  sourcecode/data_exposure.py,sha256=73EcuHsN3ESKdSvSj1B2FzD1myGJqzlNJpYO1NpI5WM,19963
58
58
  sourcecode/data_labels.py,sha256=9v0JaICRH7jIZ015T1_DuT5E0MFfeL-Wcq1WOPE0Fe0,10965
@@ -96,7 +96,7 @@ sourcecode/identity_fallback.py,sha256=uRC_j_80uaI4Ue4tf-OhKX4h472dmufYcjEFGSxY2
96
96
  sourcecode/integration_coordinates.py,sha256=rvuApnS7WdRUbeQLg_oKj-Gi0koO2RKdw6HenXNgrDo,12443
97
97
  sourcecode/intent_router.py,sha256=64d5Q1TKj9h2vv_iuWbCEb0eeeG6H5wYvSX7V_7w-gw,15511
98
98
  sourcecode/jdk_exports.py,sha256=fCrlwNAXUT9gge_joq6kMnY3zJxYB2pxqy-0w3o3MJI,874
99
- sourcecode/license.py,sha256=yXMZkgP8uDFxj6bWQLS2eM9go2o1OA1LiPa3d5bovxM,29913
99
+ sourcecode/license.py,sha256=Rx_TXEztUJB9TzI352hEh8cmCd-30J9WS6iT4EndcW0,30974
100
100
  sourcecode/manifest_cache.py,sha256=aPRHUs781d00fVbtlTiSaq0-AS9bAqS7_zJ0snZvlJs,2244
101
101
  sourcecode/mcp_nudge.py,sha256=beNjfyb7z09T1rnS4t6Q4qfbLzZQNaP6ddCA4XpmV0s,3256
102
102
  sourcecode/metrics_analyzer.py,sha256=gLoRWKygF18jLgwsqmGXSWopw-f5iO1VbM7Jjdif6ag,22809
@@ -276,10 +276,10 @@ sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec
276
276
  sourcecode/_docs/DEFECT-LEDGER.md,sha256=EkYSJieDrazfADCZ8tsCsybyW9U0nAzWDQ8rS4KmGTU,1115164
277
277
  sourcecode/_docs/LICENSE,sha256=23-fk61oSWGVmkWyka-FIM0rhAPM2hcYHnsaridL8Uo,1859
278
278
  sourcecode/_docs/NOTICE,sha256=Br-AA8Y3iJo1WXa5uP8QBpzqI5nDu3iGLbFyvDt3BOs,321
279
- sourcecode/_docs/USER_GUIDE.md,sha256=Hs6k1Au2LqvCO1_eydxRUj0q5l_7cd17AED81QTeobg,103394
280
- sourcecode-5.9.2.dist-info/METADATA,sha256=oGUpHe6x5KBnysYq5ke8YU9PV-iHDWXQQbUrLinnwRA,63813
281
- sourcecode-5.9.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
282
- sourcecode-5.9.2.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
283
- sourcecode-5.9.2.dist-info/licenses/LICENSE,sha256=23-fk61oSWGVmkWyka-FIM0rhAPM2hcYHnsaridL8Uo,1859
284
- sourcecode-5.9.2.dist-info/licenses/NOTICE,sha256=Br-AA8Y3iJo1WXa5uP8QBpzqI5nDu3iGLbFyvDt3BOs,321
285
- sourcecode-5.9.2.dist-info/RECORD,,
279
+ sourcecode/_docs/USER_GUIDE.md,sha256=cE9Q2IMoCXELmhwpvZHslppGBGK2nlR0RSzksifni7U,104721
280
+ sourcecode-5.9.4.dist-info/METADATA,sha256=PivRgNyboaCJ75FC86-6HQ4OIIk0l5TAperOYb4uTsI,64752
281
+ sourcecode-5.9.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
282
+ sourcecode-5.9.4.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
283
+ sourcecode-5.9.4.dist-info/licenses/LICENSE,sha256=23-fk61oSWGVmkWyka-FIM0rhAPM2hcYHnsaridL8Uo,1859
284
+ sourcecode-5.9.4.dist-info/licenses/NOTICE,sha256=Br-AA8Y3iJo1WXa5uP8QBpzqI5nDu3iGLbFyvDt3BOs,321
285
+ sourcecode-5.9.4.dist-info/RECORD,,