personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(release): publish the unsupported-platform tombstone ahead of base

Remove the unsupported-platform tombstone from the aggregate base upload set so base publish cannot upload it. The fail-closed prerequisite stays ahead of classification and upload, and pre-upload index validation now treats tombstone-published/base-empty as the expected production state instead of rejecting it as divergent; previously the rail could not express the required ordering and an empty index would have uploaded the tombstone together with base.

Make check_native_sol_conformance fail on zero authorities, zero Flask routes, zero OpenAPI operations, or a missing top-level import entry instead of passing vacuously on an empty scan.

Delete check_tools_http_only.py and check_call_http_only.py, plus their tests and Makefile wiring. Their subjects no longer exist, so they reported success over an empty scan while still running in make ci.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

+112 -999
-16
Makefile
··· 506 506 @echo "" 507 507 @echo "=== Running provider-start-command check ===" 508 508 @$(MAKE) check-provider-start-commands 509 - @echo "" 510 - @echo "=== Running call-http-only check ===" 511 - @$(MAKE) check-call-http-only 512 - @echo "" 513 509 @echo "=== Running legacy-chat surface check ===" 514 510 @$(MAKE) check-no-legacy-chat 515 511 @echo "" ··· 524 520 @echo "" 525 521 @echo "=== Running rust release-manifest check ===" 526 522 @$(MAKE) check-rust-release-manifest 527 - @echo "" 528 - @echo "=== Running tools-http-only check ===" 529 - @$(MAKE) check-tools-http-only 530 - @echo "" 531 523 @echo "=== Running access-imports-clean check ===" 532 524 @$(MAKE) check-access-imports-clean 533 525 @echo "" ··· 700 692 check-provider-start-commands: .installed 701 693 $(VENV_BIN)/python scripts/check_provider_start_commands.py 702 694 703 - # sol call HTTP-only gate (call.py reaches the journal only over HTTP) 704 - check-call-http-only: .installed 705 - $(VENV_BIN)/python scripts/check_call_http_only.py 706 - 707 695 # Removed chat surfaces stay out of tracked Python, HTML, and JavaScript. 708 696 check-no-legacy-chat: .installed 709 697 $(VENV_BIN)/python scripts/check_no_legacy_chat.py ··· 727 715 # Package dependency and script ownership consistency gate 728 716 check-extras-consistency: .installed 729 717 $(VENV_BIN)/python scripts/check_extras_consistency.py 730 - 731 - # Built-in sol call tools HTTP-only gate 732 - check-tools-http-only: .installed 733 - $(VENV_BIN)/python scripts/check_tools_http_only.py 734 718 735 719 # Thin sol access surface import-clean gate (fast meta_path simulation; in ci) 736 720 check-access-imports-clean: .installed
-386
scripts/check_call_http_only.py
··· 1 - #!/usr/bin/env python3 2 - # SPDX-License-Identifier: AGPL-3.0-only 3 - # Copyright (c) 2026 sol pbc 4 - 5 - """``sol call`` HTTP-only lint. 6 - 7 - Invariant: a direct-child ``solstone/apps/*/call.py`` must reach the journal 8 - only over the Convey HTTP client. It must not (a) import a journal/domain/server 9 - module (``solstone.think.*`` / ``solstone.apps.*`` / ``solstone.convey.*``) 10 - outside the enumerated allow-set, nor (b) perform a direct filesystem I/O 11 - mechanic. 12 - 13 - The enumerated allow-set is two path strings, compared as strings: 14 - ``solstone.think.convey_client`` and ``solstone.convey.reasons``. The gate never 15 - imports these modules. This is not a purity predicate; a future pure shared 16 - module is a one-line addition to the allow-set. 17 - 18 - All imports count, including function-local/lazy ones. That keeps the eventual 19 - capstone zero-violation invariant evasion-proof: a cutover cannot hide a journal 20 - reach inside a function body. 21 - 22 - The committed ``ALLOWLIST`` is keyed by ``(file, kind)`` with an allowed count. 23 - This is a ``!=`` self-check: a live count above the allowlisted count fails as a 24 - new/over violation, and a live count below the allowlisted count (including a 25 - deleted/renamed/now-clean file with live count 0) fails as a stale entry. The 26 - gate iterates the union of discovered keys and allowlist keys so stale entries 27 - for vanished files are still visited. 28 - 29 - ``EXCLUDED_FILES`` lists ``call.py`` surfaces that are not journal-data command 30 - surfaces — the invariant does not apply to them. ``discover_modules`` skips 31 - them, so they are neither scanned nor allowlisted; a file must be both skipped 32 - here and absent from ``ALLOWLIST`` to be invisible to the union self-check. Each 33 - retained exclusion is a documented architectural exception, not an evasion. 34 - 35 - Explicitly not flagged: ``subprocess.*`` (residual subprocess filesystem access 36 - is a cutover-review judgment, not this gate's job), ``os.environ`` / 37 - ``os.getenv`` (neither a journal import nor a filesystem mechanic), pure path 38 - algebra (``/``, ``.parent``, ``.name``, ``.suffix``, ``.stem``, 39 - ``.with_suffix`` / ``.with_name`` / ``.with_stem``), and ``os.path.*`` 40 - path-algebra/metadata helpers. 41 - 42 - Known limitation: this gate keys off absolute ``solstone.*`` module strings. 43 - Relative imports such as ``from .sibling import x`` are not resolved. The tree 44 - uses absolute imports by convention, so this is not a current gap. 45 - 46 - Exit codes: 47 - 0 - clean 48 - 1 - any over violation or stale allowlist entry 49 - """ 50 - 51 - from __future__ import annotations 52 - 53 - import argparse 54 - import ast 55 - import sys 56 - from pathlib import Path 57 - 58 - ROOT = Path(__file__).resolve().parent.parent 59 - 60 - ALLOW_SET: frozenset[str] = frozenset( 61 - { 62 - "solstone.apps.support.copy", 63 - "solstone.convey.reasons", 64 - "solstone.think.convey_client", 65 - } 66 - ) 67 - FLAGGED_NAMESPACES: tuple[str, ...] = ( 68 - "solstone.think", 69 - "solstone.apps", 70 - "solstone.convey", 71 - ) 72 - IO_METHODS: frozenset[str] = frozenset( 73 - { 74 - "read_text", 75 - "read_bytes", 76 - "write_text", 77 - "write_bytes", 78 - "open", 79 - "unlink", 80 - "mkdir", 81 - "rmdir", 82 - "touch", 83 - "glob", 84 - "iterdir", 85 - "rglob", 86 - } 87 - ) 88 - OS_FS_FUNCS: frozenset[str] = frozenset( 89 - { 90 - "remove", 91 - "unlink", 92 - "mkdir", 93 - "makedirs", 94 - "rmdir", 95 - "removedirs", 96 - "rename", 97 - "renames", 98 - "replace", 99 - "symlink", 100 - "link", 101 - "listdir", 102 - "scandir", 103 - "walk", 104 - "chmod", 105 - "chown", 106 - "truncate", 107 - "mkfifo", 108 - "mknod", 109 - "open", 110 - } 111 - ) 112 - SHUTIL_FS_FUNCS: frozenset[str] = frozenset( 113 - { 114 - "move", 115 - "copy", 116 - "copy2", 117 - "copyfile", 118 - "copytree", 119 - "rmtree", 120 - "copyfileobj", 121 - "make_archive", 122 - "unpack_archive", 123 - "copymode", 124 - "copystat", 125 - "chown", 126 - } 127 - ) 128 - 129 - # Out-of-scope CLIs: not journal-data command surfaces, so the HTTP-only 130 - # invariant does not apply. Skipped by discover_modules — neither scanned nor 131 - # allowlisted. Each retained file would be a documented, principled exception; 132 - # the set is currently empty (support graduated to a pure Convey HTTP client, 133 - # timeline rollups moved off sol-call onto journal maintenance). 134 - EXCLUDED_FILES: frozenset[str] = frozenset() 135 - 136 - EXCLUDED_PREFIXES: tuple[str, ...] = () 137 - 138 - # Committed allowlist of current direct app-call violations, keyed by 139 - # (posix-relative-path, kind) -> allowed count. Ratchets toward empty: lower a 140 - # count as occurrences are converted; stale entries fail until lowered/removed. 141 - ALLOWLIST: dict[tuple[str, str], int] = {} 142 - 143 - 144 - def _is_under_namespace(module: str) -> bool: 145 - return any( 146 - module == namespace or module.startswith(f"{namespace}.") 147 - for namespace in FLAGGED_NAMESPACES 148 - ) 149 - 150 - 151 - def _is_excluded(rel: Path) -> bool: 152 - rel_str = rel.as_posix() 153 - return rel_str in EXCLUDED_FILES or any( 154 - rel_str.startswith(prefix) for prefix in EXCLUDED_PREFIXES 155 - ) 156 - 157 - 158 - def discover_modules(root: Path) -> list[Path]: 159 - """Return posix-relative direct-child ``solstone/apps/*/call.py`` files.""" 160 - apps_dir = root / "solstone" / "apps" 161 - if not apps_dir.is_dir(): 162 - return [] 163 - 164 - found: list[Path] = [] 165 - for path in sorted(apps_dir.glob("*/call.py")): 166 - rel = path.relative_to(root) 167 - if "__pycache__" in rel.parts: 168 - continue 169 - if _is_excluded(rel): 170 - continue 171 - found.append(rel) 172 - return found 173 - 174 - 175 - def _collect_bindings( 176 - tree: ast.AST, 177 - ) -> tuple[set[str], dict[str, str], set[str], dict[str, str]]: 178 - os_aliases: set[str] = set() 179 - os_direct_names: dict[str, str] = {} 180 - shutil_aliases: set[str] = set() 181 - shutil_direct_names: dict[str, str] = {} 182 - 183 - for node in ast.walk(tree): 184 - if isinstance(node, ast.Import): 185 - for alias in node.names: 186 - bound = alias.asname or alias.name 187 - if alias.name == "os": 188 - os_aliases.add(bound) 189 - elif alias.name == "shutil": 190 - shutil_aliases.add(bound) 191 - elif isinstance(node, ast.ImportFrom): 192 - module = node.module or "" 193 - for alias in node.names: 194 - bound = alias.asname or alias.name 195 - if module == "os" and alias.name in OS_FS_FUNCS: 196 - os_direct_names[bound] = alias.name 197 - elif module == "shutil" and alias.name in SHUTIL_FS_FUNCS: 198 - shutil_direct_names[bound] = alias.name 199 - 200 - return os_aliases, os_direct_names, shutil_aliases, shutil_direct_names 201 - 202 - 203 - def _scan_import(node: ast.Import | ast.ImportFrom) -> list[tuple[int, str, str]]: 204 - findings: list[tuple[int, str, str]] = [] 205 - if isinstance(node, ast.Import): 206 - for alias in node.names: 207 - module = alias.name 208 - if _is_under_namespace(module) and module not in ALLOW_SET: 209 - findings.append((node.lineno, "import", module)) 210 - return findings 211 - 212 - module = node.module or "" 213 - if not _is_under_namespace(module): 214 - return findings 215 - if module in ALLOW_SET: 216 - return findings 217 - if len(node.names) == 1 and f"{module}.{node.names[0].name}" in ALLOW_SET: 218 - return findings 219 - return [(node.lineno, "import", module)] 220 - 221 - 222 - def _scan_call( 223 - node: ast.Call, 224 - os_aliases: set[str], 225 - os_direct_names: dict[str, str], 226 - shutil_aliases: set[str], 227 - shutil_direct_names: dict[str, str], 228 - ) -> tuple[int, str, str] | None: 229 - func = node.func 230 - 231 - if isinstance(func, ast.Name): 232 - if func.id == "open": 233 - return node.lineno, "fs", "open" 234 - if func.id in os_direct_names: 235 - return node.lineno, "fs", f"os.{os_direct_names[func.id]}" 236 - if func.id in shutil_direct_names: 237 - return node.lineno, "fs", f"shutil.{shutil_direct_names[func.id]}" 238 - return None 239 - 240 - if not isinstance(func, ast.Attribute): 241 - return None 242 - 243 - if func.attr in IO_METHODS: 244 - return node.lineno, "fs", f"Path.{func.attr}" 245 - 246 - if ( 247 - isinstance(func.value, ast.Name) 248 - and func.value.id in os_aliases 249 - and func.attr in OS_FS_FUNCS 250 - ): 251 - return node.lineno, "fs", f"os.{func.attr}" 252 - 253 - if ( 254 - isinstance(func.value, ast.Name) 255 - and func.value.id in shutil_aliases 256 - and func.attr in SHUTIL_FS_FUNCS 257 - ): 258 - return node.lineno, "fs", f"shutil.{func.attr}" 259 - 260 - return None 261 - 262 - 263 - def scan_source(source: str, filename: str = "<source>") -> list[tuple[int, str, str]]: 264 - """Return sorted ``(lineno, kind, detail)`` violations for source.""" 265 - tree = ast.parse(source, filename=filename) 266 - os_aliases, os_direct_names, shutil_aliases, shutil_direct_names = ( 267 - _collect_bindings(tree) 268 - ) 269 - 270 - findings: list[tuple[int, str, str]] = [] 271 - for node in ast.walk(tree): 272 - if isinstance(node, (ast.Import, ast.ImportFrom)): 273 - findings.extend(_scan_import(node)) 274 - elif isinstance(node, ast.Call): 275 - finding = _scan_call( 276 - node, 277 - os_aliases, 278 - os_direct_names, 279 - shutil_aliases, 280 - shutil_direct_names, 281 - ) 282 - if finding: 283 - findings.append(finding) 284 - 285 - findings.sort() 286 - return findings 287 - 288 - 289 - def scan_file(path: Path) -> list[tuple[int, str, str]]: 290 - return scan_source(path.read_text(encoding="utf-8"), filename=str(path)) 291 - 292 - 293 - def count_violations(root: Path) -> dict[tuple[str, str], int]: 294 - """Map ``(posix-relpath, kind)`` -> occurrence count across app calls.""" 295 - counts: dict[tuple[str, str], int] = {} 296 - for rel in discover_modules(root): 297 - for _lineno, kind, _detail in scan_file(root / rel): 298 - key = (rel.as_posix(), kind) 299 - counts[key] = counts.get(key, 0) + 1 300 - return counts 301 - 302 - 303 - def evaluate( 304 - root: Path, 305 - allowlist: dict[tuple[str, str], int], 306 - ) -> tuple[list[str], list[str], list[str]]: 307 - """Return ``(over, stale, tracked)`` human-readable lines.""" 308 - live: dict[tuple[str, str], list[int]] = {} 309 - for rel in discover_modules(root): 310 - rel_str = rel.as_posix() 311 - for lineno, kind, _detail in scan_file(root / rel): 312 - live.setdefault((rel_str, kind), []).append(lineno) 313 - 314 - over: list[str] = [] 315 - stale: list[str] = [] 316 - tracked: list[str] = [] 317 - 318 - for rel_str, kind in sorted(set(live) | set(allowlist)): 319 - key = (rel_str, kind) 320 - linenos = sorted(live.get(key, [])) 321 - count = len(linenos) 322 - allowed = allowlist.get(key, 0) 323 - if count > allowed: 324 - lines = ", ".join(str(n) for n in linenos) 325 - over.append( 326 - f"{rel_str}: {kind} count {count} exceeds allowed {allowed} " 327 - f"at line(s) {lines} - this call.py must reach the journal only " 328 - "via the convey HTTP client (solstone.think.convey_client); see " 329 - "the check_call_http_only gate." 330 - ) 331 - elif count < allowed: 332 - delete_hint = " (delete it)" if count == 0 else "" 333 - stale.append( 334 - f"{rel_str}: {kind} allowlisted at {allowed} but {count} live - " 335 - f"lower the entry to {count}{delete_hint} - check_call_http_only " 336 - "ratchets toward empty; a converted call.py removes its entry." 337 - ) 338 - elif allowed: 339 - tracked.append(f"{rel_str}: {count}/{allowed} {kind} (allowlisted)") 340 - 341 - return over, stale, tracked 342 - 343 - 344 - def main(argv: list[str] | None = None) -> int: 345 - parser = argparse.ArgumentParser(description="sol call HTTP-only lint") 346 - parser.add_argument( 347 - "--root", 348 - type=Path, 349 - default=ROOT, 350 - help="Repository root to scan (defaults to the checkout root).", 351 - ) 352 - args = parser.parse_args(argv) 353 - 354 - over, stale, tracked = evaluate(args.root, ALLOWLIST) 355 - 356 - if tracked: 357 - print("call-http-only: known violations (allowlisted, ratcheting down):") 358 - for line in tracked: 359 - print(f" {line}") 360 - print() 361 - 362 - if over or stale: 363 - if over: 364 - print("call-http-only: NEW violations:", file=sys.stderr) 365 - for line in over: 366 - print(f" {line}", file=sys.stderr) 367 - print(file=sys.stderr) 368 - if stale: 369 - print("call-http-only: STALE allowlist entries:", file=sys.stderr) 370 - for line in stale: 371 - print(f" {line}", file=sys.stderr) 372 - print(file=sys.stderr) 373 - print( 374 - "sol call command modules must reach the journal only via " 375 - "solstone.think.convey_client; lower stale allowlist counts as " 376 - "call.py files are converted.", 377 - file=sys.stderr, 378 - ) 379 - return 1 380 - 381 - print("call-http-only: pass") 382 - return 0 383 - 384 - 385 - if __name__ == "__main__": 386 - raise SystemExit(main())
+9
scripts/check_native_sol_conformance.py
··· 93 93 raw_authority_by_operation = load_raw_authority_entries(root) 94 94 contract_by_operation = collect_contract_operations(document) 95 95 96 + if not authorities: 97 + errors.append("native sol conformance discovered zero authorities") 98 + if not route_map: 99 + errors.append("native sol conformance discovered zero Flask routes") 100 + if not contract_by_operation: 101 + errors.append("native sol conformance discovered zero OpenAPI operations") 102 + if not any(authority.entry_type == "top-level-import" for authority in authorities): 103 + errors.append("native sol conformance missing top-level import authority") 104 + 96 105 for authority in sorted(authorities, key=lambda entry: entry.operation_id): 97 106 raw_authority = raw_authority_by_operation.get(authority.operation_id) 98 107 if authority.entry_type == "http":
+8 -8
scripts/check_rust_release_manifest.py
··· 1296 1296 repair="use the models version derived from package metadata", 1297 1297 ) 1298 1298 ) 1299 - if expected_count == 16 and model_like: 1299 + if expected_count == 15 and model_like: 1300 1300 failures.append( 1301 1301 _failure( 1302 - "16-file candidate contains models archive leftover", 1303 - expected="no solstone_journal_models archives in a 16-file candidate", 1302 + "15-file candidate contains models archive leftover", 1303 + expected="no solstone_journal_models archives in a 15-file candidate", 1304 1304 actual=", ".join(sorted(model_like)), 1305 1305 repair="remove skipped models artifacts from the release candidate", 1306 1306 ) ··· 1338 1338 failures.extend(_case_collision_failures(entries)) 1339 1339 for entry in entries: 1340 1340 failures.extend(_validate_regular_file(entry, label=entry.name)) 1341 - if len(entries) not in {16, 18}: 1341 + if len(entries) not in {15, 17}: 1342 1342 failures.append( 1343 1343 _failure( 1344 - "release directory must contain exactly 16 or 18 files", 1345 - expected="16 files without models or 18 files with models", 1344 + "release directory must contain exactly 15 or 17 files", 1345 + expected="15 files without models or 17 files with models", 1346 1346 actual=str(len(entries)), 1347 1347 repair="validate the exact release candidate payload directory", 1348 1348 ) ··· 1362 1362 ) 1363 1363 expected_without_models = set(expected_package_names(include_models=False)) 1364 1364 expected_with_models = set(expected_package_names(include_models=True)) 1365 - include_models = len(entries) == 18 1365 + include_models = len(entries) == 17 1366 1366 expected_packages = ( 1367 1367 expected_with_models if include_models else expected_without_models 1368 1368 ) ··· 1394 1394 "release directory contains extra assets", 1395 1395 expected=", ".join(sorted(expected_packages)), 1396 1396 actual=", ".join(sorted(extra)), 1397 - repair="remove assets outside the exact 16/18-file release payload", 1397 + repair="remove assets outside the exact 15/17-file release payload", 1398 1398 ) 1399 1399 ) 1400 1400 try:
-246
scripts/check_tools_http_only.py
··· 1 - #!/usr/bin/env python3 2 - # SPDX-License-Identifier: AGPL-3.0-only 3 - # Copyright (c) 2026 sol pbc 4 - 5 - """Built-in ``sol call`` tools HTTP-only lint.""" 6 - 7 - from __future__ import annotations 8 - 9 - import argparse 10 - import ast 11 - import sys 12 - from pathlib import Path 13 - 14 - ROOT = Path(__file__).resolve().parent.parent 15 - 16 - TARGETS: tuple[str, ...] = ( 17 - "solstone/think/tools/health.py", 18 - "solstone/think/tools/ledger.py", 19 - "solstone/think/tools/profile.py", 20 - ) 21 - ALLOW_SET: frozenset[str] = frozenset( 22 - { 23 - "solstone.think.convey_client", 24 - "solstone.convey.reasons", 25 - "solstone.think.pipeline_health", 26 - } 27 - ) 28 - FLAGGED_NAMESPACES: tuple[str, ...] = ( 29 - "solstone.think", 30 - "solstone.apps", 31 - "solstone.convey", 32 - ) 33 - IO_METHODS: frozenset[str] = frozenset( 34 - { 35 - "read_text", 36 - "read_bytes", 37 - "write_text", 38 - "write_bytes", 39 - "open", 40 - "unlink", 41 - "mkdir", 42 - "rmdir", 43 - "touch", 44 - "glob", 45 - "iterdir", 46 - "rglob", 47 - } 48 - ) 49 - OS_FS_FUNCS: frozenset[str] = frozenset( 50 - { 51 - "remove", 52 - "unlink", 53 - "mkdir", 54 - "makedirs", 55 - "rmdir", 56 - "removedirs", 57 - "rename", 58 - "renames", 59 - "replace", 60 - "symlink", 61 - "link", 62 - "listdir", 63 - "scandir", 64 - "walk", 65 - "chmod", 66 - "chown", 67 - "truncate", 68 - "mkfifo", 69 - "mknod", 70 - "open", 71 - } 72 - ) 73 - SHUTIL_FS_FUNCS: frozenset[str] = frozenset( 74 - { 75 - "move", 76 - "copy", 77 - "copy2", 78 - "copyfile", 79 - "copytree", 80 - "rmtree", 81 - "copyfileobj", 82 - "make_archive", 83 - "unpack_archive", 84 - "copymode", 85 - "copystat", 86 - "chown", 87 - } 88 - ) 89 - 90 - 91 - def _is_under_namespace(module: str) -> bool: 92 - return any( 93 - module == namespace or module.startswith(f"{namespace}.") 94 - for namespace in FLAGGED_NAMESPACES 95 - ) 96 - 97 - 98 - def _collect_bindings( 99 - tree: ast.AST, 100 - ) -> tuple[set[str], dict[str, str], set[str], dict[str, str]]: 101 - os_aliases: set[str] = set() 102 - os_direct_names: dict[str, str] = {} 103 - shutil_aliases: set[str] = set() 104 - shutil_direct_names: dict[str, str] = {} 105 - 106 - for node in ast.walk(tree): 107 - if isinstance(node, ast.Import): 108 - for alias in node.names: 109 - bound = alias.asname or alias.name 110 - if alias.name == "os": 111 - os_aliases.add(bound) 112 - elif alias.name == "shutil": 113 - shutil_aliases.add(bound) 114 - elif isinstance(node, ast.ImportFrom): 115 - module = node.module or "" 116 - for alias in node.names: 117 - bound = alias.asname or alias.name 118 - if module == "os" and alias.name in OS_FS_FUNCS: 119 - os_direct_names[bound] = alias.name 120 - elif module == "shutil" and alias.name in SHUTIL_FS_FUNCS: 121 - shutil_direct_names[bound] = alias.name 122 - 123 - return os_aliases, os_direct_names, shutil_aliases, shutil_direct_names 124 - 125 - 126 - def _scan_import(node: ast.Import | ast.ImportFrom) -> list[tuple[int, str, str]]: 127 - findings: list[tuple[int, str, str]] = [] 128 - if isinstance(node, ast.Import): 129 - for alias in node.names: 130 - module = alias.name 131 - if _is_under_namespace(module) and module not in ALLOW_SET: 132 - findings.append((node.lineno, "import", module)) 133 - return findings 134 - 135 - module = node.module or "" 136 - if not _is_under_namespace(module): 137 - return findings 138 - if module in ALLOW_SET: 139 - return findings 140 - if len(node.names) == 1 and f"{module}.{node.names[0].name}" in ALLOW_SET: 141 - return findings 142 - return [(node.lineno, "import", module)] 143 - 144 - 145 - def _scan_call( 146 - node: ast.Call, 147 - os_aliases: set[str], 148 - os_direct_names: dict[str, str], 149 - shutil_aliases: set[str], 150 - shutil_direct_names: dict[str, str], 151 - ) -> tuple[int, str, str] | None: 152 - func = node.func 153 - 154 - if isinstance(func, ast.Name): 155 - if func.id == "open": 156 - return node.lineno, "fs", "open" 157 - if func.id in os_direct_names: 158 - return node.lineno, "fs", f"os.{os_direct_names[func.id]}" 159 - if func.id in shutil_direct_names: 160 - return node.lineno, "fs", f"shutil.{shutil_direct_names[func.id]}" 161 - return None 162 - 163 - if not isinstance(func, ast.Attribute): 164 - return None 165 - 166 - if func.attr in IO_METHODS: 167 - return node.lineno, "fs", f"Path.{func.attr}" 168 - 169 - if ( 170 - isinstance(func.value, ast.Name) 171 - and func.value.id in os_aliases 172 - and func.attr in OS_FS_FUNCS 173 - ): 174 - return node.lineno, "fs", f"os.{func.attr}" 175 - 176 - if ( 177 - isinstance(func.value, ast.Name) 178 - and func.value.id in shutil_aliases 179 - and func.attr in SHUTIL_FS_FUNCS 180 - ): 181 - return node.lineno, "fs", f"shutil.{func.attr}" 182 - 183 - return None 184 - 185 - 186 - def scan_source(source: str, filename: str = "<source>") -> list[tuple[int, str, str]]: 187 - tree = ast.parse(source, filename=filename) 188 - os_aliases, os_direct_names, shutil_aliases, shutil_direct_names = ( 189 - _collect_bindings(tree) 190 - ) 191 - 192 - findings: list[tuple[int, str, str]] = [] 193 - for node in ast.walk(tree): 194 - if isinstance(node, (ast.Import, ast.ImportFrom)): 195 - findings.extend(_scan_import(node)) 196 - elif isinstance(node, ast.Call): 197 - finding = _scan_call( 198 - node, 199 - os_aliases, 200 - os_direct_names, 201 - shutil_aliases, 202 - shutil_direct_names, 203 - ) 204 - if finding: 205 - findings.append(finding) 206 - 207 - findings.sort() 208 - return findings 209 - 210 - 211 - def scan_file(path: Path) -> list[tuple[int, str, str]]: 212 - return scan_source(path.read_text(encoding="utf-8"), filename=str(path)) 213 - 214 - 215 - def discover_modules(root: Path) -> list[Path]: 216 - return [Path(target) for target in TARGETS if (root / target).is_file()] 217 - 218 - 219 - def main(argv: list[str] | None = None) -> int: 220 - parser = argparse.ArgumentParser( 221 - description="built-in sol call tools HTTP-only lint" 222 - ) 223 - parser.add_argument( 224 - "--root", 225 - type=Path, 226 - default=ROOT, 227 - help="Repository root to scan (defaults to the checkout root).", 228 - ) 229 - args = parser.parse_args(argv) 230 - 231 - findings: list[str] = [] 232 - for rel in discover_modules(args.root): 233 - for lineno, kind, detail in scan_file(args.root / rel): 234 - findings.append(f"{rel.as_posix()}:{lineno}: {kind} {detail}") 235 - 236 - if findings: 237 - for finding in findings: 238 - print(finding, file=sys.stderr) 239 - return 1 240 - 241 - print("tools-http-only: pass") 242 - return 0 243 - 244 - 245 - if __name__ == "__main__": 246 - raise SystemExit(main())
-4
scripts/check_wheel_contents.py
··· 782 782 (dist_dir / f"solstone-{version}.tar.gz", "root sdist"), 783 783 (dist_dir / f"solstone-{version}-py3-none-any.whl", "root any wheel"), 784 784 (dist_dir / f"solstone_core-{version}.tar.gz", "core sdist"), 785 - ( 786 - dist_dir / f"solstone_core_unsupported_platform-{version}.tar.gz", 787 - "core unsupported-platform tombstone sdist", 788 - ), 789 785 (dist_dir / f"solstone_journal-{version}.tar.gz", "journal CPU sdist"), 790 786 ( 791 787 dist_dir / f"solstone_journal-{version}-py3-none-any.whl",
-11
scripts/release_candidate_driver.py
··· 105 105 ROOT_WORKSPACE_PACKAGE = "solstone" 106 106 MODELS_WORKSPACE_PACKAGE = "solstone-journal-models" 107 107 CORE_WORKSPACE_PACKAGE = "solstone-core" 108 - CORE_UNSUPPORTED_TOMBSTONE_DIR = "scripts/solstone-core-unsupported-platform-tombstone" 109 - CORE_UNSUPPORTED_TOMBSTONE_ALLOW_BUILD_ENV = ( 110 - "SOLSTONE_CORE_UNSUPPORTED_PLATFORM_TOMBSTONE_ALLOW_BUILD" 111 - ) 112 108 113 109 114 110 @dataclass(frozen=True) ··· 512 508 *, include_models: bool, version: str 513 509 ) -> tuple[tuple[tuple[str, ...], str], ...]: 514 510 render_check = (("python3", "scripts/render_packaging.py", "--check"), "") 515 - tombstone_sdist = ( 516 - ("uv", "build", CORE_UNSUPPORTED_TOMBSTONE_DIR, "--sdist", "--out-dir", "dist"), 517 - "", 518 - ) 519 511 package_builds = tuple( 520 512 (("uv", "build", "--package", package), CORE_X86_64_MATURIN_ARGS) 521 513 for package in _expected_local_build_packages(include_models=include_models) ··· 536 528 ) 537 529 return ( 538 530 render_check, 539 - tombstone_sdist, 540 531 *package_builds, 541 532 core_sdist, 542 533 x86_64_core, ··· 573 564 version=version, 574 565 ): 575 566 env = _scrubbed_build_env(root, maturin_args) 576 - if argv[0:3] == ("uv", "build", CORE_UNSUPPORTED_TOMBSTONE_DIR): 577 - env[CORE_UNSUPPORTED_TOMBSTONE_ALLOW_BUILD_ENV] = "1" 578 567 _run_stdout( 579 568 runner, 580 569 list(argv),
-182
tests/test_check_call_http_only.py
··· 1 - # SPDX-License-Identifier: AGPL-3.0-only 2 - # Copyright (c) 2026 sol pbc 3 - 4 - """Self-test for scripts/check_call_http_only.py.""" 5 - 6 - from __future__ import annotations 7 - 8 - import importlib.util 9 - import subprocess 10 - import sys 11 - from pathlib import Path 12 - 13 - import pytest 14 - 15 - REPO_ROOT = Path(__file__).resolve().parents[1] 16 - SCRIPT = REPO_ROOT / "scripts" / "check_call_http_only.py" 17 - 18 - 19 - def _load_checker(): 20 - spec = importlib.util.spec_from_file_location("check_call_http_only", SCRIPT) 21 - assert spec and spec.loader 22 - module = importlib.util.module_from_spec(spec) 23 - spec.loader.exec_module(module) 24 - return module 25 - 26 - 27 - ccho = _load_checker() 28 - 29 - 30 - def _write_file(root: Path, rel: str, content: str) -> None: 31 - path = root / rel 32 - path.parent.mkdir(parents=True, exist_ok=True) 33 - path.write_text(content, encoding="utf-8") 34 - 35 - 36 - def _run(root: Path) -> subprocess.CompletedProcess: 37 - return subprocess.run( 38 - [sys.executable, str(SCRIPT), "--root", str(root)], 39 - capture_output=True, 40 - text=True, 41 - ) 42 - 43 - 44 - @pytest.mark.parametrize( 45 - ("source", "kind"), 46 - [ 47 - ("from solstone.think.utils import get_journal\n", "import"), 48 - ("from solstone.convey.utils import contained_path\n", "import"), 49 - ("from solstone.think.journal_io import LockTimeout\n", "import"), 50 - ("import solstone.apps.observer.share_delete\n", "import"), 51 - ( 52 - "def f():\n" 53 - " from solstone.think.utils import get_journal\n" 54 - " return get_journal()\n", 55 - "import", 56 - ), 57 - ("def f():\n open('x')\n", "fs"), 58 - ("def f(p):\n p.mkdir(parents=True)\n", "fs"), 59 - ("import shutil\n\ndef f(a, b):\n shutil.move(a, b)\n", "fs"), 60 - ("def f(dst):\n dst.parent.mkdir()\n", "fs"), 61 - ("def f(p):\n p.write_text('x')\n", "fs"), 62 - ("def f(p):\n p.unlink()\n", "fs"), 63 - ("import os\n\ndef f(p):\n os.remove(p)\n", "fs"), 64 - ], 65 - ) 66 - def test_scan_source_flags_http_only_violations(source: str, kind: str) -> None: 67 - findings = ccho.scan_source(source) 68 - assert [finding[1] for finding in findings] == [kind] 69 - 70 - 71 - def test_mixed_parent_mkdir_flags_once() -> None: 72 - findings = ccho.scan_source("def f(dst):\n dst.parent.mkdir()\n") 73 - assert findings == [(2, "fs", "Path.mkdir")] 74 - 75 - 76 - @pytest.mark.parametrize( 77 - "source", 78 - [ 79 - "from solstone.think.convey_client import call_get\n", 80 - "from solstone.convey.reasons import Reason\n", 81 - "import typer\n", 82 - "from pathlib import Path\n", 83 - "def f(Path, x):\n return Path(x) / 'y'\n", 84 - "def f(p):\n return p.parent\n", 85 - "def f(p):\n return p.with_suffix('.tmp')\n", 86 - "def f(p):\n return p.name\n", 87 - "import os\n\ndef f():\n return os.environ.get('X')\n", 88 - "import os\n\ndef f():\n return os.getenv('X')\n", 89 - "import subprocess\n\ndef f():\n return subprocess.run(['x'])\n", 90 - "import os\n\ndef f(a, b):\n return os.path.join(a, b)\n", 91 - "import os.path\n\ndef f(a, b):\n return os.path.join(a, b)\n", 92 - ], 93 - ) 94 - def test_scan_source_ignores_allowed_and_out_of_scope_surfaces(source: str) -> None: 95 - assert ccho.scan_source(source) == [] 96 - 97 - 98 - def test_ratchet_by_file_kind_count(tmp_path: Path) -> None: 99 - root = tmp_path / "bad" 100 - _write_file( 101 - root, 102 - "solstone/apps/badapp/call.py", 103 - "from solstone.think.utils import get_journal\n\n" 104 - "def f():\n" 105 - " open('x')\n" 106 - " return get_journal()\n", 107 - ) 108 - 109 - over, stale, tracked = ccho.evaluate(root, {}) 110 - assert over 111 - assert stale == [] 112 - assert tracked == [] 113 - 114 - counts = ccho.count_violations(root) 115 - over_exact, stale_exact, tracked_exact = ccho.evaluate(root, counts) 116 - assert over_exact == [] 117 - assert stale_exact == [] 118 - assert tracked_exact 119 - 120 - key = next(iter(counts)) 121 - ratcheted = dict(counts) 122 - ratcheted[key] = counts[key] - 1 123 - over_lowered, stale_lowered, _ = ccho.evaluate(root, ratcheted) 124 - assert over_lowered 125 - assert stale_lowered == [] 126 - 127 - 128 - def test_stale_allowlist_entries_are_reported_for_vanished_keys( 129 - tmp_path: Path, 130 - ) -> None: 131 - root = tmp_path / "clean" 132 - _write_file(root, "solstone/apps/clean/call.py", "import typer\n") 133 - allowlist = {("solstone/apps/vanished/call.py", "import"): 1} 134 - 135 - over, stale, tracked = ccho.evaluate(root, allowlist) 136 - assert over == [] 137 - assert any("solstone/apps/vanished/call.py" in line for line in stale) 138 - assert tracked == [] 139 - 140 - 141 - def test_subprocess_reports_new_violations(tmp_path: Path) -> None: 142 - root = tmp_path / "bad" 143 - _write_file( 144 - root, 145 - "solstone/apps/badapp/call.py", 146 - "from solstone.think.utils import get_journal\n\n" 147 - "def main():\n" 148 - " return get_journal()\n", 149 - ) 150 - 151 - result = _run(root) 152 - 153 - assert result.returncode == 1, result.stdout + result.stderr 154 - assert "call-http-only: NEW violations:" in result.stderr 155 - assert "solstone/apps/badapp/call.py" in result.stderr 156 - 157 - 158 - def test_excluded_file_with_violations_is_not_flagged( 159 - tmp_path: Path, monkeypatch: pytest.MonkeyPatch 160 - ) -> None: 161 - root = tmp_path / "excluded" 162 - # EXCLUDED_FILES is currently empty (every journal-data call.py is a pure 163 - # Convey HTTP client), so inject a synthetic exclusion to exercise the 164 - # skip-an-excluded-file mechanism itself. 165 - excluded_rel = "solstone/apps/excluded_app/call.py" 166 - monkeypatch.setattr(ccho, "EXCLUDED_FILES", frozenset({excluded_rel})) 167 - assert excluded_rel in ccho.EXCLUDED_FILES 168 - _write_file( 169 - root, 170 - excluded_rel, 171 - "from solstone.think.utils import get_journal\n\n" 172 - "def f():\n" 173 - " open('x')\n" 174 - " return get_journal()\n", 175 - ) 176 - 177 - # Excluded file is skipped by discover_modules, so its import+fs violations 178 - # never reach `live`; with an empty allowlist nothing is over/stale/tracked. 179 - over, stale, tracked = ccho.evaluate(root, {}) 180 - assert over == [] 181 - assert stale == [] 182 - assert tracked == []
+6 -6
tests/test_check_rust_release_manifest.py
··· 514 514 ) 515 515 516 516 517 - def test_release_dir_accepts_exact_16_without_models(tmp_path: Path) -> None: 517 + def test_release_dir_accepts_exact_15_without_models(tmp_path: Path) -> None: 518 518 release_dir = _candidate(tmp_path) 519 519 520 - assert len(list(release_dir.iterdir())) == 16 520 + assert len(list(release_dir.iterdir())) == 15 521 521 assert ( 522 522 checker.validate_release_dir(release_dir, expected_source_commit=VALID_COMMIT) 523 523 == [] 524 524 ) 525 525 526 526 527 - def test_release_dir_accepts_exact_18_with_models(tmp_path: Path) -> None: 527 + def test_release_dir_accepts_exact_17_with_models(tmp_path: Path) -> None: 528 528 release_dir = _candidate(tmp_path, include_models=True) 529 529 530 - assert len(list(release_dir.iterdir())) == 18 530 + assert len(list(release_dir.iterdir())) == 17 531 531 assert ( 532 532 checker.validate_release_dir(release_dir, expected_source_commit=VALID_COMMIT) 533 533 == [] ··· 573 573 release_dir, expected_source_commit=VALID_COMMIT 574 574 ) 575 575 576 - _assert_error(failures, "16-file candidate contains models archive leftover") 576 + _assert_error(failures, "15-file candidate contains models archive leftover") 577 577 578 578 579 579 def test_release_dir_rejects_unknown_missing_extra_assets_and_case_collision( ··· 1558 1558 assert failures == [] 1559 1559 assert ready.is_dir() 1560 1560 assert not (tmp_path / "ready.staging").exists() 1561 - assert len(list(ready.iterdir())) == 16 1561 + assert len(list(ready.iterdir())) == 15 1562 1562 assert ( 1563 1563 checker.validate_release_dir(ready, expected_source_commit=VALID_COMMIT) == [] 1564 1564 )
-100
tests/test_check_tools_http_only.py
··· 1 - # SPDX-License-Identifier: AGPL-3.0-only 2 - # Copyright (c) 2026 sol pbc 3 - 4 - """Self-test for scripts/check_tools_http_only.py.""" 5 - 6 - from __future__ import annotations 7 - 8 - import importlib.util 9 - import subprocess 10 - import sys 11 - from pathlib import Path 12 - 13 - import pytest 14 - 15 - REPO_ROOT = Path(__file__).resolve().parents[1] 16 - SCRIPT = REPO_ROOT / "scripts" / "check_tools_http_only.py" 17 - 18 - 19 - def _load_checker(): 20 - spec = importlib.util.spec_from_file_location("check_tools_http_only", SCRIPT) 21 - assert spec and spec.loader 22 - module = importlib.util.module_from_spec(spec) 23 - spec.loader.exec_module(module) 24 - return module 25 - 26 - 27 - ctho = _load_checker() 28 - 29 - 30 - def _write_file(root: Path, rel: str, content: str) -> None: 31 - path = root / rel 32 - path.parent.mkdir(parents=True, exist_ok=True) 33 - path.write_text(content, encoding="utf-8") 34 - 35 - 36 - def _run(root: Path) -> subprocess.CompletedProcess: 37 - return subprocess.run( 38 - [sys.executable, str(SCRIPT), "--root", str(root)], 39 - capture_output=True, 40 - text=True, 41 - ) 42 - 43 - 44 - @pytest.mark.parametrize( 45 - ("source", "kind"), 46 - [ 47 - ("from solstone.think.surfaces import health\n", "import"), 48 - ("from solstone.think.utils import get_journal\n", "import"), 49 - ("from solstone.think.utils import require_solstone\n", "import"), 50 - ("from solstone.think.journal_io import get_journal\n", "import"), 51 - ("def f():\n open('x')\n", "fs"), 52 - ("def f(p):\n p.mkdir(parents=True)\n", "fs"), 53 - ("def f(p):\n p.write_text('x')\n", "fs"), 54 - ("import os\n\ndef f(p):\n os.remove(p)\n", "fs"), 55 - ], 56 - ) 57 - def test_scan_source_flags_tools_http_only_violations(source: str, kind: str) -> None: 58 - findings = ctho.scan_source(source) 59 - assert [finding[1] for finding in findings] == [kind] 60 - 61 - 62 - @pytest.mark.parametrize( 63 - "source", 64 - [ 65 - "from solstone.think.convey_client import get_client\n", 66 - "from solstone.convey.reasons import ENTITY_NOT_FOUND\n", 67 - "from solstone.think.pipeline_health import summarize_pipeline_day\n", 68 - "from solstone.think import convey_client\n", 69 - "from solstone.convey import reasons\n", 70 - "from solstone.think import pipeline_health\n", 71 - "import typer\n", 72 - "import json\n", 73 - "from pathlib import Path\n", 74 - "def f(Path, x):\n return Path(x) / 'y'\n", 75 - "def f(p):\n return p.parent\n", 76 - "def f(p):\n return p.name\n", 77 - ], 78 - ) 79 - def test_scan_source_ignores_allowed_imports_and_path_algebra(source: str) -> None: 80 - assert ctho.scan_source(source) == [] 81 - 82 - 83 - def test_main_flags_fixed_targets(tmp_path: Path) -> None: 84 - root = tmp_path / "bad" 85 - _write_file( 86 - root, 87 - "solstone/think/tools/health.py", 88 - "from solstone.think.utils import get_journal\n\n" 89 - "def f():\n" 90 - " open('x')\n" 91 - " return get_journal()\n", 92 - ) 93 - 94 - result = _run(root) 95 - 96 - assert result.returncode == 1 97 - assert ( 98 - "solstone/think/tools/health.py:1: import solstone.think.utils" in result.stderr 99 - ) 100 - assert "solstone/think/tools/health.py:4: fs open" in result.stderr
+15
tests/test_check_wheel_contents.py
··· 374 374 ) 375 375 376 376 377 + def test_release_artifacts_exclude_core_unsupported_tombstone( 378 + tmp_path: Path, 379 + ) -> None: 380 + artifacts = checker.release_artifacts( 381 + tmp_path, 382 + release_scope="all-hosts", 383 + models_decision="skip", 384 + ) 385 + 386 + assert not any( 387 + path.name.startswith("solstone_core_unsupported_platform-") 388 + for path in artifacts 389 + ) 390 + 391 + 377 392 def test_core_sdist_validator_requires_rust_workspace_sources( 378 393 tmp_path: Path, 379 394 ) -> None:
+13
tests/test_native_sol_conformance.py
··· 7 7 from scripts.build_native_sol_inventory import AuthorityEntry 8 8 9 9 10 + def test_native_sol_conformance_rejects_empty_authority_scan() -> None: 11 + errors = conformance.check_conformance( 12 + authorities=[], 13 + route_map={}, 14 + document={"paths": {}}, 15 + ) 16 + 17 + assert "native sol conformance discovered zero authorities" in errors 18 + assert "native sol conformance discovered zero Flask routes" in errors 19 + assert "native sol conformance discovered zero OpenAPI operations" in errors 20 + assert "native sol conformance missing top-level import authority" in errors 21 + 22 + 10 23 def test_native_sol_conformance_self_test_detects_authority_route_mismatch() -> None: 11 24 authorities = [ 12 25 AuthorityEntry(
+1 -40
tests/test_release_candidate_driver.py
··· 89 89 for name in expected 90 90 if name.startswith("solstone_core-") and name.endswith(".tar.gz") 91 91 } 92 - if args == ( 93 - "uv", 94 - "build", 95 - driver.CORE_UNSUPPORTED_TOMBSTONE_DIR, 96 - "--sdist", 97 - "--out-dir", 98 - "dist", 99 - ): 100 - return { 101 - name 102 - for name in expected 103 - if name.startswith("solstone_core_unsupported_platform-") 104 - and name.endswith(".tar.gz") 105 - } 106 92 if len(args) == 4 and args[:3] == ("uv", "build", "--package"): 107 93 package = args[3] 108 94 prefix = f"{package.replace('-', '_')}-" ··· 1707 1693 expected_aarch64_env = _expected_scrubbed_env( 1708 1694 tmp_path, driver.CORE_AARCH64_MATURIN_ARGS 1709 1695 ) 1710 - expected_tombstone_env = { 1711 - **_expected_scrubbed_env(tmp_path, ""), 1712 - driver.CORE_UNSUPPORTED_TOMBSTONE_ALLOW_BUILD_ENV: "1", 1713 - } 1714 1696 core_sdist_path = f"dist/solstone_core-{checker._current_version()}.tar.gz" 1715 1697 assert calls == [ 1716 1698 ( ··· 1718 1700 _expected_scrubbed_env(tmp_path, ""), 1719 1701 ), 1720 1702 ( 1721 - ( 1722 - "uv", 1723 - "build", 1724 - driver.CORE_UNSUPPORTED_TOMBSTONE_DIR, 1725 - "--sdist", 1726 - "--out-dir", 1727 - "dist", 1728 - ), 1729 - expected_tombstone_env, 1730 - ), 1731 - ( 1732 1703 ("uv", "build", "--package", "solstone"), 1733 1704 expected_x86_env, 1734 1705 ), ··· 1757 1728 assert [ 1758 1729 env["MATURIN_PEP517_ARGS"] 1759 1730 for argv, env in calls 1760 - if argv[:2] == ("uv", "build") 1761 - and "--wheel" not in argv 1762 - and argv[2] != driver.CORE_UNSUPPORTED_TOMBSTONE_DIR 1731 + if argv[:2] == ("uv", "build") and "--wheel" not in argv 1763 1732 ] == [driver.CORE_X86_64_MATURIN_ARGS] * 3 + [""] 1764 1733 assert [env["MATURIN_PEP517_ARGS"] for argv, env in calls if "--wheel" in argv] == [ 1765 1734 driver.CORE_X86_64_MATURIN_ARGS, ··· 1815 1784 core_sdist_path = f"dist/solstone_core-{checker._current_version()}.tar.gz" 1816 1785 assert calls == [ 1817 1786 ("python3", "scripts/render_packaging.py", "--check"), 1818 - ( 1819 - "uv", 1820 - "build", 1821 - driver.CORE_UNSUPPORTED_TOMBSTONE_DIR, 1822 - "--sdist", 1823 - "--out-dir", 1824 - "dist", 1825 - ), 1826 1787 ("uv", "build", "--package", "solstone"), 1827 1788 ("uv", "build", "--package", "solstone-journal"), 1828 1789 ("uv", "build", "--package", "solstone-journal-cuda"),
+60
tests/test_release_publish.py
··· 239 239 return {(project.project, project.version): None for project in projects} 240 240 241 241 242 + def _partially_published_base_snapshot( 243 + projects: Sequence[publisher.ProjectExpectation], 244 + ) -> Mapping[tuple[str, str], Mapping[str, Any] | None]: 245 + snapshot: dict[tuple[str, str], Mapping[str, Any] | None] = dict( 246 + _empty_snapshot(projects) 247 + ) 248 + first_project = projects[0] 249 + snapshot[(first_project.project, first_project.version)] = _full_snapshot( 250 + [first_project] 251 + )[(first_project.project, first_project.version)] 252 + return snapshot 253 + 254 + 242 255 def _divergent_snapshot( 243 256 projects: Sequence[publisher.ProjectExpectation], 244 257 ) -> Mapping[tuple[str, str], Mapping[str, Any] | None]: ··· 576 589 assert result.witness_status.state == "created" 577 590 578 591 592 + def test_production_accepts_published_tombstone_with_empty_base_index( 593 + tmp_path: Path, monkeypatch: pytest.MonkeyPatch 594 + ) -> None: 595 + report = _candidate(tmp_path) 596 + _patch_recover(monkeypatch, report, rehash_payloads=True) 597 + calls: list[str] = [] 598 + index = RecordingIndex(calls, [_empty_snapshot, _full_snapshot]) 599 + 600 + result = _run_publish( 601 + _config(tmp_path, report, mode="production"), 602 + calls=calls, 603 + index=index, 604 + git_runner=RecordingGit(calls), 605 + gh_runner=_gh_runner(calls), 606 + ) 607 + 608 + queried_projects = {project for snapshot in index.projects for project in snapshot} 609 + assert ( 610 + manifest.CORE_UNSUPPORTED_TOMBSTONE_PROJECT, 611 + report.version, 612 + ) not in queried_projects 613 + assert not any( 614 + name.startswith("solstone_core_unsupported_platform-") 615 + for name in publisher.expected_package_names(include_models=False) 616 + ) 617 + assert result.upload_state == "uploaded" 618 + 619 + 579 620 def test_upload_seam_receives_ledger_pypi_set_with_matching_digests( 580 621 tmp_path: Path, monkeypatch: pytest.MonkeyPatch 581 622 ) -> None: ··· 633 674 assert result.upload_state == "uploaded" 634 675 assert set(ledger_uploads) == expected_uploads 635 676 assert {path.name for path in captured_paths} == expected_uploads 677 + assert not any( 678 + path.name.startswith("solstone_core_unsupported_platform-") 679 + for path in captured_paths 680 + ) 636 681 for path in captured_paths: 637 682 assert ( 638 683 hashlib.sha256(path.read_bytes()).hexdigest() == ledger_uploads[path.name] ··· 789 834 _patch_recover(monkeypatch, report, rehash_payloads=True) 790 835 calls: list[str] = [] 791 836 index = RecordingIndex(calls, [_divergent_snapshot]) 837 + 838 + with pytest.raises(DriverError) as excinfo: 839 + _run_publish(_config(tmp_path, report), calls=calls, index=index) 840 + 841 + assert _first_failure(excinfo.value) == "release publish package index is divergent" 842 + assert calls == ["index"] 843 + 844 + 845 + def test_partially_published_base_index_refuses_before_upload( 846 + tmp_path: Path, monkeypatch: pytest.MonkeyPatch 847 + ) -> None: 848 + report = _candidate(tmp_path) 849 + _patch_recover(monkeypatch, report, rehash_payloads=True) 850 + calls: list[str] = [] 851 + index = RecordingIndex(calls, [_partially_published_base_snapshot]) 792 852 793 853 with pytest.raises(DriverError) as excinfo: 794 854 _run_publish(_config(tmp_path, report), calls=calls, index=index)