Skip to content

release: promote develop → main for v0.37.1 (STX-TQ false-green fix)#437

Merged
ywatanabe1989 merged 71 commits into
mainfrom
develop
Jul 24, 2026
Merged

release: promote develop → main for v0.37.1 (STX-TQ false-green fix)#437
ywatanabe1989 merged 71 commits into
mainfrom
develop

Conversation

@ywatanabe1989

Copy link
Copy Markdown
Collaborator

Pilot run of the tag-driven release flow the operator approved (2026-07-24: 「まず1個丁寧に、良さそうだったら手を離す」). scitex-dev is the pilot repo: it is the package that must ship the fix, and its blast radius is the smallest.

Why now

#436 fixed a FALSE-GREEN in the shared quality audit: STX-TQ (PA-307) located the tests/ tree from the import-resolved package (an installed wheel has no tests/), found 0 candidates, and returned a SILENT 0 that read as a clean pass. figrecipe caught it with a positive control.

Fleet exposure (measured today, this checkout)

  • 55 workflow files across the fleet run ecosystem audit-all.
  • 21 pass --path; 34 files across 27 repos do not — the same shape that let TQ silently skip. scitex-dev itself is one of them.
  • 36 files pass --no-version-check; 9 pass --new-only (all 9 pass both) — the constitution's named anti-pattern.

Caveat stated honestly: missing --path is NECESSARY but not SUFFICIENT for a skip — if CI installs the package editable, the import-resolved path lands back in the repo and TQ does run. Per-repo positive controls are the definitive test and are the follow-up.

Why this release fixes all of them

resolve_target_tree resolves explicit(--path) → cwd checkout → registry. #436 threads that resolved root into the TQ check, so with the fixed release installed, a CI job running in its own checkout gets the right tree WITHOUT needing --path. Shipping this is what closes the fleet exposure.

Promotion of 71 commits accumulated on develop. Merges on real CI green only.

