diff --git a/.github/knowledge-model-policy.json b/.github/knowledge-model-policy.json index ccc3f9c36..f50296531 100644 --- a/.github/knowledge-model-policy.json +++ b/.github/knowledge-model-policy.json @@ -10,9 +10,6 @@ "docs/knowledge-base.md", "docs/knowledge-operations.md", "docs/knowledge-store.md", - "src/inferrsi/catalog.py", - "src/inferrsi/skill_catalog.py", - "src/inferrsi/knowledge_operations.py", "src/inferrsi/knowledge/catalog.py", "src/inferrsi/knowledge/skill_catalog.py", "src/inferrsi/knowledge/knowledge_operations.py" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bf42a69e..4b1db2ba6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,100 @@ ### Changed +- Eleven legacy root modules are removed (Refs #412, R1). Their implementations + already lived in `inferrsi.knowledge` and `inferrsi.apps`. The root files are + deleted in the change that moves their last consumers, with no forwarding + shells, `sys.modules` aliases or import fallbacks: + - `inferrsi._skill_selection` → `inferrsi.knowledge._skill_selection` + - `inferrsi._source_snapshot` → `inferrsi.knowledge._source_snapshot` + - `inferrsi.catalog` → `inferrsi.knowledge.catalog`, and + `inferrsi.resources.repository_packs` for `REPO_TYPES` and `repository_pack` + - `inferrsi.knowledge_operations` → `inferrsi.knowledge.knowledge_operations` + - `inferrsi.knowledge_store` → `inferrsi.knowledge.knowledge_store` + - `inferrsi.research` → `inferrsi.knowledge.research` + - `inferrsi.skill_catalog` → `inferrsi.knowledge.skill_catalog` + - `inferrsi.evaluation` → `inferrsi.apps.legacy.evaluation` + - `inferrsi.execution` → `inferrsi.apps.legacy.run` for `run_experiment` and + for `execute_trial`, the former `_execute`; `inferrsi.infra.targets` for + `repository_identity`; `inferrsi.harness.local_process` for `_WindowsJob` + and `_stop_tree` + - `inferrsi.reporting` → `inferrsi.apps.legacy.reporting` + - `inferrsi.inspection` → `inferrsi.apps.inspection` + - Importing any of the eleven old module paths now raises + `ModuleNotFoundError`, in external Python code as well as in this + repository. A pickle that names a class or function by one of those paths + no longer loads. Objects defined in the canonical modules have named them + in new pickles since they moved there. `inferrsi.execution.run_experiment` + and `inferrsi.inspection.inspect_environment` were defined in the root + files themselves, so every existing pickle of either function reference is + affected. Such pickles need explicit migration or the prior release. `src/` + has no pickle usage, and frozen evidence is not rewritten: a historical + campaign replays with its original source-pinned bundle or wheel. + - No CLI command, flag, output or exit code changes. Root `cli.py` and + `arguments.py` stay in place until A1 (#408); this change edits only their + imports. `arguments.REPO_TYPES` is still the one + `inferrsi.resources.repository_packs.REPO_TYPES` tuple object, now imported + from that module directly instead of through root `catalog`. + - Root `inspection.py` was not an alias: it bound the Infra facts API to the + host. `inferrsi.apps.inspection.inspect_environment` keeps its signature and + result and holds that binding. `_command` is `infra.commands.probe_command` + and `repository_identity` is `infra.targets.repository_identity`, both read + from the module on each call. Test injection points move with it, from + `inferrsi.inspection._command` and `inferrsi.execution.repository_identity` + to those two names on `inferrsi.apps.inspection`. `infra/inspection.py` is + unchanged and imports no Apps module, and an inspection no longer loads the + old-run composition or Benchmark to obtain the snapshot. + - `inferrsi.apps.legacy.run.run_experiment(spec, environment)` is the + two-argument entry that root `execution.run_experiment` was. Its `execute` + and `snapshot` keyword defaults are now `None` and resolve to the module's + `execute_trial` and `repository_identity` when the run starts. A test that + replaced `inferrsi.execution._execute` now replaces + `inferrsi.apps.legacy.run.execute_trial`. An explicitly passed port still + wins. + - `docs/legacy-knowledge-manifest.json` ends at zero `compatibility_pairs`, + from nine. Eight were edges of the seven deleted Knowledge shells; root + `catalog` had two. The nine records and root `catalog`'s two routes are + kept, unchanged, under the manifest's last `revision_history` entry. The + ninth pair, `infra.inspection` → `inferrsi.resources.repository_packs`, is a + live import that no deletion removes. It is now a permanent exact rule + outside the allowlist: `technical_leaf.permanent_importers` in the manifest + and `LEAF_IMPORTERS` in `tests/_gate_policy_support.py`. That one module + binds only `repository_pack`, with one plain module-level `from` import. + Any other importer, name, alias, import of the leaf as a module object, + nested import, `sys.modules` use or dynamic import fails, and an + `inferrsi.resources.` prefix allows nothing. The leaf's bytes, locked + SHA-256, allowed imports, two symbols, top-level shape and import-free + parent initializer are unchanged. + - One temporary entry is declared beside that rule and is not part of it. + Root `arguments.py` reads `REPO_TYPES` from the leaf + (`technical_leaf.temporary_importers`, `TEMPORARY_LEAF_IMPORTERS`), and it + joins `cli.py` in the gate's exact `APPLICATION_ENTRIES` because it now + names `inferrsi.knowledge` itself. Both end in A1 (#408), which removes root + `arguments.py`. Until then the report carries a + `temporary leaf importers: 1` blocker. + - The gate no longer registers `LEGACY_RUN_FORWARDERS`. The eleven names are + `RETIRED_ROOT_MODULES`: a file that returns under one is a violation, and + no Knowledge, Harness, Benchmark or Apps closure may reach one. + - `first_wave_report()` on this tree has no violation, and the `allowlist` + and `legacy run forwarders` blockers are gone. `isolation_complete` and + `new_core_closure_isolated` are still false. Three blockers remain: the + temporary entry above, twelve `harness` → `inferrsi.infra` hits in six + Harness modules (R2), and five declared runtime resource loaders (R3). + - The permanent importer rule, the temporary entry and this manifest revision + require formal architecture review. Passing tests do not supply it, and the + implementer does not self-approve them. + - `.github/knowledge-model-policy.json` drops the three deleted shell paths + (`src/inferrsi/catalog.py`, `src/inferrsi/skill_catalog.py` and + `src/inferrsi/knowledge_operations.py`) from `protected_paths`. The three + `src/inferrsi/knowledge/` implementations stay protected, and no other + policy field changes. + - The exact root inventory in `tests/test_package_root_inventory.py` shrinks + from 21 to 10 tracked files: `__init__.py`, `__main__.py`, + `_mindie_read_worker.py`, `arguments.py`, `benchmark_operations.py`, + `cli.py`, `mindie_context.py`, `mindie_mcp.py`, `mindie_project.py` and + `summary_chart.py`. The final two-file root waits for the A1/A2 (#408) and + M1 (#452) moves. The build-generated `_version.py` stays wheel metadata, not + a tracked root file. - Repository-pack access and the evidence path rules leave the package root (Refs #451). Root `_repo_pack_data.py` and `evidence_paths.py` are removed in the same change, with no forwarding shells or `sys.modules` aliases: @@ -42,6 +136,9 @@ `summary_chart.py`. It is an exact set that each later removal shrinks, not the final two-file root. The build-generated `_version.py` is wheel metadata, not a tracked root file. + - The #412 entry above supersedes three statements in this entry: root + `catalog` is no longer a `REPO_TYPES` consumer, the nine compatibility pairs + are retired, and the root inventory is 10 files. - Shared technical contracts now live in `inferrsi.contracts` (Refs #451). The four root files `_json_v1.py`, `runspec.py`, `claim.py`, and `contracts.py` are removed in the same change. Canonical implementations are `contracts/json_v1.py`, @@ -73,6 +170,7 @@ - The old `run` chain is split by owner. The CLI, its exit codes 0/1/2 and the `evaluation.json` bytes are unchanged. Root `inferrsi.evaluation`, `inferrsi.execution` and `inferrsi.reporting` keep working as forwarders. + That held until the #412 entry above removed all three. - Moved import paths, with no forwarder left under `inferrsi.harness`: - `inferrsi.harness.evaluation` → `inferrsi.apps.legacy.evaluation` (`evaluate` with a `RunSpec`) and `inferrsi.benchmark.legacy_run_v0` (the gates). diff --git a/docs/architecture.md b/docs/architecture.md index 67ca5fba5..f9931dc0a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -99,10 +99,21 @@ Dependencies flow from entrypoints to Harness, from scenarios to Model/Infra, and from Model to Infra utilities. Infra does not import Model or Harness. None of these shared packages imports a campaign from `examples/`. +Environment inspection follows that direction. `infra/inspection.py` is the +facts API: it takes the command probe and the repository snapshot as ports. +`apps/inspection.py` binds those ports to this host with +`infra.commands.probe_command` and `infra.targets.repository_identity`, and the +CLI calls that composition. Infra reaches no Apps, Knowledge, Harness or +Benchmark module and is not a fourth core. `infra/inspection.py` reads a +repository-pack description through the neutral `resources.repository_packs` +leaf, under the one permanent importer rule in +[module-layout.md](module-layout.md#permanent-neutral-leaf-importer-rule-proposed). + The implemented table above is not the target four-module layout. The suggested rule, including what the import gate does not prove, is [module-layout.md](module-layout.md). That page does not move code. It also -lists the Proposed first-wave compatibility exceptions recorded in +records the first-wave compatibility pairs that #412 R1 retired and the +permanent neutral-leaf importer rule, both held in [legacy-knowledge-manifest.json](legacy-knowledge-manifest.json). The caller still prepares the benchmark runtime and baseline/candidate source @@ -165,6 +176,7 @@ src/inferrsi/ skill_evaluation.py Composed bundle export, trial run and summary skill_eval_engineering.py Composed artifact check: task rules around sandbox replay improvement.py Composed assessment, selection, rerun and holdout acceptance + inspection.py Environment inspection bound to the host probe and Git identity legacy/ run.py Old run composed from Harness, Benchmark and the report evaluation.py Old evaluate entry: RunSpec to the Benchmark gates @@ -177,12 +189,6 @@ src/inferrsi/ resources/ __init__.py Import-free initializer repository_packs.py Neutral REPO_TYPES and pack.json reader; imports no inferrsi module - catalog.py Knowledge search and cited context - skill_catalog.py Packaged/default or custom skill metadata/body/resource resolution - knowledge_operations.py Normalized reads and bounded Collect (KnowledgeReader) - knowledge_store.py Authorized local store Create/Update (KnowledgeStore) - research.py Read-only model-aware discovery joined over catalog queries - _skill_selection.py Deterministic lexical skill ranking and context view summary_chart.py Stdlib SVG gain chart (chain/branch) and report figure spec apps/ evidence_layout.py Create the plan/results/patch/validator evidence directories @@ -205,6 +211,13 @@ src/inferrsi/ codex_search.py Bounded attributable file-search evidence from Codex rollouts codex_tool_returns.py Correlate Codex tool-return windows knowledge/ + catalog.py Knowledge search and cited context + skill_catalog.py Packaged/default or custom skill metadata/body/resource resolution + knowledge_operations.py Normalized reads and bounded Collect (KnowledgeReader) + knowledge_store.py Authorized local store Create/Update (KnowledgeStore) + research.py Read-only model-aware discovery joined over catalog queries + _skill_selection.py Deterministic lexical skill ranking and context view + _source_snapshot.py Shared source snapshot for stored records: manifest, compact, resolve context_projection.py Collected context text and item projection harness/ api.py H public allocate/execute/read over one RunStore and CPU target @@ -244,7 +257,7 @@ src/inferrsi/ processes.py Supervised Linux serving workers and their group cleanup targets/git.py Repository identity and source snapshots platforms/ Backend discovery registry; CUDA qualification - inspection.py Environment inspection coordinator + inspection.py Environment inspection facts API; command and snapshot ports commands.py Bounded read-only probes environment.py Dependency versions and storage checks http.py HTTP POST/download timing @@ -254,14 +267,22 @@ src/inferrsi/ repo_packs/ Contribution descriptors and versioned tuning knowledge knowledge_packs/ Dependency and peer knowledge, preserving existing tiers skill_library/ Complete categorized English skills and resources - execution.py Compatibility entrypoint -> apps.legacy.run - evaluation.py Compatibility exports -> apps.legacy.evaluation - inspection.py Compatibility entrypoint -> infra.inspection - reporting.py Compatibility export -> apps.legacy.reporting examples/ Current Qwen runner, image validator and v4 adapter tests/fixtures/legacy/ Frozen historical validation dependencies for regressions ``` +The package root holds exactly ten tracked files at this stage: `__init__.py`, +`__main__.py`, `cli.py`, `arguments.py`, `benchmark_operations.py`, +`summary_chart.py`, `mindie_context.py`, `mindie_mcp.py`, `mindie_project.py` +and `_mindie_read_worker.py`. #412 R1 deleted eleven root modules with no +forwarding shell or alias: the seven Knowledge shells, whose code is in +`knowledge/`; the three old-run forwarders `evaluation.py`, `execution.py` and +`reporting.py`, whose code is in `apps/legacy/`; and `inspection.py`, whose +host binding is `apps/inspection.py`. The final two-file root (`__init__.py` +and `__main__.py`) waits for the A1/A2 (#408) and M1 (#452) moves. +[module-layout.md](module-layout.md#retired-root-modules) maps each retired +path to its replacement and lists what still keeps the isolation gate false. + ## Where to extend | Add | Main extension locations | Required qualification | @@ -281,7 +302,10 @@ synthetic contract fixtures, not accelerator measurements. `apps.legacy.run.run_experiment(spec, environment, *, execute, snapshot)` composes the old run and accepts small callable adapters. It passes them, the Benchmark v0 gates and the report to `harness.runner.run_experiment`, whose four ports -(`execute`, `snapshot`, `assess`, `report`) have no defaults. For a new external +(`execute`, `snapshot`, `assess`, `report`) have no defaults. An omitted +`execute` or `snapshot` is read from `apps.legacy.run` when the run starts +(`execute_trial` and `repository_identity`), which is the two-argument call +that the retired root `execution.run_experiment` provided. For a new external benchmark format, reuse the standard executor and only replace metric interpretation: ```python @@ -344,7 +368,7 @@ the default control package continues to require only the standard library. Register a device probe in `infra.platforms.PLATFORM_PROBES`; supply the selected IDs, device model and bounded command runner. A new backend name additionally -needs an entry in `catalog.BACKENDS`, which supplies the CLI `--backend` choices +needs an entry in `knowledge.catalog.BACKENDS`, which supplies the CLI `--backend` choices and the accepted knowledge and skill backends; no package-data descriptor is required. Adding a device SKU within a backend is a parameter/configuration change. CUDA qualification takes an explicit device count and model; it contains @@ -367,8 +391,11 @@ group. Windows Job Object support in the general command executor (`harness.local_process`) does not extend to these server helpers. These constraints do not restrict read-only knowledge operations or the control CLI to Linux. -- Existing CLI flags, root import paths, external JSON metrics, run artifacts, - cancellation handling and report generation are retained. +- Existing CLI flags, external JSON metrics, run artifacts, cancellation + handling and report generation are retained. Root Python import paths are + not all retained: #451 and #412 R1 retired the root modules listed in + [CHANGELOG.md](../CHANGELOG.md) with no forwarding shell or alias. That file + maps each old path to its replacement and states the pickle impact. - `examples/qwen21_turbo_b300.py` retains its command interface and campaign settings while delegating lifecycle, requests and sampling to shared modules. - The LTX helper and quality script, plus the pre-cleanup Qwen quality script, diff --git a/docs/contracts/current-design.md b/docs/contracts/current-design.md index 541fd47e3..c39a10292 100644 --- a/docs/contracts/current-design.md +++ b/docs/contracts/current-design.md @@ -25,14 +25,14 @@ Historical candidate text, review dispositions, and the earlier Proposed drafts ## D001 — One distribution, three cores plus Apps, document contracts rather than shared business DTOs -Decision: keep Knowledge, Harness, and Benchmark as sibling independent cores; Apps is the only assembler. Each core owns its types. Cross-boundary traffic uses versioned JSON envelopes with explicit conversion; there is no shared business `contracts` package. The technical-only `inferrsi.contracts` package re-exports only `RunSpec`, defined once in `contracts/runspec.py`; `contracts/claim.py` owns the same Verdict enum re-exported by Harness. These are not shared business DTOs. The old root `contracts.py`, `runspec.py`, `claim.py`, and `_json_v1.py` files are retired without forwarding shells, and so are the old root `_repo_pack_data.py` and `evidence_paths.py`. +Decision: keep Knowledge, Harness, and Benchmark as sibling independent cores; Apps is the only assembler. Each core owns its types. Cross-boundary traffic uses versioned JSON envelopes with explicit conversion; there is no shared business `contracts` package. The technical-only `inferrsi.contracts` package re-exports only `RunSpec`, defined once in `contracts/runspec.py`; `contracts/claim.py` owns the same Verdict enum re-exported by Harness. These are not shared business DTOs. The old root `contracts.py`, `runspec.py`, `claim.py`, and `_json_v1.py` files are retired without forwarding shells, and so are the old root `_repo_pack_data.py` and `evidence_paths.py`. R1 (#412) retired eleven more root modules the same way: the seven Knowledge compatibility shells, the three old-run forwarders `evaluation.py`, `execution.py` and `reporting.py`, and root `inspection.py`, whose host binding is now `inferrsi.apps.inspection` over the unchanged `inferrsi.infra.inspection` facts API. [`CHANGELOG.md`](../../CHANGELOG.md) maps every retired path to its replacement and states the import and pickle impact. Permitted shared technical leaves are only: - `inferrsi.contracts.json_v1` — the canonical encoder (D002); - `inferrsi.contracts.runspec` and `inferrsi.contracts.claim` — the existing RunSpec and Verdict technical types, with only RunSpec re-exported by the package initializer; - `inferrsi.contracts.evidence` — the existing evidence segment names and pure path rules; it reads and writes no filesystem state and is not re-exported by the package initializer; -- `inferrsi.resources.repository_packs` — the neutral raw pack-data reader and the existing static data enumeration (D020), formerly root `inferrsi._repo_pack_data`. Its parent initializer `resources/__init__.py` imports nothing. +- `inferrsi.resources.repository_packs` — the neutral raw pack-data reader and the existing static data enumeration (D020), formerly root `inferrsi._repo_pack_data`. Its parent initializer `resources/__init__.py` imports nothing. Outside Knowledge it has one permanent importer, `inferrsi.infra.inspection`, which binds only `repository_pack`; that exact rule is not a compatibility pair. Root `arguments.py` reads `REPO_TYPES` from it under a separate temporary entry that A1 (#408) removes. The neutral leaf carries no target selection, task, scoring, scheduling, or public business DTO, and imports none of K/H/B/Apps. It still finds the Harness-owned `pack.json` through `files("inferrsi").joinpath("harness", "data", "repo_packs", ...)`, so reading it never imports Harness. Creating the evidence directories is host I/O and stays in Apps, in `inferrsi.apps.evidence_layout`; that module is not a shared leaf. The list is exact: a `contracts.` or `resources.` prefix admits no other module. @@ -115,6 +115,8 @@ These are target names; `src` does not yet contain a production RunStore or work Decision: see [`gate-policy-runstore-and-first-prs.md`](gate-policy-runstore-and-first-prs.md) §B. Seven precise Knowledge root compatibility re-exports are the entire first wave; the current gate is unchanged until a formal architecture review accepts the exact policy first. No dynamic import escapes, no star imports, no `sys.modules` substitution, no blanket `harness/*` exception. `overall isolation_complete` stays false while any allowlist entry or registered residual legacy edge exists. +That is the first-wave decision as it was taken, and the older contract documents keep it as written. Current status after R1 (#412): the seven root re-exports are deleted and the allowlist is empty. D018 below records what replaced the last pair and what still keeps isolation false. + ## D010 — three budget facts and no scope reset Decision: the ExecutionRecord minimal list, execute delivery, Apps→B mapping, and every figure carry capacity/reservation, actual usage, and releasable leases with confirmed release as three independent facts. Unknown is `null` with a reason; not-applicable is explicit; zero is never used to impersonate unknown; reservations are not charged as actual usage; units are never summed across currencies. @@ -141,6 +143,8 @@ Decision: see [`gate-policy-runstore-and-first-prs.md`](gate-policy-runstore-and Decision: old entry points, arguments, `Path`, stdout, stderr, and old run 0/1/2 stay byte-for-byte compatible. New Apps naming/envelope/exit mapping is fixed by D022, but K01/H01 do not implement a CLI. +Current reading: this covers the CLI commands and the `inferrsi` and `python -m inferrsi` entry points. It no longer covers old root Python module paths. The revised delivery decision in #412, #451 and #408 retires those paths without aliases, in the change that moves their last consumers. R1 (#412) removed the old-run root modules `evaluation`, `execution` and `reporting` together with root `inspection`; the old run is `inferrsi.apps.legacy` and the host inspection binding is `inferrsi.apps.inspection`. Root `cli.py` and `arguments.py` stay in place until A1 (#408), and R1 changed only their imports. + ## D014 — events remain local, not a new shared event schema Decision: #333 and #371 are both **Proposed**, not accepted event schemas. `ExecutionEventV1` is only an H-local fact read model; it is explicitly not a new format for #333 production events and does not cross-write legacy `events.jsonl`. Legacy STAGES/verdict capture/reconciliation belong to H; presentation belongs to Apps. A formal unified event implementation belongs to its own later slice and does not block K01/H01. @@ -157,6 +161,16 @@ Decision: see [`schema-and-semantics.md`](schema-and-semantics.md). Draft 2020-1 Decision: [`gate-policy-runstore-and-first-prs.md`](gate-policy-runstore-and-first-prs.md) §B: exact seven leaf→module allowances, shape checks, transitive-closure checks, the registered residual `harness/skill_evaluation.py → knowledge_operations → knowledge.*` edge, the staged isolation test, and the rule that a non-empty allowlist keeps `overall isolation_complete=false`. The skill-evaluation split (#410) later removed the registered residual edge and its exemption. +Current status after R1 (#412): + +- The seven root leaves are deleted, so `compatibility_pairs` in [`../legacy-knowledge-manifest.json`](../legacy-knowledge-manifest.json) is empty. The nine pair records stay in its `revision_history`, unchanged, as the record of this decision. +- The former ninth pair, `infra.inspection` → `inferrsi.resources.repository_packs`, is a live import that no root deletion removes. It is now a permanent exact rule outside the allowlist: that one module binds the one name `repository_pack` with one plain module-level import. The leaf's bytes, SHA-256, allowed imports, shape and import-free parent initializer stay locked, and no `resources.` or `contracts.` prefix is allowed. +- Root `arguments.py` → the same leaf, for `REPO_TYPES`, is a separate temporary entry. It adds a blocker until A1 (#408) removes root `arguments.py`. +- `overall isolation_complete` is still false. That temporary entry, twelve `harness` → `inferrsi.infra` hits in six Harness modules (R2) and five declared runtime resource loaders (R3) remain. The package root holds ten tracked files; the final two-file root waits for A1/A2 (#408) and M1 (#452). +- The permanent rule and that manifest revision require formal architecture review. The implementer does not self-approve them, and passing tests are not that review. + +[`../module-layout.md`](../module-layout.md) has the exact rules and their negative controls. + ## D019 — first PRs and numbering Decision: [`gate-policy-runstore-and-first-prs.md`](gate-policy-runstore-and-first-prs.md) §D. PR-00a (this contract set), PR-00b (gate policy), K01 (seven K modules), H01 (persistent record core), H02 (single-CPU execute), K02 (physical resource migration), then B01/Apps. H01 does not depend on K01 or H02; B/Apps do not block K01/H01. diff --git a/docs/knowledge-base.md b/docs/knowledge-base.md index 34493b00f..f58382d85 100644 --- a/docs/knowledge-base.md +++ b/docs/knowledge-base.md @@ -25,8 +25,8 @@ src/inferrsi/knowledge/data/skill_library/ //metadata.json Provenance, state and relationships //references/ Owner's mechanisms, readsets and comparisons //cases/ Owner's scoped cases (optional) -src/inferrsi/catalog.py Installed knowledge reader -src/inferrsi/skill_catalog.py Packaged or custom-root skill reader +src/inferrsi/knowledge/catalog.py Installed knowledge reader +src/inferrsi/knowledge/skill_catalog.py Packaged or custom-root skill reader src/inferrsi/knowledge/data/repo_packs/ Contribution records and existing public IDs src/inferrsi/knowledge/data/repo_packs/inferrsi/ Generated records linking full packaged skills src/inferrsi/knowledge/data/knowledge_packs/ Dependency and peer records diff --git a/docs/knowledge-contribution-workflow.md b/docs/knowledge-contribution-workflow.md index 9f7128a05..5dd07ae49 100644 --- a/docs/knowledge-contribution-workflow.md +++ b/docs/knowledge-contribution-workflow.md @@ -65,7 +65,7 @@ the registry hash: prefix `data/skill_library/`. A later move again needs an explicit new revision that regenerates every record; do not hand-edit the generated files. -`inferrsi.catalog` resolves each stored record into a public schema-1 record with +`inferrsi.knowledge.catalog` resolves each stored record into a public schema-1 record with the same fields and field order as before: the `sources` entry with the full commit URL, and `source_files` including the registry hash. Search, `knowledge show`, Collect and installed wheels keep reading schema 1. The generator refuses to write a @@ -106,6 +106,7 @@ same matrix row, remain ordinary content conflicts that must be resolved by hand as unavailable. This already happened with the previous format: the projections on `main` before this change pinned a squashed branch commit. - Raw stored records changed from schema 1 to schema 2. Code that reads the JSON - files directly must use `inferrsi.catalog` or `inferrsi._source_snapshot.resolve`. + files directly must use `inferrsi.knowledge.catalog` or + `inferrsi.knowledge._source_snapshot.resolve`. - Generated files are protected knowledge paths. Regenerating them is material authorship work and needs a valid declaration. diff --git a/docs/knowledge-model-policy.md b/docs/knowledge-model-policy.md index 707a4f5c1..5fde30750 100644 --- a/docs/knowledge-model-policy.md +++ b/docs/knowledge-model-policy.md @@ -35,11 +35,10 @@ The exact protected scope is: - `src/inferrsi/knowledge/data/repo_packs/**/knowledge/**` - `docs/reference-matrix.md`, `docs/searchspace-matrix.md` - `docs/knowledge-base.md`, `docs/knowledge-operations.md`, `docs/knowledge-store.md` -- `src/inferrsi/catalog.py`, `src/inferrsi/skill_catalog.py`, - `src/inferrsi/knowledge_operations.py` - `src/inferrsi/knowledge/catalog.py`, `src/inferrsi/knowledge/skill_catalog.py`, - `src/inferrsi/knowledge/knowledge_operations.py`, the implementations behind - the three root compatibility shells above, which stay protected + `src/inferrsi/knowledge/knowledge_operations.py`, the Knowledge implementations. + The three root compatibility shells of the same names were deleted by #412 R1, + and their paths left this list with the files. The implementations stay protected Deletions and both rename paths count. Mechanical/generated changes have no automatic exemption. Running a generator or obtaining an approved-model review diff --git a/docs/knowledge-operations.md b/docs/knowledge-operations.md index 20026a68a..c82096f05 100644 --- a/docs/knowledge-operations.md +++ b/docs/knowledge-operations.md @@ -8,7 +8,7 @@ neither requires a conversation or workflow object. ```python from pathlib import Path -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.knowledge_operations import KnowledgeReader reader = KnowledgeReader(skill_root=Path("src/inferrsi/knowledge/data/skill_library")) result = reader.collect(ids=["calibrate-serving-simulation"], material=True, limit=1) diff --git a/docs/knowledge-store.md b/docs/knowledge-store.md index d235e4fe6..fac03b77e 100644 --- a/docs/knowledge-store.md +++ b/docs/knowledge-store.md @@ -9,8 +9,8 @@ PR path. ```python from pathlib import Path -from inferrsi.knowledge_operations import Bundle, KnowledgeReader -from inferrsi.knowledge_store import KnowledgeStore +from inferrsi.knowledge.knowledge_operations import Bundle, KnowledgeReader +from inferrsi.knowledge.knowledge_store import KnowledgeStore skills = Path("src/inferrsi/knowledge/data/skill_library") store = KnowledgeStore(Path("/authorized/local/knowledge-workspace"), skill_root=skills) diff --git a/docs/legacy-knowledge-manifest.json b/docs/legacy-knowledge-manifest.json index 7b213283c..b98ec7e32 100644 --- a/docs/legacy-knowledge-manifest.json +++ b/docs/legacy-knowledge-manifest.json @@ -1,7 +1,7 @@ { "$schema": "urn:inferrsi:docs:legacy-knowledge-manifest:v1", - "description": "First-wave compatibility exceptions for seven root Knowledge leaves plus the neutral technical data leaf. P0b froze the first revision at 0da7e2f960cd4ec994ae8eddea5b71023c5379ca and K01 re-recorded every leaf hash before the code move. K02 changed only the neutral data leaf's locked file hash, because the pack.json resource moved to the Harness root. This C2 revision (#451) changes only where that leaf lives: it moves byte for byte from inferrsi._repo_pack_data to inferrsi.resources.repository_packs, and the two declared importers of the leaf point at the new module. The leaf's bytes and locked file hash, the seven root leaf hashes and the nine-pair count are unchanged. It is not a wildcard approval and changes only through formal review. D018/D020 chose these exact edges. file_sha256 is the old root leaf before the move, and line references are at the base_commit recorded by the revision that wrote them; this revision re-records none of them.", - "base_commit": "cd8fcb452bd2c7969a7a6e2544aa1cb5f8ad5332", + "description": "First-wave Knowledge compatibility record and the neutral technical data leaf's locks. P0b froze the first revision at 0da7e2f960cd4ec994ae8eddea5b71023c5379ca, K01 re-recorded every leaf hash before the code move, K02 changed only the data leaf's locked file hash, and C2 (#451) moved that leaf byte for byte to inferrsi.resources.repository_packs. This R1 revision (#412) deletes the seven root Knowledge shells with their eight pairs, so compatibility_pairs is empty and the allowlist blocker is gone. The former ninth pair, infra.inspection -> the data leaf, is now the permanent exact rule in technical_leaf.permanent_importers, outside the allowlist. The nine retired pair records, with the old root leaf hashes and export lists, are kept unchanged under the last revision_history entry. The leaf's bytes and locked file hash are unchanged. Nothing here is a wildcard approval, and it changes only through formal review. Line references are at the base_commit recorded by the revision that wrote them; this revision re-records none of them.", + "base_commit": "5db1bc94042f386f0b4b35c3e6fe91598fef118f", "revision_history": [ { "base_commit": "0da7e2f960cd4ec994ae8eddea5b71023c5379ca", @@ -31,232 +31,239 @@ "module": "inferrsi._repo_pack_data", "file": "src/inferrsi/_repo_pack_data.py" } - } - ], - "compatibility_pairs": [ - { - "importer": "catalog", - "target": "inferrsi.knowledge.catalog", - "file": "src/inferrsi/catalog.py", - "file_sha256": "e3241a3203595f116925ebb3fa6b4e2c0cb588b4c3a3575369bf50f253a3a947", - "exports": { - "public": [ - "REPO_TYPES", - "KNOWLEDGE_SCOPES", - "KNOWLEDGE_REPOS", - "BACKENDS", - "SCENARIO_ALIASES", - "knowledge_records", - "search_knowledge", - "get_knowledge" - ], - "private_used": [ - "_walk", - "_validate_record", - "files" - ] - }, - "audited_definitions": [ - "REPO_TYPES is the same tuple object through root catalog, the neutral leaf, and K.catalog", - "Derived KNOWLEDGE_SCOPES and KNOWLEDGE_REPOS stay unchanged" - ], - "consumer_evidence": "tests/test_cli_catalog.py (L189) and tests/test_knowledge_tiers.py (L89) patch catalog.files; tests/test_research.py (L91, L120) patch catalog.knowledge_records; infra.inspection (L10, L56) imports and calls repository_pack; arguments (L12) imports BACKENDS, KNOWLEDGE_REPOS, KNOWLEDGE_SCOPES, REPO_TYPES; cli (L12) imports get_knowledge, search_knowledge; observability/campaign_records (L12) imports BACKENDS. On this K01 head, tests/test_cli_catalog.py (L191), tests/test_knowledge_tiers.py (L91), and tests/test_research.py (L92, L121) patch inferrsi.knowledge.catalog; a root-shell patch is no longer effective.", - "retirement_evidence": "catalog sheds repository_pack to the neutral leaf; K.catalog does not export repository_pack" - }, - { - "importer": "catalog", - "target": "inferrsi.resources.repository_packs", - "note": "Root catalog statically re-exports REPO_TYPES and repository_pack from the business-free neutral technical leaf" - }, - { - "importer": "_source_snapshot", - "target": "inferrsi.knowledge._source_snapshot", - "file": "src/inferrsi/_source_snapshot.py", - "file_sha256": "ba6b799731a73cca55f211813d512fcaf46a123ce3997ad3ff918ce65ea7f39b", - "exports": { - "public": [ - "MANIFEST", - "REPOSITORY", - "LIBRARY", - "REGISTRY", - "STORED_SCHEMA", - "manifest", - "compact", - "resolve" - ], - "private_used": [ - "_snapshot" - ] - }, - "audited_definitions": [ - "manifest/compact/resolve and existing source/hash identity unchanged" - ], - "consumer_evidence": "catalog (L10, L103, L108, L113, L117) imports and uses MANIFEST, STORED_SCHEMA, resolve; tests cover Bundle/source_snapshot contract" - }, - { - "importer": "skill_catalog", - "target": "inferrsi.knowledge.skill_catalog", - "file": "src/inferrsi/skill_catalog.py", - "file_sha256": "86d465e245b4cd6b72ca175d05124dbb88d510a78568b6bc4c44b332870c08a3", - "exports": { - "public": [ - "skill_directories", - "skill_directory", - "skill_metadata", - "skill_records", - "get_skill", - "skill_instructions", - "skill_resource", - "skill_reference" - ], - "private_used": [ - "_frontmatter", - "_skill_name", - "_directory", - "_unique_fields", - "_indexed_directories", - "_entry", - "_sidecar", - "_references", - "_strings", - "_applicability", - "_effective", - "_metadata" - ] - }, - "audited_definitions": [ - "root:Path and checkout physical resource semantics unchanged" - ], - "consumer_evidence": "knowledge_operations (L279, L317, L435 lazy imports; calls at L281, L319, L438, L439, L442, L445) imports skill_directories, get_skill, skill_directory, skill_metadata, skill_reference; KnowledgeReader._skill uses all skill_* APIs; cli (L129) lazily imports get_skill, skill_instructions, skill_records, skill_reference, skill_resource; tools/skill_eval_snapshot.py (L47) imports _skill_name, _unique_fields, skill_metadata; tests/test_skill_catalog.py covers skill enumeration and loading" - }, - { - "importer": "_skill_selection", - "target": "inferrsi.knowledge._skill_selection", - "file": "src/inferrsi/_skill_selection.py", - "file_sha256": "7bcf1930b73e09fcadf78e7c9e384b30411ed599d83c32e6837f04ca25cbda77", - "exports": { - "public": [ - "RELEVANCE_METHODS", - "SkillIndexError", - "words", - "lexical_score", - "raw_string_lexical_score", - "indexed_skill", - "skill_score", - "context_view", - "TASK_RELEVANCE", - "task_score", - "record_context" - ], - "private_used": [] - }, - "audited_definitions": [ - "RELEVANCE_METHODS, TASK_RELEVANCE, and the scoring and context functions are the same objects; selection algorithm not rewritten", - "#389 audit: TASK_RELEVANCE is \"task-lexical-v1\" and is the third RELEVANCE_METHODS element; skill_score accepts only the two skill-only methods and rejects task-lexical-v1 with \"Unknown relevance method for skill-only ranking\"; task_score ranks skill and package records by whole task terms with a portable-or-in-scope backend factor; record_context returns the whole package record entrypoint as a context view or a context_limit omission" - ], - "consumer_evidence": "knowledge_operations (L12-L21) imports RELEVANCE_METHODS, TASK_RELEVANCE, context_view, indexed_skill, record_context, skill_score, task_score, words; collect uses them for relevance selection and context views; observability/knowledge_capture (L20) imports TASK_RELEVANCE through the root leaf and uses it at L74-L75, and cli (L335) and observability/optimization_story (L14) load that module; arguments (L11) imports RELEVANCE_METHODS; tools/evaluate_indexer.py (L138) imports SkillIndexError, indexed_skill; tests/test_raw_string_selection.py covers scoring and the skill_score rejection (L84-L86); tests/test_collect_selection.py covers context_view; tests/test_campaign_task_selection.py covers task-lexical-v1 through root KnowledgeReader. The observability/knowledge_capture import of TASK_RELEVANCE is removed by OBS (#413); K01 only keeps the root-shell pass-through of that name." - }, - { - "importer": "research", - "target": "inferrsi.knowledge.research", - "file": "src/inferrsi/research.py", - "file_sha256": "9bfe59dd17b5476c18ce50d671711053c62800521c50d6b710e8140a2b4dca04", - "exports": { - "public": [ - "research_knowledge" - ], - "private_used": [ - "_profile", - "PROCEDURE" - ] - }, - "audited_definitions": [ - "research_knowledge and the existing public surface preserved" - ], - "consumer_evidence": "tests/test_research.py covers research_knowledge; tests/test_husky_document_research.py exercises profile-based selection" }, { - "importer": "knowledge_store", - "target": "inferrsi.knowledge.knowledge_store", - "file": "src/inferrsi/knowledge_store.py", - "file_sha256": "70dfca059b0a682579206d4376d191667934b3ca869ae847098c4c6964d86abe", - "exports": { - "public": [ - "KnowledgeStore" - ], - "private_used": [ - "_protected" - ] - }, - "audited_definitions": [ - "KnowledgeStore type identity, CAS, and exceptions unchanged" - ], - "consumer_evidence": "cli (L16, L29) imports and instantiates KnowledgeStore; tests/test_knowledge_store.py covers create/update/read/history; tests/test_knowledge_contract.py covers reader integration" - }, - { - "importer": "knowledge_operations", - "target": "inferrsi.knowledge.knowledge_operations", - "file": "src/inferrsi/knowledge_operations.py", - "file_sha256": "686b196e03ca0a1dba5ff5e246563f305ba2bea412172833905af41a6c6ad886", - "exports": { - "public": [ - "AUTHORITIES", - "IDENTITY", - "RevisionConflict", - "IdentityError", - "safe_path", - "Bundle", - "KnowledgeReader", - "INCIDENTAL" - ], - "private_used": [ - "_json", - "_hash", - "_normalized", - "_sources", - "_tree" - ] - }, - "audited_definitions": [ - "Bundle/KnowledgeReader/RevisionConflict/IdentityError/safe_path/AUTHORITIES/IDENTITY are the same objects; signatures/defaults/Path/exceptions and used symbols preserved", - "The test-injection points for _json and _hash (D1, D2) are on the implementation module inferrsi.knowledge.knowledge_operations; the root shell only re-exports the same names as pass-throughs. Patching the root shell does not affect Bundle.manifest or KnowledgeReader.collect.", - "#389 audit of KnowledgeReader.collect: signature, defaults, and validation order are unchanged; relevance_method=\"task-lexical-v1\" ranks every package authority record with task_score, lets a packaged skills.discovery skill pass a scenario filter only for ranked selection without ids, builds non-skill contexts with record_context, and adds selection.scope \"package-skills-and-records\" with matched_count and returned_count; the two earlier methods keep the packaged-skill-only path and selection.scope \"generated-packaged-skills\". No top-level name was added or removed." + "base_commit": "cd8fcb452bd2c7969a7a6e2544aa1cb5f8ad5332", + "stage": "C2", + "superseded_by": "R1 retirement revision at 5db1bc94042f386f0b4b35c3e6fe91598fef118f", + "reason": "R1 (#412) deletes the seven root Knowledge shells in the change that moves their last consumers to inferrsi.knowledge, so the eight shell pairs retire with their files and compatibility_pairs is empty. The ninth pair, infra.inspection -> the data leaf, is a live import that no deletion removes. It leaves the allowlist and is restated as technical_leaf.permanent_importers. The technical leaf's module, file, bytes, locked hash, allowed imports, two symbols, top-level shape and import-free parent initializer are unchanged.", + "retired_compatibility_pairs": [ + { + "importer": "catalog", + "target": "inferrsi.knowledge.catalog", + "file": "src/inferrsi/catalog.py", + "file_sha256": "e3241a3203595f116925ebb3fa6b4e2c0cb588b4c3a3575369bf50f253a3a947", + "exports": { + "public": [ + "REPO_TYPES", + "KNOWLEDGE_SCOPES", + "KNOWLEDGE_REPOS", + "BACKENDS", + "SCENARIO_ALIASES", + "knowledge_records", + "search_knowledge", + "get_knowledge" + ], + "private_used": [ + "_walk", + "_validate_record", + "files" + ] + }, + "audited_definitions": [ + "REPO_TYPES is the same tuple object through root catalog, the neutral leaf, and K.catalog", + "Derived KNOWLEDGE_SCOPES and KNOWLEDGE_REPOS stay unchanged" + ], + "consumer_evidence": "tests/test_cli_catalog.py (L189) and tests/test_knowledge_tiers.py (L89) patch catalog.files; tests/test_research.py (L91, L120) patch catalog.knowledge_records; infra.inspection (L10, L56) imports and calls repository_pack; arguments (L12) imports BACKENDS, KNOWLEDGE_REPOS, KNOWLEDGE_SCOPES, REPO_TYPES; cli (L12) imports get_knowledge, search_knowledge; observability/campaign_records (L12) imports BACKENDS. On this K01 head, tests/test_cli_catalog.py (L191), tests/test_knowledge_tiers.py (L91), and tests/test_research.py (L92, L121) patch inferrsi.knowledge.catalog; a root-shell patch is no longer effective.", + "retirement_evidence": "catalog sheds repository_pack to the neutral leaf; K.catalog does not export repository_pack" + }, + { + "importer": "catalog", + "target": "inferrsi.resources.repository_packs", + "note": "Root catalog statically re-exports REPO_TYPES and repository_pack from the business-free neutral technical leaf" + }, + { + "importer": "_source_snapshot", + "target": "inferrsi.knowledge._source_snapshot", + "file": "src/inferrsi/_source_snapshot.py", + "file_sha256": "ba6b799731a73cca55f211813d512fcaf46a123ce3997ad3ff918ce65ea7f39b", + "exports": { + "public": [ + "MANIFEST", + "REPOSITORY", + "LIBRARY", + "REGISTRY", + "STORED_SCHEMA", + "manifest", + "compact", + "resolve" + ], + "private_used": [ + "_snapshot" + ] + }, + "audited_definitions": [ + "manifest/compact/resolve and existing source/hash identity unchanged" + ], + "consumer_evidence": "catalog (L10, L103, L108, L113, L117) imports and uses MANIFEST, STORED_SCHEMA, resolve; tests cover Bundle/source_snapshot contract" + }, + { + "importer": "skill_catalog", + "target": "inferrsi.knowledge.skill_catalog", + "file": "src/inferrsi/skill_catalog.py", + "file_sha256": "86d465e245b4cd6b72ca175d05124dbb88d510a78568b6bc4c44b332870c08a3", + "exports": { + "public": [ + "skill_directories", + "skill_directory", + "skill_metadata", + "skill_records", + "get_skill", + "skill_instructions", + "skill_resource", + "skill_reference" + ], + "private_used": [ + "_frontmatter", + "_skill_name", + "_directory", + "_unique_fields", + "_indexed_directories", + "_entry", + "_sidecar", + "_references", + "_strings", + "_applicability", + "_effective", + "_metadata" + ] + }, + "audited_definitions": [ + "root:Path and checkout physical resource semantics unchanged" + ], + "consumer_evidence": "knowledge_operations (L279, L317, L435 lazy imports; calls at L281, L319, L438, L439, L442, L445) imports skill_directories, get_skill, skill_directory, skill_metadata, skill_reference; KnowledgeReader._skill uses all skill_* APIs; cli (L129) lazily imports get_skill, skill_instructions, skill_records, skill_reference, skill_resource; tools/skill_eval_snapshot.py (L47) imports _skill_name, _unique_fields, skill_metadata; tests/test_skill_catalog.py covers skill enumeration and loading" + }, + { + "importer": "_skill_selection", + "target": "inferrsi.knowledge._skill_selection", + "file": "src/inferrsi/_skill_selection.py", + "file_sha256": "7bcf1930b73e09fcadf78e7c9e384b30411ed599d83c32e6837f04ca25cbda77", + "exports": { + "public": [ + "RELEVANCE_METHODS", + "SkillIndexError", + "words", + "lexical_score", + "raw_string_lexical_score", + "indexed_skill", + "skill_score", + "context_view", + "TASK_RELEVANCE", + "task_score", + "record_context" + ], + "private_used": [] + }, + "audited_definitions": [ + "RELEVANCE_METHODS, TASK_RELEVANCE, and the scoring and context functions are the same objects; selection algorithm not rewritten", + "#389 audit: TASK_RELEVANCE is \"task-lexical-v1\" and is the third RELEVANCE_METHODS element; skill_score accepts only the two skill-only methods and rejects task-lexical-v1 with \"Unknown relevance method for skill-only ranking\"; task_score ranks skill and package records by whole task terms with a portable-or-in-scope backend factor; record_context returns the whole package record entrypoint as a context view or a context_limit omission" + ], + "consumer_evidence": "knowledge_operations (L12-L21) imports RELEVANCE_METHODS, TASK_RELEVANCE, context_view, indexed_skill, record_context, skill_score, task_score, words; collect uses them for relevance selection and context views; observability/knowledge_capture (L20) imports TASK_RELEVANCE through the root leaf and uses it at L74-L75, and cli (L335) and observability/optimization_story (L14) load that module; arguments (L11) imports RELEVANCE_METHODS; tools/evaluate_indexer.py (L138) imports SkillIndexError, indexed_skill; tests/test_raw_string_selection.py covers scoring and the skill_score rejection (L84-L86); tests/test_collect_selection.py covers context_view; tests/test_campaign_task_selection.py covers task-lexical-v1 through root KnowledgeReader. The observability/knowledge_capture import of TASK_RELEVANCE is removed by OBS (#413); K01 only keeps the root-shell pass-through of that name." + }, + { + "importer": "research", + "target": "inferrsi.knowledge.research", + "file": "src/inferrsi/research.py", + "file_sha256": "9bfe59dd17b5476c18ce50d671711053c62800521c50d6b710e8140a2b4dca04", + "exports": { + "public": [ + "research_knowledge" + ], + "private_used": [ + "_profile", + "PROCEDURE" + ] + }, + "audited_definitions": [ + "research_knowledge and the existing public surface preserved" + ], + "consumer_evidence": "tests/test_research.py covers research_knowledge; tests/test_husky_document_research.py exercises profile-based selection" + }, + { + "importer": "knowledge_store", + "target": "inferrsi.knowledge.knowledge_store", + "file": "src/inferrsi/knowledge_store.py", + "file_sha256": "70dfca059b0a682579206d4376d191667934b3ca869ae847098c4c6964d86abe", + "exports": { + "public": [ + "KnowledgeStore" + ], + "private_used": [ + "_protected" + ] + }, + "audited_definitions": [ + "KnowledgeStore type identity, CAS, and exceptions unchanged" + ], + "consumer_evidence": "cli (L16, L29) imports and instantiates KnowledgeStore; tests/test_knowledge_store.py covers create/update/read/history; tests/test_knowledge_contract.py covers reader integration" + }, + { + "importer": "knowledge_operations", + "target": "inferrsi.knowledge.knowledge_operations", + "file": "src/inferrsi/knowledge_operations.py", + "file_sha256": "686b196e03ca0a1dba5ff5e246563f305ba2bea412172833905af41a6c6ad886", + "exports": { + "public": [ + "AUTHORITIES", + "IDENTITY", + "RevisionConflict", + "IdentityError", + "safe_path", + "Bundle", + "KnowledgeReader", + "INCIDENTAL" + ], + "private_used": [ + "_json", + "_hash", + "_normalized", + "_sources", + "_tree" + ] + }, + "audited_definitions": [ + "Bundle/KnowledgeReader/RevisionConflict/IdentityError/safe_path/AUTHORITIES/IDENTITY are the same objects; signatures/defaults/Path/exceptions and used symbols preserved", + "The test-injection points for _json and _hash (D1, D2) are on the implementation module inferrsi.knowledge.knowledge_operations; the root shell only re-exports the same names as pass-throughs. Patching the root shell does not affect Bundle.manifest or KnowledgeReader.collect.", + "#389 audit of KnowledgeReader.collect: signature, defaults, and validation order are unchanged; relevance_method=\"task-lexical-v1\" ranks every package authority record with task_score, lets a packaged skills.discovery skill pass a scenario filter only for ranked selection without ids, builds non-skill contexts with record_context, and adds selection.scope \"package-skills-and-records\" with matched_count and returned_count; the two earlier methods keep the packaged-skill-only path and selection.scope \"generated-packaged-skills\". No top-level name was added or removed." + ], + "consumer_evidence": "harness/skill_evaluation (L9) imports KnowledgeReader; cli (L15, L25, L33) imports and uses KnowledgeReader; knowledge_store (L10-L19) imports KO symbols; tests/test_bundle_manifest.py (L73) patches _hash; tests/test_collect_limit_serialization.py (L39-47) patches _json; tests/test_knowledge_operations.py covers Bundle/KnowledgeReader contract; tests/test_campaign_task_selection.py covers the #389 task-lexical-v1 collect path. On this K01 head, tests/test_bundle_manifest.py (L74-L75) and tests/test_collect_limit_serialization.py (L48-L49) patch inferrsi.knowledge.knowledge_operations; a root-shell patch is no longer effective." + }, + { + "importer": "infra.inspection", + "target": "inferrsi.resources.repository_packs", + "note": "infra.inspection imports the neutral leaf directly, never via catalog or H. D020 required the import rewrite; K01 changed only the import line (L10: from ..catalog import repository_pack → from .._repo_pack_data import repository_pack) and kept original functions, injected parameters, and returns unchanged. C2 (#451) changes that same line to from ..resources.repository_packs import repository_pack and nothing else in the module." + } ], - "consumer_evidence": "harness/skill_evaluation (L9) imports KnowledgeReader; cli (L15, L25, L33) imports and uses KnowledgeReader; knowledge_store (L10-L19) imports KO symbols; tests/test_bundle_manifest.py (L73) patches _hash; tests/test_collect_limit_serialization.py (L39-47) patches _json; tests/test_knowledge_operations.py covers Bundle/KnowledgeReader contract; tests/test_campaign_task_selection.py covers the #389 task-lexical-v1 collect path. On this K01 head, tests/test_bundle_manifest.py (L74-L75) and tests/test_collect_limit_serialization.py (L48-L49) patch inferrsi.knowledge.knowledge_operations; a root-shell patch is no longer effective." - }, - { - "importer": "infra.inspection", - "target": "inferrsi.resources.repository_packs", - "note": "infra.inspection imports the neutral leaf directly, never via catalog or H. D020 required the import rewrite; K01 changed only the import line (L10: from ..catalog import repository_pack → from .._repo_pack_data import repository_pack) and kept original functions, injected parameters, and returns unchanged. C2 (#451) changes that same line to from ..resources.repository_packs import repository_pack and nothing else in the module." + "retired_catalog_two_routes": { + "knowledge_export": { + "target": "inferrsi.knowledge.catalog", + "named_symbols": [ + "REPO_TYPES", + "KNOWLEDGE_SCOPES", + "KNOWLEDGE_REPOS", + "BACKENDS", + "SCENARIO_ALIASES", + "knowledge_records", + "search_knowledge", + "get_knowledge" + ], + "not_exported": [ + "repository_pack" + ], + "note": "K.catalog does not export repository_pack; the named-symbol-only K export excludes repository_pack" + }, + "neutral_leaf_export": { + "target": "inferrsi.resources.repository_packs", + "symbols": [ + "REPO_TYPES", + "repository_pack" + ], + "note": "Root catalog also re-exports REPO_TYPES and repository_pack from the neutral data leaf" + } + } } ], - "catalog_two_routes": { - "knowledge_export": { - "target": "inferrsi.knowledge.catalog", - "named_symbols": [ - "REPO_TYPES", - "KNOWLEDGE_SCOPES", - "KNOWLEDGE_REPOS", - "BACKENDS", - "SCENARIO_ALIASES", - "knowledge_records", - "search_knowledge", - "get_knowledge" - ], - "not_exported": [ - "repository_pack" - ], - "note": "K.catalog does not export repository_pack; the named-symbol-only K export excludes repository_pack" - }, - "neutral_leaf_export": { - "target": "inferrsi.resources.repository_packs", - "symbols": [ - "REPO_TYPES", - "repository_pack" - ], - "note": "Root catalog also re-exports REPO_TYPES and repository_pack from the neutral data leaf" - } - }, + "compatibility_pairs": [], "technical_leaf": { "module": "inferrsi.resources.repository_packs", "note": "K01 created src/inferrsi/_repo_pack_data.py from the recorded original fragment plus a leading comment, the future import, and the three listed stdlib imports. K02 repointed the reader to the Harness resource root and extended that comment, so file_sha256 was relocked then. C2 (#451) moved the file byte for byte to src/inferrsi/resources/repository_packs.py and deleted the root file; file_sha256 is checked against the relocated file and keeps its K02 value. original_fragment keeps the P0b provenance text; current_fragment is the body in the file today. P0b recorded only the fragment.", @@ -309,6 +316,45 @@ "parent_initializer": "src/inferrsi/resources/__init__.py", "unchanged": "The file bytes, the importlib anchor files(\"inferrsi\") joined with harness/data/repo_packs//pack.json, allowed_imports, the two symbols, REPO_TYPES object identity and the two permitted importers outside Knowledge (root catalog and infra.inspection) are unchanged. The leaf still imports no inferrsi module, and its new parent initializer imports nothing, so infra gains no edge into Harness.", "review": "This manifest revision requires formal architecture review, as the K02 resource_path_revision did. The implementer does not self-approve it, and passing tests are not that review." + }, + "permanent_importers": { + "stage": "R1", + "issue": "#412", + "rule": "Outside inferrsi.knowledge, exactly the listed module may import the leaf. It binds only its listed name, with the plain module-level from-import recorded as import_line. Any other importer, any other name, an alias, the leaf imported as a module object, an import below module level, a sys.modules use or a dynamic import fails the gate. An inferrsi.resources prefix allows nothing, and the leaf's one parent initializer stays import-free.", + "importers": [ + { + "importer": "inferrsi.infra.inspection", + "file": "src/inferrsi/infra/inspection.py", + "names": [ + "repository_pack" + ], + "import_line": "from ..resources.repository_packs import repository_pack", + "note": "D020. Infra reads one pack description directly, never through Knowledge or Harness. This is the edge that was the ninth compatibility pair." + } + ], + "not_a_compatibility_pair": "The entry is not in compatibility_pairs and is never counted by the allowlist blocker. It does not retire with a shell, because the importer is not a shell.", + "gate": "tests/_gate_policy_support.py LEAF_IMPORTERS holds the same importer and name. tests/test_knowledge_compatibility_shells.py requires the two to match exactly.", + "review": "This rule requires formal architecture review. The implementer does not self-approve it, and passing tests are not that review." + }, + "temporary_importers": { + "stage": "R1", + "issue": "#412", + "due": "A1 (#408)", + "rule": "Root catalog is retired, so the root CLI parser takes the one REPO_TYPES tuple from the leaf itself for its --repo-type choices. The entry is held to the same exact import shape as the permanent rule. It is not part of the permanent rule and it is not an allowance for Apps modules in general.", + "importers": [ + { + "importer": "inferrsi.arguments", + "file": "src/inferrsi/arguments.py", + "names": [ + "REPO_TYPES" + ], + "import_line": "from .resources.repository_packs import REPO_TYPES" + } + ], + "blocker": "While a listed module imports the leaf, first_wave_report() carries a 'temporary leaf importers' blocker, so isolation_complete stays false.", + "removal": "A1 deletes root arguments.py and removes this entry in the same change. How the moved parser reads REPO_TYPES is decided by that change's review, not here.", + "gate": "tests/_gate_policy_support.py TEMPORARY_LEAF_IMPORTERS holds the same importer and name.", + "review": "This entry requires formal architecture review. The implementer does not self-approve it, and passing tests are not that review." } }, "infra_inspection_rewrite": { @@ -318,5 +364,5 @@ "canonical_import": "from ..resources.repository_packs import repository_pack", "note": "K01 changed only this import line, from current_import to required_new_import; both record that K01 rewrite. C2 (#451) changes the same line to canonical_import, which is the line in the module today. Original functions, injected parameters, and returns stay the same." }, - "intra_closure_note": "An import of inferrsi.resources.repository_packs from inside inferrsi.knowledge (for example inferrsi/knowledge/catalog.py) is intra-closure, not a tenth compatibility exception; it is governed by the leaf's no-inferrsi.*-import rule, its import-free parent initializer and REPO_TYPES object identity, not by this allowlist." + "intra_closure_note": "An import of inferrsi.resources.repository_packs from inside inferrsi.knowledge (for example inferrsi/knowledge/catalog.py) is intra-closure. It is neither a compatibility exception nor a permanent-importer entry; it is governed by the leaf's no-inferrsi.*-import rule, its import-free parent initializer and REPO_TYPES object identity. inferrsi.knowledge.catalog holds that one REPO_TYPES tuple and does not export repository_pack." } diff --git a/docs/module-layout.md b/docs/module-layout.md index 173630a6a..2cfae4992 100644 --- a/docs/module-layout.md +++ b/docs/module-layout.md @@ -2,10 +2,12 @@ This page records the directory rule for the integration slice. The first-wave Knowledge move (K01) is implemented: `src/inferrsi/knowledge/` holds the seven -moved Knowledge bodies, and the old root modules are static compatibility -shells. The engineering task contribution now implements `benchmark/registry.py` -and `benchmark/tasks/`. The current flat `cli.py` and `benchmark_operations.py` -assemble this slice. The former `observability/` package is dissolved. Its +moved Knowledge bodies. R1 (#412) deleted the seven static root compatibility +shells that K01 left, so those names import only from `inferrsi.knowledge`. +The engineering task contribution now implements `benchmark/registry.py` +and `benchmark/tasks/`. The current flat `cli.py`, `arguments.py` and +`benchmark_operations.py` assemble this slice. The former `observability/` +package is dissolved. Its campaign records, traces, logs and profile captures are in `harness/records/`, its dashboard views and templates are in `apps/views/`, and the collected-context projection is in `knowledge/context_projection.py`. @@ -17,6 +19,8 @@ The old `run` chain is split by owner. Its gates are in by the confirmed split for this slice; that use does not accept #371. `cli.py` imports the old run only inside its `run` command, so the other commands, including ordinary optimization, still load no Benchmark module. +R1 deleted the three root forwarders of that chain (`evaluation.py`, +`execution.py` and `reporting.py`), and `cli.py` imports `apps/legacy/` itself. ## Target modules @@ -39,10 +43,16 @@ no forwarding shell or alias. The flat package root is retired in stages. `tests/test_package_root_inventory.py` pins the root to an exact file set: 23 -tracked files after C1 and 21 after C2. Each later removal PR shrinks that set -in the same change. The guard does not assert the final two-file root -(`__init__.py` and `__main__.py`) early, and the build-generated `_version.py` -is ignored wheel metadata, not a tracked root file. +tracked files after C1, 21 after C2 and 10 after R1 (#412). The ten are +`__init__.py`, `__main__.py`, `_mindie_read_worker.py`, `arguments.py`, +`benchmark_operations.py`, `cli.py`, `mindie_context.py`, `mindie_mcp.py`, +`mindie_project.py` and `summary_chart.py`. Each later removal PR shrinks that +set in the same change: A1 and A2 (#408) move the CLI, the chart and the +benchmark launcher, and M1 (#452) moves the four MindIE files. The guard does +not assert the final two-file root (`__init__.py` and `__main__.py`) early, +and the build-generated `_version.py` is ignored wheel metadata, not a tracked +root file. [Retired root modules](#retired-root-modules) lists the eleven +paths that R1 removed. The current `src/inferrsi/harness/` tree is a mixed implementation. Its presence is not the target Harness boundary and is not evidence that the four @@ -94,7 +104,7 @@ Several flat modules remain shared seams until their slice starts. | Current path | Tentative reading | This slice | | --- | --- | --- | -| `cli.py`, `arguments.py`, `__main__.py` | Future Apps entry, after #371 | Not moved | +| `cli.py`, `arguments.py`, `__main__.py` | Future Apps entry, after #371 | Not moved; R1 changed only the imports of `cli.py` and `arguments.py`, and A1 (#408) moves both | | `benchmark_operations.py` | Single-task application launcher | Resolves a task entry and invokes the Harness runner; ordinary projects do not use it | | `benchmark/registry.py`, `benchmark/tasks/` | Task discovery, definitions and specialized rules | Task-specific quality and measurement code stays with each task | | `harness/optimization_session.py`, `harness/project.py`, `harness/task_protocol.py` | Shared preparation, execution and retained-result assessment | CLI supplies task resources and knowledge access; ordinary project lifecycle does not import Benchmark | @@ -103,16 +113,16 @@ Several flat modules remain shared seams until their slice starts. | `harness/skill_evaluation.py`, `harness/artifact_sandbox.py`, `harness/improvement.py`, `harness/atomic_json.py` | Skill-evaluation host adapter, trial execution and retention, sandbox replay, search progression | Verdict rules and task definitions arrive as injected values; no Model, Infra, Knowledge, Benchmark or Apps import | | `benchmark/skill_evaluation.py`, `benchmark/skill_eval_diagnostic.py`, `benchmark/skill_eval_engineering.py`, `benchmark/improvement.py` | Skill-evaluation criteria, task definitions and acceptance rules | Pure judgments over retained facts; no Harness import and no process start | | `apps/skill_evaluation.py`, `apps/skill_eval_engineering.py`, `apps/improvement.py` | Composed skill-evaluation entries | Join Knowledge reads, Harness execution and Benchmark rules behind the signatures that `harness/` held before the split | -| `knowledge_operations.py`, `knowledge_store.py`, `catalog.py`, `research.py`, `skill_catalog.py`, `_skill_selection.py`, `_source_snapshot.py` | Static compatibility shells since K01; bodies in `knowledge/` | Interfaces unchanged | +| `knowledge/knowledge_operations.py`, `knowledge/knowledge_store.py`, `knowledge/catalog.py`, `knowledge/research.py`, `knowledge/skill_catalog.py`, `knowledge/_skill_selection.py`, `knowledge/_source_snapshot.py` | The seven Knowledge implementations, moved by K01 | Interfaces unchanged; the same-named root shells were deleted by R1 | | `harness/records/` | Campaign manifests, traces, logs, profile captures and delivered-context captures | Imports no Knowledge or Apps module; `apps/campaign.py` supplies the backend names, the selection default and the context projection | | `apps/views/`, `apps/campaign.py` | Dashboard views, templates and the Knowledge-to-records connection | Apps path still follows the #371 proposal | | `harness/` | Mixed legacy tree, not target isolation | Not redefined | | `harness/runner.py`, `harness/scenarios/external.py`, `harness/atomic_json.py` | Old run execution facts and Harness-owned JSON writes | Ports injected; no Model edge, and no Infra edge except the local process executor | | `benchmark/legacy_run_v0.py`, `benchmark/sampling.py` | Old run gates and metric validation; image/video sample collection | Imports only `model/protocols/metrics`; owns its rule inputs and verdict labels | | `apps/legacy/` | Old run composition, old `evaluate` entry and HTML report | Supplies the Git source snapshot from `infra/targets`; path follows the confirmed split, #371 still proposed | -| `evaluation.py`, `execution.py`, `reporting.py` | Root forwarders of the old run chain | Each forwards to one `apps.legacy` module; retired by R01 | +| `apps/inspection.py` | Prepared-environment inspection bound to this host | Binds the `infra/inspection.py` facts API to `infra.commands.probe_command` and `infra.targets.repository_identity`; replaces root `inspection.py`, which R1 deleted with the three old-run root forwarders | | `model/validators/` | Benchmark-leaning checks | Not moved | -| `infra/`, `inspection.py` | Seams used by the current tree | Not a fourth core | +| `infra/` | Seams used by the current tree, including the `infra/inspection.py` facts API | Not a fourth core; reaches no Knowledge, Harness, Benchmark or Apps module | | `summary_chart.py` | Current report helper | Not moved | | `contracts/` (`runspec.py`, `json_v1.py`, `claim.py`, `evidence.py`) | Technical-only contracts: RunSpec, canonical JSON, verdict vocabulary, evidence segment and path rules | Exact module set; the initializer re-exports only `RunSpec`; not a fourth core | | `apps/evidence_layout.py` | Evidence directory creation over the `contracts/evidence.py` path rules | The `mkdir` half of former root `evidence_paths.py`; host I/O stays in Apps | @@ -124,23 +134,30 @@ Several flat modules remain shared seams until their slice starts. `tests/test_target_module_import_gate.py` fails on a new import of `inferrsi.knowledge`, `inferrsi.benchmark`, or `inferrsi.apps` from anywhere except application assembly, and on Knowledge or Benchmark importing Harness. -Application assembly includes `inferrsi.apps` and the two current flat entries -`cli.py` and `benchmark_operations.py`. Other flat modules retain the same -restrictions. The gate checks Benchmark task scripts as well as module code. +Application assembly includes `inferrsi.apps` and the three current flat +entries `cli.py`, `arguments.py` and `benchmark_operations.py`, listed by exact +file name as `APPLICATION_ENTRIES` in `tests/_gate_policy_support.py`. +`arguments.py` joined that list in R1: the CLI parser names +`inferrsi.knowledge` itself now that the root shells are gone. The listing is +temporary. A1 (#408) moves `cli.py` and `arguments.py` into `apps/cli/` and +removes both entries, and A2 removes the last one. Other flat modules retain +the same restrictions. The gate checks Benchmark task scripts as well as +module code. `from inferrsi import knowledge` counts as an import of that core. Same-core imports are allowed, including a relative import rewritten to an absolute name such as `from .bar import VALUE` inside `knowledge/`. Each forbidden -importer and target pair is reported once. The only exceptions are the exact -first-wave pairs below, read from `docs/legacy-knowledge-manifest.json`. +importer and target pair is reported once. The only exceptions are exact pairs +read from `compatibility_pairs` in `docs/legacy-knowledge-manifest.json`. +Since R1 that list is empty, so the gate admits no exception. The Benchmark package now provides resource discovery and task scripts. The CLI and single-task application launcher assemble them with Harness; task scripts receive execution support without importing another core. "There is no exception list of current edges" described the tree before K01, when the three package names were unused and no current edge needed an -exception. It was never a permanent ban on reviewed exceptions; since K01 the -exact first-wave Knowledge pairs below are the only Knowledge compatibility -exceptions, and they remain scoped to that move. +exception. It was never a permanent ban on reviewed exceptions. From K01 until +R1 the nine exact first-wave pairs recorded below were the only compatibility +exceptions, scoped to that move. R1 retired them. The gate scans `src/inferrsi/**` as the transfer graph. It resolves absolute, relative, and `from package import name` imports, includes the parent-package @@ -176,41 +193,58 @@ checked. These declarations do not make arbitrary candidate code isolated. The report adds a `runtime resource loaders` blocker when these loaders are present and does not claim either complete isolation or new-core closure -isolation. The seven Knowledge shells and their nine exact pairs are unchanged. +isolation. R1 changed none of these five declarations; closing them is R3 (#412). -The old run chain keeps three root forwarders, declared as -`LEGACY_RUN_FORWARDERS` in `tests/_gate_policy_support.py`. They are separate -from the Knowledge compatibility pairs and are not in the manifest. Each root -module may import exactly one Apps module: +R1 (#412) deleted the three root forwarders of the old run chain and removed +their `LEGACY_RUN_FORWARDERS` registration from `tests/_gate_policy_support.py`. +With the seven Knowledge shells and root `inspection`, their names are now +`RETIRED_ROOT_MODULES` in that file. Nothing is registered for them: -| Root module | Exact target | -| --- | --- | -| `evaluation` | `inferrsi.apps.legacy.evaluation` | -| `execution` | `inferrsi.apps.legacy.run` | -| `reporting` | `inferrsi.apps.legacy.reporting` | +- A file or package that returns under one of the eleven names is a violation + by itself, whatever it contains. +- A returning forwarder or shell also fails as a root-file import of Apps or + Knowledge, as any unlisted flat module does. +- Every checked closure still treats each name as an old root leaf. A + Knowledge, Harness, Benchmark or Apps module that imports one fails, with or + without a file behind the name. -An adjacent Apps module and a second root importer both fail. A Harness or -Benchmark module that reaches a forwarder reaches Apps and fails. An Apps module -may not import a forwarder back. Every forwarder that exists adds the -`legacy run forwarders` blocker, so overall isolation stays false until R01 -retires them. This rule is Proposed and needs formal gate review. +No `legacy run forwarders` blocker exists any more. +[Retired root modules](#retired-root-modules) gives each replacement. A clean result means no new cross-core import of those names. It does not mean the modules are isolated. Isolation stays false while any one of these holds: `knowledge/`, `benchmark/`, or `apps/` is absent or has no scannable module; the compatibility allowlist -is non-empty; a registered old-run root forwarder exists; or +is non-empty; a temporary neutral-leaf importer still imports the leaf; `harness/**` still imports Model or Infra, -directly or through another module. The three packages may appear in stages. +directly or through another module; a declared runtime resource loader is +present; or the scan reports a violation. The three packages may appear in stages. A clean graph for the new cores is reported separately as "new core business closure isolated" and never sets overall `isolation_complete` to true. +On the tree after R1, `first_wave_report()` has no violation and three +blockers, so `isolation_complete` is false and R1 does not claim otherwise: + +| Blocker | Cause | Closed by | +| --- | --- | --- | +| `temporary leaf importers: 1` | Root `arguments.py` reads `REPO_TYPES` from the neutral leaf | A1 (#408) | +| `harness reaches model/infra` (12 hits) | `agent_sessions`, `candidates`, `optimization`, `optimization_session`, `project` and `task_protocol` each reach `inferrsi.infra` and `inferrsi.infra.artifacts` | R2 (#412) | +| `runtime resource loaders: 5` | The five declared loaders in the table above | R3 (#412) | + +R1 cleared the `allowlist` and `legacy run forwarders` blockers. The loaders +also keep `new_core_closure_isolated` false. + `knowledge-model-policy` stays the only required status. Until a separate process decision makes this import gate required, every migration PR attaches the gate result and does not describe it as branch protection. ## First-wave compatibility exceptions (Proposed) +Status after R1 (#412): none of these exceptions is in force. The seven root +shells are deleted and `compatibility_pairs` in the manifest is empty. This +section keeps the record of the nine pairs and the rules that still apply. The +heading is unchanged because other pages link to it. + These exceptions implement D018/D020 in [`contracts/gate-policy-runstore-and-first-prs.md`](contracts/gate-policy-runstore-and-first-prs.md) and @@ -218,8 +252,8 @@ and They stay Proposed until this policy passes formal architecture review. K01 must not start before that review accepts them. -Relative to `src/inferrsi/`, exactly nine (importer, target) pairs are -permitted: +Relative to `src/inferrsi/`, exactly nine (importer, target) pairs were +permitted from K01 until R1. None is permitted now: | Importer | Exact target | Route | | --- | --- | --- | @@ -233,23 +267,30 @@ permitted: | `knowledge_operations` | `inferrsi.knowledge.knowledge_operations` | Same-named Knowledge module | | `infra.inspection` | `inferrsi.resources.repository_packs` | Direct, never through `catalog` or H | -These nine pairs are the only compatibility exceptions. A module prefix does +These nine pairs were the only compatibility exceptions. A module prefix did not authorize an adjacent module, and a second importer of an allowed -Knowledge target fails. C2 (#451) redirected the two data-leaf targets from +Knowledge target failed. C2 (#451) redirected the two data-leaf targets from `inferrsi._repo_pack_data` to `inferrsi.resources.repository_packs`; the count -stays nine. Retiring the eight root-shell pairs and replacing the -`infra.inspection` edge with a permanent exact rule outside this allowlist is -later R1 work (#412) and is not done by C2. -`inferrsi.resources.repository_packs` has exactly two permitted -importers outside Knowledge: root `catalog` and `infra.inspection`. An import -of that leaf from inside `inferrsi.knowledge`, for example from -`inferrsi/knowledge/catalog.py`, is intra-closure and not a tenth exception. +stayed nine. R1 (#412) then ended at zero pairs. The first eight rows were +edges of the seven root shells, two of them from root `catalog`, and retired +when R1 deleted those files. The ninth row is not a root-file edge: +`infra/inspection.py` imports the leaf and calls it, so no deletion removes +it. It left the allowlist and is the +[permanent rule below](#permanent-neutral-leaf-importer-rule-proposed). +The manifest keeps all nine records and root `catalog`'s two routes, +unchanged, under its last `revision_history` entry as +`retired_compatibility_pairs` and `retired_catalog_two_routes`. +An import of the leaf from inside `inferrsi.knowledge`, for example from +`inferrsi/knowledge/catalog.py`, is intra-closure and was never an exception. The leaf's own import rule and `REPO_TYPES` object identity govern it. -`REPO_TYPES` is the same tuple object through root `catalog`, the leaf, and -`inferrsi.knowledge.catalog`. - -- **Shell shape.** An old root leaf that imports its Knowledge target or the - data leaf is a compatibility shell. It may contain only a module docstring, +`REPO_TYPES` is the same tuple object in the leaf, +`inferrsi.knowledge.catalog` and root `arguments.py`. + +- **Shell shape.** No shell exists after R1. The rule is kept for a pair that + review stages again, and `tests/test_first_wave_isolation_policy.py` still + drives it with the nine retired pair records. A staged root leaf that imports + its Knowledge target or the data leaf is a compatibility shell. It may + contain only a module docstring, a `__future__` import, `ImportFrom` lines from its listed targets with listed names (an alias only as `name as name`), and an `__all__` that is a literal list of constant strings. Star imports, function or class bodies, @@ -270,7 +311,8 @@ The leaf's own import rule and `REPO_TYPES` object identity govern it. not arbitrary `contracts.*` or `resources.*` modules, and `contracts.evidence` is not a Knowledge input. Benchmark modules and every `inferrsi.harness.*` module reach no other core, no Apps module, and no old root leaf. Apps reaches no old root - leaf. `inferrsi.infra` + leaf. Since R1 an old root leaf is any of the eleven retired root module + names, with or without a file behind it. `inferrsi.infra` reaches no Knowledge, Harness, Benchmark, or Apps module, so `infra` → root `catalog` → Knowledge fails while `infra` → the data leaf passes. - **New H entries.** The registered entry modules are @@ -327,6 +369,114 @@ provenance. The nine pairs remain, so the allowlist blocker remains. That manifest revision requires formal architecture review; the implementer does not self-approve it, and passing tests are not that review. +R1 (#412) deleted the seven root shells in the change that moved their last +consumers to `inferrsi.knowledge`, so the manifest's `compatibility_pairs` is +now empty and the allowlist blocker is gone. The C2 revision joined +`revision_history` with the nine retired pair records and root `catalog`'s two +routes, each unchanged, including the old root leaf hashes that K01 recorded. +`technical_leaf` keeps its module, file, locked `file_sha256`, allowed imports, +two symbols, top-level shape, both fragments and the K02 and C2 revision +records. It gains `permanent_importers` and `temporary_importers`, described +in the next section. The K01, K02 and C2 paragraphs above stay as provenance +and describe the tree at their own revisions. This R1 manifest revision +requires formal architecture review; the implementer does not self-approve +it, and passing tests are not that review. + +## Permanent neutral-leaf importer rule (Proposed) + +Outside `inferrsi.knowledge`, exactly one module may import +`inferrsi.resources.repository_packs`: + +| Importer | Bound name | Exact import line | +| --- | --- | --- | +| `inferrsi.infra.inspection` | `repository_pack` | `from ..resources.repository_packs import repository_pack` | + +This is D020's direct read: Infra gets one pack description without reaching +Knowledge or Harness. The rule is `LEAF_IMPORTERS` in +`tests/_gate_policy_support.py`, restated as +`technical_leaf.permanent_importers` in the manifest. +`tests/test_knowledge_compatibility_shells.py` requires the two to match, and +requires that line to be the file's only import under `inferrsi.resources`. +The rule is not a `compatibility_pairs` entry. It adds no blocker and does not +retire, because the importer is not a shell. + +The rule covers that one importer, target, name and shape, and nothing wider: + +- Any other module outside Knowledge that imports the leaf fails as a second + importer. That includes another `infra` module, `infra/__init__.py`, an Apps, + Harness or Benchmark module and any root file. +- In the declared importer, another leaf name, an alias, a star import, the + leaf imported as a module object, an import below module level, a + `sys.modules` use and a dynamic import each fail. +- An `inferrsi.resources.` prefix allows nothing: the package itself and a + module beside the leaf are refused, for Knowledge too. The rule adds no + `inferrsi.contracts.` allowance either; that package keeps its own exact + module list. +- The leaf's locks are unchanged: its bytes and `file_sha256`, the listed + standard-library imports, the two symbols, the top-level shape and the + docstring-only parent initializer `resources/__init__.py`. +- A forwarding module under a retired name, a re-export through the leaf's + initializer and a dynamic import from an undeclared module fail. + +`tests/test_first_wave_isolation_policy.py` holds these negative controls. One +of them removes the declaration and shows that the scan then rejects the +`infra.inspection` edge, so the edge passes by the rule and not by a relaxed +scan. + +One temporary entry sits beside the rule and is not part of it. Root `catalog` +is gone, so root `arguments.py` binds `REPO_TYPES` with +`from .resources.repository_packs import REPO_TYPES` for the `--repo-type` +choices. It is `TEMPORARY_LEAF_IMPORTERS` in the gate and +`technical_leaf.temporary_importers` in the manifest, held to the same exact +shape. It names root `arguments.py` only: the parser at an Apps path, or any +other Apps module, is not covered. While it exists the report carries the +`temporary leaf importers` blocker. A1 (#408) deletes root `arguments.py` and +removes the entry in the same change; how the moved parser reads `REPO_TYPES` +is decided by that change's review. + +The permanent rule and the temporary entry are Proposed. Both require formal +architecture review. The implementer does not self-approve them, and passing +tests are not that review. + +## Retired root modules + +R1 (#412) deleted these eleven root modules with no forwarding shell, +`sys.modules` alias or import fallback: + +| Retired root module | Canonical module | +| --- | --- | +| `inferrsi._skill_selection` | `inferrsi.knowledge._skill_selection` | +| `inferrsi._source_snapshot` | `inferrsi.knowledge._source_snapshot` | +| `inferrsi.catalog` | `inferrsi.knowledge.catalog`; `REPO_TYPES` and `repository_pack` from `inferrsi.resources.repository_packs` | +| `inferrsi.knowledge_operations` | `inferrsi.knowledge.knowledge_operations` | +| `inferrsi.knowledge_store` | `inferrsi.knowledge.knowledge_store` | +| `inferrsi.research` | `inferrsi.knowledge.research` | +| `inferrsi.skill_catalog` | `inferrsi.knowledge.skill_catalog` | +| `inferrsi.evaluation` | `inferrsi.apps.legacy.evaluation` | +| `inferrsi.execution` | `inferrsi.apps.legacy.run` | +| `inferrsi.reporting` | `inferrsi.apps.legacy.reporting` | +| `inferrsi.inspection` | `inferrsi.apps.inspection` | + +Root `inspection.py` was not an alias. It bound the Infra facts API to the +host, and `apps/inspection.py` now holds that binding: `_command` is +`infra.commands.probe_command` and `repository_identity` is +`infra.targets.repository_identity`, both read from the module on each call, +so a test replaces either name there. `infra/inspection.py` is unchanged and +imports no Apps module. Root `execution.py` defined the two-argument +`run_experiment`; `apps.legacy.run.run_experiment` is that entry, and an +omitted `execute` or `snapshot` port is read from the module when the run +starts. + +Importing a retired path raises `ModuleNotFoundError`, in external code too, +and a pickle that names a class or function by one no longer loads. +[`CHANGELOG.md`](../CHANGELOG.md) states the full import and pickle impact. +Frozen evidence is not rewritten; a historical campaign replays with its +original bundle. + +The root now holds the ten files listed under +[Target modules](#target-modules). The final two-file root waits for the +A1/A2 (#408) and M1 (#452) moves. + ## Old `run` exit codes Frozen from diff --git a/docs/operations.md b/docs/operations.md index be3d6b2e4..b5031ebe6 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -126,7 +126,7 @@ result does not mean that no checkout or store knowledge exists. See the - **Side effects:** none; no network requests, hardware probes, model download or engine execution. Target annotations do not add runtime backends. - **Failure behavior:** no match returns an explicit empty envelope and exits 0; invalid limits/memory or unsupported catalog filters exit 2. A zero-match dimension stays visible. - **Example:** `inferrsi knowledge research MiniMax-H3 --framework mlx --target-backend metal --target-device "M5 Mac Studio" --target-memory-gib 64 --limit 12` -- **Python:** `from inferrsi.research import research_knowledge`; `research_knowledge("MiniMax-H3", frameworks=["mlx"], modalities=["video"], limit=12)`. +- **Python:** `from inferrsi.knowledge.research import research_knowledge`; `research_knowledge("MiniMax-H3", frameworks=["mlx"], modalities=["video"], limit=12)`. Exact `knowledge search` and `context` still AND query tokens. Independent research queries broaden discovery without relaxing the active package filters. diff --git a/docs/reference-matrix.md b/docs/reference-matrix.md index b53e9f979..38c4f35a9 100644 --- a/docs/reference-matrix.md +++ b/docs/reference-matrix.md @@ -255,7 +255,7 @@ SSD/OS systems columns. | InferMatrixLab/InferRSI PR #9 @ 4ad551a9 | reference | `skill_library/` layout + knowledge records | `npu-skill-factory` (bundle layout) | | RRSI — arXiv 2609.24972v3 | search; mined via reviewed Pack F | bounded harness proposals; calibrated noise/cost/novelty gates; grouped-ablation limits, replacing the earlier generic constraint summary | `optimization-search-policy`; [exact ledger](../src/inferrsi/knowledge/data/skill_library/methods/optimization-search-policy/references/papers-20261015/references/source-notes.md#f1--rrsi) | | arXiv 2608.01867 (CRISP) | search | critical-step classification | `optimization-search-policy` | -| **A1 / Agent Skills** @ `69ef37e9424c0a7ea9dd2293b559e43ec8176379` | reference; Dot A-approved minimal consumption | Specification frontmatter/body/progressive disclosure and client guide catalog/activation/resource sections directly verified 2026-10-15; format guidance, not an execution permission grant | `src/inferrsi/skill_catalog.py`, existing CLI/arguments; metadata projection + explicit selection + resource resolution; no installer/router/client configuration | +| **A1 / Agent Skills** @ `69ef37e9424c0a7ea9dd2293b559e43ec8176379` | reference; Dot A-approved minimal consumption | Specification frontmatter/body/progressive disclosure and client guide catalog/activation/resource sections directly verified 2026-10-15; format guidance, not an execution permission grant | `src/inferrsi/knowledge/skill_catalog.py`, existing CLI/arguments; metadata projection + explicit selection + resource resolution; no installer/router/client configuration | | **CR6 / Stas Bekman ML Engineering Open Book** @ `ee7517e0f562ff0e7d56c3e88c619d0c0c496ba7` (CC BY-SA 4.0 declared by source skill) | expert methodology | `skills/ml-engineering/SKILL.md` scoped principles, `skills/evaluate-cluster/SKILL.md` first 100 lines, targeted performance README MFU/HFU/NUMA/GC sections; scripts identified only, excluded storage/input/recovery lanes not mined | `training-resource-budget` + explicitly attributed/share-alike `references/stas-decision-notes.md`; Accelerator/RAM/Network/OS diagnosis and proposed verification, no local results | ### Reviewed paper methodology and cases diff --git a/docs/skill-coverage.md b/docs/skill-coverage.md index ab2806100..4dcb54ccb 100644 --- a/docs/skill-coverage.md +++ b/docs/skill-coverage.md @@ -4,7 +4,7 @@ Generated from the registry, the skill sidecars and the two maintained matrices. **Do not edit by hand**; regenerate after any registry, skill, matrix or frozen-plan change. -- Input digest: `sha256:fbc4d29e40a03beefb6c204f5defd7f817f5d5f68fb102926b3e746bc8801eee` +- Input digest: `sha256:fa0002a1fd489bb9d70a4462ed2e941b98f5641e2cc7ef72a682ed0c5a0d6bb9` - Regenerate: `python tools/build_skill_coverage.py` - Verify without writing: `python tools/build_skill_coverage.py --check` diff --git a/src/inferrsi/_skill_selection.py b/src/inferrsi/_skill_selection.py deleted file mode 100644 index 23309a0fc..000000000 --- a/src/inferrsi/_skill_selection.py +++ /dev/null @@ -1,30 +0,0 @@ -# Compatibility shell. The implementation lives in inferrsi.knowledge._skill_selection. -from __future__ import annotations - -from inferrsi.knowledge._skill_selection import ( - RELEVANCE_METHODS, - TASK_RELEVANCE, - SkillIndexError, - context_view, - indexed_skill, - lexical_score, - raw_string_lexical_score, - record_context, - skill_score, - task_score, - words, -) - -__all__ = [ - "RELEVANCE_METHODS", - "SkillIndexError", - "words", - "lexical_score", - "raw_string_lexical_score", - "indexed_skill", - "skill_score", - "context_view", - "TASK_RELEVANCE", - "task_score", - "record_context", -] diff --git a/src/inferrsi/_source_snapshot.py b/src/inferrsi/_source_snapshot.py deleted file mode 100644 index b898386dc..000000000 --- a/src/inferrsi/_source_snapshot.py +++ /dev/null @@ -1,26 +0,0 @@ -# Compatibility shell. The implementation lives in inferrsi.knowledge._source_snapshot. -from __future__ import annotations - -from inferrsi.knowledge._source_snapshot import ( - LIBRARY, - MANIFEST, - REGISTRY, - REPOSITORY, - STORED_SCHEMA, - _snapshot, - compact, - manifest, - resolve, -) - -__all__ = [ - "MANIFEST", - "REPOSITORY", - "LIBRARY", - "REGISTRY", - "STORED_SCHEMA", - "manifest", - "compact", - "resolve", - "_snapshot", -] diff --git a/src/inferrsi/apps/inspection.py b/src/inferrsi/apps/inspection.py new file mode 100644 index 000000000..100544770 --- /dev/null +++ b/src/inferrsi/apps/inspection.py @@ -0,0 +1,33 @@ +"""Prepared-environment inspection bound to this host. + +``infra.inspection.inspect_environment`` is the facts API: it takes the command +probe and the repository snapshot as ports. This composition supplies the real +host probe and the Git repository identity. Infra imports nothing from here. +""" + +from ..infra.commands import probe_command as _command +from ..infra.inspection import inspect_environment as _inspect_environment +from ..infra.targets import repository_identity + + +def inspect_environment( + repo: str, + repo_type: str, + backend: str, + *, + device_ids: tuple[str, ...] = (), + device_model: str | None = None, + python: str | None = None, +) -> dict: + # Both ports are read from this module on every call, so replacing + # ``_command`` or ``repository_identity`` here changes the next inspection. + return _inspect_environment( + repo, + repo_type, + backend, + device_ids=device_ids, + device_model=device_model, + python=python, + command=_command, + snapshot=repository_identity, + ) diff --git a/src/inferrsi/apps/legacy/run.py b/src/inferrsi/apps/legacy/run.py index 41b302f3f..2a52fb639 100644 --- a/src/inferrsi/apps/legacy/run.py +++ b/src/inferrsi/apps/legacy/run.py @@ -42,10 +42,16 @@ def run_experiment( spec: RunSpec, environment: dict, *, - execute: Callable[[tuple[str, ...], Path, Path, float, str | None], dict] = execute_trial, - snapshot: Callable[[Path, Path | None], dict] = repository_identity, + execute: Callable[[tuple[str, ...], Path, Path, float, str | None], dict] | None = None, + snapshot: Callable[[Path, Path | None], dict] | None = None, ) -> Path: """Run paired commands, judge them with the v0 gates and write the report.""" + # An omitted port is read from this module on every call, as the report writer + # below already is. The retired root ``execution`` entry bound the same two + # ports that way, so replacing ``execute_trial`` or ``repository_identity`` + # here changes the next two-argument run. + execute = execute_trial if execute is None else execute + snapshot = repository_identity if snapshot is None else snapshot def assess(baseline, candidate, validation, integrity, error): return _evaluate( diff --git a/src/inferrsi/arguments.py b/src/inferrsi/arguments.py index c1eb23666..4d63775a5 100644 --- a/src/inferrsi/arguments.py +++ b/src/inferrsi/arguments.py @@ -8,9 +8,10 @@ from pathlib import Path from . import __version__ -from ._skill_selection import RELEVANCE_METHODS -from .catalog import BACKENDS, KNOWLEDGE_REPOS, KNOWLEDGE_SCOPES, REPO_TYPES from .contracts import RunSpec +from .knowledge._skill_selection import RELEVANCE_METHODS +from .knowledge.catalog import BACKENDS, KNOWLEDGE_REPOS, KNOWLEDGE_SCOPES +from .resources.repository_packs import REPO_TYPES def _fixed_attempt_args( diff --git a/src/inferrsi/catalog.py b/src/inferrsi/catalog.py deleted file mode 100644 index 0f26b96f3..000000000 --- a/src/inferrsi/catalog.py +++ /dev/null @@ -1,32 +0,0 @@ -# Compatibility shell. Knowledge names live in inferrsi.knowledge.catalog; -# REPO_TYPES and repository_pack live in the neutral inferrsi.resources.repository_packs. -from __future__ import annotations - -from inferrsi.knowledge.catalog import ( - BACKENDS, - KNOWLEDGE_REPOS, - KNOWLEDGE_SCOPES, - SCENARIO_ALIASES, - _validate_record, - _walk, - files, - get_knowledge, - knowledge_records, - search_knowledge, -) -from inferrsi.resources.repository_packs import REPO_TYPES, repository_pack - -__all__ = [ - "REPO_TYPES", - "KNOWLEDGE_SCOPES", - "KNOWLEDGE_REPOS", - "BACKENDS", - "SCENARIO_ALIASES", - "knowledge_records", - "search_knowledge", - "get_knowledge", - "repository_pack", - "_walk", - "_validate_record", - "files", -] diff --git a/src/inferrsi/cli.py b/src/inferrsi/cli.py index f74747dfe..5c2880b12 100644 --- a/src/inferrsi/cli.py +++ b/src/inferrsi/cli.py @@ -9,14 +9,14 @@ from pathlib import Path from .apps.campaign import capture_context, create_campaign +from .apps.inspection import inspect_environment from .apps.knowledge import knowledge_reader from .apps.knowledge import read as read_knowledge +from .apps.legacy.reporting import write_report from .arguments import device_ids, make_spec, parser -from .catalog import get_knowledge, search_knowledge -from .inspection import inspect_environment -from .knowledge_operations import KnowledgeReader -from .reporting import write_report -from .research import research_knowledge +from .knowledge.catalog import get_knowledge, search_knowledge +from .knowledge.knowledge_operations import KnowledgeReader +from .knowledge.research import research_knowledge def _print(value: object) -> None: @@ -216,7 +216,7 @@ def main(argv: list[str] | None = None) -> int: if args.command == "skills": # Optional dependencies load only for the skills command; the core # CLI stays independent of them. - from .skill_catalog import ( + from .knowledge.skill_catalog import ( get_skill, skill_instructions, skill_records, @@ -544,7 +544,7 @@ def main(argv: list[str] | None = None) -> int: # The old run loads its Benchmark gates. Import it here so that the # other commands, including ordinary optimization, never load Benchmark. - from .execution import run_experiment + from .apps.legacy.run import run_experiment directory = run_experiment(spec, environment) (directory / "args.json").write_text( diff --git a/src/inferrsi/evaluation.py b/src/inferrsi/evaluation.py deleted file mode 100644 index 5cf84eae8..000000000 --- a/src/inferrsi/evaluation.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Compatibility imports for the old run evaluator, composed in apps.legacy.""" - -from .apps.legacy.evaluation import _summary, evaluate, read_metric - -__all__ = ["evaluate", "read_metric", "_summary"] diff --git a/src/inferrsi/execution.py b/src/inferrsi/execution.py deleted file mode 100644 index 4ffd048f7..000000000 --- a/src/inferrsi/execution.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Compatibility entry points for the local experiment runner. - -The old run is composed in apps.legacy.run. Existing imports and legacy -injection points continue to delegate to the same implementations. -""" - -from pathlib import Path - -from .apps.legacy.run import execute_trial as _execute -from .apps.legacy.run import run_experiment as _run_experiment -from .contracts import RunSpec -from .harness.local_process import _stop_tree, _WindowsJob -from .infra.targets import repository_identity - -__all__ = ["run_experiment", "repository_identity", "_execute", "_WindowsJob", "_stop_tree"] - - -def run_experiment(spec: RunSpec, environment: dict) -> Path: - return _run_experiment(spec, environment, execute=_execute, snapshot=repository_identity) diff --git a/src/inferrsi/inspection.py b/src/inferrsi/inspection.py deleted file mode 100644 index e7079b826..000000000 --- a/src/inferrsi/inspection.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Compatibility entry point for prepared-environment inspection.""" - -from .infra.commands import probe_command as _command -from .infra.inspection import inspect_environment as _inspect_environment - - -def inspect_environment( - repo: str, - repo_type: str, - backend: str, - *, - device_ids: tuple[str, ...] = (), - device_model: str | None = None, - python: str | None = None, -) -> dict: - from .execution import repository_identity - - return _inspect_environment( - repo, - repo_type, - backend, - device_ids=device_ids, - device_model=device_model, - python=python, - command=_command, - snapshot=repository_identity, - ) diff --git a/src/inferrsi/knowledge_operations.py b/src/inferrsi/knowledge_operations.py deleted file mode 100644 index fab038591..000000000 --- a/src/inferrsi/knowledge_operations.py +++ /dev/null @@ -1,35 +0,0 @@ -# Compatibility shell. The implementation lives in inferrsi.knowledge.knowledge_operations; -# patch implementation globals such as _json and _hash on that module. -from __future__ import annotations - -from inferrsi.knowledge.knowledge_operations import ( - AUTHORITIES, - IDENTITY, - INCIDENTAL, - Bundle, - IdentityError, - KnowledgeReader, - RevisionConflict, - _hash, - _json, - _normalized, - _sources, - _tree, - safe_path, -) - -__all__ = [ - "AUTHORITIES", - "IDENTITY", - "RevisionConflict", - "IdentityError", - "safe_path", - "Bundle", - "KnowledgeReader", - "INCIDENTAL", - "_json", - "_hash", - "_normalized", - "_sources", - "_tree", -] diff --git a/src/inferrsi/knowledge_store.py b/src/inferrsi/knowledge_store.py deleted file mode 100644 index 9c699ca48..000000000 --- a/src/inferrsi/knowledge_store.py +++ /dev/null @@ -1,6 +0,0 @@ -# Compatibility shell. The implementation lives in inferrsi.knowledge.knowledge_store. -from __future__ import annotations - -from inferrsi.knowledge.knowledge_store import KnowledgeStore, _protected - -__all__ = ["KnowledgeStore", "_protected"] diff --git a/src/inferrsi/reporting.py b/src/inferrsi/reporting.py deleted file mode 100644 index 4faa29811..000000000 --- a/src/inferrsi/reporting.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Compatibility import for HTML evidence reports.""" - -from .apps.legacy.reporting import write_report - -__all__ = ["write_report"] diff --git a/src/inferrsi/research.py b/src/inferrsi/research.py deleted file mode 100644 index 45c0852c5..000000000 --- a/src/inferrsi/research.py +++ /dev/null @@ -1,6 +0,0 @@ -# Compatibility shell. The implementation lives in inferrsi.knowledge.research. -from __future__ import annotations - -from inferrsi.knowledge.research import PROCEDURE, _profile, research_knowledge - -__all__ = ["research_knowledge", "_profile", "PROCEDURE"] diff --git a/src/inferrsi/skill_catalog.py b/src/inferrsi/skill_catalog.py deleted file mode 100644 index ac38ab091..000000000 --- a/src/inferrsi/skill_catalog.py +++ /dev/null @@ -1,49 +0,0 @@ -# Compatibility shell. The implementation lives in inferrsi.knowledge.skill_catalog; -# importing it still requires the optional PyYAML 'skills' extra. -from __future__ import annotations - -from inferrsi.knowledge.skill_catalog import ( - _applicability, - _directory, - _effective, - _entry, - _frontmatter, - _indexed_directories, - _metadata, - _references, - _sidecar, - _skill_name, - _strings, - _unique_fields, - get_skill, - skill_directories, - skill_directory, - skill_instructions, - skill_metadata, - skill_records, - skill_reference, - skill_resource, -) - -__all__ = [ - "skill_directories", - "skill_directory", - "skill_metadata", - "skill_records", - "get_skill", - "skill_instructions", - "skill_resource", - "skill_reference", - "_frontmatter", - "_skill_name", - "_directory", - "_unique_fields", - "_indexed_directories", - "_entry", - "_sidecar", - "_references", - "_strings", - "_applicability", - "_effective", - "_metadata", -] diff --git a/tests/_gate_policy_support.py b/tests/_gate_policy_support.py index 570de349e..f09c433f1 100644 --- a/tests/_gate_policy_support.py +++ b/tests/_gate_policy_support.py @@ -1,4 +1,4 @@ -"""First-wave Knowledge compatibility gate helpers (P0b). +"""First-wave isolation gate helpers (P0b, revised through #412 R1). Imported by the import-gate and isolation-policy tests. This is a test support module, not a runtime package. @@ -19,7 +19,10 @@ CORE_NAMES = ("knowledge", "harness", "benchmark", "apps") # Current application seams, until the Apps package migration. These are not # Knowledge compatibility shells and do not grant access to other flat modules. -APPLICATION_ENTRIES = ("cli.py", "benchmark_operations.py") +# #412 R1 retired the root shells that ``arguments.py`` read its Knowledge choices +# through, so the CLI parser is listed beside the CLI it belongs to. A1 (#408) moves +# both files into ``apps/cli/`` and removes both entries. +APPLICATION_ENTRIES = ("cli.py", "arguments.py", "benchmark_operations.py") # Reviewed execution boundaries: owner function (or module scope) and the exact # local spec variable. They execute task resources/candidate code, not a hidden # core compatibility import. Their presence keeps isolation incomplete. @@ -30,15 +33,47 @@ "inferrsi.benchmark.tasks.qwen21-denoise-npu.benchmark": ("load", "spec"), "inferrsi.benchmark.tasks.qwen3-tts-npu.benchmark": ("", "_evaluation_spec"), } -# B01: the old run chain's three root forwarders, each to one exact Apps legacy -# module. They are not Knowledge shells and are not in the manifest. A forwarder -# that exists keeps overall false until R01 retires it. An adjacent Apps module -# or a second root importer is not covered. -LEGACY_RUN_FORWARDERS = ( - ("inferrsi.evaluation", "inferrsi.apps.legacy.evaluation"), - ("inferrsi.execution", "inferrsi.apps.legacy.run"), - ("inferrsi.reporting", "inferrsi.apps.legacy.reporting"), +# #412 R1 deleted these eleven root modules: the seven Knowledge shells, the three +# old-run forwarders and the root inspection binding. None is registered any more. +# A file that returns under one of these names is a violation by itself, and every +# checked closure still treats the name as an old root leaf it may not reach. +RETIRED_ROOT_MODULES = frozenset( + { + "inferrsi._skill_selection", + "inferrsi._source_snapshot", + "inferrsi.catalog", + "inferrsi.knowledge_operations", + "inferrsi.knowledge_store", + "inferrsi.research", + "inferrsi.skill_catalog", + "inferrsi.evaluation", + "inferrsi.execution", + "inferrsi.reporting", + "inferrsi.inspection", + } ) +# #412 R1, permanent: the one importer of the neutral data leaf outside +# ``inferrsi.knowledge``, with the only name it may bind (D020: Infra reads one pack +# description and reaches no Knowledge or Harness). This is not a manifest +# ``compatibility_pairs`` entry: it never counts toward the allowlist blocker and it +# does not retire. It covers this one module and this one name, bound by a plain +# module-level ``from import `` line. Another importer, another name, an +# alias, the leaf imported as a module object, a nested import, a ``sys.modules`` use +# or a dynamic import fails. No ``inferrsi.resources.`` prefix is ever allowed. +# ``technical_leaf.permanent_importers`` in the manifest restates this rule and +# records that it needs formal architecture review. +LEAF_IMPORTERS = { + "inferrsi.infra.inspection": frozenset({"repository_pack"}), +} +# #412 R1, temporary and due in A1 (#408): root ``catalog`` is retired, so the root CLI +# parser takes the one REPO_TYPES tuple from the leaf itself. The entry is held to the +# same exact shape as the permanent rule and names one module and one name. It is not +# part of that rule and not an Apps-wide allowance. While a listed module imports the +# leaf, the report carries a ``temporary leaf importers`` blocker, so isolation cannot +# be reported complete on it. A1 deletes root ``arguments.py`` and removes this entry. +TEMPORARY_LEAF_IMPORTERS = { + "inferrsi.arguments": frozenset({"REPO_TYPES"}), +} # New H entries: module paths whose closures count toward the new-core result. # H01 added ``run_store`` and ``errors``; H02 added the public ``api`` with # ``execution_service``, ``input_binding`` and ``local_process``. @@ -187,9 +222,13 @@ def exact_pairs(manifest: dict[str, Any]) -> frozenset[tuple[str, str]]: def shell_sources(manifest: dict[str, Any]) -> dict[str, dict[str, set[str]]]: - """Old root leaf -> {exact target: names that leaf may re-export}.""" + """Staged root shell -> {exact target: names that shell may re-export}. + + Empty since #412 R1 emptied ``compatibility_pairs``. A pair that review stages + again is held to the same shell shape as before. + """ leaf = manifest["technical_leaf"]["module"] - leaf_names = set(manifest["catalog_two_routes"]["neutral_leaf_export"]["symbols"]) + leaf_names = set(manifest["technical_leaf"]["two_symbols"]) sources: dict[str, dict[str, set[str]]] = {} for item in manifest["compatibility_pairs"]: if _core_of(item["target"]) == "knowledge": @@ -472,6 +511,58 @@ def _leaf_errors( return errors +def _leaf_importer_errors( + module: str, tree: ast.Module, package: str, leaf: str, names: frozenset[str] +) -> list[str]: + """A declared leaf importer: listed names, plain module-level from-imports only. + + Nothing else under the leaf's package is covered: the leaf as a module object, the + package itself and a module beside the leaf all fail. + """ + parent = leaf.rpartition(".")[0] + top_level = {id(node) for node in tree.body} + errors: list[str] = [] + + def outside(target: str) -> list[str]: + if target == leaf: + return [f"{module} imports {leaf} as a module object"] + if _matches(target, parent): + return [f"{module} imports {target} beside {leaf}"] + return [] + + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + errors += outside(alias.name) + elif isinstance(node, ast.ImportFrom): + base = _from_base(node, package) + if base != leaf: + for alias in node.names: + errors += outside(f"{base}.{alias.name}") + continue + if id(node) not in top_level: + errors.append(f"{module} imports {leaf} below module level") + for alias in node.names: + if alias.name == "*": + errors.append(f"{module} has a star import from {leaf}") + elif alias.name not in names: + errors.append(f"{module} imports unlisted name {leaf}.{alias.name}") + elif alias.asname not in (None, alias.name): + errors.append(f"{module} aliases {alias.name} as {alias.asname}") + for node in ast.walk(tree): + if ( + isinstance(node, ast.Attribute) + and node.attr == "modules" + and isinstance(node.value, ast.Name) + and node.value.id == "sys" + ): + errors.append(f"{module} touches sys.modules") + break + if _has_dynamic_import(tree): + errors.append(f"{module} has a dynamic import") + return errors + + def _initializer_errors(module: str, tree: ast.Module, package: str) -> list[str]: """A parent initializer of the data leaf: a docstring and no other statement. @@ -571,14 +662,22 @@ def first_wave_report( pairs = exact_pairs(manifest) leaf = manifest["technical_leaf"]["module"] sources = shell_sources(manifest) - old_leaves = set(sources) + # A retired root module stays an old root leaf for every closure rule below. + old_leaves = set(sources) | RETIRED_ROOT_MODULES graph = _scan(root, pairs) modules = sorted(graph.trees) - forwarders = frozenset(LEGACY_RUN_FORWARDERS) - forwarder_leaves = {importer for importer, _ in forwarders} - violations = cross_core_violations(root, pairs | forwarders) + violations = cross_core_violations(root, pairs) + violations += [ + f"{name} is a retired root module" + for name in modules + if name in RETIRED_ROOT_MODULES and name not in sources + ] + # Outside Knowledge the leaf has its one permanent importer, the temporary entry + # due in A1 and any staged pair that names it. Every other importer fails. + declared_importers = {**TEMPORARY_LEAF_IMPORTERS, **LEAF_IMPORTERS} leaf_importers = {importer for importer, target in pairs if target == leaf} + leaf_importers |= set(declared_importers) violations += [ f"{name} is a second importer of {leaf}" for name in modules @@ -586,8 +685,12 @@ def first_wave_report( and name not in leaf_importers and _core_of(name) != "knowledge" ] - for name in sorted(old_leaves & set(modules)): - # An old leaf that reaches into K or the data leaf is a shell. + for name in sorted(set(declared_importers) & set(modules)): + violations += _leaf_importer_errors( + name, graph.trees[name], graph.packages[name], leaf, declared_importers[name] + ) + for name in sorted(set(sources) & set(modules)): + # A staged root leaf that reaches into K or the data leaf is a shell. if any(_core_of(t) == "knowledge" or t == leaf for t in graph.edges[name]): violations += _shell_errors( name, graph.trees[name], graph.packages[name], sources[name] @@ -635,9 +738,9 @@ def h_entry_forbidden(node: str) -> bool: rules: dict[str, Callable[[str], bool]] = { "knowledge": outside_knowledge, "benchmark": benchmark_forbidden, - # Apps calls core public APIs and never passes through an old root leaf - # or one of its own root forwarders. - "apps": lambda node: node in old_leaves or node in forwarder_leaves, + # Apps calls core public APIs and never passes through an old root leaf, + # which since #412 R1 includes the three retired old-run forwarders. + "apps": lambda node: node in old_leaves, } starts = [(name, rules[_core_of(name)]) for name in modules if _core_of(name) in rules] starts += [(name, h_entry_forbidden) for name in h_entries if name in graph.trees] @@ -668,14 +771,15 @@ def h_entry_forbidden(node: str) -> bool: elif not any(package.rglob("*.py")): # An empty directory is not a present, scannable package (A01-4). blockers.append(f"unscannable: {name}/") + # #412 R1 left the manifest with no pair. One that review stages again blocks. if pairs: blockers.append(f"allowlist: {len(pairs)} compatibility pairs") - forwarding = [ - importer for importer, target in LEGACY_RUN_FORWARDERS - if target in graph.edges.get(importer, ()) + temporary = [ + name for name in sorted(TEMPORARY_LEAF_IMPORTERS) + if leaf in graph.edges.get(name, ()) ] - if forwarding: - blockers.append(f"legacy run forwarders: {len(forwarding)}") + if temporary: + blockers.append(f"temporary leaf importers: {len(temporary)}") legacy = [ path for name in modules diff --git a/tests/architecture_real_ports.py b/tests/architecture_real_ports.py index 9303eb345..2ed1bbd1f 100644 --- a/tests/architecture_real_ports.py +++ b/tests/architecture_real_ports.py @@ -16,10 +16,10 @@ from pathlib import Path from unittest.mock import patch +from inferrsi.apps.legacy.evaluation import evaluate from inferrsi.contracts import RunSpec -from inferrsi.evaluation import evaluate from inferrsi.harness.scenarios.external import execute_trial -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.knowledge_operations import KnowledgeReader from tests.architecture_closed_loop import _canonical, _digest # Installed package card. Its bundle is record.json only, so this read does diff --git a/tests/check_installed.py b/tests/check_installed.py index 6982c085c..79abc2255 100644 --- a/tests/check_installed.py +++ b/tests/check_installed.py @@ -13,12 +13,13 @@ import inferrsi import inferrsi.harness from inferrsi.benchmark.registry import builtin_tasks, describe_task, task_resources -from inferrsi.catalog import REPO_TYPES, knowledge_records, repository_pack, search_knowledge from inferrsi.cli import main from inferrsi.contracts import RunSpec, claim, evidence, json_v1 from inferrsi.knowledge._source_snapshot import BUNDLE_LIBRARY, LIBRARY -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.catalog import knowledge_records, search_knowledge +from inferrsi.knowledge.knowledge_operations import KnowledgeReader from inferrsi.resources import repository_packs +from inferrsi.resources.repository_packs import REPO_TYPES, repository_pack assert Path(inferrsi.__file__).resolve().parent.parent.name != "src", ( "This check requires an installed package" @@ -615,6 +616,48 @@ def _inventory(root, prefix): for retired in ("_repo_pack_data", "evidence_paths"): assert find_spec(f"inferrsi.{retired}") is None, retired assert not installed_root & {"_repo_pack_data.py", "evidence_paths.py"} + +# #412 R1: the seven root Knowledge shells, the three old-run forwarders and the root +# inspection binding are not in the wheel and have no alias. Their implementations ship +# at the canonical Knowledge and Apps paths, and the wheel root is the exact ten files +# plus the build-generated version metadata. +r1_retired = { + "_skill_selection": "inferrsi.knowledge._skill_selection", + "_source_snapshot": "inferrsi.knowledge._source_snapshot", + "catalog": "inferrsi.knowledge.catalog", + "knowledge_operations": "inferrsi.knowledge.knowledge_operations", + "knowledge_store": "inferrsi.knowledge.knowledge_store", + "research": "inferrsi.knowledge.research", + "skill_catalog": "inferrsi.knowledge.skill_catalog", + "evaluation": "inferrsi.apps.legacy.evaluation", + "execution": "inferrsi.apps.legacy.run", + "reporting": "inferrsi.apps.legacy.reporting", + "inspection": "inferrsi.apps.inspection", +} +assert len(r1_retired) == 11 +for retired, canonical in r1_retired.items(): + assert find_spec(f"inferrsi.{retired}") is None, retired + assert f"inferrsi.{retired}" not in sys.modules, retired + assert not hasattr(inferrsi, retired), retired + assert find_spec(canonical) is not None, canonical + try: + import_module(f"inferrsi.{retired}") + except ModuleNotFoundError: + pass + else: + raise AssertionError(f"retired module inferrsi.{retired} must not import") +assert not installed_root & {f"{name}.py" for name in r1_retired} +assert installed_root - {"_version.py"} == { + "__init__.py", "__main__.py", "_mindie_read_worker.py", "arguments.py", + "benchmark_operations.py", "cli.py", "mindie_context.py", "mindie_mcp.py", + "mindie_project.py", "summary_chart.py", +}, sorted(installed_root) +host_inspection = import_module("inferrsi.apps.inspection") +assert host_inspection._command is import_module("inferrsi.infra.commands").probe_command +assert host_inspection.repository_identity is ( + import_module("inferrsi.infra.targets").repository_identity +) +assert import_module("inferrsi.arguments").REPO_TYPES is REPO_TYPES # A fresh isolated interpreter reads all seven packs with only the facade and its # import-free parent loaded: no Harness, Knowledge, Infra or Apps module. facade_probe = ( diff --git a/tests/test_architecture_closed_loop.py b/tests/test_architecture_closed_loop.py index 8ce862e5f..5e61cdcb2 100644 --- a/tests/test_architecture_closed_loop.py +++ b/tests/test_architecture_closed_loop.py @@ -11,7 +11,7 @@ from pathlib import Path from unittest.mock import patch -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.knowledge_operations import KnowledgeReader from tests.architecture_closed_loop import MemoryArchive, _canonical, _digest, run_loop from tests.architecture_real_ports import ( PACKAGE_READ, @@ -24,6 +24,23 @@ PORTS = Path(__file__).with_name("architecture_real_ports.py") LAYOUT = Path(__file__).resolve().parents[1] / "docs" / "module-layout.md" ROOT = Path(__file__).resolve().parents[1] +# Families the CPU/offline assembler must not couple to: the CLI, its parser, the +# Harness runner, and Apps, Knowledge or Benchmark as packages. +BANNED_FAMILIES = ( + "inferrsi.cli", + "inferrsi.arguments", + "inferrsi.harness.runner", + "inferrsi.apps", + "inferrsi.knowledge", + "inferrsi.benchmark", +) +# #412 R1 deleted the root shells through which the assembler reached Knowledge and the +# old evaluator, so those two seams are now canonical modules inside two banned +# families. They are exempt by exact module name only. The package above a seam, a +# module beside it and a module below it stay banned. +EXACT_SEAMS = frozenset( + {"inferrsi.knowledge.knowledge_operations", "inferrsi.apps.legacy.evaluation"} +) class FakeKnowledge: @@ -357,27 +374,25 @@ def test_real_cpu_offline_ports_connect_three_refs(self) -> None: def test_real_ports_stay_on_three_existing_seams(self) -> None: imported = _import_modules(PORTS) - self.assertIn("inferrsi.knowledge_operations", imported) + self.assertIn("inferrsi.knowledge.knowledge_operations", imported) self.assertIn("inferrsi.harness.scenarios.external", imported) - self.assertIn("inferrsi.evaluation", imported) - banned = ( - "inferrsi.cli", - "inferrsi.arguments", - "inferrsi.harness.runner", - "inferrsi.apps", - "inferrsi.knowledge", - "inferrsi.benchmark", - ) - for name in imported: - for prefix in banned: - self.assertFalse(name == prefix or name.startswith(prefix + ".")) + self.assertIn("inferrsi.apps.legacy.evaluation", imported) + # The six banned families are unchanged. Two of the three seams are canonical + # Knowledge and Apps modules since #412 R1; the guard exempts those two exact + # names and nothing else in either family. + source = PORTS.read_text(encoding="utf-8") + self.assertEqual(_seam_violations(source), []) + # Each exempt name is a seam this assembler really imports; none is spare. + self.assertLessEqual(EXACT_SEAMS, set(imported)) + # The guard reads every name the earlier per-module view read, and more. + self.assertLessEqual(set(imported), set(_named_modules(source))) calls = _call_names(PORTS) self.assertIn("read", calls) self.assertIn("execute_trial", calls) self.assertIn("evaluate", calls) self.assertNotIn("search_knowledge", calls) self.assertNotIn("collect", calls) - # The root module is a static shell; check the implementation's imports. + # #412 R1 deleted the root shell; these are the implementation's own imports. knowledge = _import_modules(ROOT / "src/inferrsi/knowledge/knowledge_operations.py") harness = _import_modules(ROOT / "src/inferrsi/harness/scenarios/external.py") self.assertFalse(any("harness" in name or "evaluation" in name for name in knowledge)) @@ -386,6 +401,134 @@ def test_real_ports_stay_on_three_existing_seams(self) -> None: self.assertIn("does not call `collect` or `search_knowledge`", text) self.assertIn("campaign `PASS`", text) + def test_seam_guard_exempts_only_the_two_exact_seams(self) -> None: + # The guard that passes the real assembler above, on single import lines. + allowed = ( + "from inferrsi.knowledge.knowledge_operations import KnowledgeReader\n", + "from inferrsi.apps.legacy.evaluation import evaluate\n", + "import inferrsi.knowledge.knowledge_operations\n", + "import inferrsi.apps.legacy.evaluation as old_evaluate\n", + "from inferrsi.harness.scenarios.external import execute_trial\n", + "from inferrsi.contracts import RunSpec\n", + "import json\nfrom pathlib import Path\n", + ) + for source in allowed: + with self.subTest(allowed=source): + self.assertEqual(_seam_violations(source), []) + refused = ( + # A module beside a seam, including one whose name only starts like it. + ("from inferrsi.apps.legacy.run import run_experiment\n", "inferrsi.apps.legacy.run"), + ("from inferrsi.apps.legacy.reporting import write_report\n", + "inferrsi.apps.legacy.reporting"), + ("import inferrsi.apps.legacy.evaluation_extra\n", + "inferrsi.apps.legacy.evaluation_extra"), + ("from inferrsi.knowledge.catalog import search_knowledge\n", + "inferrsi.knowledge.catalog"), + ("from inferrsi.knowledge.knowledge_store import KnowledgeStore\n", + "inferrsi.knowledge.knowledge_store"), + ("import inferrsi.knowledge.knowledge_operations_v2\n", + "inferrsi.knowledge.knowledge_operations_v2"), + # Another Apps or Knowledge module that is no seam of this assembler. + ("from inferrsi.apps.inspection import inspect_environment\n", + "inferrsi.apps.inspection"), + ("from inferrsi.apps.api import read_attempt_view\n", "inferrsi.apps.api"), + ("from inferrsi.knowledge.skill_catalog import get_skill\n", + "inferrsi.knowledge.skill_catalog"), + # The package above a seam, by any spelling. Binding a seam as a module + # object names its package, so only the plain from-import form passes. + ("import inferrsi.apps\n", "inferrsi.apps"), + ("import inferrsi.apps.legacy\n", "inferrsi.apps.legacy"), + ("import inferrsi.knowledge\n", "inferrsi.knowledge"), + ("from inferrsi import apps\n", "inferrsi.apps"), + ("from inferrsi import knowledge\n", "inferrsi.knowledge"), + ("from inferrsi.apps import legacy\n", "inferrsi.apps.legacy"), + ("from inferrsi.apps.legacy import evaluation\n", "inferrsi.apps.legacy"), + ("from inferrsi.knowledge import knowledge_operations\n", "inferrsi.knowledge"), + ("from inferrsi.knowledge import *\n", "inferrsi.knowledge"), + # A module below a seam. + ("import inferrsi.apps.legacy.evaluation.rules\n", + "inferrsi.apps.legacy.evaluation.rules"), + ("from inferrsi.apps.legacy.evaluation.rules import VALUE\n", + "inferrsi.apps.legacy.evaluation.rules"), + ("import inferrsi.knowledge.knowledge_operations.bundle\n", + "inferrsi.knowledge.knowledge_operations.bundle"), + # An alias, a nested import and a seam on the line before change nothing. + ("import inferrsi.apps.legacy.run as evaluation\n", "inferrsi.apps.legacy.run"), + ("def late():\n from inferrsi.knowledge.catalog import search_knowledge\n", + "inferrsi.knowledge.catalog"), + ("from inferrsi.apps.legacy.evaluation import evaluate\n" + "from inferrsi.apps.legacy import run\n", "inferrsi.apps.legacy.run"), + ) + for source, expected in refused: + with self.subTest(refused=source): + self.assertIn(expected, _seam_violations(source)) + # A name taken from a seam is an attribute while no module of that name exists + # below it. If a seam ever becomes a package, such a module is refused too. + self.assertTrue(_is_module("inferrsi.apps.legacy.evaluation")) + self.assertTrue(_is_module("inferrsi.apps.legacy")) + self.assertFalse(_is_module("inferrsi.apps.legacy.evaluation.evaluate")) + below = "from inferrsi.apps.legacy.evaluation import rules\n" + self.assertEqual(_seam_violations(below), []) + with patch.object(sys.modules[__name__], "_is_module", return_value=True): + self.assertEqual( + _seam_violations(below), ["inferrsi.apps.legacy.evaluation.rules"] + ) + + def test_seam_guard_keeps_the_cli_parser_runner_and_benchmark_bans(self) -> None: + refused = ( + ("from inferrsi.cli import main\n", "inferrsi.cli"), + ("import inferrsi.cli\n", "inferrsi.cli"), + ("from inferrsi import cli\n", "inferrsi.cli"), + ("from inferrsi.arguments import parser\n", "inferrsi.arguments"), + ("import inferrsi.arguments as options\n", "inferrsi.arguments"), + ("from inferrsi import arguments\n", "inferrsi.arguments"), + ("from inferrsi.harness.runner import run_experiment\n", "inferrsi.harness.runner"), + ("import inferrsi.harness.runner as loop\n", "inferrsi.harness.runner"), + ("from inferrsi.harness import runner\n", "inferrsi.harness.runner"), + ("from inferrsi.benchmark.legacy_run_v0 import evaluate\n", + "inferrsi.benchmark.legacy_run_v0"), + ("import inferrsi.benchmark\n", "inferrsi.benchmark"), + ("from inferrsi import benchmark\n", "inferrsi.benchmark"), + ("from inferrsi.benchmark import registry\n", "inferrsi.benchmark.registry"), + ("def late():\n import inferrsi.cli\n", "inferrsi.cli"), + ) + for source, expected in refused: + with self.subTest(refused=source): + self.assertIn(expected, _seam_violations(source)) + # Harness is not banned whole: only its runner is, and a lookalike is not it. + self.assertEqual(_seam_violations("from inferrsi.harness import atomic_json\n"), []) + self.assertEqual(_seam_violations("import inferrsi.harness.runner_notes\n"), []) + + def test_real_assembler_fails_the_same_guard_once_a_coupling_is_added(self) -> None: + text = PORTS.read_text(encoding="utf-8") + seam = "from inferrsi.apps.legacy.evaluation import evaluate\n" + self.assertEqual(text.count(seam), 1) + self.assertEqual(_seam_violations(text), []) + mutations = ( + (text.replace(seam, "from inferrsi.apps.legacy.run import run_experiment\n"), + "inferrsi.apps.legacy.run"), + (text.replace(seam, "from inferrsi.apps.legacy import evaluation\n"), + "inferrsi.apps.legacy"), + (text + "\nfrom inferrsi.knowledge.catalog import search_knowledge\n", + "inferrsi.knowledge.catalog"), + (text + "\nfrom inferrsi.cli import main\n", "inferrsi.cli"), + (text + "\nfrom inferrsi.arguments import parser\n", "inferrsi.arguments"), + (text + "\nfrom inferrsi.harness.runner import run_experiment\n", + "inferrsi.harness.runner"), + (text + "\nfrom inferrsi.benchmark.legacy_run_v0 import evaluate\n", + "inferrsi.benchmark.legacy_run_v0"), + ) + for source, expected in mutations: + with self.subTest(expected=expected): + self.assertIn(expected, _seam_violations(source)) + # Both families are still banned whole: without the exact exemption, the two + # canonical seams of the unchanged assembler are refused as well. + seams = sorted(EXACT_SEAMS) + with patch.object(sys.modules[__name__], "EXACT_SEAMS", frozenset()): + unexempted = _seam_violations(text) + for name in seams: + self.assertIn(name, unexempted) + def _import_modules(path: Path) -> list[str]: tree = ast.parse(path.read_text(encoding="utf-8")) @@ -411,5 +554,45 @@ def _call_names(path: Path) -> set[str]: return names +def _is_module(name: str) -> bool: + path = ROOT.joinpath("src", *name.split(".")) + return path.with_name(path.name + ".py").is_file() or (path / "__init__.py").is_file() + + +def _named_modules(source: str) -> list[str]: + """Every module a source may import. + + This holds each name ``_import_modules`` records, and adds ``package.name`` for an + absolute ``from package import name``, which that helper cannot see. A name taken + from an exact seam module is one of its attributes, so it is added only when a + module of that name exists below the seam. + """ + names: list[str] = [] + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.Import): + names.extend(alias.name for alias in node.names) + elif isinstance(node, ast.ImportFrom) and node.module: + names.append(node.module) + if node.level: + continue + for alias in node.names: + candidate = f"{node.module}.{alias.name}" + if alias.name != "*" and ( + node.module not in EXACT_SEAMS or _is_module(candidate) + ): + names.append(candidate) + return names + + +def _seam_violations(source: str) -> list[str]: + """Banned-family modules that assembler source imports, other than an exact seam.""" + return [ + name + for name in _named_modules(source) + if name not in EXACT_SEAMS + and any(name == prefix or name.startswith(prefix + ".") for prefix in BANNED_FAMILIES) + ] + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_ascend_video_diffusion_skill.py b/tests/test_ascend_video_diffusion_skill.py index 0bf208eb3..62e9e11c9 100644 --- a/tests/test_ascend_video_diffusion_skill.py +++ b/tests/test_ascend_video_diffusion_skill.py @@ -5,7 +5,7 @@ import pytest -from inferrsi.skill_catalog import get_skill, skill_instructions, skill_records +from inferrsi.knowledge.skill_catalog import get_skill, skill_instructions, skill_records ROOT = Path(__file__).resolve().parents[1] SKILL_ROOT = ROOT / "src/inferrsi/knowledge/data/skill_library" diff --git a/tests/test_bundle_manifest.py b/tests/test_bundle_manifest.py index c3a6d2257..83f54bf33 100644 --- a/tests/test_bundle_manifest.py +++ b/tests/test_bundle_manifest.py @@ -5,7 +5,7 @@ import pytest from inferrsi.knowledge import knowledge_operations -from inferrsi.knowledge_operations import Bundle, RevisionConflict +from inferrsi.knowledge.knowledge_operations import Bundle, RevisionConflict def _hash(data): diff --git a/tests/test_campaign_record_injection.py b/tests/test_campaign_record_injection.py index 6b250c02e..8842bfd75 100644 --- a/tests/test_campaign_record_injection.py +++ b/tests/test_campaign_record_injection.py @@ -16,10 +16,9 @@ from tests._gate_policy_support import ( NEW_H_ENTRIES, REPO, + RETIRED_ROOT_MODULES, ROOT, first_wave_report, - load_manifest, - shell_sources, ) TEXT = "Capture repeated launches.\n" @@ -364,7 +363,9 @@ def _record_modules(): def test_harness_records_load_no_knowledge_or_apps_module(): - leaves = sorted(shell_sources(load_manifest())) + # #412 R1 retired the root shells; none of those names may be loaded either. + leaves = sorted(RETIRED_ROOT_MODULES) + assert len(leaves) == 11 script = ( "import importlib, sys\n" f"for name in {_record_modules()!r}:\n" diff --git a/tests/test_campaign_task_selection.py b/tests/test_campaign_task_selection.py index dd9e102cb..5d1a2da1a 100644 --- a/tests/test_campaign_task_selection.py +++ b/tests/test_campaign_task_selection.py @@ -1,7 +1,7 @@ import json from inferrsi.apps.campaign import capture_context, create_campaign, verify_capture -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.knowledge_operations import KnowledgeReader BASELINE_CUE = "reproducible diffusion baseline correctness fixed seed end to end benchmark" diff --git a/tests/test_cli_catalog.py b/tests/test_cli_catalog.py index 55265e667..f9649da13 100644 --- a/tests/test_cli_catalog.py +++ b/tests/test_cli_catalog.py @@ -13,11 +13,11 @@ import pytest -from inferrsi import catalog from inferrsi.arguments import make_spec, parser -from inferrsi.catalog import get_knowledge, knowledge_records, repository_pack, search_knowledge from inferrsi.cli import main from inferrsi.knowledge import catalog as knowledge_catalog +from inferrsi.knowledge.catalog import get_knowledge, knowledge_records, search_knowledge +from inferrsi.resources.repository_packs import REPO_TYPES, repository_pack PROJECT = Path(__file__).resolve().parents[1] SOURCE_REPOSITORIES = { @@ -66,7 +66,7 @@ def test_unknown_and_invalid_search_parameters(self): get_knowledge("not-a-record") with self.assertRaises(ValueError): search_knowledge(limit=0) - for name in catalog.REPO_TYPES: + for name in REPO_TYPES: pack = repository_pack(name) self.assertEqual(pack["execution_support"], "caller_supplied_external_commands") self.assertTrue( diff --git a/tests/test_collect_limit_serialization.py b/tests/test_collect_limit_serialization.py index 7f8ffd1d8..6afbd1634 100644 --- a/tests/test_collect_limit_serialization.py +++ b/tests/test_collect_limit_serialization.py @@ -2,7 +2,7 @@ import pytest from inferrsi.knowledge import knowledge_operations as operations -from inferrsi.knowledge_operations import Bundle, KnowledgeReader +from inferrsi.knowledge.knowledge_operations import Bundle, KnowledgeReader class CustomBundle(Bundle): diff --git a/tests/test_collect_selection.py b/tests/test_collect_selection.py index 1acf8c7fd..49cb5fde5 100644 --- a/tests/test_collect_selection.py +++ b/tests/test_collect_selection.py @@ -4,10 +4,10 @@ import pytest -from inferrsi._skill_selection import lexical_score from inferrsi.cli import main -from inferrsi.knowledge_operations import Bundle, KnowledgeReader, _hash, _json -from inferrsi.knowledge_store import KnowledgeStore +from inferrsi.knowledge._skill_selection import lexical_score +from inferrsi.knowledge.knowledge_operations import Bundle, KnowledgeReader, _hash, _json +from inferrsi.knowledge.knowledge_store import KnowledgeStore ROOT = Path(__file__).resolve().parents[1] / "src/inferrsi/knowledge/data/skill_library" CGROUP = "inferrsi.common.skills.cgroup_resource_controls" diff --git a/tests/test_contracts_package.py b/tests/test_contracts_package.py index b2ea1e224..533d419ff 100644 --- a/tests/test_contracts_package.py +++ b/tests/test_contracts_package.py @@ -21,7 +21,7 @@ import inferrsi.contracts as contracts import inferrsi.harness as harness -from inferrsi import arguments, execution +from inferrsi import arguments from inferrsi.apps.legacy import evaluation as legacy_evaluation from inferrsi.apps.legacy import run as legacy_run from inferrsi.contracts import RunSpec, claim, json_v1, runspec @@ -125,7 +125,7 @@ def test_initializer_reexports_only_runspec_and_there_is_one_class() -> None: assert contracts.RunSpec is runspec.RunSpec is RunSpec assert (RunSpec.__module__, RunSpec.__qualname__) == ("inferrsi.contracts.runspec", "RunSpec") # Every consumer in the tree holds that same class object. - for consumer in (arguments, execution, legacy_evaluation, legacy_run, runner): + for consumer in (arguments, legacy_evaluation, legacy_run, runner): assert consumer.RunSpec is RunSpec, consumer.__name__ diff --git a/tests/test_first_wave_isolation_policy.py b/tests/test_first_wave_isolation_policy.py index dafe3a698..b93261179 100644 --- a/tests/test_first_wave_isolation_policy.py +++ b/tests/test_first_wave_isolation_policy.py @@ -5,6 +5,7 @@ import unittest from pathlib import Path from typing import Any +from unittest import mock from tests import _gate_policy_support as support from tests._gate_policy_support import ( @@ -18,8 +19,13 @@ MANIFEST = load_manifest() PAIRS = exact_pairs(MANIFEST) -SOURCES = shell_sources(MANIFEST) LEAF = MANIFEST["technical_leaf"]["module"] +# #412 R1 emptied the allowlist. The nine retired pair records stay in the manifest +# history. Staging them again drives the staged-pair rules that the gate still holds. +RETIRED_PAIRS = MANIFEST["revision_history"][-1]["retired_compatibility_pairs"] +STAGED = {**MANIFEST, "compatibility_pairs": RETIRED_PAIRS} +STAGED_PAIRS = exact_pairs(STAGED) +SOURCES = shell_sources(STAGED) EMPTY_ALLOWLIST = {**MANIFEST, "compatibility_pairs": []} # The future neutral leaf, built from the manifest fragment and allowed imports. LEAF_SOURCE = ( @@ -107,6 +113,25 @@ def _k01_tree() -> dict[str, str]: return files +# The two declared importers of the data leaf, as the real tree writes them. +INSPECTION = "from ..resources.repository_packs import repository_pack\n" +ARGUMENTS = ( + "from .knowledge.catalog import BACKENDS\n" + "from .resources.repository_packs import REPO_TYPES\n" +) + + +def _r1_tree() -> dict[str, str]: + """The R1 shape: the K01 tree with no root shell, plus the root CLI parser.""" + files = { + relative: source + for relative, source in _k01_tree().items() + if f"inferrsi.{relative.removesuffix('.py')}" not in SOURCES + } + files["arguments.py"] = ARGUMENTS + return files + + def _report( changes: dict[str, str | None], manifest: dict[str, Any] = MANIFEST, @@ -114,7 +139,7 @@ def _report( base: dict[str, str] | None = None, ) -> IsolationReport: """Scan a temporary tree; a ``None`` change drops that file.""" - files = {**(_k01_tree() if base is None else base), **changes} + files = {**(_r1_tree() if base is None else base), **changes} with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) for relative, source in files.items(): @@ -130,8 +155,13 @@ def _violated(report: IsolationReport, *parts: str) -> bool: return any(all(part in message for part in parts) for message in report.violations) +def _staged(changes: dict[str, str | None]) -> IsolationReport: + """The K01 shape with the nine retired pairs staged again.""" + return _report(changes, manifest=STAGED, base=_k01_tree()) + + class ManifestTest(unittest.TestCase): - def test_allowlist_is_exactly_nine_pairs(self) -> None: + def test_allowlist_is_empty_and_the_nine_retired_pairs_are_recorded(self) -> None: leaves = ( "catalog", "_source_snapshot", @@ -143,16 +173,26 @@ def test_allowlist_is_exactly_nine_pairs(self) -> None: ) same_named = {(f"inferrsi.{n}", f"inferrsi.knowledge.{n}") for n in leaves} leaf_routes = {("inferrsi.catalog", LEAF), ("inferrsi.infra.inspection", LEAF)} - self.assertEqual(len(MANIFEST["compatibility_pairs"]), 9) - self.assertEqual(PAIRS, same_named | leaf_routes) - # #451 C2 redirected the two leaf routes to the canonical module and left - # nothing on the retired root path. + # R1 ends at zero pairs: nothing is staged, so no shell source is declared. + self.assertEqual(MANIFEST["compatibility_pairs"], []) + self.assertEqual(PAIRS, frozenset()) + self.assertEqual(shell_sources(MANIFEST), {}) + # The record of what retired is the exact nine, kept with its export lists. + self.assertEqual(len(RETIRED_PAIRS), 9) + self.assertEqual(STAGED_PAIRS, same_named | leaf_routes) + self.assertEqual({importer for importer, _ in same_named}, set(SOURCES)) + self.assertLess(set(SOURCES), support.RETIRED_ROOT_MODULES) self.assertEqual(LEAF, "inferrsi.resources.repository_packs") self.assertEqual( - MANIFEST["catalog_two_routes"]["neutral_leaf_export"]["target"], LEAF + MANIFEST["revision_history"][-1]["retired_catalog_two_routes"][ + "neutral_leaf_export" + ]["target"], + LEAF, ) + self.assertNotIn("catalog_two_routes", MANIFEST) self.assertFalse( - [pair for pair in PAIRS if "inferrsi._repo_pack_data" in pair], PAIRS + [pair for pair in STAGED_PAIRS if "inferrsi._repo_pack_data" in pair], + STAGED_PAIRS, ) def test_catalog_routes_keep_repository_pack_out_of_knowledge(self) -> None: @@ -170,17 +210,26 @@ def test_fragment_hash_matches_recorded_source(self) -> None: class AllowlistTest(unittest.TestCase): - """A01-1: nine exact (importer, target) pairs and nothing else.""" + """A01-1: only exact (importer, target) pairs pass, and R1 leaves none staged.""" - def test_nine_exact_pairs_pass(self) -> None: + def test_r1_tree_passes_with_no_pair(self) -> None: report = _report({}) self.assertEqual(report.violations, ()) - self.assertNotIn(("inferrsi.knowledge.catalog", LEAF), PAIRS) + self.assertNotIn("allowlist", blocker_kinds(report)) + + def test_nine_staged_pairs_still_pass_and_block(self) -> None: + # The staged-pair rules were kept, not erased: the K01 shape is clean under + # the nine retired pairs, and staging them is what blocks. + report = _staged({}) + self.assertEqual(report.violations, ()) + self.assertNotIn(("inferrsi.knowledge.catalog", LEAF), STAGED_PAIRS) + self.assertEqual(report.blockers, ("allowlist: 9 compatibility pairs",)) + self.assertFalse(report.isolation_complete) def test_adjacent_knowledge_target_fails(self) -> None: shell = _shell("inferrsi.catalog") shell += "from inferrsi.knowledge.catalog_extra import VALUE as VALUE\n" - report = _report({"catalog.py": shell, "knowledge/catalog_extra.py": "VALUE = 1\n"}) + report = _staged({"catalog.py": shell, "knowledge/catalog_extra.py": "VALUE = 1\n"}) self.assertTrue(_violated(report, "catalog.py imports inferrsi.knowledge")) self.assertTrue( _violated(report, "unlisted import from inferrsi.knowledge.catalog_extra") @@ -217,10 +266,11 @@ def test_second_importer_fails(self) -> None: "inferrsi.resources.repository_packs", ), ) - for path, source, expected in cases: - with self.subTest(path=path, source=source): - report = _report({path: source}) - self.assertTrue(_violated(report, expected), report.violations) + for manifest, base in ((MANIFEST, _r1_tree()), (STAGED, _k01_tree())): + for path, source, expected in cases: + with self.subTest(path=path, source=source, pairs=len(exact_pairs(manifest))): + report = _report({path: source}, manifest=manifest, base=base) + self.assertTrue(_violated(report, expected), report.violations) def test_only_declared_infra_importer_may_use_the_leaf(self) -> None: # D020: infra -> the data leaf passes, and only infra.inspection is @@ -228,11 +278,11 @@ def test_only_declared_infra_importer_may_use_the_leaf(self) -> None: report = _report({}) self.assertEqual(report.violations, ()) infra_leaf = { - (importer, target) - for importer, target in PAIRS - if importer.startswith("inferrsi.infra.") and target == LEAF + importer + for importer in (*support.LEAF_IMPORTERS, *support.TEMPORARY_LEAF_IMPORTERS) + if importer.startswith("inferrsi.infra") } - self.assertEqual(infra_leaf, {("inferrsi.infra.inspection", LEAF)}) + self.assertEqual(infra_leaf, {"inferrsi.infra.inspection"}) report = _report( {"infra/other.py": "from ..resources.repository_packs import REPO_TYPES\n"} ) @@ -246,6 +296,228 @@ def test_only_declared_infra_importer_may_use_the_leaf(self) -> None: ) +def _importer_defects(line: str, name: str, other: str) -> tuple[tuple[str, str], ...]: + """Every way a declared importer may not reach the leaf: (source, expected).""" + relative = line.split(" import ")[0].removeprefix("from ") + package = relative.removesuffix(".repository_packs") + bound = f"import {name}" + return ( + (line.replace(bound, f"import {other}"), f"imports unlisted name {LEAF}.{other}"), + ( + line.replace(bound, f"import {name}, {other}"), + f"imports unlisted name {LEAF}.{other}", + ), + (line.replace(bound, f"import {name} as loaded"), f"aliases {name} as loaded"), + (line.replace(bound, "import *"), f"has a star import from {LEAF}"), + (f"from {package} import repository_packs\n", f"imports {LEAF} as a module object"), + ( + f"from {package} import repository_packs as packs\n", + f"imports {LEAF} as a module object", + ), + (f"import {LEAF}\n", f"imports {LEAF} as a module object"), + (f"import {LEAF} as packs\n", f"imports {LEAF} as a module object"), + (line + "import inferrsi.resources\n", f"imports inferrsi.resources beside {LEAF}"), + ( + line + f"from {package}.extra import VALUE\n", + f"imports inferrsi.resources.extra.VALUE beside {LEAF}", + ), + ( + line + f"from {package} import extra\n", + f"imports inferrsi.resources.extra beside {LEAF}", + ), + (f"def load():\n {line} return {name}\n", f"imports {LEAF} below module level"), + ( + line + "import sys\nsys.modules['inferrsi._repo_pack_data'] = sys.modules[__name__]\n", + "touches sys.modules", + ), + ( + line + f"import importlib\nimportlib.import_module('{LEAF}')\n", + "has a dynamic import", + ), + (line + f"__import__('{LEAF}')\n", "has a dynamic import"), + ) + + +class PermanentLeafImporterTest(unittest.TestCase): + """#412 R1: one exact importer of the data leaf, outside the staged allowlist.""" + + def test_the_rule_is_one_importer_and_one_name_and_not_a_pair(self) -> None: + self.assertEqual( + support.LEAF_IMPORTERS, + {"inferrsi.infra.inspection": frozenset({"repository_pack"})}, + ) + self.assertEqual( + support.TEMPORARY_LEAF_IMPORTERS, + {"inferrsi.arguments": frozenset({"REPO_TYPES"})}, + ) + self.assertEqual(MANIFEST["compatibility_pairs"], []) + # No Knowledge, Harness, Benchmark or Apps module is declared, and no prefix. + for importer in (*support.LEAF_IMPORTERS, *support.TEMPORARY_LEAF_IMPORTERS): + self.assertIsNone(support._core_of(importer), importer) + self.assertNotIn("*", importer) + + def test_the_permanent_rule_alone_does_not_block(self) -> None: + # The infra edge is admitted with nothing staged and adds no blocker. + report = _report({"arguments.py": None}) + self.assertEqual(report.violations, ()) + self.assertEqual(report.blockers, ()) + self.assertTrue(report.isolation_complete) + + def test_the_temporary_parser_entry_blocks_until_a1_removes_it(self) -> None: + report = _report({}) + self.assertEqual(report.violations, ()) + self.assertEqual(report.blockers, ("temporary leaf importers: 1",)) + self.assertTrue(report.new_core_closure_isolated) + self.assertFalse(report.isolation_complete) + # The entry names root arguments.py only. The parser at an Apps path, or any + # other Apps module, is not covered by it. + for path in ("apps/cli/arguments.py", "apps/main.py", "cli.py"): + with self.subTest(path=path): + moved = _report( + {"arguments.py": None, path: "from inferrsi.resources.repository_packs " + "import REPO_TYPES\n"} + ) + self.assertTrue(_violated(moved, f"is a second importer of {LEAF}"), + moved.violations) + + def test_the_edge_is_admitted_by_the_rule_and_not_by_an_erased_scan(self) -> None: + with mock.patch.dict(support.LEAF_IMPORTERS, clear=True): + report = _report({}) + self.assertTrue( + _violated(report, f"inferrsi.infra.inspection is a second importer of {LEAF}"), + report.violations, + ) + with mock.patch.dict(support.TEMPORARY_LEAF_IMPORTERS, clear=True): + report = _report({}) + self.assertTrue( + _violated(report, f"inferrsi.arguments is a second importer of {LEAF}"), + report.violations, + ) + + def test_staging_the_infra_pair_again_blocks(self) -> None: + pair = [item for item in RETIRED_PAIRS if item["importer"] == "infra.inspection"] + self.assertEqual(len(pair), 1) + report = _report( + {"arguments.py": None}, manifest={**MANIFEST, "compatibility_pairs": pair} + ) + self.assertEqual(report.violations, ()) + self.assertEqual(report.blockers, ("allowlist: 1 compatibility pairs",)) + + def test_another_importer_fails(self) -> None: + line = "from inferrsi.resources.repository_packs import repository_pack\n" + paths = ( + "infra/other.py", "infra/inspection_helpers.py", "infra/__init__.py", + "infra/platforms/__init__.py", "harness/api.py", "harness/runner.py", + "benchmark/probe.py", "apps/main.py", "apps/inspection.py", "model/probe.py", + "cli.py", "benchmark_operations.py", "helper.py", "catalog.py", + "_repo_pack_data.py", + ) + for path in paths: + with self.subTest(path=path): + report = _report({path: line}) + module = "inferrsi." + path.removesuffix(".py").removesuffix( + "/__init__" + ).replace("/", ".") + self.assertTrue( + _violated(report, f"{module} is a second importer of {LEAF}"), + report.violations, + ) + + def test_declared_importer_shape_is_exact(self) -> None: + declared = ( + ("infra/inspection.py", "inferrsi.infra.inspection", INSPECTION, + "repository_pack", "REPO_TYPES"), + ("arguments.py", "inferrsi.arguments", + "from .resources.repository_packs import REPO_TYPES\n", + "REPO_TYPES", "repository_pack"), + ) + for path, module, line, name, other in declared: + clean = _report({path: line, "resources/extra.py": None}) + self.assertEqual(clean.violations, ()) + for source, expected in _importer_defects(line, name, other): + with self.subTest(module=module, source=source): + report = _report({path: source, "resources/extra.py": "VALUE = 1\n"}) + self.assertTrue( + _violated(report, f"{module} ", expected), report.violations + ) + + def test_hidden_routes_around_the_rule_fail(self) -> None: + dynamic = f"import importlib\nimportlib.import_module('{LEAF}')\n" + cases = ( + # A forwarding module under the retired leaf name, static or star. + ({"_repo_pack_data.py": f"from {LEAF} import *\n"}, + f"inferrsi._repo_pack_data is a second importer of {LEAF}"), + # The retired root catalog route back to the leaf. + ({"catalog.py": _shell("inferrsi.catalog")}, + f"inferrsi.catalog is a second importer of {LEAF}"), + ({"catalog.py": _shell("inferrsi.catalog")}, + "inferrsi.catalog is a retired root module"), + # A re-export through the leaf's own initializer. + ({"resources/__init__.py": RESOURCES_INIT + "from .repository_packs import " + "REPO_TYPES\n"}, + "inferrsi.resources imports inferrsi.resources.repository_packs.REPO_TYPES"), + # A dynamic import from an undeclared Infra, Harness or Knowledge module. + ({"infra/other.py": dynamic}, + "inferrsi.infra.other reaches an undeclared dynamic import"), + ({"harness/api.py": dynamic}, + "inferrsi.harness.api reaches an undeclared dynamic import"), + ({"knowledge/research.py": dynamic}, + "inferrsi.knowledge.research reaches an undeclared dynamic import"), + # A module beside the leaf is not the leaf, for Knowledge either. + ({"resources/extra.py": "VALUE = 1\n", + "knowledge/research.py": "from inferrsi.resources import extra\n"}, + "inferrsi.knowledge.research reaches inferrsi.resources.extra"), + ) + for changes, expected in cases: + with self.subTest(expected=expected): + report = _report(changes) + self.assertTrue(_violated(report, expected), report.violations) + self.assertFalse(report.isolation_complete) + + +class RetiredRootModuleTest(unittest.TestCase): + """#412 R1: the eleven deleted root modules cannot return, as any kind of file.""" + + def test_the_retired_set_is_the_exact_eleven_and_absent_from_the_tree(self) -> None: + self.assertEqual( + support.RETIRED_ROOT_MODULES, + { + "inferrsi._skill_selection", "inferrsi._source_snapshot", "inferrsi.catalog", + "inferrsi.knowledge_operations", "inferrsi.knowledge_store", + "inferrsi.research", "inferrsi.skill_catalog", "inferrsi.evaluation", + "inferrsi.execution", "inferrsi.reporting", "inferrsi.inspection", + }, + ) + for module in support.RETIRED_ROOT_MODULES: + name = module.removeprefix("inferrsi.") + self.assertFalse((support.ROOT / f"{name}.py").exists(), name) + self.assertFalse((support.ROOT / name).exists(), name) + self.assertFalse(hasattr(support, "LEGACY_RUN_FORWARDERS")) + + def test_a_returning_file_fails_whatever_it_holds(self) -> None: + for module in sorted(support.RETIRED_ROOT_MODULES): + name = module.removeprefix("inferrsi.") + bodies = ['"""Returned."""\n', "VALUE = 1\n"] + if module in SOURCES: + bodies.append(_shell(module)) + for path in (f"{name}.py", f"{name}/__init__.py"): + for body in bodies: + with self.subTest(path=path, body=body[:24]): + report = _report({path: body}) + self.assertIn(f"{module} is a retired root module", report.violations) + self.assertFalse(report.isolation_complete) + + def test_a_returning_shell_also_fails_as_a_cross_core_import(self) -> None: + for module in sorted(SOURCES): + name = module.removeprefix("inferrsi.") + with self.subTest(module=module): + report = _report({f"{name}.py": _shell(module)}) + self.assertTrue( + _violated(report, f"{name}.py imports inferrsi.knowledge"), + report.violations, + ) + + SHELL_DEFECTS = ( ("def research_knowledge():\n return None\n", "function or class body research_knowledge"), ("class Shim:\n pass\n", "function or class body Shim"), @@ -279,7 +551,7 @@ class ShellShapeTest(unittest.TestCase): def test_illegal_shell_shapes_fail(self) -> None: for snippet, expected in SHELL_DEFECTS: with self.subTest(snippet=snippet): - report = _report({"research.py": _shell("inferrsi.research") + snippet}) + report = _staged({"research.py": _shell("inferrsi.research") + snippet}) self.assertTrue( _violated(report, "inferrsi.research ", expected), report.violations ) @@ -287,7 +559,7 @@ def test_illegal_shell_shapes_fail(self) -> None: def test_old_leaf_keeping_a_body_beside_its_k_module_fails(self) -> None: # The declared export edge makes root catalog a shell once K.catalog # exists, so a second copy of the body is rejected. - report = _report( + report = _staged( { "catalog.py": "def search_knowledge():\n return []\n", "infra/inspection.py": "from ..catalog import search_knowledge\n", @@ -311,15 +583,25 @@ def test_knowledge_through_old_shell_fails(self) -> None: ) for source in forms: with self.subTest(source=source): - report = _report({"knowledge/research.py": source}) + # The shell is back on disk: Knowledge may not reach it by any form. + report = _report( + {"catalog.py": _shell("inferrsi.catalog"), "knowledge/research.py": source} + ) self.assertTrue( _violated(report, "inferrsi.knowledge.research reaches inferrsi.catalog"), report.violations, ) def test_apps_through_old_shell_fails(self) -> None: - report = _report({"apps/main.py": "from inferrsi.knowledge_store import KnowledgeStore\n"}) - self.assertTrue(_violated(report, "inferrsi.apps.main reaches inferrsi.knowledge_store")) + source = "from inferrsi.knowledge_store import KnowledgeStore\n" + # With or without the file, the retired name is an old root leaf for Apps. + for shell in (None, _shell("inferrsi.knowledge_store")): + with self.subTest(returned=shell is not None): + report = _report({"apps/main.py": source, "knowledge_store.py": shell}) + self.assertTrue( + _violated(report, "inferrsi.apps.main reaches inferrsi.knowledge_store"), + report.violations, + ) def test_data_leaf_accepts_only_listed_stdlib_imports(self) -> None: self.assertFalse(_violated(_report({}), LEAF)) @@ -412,7 +694,12 @@ def test_retired_root_leaf_is_outside_the_knowledge_closure(self) -> None: ) def test_infra_through_root_catalog_fails(self) -> None: - report = _report({"infra/inspection.py": "from ..catalog import repository_pack\n"}) + report = _report( + { + "catalog.py": _shell("inferrsi.catalog"), + "infra/inspection.py": "from ..catalog import repository_pack\n", + } + ) self.assertIn( "inferrsi.infra.inspection reaches inferrsi.knowledge.catalog: " "inferrsi.infra.inspection -> inferrsi.catalog -> inferrsi.knowledge.catalog", @@ -774,6 +1061,7 @@ def test_old_leaf_detection_is_not_sort_dependent(self) -> None: report = _report( { **direct, + "knowledge_operations.py": _shell("inferrsi.knowledge_operations"), f"{name}.py": "from . import knowledge_operations\n", "harness/runner.py": ( f"from . import skill_evaluation\nfrom .. import {name}\n" @@ -870,7 +1158,8 @@ def test_core_submodule_walk_includes_own_package_init(self) -> None: ) for changes, expected in cases: with self.subTest(expected=expected): - report = _report(changes) + # ``from inferrsi import catalog`` names a module only while the file exists. + report = _report({"catalog.py": _shell("inferrsi.catalog"), **changes}) self.assertIn(expected, report.violations, report.violations) def test_returning_skill_evaluation_infra_edge_fails(self) -> None: @@ -929,7 +1218,8 @@ def test_each_trigger_alone_keeps_overall_false(self) -> None: EMPTY_ALLOWLIST), ("absent: benchmark/", {"benchmark/__init__.py": None}, EMPTY_ALLOWLIST), ("absent: apps/", {"apps/__init__.py": None, "apps/main.py": None}, EMPTY_ALLOWLIST), - ("allowlist: 9 compatibility pairs", {}, MANIFEST), + ("allowlist: 9 compatibility pairs", {}, STAGED), + ("temporary leaf importers: 1", {"arguments.py": ARGUMENTS}, EMPTY_ALLOWLIST), ( "harness reaches model/infra: inferrsi.harness.skill_evaluation" " -> inferrsi.infra", @@ -950,13 +1240,14 @@ def test_each_trigger_alone_keeps_overall_false(self) -> None: EMPTY_ALLOWLIST, ), ( - "harness reaches model/infra: inferrsi.harness.runner -> inferrsi.execution" + "harness reaches model/infra: inferrsi.harness.runner -> inferrsi.bridge" " -> inferrsi.model", { - "execution.py": "from inferrsi.model import metrics\n", + # A root helper that is not a retired name: the path alone blocks. + "bridge.py": "from inferrsi.model import metrics\n", "model/__init__.py": "", "model/metrics.py": "", - "harness/runner.py": "from inferrsi import execution\n", + "harness/runner.py": "from inferrsi import bridge\n", }, EMPTY_ALLOWLIST, ), @@ -992,11 +1283,15 @@ def test_empty_package_directories_are_unscannable(self) -> None: self.assertNotIn(f"absent: {name}/", report.blockers) def test_clean_new_core_graph_never_sets_overall_true(self) -> None: - report = _report({}) - self.assertEqual(report.violations, ()) - self.assertTrue(report.new_core_closure_isolated) - self.assertEqual(blocker_kinds(report), {"allowlist"}) - self.assertFalse(report.isolation_complete) + for report, kinds in ( + (_staged({}), {"allowlist"}), + (_report({}), {"temporary leaf importers"}), + ): + with self.subTest(kinds=kinds): + self.assertEqual(report.violations, ()) + self.assertTrue(report.new_core_closure_isolated) + self.assertEqual(blocker_kinds(report), kinds) + self.assertFalse(report.isolation_complete) if __name__ == "__main__": diff --git a/tests/test_flare_knowledge.py b/tests/test_flare_knowledge.py index 85df4d0b9..f33c7d38f 100644 --- a/tests/test_flare_knowledge.py +++ b/tests/test_flare_knowledge.py @@ -1,9 +1,9 @@ import json from pathlib import Path -from inferrsi.catalog import get_knowledge, search_knowledge -from inferrsi.knowledge_operations import KnowledgeReader -from inferrsi.skill_catalog import get_skill, skill_metadata, skill_resource +from inferrsi.knowledge.catalog import get_knowledge, search_knowledge +from inferrsi.knowledge.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.skill_catalog import get_skill, skill_metadata, skill_resource ROOT = Path(__file__).resolve().parents[1] / "src/inferrsi/knowledge/data/skill_library" NAME = "audit-flare-session-lifecycle" diff --git a/tests/test_h3_knowledge.py b/tests/test_h3_knowledge.py index 438b477a6..174670a4d 100644 --- a/tests/test_h3_knowledge.py +++ b/tests/test_h3_knowledge.py @@ -5,10 +5,10 @@ import re from pathlib import Path -from inferrsi.catalog import get_knowledge, search_knowledge from inferrsi.cli import main -from inferrsi.knowledge_operations import KnowledgeReader -from inferrsi.skill_catalog import get_skill, skill_metadata, skill_resource +from inferrsi.knowledge.catalog import get_knowledge, search_knowledge +from inferrsi.knowledge.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.skill_catalog import get_skill, skill_metadata, skill_resource ROOT = Path(__file__).resolve().parents[1] / "src/inferrsi/knowledge/data/skill_library" NAME = "audit-h3-runtime-residency" diff --git a/tests/test_h3_metal_knowledge.py b/tests/test_h3_metal_knowledge.py index c356b91e4..d3c9666af 100644 --- a/tests/test_h3_metal_knowledge.py +++ b/tests/test_h3_metal_knowledge.py @@ -18,9 +18,9 @@ import pytest -from inferrsi.catalog import get_knowledge, search_knowledge -from inferrsi.knowledge_operations import KnowledgeReader -from inferrsi.skill_catalog import get_skill, skill_metadata +from inferrsi.knowledge.catalog import get_knowledge, search_knowledge +from inferrsi.knowledge.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.skill_catalog import get_skill, skill_metadata PROJECT = Path(__file__).resolve().parents[1] LIBRARY = PROJECT / "src/inferrsi/knowledge/data/skill_library" diff --git a/tests/test_hopper_knowledge.py b/tests/test_hopper_knowledge.py index 0e24e4a9c..4baec9002 100644 --- a/tests/test_hopper_knowledge.py +++ b/tests/test_hopper_knowledge.py @@ -5,9 +5,14 @@ from collections import Counter from pathlib import Path -from inferrsi.catalog import get_knowledge, search_knowledge -from inferrsi.knowledge_operations import KnowledgeReader -from inferrsi.skill_catalog import get_skill, skill_metadata, skill_reference, skill_resource +from inferrsi.knowledge.catalog import get_knowledge, search_knowledge +from inferrsi.knowledge.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.skill_catalog import ( + get_skill, + skill_metadata, + skill_reference, + skill_resource, +) ROOT = Path(__file__).resolve().parents[1] / "src/inferrsi/knowledge/data/skill_library" NAME = "reason-hopper-kernels" diff --git a/tests/test_inspection.py b/tests/test_inspection.py index 38b72fef0..13b7a8e90 100644 --- a/tests/test_inspection.py +++ b/tests/test_inspection.py @@ -1,15 +1,26 @@ """Inspection facts must not become accelerator compatibility claims.""" +import importlib +import importlib.util +import inspect import json +import os +import pickle +import subprocess import sys import unittest from importlib import metadata from pathlib import Path from unittest.mock import patch +import pytest + +from inferrsi.apps import inspection as host_inspection +from inferrsi.apps.inspection import inspect_environment from inferrsi.apps.legacy.reporting import write_report +from inferrsi.infra import commands, targets +from inferrsi.infra import inspection as facts from inferrsi.infra.artifacts import atomic_write_json -from inferrsi.inspection import inspect_environment ROCM_INFO = ( "=====================\n" @@ -57,10 +68,10 @@ def command(argv): with ( patch( - "inferrsi.execution.repository_identity", + "inferrsi.apps.inspection.repository_identity", return_value={"available": True, "head": "test"}, ), - patch("inferrsi.inspection._command", side_effect=command), + patch("inferrsi.apps.inspection._command", side_effect=command), ): return inspect_environment(".", "vllm_omni", "cuda", device_ids=ids, device_model=model) @@ -92,10 +103,10 @@ def command(argv): with ( patch( - "inferrsi.execution.repository_identity", + "inferrsi.apps.inspection.repository_identity", return_value={"available": True, "head": "test"}, ), - patch("inferrsi.inspection._command", side_effect=command), + patch("inferrsi.apps.inspection._command", side_effect=command), ): return inspect_environment( ".", "vllm_omni", backend, device_ids=ids, device_model=model @@ -221,5 +232,123 @@ def test_hostname_is_not_captured_in_environment_or_report(tmp_path): assert hostname not in report.read_text(encoding="utf-8") +def _fake_command(argv): + stdout = json.dumps({"python": "test", "packages": {}}) if "-c" in argv else "" + return {"argv": argv, "returncode": 0, "stdout": stdout, "stderr": ""} + + +def test_canonical_binding_is_the_real_host_probe_and_git_identity(): + # #412 R1: the Apps composition binds the Infra facts API to this host. + assert host_inspection._command is commands.probe_command + assert host_inspection.repository_identity is targets.repository_identity + assert host_inspection._inspect_environment is facts.inspect_environment + assert inspect_environment.__module__ == "inferrsi.apps.inspection" + assert str(inspect.signature(inspect_environment)) == ( + "(repo: str, repo_type: str, backend: str, *, device_ids: tuple[str, ...] = (), " + "device_model: str | None = None, python: str | None = None) -> dict" + ) + # The facts API keeps its own injectable ports and their real defaults. + parameters = inspect.signature(facts.inspect_environment).parameters + assert parameters["command"].default is commands.probe_command + assert parameters["snapshot"].default is targets.repository_identity + + +def test_both_ports_are_read_from_the_canonical_module_on_each_call(monkeypatch): + seen = [] + + def snapshot(path): + seen.append(("snapshot", path)) + return {"available": True, "head": "injected"} + + def command(argv): + seen.append(("command", argv[0])) + return _fake_command(argv) + + monkeypatch.setattr(host_inspection, "repository_identity", snapshot) + monkeypatch.setattr(host_inspection, "_command", command) + result = inspect_environment(".", "vllm_omni", "cpu", python="python-probe") + assert seen[0] == ("snapshot", Path(".").resolve()) + assert ("command", "python-probe") in seen + assert result["repository"] == {"available": True, "head": "injected"} + assert result["target_python"]["environment"] == {"python": "test", "packages": {}} + assert result["repository_pack"]["id"] == "vllm_omni" + # Replacing a port again changes the very next call: nothing was captured early. + monkeypatch.setattr( + host_inspection, "repository_identity", lambda path: {"available": False} + ) + assert inspect_environment(".", "vllm_omni", "cpu")["repository"] == {"available": False} + + +def test_port_failures_reach_the_caller_unchanged(monkeypatch): + def broken(path): + raise OSError("repository identity unavailable") + + def crashed(argv): + raise RuntimeError("probe crashed") + + monkeypatch.setattr(host_inspection, "_command", _fake_command) + monkeypatch.setattr(host_inspection, "repository_identity", broken) + with pytest.raises(OSError, match="repository identity unavailable"): + inspect_environment(".", "vllm_omni", "cpu") + monkeypatch.setattr( + host_inspection, "repository_identity", lambda path: {"available": False} + ) + monkeypatch.setattr(host_inspection, "_command", crashed) + with pytest.raises(RuntimeError, match="probe crashed"): + inspect_environment(".", "vllm_omni", "cpu") + # The pack reader still refuses an unknown repository type. + monkeypatch.setattr(host_inspection, "_command", _fake_command) + with pytest.raises(ValueError, match="Unknown repository type: not-a-pack"): + inspect_environment(".", "not-a-pack", "cpu") + + +def test_probe_results_are_recorded_as_the_port_returned_them(monkeypatch): + monkeypatch.setattr( + host_inspection, "repository_identity", lambda path: {"available": False} + ) + missing = {"returncode": None, "stdout": "", "stderr": "Executable not found"} + monkeypatch.setattr(host_inspection, "_command", lambda argv: {"argv": argv, **missing}) + result = inspect_environment(".", "vllm_omni", "cpu", python="absent-python") + assert result["target_python"]["argv"][0] == "absent-python" + assert result["target_python"]["stderr"] == "Executable not found" + assert "environment" not in result["target_python"] + garbled = {"returncode": 0, "stdout": "not json", "stderr": ""} + monkeypatch.setattr(host_inspection, "_command", lambda argv: {"argv": argv, **garbled}) + result = inspect_environment(".", "vllm_omni", "cpu") + assert result["target_python"]["parse_error"] == ( + "Target Python did not emit a JSON environment" + ) + assert result["compatibility"] == "unverified" + + +def test_retired_root_inspection_is_gone_and_infra_loads_no_apps(): + assert importlib.util.find_spec("inferrsi.inspection") is None + with pytest.raises(ModuleNotFoundError): + importlib.import_module("inferrsi.inspection") + # A pickle that names the retired function no longer loads; the canonical one does. + with pytest.raises(ModuleNotFoundError): + pickle.loads(b"cinferrsi.inspection\ninspect_environment\n.") + assert pickle.loads(pickle.dumps(inspect_environment)) is inspect_environment + script = ( + "import sys\n" + "import inferrsi.infra.inspection\n" + "cores = ('inferrsi.apps', 'inferrsi.knowledge', 'inferrsi.harness',\n" + " 'inferrsi.benchmark')\n" + "loaded = [name for name in sys.modules if name.startswith(cores)]\n" + "assert not loaded, loaded\n" + # The retired root binding reached the snapshot through the old run. The + # canonical one loads no Benchmark module and no old-run composition. + "import inferrsi.apps.inspection\n" + "old_run = ('inferrsi.benchmark', 'inferrsi.apps.legacy', 'inferrsi.execution')\n" + "loaded = [name for name in sys.modules if name.startswith(old_run)]\n" + "assert not loaded, loaded\n" + ) + result = subprocess.run( + [sys.executable, "-c", script], capture_output=True, text=True, check=False, + env={**os.environ, "PYTHONPATH": str(Path(__file__).resolve().parents[1] / "src")}, + ) + assert result.returncode == 0, result.stderr + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_knowledge_cli.py b/tests/test_knowledge_cli.py index a91af905d..b0e184145 100644 --- a/tests/test_knowledge_cli.py +++ b/tests/test_knowledge_cli.py @@ -9,8 +9,8 @@ import pytest from inferrsi.cli import main -from inferrsi.knowledge_operations import Bundle, KnowledgeReader -from inferrsi.knowledge_store import KnowledgeStore +from inferrsi.knowledge.knowledge_operations import Bundle, KnowledgeReader +from inferrsi.knowledge.knowledge_store import KnowledgeStore REPO = Path(__file__).resolve().parents[1] SKILLS = REPO / "src/inferrsi/knowledge/data/skill_library" diff --git a/tests/test_knowledge_compatibility_shells.py b/tests/test_knowledge_compatibility_shells.py index 3437563b5..a51b37408 100644 --- a/tests/test_knowledge_compatibility_shells.py +++ b/tests/test_knowledge_compatibility_shells.py @@ -1,47 +1,88 @@ -# K01: root Knowledge names are static re-exports of the moved implementations. +# K01 moved the Knowledge implementations and left static root shells. #412 R1 deleted +# those seven shells: the root names no longer import, the moved modules keep every +# recorded name, and the data leaf keeps its locks and its one permanent importer. from __future__ import annotations import ast import hashlib import importlib +import importlib.util import os +import pickle import subprocess import sys import pytest -from tests._gate_policy_support import REPO, ROOT, blocker_kinds, first_wave_report, load_manifest +from tests._gate_policy_support import ( + LEAF_IMPORTERS, + REPO, + RETIRED_ROOT_MODULES, + ROOT, + TEMPORARY_LEAF_IMPORTERS, + exact_pairs, + first_wave_report, + load_manifest, +) MANIFEST = load_manifest() LEAF = MANIFEST["technical_leaf"] -LEAF_NAMES = MANIFEST["catalog_two_routes"]["neutral_leaf_export"]["symbols"] -MOVED = [item for item in MANIFEST["compatibility_pairs"] if "exports" in item] +# The nine pairs and the two root catalog routes as they stood before R1 retired them. +RETIRED = MANIFEST["revision_history"][-1] +LEAF_NAMES = RETIRED["retired_catalog_two_routes"]["neutral_leaf_export"]["symbols"] +MOVED = [item for item in RETIRED["retired_compatibility_pairs"] if "exports" in item] @pytest.mark.parametrize("item", MOVED, ids=lambda item: item["importer"]) -def test_root_names_are_the_moved_objects(item): - # K01-1: identity carries signatures, defaults, exception types and Path behavior. - root = importlib.import_module(f"inferrsi.{item['importer']}") +def test_retired_root_name_fails_and_the_moved_module_keeps_every_name(item): + retired = f"inferrsi.{item['importer']}" + assert retired in RETIRED_ROOT_MODULES + assert not (ROOT / f"{item['importer']}.py").exists() + assert importlib.util.find_spec(retired) is None + with pytest.raises(ModuleNotFoundError): + importlib.import_module(retired) + assert retired not in sys.modules + assert not hasattr(sys.modules["inferrsi"], item["importer"]) + # K01-1 carried identity through the shell. The same names are now read where + # they are defined, so nothing the shell exported was lost with it. moved = importlib.import_module(item["target"]) names = [*item["exports"]["public"], *item["exports"]["private_used"]] + assert len(names) == len(set(names)) for name in names: - assert getattr(root, name) is getattr(moved, name), name - extra = LEAF_NAMES if item["importer"] == "catalog" else [] - # REPO_TYPES is listed on both catalog routes; __all__ names it once. - assert len(root.__all__) == len(set(root.__all__)) - assert set(root.__all__) == {*names, *extra} + assert hasattr(moved, name), name + # A pickle that names a class or function by the retired root path no longer loads. + with pytest.raises(ModuleNotFoundError): + pickle.loads(f"c{retired}\n{names[0]}\n.".encode("ascii")) -def test_catalog_routes_share_one_repo_types_tuple(): - from inferrsi import arguments, catalog +def test_retired_record_is_the_seven_shells_and_the_manifest_stages_nothing(): + assert MANIFEST["compatibility_pairs"] == [] + assert exact_pairs(MANIFEST) == frozenset() + assert "catalog_two_routes" not in MANIFEST + assert RETIRED["stage"] == "C2" + assert MANIFEST["base_commit"] in RETIRED["superseded_by"] + assert len(RETIRED["retired_compatibility_pairs"]) == 9 + assert len(MOVED) == 7 + assert {f"inferrsi.{item['importer']}" for item in MOVED} < RETIRED_ROOT_MODULES + # Every retired shell record still carries the old root file hash it was audited at. + for item in MOVED: + assert item["file"] == f"src/inferrsi/{item['importer']}.py" + assert len(item["file_sha256"]) == 64 + assert not (REPO / item["file"]).exists() + + +def test_one_repo_types_tuple_reaches_knowledge_and_the_parser_directly(): + from inferrsi import arguments from inferrsi.knowledge import catalog as knowledge_catalog from inferrsi.resources import repository_packs - assert catalog.REPO_TYPES is repository_packs.REPO_TYPES is knowledge_catalog.REPO_TYPES - # The CLI argument surface takes the tuple through root catalog: still one object. + assert knowledge_catalog.REPO_TYPES is repository_packs.REPO_TYPES + # Root catalog is retired, so the CLI parser binds the tuple from the leaf itself: + # still one object, never a copy. assert arguments.REPO_TYPES is repository_packs.REPO_TYPES - assert catalog.repository_pack is repository_packs.repository_pack + assert LEAF_NAMES == ["REPO_TYPES", "repository_pack"] + assert not hasattr(arguments, "repository_pack") assert not hasattr(knowledge_catalog, "repository_pack") assert knowledge_catalog.KNOWLEDGE_SCOPES["contribution"] == ( *repository_packs.REPO_TYPES, "vllm_ascend", "inferrsi" @@ -85,12 +126,15 @@ def test_c2_relock_moves_the_leaf_without_changing_a_byte(): # The review requirement is recorded, never an approval. assert "requires formal architecture review" in moved["review"] assert "does not self-approve" in moved["review"] - superseded = MANIFEST["revision_history"][-1] - assert superseded["stage"] == "K02" - assert superseded["changed_technical_leaf"] == { + # R1 appended the C2 record after the K02 one and rewrote neither. + k02, c2 = MANIFEST["revision_history"][-2:] + assert (k02["stage"], c2["stage"]) == ("K02", "C2") + assert k02["changed_technical_leaf"] == { "module": moved["old_module"], "file": moved["old_file"], } - assert MANIFEST["base_commit"] in superseded["superseded_by"] + assert c2["base_commit"] in k02["superseded_by"] + assert MANIFEST["base_commit"] in c2["superseded_by"] + assert MANIFEST["base_commit"] != c2["base_commit"] def _leaf_imports(tree: ast.Module) -> set[str]: @@ -142,6 +186,60 @@ def test_infra_inspection_imports_the_leaf_by_the_recorded_line(): assert not [line for line in lines if "_repo_pack_data" in line or "catalog" in line] +def _declared(section: str) -> dict[str, frozenset[str]]: + return { + item["importer"]: frozenset(item["names"]) for item in LEAF[section]["importers"] + } + + +def test_manifest_restates_the_exact_gate_importer_rules(): + # #412 R1: the permanent rule is one importer and one name, outside the allowlist. + permanent, temporary = LEAF["permanent_importers"], LEAF["temporary_importers"] + assert _declared("permanent_importers") == LEAF_IMPORTERS == { + "inferrsi.infra.inspection": frozenset({"repository_pack"}), + } + assert _declared("temporary_importers") == TEMPORARY_LEAF_IMPORTERS == { + "inferrsi.arguments": frozenset({"REPO_TYPES"}), + } + assert MANIFEST["compatibility_pairs"] == [] + assert permanent["importers"][0]["import_line"] == ( + MANIFEST["infra_inspection_rewrite"]["canonical_import"] + ) + # The review requirement is recorded for both, never an approval. + for section in (permanent, temporary): + assert (section["stage"], section["issue"]) == ("R1", "#412") + assert "requires formal architecture review" in section["review"] + assert "does not self-approve" in section["review"] + # The parser entry is temporary: A1 removes it, and it blocks until then. + assert temporary["due"] == "A1 (#408)" + assert "temporary leaf importers" in temporary["blocker"] + assert "not an allowance for Apps modules in general" in temporary["rule"] + assert "temporary leaf importers: 1" in first_wave_report().blockers + + +@pytest.mark.parametrize("section", ["permanent_importers", "temporary_importers"]) +def test_each_declared_importer_binds_the_leaf_by_its_one_recorded_line(section): + for item in LEAF[section]["importers"]: + source = (REPO / item["file"]).read_text(encoding="utf-8") + assert source.splitlines().count(item["import_line"]) == 1, item["importer"] + tree = ast.parse(source) + package = item["importer"].rpartition(".")[0] + found = [] + for node in ast.walk(tree): + if isinstance(node, ast.Import): + found += [alias.name for alias in node.names if "resources" in alias.name] + elif isinstance(node, ast.ImportFrom) and "resources" in (node.module or ""): + parts = package.split(".") + keep = parts[: len(parts) - node.level + 1] if node.level else [] + base = ".".join([*keep, node.module]) + if not base.startswith("inferrsi."): + continue + assert node in tree.body, "the leaf import is at module level" + found += [(base, alias.name, alias.asname) for alias in node.names] + # That line is the file's only import of anything under inferrsi.resources. + assert found == [(LEAF["module"], name, None) for name in item["names"]] + + def _fresh(script: str) -> None: environment = {**os.environ, "PYTHONPATH": str(REPO / "src")} result = subprocess.run( @@ -168,19 +266,23 @@ def test_package_init_and_infra_inspection_load_no_knowledge_body(): ) -def test_knowledge_closure_is_isolated_while_overall_stays_false(): - # K01-5: the Knowledge package exists and its closure is clean, but the - # allowlist and harness -> model/infra remain. S01 removed the residual. - # After #338, runtime resource loaders keep new_core_closure_isolated - # false; that is not a Knowledge-closure regression. +def test_knowledge_closure_is_clean_while_overall_stays_false(): + # K01-5: the Knowledge package exists and its closure is clean. R1 cleared the + # allowlist and the old-run forwarders. The parser entry due in A1, the twelve + # harness -> infra hits (R2) and the five runtime resource loaders (R3) remain, + # and the loaders keep new_core_closure_isolated false. report = first_wave_report() assert report.violations == () - assert "absent: knowledge/" not in report.blockers - assert { - "allowlist", - "harness reaches model/infra", - "runtime resource loaders", - } <= blocker_kinds(report) - assert "residual" not in blocker_kinds(report) + assert report.blockers == ( + "temporary leaf importers: 1", + "harness reaches model/infra: inferrsi.harness.agent_sessions -> inferrsi.infra (12 hits)", + "runtime resource loaders: 5", + ) + assert len(report.harness_model_infra) == 12 + assert {path.split(" -> ")[0] for path in report.harness_model_infra} == { + f"inferrsi.harness.{name}" + for name in ("agent_sessions", "candidates", "optimization", "optimization_session", + "project", "task_protocol") + } assert not report.new_core_closure_isolated assert not report.isolation_complete diff --git a/tests/test_knowledge_contract.py b/tests/test_knowledge_contract.py index 082783f3e..014297398 100644 --- a/tests/test_knowledge_contract.py +++ b/tests/test_knowledge_contract.py @@ -4,8 +4,8 @@ import pytest -from inferrsi.knowledge_operations import KnowledgeReader -from inferrsi.skill_catalog import ( +from inferrsi.knowledge.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.skill_catalog import ( skill_directories, skill_directory, skill_records, diff --git a/tests/test_knowledge_install.py b/tests/test_knowledge_install.py index fc809786b..e360bbbaa 100644 --- a/tests/test_knowledge_install.py +++ b/tests/test_knowledge_install.py @@ -8,7 +8,7 @@ import pytest -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.knowledge_operations import KnowledgeReader REPO = Path(__file__).resolve().parents[1] SKILLS = REPO / "src" / "inferrsi" / "knowledge" / "data" / "skill_library" diff --git a/tests/test_knowledge_model_policy.py b/tests/test_knowledge_model_policy.py index edb848cf2..2e3716974 100644 --- a/tests/test_knowledge_model_policy.py +++ b/tests/test_knowledge_model_policy.py @@ -173,8 +173,7 @@ def test_stale_head_and_unsupported_schema_fail(): "src/inferrsi/knowledge/data/skill_library/skill-index.json", "src/inferrsi/knowledge/data/knowledge_packs/peer/example/case.json", KNOWLEDGE, "docs/reference-matrix.md", "docs/searchspace-matrix.md", "docs/knowledge-base.md", - "docs/knowledge-operations.md", "docs/knowledge-store.md", "src/inferrsi/catalog.py", - "src/inferrsi/skill_catalog.py", "src/inferrsi/knowledge_operations.py", + "docs/knowledge-operations.md", "docs/knowledge-store.md", "src/inferrsi/knowledge/catalog.py", "src/inferrsi/knowledge/skill_catalog.py", "src/inferrsi/knowledge/knowledge_operations.py", ]) @@ -184,9 +183,13 @@ def test_explicit_protected_paths_require_declarations(policy, path): assert assess(item, {path}, policy)["state"] == "failure" +# #412 R1 deleted the three root shells that were listed beside their implementations. +# Their exact paths left the scope with the files; the implementations above stay in it. @pytest.mark.parametrize("path", ["README.md", "src/inferrsi/cli.py", "src/inferrsi/contracts/__init__.py", - "src/inferrsi/knowledge_store.py", "tests/test_claim.py"]) + "src/inferrsi/knowledge_store.py", "tests/test_claim.py", + "src/inferrsi/catalog.py", "src/inferrsi/skill_catalog.py", + "src/inferrsi/knowledge_operations.py"]) def test_no_blanket_source_scope_and_stable_not_applicable(policy, path): item = pr() item["body"] = None diff --git a/tests/test_knowledge_operations.py b/tests/test_knowledge_operations.py index 6b191ccb1..77ba27841 100644 --- a/tests/test_knowledge_operations.py +++ b/tests/test_knowledge_operations.py @@ -5,7 +5,7 @@ import pytest -from inferrsi.knowledge_operations import ( +from inferrsi.knowledge.knowledge_operations import ( Bundle, IdentityError, KnowledgeReader, diff --git a/tests/test_knowledge_store.py b/tests/test_knowledge_store.py index a30cd7c38..870434cb4 100644 --- a/tests/test_knowledge_store.py +++ b/tests/test_knowledge_store.py @@ -11,8 +11,13 @@ import pytest import inferrsi -from inferrsi.knowledge_operations import Bundle, IdentityError, KnowledgeReader, RevisionConflict -from inferrsi.knowledge_store import KnowledgeStore +from inferrsi.knowledge.knowledge_operations import ( + Bundle, + IdentityError, + KnowledgeReader, + RevisionConflict, +) +from inferrsi.knowledge.knowledge_store import KnowledgeStore REPO = Path(__file__).resolve().parents[1] ROOT = REPO / "src/inferrsi/knowledge/data/skill_library" @@ -254,8 +259,8 @@ def test_workspace_location_and_symlinks(tmp_path): import sys from pathlib import Path -from inferrsi.knowledge_operations import Bundle -from inferrsi.knowledge_store import KnowledgeStore +from inferrsi.knowledge.knowledge_operations import Bundle +from inferrsi.knowledge.knowledge_store import KnowledgeStore export = Path(sys.argv[1]) workspace = Path(sys.argv[2]) @@ -364,13 +369,13 @@ def test_operations_leave_sources_evidence_and_matrices_unchanged(tmp_path): def test_package_only_collect_without_checkout_or_yaml(tmp_path): script = ( "import sys; sys.modules['yaml'] = None\n" - "from inferrsi.knowledge_operations import KnowledgeReader\n" + "from inferrsi.knowledge.knowledge_operations import KnowledgeReader\n" "reader = KnowledgeReader()\n" "result = reader.collect(tier='dependency', material=True, limit=2)\n" "item = result['items'][0]\n" "assert item['authority'] == 'package', item\n" "assert reader.read(item['ref'], revision=item['revision']).revision == item['revision']\n" - "assert 'inferrsi.skill_catalog' not in sys.modules\n" + "assert 'inferrsi.knowledge.skill_catalog' not in sys.modules\n" "assert 'inferrsi.knowledge.skill_catalog' not in sys.modules\n" ) env = {**os.environ, "PYTHONPATH": str(REPO / "src"), "PYTHONUTF8": "1"} diff --git a/tests/test_knowledge_tiers.py b/tests/test_knowledge_tiers.py index ba8434dce..0d86edaab 100644 --- a/tests/test_knowledge_tiers.py +++ b/tests/test_knowledge_tiers.py @@ -6,11 +6,11 @@ import pytest -from inferrsi import catalog from inferrsi.arguments import parser -from inferrsi.catalog import get_knowledge, repository_pack, search_knowledge from inferrsi.cli import main from inferrsi.knowledge import catalog as knowledge_catalog +from inferrsi.knowledge.catalog import get_knowledge, search_knowledge +from inferrsi.resources.repository_packs import repository_pack ROOT = Path(__file__).resolve().parents[1] / "src/inferrsi/knowledge/data/knowledge_packs" @@ -22,7 +22,7 @@ def test_external_snapshot_has_complete_navigation_and_pinned_sources(): assert {ROOT / link for link in links} == paths for tier in ("dependency", "peer"): records = search_knowledge(tier=tier, limit=100) - assert {r["repo"] for r in records} == set(catalog.KNOWLEDGE_SCOPES[tier]) + assert {r["repo"] for r in records} == set(knowledge_catalog.KNOWLEDGE_SCOPES[tier]) for record in records: assert record["status"] == "imported" assert record["verification"]["locally_verified"] is False @@ -96,7 +96,7 @@ def test_misclassified_external_card_is_rejected_before_retrieval( def test_legacy_engine_library_and_peer_cards_coexist_without_duplicate_choices(): - assert len(catalog.KNOWLEDGE_REPOS) == len(set(catalog.KNOWLEDGE_REPOS)) + assert len(knowledge_catalog.KNOWLEDGE_REPOS) == len(set(knowledge_catalog.KNOWLEDGE_REPOS)) contribution = search_knowledge(tier="contribution", repo="sglang", limit=100) peer = search_knowledge(tier="peer", repo="sglang", limit=100) assert len(contribution) == 3 and all("skill_documents" in r for r in contribution) diff --git a/tests/test_legacy_run_forwarder_gate.py b/tests/test_legacy_run_forwarder_gate.py index b9557f26b..0c2435f25 100644 --- a/tests/test_legacy_run_forwarder_gate.py +++ b/tests/test_legacy_run_forwarder_gate.py @@ -1,16 +1,26 @@ -# Gate rules for the three old-run root forwarders: one exact Apps legacy target -# each, no adjacent module, no second importer, and overall isolation stays false. +# Gate rules for the three old-run root forwarders that #412 R1 retired. None is +# registered any more: a forwarder that returns is a violation, whatever it imports, +# and no core or Apps module may reach one. import pytest -from tests._gate_policy_support import LEGACY_RUN_FORWARDERS, first_wave_report +from tests import _gate_policy_support as support +from tests._gate_policy_support import first_wave_report from tests.test_first_wave_isolation_policy import CLEARED_TREE, EMPTY_ALLOWLIST, _report +# The exact targets that were registered before R1. +RETIRED_FORWARDERS = { + "evaluation": "apps.legacy.evaluation", + "execution": "apps.legacy.run", + "reporting": "apps.legacy.reporting", +} +# The old forwarder is back on disk in this tree, importing its former exact target. FORWARDER_TREE = { **CLEARED_TREE, "apps/legacy/__init__.py": "", "apps/legacy/evaluation.py": "VALUE = 1\n", "apps/legacy/run.py": "VALUE = 1\n", + "apps/legacy/reporting.py": "VALUE = 1\n", "evaluation.py": "from .apps.legacy.evaluation import VALUE\n", } @@ -19,33 +29,58 @@ def _scan(changes): return _report(changes, manifest=EMPTY_ALLOWLIST, base=FORWARDER_TREE) -def test_registered_forwarder_passes_but_keeps_isolation_false(): +def test_returning_forwarder_fails_and_removing_it_clears(): + # Before R1 this exact import passed and only blocked. It is now two violations. report = _scan({}) - assert report.violations == () - assert report.blockers == ("legacy run forwarders: 1",) + assert report.violations == ( + "evaluation.py imports inferrsi.apps", + "inferrsi.evaluation is a retired root module", + ) + assert report.blockers == ("violations: 2",) assert not report.isolation_complete - # Removing the forwarder is what clears the blocker. retired = _scan({"evaluation.py": None}) + assert retired.violations == () assert retired.blockers == () and retired.isolation_complete + assert not hasattr(support, "LEGACY_RUN_FORWARDERS") + + +@pytest.mark.parametrize("name", sorted(RETIRED_FORWARDERS)) +def test_no_former_pair_is_still_registered(name): + assert f"inferrsi.{name}" in support.RETIRED_ROOT_MODULES + source = f"from .{RETIRED_FORWARDERS[name]} import VALUE\n" + report = _scan({"evaluation.py": None, f"{name}.py": source}) + assert report.violations == ( + f"{name}.py imports inferrsi.apps", + f"inferrsi.{name} is a retired root module", + ) + assert not report.isolation_complete @pytest.mark.parametrize( "changes,expected", [ - # The pair names one target module; the module beside it is not covered. + # The module beside the former target is not covered either. ({"evaluation.py": "from .apps.legacy.run import VALUE\n"}, "evaluation.py imports inferrsi.apps"), ({"evaluation.py": "from .apps import legacy\n"}, "evaluation.py imports inferrsi.apps"), - # Another root module may not use a registered target. + # Another root module may not use a former target. ({"inspection.py": "from .apps.legacy.evaluation import VALUE\n"}, "inspection.py imports inferrsi.apps"), - # No core reaches Apps through a forwarder, and Apps does not loop back. + ({"helper.py": "from .apps.legacy.evaluation import VALUE\n"}, + "helper.py imports inferrsi.apps"), + # No core passes through a forwarder: the walk now stops at the retired + # module itself, before it reaches Apps. Apps does not loop back. ({"harness/runner.py": "from ..evaluation import VALUE\n"}, - "inferrsi.harness.runner reaches inferrsi.apps"), + "inferrsi.harness.runner reaches inferrsi.evaluation"), ({"benchmark/rules.py": "from ..evaluation import VALUE\n"}, - "inferrsi.benchmark.rules reaches inferrsi.apps"), + "inferrsi.benchmark.rules reaches inferrsi.evaluation"), ({"apps/legacy/run.py": "from ...evaluation import VALUE\n"}, "inferrsi.apps.legacy.run reaches inferrsi.evaluation"), + # A retired name is refused even with no file behind it. + ({"harness/runner.py": "from ..execution import run_experiment\n"}, + "inferrsi.harness.runner reaches inferrsi.execution"), + ({"apps/legacy/run.py": "from ...reporting import write_report\n"}, + "inferrsi.apps.legacy.run reaches inferrsi.reporting"), ], ) def test_forwarder_misuse_is_a_violation(changes, expected): @@ -54,8 +89,13 @@ def test_forwarder_misuse_is_a_violation(changes, expected): assert not report.isolation_complete -def test_real_tree_holds_exactly_the_three_registered_forwarders(): +def test_real_tree_holds_no_forwarder_and_stays_incomplete(): report = first_wave_report() assert report.violations == () - assert f"legacy run forwarders: {len(LEGACY_RUN_FORWARDERS)}" in report.blockers + for name in RETIRED_FORWARDERS: + assert not (support.ROOT / f"{name}.py").exists(), name + # What is left belongs to R2 and R3, plus the parser entry due in A1. + assert support.blocker_kinds(report) == { + "temporary leaf importers", "harness reaches model/infra", "runtime resource loaders", + } assert not report.isolation_complete diff --git a/tests/test_legacy_run_split.py b/tests/test_legacy_run_split.py index 6e0a41090..8b7136cb6 100644 --- a/tests/test_legacy_run_split.py +++ b/tests/test_legacy_run_split.py @@ -1,8 +1,11 @@ # Acceptance for the old run/evaluate split: the removed Harness edges, the # injected runner ports, Benchmark's own v0 rules, and the composed old entries. +import importlib +import importlib.util import inspect import json +import pickle import subprocess import sys import time @@ -10,7 +13,6 @@ import pytest -from inferrsi import evaluation, execution, reporting from inferrsi.apps.legacy import evaluation as legacy_evaluation from inferrsi.apps.legacy import reporting as legacy_reporting from inferrsi.apps.legacy import run as legacy_run @@ -19,7 +21,7 @@ from inferrsi.harness import atomic_json from inferrsi.harness.runner import run_experiment from inferrsi.harness.scenarios.external import execute_trial -from inferrsi.infra import artifacts +from inferrsi.infra import artifacts, targets from inferrsi.model.protocols import metrics from tests._gate_policy_support import ROOT, _scan, _walk @@ -216,12 +218,110 @@ def test_trial_metric_applies_the_contract_to_any_reader(): legacy_run_v0.trial_metric({}, "k", reader=lambda document, key: 0.0) -def test_root_entries_forward_to_the_same_objects(): - assert evaluation.evaluate is legacy_evaluation.evaluate - assert evaluation._summary is legacy_run_v0._summary - assert evaluation.read_metric is metrics.read_metric - assert reporting.write_report is legacy_reporting.write_report - assert execution._execute is legacy_run.execute_trial +@pytest.mark.parametrize("name", ["evaluation", "execution", "reporting"]) +def test_retired_root_entries_no_longer_import(name): + # #412 R1 deleted the three root forwarders with no alias behind them. + retired = f"inferrsi.{name}" + assert importlib.util.find_spec(retired) is None + with pytest.raises(ModuleNotFoundError): + importlib.import_module(retired) + assert retired not in sys.modules + assert not hasattr(sys.modules["inferrsi"], name) + + +def test_canonical_entries_hold_the_objects_the_root_entries_forwarded(): + assert legacy_evaluation._summary is legacy_run_v0._summary + assert legacy_evaluation.read_metric is metrics.read_metric + assert legacy_evaluation.evaluate.__module__ == "inferrsi.apps.legacy.evaluation" + assert legacy_reporting.write_report.__module__ == "inferrsi.apps.legacy.reporting" + assert legacy_run.execute_trial.__module__ == "inferrsi.apps.legacy.run" + assert legacy_run.repository_identity is targets.repository_identity + assert legacy_run.write_report is legacy_reporting.write_report + # The two-argument entry that root ``execution`` defined is this one function now. + assert legacy_run.run_experiment.__module__ == "inferrsi.apps.legacy.run" + parameters = inspect.signature(legacy_run.run_experiment).parameters + assert list(parameters) == ["spec", "environment", "execute", "snapshot"] + for port in ("execute", "snapshot"): + assert parameters[port].kind is inspect.Parameter.KEYWORD_ONLY + assert parameters[port].default is None + # A pickle that names the retired entry no longer loads; the canonical one does. + with pytest.raises(ModuleNotFoundError): + pickle.loads(b"cinferrsi.execution\nrun_experiment\n.") + assert pickle.loads(pickle.dumps(legacy_run.run_experiment)) is legacy_run.run_experiment + + +def _port_spec(tmp_path): + repo = tmp_path / "repo" + repo.mkdir() + return RunSpec( + repo=str(repo), baseline_command=("baseline",), candidate_command=("candidate",), + repetitions=1, output_dir=str(tmp_path / "runs"), max_minutes=1, + ) + + +def test_two_argument_run_reads_both_ports_from_the_module_on_each_call(tmp_path, monkeypatch): + # The retired root entry looked up its executor and snapshot when it was called. + # The canonical entry keeps that: replacing either name here changes the next run. + calls = [] + + def execute(command, cwd, trial_dir, deadline, metric_key): + trial_dir.mkdir(parents=True) + calls.append(("execute", tuple(command))) + return {"status": "completed", "command": list(command), "metric": 1.0} + + def snapshot(path, exclude=None): + calls.append(("snapshot", str(path))) + return {"available": True, "snapshot_hash": "fixed"} + + monkeypatch.setattr(legacy_run, "execute_trial", execute) + monkeypatch.setattr(legacy_run, "repository_identity", snapshot) + monkeypatch.setattr(legacy_run, "write_report", lambda run_dir: None) + run_dir = legacy_run.run_experiment(_port_spec(tmp_path), {"backend": "cpu"}) + assert [call for call in calls if call[0] == "execute"] == [ + ("execute", ("baseline",)), ("execute", ("candidate",)), + ] + assert [call for call in calls if call[0] == "snapshot"] + assert json.loads((run_dir / "status.json").read_text(encoding="utf-8"))["state"] == ( + "completed" + ) + + +def test_an_explicit_port_wins_over_the_module_name(tmp_path, monkeypatch): + def forbidden(*args, **kwargs): + raise AssertionError("the module-level port must not run when one is passed") + + seen = [] + + def execute(command, cwd, trial_dir, deadline, metric_key): + trial_dir.mkdir(parents=True) + seen.append(tuple(command)) + return {"status": "completed", "command": list(command), "metric": 1.0} + + monkeypatch.setattr(legacy_run, "execute_trial", forbidden) + monkeypatch.setattr(legacy_run, "repository_identity", forbidden) + monkeypatch.setattr(legacy_run, "write_report", lambda run_dir: None) + legacy_run.run_experiment( + _port_spec(tmp_path), {}, execute=execute, + snapshot=lambda path, exclude=None: {"available": True, "snapshot_hash": "fixed"}, + ) + assert seen == [("baseline",), ("candidate",)] + + +def test_a_failing_module_level_executor_is_recorded_not_swallowed(tmp_path, monkeypatch): + def unavailable(*args, **kwargs): + raise OSError("executor unavailable") + + monkeypatch.setattr(legacy_run, "execute_trial", unavailable) + monkeypatch.setattr( + legacy_run, "repository_identity", + lambda path, exclude=None: {"available": True, "snapshot_hash": "fixed"}, + ) + run_dir = legacy_run.run_experiment(_port_spec(tmp_path), {"backend": "cpu"}) + status = json.loads((run_dir / "status.json").read_text(encoding="utf-8")) + evaluation = json.loads((run_dir / "evaluation.json").read_text(encoding="utf-8")) + assert status["state"] == "failed" + assert evaluation["verdict"] == "INCONCLUSIVE" + assert "executor unavailable" in evaluation["reasons"] def test_cli_import_loads_no_benchmark_and_no_old_run_composition(): @@ -229,7 +329,9 @@ def test_cli_import_loads_no_benchmark_and_no_old_run_composition(): code = ( "import sys; sys.path.insert(0, sys.argv[1]); import inferrsi.cli; " "print(sorted(name for name in sys.modules if name.startswith(('inferrsi.benchmark', " - "'inferrsi.execution', 'inferrsi.evaluation', 'inferrsi.apps.legacy.run'))))" + "'inferrsi.execution', 'inferrsi.evaluation', 'inferrsi.reporting', " + "'inferrsi.inspection', 'inferrsi.apps.legacy.run', " + "'inferrsi.apps.legacy.evaluation'))))" ) result = subprocess.run( [sys.executable, "-c", code, str(ROOT.parent)], capture_output=True, text=True, check=True diff --git a/tests/test_legacy_run_v0_frozen.py b/tests/test_legacy_run_v0_frozen.py index 445dbff9f..4494e59b4 100644 --- a/tests/test_legacy_run_v0_frozen.py +++ b/tests/test_legacy_run_v0_frozen.py @@ -101,7 +101,10 @@ def run_scenario(name: str, root: Path) -> tuple[int, bytes]: stack.enter_context(contextlib.redirect_stdout(stream)) if scenario.executor_error is not None: stack.enter_context( - patch("inferrsi.execution._execute", side_effect=OSError(scenario.executor_error)) + patch( + "inferrsi.apps.legacy.run.execute_trial", + side_effect=OSError(scenario.executor_error), + ) ) code = main(arguments) printed = json.loads(stream.getvalue()) diff --git a/tests/test_local_dream_repo_to_skill.py b/tests/test_local_dream_repo_to_skill.py index 311f8dd2b..157c23908 100644 --- a/tests/test_local_dream_repo_to_skill.py +++ b/tests/test_local_dream_repo_to_skill.py @@ -178,7 +178,7 @@ def test_registration_and_catalog_preserve_candidate_boundary(): assert index["skills"]["local-dream"] == { "path": "repositories/local-dream", "backend": "common", } - from inferrsi.skill_catalog import skill_metadata, skill_resource + from inferrsi.knowledge.skill_catalog import skill_metadata, skill_resource assert skill_metadata(ROOT.parents[1], "local-dream")["status"] == "candidate" assert "desired" in skill_resource(ROOT.parents[1], "local-dream", "references/android-lifecycle.md").read_text().lower() diff --git a/tests/test_ltx25_campaign.py b/tests/test_ltx25_campaign.py index 357de8335..2f366cb25 100644 --- a/tests/test_ltx25_campaign.py +++ b/tests/test_ltx25_campaign.py @@ -15,8 +15,9 @@ from unittest.mock import Mock, patch import inferrsi -from inferrsi import execution -from inferrsi.execution import _execute, repository_identity +from inferrsi.apps.legacy import run as legacy_run +from inferrsi.apps.legacy.run import execute_trial as _execute +from inferrsi.apps.legacy.run import repository_identity SCRIPT = Path(__file__).resolve().parents[1] / "tests" / "fixtures" / "legacy" / "ltx25_b300.py" SPEC = importlib.util.spec_from_file_location("ltx25_campaign", SCRIPT) @@ -37,7 +38,11 @@ class CampaignTests(unittest.TestCase): def setUp(self): # The frozen helper imports its historical package name. Confine that # alias to these archived-protocol tests; the installed API is inferrsi. - aliases = patch.dict(sys.modules, {"omnirsi": inferrsi, "omnirsi.execution": execution}) + # It asks ``omnirsi.execution`` only for ``repository_identity``, which the + # old-run composition holds since root ``execution`` was retired. + aliases = patch.dict( + sys.modules, {"omnirsi": inferrsi, "omnirsi.execution": legacy_run} + ) aliases.start() self.addCleanup(aliases.stop) self.temporary = tempfile.TemporaryDirectory() diff --git a/tests/test_mlx_discovery.py b/tests/test_mlx_discovery.py index 9e628400b..019447f90 100644 --- a/tests/test_mlx_discovery.py +++ b/tests/test_mlx_discovery.py @@ -5,13 +5,13 @@ import pytest -from inferrsi import catalog -from inferrsi.catalog import get_knowledge, search_knowledge from inferrsi.cli import main from inferrsi.infra.commands import probe_command from inferrsi.infra.platforms import inspect_device from inferrsi.infra.platforms.mlx import _parse_sysctl_info -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.catalog import get_knowledge, search_knowledge +from inferrsi.knowledge.knowledge_operations import KnowledgeReader +from inferrsi.resources.repository_packs import REPO_TYPES PROJECT = Path(__file__).resolve().parents[1] IDS = { @@ -79,7 +79,7 @@ def test_discrete_ids_are_rejected_by_real_adapter(): def test_mlx_cards_are_source_scoped_and_readable(capsys): records = search_knowledge(repo="mlx", tier="dependency", backend="mlx", limit=100) assert {item["id"] for item in records} == IDS - assert "mlx" not in catalog.REPO_TYPES + assert "mlx" not in REPO_TYPES assert search_knowledge(repo="mlx", tier="peer", backend="mlx") == [] assert search_knowledge(repo="mlx", backend="cuda") == [] assert main([ diff --git a/tests/test_mlx_knowledge.py b/tests/test_mlx_knowledge.py index 646f94155..cf2c3c329 100644 --- a/tests/test_mlx_knowledge.py +++ b/tests/test_mlx_knowledge.py @@ -1,8 +1,8 @@ from pathlib import Path -from inferrsi.catalog import get_knowledge, search_knowledge -from inferrsi.knowledge_operations import KnowledgeReader -from inferrsi.skill_catalog import skill_metadata, skill_resource +from inferrsi.knowledge.catalog import get_knowledge, search_knowledge +from inferrsi.knowledge.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.skill_catalog import skill_metadata, skill_resource LIBRARY = Path(__file__).resolve().parents[1] / "src/inferrsi/knowledge/data/skill_library" SKILL = "lazy-evaluation-stream-lifetimes" diff --git a/tests/test_mlx_lm_knowledge.py b/tests/test_mlx_lm_knowledge.py index dbb6000a7..8845adb78 100644 --- a/tests/test_mlx_lm_knowledge.py +++ b/tests/test_mlx_lm_knowledge.py @@ -1,8 +1,8 @@ from pathlib import Path -from inferrsi.catalog import get_knowledge, search_knowledge -from inferrsi.knowledge_operations import KnowledgeReader -from inferrsi.skill_catalog import skill_metadata, skill_resource +from inferrsi.knowledge.catalog import get_knowledge, search_knowledge +from inferrsi.knowledge.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.skill_catalog import skill_metadata, skill_resource LIBRARY = Path(__file__).resolve().parents[1] / "src/inferrsi/knowledge/data/skill_library" SKILL = "generation-cache-lifecycle" diff --git a/tests/test_optimization_session.py b/tests/test_optimization_session.py index b1a052e8f..dc6f5987a 100644 --- a/tests/test_optimization_session.py +++ b/tests/test_optimization_session.py @@ -281,7 +281,7 @@ def test_task_runner_does_not_expose_harness_as_top_level_modules(task, tmp_path def test_knowledge_delivery_is_retained_and_bound_to_round(session, monkeypatch, capsys): - from inferrsi.knowledge_operations import KnowledgeReader + from inferrsi.knowledge.knowledge_operations import KnowledgeReader def collect(self, query, **options): import hashlib diff --git a/tests/test_optimization_story.py b/tests/test_optimization_story.py index 38db83ecf..667aba66e 100644 --- a/tests/test_optimization_story.py +++ b/tests/test_optimization_story.py @@ -19,7 +19,7 @@ record_decision, ) from inferrsi.harness.records.optimization_log import LogError, parse_optimization_log, record_log -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.knowledge_operations import KnowledgeReader class FakeReader: diff --git a/tests/test_package_root_inventory.py b/tests/test_package_root_inventory.py index b78df8ab4..2b8f78aa7 100644 --- a/tests/test_package_root_inventory.py +++ b/tests/test_package_root_inventory.py @@ -1,8 +1,8 @@ -"""Exact inventory of the files directly under ``src/inferrsi/`` (#451 C1, C2). +"""Exact inventory of the files directly under ``src/inferrsi/`` (#451 C1, C2; #412 R1). The flat package root is being retired. This guard pins the root to the exact file -set left after C2 and rejects any other root file. Each later removal PR (R1, A1/A2, -M1) shrinks ``ROOT_FILES`` in the same change, until only ``__init__.py`` and +set left after R1 and rejects any other root file. Each later removal PR (A1/A2, M1) +shrinks ``ROOT_FILES`` in the same change, until only ``__init__.py`` and ``__main__.py`` remain. It does not assert that final state early. """ @@ -15,33 +15,39 @@ REPO = Path(__file__).resolve().parents[1] PACKAGE = "src/inferrsi" -# The 27 tracked root files before C1, minus the four that C1 moved into contracts/ -# and the two that C2 moved into resources/, contracts/ and apps/. +# The 27 tracked root files before C1, minus the four that C1 moved into contracts/, +# the two that C2 moved into resources/, contracts/ and apps/, and the eleven that R1 +# retired: seven Knowledge shells, three old-run forwarders and the root inspection +# binding, whose implementations are in knowledge/ and apps/. RETIRED_BY_C1 = frozenset({"_json_v1.py", "claim.py", "contracts.py", "runspec.py"}) RETIRED_BY_C2 = frozenset({"_repo_pack_data.py", "evidence_paths.py"}) -RETIRED = RETIRED_BY_C1 | RETIRED_BY_C2 +RETIRED_BY_R1 = frozenset( + { + "_skill_selection.py", + "_source_snapshot.py", + "catalog.py", + "knowledge_operations.py", + "knowledge_store.py", + "research.py", + "skill_catalog.py", + "evaluation.py", + "execution.py", + "reporting.py", + "inspection.py", + } +) +RETIRED = RETIRED_BY_C1 | RETIRED_BY_C2 | RETIRED_BY_R1 ROOT_FILES = frozenset( { "__init__.py", "__main__.py", "_mindie_read_worker.py", - "_skill_selection.py", - "_source_snapshot.py", "arguments.py", "benchmark_operations.py", - "catalog.py", "cli.py", - "evaluation.py", - "execution.py", - "inspection.py", - "knowledge_operations.py", - "knowledge_store.py", "mindie_context.py", "mindie_mcp.py", "mindie_project.py", - "reporting.py", - "research.py", - "skill_catalog.py", "summary_chart.py", } ) @@ -82,21 +88,39 @@ def root_files(repo: Path) -> set[str]: return present - _listed(repo, "--deleted") -def test_inventory_is_the_exact_post_c2_set() -> None: - assert len(ROOT_FILES) == 21 - assert len(RETIRED_BY_C1) == 4 and len(RETIRED_BY_C2) == 2 +def test_inventory_is_the_exact_post_r1_set() -> None: + assert len(ROOT_FILES) == 10 + assert len(RETIRED_BY_C1) == 4 and len(RETIRED_BY_C2) == 2 and len(RETIRED_BY_R1) == 11 assert not ROOT_FILES & RETIRED - # 27 before C1, 23 after C1, 21 after C2. + assert len(RETIRED) == 17 + # 27 before C1, 23 after C1, 21 after C2, 10 after R1. assert len(ROOT_FILES | RETIRED) == 27 - assert len(ROOT_FILES | RETIRED_BY_C2) == 23 + assert len(ROOT_FILES | RETIRED_BY_C2 | RETIRED_BY_R1) == 23 + assert len(ROOT_FILES | RETIRED_BY_R1) == 21 assert GENERATED not in ROOT_FILES assert {"__init__.py", "__main__.py"} <= ROOT_FILES + # The final two-file root waits for the A1/A2 and M1 moves; it is not asserted here. + assert ROOT_FILES - {"__init__.py", "__main__.py"} == { + "_mindie_read_worker.py", "arguments.py", "benchmark_operations.py", "cli.py", + "mindie_context.py", "mindie_mcp.py", "mindie_project.py", "summary_chart.py", + } # C2 moved its two files; it left nothing behind under either old name. assert (REPO / PACKAGE / "resources" / "repository_packs.py").is_file() assert (REPO / PACKAGE / "contracts" / "evidence.py").is_file() assert (REPO / PACKAGE / "apps" / "evidence_layout.py").is_file() + # R1 deleted its eleven files. Each implementation is at its canonical path. + for name in RETIRED_BY_R1 - {"evaluation.py", "execution.py", "reporting.py", + "inspection.py"}: + assert (REPO / PACKAGE / "knowledge" / name).is_file(), name + for name in ("legacy/evaluation.py", "legacy/run.py", "legacy/reporting.py", + "inspection.py"): + assert (REPO / PACKAGE / "apps" / name).is_file(), name for name in RETIRED: assert not (REPO / PACKAGE / name).exists(), name + # No package directory stands in for a retired module either. + assert not (REPO / PACKAGE / name.removesuffix(".py") / "__init__.py").exists() or ( + name == "contracts.py" + ), name def test_on_disk_root_matches_the_inventory() -> None: @@ -127,19 +151,39 @@ def test_a_removed_root_file_must_shrink_the_inventory() -> None: f"missing root file {PACKAGE}/cli.py: shrink ROOT_FILES in the PR that removes it" ] # A later removal PR passes once it shrinks the expected set with the files. - smaller = ROOT_FILES - {"catalog.py", "inspection.py"} + smaller = ROOT_FILES - {"cli.py", "arguments.py"} assert inventory_errors(set(smaller), smaller) == [] - # C2 itself: the 23-file set it started from no longer matches the 21-file root. - before_c2 = ROOT_FILES | RETIRED_BY_C2 - assert inventory_errors(set(ROOT_FILES), before_c2) == [ + # C2 itself: the 23-file set it started from no longer matched its 21-file root. + after_c2 = ROOT_FILES | RETIRED_BY_R1 + before_c2 = after_c2 | RETIRED_BY_C2 + assert inventory_errors(set(after_c2), before_c2) == [ f"missing root file {PACKAGE}/{name}: shrink ROOT_FILES in the PR that removes it" for name in sorted(RETIRED_BY_C2) ] - # Keeping either old file beside its moved implementation is rejected. - assert inventory_errors(set(before_c2)) == [ + # R1 itself: the 21-file set it started from no longer matches the 10-file root. + assert inventory_errors(set(ROOT_FILES), after_c2) == [ + f"missing root file {PACKAGE}/{name}: shrink ROOT_FILES in the PR that removes it" + for name in sorted(RETIRED_BY_R1) + ] + # Keeping any old file beside its moved implementation is rejected, one by one + # and all together. + assert inventory_errors(set(after_c2)) == [ + f"unexpected root file {PACKAGE}/{name}: a new module belongs in a subpackage" + for name in sorted(RETIRED_BY_R1) + ] + assert inventory_errors(set(ROOT_FILES) | RETIRED_BY_C2) == [ f"unexpected root file {PACKAGE}/{name}: a new module belongs in a subpackage" for name in sorted(RETIRED_BY_C2) ] + # Deleting only ten of the eleven, or one file too many, is not the R1 root. + for name in sorted(RETIRED_BY_R1): + assert inventory_errors(set(ROOT_FILES) | {name}) == [ + f"unexpected root file {PACKAGE}/{name}: a new module belongs in a subpackage" + ] + for name in sorted(ROOT_FILES): + assert inventory_errors(set(ROOT_FILES) - {name}) == [ + f"missing root file {PACKAGE}/{name}: shrink ROOT_FILES in the PR that removes it" + ] def test_git_view_reports_root_changes_before_and_after_staging(tmp_path: Path) -> None: diff --git a/tests/test_performance_ceiling.py b/tests/test_performance_ceiling.py index 859d0f1da..374c2efdb 100644 --- a/tests/test_performance_ceiling.py +++ b/tests/test_performance_ceiling.py @@ -7,7 +7,7 @@ import pytest -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.knowledge_operations import KnowledgeReader ROOT = Path(__file__).resolve().parents[1] OWNER = ROOT / "src/inferrsi/knowledge/data/skill_library/methods/performance-ceiling-analysis" diff --git a/tests/test_pytorch_torchnpu_integration.py b/tests/test_pytorch_torchnpu_integration.py index 6e5821e9e..266c0ade1 100644 --- a/tests/test_pytorch_torchnpu_integration.py +++ b/tests/test_pytorch_torchnpu_integration.py @@ -8,10 +8,10 @@ import pytest import yaml -from inferrsi.catalog import get_knowledge from inferrsi.knowledge._source_snapshot import source_entry -from inferrsi.knowledge_operations import KnowledgeReader -from inferrsi.skill_catalog import skill_metadata, skill_resource +from inferrsi.knowledge.catalog import get_knowledge +from inferrsi.knowledge.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.skill_catalog import skill_metadata, skill_resource PROJECT = Path(__file__).resolve().parents[1] LIBRARY = PROJECT / "src/inferrsi/knowledge/data/skill_library" diff --git a/tests/test_qwen3_tts_knowledge.py b/tests/test_qwen3_tts_knowledge.py index f7709b53f..ac085c308 100644 --- a/tests/test_qwen3_tts_knowledge.py +++ b/tests/test_qwen3_tts_knowledge.py @@ -2,8 +2,9 @@ import pytest -from inferrsi.catalog import get_knowledge, repository_pack, search_knowledge from inferrsi.cli import main +from inferrsi.knowledge.catalog import get_knowledge, search_knowledge +from inferrsi.resources.repository_packs import repository_pack def test_qwen_tts_peer_scope_and_runtime_boundary(): diff --git a/tests/test_raw_string_selection.py b/tests/test_raw_string_selection.py index a33232737..b7d7e159f 100644 --- a/tests/test_raw_string_selection.py +++ b/tests/test_raw_string_selection.py @@ -6,16 +6,16 @@ import pytest -from inferrsi._skill_selection import ( +from inferrsi.cli import main +from inferrsi.knowledge._skill_selection import ( indexed_skill, lexical_score, raw_string_lexical_score, skill_score, ) -from inferrsi.cli import main from inferrsi.knowledge._source_snapshot import source_entry -from inferrsi.knowledge_operations import KnowledgeReader, _hash, _json -from inferrsi.knowledge_store import KnowledgeStore +from inferrsi.knowledge.knowledge_operations import KnowledgeReader, _hash, _json +from inferrsi.knowledge.knowledge_store import KnowledgeStore RAW = "weighted-raw-string-v1" LEGACY = "weighted-lexical-v1" diff --git a/tests/test_repository_packs_facade.py b/tests/test_repository_packs_facade.py index 32a82a8ab..7b96d47b6 100644 --- a/tests/test_repository_packs_facade.py +++ b/tests/test_repository_packs_facade.py @@ -23,7 +23,7 @@ import pytest import inferrsi.resources as resources -from inferrsi import arguments, catalog +from inferrsi import arguments from inferrsi.infra import inspection from inferrsi.knowledge import catalog as knowledge_catalog from inferrsi.resources import repository_packs @@ -35,7 +35,9 @@ SEVEN = ("vllm", "vllm_omni", "afd_plugin", "vllm_rlt", "strata", "sglang", "sglang_omni") RETIRED = "inferrsi._repo_pack_data" FACADE_CLOSURE = {"inferrsi", "inferrsi.resources", "inferrsi.resources.repository_packs"} -CORES = ("inferrsi.harness", "inferrsi.knowledge", "inferrsi.catalog", "inferrsi.infra", +# The facade needs none of these. #412 R1 retired root catalog, so the CLI parser that +# now reads the tuple directly stands in its place. +CORES = ("inferrsi.harness", "inferrsi.knowledge", "inferrsi.arguments", "inferrsi.infra", "inferrsi.apps", "inferrsi.benchmark", "inferrsi.contracts") # Every other inferrsi import is refused, so the facade works only if it needs none. BLOCKED = """ @@ -108,10 +110,14 @@ def _without_version(names) -> set[str]: def test_one_repo_types_object_reaches_every_consumer() -> None: canonical = repository_packs.REPO_TYPES assert canonical == SEVEN and type(canonical) is tuple and len(canonical) == 7 - assert catalog.REPO_TYPES is canonical assert knowledge_catalog.REPO_TYPES is canonical + # #412 R1: root catalog is retired, so the parser binds the tuple from the facade + # itself. It is the same object, read directly and never copied. assert arguments.REPO_TYPES is canonical - assert catalog.repository_pack is repository_packs.repository_pack + assert importlib.util.find_spec("inferrsi.catalog") is None + source = (ROOT / "arguments.py").read_text(encoding="utf-8") + assert source.splitlines().count("from .resources.repository_packs import REPO_TYPES") == 1 + assert "repository_pack(" not in source and not hasattr(arguments, "repository_pack") assert inspection.repository_pack is repository_packs.repository_pack assert not hasattr(knowledge_catalog, "repository_pack") assert knowledge_catalog.KNOWLEDGE_SCOPES["contribution"] == ( @@ -149,7 +155,8 @@ def test_each_pack_parses_to_its_packaged_bytes(repo_type: str) -> None: assert type(pack) is dict and pack["id"] == repo_type # Each call parses again; callers never share one mutable dict. assert repository_packs.repository_pack(repo_type) is not pack - assert catalog.repository_pack(repo_type) == pack + # The one permanent importer outside Knowledge reads the same bytes. + assert inspection.repository_pack(repo_type) == pack def test_the_seven_packs_are_the_whole_harness_resource_root() -> None: diff --git a/tests/test_research.py b/tests/test_research.py index cdb8f20c8..38d5f27f3 100644 --- a/tests/test_research.py +++ b/tests/test_research.py @@ -6,11 +6,10 @@ import pytest -from inferrsi import catalog -from inferrsi.catalog import get_knowledge, search_knowledge from inferrsi.cli import main from inferrsi.knowledge import catalog as knowledge_catalog -from inferrsi.research import research_knowledge +from inferrsi.knowledge.catalog import get_knowledge, search_knowledge +from inferrsi.knowledge.research import research_knowledge def test_h3_retrieves_framework_and_task_knowledge_with_source_boundaries(): @@ -52,7 +51,7 @@ def test_h3_retrieves_framework_and_task_knowledge_with_source_boundaries(): assert result["target"]["memory_gib"] == 64 assert result["target"]["backend"] == "metal" assert result["target_validation"] == "unverified" - assert "metal" not in catalog.BACKENDS + assert "metal" not in knowledge_catalog.BACKENDS assert {part["role"] for part in result["components"]} == { "denoiser", "text_encoder", "video_vae", "audio_vae", } diff --git a/tests/test_resource_migration.py b/tests/test_resource_migration.py index 2e13e0fd6..d0a5d52c1 100644 --- a/tests/test_resource_migration.py +++ b/tests/test_resource_migration.py @@ -18,9 +18,9 @@ import pytest -from inferrsi.catalog import REPO_TYPES, repository_pack from inferrsi.knowledge import _source_snapshot -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.knowledge_operations import KnowledgeReader +from inferrsi.resources.repository_packs import REPO_TYPES, repository_pack try: import tomllib diff --git a/tests/test_runtime.py b/tests/test_runtime.py index ea480e718..090c70a27 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -13,12 +13,11 @@ from unittest.mock import patch from inferrsi.apps.legacy.evaluation import _summary, evaluate -from inferrsi.apps.legacy.run import execute_trial +from inferrsi.apps.legacy.reporting import write_report +from inferrsi.apps.legacy.run import execute_trial, repository_identity, run_experiment from inferrsi.apps.legacy.run import run_experiment as run_with_adapters from inferrsi.contracts import RunSpec -from inferrsi.execution import repository_identity, run_experiment from inferrsi.infra.environment import storage_locations -from inferrsi.reporting import write_report def benchmark(value: object) -> tuple[str, ...]: @@ -534,14 +533,16 @@ def test_successful_command_also_cleans_up_background_descendants(self): ) def test_keyboard_interrupt_preserves_cancelled_run(self): - with patch("inferrsi.execution._execute", side_effect=KeyboardInterrupt): + with patch("inferrsi.apps.legacy.run.execute_trial", side_effect=KeyboardInterrupt): path, evaluation = self.run_spec() self.assertEqual(json.loads((path / "status.json").read_text())["state"], "cancelled") self.assertEqual(evaluation["verdict"], "INCONCLUSIVE") self.assertTrue((path / "report.html").exists()) def test_execution_exception_preserves_failed_status_and_inconclusive_verdict(self): - with patch("inferrsi.execution._execute", side_effect=OSError("executor unavailable")): + with patch( + "inferrsi.apps.legacy.run.execute_trial", side_effect=OSError("executor unavailable") + ): path, evaluation = self.run_spec() status = json.loads((path / "status.json").read_text()) self.assertEqual(status["state"], "failed") diff --git a/tests/test_skill_catalog.py b/tests/test_skill_catalog.py index 9321356eb..4faee0e51 100644 --- a/tests/test_skill_catalog.py +++ b/tests/test_skill_catalog.py @@ -4,7 +4,7 @@ import pytest from inferrsi.cli import main -from inferrsi.skill_catalog import ( +from inferrsi.knowledge.skill_catalog import ( get_skill, skill_directories, skill_instructions, diff --git a/tests/test_skill_coverage_view.py b/tests/test_skill_coverage_view.py index e254e845e..9c9d63061 100644 --- a/tests/test_skill_coverage_view.py +++ b/tests/test_skill_coverage_view.py @@ -251,7 +251,7 @@ def test_list_valued_scope_is_rendered_not_treated_as_missing(tmp_path): def test_applicability_is_descriptive_and_never_promotes_validation(): """Filling conditions must not raise locally_verified or change declared state.""" - from inferrsi.skill_catalog import skill_metadata + from inferrsi.knowledge.skill_catalog import skill_metadata rows, _, _, _, _ = coverage.collect(PROJECT) described = [row for row in rows if row["typed_conditions"]] @@ -273,7 +273,7 @@ def test_recommendations_default_only_when_the_sidecar_is_silent(): def test_resource_layer_decisions_preserve_unknown_na_and_conflicts(tmp_path): - from inferrsi.skill_catalog import skill_metadata + from inferrsi.knowledge.skill_catalog import skill_metadata from tools import build_skill_index as index_builder library = tmp_path / coverage.LIBRARY diff --git a/tests/test_skill_eval_compare.py b/tests/test_skill_eval_compare.py index 40b3b57f9..4a64ea080 100644 --- a/tests/test_skill_eval_compare.py +++ b/tests/test_skill_eval_compare.py @@ -10,7 +10,7 @@ import pytest from inferrsi.infra.artifacts import sha256, write_json -from inferrsi.knowledge_operations import KnowledgeReader, RevisionConflict +from inferrsi.knowledge.knowledge_operations import KnowledgeReader, RevisionConflict from tests.fixtures.skill_eval_compare_support import ( DATA, ROOT, diff --git a/tests/test_skill_eval_optional_dependencies.py b/tests/test_skill_eval_optional_dependencies.py index ffc0ed65e..c7ed2299f 100644 --- a/tests/test_skill_eval_optional_dependencies.py +++ b/tests/test_skill_eval_optional_dependencies.py @@ -29,7 +29,7 @@ def test_comparison_reporting_without_yaml(self): assert set(summary) == {'baseline', 'candidate'} assert all(item['input_tokens'] is None for item in summary.values()) assert callable(read_report) - assert 'inferrsi.skill_catalog' not in sys.modules + assert 'inferrsi.knowledge.skill_catalog' not in sys.modules assert 'inferrsi.knowledge.skill_catalog' not in sys.modules """) @@ -41,7 +41,7 @@ def test_checkpoint_without_yaml(self): 'input_sha256': '0' * 64}) assert result.action == 'repair_evidence' assert result.reasons == ('missing_evidence',) - assert 'inferrsi.skill_catalog' not in sys.modules + assert 'inferrsi.knowledge.skill_catalog' not in sys.modules assert 'inferrsi.knowledge.skill_catalog' not in sys.modules """) @@ -56,7 +56,7 @@ def test_daily_controller_without_yaml(self): assert 'Audited global daily genesis' in str(error) else: raise AssertionError('Daily controller accepted missing budget configuration') - assert 'inferrsi.skill_catalog' not in sys.modules + assert 'inferrsi.knowledge.skill_catalog' not in sys.modules assert 'inferrsi.knowledge.skill_catalog' not in sys.modules """) diff --git a/tests/test_skill_eval_snapshot.py b/tests/test_skill_eval_snapshot.py index 2b81916ef..7cb3f1257 100644 --- a/tests/test_skill_eval_snapshot.py +++ b/tests/test_skill_eval_snapshot.py @@ -5,7 +5,7 @@ import unittest from pathlib import Path -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.knowledge_operations import KnowledgeReader from tools.skill_eval_compare import snapshot from tools.skill_eval_snapshot import selected_snapshot diff --git a/tests/test_skill_evaluation.py b/tests/test_skill_evaluation.py index d68f9dcc9..791f4f5f6 100644 --- a/tests/test_skill_evaluation.py +++ b/tests/test_skill_evaluation.py @@ -9,7 +9,7 @@ from inferrsi.apps.skill_evaluation import export_bundle, run_trial, summarize from inferrsi.benchmark.skill_evaluation import task_verdict from inferrsi.harness.skill_evaluation import CodexClient, toml_value, trace_metrics -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.knowledge_operations import KnowledgeReader ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_skill_index.py b/tests/test_skill_index.py index ab1ec3c28..fbd11fb01 100644 --- a/tests/test_skill_index.py +++ b/tests/test_skill_index.py @@ -6,10 +6,11 @@ import pytest from inferrsi.arguments import parser -from inferrsi.catalog import REPO_TYPES, repository_pack, search_knowledge from inferrsi.cli import main -from inferrsi.knowledge_operations import KnowledgeReader -from inferrsi.skill_catalog import skill_metadata, skill_records +from inferrsi.knowledge.catalog import search_knowledge +from inferrsi.knowledge.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.skill_catalog import skill_metadata, skill_records +from inferrsi.resources.repository_packs import REPO_TYPES, repository_pack from tools import build_skill_index as index_builder from tools.build_skill_index import DESTINATION, LIBRARY, build_index, render_index, source_inputs diff --git a/tests/test_skill_library.py b/tests/test_skill_library.py index 2eb3bb165..df82e0153 100644 --- a/tests/test_skill_library.py +++ b/tests/test_skill_library.py @@ -7,8 +7,8 @@ import pytest -from inferrsi.catalog import get_knowledge, knowledge_records, search_knowledge from inferrsi.cli import main +from inferrsi.knowledge.catalog import get_knowledge, knowledge_records, search_knowledge PACKAGE = Path(__file__).resolve().parents[1] / "src/inferrsi" LIBRARY = PACKAGE / "knowledge/data/skill_library" diff --git a/tests/test_skill_library_readers.py b/tests/test_skill_library_readers.py index c2faaba42..a3dc6193e 100644 --- a/tests/test_skill_library_readers.py +++ b/tests/test_skill_library_readers.py @@ -7,8 +7,8 @@ import pytest from inferrsi.cli import main -from inferrsi.knowledge_operations import IdentityError, KnowledgeReader, RevisionConflict -from inferrsi.skill_catalog import ( +from inferrsi.knowledge.knowledge_operations import IdentityError, KnowledgeReader, RevisionConflict +from inferrsi.knowledge.skill_catalog import ( get_skill, skill_directories, skill_directory, @@ -215,12 +215,12 @@ def test_package_catalog_and_reader_do_not_require_yaml(): sys.executable, "-c", "import sys; sys.modules['yaml'] = None\n" - "from inferrsi.catalog import knowledge_records\n" - "from inferrsi.knowledge_operations import KnowledgeReader\n" + "from inferrsi.knowledge.catalog import knowledge_records\n" + "from inferrsi.knowledge.knowledge_operations import KnowledgeReader\n" "records = knowledge_records()\n" "identity = next(r['id'] for r in records if r.get('skill_documents'))\n" "assert KnowledgeReader().read(identity).content\n" - "assert 'inferrsi.skill_catalog' not in sys.modules\n" + "assert 'inferrsi.knowledge.skill_catalog' not in sys.modules\n" "assert 'inferrsi.knowledge.skill_catalog' not in sys.modules\n", ], capture_output=True, diff --git a/tests/test_source_snapshot.py b/tests/test_source_snapshot.py index e25a7e01b..f75546544 100644 --- a/tests/test_source_snapshot.py +++ b/tests/test_source_snapshot.py @@ -6,9 +6,9 @@ import pytest -from inferrsi import _source_snapshot as snapshot -from inferrsi.catalog import search_knowledge +from inferrsi.knowledge import _source_snapshot as snapshot from inferrsi.knowledge._source_snapshot import source_entry +from inferrsi.knowledge.catalog import search_knowledge from tools import build_skill_index as index_builder from tools.build_skill_index import DESTINATION diff --git a/tests/test_summary_chart.py b/tests/test_summary_chart.py index a2fa6e7b2..6cc4a7200 100644 --- a/tests/test_summary_chart.py +++ b/tests/test_summary_chart.py @@ -7,7 +7,7 @@ import pytest from inferrsi import summary_chart -from inferrsi.reporting import write_report +from inferrsi.apps.legacy.reporting import write_report PROJECT = Path(__file__).resolve().parents[1] EXAMPLES = PROJECT / "evidence" / "_template" / "results" diff --git a/tests/test_target_module_import_gate.py b/tests/test_target_module_import_gate.py index c9ef0281f..64d9ba60c 100644 --- a/tests/test_target_module_import_gate.py +++ b/tests/test_target_module_import_gate.py @@ -9,12 +9,12 @@ import tempfile import unittest from pathlib import Path +from unittest import mock import pytest from tests import _gate_policy_support as support from tests._gate_policy_support import ( - LEGACY_RUN_FORWARDERS, NEW_CORES, ROOT, blocker_kinds, @@ -57,14 +57,23 @@ def test_resource_loader_declaration_does_not_cover_an_adjacent_module(tmp_path) class TargetModuleImportGateTest(unittest.TestCase): def test_no_new_cross_core_imports(self) -> None: - allowed = exact_pairs(load_manifest()) | frozenset(LEGACY_RUN_FORWARDERS) + # #412 R1: nothing is allowlisted any more, and the tree is still clean. + allowed = exact_pairs(load_manifest()) + self.assertEqual(allowed, frozenset()) self.assertEqual(cross_core_violations(allowed=allowed), []) - # Without the registered forwarders, each of the three root files fails. + self.assertFalse(hasattr(support, "LEGACY_RUN_FORWARDERS")) + # The root CLI parser now names Knowledge itself. Its exact application-entry + # listing is what admits that, and each listed file exists in the root, so an + # entry cannot outlive the file that A1 or A2 removes. self.assertEqual( - cross_core_violations(allowed=exact_pairs(load_manifest())), - [f"{name}.py imports inferrsi.apps" for name in ("evaluation", "execution", - "reporting")], + support.APPLICATION_ENTRIES, ("cli.py", "arguments.py", "benchmark_operations.py") ) + for entry in support.APPLICATION_ENTRIES: + self.assertTrue((ROOT / entry).is_file(), entry) + with mock.patch.object( + support, "APPLICATION_ENTRIES", ("cli.py", "benchmark_operations.py") + ): + self.assertEqual(cross_core_violations(), ["arguments.py imports inferrsi.knowledge"]) def test_first_wave_policy_holds_on_current_tree(self) -> None: self.assertEqual(first_wave_report().violations, ()) @@ -82,6 +91,13 @@ def test_clean_scan_is_not_isolation(self) -> None: for retired in ("contracts.py", "_json_v1.py", "runspec.py", "claim.py", "_repo_pack_data.py", "evidence_paths.py"): self.assertFalse((ROOT / retired).exists(), retired) + # #412 R1: the eleven retired root modules are gone, with no package in their place. + self.assertEqual(len(support.RETIRED_ROOT_MODULES), 11) + for module in support.RETIRED_ROOT_MODULES: + name = module.removeprefix("inferrsi.") + self.assertFalse((ROOT / f"{name}.py").exists(), name) + self.assertFalse((ROOT / name).exists(), name) + self.assertTrue((ROOT / "apps" / "inspection.py").is_file()) self.assertEqual( support.CONTRACTS_MODULES, { @@ -108,15 +124,28 @@ def test_clean_scan_is_not_isolation(self) -> None: self.assertEqual(f"absent: {name}/" in report.blockers, absent) self.assertNotIn(f"unscannable: {name}/", report.blockers) kinds = blocker_kinds(report) - self.assertIn("allowlist", kinds) + # #412 R1: the allowlist is empty and no old-run forwarder is registered. + self.assertNotIn("allowlist", kinds) + self.assertNotIn("legacy run forwarders", kinds) # No residual: the gate registers no exempt edge, and the scan above # already fails any harness module that reaches an old root leaf. self.assertFalse(hasattr(support, "REGISTERED_RESIDUALS")) self.assertFalse(hasattr(report, "residuals")) self.assertNotIn("residual", kinds) self.assertEqual(report.violations, ()) - self.assertIn("harness reaches model/infra", kinds) - self.assertIn("legacy run forwarders: 3", report.blockers) + # What keeps isolation false after R1: the parser entry due in A1, the six + # Harness modules that R2 closes and the five loaders that R3 closes. + self.assertEqual( + report.blockers, + ( + "temporary leaf importers: 1", + "harness reaches model/infra: inferrsi.harness.agent_sessions -> " + "inferrsi.infra (12 hits)", + "runtime resource loaders: 5", + ), + ) + self.assertEqual(len(report.harness_model_infra), 12) + self.assertFalse(report.new_core_closure_isolated) def test_split_skill_eval_modules_reach_no_model_or_infra(self) -> None: # S01: the split Harness modules own their writes and digests. The @@ -153,6 +182,9 @@ def test_real_tree_allows_apps_and_same_core_only(self) -> None: "benchmark/edge.py": "from inferrsi.benchmark import edge\n", "benchmark/forbidden.py": "import inferrsi.harness\n", "cli.py": "from inferrsi.benchmark.registry import builtin_tasks\n", + "arguments.py": "from inferrsi.knowledge import catalog\n", + # The listing is the exact file name: a module beside it gains nothing. + "arguments_extra.py": "from inferrsi.knowledge import catalog\n", "benchmark_operations.py": "import inferrsi.benchmark\n", "unowned.py": "import inferrsi.benchmark\n", } @@ -164,6 +196,7 @@ def test_real_tree_allows_apps_and_same_core_only(self) -> None: self.assertEqual( violations, [ + "arguments_extra.py imports inferrsi.knowledge", "benchmark/forbidden.py imports inferrsi.harness", "harness/probe.py imports inferrsi.knowledge", "knowledge/out.py imports inferrsi.harness", diff --git a/tests/test_vllm_ascend_rc_skill.py b/tests/test_vllm_ascend_rc_skill.py index 3fe4b6b86..53b4494a9 100644 --- a/tests/test_vllm_ascend_rc_skill.py +++ b/tests/test_vllm_ascend_rc_skill.py @@ -7,9 +7,9 @@ import pytest -from inferrsi.catalog import search_knowledge -from inferrsi.knowledge_operations import KnowledgeReader -from inferrsi.skill_catalog import skill_instructions, skill_metadata, skill_resource +from inferrsi.knowledge.catalog import search_knowledge +from inferrsi.knowledge.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.skill_catalog import skill_instructions, skill_metadata, skill_resource LIBRARY = Path(__file__).resolve().parents[1] / "src/inferrsi/knowledge/data/skill_library" NAME = "vllm-ascend" diff --git a/tools/build_skill_coverage.py b/tools/build_skill_coverage.py index 639bd49d6..e06a8d087 100644 --- a/tools/build_skill_coverage.py +++ b/tools/build_skill_coverage.py @@ -6,7 +6,7 @@ from difflib import unified_diff from pathlib import Path -from inferrsi.skill_catalog import skill_directories, skill_metadata +from inferrsi.knowledge.skill_catalog import skill_directories, skill_metadata LIBRARY = Path("src/inferrsi/knowledge/data/skill_library") INDEX = LIBRARY / "skill-index.json" diff --git a/tools/build_skill_index.py b/tools/build_skill_index.py index ee036da42..9962d4b88 100644 --- a/tools/build_skill_index.py +++ b/tools/build_skill_index.py @@ -13,8 +13,8 @@ import subprocess from pathlib import Path -from inferrsi import _source_snapshot -from inferrsi.skill_catalog import skill_metadata, skill_records +from inferrsi.knowledge import _source_snapshot +from inferrsi.knowledge.skill_catalog import skill_metadata, skill_records PROJECT = Path(__file__).resolve().parents[1] # Where the inputs and generated records live today. diff --git a/tools/evaluate_indexer.py b/tools/evaluate_indexer.py index 9dbbb5814..60099ab5b 100644 --- a/tools/evaluate_indexer.py +++ b/tools/evaluate_indexer.py @@ -94,8 +94,8 @@ def cli(root, mode, case): def worker(root, action, payload): sys.path.insert(0, str(root / "src")) - from inferrsi.catalog import search_knowledge - from inferrsi.knowledge_operations import KnowledgeReader + from inferrsi.knowledge.catalog import search_knowledge + from inferrsi.knowledge.knowledge_operations import KnowledgeReader if action == "identity": return identity(root) @@ -136,7 +136,7 @@ def worker(root, action, payload): raise ValueError("API/CLI payload mismatch: " + api) return rows if action == "contracts": - from inferrsi._skill_selection import SkillIndexError, indexed_skill + from inferrsi.knowledge._skill_selection import SkillIndexError, indexed_skill reader = KnowledgeReader() cgroup = "inferrsi.common.skills.cgroup_resource_controls" afd = "inferrsi.cuda.skills.audit_afd_p2p_exchanges" diff --git a/tools/skill_eval_compare.py b/tools/skill_eval_compare.py index baf144227..5615d61cd 100644 --- a/tools/skill_eval_compare.py +++ b/tools/skill_eval_compare.py @@ -20,7 +20,7 @@ from inferrsi.harness.skill_evaluation import CodexClient from inferrsi.harness.usage import normalize_usage from inferrsi.infra.artifacts import atomic_write_json, sha256, write_json -from inferrsi.knowledge_operations import KnowledgeReader, safe_path +from inferrsi.knowledge.knowledge_operations import KnowledgeReader, safe_path from inferrsi.model.anthropic_agent import ( FULL_CONTEXT_KIND, AnthropicAgentClient, diff --git a/tools/skill_eval_improve.py b/tools/skill_eval_improve.py index abc19a595..46bc6d705 100644 --- a/tools/skill_eval_improve.py +++ b/tools/skill_eval_improve.py @@ -17,7 +17,7 @@ from inferrsi.harness.skill_evaluation import CodexClient from inferrsi.harness.usage import usage_totals from inferrsi.infra.artifacts import sha256, write_json -from inferrsi.knowledge_operations import KnowledgeReader, safe_path +from inferrsi.knowledge.knowledge_operations import KnowledgeReader, safe_path from tools import skill_eval_compare as compare CONTROLLERS = (*compare.CONTROLLERS, "tools/skill_eval_improve.py", diff --git a/tools/skill_eval_pilot.py b/tools/skill_eval_pilot.py index aca3cebf6..7d17f8537 100644 --- a/tools/skill_eval_pilot.py +++ b/tools/skill_eval_pilot.py @@ -11,7 +11,7 @@ from inferrsi.harness.skill_evaluation import CodexClient from inferrsi.harness.usage import normalize_usage from inferrsi.infra.artifacts import sha256, write_json -from inferrsi.knowledge_operations import KnowledgeReader +from inferrsi.knowledge.knowledge_operations import KnowledgeReader ROOT = Path(__file__).resolve().parents[1] SKILLS = ("inference-benchmark-design", "dense-performance-evidence") diff --git a/tools/skill_eval_snapshot.py b/tools/skill_eval_snapshot.py index c6db86b32..bcf0593c9 100644 --- a/tools/skill_eval_snapshot.py +++ b/tools/skill_eval_snapshot.py @@ -14,8 +14,8 @@ import unicodedata from pathlib import Path, PurePosixPath -from inferrsi.catalog import BACKENDS -from inferrsi.knowledge_operations import KnowledgeReader, safe_path +from inferrsi.knowledge.catalog import BACKENDS +from inferrsi.knowledge.knowledge_operations import KnowledgeReader, safe_path GIT_READ = ("git", "--no-replace-objects", "--no-lazy-fetch") @@ -44,7 +44,7 @@ def selected_snapshot(repo: Path, revision: str, skill_root: str, skill: str, """ # Only materializing checkout skills requires the optional PyYAML parser; # reporting and controller imports must remain usable in the base install. - from inferrsi.skill_catalog import _skill_name, _unique_fields, skill_metadata + from inferrsi.knowledge.skill_catalog import _skill_name, _unique_fields, skill_metadata if not re.fullmatch(r"[0-9a-f]{40}", revision): raise ValueError("Selected snapshot requires a full immutable Git commit SHA")