diff --git a/.agents/commands/flext-law.md b/.agents/commands/flext-law.md deleted file mode 100644 index c997482b4..000000000 --- a/.agents/commands/flext-law.md +++ /dev/null @@ -1,4 +0,0 @@ -# FLEXT Law session - -Read `.agents/skills/flext-context-routing/SKILL.md` and follow its composition. -Do not resolve `flext-law` by unqualified catalog name. diff --git a/.agents/provider.toml b/.agents/provider.toml deleted file mode 100644 index b51cd6784..000000000 --- a/.agents/provider.toml +++ /dev/null @@ -1,12 +0,0 @@ -source_project_id = "flext-sh/flext" -marker_distribution = "flext-core" -router_skill = "flext-context-routing" -session_command = ".agents/commands/flext-law.md" -codemod_provider = "flext-infra/src/flext_infra/codemod/sgconfig.yml" -marketplaces_key = "extraKnownMarketplaces" -enabled_plugins_key = "enabledPlugins" -required_surfaces = ["flext-infra/src/flext_infra/codemod/sgconfig.yml"] - -[surfaces] -always = [".agents/skills/flext-context-routing/SKILL.md"] -on_demand = [".agents/skills/flext-law/SKILL.md"] diff --git a/.agents/skills/flext-context-routing/SKILL.md b/.agents/skills/flext-context-routing/SKILL.md deleted file mode 100644 index 17bb47e3b..000000000 --- a/.agents/skills/flext-context-routing/SKILL.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: flext-context-routing -description: Route FLEXT repositories through global execution skills and the branch-matched local flext-law domain delta after flext-core dependency detection. ---- - -# FLEXT Context Routing - -This is the sole always-loaded local FLEXT surface. It selects the exact -branch-matched FLEXT law without duplicating universal execution governance. - -## Required composition - -1. Read `${config.AiHub.paths.agents_home}/skills/inviolable-rules/SKILL.md`. -2. Before build, generation, docs, checks, tests, or diagnosis, read - `${config.AiHub.paths.agents_home}/skills/make-check/SKILL.md`. -3. For every FLEXT task, read the exact local - `.agents/skills/flext-law/SKILL.md`. -4. At every completion boundary, read - `${config.AiHub.paths.agents_home}/skills/verification-loop/SKILL.md`. - -Fail closed if a required file is absent. Never resolve `flext-law` by an -unqualified catalog name, from `main`, or from another checkout. - -## Detection and scope - -- Activate when the workspace provider marker or dependency graph contains - `flext-core`. -- In workspace mode, use the active workspace root and its checked-out law. -- In standalone mode, use the FLEXT root law pinned to the same branch or - release; never fall back to `main`. -- Load only local surfaces declared in `.agents/provider.toml`. Global skills - remain owned by `config.AiHub.paths.agents_home` and are not copied into the local provider. - -## Memory and MCP - -- `AGENTS.md` Learned sections: continual-learning only (≤12 bullets/section, high-signal; never promote law). -- Index: `.cursor/hooks/state/continual-learning-index.json`. -- MCP via ai-hub gateway: see `${config.AiHub.paths.ai_hub}/docs/MCP_AGENT_GUIDE.md` (beads/memory/CRG/ast-grep; Make for Done). - -## ADR boundary - -Architecture decisions remain in `docs/architecture/adr/`. This skill routes to law; it does not replace ADR ownership. diff --git a/.agents/skills/flext-law/SKILL.md b/.agents/skills/flext-law/SKILL.md deleted file mode 100644 index c437caa0f..000000000 --- a/.agents/skills/flext-law/SKILL.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -name: flext-law -description: Apply the FLEXT-only architecture, workspace, generation, import, and fleet delta over canonical global execution governance. ---- - -# FLEXT Law - -## Composition - -Sole `flext-law` owner. Globals: `inviolable-rules`, `make-check`, `verification-loop`. Load via `flext-context-routing` only. - -## Architecture and imports - -`flext-core <- consumers`. `flext-infra` never runtime. Facades `c→t→p→m→u` + `r/e/x/h/d/s`; reverse `TYPE_CHECKING`-only. One `api.py`; lazy generated root. `from import s`. Declarations = data; Pydantic v2 + `t.*`/`p.*` at boundaries. - -## Search before inventing - -Search `c→t→p→m→u` (+`r`/`e`) before helpers. Prefer `u.*`. Bead-note if inventing. - -## Sources, generation, and commands - -SSOT `config/*.yaml`+settings/schemas. Cutover: owner→regen→delete old. No hand-edit generated consumers. See `make-check`. Fixtures via `flext-tests` facades. - -## Config Settings Are SSOT - -Config owns configurable facts — never hardcode in code/tests (`UNIVERSAL_CORE` P0). - -## Runtime First Completion Gates - -See `verification-loop` + public-facade QA. - -## Fleet boundary - -First-party/standalone share branch-matched law. External/content-only: no mutate. CI = conform + overlays. - -## Fleet ancestry and managed topology - -Typed inventory+Beads. `0.12.0-dev` ancestor of `main`/`0.20.0-dev`/required branches/worktrees. Merge-forward only. Exclude `gh/*`, Dolt, archives, `external`/`content_only`. Members before superproject gitlinks. - -## Toolchain and conform - -Conform from Git+manifest. Mise binaries; no `uv`/Python patch pins (`3.13.*`). Ruff+Pyrefly; changed Pyright/Mypy/Pytest. Helm serialized. Release = `flext-infra` Make release/version. `ast-grep` for systemic transforms. - -### `make work` lane saga - -Public WHAT is only `start|status|land|finish`. `FlextInfraWorkService` is an internal engine — do not expose `WHAT=worktree`. Use `PROJECT=` (or `WORKSPACE=`) so land/finish resolve the member git primary. Land owns the lane PR; finish binds `metadata.worktree` to `registered_lane`, refuses permanent/primary lanes, requires `head_oid` CAS when the lane still exists, and requires a merged PR when `metadata.pr_number` is set (otherwise refuses an open PR on the branch). -Guide: `docs/guides/make-commands.md` · ADR-0016. - -## Documentation and ADRs - -Living docs follow `docs/standards/documentation.md`. Architecture decisions live in `docs/architecture/adr/` (ADR registry). Docs validation requires these ADR references in provider skills. diff --git a/.beads/config.yaml b/.beads/config.yaml index c46b9f962..890d539c2 100644 --- a/.beads/config.yaml +++ b/.beads/config.yaml @@ -7,8 +7,8 @@ # ledger_prefix is an optional distinct issue-prefix override. # Source: dolt server block = flext-infra config/codegen.yaml # toolchain.beads.server (machine-wide shared Dolt server facts). -# Routing-only client config: this attached standalone owns no tracker state; -# bd commands route to the shared ledger below. Local data is never written. +# Owned ledger config: the workspace root owns this tracker; conform +# initializes and verifies it against the pinned binary. issue-prefix: "mcb" # Dolt shared-server connection (machine-wide shared server on :3308). diff --git a/.continue/rules/mcb.md b/.continue/rules/mcb.md index 571c8b0ae..549cc66cb 100644 --- a/.continue/rules/mcb.md +++ b/.continue/rules/mcb.md @@ -2,6 +2,7 @@ name: MCB Project Rules globs: ["**/*.rs", "**/*.toml", "**/*.md"] --- + # MCB — Continue.dev Rules diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fd5f87403..320592668 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -13,10 +13,10 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error **Expected behavior** A clear and concise description of what you expected to happen. @@ -26,16 +26,16 @@ If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** -- OS: [e.g. iOS] -- Browser [e.g. chrome, safari] -- Version [e.g. 22] +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] **Smartphone (please complete the following information):** -- Device: [e.g. iPhone6] -- OS: [e.g. iOS8.1] -- Browser [e.g. stock browser, safari] -- Version [e.g. 22] +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index adc48671e..e10ad7790 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,12 +7,6 @@ updates: interval: weekly open-pull-requests-limit: 5 labels: [dependencies, github-actions] - - package-ecosystem: devcontainers - directory: / - schedule: - interval: weekly - open-pull-requests-limit: 5 - labels: [dependencies, devcontainers] - package-ecosystem: pip directory: / diff --git a/.github/workflows/ci-matrix.yml b/.github/workflows/ci-matrix.yml index 615f300c1..8054d103d 100644 --- a/.github/workflows/ci-matrix.yml +++ b/.github/workflows/ci-matrix.yml @@ -44,6 +44,8 @@ jobs: # full project bootstrap at image-build time), then smoke the verb surface # inside the built container. runs-on: ubuntu-latest + env: + CI: Y strategy: fail-fast: false matrix: @@ -78,6 +80,8 @@ jobs: CI: Y steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: submodules: false # No step needs authenticated git after checkout; persisting the @@ -112,6 +116,8 @@ jobs: CI: Y steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: submodules: false # No step needs authenticated git after checkout; persisting the diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 866c90570..b10997ea4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -88,7 +88,7 @@ jobs: } # mro-o6h5 (agent: kimi) — include-hidden-files: .reports/ is a dot-dir and - # upload-artifact v4 skips hidden paths by default (artifact silently empty). + # upload-artifact skips hidden paths by default (artifact silently empty). - name: Upload docs reports on failure if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.gitignore b/.gitignore index b5fc52d96..c4d431bf0 100644 --- a/.gitignore +++ b/.gitignore @@ -280,44 +280,6 @@ skill-create-output/ .mcp.json CLAUDE.local.md -# BLOCK: machine-local build/editor configuration. -# These encode one workstation (linker, job counts, cache sizes, editor state) -# and break every other environment when versioned. Keep them on disk, out of git. -# Leading slash anchors each rule to the repository root, so test fixtures that -# legitimately ship their own .cargo/ or editor files stay tracked. -/.cargo/ -/.vscode/ -/.waza.yaml -/opencode.jsonc - -# BLOCK: tool output. Reports are produced by a gate, never authored. -# Regenerate with the owning make verb instead of committing a snapshot. -*.sarif -*violations*.txt -build_err.txt -clippy_output.txt -validate_output.txt -test_003_warnings.txt -staged_files.txt -pr[0-9]*_comments.txt -coverage/ - -# BLOCK: compiled and captured test artifacts -*.rlib -/data/vectors/ -/crates/mcb-providers/src/database/sqlite/test_file.txt - -# BLOCK: captured runtime logs. tests/mcb. is server stdout, not a test. -tests/mcb.[0-9]* - -# BLOCK: agent/session scratch -tmp_*.txt -.beads.gate.lock - -# BLOCK: parallel documentation tree. docs/ is the single documentation owner -# (Law 4). context/ duplicated it and described dill/figment, removed by ADR-050/051. -/context/ - # Tracked managed artifacts !.github/ci-template/ !.github/ci-template/ci.yml @@ -343,9 +305,33 @@ tmp_*.txt !.github/workflows/ci-matrix.yml !.github/workflows/ci.yml !.github/workflows/docs.yml -!.github/workflows/release-binaries.yml +!.github/workflows/release.yml !sgconfig.yml +!.vscode/settings.json !LICENSE !config/ !config/workspace.yaml !custom.mk + +# Project-local exceptions (config/workspace.yaml overlay) +/.archive/ +/.cargo/ +/.vscode/ +/.waza.yaml +/opencode.jsonc +*.sarif +*violations*.txt +build_err.txt +clippy_output.txt +validate_output.txt +test_003_warnings.txt +staged_files.txt +pr[0-9]*_comments.txt +coverage/ +*.rlib +/data/vectors/ +/crates/mcb-providers/src/database/sqlite/test_file.txt +tests/mcb.[0-9]* +tmp_*.txt +.beads.gate.lock +/context/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ea15340d5..000000000 --- a/.gitmodules +++ /dev/null @@ -1,2 +0,0 @@ -# third-party/ submodules were removed; SeaQL/Loco forks are now pinned git -# dependencies in Cargo.toml. This file is intentionally empty. diff --git a/.markdownlintignore b/.markdownlintignore index 5e4222b5f..d3a85c472 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -13,3 +13,5 @@ tests/node_modules context/ book/ docs/superpowers/ +.serena/memories/ +crates/mcb-validate/tests/fixtures/rustlings/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c99d382a..a16bfef1e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,45 +2,132 @@ repos: - repo: local hooks: - # Why: fast fixes run before each commit; expensive gates run before push. - # Pre-push is local operator validation and runs every BLOCKING gate: CI=Y - # is never injected here, because it skips the lint/format/pyrefly gates - # that must block a push (9b604d43). make test never inherits that token - # either (mro-v4p5 forbids pytest under CI=Y). + # Why: each make verb is its own pre-commit/pre-push hook so the operator + # sees per-step status (Passed/Failed) natively via the pre-commit runner. + # Every step states the CI token instead of inheriting it. A git hook runs + # with the invoking environment, so a caller that already exported + # CI=Y would otherwise reach the pre-push steps and + # revoke exactly the gates a push must not skip (9b604d43: lint, format, + # pyrefly; mro-v4p5: pytest). Declaring the token closes that carrier. + # pre-commit is the fast mutation pass and runs the CI gate set + # (CI=Y). pre-push is the local blocking gate and runs + # the full set with CI=N, the local arm of the same + # ternary: only the exact value Y disables coverage and + # narrows CHECK_GATES, so N keeps every gate and the + # complete pytest+coverage run. + # pre-commit runs `entry` as an argv, not through a shell, so a leading + # VAR=value would be taken as the program name ("Executable CI=Y not + # found"). `env` is the portable way to state the variable for a step that + # needs no shell; the pre-push steps already own one for `unset`. # A step declaring gates_skip (mro-ucxgl) narrows CHECK_GATES for the hook # context only: local `make check` and the `docs` verb keep the full set. - # WHAT never leaks in from the caller: git hooks inherit the invoking - # environment, so `make work WHAT=land` reached these steps two ways and - # any step without its own WHAT (e.g. `make gen`) rejected it as an - # unsupported selector, making every lane unpushable. BOTH carriers are - # cleared: the exported shell variable, and MAKEFLAGS, which the parent - # make exports as ` -- WHAT=land` and a child make re-reads with - # command-line precedence — so unsetting the variable alone leaves the - # override in place (MAKEOVERRIDES is never exported and clearing it here - # would change nothing). APPLY leaks the same way and - # is cleared with them: `make work ... APPLY=Y` exports it, the hook's - # read-only steps inherit it, and a read-only verb rejects the token - # outright ("verb check is read-only and does not accept APPLY"), so a - # landing lane could not push. Each step then states its selector: the - # declared `what` when it has one, otherwise the verb's own declared - # default — a hook validates everything, never a caller-narrowed subset. - # Mutating steps pass APPLY explicitly, so the verb resolves its own - # apply_what (gen/fmt/fix -> apply) instead of a read-only selector. - - id: flext-pre-commit - name: FLEXT pre-commit workflow - language: system - entry: >- - bash -eu -o pipefail -c - 'unset WHAT MAKEFLAGS APPLY; make fmt APPLY=Y && make fix APPLY=Y' + + - id: flext-pre-commit-setup + name: make setup + language: system + entry: >- + env CI=Y make setup + pass_filenames: false + always_run: true + stages: [pre-commit] + + - id: flext-pre-commit-gen + name: make gen APPLY=Y + language: system + entry: >- + env CI=Y make gen APPLY=Y + pass_filenames: false + always_run: true + stages: [pre-commit] + + - id: flext-pre-commit-fmt + name: make fmt APPLY=Y + language: system + entry: >- + env CI=Y make fmt APPLY=Y + pass_filenames: false + always_run: true + stages: [pre-commit] + + - id: flext-pre-commit-fix + name: make fix APPLY=Y + language: system + entry: >- + env CI=Y make fix APPLY=Y pass_filenames: false always_run: true stages: [pre-commit] - - id: flext-pre-push - name: FLEXT pre-push workflow + + - id: flext-pre-commit-check + name: make check + language: system + entry: >- + env CI=Y make check + pass_filenames: false + always_run: true + stages: [pre-commit] + + + - id: flext-pre-push-setup + name: make setup + language: system + entry: >- + bash -eu -o pipefail -c 'unset $(git rev-parse --local-env-vars); CI=N make setup' + pass_filenames: false + always_run: true + stages: [pre-push] + + - id: flext-pre-push-gen + name: make gen APPLY=Y + language: system + entry: >- + bash -eu -o pipefail -c 'unset $(git rev-parse --local-env-vars); CI=N make gen APPLY=Y' + pass_filenames: false + always_run: true + stages: [pre-push] + + - id: flext-pre-push-fmt + name: make fmt APPLY=Y + language: system + entry: >- + bash -eu -o pipefail -c 'unset $(git rev-parse --local-env-vars); CI=N make fmt APPLY=Y' + pass_filenames: false + always_run: true + stages: [pre-push] + + - id: flext-pre-push-fix + name: make fix APPLY=Y + language: system + entry: >- + bash -eu -o pipefail -c 'unset $(git rev-parse --local-env-vars); CI=N make fix APPLY=Y' + pass_filenames: false + always_run: true + stages: [pre-push] + + - id: flext-pre-push-check + name: make check + language: system + entry: >- + bash -eu -o pipefail -c 'unset $(git rev-parse --local-env-vars); CI=N make check' + pass_filenames: false + always_run: true + stages: [pre-push] + + - id: flext-pre-push-test-cache-clear + name: make test WHAT=cache-clear APPLY=Y + language: system + entry: >- + bash -eu -o pipefail -c 'unset $(git rev-parse --local-env-vars); CI=N make test WHAT=cache-clear APPLY=Y' + pass_filenames: false + always_run: true + stages: [pre-push] + + - id: flext-pre-push-test-full + name: make test WHAT=full language: system entry: >- - bash -eu -o pipefail -c - 'unset WHAT MAKEFLAGS APPLY; unset $(git rev-parse --local-env-vars); make gen APPLY=Y && make check WHAT=all && make test WHAT=cache-clear APPLY=Y && make test WHAT=all' + bash -eu -o pipefail -c 'unset $(git rev-parse --local-env-vars); CI=N make test WHAT=full' pass_filenames: false always_run: true stages: [pre-push] + diff --git a/.serena/memories/architecture.md b/.serena/memories/architecture.md deleted file mode 100644 index 2ad652ecb..000000000 --- a/.serena/memories/architecture.md +++ /dev/null @@ -1,43 +0,0 @@ -# MCB Architecture - -## Clean Architecture Layers - -Dependency flow is strictly inward: -``` -server → infrastructure → providers → domain → utils -``` - -### Layer Rules -| Crate | MUST NOT depend on | Allowed Dependencies | -|-------|-------------------|---------------------| -| mcb-utils | Any internal crate | None (pure utilities) | -| mcb-domain | Any internal crate except mcb-utils | mcb-utils only | -| mcb-providers | mcb-infrastructure, mcb-server | mcb-domain, mcb-utils | -| mcb-infrastructure | mcb-server | mcb-domain, mcb-providers, mcb-utils | -| mcb-server | None | mcb-infrastructure, mcb-utils | - -## Dependency Injection -- **Compile-time registration**: linkme `#[distributed_slice]` for provider discovery -- **Runtime switching**: RwLock-wrapped handles in AppContext -- **Composition root**: `mcb-infrastructure/src/di/bootstrap.rs` - -## Provider Pattern -All external integrations implement domain port traits: -- Location: `mcb-domain/src/ports/providers/` -- Examples: `EmbeddingProvider`, `VectorStoreProvider`, `CacheProvider` -- Registration: linkme distributed slice in provider module - -## Key Architectural Decisions -- **ADR-050**: Manual composition root (no framework DI) -- **ADR-029**: All port traits in mcb-domain (single source of truth) -- **ADR-013**: Clean Architecture crate separation -- **ADR-054/055**: Constants consolidated in mcb-utils - -## Validation -Run `make validate` to enforce architecture rules via mcb-validate: -- Phase 1: Linter checks (Clippy) -- Phase 2: AST pattern queries (Tree-sitter) -- Phase 3: Rule engine validation -- Phase 4: Metrics analysis -- Phase 5: Duplication detection -- Phase 6: Architecture validation diff --git a/.serena/memories/build_test_guide.md b/.serena/memories/build_test_guide.md deleted file mode 100644 index 3129602a4..000000000 --- a/.serena/memories/build_test_guide.md +++ /dev/null @@ -1,63 +0,0 @@ -# MCB Build and Test Guide - -## Essential Commands -All development flows through `make` verbs backed by `scripts/lib/mcb.sh`: - -### Build -```bash -make build # Debug build -make build RELEASE=1 # Release build -``` - -### Test -```bash -make test SCOPE=unit # Unit tests only -make test SCOPE=integration # Integration tests -make test SCOPE=doc # Doctests -make test SCOPE=all # Full suite -``` - -### Quality Gates -```bash -make check WHAT=fmt # Formatting (rustfmt) -make check WHAT=lint # Clippy lints -make check WHAT=validate # Architecture validation -make check WHAT=audit # Security audit -make check WHAT=all # All checks (CI gate) -``` - -### Fix -```bash -make fix WHAT=fmt # Auto-format -make fix WHAT=lint # Auto-fix clippy -make fix WHAT=all # All auto-fixes -``` - -### Validation -```bash -make validate # Run mcb-validate architecture checks -``` - -### Single Test Debugging -```bash -cargo test -p mcb-server --test unit -- test_name -``` - -## Pre-commit Hooks -Installed via `make setup WHAT=hooks`: -- Staged `guard` (banned patterns) -- `rustfmt` -- `clippy --workspace` -- `typos` -- Unit tests - -## CI Gate -```bash -make ci # Equivalent to `check WHAT=all` -``` - -## Architecture Validation -The `mcb-validate` crate enforces Clean Architecture compliance: -```bash -cargo run -p mcb-validate -- validate -``` diff --git a/.serena/memories/coding_standards.md b/.serena/memories/coding_standards.md deleted file mode 100644 index fd2a36455..000000000 --- a/.serena/memories/coding_standards.md +++ /dev/null @@ -1,43 +0,0 @@ -# MCB Coding Standards (Rust) - -## Error Handling -- **NEVER** use `unwrap()`, `expect()`, `panic!()`, `todo!()`, `unimplemented!()` in production paths -- Use `thiserror` constructors: `Error::vcs("msg")` -- Use `?` for propagation -- Prefer `Result` over panics - -## Imports Order -1. `std` -2. External crates -3. `mcb_*` crates -4. Local modules - -## Type Safety -- Most restrictive type that compiles -- No `Any`, bare `object`, or unchecked casts -- No suppression directives (`# type: ignore`, blanket `# noqa`) - -## Code Style -- Source files should stay under ~200 lines; split modules before growing -- Use existing macros: `tool_action!`, `tool_schema!`, `tool_enum!`, `register_tool!` -- Generated docs and reports fixed at generator/template - -## Lint Policy (from Cargo.toml) -**Denied**: `unsafe_code`, `dead_code`, `unused_imports`, `dbg_macro`, `todo`, `unimplemented`, `exit`, `rc_mutex`, `try_err` -**Warned**: `unwrap_used`, `expect_used`, `panic`, `print_stdout`, `print_stderr` - -## Unsafe Code -- `unsafe_code = "deny"` at workspace level -- No exceptions without explicit ADR - -## Testing -- Unit tests: `make test SCOPE=unit` -- Integration tests: `make test SCOPE=integration` -- Doctests: `make test SCOPE=doc` -- Use `mockall` for mocking, `rstest` for parameterized tests - -## Documentation -- `missing_docs = "warn"` at workspace level -- Document `Result` error conditions (`missing_errors_doc`) -- Document panic conditions (`missing_panics_doc`) -- Use backticks for code in docs (`doc_markdown`) diff --git a/.serena/memories/memory_maintenance.md b/.serena/memories/memory_maintenance.md deleted file mode 100644 index 6f84514d7..000000000 --- a/.serena/memories/memory_maintenance.md +++ /dev/null @@ -1,33 +0,0 @@ -# Memory Maintenance - -## Discovery Model - -- Core principle: progressive discovery through references, building a graph of memories. -- Initially, agents are provided with the list of all memories (names only). -- Agents should read `mem:core` as the top-level entry point (graph root). - This memory should contain references to other memories covering major project domains. - The referenced memories shall, in turn, shall contain references to even more specific memories, and so on. - The depth of the graph shall depend on the project complexity. -- Use topics/folders to group related memories in order to make the content structure explicit. - Folders can mirror project structure (e.g. modules like frontend/backend) or topics like debugging, architecture, etc. -- Memory references must use a mem: prefix inside backticks, e.g. `mem:frontend/core`. - The surrounding text should clearly indicate when to read the memory/which content to expect. - The text should provide more precise guidance than the memory name alone, - i.e. avoid a reference like "frontend debugging: `mem:frontend/debugging` and instead make clear which aspects of frontend debugging are covered. -- Memories themselves should not contain information about when to read them; this is the responsibility of the referring memory. - -## Style - -Dense agent notes, not prose docs. Prefer invariants, terse bullets. -Avoid obvious context, rationale, and examples unless they prevent likely mistakes. -Keep guidance durable and generalizable, not task-local. - -## Add/update threshold - -Add or update memories only with stable, non-obvious project conventions that avoid complex rediscovery in the future. -Do not add: quick-read facts; generic language/framework knowledge; one-off task notes; volatile line-level details; behavior likely to change soon. - -## Maintenance Actions - -- Renaming memories: References are updated automatically if handled via Serena's memory rename tool. -- Checking for stale memories (e.g. after deletion): Call `serena memories check` for a report. \ No newline at end of file diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md deleted file mode 100644 index 87159225d..000000000 --- a/.serena/memories/project_overview.md +++ /dev/null @@ -1,43 +0,0 @@ -# MCB Project Overview - -## Identity -- **Name**: Memory Context Browser (MCB) -- **Version**: 0.3.2 -- **Repository**: https://github.com/marlonsc/mcb -- **License**: MIT -- **Author**: Marlon Costa - -## Purpose -MCB is a high-performance, extensible Model Context Protocol (MCP) server that provides semantic code search, persistent agent memory, and architecture validation capabilities to AI assistants. - -## Core Capabilities -- **Semantic Code Search**: Natural language to code search using vector embeddings -- **Persistent Agent Memory**: Store and retrieve observations, sessions, and context -- **Architecture Validation**: Enforce Clean Architecture rules and code quality gates -- **Multi-Language Support**: AST-based parsing for Rust, Python, JavaScript, TypeScript, and more - -## Technology Stack -- **Language**: Rust 2024 edition, MSRV 1.92 -- **Async Runtime**: Tokio -- **Web Framework**: Axum (HTTP), Loco.rs (application framework) -- **ORM**: SeaORM 2.0.0-rc.38 (SQLite/PostgreSQL) -- **Vector Stores**: EdgeVec (default), Milvus -- **Embeddings**: FastEmbed (default), Ollama, OpenAI -- **DI Pattern**: linkme distributed slices + AppContext composition root - -## Workspace Structure -7 crates in Clean Architecture layers: -1. `mcb-utils` — pure utilities, zero internal deps -2. `mcb-domain` — entities, ports, errors -3. `mcb-providers` — port adapters (embedding, vector store, cache, etc.) -4. `mcb-infrastructure` — composition root, config, DI wiring -5. `mcb-server` — MCP protocol handlers, HTTP API -6. `mcb-validate` — architecture rule engine -7. `mcb` — CLI facade binary - -## Key Files -- `Cargo.toml` — workspace definition, dependencies, lint policy -- `Makefile` — canonical developer commands -- `AGENTS.md` — project rules and conventions -- `config/development.yaml` — runtime configuration -- `docs/architecture/ARCHITECTURE.md` — comprehensive architecture docs diff --git a/.superpowers/sdd/progress.md b/.superpowers/sdd/progress.md index ac32e5f2c..f249f20be 100644 --- a/.superpowers/sdd/progress.md +++ b/.superpowers/sdd/progress.md @@ -6,8 +6,10 @@ Bead: mcb-c1kn ## Tasks ### Task 1: CI gate + commit nextest changes + push + Status: IN_PROGRESS Base commit: dee9dc581 ### Task 2: Close bead mcb-c1kn + Status: PENDING diff --git a/AGENTS.md b/AGENTS.md index 2b504d067..022a4df51 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — mcb -> **Parent workspace law** lives in [`../AGENTS.md`](../AGENTS.md) — read it first. +> **Parent workspace law** lives in [`AGENTS.md`](https://raw.githubusercontent.com/marlonsc/agents/0.12.0-dev/AGENTS.md) — read it first. > Universal engineering core: `~/.agents/UNIVERSAL_CORE.md`. Composition: global skills + parent/root `AGENTS.md` + this scope delta. Do not re-embed universal law. > > **Standalone / independent mode:** when `../AGENTS.md` does not resolve, pin the parent raw `AGENTS.md` URL to the same branch/release as this package (never `main`). @@ -8,11 +8,3 @@ - - -## Workspace providers - -These routes are generated from provider-owned manifests. - -- flext: read `.agents/skills/flext-context-routing/SKILL.md` first. - diff --git a/FLEXT_TO_MCB_MAPPING.md b/FLEXT_TO_MCB_MAPPING.md deleted file mode 100644 index 8b672402e..000000000 --- a/FLEXT_TO_MCB_MAPPING.md +++ /dev/null @@ -1,136 +0,0 @@ - -# FLEXT → MCB Python Mapping - -This document maps [FLEXT](https://github.com/marlonsc/flext) primitives to the -names and conventions used in MCB's Python tooling under `scripts/`. - -## Result container - -| FLEXT | MCB | Notes | -|-------|-----|-------| -| `flext_core.FlextResult[T]` | `lib.core.r[T]` / `McbResult[T]` | Imported as `r` for brevity. | -| `.success` / `.failure` | same | Boolean status. | -| `.unwrap()` | same | Returns value or raises `RuntimeError`. | -| `.error` | same | `str \| None` message. | -| `.ok(value)` / `.fail(msg)` | same | Constructors. | - -**Good:** - -```python -from lib.core import r - -def parse(path: Path) -> r[list[Issue]]: - if not path.exists(): - return r[list[Issue]].fail(f"not found: {path}") - return r[list[Issue]].ok(_do_parse(path)) -``` - -**Bad:** - -```python -def parse(path: Path) -> list[Issue]: - if not path.exists(): - print(f"not found: {path}") - return [] -``` - -## Settings - -| FLEXT | MCB | Notes | -|-------|-----|-------| -| `FlextSettingsBase` | `lib.settings.BaseMcbSettings` | `MCB_` env prefix. | -| — | `lib.settings.BaseCommandSettings` | No env prefix; for `cosmos-command` scripts. | -| — | `lib.settings.McbSettings` | Shared MCB paths (`project_root`, `k8s_dir`, `docs_dir`, `qlty_*`). | -| `.model_config` | same | Pydantic `SettingsConfigDict`. | - -**Good:** - -```python -from lib.core import BaseCommandSettings, get_logger, r -from pydantic import Field - -class GitopsSettings(BaseCommandSettings): - root: Path = Field(default=Path("."), description="Project root") -``` - -**Bad:** - -```python -ROOT = Path(__file__).resolve().parents[2] # hardcoded traversal -``` - -## Logging - -| FLEXT | MCB | Notes | -|-------|-----|-------| -| `FlextLogger.fetch_logger(name)` | `lib.core.get_logger(name)` | Returns structlog logger. | - -**Good:** - -```python -logger = get_logger(__name__) -logger.info("validated", count=len(items)) -``` - -**Bad:** - -```python -print("validated", len(items)) -``` - -## Service base - -| FLEXT | MCB | Notes | -|-------|-----|-------| -| `FlextService` | `lib.core.McbScriptsService` | For stateful scripts that need lifecycle. | - -Use when a script holds clients, caches, or background resources. Most -command-line scripts do not need it. - -## CLI wiring - -MCB uses `lib.cli.create_app_with_common_params` and -`lib.cli.register_result_command` to expose a handler that returns `r[T]`. - -**Good:** - -```python -from lib.cli import create_app_with_common_params, register_result_command - -def run(settings: MySettings) -> r[int]: - ... - -def main() -> None: - app = create_app_with_common_params(name="my-cmd", help_text="...") - register_result_command(app, name="run", help_text="...", model_cls=MySettings, handler=run) - app() -``` - -## Import order - -Groups, separated by a blank line: - -1. `from __future__ import annotations` -2. `stdlib` (`pathlib`, `subprocess`, ...) -3. Third-party (`typer`, `pydantic`, `flext_core`) -4. `lib.*` helpers -5. Project subpackages (`qlty.*`, `docs.py.*`) - -Run `ruff check --select I` to verify. - -## Testing - -| Practice | Example | -|----------|---------| -| Unwrap results | `result = fn(); assert not result.failure; value = result.unwrap()` | -| Use pytest | `pytest scripts/lib/tests` | -| Avoid `print` / `sys.exit` in handlers | Return `r[T]` and let CLI handle exit codes | - -## Banned patterns - -Enforced by `make check WHAT=guard`: - -- `unwrap()`, `expect()`, `panic!`, `todo!`, `unimplemented!` in production paths -- `TODO` / `FIXME` comments -- `print()` / `sys.exit()` in handler logic -- Hardcoded paths outside `McbSettings` diff --git a/Makefile b/Makefile index b8dbf5011..4d9fa6c1a 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ PYTEST_PROCESS_TIMEOUT_SECONDS ?= 1920 # MYPY_BOUNDED, so a hung run is terminated even if the typed runner stalls. PYTEST_BOUNDED = timeout --signal=TERM --kill-after=5s "$(PYTEST_PROCESS_TIMEOUT_SECONDS)s" PYTEST_REPORTS_DIR ?= .reports/tests -override PYTEST_CASE_TIMEOUT_SECONDS := 30 +override PYTEST_CASE_TIMEOUT_SECONDS := 90 override PYTEST_RUN_TIMEOUT_SECONDS := 1800 override PYTEST_TERMINATION_GRACE_SECONDS := 2 override PYTEST_TIMEOUT_EXIT_CODE := 124 @@ -118,8 +118,8 @@ endif # === SECTION: verb dispatch (managed) === # Source: config:make.verbs[*].whats, config:make.check_gates_allowed, # config:make.check_gates_default -PUBLIC_VERBS := help setup deps build check test fmt fix run status docs clean release gen work mod -BUILTIN_VERBS := help setup deps build check test fmt fix run status docs clean release gen work mod +PUBLIC_VERBS := help setup deps build check test fmt fix run status clean release gen work mod +BUILTIN_VERBS := help setup deps build check test fmt fix run status clean release gen work mod SCRIPT_VERBS := _ALLOWED_WHATS_help := usage $(shell sed -n 's/^_custom_help_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') @@ -132,7 +132,6 @@ _ALLOWED_WHATS_fmt := check all apply $(shell sed -n 's/^_custom_fmt_\([a-z0-9_- _ALLOWED_WHATS_fix := check all apply $(shell sed -n 's/^_custom_fix_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') _ALLOWED_WHATS_run := default $(shell sed -n 's/^_custom_run_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') _ALLOWED_WHATS_status := diagnostics $(shell sed -n 's/^_custom_status_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') -_ALLOWED_WHATS_docs := all generate fix audit build validate $(shell sed -n 's/^_custom_docs_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') _ALLOWED_WHATS_clean := status generated $(shell sed -n 's/^_custom_clean_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') _ALLOWED_WHATS_release := status rel $(shell sed -n 's/^_custom_release_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') _ALLOWED_WHATS_gen := check all apply $(shell sed -n 's/^_custom_gen_\([a-z0-9_-]*\):.*/\1/p' "$(MAKEFILE_ROOT)/custom.mk" 2>/dev/null | sort -u | tr '\n' ' ') @@ -141,13 +140,12 @@ _ALLOWED_WHATS_mod := check all apply $(shell sed -n 's/^_custom_mod_\([a-z0-9_- CHECK_GATES_ALLOWED := lint format pyrefly mypy pyright security markdown smells CHECK_GATES_DEFAULT := lint pyrefly mypy pyright security markdown smells -DOCS_ACTIONS := generate fix audit build validate # End SECTION: verb dispatch # === SECTION: lint/type paths (managed) === # Source: template + computed (script_dispatch conditional) -RUFF_PATHS := $(PROJECT_ROOT)/src $(PROJECT_ROOT)/tests -MYPY_PATHS := $(PROJECT_ROOT)/src $(PROJECT_ROOT)/tests +RUFF_PATHS := $(PROJECT_ROOT)/src $(PROJECT_ROOT)/tests $(PROJECT_ROOT)/scripts +MYPY_PATHS := $(PROJECT_ROOT)/src $(PROJECT_ROOT)/tests $(PROJECT_ROOT)/scripts # End SECTION: lint/type paths # === SECTION: infra bootstrap (managed) === @@ -186,7 +184,6 @@ _DEFAULT_fmt := check _DEFAULT_fix := check _DEFAULT_run := default _DEFAULT_status := diagnostics -_DEFAULT_docs := validate _DEFAULT_clean := status _DEFAULT_release := status _DEFAULT_gen := check @@ -198,7 +195,6 @@ _APPLY_WHAT_test := all _APPLY_WHAT_fmt := apply _APPLY_WHAT_fix := apply _APPLY_WHAT_run := default -_APPLY_WHAT_docs := generate _APPLY_WHAT_clean := generated _APPLY_WHAT_release := rel _APPLY_WHAT_gen := apply @@ -223,7 +219,6 @@ endif # End SECTION: profile routing RUNTIME_VENV := $(RUNTIME_ROOT)/.venv -PROJECT_VENV := $(PROJECT_ROOT)/.venv FLEXT_INFRA_RUNTIME_ROOT := $(if $(filter $(MAKEFILE_ROOT),$(PROJECT_ROOT)),$(RUNTIME_ROOT),$(MAKEFILE_ROOT)) ifeq ($(OS),Windows_NT) RUNTIME_BIN := $(RUNTIME_VENV)/Scripts @@ -275,6 +270,7 @@ else CODEGEN_SCOPE := self ALLOWED_PROJECTS := . endif +CODEGEN_PROJECT_ARGS := $(if $(filter self,$(CODEGEN_SCOPE)),--projects .,) # Workspace-root gate verbs fan out across declared members through the generic # `flext-infra workspace orchestrate` primitive (verb allowlist + CLI group come @@ -287,8 +283,8 @@ endif # clearing a present one is destruction, so it never happens. # A symlinked RUNTIME_VENV points at ANOTHER checkout's environment. `uv` # records editable installs as per-environment `.pth` files holding absolute -# paths, so every import through a borrowed environment loads the owner's -# sources: a lane silently validates the owner's code instead of its own. +# paths, so imports through that environment load the owner's sources: a lane +# silently validates the owner's code instead of its own. # Each checkout therefore owns the environment its own name resolves to. The # link is replaced (removing a link destroys no environment); a real local # environment is never cleared, because a concurrent process may be using it. @@ -314,16 +310,6 @@ SETUP_ENVIRONMENT_RECIPE = set -eu; \ $(UV) sync --project "$(PROJECT_ROOT)" $(UV_SYNC_FLAGS) --link-mode "$(UV_LINK_MODE)"; \ fi -# A delegated runtime lives in another checkout, so this project has no local -# environment of its own. Generated tooling still addresses the environment by -# its project-local name (`$${workspaceFolder}/.venv`), which must never be -# rewritten into a cross-project relative hop: the link makes that name resolve. -# Linking is provisioning, so a real local environment is never replaced. -BORROW_RUNTIME_VENV_RECIPE = set -eu; \ - if [ ! -e "$(PROJECT_VENV)" ] || [ -L "$(PROJECT_VENV)" ]; then \ - ln -sfn "$(RUNTIME_VENV)" "$(PROJECT_VENV)"; \ - fi - WORKSPACE_ORCHESTRATE = $(UV_RUN) python -m flext_infra workspace orchestrate REQUESTED_PROJECTS := $(strip $(if $(PROJECT),$(PROJECT),$(PROJECTS))) # A workspace root owns no local gate implementation: its verbs fan out to the @@ -336,27 +322,17 @@ SELECTED_PROJECTS := $(if $(strip $(REQUESTED_PROJECTS)),$(REQUESTED_PROJECTS),$ WORKSPACE_PROJECT_ARGS := $(foreach project,$(SELECTED_PROJECTS),--projects $(project)) WORKSPACE_CHECK_ARGS := $(if $(strip $(CHECK_GATES)),--make-arg "CHECK_GATES=$(strip $(CHECK_GATES))") WORKSPACE_TEST_ARGS := $(if $(strip $(FLEXT_PYTEST_FILE_RAW)),--file "$${FLEXT_PYTEST_FILE_RAW}") $(if $(strip $(FLEXT_PYTEST_MATCH_RAW)),--match "$${FLEXT_PYTEST_MATCH_RAW}") $(if $(strip $(FLEXT_PYTEST_WHAT_RAW)),--what "$${FLEXT_PYTEST_WHAT_RAW}") -DOCS_PROJECT_ARGS := $(foreach project,$(REQUESTED_PROJECTS),--projects $(project)) -ORCHESTRATED_VERBS := build check clean docs fmt fix scan test val +ORCHESTRATED_VERBS := build check clean fmt fix scan test val -# A borrowed RUNTIME_VENV keeps the primary editable install. Clearing -# PYTHONPATH would make `make test` in a linked worktree execute that primary -# tree instead of this checkout. Prefer PROJECT_ROOT/src so the Makefile owner -# always wins over the shared editable (terminus T4 / path-purity). UV_RUN := env -u MYPYPATH PYTHONPATH="$(PROJECT_ROOT)/src" $(UV) run --project "$(RUNTIME_ROOT)" --no-sync PROJECT_INFRA_PYTHONPATH ?= $(MAKEFILE_ROOT)/src PROJECT_FLEXT_INFRA := test -x "$(FLEXT_INFRA_PYTHON)" || { printf 'ERROR: FLEXT_INFRA_PYTHON must name an executable managed Python\n' >&2; exit 2; }; env -u PYTHONPATH -u MYPYPATH -u VIRTUAL_ENV -u UV_PROJECT -u UV_PROJECT_ENVIRONMENT PATH="$(dir $(FLEXT_INFRA_PYTHON)):$(SANITIZED_CALLER_PATH)" PYTHONPATH="$(PROJECT_INFRA_PYTHONPATH)" $(FLEXT_INFRA_PYTHON) -m flext_infra # mro-j47u (codex): scaffold dev tools live in the validated optional dev # profile; a fresh project must create its lock before later check-mode locks. -# Keyed on the environment's OWNER, not on the caller's profile. A member has -# no local venv -- RUNTIME_VENV is RUNTIME_ROOT/.venv -- so every checkout that -# provisions a shared environment must describe the same contents. A member -# syncing without --all-packages treats the siblings already installed there as -# surplus and uninstalls them, undoing the root's provisioning and leaving -# `uv sync --check` permanently divergent. A standalone project owns its venv -# alone and has no workspace packages to include. -SHARED_RUNTIME := $(if $(filter-out $(PROJECT_ROOT),$(RUNTIME_ROOT)),1,$(if $(strip $(WORKSPACE_MEMBERS)),1,)) -UV_SYNC_FLAGS := $(if $(SHARED_RUNTIME),--all-packages ,)--all-extras --all-groups +# Workspace roots sync every declared package into their local runtime. A +# standalone project has no workspace packages to include. +WORKSPACE_SYNC := $(if $(strip $(WORKSPACE_MEMBERS)),1,) +UV_SYNC_FLAGS := $(if $(WORKSPACE_SYNC),--all-packages ,)--all-extras --all-groups ifneq ($(strip $(PROJECT)),) ifneq ($(strip $(PROJECTS)),) @@ -364,6 +340,11 @@ $(error ERROR: Cannot use PROJECT and PROJECTS together) endif endif +# Script command framework: non-builtin verbs/WHATs route to scripts//. +# A hyphenated WHAT (make check WHAT=no-fallbacks) maps to the underscore module +# stem (scripts/check/no_fallbacks.py) so PEP 8 module names stay valid. +_SCRIPT_DISPATCH_ROOTS := scripts + -include custom.mk SELF_MAKE := $(MAKE) --no-print-directory -f "$(SELF_MAKEFILE)" @@ -400,7 +381,23 @@ define _dispatch if [ "$$custom_rc" -ne 2 ]; then \ $(SELF_MAKE) "$$custom" || exit $$?; \ else \ - $(SELF_MAKE) "$$builtin" || exit $$?; \ + what_norm=$$(printf '%s' "$$what" | tr '-' '_'); \ + script=''; script_what=''; \ + for root in $(_SCRIPT_DISPATCH_ROOTS); do \ + for cand in "$$what_norm" "$$what"; do \ + for ext in py sh; do \ + if [ -z "$$script" ] && [ -f "$(PROJECT_ROOT)/$$root/$(1)/$$cand.$$ext" ]; then \ + script="$(PROJECT_ROOT)/$$root/$(1)/$$cand.$$ext"; script_what="$$cand"; \ + fi; \ + done; \ + done; \ + done; \ + case " $(BUILTIN_VERBS) " in \ + *" $(1) "*) $(SELF_MAKE) "$$builtin" || exit $$? ;; \ + *) if [ -n "$$script" ]; then \ + WHAT="$$script_what" $(UV_RUN) python "$(PROJECT_ROOT)/scripts/dispatch.py" "$(1)" || exit $$?; \ + else printf 'ERROR: declared handler script is missing for %s WHAT=%s\n' "$(1)" "$$what" >&2; exit 2; fi ;; \ + esac; \ fi; \ for hook in "post-$(1)-$$what" "post-$(1)"; do \ $(SELF_MAKE) -q "$$hook" >/dev/null 2>&1; rc=$$?; \ @@ -430,7 +427,7 @@ define _run_for_selected_projects done endef -.PHONY: $(PUBLIC_VERBS) _builtin_help_usage _builtin_setup_environment _builtin_deps_check _builtin_deps_lock _builtin_deps_upgrade _builtin_build_artifacts _builtin_check_all _builtin_test_all _builtin_test_full _builtin_test_cache-status _builtin_test_cache-clear _builtin_test_cache-checkpoint _builtin_fmt_check _builtin_fmt_all _builtin_fmt_apply _builtin_fix_check _builtin_fix_all _builtin_fix_apply _builtin_run_default _builtin_status_diagnostics _builtin_docs_all _builtin_docs_generate _builtin_docs_fix _builtin_docs_audit _builtin_docs_build _builtin_docs_validate _builtin_clean_status _builtin_clean_generated _builtin_release_status _builtin_release_rel _builtin_gen_check _builtin_gen_all _builtin_gen_apply _builtin_work_start _builtin_work_status _builtin_work_land _builtin_work_finish _builtin_mod_check _builtin_mod_all _builtin_mod_apply +.PHONY: $(PUBLIC_VERBS) _builtin_help_usage _builtin_setup_environment _builtin_deps_check _builtin_deps_lock _builtin_deps_upgrade _builtin_build_artifacts _builtin_check_all _builtin_test_all _builtin_test_full _builtin_test_cache-status _builtin_test_cache-clear _builtin_test_cache-checkpoint _builtin_fmt_check _builtin_fmt_all _builtin_fmt_apply _builtin_fix_check _builtin_fix_all _builtin_fix_apply _builtin_run_default _builtin_status_diagnostics _builtin_clean_status _builtin_clean_generated _builtin_release_status _builtin_release_rel _builtin_gen_check _builtin_gen_all _builtin_gen_apply _builtin_work_start _builtin_work_status _builtin_work_land _builtin_work_finish _builtin_mod_check _builtin_mod_all _builtin_mod_apply # Every public verb dispatches straight into its private builtin. The verbs # that used to round-trip through the Python serializer keep the environment @@ -537,10 +534,6 @@ _builtin_help_usage: - @printf ' %-10s WHAT=%s APPLY=Y\n' 'docs' "$$(printf '%s' '$(_ALLOWED_WHATS_docs)' | awk '{$$1=$$1; gsub(/ /, "|"); print}')"; - - - @printf ' %-10s WHAT=%s APPLY=Y\n' 'clean' "$$(printf '%s' '$(_ALLOWED_WHATS_clean)' | awk '{$$1=$$1; gsub(/ /, "|"); print}')"; @@ -565,8 +558,10 @@ _builtin_help_usage: @printf ' %-10s %s\n' 'WORKSPACE' 'target repository (default: current project)'; @printf ' %-10s %s\n' 'PROJECT' 'member checkout for work when WORKSPACE unset'; @printf ' %-10s %s\n' 'BEAD' 'lane-root bead id for work start/land/finish'; - @printf ' %-10s %s\n' 'KIND/NAME' 'GitFlow kind and slug for work start'; + @printf ' %-10s %s\n' 'NAME' 'required lane slug for work start'; + @printf ' %-10s %s\n' 'KIND' 'optional feature|bugfix|hotfix|release; omitted derives from Bead issue_type'; @printf ' %-10s %s\n' 'BASE' 'optional integration base override for work start'; + @printf ' %-10s %s\n' 'EPIC' 'registered epic bead id; nests work start as its child lane'; @printf '\n%s\n' 'Custom hooks (custom.mk):'; @printf ' %s\n' 'Define pre-, post-, pre--, post--'; @printf ' %s\n' 'in custom.mk to wrap one declared handler.'; @@ -742,9 +737,21 @@ _builtin_setup_submodules: validate_submodule "$$root" "$$child_path"; \ done +# Every verb depends on this guard. When the interpreter is missing the guard +# provisions it by invoking `setup` instead of failing: a fresh clone, a new +# worktree, or a CI runner has no venv yet, and forcing the operator to run +# `make setup` by hand turns every managed verb into a two-step ritual and +# breaks git hooks (which call `make fmt`/`make fix` in a bare checkout). +# `setup` is idempotent and cheap when the tooling already matches the lock, +# so the auto-provision path costs nothing in the common case. Only a setup +# that itself fails to produce the interpreter is a real error. _builtin_require_environment: @if [ ! -x "$(RUNTIME_PYTHON)" ]; then \ - printf 'ERROR: missing environment interpreter %s; make setup creates it\n' "$(RUNTIME_PYTHON)" >&2; \ + printf '==> environment interpreter missing; provisioning via setup\n' >&2; \ + $(SELF_MAKE) setup || exit $$?; \ + fi + @if [ ! -x "$(RUNTIME_PYTHON)" ]; then \ + printf 'ERROR: setup did not produce the environment interpreter %s\n' "$(RUNTIME_PYTHON)" >&2; \ exit 2; \ fi @@ -774,8 +781,8 @@ _builtin_setup_environment: _builtin_setup_submodules @if [ "$(RUNTIME_ROOT)" = "$(PROJECT_ROOT)" ]; then \ $(SETUP_ENVIRONMENT_RECIPE); \ else \ - $(MAKE) -C "$(RUNTIME_ROOT)" _builtin_setup_environment; \ - $(BORROW_RUNTIME_VENV_RECIPE); \ + env -u MAKEFILES -u GNUMAKEFLAGS -u MAKEFLAGS -u MAKELEVEL -u MAKEOVERRIDES -u MFLAGS -u PYTHONPATH \ + $(MAKE) -C "$(RUNTIME_ROOT)" _builtin_setup_environment; \ fi @$(FLEXT_BINDING_RECIPE) else ifeq ($(MAKE_PROFILE),workspace-root) @@ -825,26 +832,15 @@ _builtin_build_artifacts: # by `make fix APPLY=Y` and formatting by `make fmt APPLY=Y`, both run BEFORE # check. APPLY here made the same tools run twice with conflicting intents, # so it is rejected instead of silently honoured; FIX=1 became the `fix` verb. -# CI=Y omits make.ci.check_gates_skip (ruff + pyrefly). +# CI=Y runs make.ci.check_gates (RULING 2: rules not skip-list). _builtin_check_all: _builtin_require_environment @set -eu; \ gates="$(strip $(CHECK_GATES))"; \ if [ -z "$$gates" ]; then gates="$$(printf '%s' '$(CHECK_GATES_DEFAULT)' | tr ' ' ',')"; fi; \ gates="$$(printf '%s' "$$gates" | tr -d '[:space:]')"; \ if [ "$(strip $(CI))" = "Y" ]; then \ - filtered=""; \ - for gate in $$(printf '%s' "$$gates" | tr ',' ' '); do \ - skip=0; \ - if [ "$$gate" = "lint" ]; then skip=1; fi; \ - if [ "$$gate" = "format" ]; then skip=1; fi; \ - if [ "$$gate" = "pyrefly" ]; then skip=1; fi; \ - if [ "$$gate" = "markdown" ]; then skip=1; fi; \ - if [ "$$skip" -eq 0 ]; then \ - if [ -n "$$filtered" ]; then filtered="$$filtered,$$gate"; else filtered="$$gate"; fi; \ - fi; \ - done; \ - gates="$$filtered"; \ - printf 'INFO: CI=Y omits check gates: lint format pyrefly markdown\n'; \ + gates="mypy,pyright,security,markdown,smells"; \ + printf 'INFO: CI=Y runs check gates: mypy pyright security markdown smells\n'; \ fi; \ for gate in $$(printf '%s' "$$gates" | tr ',' ' '); do \ case " $(CHECK_GATES_ALLOWED) " in *" $$gate "*) ;; \ @@ -918,35 +914,6 @@ _builtin_status_diagnostics: _builtin_require_environment fi @git -C "$(PROJECT_ROOT)" status --short -_builtin_docs_all: - @set -eu; \ - for action in $(DOCS_ACTIONS); do \ - case "$$action" in generate|fix) mode=$(if $(filter Y,$(APPLY)),--apply,--check) ;; *) mode= ;; esac; \ - $(PROJECT_FLEXT_INFRA) docs "$$action" --workspace "$(PROJECT_ROOT)" --output-dir "$(PROJECT_ROOT)/.reports/docs" $$mode $(DOCS_PROJECT_ARGS); \ - done - - -_builtin_docs_generate: - @$(PROJECT_FLEXT_INFRA) docs generate --workspace "$(PROJECT_ROOT)" --output-dir "$(PROJECT_ROOT)/.reports/docs" $(if $(filter Y,$(APPLY)),--apply,--check) $(DOCS_PROJECT_ARGS) - - -_builtin_docs_fix: - @$(PROJECT_FLEXT_INFRA) docs fix --workspace "$(PROJECT_ROOT)" --output-dir "$(PROJECT_ROOT)/.reports/docs" $(if $(filter Y,$(APPLY)),--apply,--check) $(DOCS_PROJECT_ARGS) - - -_builtin_docs_audit: - @$(PROJECT_FLEXT_INFRA) docs audit --workspace "$(PROJECT_ROOT)" --output-dir "$(PROJECT_ROOT)/.reports/docs" $(DOCS_PROJECT_ARGS) - - -_builtin_docs_build: - @$(PROJECT_FLEXT_INFRA) docs build --workspace "$(PROJECT_ROOT)" --output-dir "$(PROJECT_ROOT)/.reports/docs" $(DOCS_PROJECT_ARGS) - - -_builtin_docs_validate: - @$(PROJECT_FLEXT_INFRA) docs validate --workspace "$(PROJECT_ROOT)" --output-dir "$(PROJECT_ROOT)/.reports/docs" $(DOCS_PROJECT_ARGS) - - - # Disposable artifacts (caches, reports, traces) are owned by the flext-infra # clean service and declared in config.make.clean, so the recipe stays a thin # dispatch like every other verb instead of shell that drifts per project. @@ -986,24 +953,17 @@ _builtin_release_rel: _builtin_require_environment $$push_flag \ $$projects_args -# Every command here writes to the SAME root, derived from the invocation -# point. `deps modernize`/`extra-paths` used to receive WORKSPACE_ROOT while -# `conform` received PROJECT_ROOT, so a gen run inside one member rewrote the -# pyproject of ~30 siblings and left each dirty. Because gen runs inside check -# and check runs in the pre-commit hook, one commit in any lane dirtied every -# sibling. It also kept the fixed point out of reach: each run rewrote the siblings, so -# the next run found a difference again. At the workspace root PROJECT_ROOT is -# already the workspace, so fan-out survives exactly where it belongs. +# Generation preserves the caller's scope. Conform owns analyzer roots in the +# rendered tooling context; dependency modernization owns only dependency +# settings that conform does not render. _builtin_gen_check: _builtin_require_environment @$(PROJECT_FLEXT_INFRA) codegen conform --root "$(PROJECT_ROOT)" --scope "$(CODEGEN_SCOPE)" --mode check - @$(PROJECT_FLEXT_INFRA) deps modernize --workspace "$(PROJECT_ROOT)" --check - @$(PROJECT_FLEXT_INFRA) deps extra-paths --workspace "$(PROJECT_ROOT)" --check + @$(PROJECT_FLEXT_INFRA) deps modernize --workspace "$(PROJECT_ROOT)" --check $(CODEGEN_PROJECT_ARGS) _builtin_gen_all: _builtin_require_environment $(call _require_apply) @$(PROJECT_FLEXT_INFRA) codegen conform --root "$(PROJECT_ROOT)" --scope "$(CODEGEN_SCOPE)" --mode apply - @$(PROJECT_FLEXT_INFRA) deps modernize --workspace "$(PROJECT_ROOT)" --apply - @$(PROJECT_FLEXT_INFRA) deps extra-paths --workspace "$(PROJECT_ROOT)" --apply + @$(PROJECT_FLEXT_INFRA) deps modernize --workspace "$(PROJECT_ROOT)" --apply $(CODEGEN_PROJECT_ARGS) _builtin_gen_apply: _builtin_gen_all @@ -1012,7 +972,7 @@ _builtin_work_status: _builtin_work_start: $(call _require_apply) - @$(PROJECT_FLEXT_INFRA) workspace work --workspace "$(WORKSPACE)" --operation start --bead "$(BEAD)" --kind "$(KIND)" --name "$(NAME)" --base "$(BASE)" --apply + @$(PROJECT_FLEXT_INFRA) workspace work --workspace "$(WORKSPACE)" --operation start --bead "$(BEAD)" $(if $(strip $(KIND)),--kind "$(KIND)") --name "$(NAME)" --base "$(BASE)" --epic "$(EPIC)" --apply _builtin_work_land: $(call _require_apply) @@ -1026,7 +986,7 @@ _builtin_work_finish: # handler was ever generated, so the dispatcher resolved a non-existent target # and the verb was unreachable. The codemod engine already exists behind # `flext-infra refactor mod`; these handlers are the missing dispatch into it. -# check is the read-only fixed-point (reports pending fixes); apply runs the +# check reports pending fixes; apply runs the # batch under the ruff/pyrefly rollback circuit. _builtin_mod_check: _builtin_require_environment @$(PROJECT_FLEXT_INFRA) refactor mod --workspace "$(PROJECT_ROOT)" diff --git a/config/managed-artifacts.yaml b/config/managed-artifacts.yaml index f277a0659..f06d8865a 100644 --- a/config/managed-artifacts.yaml +++ b/config/managed-artifacts.yaml @@ -66,19 +66,42 @@ ManagedArtifacts: - TRY400 - TRY401 # The `# /// cosmos-command` header is a DATA contract parsed by - # scripts/lib/cosmos_command.py (HEADER_START), not disabled code. + # src/mcb_scripts/cosmos_command.py (HEADER_START), not disabled code. "scripts/check/gitops.py": - ERA001 - "scripts/check/surface.py": - - ERA001 # TRY003: these modules raise ONE typed error class each (RegistryError, # etc.) whose value is the per-call-site context in the message. Minting # a subclass per message would add ~30 classes with no new behavior. - "scripts/lib/cosmos_command.py": + "src/mcb_scripts/cosmos_command.py": - TRY003 - "scripts/lib/agent_pointers.py": + # S404/S603: this module IS the command dispatcher. Executing a + # repository command is its entire purpose, and the argv it builds + # comes from a validated TOML header plus a shell resolved through + # shutil.which, never from a shell string or unvalidated input. + - S404 + - S603 + "src/mcb_scripts/workspace_command.py": + # Same dispatcher contract as cosmos_command.py. + - S404 + - S603 + "src/mcb_scripts/qlty/runner.py": + # Runs the qlty CLI with a fixed argv; the executable is resolved to an + # absolute path first, so PATH order cannot select a different binary. + - S404 + - S603 + "src/mcb_scripts/gitops.py": + # Renders manifests through helm/kustomize, both resolved absolutely. + - S404 + - S603 + "src/mcb_scripts/agent_pointers.py": - TRY003 - "scripts/lib/tests/test_agent_pointers.py": + # Tests drive the real CLIs and shell entrypoints on purpose: asserting + # observable behaviour is the contract, and a mocked subprocess would + # assert the mock instead of the program. + "tests/python/**": + - S404 + - S603 + - S607 - TRY003 "scripts/codegen-conversions.py": - TRY003 diff --git a/config/workspace.yaml b/config/workspace.yaml index c9910fc16..8035fb9f0 100644 --- a/config/workspace.yaml +++ b/config/workspace.yaml @@ -16,9 +16,22 @@ repository: state: active checkout: independent codegen: conform - package: false - editable: false + # The shared Python library lives at src/mcb_scripts/, so this repository IS + # a package. (This flag does not feed analyzer-root ordering.) + package: true + # The test suite and the scripts/ command tree import mcb_scripts.*, so the + # package must be installed into the runtime venv, not merely present on disk. + editable: true read_only: false + # mcb owns two Python roots: the library at src/mcb_scripts/ and the command + # tree at scripts//, executed by scripts/dispatch.py. Declaring + # the dispatch seam is what makes the generated Makefile append + # $(PROJECT_ROOT)/scripts to RUFF_PATHS/MYPY_PATHS, so the official ruff and + # mypy gates reach the command tree instead of only the library. + script_dispatch: + dispatcher: scripts/dispatch.py + roots: + - scripts project: package_name: "mcb_scripts" class_stem: "McbScripts" @@ -46,3 +59,46 @@ repository_policy_overlays: - project: mcb-scripts beads_enabled: true ci_enabled: true + # mro-jnm1.3 seam: project-local ignore rules belong here, never in the + # generated .gitignore. PR #178 merged 619 files of machine config and + # session residue into develop as if it were source; PR #201 removed them + # and added these rules as the barrier against a repeat. Declared at the + # generator input so `make gen` reproduces them instead of stripping them. + extra_ignored_patterns: + # Recovery archive written by the archive-not-delete rule. It preserves + # superseded files so a removal stays reversible; it is never source, and + # versioning it would resurrect exactly what the removal took out. + - "/.archive/" + # Machine-local build/editor configuration. These encode one workstation + # (linker, job counts, cache sizes, editor state) and break every other + # environment when versioned. The leading slash anchors each rule to the + # repository root, so test fixtures that legitimately ship their own + # .cargo/ or editor files stay tracked. + - "/.cargo/" + - "/.vscode/" + - "/.waza.yaml" + - "/opencode.jsonc" + # Tool output. A report is produced by a gate, never authored; regenerate + # it with the owning make verb instead of committing a snapshot. + - "*.sarif" + - "*violations*.txt" + - "build_err.txt" + - "clippy_output.txt" + - "validate_output.txt" + - "test_003_warnings.txt" + - "staged_files.txt" + - "pr[0-9]*_comments.txt" + - "coverage/" + # Compiled and captured test artifacts. + - "*.rlib" + - "/data/vectors/" + - "/crates/mcb-providers/src/database/sqlite/test_file.txt" + # Captured runtime logs. tests/mcb. is server stdout, not a test. + - "tests/mcb.[0-9]*" + # Agent and session scratch. + - "tmp_*.txt" + - ".beads.gate.lock" + # Parallel documentation tree. docs/ is the single documentation owner; + # context/ duplicated it and described dill and figment, both removed by + # ADR-050 and ADR-051. + - "/context/" diff --git a/crates/mcb-validate/tests/fixtures/rustlings/CHANGELOG.md b/crates/mcb-validate/tests/fixtures/rustlings/CHANGELOG.md index 84985f32c..e79030593 100644 --- a/crates/mcb-validate/tests/fixtures/rustlings/CHANGELOG.md +++ b/crates/mcb-validate/tests/fixtures/rustlings/CHANGELOG.md @@ -59,12 +59,12 @@ ### Added - Add the following exercise lints: - - `forbid(unsafe_code)`: You shouldn't write unsafe code in Rustlings. - - `forbid(unstable_features)`: You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust. - - `forbid(todo)`: You forgot a `todo!()`. - - `forbid(empty_loop)`: This can only happen by mistake in Rustlings. - - `deny(infinite_loop)`: No infinite loops are needed in Rustlings. - - `deny(mem_forget)`: You shouldn't leak memory while still learning Rust. + - `forbid(unsafe_code)`: You shouldn't write unsafe code in Rustlings. + - `forbid(unstable_features)`: You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust. + - `forbid(todo)`: You forgot a `todo!()`. + - `forbid(empty_loop)`: This can only happen by mistake in Rustlings. + - `deny(infinite_loop)`: No infinite loops are needed in Rustlings. + - `deny(mem_forget)`: You shouldn't leak memory while still learning Rust. - Show a link to every exercise file in the list. - Add scroll padding in the list. - Break the help footer of the list into two lines when the terminal width isn't big enough. @@ -111,7 +111,7 @@ ## 6.1.0 (2024-07-10) -#### Added +### Added - `dev check`: Check that all exercises (including community ones) include at least one `TODO` comment. - `dev check`: Check that all exercises actually fail to run (not already solved). @@ -198,7 +198,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.6.1 (2023-09-18) -#### Changed +### Changed - Converted all exercises with assertions to test mode. @@ -213,7 +213,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.6.0 (2023-09-04) -#### Added +### Added - New exercise: `if3`, teaching the user about `if let` statements. - `hashmaps2`: Added an extra test function to check if the amount of fruits is higher than zero. @@ -232,8 +232,8 @@ Then follow the link to the guide about [community exercises](https://rustlings. #### Fixed - `iterators5`: - - Removed an outdated part of the hint. - - Renamed variables to use snake_case. + - Removed an outdated part of the hint. + - Renamed variables to use snake_case. - `vecs2`: Updated the hint to reference the renamed loop variable. - `enums3`: Changed message String in test so that it gets properly tested. - `strings2`: Corrected line number in hint, then removed it (this both happened as part of this release cycle). @@ -252,13 +252,13 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.5.1 (2023-05-17) -#### Fixed +### Fixed - Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix. ## 5.5.0 (2023-05-17) -#### Added +### Added - `strings2`: Added a reference to the book chapter for reference conversion - `lifetimes`: Added a link to the lifetimekata project @@ -292,7 +292,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.4.1 (2023-03-10) -#### Changed +### Changed - `vecs`: Added links to `iter_mut` and `map` to README.md - `cow1`: Changed main to tests @@ -307,17 +307,17 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.4.0 (2023-02-12) -#### Changed +### Changed - Reordered exercises - - Unwrapped `standard_library_types` into `iterators` and `smart_pointers` - - Moved smart pointer exercises behind threads - - Ordered `rc1` before `arc1` + - Unwrapped `standard_library_types` into `iterators` and `smart_pointers` + - Moved smart pointer exercises behind threads + - Ordered `rc1` before `arc1` - **intro1**: Added a note on `rustlings lsp` - **threads1**: Panic if threads are not joined - **cli**: - - Made progress bar update proportional to amount of files verified - - Decreased `watch` delay from 2 to 1 second + - Made progress bar update proportional to amount of files verified + - Decreased `watch` delay from 2 to 1 second #### Fixed @@ -326,8 +326,8 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **quiz2, clippy1, iterators1**: Fixed a typo - **rc1**: Fixed a prompt error - **cli**: - - Fixed a typo in a method name - - Specified the edition in `rustc` commands + - Fixed a typo in a method name + - Specified the edition in `rustc` commands #### Housekeeping @@ -335,7 +335,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.3.0 (2022-12-23) -#### Added +### Added - **cli**: Added a percentage display in watch mode - Added a `flake.nix` for Nix users @@ -367,7 +367,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.2.1 (2022-09-06) -#### Fixed +### Fixed - **quiz1**: Reworded the comment to actually reflect what's going on in the tests. Also added another assert just to make sure. @@ -380,7 +380,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.2.0 (2022-08-27) -#### Added +### Added - Added a `reset` command @@ -396,13 +396,13 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.1.1 (2022-08-17) -#### Bug Fixes +### Bug Fixes - Fixed an incorrect assertion in options1 ## 5.1.0 (2022-08-16) -#### Features +### Features - Added a new `rc1` exercise. - Added a new `cow1` exercise. @@ -414,8 +414,8 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **strings3**: Clarified comment wording - **traits4, traits5**: Fixed line number reference - **traits5**: - - Fixed typo in "parameter" - - Made exercise prefer a traits-based solution + - Fixed typo in "parameter" + - Made exercise prefer a traits-based solution - **lifetimes2**: Improved hint - **threads3**: Fixed typo in hint - **box1**: Replaced `unimplemented!` with `todo!` @@ -423,8 +423,8 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **quiz2**: Fixed a typo - **macros**: Updated the macros book link - **options1**: - - Removed unused code - - Added more granular tests + - Removed unused code + - Added more granular tests - Fixed some comment syntax shenanigans in info.toml #### Housekeeping @@ -436,7 +436,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 5.0.0 (2022-07-16) -#### Features +### Features - Hint comments in exercises now also include a reference to the `hint` watch mode subcommand. @@ -508,7 +508,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.8.0 (2022-07-01) -#### Features +### Features - Added a progress indicator for `rustlings watch`. - The installation script now checks for Rustup being installed. @@ -528,7 +528,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.7.1 (2022-04-20) -#### Features +### Features - The amount of dependency crates that need to be compiled went down from ~65 to ~45 by bumping dependency versions. @@ -548,7 +548,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.7.0 (2022-04-14) -#### Features +### Features - Add move_semantics6.rs exercise (#908) ([3f0e1303](https://github.com/rust-lang/rustlings/commit/3f0e1303e0b3bf3fecc0baced3c8b8a37f83c184)) - **intro:** Add intro section. ([21c9f441](https://github.com/rust-lang/rustlings/commit/21c9f44168394e08338fd470b5f49b1fd235986f)) @@ -558,14 +558,14 @@ Then follow the link to the guide about [community exercises](https://rustlings. - Fix a few spelling mistakes ([1c0fe3cb](https://github.com/rust-lang/rustlings/commit/1c0fe3cbcca85f90b3985985b8e265ee872a2ab2)) - **cli:** - - Move long text strings into constants. ([f78c4802](https://github.com/rust-lang/rustlings/commit/f78c48020830d7900dd8d81f355606581670446d)) - - Replace `filter_map()` with `find_map()` ([9b27e8d](https://github.com/rust-lang/rustlings/commit/9b27e8d993ca20232fe38a412750c3f845a83b65)) + - Move long text strings into constants. ([f78c4802](https://github.com/rust-lang/rustlings/commit/f78c48020830d7900dd8d81f355606581670446d)) + - Replace `filter_map()` with `find_map()` ([9b27e8d](https://github.com/rust-lang/rustlings/commit/9b27e8d993ca20232fe38a412750c3f845a83b65)) - **clippy1:** - - Set clippy::float_cmp lint to deny (#907) ([71a06044](https://github.com/rust-lang/rustlings/commit/71a06044e6a96ff756dc31d7b0ed665ae4badb57)) - - Updated code to test correctness clippy lint with approx_constant lint rule ([f2650de3](https://github.com/rust-lang/rustlings/commit/f2650de369810867d2763e935ac0963c32ec420e)) + - Set clippy::float_cmp lint to deny (#907) ([71a06044](https://github.com/rust-lang/rustlings/commit/71a06044e6a96ff756dc31d7b0ed665ae4badb57)) + - Updated code to test correctness clippy lint with approx_constant lint rule ([f2650de3](https://github.com/rust-lang/rustlings/commit/f2650de369810867d2763e935ac0963c32ec420e)) - **errors1:** - - Add a comment to make the purpose more clear (#486) ([cbcde345](https://github.com/rust-lang/rustlings/commit/cbcde345409c3e550112e449242848eaa3391bb6)) - - Don't modify tests (#958) ([60bb7cc](https://github.com/rust-lang/rustlings/commit/60bb7cc3931d21d3986ad52b2b302e632a93831c)) + - Add a comment to make the purpose more clear (#486) ([cbcde345](https://github.com/rust-lang/rustlings/commit/cbcde345409c3e550112e449242848eaa3391bb6)) + - Don't modify tests (#958) ([60bb7cc](https://github.com/rust-lang/rustlings/commit/60bb7cc3931d21d3986ad52b2b302e632a93831c)) - **errors6:** Remove existing answer code ([43d0623](https://github.com/rust-lang/rustlings/commit/43d0623086edbc46fe896ba59c7afa22c3da9f7a)) - **functions5:** Remove wrong new line and small English improvements (#885) ([8ef4869b](https://github.com/rust-lang/rustlings/commit/8ef4869b264094e5a9b50452b4534823a9df19c3)) - **install:** protect path with whitespaces using quotes and stop at the first error ([d114847f](https://github.com/rust-lang/rustlings/commit/d114847f256c5f571c0b4c87e04b04bce3435509)) @@ -575,9 +575,9 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **move_semantics5:** correct typo (#857) ([46c28d5c](https://github.com/rust-lang/rustlings/commit/46c28d5cef3d8446b5a356b19d8dbc725f91a3a0)) - **quiz1:** update to say quiz covers "If" ([1622e8c1](https://github.com/rust-lang/rustlings/commit/1622e8c198d89739765c915203efff0091bdeb78)) - **structs3:** - - Add a hint for panic (#608) ([4f7ff5d9](https://github.com/rust-lang/rustlings/commit/4f7ff5d9c7b2d8b045194c1a9469d37e30257c4a)) - - remove redundant 'return' (#852) ([bf33829d](https://github.com/rust-lang/rustlings/commit/bf33829da240375d086f96267fc2e02fa6b07001)) - - Assigned value to `cents_per_gram` in test ([d1ee2da](https://github.com/rust-lang/rustlings/commit/d1ee2daf14f19105e6db3f9c610f44293d688532)) + - Add a hint for panic (#608) ([4f7ff5d9](https://github.com/rust-lang/rustlings/commit/4f7ff5d9c7b2d8b045194c1a9469d37e30257c4a)) + - remove redundant 'return' (#852) ([bf33829d](https://github.com/rust-lang/rustlings/commit/bf33829da240375d086f96267fc2e02fa6b07001)) + - Assigned value to `cents_per_gram` in test ([d1ee2da](https://github.com/rust-lang/rustlings/commit/d1ee2daf14f19105e6db3f9c610f44293d688532)) - **structs3.rs:** assigned value to cents_per_gram in test ([d1ee2daf](https://github.com/rust-lang/rustlings/commit/d1ee2daf14f19105e6db3f9c610f44293d688532)) - **traits1:** rename test functions to snake case (#854) ([1663a16e](https://github.com/rust-lang/rustlings/commit/1663a16eade6ca646b6ed061735f7982434d530d)) @@ -588,7 +588,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.6.0 (2021-09-25) -#### Features +### Features - add advanced_errs2 ([abd6b70c](https://github.com/rust-lang/rustlings/commit/abd6b70c72dc6426752ff41f09160b839e5c449e)) - add advanced_errs1 ([882d535b](https://github.com/rust-lang/rustlings/commit/882d535ba8628d5e0b37e8664b3e2f26260b2671)) @@ -604,13 +604,13 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **from_str, try_from_into:** custom error types ([2dc93cad](https://github.com/rust-lang/rustlings/commit/2dc93caddad43821743e4903d89b355df58d7a49)) - **modules2:** fix typo (#835) ([1c3beb0a](https://github.com/rust-lang/rustlings/commit/1c3beb0a59178c950dc05fe8ee2346b017429ae0)) - **move_semantics5:** - - change &mut \*y to &mut x (#814) ([d75759e8](https://github.com/rust-lang/rustlings/commit/d75759e829fdcd64ef071cf4b6eae2a011a7718b)) - - Clarify instructions ([df25684c](https://github.com/rust-lang/rustlings/commit/df25684cb79f8413915e00b5efef29369849cef1)) + - change &mut \*y to &mut x (#814) ([d75759e8](https://github.com/rust-lang/rustlings/commit/d75759e829fdcd64ef071cf4b6eae2a011a7718b)) + - Clarify instructions ([df25684c](https://github.com/rust-lang/rustlings/commit/df25684cb79f8413915e00b5efef29369849cef1)) - **quiz1:** Fix inconsistent wording (#826) ([03131a3d](https://github.com/rust-lang/rustlings/commit/03131a3d35d9842598150f9da817f7cc26e2669a)) ## 4.5.0 (2021-07-07) -#### Features +### Features - Add move_semantics5 exercise. (#746) ([399ab328](https://github.com/rust-lang/rustlings/commit/399ab328d8d407265c09563aa4ef4534b2503ff2)) - **cli:** Add "next" to run the next unsolved exercise. (#785) ([d20e413a](https://github.com/rust-lang/rustlings/commit/d20e413a68772cd493561f2651cf244e822b7ca5)) @@ -630,15 +630,15 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.4.0 (2021-04-24) -#### Bug Fixes +### Bug Fixes - Fix spelling error in main.rs ([91ee27f2](https://github.com/rust-lang/rustlings/commit/91ee27f22bd3797a9db57e5fd430801c170c5db8)) - typo in default out text ([644c49f1](https://github.com/rust-lang/rustlings/commit/644c49f1e04cbb24e95872b3a52b07d692ae3bc8)) - **collections:** Naming exercises for vectors and HashMap ([bef39b12](https://github.com/rust-lang/rustlings/commit/bef39b125961310b34b34871e480a82e82af4678)) - **from_str:** - - Correct typos ([5f7c89f8](https://github.com/rust-lang/rustlings/commit/5f7c89f85db1f33da01911eaa479c3a2d4721678)) - - test for error instead of unwrap/should_panic ([15e71535](https://github.com/rust-lang/rustlings/commit/15e71535f37cfaed36e22eb778728d186e2104ab)) - - use trait objects for from_str ([c3e7b831](https://github.com/rust-lang/rustlings/commit/c3e7b831786c9172ed8bd5d150f3c432f242fba9)) + - Correct typos ([5f7c89f8](https://github.com/rust-lang/rustlings/commit/5f7c89f85db1f33da01911eaa479c3a2d4721678)) + - test for error instead of unwrap/should_panic ([15e71535](https://github.com/rust-lang/rustlings/commit/15e71535f37cfaed36e22eb778728d186e2104ab)) + - use trait objects for from_str ([c3e7b831](https://github.com/rust-lang/rustlings/commit/c3e7b831786c9172ed8bd5d150f3c432f242fba9)) - **functions3:** improve function argument type (#687) ([a6509cc4](https://github.com/rust-lang/rustlings/commit/a6509cc4d545d8825f01ddf7ee37823b372154dd)) - **hashmap2:** Update incorrect assertion (#660) ([72aaa15e](https://github.com/rust-lang/rustlings/commit/72aaa15e6ab4b72b3422f1c6356396e20a2a2bb8)) - **info:** Fix typo (#635) ([cddc1e86](https://github.com/rust-lang/rustlings/commit/cddc1e86e7ec744ee644cc774a4887b1a0ded3e8)) @@ -646,15 +646,15 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **iterators3:** Enabled iterators3.rs to run without commented out tests. ([c6712dfc](https://github.com/rust-lang/rustlings/commit/c6712dfccd1a093e590ad22bbc4f49edc417dac0)) - **main:** Let find_exercise work with borrows ([347f30bd](https://github.com/rust-lang/rustlings/commit/347f30bd867343c5ace1097e085a1f7e356553f7)) - **move_semantics4:** - - Remove redundant "instead" (#640) ([cc266d7d](https://github.com/rust-lang/rustlings/commit/cc266d7d80b91e79df3f61984f231b7f1587218e)) - - Small readbility improvement (#617) ([10965920](https://github.com/rust-lang/rustlings/commit/10965920fbdf8a1efc85bed869e55a1787006404)) + - Remove redundant "instead" (#640) ([cc266d7d](https://github.com/rust-lang/rustlings/commit/cc266d7d80b91e79df3f61984f231b7f1587218e)) + - Small readbility improvement (#617) ([10965920](https://github.com/rust-lang/rustlings/commit/10965920fbdf8a1efc85bed869e55a1787006404)) - **option2:** Rename uninformative variables (#675) ([b4de6594](https://github.com/rust-lang/rustlings/commit/b4de6594380636817d13c2677ec6f472a964cf43)) - **quiz3:** Force an answer to Q2 (#672) ([0d894e6f](https://github.com/rust-lang/rustlings/commit/0d894e6ff739943901e1ae8c904582e5c2f843bd)) - **structs:** Add 5.3 to structs/README (#652) ([6bd791f2](https://github.com/rust-lang/rustlings/commit/6bd791f2f44aa7f0ad926df767f6b1fa8f12a9a9)) - **structs2:** correct grammar in hint (#663) ([ebdb66c7](https://github.com/rust-lang/rustlings/commit/ebdb66c7bfb6d687a14cc511a559a222e6fc5de4)) - **structs3:** - - reword heading comment (#664) ([9f3e8c2d](https://github.com/rust-lang/rustlings/commit/9f3e8c2dde645e5264c2d2200e68842b5f47bfa3)) - - add check to prevent naive implementation of is_international ([05a753fe](https://github.com/rust-lang/rustlings/commit/05a753fe6333d36dbee5f68c21dec04eacdc75df)) + - reword heading comment (#664) ([9f3e8c2d](https://github.com/rust-lang/rustlings/commit/9f3e8c2dde645e5264c2d2200e68842b5f47bfa3)) + - add check to prevent naive implementation of is_international ([05a753fe](https://github.com/rust-lang/rustlings/commit/05a753fe6333d36dbee5f68c21dec04eacdc75df)) - **threads1:** line number correction ([7857b0a6](https://github.com/rust-lang/rustlings/commit/7857b0a689b0847f48d8c14cbd1865e3b812d5ca)) - **try_from_into:** use trait objects ([2e93a588](https://github.com/rust-lang/rustlings/commit/2e93a588e0abe8badb7eafafb9e7d073c2be5df8)) @@ -666,12 +666,12 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **arc1:** Add more details to description and hint (#710) ([81be4044](https://github.com/rust-lang/rustlings/commit/81be40448777fa338ebced3b0bfc1b32d6370313)) - **cli:** Improve the list command with options, and then some ([8bbe4ff1](https://github.com/rust-lang/rustlings/commit/8bbe4ff1385c5c169c90cd3ff9253f9a91daaf8e)) - **list:** - - updated progress percentage ([1c6f7e4b](https://github.com/rust-lang/rustlings/commit/1c6f7e4b7b9b3bd36f4da2bb2b69c549cc8bd913)) - - added progress info ([c0e3daac](https://github.com/rust-lang/rustlings/commit/c0e3daacaf6850811df5bc57fa43e0f249d5cfa4)) + - updated progress percentage ([1c6f7e4b](https://github.com/rust-lang/rustlings/commit/1c6f7e4b7b9b3bd36f4da2bb2b69c549cc8bd913)) + - added progress info ([c0e3daac](https://github.com/rust-lang/rustlings/commit/c0e3daacaf6850811df5bc57fa43e0f249d5cfa4)) ## 4.3.0 (2020-12-29) -#### Features +### Features - Rewrite default out text ([44d39112](https://github.com/rust-lang/rustlings/commit/44d39112ff122b29c9793fe52e605df1612c6490)) - match exercise order to book chapters (#541) ([033bf119](https://github.com/rust-lang/rustlings/commit/033bf1198fc8bfce1b570e49da7cde010aa552e3)) @@ -687,13 +687,13 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **functions2:** Change signature to trigger precise error message: (#605) ([0ef95947](https://github.com/rust-lang/rustlings/commit/0ef95947cc30482e63a7045be6cc2fb6f6dcb4cc)) - **structs1:** Adjust wording (#573) ([9334783d](https://github.com/rust-lang/rustlings/commit/9334783da31d821cc59174fbe8320df95828926c)) - **try_from_into:** - - type error ([4f4cfcf3](https://github.com/rust-lang/rustlings/commit/4f4cfcf3c36c8718c7c170c9c3a6935e6ef0618c)) - - Update description (#584) ([96347df9](https://github.com/rust-lang/rustlings/commit/96347df9df294f01153b29d9ad4ba361f665c755)) + - type error ([4f4cfcf3](https://github.com/rust-lang/rustlings/commit/4f4cfcf3c36c8718c7c170c9c3a6935e6ef0618c)) + - Update description (#584) ([96347df9](https://github.com/rust-lang/rustlings/commit/96347df9df294f01153b29d9ad4ba361f665c755)) - **vec1:** Have test compare every element in a and v ([9b6c6293](https://github.com/rust-lang/rustlings/commit/9b6c629397b24b944f484f5b2bbd8144266b5695)) ## 4.2.0 (2020-11-07) -#### Features +### Features - Add HashMap exercises ([633c00cf](https://github.com/rust-lang/rustlings/commit/633c00cf8071e1e82959a3010452a32f34f29fc9)) - Add Vec exercises ([0c12fa31](https://github.com/rust-lang/rustlings/commit/0c12fa31c57c03c6287458a0a8aca7afd057baf6)) @@ -707,13 +707,13 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **installation:** Update the MinRustVersion ([21bfb2d4](https://github.com/rust-lang/rustlings/commit/21bfb2d4777429c87d8d3b5fbf0ce66006dcd034)) - **iterators2:** Update description (#578) ([197d3a3d](https://github.com/rust-lang/rustlings/commit/197d3a3d8961b2465579218a6749b2b2cefa8ddd)) - **primitive_types6:** - - remove 'unused doc comment' warning ([472d8592](https://github.com/rust-lang/rustlings/commit/472d8592d65c8275332a20dfc269e7ac0d41bc88)) - - missing comma in test ([4fb230da](https://github.com/rust-lang/rustlings/commit/4fb230daf1251444fcf29e085cee222a91f8a37e)) + - remove 'unused doc comment' warning ([472d8592](https://github.com/rust-lang/rustlings/commit/472d8592d65c8275332a20dfc269e7ac0d41bc88)) + - missing comma in test ([4fb230da](https://github.com/rust-lang/rustlings/commit/4fb230daf1251444fcf29e085cee222a91f8a37e)) - **quiz3:** Second test is for odd numbers, not even. (#553) ([18e0bfef](https://github.com/rust-lang/rustlings/commit/18e0bfef1de53071e353ba1ec5837002ff7290e6)) ## 4.1.0 (2020-10-05) -#### Bug Fixes +### Bug Fixes - Update rustlings version in Cargo.lock ([1cc40bc9](https://github.com/rust-lang/rustlings/commit/1cc40bc9ce95c23d56f6d91fa1c4deb646231fef)) - **arc1:** index mod should equal thread count ([b4062ef6](https://github.com/rust-lang/rustlings/commit/b4062ef6993e80dac107c4093ea85166ad3ee0fa)) @@ -735,7 +735,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 4.0.0 (2020-07-08) -#### Breaking Changes +### Breaking Changes - Add a --nocapture Option to display test harnesses' outputs ([8ad5f9bf](https://github.com/rust-lang/rustlings/commit/8ad5f9bf531a4848b1104b7b389a20171624c82f)) - Rename test to quiz, fixes #244 ([010a0456](https://github.com/rust-lang/rustlings/commit/010a04569282149cea7f7a76fc4d7f4c9f0f08dd)) @@ -764,11 +764,11 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **from_into.rs:** typo ([a901499e](https://github.com/rust-lang/rustlings/commit/a901499ededd3ce1995164700514fe4e9a0373ea)) - **generics2:** Guide students to the answer (#430) ([e6bd8021](https://github.com/rust-lang/rustlings/commit/e6bd8021d9a7dd06feebc30c9d5f953901d7b419)) - **installation:** - - Provide a backup git reference when tag can't be curl ([9e4fb100](https://github.com/rust-lang/rustlings/commit/9e4fb1009f1c9e3433915c03e22c2af422e5c5fe)) - - Check if python is available while checking for git,rustc and Cargo ([9cfb617d](https://github.com/rust-lang/rustlings/commit/9cfb617d5b0451b4b51644a1298965390cda9884)) + - Provide a backup git reference when tag can't be curl ([9e4fb100](https://github.com/rust-lang/rustlings/commit/9e4fb1009f1c9e3433915c03e22c2af422e5c5fe)) + - Check if python is available while checking for git,rustc and Cargo ([9cfb617d](https://github.com/rust-lang/rustlings/commit/9cfb617d5b0451b4b51644a1298965390cda9884)) - **option1:** - - Don't add only zeros to the numbers array ([cce6a442](https://github.com/rust-lang/rustlings/commit/cce6a4427718724a9096800754cd3abeca6a1580)) - - Add cast to usize, as it is confusing in the context of an exercise about Option ([f6cffc7e](https://github.com/rust-lang/rustlings/commit/f6cffc7e487b42f15a6f958e49704c93a8d4465b)) + - Don't add only zeros to the numbers array ([cce6a442](https://github.com/rust-lang/rustlings/commit/cce6a4427718724a9096800754cd3abeca6a1580)) + - Add cast to usize, as it is confusing in the context of an exercise about Option ([f6cffc7e](https://github.com/rust-lang/rustlings/commit/f6cffc7e487b42f15a6f958e49704c93a8d4465b)) - **option2:** Add TODO to comments (#400) ([10967bce](https://github.com/rust-lang/rustlings/commit/10967bce57682812dc0891a9f9757da1a9d87404)) - **options1:** Add hint about Array Initialization (#389) ([9f75554f](https://github.com/rust-lang/rustlings/commit/9f75554f2a30295996f03f0160b98c0458305502)) - **test2:** name of type String and &str (#394) ([d6c0a688](https://github.com/rust-lang/rustlings/commit/d6c0a688e6a96f93ad60d540d4b326f342fc0d45)) @@ -776,7 +776,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 3.0.0 (2020-04-11) -#### Breaking Changes +### Breaking Changes - make "compile" exercises print output (#278) ([3b6d5c](https://github.com/fmoko/rustlings/commit/3b6d5c3aaa27a242a832799eb66e96897d26fde3)) @@ -785,8 +785,8 @@ Then follow the link to the guide about [community exercises](https://rustlings. - **primitive_types:** revert primitive_types4 (#296) ([b3a3351e](https://github.com/rust-lang/rustlings/commit/b3a3351e8e6a0bdee07077d7b0382953821649ae)) - **run:** compile clippy exercise files (#295) ([3ab084a4](https://github.com/rust-lang/rustlings/commit/3ab084a421c0f140ae83bf1fc3f47b39342e7373)) - **conversions:** - - add additional test to meet exercise rules (#284) ([bc22ec3](https://github.com/fmoko/rustlings/commit/bc22ec382f843347333ef1301fc1bad773657f38)) - - remove duplicate not done comment (#292) ([dab90f](https://github.com/fmoko/rustlings/commit/dab90f7b91a6000fe874e3d664f244048e5fa342)) + - add additional test to meet exercise rules (#284) ([bc22ec3](https://github.com/fmoko/rustlings/commit/bc22ec382f843347333ef1301fc1bad773657f38)) + - remove duplicate not done comment (#292) ([dab90f](https://github.com/fmoko/rustlings/commit/dab90f7b91a6000fe874e3d664f244048e5fa342)) - don't hardcode documentation version for traits (#288) ([30e6af](https://github.com/fmoko/rustlings/commit/30e6af60690c326fb5d3a9b7335f35c69c09137d)) #### Features @@ -808,20 +808,20 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 2.2.0 (2020-02-25) -#### Bug Fixes +### Bug Fixes - Update deps to version compatible with aarch64-pc-windows (#263) ([19a93428](https://github.com/rust-lang/rustlings/commit/19a93428b3c73d994292671f829bdc8e5b7b3401)) - **docs:** - - Added a necessary step to Windows installation process (#242) ([3906efcd](https://github.com/rust-lang/rustlings/commit/3906efcd52a004047b460ed548037093de3f523f)) - - Fixed mangled sentence from book; edited for clarity (#266) ([ade52ff](https://github.com/rust-lang/rustlings/commit/ade52ffb739987287ddd5705944c8777705faed9)) - - Updated iterators README to account for iterators4 exercise (#273) ([bec8e3a](https://github.com/rust-lang/rustlings/commit/bec8e3a644cbd88db1c73ea5f1d8a364f4a34016)) + - Added a necessary step to Windows installation process (#242) ([3906efcd](https://github.com/rust-lang/rustlings/commit/3906efcd52a004047b460ed548037093de3f523f)) + - Fixed mangled sentence from book; edited for clarity (#266) ([ade52ff](https://github.com/rust-lang/rustlings/commit/ade52ffb739987287ddd5705944c8777705faed9)) + - Updated iterators README to account for iterators4 exercise (#273) ([bec8e3a](https://github.com/rust-lang/rustlings/commit/bec8e3a644cbd88db1c73ea5f1d8a364f4a34016)) - **installation:** make fatal errors more obvious (#272) ([17d0951e](https://github.com/rust-lang/rustlings/commit/17d0951e66fda8e11b204d5c4c41a0d5e22e78f7)) - **iterators2:** - - Remove reference to missing iterators2.rs (#245) ([419f7797](https://github.com/rust-lang/rustlings/commit/419f7797f294e4ce6a2b883199731b5bde77d262)) + - Remove reference to missing iterators2.rs (#245) ([419f7797](https://github.com/rust-lang/rustlings/commit/419f7797f294e4ce6a2b883199731b5bde77d262)) - **as_ref_mut:** Enable a test and improve per clippy's suggestion (#256) ([dfdf809](https://github.com/rust-lang/rustlings/commit/dfdf8093ebbd4145864995627b812780de52f902)) - **tests1:** - - Change test command ([fe10e06c](https://github.com/rust-lang/rustlings/commit/fe10e06c3733ddb4a21e90d09bf79bfe618e97ce) - - Correct test command in tests1.rs comment (#263) ([39fa7ae](https://github.com/rust-lang/rustlings/commit/39fa7ae8b70ad468da49b06f11b2383135a63bcf)) + - Change test command ([fe10e06c](https://github.com/rust-lang/rustlings/commit/fe10e06c3733ddb4a21e90d09bf79bfe618e97ce) + - Correct test command in tests1.rs comment (#263) ([39fa7ae](https://github.com/rust-lang/rustlings/commit/39fa7ae8b70ad468da49b06f11b2383135a63bcf)) #### Features @@ -835,14 +835,14 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 2.1.0 (2019-11-27) -#### Bug Fixes +### Bug Fixes - add line numbers in several exercises and hints ([b565c4d3](https://github.com/rust-lang/rustlings/commit/b565c4d3e74e8e110bef201a082fa1302722a7c3)) - **arc1:** Fix some words in the comment ([c42c3b21](https://github.com/rust-lang/rustlings/commit/c42c3b2101df9164c8cd7bb344def921e5ba3e61)) - **enums:** Add link to chapter on pattern syntax (#242) ([615ce327](https://github.com/rust-lang/rustlings/commit/615ce3279800c56d89f19d218ccb7ef576624feb)) - **primitive_types4:** - - update outdated hint ([4c5189df](https://github.com/rust-lang/rustlings/commit/4c5189df2bdd9a231f6b2611919ba5aa14da0d3f)) - - update outdated comment ([ded2c034](https://github.com/rust-lang/rustlings/commit/ded2c034ba93fa1e3c2c2ea16b83abc1a57265e8)) + - update outdated hint ([4c5189df](https://github.com/rust-lang/rustlings/commit/4c5189df2bdd9a231f6b2611919ba5aa14da0d3f)) + - update outdated comment ([ded2c034](https://github.com/rust-lang/rustlings/commit/ded2c034ba93fa1e3c2c2ea16b83abc1a57265e8)) - **strings2:** update line number in hint ([a09f684f](https://github.com/rust-lang/rustlings/commit/a09f684f05c58d239a6fc59ec5f81c2533e8b820)) - **variables1:** Correct wrong word in comment ([fda5a470](https://github.com/rust-lang/rustlings/commit/fda5a47069e0954f16a04e8e50945e03becb71a5)) @@ -852,7 +852,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 2.0.0 (2019-11-12) -#### Bug Fixes +### Bug Fixes - **default:** Clarify the installation procedure ([c371b853](https://github.com/rust-lang/rustlings/commit/c371b853afa08947ddeebec0edd074b171eeaae0)) - **info:** Fix trailing newlines for hints ([795b6e34](https://github.com/rust-lang/rustlings/commit/795b6e348094a898e9227a14f6232f7bb94c8d31)) @@ -883,21 +883,21 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 1.5.0 (2019-11-09) -#### Bug Fixes +### Bug Fixes - **test1:** Rewrite logic ([79a56942](https://github.com/rust-lang/rustlings/commit/79a569422c8309cfc9e4aed25bf4ab3b3859996b)) - **installation:** Fix rustlings installation check ([7a252c47](https://github.com/rust-lang/rustlings/commit/7a252c475551486efb52f949b8af55803b700bc6)) - **iterators:** Rename iterator3.rs ([433d2115](https://github.com/rust-lang/rustlings/commit/433d2115bc1c04b6d34a335a18c9a8f3e2672bc6)) - **iterators2:** Remove syntax resulting in misleading error message ([4cde8664](https://github.com/rust-lang/rustlings/commit/4cde86643e12db162a66e62f23b78962986046ac)) - **option1:** - - Fix arguments passed to assert! macro (#222) ([4c2cf6da](https://github.com/rust-lang/rustlings/commit/4c2cf6da755efe02725e05ecc3a303304c10a6da)) - - Fix arguments passed to assert! macro ([ead4f7af](https://github.com/rust-lang/rustlings/commit/ead4f7af9e10e53418efdde5c359159347282afd)) - - Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6)) + - Fix arguments passed to assert! macro (#222) ([4c2cf6da](https://github.com/rust-lang/rustlings/commit/4c2cf6da755efe02725e05ecc3a303304c10a6da)) + - Fix arguments passed to assert! macro ([ead4f7af](https://github.com/rust-lang/rustlings/commit/ead4f7af9e10e53418efdde5c359159347282afd)) + - Add test for prematurely passing exercise ([a750e4a1](https://github.com/rust-lang/rustlings/commit/a750e4a1a3006227292bb17d57d78ce84da6bfc6)) - **primitive_types4:** Fail on a slice covering the wrong area ([5b1e673c](https://github.com/rust-lang/rustlings/commit/5b1e673cec1658afc4ebbbc800213847804facf5)) - **README:** HTTP to https ([70946b85](https://github.com/rust-lang/rustlings/commit/70946b85e536e80e70ed9505cb650ca0a3a1fbb5)) - **test1:** - - Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446)) - - renamed function name to snake case closes #180 ([89d5186c](https://github.com/rust-lang/rustlings/commit/89d5186c0dae8135ecabf90ee8bb35949bc2d29b)) + - Swap assertion parameter order ([4086d463](https://github.com/rust-lang/rustlings/commit/4086d463a981e81d97781851d17db2ced290f446)) + - renamed function name to snake case closes #180 ([89d5186c](https://github.com/rust-lang/rustlings/commit/89d5186c0dae8135ecabf90ee8bb35949bc2d29b)) #### Features @@ -915,7 +915,7 @@ Then follow the link to the guide about [community exercises](https://rustlings. ## 1.4.0 (2019-07-13) -#### Bug Fixes +### Bug Fixes - **installation:** Fix rustlings installation check ([7a252c47](https://github.com/rust-lang/rustlings/commit/7a252c475551486efb52f949b8af55803b700bc6)) - **iterators:** Rename iterator3.rs ([433d2115](https://github.com/rust-lang/rustlings/commit/433d2115bc1c04b6d34a335a18c9a8f3e2672bc6)) diff --git a/crates/mcb-validate/tests/fixtures/rustlings/CONTRIBUTING.md b/crates/mcb-validate/tests/fixtures/rustlings/CONTRIBUTING.md index 10892d314..428666193 100644 --- a/crates/mcb-validate/tests/fixtures/rustlings/CONTRIBUTING.md +++ b/crates/mcb-validate/tests/fixtures/rustlings/CONTRIBUTING.md @@ -14,7 +14,7 @@ I want to … ## Issues -You can open an issue [here](https://github.com/rust-lang/rustlings/issues/new). +You can open an issue [the rustlings contributing guide](https://github.com/rust-lang/rustlings/issues/new). If you're reporting a bug, please include the output of the following commands: - `cargo --version` diff --git a/crates/mcb-validate/tests/fixtures/rustlings/website/content/setup/index.md b/crates/mcb-validate/tests/fixtures/rustlings/website/content/setup/index.md index be574b1c0..7a03fc571 100644 --- a/crates/mcb-validate/tests/fixtures/rustlings/website/content/setup/index.md +++ b/crates/mcb-validate/tests/fixtures/rustlings/website/content/setup/index.md @@ -14,7 +14,7 @@ This will also install *Cargo*, Rust's package/project manager. > > Debian: `sudo apt install gcc`\ > Fedora: `sudo dnf install gcc` - +> > 🍎 If you are on **MacOS**, make sure you have *Xcode and its developer tools* installed: `xcode-select --install` ## Installing Rustlings diff --git a/custom.mk b/custom.mk index a0aad250d..20fe016f1 100644 --- a/custom.mk +++ b/custom.mk @@ -1,10 +1,19 @@ # MCB private handlers for the FLEXT-generated Make surface. # Public verbs and environment ownership remain in the generated Makefile. +# +# SCOPE RULE: a handler here exists only for what the generated Makefile does +# NOT already own. The dispatcher REPLACES the builtin when a `_custom__ +# ` target exists, so defining one for a verb the owner already +# implements silently disables the official gate. Everything Python — ruff +# format, ruff check, mypy/pyrefly/pyright, pytest — is owned by +# `_builtin_fmt_*`, `_builtin_fix_*`, `_builtin_check_all` and +# `_builtin_test_*`, and is therefore NEVER restated here. What remains is the +# Rust toolchain, which the FLEXT surface does not cover. post-setup: - @hooks_dir=$$(git rev-parse --git-path hooks); \ - cp scripts/hooks/pre-commit scripts/hooks/pre-push "$$hooks_dir/"; \ - chmod +x "$$hooks_dir/pre-commit" "$$hooks_dir/pre-push" + @# Hook installation is NOT done here. codegen delegates it to + @# `pre-commit install` so exactly one shim exists; copying a second script + @# over .git/hooks made hook behaviour depend on whichever ran last. @# Why (mcb-o96i.19): CI runners need sccache installed before any cargo @# invocation because .cargo/config.toml sets rustc-wrapper = "sccache". @# The setup-ci.sh script installs sccache when not present. On local @@ -12,7 +21,6 @@ post-setup: @if [ "$$CI" = "Y" ] && [ -f .github/setup-ci.sh ]; then \ bash .github/setup-ci.sh; \ fi - @printf '%s\n' 'MCB hooks installed' _custom_build_artifacts: @if [ "$(RELEASE)" = "1" ]; then \ @@ -21,13 +29,8 @@ _custom_build_artifacts: bash scripts/lib/mcb.sh run cargo build; \ fi -_custom_test_all: - @if cargo nextest --version >/dev/null 2>&1; then \ - MCB_MODEL_ID=test-model bash scripts/lib/mcb.sh run cargo nextest run --workspace; \ - else \ - MCB_MODEL_ID=test-model bash scripts/lib/mcb.sh run cargo test --workspace --all-targets; \ - fi - +# Rust test selectors. `all`/`full` stay with the builtin so the FLEXT pytest +# entry keeps owning the Python suite; these are additional WHATs only. _custom_test_unit: @if cargo nextest --version >/dev/null 2>&1; then \ MCB_MODEL_ID=test-model bash scripts/lib/mcb.sh run cargo nextest run --workspace --test unit; \ @@ -44,35 +47,19 @@ _custom_test_doc: _custom_test_golden: @MCB_MODEL_ID=test-model bash scripts/lib/mcb.sh run cargo test -p mcb-server --test e2e "$(if $(strip $(MATCH)),$(MATCH),golden)" -_custom_fmt_check: - @bash scripts/lib/mcb.sh run cargo fmt --all -- --check - @UV_CACHE_DIR=.cache/uv uv run --no-sync ruff format --check scripts - -_custom_fmt_all: - $(call _require_apply) - @bash scripts/lib/mcb.sh run cargo fmt --all - @UV_CACHE_DIR=.cache/uv uv run --no-sync ruff format scripts - -_custom_fmt_apply: _custom_fmt_all - -_custom_fix_check: - @UV_CACHE_DIR=.cache/uv uv run --no-sync ruff check scripts - -_custom_fix_all: - $(call _require_apply) - @UV_CACHE_DIR=.cache/uv uv run --no-sync ruff check --fix scripts - -_custom_fix_apply: _custom_fix_all +_custom_test_rust: + @if cargo nextest --version >/dev/null 2>&1; then \ + MCB_MODEL_ID=test-model bash scripts/lib/mcb.sh run cargo nextest run --workspace; \ + else \ + MCB_MODEL_ID=test-model bash scripts/lib/mcb.sh run cargo test --workspace --all-targets; \ + fi +# Rust-only check selectors. `all` stays with the builtin so `make check` +# keeps running the owner's declared CHECK_GATES under its own CI ternary. _custom_check_lint: @bash scripts/lib/mcb.sh run cargo fmt --all -- --check @bash scripts/lib/mcb.sh run cargo clippy --all-targets -- -D warnings -_custom_check_python: - @UV_CACHE_DIR=.cache/uv uv run --no-sync ruff check scripts - @UV_CACHE_DIR=.cache/uv MYPYPATH=scripts uv run --no-sync mypy scripts/lib - @UV_CACHE_DIR=.cache/uv uv run --no-sync pytest -m 'not slow' scripts/lib/tests - _custom_check_validate: @bash scripts/lib/mcb.sh validate $(if $(filter 1,$(QUICK)),quick,full) @@ -80,55 +67,12 @@ _custom_check_guard: @bash scripts/lib/mcb.sh guard _custom_check_gitops: - @UV_CACHE_DIR=.cache/uv uv run --no-sync python scripts/check/gitops.py + @$(UV_RUN) python scripts/check/gitops.py -_custom_check_surface: - @UV_CACHE_DIR=.cache/uv uv run --no-sync python scripts/check/surface.py _custom_check_audit: @bash scripts/lib/mcb.sh run cargo audit -_custom_check_all: - @UV_CACHE_DIR=.cache/uv uv run --no-sync ruff check scripts - @UV_CACHE_DIR=.cache/uv MYPYPATH=scripts uv run --no-sync mypy scripts/lib - @UV_CACHE_DIR=.cache/uv uv run --no-sync pytest -m 'not slow' scripts/lib/tests - @bash scripts/lib/mcb.sh run cargo fmt --all -- --check - @bash scripts/lib/mcb.sh run cargo clippy --all-targets -- -D warnings - @MCB_MODEL_ID=test-model bash scripts/lib/mcb.sh run cargo test --workspace --all-targets - @bash scripts/lib/mcb.sh validate quick - @bash scripts/lib/mcb.sh guard - -_custom_run_mcb-hooks: - $(call _require_apply) - @$(MAKE) --no-print-directory post-setup - -_custom_run_mcb-hook-pre-commit: - @bash scripts/lib/mcb.sh guard --staged - @UV_CACHE_DIR=.cache/uv uv run --no-sync ruff check scripts - @UV_CACHE_DIR=.cache/uv uv run --no-sync pytest -m 'not slow' scripts/lib/tests - @bash scripts/lib/mcb.sh run cargo fmt --all -- --check - @bash scripts/lib/mcb.sh run cargo clippy --workspace -- -D warnings - -_custom_run_mcb-hook-pre-push: - @UV_CACHE_DIR=.cache/uv uv run --no-sync ruff check scripts - @UV_CACHE_DIR=.cache/uv MYPYPATH=scripts uv run --no-sync mypy scripts/lib - @UV_CACHE_DIR=.cache/uv uv run --no-sync pytest -m 'not slow' scripts/lib/tests - @bash scripts/lib/mcb.sh run cargo fmt --all -- --check - @bash scripts/lib/mcb.sh run cargo clippy --all-targets -- -D warnings - @MCB_MODEL_ID=test-model bash scripts/lib/mcb.sh run cargo test --workspace --all-targets - @bash scripts/lib/mcb.sh run cargo test --workspace --doc - @bash scripts/lib/mcb.sh validate quick - @bash scripts/lib/mcb.sh guard - _custom_gen_agent-pointers: @if [ "$(CHECK)" != "1" ] && [ "$(APPLY)" != "Y" ]; then printf 'ERROR: this action requires APPLY=Y\n' >&2; exit 2; fi - @UV_CACHE_DIR=.cache/uv uv run --no-sync python scripts/lib/agent_pointers.py $(if $(filter 1,$(CHECK)),--check,) - -_custom_status_done-check: - @base=$$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || printf '%s\n' origin/main); \ - files=$$(git diff --name-only --diff-filter=d "$$base"...HEAD -- '*.py'); \ - if [ -z "$$files" ]; then \ - printf '%s\n' 'done-check: no committed Python changes'; \ - else \ - printf '%s\n' "$$files" | xargs -r ruff check --quiet; \ - fi + @$(UV_RUN) python scripts/lib/agent_pointers.py $(if $(filter 1,$(CHECK)),--check,) diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 476b40221..7c8658e61 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -16,4 +16,5 @@ The canonical source of the public API is the re-exports in the root crate: - [`crates/mcb/src/lib.rs`](../crates/mcb/src/lib.rs) --- + *Updated 2026-02-20 - Consolidated duplicate documentation.* diff --git a/docs/adr/006-code-audit-and-improvements.md b/docs/adr/006-code-audit-and-improvements.md index 39e73fb15..02fd102e2 100644 --- a/docs/adr/006-code-audit-and-improvements.md +++ b/docs/adr/006-code-audit-and-improvements.md @@ -307,6 +307,7 @@ config = "0.13" | Cyclomatic complexity | >15 | <10 | Cargo +nightly rustc -- -Zunpretty=hir | | Memory usage | Baseline | <10% increase | Valgrind massif | | Error handling coverage | Partial | Complete | Manual review | +   diff --git a/docs/adr/007-integrated-web-administration-interface.md b/docs/adr/007-integrated-web-administration-interface.md new file mode 100644 index 000000000..b75193ef1 --- /dev/null +++ b/docs/adr/007-integrated-web-administration-interface.md @@ -0,0 +1,438 @@ + +--- +adr: 7 +title: Integrated Web Administration Interface +status: IMPLEMENTED +created: +updated: 2026-02-05 +related: [1, 2, 6, 8, 12, 13] +supersedes: [] +superseded_by: [] +implementation_status: Complete +--- + + + +## ADR 007: Integrated Web Administration Interface + +## Status + +**Implemented** (v0.2.0) + +> Backend infrastructure implemented in `crates/mcb-server/src/admin/`: +> +> - AdminService trait with 32 methods (traits.rs) +> - AdminServiceImpl with full implementation (implementation.rs, helpers/) +> - REST API routes for config, health, backup, maintenance (routes.rs, handlers.rs) +> - JWT authentication integration +> - **New (v0.2.0)**: +> - Unified port architecture (MCP + Admin + Metrics on port 3000) +> - Subsystem control via EventBus (6 new AdminService methods) +> - Configuration persistence with explicit save pattern +> - 14 SystemEvent variants for inter-subsystem communication +> - **Pending**: Frontend HTML/CSS/JS refinement, WebSocket real-time updates + +## Context + +Memory Context Browser provides comprehensive system monitoring and metrics through HTTP endpoints on a unified port, but lacks a user-friendly web interface for administration, configuration, and visualization. Users currently need to interact with the system through: + +1. Environment variables for configuration +2. MCP protocol tools for basic operations +3. HTTP API endpoints for metrics (no authentication) +4. Terminal/command-line for management tasks + +This creates barriers for non-technical users and makes it difficult to: + +- Monitor system health in real-time +- Configure providers dynamically +- Manage indexes and search operations +- Troubleshoot issues without technical expertise +- Visualize performance metrics and trends + +The existing infrastructure already includes: + +- HTTP metrics server on unified port 3000 +- Comprehensive metrics collection +- Provider management capabilities +- Configuration system + +## Decision + +We will implement an integrated web administration interface that runs on the same port as the metrics server (3000), providing a modern, responsive web UI for: + +1. **System Dashboard**: Real-time metrics visualization with interactive charts +2. **Configuration Management**: Dynamic provider and system configuration +3. **Provider Management**: Add/remove/configure embedding and vector store providers +4. **Index Management**: Control indexing operations, view status, clear indexes +5. **Security**: Basic authentication for administrative access +6. **Monitoring**: Enhanced health monitoring and alerting + +The interface will be implemented using: + +- **Backend**: Extend existing Rocket HTTP server with new REST endpoints +- **Frontend**: Modern HTML/CSS/JavaScript with responsive design +- **Authentication**: JWT-based authentication for admin access +- **Real-time**: WebSocket support for live metrics updates +- **API**: RESTful JSON API for all administrative operations + +### Consequences + +### Positive Consequences + +- **Improved User Experience**: Non-technical users can manage the system through a web interface +- **Real-time Monitoring**: Live dashboards with interactive charts and alerts +- **Operational Efficiency**: Faster troubleshooting and configuration changes +- **Security Enhancement**: Authentication and authorization for administrative access +- **Unified Interface**: Single port (3000) serves both metrics API and admin interface +- **Extensibility**: Foundation for future web-based features +- **Developer Productivity**: Easier testing and development workflows + +### Negative Consequences + +- **Increased Complexity**: Additional code and maintenance overhead +- **Security Surface**: Web interface introduces new attack vectors +- **Resource Usage**: Additional memory/CPU for serving static assets and WebSocket connections +- **Deployment Complexity**: Web assets need to be bundled and served +- **Browser Dependencies**: Interface requires modern browsers with JavaScript enabled + +## Alternatives Considered + +### Alternative 1: Separate Administration Service + +- **Description**: Create a standalone web service on a different port for administration +- **Pros**: Clean separation, independent scaling, dedicated resources +- **Cons**: Additional port management, deployment complexity, CORS issues +- **Rejection Reason**: Increases operational complexity and goes against the requirement to run on the same port + +### Alternative 2: Terminal-based Administration Only + +- **Description**: Enhance CLI tools and keep all administration terminal-based +- **Pros**: Lower resource usage, simpler architecture, no web dependencies +- **Cons**: Poor user experience, limited visualization, accessibility issues +- **Rejection Reason**: Doesn't address the need for web-based administration and visualization + +### Alternative 3: Third-party Admin Interface + +- **Description**: Use existing tools like Grafana or custom dashboards +- **Pros**: Leverage existing ecosystems, faster implementation +- **Cons**: External dependencies, integration complexity, customization limitations +- **Rejection Reason**: Doesn't provide integrated experience and requires additional setup + +### Alternative 4: Desktop Application + +- **Description**: Native desktop app for administration +- **Pros**: Better performance, native integrations +- **Cons**: Platform-specific development, deployment challenges, additional maintenance +- **Rejection Reason**: Web-based requirement and cross-platform needs + +## Implementation Notes + +### Architecture Integration + +The web interface will extend the existing `MetricsApiServer` in `crates/mcb-infrastructure/src/metrics/http_server.rs`: + +```rust +pub struct AdminApiServer { + metrics_server: MetricsApiServer, + admin_routes: Router, + websocket_handler: WebSocketHandler, +} +``` + +### API Endpoints + +New REST endpoints under `/admin/` prefix: + +- `GET /admin/` - Serve main admin interface +- `GET /admin/config` - Get current configuration +- `PUT /admin/config` - Update configuration +- `GET /admin/providers` - List providers +- `POST /admin/providers` - Add provider +- `DELETE /admin/providers/{id}` - Remove provider +- `GET /admin/indexes` - List indexes +- `POST /admin/indexes/{id}/clear` - Clear index +- `POST /admin/auth/login` - Authentication + +### Frontend Structure + +Templates are**embedded at compile time** using `include_str!` macro, making the binary self-contained: + +```text +crates/mcb-server/src/admin/web/templates/ +├── base.html # Master layout (Tailwind + Alpine.js + HTMX) +├── dashboard.html # Main dashboard +├── providers.html # Provider management +├── indexes.html # Index management +├── configuration.html # Config editor +├── maintenance.html # Maintenance operations +├── diagnostics.html # Diagnostic tools +├── data_management.html # Backup UI +├── logs.html # Log viewer +├── login.html # Authentication +├── admin.css # Custom styles +└── htmx/ # HTMX partials for dynamic updates + ├── dashboard_metrics.html + ├── providers_list.html + ├── indexes_list.html + ├── subsystems_list.html + └── config_diff.html +``` + +**Key implementation detail**: All templates are loaded via `include_str!` in `crates/mcb-server/src/admin/web.rs` and added to Tera at compile time, eliminating runtime filesystem access. + +### Security Implementation + +- JWT authentication with configurable expiration +- Role-based access (admin vs read-only) +- CSRF protection for state-changing operations +- HTTPS enforcement in production +- Rate limiting for API endpoints + +### Testing Strategy + +- Unit tests for new API endpoints +- Integration tests for web interface functionality +- E2E tests for critical admin workflows +- Security testing for authentication and authorization + +### Migration Path + +1. **Phase 1**: Extend HTTP server with admin routes +2. **Phase 2**: Implement authentication middleware +3. **Phase 3**: Create basic HTML/CSS/JS interface +4. **Phase 4**: Add WebSocket support for real-time updates +5. **Phase 5**: Implement advanced features (charts, provider management) + +### Performance Considerations + +- Static asset compression and caching +- Lazy loading for JavaScript modules +- Efficient WebSocket connection management +- Minimal impact on existing metrics endpoints + +### Rollback Plan + +- Feature flag to disable admin interface +- Separate admin routes can be easily removed +- Database migrations (if any) are reversible +- Static assets can be excluded from builds + +## Unified Port Architecture (v0.2.0) + +All HTTP services run on a single unified port (default: 3000). + +### Port Configuration + +Configure via environment variable: + +```bash +export MCP__SERVER__NETWORK__PORT=3000 # Default unified port for Admin + Metrics + MCP +``` + +### URL Structure + +```text +Port 3000 (Unified: Admin + Metrics + MCP HTTP) +├── / - Admin dashboard (root redirects to dashboard) +├── /dashboard - Admin dashboard +├── /providers - Provider management UI +├── /indexes - Index management UI +├── /config - Configuration UI +├── /logs - Log viewer UI +├── /maintenance - Maintenance UI +├── /diagnostics - Diagnostic tools UI +├── /data - Data management UI +├── /login - Authentication page +├── /admin.css - Admin stylesheet +├── /htmx*/ - HTMX partial endpoints +├── /admin*/ - Admin REST API endpoints +├── /api*/ - Metrics API endpoints +└── /mcp*/ - MCP protocol HTTP transport +``` + +**Note**: The root path `/` serves the admin dashboard directly, making it the default landing page. + +### Implementation + +The `MetricsApiServer` now accepts multiple routers via builder pattern: + +```rust +// crates/mcb-infrastructure/src/metrics/http_server.rs +impl MetricsApiServer { + pub fn with_external_router(mut self, router: Router) -> Self; + pub fn with_mcp_router(mut self, router: Router) -> Self; +} +``` + +Initialization in `crates/mcb-server/src/init.rs`: + +```rust +let metrics_server = MetricsApiServer::with_limits(...) + .with_external_router(admin_router) // Admin routes + .with_mcp_router(mcp_router); // MCP protocol +``` + +### Benefits + +- Single port simplifies firewall/proxy configuration +- Eliminates port 3002 (previously MCP HTTP transport) +- Unified graceful shutdown via ConnectionTracker +- Consistent rate limiting and CORS across all endpoints + +## Subsystem Control Protocol (v0.2.0) + +Web admin can monitor and control all MCP subsystems via EventBus. + +### New SystemEvent Variants + +Added to `crates/mcb-infrastructure/src/events/mod.rs`: + +```rust +pub enum SystemEvent { + // Existing (10 variants): CacheClear, BackupCreate, BackupRestore, + // IndexRebuild, ConfigReloaded, Shutdown, Reload, Respawn, + // BinaryUpdated, SyncCompleted + + // New (4 variants for subsystem control): + ProviderRestart { provider_type: String, provider_id: String }, + ProviderReconfigure { provider_type: String, config: serde_json::Value }, + SubsystemHealthCheck { subsystem_id: String }, + RouterReload, +} +``` + +### New AdminService Methods + +Added to `crates/mcb-server/src/admin/service/traits.rs`: + +```rust +#[async_trait] +pub trait AdminService: Interface + Send + Sync { + // ... existing 26 methods ... + + // Subsystem introspection (6 new methods) + async fn get_subsystems(&self) -> Result, AdminError>; + async fn send_subsystem_signal(&self, id: &str, signal: SubsystemSignal) -> Result; + async fn get_routes(&self) -> Result, AdminError>; + async fn reload_routes(&self) -> Result; + async fn persist_configuration(&self) -> Result; + async fn get_config_diff(&self) -> Result; +} +``` + +### Subsystem Types + +Defined in `crates/mcb-server/src/admin/service/types.rs`: + +```rust +pub enum SubsystemType { + Embedding, // Embedding providers + VectorStore, // Vector database providers + Search, // Search service + Indexing, // Indexing service + Cache, // Cache manager + Metrics, // Metrics collector + Daemon, // Background daemon + HttpTransport, // HTTP server +} + +pub enum SubsystemStatus { + Running, Stopped, Error, Starting, Paused, Unknown +} + +pub struct SubsystemInfo { + pub id: String, + pub name: String, + pub subsystem_type: SubsystemType, + pub status: SubsystemStatus, + pub health: HealthCheck, + pub config: serde_json::Value, + pub metrics: SubsystemMetrics, +} +``` + +### Event Flow Example + +```text + +1. User clicks "Restart" on embedding subsystem +2. POST /admin/subsystems/embedding:ollama/signal {"signal":"restart"} +3. AdminService::send_subsystem_signal() called +4. EventBus publishes SystemEvent::ProviderRestart +5. Embedding provider's event listener receives and restarts +6. Dashboard HTMX poll shows updated status + +``` + +## Configuration Management (v0.2.0) + +Implements explicit save pattern for configuration changes. + +### Runtime vs Persisted Configuration + +- **Runtime**: Changes via `update_configuration()` apply immediately (ArcSwap) +- **Persisted**: `persist_configuration()` writes to `~/.context/config.toml` +- **Diff**: `get_config_diff()` shows runtime vs file differences + +### API Flow + +```text + +1. GET /admin/configuration # View current config +2. PUT /admin/configuration # Update runtime only +3. POST /admin/configuration/save # Persist to file +4. GET /admin/configuration/diff # Compare runtime vs file + +``` + +### Implementation Pattern + +```rust +// Runtime update (immediate, not persisted) +pub async fn update_configuration(&self, updates: HashMap, user: &str) + -> Result; + +// Explicit persist to file +pub async fn persist_configuration(&self) -> Result; + +// Check for unsaved changes +pub async fn get_config_diff(&self) -> Result; +``` + +## Template Organization + +Templates located in `crates/mcb-server/src/admin/web/templates/`: + +```text +templates/ +├── base.html # Master layout (Alpine.js + Tailwind + HTMX) +├── dashboard.html # Real-time metrics dashboard +├── providers.html # Provider management +├── indexes.html # Index management +├── configuration.html # Config editor +├── maintenance.html # Maintenance operations +├── diagnostics.html # Diagnostic tools +├── data_management.html # Backup UI +├── logs.html # Log viewer +├── login.html # Authentication +├── admin.css # Custom styles +└── htmx/ # HTMX partials for dynamic updates + ├── dashboard_metrics.html + ├── providers_list.html + └── indexes_list.html +``` + +## Related ADRs + +- [ADR-001: Modular Crates Architecture](001-modular-crates-architecture.md) - Provider pattern for admin services +- [ADR-002: Async-First Architecture](002-async-first-architecture.md) - Async handlers +- [ADR-006: Code Audit and Improvements](006-code-audit-and-improvements.md) - Code quality standards +- [ADR-008: Git-Aware Semantic Indexing](008-git-aware-semantic-indexing-v0.2.0.md) - Git integration for admin UI +- [ADR-012: Two-Layer DI Strategy](012-di-strategy-two-layer-approach.md) - DI for admin services +- [ADR-013: Clean Architecture Crate Separation](013-clean-architecture-crate-separation.md) - Crate organization + +## References + +- [Admin Module] (removed by ADR-050) - Admin routes, handlers, and web UI +- [HTTP Transport] (removed by ADR-050) - HTTP server implementation +- [dill Documentation](https://docs.rs/dill) - Current DI framework (ADR-029) diff --git a/docs/adr/009-persistent-session-memory-v0.2.0.md b/docs/adr/009-persistent-session-memory-v0.2.0.md index a4196173d..66a2ef788 100644 --- a/docs/adr/009-persistent-session-memory-v0.2.0.md +++ b/docs/adr/009-persistent-session-memory-v0.2.0.md @@ -85,6 +85,7 @@ Implement persistent session memory in mcb v0.2.0 by porting Claude-mem's core a All memory-related MCP tools use the `memory_` prefix to avoid namespace collisions: + | ADR Original Name | Canonical Name (v1.0.0) | Rationale | | ------------------- | ------------------------- | ----------- | | `search` | `memory (action=list, resource=observation)` | Avoids collision with `search (resource=code)` | @@ -1400,8 +1401,9 @@ Memory search uses hybrid retrieval (BM25 + vector) with Reciprocal Rank Fusion ### Layer Separation (Clean Architecture) + | Layer | Responsibility | Implementation | -| ------- | | ---------------- | ---------------- | +| ------- | | ---------------- | | Domain | `MemoryRepository` port with `search(query_embedding, filter, limit)` | No engine-specific logic | | Application | `MemorySearchService` orchestrates FTS + vector retrieval, performs RRF fusion | Pure business logic | | Infrastructure | `SqliteMemoryRepository` implements FTS5 queries + calls VectorStoreProvider | Database-specific | diff --git a/docs/adr/027-architecture-evolution-v013.md b/docs/adr/027-architecture-evolution-v013.md index 9a73c7237..8df73c642 100644 --- a/docs/adr/027-architecture-evolution-v013.md +++ b/docs/adr/027-architecture-evolution-v013.md @@ -45,6 +45,7 @@ Analysis of the [kamu-cli](https://github.com/kamu-data/kamu-cli) production codebase revealed opportunities to evolve MCB without rewriting: + | Aspect | MCB Current | kamu-cli Pattern | Opportunity | | -------- | ------------- | ------------------ | ------------- | | Module Organization | By layer (entities/, ports/, services/) | By bounded context (workspace/, indexing/, search/) | Feature-centric navigation | diff --git a/docs/adr/029-hexagonal-architecture-dill.md b/docs/adr/029-hexagonal-architecture-dill.md new file mode 100644 index 000000000..609311002 --- /dev/null +++ b/docs/adr/029-hexagonal-architecture-dill.md @@ -0,0 +1,194 @@ + +--- +adr: 29 +title: Hexagonal Architecture with dill IoC +status: IMPLEMENTED +created: +updated: 2026-02-05 +related: [] +supersedes: [] +superseded_by: [] +implementation_status: Complete +--- + + + +# ADR 029: Hexagonal Architecture with dill IoC + +## Status + +**Implemented** (v0.1.2) + +> Evolution of [ADR 024: Simplified Dependency Injection] +> (024-simplified-dependency-injection.md), adding dill Catalog as IoC +> container while maintaining the handle-based pattern. + +## Context + +The previous architecture (ADR-024) used a handle-based DI pattern with linkme +registry for compile-time provider discovery. While effective, this approach had +coupling issues: + +1. **Infrastructure imported concrete types from Application** + - `domain_services.rs` imported `ContextServiceImpl`, `SearchServiceImpl` + +2. **Application ports were duplicated** + - `mcb-domain/src/ports/providers/` (correct location) + - `mcb-application/src/ports/providers/` (duplication) + +3. **No IoC container for service lifecycle management** + - Manual wiring in bootstrap.rs + - No dependency graph validation + +## Decision + +We implement a proper hexagonal architecture with dill IoC container: + +### 1. Ports in mcb-domain (Single Source of Truth) + +All provider ports are defined in `mcb-domain/src/ports/providers/`: + +```rust +// mcb-domain/src/ports/providers/embedding.rs +pub trait EmbeddingProvider: Send + Sync { + fn embed(&self, text: &str) -> Result; +} + +// mcb-domain/src/ports/providers/vector_store.rs +pub trait VectorStoreProvider: Send + Sync { + fn store(&self, embedding: &Embedding) -> Result<()>; + fn search(&self, query: &Embedding) -> Result>; +} +``` + +Application layer does not own provider ports. Import provider traits directly from +`mcb-domain/src/ports/providers/` to avoid duplicate declarations and compatibility shims. + +### 2. dill Catalog as IoC Container + +The dill `Catalog` manages service registration and resolution: + +```rust +// mcb-infrastructure/src/di/catalog.rs +pub async fn build_catalog(config: AppConfig) -> Result { + CatalogBuilder::new() + // Configuration + .add_value(config) + // Providers (from linkme registry) + .add_value(embedding_provider) + .add_value(vector_store_provider) + // Handles (for runtime switching) + .add_value(embedding_handle) + .add_value(vector_store_handle) + // Admin services + .add_value(embedding_admin) + .add_value(vector_store_admin) + .build() +} + +// Service retrieval via AppContext (bootstrap.rs) +// AppContext holds all resolved providers as typed fields: +// app_context.embedding_handle() → Arc +// app_context.vector_store_handle() → Arc +// app_context.cache_handle() → Arc +``` + +### 3. Architecture Layers + +```text +┌──────────────────────────────────────────────────────────────┐ +│ mcb-domain │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ PORTS (trait definitions) │ │ +│ │ - EmbeddingProvider │ │ +│ │ - VectorStoreProvider │ │ +│ │ - CacheProvider │ │ +│ │ - LanguageChunkingProvider │ │ +│ └────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ + ↑ +┌──────────────────────────────────────────────────────────────┐ +│ mcb-application │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ USE CASES (import ports from mcb-domain) │ │ +│ │ - ContextServiceImpl │ │ +│ │ - SearchServiceImpl │ │ +│ │ - IndexingServiceImpl │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ REGISTRY (linkme distributed slices) │ │ +│ │ - EMBEDDING_PROVIDERS │ │ +│ │ - VECTOR_STORE_PROVIDERS │ │ +│ └────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ + ↑ +┌──────────────────────────────────────────────────────────────┐ +│ mcb-infrastructure │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ COMPOSITION ROOT (dill Catalog) │ │ +│ │ - build_catalog() creates IoC container │ │ +│ │ - Provider Handles for runtime switching │ │ +│ │ - Admin Services for API-based management │ │ +│ └────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ + ↑ +┌──────────────────────────────────────────────────────────────┐ +│ mcb-providers │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ ADAPTERS (implement ports from mcb-domain) │ │ +│ │ - OllamaEmbeddingProvider │ │ +│ │ - MilvusVectorStore │ │ +│ │ - MokaCacheProvider │ │ +│ │ - Register via linkme distributed slices │ │ +│ └────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ +``` + +### 4. Validation Rules + +New mcb-validate rules enforce the architecture: + +| Rule ID | Description | +| --------- | ------------- | +| CA007 | Infrastructure cannot import concrete types from Application | +| CA008 | Application must import ports from mcb-domain | + +## Consequences + +### Positive + +1. **Clear layer separation**: Ports in domain, implementations in providers +2. **IoC container benefits**: dill Catalog manages service lifecycle +3. **Gradual migration**: `add_value()` allows mixing with existing pattern +4. **Compile-time validation**: mcb-validate enforces architecture +5. **Runtime switching**: Provider handles still support admin API + +### Negative + +1. **Additional dependency**: dill crate added to workspace +2. **Learning curve**: Developers must understand dill API +3. **Migration effort**: Existing code updated to new import paths + +### Neutral + +1. **Bootstrap still exists**: `init_app()` wraps `build_catalog()` +2. **AppContext unchanged**: Same public interface for consumers + +## Canonical References + +> **Note**: This ADR is a historical decision record. For current architecture +> details, consult the normative documents below. The code paths in this ADR +> reflect the state at the time of writing; the current single source of truth +> for port trait locations is `mcb-domain/src/ports/providers/` (not +> `mcb-application/src/ports/providers/`, which was removed as duplicated). + +- [ARCHITECTURE_BOUNDARIES.md](../architecture/ARCHITECTURE_BOUNDARIES.md) — Layer rules and module ownership (normative) +- [PATTERNS.md](../architecture/PATTERNS.md) — Technical patterns reference (normative) +- [ARCHITECTURE.md](../architecture/ARCHITECTURE.md) — Full system architecture (normative) + +## References + +- [dill-rs Documentation](https://docs.rs/dill/latest/dill/) +- [ADR 023: Inventory to linkme Migration](023-inventory-to-linkme-migration.md) +- [ADR 024: Simplified Dependency Injection](024-simplified-dependency-injection.md) +- [Clean Architecture]() diff --git a/docs/adr/032-agent-quality-domain-extension.md b/docs/adr/032-agent-quality-domain-extension.md index a98b9dc66..0e4bcc04d 100644 --- a/docs/adr/032-agent-quality-domain-extension.md +++ b/docs/adr/032-agent-quality-domain-extension.md @@ -134,16 +134,15 @@ project_log_decision → Log decision ## Implementation Plan -| Phase | Goal | LOC | Tools | -| ------- | | ------ | --- | ------- |-- | -| 1 | Agent Sessions | ~700 | 7 `agent_*` | -| 2 | Executions | ~400 | 2 `memory_*` | -| 3 | Quality Gates | ~500 | 3 `quality_*` | -| 4 | Error Patterns | ~400 | 2 `memory_*` | -| 5 | Project State | ~800 | 9 `project_*` | -| 6 | Context Assembly | ~400 | 1 `memory_*` | - -Total: ~3200 LOC | 14 plans | 24 tools | 9 tables +| Phase | Goal | LOC | Tools | +| ---------------- | ---------------- | -------- | ------------- | +| 1 | Agent Sessions | ~700 | 7 `agent_*` | +| 2 | Executions | ~400 | 2 `memory_*` | +| 3 | Quality Gates | ~500 | 3 `quality_*` | +| 4 | Error Patterns | ~400 | 2 `memory_*` | +| 5 | Project State | ~800 | 9 `project_*` | +| 6 | Context Assembly | ~400 | 1 `memory_*` | +| Total: ~3200 LOC | 14 plans | 24 tools | 9 tables | ## Alternatives Considered diff --git a/docs/adr/033-mcp-handler-consolidation.md b/docs/adr/033-mcp-handler-consolidation.md index 6a1e7f0ee..42dc98f47 100644 --- a/docs/adr/033-mcp-handler-consolidation.md +++ b/docs/adr/033-mcp-handler-consolidation.md @@ -28,6 +28,7 @@ parameterization. ### Current Tool Inventory (38 tools) + | Category | Tools | Count | | ---------- | ------- | ------- | | Index/Search | index (action=start), search (resource=code), index (action=status), index (action=clear) | 4 | @@ -45,6 +46,7 @@ Consolidate to**8 tools** using resource-action parameterization pattern: ### New Tool Architecture + | Tool | Replaces | Pattern | | ------ | ---------- | --------- | | `index` | index (action=start), index (action=status), index (action=clear) | action: start, status, clear | @@ -124,7 +126,7 @@ struct ProjectArgs { ### Tool Count Reduction | Category | Before | After | Reduction | -| ---------- | -------- | ------- | | ----------- | +| ---------- | -------- | ------- | | | Index | 4 | 1 | -3 | | Search | 3 | 1 | -2 | | Validation | 5 | 1 | -4 | diff --git a/docs/adr/038-multi-tier-execution-model.md b/docs/adr/038-multi-tier-execution-model.md index a5c7ea1d4..caac19912 100644 --- a/docs/adr/038-multi-tier-execution-model.md +++ b/docs/adr/038-multi-tier-execution-model.md @@ -461,7 +461,7 @@ OR at any point: - Executing → Verifying: Agents completed (or timeout) - Verifying → AwaitingMerge: All policy checks pass (tests, reviews, security scans) - AwaitingMerge → Merged: (can be skipped if auto-merge enabled) -- - → Failed: At any point if error or operator rejection +- → Failed: At any point if error or operator rejection #### 2.3 OperatorState (Decision Loop) diff --git a/docs/adr/048-observability-strategy.md b/docs/adr/048-observability-strategy.md index ae42319b0..23c7d9672 100644 --- a/docs/adr/048-observability-strategy.md +++ b/docs/adr/048-observability-strategy.md @@ -59,7 +59,7 @@ To achieve "gap-free" visibility, we instrument the **Edges** and the **Core**: 1. **HTTP Edge (`http.rs`)**: - **Mechanism**: `TracingFairing`. - - **Role**: Extracts W3C Trace Context from HTTP headers. Ensures web requests are part of distributed traces. + - **Role**: Extracts W3C Trace Context from HTTP headers. Ensures web requests are part of distributed traces. 2. **Stdio Edge / Core (`mcp_server.rs`)**: - **Mechanism**: `#[instrument]` on `call_tool`. manual context extraction from `meta`. diff --git a/docs/adr/README.md b/docs/adr/README.md index 022747214..10151d651 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -92,7 +92,7 @@ The v0.3.0 release is a full platform rebuild on SeaQL (SeaORM, SeaQuery, SeaSch - [ADR 043: Hybrid Search Engine](phase-9/README.md#adr-043-hybrid-search-engine) — RRF fusion algorithm - [ADR 044: Model Selection](phase-9/README.md#adr-044-model-selection) — Embedding and search model choices - [ADR 045: Context Versioning](phase-9/README.md#adr-045-context-versioning) — Snapshot and temporal queries -- [ADR 046: Integration Patterns](phase-9/README.md#adr-046-integration-patterns) — MCP tool integration +- [ADR 046: Integration Patterns](046-integration-adr-034-037-policies.md) — MCP tool integration - [ADR 047: Project Architecture](047-project-architecture.md) — Central Hub and Multi-Dimensional Coordination ## Archived (Superseded) diff --git a/docs/adr/archive/superseded-032-agent-quality-domain.md b/docs/adr/archive/superseded-032-agent-quality-domain.md index 28d4a16fb..7d5edb50d 100644 --- a/docs/adr/archive/superseded-032-agent-quality-domain.md +++ b/docs/adr/archive/superseded-032-agent-quality-domain.md @@ -117,16 +117,15 @@ project_log_decision → Log decision ## Implementation Plan -| Phase | Goal | LOC | Tools | -| ------- | | ------ | --- | ------- |-- | -| 1 | Agent Sessions | ~700 | 7 `agent_*` | -| 2 | Executions | ~400 | 2 `memory_*` | -| 3 | Quality Gates | ~500 | 3 `quality_*` | -| 4 | Error Patterns | ~400 | 2 `memory_*` | -| 5 | Project State | ~800 | 9 `project_*` | -| 6 | Context Assembly | ~400 | 1 `memory_*` | - -Total: ~3200 LOC | 14 plans | 24 tools | 9 tables +| Phase | Goal | LOC | Tools | +| ---------------- | ---------------- | -------- | ------------- | +| 1 | Agent Sessions | ~700 | 7 `agent_*` | +| 2 | Executions | ~400 | 2 `memory_*` | +| 3 | Quality Gates | ~500 | 3 `quality_*` | +| 4 | Error Patterns | ~400 | 2 `memory_*` | +| 5 | Project State | ~800 | 9 `project_*` | +| 6 | Context Assembly | ~400 | 1 `memory_*` | +| Total: ~3200 LOC | 14 plans | 24 tools | 9 tables | ## Alternatives Considered diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index 25c9a4809..c08644c14 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -598,7 +598,7 @@ Run `make check WHAT=validate` to check compliance. The mcb-validate crate enfor Provider port traits are defined in `mcb-domain/src/ports/providers/`: | Trait | Purpose | -| ------- | | --------- | +| ------- | | | `EmbeddingProvider` | Generate vector embeddings from text | | `VectorStoreProvider` | Store and search vector embeddings | | `CacheProvider` | Caching abstraction | @@ -2359,7 +2359,7 @@ impl BackupManager { - [ADR-043: Hybrid Search Engine](../adr/phase-9/README.md#adr-043-hybrid-search-engine) - [ADR-044: Model Selection](../adr/phase-9/README.md#adr-044-model-selection) - [ADR-045: Context Versioning](../adr/phase-9/README.md#adr-045-context-versioning) -- [ADR-046: Integration Patterns](../adr/phase-9/README.md#adr-046-integration-patterns) +- [ADR-046: Integration Patterns](../adr/046-integration-adr-034-037-policies.md) - [ADR-047: Project Architecture](../adr/047-project-architecture.md) ### See Also diff --git a/docs/architecture/CLEAN_ARCHITECTURE.md b/docs/architecture/CLEAN_ARCHITECTURE.md index 1de091b8c..1828053b3 100644 --- a/docs/architecture/CLEAN_ARCHITECTURE.md +++ b/docs/architecture/CLEAN_ARCHITECTURE.md @@ -9,8 +9,8 @@ | Topic | Document | | --- | --- | -| Layer definitions (Domain → Server) | [ARCHITECTURE.md § Layers](./ARCHITECTURE.md#the-clean-architecture-layers) | -| Dependency direction & crate graph | [ARCHITECTURE.md § Dependency Rules](./ARCHITECTURE.md#dependency-rules) | +| Layer definitions (Domain → Server) | [ARCHITECTURE.md § Layers](./ARCHITECTURE.md#clean-architecture--dependency-injection) | +| Dependency direction & crate graph | [ARCHITECTURE.md § Dependency Rules](./ARCHITECTURE.md#dependency-validation-rules) | | Port categories & trait locations | [ARCHITECTURE.md § Port Categories](./ARCHITECTURE.md#port-categories) | | Extension patterns (new providers) | [ARCHITECTURE.md § Extensibility](./ARCHITECTURE.md#extensibility---adding-new-providers) | | Two-layer DI (linkme → Handle) | [PATTERNS.md § Two-Layer DI](./PATTERNS.md#two-layer-di-linkme--handle-adr-050) | diff --git a/docs/configuration/ENVIRONMENT_VARIABLES.md b/docs/configuration/ENVIRONMENT_VARIABLES.md index 744d3000b..ea4a2cbb1 100644 --- a/docs/configuration/ENVIRONMENT_VARIABLES.md +++ b/docs/configuration/ENVIRONMENT_VARIABLES.md @@ -391,7 +391,7 @@ export MCP_PROVIDERS__VECTOR_STORE__DIMENSIONS=768 ## Admin Defaults -See [admin helpers](../../crates/mcb-server/src/admin/web/helpers.rs) for operational defaults and helper constants. +See [admin helpers](../../crates/mcb-server/src/utils/mcp/helpers.rs) for operational defaults and helper constants. | Variable | Default | Description | | ---------- | --------- | ------------- | @@ -596,5 +596,5 @@ redis-cli ping # Should respond with PONG ## See Also - [Configuration Types](../../crates/mcb-infrastructure/src/config/) - Source code -- [Admin Helpers](../../crates/mcb-server/src/admin/web/helpers.rs) - Operational settings +- [Admin Helpers](../../crates/mcb-server/src/utils/mcp/helpers.rs) - Operational settings - [CONFIGURATION.md](../CONFIGURATION.md) - General configuration guide diff --git a/docs/design/workflow-management/SCHEMA.md b/docs/design/workflow-management/SCHEMA.md new file mode 100644 index 000000000..e5fbe9188 --- /dev/null +++ b/docs/design/workflow-management/SCHEMA.md @@ -0,0 +1,364 @@ + +# Agent & Quality Domain Extension - Schema Additions + +## Overview + +**Adições incrementais** ao schema MCB existente para suportar agent tracking e quality enforcement. Segue os padrões de nomenclatura dos ADRs existentes (ADR-009, ADR-013). + +## Design Principles + +1. **Extend, don't replace** - Usar infraestrutura existente +2. **Naming per ADR-009** - Tools: `agent_`, `quality_`, `memory_`, `project_` +3. **Tables per MCB pattern** - Entity names sem prefixos genéricos + +--- + +## Part 1: Entity Extensions + +### 1.1 ObservationType Enum Extension + +**Existing** (mcb-domain/src/entities/memory.rs): + +```rust +pub enum ObservationType { + Code, + Decision, + Context, + Error, + Summary, +} +``` + +### Add + +```rust +pub enum ObservationType { + Code, + Decision, + Context, + Error, + Summary, + Execution, // NEW: test/lint/build executions +} +``` + +### 1.2 ExecutionMetadata (new struct) + +Para Observations type=Execution: + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExecutionMetadata { + pub command: String, // "make test", "cargo clippy" + pub exit_code: i32, + pub duration_ms: u64, + pub success: bool, + pub execution_type: ExecutionType, + pub coverage: Option, + pub files_affected: Vec, + pub output_summary: String, + pub warnings_count: Option, + pub errors_count: Option, +} + +pub enum ExecutionType { + Test, + Lint, + Build, + CI, +} +``` + +### 1.3 ErrorMetadata Extension + +### Add to Error observation metadata + +```rust +pub struct ErrorMetadata { + // ... existing fields ... + pub fix_pattern: Option, // How this was fixed + pub fix_verified: bool, // Was the fix confirmed working + pub error_hash: String, // For matching similar errors +} +``` + +--- + +## Part 2: Agent Tables + +FK to existing `session_summaries`. + +### 2.1 agent_sessions + +```sql +CREATE TABLE agent_sessions ( + id TEXT PRIMARY KEY, + session_summary_id TEXT NOT NULL REFERENCES session_summaries(id), + agent_type TEXT NOT NULL, -- "sisyphus", "oracle", "explore" + model TEXT NOT NULL, -- "claude-opus-4-5" + parent_session_id TEXT REFERENCES agent_sessions(id), + started_at INTEGER NOT NULL, + ended_at INTEGER, + duration_ms INTEGER, + status TEXT NOT NULL DEFAULT 'active', + prompt_summary TEXT, + result_summary TEXT, + token_count INTEGER, + tool_calls_count INTEGER DEFAULT 0, + delegations_count INTEGER DEFAULT 0 +); + +CREATE INDEX idx_agent_sessions_parent ON agent_sessions(parent_session_id); +CREATE INDEX idx_agent_sessions_type ON agent_sessions(agent_type); +CREATE INDEX idx_agent_sessions_started ON agent_sessions(started_at); +``` + +### 2.2 delegations + +```sql +CREATE TABLE delegations ( + id TEXT PRIMARY KEY, + parent_session_id TEXT NOT NULL REFERENCES agent_sessions(id), + child_session_id TEXT NOT NULL REFERENCES agent_sessions(id), + prompt TEXT NOT NULL, + prompt_embedding_id TEXT, + result TEXT, + success BOOLEAN NOT NULL DEFAULT TRUE, + created_at INTEGER NOT NULL, + completed_at INTEGER, + duration_ms INTEGER +); + +CREATE INDEX idx_delegations_parent ON delegations(parent_session_id); +CREATE INDEX idx_delegations_child ON delegations(child_session_id); +``` + +### 2.3 tool_calls + +```sql +CREATE TABLE tool_calls ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES agent_sessions(id), + tool_name TEXT NOT NULL, + params_summary TEXT, + success BOOLEAN NOT NULL, + error_message TEXT, + duration_ms INTEGER, + created_at INTEGER NOT NULL +); + +CREATE INDEX idx_tool_calls_session ON tool_calls(session_id); +CREATE INDEX idx_tool_calls_tool ON tool_calls(tool_name); +``` + +### 2.4 checkpoints + +```sql +CREATE TABLE checkpoints ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES agent_sessions(id), + checkpoint_type TEXT NOT NULL, -- "git", "file", "config" + description TEXT NOT NULL, + snapshot_data TEXT NOT NULL, -- JSON + created_at INTEGER NOT NULL, + restored_at INTEGER, + expired BOOLEAN DEFAULT FALSE +); + +CREATE INDEX idx_checkpoints_session ON checkpoints(session_id); +``` + +--- + +## Part 3: Project State Tables + +Additions to the canonical `Schema`. + +### 3.1 phases + +GSD phases: + +```sql +CREATE TABLE phases ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id), + phase_number TEXT NOT NULL, -- "70", "80.1" + name TEXT NOT NULL, + goal TEXT, + status TEXT NOT NULL DEFAULT 'pending', + progress INTEGER DEFAULT 0, + depends_on TEXT, -- JSON array + started_at INTEGER, + completed_at INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + + UNIQUE(project_id, phase_number) +); + +CREATE INDEX idx_phases_project ON phases(project_id); +CREATE INDEX idx_phases_status ON phases(status); +``` + +### 3.2 issues + +Beads issues: + +```sql +CREATE TABLE issues ( + id TEXT PRIMARY KEY, -- "mcb-7b2" + project_id TEXT NOT NULL REFERENCES projects(id), + phase_id TEXT REFERENCES phases(id), + title TEXT NOT NULL, + description TEXT, + issue_type TEXT NOT NULL DEFAULT 'task', + priority INTEGER NOT NULL DEFAULT 2, + status TEXT NOT NULL DEFAULT 'open', + assignee TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + closed_at INTEGER, + close_reason TEXT, + metadata TEXT -- JSON +); + +CREATE INDEX idx_issues_project ON issues(project_id); +CREATE INDEX idx_issues_phase ON issues(phase_id); +CREATE INDEX idx_issues_status ON issues(status); +CREATE INDEX idx_issues_priority ON issues(priority); +``` + +### 3.3 issue_dependencies + +```sql +CREATE TABLE issue_dependencies ( + id TEXT PRIMARY KEY, + issue_id TEXT NOT NULL REFERENCES issues(id) ON DELETE CASCADE, + depends_on_id TEXT NOT NULL REFERENCES issues(id) ON DELETE CASCADE, + dependency_type TEXT NOT NULL DEFAULT 'blocks', + created_at INTEGER NOT NULL, + + UNIQUE(issue_id, depends_on_id, dependency_type) +); + +CREATE INDEX idx_issue_deps_issue ON issue_dependencies(issue_id); +CREATE INDEX idx_issue_deps_depends ON issue_dependencies(depends_on_id); +``` + +### 3.4 decisions + +```sql +CREATE TABLE decisions ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id), + phase_id TEXT REFERENCES phases(id), + session_id TEXT REFERENCES agent_sessions(id), + decision TEXT NOT NULL, + rationale TEXT, + outcome TEXT, + decided_at INTEGER NOT NULL, + created_at INTEGER NOT NULL +); + +CREATE INDEX idx_decisions_project ON decisions(project_id); +CREATE INDEX idx_decisions_phase ON decisions(phase_id); +``` + +--- + +## Part 4: Quality Gate Table + +### 4.1 quality_gate_configs + +```sql +CREATE TABLE quality_gate_configs ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id), + require_tests BOOLEAN DEFAULT TRUE, + require_lint BOOLEAN DEFAULT TRUE, + require_build BOOLEAN DEFAULT FALSE, + min_coverage REAL, + max_warnings INTEGER, + enabled BOOLEAN DEFAULT TRUE, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + + UNIQUE(project_id) +); +``` + +--- + +## Part 5: Views + +### 5.1 ready_issues + +```sql +CREATE VIEW ready_issues AS +WITH blocked AS ( + SELECT DISTINCT d.issue_id + FROM issue_dependencies d + JOIN issues blocker ON d.depends_on_id = blocker.id + WHERE blocker.status NOT IN ('closed') + AND d.dependency_type = 'blocks' +) +SELECT i.* +FROM issues i +WHERE i.status = 'open' + AND i.id NOT IN (SELECT issue_id FROM blocked) +ORDER BY i.priority, i.created_at; +``` + +### 5.2 recent_executions + +```sql +CREATE VIEW recent_executions AS +SELECT + o.id, + o.observation_type, + json_extract(o.metadata, '$.execution_type') as execution_type, + json_extract(o.metadata, '$.success') as success, + json_extract(o.metadata, '$.duration_ms') as duration_ms, + o.created_at +FROM observations o +WHERE o.observation_type = 'Execution' +ORDER BY o.created_at DESC +LIMIT 100; +``` + +--- + +## Summary + +### New Tables (9 total) + +| Category | Tables | +| ---------- | -------- | +| Agent (4) | `agent_sessions`, `delegations`, `tool_calls`, `checkpoints` | +| Project (4) | `phases`, `issues`, `issue_dependencies`, `decisions` | +| Quality (1) | `quality_gate_configs` | + +### Entity Extensions + +| Entity | Extension | +| -------- | ----------- | +| `ObservationType` | Add `Execution` variant | +| `Observation` (Error) | Add `fix_pattern`, `fix_verified` to metadata | + +### Size Estimates + +| Table | Est. Rows/year | Est. Size | +| ------- | ---------------- | ----------- | +| agent_sessions | ~10K | ~2 MB | +| delegations | ~50K | ~5 MB | +| tool_calls | ~500K | ~50 MB | +| checkpoints | ~5K | ~10 MB | +| phases | ~500 | ~100 KB | +| issues | ~5K | ~1 MB | +| decisions | ~2K | ~500 KB | + +**Total**: ~70 MB/year + +--- + +> Last updated: 2026-02-03 - aligned with MCB ADR naming patterns diff --git a/docs/developer/CONTRIBUTING.md b/docs/developer/CONTRIBUTING.md index 7cc328c55..ce93589c8 100644 --- a/docs/developer/CONTRIBUTING.md +++ b/docs/developer/CONTRIBUTING.md @@ -258,7 +258,6 @@ make build WHAT=docs ACT=validate QUICK=1 - [ARCHITECTURE.md](../architecture/ARCHITECTURE.md) — System overview - [PATTERNS.md](../architecture/PATTERNS.md) — Implementation patterns - [FLEXT_TO_MCB_MAPPING.md](./FLEXT_TO_MCB_MAPPING.md) — FLEXT pattern translation for MCB -- [SKILL_INDEX.md](./SKILL_INDEX.md) — Project ECC skills (under `.agents/skills/`) index +- [SKILL_INDEX.md](./SKILL_INDEX.md) — Project ECC skills index - [ROADMAP.md](./ROADMAP.md) — Project state and roadmap -- [IMPLEMENTATION_STATUS.md](./IMPLEMENTATION_STATUS.md) — Current state - [DEPLOYMENT.md](../operations/DEPLOYMENT.md) — Deployment guide diff --git a/docs/developer/FLEXT_TO_MCB_MAPPING.md b/docs/developer/FLEXT_TO_MCB_MAPPING.md index 308515cd1..066f550dc 100644 --- a/docs/developer/FLEXT_TO_MCB_MAPPING.md +++ b/docs/developer/FLEXT_TO_MCB_MAPPING.md @@ -6,7 +6,7 @@ **Sources:** -- FLEXT: `/home/marlonsc/flext/AGENTS.md`, `flext-core/README.md`, `flext-cli/README.md`, `flext-tests/README.md`, `.agents/skills/flext-*` +- FLEXT: the published `flext-sh` repositories on branch `0.12.0-dev` (`flext-core`, `flext-cli`, `flext-tests`), consumed as pinned git dependencies — never a local checkout - MCB: `AGENTS.md`, `Makefile`, `Cargo.toml`, `docs/architecture/PATTERNS.md` ## High-level analogy @@ -254,4 +254,4 @@ static OPENAI: EmbeddingProviderEntry = EmbeddingProviderEntry { - `.agents/skills/mcb-patterns/SKILL.md` — central MCB index - `docs/architecture/PATTERNS.md` — detailed MCB patterns - `docs/developer/CONTRIBUTING.md` — MCB contribution guide -- `/home/marlonsc/flext/AGENTS.md` — FLEXT canonical law +- — FLEXT canonical law on the published branch diff --git a/docs/developer/SKILL_INDEX.md b/docs/developer/SKILL_INDEX.md index 11b31d213..608515211 100644 --- a/docs/developer/SKILL_INDEX.md +++ b/docs/developer/SKILL_INDEX.md @@ -2,26 +2,32 @@ # MCB Project Skills Index -Project-specific ECC skills for the MCB Rust workspace. Load the relevant skill before editing code. +Project-specific ECC skills for the MCB Rust workspace. Load the relevant skill +before editing code. + +The skill files live under `.claude/skills//SKILL.md`. That tree is +workstation-local: `.gitignore` excludes `.claude/*`, so the skills are loaded by +the agent runtime rather than versioned here, and this index names them instead +of linking to repository paths that do not exist. ## Central index -- **[mcb-patterns](../../.agents/skills/mcb-patterns/SKILL.md)** — Central quick reference and skill router. +- **`mcb-patterns`** — Central quick reference and skill router. ## Domain skills | Skill | Use when | |-------|----------| -| [mcb-make-verbs](../../.agents/skills/mcb-make-verbs/SKILL.md) | Running build, test, lint, validate, ship, or bootstrap commands | -| [mcb-architecture-layers](../../.agents/skills/mcb-architecture-layers/SKILL.md) | Adding modules, reviewing crate boundaries, or moving responsibilities | -| [mcb-error-handling](../../.agents/skills/mcb-error-handling/SKILL.md) | Writing fallible code paths, choosing `?` vs `.context()` vs `Error::*` | -| [mcb-import-rules](../../.agents/skills/mcb-import-rules/SKILL.md) | Adding imports, resolving cycles, reviewing visibility | -| [mcb-testing-patterns](../../.agents/skills/mcb-testing-patterns/SKILL.md) | Writing or reviewing tests, fixtures, and test helpers | -| [mcb-quality-gates](../../.agents/skills/mcb-quality-gates/SKILL.md) | Validating changes before commit or debugging gate failures | +| `mcb-make-verbs` | Running build, test, lint, validate, ship, or bootstrap commands | +| `mcb-architecture-layers` | Adding modules, reviewing crate boundaries, or moving responsibilities | +| `mcb-error-handling` | Writing fallible code paths, choosing `?` vs `.context()` vs `Error::*` | +| `mcb-import-rules` | Adding imports, resolving cycles, reviewing visibility | +| `mcb-testing-patterns` | Writing or reviewing tests, fixtures, and test helpers | +| `mcb-quality-gates` | Validating changes before commit or debugging gate failures | ## Coordination skill -- **[orchestrate](../../.agents/skills/orchestrate/SKILL.md)** — Multi-step coordinator↔executor loop with beads. +- **`orchestrate`** — Multi-step coordinator↔executor loop with beads. ## Usage diff --git a/docs/guides/features/INTEGRATED_CONTEXT.md b/docs/guides/features/INTEGRATED_CONTEXT.md new file mode 100644 index 000000000..7ab37376e --- /dev/null +++ b/docs/guides/features/INTEGRATED_CONTEXT.md @@ -0,0 +1,301 @@ + +# Integrated Context System (v0.4.0) + +## Overview + +The Integrated Context System in v0.4.0 introduces a knowledge graph-based approach to code understanding, enabling freshness tracking, time-travel queries, and policy-driven context discovery. This system builds on the workflow FSM foundation (Phase 8) to provide intelligent, adaptive code search and analysis. + +## Core Concepts + +### 1. Freshness Tracking + +Code context degrades over time as repositories evolve. The freshness system tracks: + +- **Temporal Metadata**: Last modified timestamps, commit history, branch information +- **Staleness Signals**: Deprecated APIs, outdated patterns, version mismatches +- **Freshness Policies**: Rules for acceptable staleness by context type (e.g., "API docs must be < 7 days old") + +#### Example Workflow + +```text +User Query: "How do I authenticate users?" + ↓ +Search finds 3 matching code patterns + ↓ +Freshness check: Pattern A (2 days old) ✓, Pattern B (45 days old) ⚠, Pattern C (6 months old) ✗ + ↓ +Return Pattern A + B with staleness warnings +``` + +See**ADR-035: Freshness Tracking** for design details. + +### 2. Time-Travel Queries + +Understand code evolution by querying historical snapshots: + +- **Snapshot Versioning**: Capture code state at specific commits/dates +- **Temporal Queries**: "Show me how this function evolved over 6 months" +- **Regression Detection**: Identify when patterns were introduced/removed + +### Example + +```text +Query: "Show authentication patterns from v0.2.0" + ↓ +System retrieves snapshot from v0.2.0 tag + ↓ +Returns code patterns as they existed then + ↓ +Compare with current patterns to show evolution +``` + +See**ADR-045: Context Versioning** for implementation details. + +### 3. Compensation & Rollback + +When context becomes stale or invalid, the system can: + +- **Detect Invalidation**: Policy violations, breaking changes, deprecated APIs +- **Trigger Compensation**: Refresh context, notify users, suggest alternatives +- **Rollback**: Revert to previous valid context snapshot + +#### Example Workflow + +```text +Context: "Use OAuth2 for auth" + ↓ +Breaking change detected: OAuth2 endpoint deprecated + ↓ +Compensation triggered: Fetch new OAuth2 endpoint, update context + ↓ +User notified: "Context updated - OAuth2 endpoint changed" +``` + +See**ADR-037: Compensation & Orchestration** for orchestration patterns. + +## Architecture + +### 5-Layer Context System + +```text +┌─────────────────────────────────────────────────────┐ +│ Layer 5: Integration & Policies │ +│ (Policy enforcement, compensation triggers) │ +└─────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────┐ +│ Layer 4: Versioning & Snapshots │ +│ (Context snapshots, temporal queries) │ +└─────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────┐ +│ Layer 3: Hybrid Search Engine │ +│ (RRF fusion, semantic + keyword search) │ +└─────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────┐ +│ Layer 2: Knowledge Graph │ +│ (Code relationships, freshness metadata) │ +└─────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────┐ +│ Layer 1: Code Indexing & Embeddings │ +│ (AST parsing, vector embeddings, storage) │ +└─────────────────────────────────────────────────────┘ +``` + +#### Key Components + +**CodeGraph** (petgraph-based): + +- Nodes: Code entities (functions, classes, modules) +- Edges: Relationships (calls, imports, extends, implements) +- Metadata: Freshness, version, staleness signals + +### HybridSearchEngine + +- Semantic search via embeddings +- Keyword search via full-text index +- RRF (Reciprocal Rank Fusion) for Result ranking +- Freshness filtering and sorting + +### ContextSnapshot + +- Immutable capture of code state at specific commit/date +- Includes graph, embeddings, metadata +- Enables time-travel queries and regression detection + +## Workflows + +### Workflow 1: Freshness-Aware Search + +```text +1. User submits query: "How to handle errors?" +2. System searches code graph + embeddings +3. Results ranked by: + - Semantic relevance (embedding similarity) + - Keyword match (TF-IDF) + - Freshness score (recency + staleness signals) +4. Return top results with freshness metadata +5. User can filter by freshness threshold +``` + +### Workflow 2: Time-Travel Query + +```text +1. User asks: "Show me error handling from v0.2.0" +2. System retrieves snapshot for v0.2.0 tag +3. Searches within that snapshot's graph +4. Returns historical patterns +5. Optionally compare with current patterns +``` + +### Workflow 3: Policy-Driven Context Discovery + +```text +1. Policy defined: "API docs must be < 7 days old" +2. User searches for API documentation +3. System applies policy filter during search +4. Results filtered to only include fresh docs +5. If no fresh results, trigger compensation: + - Refresh docs from source + - Notify user of update + - Cache new version +``` + +### Workflow 4: Compensation & Rollback + +```text +1. Context in use: "Use endpoint /api/v1/auth" +2. Breaking change detected: Endpoint deprecated +3. System triggers compensation: + - Fetch new endpoint: /api/v2/auth + - Update context snapshot + - Notify dependent systems +4. If compensation fails: + - Rollback to previous snapshot + - Mark context as invalid + - Suggest manual review +``` + +## Integration Points + +### With Workflow FSM (Phase 8) + +The Integrated Context System integrates with the Workflow FSM: + +- **FSM Gates**: Context freshness gates workflow transitions +- **Policy Enforcement**: Policies applied at FSM state boundaries +- **Compensation Hooks**: FSM triggers compensation on policy violations + +See**ADR-034: Workflow FSM** for FSM details. + +### With MCP Tools + +New MCP tools expose context system capabilities: + +- `search_code`: Semantic search with freshness filtering +- `get_context_snapshot`: Retrieve historical context +- `apply_policy`: Apply freshness/validation policies +- `trigger_compensation`: Manually trigger compensation + +## Configuration + +### Freshness Policies + +```toml +[freshness] + +# Default staleness threshold (days) +default_max_age = 30 + +# Per-context-type policies +[freshness.policies] +api_docs = { max_age = 7, signal = "deprecated" } +examples = { max_age = 14, signal = "outdated" } +patterns = { max_age = 60, signal = "legacy" } +``` + +## Snapshot Retention + +```toml +[snapshots] + +# Keep snapshots for last N commits +retention_commits = 100 + +# Keep snapshots for last N days +retention_days = 90 + +# Snapshot frequency (commits) +frequency = 10 +``` + +## Examples + +### Example 1: Search with Freshness + +```bash + +# Search for authentication patterns, only fresh results +mcb search --query "authenticate user" --freshness-max-age 7 + +# Returns + +# 1. OAuth2 implementation (2 days old) ✓ + +# 2. JWT pattern (5 days old) ✓ + +# 3. Session-based auth (45 days old) ⚠ [STALE] +``` + +## Example 2: Time-Travel Query + +```bash + +# Show authentication patterns from v0.2.0 +mcb search --query "authenticate" --snapshot v0.2.0 + +# Compare with current +mcb search --query "authenticate" --snapshot v0.2.0 --compare-current + +# Output shows evolution + +# v0.2.0: Session-based auth + +# v0.3.0: Added JWT support + +# v0.4.0: OAuth2 + JWT + Session (multi-strategy) +``` + +## Example 3: Policy-Driven Search + +```bash + +# Apply "API docs must be fresh" policy +mcb search --query "API reference" --policy api_docs + +# Only returns docs < 7 days old + +# If no fresh docs found, triggers compensation +``` + +## Related Documentation + +- **ADR-034**: Workflow FSM – State machine for context workflows +- **ADR-035**: Freshness Tracking – Temporal metadata and staleness signals +- **ADR-036**: Policies & Validation – Policy enforcement framework +- **ADR-037**: Compensation & Orchestration – Rollback and recovery patterns +- **ADR-041**: Context Architecture – System design and layers +- **ADR-042**: Knowledge Graph – Graph structure and relationships +- **ADR-043**: Hybrid Search – Search engine design +- **ADR-044**: Model Selection – Embedding and search model choices +- **ADR-045**: Context Versioning – Snapshot and temporal query design +- **ADR-046**: Integration Patterns – MCP tool integration + +## Next Steps + +1. Review ADR-034-037 for workflow and policy foundations +2. Review ADR-041-046 for context system implementation +3. See [`docs/implementation/phase-9-roadmap.md`](../../implementation/phase-9-roadmap.md) for 4-week execution plan +4. Check [`docs/migration/v0.3-to-v0.4.md`](../../migration/v0.3-to-v0.4.md) for upgrade guide diff --git a/docs/implementation/phase-9-roadmap.md b/docs/implementation/phase-9-roadmap.md new file mode 100644 index 000000000..92c191394 --- /dev/null +++ b/docs/implementation/phase-9-roadmap.md @@ -0,0 +1,344 @@ + +# Phase 9 Roadmap: Integrated Context System (v0.4.0) + +## Overview + +**Phase 9** implements the Integrated Context System with knowledge graphs, freshness tracking, and time-travel queries. This is a 4-week execution plan (Feb 17 - Mar 16, 2026) with 70+ tests and 6 ADRs (ADR-041-046). + +**Builds on**: Phase 8 (Workflow FSM, Freshness Policies, Compensation) + +## Timeline + +| Week | Dates | Focus | Deliverables | +| ------ | ------- | ------- | -------------- | +| 1 | Feb 17-23 | Context Architecture & Graph | ADR-041, ADR-042, CodeGraph implementation | +| 2 | Feb 24-Mar 2 | Hybrid Search & Versioning | ADR-043, ADR-044, HybridSearchEngine, ContextSnapshot | +| 3 | Mar 3-9 | Integration & Policies | ADR-045, ADR-046, MCP tools, policy enforcement | +| 4 | Mar 10-16 | Testing & Documentation | 70+ tests, docs, migration guide, release | + +## Week 1: Context Architecture & Graph (Feb 17-23) + +### Goals + +- Define context system architecture (5 layers) +- Implement knowledge graph (petgraph-based) +- Create CodeGraph with relationships +- 15+ tests + +#### Tasks + +### ADR-041: Context Architecture + +- [ ] Define 5-layer context system +- [ ] Document layer responsibilities +- [ ] Define component interfaces +- [ ] Create architecture diagrams +- [ ] Beads issue: ADR-041 implementation + +### ADR-042: Knowledge Graph + +- [ ] Design graph structure (nodes, edges, metadata) +- [ ] Define relationship types (calls, imports, extends, implements) +- [ ] Design freshness metadata storage +- [ ] Create graph traversal algorithms +- [ ] Beads issue: ADR-042 implementation + +**CodeGraph Implementation** (mcb-application) + +- [ ] Create CodeGraph struct (petgraph-based) +- [ ] Implement node insertion (code entities) +- [ ] Implement edge insertion (relationships) +- [ ] Implement graph traversal (BFS, DFS) +- [ ] Add freshness metadata tracking +- [ ] 15+ unit tests + +#### Deliverables + +- ADR-041 document +- ADR-042 document +- CodeGraph implementation (mcb-application/src/graph/) +- 15+ tests passing + +### Beads Issues + +```bash +bd create "ADR-041: Context Architecture" -t feature -p 1 +bd create "ADR-042: Knowledge Graph" -t feature -p 1 +bd create "Implement CodeGraph (petgraph)" -t task -p 1 +bd create "Graph traversal algorithms" -t task -p 2 +``` + +## Week 2: Hybrid Search & Versioning (Feb 24-Mar 2) + +### Goals + +- Implement hybrid search engine (semantic + keyword) +- Implement context snapshots and versioning +- Create temporal query support +- 20+ tests + +#### Tasks + +### ADR-043: Hybrid Search + +- [ ] Design RRF (Reciprocal Rank Fusion) algorithm +- [ ] Define search modes (semantic, keyword, hybrid) +- [ ] Design freshness filtering +- [ ] Create search Result ranking +- [ ] Beads issue: ADR-043 implementation + +### ADR-044: Model Selection + +- [ ] Evaluate embedding models (OpenAI, VoyageAI, Ollama) +- [ ] Evaluate search algorithms (BM25, TF-IDF, RRF) +- [ ] Document model choices and trade-offs +- [ ] Create model configuration +- [ ] Beads issue: ADR-044 implementation + +**HybridSearchEngine Implementation** (mcb-application) + +- [ ] Create HybridSearchEngine struct +- [ ] Implement semantic search (embedding-based) +- [ ] Implement keyword search (full-text index) +- [ ] Implement RRF fusion algorithm +- [ ] Implement freshness filtering +- [ ] 15+ unit tests + +**ContextSnapshot Implementation** (mcb-application) + +- [ ] Create ContextSnapshot struct +- [ ] Implement snapshot creation (at commit/date) +- [ ] Implement snapshot storage +- [ ] Implement snapshot retrieval +- [ ] Implement temporal queries +- [ ] 10+ unit tests + +#### Deliverables + +- ADR-043 document +- ADR-044 document +- HybridSearchEngine implementation (mcb-application/src/search/) +- ContextSnapshot implementation (mcb-application/src/snapshot/) +- 25+ tests passing + +### Beads Issues + +```bash +bd create "ADR-043: Hybrid Search Engine" -t feature -p 1 +bd create "ADR-044: Model Selection" -t feature -p 1 +bd create "Implement HybridSearchEngine" -t task -p 1 +bd create "Implement ContextSnapshot" -t task -p 1 +bd create "RRF fusion algorithm" -t task -p 2 +``` + +## Week 3: Integration & Policies (Mar 3-9) + +### Goals + +- Integrate context system with workflow FSM +- Implement policy enforcement +- Create MCP tools for context operations +- 20+ tests + +#### Tasks + +### ADR-045: Context Versioning + +- [ ] Design snapshot versioning scheme +- [ ] Design temporal query language +- [ ] Design snapshot retention policies +- [ ] Create version comparison algorithms +- [ ] Beads issue: ADR-045 implementation + +### ADR-046: Integration Patterns + +- [ ] Design MCP tool integration +- [ ] Design policy enforcement hooks +- [ ] Design compensation triggers +- [ ] Design event system +- [ ] Beads issue: ADR-046 implementation + +**MCP Tool Implementation** (mcb-server) + +- [ ] Implement `search` tool (with freshness, snapshots, policies) +- [ ] Implement `index` tool (with snapshot creation) +- [ ] Implement `memory` tool (context storage) +- [ ] Implement `session` tool (workflow sessions) +- [ ] 15+ integration tests + +**Policy Enforcement** (mcb-application) + +- [ ] Create PolicyEngine struct +- [ ] Implement freshness policy enforcement +- [ ] Implement validation policies +- [ ] Implement compensation triggers +- [ ] Integrate with workflow FSM +- [ ] 10+ unit tests + +#### Deliverables + +- ADR-045 document +- ADR-046 document +- MCP tool implementations (mcb-server/src/handlers/) +- PolicyEngine implementation (mcb-application/src/policy/) +- 25+ tests passing + +### Beads Issues + +```bash +bd create "ADR-045: Context Versioning" -t feature -p 1 +bd create "ADR-046: Integration Patterns" -t feature -p 1 +bd create "Implement MCP tools (search, index, memory)" -t task -p 1 +bd create "Implement PolicyEngine" -t task -p 1 +bd create "FSM + context system integration" -t task -p 1 +``` + +## Week 4: Testing & Documentation (Mar 10-16) + +### Goals + +- Achieve 70+ tests across all components +- Complete documentation and migration guide +- Release v0.4.0 +- 15+ tests + +#### Tasks + +### Testing + +- [ ] Unit tests for CodeGraph (15+) +- [ ] Unit tests for HybridSearchEngine (15+) +- [ ] Unit tests for ContextSnapshot (10+) +- [ ] Unit tests for PolicyEngine (10+) +- [ ] Integration tests for MCP tools (15+) +- [ ] End-to-end tests (5+) +- [ ] Total: 70+ tests + +### Documentation + +- [ ] Complete ADR-041-046 documents +- [ ] Create migration guide (v0.3 → v0.4.0) +- [ ] Create feature guide (integrated-context.md) +- [ ] Create architecture documentation +- [ ] Update ROADMAP.md +- [ ] Update CHANGELOG.md +- [ ] Update README.md + +### Release + +- [ ] Run full test suite (`make test`) +- [ ] Run quality gates (`make check`) +- [ ] Run architecture validation (`make validate`) +- [ ] Create release notes +- [ ] Tag v0.4.0 +- [ ] Push to remote + +#### Deliverables + +- 70+ tests passing +- Complete documentation +- v0.4.0 release +- Migration guide +- Feature guides + +### Beads Issues + +```bash +bd create "Write 70+ tests for Phase 9" -t task -p 1 +bd create "Complete documentation (ADRs, guides, migration)" -t task -p 1 +bd create "Release v0.4.0" -t task -p 1 +``` + +## Test Coverage + +### Unit Tests (50+) + +**CodeGraph** (15+): + +- Node insertion and retrieval +- Edge insertion and retrieval +- Relationship type handling +- Freshness metadata tracking +- Graph traversal (BFS, DFS) +- Cycle detection +- Path finding + +**HybridSearchEngine** (15+): + +- Semantic search +- Keyword search +- RRF fusion +- Freshness filtering +- Result ranking +- Edge cases (empty results, single Result) + +**ContextSnapshot** (10+): + +- Snapshot creation +- Snapshot storage and retrieval +- Temporal queries +- Version comparison +- Snapshot retention + +**PolicyEngine** (10+): + +- Freshness policy enforcement +- Validation policy enforcement +- Compensation triggers +- Policy composition + +### Integration Tests (15+) + +### MCP Tools + +- `search` with freshness filtering +- `search` with snapshots +- `search` with policies +- `index` with snapshot creation +- `memory` context storage +- `session` workflow integration + +### FSM Integration + +- Context gates in workflow transitions +- Policy enforcement at state boundaries +- Compensation triggers + +### End-to-End Tests (5+) + +### Workflows + +- Freshness-aware search workflow +- Time-travel query workflow +- Policy-driven context discovery +- Compensation and rollback workflow + +## Success Criteria + +- [ ] All 70+ tests passing +- [ ] Zero architecture violations (`make validate`) +- [ ] Clean lint (`make lint`) +- [ ] Clean markdown lint (`make docs-lint`) +- [ ] ADR-041-046 complete and linked +- [ ] Migration guide complete +- [ ] Feature guides complete +- [ ] v0.4.0 released and tagged + +## Related Documentation + +- **ADR-034**: Workflow FSM – Foundation for context workflows +- **ADR-035**: Freshness Tracking – Temporal metadata +- **ADR-036**: Policies & Validation – Policy framework +- **ADR-037**: Compensation & Orchestration – Rollback patterns +- **ADR-041-046**: Phase 9 implementation details +- [`docs/guides/features/INTEGRATED_CONTEXT.md`](../guides/features/INTEGRATED_CONTEXT.md) – Feature overview +- [`docs/migration/v0.3-to-v0.4.md`](../migration/v0.3-to-v0.4.md) – Migration guide +- [`docs/architecture/CLEAN_ARCHITECTURE.md`](../architecture/CLEAN_ARCHITECTURE.md) – Architecture patterns + +## Next Steps + +1. Review ADR-034-037 (Phase 8 foundation) +2. Review ADR-041-046 (Phase 9 design) +3. Create Beads issues for each task +4. Start Week 1 (Feb 17) +5. Track progress weekly diff --git a/docs/modules/domain.md b/docs/modules/domain.md index 3e1ec7b12..14bd35823 100644 --- a/docs/modules/domain.md +++ b/docs/modules/domain.md @@ -86,12 +86,12 @@ Events published through the `EventPublisher` interface: | Port | Operations | Implementations | | ------ | ----------- | ---------------- | | [`EmbeddingProvider`](../../crates/mcb-domain/src/ports/providers/embedding.rs) | `embed`, `embed_batch`, `dimensions` | OpenAI, VoyageAI, Ollama, Gemini, FastEmbed, Anthropic | -| [`VectorStoreProvider`](../../crates/mcb-domain/src/ports/providers/vector_store/provider.rs) | `create_collection`, `insert`, `search` | EdgeVec, Milvus, Qdrant, Pinecone, Encrypted | +| [`VectorStoreProvider`](../../crates/mcb-domain/src/ports/providers/vector_store.rs) | `create_collection`, `insert`, `search` | EdgeVec, Milvus, Qdrant, Pinecone, Encrypted | | [`HybridSearchProvider`](../../crates/mcb-domain/src/ports/providers/hybrid_search.rs) | BM25 lexical + semantic search | Composite implementation | | [`LanguageChunkingProvider`](../../crates/mcb-domain/src/ports/providers/language_chunking.rs) | Language-specific AST parsing | 13 tree-sitter processors | | [`VcsProvider`](../../crates/mcb-domain/src/ports/providers/vcs.rs) | `clone`, `fetch`, `branches`, `commits` | git2 v0.20 | | [`CryptoProvider`](../../crates/mcb-domain/src/ports/providers/crypto.rs) | Encryption/decryption | AES-256-GCM, Argon2 | -| [`CacheProvider`](../../crates/mcb-domain/src/ports/providers/cache/provider.rs) | Distributed caching with TTL | Moka, Redis | +| [`CacheProvider`](../../crates/mcb-domain/src/`CacheProvider` (removed)) | Distributed caching with TTL | Moka, Redis | | [`ProjectDetectionProvider`](../../crates/mcb-domain/src/ports/providers/project_detection.rs) | Detect project type from manifests | Cargo, npm, Python, Go, Maven | @@ -100,30 +100,30 @@ Events published through the `EventPublisher` interface: | Port | Purpose | Implementation Location | | ------ | --------- | ------------------------ | | `ChunkRepository` | Persistence of AST-parsed code chunks and search statistics | [`mcb-providers`](../../crates/mcb-providers/src/lib.rs) | -| `MemoryRepository` | Multi-tenant observation storage with FTS5 lexical search capabilities | [`memory_repository.rs`](../../crates/mcb-providers/src/database/sqlite/memory_repository.rs) | -| `AgentRepository` | Composite management of **Agent Sessions**, **Delegations**, **Tool Calls**, and **Checkpoints** | [`agent_repository.rs`](../../crates/mcb-providers/src/database/sqlite/agent_repository.rs) | -| `ProjectRepository` | Persistence of **Project** root entities (multi-tenant boundary) | [`project_repository.rs`](../../crates/mcb-providers/src/database/sqlite/project_repository.rs) | -| `VcsEntityRepository` | Composite management of **Repositories**, **Branches**, **Worktrees**, and **Agent-Worktree Assignments** | [`vcs_entity_repository.rs`](../../crates/mcb-providers/src/database/sqlite/vcs_entity_repository.rs) | -| `PlanEntityRepository` | Persistence for **Plans**, **Versions**, and **Reviews** (Execution planning) | [`plan_entity_repository.rs`](../../crates/mcb-providers/src/database/sqlite/plan_entity_repository.rs) | -| `IssueEntityRepository` | Composite management of **Issues**, **Comments**, **Labels**, and **Label Assignments** | [`issue_entity_repository.rs`](../../crates/mcb-providers/src/database/sqlite/issue_entity_repository.rs) | -| `OrgEntityRepository` | Composite management of **Organizations**, **Users**, **Teams**, and **API Keys** | [`org_entity_repository.rs`](../../crates/mcb-providers/src/database/sqlite/org_entity_repository.rs) | +| `MemoryRepository` | Multi-tenant observation storage with FTS5 lexical search capabilities | [`memory_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/observation.rs) | +| `AgentRepository` | Composite management of **Agent Sessions**, **Delegations**, **Tool Calls**, and **Checkpoints** | [`agent_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/agent.rs) | +| `ProjectRepository` | Persistence of **Project** root entities (multi-tenant boundary) | [`project_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/project.rs) | +| `VcsEntityRepository` | Composite management of **Repositories**, **Branches**, **Worktrees**, and **Agent-Worktree Assignments** | [`vcs_entity_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/entity_repository.rs) | +| `PlanEntityRepository` | Persistence for **Plans**, **Versions**, and **Reviews** (Execution planning) | [`plan_entity_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/plans.rs) | +| `IssueEntityRepository` | Composite management of **Issues**, **Comments**, **Labels**, and **Label Assignments** | [`issue_entity_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/issues.rs) | +| `OrgEntityRepository` | Composite management of **Organizations**, **Users**, **Teams**, and **API Keys** | [`org_entity_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/org.rs) | ### Service Ports | Port | Purpose | | ------ | --------- | -| [`IndexingServiceInterface`](../../crates/mcb-domain/src/ports/services/indexing.rs) | Codebase indexing orchestration | -| [`BatchIndexingServiceInterface`](../../crates/mcb-domain/src/ports/services/indexing.rs) | Batch indexing orchestration | -| [`SearchServiceInterface`](../../crates/mcb-domain/src/ports/services/search.rs) | Semantic search with filters | -| [`ContextServiceInterface`](../../crates/mcb-domain/src/ports/services/context.rs) | Context aggregation | -| [`ValidationServiceInterface`](../../crates/mcb-domain/src/ports/services/validation.rs) | Code quality validation (12 rules) | -| [`MemoryServiceInterface`](../../crates/mcb-domain/src/ports/services/memory.rs) | Observation management | -| [`AgentSessionServiceInterface`](../../crates/mcb-domain/src/ports/services/agent.rs) | Agent lifecycle management | -| [`ProjectDetectorService`](../../crates/mcb-domain/src/ports/services/project.rs) | Project type detection orchestration | -| [`FileHashService`](../../crates/mcb-domain/src/ports/services/hash.rs) | File hashing service boundary | -| [`ChunkingOrchestratorInterface`](../../crates/mcb-domain/src/ports/services/chunking.rs) | Chunking orchestration boundary | -| [`CodeChunker`](../../crates/mcb-domain/src/ports/services/chunking.rs) | Language chunking service boundary | +| [`IndexingServiceInterface`](../../crates/mcb-domain/src/ports/services/indexing_service.rs) | Codebase indexing orchestration | +| [`BatchIndexingServiceInterface`](../../crates/mcb-domain/src/ports/services/indexing_service.rs) | Batch indexing orchestration | +| [`SearchServiceInterface`](../../crates/mcb-domain/src/ports/services/search_service.rs) | Semantic search with filters | +| [`ContextServiceInterface`](../../crates/mcb-domain/src/ports/services/context_service.rs) | Context aggregation | +| [`ValidationServiceInterface`](../../crates/mcb-domain/src/ports/services/validation_service.rs) | Code quality validation (12 rules) | +| [`MemoryServiceInterface`](../../crates/mcb-domain/src/ports/services/memory_service.rs) | Observation management | +| [`AgentSessionServiceInterface`](../../crates/mcb-domain/src/ports/services/agent_service.rs) | Agent lifecycle management | +| [`ProjectDetectorService`](../../crates/mcb-domain/src/ports/services/project_service.rs) | Project type detection orchestration | +| [`FileHashService`](../../crates/mcb-domain/src/ports/services/hash_service.rs) | File hashing service boundary | +| [`ChunkingOrchestratorInterface`](../../crates/mcb-domain/src/ports/services/chunking_service.rs) | Chunking orchestration boundary | +| [`CodeChunker`](../../crates/mcb-domain/src/ports/services/chunking_service.rs) | Language chunking service boundary | ## Key Enums & State Machines @@ -255,7 +255,7 @@ crates/mcb-domain/src/ | Utility | File | Purpose | | ------- | ---- | ------- | -| **Test Utils** | [`test_utils.rs`](../../crates/mcb-domain/src/test_utils.rs) | Shared domain specimen creation (Projects, Phases, Agents) | +| **Test Utils** | [`test_utils.rs`](../../crates/mcb-domain/src/utils/tests/) | Shared domain specimen creation (Projects, Phases, Agents) | --- diff --git a/docs/modules/infrastructure.md b/docs/modules/infrastructure.md index b6f3f9ade..abaeb6920 100644 --- a/docs/modules/infrastructure.md +++ b/docs/modules/infrastructure.md @@ -34,9 +34,9 @@ EMBEDDING_PROVIDERS → Resolver → init_app() → Handle (RwLock) (switch via API) ``` -- **Bootstrap** ([`bootstrap.rs`](../../crates/mcb-infrastructure/src/di/bootstrap.rs)): Application initialization. -- **Handles** ([`handles.rs`](../../crates/mcb-infrastructure/src/di/handles.rs)): RwLock wrappers for runtime switching. -- **Composition Root** ([`bootstrap.rs`](../../crates/mcb-infrastructure/src/di/bootstrap.rs)): AppContext manual composition root configuration. +- **Bootstrap** ([`bootstrap.rs`](../../crates/mcb-infrastructure/src/infrastructure/mod.rs)): Application initialization. +- **Handles** ([`handles.rs`](../../crates/mcb-infrastructure/src/infrastructure/mod.rs)): RwLock wrappers for runtime switching. +- **Composition Root** ([`bootstrap.rs`](../../crates/mcb-infrastructure/src/infrastructure/mod.rs)): AppContext manual composition root configuration. --- @@ -46,7 +46,7 @@ Type-safe, layered configuration management with environment variable overrides. ### Configuration Structure -- **Types** ([`types.rs`](../../crates/mcb-infrastructure/src/config/types.rs)): Hierarchical structures (`AppConfig`, `ServerConfig`, `AuthConfig`). +- **Types** ([`types.rs`](../../crates/mcb-infrastructure/src/config/app.rs)): Hierarchical structures (`AppConfig`, `ServerConfig`, `AuthConfig`). - **Loader** ([`loader.rs`](../../crates/mcb-infrastructure/src/config/loader.rs)): Multi-source loading (Environment + `.toml`). 👉 **Canonical Env Var Matrix**: [`ENVIRONMENT_VARIABLES.md`](../configuration/ENVIRONMENT_VARIABLES.md) @@ -55,10 +55,10 @@ Type-safe, layered configuration management with environment variable overrides. ## Shared Technical Areas -- [`cache/`](../../crates/mcb-infrastructure/src/cache/) - Shared caching infrastructure. -- [`logging/`](../../crates/mcb-infrastructure/src/logging/) - Contextual logging (Tracing/OpenTelemetry). +- [`cache/`](../../crates/mcb-infrastructure/src/crypto/) - Shared caching infrastructure. +- [`logging/`](../../crates/mcb-infrastructure/src/logging.rs) - Contextual logging (Tracing/OpenTelemetry). - [`crypto/`](../../crates/mcb-infrastructure/src/crypto/) - AES-256 and SHA-256 utilities. -- [`health.rs`](../../crates/mcb-infrastructure/src/health.rs) - System health check orchestration. +- [`health.rs`](../../crates/mcb-infrastructure/src/routing/health.rs) - System health check orchestration. ## File Structure diff --git a/docs/modules/project.md b/docs/modules/project.md index f6d74c51b..bb8174571 100644 --- a/docs/modules/project.md +++ b/docs/modules/project.md @@ -9,8 +9,8 @@ |---------|----| |Entity (Project)|[`crates/mcb-domain/src/entities/project.rs`](../../crates/mcb-domain/src/entities/project.rs)| |Entity (Issue)|[`crates/mcb-domain/src/entities/issue.rs`](../../crates/mcb-domain/src/entities/issue.rs)| -|Repository (Project)|[`crates/mcb-providers/src/database/sqlite/project_repository.rs`](../../crates/mcb-providers/src/database/sqlite/project_repository.rs)| -|Repository (Issue)|[`crates/mcb-providers/src/database/sqlite/issue_entity_repository.rs`](../../crates/mcb-providers/src/database/sqlite/issue_entity_repository.rs)| +|Repository (Project)|[`crates/mcb-providers/src/database/sqlite/project_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/project.rs)| +|Repository (Issue)|[`crates/mcb-providers/src/database/sqlite/issue_entity_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/issues.rs)| |ADR|[`ADR-047`](../adr/047-project-architecture.md)| |CLI Guide|[`project-cli.md`](./project-cli.md)| diff --git a/docs/modules/providers.md b/docs/modules/providers.md index 6b87092a6..f4e91da2d 100644 --- a/docs/modules/providers.md +++ b/docs/modules/providers.md @@ -62,10 +62,10 @@ pub trait VectorStoreProvider: Send + Sync { | Provider | Source | Protocol | Auth | Algorithm | Use Case | | ---------- | ------ | ---------- | ------ | ----------- | ---------- | -| EdgeVec | [`edgevec.rs`](../../crates/mcb-providers/src/vector_store/edgevec.rs) | In-process | None | HNSW (M=16, EF=100) | Dev/test, single-instance | -| Milvus | [`milvus.rs`](../../crates/mcb-providers/src/vector_store/milvus.rs) | gRPC | Optional | IVF_FLAT (NLIST=128) | Production cloud | -| Qdrant | [`qdrant.rs`](../../crates/mcb-providers/src/vector_store/qdrant.rs) | HTTP REST | API key | HNSW configurable | Production cloud | -| Pinecone | [`pinecone.rs`](../../crates/mcb-providers/src/vector_store/pinecone.rs) | HTTP REST | API key | Pre-created index | Managed cloud | +| EdgeVec | [`edgevec.rs`](../../crates/mcb-providers/src/vector_store/edgevec/) | In-process | None | HNSW (M=16, EF=100) | Dev/test, single-instance | +| Milvus | [`milvus.rs`](../../crates/mcb-providers/src/vector_store/milvus/) | gRPC | Optional | IVF_FLAT (NLIST=128) | Production cloud | +| Qdrant | [`qdrant.rs`](../../crates/mcb-providers/src/vector_store/qdrant/) | HTTP REST | API key | HNSW configurable | Production cloud | +| Pinecone | [`pinecone.rs`](../../crates/mcb-providers/src/vector_store/pinecone/) | HTTP REST | API key | Pre-created index | Managed cloud | | Encrypted | [`encrypted.rs`](../../crates/mcb-providers/src/vector_store/encrypted.rs) | Wraps any | N/A | AES-256-GCM decorator | Security-sensitive | ## Database @@ -79,13 +79,13 @@ pub trait VectorStoreProvider: Send + Sync { | Repository | Source | Domain Port | Purpose | | ----------- | ------ | ------------- | --------- | -| MemoryRepo | [`memory_repository.rs`](../../crates/mcb-providers/src/database/sqlite/memory_repository.rs) | `MemoryRepository` | Observation storage + FTS search | -| AgentRepo | [`agent_repository.rs`](../../crates/mcb-providers/src/database/sqlite/agent_repository.rs) | `AgentRepository` | Agent session persistence + query | -| OrgRepo | [`org_entity_repository.rs`](../../crates/mcb-providers/src/database/sqlite/org_entity_repository.rs) | `OrgEntityRepository` | Multi-tenant org data | -| VcsRepo | [`vcs_entity_repository.rs`](../../crates/mcb-providers/src/database/sqlite/vcs_entity_repository.rs) | `VcsEntityRepository` | Repository/branch persistence | -| PlanRepo | [`plan_entity_repository.rs`](../../crates/mcb-providers/src/database/sqlite/plan_entity_repository.rs) | `PlanEntityRepository` | Plan version/review persistence | -| IssueRepo | [`issue_entity_repository.rs`](../../crates/mcb-providers/src/database/sqlite/issue_entity_repository.rs) | `IssueEntityRepository` | Issue tracking persistence | -| ProjectRepo | [`project_repository.rs`](../../crates/mcb-providers/src/database/sqlite/project_repository.rs) | `ProjectRepository` | Project CRUD | +| MemoryRepo | [`memory_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/observation.rs) | `MemoryRepository` | Observation storage + FTS search | +| AgentRepo | [`agent_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/agent.rs) | `AgentRepository` | Agent session persistence + query | +| OrgRepo | [`org_entity_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/org.rs) | `OrgEntityRepository` | Multi-tenant org data | +| VcsRepo | [`vcs_entity_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/entity_repository.rs) | `VcsEntityRepository` | Repository/branch persistence | +| PlanRepo | [`plan_entity_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/plans.rs) | `PlanEntityRepository` | Plan version/review persistence | +| IssueRepo | [`issue_entity_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/issues.rs) | `IssueEntityRepository` | Issue tracking persistence | +| ProjectRepo | [`project_repository.rs`](../../crates/mcb-providers/src/database/seaorm/repos/project.rs) | `ProjectRepository` | Project CRUD | ## Hybrid Search diff --git a/docs/modules/validate.md b/docs/modules/validate.md index 5c65902af..12519d879 100644 --- a/docs/modules/validate.md +++ b/docs/modules/validate.md @@ -50,17 +50,17 @@ Enforces layer boundaries and dependency direction. | Rule ID | Name | Description | Source | | ------- | ---- | ----------- | ------ | -| `CA001` | Domain Independence | Domain crate must not depend on any internal crates | [`CA001_domain-independence.yml`](../../crates/mcb-validate/src/rules/clean-architecture/CA001_domain-independence.yml) | -| `CA003` | Domain Traits Only | Domain ports must be traits, not concrete implementations | [`CA003_domain-traits-only.yml`](../../crates/mcb-validate/src/rules/clean-architecture/CA003_domain-traits-only.yml) | -| `CA009` | Infra NO Application | Infrastructure cannot depend on Application services | [`CA009_infrastructure-no-application.yml`](../../crates/mcb-validate/src/rules/clean-architecture/CA009_infrastructure-no-application.yml) | +| `CA001` | Domain Independence | Domain crate must not depend on any internal crates | [`CA001_domain-independence.yml`](../../crates/mcb-validate/rules/clean-architecture/CA001_domain-independence.yml) | +| `CA003` | Domain Traits Only | Domain ports must be traits, not concrete implementations | [`CA003_domain-traits-only.yml`](../../crates/mcb-validate/rules/clean-architecture/CA003_domain-traits-only.yml) | +| `CA009` | Infra NO Application | Infrastructure cannot depend on Application services | [`CA009_infrastructure-no-application.yml`](../../crates/mcb-validate/rules/clean-architecture/CA009_infrastructure-no-application.yml) | ### 📁 Organization (ORG) Validates file placement, module structure, and domain purity. | Rule ID | Name | Description | Source | | ------- | ---- | ----------- | ------ | -| `ORG015` | Adapter Location | Adapters must reside in `crates/mcb-providers/src/` | [`ORG015_adapter-location.yml`](../../crates/mcb-validate/src/rules/organization/ORG015_adapter-location.yml) | -| `ORG018` | Port Location | Traits/Ports must reside in `crates/mcb-domain/src/ports/` | [`ORG018_port-location.yml`](../../crates/mcb-validate/src/rules/organization/ORG018_port-location.yml) | +| `ORG015` | Adapter Location | Adapters must reside in `crates/mcb-providers/src/` | [`ORG015_adapter-location.yml`](../../crates/mcb-validate/rules/organization/ORG015_adapter-location.yml) | +| `ORG018` | Port Location | Traits/Ports must reside in `crates/mcb-domain/src/ports/` | [`ORG018_port-location.yml`](../../crates/mcb-validate/rules/organization/ORG018_port-location.yml) | | `ORG020` | Domain Purity | Domain logic cannot leak into infrastructure adapters | [`domain_purity.rs`](../../crates/mcb-validate/src/validators/organization/domain_purity.rs) | ### ♻️ Refactoring (REF) @@ -97,7 +97,6 @@ The system extracts facts from the AST for rule evaluation: - `hybrid_engine.rs` — Combined engine approach (Static + Dynamic) - `rete_engine.rs` — RETE algorithm for high-performance pattern matching - `expression_engine.rs` — `evalexpr`-based logic evaluation - - `analyzer.rs` — Duplication analysis orchestration - `detector.rs` — Clone detection logic - `fingerprint.rs` — Token fingerprinting @@ -260,10 +259,10 @@ crates/mcb-validate/src/ ## Related Documentation -- [Architecture Overview](../architecture/ARCHITECTURE.md#validation-layer) - Validation layer details +- [Architecture Overview](../architecture/ARCHITECTURE.md) - Validation layer details - [ADR-013](../adr/013-clean-architecture-crate-separation.md) - Clean Architecture separation -- [SSOT Principles](./README.md#documentation-principles) - Single Source of Truth -- [Validators Implementation](./validate.md#validators-validators) - List of active validators +- [SSOT Principles](./README.md) - Single Source of Truth +- [Validators Implementation](./validate.md#rules--validators) - List of active validators --- diff --git a/docs/testing/GOLDEN_TESTS_CONTRACT.md b/docs/testing/GOLDEN_TESTS_CONTRACT.md index cac6d7796..a51f2e4d6 100644 --- a/docs/testing/GOLDEN_TESTS_CONTRACT.md +++ b/docs/testing/GOLDEN_TESTS_CONTRACT.md @@ -25,6 +25,7 @@ and content. ## 1. E2E workflow + | Test | Contract (what must hold) | | ------ | --------------------------- | | `golden_e2e_complete_workflow` | (1) index (action=clear)(collection) succeeds and response contains "clear"/"Clear"/"cleared". (2) index (action=status)(collection) succeeds, not error, text contains "Indexing Status" or "Idle" or "indexing". (3) index (action=start)(path, collection) succeeds, not error, text contains "chunks"/"file"/"Index"/"Files processed"/"Indexing Started". (4) index (action=status) again succeeds. (5) search (resource=code)(collection, query) succeeds, not error, text contains search result text and at least one sample-codebase file. (6) index (action=clear) again succeeds. (7) index (action=status) again succeeds. | @@ -38,6 +39,7 @@ and content. ## 2. Index + | Test | Contract | | ------ | ---------- | | `golden_index_test_repository` | index (action=start)(sample_codebase_path, collection) succeeds, not error, response content non-empty and contains "chunk"/"file"/"Index"/"Files processed"/"Indexing Started"/"Source directory"/"Path:". | @@ -49,6 +51,7 @@ and content. ## 3. MCP response schema (content shape) + | Test | Contract | | ------ | ---------- | | `golden_mcp_index (action=start)_schema` | index (action=start) response: Ok, content non-empty, not is_error. | @@ -62,6 +65,7 @@ and content. ## 4. Search validation + | Test | Contract | | ------ | ---------- | | `golden_search_returns_relevant_results` | After indexing sample_codebase into collection, search (resource=code)(collection, "handle MCP search request") succeeds, is not an error, and returns at least one sample-codebase file such as `handlers.rs`. | @@ -75,6 +79,7 @@ and content. ## 5. Golden queries E2E (split to avoid timeout) + | Test | Contract | | ------ | ---------- | | `golden_e2e_golden_queries_setup` | index (action=clear), index (action=start), then poll index (action=status) until Idle/processed (bounded wait: 20 × 50ms). | @@ -99,6 +104,7 @@ and content. ## 6. Org Entity CRUD (19 tests) + | Test | Contract | | ------ | ---------- | | `golden_org_create_and_get` | org_entity (action=create, resource=org) with data succeeds; org_entity (action=get, resource=org) with id succeeds; response contains id and name. | @@ -126,6 +132,7 @@ and content. ## 7. Data Isolation (6 tests) + | Test | Contract | | ------ | ---------- | | `golden_isolation_users_scoped_to_org` | Users created in org-A do not appear in org-B user list. | @@ -140,6 +147,7 @@ and content. ## 8. API Key Lifecycle (9 tests) + | Test | Contract | | ------ | ---------- | | `golden_api_key_create_and_get` | org_entity (action=create, resource=api_key) with data succeeds; get returns key with id, user_id, org_id, name, key_hash. | @@ -157,6 +165,7 @@ and content. ## 9. Session Lifecycle (6 tests) + | Test | Contract | | ------ | ---------- | | `golden_session_create_and_get` | session (action=create) with data succeeds; get returns session with id, status=active, agent_type, model, started_at. | @@ -171,6 +180,7 @@ and content. ## 10. VCS Entity CRUD (10 tests) + | Test | Contract | | ------ | ---------- | | `golden_vcs_repo_create_and_get` | vcs_entity (action=create, resource=repository) with data succeeds; get returns repo with id, name, url. | @@ -189,6 +199,7 @@ and content. ## 11. Plan Entity CRUD (12 tests) + | Test | Contract | | ------ | ---------- | | `golden_plan_create_and_get` | plan_entity (action=create, resource=plan) with data succeeds; get returns plan with id, title, project_id. | @@ -209,6 +220,7 @@ and content. ## 12. Issue Entity CRUD (13 tests) + | Test | Contract | | ------ | ---------- | | `golden_issue_create_and_get` | issue_entity (action=create, resource=issue) with data succeeds; get returns issue with id, title, project_id. | @@ -230,6 +242,7 @@ and content. ## 13. Validate Operations (4 tests) + | Test | Contract | | ------ | ---------- | | `golden_validate_analyze` | validate (action=analyze, scope=file) with valid file path succeeds; response is not error. | @@ -242,6 +255,7 @@ and content. ## 14. Project Operations (5 tests) + | Test | Contract | | ------ | ---------- | | `golden_project_create_get_update_delete` | project create/get/update/list/delete succeeds and list contains the created project before deletion. | diff --git a/docs/v040-KNOWLEDGE-GRAPH-SPEC.md b/docs/v040-KNOWLEDGE-GRAPH-SPEC.md index 6cb5a01f7..433b43707 100644 --- a/docs/v040-KNOWLEDGE-GRAPH-SPEC.md +++ b/docs/v040-KNOWLEDGE-GRAPH-SPEC.md @@ -14,6 +14,7 @@ network of entities, enabling high-fidelity code reasoning and hybrid search. Nodes represent semantic entities extracted from the source code. + | Node Type | Description | Attributes | | ----------- | ------------- | ------------ | | `Module` | A file or logical grouping of code | `path`, `is_external` | diff --git a/pyproject.toml b/pyproject.toml index 46500c2be..54e0eacd5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,9 @@ source = ["src"] known_first_party = ["flext_cli", "flext_core", "flext_tests", "mcb_scripts"] pep621_dev_dependency_groups = ["dev"] +[tool.hatch.build.targets.wheel] +packages = ["src/mcb_scripts"] + [tool.hatch.metadata] allow-direct-references = true @@ -96,7 +99,7 @@ files = ["scripts/check", "scripts/lib", "scripts/qlty"] follow_imports = "normal" ignore_missing_imports = true local_partial_types = true -mypy_path = ["."] +mypy_path = ["src", "."] namespace_packages = true no_implicit_reexport = true plugins = ["pydantic.mypy"] @@ -142,9 +145,9 @@ project-excludes = [ "typings/**", "vendor/**", ] -project-includes = ["scripts/**/*.py*", "tests/**/*.py*"] +project-includes = ["scripts/**/*.py*", "src/**/*.py*", "tests/**/*.py*"] python-version = "3.13" -search-path = ["."] +search-path = ["src", "."] use-ignore-files = false [tool.pyrefly.errors] @@ -262,7 +265,7 @@ exclude = [ "vendor", ] extraPaths = ["src", "."] -include = ["scripts", "tests"] +include = ["scripts", "src", "tests"] pythonPlatform = "Linux" pythonVersion = "3.13" reportArgumentType = "none" @@ -294,8 +297,6 @@ reportUnusedFunction = "error" reportUnusedImport = "error" reportUnusedVariable = "error" typeCheckingMode = "strict" -venv = ".venv" -venvPath = "." [[tool.pyright.executionEnvironments]] extraPaths = [".", "src"] @@ -315,6 +316,23 @@ reportUnknownVariableType = "none" reportUnusedImport = "none" root = "scripts" +[[tool.pyright.executionEnvironments]] +extraPaths = ["src"] +reportAssignmentType = "none" +reportAttributeAccessIssue = "none" +reportGeneralTypeIssues = "none" +reportInvalidTypeForm = "none" +reportPrivateImportUsage = "none" +reportPrivateUsage = "error" +reportReturnType = "none" +reportUnknownArgumentType = "none" +reportUnknownLambdaType = "none" +reportUnknownMemberType = "none" +reportUnknownParameterType = "none" +reportUnknownVariableType = "none" +reportUnusedImport = "none" +root = "src" + [[tool.pyright.executionEnvironments]] extraPaths = [".", "src"] reportArgumentType = "none" @@ -340,7 +358,7 @@ addopts = [ "--durations=10", "--markdown-docs", "--strict-markers", - "--timeout=30", + "--timeout=90", ] filterwarnings = [ "error", @@ -363,7 +381,7 @@ markers = [ minversion = "8.0" python_classes = ["Test*"] python_files = ["*_test.py", "*_tests.py", "test_*.py"] -pythonpath = ["scripts"] +pythonpath = ["src"] testpaths = [".", "docs", "examples", "scripts", "src", "tests"] # [MANAGED] ruff @@ -541,14 +559,13 @@ split-on-trailing-comma = false "TRY401", ] "scripts/check/gitops.py" = ["ERA001"] -"scripts/check/surface.py" = ["ERA001"] -"scripts/lib/cosmos_command.py" = ["TRY003"] -"scripts/lib/agent_pointers.py" = ["TRY003"] -"scripts/lib/tests/test_agent_pointers.py" = ["TRY003"] "scripts/codegen-conversions.py" = ["TRY003"] - -[tool.setuptools] -packages = [] +"src/mcb_scripts/cosmos_command.py" = ["S404", "S603", "TRY003"] +"src/mcb_scripts/agent_pointers.py" = ["TRY003"] +"src/mcb_scripts/workspace_command.py" = ["S404", "S603"] +"src/mcb_scripts/qlty/runner.py" = ["S404", "S603"] +"src/mcb_scripts/gitops.py" = ["S404", "S603"] +"tests/python/**" = ["S404", "S603", "S607", "TRY003"] # [MANAGED] tomlsort [tool.tomlsort] @@ -559,7 +576,7 @@ sort_first = ["build-system", "dependency-groups", "project", "tool"] [tool.uv] exclude-newer = "7 days" link-mode = "copy" -package = false +package = true [tool.vulture] exclude = ["*/_protocols/*"] @@ -579,7 +596,7 @@ whitelines = 1 dev = [ "actionlint-py>=1.7.12.24", "bandit>=1.8", - "code-review-graph[all]>=2.3.6", + "code-review-graph[all]>=2.3.7", "flext-tests @ git+https://github.com/flext-sh/flext-tests.git@0.12.0-dev", "mypy>=2.3.0", "pre-commit>=4.6.1", diff --git a/scripts/analyze_qlty.py b/scripts/analyze_qlty.py index f93772ac1..c448bf4ec 100755 --- a/scripts/analyze_qlty.py +++ b/scripts/analyze_qlty.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Analyze Qlty. -Thin entrypoint that delegates to scripts/qlty/main.py. +Thin entrypoint that delegates to mcb_scripts.qlty.main. Copyright (c) 2025 MCB Contributors. All rights reserved. SPDX-License-Identifier: MIT @@ -9,14 +9,7 @@ from __future__ import annotations -import sys -from pathlib import Path - -SCRIPTS = Path(__file__).resolve().parent -if str(SCRIPTS) not in sys.path: - sys.path.insert(0, str(SCRIPTS)) - -from qlty.main import main # ruff: ignore[module-import-not-at-top-of-file] +from mcb_scripts.qlty.main import main if __name__ == "__main__": main() diff --git a/scripts/check/gitops.py b/scripts/check/gitops.py index d1b0ea216..0211d7c9a 100644 --- a/scripts/check/gitops.py +++ b/scripts/check/gitops.py @@ -16,18 +16,14 @@ # /// from __future__ import annotations -import sys from pathlib import Path -SCRIPTS = Path(__file__).resolve().parents[1] -if str(SCRIPTS) not in sys.path: - sys.path.insert(0, str(SCRIPTS)) -from lib.cli import create_app_with_common_params, register_result_command # ruff: ignore[module-import-not-at-top-of-file] -from lib.core import BaseCommandSettings, get_logger, r # ruff: ignore[module-import-not-at-top-of-file] -from lib.gitops import GitOpsSummary, summarize # ruff: ignore[module-import-not-at-top-of-file] -from lib.settings import McbSettings # ruff: ignore[module-import-not-at-top-of-file] -from pydantic import Field # ruff: ignore[module-import-not-at-top-of-file] +from flext_cli import cli +from mcb_scripts.core import BaseCommandSettings, get_logger, r +from mcb_scripts.gitops import summarize +from mcb_scripts.settings import McbSettings +from pydantic import Field logger = get_logger(__name__) @@ -46,13 +42,25 @@ class GitopsSettings(BaseCommandSettings): ) -def run(settings: GitopsSettings) -> r[GitOpsSummary]: - """Discover and validate GitOps manifests.""" +# `from __future__ import annotations` defers every annotation to a string, and +# the CLI facade resolves the model in ITS namespace, where names like Path are +# absent. Rebuilding here binds them in the module that actually declares them. +GitopsSettings.model_rebuild() + + +def run(settings: GitopsSettings) -> r[str]: + """Discover and validate GitOps manifests. + + Returns the status string rather than the whole summary: the CLI facade + serializes a successful result as a JSON value, and GitOpsSummary carries + Path and nested report objects that are not JSON values. The summary is + still reported in full through the logger below. + """ k8s_root = settings.root / str(McbSettings().k8s_dir) summary_result = summarize(k8s_root) if summary_result.failure: logger.error(summary_result.error or "gitops discovery failed") - return summary_result + return r[str].fail(summary_result.error or "gitops discovery failed") summary = summary_result.unwrap() logger.info(f"GITOPS {summary.status}: {summary.message}") if summary.report.total_issues: @@ -60,23 +68,26 @@ def run(settings: GitopsSettings) -> r[GitOpsSummary]: for target in summary.targets: logger.info(f"{target.kind}\t{target.path}") if summary.status not in {"OK", "SKIP"}: - return r[GitOpsSummary].fail(summary.message) - return r[GitOpsSummary].ok(summary) + return r[str].fail(summary.message) + return r[str].ok(summary.status) def main() -> None: """Entrypoint used by the cosmos-command dispatcher and direct CLI runs.""" - app = create_app_with_common_params( + app = cli.create_app_with_common_params( name="check-gitops", help_text="Run MCB GitOps validation discovery." ) - register_result_command( + cli.register_result_command( app, name="run", help_text="Discover and validate GitOps manifests.", model_cls=GitopsSettings, handler=run, ) - app() + result = cli.execute_app(app, prog_name="check-gitops") + if result.failure: + logger.error(result.error or "cli execution failed") + raise SystemExit(1) if __name__ == "__main__": diff --git a/scripts/check/surface.py b/scripts/check/surface.py deleted file mode 100755 index 51ed7ea88..000000000 --- a/scripts/check/surface.py +++ /dev/null @@ -1,303 +0,0 @@ -#!/usr/bin/env python3 -"""Check Surface. - -Copyright (c) 2025 MCB Contributors. All rights reserved. -SPDX-License-Identifier: MIT -""" - -# /// cosmos-command -# verb = "check" -# what = "surface" -# domain = "quality" -# summary = "Validate the public make verb/WHAT/ACT command surface" -# description = "Runs the safe command-surface matrix: read-only checks, invalid-choice errors, and mutating dry-runs." -# example = "make check WHAT=surface" -# mutates = false -# /// -from __future__ import annotations - -import os -import subprocess -import sys -import shutil -from dataclasses import dataclass -from pathlib import Path -from typing import cast - -SCRIPTS = Path(__file__).resolve().parents[1] -if str(SCRIPTS) not in sys.path: - sys.path.insert(0, str(SCRIPTS)) - -from lib.core import BaseCommandSettings, McbResult, get_logger # ruff: ignore[module-import-not-at-top-of-file] - -logger = get_logger(__name__) - -ROOT = Path(__file__).resolve().parents[2] - - -@dataclass(frozen=True, slots=True) -class SurfaceCase: - """One public make invocation to validate.""" - - name: str - args: tuple[str, ...] - expected_rc: int = 0 - must_contain: tuple[str, ...] = () - - -READ_ONLY_CASES = ( - SurfaceCase("help", ("help",), must_contain=("mcb-scripts", "setup", "work")), - SurfaceCase("status git", ("status", "WHAT=git")), - SurfaceCase("work tags", ("work", "WHAT=tags")), - SurfaceCase("work branch list", ("work", "WHAT=branch")), - SurfaceCase("work sub status", ("work", "WHAT=sub-status")), - SurfaceCase( - "release version list", ("release", "WHAT=version"), must_contain=("Current:",) - ), - SurfaceCase("docs check", ("build", "WHAT=docs-check")), - SurfaceCase( - "docs adr list", - ("build", "WHAT=docs-adr"), - must_contain=("Architecture Decision Records",), - ), - SurfaceCase("gitops check", ("check", "WHAT=gitops"), must_contain=("GITOPS",)), - SurfaceCase( - "optimize cache dry-run", - ("check", "WHAT=optimize-cache"), - must_contain=("DRY-RUN",), - ), -) - -DRY_RUN_CASES = ( - SurfaceCase( - "codegen all", ("build", "WHAT=codegen-all"), must_contain=("DRY-RUN",) - ), - SurfaceCase("docs build", ("build", "WHAT=docs-build"), must_contain=("DRY-RUN",)), - SurfaceCase("docs serve", ("build", "WHAT=docs-serve"), must_contain=("DRY-RUN",)), - SurfaceCase("docs sync", ("build", "WHAT=docs-sync"), must_contain=("DRY-RUN",)), - SurfaceCase("docs setup", ("build", "WHAT=docs-setup"), must_contain=("DRY-RUN",)), - SurfaceCase( - "docs adr-new", ("build", "WHAT=docs-adr-new"), must_contain=("DRY-RUN",) - ), - SurfaceCase( - "docs diagrams", ("build", "WHAT=docs-diagrams"), must_contain=("DRY-RUN",) - ), - SurfaceCase( - "docs lint fix", ("build", "WHAT=docs-lint", "FIX=1"), must_contain=("DRY-RUN",) - ), - SurfaceCase("test e2e", ("test", "WHAT=e2e"), must_contain=("DRY-RUN",)), - SurfaceCase("check fix", ("check", "WHAT=fix-all"), must_contain=("DRY-RUN",)), - SurfaceCase("run dev", ("run", "WHAT=dev-run"), must_contain=("DRY-RUN",)), - SurfaceCase("clean all", ("clean", "WHAT=all"), must_contain=("DRY-RUN",)), - SurfaceCase( - "work add", ("work", "WHAT=add", "FILES=Makefile"), must_contain=("DRY-RUN",) - ), - SurfaceCase( - "work commit", - ("work", "WHAT=commit", "MSG=surface-check"), - must_contain=("DRY-RUN",), - ), - SurfaceCase("work push", ("work", "WHAT=push"), must_contain=("DRY-RUN",)), - SurfaceCase("work pull", ("work", "WHAT=pull"), must_contain=("DRY-RUN",)), - SurfaceCase( - "work branch create", - ("work", "WHAT=branch", "REF=surface-check-probe", "BASE=HEAD"), - must_contain=("DRY-RUN",), - ), - SurfaceCase( - "work checkout", - ("work", "WHAT=checkout", "REF=HEAD"), - must_contain=("DRY-RUN",), - ), - SurfaceCase( - "work tag", - ("work", "WHAT=tag", "TAG=surface-check-probe"), - must_contain=("DRY-RUN",), - ), - SurfaceCase("work stash", ("work", "WHAT=stash"), must_contain=("DRY-RUN",)), - SurfaceCase( - "work stash-pop", ("work", "WHAT=stash-pop"), must_contain=("DRY-RUN",) - ), - SurfaceCase( - "work merge", ("work", "WHAT=merge", "REF=HEAD"), must_contain=("DRY-RUN",) - ), - SurfaceCase( - "work rebase", ("work", "WHAT=rebase", "BASE=HEAD"), must_contain=("DRY-RUN",) - ), - SurfaceCase( - "work unstage", - ("work", "WHAT=unstage", "FILES=Makefile"), - must_contain=("DRY-RUN",), - ), - SurfaceCase( - "work push-tags", - ("work", "WHAT=push-tags", "TAG=surface-check-probe"), - must_contain=("DRY-RUN",), - ), - SurfaceCase( - "pr merge", ("work", "WHAT=pr-merge", "PR=1"), must_contain=("DRY-RUN",) - ), - SurfaceCase( - "pr rerun", ("work", "WHAT=pr-rerun", "RUN=1"), must_contain=("DRY-RUN",) - ), - SurfaceCase( - "release package", ("release", "WHAT=package"), must_contain=("DRY-RUN",) - ), - SurfaceCase( - "release version bump", - ("release", "WHAT=version", "BUMP=patch"), - must_contain=("DRY-RUN",), - ), - SurfaceCase( - "release install", ("release", "WHAT=install"), must_contain=("DRY-RUN",) - ), -) - -INVALID_CASES = ( - SurfaceCase( - "run invalid", - ("run", "WHAT=__invalid__"), - expected_rc=2, - must_contain=("ERROR:",), - ), - SurfaceCase( - "build invalid", - ("build", "WHAT=__invalid__"), - expected_rc=2, - must_contain=("ERROR:",), - ), - SurfaceCase( - "test invalid", - ("test", "WHAT=__invalid__"), - expected_rc=2, - must_contain=("unsupported test WHAT=",), - ), - SurfaceCase( - "check invalid", - ("check", "WHAT=__invalid__"), - expected_rc=2, - must_contain=("unsupported check WHAT=",), - ), - SurfaceCase( - "work invalid", - ("work", "WHAT=__invalid__"), - expected_rc=2, - must_contain=("unsupported work WHAT=",), - ), - SurfaceCase( - "clean invalid", - ("clean", "WHAT=__invalid__"), - expected_rc=2, - must_contain=("unsupported clean WHAT=",), - ), - SurfaceCase( - "codegen invalid", - ("build", "WHAT=codegen-__invalid__"), - expected_rc=2, - must_contain=("ERROR:",), - ), - SurfaceCase( - "docs invalid", - ("build", "WHAT=docs-__invalid__"), - expected_rc=2, - must_contain=("ERROR:",), - ), - SurfaceCase( - "fix invalid", - ("check", "WHAT=fix-__invalid__"), - expected_rc=2, - must_contain=("unsupported check WHAT=",), - ), - SurfaceCase( - "dev invalid", - ("check", "WHAT=dev-__invalid__"), - expected_rc=2, - must_contain=("unsupported check WHAT=",), - ), - SurfaceCase( - "pr invalid", - ("work", "WHAT=pr-__invalid__"), - expected_rc=2, - must_contain=("unsupported work WHAT=",), - ), - SurfaceCase( - "sub invalid", - ("work", "WHAT=sub-__invalid__"), - expected_rc=2, - must_contain=("unsupported work WHAT=",), - ), - SurfaceCase( - "release invalid", - ("release", "WHAT=__invalid__"), - expected_rc=2, - must_contain=("ERROR:",), - ), -) - - -class SurfaceSettings(BaseCommandSettings): - """Settings for the surface check command. - - cosmos-command exposes parameters unprefixed, so this base disables the - default ``MCB_`` prefix while keeping the FLEXT settings lifecycle. - """ - - -def _run_case(case: SurfaceCase) -> str | None: - env = os.environ.copy() - env["APPLY"] = "N" - env["QUICK"] = "1" - env["TERM"] = "dumb" - - make_bin = shutil.which("make") or "make" - result = subprocess.run( - (make_bin, "APPLY=N", *case.args), - cwd=ROOT, - env=env, - check=False, - capture_output=True, - text=True, - timeout=300, - ) - combined = result.stdout + result.stderr - if result.returncode != case.expected_rc: - return f"{case.name}: expected rc {case.expected_rc}, got {result.returncode}\n{combined}" - missing = [needle for needle in case.must_contain if needle not in combined] - if missing: - return f"{case.name}: missing {missing!r}\n{combined}" - return None - - -def run(_settings: SurfaceSettings) -> McbResult[int]: - """Validate the public make command surface.""" - failures: list[str] = [] - cases = (*READ_ONLY_CASES, *DRY_RUN_CASES, *INVALID_CASES) - for case in cases: - failure = _run_case(case) - if failure: - failures.append(failure) - - if failures: - logger.info("SURFACE FAIL") - for failure in failures: - logger.info(f"\n--- {failure}") - return cast("McbResult[int]", McbResult[int].fail("surface validation failed")) - - logger.info(f"SURFACE OK: {len(cases)} command cases validated") - logger.info( - "External/long-running operations are represented by APPLY-gated dry-runs." - ) - return cast("McbResult[int]", McbResult[int].ok(len(cases))) - - -def main() -> int: - """Entrypoint used by the cosmos-command dispatcher.""" - result = run(SurfaceSettings()) - if result.failure: - logger.error(result.error or "surface validation failed") - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/codegen-conversions.py b/scripts/codegen-conversions.py index 6babb4df9..e804aaf14 100644 --- a/scripts/codegen-conversions.py +++ b/scripts/codegen-conversions.py @@ -482,26 +482,27 @@ def gen_conversion_file(name: str, entity: EntityConfig) -> str: convert = entity.get("convert", {}) lines = [GENERATED_HEADER, ""] - lines.append("use sea_orm::ActiveValue;") - - lines.append("") - lines.append(f"use crate::database::seaorm::entities::{entity_module};") - lines.append(f"use {main_import};") + lines.extend(( + "use sea_orm::ActiveValue;", + "", + f"use crate::database::seaorm::entities::{entity_module};", + f"use {main_import};", + )) lines.extend(f"use {imp};" for imp in extra_imports) from_model_fields = [ gen_from_model_field(f, convert) for f in all_fields_ordered(entity) ] - lines.append("") - lines.append(f"impl From<{entity_module}::Model> for {domain_type} {{") - lines.append(f" fn from(m: {entity_module}::Model) -> Self {{") - lines.append(" Self {") + lines.extend(( + "", + f"impl From<{entity_module}::Model> for {domain_type} {{", + f" fn from(m: {entity_module}::Model) -> Self {{", + " Self {", + )) for fline in from_model_fields: lines.append(fline) - lines.append(" }") - lines.append(" }") - lines.append("}") + lines.extend((" }", " }", "}")) to_active_fields = [] for f in all_fields_ordered(entity): @@ -512,15 +513,15 @@ def gen_conversion_file(name: str, entity: EntityConfig) -> str: f" {ns_field}: ActiveValue::NotSet," for ns_field in not_set ) - lines.append("") - lines.append(f"impl From<{domain_type}> for {entity_module}::ActiveModel {{") - lines.append(f" fn from(e: {domain_type}) -> Self {{") - lines.append(" Self {") + lines.extend(( + "", + f"impl From<{domain_type}> for {entity_module}::ActiveModel {{", + f" fn from(e: {domain_type}) -> Self {{", + " Self {", + )) for fline in to_active_fields: lines.append(fline) - lines.append(" }") - lines.append(" }") - lines.append("}") + lines.extend((" }", " }", "}")) test_block = gen_test_block(name, entity) if test_block: @@ -536,7 +537,7 @@ def gen_mod_rs(names: list[str]) -> str: def main() -> None: - with open(CONFIG_PATH, "rb") as f: + with Path(CONFIG_PATH).open("rb") as f: config = TypeAdapter(dict[str, EntityConfig]).validate_python( tomllib.load(f), experimental_allow_partial=True ) diff --git a/scripts/codegen-post-process.py b/scripts/codegen-post-process.py index 402152997..736c2d0fc 100644 --- a/scripts/codegen-post-process.py +++ b/scripts/codegen-post-process.py @@ -8,6 +8,7 @@ import re import sys +import pathlib IRREGULAR_PLURALS = { "branches": "branch", @@ -36,8 +37,7 @@ def main() -> None: sys.exit(1) mod_path = sys.argv[1] - with open(mod_path) as f: - content = f.read() + content = pathlib.Path(mod_path).read_text() modules = re.findall(r"pub mod (\w+);", content) modules = [m for m in modules if m != "prelude"] @@ -51,8 +51,7 @@ def main() -> None: if aliases: content = content.rstrip() + "\n\n" + "\n".join(aliases) + "\n" - with open(mod_path, "w") as f: - f.write(content) + pathlib.Path(mod_path).write_text(content) if __name__ == "__main__": diff --git a/scripts/dispatch.py b/scripts/dispatch.py index 66251e5c4..fda051f90 100644 --- a/scripts/dispatch.py +++ b/scripts/dispatch.py @@ -6,7 +6,7 @@ from __future__ import annotations -from lib.cosmos_command import main +from mcb_scripts.cosmos_command import main if __name__ == "__main__": raise SystemExit(main()) diff --git a/scripts/docs/py/check_links.py b/scripts/docs/py/check_links.py index c1369a13c..fc7b04008 100755 --- a/scripts/docs/py/check_links.py +++ b/scripts/docs/py/check_links.py @@ -8,19 +8,15 @@ from __future__ import annotations import os -import sys from pathlib import Path -SCRIPTS = Path(__file__).resolve().parents[2] -if str(SCRIPTS) not in sys.path: - sys.path.insert(0, str(SCRIPTS)) -from lib.cli import create_app_with_common_params, register_result_command # ruff: ignore[module-import-not-at-top-of-file] -from lib.core import BaseMcbSettings, get_logger, r # ruff: ignore[module-import-not-at-top-of-file] -from lib.settings import McbSettings # ruff: ignore[module-import-not-at-top-of-file] -from pydantic import Field # ruff: ignore[module-import-not-at-top-of-file] +from flext_cli import cli +from mcb_scripts.core import BaseMcbSettings, get_logger, r +from mcb_scripts.settings import McbSettings +from pydantic import Field -from docs.py import utils # ruff: ignore[module-import-not-at-top-of-file] +from mcb_scripts.docs import utils logger = get_logger(__name__) @@ -28,11 +24,17 @@ class CheckLinksSettings(BaseMcbSettings): """Settings for the broken-link documentation check.""" - root: Path = Field(default=Path("."), description="Project root directory") + root: Path = Field(default=Path(), description="Project root directory") + + +# `from __future__ import annotations` defers every annotation to a string, and +# the CLI facade resolves the model in ITS namespace, where names like Path are +# absent. Rebuilding here binds them in the module that actually declares them. +CheckLinksSettings.model_rebuild() def _process_links( - links: list[tuple[str, str]], filepath: str, rel_filepath: str, project_root: str + links: list[tuple[str, str]], filepath: str, rel_filepath: str, project_root: Path ) -> tuple[list[tuple[str, str, str, str]], int]: broken_in_file: list[tuple[str, str, str, str]] = [] checked_in_file = 0 @@ -45,9 +47,9 @@ def _process_links( if link.startswith("/"): target = os.path.join(project_root, link.lstrip("/")) else: - target = os.path.normpath(os.path.join(os.path.dirname(filepath), link)) + target = os.path.normpath(os.path.join(Path(filepath).parent, link)) - if not os.path.exists(target): + if not Path(target).exists(): broken_in_file.append(( rel_filepath, text, @@ -59,7 +61,7 @@ def _process_links( def _check_files( - docs_dir: str, project_root: str + docs_dir: str, project_root: Path ) -> tuple[list[tuple[str, str, str, str]], int, int]: broken: list[tuple[str, str, str, str]] = [] checked_files = 0 @@ -72,8 +74,7 @@ def _check_files( checked_files += 1 try: - with open(filepath, encoding="utf-8") as fh: - content = fh.read() + content = Path(filepath).read_text(encoding="utf-8") except Exception as e: # noqa: BLE001 logger.error(f"Error reading {rel_filepath}: {e}") continue @@ -91,13 +92,13 @@ def _check_files( def run(settings: CheckLinksSettings) -> r[int]: """Check broken internal links in documentation.""" - project_root = os.path.abspath(settings.root) - if settings.root == Path("."): + project_root = Path(settings.root).resolve() + if settings.root == Path(): project_root = utils.get_project_root() docs_dir = os.path.join(project_root, str(McbSettings().docs_dir)) - if not os.path.exists(docs_dir): + if not Path(docs_dir).exists(): return r[int].fail(f"docs directory not found at {docs_dir}") broken, checked_files, checked_links = _check_files(docs_dir, project_root) @@ -115,17 +116,19 @@ def run(settings: CheckLinksSettings) -> r[int]: def main() -> None: - app = create_app_with_common_params( + app = cli.create_app_with_common_params( name="check-links", help_text="Check broken internal links in docs." ) - register_result_command( + cli.register_result_command( app, name="run", help_text="Check broken internal links in documentation.", model_cls=CheckLinksSettings, handler=run, ) - app() + result = cli.execute_app(app, prog_name="check-links") + if result.failure: + raise SystemExit(1) if __name__ == "__main__": diff --git a/scripts/docs/py/check_outdated.py b/scripts/docs/py/check_outdated.py index 354b1db2b..1d74e600a 100755 --- a/scripts/docs/py/check_outdated.py +++ b/scripts/docs/py/check_outdated.py @@ -9,19 +9,15 @@ import os import re -import sys from pathlib import Path -SCRIPTS = Path(__file__).resolve().parents[2] -if str(SCRIPTS) not in sys.path: - sys.path.insert(0, str(SCRIPTS)) -from lib.cli import create_app_with_common_params, register_result_command # ruff: ignore[module-import-not-at-top-of-file] -from lib.core import BaseMcbSettings, get_logger, r # ruff: ignore[module-import-not-at-top-of-file] -from lib.settings import McbSettings # ruff: ignore[module-import-not-at-top-of-file] -from pydantic import Field # ruff: ignore[module-import-not-at-top-of-file] +from flext_cli import cli +from mcb_scripts.core import BaseMcbSettings, get_logger, r +from mcb_scripts.settings import McbSettings +from pydantic import Field -from docs.py import utils # ruff: ignore[module-import-not-at-top-of-file] +from mcb_scripts.docs import utils logger = get_logger(__name__) @@ -47,7 +43,13 @@ class CheckOutdatedSettings(BaseMcbSettings): """Settings for the outdated-content documentation check.""" - root: Path = Field(default=Path("."), description="Project root directory") + root: Path = Field(default=Path(), description="Project root directory") + + +# `from __future__ import annotations` defers every annotation to a string, and +# the CLI facade resolves the model in ITS namespace, where names like Path are +# absent. Rebuilding here binds them in the module that actually declares them. +CheckOutdatedSettings.model_rebuild() def _is_suppressed(line: str) -> bool: @@ -60,7 +62,7 @@ def _process_lines( issues_in_file: list[tuple[str, int, str, str]] = [] for i, line in enumerate(lines, 1): stripped = line.strip() - if not stripped or stripped.startswith("", "", content, flags=re.DOTALL) diff --git a/scripts/lib/external_services_check.py b/src/mcb_scripts/external_services_check.py similarity index 85% rename from scripts/lib/external_services_check.py rename to src/mcb_scripts/external_services_check.py index d998045df..5e109eaa4 100644 --- a/scripts/lib/external_services_check.py +++ b/src/mcb_scripts/external_services_check.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """Check external test service availability for dynamic test grouping. Copyright (c) 2025 MCB Contributors. All rights reserved. @@ -12,10 +11,15 @@ import tomllib from pathlib import Path +from mcb_scripts.core import get_logger + ROOT = Path(__file__).resolve().parents[2] CONFIG_PATH = ROOT / "config" / "tests.toml" +logger = get_logger(__name__) + + def parse_url(url: str) -> tuple[str, int] | None: """Extract host and port from a URL-ish string.""" rest = url.rsplit("://", 1)[-1] @@ -51,26 +55,27 @@ def main() -> int: """Print availability summary and exit 0 only if all configured services are up.""" services = load_services() if not services: - print( - "No external test services configured in config/tests.toml; skipping external test group." + logger.info( + "No external test services configured in config/tests.toml; " + "skipping external test group." ) return 1 - print("External service availability:") + logger.info("External service availability:") all_available = True for key, url in sorted(services.items()): parsed = parse_url(url) available = parsed is not None and is_reachable(*parsed) marker = "✓" if available else "✗" - print(f" {marker} {key}: {url}") + logger.info(f" {marker} {key}: {url}") if not available: all_available = False if all_available: - print("All configured external services are available.") + logger.info("All configured external services are available.") return 0 - print( + logger.info( "One or more external services are unavailable; skipping external test group." ) return 1 diff --git a/scripts/lib/gitops.py b/src/mcb_scripts/gitops.py similarity index 87% rename from scripts/lib/gitops.py rename to src/mcb_scripts/gitops.py index cf6ed78c5..91f2653ff 100644 --- a/scripts/lib/gitops.py +++ b/src/mcb_scripts/gitops.py @@ -7,6 +7,7 @@ from __future__ import annotations import hashlib +import shutil import subprocess from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass @@ -20,13 +21,13 @@ ValidationError, validate_resource, ) -from qlty.model import SarifIssue, Severity -from qlty.report import AnalysisReport, analyze_issues +from mcb_scripts.qlty.model import SarifIssue, Severity +from mcb_scripts.qlty.report import AnalysisReport, analyze_issues from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedMap, CommentedSeq from ruamel.yaml.error import YAMLError -from lib.core import get_logger +from mcb_scripts.core import get_logger logger = get_logger(__name__) @@ -71,7 +72,6 @@ class ImageReference: def discover_targets(root: Path) -> list[GitOpsTarget]: """Discover Helm and Kustomize render targets below ``root``.""" - if not root.exists(): return [] @@ -98,7 +98,6 @@ def discover_targets(root: Path) -> list[GitOpsTarget]: def summarize(root: Path) -> p.Result[GitOpsSummary]: """Return a discovery summary for GitOps targets below ``root``.""" - targets = discover_targets(root) report_result = analyze(root) if report_result.failure: @@ -137,14 +136,12 @@ def summarize(root: Path) -> p.Result[GitOpsSummary]: def analyze(root: Path) -> p.Result[AnalysisReport]: """Analyze GitOps source manifests through the existing qlty report model.""" - issues = policy_issues(root) + rendered_issues(root) return analyze_issues(issues) def policy_issues(root: Path) -> list[SarifIssue]: """Return native GitOps policy issues discovered in source manifests.""" - issues: list[SarifIssue] = [] for path in _yaml_files(root): try: @@ -155,22 +152,21 @@ def policy_issues(root: Path) -> list[SarifIssue]: ) continue for document in documents: - for image in _image_references(document): - if image.value.endswith(":latest"): - issues.append( - _issue( - "gitops:no-latest-image", - f"Container image must not use the mutable latest tag: {image.value}", - path, - image.line, - ) - ) + issues.extend( + _issue( + "gitops:no-latest-image", + f"Container image must not use the mutable latest tag: {image.value}", + path, + image.line, + ) + for image in _image_references(document) + if image.value.endswith(":latest") + ) return issues def rendered_issues(root: Path, threads: int = 4) -> list[SarifIssue]: """Render Helm/Kustomize targets and validate emitted manifests.""" - targets = discover_targets(root) if not targets: return [] @@ -184,8 +180,7 @@ def rendered_issues(root: Path, threads: int = 4) -> list[SarifIssue]: def _render_and_validate(target: GitOpsTarget) -> list[SarifIssue]: """Render a single target and run schema validation on the output.""" - - rendered = _cached_render(target) + rendered = cached_render(target) if rendered is None: return [] issues: list[SarifIssue] = [] @@ -227,10 +222,9 @@ def _render_and_validate(target: GitOpsTarget) -> list[SarifIssue]: return issues -def _cached_render(target: GitOpsTarget) -> str | None: +def cached_render(target: GitOpsTarget) -> str | None: """Render a target, caching the result by input content hash.""" - - cache_key = _render_cache_key(target) + cache_key = render_cache_key(target) cache_path = CACHE_DIR / f"{cache_key}.yaml" if cache_path.exists(): return cache_path.read_text(encoding="utf-8") @@ -243,9 +237,8 @@ def _cached_render(target: GitOpsTarget) -> str | None: return rendered -def _render_cache_key(target: GitOpsTarget) -> str: +def render_cache_key(target: GitOpsTarget) -> str: """Return a stable hash for the target's inputs.""" - hasher = hashlib.sha256() hasher.update(target.kind.encode()) hasher.update(str(target.path).encode()) @@ -257,14 +250,22 @@ def _render_cache_key(target: GitOpsTarget) -> str: def _render_target(target: GitOpsTarget) -> str | None: """Run helm template or kustomize build for a target.""" - if target.kind == "helm": - cmd = ["helm", "template", str(target.path)] + tool, args = "helm", ["template", str(target.path)] elif target.kind == "kustomize": - cmd = ["kustomize", "build", str(target.path)] + tool, args = "kustomize", ["build", str(target.path)] else: return None + # Resolve the renderer to an absolute path: a bare name lets PATH order pick + # the binary, and a shadowed helm/kustomize would silently render different + # manifests than the ones this gate is meant to validate. + executable = shutil.which(tool) + if executable is None: + logger.warning(f"{target.kind} CLI not installed; skipping {target.path}") + return None + cmd = [executable, *args] + try: result = subprocess.run( cmd, capture_output=True, text=True, check=False, timeout=60 diff --git a/scripts/lib/logger.py b/src/mcb_scripts/logger.py similarity index 92% rename from scripts/lib/logger.py rename to src/mcb_scripts/logger.py index d0741a032..3e4a1341a 100644 --- a/scripts/lib/logger.py +++ b/src/mcb_scripts/logger.py @@ -9,7 +9,7 @@ from flext_core import FlextUtilitiesLogging, p, u -def configure_logging(json_format: bool = False) -> None: +def configure_logging(*, json_format: bool = False) -> None: """Configure structlog for MCB scripts. Call once at each CLI entrypoint. In normal dev mode logs are rendered as diff --git a/scripts/qlty/__init__.py b/src/mcb_scripts/qlty/__init__.py similarity index 100% rename from scripts/qlty/__init__.py rename to src/mcb_scripts/qlty/__init__.py diff --git a/scripts/qlty/main.py b/src/mcb_scripts/qlty/main.py similarity index 81% rename from scripts/qlty/main.py rename to src/mcb_scripts/qlty/main.py index 81e835f09..015747e0e 100644 --- a/scripts/qlty/main.py +++ b/src/mcb_scripts/qlty/main.py @@ -9,22 +9,23 @@ import fnmatch from pathlib import Path -import typer from flext_core import p -from lib.cli import create_app_with_common_params, register_result_command -from lib.core import get_logger, r -from lib.settings import McbSettings +from flext_cli import cli +from mcb_scripts.core import get_logger, r +from mcb_scripts.settings import McbSettings from pydantic import BaseModel, Field -from qlty.model import SarifIssue, Severity -from qlty.parser import parse_sarif_file -from qlty.report import AnalysisReport, analyze_issues -from qlty.runner import run_qlty_check, run_qlty_smells +from mcb_scripts.qlty.model import SarifIssue, Severity +from mcb_scripts.qlty.parser import parse_sarif_file +from mcb_scripts.qlty.report import analyze_issues +from mcb_scripts.qlty.runner import run_qlty_check, run_qlty_smells logger = get_logger(__name__) class QltyParams(BaseModel): + """Command parameters for the qlty analysis verb.""" + scan: bool = False checks_file: Path | None = None smells_file: Path = Field(default_factory=lambda: McbSettings().qlty_smells_sarif) @@ -42,6 +43,12 @@ class QltyParams(BaseModel): report_file: Path = Field(default_factory=lambda: McbSettings().qlty_report_md) +# `from __future__ import annotations` defers every annotation to a string, and +# the CLI facade resolves the model in ITS namespace, where names like Path are +# absent. Rebuilding here binds them in the module that actually declares them. +QltyParams.model_rebuild() + + def _load_checks_from_file(checks_file: Path, all_issues: list[SarifIssue]) -> r[None]: if not checks_file.exists(): return r[None].ok(None) @@ -83,9 +90,7 @@ def _collect_smells_issues(params: QltyParams, all_issues: list[SarifIssue]) -> def _collect_checks_issues(params: QltyParams, all_issues: list[SarifIssue]) -> r[None]: if params.scan: - outfile = ( - params.checks_file if params.checks_file else McbSettings().qlty_check_sarif - ) + outfile = params.checks_file or McbSettings().qlty_check_sarif checks_result = run_qlty_check(output_file=outfile) if checks_result.failure: return r[None].fail(checks_result.error or "qlty check failed") @@ -106,8 +111,8 @@ def _collect_checks_issues(params: QltyParams, all_issues: list[SarifIssue]) -> def _resolve_issue_types(params: QltyParams) -> tuple[bool, bool]: - do_checks = params.type in ("checks", "both") - do_smells = params.type in ("smells", "both") + do_checks = params.type in {"checks", "both"} + do_smells = params.type in {"smells", "both"} if params.check: do_checks = True @@ -213,17 +218,24 @@ def _apply_exclude_file_filter( return filtered -def analyze(params: QltyParams) -> p.Result[AnalysisReport]: - """Analyze SARIF quality reports.""" +def analyze(params: QltyParams) -> p.Result[str]: + """Analyze SARIF quality reports. + + Returns a short status string rather than the report object: the CLI facade + serializes a successful result as a JSON value, and AnalysisReport carries + Counter and dataclass members that are not JSON values. The full report is + still emitted through the logger and, unless --summary-only, written to the + report file. + """ issues_result = _collect_all_issues(params) if issues_result.failure: - return r[AnalysisReport].fail(issues_result.error or "issue collection failed") + return r[str].fail(issues_result.error or "issue collection failed") all_issues = issues_result.unwrap() if not all_issues: logger.info("✅ No issues found to analyze") - return r[AnalysisReport].ok(AnalysisReport(issues=[])) + return r[str].ok("no issues matched filters") filtered = all_issues filtered = _apply_severity_filter(params.severity, filtered) @@ -236,35 +248,38 @@ def analyze(params: QltyParams) -> p.Result[AnalysisReport]: if not filtered: logger.info("✅ No issues matched filters") - return r[AnalysisReport].ok(AnalysisReport(issues=[])) + return r[str].ok("no issues matched filters") report_result = analyze_issues(filtered) if report_result.failure: - return r[AnalysisReport].fail(report_result.error or "analysis failed") + return r[str].fail(report_result.error or "analysis failed") report = report_result.unwrap() - logger.info("\n" + report.generate_summary()) + logger.info(f"\n{report.generate_summary()}") if not params.summary_only: md_content = report.generate_markdown() params.report_file.write_text(md_content, encoding="utf-8") logger.info(f"\n📝 Detailed report written to {params.report_file}") - return r[AnalysisReport].ok(report) + return r[str].ok(f"{report.total_issues} issues analyzed") def main() -> None: - app = create_app_with_common_params( + """Entry point for the qlty SARIF analysis command.""" + app = cli.create_app_with_common_params( name="qlty", help_text="Analyze SARIF quality reports." ) - register_result_command( + cli.register_result_command( app, name="analyze", help_text="Analyze SARIF quality reports.", model_cls=QltyParams, handler=analyze, ) - typer.main.get_command(app)() + result = cli.execute_app(app, prog_name="qlty") + if result.failure: + raise SystemExit(1) if __name__ == "__main__": diff --git a/scripts/qlty/model.py b/src/mcb_scripts/qlty/model.py similarity index 94% rename from scripts/qlty/model.py rename to src/mcb_scripts/qlty/model.py index 392d8d571..d424dbcc1 100644 --- a/scripts/qlty/model.py +++ b/src/mcb_scripts/qlty/model.py @@ -21,10 +21,12 @@ class Severity(IntEnum): @classmethod def from_str(cls, s: str) -> Severity: + """Parse a SARIF severity token into a Severity.""" mapping = {"error": cls.ERROR, "warning": cls.WARNING, "note": cls.INFO} return mapping.get(s.lower(), cls.NONE) def to_emoji(self) -> str: + """Return the display glyph for this severity.""" return {self.ERROR: "🔴", self.WARNING: "🟠", self.INFO: "🔵", self.NONE: "⚪"}[ self ] @@ -118,6 +120,7 @@ class SarifIssue(BaseModel): @property def location_str(self) -> str: + """Format this issue's file and line as a single location string.""" if self.end_line and self.end_line != self.start_line: return f"{self.file_path}:{self.start_line}-{self.end_line}" return f"{self.file_path}:{self.start_line}" diff --git a/scripts/qlty/parser.py b/src/mcb_scripts/qlty/parser.py similarity index 93% rename from scripts/qlty/parser.py rename to src/mcb_scripts/qlty/parser.py index d1e44fc58..d92ed8226 100644 --- a/scripts/qlty/parser.py +++ b/src/mcb_scripts/qlty/parser.py @@ -9,9 +9,9 @@ import json from pathlib import Path -from lib.core import get_logger, r +from mcb_scripts.core import get_logger, r -from qlty.model import SarifIssue, SarifResult, SarifRun, Severity +from mcb_scripts.qlty.model import SarifIssue, SarifResult, SarifRun, Severity logger = get_logger(__name__) diff --git a/scripts/qlty/report.py b/src/mcb_scripts/qlty/report.py similarity index 70% rename from scripts/qlty/report.py rename to src/mcb_scripts/qlty/report.py index d9874b2fc..d5b4621a0 100644 --- a/scripts/qlty/report.py +++ b/src/mcb_scripts/qlty/report.py @@ -10,10 +10,10 @@ from dataclasses import dataclass, field from flext_core import p -from lib.core import r +from mcb_scripts.core import r -from qlty.model import SarifIssue, Severity -from qlty.strategies import get_strategy +from mcb_scripts.qlty.model import SarifIssue, Severity +from mcb_scripts.qlty.strategies import get_strategy @dataclass @@ -32,51 +32,40 @@ class AnalysisReport: def generate_summary(self) -> str: """Generate human-readable summary.""" lines: list[str] = [] - lines.append("━" * 72) - lines.append(f"📊 ANALYSIS SUMMARY: {self.total_issues} issues") - lines.append("━" * 72) - lines.append("") - - # Severity breakdown - lines.append("## By Severity") - lines.append("") + lines.extend(( + "━" * 72, + f"📊 ANALYSIS SUMMARY: {self.total_issues} issues", + "━" * 72, + "", + # Severity breakdown + "## By Severity", + "", + )) for sev in [Severity.ERROR, Severity.WARNING, Severity.INFO]: count = self.by_severity.get(sev, 0) pct = (count / self.total_issues * 100) if self.total_issues > 0 else 0 lines.append(f"{sev.to_emoji()} {sev.name:8s} {count:4d} ({pct:5.1f}%)") - lines.append("") - - # Category breakdown - lines.append("## By Category") - lines.append("") + lines.extend(("", "## By Category", "")) for cat, count in self.by_category.most_common(10): pct = (count / self.total_issues * 100) if self.total_issues > 0 else 0 lines.append(f" {cat:20s} {count:4d} ({pct:5.1f}%)") - lines.append("") - - # Top rules - lines.append("## Top 10 Rules") - lines.append("") + lines.extend(("", "## Top 10 Rules", "")) for rule, count in self.top_rules[:10]: pct = (count / self.total_issues * 100) if self.total_issues > 0 else 0 lines.append(f" {count:4d} ({pct:5.1f}%) {rule}") - lines.append("") - - # Top files - lines.append("## Top 10 Files") - lines.append("") + lines.extend(("", "## Top 10 Files", "")) for file_path, count in self.top_files[:10]: lines.append(f" {count:4d} {file_path}") - lines.append("") - - lines.append("━" * 72) + lines.extend(("", "━" * 72)) return "\n".join(lines) def _generate_severity_table(self, lines: list[str]) -> None: - lines.append("## Severity Distribution") - lines.append("") - lines.append("| Severity | Count | Percentage |") - lines.append("| ---------- | ------- | ------------ |") + lines.extend(( + "## Severity Distribution", + "", + "| Severity | Count | Percentage |", + "| ---------- | ------- | ------------ |", + )) for sev in [Severity.ERROR, Severity.WARNING, Severity.INFO]: count = self.by_severity.get(sev, 0) pct = (count / self.total_issues * 100) if self.total_issues > 0 else 0 @@ -84,30 +73,36 @@ def _generate_severity_table(self, lines: list[str]) -> None: lines.append("") def _generate_category_table(self, lines: list[str]) -> None: - lines.append("## Category Breakdown") - lines.append("") - lines.append("| Category | Count | Percentage |") - lines.append("| ---------- | ------- | ------------ |") + lines.extend(( + "## Category Breakdown", + "", + "| Category | Count | Percentage |", + "| ---------- | ------- | ------------ |", + )) for cat, count in self.by_category.most_common(): pct = (count / self.total_issues * 100) if self.total_issues > 0 else 0 lines.append(f"| {cat} | {count} | {pct:.1f}% |") lines.append("") def _generate_rules_table(self, lines: list[str]) -> None: - lines.append("## Top Rules") - lines.append("") - lines.append("| Rule | Count | Percentage |") - lines.append("| ------ | ------- | ------------ |") + lines.extend(( + "## Top Rules", + "", + "| Rule | Count | Percentage |", + "| ------ | ------- | ------------ |", + )) for rule, count in self.top_rules[:20]: pct = (count / self.total_issues * 100) if self.total_issues > 0 else 0 lines.append(f"| `{rule}` | {count} | {pct:.1f}% |") lines.append("") def _generate_files_table(self, lines: list[str]) -> None: - lines.append("## Most Affected Files") - lines.append("") - lines.append("| File | Issues |") - lines.append("| ------ | -------- |") + lines.extend(( + "## Most Affected Files", + "", + "| File | Issues |", + "| ------ | -------- |", + )) for file_path, count in self.top_files[:20]: lines.append(f"| `{file_path}` | {count} |") lines.append("") @@ -115,15 +110,16 @@ def _generate_files_table(self, lines: list[str]) -> None: def _generate_rule_section( self, lines: list[str], rule: str, rule_issues: list[SarifIssue] ) -> None: - lines.append(f"### {rule} ({len(rule_issues)} issues)") - lines.append("") + lines.extend((f"### {rule} ({len(rule_issues)} issues)", "")) strategy = get_strategy(rule) if strategy: - lines.append(f"**Strategy:** {strategy.title}") - lines.append("") - # Ensure blank line before list for MD032 compliance - lines.append(strategy.instructions.replace(":\\n-", ":\\n\\n-")) + # Blank line before the list keeps MD032 satisfied. + lines.extend(( + f"**Strategy:** {strategy.title}", + "", + strategy.instructions.replace(":\\n-", ":\\n\\n-"), + )) lines.append("") # Show up to 50 issues per rule to avoid massive files @@ -131,8 +127,7 @@ def _generate_rule_section( count = len(rule_issues) for issue in rule_issues[:limit]: - lines.append(f"#### `{issue.location_str}`") - lines.append("") + lines.extend((f"#### `{issue.location_str}`", "")) func = issue.fingerprints.get("function.name") if func: @@ -144,33 +139,31 @@ def _generate_rule_section( lines.append("") if count > limit: - lines.append(f"*...and {count - limit} more issues.*") - lines.append("") + lines.extend((f"*...and {count - limit} more issues.*", "")) def _generate_severity_section(self, lines: list[str], sev: Severity) -> None: sev_issues = [i for i in self.issues if i.level == sev] if not sev_issues: return - lines.append(f"## {sev.to_emoji()} {sev.name} Issues ({len(sev_issues)})") - lines.append("") + lines.extend((f"## {sev.to_emoji()} {sev.name} Issues ({len(sev_issues)})", "")) by_rule: defaultdict[str, list[SarifIssue]] = defaultdict(list) for issue in sev_issues: by_rule[issue.rule_id].append(issue) + def _issue_count(item: tuple[str, list[SarifIssue]]) -> int: + return len(item[1]) + for rule, rule_issues in sorted( - by_rule.items(), key=lambda x: len(x[1]), reverse=True + by_rule.items(), key=_issue_count, reverse=True ): self._generate_rule_section(lines, rule, rule_issues) def generate_markdown(self, title: str = "Quality Analysis Report") -> str: """Generate detailed markdown report.""" lines: list[str] = [] - lines.append(f"# {title}") - lines.append("") - lines.append(f"**Total Issues:** {self.total_issues}") - lines.append("") + lines.extend((f"# {title}", "", f"**Total Issues:** {self.total_issues}", "")) self._generate_severity_table(lines) self._generate_category_table(lines) diff --git a/scripts/qlty/runner.py b/src/mcb_scripts/qlty/runner.py similarity index 71% rename from scripts/qlty/runner.py rename to src/mcb_scripts/qlty/runner.py index f94e5bd82..ab4d87071 100644 --- a/scripts/qlty/runner.py +++ b/src/mcb_scripts/qlty/runner.py @@ -6,26 +6,41 @@ from __future__ import annotations -import subprocess # nosec B404 +import shutil +import subprocess from pathlib import Path -from lib.core import get_logger, r -from lib.settings import McbSettings +from mcb_scripts.core import get_logger, r +from mcb_scripts.settings import McbSettings -from qlty.model import SarifIssue -from qlty.parser import parse_sarif_file +from mcb_scripts.qlty.model import SarifIssue +from mcb_scripts.qlty.parser import parse_sarif_file logger = get_logger(__name__) +def _resolve_qlty() -> str | None: + """Resolve the qlty executable to an absolute path. + + Passing a bare name lets PATH order decide which binary runs; resolving + it here pins the decision and turns a missing tool into a typed failure + instead of an OSError raised from deep inside subprocess. + """ + return shutil.which("qlty") + + def run_qlty_check(output_file: Path | None = None) -> r[list[SarifIssue]]: """Run qlty check --all --sarif, save to file, and parse SARIF output.""" output_file = output_file or McbSettings().qlty_check_sarif logger.info("Running qlty check --all --sarif...") + executable = _resolve_qlty() + if executable is None: + return r[list[SarifIssue]].fail("qlty executable not found on PATH") + try: - result = subprocess.run( # nosec B603 B607 - ["qlty", "check", "--all", "--sarif"], + result = subprocess.run( + [executable, "check", "--all", "--sarif"], capture_output=True, text=True, timeout=300, @@ -56,9 +71,13 @@ def run_qlty_smells(output_file: Path | None = None) -> r[list[SarifIssue]]: output_file = output_file or McbSettings().qlty_smells_sarif logger.info("Running qlty smells --all --sarif...") + executable = _resolve_qlty() + if executable is None: + return r[list[SarifIssue]].fail("qlty executable not found on PATH") + try: - result = subprocess.run( # nosec B603 B607 - ["qlty", "smells", "--all", "--sarif"], + result = subprocess.run( + [executable, "smells", "--all", "--sarif"], capture_output=True, text=True, timeout=300, diff --git a/scripts/qlty/strategies.py b/src/mcb_scripts/qlty/strategies.py similarity index 93% rename from scripts/qlty/strategies.py rename to src/mcb_scripts/qlty/strategies.py index 4167c5371..28f428a0b 100644 --- a/scripts/qlty/strategies.py +++ b/src/mcb_scripts/qlty/strategies.py @@ -6,26 +6,24 @@ from __future__ import annotations -import abc +from typing import ClassVar -class FixStrategy(abc.ABC): - """Base for smell-fix strategies.""" +class FixStrategy: + """Base for smell-fix strategies. - @property - @abc.abstractmethod - def rule(self) -> str: - """Short rule name.""" + The three members are class-level CONSTANTS, not computed properties: every + subclass states literal text. Declaring them as abstract properties while + subclasses assigned plain strings made each assignment an incompatible + override of a property, which is what the type checkers reported. + """ - @property - @abc.abstractmethod - def title(self) -> str: - """Human-readable title.""" - - @property - @abc.abstractmethod - def instructions(self) -> str: - """English fix instructions.""" + #: Short rule name. + rule: ClassVar[str] = "" + #: Human-readable title. + title: ClassVar[str] = "" + #: English fix instructions. + instructions: ClassVar[str] = "" class IdenticalCodeStrategy(FixStrategy): @@ -200,5 +198,5 @@ class ReturnStatementsStrategy(FixStrategy): def get_strategy(rule_id: str) -> FixStrategy | None: """Get the appropriate fix strategy for a given rule ID.""" # Rule ID might be "qlty:similar-code" or just "similar-code" - short = rule_id.split(":")[-1] + short = rule_id.rsplit(":", maxsplit=1)[-1] return STRATEGIES.get(short) diff --git a/scripts/lib/result.py b/src/mcb_scripts/result.py similarity index 100% rename from scripts/lib/result.py rename to src/mcb_scripts/result.py diff --git a/scripts/lib/service.py b/src/mcb_scripts/service.py similarity index 93% rename from scripts/lib/service.py rename to src/mcb_scripts/service.py index 1487d079e..9531d0feb 100644 --- a/scripts/lib/service.py +++ b/src/mcb_scripts/service.py @@ -11,7 +11,7 @@ from flext_core import FlextService from pydantic import Field -from .settings import BaseMcbSettings +from mcb_scripts.settings import BaseMcbSettings class McbScriptsService(FlextService): diff --git a/scripts/lib/settings.py b/src/mcb_scripts/settings.py similarity index 94% rename from scripts/lib/settings.py rename to src/mcb_scripts/settings.py index 56ed32c86..91f3a4b00 100644 --- a/scripts/lib/settings.py +++ b/src/mcb_scripts/settings.py @@ -14,7 +14,7 @@ from pydantic import Field from pydantic_settings import SettingsConfigDict -from .constants import c +from mcb_scripts.constants import c class BaseMcbSettings(FlextSettings): @@ -72,7 +72,7 @@ class McbSettings(BaseMcbSettings): inputs/outputs without editing source. """ - project_root: Path = Field(default=Path("."), description="Project root directory") + project_root: Path = Field(default=Path(), description="Project root directory") k8s_dir: Path = Field( default=Path("k8s"), description="Kubernetes manifests directory" ) @@ -91,4 +91,4 @@ class McbSettings(BaseMcbSettings): ) -__all__ = ["BaseMcbSettings", "BaseCommandSettings", "McbSettings"] +__all__ = ["BaseCommandSettings", "BaseMcbSettings", "McbSettings"] diff --git a/scripts/lib/workspace.py b/src/mcb_scripts/workspace.py similarity index 100% rename from scripts/lib/workspace.py rename to src/mcb_scripts/workspace.py diff --git a/scripts/lib/workspace_command.py b/src/mcb_scripts/workspace_command.py similarity index 87% rename from scripts/lib/workspace_command.py rename to src/mcb_scripts/workspace_command.py index cb4cb84f7..8bc3095ef 100644 --- a/scripts/lib/workspace_command.py +++ b/src/mcb_scripts/workspace_command.py @@ -9,17 +9,18 @@ import os import runpy +import shutil import subprocess import sys +import tomllib from collections.abc import Callable, Iterable, Mapping, Sequence from dataclasses import dataclass from datetime import date, datetime, time from pathlib import Path from typing import NoReturn -import toml - -from lib.workspace import ( +from mcb_scripts.core import get_logger +from mcb_scripts.workspace import ( LOCAL_PYTHON, ROOT, SCRIPTS, @@ -56,6 +57,8 @@ @dataclass(frozen=True, slots=True) class Param: + """One declared parameter of a workspace command, from its TOML header.""" + name: str help: str required: bool = False @@ -65,6 +68,8 @@ class Param: @dataclass(frozen=True, slots=True) class Command: + """One executable workspace command resolved from the scripts tree.""" + verb: str what: str domain: str @@ -80,6 +85,8 @@ class Command: @dataclass(frozen=True, slots=True) class AliasTarget: + """The verb/WHAT pair an alias resolves to.""" + verb: str what: str @@ -92,10 +99,12 @@ class Registry: """In-memory view discovered from script headers; never a static catalog.""" def __init__(self) -> None: + """Create an empty registry.""" self._commands: dict[str, dict[str, Command]] = {} self._aliases: dict[str, AliasTarget] = {} def add(self, command: Command) -> None: + """Register one command, rejecting duplicate verb/WHAT or alias pairs.""" by_what = self._commands.setdefault(command.verb, {}) if command.what in by_what: msg = f"comando duplicado: {command.verb} WHAT={command.what}" @@ -114,6 +123,7 @@ def add(self, command: Command) -> None: self._aliases[alias_name] = target def validate(self) -> None: + """Reject a registry that violates the verb and alias contracts.""" if not self._commands: msg = "nenhum comando promovido encontrado em scripts//" raise RegistryError(msg) @@ -144,9 +154,11 @@ def validate(self) -> None: raise RegistryError(msg) def verbs(self) -> list[str]: + """List every promoted verb name.""" return sorted(self._commands) def resolve_verb(self, verb: str) -> str: + """Resolve an alias to its promoted verb, rejecting unknown names.""" target = self._aliases.get(verb) resolved = target.verb if target is not None else verb if resolved not in self._commands: @@ -155,12 +167,15 @@ def resolve_verb(self, verb: str) -> str: return resolved def alias_target(self, verb: str) -> AliasTarget | None: + """Return the verb/WHAT an alias resolves to, or None.""" return self._aliases.get(verb) def commands(self, verb: str) -> Mapping[str, Command]: + """Return every WHAT selector registered for a verb.""" return self._commands[self.resolve_verb(verb)] def command(self, verb: str, what: str) -> Command: + """Return one command, rejecting an unknown WHAT for the verb.""" commands = self.commands(verb) if what not in commands: valid = " ".join(sorted(commands)) @@ -169,12 +184,17 @@ def command(self, verb: str, what: str) -> Command: return commands[what] def aliases_for(self, verb: str) -> list[str]: + """List the aliases that point at a promoted verb.""" return sorted( alias for alias, target in self._aliases.items() if target.verb == verb ) +logger = get_logger(__name__) + + def main(argv: Sequence[str] | None = None) -> int: + """Entry point: discover workspace commands and dispatch the verb.""" args = tuple(sys.argv[1:] if argv is None else argv) try: ensure_local_python() @@ -190,6 +210,7 @@ def main(argv: Sequence[str] | None = None) -> int: def dispatch(registry: Registry, requested_verb: str) -> int: + """Route the requested verb to its command, or render help for it.""" alias_target = registry.alias_target(requested_verb) verb = registry.resolve_verb(requested_verb) requested_what = os.environ.get("WHAT", "").strip() @@ -207,17 +228,29 @@ def dispatch(registry: Registry, requested_verb: str) -> int: return run(command) +def _resolve_shell() -> str: + """Resolve the shell used to run non-Python commands. + + A bare "bash" lets PATH order decide which interpreter runs a repository + command; resolving it once pins that decision, and falls back to the + conventional absolute path when PATH carries no bash at all. + """ + return shutil.which("bash") or "/bin/bash" + + def run(command: Command) -> int: + """Execute a command, choosing the Python or shell runner by suffix.""" env = command_env(command) cwd = command_cwd(command) if command.path.suffix == ".py": return run_python(command, env, cwd) return subprocess.run( - ["bash", str(command.path)], cwd=cwd, env=env, check=False + [_resolve_shell(), str(command.path)], cwd=cwd, env=env, check=False ).returncode def run_python(command: Command, env: Mapping[str, str], cwd: Path) -> int: + """Execute a Python command in-process with an isolated environment.""" previous_env = os.environ.copy() previous_argv = sys.argv[:] previous_cwd = Path.cwd() @@ -226,7 +259,9 @@ def run_python(command: Command, env: Mapping[str, str], cwd: Path) -> int: os.environ.clear() os.environ.update(env) sys.argv = [str(command.path)] - sys.path.insert(0, str(cwd / "scripts")) + # The dispatched script runs in-process, so it must resolve the shared + # library from the canonical source root, never from scripts/. + sys.path.insert(0, str(cwd / "src")) sys.path.insert(1, str(cwd)) os.chdir(cwd) try: @@ -304,6 +339,7 @@ def ensure_local_python() -> None: def discover() -> Registry: + """Build the registry by scanning the workspace scripts tree.""" registry = Registry() script_roots = _script_roots() if not any(root.exists() for root in script_roots): @@ -354,6 +390,7 @@ def _has_command_files(verb_dir: Path) -> bool: def has_header(path: Path) -> bool: + """Report whether a file declares a workspace-command header.""" try: header_data(path) except RegistryError: @@ -362,6 +399,7 @@ def has_header(path: Path) -> bool: def load_command(path: Path, expected_verb: str) -> Command: + """Parse one workspace command file into a Command.""" data = header_data(path) verb = require_string(data, "verb", path) what = require_string(data, "what", path) @@ -387,6 +425,7 @@ def load_command(path: Path, expected_verb: str) -> Command: def header_data(path: Path) -> TomlTable: + """Extract and parse the TOML header block from a command file.""" lines = path.read_text(encoding="utf-8").splitlines()[:160] in_header = False payload: list[str] = [] @@ -404,13 +443,14 @@ def header_data(path: Path) -> TomlTable: msg = f"{path}: sem header workspace-command" raise RegistryError(msg) try: - return normalize_toml_table(toml.loads("\n".join(payload)), path) - except toml.TomlDecodeError as exc: + return normalize_toml_table(tomllib.loads("\n".join(payload)), path) + except tomllib.TOMLDecodeError as exc: msg = f"{path}: header TOML invalido: {exc}" raise RegistryError(msg) from exc def normalize_toml_table(data: RawTomlValue, path: Path) -> TomlTable: + """Normalize a parsed TOML table into the supported value shapes.""" if not isinstance(data, dict): msg = f"{path}: header TOML deve ser uma tabela" raise RegistryError(msg) @@ -421,6 +461,7 @@ def normalize_toml_table(data: RawTomlValue, path: Path) -> TomlTable: def normalize_toml_value(value: RawTomlValue, path: Path) -> TomlValue: + """Normalize one parsed TOML value into a supported shape.""" if isinstance(value, str | bool | int | float): return value if isinstance(value, list): @@ -438,6 +479,7 @@ def normalize_toml_value(value: RawTomlValue, path: Path) -> TomlValue: def require_string(data: Mapping[str, TomlValue], key: str, path: Path) -> str: + """Read a mandatory non-empty string field from a parsed header.""" value = data.get(key) if not isinstance(value, str) or not value.strip(): msg = f"{path}: campo obrigatorio ausente: {key}" @@ -446,6 +488,7 @@ def require_string(data: Mapping[str, TomlValue], key: str, path: Path) -> str: def require_bool(data: Mapping[str, TomlValue], key: str, path: Path) -> bool: + """Read a mandatory boolean field from a parsed header.""" value = data.get(key) if not isinstance(value, bool): msg = f"{path}: campo booleano obrigatorio ausente: {key}" @@ -454,10 +497,12 @@ def require_bool(data: Mapping[str, TomlValue], key: str, path: Path) -> bool: def parse_aliases(value: TomlValue | None, path: Path) -> tuple[str, ...]: + """Parse the alias list declared by a command header.""" return parse_string_list(value, "aliases", path) def parse_alias_spec(alias: str, command: Command) -> tuple[str, str]: + """Split one alias declaration into its verb and WHAT parts.""" alias_name, separator, target_what = alias.partition("=") alias_name = alias_name.strip() target_what = target_what.strip() if separator else command.what @@ -470,6 +515,7 @@ def parse_alias_spec(alias: str, command: Command) -> tuple[str, str]: def parse_string_list( value: TomlValue | None, field: str, path: Path ) -> tuple[str, ...]: + """Parse a header field that must hold a list of strings.""" if value is None: return () if not isinstance(value, list): @@ -485,6 +531,7 @@ def parse_string_list( def parse_params(value: TomlValue | None, path: Path) -> tuple[Param, ...]: + """Parse the params table declared by a command header.""" if value is None: return () if not isinstance(value, list): @@ -500,6 +547,7 @@ def parse_params(value: TomlValue | None, path: Path) -> tuple[Param, ...]: def parse_param(data: Mapping[str, TomlValue], path: Path) -> Param: + """Parse one entry of a command header params table.""" name = require_string(data, "name", path) help_text = require_string(data, "help", path) required_raw = data.get("required", False) @@ -520,6 +568,7 @@ def parse_param(data: Mapping[str, TomlValue], path: Path) -> Param: def render_requested_help(registry: Registry, requested: str) -> str: + """Render help for whatever scope the caller requested.""" if not requested: return render_global_help(registry) if "/" in requested: @@ -532,6 +581,7 @@ def render_requested_help(registry: Registry, requested: str) -> str: def render_global_help(registry: Registry) -> str: + """Render the top-level listing of every promoted verb.""" lines = ["workspace - make WHAT= [PARAM=value ...]", ""] for verb in registry.verbs(): command = registry.command(verb, "all") @@ -550,6 +600,7 @@ def render_global_help(registry: Registry) -> str: def render_verb_help(registry: Registry, requested_verb: str) -> str: + """Render the WHAT selectors available for one verb.""" verb = registry.resolve_verb(requested_verb) aliases = registry.aliases_for(verb) alias_suffix = f" (alias: {', '.join(aliases)})" if aliases else "" @@ -589,6 +640,7 @@ def render_verb_help(registry: Registry, requested_verb: str) -> str: def render_command_help(registry: Registry, requested_verb: str, what: str) -> str: + """Render the full help text for one verb/WHAT pair.""" command = registry.command(requested_verb, what) lines = [ f"make {requested_verb} WHAT={what}", @@ -614,6 +666,7 @@ def render_command_help(registry: Registry, requested_verb: str, what: str) -> s def render_dry_run(command: Command, requested_verb: str, what: str) -> str: + """Render what an invocation would execute, without running it.""" lines = [ "DRY-RUN: nenhuma mutacao executada.", f"Comando: make {requested_verb} WHAT={what}", @@ -650,6 +703,7 @@ def render_dry_run(command: Command, requested_verb: str, what: str) -> str: def format_params_inline(params: Iterable[Param]) -> str: + """Format a parameter sequence for single-line help output.""" parts: list[str] = [] for param in params: suffix = "*" if param.required else "" @@ -666,6 +720,7 @@ def format_params_inline(params: Iterable[Param]) -> str: def example_for(command: Command, requested_verb: str) -> str: + """Build a copy-pasteable example invocation for a command.""" canonical = f"make {command.verb}" requested = f"make {requested_verb}" if ( @@ -679,10 +734,12 @@ def example_for(command: Command, requested_verb: str) -> str: def env_enabled(name: str) -> bool: + """Report whether an environment flag is set to a truthy value.""" return os.environ.get(name, "N").upper() in {"1", "Y", "YES", "TRUE"} def validate_invocation(command: Command, *, require_required: bool = True) -> None: + """Reject an invocation whose declared parameters are unsatisfied.""" for param in command.params: value = param_value(param, command) if require_required and param.required and not value: @@ -698,6 +755,7 @@ def validate_invocation(command: Command, *, require_required: bool = True) -> N def validate_command_contract(command: Command) -> None: + """Reject a command file that violates the header contract.""" param_by_name = {param.name: param for param in command.params} if command.mutates: ensure_required_params(command, param_by_name, MUTATION_REQUIRED_PARAMS) @@ -712,6 +770,7 @@ def validate_command_contract(command: Command) -> None: def validate_all_choices(verb: str, commands: Mapping[str, Command]) -> None: + """Reject a verb whose WHAT=all omits any sibling selector.""" all_command = commands["all"] what_param = next( (param for param in all_command.params if param.name == "WHAT"), None @@ -731,6 +790,7 @@ def validate_all_choices(verb: str, commands: Mapping[str, Command]) -> None: def ensure_required_params( command: Command, params: Mapping[str, Param], names: Iterable[str] ) -> None: + """Reject an invocation missing any required parameter.""" for name in names: param = params.get(name) if param is None or not param.required: @@ -739,6 +799,7 @@ def ensure_required_params( def param_value(param: Param, command: Command) -> str: + """Resolve the effective value of a parameter for an invocation.""" if param.name == "WHAT": return command.what return os.environ.get(param.name, param.default).strip() @@ -761,10 +822,16 @@ def env_value(name: str, default: str = "") -> str: def require_env(name: str, usage: str | None = None) -> str: - """Return a required environment value or fail with the command contract code.""" + """Return a required environment value or fail with the command contract code. + + The optional ``usage`` string is the caller's contract line; it is reported + on failure so the operator learns WHICH selector was missing and how to + supply it, instead of receiving a bare exit code. + """ value = env_value(name) if value: return value + logger.error(usage or f"missing required environment value: {name}") raise SystemExit(2) diff --git a/tests/fixtures/test_repo/src/string_utils.py b/tests/fixtures/test_repo/src/string_utils.py index 1fc341094..8dad35c19 100644 --- a/tests/fixtures/test_repo/src/string_utils.py +++ b/tests/fixtures/test_repo/src/string_utils.py @@ -1,8 +1,8 @@ -"""String utility functions for testing semantic search across languages""" +"""String utility functions for testing semantic search across languages.""" def reverse_string(s: str) -> str: - """Reverses a string + """Reverse a string. Args: s: The string to reverse @@ -15,7 +15,7 @@ def reverse_string(s: str) -> str: def count_words(text: str) -> int: - """Counts the number of words in a text + """Count the number of words in a text. Args: text: The text to analyze @@ -28,7 +28,7 @@ def count_words(text: str) -> int: def is_palindrome(s: str) -> bool: - """Checks if a string is a palindrome + """Check if a string is a palindrome. Args: s: The string to check diff --git a/scripts/lib/tests/__init__.py b/tests/python/scripts_lib/__init__.py similarity index 100% rename from scripts/lib/tests/__init__.py rename to tests/python/scripts_lib/__init__.py diff --git a/scripts/lib/tests/_fixtures/__init__.py b/tests/python/scripts_lib/_fixtures/__init__.py similarity index 100% rename from scripts/lib/tests/_fixtures/__init__.py rename to tests/python/scripts_lib/_fixtures/__init__.py diff --git a/scripts/lib/tests/_utilities/__init__.py b/tests/python/scripts_lib/_utilities/__init__.py similarity index 100% rename from scripts/lib/tests/_utilities/__init__.py rename to tests/python/scripts_lib/_utilities/__init__.py diff --git a/scripts/lib/tests/_utilities/matchers.py b/tests/python/scripts_lib/_utilities/matchers.py similarity index 74% rename from scripts/lib/tests/_utilities/matchers.py rename to tests/python/scripts_lib/_utilities/matchers.py index 321f27d7b..907eac9c2 100644 --- a/scripts/lib/tests/_utilities/matchers.py +++ b/tests/python/scripts_lib/_utilities/matchers.py @@ -6,8 +6,6 @@ from __future__ import annotations -from typing import Any - from flext_core import p @@ -15,7 +13,7 @@ class TestMatchers: """Fluent assertions for common MCB test patterns.""" @staticmethod - def ok(result: p.Result[Any], value: Any | None = None) -> None: + def ok[T](result: p.Result[T], value: T | None = None) -> None: """Assert ``result`` is successful and optionally carries ``value``.""" assert result.success, f"expected success, got failure: {result.error}" if value is not None: @@ -24,7 +22,7 @@ def ok(result: p.Result[Any], value: Any | None = None) -> None: ) @staticmethod - def fail(result: p.Result[Any], contains: str | None = None) -> None: + def fail[T](result: p.Result[T], contains: str | None = None) -> None: """Assert ``result`` is a failure and optionally contains ``contains``.""" assert result.failure, f"expected failure, got success: {result.unwrap()!r}" if contains is not None: @@ -33,11 +31,6 @@ def fail(result: p.Result[Any], contains: str | None = None) -> None: f"expected error to contain {contains!r}, got {error!r}" ) - @staticmethod - def that(condition: bool, message: str = "assertion failed") -> None: - """Generic assertion wrapper with a human-readable message.""" - assert condition, message - tm = TestMatchers() diff --git a/scripts/lib/tests/conftest.py b/tests/python/scripts_lib/conftest.py similarity index 77% rename from scripts/lib/tests/conftest.py rename to tests/python/scripts_lib/conftest.py index a6b71d768..d61e7dd31 100644 --- a/scripts/lib/tests/conftest.py +++ b/tests/python/scripts_lib/conftest.py @@ -8,13 +8,17 @@ from collections.abc import Callable, Generator, MutableMapping from pathlib import Path -from typing import Any, cast +from typing import Any import pytest from pydantic import create_model from structlog.testing import capture_logs -from lib.core import BaseMcbSettings +from mcb_scripts.core import BaseMcbSettings + + +SettingsFactory = Callable[..., BaseMcbSettings] +"""Factory that builds a fresh settings subclass instance per test.""" @pytest.fixture(autouse=True) @@ -26,19 +30,16 @@ def reset_settings() -> Generator[None]: @pytest.fixture -def settings_factory() -> Callable[..., BaseMcbSettings]: +def settings_factory() -> SettingsFactory: """Return a factory that creates a fresh settings subclass instance.""" - def _make(**fields: Any) -> BaseMcbSettings: + def _make(**fields: object) -> BaseMcbSettings: defs: dict[str, Any] = { name: (type(value), value) for name, value in fields.items() } - _Settings = cast( - type[BaseMcbSettings], - create_model("_Settings", __base__=BaseMcbSettings, **defs), - ) - _Settings.model_rebuild() - return _Settings.fetch_global() + settings_cls = create_model("_Settings", __base__=BaseMcbSettings, **defs) + settings_cls.model_rebuild() + return settings_cls.fetch_global() return _make diff --git a/scripts/lib/tests/test_agent_pointers.py b/tests/python/scripts_lib/test_agent_pointers.py similarity index 82% rename from scripts/lib/tests/test_agent_pointers.py rename to tests/python/scripts_lib/test_agent_pointers.py index 3cc3c8c03..3c0cb5f23 100644 --- a/scripts/lib/tests/test_agent_pointers.py +++ b/tests/python/scripts_lib/test_agent_pointers.py @@ -6,20 +6,14 @@ from __future__ import annotations -import importlib.util from pathlib import Path import pytest -from ._utilities.matchers import tm + +from mcb_scripts import agent_pointers ROOT = Path(__file__).resolve().parents[3] -MODULE_PATH = ROOT / "scripts" / "lib" / "agent_pointers.py" -SPEC = importlib.util.spec_from_file_location("mcb_agent_pointers", MODULE_PATH) -if SPEC is None or SPEC.loader is None: - raise RuntimeError(f"failed to load module spec for {MODULE_PATH}") -agent_pointers = importlib.util.module_from_spec(SPEC) -SPEC.loader.exec_module(agent_pointers) def test_all_rendered_pointers_reference_agents_ssot() -> None: @@ -45,7 +39,7 @@ def test_rendered_pointer_files_are_not_ignored() -> None: } for path in agent_pointers.render_all(): - tm.that(path not in ignored, f"{path} is ignored") + assert path not in ignored, f"{path} is ignored" def test_check_detects_drift_without_writing(temp_dir: Path) -> None: diff --git a/scripts/lib/tests/test_core.py b/tests/python/scripts_lib/test_core.py similarity index 88% rename from scripts/lib/tests/test_core.py rename to tests/python/scripts_lib/test_core.py index b4b015845..41de68bc3 100644 --- a/scripts/lib/tests/test_core.py +++ b/tests/python/scripts_lib/test_core.py @@ -1,4 +1,4 @@ -"""Tests for scripts.lib.core — FLEXT-style kernel. +"""Tests for mcb_scripts.core — FLEXT-style kernel. Copyright (c) 2025 MCB Contributors. All rights reserved. SPDX-License-Identifier: MIT @@ -11,8 +11,9 @@ import pytest from pydantic import BaseModel -from ..core import McbResult, McbService, configure_logging, get_logger, r, s +from mcb_scripts.core import McbResult, McbService, configure_logging, get_logger, r, s from ._utilities.matchers import tm +from .conftest import SettingsFactory class TestResult: @@ -136,8 +137,12 @@ def test_map_error(self) -> None: def test_map_or(self) -> None: assert r[int].ok(42).map_or(0) == 42 assert r[int].fail("boom").map_or(0) == 0 - assert r[int].ok(21).map_or(0, lambda x: x * 2) == 42 - assert r[int].fail("boom").map_or(0, lambda x: x * 2) == 0 + + def _double(value: int) -> int: + return value * 2 + + assert r[int].ok(21).map_or(0, _double) == 42 + assert r[int].fail("boom").map_or(0, _double) == 0 def test_fail_op(self) -> None: result = r[int].fail_op("load") @@ -179,7 +184,8 @@ def double(x: int) -> int: @McbResult.safe def explode() -> int: - raise ValueError("boom") + msg = "boom" + raise ValueError(msg) result = explode() tm.fail(result, "boom") @@ -187,38 +193,40 @@ def explode() -> int: class TestSettings: def test_base_settings_read_env_with_prefix( - self, monkeypatch: pytest.MonkeyPatch, settings_factory: Any + self, monkeypatch: pytest.MonkeyPatch, settings_factory: SettingsFactory ) -> None: monkeypatch.setenv("MCB_LOG_LEVEL", "debug") settings = settings_factory(log_level="info") assert settings.log_level == "debug" def test_base_settings_ignore_extra_env( - self, monkeypatch: pytest.MonkeyPatch, settings_factory: Any + self, monkeypatch: pytest.MonkeyPatch, settings_factory: SettingsFactory ) -> None: monkeypatch.setenv("MCB_UNKNOWN_VAR", "ignored") settings = settings_factory(log_level="info") assert settings.log_level == "info" - def test_singleton_fetch_global(self, settings_factory: Any) -> None: + def test_singleton_fetch_global(self, settings_factory: SettingsFactory) -> None: settings = settings_factory(name="default") first = settings.fetch_global() second = settings.fetch_global() assert first is second - def test_clone_is_isolated(self, settings_factory: Any) -> None: + def test_clone_is_isolated(self, settings_factory: SettingsFactory) -> None: settings = settings_factory(name="default") global_settings = settings.fetch_global() clone = global_settings.clone(name="cloned") - assert clone.name == "cloned" - assert global_settings.name == "default" + assert clone.model_dump()["name"] == "cloned" + assert global_settings.model_dump()["name"] == "default" - def test_update_global_propagates(self, settings_factory: Any) -> None: + def test_update_global_propagates(self, settings_factory: SettingsFactory) -> None: settings = settings_factory(name="default") settings.update_global(name="updated") - assert settings.fetch_global().name == "updated" + assert settings.fetch_global().model_dump()["name"] == "updated" - def test_validate_overrides_rejects_unknown(self, settings_factory: Any) -> None: + def test_validate_overrides_rejects_unknown( + self, settings_factory: SettingsFactory + ) -> None: settings = settings_factory(name="default") with pytest.raises(ValueError, match="Unknown settings override"): settings.clone(unknown="value") @@ -266,7 +274,7 @@ class SettingsService(s): assert isinstance(SettingsService.fetch_global(), McbService) SettingsService.reset_for_testing() - def test_service_with_settings(self, settings_factory: Any) -> None: + def test_service_with_settings(self, settings_factory: SettingsFactory) -> None: settings = settings_factory(name="default") class DemoService(McbService): @@ -274,6 +282,6 @@ class DemoService(McbService): DemoService.reset_for_testing() service = DemoService.with_settings(settings.fetch_global()) - runtime_settings = cast(Any, service.runtime_settings) + runtime_settings = cast("Any", service.runtime_settings) assert runtime_settings.name == "default" DemoService.reset_for_testing() diff --git a/scripts/lib/tests/test_dev_env_optimize.py b/tests/python/scripts_lib/test_dev_env_optimize.py similarity index 100% rename from scripts/lib/tests/test_dev_env_optimize.py rename to tests/python/scripts_lib/test_dev_env_optimize.py diff --git a/scripts/lib/tests/test_external_services_check.py b/tests/python/scripts_lib/test_external_services_check.py similarity index 85% rename from scripts/lib/tests/test_external_services_check.py rename to tests/python/scripts_lib/test_external_services_check.py index 46fd68b1a..823b9012d 100644 --- a/scripts/lib/tests/test_external_services_check.py +++ b/tests/python/scripts_lib/test_external_services_check.py @@ -10,7 +10,12 @@ from pathlib import Path from unittest import mock -from lib.external_services_check import is_reachable, load_services, main, parse_url +from mcb_scripts.external_services_check import ( + is_reachable, + load_services, + main, + parse_url, +) ROOT = Path(__file__).resolve().parents[3] @@ -33,7 +38,7 @@ def test_is_reachable_false_when_connection_refused() -> None: assert is_reachable("localhost", 0, timeout=0.1) is False -def test_is_reachable_true_on_open_port(tmp_path: Path) -> None: +def test_is_reachable_true_on_open_port() -> None: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(("127.0.0.1", 0)) sock.listen(1) @@ -55,7 +60,7 @@ def test_load_services_returns_dict_from_config() -> None: def test_main_returns_one_when_no_services_configured(tmp_path: Path) -> None: config = tmp_path / "tests.toml" config.write_text("[test_services]\n", encoding="utf-8") - with mock.patch("lib.external_services_check.CONFIG_PATH", config): + with mock.patch("mcb_scripts.external_services_check.CONFIG_PATH", config): assert main() == 1 @@ -64,5 +69,5 @@ def test_main_returns_one_when_service_unreachable(tmp_path: Path) -> None: config.write_text( '[test_services]\nmilvus = "http://localhost:1"\n', encoding="utf-8" ) - with mock.patch("lib.external_services_check.CONFIG_PATH", config): + with mock.patch("mcb_scripts.external_services_check.CONFIG_PATH", config): assert main() == 1 diff --git a/scripts/lib/tests/test_gitops.py b/tests/python/scripts_lib/test_gitops.py similarity index 88% rename from scripts/lib/tests/test_gitops.py rename to tests/python/scripts_lib/test_gitops.py index 3077c7bff..aa4b42f72 100644 --- a/scripts/lib/tests/test_gitops.py +++ b/tests/python/scripts_lib/test_gitops.py @@ -12,10 +12,10 @@ import pytest -from lib.gitops import ( +from mcb_scripts.gitops import ( GitOpsTarget, - _cached_render, - _render_cache_key, + cached_render, + render_cache_key, analyze, discover_targets, summarize, @@ -23,7 +23,8 @@ from ._utilities.matchers import tm -SCRIPTS = Path(__file__).resolve().parents[2] +ROOT = Path(__file__).resolve().parents[3] +SCRIPTS = ROOT / "scripts" def _write_k8s_file(root: Path, rel_path: str, content: str) -> Path: @@ -116,7 +117,7 @@ def test_command_skips_without_using_cluster_clis() -> None: command = SCRIPTS / "check" / "gitops.py" result = subprocess.run( - [sys.executable, str(command), "--root", str(SCRIPTS.parents[0])], + [sys.executable, str(command), "run", "--root", str(ROOT)], check=False, capture_output=True, text=True, @@ -148,7 +149,7 @@ def test_command_fails_on_latest_image_policy_issue(temp_dir: Path) -> None: ) result = subprocess.run( - [sys.executable, str(command), "--root", str(temp_dir)], + [sys.executable, str(command), "run", "--root", str(temp_dir)], check=False, capture_output=True, text=True, @@ -162,16 +163,16 @@ def test_render_cache_key_is_stable_for_same_inputs(temp_dir: Path) -> None: _write_k8s_file(temp_dir, "k8s/chart/Chart.yaml", "apiVersion: v2\nname: sample\n") target = GitOpsTarget(kind="helm", path=temp_dir / "k8s" / "chart") - assert _render_cache_key(target) == _render_cache_key(target) + assert render_cache_key(target) == render_cache_key(target) def test_render_cache_key_changes_when_input_changes(temp_dir: Path) -> None: chart_dir = temp_dir / "k8s" / "chart" _write_k8s_file(temp_dir, "k8s/chart/Chart.yaml", "apiVersion: v2\nname: sample\n") target = GitOpsTarget(kind="helm", path=chart_dir) - before = _render_cache_key(target) + before = render_cache_key(target) (chart_dir / "values.yaml").write_text("foo: bar\n", encoding="utf-8") - after = _render_cache_key(target) + after = render_cache_key(target) assert before != after @@ -186,15 +187,17 @@ def test_cached_render_writes_and_reuses_cache( calls: list[list[str]] = [] - def fake_run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + def fake_run( + cmd: list[str], **_subprocess_kwargs: object + ) -> subprocess.CompletedProcess[str]: calls.append(cmd) return subprocess.CompletedProcess( args=cmd, returncode=0, stdout=rendered, stderr="" ) monkeypatch.setattr(subprocess, "run", fake_run) - first = _cached_render(target) - second = _cached_render(target) + first = cached_render(target) + second = cached_render(target) assert first == rendered assert second == rendered diff --git a/tests/python/scripts_lib/test_make_surface.py b/tests/python/scripts_lib/test_make_surface.py new file mode 100644 index 000000000..e2e6d65a2 --- /dev/null +++ b/tests/python/scripts_lib/test_make_surface.py @@ -0,0 +1,199 @@ +"""Lib Tests Test Make Surface. + +Copyright (c) 2025 MCB Contributors. All rights reserved. +SPDX-License-Identifier: MIT +""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import pytest +from flext_cli import cli, t as flext_t + + +ROOT = Path(__file__).resolve().parents[3] + + +def _json_list(value: flext_t.JsonValue) -> list[flext_t.JsonValue]: + """Return `value` as a list, or an empty list when it is anything else. + + cli.read_yaml_file yields a JsonValue union, so every nested lookup has to + state which shape it expects before iterating. + """ + return value if isinstance(value, list) else [] + + +def _json_dict(value: flext_t.JsonValue) -> dict[str, flext_t.JsonValue]: + """Return `value` as a mapping, or an empty mapping when it is anything else.""" + return value if isinstance(value, dict) else {} + + +def _run_make(*args: str) -> subprocess.CompletedProcess[str]: + env = os.environ.copy() + for name in ("MAKEFLAGS", "MFLAGS", "MAKELEVEL"): + env.pop(name, None) + return subprocess.run( + ["make", *args], cwd=ROOT, check=False, capture_output=True, text=True, env=env + ) + + +def test_help_lists_flext_public_verbs() -> None: + result = _run_make("help", "WHAT=usage") + combined = result.stdout + result.stderr + + assert result.returncode == 0, combined + assert "work WHAT=start|status|land|finish" in result.stdout + + assert "golden" in result.stdout + + +def test_custom_mutations_require_apply() -> None: + # The public verbs are the contract a caller can invoke; the internal + # `_serialized_*` targets are a generator implementation detail and were + # removed when flext-infra dropped the `serialize-make` CLI route. + commands = [ + ("fmt", "WHAT=apply", "APPLY=N"), + ("fix", "WHAT=apply", "APPLY=N"), + ("gen", "WHAT=agent-pointers", "APPLY=N"), + ] + + for command in commands: + result = _run_make(*command) + combined = result.stdout + result.stderr + assert result.returncode != 0, ( + f"{command}: mutation ran without APPLY=Y\n{combined}" + ) + assert "requires APPLY=Y" in combined, ( + f"{command}: missing APPLY gate\n{combined}" + ) + + +@pytest.mark.slow +@pytest.mark.timeout(900) +def test_invalid_nested_choices_fail_before_dry_run_gates() -> None: + commands = [ + ["make", "build", "WHAT=codegen-__invalid__"], + ["make", "check", "WHAT=fix-__invalid__"], + ["make", "check", "WHAT=dev-__invalid__"], + ["make", "release", "WHAT=__invalid__"], + ["make", "work", "WHAT=pr-__invalid__"], + ["make", "work", "WHAT=sub-__invalid__"], + ["make", "clean", "WHAT=__invalid__"], + ] + + for command in commands: + result = subprocess.run( + command, cwd=ROOT, check=False, capture_output=True, text=True + ) + combined = result.stdout + result.stderr + assert result.returncode != 0, ( + f"{' '.join(command)}: expected failure, got {result.returncode}\n{combined}" + ) + assert "ERROR:" in combined or "unsupported" in combined, ( + f"{' '.join(command)}: missing flext error marker" + ) + + +def test_generated_gitignore_keeps_declared_project_exceptions() -> None: + """Regeneration must not drop the project's own ignore rules. + + `.gitignore` is a generated projection, so the project's rules live in the + `extra_ignored_patterns` overlay of config/workspace.yaml (the mro-jnm1.3 + seam). Both sides are read from their real files here: if the overlay ever + stops reaching the rendered artifact, the barrier that keeps machine + config and tool output out of version control silently disappears. + """ + loaded = cli.read_yaml_file(ROOT / "config" / "workspace.yaml").unwrap() + assert isinstance(loaded, dict), "workspace.yaml must parse to a mapping" + manifest: dict[str, flext_t.JsonValue] = loaded + declared: list[str] = [ + str(pattern) + for overlay in _json_list(manifest.get("repository_policy_overlays")) + for pattern in _json_list(_json_dict(overlay).get("extra_ignored_patterns")) + ] + + assert bool(declared), ( + "config/workspace.yaml declares no extra_ignored_patterns, so the " + "project's ignore rules are not owned by the generator input" + ) + + rendered = { + line.strip() + for line in (ROOT / ".gitignore").read_text().splitlines() + if line.strip() and not line.lstrip().startswith("#") + } + missing = [pattern for pattern in declared if pattern not in rendered] + assert not missing, ( + "declared ignore patterns absent from the generated .gitignore:\n" + + "\n".join(missing) + ) + + +def test_git_hooks_have_exactly_one_owner() -> None: + """Only pre-commit may write the installed hook shims. + + codegen delegates installation to `pre-commit install` precisely so a + second, divergent shim never exists. A project recipe that copies its own + scripts over .git/hooks makes hook behaviour depend on whichever ran last: + the framework shim honours .pre-commit-config.yaml, the copied script does + not, so the same commit is gated differently from one checkout to the next. + """ + hooks_dir = subprocess.run( + ["git", "rev-parse", "--git-path", "hooks"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + hooks_path = (ROOT / hooks_dir).resolve() + + foreign: list[str] = [] + for stage in ("pre-commit", "pre-push"): + shim = hooks_path / stage + if not shim.exists(): + continue + head = shim.read_text(errors="replace")[:400] + if "generated by pre-commit" not in head.lower(): + lines = head.splitlines() + foreign.append(f"{stage}: {lines[1] if len(lines) > 1 else head!r}") + + assert not foreign, ( + "installed git hooks are not owned by pre-commit:\n" + "\n".join(foreign) + ) + + +def test_generated_hook_entries_are_executable_argv() -> None: + """Every generated hook entry must name a real program first. + + pre-commit runs `entry` as an argv vector, never through a shell, so a + leading VAR=value is looked up as the program name and the hook dies with + "Executable VAR=value not found". A variable therefore has to arrive via + `env` or inside an explicit shell. + """ + loaded = cli.read_yaml_file(ROOT / ".pre-commit-config.yaml").unwrap() + assert isinstance(loaded, dict), ".pre-commit-config.yaml must parse to a mapping" + config: dict[str, flext_t.JsonValue] = loaded + entries = [ + (str(_json_dict(hook)["id"]), " ".join(str(_json_dict(hook)["entry"]).split())) + for repo in _json_list(config.get("repos")) + for hook in _json_list(_json_dict(repo).get("hooks")) + if _json_dict(hook).get("entry") + ] + + assert bool(entries), ".pre-commit-config.yaml declares no hook entries" + broken = [ + f"{hook_id}: {entry}" + for hook_id, entry in entries + if "=" in entry.split(maxsplit=1)[0] + ] + assert not broken, ( + "hook entries whose first argv token is a variable assignment:\n" + + "\n".join(broken) + ) + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/scripts/lib/tests/test_mcb_sh.py b/tests/python/scripts_lib/test_mcb_sh.py similarity index 98% rename from scripts/lib/tests/test_mcb_sh.py rename to tests/python/scripts_lib/test_mcb_sh.py index d53e25b6a..218ab6ab8 100644 --- a/scripts/lib/tests/test_mcb_sh.py +++ b/tests/python/scripts_lib/test_mcb_sh.py @@ -14,7 +14,6 @@ import pytest -from ._utilities.matchers import tm ROOT = Path(__file__).resolve().parents[3] MCB_SH = ROOT / "scripts" / "lib" / "mcb.sh" @@ -48,7 +47,7 @@ def test_mcb_bin_prefers_workspace_binary_over_path_binary(temp_dir: Path) -> No text=True, ) - tm.that(result.returncode == 0, result.stderr) + assert result.returncode == 0, result.stderr assert result.stdout.strip() == str(target_bin) diff --git a/uv.lock b/uv.lock index 47297483d..13318386d 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = "==3.13.*" [options] -exclude-newer = "2026-08-02T23:38:58.153627871Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P7D" [[package]] @@ -14,14 +14,14 @@ sdist = { url = "https://files.pythonhosted.org/packages/6f/0b/3f29683dfbe94208f [[package]] name = "aiofile" -version = "3.11.1" +version = "3.12.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "caio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/41/2fea7e193e061ce54eacc3b7bc0e6a99e4fcff43c78cf0a76dd781ed8334/aiofile-3.11.1.tar.gz", hash = "sha256:1f91912c6643d2a4e49ca4ae3514f0bf3867ce948a36d99a6411b8f4755f4cf9", size = 19342, upload-time = "2026-05-16T08:18:33.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/31/edb06aabd8f8f0b56d659f30800795f40b93cba96be946ce179f6931e3a5/aiofile-3.12.3.tar.gz", hash = "sha256:caa6aa746b5e47e2165f7abd741b6415e49cf4d44fddc0f61844612cc3924d41", size = 21600, upload-time = "2026-08-04T22:59:27.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/cd/0d76dfc5de72bde52f55f53e925c7d152d9c7906634ec1e0cbc7e8d4ad93/aiofile-3.11.1-py3-none-any.whl", hash = "sha256:ce77d14ac07f77bc2b757834a5c129321f3f705c474593deed5ab209079a52c9", size = 20446, upload-time = "2026-05-16T08:18:32.051Z" }, + { url = "https://files.pythonhosted.org/packages/4e/79/6e45e778c4c3cab39e0937b007b720c15f76c50c6453d153282d0fcc3588/aiofile-3.12.3-py3-none-any.whl", hash = "sha256:5c1bcc9e929c50834608e8cc1a4cc1d7503eb60c15a535b779fd39e2f372c017", size = 22122, upload-time = "2026-08-04T22:59:25.838Z" }, ] [[package]] @@ -218,15 +218,17 @@ wheels = [ [[package]] name = "caio" -version = "0.9.25" +version = "0.12.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/c8/82b3c760141a1076408164b03e8789b51809add6aecd48aa9d7651cf6b59/caio-0.12.2.tar.gz", hash = "sha256:87a67c0dccc60e432888bd532ec504b66e124a5d8b391aab894583b55abd39ea", size = 80927, upload-time = "2026-08-04T14:43:33.726Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, - { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, - { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, - { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, - { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9b/31f0b49a2542ffa2f9d6140267e2b568e722a1feeb05cfbffea97666c62b/caio-0.12.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:40ebea9ebe3a3a66ae85fa00d4112d163654a33c82dcf9b26a99f7d30de13317", size = 84656, upload-time = "2026-08-04T14:43:10.513Z" }, + { url = "https://files.pythonhosted.org/packages/99/bc/62568d688af9712a34fe3f958d7a98c53bb2017e263260cd5deae67a90e9/caio-0.12.2-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:6003ec389a68d5ec8f089df82b2dc8915293dd630a4d11322d7e3455045981fd", size = 198443, upload-time = "2026-08-04T14:43:11.767Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e4/5ed627860285612e5307f06c109913c5918c947fbc223b55599e484c64b0/caio-0.12.2-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:eee9376d0e2af25b6defc5bce39f6efa90521c803aaf12eba931bd898a397cfc", size = 196356, upload-time = "2026-08-04T14:43:13.206Z" }, + { url = "https://files.pythonhosted.org/packages/81/e2/2a8cfc6ba3ef3f19e7c778e9fb6f98600f0971cca78bbdfc23a413a66349/caio-0.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:78e3ccafc98e009fcb00a97ad441585551e52c0ae7ecc50427a3ccd9b11502fd", size = 195893, upload-time = "2026-08-04T14:43:14.649Z" }, + { url = "https://files.pythonhosted.org/packages/d1/87/77c40fb2301d0b5bb27c2e79ae42fce718ed75396d5fe3e1c09d8e1400b1/caio-0.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f2355db8917f5a0f3638bf332fe0d87549c80e978fca01db84a8a14b9df56a05", size = 195969, upload-time = "2026-08-04T14:43:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b5/0ceca97eb546fe6bbace3399c8b11dfc503efcc7509d708a7a3f09ab50e9/caio-0.12.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8054cba5e7ee623bea34946e2b59eb7c7c2be8872d0a5d12215d6ff564938d5f", size = 78621, upload-time = "2026-08-04T14:43:17.316Z" }, + { url = "https://files.pythonhosted.org/packages/61/8a/71b0144f783468ba9f1bbf8a2f8e45c7d85ae31ec192f10650aa46f31702/caio-0.12.2-py3-none-any.whl", hash = "sha256:5233e797c9fe2b541914b1bc2e2df82677e2206b537e44e252188f3c2cbb0ea9", size = 62548, upload-time = "2026-08-04T14:43:32.394Z" }, ] [[package]] @@ -240,27 +242,27 @@ wheels = [ [[package]] name = "cffi" -version = "2.1.0" +version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, - { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, - { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, - { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, - { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, - { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, - { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, - { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, - { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, - { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, ] [[package]] @@ -468,43 +470,43 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cufft = [ - { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cufile = [ - { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] curand = [ - { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cusolver = [ - { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] [[package]] @@ -518,7 +520,7 @@ wheels = [ [[package]] name = "cyclopts" -version = "4.22.4" +version = "4.22.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -526,9 +528,9 @@ dependencies = [ { name = "rich" }, { name = "rich-rst" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/8b/fa4bfca58481ff7ef3d48ba706ccd4a7eaa1e27e7b1d9e10cbb3ae0f780f/cyclopts-4.22.4.tar.gz", hash = "sha256:d48c17e8d4a334b3f33b82920afabbf52c877a2e21edd55a172433c288bc7720", size = 194636, upload-time = "2026-08-02T14:04:17.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/05/689617b7e86503417c172f577d791524cb13b9697303d5d44409a971ba10/cyclopts-4.22.5.tar.gz", hash = "sha256:94044506317462cad90fb01a917dadce1f48a0915ba3605dc8d178dea1229e24", size = 195144, upload-time = "2026-08-04T13:53:00.303Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/49/f50d1ebb472902c952835d6141bfe920a16c402660a25c813151a861ca4c/cyclopts-4.22.4-py3-none-any.whl", hash = "sha256:90debd2468c5b33d7ca55ca64209a3bdf2667c11a2f75d973584198b58ca5e46", size = 234023, upload-time = "2026-08-02T14:04:16.282Z" }, + { url = "https://files.pythonhosted.org/packages/83/58/bcab9c33fb7a25a1f5970f357c5b19729bc81d50615d2f737b20c4255909/cyclopts-4.22.5-py3-none-any.whl", hash = "sha256:cf9ce285836053d156730ea4ea0ad0c75cf63beb3f3d8edf222a795bc57666ab", size = 234557, upload-time = "2026-08-04T13:52:58.509Z" }, ] [[package]] @@ -692,19 +694,19 @@ wheels = [ [[package]] name = "fastmcp" -version = "3.4.5" +version = "3.4.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastmcp-slim", extra = ["client", "server"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/14/c1ffb91b7d1fece86c81e1f9df5474f30fd97e4cdaa398814bbbeee88568/fastmcp-3.4.5.tar.gz", hash = "sha256:a95f2bc876bef42e8b50f7872f24f3f2fe3b1d37408c734e8b9d9e03014b72d3", size = 28800521, upload-time = "2026-07-27T19:20:01.231Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/e2c78e26233cd8a416b21513e1925435d54c008a0ec467dbdaa80369daf7/fastmcp-3.4.6.tar.gz", hash = "sha256:2287938da8364ad7071bec2d2393af6ae10fd4e836f06f506569f1456cc87eb4", size = 28808130, upload-time = "2026-08-05T14:54:42.177Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/4f/73450a436c963c0382d15a882fc5d08f15aadc329194df1b54495a7c8383/fastmcp-3.4.5-py3-none-any.whl", hash = "sha256:5d3d438eb2917e63e6faf53e8cb8fe26d887ec3232f848093a4eecad7fa34861", size = 8017, upload-time = "2026-07-27T19:19:57.942Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/c02275db111892388972edbb05fbcbfdf1e83cbd1fd03356b3a49b93f839/fastmcp-3.4.6-py3-none-any.whl", hash = "sha256:2a29967be9f68cdd1b4cefb413ede74f83adefd923919a37aaac3611eccdd749", size = 8017, upload-time = "2026-08-05T14:54:38.473Z" }, ] [[package]] name = "fastmcp-slim" -version = "3.4.5" +version = "3.4.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "platformdirs" }, @@ -714,9 +716,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/1d/f3e271fbcd01ce01a4cf623b336d8e1305c192aa5d5e8e0223b7167462e9/fastmcp_slim-3.4.5.tar.gz", hash = "sha256:5badc3bceee61f61297eeb9494f499325f3ce1cafabf4611b31f6c3e9d7dff59", size = 591622, upload-time = "2026-07-27T19:15:19.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/b6/b5b9e81e67a3f39534881d2af6aa3fbd2dc367eaa070c3c932770a0c062f/fastmcp_slim-3.4.6.tar.gz", hash = "sha256:6a1e6e42c697ba90abcb1be617a26947d07316f13c6fa138ae7b0de24558e32c", size = 594167, upload-time = "2026-08-05T14:54:15.924Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/3b/16d8aa8224094519f30b078138e725b8a731bf0a13f1f850e58b5f9b3cc4/fastmcp_slim-3.4.5-py3-none-any.whl", hash = "sha256:bc31217827c4999812543c83ee95ed9a47f3ed1e3fd0bd4f64371e375b748eca", size = 766478, upload-time = "2026-07-27T19:15:18.015Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/02c254c46b4323ae5262b76a29af73b50a863efc5739a3454d144d3605ee/fastmcp_slim-3.4.6-py3-none-any.whl", hash = "sha256:3e08e6acb03523a47aa17f4d2ab9943e648a41e20b24084da491a732c46dffdc", size = 769174, upload-time = "2026-08-05T14:54:14.663Z" }, ] [package.optional-dependencies] @@ -765,7 +767,7 @@ wheels = [ [[package]] name = "flext-cli" version = "0.12.0rc0" -source = { git = "https://github.com/flext-sh/flext-cli.git?rev=0.12.0-dev#085a37867b24bfb43b7d9da2f703fe8521433b55" } +source = { git = "https://github.com/flext-sh/flext-cli.git?rev=0.12.0-dev#d8422f0f3eae8789b4179162a9677c8cd4991f2c" } dependencies = [ { name = "cachetools" }, { name = "click" }, @@ -791,8 +793,8 @@ dependencies = [ [[package]] name = "flext-core" -version = "0.12.0" -source = { git = "https://github.com/flext-sh/flext-core.git?rev=0.12.0-dev#f442a729b595565f369432f5c5ca1cd095b920a8" } +version = "0.12.0rc1" +source = { git = "https://github.com/flext-sh/flext-core.git?rev=0.12.0-dev#4e3ceb15f0db43944c83f5587daac18102ec4359" } dependencies = [ { name = "beartype" }, { name = "cachetools" }, @@ -818,7 +820,7 @@ dependencies = [ [[package]] name = "flext-infra" version = "0.12.0rc0" -source = { git = "https://github.com/flext-sh/flext-infra.git?rev=0.12.0-dev#b76af238f3b425d55daecdd7ac9e0f9311e5dfae" } +source = { git = "https://github.com/flext-sh/flext-infra.git?rev=0.12.0-dev#26c848a8854ea3afa05a98deff05edc534c0b7ac" } dependencies = [ { name = "defusedxml" }, { name = "filelock" }, @@ -840,7 +842,7 @@ dependencies = [ [[package]] name = "flext-tests" version = "0.12.0rc0" -source = { git = "https://github.com/flext-sh/flext-tests.git?rev=0.12.0-dev#bbb1a60495fa6ed714feef707eed5b90ecc379ef" } +source = { git = "https://github.com/flext-sh/flext-tests.git?rev=0.12.0-dev#7e84ad5ce617d8b39dc269a12fbfca5db073f623" } dependencies = [ { name = "docker" }, { name = "factory-boy" }, @@ -958,14 +960,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.57" +version = "3.1.58" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/0d/132ed135c871b6bf91adf16a0e43797cd535b81d4973b5d09291c54fc5ee/gitpython-3.1.57.tar.gz", hash = "sha256:c493ec57c0ef6b19743798b6a5af859c71814b524e7e6f97baa2f8e658961488", size = 225898, upload-time = "2026-07-26T07:33:26.351Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/d6/5f358ff283325580c2003a6d953aea18cfe10ae87b46f5ebc80fa3a386dc/gitpython-3.1.58.tar.gz", hash = "sha256:621416df10ef3fd0e19fabf9172ddeed0fa704d353d04f194eec56a625a95b22", size = 228498, upload-time = "2026-08-04T15:05:49.47Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/6e/2139de986d9c7c3ac86f1f8be43858ce90bdfe2f7175e6c80c650ba15242/gitpython-3.1.57-py3-none-any.whl", hash = "sha256:4ccf7d73c10f5c9e76043fbb2675ac5a1b3ff5b41e648f56bcbed5f63792ecaf", size = 217151, upload-time = "2026-07-26T07:33:24.838Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/9d8752098bc442f0726e64aa6135940b3a96809915d1aa4206c1bb97881d/gitpython-3.1.58-py3-none-any.whl", hash = "sha256:d331e722577f0fd7fc1f857419b3ecc07af66282b933d2a4d95f84a042fdd50f", size = 220183, upload-time = "2026-08-04T15:05:48.025Z" }, ] [[package]] @@ -988,18 +990,18 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.5.2" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/39/67be8d71f900d9a55761b6022821d6679fb56c64f1b6063d5af2c2606727/hf_xet-1.5.2.tar.gz", hash = "sha256:73044bd31bae33c984af832d19c752a0dffb67518fee9ddbd91d616e1101cf47", size = 903674, upload-time = "2026-07-16T17:29:56.833Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/ba/2b70603c7552db82baeb2623e2336898304a17328845151be4fe1f48d420/hf_xet-1.5.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f922b8f5fb84f1dd3d7ab7a1316354a1bca9b1c73ecfc19c76e51a2a49d29799", size = 4033760, upload-time = "2026-07-16T17:29:43.884Z" }, - { url = "https://files.pythonhosted.org/packages/60/ac/b097a86a1e4a6098f3a79382643ab09d5733d87ccc864877ad1e12b49b70/hf_xet-1.5.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:045f84440c55cdeb659cf1a1dd48c77bcd0d2e93632e2fea8f2c3bdee79f38ed", size = 3841438, upload-time = "2026-07-16T17:29:45.539Z" }, - { url = "https://files.pythonhosted.org/packages/d3/35/db860aa3a0780660324a506ad4b3d322ddc6ecbba4b9340aed0942cbf21c/hf_xet-1.5.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db78c39c83d6279daddc98e2238f373ab8980685556d42472b4ec51abcf03e8c", size = 4428006, upload-time = "2026-07-16T17:29:46.996Z" }, - { url = "https://files.pythonhosted.org/packages/af/6b/832dd980af4b0c3ae0660e309285f2ffcdff2faa38129390dbb47aa4a3f9/hf_xet-1.5.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7db73c810500c54c6760be8c39d4b2e476974de85424c50063efc22fdda13025", size = 4221099, upload-time = "2026-07-16T17:29:48.525Z" }, - { url = "https://files.pythonhosted.org/packages/9e/05/ae50f0d34e3254e6c3e208beb2519f6b8673016fc4b3643badaf6450d186/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6395cfe3c9cbead4f16b31808b0e67eac428b66c656f856e99636adaddea878f", size = 4420766, upload-time = "2026-07-16T17:29:50.092Z" }, - { url = "https://files.pythonhosted.org/packages/07/a9/c050bc2743a2bcd68928bfee157b08681667a164a24ec95fbfcfcd717e08/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cde8cd167126bb6109b2ceb19b844433a4988643e8f3e01dd9dd0e4a34535097", size = 4636716, upload-time = "2026-07-16T17:29:51.62Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f8/68b01c5c2edb56ac9a67b3d076ffddcb90867abaee923923eb34e7a14e76/hf_xet-1.5.2-cp38-abi3-win_amd64.whl", hash = "sha256:ecf63d1cb69a9a7319910f8f83fcf9b46e7a32dfcf4b8f8eeddb55f647306e65", size = 3988373, upload-time = "2026-07-16T17:29:53.395Z" }, - { url = "https://files.pythonhosted.org/packages/39/c6/988383e9dc17294d536fcbcd6fd16eed882e411ad16c954984a53e47b09c/hf_xet-1.5.2-cp38-abi3-win_arm64.whl", hash = "sha256:1da28519496eb7c8094c11e4d25509b4a468457a0302d58136099db2fd9a671d", size = 3816957, upload-time = "2026-07-16T17:29:54.991Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" }, + { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" }, + { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" }, ] [[package]] @@ -1073,33 +1075,33 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.165.0" +version = "6.165.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/6f/324d76c305075036519f63f986e81e8c3f7d0a3510eb88510bc407967ab0/hypothesis-6.165.0.tar.gz", hash = "sha256:b54e86cb1d7d049eb934e7843f179889252263b24290ebd38549466c9c9351d9", size = 493017, upload-time = "2026-08-02T00:20:26.755Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/f2/c85859811281475dc3dddd628979be6683ea5012494316250d29a6d9d72a/hypothesis-6.165.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dda3707dfdfdc1397b7489ecb81534f47f22f98d468c51c92d591d6967c58b29", size = 772599, upload-time = "2026-08-02T00:19:34.099Z" }, - { url = "https://files.pythonhosted.org/packages/a0/05/a28b2ad55773b36812a1ad6f88d55b051ef324c606efa880c78ccceefbd6/hypothesis-6.165.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a384468744915435a25a9f2dcd0a74e9f78f8d56cb51f5ea1c1c4973bd40bd5b", size = 768129, upload-time = "2026-08-02T00:19:38.326Z" }, - { url = "https://files.pythonhosted.org/packages/58/ab/3567e7f5aa16319ee9e0fb014e588149e4ef6080028fc07f2748deed9f0d/hypothesis-6.165.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d65562b9866a5a4be4c731bd4018152861997675b92ed6d0f29e925999928f9a", size = 1097411, upload-time = "2026-08-02T00:18:58.073Z" }, - { url = "https://files.pythonhosted.org/packages/bd/5d/312c3b3831183668beff6974a290390f2327846e3bd0baa44f116eecc1bc/hypothesis-6.165.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b402560b0463e643170d904a5987f61bc388718f7b48d4521b7a5b22e6a27019", size = 1125993, upload-time = "2026-08-02T00:19:36.967Z" }, - { url = "https://files.pythonhosted.org/packages/bd/39/f47f529c41853d56c0ceac5dcb69a6ffae43d1367877575b2b57c9c60fb3/hypothesis-6.165.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1e134b573c18fb7e79f915ccad3585f81c0b92ff7cf89f6c0b0d966a5ac5156", size = 1146830, upload-time = "2026-08-02T00:19:07.51Z" }, - { url = "https://files.pythonhosted.org/packages/fd/9f/4d613a35b834c602499746c0d8036aa5c0a9b2c7a23f294292a132a352db/hypothesis-6.165.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:22d3b846796593117a1227b6d9e8c0c1269ce6327fa6345fb50f24a038e1a0c1", size = 1102233, upload-time = "2026-08-02T00:20:25.053Z" }, - { url = "https://files.pythonhosted.org/packages/7f/69/f11136ed8feac210f247dcad53c9a5cbcf067ae42d4d51c9ba61640aafc1/hypothesis-6.165.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:47d419899477ed35704d4f90f82aadbaa50080d7d0ff33db6727bbc4b1c3956d", size = 1138969, upload-time = "2026-08-02T00:19:32.587Z" }, - { url = "https://files.pythonhosted.org/packages/b2/ad/dbd5e97bf214993033ddb93f199d99410be33d7f997996c8dc2f6255edfc/hypothesis-6.165.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:71721d9de23b60f5436a1b8b8fddccd6df606efbb406f36254195089974518ee", size = 1271210, upload-time = "2026-08-02T00:20:16.318Z" }, - { url = "https://files.pythonhosted.org/packages/9f/f1/db4ecb5f01cebc19ccfe8b4b170fa211f2cfd22946ca8c9f4007eba59140/hypothesis-6.165.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:584734f6380f1965498bff2f72af307c182b3398a9c4e1b2f31e43dd1aa9c922", size = 1399053, upload-time = "2026-08-02T00:19:55.646Z" }, - { url = "https://files.pythonhosted.org/packages/68/15/205570c5f3935e074b93c7b6bda87889039d59ba39426c70e7c93bfdc939/hypothesis-6.165.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:61d99c493f759809e64dbe544a47948537460c9f03f916d66291bb63965fc91a", size = 1271824, upload-time = "2026-08-02T00:19:52.519Z" }, - { url = "https://files.pythonhosted.org/packages/fb/5e/d77c9cf07bd4a7b2c386cc7f8eba6c614d62723e830d0366ca2381f35cb1/hypothesis-6.165.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:68629657d49b7dba3f061d0224441df068c0c03e7a17efc278782f637d810db7", size = 1313877, upload-time = "2026-08-02T00:19:13.765Z" }, - { url = "https://files.pythonhosted.org/packages/38/46/6e7f945c465b94951bb91bbc50cb1dc847a749d32e1e5efd6e47a5a7747b/hypothesis-6.165.0-cp310-abi3-win32.whl", hash = "sha256:90633ec15635385723d29a1e9b6fe8f63e17b33641353c6b58fc7d277672ef29", size = 658400, upload-time = "2026-08-02T00:19:31.133Z" }, - { url = "https://files.pythonhosted.org/packages/06/c5/81519854c72e1f57ec44df6484f6a34f1789f1fc4c0b8647d2fd1e28d25c/hypothesis-6.165.0-cp310-abi3-win_amd64.whl", hash = "sha256:bab53228c59978c74c87dba27db4656feaf991f20db5242ea263531477227247", size = 664538, upload-time = "2026-08-02T00:20:02.436Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d4/3418ba26110b6c29e866bf1baa5b2d447827d00811e181be12ce08aebb9c/hypothesis-6.165.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:82385ff61d309b77097580e7200dc3e038f5eb78d3fe1a4aa0d110e5f1afa9df", size = 774071, upload-time = "2026-08-02T00:19:23.829Z" }, - { url = "https://files.pythonhosted.org/packages/aa/f4/d89ab31f52ecbcc8328d25add616b9d530a5f76003ae4ac5a9bfc7080db5/hypothesis-6.165.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2e7b277f56786ec0730cd3e428ea3611b59e3ecae3382553f3fea958e8c2c25f", size = 765718, upload-time = "2026-08-02T00:19:35.595Z" }, - { url = "https://files.pythonhosted.org/packages/06/13/10a32a1e3ba78c3b66281aa1e2764269765ad058fb977d6b62dc6679e63a/hypothesis-6.165.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73d3d4e57a00122a2657ae400b8d066fd0cfa1621c6aa3a6b38dd4b4c268035", size = 1096108, upload-time = "2026-08-02T00:20:09.316Z" }, - { url = "https://files.pythonhosted.org/packages/ef/87/f16688e373b982f6fb3bf0edd4aa42eb731e8a758582052ec1bfe7c93b30/hypothesis-6.165.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89b41b1daa93c9e544cdad636ec87d4a45169d1922c6aff80fa282cd1edeeb74", size = 1146051, upload-time = "2026-08-02T00:19:49.262Z" }, - { url = "https://files.pythonhosted.org/packages/af/2f/a560867b9be5c908e379c4a308a7e44b8082e18faf4096b5a0708497ba30/hypothesis-6.165.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b2fef387ec63e60c85db6c4fc95b07a6b7f35c971381997c862d309af9e1e4b", size = 1269056, upload-time = "2026-08-02T00:20:21.588Z" }, - { url = "https://files.pythonhosted.org/packages/0b/60/35d03cf44a7803622fa39121e8c674253175890a9b081ae0b8137a1d32ef/hypothesis-6.165.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c681bdf0036ba7923b7c4225937a48e12db7029a286b51ddc03d9b08afe2f040", size = 1312946, upload-time = "2026-08-02T00:19:12.547Z" }, - { url = "https://files.pythonhosted.org/packages/30/a9/1b587430b9ec6576fdafa102657d70d456b5694db45ee21fba88aa0c8ea1/hypothesis-6.165.0-cp313-cp313-win_amd64.whl", hash = "sha256:29edc5a08334985cea55afe6ab564c147e4b804179265eb303c9ed8e494c211b", size = 661683, upload-time = "2026-08-02T00:19:29.812Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ea/73/fc3743243603dc49911a1ec073a3a524ea8e1c7d48218d3c2a3faa9a8709/hypothesis-6.165.2.tar.gz", hash = "sha256:680a1adf523ac792b46064f425b112ce6c08a7a8f50e65d08e029de6aa11df95", size = 502277, upload-time = "2026-08-05T21:32:43.713Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/63/46c9908fe7bd5ffa5002fa88fe289dfe6d3cea3fad1ab8942fe11f1c8a2b/hypothesis-6.165.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:33a7303566e660664f3f02ea1df85f7b966cd6723165c696996cfd8630913b3a", size = 781704, upload-time = "2026-08-05T21:32:03.548Z" }, + { url = "https://files.pythonhosted.org/packages/c5/7f/fdce62542a514f6b33c4fc0a760e6d17bb57602b28cb079b78e55b8ea32d/hypothesis-6.165.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:06d8fe4c82a935f67e610c99848360f5caeb04f547c7d7a830a5c74fb96f053a", size = 777243, upload-time = "2026-08-05T21:32:17.546Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c8/39cd922bf3e1ec84977b768d4e8be31ae051e3a6b400b4fdd7ebcddb1eed/hypothesis-6.165.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de2e3f6a6f75c876be481138c6c0802ebe10deef9f13ce1cdd6e0ed21d8e1e28", size = 1106492, upload-time = "2026-08-05T21:31:47.844Z" }, + { url = "https://files.pythonhosted.org/packages/0f/91/7bb502379a8dcc43f2538530c05cf16fd4e386afa587d65cc289484425cf/hypothesis-6.165.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:21668cb5a8a694d45ff4c43f20a8fc577a47467b5102c680a43638b027136368", size = 1135105, upload-time = "2026-08-05T21:31:49.453Z" }, + { url = "https://files.pythonhosted.org/packages/e5/04/4ce8ae1bf78d09d7543ab7037a3beedc7f3d963c7aa04e82842415284689/hypothesis-6.165.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eea4ab5cfdd6c6a23a60777559ea06c34868234fff6542ff6125c0250429348e", size = 1156034, upload-time = "2026-08-05T21:31:44.687Z" }, + { url = "https://files.pythonhosted.org/packages/49/de/b074d899f4a04fa8b99a5bbd66f209c1c02bc21f9f87404539b28b99aff0/hypothesis-6.165.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:0a6add02d9b3b73b59f4d69f5b15abbc07113cd335cc140815cec2877e6b496c", size = 1111344, upload-time = "2026-08-05T21:32:27.211Z" }, + { url = "https://files.pythonhosted.org/packages/e8/38/5a8514683a181f82a8ad9f6d084b704fea7a97c9814033939fc493b55fca/hypothesis-6.165.2-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11013896b6a2ed497079558cb9f89e8b3b564f8859782b38f72cfc1dbeca66ad", size = 1148115, upload-time = "2026-08-05T21:31:01.009Z" }, + { url = "https://files.pythonhosted.org/packages/88/c7/08cf7930d8bec7f1df971c948af2ccb5a402d5b8b19b306af655a603b180/hypothesis-6.165.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4ceabc69a95e761f381663c6452537fde12a2a6e0275e095b6822c0e0f3b1364", size = 1280321, upload-time = "2026-08-05T21:31:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/c5/91/c9ebb7da3b6e06c47aecceb959cf7df6af75025663af543373c5692f97ac/hypothesis-6.165.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:11e1ce261765ffa6acbaf540358426519ca4a5e46b825cf39b429c77c1895689", size = 1408134, upload-time = "2026-08-05T21:31:27.383Z" }, + { url = "https://files.pythonhosted.org/packages/ed/24/13c2fd9f253ba3a92d4aaa61ae45a8b130df57ab5bd6fc481e2aae520e36/hypothesis-6.165.2-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:b0250099b2e55d72872319918aacc501110a182938a3d56bcae4a999bee5db08", size = 1280884, upload-time = "2026-08-05T21:32:00.188Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fa/2820bdbe0660394544b9e120b03ea7020702d7fa5c76236166de6ababc9d/hypothesis-6.165.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:96d02928d1a0b7d59e39fd8ada75f0b7d0377ff29f91c94109f92fc2dab9af74", size = 1322998, upload-time = "2026-08-05T21:31:24.373Z" }, + { url = "https://files.pythonhosted.org/packages/f0/24/38752794eb821f5c77da08523602003c344f3498fce09f20741cd5b5e29c/hypothesis-6.165.2-cp310-abi3-win32.whl", hash = "sha256:0f2044093c8244d73893e755a7fa53154b7eca57b37e1427d9b0d9948f6c2b3e", size = 667506, upload-time = "2026-08-05T21:32:10.547Z" }, + { url = "https://files.pythonhosted.org/packages/5f/71/b28f714a127017750e450d152aa4fbff51bd144c6840090d28b529b408d3/hypothesis-6.165.2-cp310-abi3-win_amd64.whl", hash = "sha256:2aa30716066e5ee7750e56b8f90cefaf4ed28c12b4b1d66cef40014fd3f95196", size = 673650, upload-time = "2026-08-05T21:31:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/22/01/add18f19d5e5f084a59709f0dcebf3cb1edeca475ce8a31573dc33891e66/hypothesis-6.165.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e2bf15d05264ec9da8d55c3843902f906ced5e84fb3da924eafe165697ab4638", size = 783183, upload-time = "2026-08-05T21:31:55.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/4b/df2e4c24d208518a6a3dab7acabad7f5ec6c5bb0f4d0bf1701d2fb7206ce/hypothesis-6.165.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3748153d4f64d347f8c988dd41fbef3513b66819e73e9209b1c501bf0d716a13", size = 774829, upload-time = "2026-08-05T21:32:01.891Z" }, + { url = "https://files.pythonhosted.org/packages/72/a0/b75a001efbde704ff2188924a4d4bb3cdf7a22924dc51c155115af64858c/hypothesis-6.165.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f689976e0eb578afbe8ce37669cb637f00f7c545ad303412947ee4abe2f29ce6", size = 1105224, upload-time = "2026-08-05T21:32:35.189Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/4942f510c6441b5d60dc7f0d8d2af74fe444b62d3af565bdf42d9b6b803d/hypothesis-6.165.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c68a5684b2e2ad3c33500198a6073b3d04493fd7b1ef34937645ad092b797d", size = 1155166, upload-time = "2026-08-05T21:31:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/405ebff50c6949518d34f487017412d5b8f8ee9239e50ecdfdd99a745f4b/hypothesis-6.165.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b40db922ccb53fb77c68d944748a3eb5b945402967b64093d9ba970d028cf1af", size = 1278170, upload-time = "2026-08-05T21:31:43.116Z" }, + { url = "https://files.pythonhosted.org/packages/73/ff/93ad0f4b55100876604d2c316a8c6e0ee037cb0da925caaa478709e504b0/hypothesis-6.165.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d2fd48ec969b2dbe1c8e25dc86d199c6820b9222846469449b997f5546383378", size = 1322061, upload-time = "2026-08-05T21:32:07.217Z" }, + { url = "https://files.pythonhosted.org/packages/14/37/14b655c664a957e44c7f59d9498e53d79b55f1b752a1bb38fee9da403e9c/hypothesis-6.165.2-cp313-cp313-win_amd64.whl", hash = "sha256:9cf13225121280036ea5a8ff8babb82ec27a4736aea669bbe0bc9839d254575f", size = 670824, upload-time = "2026-08-05T21:32:21.25Z" }, ] [[package]] @@ -1583,7 +1585,7 @@ wheels = [ [[package]] name = "mcb-scripts" version = "0.4.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "flext-cli" }, { name = "flext-core" }, @@ -1638,7 +1640,7 @@ provides-extras = ["gitops"] dev = [ { name = "actionlint-py", specifier = ">=1.7.12.24" }, { name = "bandit", specifier = ">=1.8" }, - { name = "code-review-graph", extras = ["all"], specifier = ">=2.3.6" }, + { name = "code-review-graph", extras = ["all"], specifier = ">=2.3.7" }, { name = "flext-tests", git = "https://github.com/flext-sh/flext-tests.git?rev=0.12.0-dev" }, { name = "mypy", specifier = ">=2.3.0" }, { name = "pre-commit", specifier = ">=4.6.1" }, @@ -2207,11 +2209,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.2" +version = "26.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] @@ -3124,17 +3126,17 @@ wheels = [ [[package]] name = "rumdl" -version = "0.2.49" +version = "0.2.52" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/db/1336cea5d924626458708da174e33020a388805c8f54ba474a1c3fdfc676/rumdl-0.2.49.tar.gz", hash = "sha256:1b1651d97324da323b9ccdaf920e00ba6c8dc8bc132a13d691c026130b3564fd", size = 3166994, upload-time = "2026-08-02T20:17:36.459Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8a/63fa80c811bb328786423cf4ea25fe1dc3c4d8abbab945c2654a12553217/rumdl-0.2.52.tar.gz", hash = "sha256:ac18662ccebec4cbbb7257136c1b2af362e27f6ca4d4acaca77021bae41406c5", size = 3226686, upload-time = "2026-08-05T19:50:07.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/b6/625bf1ab401fdc4d409617f3b3a4af5a71805c78d97c2d39d1ca39c7fc0b/rumdl-0.2.49-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eb54ff2a4281c1f3aaa8c892de46d317c2664398fe8edfc6dcbcc2c6d5ac0256", size = 6310404, upload-time = "2026-08-02T20:17:29.092Z" }, - { url = "https://files.pythonhosted.org/packages/6b/56/9cd5c0f687eaffcd4f8401cddf08609f912e29df0aec339afd89287418da/rumdl-0.2.49-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1193f8de495ba9d5bd20b8da7e134cd3a8d676b5231a0c59de11a9a143c47a35", size = 5959613, upload-time = "2026-08-02T20:17:22.877Z" }, - { url = "https://files.pythonhosted.org/packages/68/db/50d07a46944fa678f2663e4bd656a459061e6cc53955b703ee7027644b81/rumdl-0.2.49-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:86981d2939423203083af2fc3bf6e3edcb468c6ccbb06e33be01c5ab1af64672", size = 6102695, upload-time = "2026-08-02T20:17:25.866Z" }, - { url = "https://files.pythonhosted.org/packages/89/a8/7e8fb295c7564d47312b7205fb2fb666bace66c0ad91100fb1ee0f775f0c/rumdl-0.2.49-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:52003801f74911856470e0657137c976344a157cfe2ba4a04637f9ba23709add", size = 6494442, upload-time = "2026-08-02T20:17:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/d0/23/15e983f9e8c567b5accf7e71bedde588c10ba9ac8033942b0885a0e04438/rumdl-0.2.49-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:082c35fb3be7bcdecb2d82529e91799221c471bff0d97161bc591c3e65ffffe5", size = 6093408, upload-time = "2026-08-02T20:17:27.531Z" }, - { url = "https://files.pythonhosted.org/packages/90/2c/443a4eb88d32f53474d38db7c41b33ec882d9aa780d75bf753503b5b9a9a/rumdl-0.2.49-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4f4e2149c71a47868cdf7e49fcb2a7f1d261e3976b9437f775299e82b80d1792", size = 6476000, upload-time = "2026-08-02T20:17:34.812Z" }, - { url = "https://files.pythonhosted.org/packages/17/ae/13af4b1458354a8eeb99a69a9e232c0f3d5596363161ec894dfe6d286fd7/rumdl-0.2.49-py3-none-win_amd64.whl", hash = "sha256:ac40c210c4f13e9f06d9abb312d36ccd6e0b7737f5b44b6b24b575b6678c0ccc", size = 6449666, upload-time = "2026-08-02T20:17:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/ee/23/50ccc5a65a9cfee4a975ee55555c19a7620c7fdefdd5f084ab344753e174/rumdl-0.2.52-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6c276497434b9ffbe50b91beefd94de3105ac04527ef455a2c35adf2aade46e8", size = 6347678, upload-time = "2026-08-05T19:49:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/f9ea21e3b1912e2a286dd6317f9494492f99a67131fbedab932f1476e9dd/rumdl-0.2.52-py3-none-macosx_11_0_arm64.whl", hash = "sha256:113687499579af88db435290b04e32b1dbd69fcc0cacf2e6288c6fd3d3cf4a33", size = 6018609, upload-time = "2026-08-05T19:49:53.059Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2f/c79eba919bea7dc8255811b243112d2ff859c432ef981ebd23a929850943/rumdl-0.2.52-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8864b18fca68f527db940935fb810a277476a3a95996d86353a490107d03c840", size = 6109533, upload-time = "2026-08-05T19:49:55.774Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4b/9c7c9b7b4c227ceecdb63dc875e92ccf61948886a2e02e32a244c7311414/rumdl-0.2.52-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:1f671b567b6f618d72537edac1abce6ec5165ada7f9dee8da9817eb994044d62", size = 6562511, upload-time = "2026-08-05T19:50:03.656Z" }, + { url = "https://files.pythonhosted.org/packages/aa/00/ec6ff9552177731d1884dab31e9e6668bc8622536fb4e0901f0a8228b9d1/rumdl-0.2.52-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dbb87c977b2de18e980fc7047713f8a07820059cf91653e6e20481c15c1a30ae", size = 6099226, upload-time = "2026-08-05T19:49:57.667Z" }, + { url = "https://files.pythonhosted.org/packages/24/bc/6de8e73a4176627bd33d3ff41ee16949d5096b43365e76f67e3656c005f6/rumdl-0.2.52-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45b17fa436953a0c963b491659ce2cf50e615099efa701c4cedd56367a234d78", size = 6549596, upload-time = "2026-08-05T19:50:05.759Z" }, + { url = "https://files.pythonhosted.org/packages/7a/27/2942a9b412315f0ba2221ddeb79649c6dd6fede37ebeee5d8dc85ad68af9/rumdl-0.2.52-py3-none-win_amd64.whl", hash = "sha256:9bb8b2a52af533774e52c9cd019f949d340e6da25def2520ff9790593e35b15c", size = 6494690, upload-time = "2026-08-05T19:50:01.792Z" }, ] [[package]] @@ -3304,27 +3306,27 @@ wheels = [ [[package]] name = "sse-starlette" -version = "3.4.6" +version = "3.4.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/10/a34c656829ffc1c4b22ef36d70d9ebb6b99c020e2aeb17cee5485099f028/sse_starlette-3.4.6.tar.gz", hash = "sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627", size = 32542, upload-time = "2026-07-20T14:16:32.201Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/00/b42a44342a054d58cb1115d7c8aa9cb4290dd9442f9c1b91a4b8173dba22/sse_starlette-3.4.8.tar.gz", hash = "sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c", size = 32548, upload-time = "2026-08-05T11:19:49.982Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/36/e10c1d1b7ca881d2625db2ec28508578499187bb1c389952c398474e1834/sse_starlette-3.4.6-py3-none-any.whl", hash = "sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6", size = 16516, upload-time = "2026-07-20T14:16:30.978Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/764912c58293d95b6dcdf4cc255f9d10de310580ced547b082eb9d72018c/sse_starlette-3.4.8-py3-none-any.whl", hash = "sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d", size = 16516, upload-time = "2026-08-05T11:19:48.748Z" }, ] [[package]] name = "starlette" -version = "1.3.1" +version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/3c/76d2fd1f1357ed0f0108d8a5aa233dcf16e2946a8559c84912fe08e01ac7/starlette-1.4.1.tar.gz", hash = "sha256:b7332de6e9375593a29ba9eee1e6ecfeb3eb2043e2e19a13b4b71da73ff35540", size = 2709041, upload-time = "2026-08-05T15:17:26.23Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/67e95f21498de41433babf7b1db0eeab449eb58872dfb831b27747a70fd0/starlette-1.4.1-py3-none-any.whl", hash = "sha256:7d078e0fbefae0d2cecfb80a799d6fb84b1c0c6acd4f14ac79d17d0e7ec27f19", size = 74019, upload-time = "2026-08-05T15:17:24.357Z" }, ] [[package]] @@ -3581,7 +3583,7 @@ wheels = [ [[package]] name = "typer" -version = "0.27.0" +version = "0.27.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -3589,9 +3591,9 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" }, ] [[package]]