ywatanabe1989 and others added 30 commits July 21, 2026 02:58
…-behind + suppress hint) (#389)

* rescue: pre-stop autosave scitex-dev@20260720T164109Z

* fix(cli): fire version-staleness guard on every invocation (warn-when-behind + suppress hint)

The staleness check existed but was wired to NO callsite — dead code. This
makes it actually fire, matching the operator's spec.

- Wire emit_if_drift() into the root `main` group callback: every scitex-dev
  CLI invocation now checks whether the install is BEHIND its remote (editable
  HEAD behind origin/<branch>) or the latest published wheel (local version
  cache), and warns — or hard-fails when the `staleness_severity` knob is
  `error` — with a copy-paste-safe `git -C <abs-path> pull --ff-only` /
  `pip install -U` remedy. Being AHEAD of a tag/remote is never flagged (the
  original false positive). Emitted via scitex-logging (WARN:/ERRO: prefix).
- Append a suppression hint to every emitted line (operator request):
  SCITEX_DEV_NO_DRIFT_WARN=1 kill-switch + the staleness_severity knob.
- Extract the recursive help-rendering helpers (_command_to_dict,
  _show_recursive_help) to _cli/_root_help.py to keep _root.py under the
  512-line limit; re-exported from _root for API/import compatibility.
- Fail-safe: the error-severity abort (SystemExit) propagates on purpose; any
  other internal fault is swallowed so the guard can never break the host CLI.
  Placed after --version / --help-recursive so a version/help query never
  trips it; shell-completion + repeat invocations are guarded inside.

---------

Co-authored-by: Yusuke Watanabe <ywata1989@gmail.com>
… (PS-220) (#390)

New project-audit check PS-220: AST-scans the shippable src/<pkg>/**.py
tree for builtin print(...) calls and flags each one. SciTeX code must
emit messages through scitex-logging (aligned WARN:/ERRO:/SUCC: prefixes),
never print.

- _check_no_print.py: check_ps220_no_print + co-located PRINT_FORBIDDEN_RULES
  (mirrors the _check_no_url_deps PS-216 contract). AST walk of ast.Call
  whose func is the bare name 'print'; attribute forms (x.print) not matched.
- Scope: src/ only; tests/scripts/examples/docs subtrees excluded. Inline
  '# noqa' opts a line out.
- Severity W during ecosystem adoption (promotes to E once compliant).
- Registered in _registry.py; wired into _audit.py after PS-216.
- 7 tests mirroring test__check_no_url_deps (fires / clean / noqa / attr /
  multi-count / excluded-subtrees). Full run: 147 passed.
…backends (#388)

Faithfully distills the phase-1 proposal from the central-compute
dispatcher vision memo (operator TG1503-1525). Policy layer over
EXISTING executors (Snakemake/Parsl/Slurm), not a new executor:
scitex-dev = policy+auditor, scitex-hpc = backend adapters.

Locks 4 interfaces now (container contract, resource-spec,
ExecutionBackend submit/status/logs/cancel/collect, receipt schema);
ships local + slurm backends only; defers cloud/routing/sharding/cost/
data-governance behind the interfaces. Includes the -n auto ->
allocated-cpus (SLURM_CPUS_PER_TASK) fix and the scitex-hpc coordination ask.
…s (anti wrong-tree footgun) (#392)

audit-project resolves the tree it grades from several sources (explicit
--path, the CWD repo-root, an editable install, or a ~/proj/<pkg> guess).
When resolution lands on a tree that is NOT the commit under test, a green
"no violations" is a confident lie about source it never read — and a
clean result is exactly the one nobody double-checks.

Before running any checks, announce the ABSOLUTE resolved path plus the
branch and short HEAD sha of the tree about to be graded:

  human rail  — a scitex-logging INFO line
                "<dist>: auditing <abs-path> (branch <b>, HEAD <sha>)"
  --json rail — new resolved_path / branch / head fields in the payload
                (present on the located AND the cannot-locate payloads).

Resolution is fail-safe: a non-git tree (sdist extract, tarball) still
surfaces its absolute path; branch/head fall back to None ("?" in the
human line). The resolver never raises and never blocks the audit.

New helper module _resolved_tree.py keeps _audit.py within the line
budget; 12 tests cover the pure resolver (None repo, non-git, real
checkout), both --json fields, and the human banner.
Root cause: STX-FM001 is a *plugin-provided* rule object. FMChecker._add
returns silently when `rules.FM001 is None` — which happens whenever the
linter interpreter's figrecipe provides no FM001 rule (absent, or a
version whose plugin predates/renames it). `@requires_figrecipe` only
proves figrecipe is IMPORTABLE, not that it registers FM001, so on the
shared self-hosted runner (figrecipe importable but rule-skewed) FM001
never fires and these 6 tests fail with `_sev_of(...) == None`.

This is the exact figrecipe detection-skew the file already tolerates for
its plugin-path siblings via `_skip_if_not_emitted` (added after the
v0.23.0 release-blocking incident, #277/#25). The FM001 tests simply
never got that guard. Apply it to all 6 FM001-presence-requiring tests.

A real regression is still caught: if FM001 FIRES but at the wrong
severity, the rule is present so `_skip_if_not_emitted` does not skip and
the severity assert fails as before. Only genuine "rule absent" skew now
skips instead of hard-failing.

Unblocks the pytest-matrix failures on #391 and #393; de-flakes develop.
…order tests (supersedes #391, #393) (#395)

* fix(audit): consolidate PS-221 [all]-closure rule + de-flake 4 xdist-order tests (supersedes #391, #393)

Brings the UNION of PRs #391 and #393 onto one branch, plus root-cause
fixes for the 5 deterministic matrix failures they shared.

Consolidated from the two PRs:
- #391: the PS-221 "[all]-closure on public extras" audit rule
  (_check_extras_all_closure.py + _audit.py/_registry.py wiring + test).
- #393: `scitex-dev[sync]` added to `[all]` in pyproject.toml, and the
  drift-emission guard in _cli/_root.py that suppresses the boot-path
  staleness line under PYTEST_CURRENT_TEST (it fired on every CLI
  invocation and polluted unrelated tests' captured output).

Root-cause fixes for the 5 CI failures (identical across py3.11/12/13):

1. test_audit.py::test_audit_all_clean - the new PS-221 rule flagged
   scitex-dev's OWN pyproject because the `sync` extra (scitex-ssh,
   scitex-scholar) was missing from `[all]`. Fixed by adding
   `scitex-dev[sync]` to `[all]` (verified: PS-221 now reports 0
   violations on this tree), which both validates the rule and makes the
   package self-compliant.

2-3. test__resolved_tree.py::test_human_output_{surfaces_resolved_path,
   banner_says_auditing} - the INFO banner is emitted through
   scitex_logging.getLogger("scitex_dev.audit"), whose StreamHandler binds
   the REAL sys.stderr at import time; contextlib.redirect_stderr (swapped
   afterwards) captured nothing whenever another test imported
   scitex-logging first (xdist-order flake). Made the test hermetic:
   _human_output now attaches its own handler to that exact logger, so the
   banner lands in the buffer deterministically regardless of global
   handler state. Real assertion (path in banner) kept.

4-5. test_check_editable_drift.py::test_{warn,error}_output_carries_
   severity_prefix - same root cause on the "scitex_dev" logger via
   _log_stale. Replaced redirect_stderr with a dedicated
   _capture_drift_log handler on that logger (levelname carries the
   WARN/ERRO prefix) and pass the buffer as the plain fallback stream.
   Real assertion (WARN:/ERRO: prefix present) kept; prod code unchanged.

Control: PR #393's matrix fails exactly on 2-5 (env-common) and NOT on
test_audit_all_clean (which is #391-rule-specific), confirming these
fixes cover both PRs.

* fix(test): pin audit-all to the checkout under test via --path (test_audit_all_clean)

The consolidated PS-221 rule flagged violations in CI even after fixing
this repo's own `[all]` closure, because `test_audit_all_clean` called
`audit_all_for_package('scitex-dev')` WITHOUT a `path=`. Without it,
audit-all falls back to a resolve-by-name `~/proj/<pkg>` development
guess; on the shared self-hosted (Spartan pooled) runner that guess
resolves the operator's `/home/ywatanabe/proj/scitex-dev` develop
checkout (HEAD cc599c9) — a DIFFERENT tree and commit than the one under
test. So the gate graded stale source whose pyproject still lacked the
`scitex-dev[sync]`-in-`[all]` fix, and reported PS-221 §3 for scitex-ssh
/ scitex-scholar. PR #392's resolved-tree banner surfaced exactly this:
`INFO: scitex-dev: auditing /home/ywatanabe/proj/scitex-dev (branch
develop, HEAD cc599c9)`.

Fix per the documented idiom in `audit_all_for_package`: anchor on the
test file's own checkout (`Path(__file__).resolve().parents[2]`, since
the file lives at <root>/tests/develop/test_audit.py), threaded through
as `--path` so every source-reading sub-auditor grades the commit under
test — which carries the `[all]`-closure fix and is PS-221-clean.

* fix(test): rename mismatched test files to double-underscore (PS-204/PS-205)

With the audit now correctly targeting the checkout under test (previous
commit's --path fix), audit-all surfaced 3 pre-existing error-severity
violations that the wrong-tree resolution had been masking on develop
(introduced by #374 and #385, not by this consolidation):

  [E] PS-205 test-name-prefix-mismatch — private `_knob_commands.py`
      must be tested by `test__knob_commands.py` (double underscore).
  [E] PS-205 test-name-prefix-mismatch — private
      `_test_execution_plugin.py` must be tested by
      `test__test_execution_plugin.py`.
  [E] PS-204 orphan-test-file — `test_knob_commands.py` mapped to a
      non-existent `knob_commands.py` (the src is private
      `_knob_commands.py`).

Both PS-204 and PS-205 on the knob file are resolved by the single
rename to the double-underscore form; the plugin file's PS-205 by its
rename. Content is unchanged (both already no-mocks + Arrange/Act/Assert,
so no test-quality rule fires), and `test__*.py` still matches pytest's
`test_*.py` collection glob. The 6 remaining §12/§13 CLI-naming lines are
WARN-severity and do not fail the gate.
…install-integrity primitive (#396)

Operator directive 2026-07-21: when a SciTeX tool is invoked, a
latest-version check must fire — at ERROR severity by default
("普通は warning ですが、私たちはエラーを選びます"). scitex-dev owns the
primitive; consumers (scitex-cards CLI entry + MCP startup) wire it
fail-open via `try: from scitex_dev.staleness import ensure_current /
except ImportError: pass`.

Two halves in ensure_current(dist_name, *, severity=None):

1. INTEGRITY — always runs, purely local, no network:
   - multiple dist-info dirs claiming the dist in one site dir →
     "ambiguous metadata";
   - any RECORD-listed file missing on disk → "partial install".
   RECORD is parsed directly, NOT via Distribution.files: Python 3.12
   silently filters missing files out of .files, which would hide the
   exact corruption this gate exists to catch.
   Motivating incident (2026-07-21): a venv carried 0.16.0 + 0.17.4
   dist-infos with 0.17.4 metadata over a 0.16-era file set;
   _store_backend.py was in RECORD but absent on disk — every version
   probe lied. Editable installs (PEP 610 editable flag) skip this
   half; freshness governs them.

2. FRESHNESS — fail-safe (no cache / offline / any error → PASS):
   - wheel: installed vs cached-latest (per-dist cache file; PyPI is
     truth; opportunistic detached background refresh with TTL —
     never a blocking live PyPI call on the invocation path);
   - editable: HEAD behind tracking remote (reuses the drift git
     machinery).

Severity ladder: explicit arg > $SCITEX_DEV_CURRENCY_SEVERITY >
currency_severity knob (config.yaml → knob-state.json) > default
error. error raises StalenessError carrying the exact remedy
(pip install -U <dist> / git -C <path> pull --ff-only); warn logs via
scitex-logging; silent no-ops. SCITEX_DEV_NO_CURRENCY_GATE=1 bypasses
both halves but emits a loud WARN — an exercised bypass is never
silent.

Shared pieces refactored out of check_editable_drift (no duplication):
_editable_dir_from_meta extraction, _compute_drift/_resolve_severity
parameterization. scitex-dev's own CLI now runs an integrity-only
self-check at warn severity next to the existing drift emission.

Tests: real dist-info/RECORD fixtures in a tmp site dir (injected via
the _search_paths seam), real git repos, no mocks; log capture via a
handler on the scitex_dev logger (#395 idiom).
…rrent checkout > registry (#397)

Operator directive (2026-07-21): audits must land where the caller
aimed (監査が worktrees などでもうまく狙ったところにいくように).
Auditing the develop checkout is legitimate in itself; the defect is
resolution silently landing on a tree the caller didn't intend.

Incident (same day): tests/develop/test_audit.py ran
audit_all_for_package('scitex-dev') on a CI runner and the per-target
audit CLIs resolved the operator's ~/proj/scitex-dev develop checkout
via the ecosystem registry's local_path — grading a different tree
than the CI checkout the test lived in. PR #395 pinned that one test
with an explicit path; this fixes the resolution itself.

Precedence, applied by audit-project / audit-django / audit-python-apis
(and therefore ecosystem audit-all, which shells out to them with the
caller's cwd):

  a. explicit --path/--repo — always wins (unchanged)
  b. the CURRENT checkout — git toplevel of the cwd (works inside
     linked worktrees) when its pyproject [project].name
     PEP-503-matches the requested distribution
  c. the registry's local_path — historical behaviour, still right for
     cross-package audits run from anywhere

Changes:
- new shared resolver _cli/audit/_target_tree.py (resolve_target_tree,
  cwd_checkout_of, PEP-503 normalize/match); the three per-target cmds
  use it instead of their registry-first block
- _project/_discovery: PEP-503 name matching in _looks_like_checkout_of;
  _resolve_repo_root_with_rule reports WHICH rule resolved
  (explicit/cwd/import/proj-guess)
- the #392 resolved-tree banner now names the winning rule (via cwd /
  via registry / ...); --json payloads carry resolved_via
- line-limit refactor: _project/_audit.py check sequence extracted
  verbatim into _project/_run_checks.py (pure move, no behaviour change)
- tests (no mocks): real tmp git repos + git-worktree-add linked
  worktrees covering explicit-wins, cwd main checkout, cwd linked
  worktree outranking an injected registry mapping, registry fallback,
  PEP-503 variants, and the banner/payload rule token
#400)

Root cause of fleet CI drift (inventory 2026-07-21): scitex-dev shipped
TWO competing canonical mechanisms — (A) the consolidated
pr-ci.yml.tmpl/release-ci.yml.tmpl pair rendered by `ecosystem
ci-template apply`, and (B) `ci runner register` copying its own in-SIF
ci.yml.template — stranding the fleet across 6 workflow generations.

Operator decision (TG, 2026-07-21): converge on ONE shape — the thin
REUSABLE caller `ci.yml` delegating every job body to
scitex-ai/.github@main (a shared workflow cannot drift per-repo), runner
selection via the CI_RUNS_ON Actions Variable with self-hosted default
["self-hosted","Linux","X64","scitex-ci"], never ubuntu-latest (PS-169).

What won / what was deleted:
- WINNER: `scitex_dev._ecosystem.ci_template` now renders ONE
  ci.yml.tmpl (caller jobs pytest-matrix / import-smoke / quality-audit
  / rtd-sphinx-build, `secrets: inherit`, PR+push+nightly+dispatch
  triggers, PR-only cancel-in-progress). pr-ci.yml.tmpl and
  release-ci.yml.tmpl are DELETED; apply now also deletes superseded
  pr-ci.yml/release-ci.yml and newb-docs* (operator-ordered fleet-wide
  removal) in target repos. cla.yml, auto-merge-to-develop.*,
  pypi-publish-*, rtd-sphinx-* stay preserved exactly as before.
- LOSER→ALIAS: `ci runner register` no longer carries a template body;
  it delegates workflow deployment to ci_template.apply and only adds
  the CI_RUNS_ON Actions Variable (canonical default). Its
  ci.yml.template is DELETED. SCITEX_CI_APPTAINER/SIF vars are no
  longer set (they configured the deleted in-SIF body only).
- Branch-protection gate now expects the workflow_call context shape
  "<caller-job-id> / <reusable job name>"; tests pin caller-job ids
  against the gate's expected prefixes so they cannot drift apart.

Org-side gaps (NOT changed here; wave-2 work in scitex-ai/.github):
- Org reusable workflows pin runs-on [self-hosted, Linux, X64,
  spartan-cpu] and declare `workflow_call: {}` (no inputs); they must
  adopt the fromJSON(vars.CI_RUNS_ON || default) expression (vars
  resolve from the caller repo) for per-repo runner selection to bind.
- quality-audit.yml runs a FULL audit (no --new-only PR-incremental
  gate); pytest-matrix pins its own python matrix; no dep-hygiene /
  CLI-help smoke equivalents.

Tests pin: caller shape (pure uses+secrets jobs), vars.CI_RUNS_ON +
self-hosted default present, zero ubuntu-latest in emitted bodies,
newb-docs + superseded-file cleanup, exactly one vendored template, and
no workflow YAML body left under ci/runner/templates.
…ai (#401)

This repo moved to the scitex-ai GitHub org, but its own docs, badges,
package metadata and sphinx html_context still pointed at the old
ywatanabe1989 org. Rewrite SELF-references only.

Cross-repo links to packages still living under ywatanabe1989 are left
untouched, as are historical records (ADRs, frozen release notes).
… store (#399)

* fix(test): session live-store shield — no test may see the fleet card store

Incident 2026-07-21 (third store wipe): the fleet's canonical
scitex-cards store (~/.scitex/cards/cards.db, 2170 cards) was
mass-DELETEd down to 18 rows. Culprit class: full-suite pytest runs in
worktrees executing with the session's AMBIENT environment, where
SCITEX_CARDS_DB / SCITEX_TODO_* point at the live store — tests that
exercise store-writing code paths then hit production data. The window
overlaps this repo's full-suite runs; scitex-cards is adding a
write-side shrink guard on their side, and this is scitex-dev's own
mechanical barrier.

A session-scoped autouse fixture (_shield_live_card_store in
tests/conftest.py) now, before any test runs:

- FORCE-SETS every store-steering env var — both SCITEX_CARDS_* and
  SCITEX_TODO_* twins of DB / TASKS / TASKS_YAML / TASKS_YAML_SHARED /
  INBOX_DB / INDEX_PATH / CI_STATE / BOARD_PIDFILE, plus SCITEX_DIR —
  to per-session tmp paths (tasks.yaml seeded as an empty valid store);
- forces backend selectors local (STORE_BACKEND / INBOX_BACKEND =
  sqlite, DUAL_WRITE / STORE_GIT_AUTOCOMMIT = 0) and unsets the
  remote-hub endpoints (HUB_URL / HUB_TOKEN / HUB_TOKEN_FILE);
- asserts (defense-in-depth) that the store path scitex_cards would
  actually resolve is NOT any live path captured from the pre-shield
  environment — failing LOUD before a single test runs;
- restores the original env only at session teardown, never
  mid-session.

REPOINT, never merely unset: with these vars absent, scitex-cards (and
scitex-dev's own _task_harvest) fall back to the default $HOME-based
paths — i.e. unsetting routes straight back to the live store.
Explicit tmp paths cover both the env tier and the HOME-fallback tier
of every resolution chain. Both prefixes must be set because
scitex_cards._env_compat.mirror_env() copies CARDS_* onto TODO_* with
"new name wins" at import time.

tests/test_live_store_shield.py proves the barrier: env vars point
into tmp during tests, and the leak assertion fires on simulated
leaks built from a saved copy of the original env (the session is
never unshielded). Verified locally: 1470 passed / 27 skipped across
linter + staleness + audit + shield subsets with the shield active.

* fix(test): move live-store-shield test under tests/scitex_dev/ (PS-203)

* fix(test): move live-store-shield test to tests/develop/ (PS-204 — meta-test has no src mirror)
… truth (#402)

Verified all 68 ECOSYSTEM entries against GitHub with
'gh api repos/<owner>/<repo> --jq .full_name' (the API follows
renames/transfers, so the returned full_name is canonical even when
queried via the old path). Counts: 66 updated to scitex-ai/* (65
owner-only + scitex-todo -> scitex-ai/scitex-cards, an owner+name
change), 2 confirmed-unchanged untransferred GitHub archives
(scitex-audit, scitex-bridge — verification comments added), 0
unverifiable.

Mechanical split for the 512-line cap: the literal table moved to
_registry_data_1.py / _registry_data_2.py, concatenated
order-preservingly in _registry.py; public surface (ECOSYSTEM,
PackageInfo, __all__, _core re-export) unchanged and now pinned by
tests. scripts/quality/ecosystem_list.py (which regex-parses the
registry source) now reads the part files, with fallbacks for the
older layouts.

Audit footer: the escalation issues URL in _audit_disclaimer.py is now
derived from the registry's own scitex-dev entry (_issues_url())
instead of a second hardcode, so an org/repo rename cannot leave a
stale self-reference again.
…, phased adoption (#403)

ADR-0004: ONE declarative JobSpec for all job families (tests, CI,
experiments, agent/fleet jobs); INTENT (one-shot|periodic|daemon|reactive)
orthogonal to MECHANISM (local-exec|slurm|gh-runner|systemd/cron);
best-effort allocation onto the shared pool (org runners + Spartan
backfill, vars.CI_RUNS_ON as the CI-facing slice); ownership split
scitex-dev (primitives+aggregation) / scitex-hpc (Spartan mechanics) /
sac (container/daemon lifecycle) per ADR-0003 seams; fail-safe invocation
path vs fail-loud gates; phased adoption P1 tests (shipped #385) ->
P2 CI (#400 wave in flight) -> P3 fleet jobs (federation), each with
exit criteria. Synthesizes docs/design/execution-fabric-phase1.md (#388),
the #385 test-execution recipe, the #400 canonical CI caller + drift
inventory, and the JobSpec federation contract.

Claude-Session: https://claude.ai/code/session_01KEDFF532gvmbxk3jhsgG63
…ames (#404)

PS-221's suggestion text recommended making an extra internal by
renaming it with a leading underscore (`_dev`, `_ci`) — but PEP
508/685 forbids leading-underscore extra names: setuptools refuses to
build, hatchling rejects the metadata, pip/uv reject the request form.
Five repos broke on this advice in 2026-07 before reverting.

- Both PS-221 violation messages + the co-located rule text now give
  closure-by-inclusion as the fix (reference every public extra from
  `all` via self-refs, the shape scitex-dev/scholar/session/logging
  ship) and explicitly warn the underscore rename is invalid.
- Checker docstring: underscore skip documented as defensive-only,
  new Remedy section; behavior unchanged (all 10 tests pass as-is).
- general/05_development/11_dependency-tiers.md reconciled with the
  enforced rule: removed the stale '[dev] NOT included by [all]'
  claim; [dev] is a public extra closed into [all] via a self-ref
  (PS-221 cited); example toml updated to the shipped shape.
- Test comments updated to describe the underscore skip as defensive
  (no assertion changes; none asserted the old suggestion text).
…egistry checkout (#405)

* fix(audit): read §6 exemption from the AUDITED tree, not the runner registry checkout

On self-hosted CI runners the home often carries a ~/proj/<pkg> checkout
at the ecosystem registry local_path; _audited_repo_root preferred it, so
a PR declaring mcp_parity_exempt / mcp_tools_allowlist could never turn
its own quality-audit green (scitex-orochi #460) — the exemption was read
from the stale tree instead of the editable-installed PR checkout.

Reorder: import-derived root (the tree actually under audit) first;
registry local_path only decides for non-editable site-packages installs
where no audited tree exists on disk. Extracted the resolution pair to
_mcp_repo_root.py (line budget; names re-exported from _mcp_parity), with
the registry resolver injectable for the precedence tests.

* chore: drop stray executable bit on _mcp_repo_root.py
…h a structural carve-out (#406)

* feat(audit): enforce the no-bare-print mandate — PS-220 to error, with a structural carve-out

PS-220 already existed and was already wired, but it could not fail: it
shipped at severity "W" in its own rule tuple, and `audit_project` drives
the exit code off "E" findings only. The default severity floor is
"error", so its findings were not even PRINTED unless someone passed
`--severity warning`. It was dormant by SEVERITY, not by exemption.

- Promote PS-220 to "E" in its co-located rule tuple.
- Fix the severity-table trap: `_patch` (which applies
  `_SEVERITY_OVERRIDES` and `_SLUGS`) ran BEFORE the co-located/sidecar
  rule merges, so the override table silently ignored 31 rules. Adding an
  entry for any of them did nothing, with no error. Apply `_patch` after
  all merges instead. Verified no rule changes severity or slug today, so
  this is a pure safety fix.
- Add the bucket-five discriminator (`_print_discriminator.py`): stderr
  always fires (scitex-logging owns stderr), prose to stdout always
  fires, a serializer payload to stdout is spared, and anything
  undecidable fires. Machine-readable stdout is spared STRUCTURALLY
  because a logger would corrupt it — a ban without the carve-out gets
  worked around rather than followed.
- Replace the blanket `# noqa` hatch with per-site `audit.exemptions`
  entries carrying a MANDATORY reason; a blank reason is rejected and
  reported. The old hatch keeps working for one release at severity W.
- Scope the error severity by project type: E for ecosystem packages, W
  for research hybrids (the operator has not ruled on research trees),
  overridable via `audit.enforce-logging`.
- Fix `_src_files` matching the exclusion set against the ABSOLUTE path:
  a checkout under any dir named docs/tests/scripts/examples skipped
  every file and reported a tree it never looked at.

* fix(audit): correct a PS-220 false positive CI caught, and declare scitex-dev's own migration state

CI (run 29906963643) failed test_audit_all_clean on all three Python legs —
not a py3.11-specific issue. Two distinct causes, both mine.

1. A REAL FALSE POSITIVE in the discriminator. `_assignments_to` collected
   EVERY assignment to a `file=` name in the enclosing function, and
   `_resolve_name_stream` is fail-closed on stderr. In the real branch chain
   at `_cli/_utils.py:35-51`, `out` is rebound three times and only the LAST
   branch is stderr — so that stderr poisoned the two stdout payload writers
   above it, and genuine `--json` sites were reported as "writes to stderr".

   Fixed by resolving to the NEAREST PRECEDING assignment (a standard
   reaching-definition approximation). Lines 37/42/44 are now correctly
   spared; 47/49/51 still correctly fire. scitex-dev's count: 135 -> 132.

   I had claimed these sites "verified spare" in the PR body. That claim was
   not earned: I tested synthetic snippets reproducing their SHAPE and never
   ran the discriminator against the real files, then reported the intended
   conclusion as a measured one. The bug lived exactly in the difference
   between the fixture (one assignment) and the file (three). Recorded in the
   survey as a seventh instance of "unearned green".

2. PS-204 orphan test. `test__ps220_exit_code.py` had no src counterpart.
   PS-204 permits a descriptor suffix (`test__foo_real.py` -> `_foo.py`),
   which is why `test__registry_severity_overrides.py` passed via `_registry`.
   Renamed to `test__check_no_print_exit_code.py`.

3. The remaining 132 findings are GENUINE — the rule firing on its own author.
   They are not migrated here and not given per-site exemptions (132 reasons
   all reading "not migrated yet" is the reasonless park the design rejects).
   Instead scitex-dev declares `audit.enforce-logging: warning` in its own
   config, because converting print -> scitex-logging moves output from STDOUT
   to STDERR and scitex-dev is a CLI the fleet parses.

   Severity stays E for every other package. `warning`, never `off` — all 132
   stay visible on every run. Retired by scitex-todo card
   `ps220-selfmigrate-scitex-dev`, named in the config comment.

Tests: 1223 passed in tests/scitex_dev/_cli/audit (was 1217; +6 branch-chain
regression tests pinning each destination in the three-branch shape).
Two audit gates ran different things and disagreed. The per-repo pytest
wrapper honoured its `skip_rules=` kwarg; the org reusable workflow
called `scitex-dev ecosystem audit-all` directly and bypassed it. So
develop could be green while unified CI was red on identical code
(measured in scitex-hub PR #433: 150 violations masked by the wrapper,
zero masked by the workflow).

Deferring a rule to a named migration campaign is the sanctioned
exemption; forcing every in-flight campaign into one commit is not a
scheduling policy anyone chose. So audit-all now reads the repo's
deferrals itself — and never honours one silently.

New `audit.skip-rules` in <repo>/.scitex/dev/config.yaml:

    audit:
      skip-rules:
        PS-139: "TQ-migration campaign — tracked in scitex-hub#412"

- A rationale is MANDATORY. A bare list (`- PS-139`) is rejected with
  exit 2 naming the entry; a deferral that cannot say why is the
  abandonment the mechanism exists to catch.
- A MASKED INVENTORY is ALWAYS emitted — total, rule ids, per-rule
  masked count, and each written reason — in normal output, not behind
  a verbosity flag, and in the --json payload too.
- The summary states BOTH numbers, for single- and multi-package runs.
  "0 errors" while 150 are masked is a lie of omission.
- The exit code stays driven by unmasked findings; one declared
  deferral never blanket-silences anything else.
- A declared rule matching zero violations is flagged removable.

Distinct from legacy `audit.skip` (bare codes, audit-project-only,
silent) and from `audit.capabilities`, both untouched.

Also fixes the summary defect sac reported: every auditor's CLEAN line
named its category ("no project-structure violations") but every
FAILURE line did not, so audit-all printed named SUCC lines then a bare
`<pkg> (<path>): 3 error(s)`. sac PRs #813/#814 both misread a real
violation as a broken gate because of it. Fixed across all five
categories. Warning-level findings are now reported separately from
errors rather than folded into the error count — the same imprecision
one level up.

`_cli/audit/_skills/_audit.py` (631 lines, over budget) split into
_registry/_violation/_discovery/_checks + facade, mirroring the sibling
_project/ package. Pure extraction.

Mutation-proven (break it, watch it go red, restore, watch it pass):
all 7 behaviours PROVEN. 1504 audit+ecosystem tests pass; audit-all on
this worktree exits 0 with every line naming its category.
#409)

`scitex-ui --version` and `scitex-app --version` printed
`scitex-dev <scitex-dev's version>` — 36 downstream packages reported
another package's identity.

`scitex_dev/_cli/__init__.py` fast-paths a bare `--version` to skip the
expensive `._root` import. The predicate matched on `sys.argv[1:]` alone,
never checking WHO was asking. Because `scitex_dev._cli` is a shared
primitive that downstream CLIs import (`._completion`, `._root`), the
block fired during `import` in any process launched as
`<other-cli> --version`: it printed scitex-dev's name and version and
raised SystemExit(0) before the downstream CLI's own `main()` ran. Exit
code 0, so nothing looked broken; the downstream packages' own `--version`
wiring was correct all along.

The fast path now additionally requires `sys.argv[0]` to identify
scitex-dev's own console script (or `python -m scitex_dev`). An
unrecognised argv[0] declines the optimisation and falls through to the
real Click group — correct, just slower. No path falls back to printing
`scitex-dev`; that fallback was the defect.

A library must never read sys.argv or exit at import time.

Mutation-proven: dropping the argv[0] gate, blanking the entry-point
basenames, and making an unknown argv[0] silently claim scitex-dev each
turn the suite red; restoring returns 14 passed.
…o error (#410)

0.35.0 (#406) promoted PS-220 to ERROR ecosystem-wide. The measured blast
radius — 44 repos newly FAILING on 1856 findings, top-5 repos carrying 64%
of them (GITIGNORED/ps220-blast-radius-20260722.md) — led the operator to
restage the rollout (Telegram 1691/1692):

    「print に関しては順次やっていきましょうか。
      とりあえず warning で、移行できたものから red で」

Staging, not retreat: every finding stays visible on every audit run, and
nothing about what the rule DETECTS changed.

* Default severity back to W for every project type
  (_check_no_print._DEFAULT_SEVERITY, the rule tuple, and
  resolve_ps220_severity).

* Per-package opt-in via the EXISTING audit.enforce-logging knob, extended
  to a mapping that carries a MANDATORY written reason:

      audit:
        enforce-logging:
          level: error
          reason: "print migration complete (PR #412)"

  `error` and `off` deviate from the default and must state why; `warning`
  is accepted bare because it IS the default and changes nothing. A
  missing/whitespace-only reason is a HARD CONFIG ERROR: the declaration
  does NOT take effect (falls back to W) AND the rejection is reported at
  E, so a package can never believe it is gated — or silenced — when it is
  not. Bare `error`/`off` shorthands (and their YAML 1.1 boolean spellings)
  are rejected on the same grounds; unrecognised values now reject loudly
  instead of falling back silently. Parsing lives in the new
  _config/_enforce_logging.py, keeping _loader.py under the line cap.

* Config errors are NOT staged. A rejected exemption entry and a rejected
  enforce-logging declaration are both reported at E regardless of the
  project's PS-220 severity. Staging covers migration debt; a malformed
  override is not debt. (This preserves #406's strength for exemptions,
  which would otherwise have been demoted to W by the restaging.)

* Removed the blanket `# noqa` hatch and its PS-220-noqa-deprecated notice.
  A sweep of all 118 repos under ~/proj (8956 src/**.py files, 4448 flagged
  sites) found ZERO users, with a planted-user control confirming the sweep
  could detect one. audit.exemptions is now the only per-site opt-out.

Tests: the registry override test injected "W" for PS-220, which becomes a
tautology once W is the registered severity — it now injects "E" and a
guard test asserts the injected severity differs from the registered one.
Both halves of the staged gate are pinned: a print does NOT fail a
non-opted-in package, and the SAME source DOES fail an opted-in one (one
variable varied). Mutation-proved: reverting the default to E reds 11
tests; dropping the mandatory-reason rule reds 18.
…t-in, --version entry-point gate (#411)

Promotes the accumulated [Unreleased] section to 0.36.0 and documents
#409, which landed without a CHANGELOG entry.

Minor, not patch: the PS-220 default severity moves E -> W (a behaviour
change for every consumer), `audit.enforce-logging` grows a new mapping
shape with a mandatory reason, and two opt-outs are REMOVED — the bare
`enforce-logging: error` / `off` scalar shorthands and the blanket
`# noqa` hatch. Under 0.x that combination is a minor bump.
…t the node (#412)

`pytest -n auto` over-subscribes an allocated node. The cause is narrower than
"auto reads the machine's core count": pytest-xdist's
`pytest_xdist_auto_num_workers` consults psutil FIRST and returns its count
immediately (xdist/plugin.py 3.8.0 L26-34), and psutil ignores cgroup/cpuset
confinement. Only if psutil is absent does xdist fall through to
`sched_getaffinity`, which is correct.

Measured on the Spartan CI runner node spartan-bm155, inside the 48-CPU lease
cgroup `job_27144058/step_extern`:

    os.sched_getaffinity(0)          48   <- the allocation
    os.cpu_count()                  128
    psutil.cpu_count(logical=False)  128   <- what `-n auto` used
    psutil.cpu_count()              128
    SLURM_CPUS_PER_TASK             (empty)
    SLURM_JOB_CPUS_PER_NODE         (empty)

i.e. ~128 workers into a 48-CPU allocation, a 2.7x continuous over-subscription
(corroborated by worker ids reaching gw121 in a scitex-hub run).

Changes:

* `_core/test_execution.py`: new `allocated_cpus()` — SLURM_CPUS_PER_TASK,
  then SLURM_JOB_CPUS_PER_NODE (incl. the "48(x2)" form), then
  `sched_getaffinity`, then `os.cpu_count()`. No magic cap and no hardcoded
  core count, so an unconstrained laptop still gets all its cores.
  `ALLOCATED_CPUS_PY_SNIPPET` carries the same policy as a shell-embeddable
  one-liner for invocations that must resolve the count on a REMOTE host.
* `ecosystem test-remote`: keeps `-n auto` but corrects it at its source by
  exporting `PYTEST_XDIST_AUTO_NUM_WORKERS`, which xdist consults ahead of
  psutil. Computed on the remote and echoed into the log.
* ADR-0004: P1(b) named `$SLURM_CPUS_PER_TASK` as the source; that variable is
  empty on the runner (ssh-adopted into the lease cgroup, so it inherits the
  cpuset but not the step env). Corrected, with the measurement recorded.

NOT changed, deliberately: `src/scitex_dev/_ecosystem/ci_template/templates/`
contains no `-n auto` at all — it is a thin caller into scitex-ai/.github. The
live `-n auto` that produced gw121 is `.github/workflows/pytest-matrix.yml:89`
in THAT repo and needs the same one-line export there; it is outside this
repo's tree.
`scitex-dev cron list` printed `installed (0): (none)` when it could not
read the crontab AT ALL — cannot-look rendered as looked-and-found-zero.
Reads failed soft (`read_crontab` returned "" on a missing binary) while
writes failed loud; only the read path reported, so the soft failure was
the one that reached an operator.

This nearly caused a false fleet-wide automation-outage escalation: a
peer agent ran `cron list` inside a container with no `crontab` binary,
read "13 jobs registered, NONE installed" — worktree-gc, ci-watch,
creds-rotate-all — and was one step from escalating. The jobs were
running fine on the host; only the container could not look.

Adds a third state at the source (`CrontabRead` / `read_crontab_state`)
and plumbs it through both reporting surfaces:

  * read, lines found -> the count, as before
  * read, empty       -> zero installed, as before (unchanged)
  * NOT readable      -> UNKNOWN, with the reason

Human: `installed: UNKNOWN ('crontab' not found on PATH)` plus an
explicit "this is NOT zero jobs installed" note; `cron status` gains a
banner and per-job `installed: unknown` instead of a fabricated `no`.

JSON: `cron list` reports `installed: null` (NOT `[]`, so a consumer
calling len() cannot silently read zero) alongside `installed_state`
and `installed_unavailable_reason`; `cron status` gains `crontab_state`
and `crontab_unavailable_reason`.

A non-zero `crontab -l` exit whose message is "no crontab for <user>"
stays READABLE-and-empty — that is an ordinary empty crontab, not a
failure to look.

The write path is untouched: `write_crontab` still raises, which is
correct. `read_crontab` keeps its lossy signature for callers that
cannot act on the difference (install / remove).

Tests are mutation-proved on both arms — collapsing UNKNOWN back into
empty reddens 10 tests; relabelling everything UNKNOWN reddens the 5
control tests that pin "readable and genuinely empty still reports
zero".
Every verification failure recorded across the fleet on 2026-07-22/23 is
one shape: a failed measurement rendered as a confident value. The new
leaf states that once and organises the defences by claim type — absence
claims need a positive control, causal claims need one varied variable
and a hunted counter-example, content claims need a second independent
reader, artifact claims need the artifact actually in use, peer claims
need corroboration independent in kind.

Also carries the six-state search-failure taxonomy, the vacuous /
inert / layer-blind / sampling control failures, the both-arms rule for
fixing a masking bug, the status words that fuse a failed measurement
into a clean one (skipped, declared-masked, 0), and the mechanism for
commissioned findings.

Every rule cites a measured instance. Draft sections with no instance
behind them were dropped.

200 lines / 10230 B — under the §4 leaf ceiling. Verified with a
positive control: inflating the leaf past the ceiling makes the checker
report §4.monolith, so the clean result is not a check that never ran.
…(132 -> 118) (#415)

* chore(ps220): migrate stderr prints to scitex-logging (trace_env, linter format/health)

First slice of the PS-220 self-migration. scitex-dev authored the
no-bare-print mandate but carried 132 findings against it; this migrates
part of the STDERR bucket, where scitex-logging provably cannot move the
output because it already writes every level to sys.stderr.

- trace_env/trace.py: the strace announce -> log.info
- linter/_cmd_format.py: not-found / no-files -> log.error / log.warning
- linter/_health.py: both one-shot L1/L2 notices -> log.warning, dropping
  the hand-rolled ANSI now that scitex-logging owns the colouring

Stdout prints are deliberately untouched: they need the console API that
is still being designed.

* chore(ps220): migrate stderr prints in linter completion + dispatch argparse

- linter/_cmd_completion.py: both unsupported-shell errors -> log.error.
  These two sites are dead legacy argparse code: nothing imports the
  module except a lazy _generate_completion_script import in
  _cmd_completion_cmds.py, register() is never called, and the live
  surface is the click group in _cmd_completion_cmds.py.
- _core/dispatch/_docs_argparse.py: LookupError report -> log.error.
- _core/dispatch/_skills_argparse.py: the three not-found reports ->
  log.error; the skills-get pair folds into one record rather than two
  separately-prefixed lines.

sys stays imported in the dispatch modules — sys.exit(2) still uses it.

* chore(ps220): migrate stderr prints in quality check + cron task-harvest

- _cli/quality/_check.py: missing-version error -> log.error.
- _cli/cron/_task_harvest.py: both harvest-failure reports -> log.error,
  dropping the now-redundant ERROR: prefix (the level supplies it) and
  the explicit flush (scitex-logging flushes its stderr handler). The
  [task-harvest <timestamp>] tag stays — it is the cron log convention
  that lets an operator grep one pass out of a shared log.

Completes the 14-site stderr slice.
…fig-layout convention (#416)

* feat(audit): PS-222 config-layout auditor (check module)

* feat(audit): register + wire PS-222 config-layout check

* test(audit): PS-222 config-layout tests incl. runtime/ control arm

* docs(skills): PS-222 config-layout enforcement rule doc
* docs(doctrine): degrade branches hide hard failures; mispositioned probes cannot fail

* docs(doctrine): trim redundant instances to make room for the masking rule

* docs(doctrine): tighten §4/§5 prose
* fix(audit): never print SUCC over live warning findings

audit-project decided its success banner from the severity-FLOOR-filtered
finding list, so at the default --severity error a tree carrying live W
findings printed output byte-identical to a genuinely clean tree:

  SUCC: <pkg> (<root>): no project-structure violations

Measured on scitex-agent-container: 53 live PS-220 findings, SUCC, exit 0.

The serious consequence is not cosmetic. A mutation proof driven through
the CLI could not fail for ANY W-severity rule: planting a bare print()
did not change one byte of output, so the check that was supposed to
detect it was structurally incapable of doing so. Every 'mutation-proved'
green produced that way this week was unearned.

It also broke the staged-rollout premise PS-220's E->W move (PR #410) was
granted on: findings stay FULLY VISIBLE, they only stop blocking.

Fix — reporting only, exit codes untouched:
  - SUCC iff zero findings at ANY severity (was: zero findings at floor)
  - headline always carries both counts: N error(s), M warning(s)
  - counts taken over all surviving findings, not the floor-filtered view
  - --json gains warnings/infos alongside the existing errors
  - below-floor findings get a line naming how to list them

W/I still never fail CI. Changing the exit code would silently re-break
every repo the 0.36.0 restage just unblocked.

Reported-by: scitex-agent-container (2026-07-23)

* test(audit): pin that the summary never claims success over live findings

Asserts on OUTPUT, not only exit codes. The pre-existing PS-220 suite
(test__check_no_print_exit_code.py) reads exit codes alone, which is
exactly why this bug survived it: the exit code was correct throughout,
and only the banner lied.

Covers the four required arms plus the control:
  - zero findings              -> SUCC printed, exit 0
  - N warnings, 0 errors       -> SUCC NOT printed, count shown, exit 0
  - N errors                   -> failure reported, exit 1 (unchanged)
  - --json                     -> both errors and warnings present
  - mutation control           -> planting one bare print in an otherwise
                                  identical tree moves warnings 0 -> 1

The last pair is the test the previous verification could not perform.
Pre-fix, both halves of it produced identical output, so no assertion on
the CLI's report could have distinguished them.

* test(audit): assert the banner on its logging transport, not on a stream

CI was red on all three pytest legs while the same tests passed locally.
Measured rather than guessed: `_emit` logs through
scitex_logging.getLogger('scitex_dev.audit'), and that logger carries NO
handlers of its own with propagate=True. The banner reaches a terminal
only via handlers scitex-logging installs on the ROOT logger — ambient
state that depends on import order and on whatever else touched logging
in the process. capfd saw it in a bare shell and not under the full CI
suite, which captured the click-printed epilogue and none of the log.

The --json assertions were green in CI throughout, which corroborates
the split: that payload really does go through click.echo to stdout.

So the banner assertions now read caplog. That is deterministic across
environments and strictly STRONGER than the stream version: it also pins
the LEVEL of each line (SUCC / WARN / ERRO), so a regression that
downgraded the summary to info — invisible in real use, since the audit
logger's default level is WARNING — now fails. No assertion was weakened;
three were added.

Also closes an ordering hazard the rewrite surfaced: naming the audit
logger in caplog.set_level() creates it through the stdlib, and
logging.getLogger caches by name, so scitex_logging then returns that
plain Logger and _emit._logger loses .success. Fixtures set the level on
root only, and the module imports _emit up front to bind the logger
before anything else can. The same hazard is a latent crash in
production — reported separately, not silently absorbed here.
…ries (#419)

* feat(ecosystem): distribution-identity enumeration keyed by origin remote

Adds _ecosystem/_enumerate.py: resolves checkout directories into
DISTRIBUTIONS by origin remote instead of directory name, collapses
duplicate checkouts and linked worktrees into reported aliases,
cross-references an org listing to surface repos with no local
checkout, and labels which tree was measured. Unreadable paths are
recorded as errors, never silently dropped.

* test(ecosystem): cover distribution-identity enumeration with real git repos

Two-armed suite: duplicate checkouts and linked worktrees collapse to
one distribution with the extras reported as aliases, while the control
arm proves two genuinely distinct repos still count as two. Also covers
org-delta surfacing, loud error reporting, and the measured-tree label.

* feat(ecosystem): expose distribution enumeration via 'ecosystem list --distributions'

Opt-in flag; the default output shape is unchanged so existing callers
keep working. Adds --org to cross-reference the GitHub org listing and
--scan-root to enumerate a directory tree (the input shape brand-wide
sweeps use, where duplicate checkouts appear). Output labels its own
source, scope, measured tree, and alias/not-checked-out counts.
Orphaned linked worktrees are now diagnosed by name rather than as a
generic unreadable path.
ywatanabe1989 and others added 28 commits July 23, 2026 08:46
…loor included

PR #417/#420 fixed the human summary COUNTS so they no longer derive
from the severity-floor-filtered `visible` list. The residual half was
the --json branch, which still built its `violations` list from
`visible`: at the default `error` floor a tree of live W findings
emitted `"violations": []` while `warnings` reported a positive count,
so any --json mutation proof written at the default floor read an empty
list and could not fail.

Keep `violations` floor-filtered so --json still honours --severity
(control-arm), and add `violations_total` carrying every surviving
finding with its severity. Additive: `violations` unchanged, and
len([v for violations_total if severity=='W']) == warnings by
construction. Applied to _project and _django; _api has no severity
floor so it does not share the defect.
docs(skills): correct the void-proofs claim in 04_verification-controls.md (census found 0)
…_total

Matched-pair coverage for the residual half of #417/#420 on the machine
path, for both audit_project and audit_django:
  - default (error) floor, W-only tree: violations_total is NOT silently
    empty and carries the finding with severity (the defect)
  - CONTROL ARM: default floor, violations (filtered) stays [] — proves
    the fix did not just 'emit everything' and destroy --severity
  - warning floor: violations lists the W finding (floor still selects)
  - exit code identical (0) across floors
  - violations_total W-count == warnings (ties the list to #417 counts)
  - premise guard: _check_no_print._DEFAULT_SEVERITY == 'W'

Mutation-proven both directions (visible-derived total -> disclosure
tests red; violations ignores floor -> control arm red).
…w-floor

fix(audit): --json discloses below-floor findings via violations_total (residual #417/#420)
Add mutation-provable, function-level tests for W rules flagged by the
#429 census as having no rule-ID proof. Each test calls the checker
directly (never the CLI summary banner) with a stub Violation, asserting
the finding on a triggering fixture and its absence on a clean control.

Project (PS): PS-145, PS-147, PS-150, PS-151, PS-157, PS-165, PS-166,
PS-167, PS-206b, PS-211, PS-212.
Django (DJ): DJ-103, DJ-105, DJ-109, DJ-202, DJ-203 added to the shared
conforming-fixture suite; DJ-107 already had a proof.
…ks.py)

The self-audit gate (tests/develop/test_audit.py::test_audit_all_clean)
scans the whole repo tree. The proof file was named
test__check_placeholder_tests_ps206b.py, which has no matching source
module — PS-204 (orphan-test-file, severity E) fired, making audit-all
exit 1. The function under test (_check_placeholder_tests) lives in
_checks.py, so test__checks.py is the mirror-correct name. Verified:
audit_project('scitex-dev') now exits 0 (was 1); the 3 PS-206b tests
stay green.
test(audit): prove 16 unproven W-severity audit rules
…skills

docs(skills): split remaining oversize skill files under caps
…-creds

fix(cron): route federated provider + creds install-cron logs under runtime/ (finish #367 remainders 1+3)
Merged #367+#433 moved cron/log output under $HOME/.scitex/<pkg>/runtime/logs/,
but nothing mechanically prevented a regression to the forbidden non-runtime
$HOME/.scitex/<pkg>/logs/ path — the directive lived only in a docstring,
disconnected from the code it governs. PS-223 closes that gap.

Co-located checker _check_logs_path.py AST-scans src/<pkg>/**.py and flags a
STRING LITERAL matching .scitex/<pkg>/logs/ (segment after <pkg>/ is 'logs',
not 'runtime/logs'). Two structural false-positive guards: docstrings and
bare string-statements are skipped by node identity, and only whitespace-free
path tokens fire — so description/help prose that merely mentions the path
never fires. Severity W (warn-first, matching PS-222 bake-in). Registered in
_registry.py as a co-located rule; dispatched from _run_checks.py.

Test test__check_logs_path.py (mirrors source module): fires on forbidden
literal, clean on runtime/logs control arm, silent on prose docstring/comment/
description; mutation-proven (suppress emit -> RED, restore -> GREEN).
feat(audit): PS-223 — flag non-runtime/ logs-path string literals in src
@ywatanabe1989
ywatanabe1989 merged commit 24c6493 into main Jul 24, 2026
17 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant