From 873aaea75071210146d6f853472192e0daa93c1e Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 20:44:26 -0600 Subject: [PATCH 01/16] fix(runtime): preserve nested tool protocols --- go/cmd/coding-ethos-run/dispatch.go | 68 +++++++++- go/cmd/coding-ethos-run/main_test.go | 117 +++++++++++++++++- go/internal/policygitcli/main.go | 11 ++ .../policygitcli/main_internal_test.go | 42 +++++++ 4 files changed, 235 insertions(+), 3 deletions(-) diff --git a/go/cmd/coding-ethos-run/dispatch.go b/go/cmd/coding-ethos-run/dispatch.go index c5a8c8e0..f7f86974 100644 --- a/go/cmd/coding-ethos-run/dispatch.go +++ b/go/cmd/coding-ethos-run/dispatch.go @@ -33,6 +33,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/realgit" "blackcat.ca/coding-ethos/go/internal/shellparse" "blackcat.ca/coding-ethos/go/internal/shellquote" + "blackcat.ca/coding-ethos/go/toolcatalog" ) const ( @@ -854,7 +855,10 @@ func runPolicyGitHandler(paths runtimePaths, rest []string) error { runtimeExecTool( paths, "coding-ethos-git", - append([]string{"--bundle", bundlePath, "--real-git", realGitPath}, rest...)...) + append( + []string{"--bundle", bundlePath, "--real-git", realGitPath, "--"}, + rest..., + )...) return nil } @@ -1003,16 +1007,78 @@ func runAgentHooksCommand(paths runtimePaths, rest []string) { } func runPolicyTool(paths runtimePaths, rest []string) error { + return runPolicyToolForParent(paths, rest, parentExecutablePath()) +} + +func runPolicyToolForParent( + paths runtimePaths, + rest []string, + parentExecutable string, +) error { if len(rest) == 0 { return apperror.StaticError("policy-tool requires a tool name") } requirePolicyBundle(paths) + + if actionlintShellcheckDependency(parentExecutable, rest[0], rest[1:]) { + tool, found := toolcatalog.HookOwnedTool("shellcheck") + if !found { + return apperror.StaticError("managed shellcheck tool is not registered") + } + + shellcheck := tool.ManagedExecutablePath(paths.EthosRoot) + requireRuntimeBinary(shellcheck, "managed shellcheck dependency") + paths.executor().execPath(shellcheck, rest[1:]...) + + return nil + } + runtimeExecLint(paths, policyToolLintArgs(paths, rest[0], rest[1:])...) return nil } +func parentExecutablePath() string { + if runtime.GOOS != linuxGOOS { + return "" + } + + path, err := os.Readlink("/proc/" + strconv.Itoa(os.Getppid()) + "/exe") + if err != nil { + return "" + } + + return path +} + +func actionlintShellcheckDependency( + parentExecutable string, + tool string, + args []string, +) bool { + if filepath.Base(parentExecutable) != "actionlint" || + tool != "shellcheck" || + len(args) == 0 || + args[len(args)-1] != "-" { + return false + } + + for index, arg := range args { + if (arg == "-f" || arg == "--format") && + index+1 < len(args) && + args[index+1] == "json" { + return true + } + + if arg == "-f=json" || arg == "--format=json" { + return true + } + } + + return false +} + func runMCP(paths runtimePaths, rest []string) { bundlePath := hookPolicyBundlePath(paths) requireRuntimeFile(bundlePath, "compiled policy bundle") diff --git a/go/cmd/coding-ethos-run/main_test.go b/go/cmd/coding-ethos-run/main_test.go index f2fbe93e..dcd5ddde 100644 --- a/go/cmd/coding-ethos-run/main_test.go +++ b/go/cmd/coding-ethos-run/main_test.go @@ -27,6 +27,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/sharedlock" "blackcat.ca/coding-ethos/go/internal/shellquote" "blackcat.ca/coding-ethos/go/internal/testlock" + "blackcat.ca/coding-ethos/go/toolcatalog" ) func TestRunnerArgsInferGitHookFromExecutableName(t *testing.T) { @@ -2400,7 +2401,7 @@ func TestPolicyGitIgnoresSpoofedAgentShellSandboxEnv(t *testing.T) { if !strings.Contains( got, "exec:coding-ethos-git --bundle "+hookPolicyBundlePath(paths)+ - " --real-git "+paths.RealGit+" status", + " --real-git "+paths.RealGit+" -- status", ) { t.Fatalf("policy-git did not execute managed git: %#v", calls) } @@ -2431,12 +2432,124 @@ func TestPolicyGitIgnoresArbitraryEnvRealGitExecutable(t *testing.T) { if !strings.Contains( got, "exec:coding-ethos-git --bundle "+hookPolicyBundlePath(paths)+ - " --real-git "+paths.RealGit+" status", + " --real-git "+paths.RealGit+" -- status", ) { t.Fatalf("policy-git did not execute managed git: %#v", calls) } } +func TestPolicyToolExecutesActionlintShellcheckDependencyRaw(t *testing.T) { + paths := runtimeTestPaths(t) + var calls []string + paths.Executor = stubRuntimeOps{calls: &calls} + + tool, found := toolcatalog.HookOwnedTool("shellcheck") + if !found { + t.Fatal("managed shellcheck tool is not registered") + } + + shellcheck := tool.ManagedExecutablePath(paths.EthosRoot) + if err := os.MkdirAll(filepath.Dir(shellcheck), 0o755); err != nil { + t.Fatalf("create managed shellcheck fixture directory: %v", err) + } + writeExecutableFixture(t, shellcheck, "#!/usr/bin/env sh\nexit 0\n") + + args := []string{ + "shellcheck", + "--norc", + "-f", "json", + "-x", + "--shell", "bash", + "-", + } + err := runPolicyToolForParent(paths, args, "/managed/bin/actionlint") + if err != nil { + t.Fatalf("run actionlint shellcheck dependency: %v", err) + } + + want := "execpath:" + shellcheck + " " + strings.Join(args[1:], " ") + if !slices.Contains(calls, want) { + t.Fatalf("managed shellcheck was not executed raw; calls = %#v", calls) + } +} + +func TestPolicyToolCapturesShellcheckOutsideActionlint(t *testing.T) { + paths := runtimeTestPaths(t) + var calls []string + paths.Executor = stubRuntimeOps{calls: &calls} + + err := runPolicyToolForParent( + paths, + []string{"shellcheck", "-f", "json", "-"}, + "/usr/bin/bash", + ) + if err != nil { + t.Fatalf("run direct shellcheck: %v", err) + } + + joined := strings.Join(calls, "\n") + if !strings.Contains(joined, "exec-lint:") || strings.Contains(joined, "execpath:") { + t.Fatalf("direct shellcheck bypassed managed capture: %#v", calls) + } +} + +func TestActionlintShellcheckDependencyRequiresJSONStdinContract(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + parent string + tool string + args []string + want bool + }{ + { + name: "actionlint json stdin", + parent: "/managed/bin/actionlint", + tool: "shellcheck", + args: []string{"--norc", "-f", "json", "-"}, + want: true, + }, + { + name: "long json format", + parent: "/managed/bin/actionlint", + tool: "shellcheck", + args: []string{"--format=json", "-"}, + want: true, + }, + { + name: "wrong parent", + parent: "/usr/bin/bash", + tool: "shellcheck", + args: []string{"-f", "json", "-"}, + }, + { + name: "not stdin", + parent: "/managed/bin/actionlint", + tool: "shellcheck", + args: []string{"-f", "json", "script.sh"}, + }, + { + name: "not json", + parent: "/managed/bin/actionlint", + tool: "shellcheck", + args: []string{"-f", "gcc", "-"}, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + got := actionlintShellcheckDependency(test.parent, test.tool, test.args) + if got != test.want { + t.Fatalf("actionlintShellcheckDependency() = %v, want %v", got, test.want) + } + }) + } +} + func TestAgentShellNativeGitBindRequiresReadOnlyMountInfo(t *testing.T) { t.Parallel() diff --git a/go/internal/policygitcli/main.go b/go/internal/policygitcli/main.go index 6d25c46e..dd7e3a77 100644 --- a/go/internal/policygitcli/main.go +++ b/go/internal/policygitcli/main.go @@ -187,6 +187,8 @@ func gitOptions( } } + argv = withoutInitialRedundantChangeDir(argv) + stdin, err := stdinForGitArgv(argv) if err != nil { return gitwrap.Options{}, err @@ -200,6 +202,15 @@ func gitOptions( }, nil } +func withoutInitialRedundantChangeDir(argv []string) []string { + index := 0 + for index+1 < len(argv) && argv[index] == "-C" && argv[index+1] == "." { + index += 2 + } + + return append([]string(nil), argv[index:]...) +} + func stdinForGitArgv(argv []string) ([]byte, error) { if !gitCommitReadsMessageFromStdin(argv) { return nil, nil diff --git a/go/internal/policygitcli/main_internal_test.go b/go/internal/policygitcli/main_internal_test.go index 2ca2606c..201d8ba6 100644 --- a/go/internal/policygitcli/main_internal_test.go +++ b/go/internal/policygitcli/main_internal_test.go @@ -84,6 +84,48 @@ func TestGitOptionsForNonStdinCommand(t *testing.T) { } } +func TestGitOptionsDropsOnlyInitialRedundantChangeDir(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + argv []string + want string + }{ + { + name: "redundant current directory", + argv: []string{"-C", ".", "diff", "--staged"}, + want: "diff --staged", + }, + { + name: "real directory change remains policy visible", + argv: []string{"-C", "../other", "diff", "--staged"}, + want: "-C ../other diff --staged", + }, + { + name: "operation change detection flag is untouched", + argv: []string{"diff", "-C", "."}, + want: "diff -C .", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + options, err := gitOptions(test.argv, t.TempDir(), false) + if err != nil { + t.Fatalf("gitOptions: %v", err) + } + + if got := strings.Join(options.Argv, " "); got != test.want { + t.Fatalf("argv = %q, want %q", got, test.want) + } + }) + } +} + func TestReadBundleAndMaybePrintJSON(t *testing.T) { t.Parallel() From 8ff94cf5a21a74536e9f100e5e65a7a4840294d5 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 23:07:46 -0600 Subject: [PATCH 02/16] fix(parent): isolate lane runtime surfaces --- Makefile | 8 +-- README.md | 10 +++ go/cmd/coding-ethos-run/main_test.go | 67 +++++++++++++++++++ go/cmd/coding-ethos-run/parent_workflow.go | 20 ++++-- ...act_agent_skill_sync_is_not_user_facing.py | 5 +- 5 files changed, 98 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 6b104b36..75634d51 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ fi endef define install_git_hooks -$(call print_info,hooks: $(1)); "$(GO_TOOLS_BIN_DIR)/coding-ethos-toolchain" install-git-hooks --hooks-dir "$(1)" --runner "$(GO_HOOK)" +$(call print_info,hooks: $(1)); "$(GO_TOOLS_BIN_DIR)/coding-ethos-toolchain" install-git-hooks --hooks-dir "$(1)" --runner "$(2)" endef HOOK_CONSUMER_ROOT := $(shell $(resolve_hook_consumer_root)) @@ -648,11 +648,11 @@ go-hook-runner-install: ensure-go ## Build the bundled Go hook runner into the c @cd "$(GO_TOOLS_DIR)" && "$(GO)" build $(GO_BUILD_FLAGS) -o "$(LOCAL_BIN_DIR)/coding-ethos-hook-runner" ./cmd/coding-ethos-hook-runner @$(call print_info,installed: $(LOCAL_BIN_DIR)/coding-ethos-hook-runner) -_sync-git-hooks: ensure-go go-tools-install +_sync-git-hooks: ensure-go go-tools-install _sync-parent-hook-runtime @$(call print_step,Syncing Git hook entrypoints) - @$(call install_git_hooks,$(LOCAL_HOOKS_DIR)) + @$(call install_git_hooks,$(LOCAL_HOOKS_DIR),$(GO_HOOK)) @if [ "$(HOOKS_DIR)" != "$(LOCAL_HOOKS_DIR)" ]; then \ - $(call install_git_hooks,$(HOOKS_DIR)); \ + $(call install_git_hooks,$(HOOKS_DIR),$(PARENT_HOOK_BIN_DIR)/coding-ethos-run); \ fi _sync-parent-hook-runtime: ensure-go go-tools-install policy-bundle-install diff --git a/README.md b/README.md index 59d169a3..af60c99c 100644 --- a/README.md +++ b/README.md @@ -931,6 +931,16 @@ install/check emit only status plus artifact-step rows, while parent lint emits the normal coding-ethos TOON lint report. See `TO_MY_PARENT.md` for the parent artifact contract. +When `parent-install` or `parent-lint` receives an external `--state-root`, it +leaves the consumer checkout's tracked `.gitignore` unchanged. Other generated +parent artifacts remain normal consumer surfaces; repo-local state retains the +runtime-ignore repair. + +Git hooks installed for a parent repository route through its stable common +Git runtime at `.git/coding-ethos-hooks/bin/coding-ethos-run`. They never point +at a worktree-local build path, so one worktree cannot strand every sibling's +hooks when its own checkout is retired or hidden by a lane sandbox. + Parent repos can opt into profile defaults in `repo_config.yaml`: ```yaml diff --git a/go/cmd/coding-ethos-run/main_test.go b/go/cmd/coding-ethos-run/main_test.go index dcd5ddde..6f692be8 100644 --- a/go/cmd/coding-ethos-run/main_test.go +++ b/go/cmd/coding-ethos-run/main_test.go @@ -665,6 +665,46 @@ func TestRuntimePolicyBundleUsesPrivateStateRoot(t *testing.T) { } } +func TestParentUsesExternalStateRoot(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + options parentWorkflowOptions + want bool + }{ + {name: "implicit repo-local", options: parentWorkflowOptions{Repo: "/repo"}}, + { + name: "explicit repo-local", + options: parentWorkflowOptions{Repo: "/repo", StateRoot: "/repo"}, + }, + { + name: "clean-equivalent repo-local", + options: parentWorkflowOptions{Repo: "/repo/.", StateRoot: "/repo"}, + }, + { + name: "external", + options: parentWorkflowOptions{Repo: "/repo", StateRoot: "/private/state"}, + want: true, + }, + { + name: "nested external", + options: parentWorkflowOptions{Repo: "/repo", StateRoot: "/repo/private-state"}, + want: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + if got := parentUsesExternalStateRoot(test.options); got != test.want { + t.Fatalf("parentUsesExternalStateRoot() = %t, want %t", got, test.want) + } + }) + } +} + func TestParentStepStatusFailsOnAnyFailedStep(t *testing.T) { t.Parallel() @@ -3326,6 +3366,33 @@ func TestMakefileRoutesLintTargetsThroughManagedGroups(t *testing.T) { } } +func TestMakefileRoutesParentGitHooksThroughStableCommonRuntime(t *testing.T) { + t.Parallel() + + payload, err := os.ReadFile(filepath.Join("..", "..", "..", "Makefile")) + if err != nil { + t.Fatalf("read Makefile: %v", err) + } + + makefile := string(payload) + for _, want := range []string{ + `$(call install_git_hooks,$(LOCAL_HOOKS_DIR),$(GO_HOOK))`, + `$(call install_git_hooks,$(HOOKS_DIR),$(PARENT_HOOK_BIN_DIR)/coding-ethos-run)`, + `_sync-git-hooks: ensure-go go-tools-install _sync-parent-hook-runtime`, + } { + if !strings.Contains(makefile, want) { + t.Fatalf("Makefile missing stable Git hook route %q", want) + } + } + + if strings.Contains( + makefile, + `$(call install_git_hooks,$(HOOKS_DIR),$(GO_HOOK))`, + ) { + t.Fatal("Makefile routes parent Git hooks through a worktree-local runner") + } +} + func fakeCIGit(t *testing.T, diffOutput string) string { t.Helper() diff --git a/go/cmd/coding-ethos-run/parent_workflow.go b/go/cmd/coding-ethos-run/parent_workflow.go index 9cdbcdd6..e813f995 100644 --- a/go/cmd/coding-ethos-run/parent_workflow.go +++ b/go/cmd/coding-ethos-run/parent_workflow.go @@ -270,14 +270,16 @@ func syncParentArtifacts( steps = append(steps, runParentStep("agent_hooks", func() error { return agenthooks.SyncSettings(options.Repo, parentAgentHookCommand(paths)) })) - steps = append(steps, runParentStep("repo_ignores", func() error { - _, err := repoignore.RepairGitignore(options.Repo) - if err != nil { - return fmt.Errorf("repair repo ignores: %w", err) - } + if !parentUsesExternalStateRoot(options) { + steps = append(steps, runParentStep("repo_ignores", func() error { + _, err := repoignore.RepairGitignore(options.Repo) + if err != nil { + return fmt.Errorf("repair repo ignores: %w", err) + } - return nil - })) + return nil + })) + } steps = append(steps, runParentStep("code_intel", func() error { return refreshParentCodeIntel(options.Repo) })) @@ -285,6 +287,10 @@ func syncParentArtifacts( return steps } +func parentUsesExternalStateRoot(options parentWorkflowOptions) bool { + return options.StateRoot != "" && !sameCleanPath(options.StateRoot, options.Repo) +} + func checkParentArtifacts( paths runtimePaths, options parentWorkflowOptions, diff --git a/tests/test_makefile_contract_agent_skill_sync_is_not_user_facing.py b/tests/test_makefile_contract_agent_skill_sync_is_not_user_facing.py index 22eaf866..9b04d6e1 100644 --- a/tests/test_makefile_contract_agent_skill_sync_is_not_user_facing.py +++ b/tests/test_makefile_contract_agent_skill_sync_is_not_user_facing.py @@ -25,7 +25,10 @@ def test_agent_skill_sync_is_not_user_facing() -> None: assert "\t_sync-parent-hook-runtime \\" not in phony_block assert "_sync-agent-skills: ensure-go\n" in makefile assert "_sync-consumer-agent-skills: ensure-go\n" in makefile - assert "_sync-git-hooks: ensure-go go-tools-install\n" in makefile + assert ( + "_sync-git-hooks: ensure-go go-tools-install _sync-parent-hook-runtime\n" + in makefile + ) assert ( "_sync-parent-hook-runtime: ensure-go go-tools-install policy-bundle-install\n" in makefile From ff2de2270374190918c45963603443e8643d3805 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 23:13:27 -0600 Subject: [PATCH 03/16] fix(go): satisfy whitespace lint --- go/cmd/coding-ethos-run/parent_workflow.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go/cmd/coding-ethos-run/parent_workflow.go b/go/cmd/coding-ethos-run/parent_workflow.go index e813f995..aba39f4d 100644 --- a/go/cmd/coding-ethos-run/parent_workflow.go +++ b/go/cmd/coding-ethos-run/parent_workflow.go @@ -270,6 +270,7 @@ func syncParentArtifacts( steps = append(steps, runParentStep("agent_hooks", func() error { return agenthooks.SyncSettings(options.Repo, parentAgentHookCommand(paths)) })) + if !parentUsesExternalStateRoot(options) { steps = append(steps, runParentStep("repo_ignores", func() error { _, err := repoignore.RepairGitignore(options.Repo) @@ -280,6 +281,7 @@ func syncParentArtifacts( return nil })) } + steps = append(steps, runParentStep("code_intel", func() error { return refreshParentCodeIntel(options.Repo) })) From ed817de2d01370dfd671ce7551f01154fabd9da3 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sun, 30 Aug 2026 17:37:43 -0600 Subject: [PATCH 04/16] fix(runtime): harden gates and code-intel replay --- README.md | 9 +- TODO.md | 6 + TO_MY_PARENT.md | 9 +- docs/CODE_INTEL.md | 13 +- docs/HOOK_RUNTIME_BOOTSTRAP.md | 130 ++-- go/cmd/coding-ethos-run/dispatch.go | 2 +- go/cmd/coding-ethos-run/main_test.go | 106 ++- go/cmd/coding-ethos-run/parent_workflow.go | 223 +++++- go/internal/codeintel/duckdb_store.go | 47 +- go/internal/codeintel/schema.go | 4 + .../codeintel/search_identity_migration.go | 198 ++++++ .../search_identity_migration_test.go | 248 +++++++ go/internal/codeintel/store.go | 94 ++- go/internal/codeintel/store_migration.go | 3 +- .../codeintel/store_migration_manifest.go | 3 +- go/internal/codeintel/store_migration_rows.go | 65 +- .../codeintel/store_migration_schema.go | 14 +- go/internal/codeintel/store_migration_test.go | 71 +- go/internal/codeintel/write.go | 146 +++- go/internal/hookrunnercli/export.go | 8 + go/internal/hookrunnercli/external_tool.go | 53 ++ .../external_tool_internal_test.go | 26 + go/internal/hookrunnercli/git_hook.go | 43 +- .../hookrunnercli/git_hook_internal_test.go | 19 + go/internal/hooks/gate_exit_status.go | 638 ++++++++++++++++++ go/internal/hooks/gate_exit_status_test.go | 64 ++ go/internal/hooks/git_wrapper_enforcement.go | 10 +- go/internal/hooks/lint_tool_capture.go | 7 +- go/internal/hooks/normalizer_internal_test.go | 20 + go/internal/hooks/proxy_output.go | 2 +- go/internal/hooks/runner.go | 4 + .../hooks/semantic_policy_injection.go | 2 +- go/internal/policy/bundle.go | 51 +- go/internal/policy/hook_route_policies.go | 38 ++ go/internal/policygitcli/main.go | 200 +++++- .../policygitcli/main_internal_test.go | 60 ++ pre-commit/PRE-COMMIT.md | 16 +- pre-commit/hooks/HOOKS.md | 14 +- 38 files changed, 2451 insertions(+), 215 deletions(-) create mode 100644 go/internal/codeintel/search_identity_migration.go create mode 100644 go/internal/codeintel/search_identity_migration_test.go create mode 100644 go/internal/hooks/gate_exit_status.go create mode 100644 go/internal/hooks/gate_exit_status_test.go create mode 100644 go/internal/hooks/normalizer_internal_test.go diff --git a/README.md b/README.md index af60c99c..d48180a0 100644 --- a/README.md +++ b/README.md @@ -931,6 +931,11 @@ install/check emit only status plus artifact-step rows, while parent lint emits the normal coding-ethos TOON lint report. See `TO_MY_PARENT.md` for the parent artifact contract. +`parent-install` rebuilds the checkout-authoritative Go tools and atomically +projects byte-identical executables into the parent repository's stable common +Git runtime. `parent-check` hashes both sides and fails if that projection is +missing, non-executable, symlinked back to a retiring checkout, or stale. + When `parent-install` or `parent-lint` receives an external `--state-root`, it leaves the consumer checkout's tracked `.gitignore` unchanged. Other generated parent artifacts remain normal consumer surfaces; repo-local state retains the @@ -939,7 +944,9 @@ runtime-ignore repair. Git hooks installed for a parent repository route through its stable common Git runtime at `.git/coding-ethos-hooks/bin/coding-ethos-run`. They never point at a worktree-local build path, so one worktree cannot strand every sibling's -hooks when its own checkout is retired or hidden by a lane sandbox. +hooks when its own checkout is retired or hidden by a lane sandbox. Running the +supported parent workflow refreshes and verifies that shared executable +projection as part of the same install/check contract. Parent repos can opt into profile defaults in `repo_config.yaml`: diff --git a/TODO.md b/TODO.md index 39f01ded..d34f5bc5 100644 --- a/TODO.md +++ b/TODO.md @@ -250,6 +250,12 @@ Goal: make the checked-out `coding-ethos` repository the single build and runtime source of truth. Consumer repository hooks should only discover, repair, and dispatch. +> Historical plan: the completed phases below describe the earlier +> checkout-local runtime. The current worktree-safe contract keeps the selected +> checkout as source/build authority and installs a byte-verified executable +> projection in the Git-common `.git/coding-ethos-hooks` runtime. See +> `docs/HOOK_RUNTIME_BOOTSTRAP.md` for the superseding architecture. + ### Phase 1 - Runtime Layout - [x] Replace the consumer `.git/coding-ethos-hooks` runtime cache with diff --git a/TO_MY_PARENT.md b/TO_MY_PARENT.md index cb4c2946..6a9ec9d4 100644 --- a/TO_MY_PARENT.md +++ b/TO_MY_PARENT.md @@ -45,9 +45,12 @@ profiles: Explicit `repo_config.yaml` settings override these profile defaults. -`parent-install` syncs generated parent artifacts. `parent-check` verifies those -artifacts without rewriting them. `parent-lint` syncs the parent artifacts, then -runs the full parent lint scope through the compiled policy bundle. +`parent-install` syncs generated parent artifacts and atomically refreshes the +compiled executables in the parent repository's common +`.git/coding-ethos-hooks/bin/` runtime. `parent-check` verifies those artifacts +without rewriting them, including byte identity and executable independence +from any retiring worktree. `parent-lint` syncs the parent artifacts, then runs +the full parent lint scope through the compiled policy bundle. ## Output Contract diff --git a/docs/CODE_INTEL.md b/docs/CODE_INTEL.md index 54b65175..92a40b32 100644 --- a/docs/CODE_INTEL.md +++ b/docs/CODE_INTEL.md @@ -17,7 +17,8 @@ Use explicit storage layers with separate logical responsibilities: content-addressed base manifest is shared through the Git common directory; each worktree owns its delta manifest, tombstones, and current-generation receipt. No shared writable DuckDB is used for these source facts. -- **DuckDB remains the v1-compatible analytical and telemetry store.** It owns +- **DuckDB is a schema-v2 analytical and telemetry store with v1 upgrade + compatibility.** It owns traces, policy decisions, remediations, outcomes, derived AST graph edges, file metadata, and full-text search. Existing v1 stores are retained during the v2 migration and can be rebuilt explicitly as derived state. @@ -156,6 +157,16 @@ remediation text. duckdb-vss is active for derived vector rows, but DuckDB facts remain the auditable source of truth. +Writable stores use schema v2. Search identities are enforced by unique +constraints on `code_intel_fts.fts_id` and on `(term, fts_id)` in +`code_intel_search_terms`. Upgrading or replaying a v1 store collapses exact +duplicate rows, fails closed when one identity carries conflicting content, +and reports row and duplicate counts through store statistics. Migration +manifests use kind v2, record `deduplicated_rows`, and exclude the schema +metadata row from migrated-data accounting. File, chunk, and graph-edge +replays use conflict-aware updates so rebuilding search state does not discard +retained AST or foreign-key evidence. + Graph facts expose provenance classes wherever repo maps, graph reports, and MCP graph surfaces show those facts. `EXTRACTED` marks parser/static-analysis facts, while `GIT_DERIVED`, `POLICY_DERIVED`, `TRACE_DERIVED`, and diff --git a/docs/HOOK_RUNTIME_BOOTSTRAP.md b/docs/HOOK_RUNTIME_BOOTSTRAP.md index cde36936..282c8a40 100644 --- a/docs/HOOK_RUNTIME_BOOTSTRAP.md +++ b/docs/HOOK_RUNTIME_BOOTSTRAP.md @@ -5,50 +5,55 @@ ## Decision -The consumer repository hook shim must not own policy behavior. +The consumer repository hook shim must not own policy behavior, and it must not +point at a worktree that can be retired while sibling worktrees still use the +same Git common directory. Its job is limited to: -- discover the consumer repository root -- locate the checked-out `coding-ethos` bundle for that repository -- verify that required built artifacts exist inside that checkout -- repair missing artifacts by running supported `make` targets in the - `coding-ethos` checkout -- dispatch to the built hook binary +- identify the hook kind +- dispatch to the compiled runner installed in the repository's stable common + Git runtime Policy evaluation, policy freshness, generated prompt packs, runtime command selection, managed capture, diagnostics parsing, and hook behavior belong to -the `coding-ethos` checkout. The shim is only a bootstrap and dispatch layer. +the compiled Coding Ethos runtime. The checked-out Coding Ethos authority is +the source and build authority; `.git/coding-ethos-hooks` is an installed, +byte-verified projection for durable sibling-worktree execution. ## Rationale -The `coding-ethos` checkout is already required for normal operation. Keeping a -second runtime cache under the consumer repository `.git` directory creates two -possible sources of truth: +Git hook entrypoints live in the Git common directory and are shared by every +worktree. Pointing those entrypoints at one authority checkout creates a hidden +lifetime dependency: retiring or sandbox-hiding that checkout strands every +sibling hook even though the common Git directory remains healthy. -- the checked-out `coding-ethos` source tree -- the installed `.git/coding-ethos-hooks` runtime cache +The common runtime is therefore a projection, not a second source of truth: -That split is fragile. Worktrees, submodules, generated policy files, touched -configuration, and branch switches can cause the hook shim, `make build`, and -runtime validation to resolve different roots. When that happens, lifecycle -hooks can fail even though the correct repair command has been run elsewhere. +- `coding-ethos/bin` is built from the selected authority checkout. +- `parent-install` atomically copies every compiled Go command into the common + runtime. +- `parent-check` requires regular executable files with byte-identical SHA-256 + content and rejects symlinks back to an authority checkout. +- `make build` installs the complete policy, pre-commit, toolchain, shim, and + executable projection. -The simpler invariant is: +The invariant is: ```text -make in the coding-ethos checkout builds the hook runtime. -hooks run the hook runtime from the coding-ethos checkout. +the selected checkout builds and verifies authority artifacts. +hooks execute the stable common projection of those artifacts. ``` -If the `coding-ethos` checkout is present and can build, the hook path should -self-heal missing runtime artifacts. If the checkout is missing or cannot build, -the error should name the exact checkout path and command required to fix it. +If the projection is missing or stale, the error must name the exact supported +`parent-install` or `make build` command. Lifecycle hooks must not silently +select another worktree as authority. ## Target Runtime Layout -Runtime artifacts live under the `coding-ethos` checkout and are ignored by -Git: +Authority build artifacts live under the selected `coding-ethos` checkout and +are ignored by Git. The installed hook runtime lives under the consumer +repository's Git common directory: ```text coding-ethos/ @@ -68,10 +73,18 @@ coding-ethos/ go-bin/{golangci-lint,shfmt} github-bin/{actionlint,dotenv-linter,hadolint,shellcheck} prefix/bin/ + +/coding-ethos-hooks/ + bin/{coding-ethos-*,cerun,git,lint} + policy/{policy-bundle.json,policy-metadata.json} + pre-commit/ + build/{policy,toolchain}/ + {coding_ethos.yml,repo_ethos.yml,config.yaml} ``` -Consumer repository hooks should not install or validate policy bundles under -the consumer `.git` directory. +The common projection is runtime state and must never be committed. It is +updated only through the supported install/build workflow and validated against +the selected authority, rather than edited directly. ## Managed Toolchain @@ -133,26 +146,26 @@ path. ## Hook Entrypoint Contract -The installed consumer repository hook entrypoint should be a small executable -script generated from the compiled `bin/coding-ethos-run` binary. The script -passes the hook kind and hook name explicitly, for example -`coding-ethos-run git-hook pre-commit "$@"`, so installed Git hooks do not rely -on `argv[0]` inference. +The installed consumer repository hook entrypoint is a small executable script. +It passes the hook kind and hook name explicitly, for example +`/coding-ethos-hooks/bin/coding-ethos-run git-hook pre-commit +"$@"`, so installed Git hooks do not rely on `argv[0]` inference or a +worktree-local path. -The compiled runner owns the bootstrap contract: +The supported install/check workflow owns the bootstrap contract: -1. Resolve the consumer repository root from Git. -2. Locate `coding-ethos`, preferably at `$consumer_root/coding-ethos`. -3. Fail with a clear submodule checkout instruction if it is missing. -4. Check for required artifacts in the `coding-ethos` checkout. -5. If artifacts are missing, run: +1. Resolve the consumer repository and its absolute Git common directory. +2. Build Go tools from the explicitly selected Coding Ethos authority. +3. Atomically install the compiled executables into the common runtime. +4. Install the remaining policy, toolchain, and hook artifacts through + `make build` when the full runtime is being refreshed. +5. Verify executable type, mode, and byte identity with: ```bash - make -C "$coding_ethos_root" build + coding-ethos/bin/coding-ethos-run parent-check --repo "$consumer_root" ``` -6. Re-check the required artifacts. -7. Exec the built hook binary from the `coding-ethos` checkout. +6. Install Git entrypoints that dispatch only to the common runner. The hook entrypoint contract must not: @@ -161,19 +174,22 @@ The hook entrypoint contract must not: - select policy files - inspect policy source configuration - rewrite generated protected files by hand -- maintain a second runtime cache in the consumer `.git` directory +- point to a worktree-local authority path +- accept a symlinked executable projection back to an authority checkout - write response caches or other transient runtime state into `.git` ## Repair Rules -Bootstrap repair should run only when required artifacts are missing or invalid -enough that they cannot be executed. It should not run because a timestamp looks -old. +Bootstrap repair is explicit. `parent-install` refreshes generated parent +artifacts and compiled common-runtime executables; `make build` refreshes the +complete projection. It should not run because a timestamp looks old. -Examples that should trigger repair: +Examples that require repair: - missing hook binary - non-executable hook binary +- symlinked common-runtime executable +- executable whose SHA-256 differs from the authority build - missing compiled policy bundle - unreadable compiled policy bundle - missing managed toolchain manifest @@ -185,7 +201,7 @@ Examples that should not block lifecycle hooks: - source config has a newer mtime than the compiled bundle - generated files were touched by checkout tools -- another worktree has a different runtime cache +- another worktree has different ignored build products Strict freshness validation belongs in explicit maintainer/CI commands such as `make validate`, `make cutover-verify`, and CI. Freshness is based on the @@ -200,11 +216,13 @@ Bootstrap needs a few guardrails: - Use an interprocess lock, preferably `flock`, so concurrent hooks do not run multiple builds over the same output directory. - Print the exact failed command and preserve build output when repair fails. -- Keep build outputs under ignored `bin/` and `build/` directories. -- Keep transient repo-local runtime caches under ignored `.coding-ethos/cache/` - paths, not under `.git`. -- Keep installed hook entrypoints as stable generated scripts and move - versioned behavior into the `coding-ethos` checkout. +- Keep authority build outputs under ignored `bin/` and `build/` directories. +- Keep response, trace, and other transient repo-local caches under ignored + `.coding-ethos/` paths, not under the Git common runtime. +- Install common-runtime executables with temporary-file sync plus atomic + rename, and verify them by content rather than mtime. +- Keep installed hook entrypoints stable and move versioned behavior into the + compiled common projection. ## Hook Execution Model @@ -266,6 +284,8 @@ complete coverage. ## Migration Direction -Runtime artifacts are built and executed from the checked-out `coding-ethos` -repository. New hook behavior must use that single source of truth instead of -adding cache-local compatibility paths. +Runtime artifacts are built from an explicitly selected `coding-ethos` +authority and executed from the stable common Git projection. New hook behavior +must preserve that one-way authority-to-projection relationship: no hook may +select an arbitrary sibling checkout, and no installed executable may link back +to a checkout that can be retired. diff --git a/go/cmd/coding-ethos-run/dispatch.go b/go/cmd/coding-ethos-run/dispatch.go index f7f86974..fc423463 100644 --- a/go/cmd/coding-ethos-run/dispatch.go +++ b/go/cmd/coding-ethos-run/dispatch.go @@ -856,7 +856,7 @@ func runPolicyGitHandler(paths runtimePaths, rest []string) error { paths, "coding-ethos-git", append( - []string{"--bundle", bundlePath, "--real-git", realGitPath, "--"}, + []string{"--bundle", bundlePath, "--real-git", realGitPath}, rest..., )...) diff --git a/go/cmd/coding-ethos-run/main_test.go b/go/cmd/coding-ethos-run/main_test.go index 6f692be8..aa4246e2 100644 --- a/go/cmd/coding-ethos-run/main_test.go +++ b/go/cmd/coding-ethos-run/main_test.go @@ -780,6 +780,79 @@ func TestParentGoToolsCheckPassesWhenBinariesAreCurrent(t *testing.T) { } } +func TestParentHookRuntimeSyncConvergesAndCheckDetectsDrift(t *testing.T) { + t.Parallel() + + paths := runtimeTestPaths(t) + paths.ToolsSource = parentGoToolsSourceFixture(t, paths.EthosRoot) + touchParentGoTools(t, paths, time.Now()) + options := parentWorkflowOptions{Repo: paths.Root} + + err := syncParentHookRuntimeExecutables(paths, options) + if err != nil { + t.Fatalf("syncParentHookRuntimeExecutables: %v", err) + } + + err = checkParentHookRuntimeExecutables(paths, options) + if err != nil { + t.Fatalf("checkParentHookRuntimeExecutables: %v", err) + } + + installed := filepath.Join( + parentHookRuntimeBinDir(paths, options), + "coding-ethos-run", + ) + writeExecutableFixture(t, installed, "#!/usr/bin/env sh\necho stale\n") + + err = checkParentHookRuntimeExecutables(paths, options) + if !errors.Is(err, errParentArtifactDrift) || + !strings.Contains(err.Error(), "coding-ethos-run(content)") || + !strings.Contains(err.Error(), "parent-install") { + t.Fatalf("check drift error = %v", err) + } + + err = syncParentHookRuntimeExecutables(paths, options) + if err != nil { + t.Fatalf("repair parent hook runtime: %v", err) + } + + err = checkParentHookRuntimeExecutables(paths, options) + if err != nil { + t.Fatalf("check repaired parent hook runtime: %v", err) + } +} + +func TestParentHookRuntimeCheckRejectsCheckoutSymlink(t *testing.T) { + t.Parallel() + + paths := runtimeTestPaths(t) + paths.ToolsSource = parentGoToolsSourceFixture(t, paths.EthosRoot) + touchParentGoTools(t, paths, time.Now()) + options := parentWorkflowOptions{Repo: paths.Root} + runtimeBin := parentHookRuntimeBinDir(paths, options) + + err := os.MkdirAll(runtimeBin, 0o755) + if err != nil { + t.Fatalf("create hook runtime: %v", err) + } + + for _, tool := range parentGoToolFixtureCommands() { + destination := filepath.Join(runtimeBin, tool) + source := filepath.Join(paths.BinDir, tool) + + err = os.Symlink(source, destination) + if err != nil { + t.Fatalf("symlink %s: %v", tool, err) + } + } + + err = checkParentHookRuntimeExecutables(paths, options) + if !errors.Is(err, errParentArtifactDrift) || + !strings.Contains(err.Error(), "coding-ethos-run(not_regular)") { + t.Fatalf("check symlink error = %v", err) + } +} + func TestSyncParentPolicyBundleUsesParentRepoConfig(t *testing.T) { t.Parallel() @@ -2441,12 +2514,41 @@ func TestPolicyGitIgnoresSpoofedAgentShellSandboxEnv(t *testing.T) { if !strings.Contains( got, "exec:coding-ethos-git --bundle "+hookPolicyBundlePath(paths)+ - " --real-git "+paths.RealGit+" -- status", + " --real-git "+paths.RealGit+" status", ) { t.Fatalf("policy-git did not execute managed git: %#v", calls) } } +func TestPolicyGitForwardsWrapperFlagsBeforeGitArgv(t *testing.T) { + paths := runtimeTestPaths(t) + var calls []string + paths.Executor = stubRuntimeOps{calls: &calls} + writePolicyBundleForTest(t, hookPolicyBundlePath(paths)) + + err := run(paths, []string{ + "policy-git", + "--admin-approved", + "--check-only", + "commit", + "-m", + "verified", + }) + if err != nil { + t.Fatalf("run policy-git with wrapper flags: %v", err) + } + + got := strings.Join(calls, "\n") + if !strings.Contains( + got, + "exec:coding-ethos-git --bundle "+hookPolicyBundlePath(paths)+ + " --real-git "+paths.RealGit+ + " --admin-approved --check-only commit -m verified", + ) { + t.Fatalf("policy-git did not preserve wrapper flags: %#v", calls) + } +} + func TestPolicyGitIgnoresArbitraryEnvRealGitExecutable(t *testing.T) { paths := runtimeTestPaths(t) var calls []string @@ -2472,7 +2574,7 @@ func TestPolicyGitIgnoresArbitraryEnvRealGitExecutable(t *testing.T) { if !strings.Contains( got, "exec:coding-ethos-git --bundle "+hookPolicyBundlePath(paths)+ - " --real-git "+paths.RealGit+" -- status", + " --real-git "+paths.RealGit+" status", ) { t.Fatalf("policy-git did not execute managed git: %#v", calls) } diff --git a/go/cmd/coding-ethos-run/parent_workflow.go b/go/cmd/coding-ethos-run/parent_workflow.go index aba39f4d..2b87b48d 100644 --- a/go/cmd/coding-ethos-run/parent_workflow.go +++ b/go/cmd/coding-ethos-run/parent_workflow.go @@ -5,6 +5,7 @@ package main import ( "context" + "crypto/sha256" "errors" "flag" "fmt" @@ -41,11 +42,14 @@ const ( ) var ( - errParentArtifactDrift = errors.New("parent artifact drift") - errParentGoToolsStale = errors.New("parent Go tools are stale") - errParentPathIsDirectory = errors.New("path is a directory, want file") - errParentPathIsNotDirectory = errors.New("path is not a directory, want directory") - errParentRootNotAbsolute = errors.New("parent workflow root must be absolute") + errParentArtifactDrift = errors.New("parent artifact drift") + errParentGoToolsStale = errors.New("parent Go tools are stale") + errParentPathIsDirectory = errors.New("path is a directory, want file") + errParentPathIsNotDirectory = errors.New("path is not a directory, want directory") + errParentRootNotAbsolute = errors.New("parent workflow root must be absolute") + errParentSourceNotExecutable = errors.New( + "source executable is not a regular executable", + ) ) type parentWorkflowOptions struct { @@ -240,6 +244,9 @@ func syncParentArtifacts( steps = append(steps, runParentStep("go_tools", func() error { return rebuildParentGoTools(paths) })) + steps = append(steps, runParentStep("hook_runtime", func() error { + return syncParentHookRuntimeExecutables(paths, options) + })) steps = append(steps, runParentStep("policy_bundle", func() error { return syncParentPolicyBundle(paths, options) })) @@ -301,6 +308,9 @@ func checkParentArtifacts( steps = append(steps, runParentStep("go_tools", func() error { return checkParentGoTools(paths, options) })) + steps = append(steps, runParentStep("hook_runtime", func() error { + return checkParentHookRuntimeExecutables(paths, options) + })) steps = append(steps, runParentStep("policy_bundle", func() error { return checkParentPolicyBundle(paths, options) })) @@ -434,6 +444,209 @@ func parentGitCommonDir(paths runtimePaths, repo string) string { return filepath.Join(repo, ".git") } +func parentHookRuntimeBinDir(paths runtimePaths, options parentWorkflowOptions) string { + return filepath.Join( + parentGitCommonDir(paths, options.Repo), + "coding-ethos-hooks", + "bin", + ) +} + +func syncParentHookRuntimeExecutables( + paths runtimePaths, + options parentWorkflowOptions, +) error { + tools, err := parentGoToolCommands(paths) + if err != nil { + return err + } + + runtimeBin := parentHookRuntimeBinDir(paths, options) + + err = os.MkdirAll(runtimeBin, parentExecutableDirMode) + if err != nil { + return fmt.Errorf("create parent hook runtime bin dir: %w", err) + } + + for _, tool := range tools { + source := filepath.Join(paths.BinDir, tool) + destination := filepath.Join(runtimeBin, tool) + + err = installParentHookRuntimeExecutable(source, destination) + if err != nil { + return fmt.Errorf("install parent hook runtime %s: %w", tool, err) + } + } + + return nil +} + +func installParentHookRuntimeExecutable(source, destination string) error { + info, err := os.Lstat(source) + if err != nil { + return fmt.Errorf("stat source executable %s: %w", source, err) + } + + if !info.Mode().IsRegular() || info.Mode()&0o111 == 0 { + return fmt.Errorf("%w: %s", errParentSourceNotExecutable, source) + } + + input, err := os.Open(source) + if err != nil { + return fmt.Errorf("open source executable %s: %w", source, err) + } + defer input.Close() + + temporary, err := os.CreateTemp( + filepath.Dir(destination), + "."+filepath.Base(destination)+"-*.tmp", + ) + if err != nil { + return fmt.Errorf("create temporary hook runtime executable: %w", err) + } + + temporaryPath := temporary.Name() + + defer func() { + _ = temporary.Close() + _ = os.Remove(temporaryPath) + }() + + err = temporary.Chmod(info.Mode().Perm()) + if err != nil { + return fmt.Errorf("set temporary hook runtime executable mode: %w", err) + } + + _, err = io.Copy(temporary, input) + if err != nil { + return fmt.Errorf("copy hook runtime executable: %w", err) + } + + err = temporary.Sync() + if err != nil { + return fmt.Errorf("sync temporary hook runtime executable: %w", err) + } + + err = temporary.Close() + if err != nil { + return fmt.Errorf("close temporary hook runtime executable: %w", err) + } + + err = os.Rename(temporaryPath, destination) + if err != nil { + return fmt.Errorf("activate hook runtime executable %s: %w", destination, err) + } + + return nil +} + +func checkParentHookRuntimeExecutables( + paths runtimePaths, + options parentWorkflowOptions, +) error { + tools, err := parentGoToolCommands(paths) + if err != nil { + return err + } + + runtimeBin := parentHookRuntimeBinDir(paths, options) + mismatched := []string{} + + for _, tool := range tools { + source := filepath.Join(paths.BinDir, tool) + destination := filepath.Join(runtimeBin, tool) + + status, err := compareParentHookRuntimeExecutable(source, destination) + if err != nil { + return fmt.Errorf("check parent hook runtime %s: %w", tool, err) + } + + if status != "" { + mismatched = append(mismatched, tool+"("+status+")") + } + } + + if len(mismatched) == 0 { + return nil + } + + return fmt.Errorf( + "%w: hook_runtime out of sync in %s checkout; run: %s; drift: %s", + errParentArtifactDrift, + parentCheckoutLocation(paths, options), + parentInstallCommand(options), + strings.Join(mismatched, " "), + ) +} + +func compareParentHookRuntimeExecutable(source, destination string) (string, error) { + sourceInfo, err := os.Lstat(source) + if err != nil { + return "", fmt.Errorf("stat source executable %s: %w", source, err) + } + + if !sourceInfo.Mode().IsRegular() || sourceInfo.Mode()&0o111 == 0 { + return "", fmt.Errorf("%w: %s", errParentSourceNotExecutable, source) + } + + destinationInfo, err := os.Lstat(destination) + if errors.Is(err, os.ErrNotExist) { + return "missing", nil + } + + if err != nil { + return "", fmt.Errorf("stat installed executable %s: %w", destination, err) + } + + if !destinationInfo.Mode().IsRegular() { + return "not_regular", nil + } + + if destinationInfo.Mode()&0o111 == 0 { + return "not_executable", nil + } + + if sourceInfo.Size() != destinationInfo.Size() { + return "content", nil + } + + sourceHash, err := parentFileSHA256(source) + if err != nil { + return "", err + } + + destinationHash, err := parentFileSHA256(destination) + if err != nil { + return "", err + } + + if sourceHash != destinationHash { + return "content", nil + } + + return "", nil +} + +func parentFileSHA256(path string) ([sha256.Size]byte, error) { + file, err := os.Open(path) + if err != nil { + return [sha256.Size]byte{}, fmt.Errorf("open executable %s: %w", path, err) + } + defer file.Close() + + hash := sha256.New() + + _, err = io.Copy(hash, file) + if err != nil { + return [sha256.Size]byte{}, fmt.Errorf("hash executable %s: %w", path, err) + } + + var digest [sha256.Size]byte + copy(digest[:], hash.Sum(nil)) + + return digest, nil +} + func compileParentPolicyBundle( paths runtimePaths, options parentWorkflowOptions, diff --git a/go/internal/codeintel/duckdb_store.go b/go/internal/codeintel/duckdb_store.go index 7900e034..4d5d00f1 100644 --- a/go/internal/codeintel/duckdb_store.go +++ b/go/internal/codeintel/duckdb_store.go @@ -20,9 +20,10 @@ import ( ) const ( - duckDBStoreMode = 0o700 - duckDBLockFileMode = 0o600 - duckDBStaleLockAge = 30 * time.Minute + duckDBStoreMode = 0o700 + duckDBLockFileMode = 0o600 + duckDBStaleLockAge = 30 * time.Minute + duckDBExtendedStatsQueryCount = 15 ) // DuckDBStore is the code-intel analytical query store. @@ -552,14 +553,7 @@ func (store *DuckDBStore) migrate(ctx context.Context) error { return err } - for _, statement := range duckDBSchemaStatements() { - _, err := store.database.ExecContext(ctx, statement) - if err != nil { - return fmt.Errorf("migrate DuckDB code-intel store: %w", err) - } - } - - return nil + return migrateStore(ctx, store.database) } func (store *DuckDBStore) ping(ctx context.Context) error { @@ -657,7 +651,8 @@ func duckDBCoreStatsQueries(stats *Stats) []statCountQuery { } func duckDBExtendedStatsQueries(stats *Stats) []statCountQuery { - return []statCountQuery{ + queries := make([]statCountQuery, 0, duckDBExtendedStatsQueryCount) + queries = append(queries, []statCountQuery{ { name: "code_chunks", query: "SELECT COUNT(*) FROM code_chunks", @@ -713,10 +708,38 @@ func duckDBExtendedStatsQueries(stats *Stats) []statCountQuery { query: "SELECT COUNT(*) FROM embedding_records", target: &stats.EmbeddingRecords, }, + }...) + + return append(queries, duckDBSearchStatsQueries(stats)...) +} + +func duckDBSearchStatsQueries(stats *Stats) []statCountQuery { + return []statCountQuery{ { name: "code_intel_fts", query: "SELECT COUNT(*) FROM code_intel_fts", target: &stats.FtsRows, }, + { + name: "duplicate code_intel_fts identities", + query: `SELECT + (SELECT COUNT(*) FROM code_intel_fts) - + (SELECT COUNT(*) FROM (SELECT DISTINCT fts_id FROM code_intel_fts) AS identities)`, + target: &stats.FtsDuplicateRows, + }, + { + name: "code_intel_search_terms", + query: "SELECT COUNT(*) FROM code_intel_search_terms", + target: &stats.SearchTermRows, + }, + { + name: "duplicate code_intel_search_terms identities", + query: `SELECT + (SELECT COUNT(*) FROM code_intel_search_terms) - + (SELECT COUNT(*) FROM ( + SELECT DISTINCT term, fts_id FROM code_intel_search_terms + ) AS identities)`, + target: &stats.SearchTermDuplicateRows, + }, } } diff --git a/go/internal/codeintel/schema.go b/go/internal/codeintel/schema.go index 453d6000..831908ec 100644 --- a/go/internal/codeintel/schema.go +++ b/go/internal/codeintel/schema.go @@ -672,6 +672,10 @@ func indexSchemaStatements() []string { ON code_chunks(normalized_hash)`, `CREATE INDEX IF NOT EXISTS idx_lsh_bands_lookup ON lsh_bands(band_hash, band_index)`, + `CREATE UNIQUE INDEX IF NOT EXISTS idx_code_intel_fts_id_unique + ON code_intel_fts(fts_id)`, + `CREATE UNIQUE INDEX IF NOT EXISTS idx_code_intel_search_terms_unique + ON code_intel_search_terms(term, fts_id)`, `CREATE INDEX IF NOT EXISTS idx_code_intel_search_terms_term ON code_intel_search_terms(term, fts_id)`, `CREATE INDEX IF NOT EXISTS idx_code_intel_search_terms_fts_id diff --git a/go/internal/codeintel/search_identity_migration.go b/go/internal/codeintel/search_identity_migration.go new file mode 100644 index 00000000..9b1f8d9f --- /dev/null +++ b/go/internal/codeintel/search_identity_migration.go @@ -0,0 +1,198 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package codeintel + +import ( + "context" + "database/sql" + "errors" + "fmt" +) + +var ( + errFTSIdentityConflict = errors.New( + "conflicting code intelligence FTS rows share identity", + ) + errFTSIdentityMissing = errors.New( + "code intelligence FTS row has no durable identity", + ) +) + +type ftsIdentityContent struct { + kind sql.NullString + recordID sql.NullString + traceID sql.NullString + policyID sql.NullString + skillID sql.NullString + path sql.NullString + message sql.NullString + searchText sql.NullString +} + +// deduplicateSearchIdentity upgrades the v1 logical keys before unique +// indexes are created. Equal crash/reindex replays collapse to one row; +// conflicting rows for the same identity fail closed because choosing either +// would silently change search evidence. +func deduplicateSearchIdentity(ctx context.Context, database *sql.DB) error { + transaction, err := database.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("begin search identity migration: %w", err) + } + defer rollbackUnlessCommitted(transaction) + + ftsDuplicates, err := inspectFTSIdentityDuplicates(ctx, transaction) + if err != nil { + return err + } + + termDuplicates, err := inspectSearchTermDuplicates(ctx, transaction) + if err != nil { + return err + } + + if ftsDuplicates > 0 { + for _, statement := range []string{ + `CREATE OR REPLACE TEMP TABLE code_intel_fts_deduplicated AS + SELECT DISTINCT * FROM code_intel_fts`, + "DELETE FROM code_intel_fts", + "INSERT INTO code_intel_fts SELECT * FROM code_intel_fts_deduplicated", + "DROP TABLE code_intel_fts_deduplicated", + } { + _, execErr := transaction.ExecContext(ctx, statement) + if execErr != nil { + return fmt.Errorf("deduplicate code intelligence FTS rows: %w", execErr) + } + } + } + + if termDuplicates > 0 { + for _, statement := range []string{ + `CREATE OR REPLACE TEMP TABLE code_intel_terms_deduplicated AS + SELECT DISTINCT term, fts_id FROM code_intel_search_terms`, + "DELETE FROM code_intel_search_terms", + `INSERT INTO code_intel_search_terms(term, fts_id) + SELECT term, fts_id FROM code_intel_terms_deduplicated`, + "DROP TABLE code_intel_terms_deduplicated", + } { + _, execErr := transaction.ExecContext(ctx, statement) + if execErr != nil { + return fmt.Errorf("deduplicate code intelligence search terms: %w", execErr) + } + } + } + + commitErr := transaction.Commit() + if commitErr != nil { + return fmt.Errorf("commit search identity migration: %w", commitErr) + } + + return nil +} + +func inspectFTSIdentityDuplicates( + ctx context.Context, + transaction *sql.Tx, +) (int, error) { + rows, err := transaction.QueryContext( + ctx, + `SELECT fts_id, kind, record_id, trace_id, policy_id, skill_id, + path, message, search_text + FROM code_intel_fts`, + ) + if err != nil { + return 0, fmt.Errorf("inspect code intelligence FTS identities: %w", err) + } + defer rows.Close() + + seen := map[string]ftsIdentityContent{} + duplicates := 0 + + for rows.Next() { + var ( + identity sql.NullString + content ftsIdentityContent + ) + + scanErr := rows.Scan( + &identity, + &content.kind, + &content.recordID, + &content.traceID, + &content.policyID, + &content.skillID, + &content.path, + &content.message, + &content.searchText, + ) + if scanErr != nil { + return 0, fmt.Errorf("scan code intelligence FTS identity: %w", scanErr) + } + + if !identity.Valid || identity.String == "" { + return 0, errFTSIdentityMissing + } + + if previous, ok := seen[identity.String]; ok { + if previous != content { + return 0, fmt.Errorf("%w: %q", errFTSIdentityConflict, identity.String) + } + + duplicates++ + + continue + } + + seen[identity.String] = content + } + + rowsErr := rows.Err() + if rowsErr != nil { + return 0, fmt.Errorf("iterate code intelligence FTS identities: %w", rowsErr) + } + + return duplicates, nil +} + +func inspectSearchTermDuplicates( + ctx context.Context, + transaction *sql.Tx, +) (int, error) { + rows, err := transaction.QueryContext( + ctx, + "SELECT term, fts_id FROM code_intel_search_terms", + ) + if err != nil { + return 0, fmt.Errorf("inspect code intelligence search term identities: %w", err) + } + defer rows.Close() + + seen := map[[2]string]struct{}{} + duplicates := 0 + + for rows.Next() { + var term, identity string + + scanErr := rows.Scan(&term, &identity) + if scanErr != nil { + return 0, fmt.Errorf("scan code intelligence search term identity: %w", scanErr) + } + + key := [2]string{term, identity} + + if _, ok := seen[key]; ok { + duplicates++ + + continue + } + + seen[key] = struct{}{} + } + + rowsErr := rows.Err() + if rowsErr != nil { + return 0, fmt.Errorf("iterate code intelligence search term identities: %w", rowsErr) + } + + return duplicates, nil +} diff --git a/go/internal/codeintel/search_identity_migration_test.go b/go/internal/codeintel/search_identity_migration_test.go new file mode 100644 index 00000000..0fd52d1a --- /dev/null +++ b/go/internal/codeintel/search_identity_migration_test.go @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package codeintel + +import ( + "context" + "database/sql" + "strings" + "testing" +) + +func TestOpenMigratesExactDuplicateSearchIdentities(t *testing.T) { + ctx := context.Background() + path, database := openLegacySearchIdentityFixture(t, ctx) + + _, err := database.ExecContext( + ctx, + `INSERT INTO code_intel_fts( + fts_id, kind, record_id, trace_id, path, message, search_text + ) VALUES + ('legacy:one', 'finding', 'one', 'trace', 'one.go', 'same', 'same text'), + ('legacy:one', 'finding', 'one', 'trace', 'one.go', 'same', 'same text'); + INSERT INTO code_intel_search_terms(term, fts_id) VALUES + ('same', 'legacy:one'), + ('same', 'legacy:one')`, + ) + if err != nil { + t.Fatalf("insert legacy duplicate identities: %v", err) + } + if err = database.Close(); err != nil { + t.Fatalf("close legacy identity fixture: %v", err) + } + + store, err := Open(ctx, path) + if err != nil { + t.Fatalf("upgrade legacy duplicate identities: %v", err) + } + defer store.Close() + + stats, err := store.Stats(ctx) + if err != nil { + t.Fatalf("read upgraded identity stats: %v", err) + } + if stats.SchemaVersion != schemaVersion || stats.FtsRows != 1 || + stats.SearchTermRows != 1 || stats.FtsDuplicateRows != 0 || + stats.SearchTermDuplicateRows != 0 { + t.Fatalf("unexpected upgraded identity stats: %#v", stats) + } + + _, err = store.Database().ExecContext( + ctx, + `INSERT INTO code_intel_fts(fts_id, kind, record_id, search_text) + VALUES ('legacy:one', 'finding', 'two', 'other')`, + ) + if err == nil { + t.Fatal("upgraded FTS identity accepted a duplicate") + } + _, err = store.Database().ExecContext( + ctx, + `INSERT INTO code_intel_search_terms(term, fts_id) + VALUES ('same', 'legacy:one')`, + ) + if err == nil { + t.Fatal("upgraded search-term identity accepted a duplicate") + } +} + +func TestOpenRejectsConflictingDuplicateSearchIdentity(t *testing.T) { + ctx := context.Background() + path, database := openLegacySearchIdentityFixture(t, ctx) + + _, err := database.ExecContext( + ctx, + `INSERT INTO code_intel_fts( + fts_id, kind, record_id, trace_id, path, message, search_text + ) VALUES + ('legacy:conflict', 'finding', 'one', 'trace', 'one.go', 'first', 'same text'), + ('legacy:conflict', 'finding', 'one', 'trace', 'one.go', 'second', 'same text')`, + ) + if err != nil { + t.Fatalf("insert conflicting legacy identities: %v", err) + } + if err = database.Close(); err != nil { + t.Fatalf("close conflicting identity fixture: %v", err) + } + + _, err = Open(ctx, path) + if err == nil || !strings.Contains( + err.Error(), + "conflicting code intelligence FTS rows share identity", + ) { + t.Fatalf("expected conflicting identity rejection, got %v", err) + } + + readOnly, err := sql.Open("duckdb", path+"?access_mode=READ_ONLY") + if err != nil { + t.Fatalf("open rejected legacy store read-only: %v", err) + } + defer readOnly.Close() + + var rows int + if err = readOnly.QueryRowContext( + ctx, + "SELECT COUNT(*) FROM code_intel_fts WHERE fts_id = 'legacy:conflict'", + ).Scan(&rows); err != nil { + t.Fatalf("count retained conflicting identities: %v", err) + } + if rows != 2 { + t.Fatalf("conflicting migration retained %d rows, want 2", rows) + } +} + +func TestRepeatedCodeIndexWriteKeepsSearchAndRelationshipsStable(t *testing.T) { + ctx := context.Background() + store, err := Open(ctx, DefaultDBPath(t.TempDir())) + if err != nil { + t.Fatalf("open repeated-index store: %v", err) + } + defer store.Close() + + file := CodeFile{ + Path: "pkg/replayed.go", + Language: "go", + ContentHash: "file-one", + SizeBytes: 30, + LineCount: 3, + IndexedAtUTC: "2026-08-30T00:00:00Z", + } + chunk := CodeChunk{ + ID: "chunk-replayed", + Path: file.Path, + Language: "go", + NodeKind: "function", + SymbolKind: "function", + SymbolName: "Replayed", + SymbolPath: "Replayed", + StartByte: 0, + EndByte: 30, + StartLine: 1, + EndLine: 3, + ContentHash: "chunk-one", + SearchText: "alpha beta", + RawText: "func Replayed() {}", + } + edge := CodeEdge{ + ID: "edge-replayed", + Kind: "calls", + Path: file.Path, + SourceChunkID: chunk.ID, + TargetName: "Other", + } + if err = store.ReplaceCodeFileIndex( + ctx, + file, + []CodeChunk{chunk}, + []CodeEdge{edge}, + ); err != nil { + t.Fatalf("write initial code index: %v", err) + } + + transaction, err := store.Database().BeginTx(ctx, nil) + if err != nil { + t.Fatalf("begin AST link fixture: %v", err) + } + if err = insertASTFindingLink(ctx, transaction, ASTFindingLink{ + ID: "link-replayed", + FindingKind: "sarif_result", + FindingID: "result-replayed", + ChunkID: chunk.ID, + Path: file.Path, + SymbolPath: chunk.SymbolPath, + ContentHash: chunk.ContentHash, + }); err != nil { + _ = transaction.Rollback() + t.Fatalf("insert AST link fixture: %v", err) + } + if err = transaction.Commit(); err != nil { + t.Fatalf("commit AST link fixture: %v", err) + } + + file.ContentHash = "file-two" + chunk.ContentHash = "chunk-two" + chunk.SearchText = "alpha gamma" + if err = store.ReplaceCodeFileIndex( + ctx, + file, + []CodeChunk{chunk}, + []CodeEdge{edge}, + ); err != nil { + t.Fatalf("replay updated code index: %v", err) + } + + stats, err := store.Stats(ctx) + if err != nil { + t.Fatalf("read replayed-index stats: %v", err) + } + if stats.CodeChunks != 1 || stats.CodeEdges != 1 || stats.ASTFindingLinks != 1 || + stats.FtsRows != 1 || stats.SearchTermRows != 2 || + stats.FtsDuplicateRows != 0 || stats.SearchTermDuplicateRows != 0 { + t.Fatalf("unexpected replayed-index stats: %#v", stats) + } + + var staleTerms int + if err = store.Database().QueryRowContext( + ctx, + `SELECT COUNT(*) FROM code_intel_search_terms + WHERE fts_id = 'code_chunk:chunk-replayed:' AND term = 'beta'`, + ).Scan(&staleTerms); err != nil { + t.Fatalf("count stale replayed search terms: %v", err) + } + if staleTerms != 0 { + t.Fatalf("replayed index retained %d stale search terms", staleTerms) + } +} + +func openLegacySearchIdentityFixture( + t *testing.T, + ctx context.Context, +) (string, *sql.DB) { + t.Helper() + + path := DefaultDBPath(t.TempDir()) + store, err := Open(ctx, path) + if err != nil { + t.Fatalf("initialize legacy search identity fixture: %v", err) + } + if err = store.Close(); err != nil { + t.Fatalf("close initialized search identity fixture: %v", err) + } + + database, err := sql.Open("duckdb", path) + if err != nil { + t.Fatalf("open legacy search identity fixture: %v", err) + } + for _, statement := range []string{ + "DROP INDEX IF EXISTS idx_code_intel_fts_id_unique", + "DROP INDEX IF EXISTS idx_code_intel_search_terms_unique", + "UPDATE schema_metadata SET value = '1' WHERE key = 'schema_version'", + } { + if _, err = database.ExecContext(ctx, statement); err != nil { + _ = database.Close() + t.Fatalf("prepare legacy search identity fixture: %v", err) + } + } + + return path, database +} diff --git a/go/internal/codeintel/store.go b/go/internal/codeintel/store.go index ef21e675..e4b5df9b 100644 --- a/go/internal/codeintel/store.go +++ b/go/internal/codeintel/store.go @@ -19,7 +19,7 @@ import ( const ( sourcePathClauseCapacityFactor = 2 sourcePathQueryArgFactor = 4 - schemaVersion = 1 + schemaVersion = 2 storeDirMode = 0o700 storeLockWait = 2 * time.Second storeLockRetryInterval = 100 * time.Millisecond @@ -33,34 +33,37 @@ type Store struct { type storeOpenFunc func(context.Context, string) (*Store, error) type Stats struct { - Traces int `json:"traces"` - HookEvents int `json:"hook_events"` - HookDecisions int `json:"hook_decisions"` - HookTargets int `json:"hook_targets"` - HookReviews int `json:"hook_reviews"` - ProxySessions int `json:"proxy_sessions"` - ProxyEvents int `json:"proxy_events"` - ProxyTransforms int `json:"proxy_transforms"` - Findings int `json:"findings"` - Files int `json:"files"` - CodeChunks int `json:"code_chunks"` - CodeEdges int `json:"code_edges"` - GitFileSignals int `json:"git_file_signals"` - GitCoChanges int `json:"git_cochanges"` - CodeHealthSnapshots int `json:"code_health_snapshots"` - CodeHealthTargets int `json:"code_health_targets"` - CodeHealthCoverage int `json:"code_health_coverage"` - ASTFindingLinks int `json:"ast_finding_links"` - Decisions int `json:"decisions"` - DecisionLinks int `json:"decision_links"` - Remediations int `json:"remediations"` - RemediationEvents int `json:"remediation_events"` - SARIFRuns int `json:"sarif_runs"` - SARIFResults int `json:"sarif_results"` - RemediationOutcomes int `json:"remediation_outcomes"` - EmbeddingRecords int `json:"embedding_records"` - FtsRows int `json:"fts_rows"` - SchemaVersion int `json:"schema_version"` + Traces int `json:"traces"` + HookEvents int `json:"hook_events"` + HookDecisions int `json:"hook_decisions"` + HookTargets int `json:"hook_targets"` + HookReviews int `json:"hook_reviews"` + ProxySessions int `json:"proxy_sessions"` + ProxyEvents int `json:"proxy_events"` + ProxyTransforms int `json:"proxy_transforms"` + Findings int `json:"findings"` + Files int `json:"files"` + CodeChunks int `json:"code_chunks"` + CodeEdges int `json:"code_edges"` + GitFileSignals int `json:"git_file_signals"` + GitCoChanges int `json:"git_cochanges"` + CodeHealthSnapshots int `json:"code_health_snapshots"` + CodeHealthTargets int `json:"code_health_targets"` + CodeHealthCoverage int `json:"code_health_coverage"` + ASTFindingLinks int `json:"ast_finding_links"` + Decisions int `json:"decisions"` + DecisionLinks int `json:"decision_links"` + Remediations int `json:"remediations"` + RemediationEvents int `json:"remediation_events"` + SARIFRuns int `json:"sarif_runs"` + SARIFResults int `json:"sarif_results"` + RemediationOutcomes int `json:"remediation_outcomes"` + EmbeddingRecords int `json:"embedding_records"` + FtsRows int `json:"fts_rows"` + FtsDuplicateRows int `json:"fts_duplicate_rows"` + SearchTermRows int `json:"search_term_rows"` + SearchTermDuplicateRows int `json:"search_term_duplicate_rows"` + SchemaVersion int `json:"schema_version"` } type RowPruneSummary struct { @@ -533,21 +536,27 @@ func migrateStore(ctx context.Context, database *sql.DB) error { } } + err := deduplicateSearchIdentity(ctx, database) + if err != nil { + return err + } + for _, statement := range indexSchemaStatements() { - _, err := database.ExecContext(ctx, statement) + _, err = database.ExecContext(ctx, statement) if err != nil { return fmt.Errorf("migrate code intelligence indexes: %w", err) } } - err := backfillSearchTerms(ctx, database) + err = backfillSearchTerms(ctx, database) if err != nil { return err } _, err = database.ExecContext( ctx, - "INSERT OR REPLACE INTO schema_metadata(key, value) VALUES('schema_version', ?)", + `INSERT INTO schema_metadata(key, value) VALUES('schema_version', ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value`, schemaVersion, ) if err != nil { @@ -697,5 +706,26 @@ func statCountQueries(stats *Stats) []statCountQuery { query: "SELECT COUNT(*) FROM code_intel_fts", target: &stats.FtsRows, }, + { + name: "duplicate code_intel_fts identities", + query: `SELECT + (SELECT COUNT(*) FROM code_intel_fts) - + (SELECT COUNT(*) FROM (SELECT DISTINCT fts_id FROM code_intel_fts) AS identities)`, + target: &stats.FtsDuplicateRows, + }, + { + name: "code_intel_search_terms", + query: "SELECT COUNT(*) FROM code_intel_search_terms", + target: &stats.SearchTermRows, + }, + { + name: "duplicate code_intel_search_terms identities", + query: `SELECT + (SELECT COUNT(*) FROM code_intel_search_terms) - + (SELECT COUNT(*) FROM ( + SELECT DISTINCT term, fts_id FROM code_intel_search_terms + ) AS identities)`, + target: &stats.SearchTermDuplicateRows, + }, } } diff --git a/go/internal/codeintel/store_migration.go b/go/internal/codeintel/store_migration.go index 82c5302e..f48ff253 100644 --- a/go/internal/codeintel/store_migration.go +++ b/go/internal/codeintel/store_migration.go @@ -329,7 +329,8 @@ func migrationTablesVerified(tables []StoreMigrationTable) bool { for _, table := range tables { if !table.SourceRowsVerified || - table.SourceRows != table.ImportedRows+table.MatchedRows { + table.SourceRows != table.ImportedRows+ + table.MatchedRows+table.DeduplicatedRows { return false } } diff --git a/go/internal/codeintel/store_migration_manifest.go b/go/internal/codeintel/store_migration_manifest.go index 2802d971..a7421a57 100644 --- a/go/internal/codeintel/store_migration_manifest.go +++ b/go/internal/codeintel/store_migration_manifest.go @@ -15,7 +15,7 @@ import ( ) const ( - storeMigrationManifestKind = "code_intel.store_migration.v1" + storeMigrationManifestKind = "code_intel.store_migration.v2" storeMigrationFileMode = 0o600 ) @@ -28,6 +28,7 @@ type StoreMigrationTable struct { SourceRows int64 `json:"source_rows"` ImportedRows int64 `json:"imported_rows"` MatchedRows int64 `json:"matched_rows"` + DeduplicatedRows int64 `json:"deduplicated_rows,omitempty"` DestinationRows int64 `json:"destination_rows"` SourceRowsVerified bool `json:"source_rows_verified"` } diff --git a/go/internal/codeintel/store_migration_rows.go b/go/internal/codeintel/store_migration_rows.go index 3aaba11f..905bab6b 100644 --- a/go/internal/codeintel/store_migration_rows.go +++ b/go/internal/codeintel/store_migration_rows.go @@ -255,6 +255,7 @@ func inspectMigrationTable( ctx, destination, destinationTable, + spec, ) if err != nil { return migrationTableBaseline{}, err @@ -308,7 +309,14 @@ func finishMigrationTable( } importedRows := destinationRows - baseline.destinationRowsBefore - if baseline.sourceRows != importedRows+baseline.matchedRows { + deduplicatedRows := int64(0) + + if spec.deduplicateRows { + deduplicatedRows = baseline.sourceRows - importedRows - baseline.matchedRows + } + + if deduplicatedRows < 0 || + baseline.sourceRows != importedRows+baseline.matchedRows+deduplicatedRows { return StoreMigrationTable{}, fmt.Errorf( "%w: row accounting mismatch in %s", errStoreMigrationIntegrity, @@ -323,6 +331,7 @@ func finishMigrationTable( SourceRows: baseline.sourceRows, ImportedRows: importedRows, MatchedRows: baseline.matchedRows, + DeduplicatedRows: deduplicatedRows, DestinationRows: destinationRows, SourceRowsVerified: true, }, nil @@ -337,6 +346,7 @@ func validateMigrationKeyConsistency( ) error { keys := quoteMigrationIdentifiers(spec.keyColumns) variantChecks := make([]string, 0, len(columns)) + rowPredicate := migrationRowPredicate(spec, "") for _, column := range migrationColumnIdentifiers(columns) { variantChecks = append( @@ -352,9 +362,10 @@ func validateMigrationKeyConsistency( // #nosec G201 -- table, keys, and columns come from the validated schema inventory. query := fmt.Sprintf( - "SELECT COUNT(*) FROM (SELECT %s FROM %s GROUP BY %s HAVING %s)", + "SELECT COUNT(*) FROM (SELECT %s FROM %s WHERE %s GROUP BY %s HAVING %s)", strings.Join(keys, ", "), table, + rowPredicate, strings.Join(keys, ", "), strings.Join(variantChecks, " OR "), ) @@ -388,26 +399,32 @@ func migrationDuplicateCounts( ) (int64, int64, error) { join := migrationKeyEquality("source", "destination", spec.keyColumns) equal := migrationColumnEquality("source", "destination", columns) + sourcePredicate := migrationRowPredicate(spec, "source") + destinationPredicate := migrationRowPredicate(spec, "destination") // #nosec G201 -- identifiers come from the validated schema inventory. query := fmt.Sprintf( `SELECT COALESCE(SUM(CASE WHEN EXISTS ( - SELECT 1 FROM %s AS destination WHERE %s + SELECT 1 FROM %s AS destination WHERE %s AND %s ) THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN EXISTS ( - SELECT 1 FROM %s AS destination WHERE %s + SELECT 1 FROM %s AS destination WHERE %s AND %s ) AND NOT EXISTS ( - SELECT 1 FROM %s AS destination WHERE %s + SELECT 1 FROM %s AS destination WHERE %s AND %s ) THEN 1 ELSE 0 END), 0) - FROM %s AS source`, + FROM %s AS source WHERE %s`, destinationTable, + destinationPredicate, equal, destinationTable, + destinationPredicate, join, destinationTable, + destinationPredicate, equal, sourceTable, + sourcePredicate, ) var matchedRows, conflictRows int64 @@ -430,20 +447,31 @@ func insertMissingMigrationRows( ) error { columnNames := migrationColumnIdentifiers(columns) selectedColumns := qualifiedMigrationIdentifiers("source", columnNames) + selectModifier := "" + + if spec.deduplicateRows { + selectModifier = "DISTINCT " + } + join := migrationKeyEquality("source", "destination", spec.keyColumns) + sourcePredicate := migrationRowPredicate(spec, "source") + destinationPredicate := migrationRowPredicate(spec, "destination") // #nosec G201 -- identifiers come from the validated schema inventory. query := fmt.Sprintf( `INSERT INTO %s(%s) - SELECT %s FROM %s AS source - WHERE NOT EXISTS ( - SELECT 1 FROM %s AS destination WHERE %s + SELECT %s%s FROM %s AS source + WHERE %s AND NOT EXISTS ( + SELECT 1 FROM %s AS destination WHERE %s AND %s )`, destinationTable, strings.Join(columnNames, ", "), + selectModifier, strings.Join(selectedColumns, ", "), sourceTable, + sourcePredicate, destinationTable, + destinationPredicate, join, ) @@ -512,9 +540,10 @@ func migrationTableRowCount( ctx context.Context, database migrationQueryer, table string, + spec migrationTableSpec, ) (int64, error) { // #nosec G201 -- table comes from the validated schema inventory. - query := "SELECT COUNT(*) FROM " + table + query := "SELECT COUNT(*) FROM " + table + " WHERE " + migrationRowPredicate(spec, "") var count int64 @@ -563,9 +592,10 @@ func queryMigrationRows( ) (*sql.Rows, error) { // #nosec G201 -- table and columns come from the validated schema inventory. query := fmt.Sprintf( - "SELECT %s FROM %s ORDER BY %s", + "SELECT %s FROM %s WHERE %s ORDER BY %s", strings.Join(migrationColumnIdentifiers(columns), ", "), table, + migrationRowPredicate(spec, ""), strings.Join(quoteMigrationIdentifiers(spec.keyColumns), ", "), ) @@ -577,6 +607,19 @@ func queryMigrationRows( return rows, nil } +func migrationRowPredicate(spec migrationTableSpec, alias string) string { + if !spec.excludeVersionRow { + return "TRUE" + } + + column := quoteMigrationIdentifier("key") + if alias != "" { + column = alias + "." + column + } + + return column + " != 'schema_version'" +} + func hashMigrationRows( rows *sql.Rows, columns []migrationColumn, diff --git a/go/internal/codeintel/store_migration_schema.go b/go/internal/codeintel/store_migration_schema.go index c581c248..f9b08a5c 100644 --- a/go/internal/codeintel/store_migration_schema.go +++ b/go/internal/codeintel/store_migration_schema.go @@ -16,6 +16,8 @@ type migrationTableSpec struct { name string keyColumns []string logicalPrimaryKey bool + deduplicateRows bool + excludeVersionRow bool } type migrationColumn struct { @@ -30,7 +32,11 @@ type migrationSchema map[string][]migrationColumn func migrationTableSpecs() []migrationTableSpec { return []migrationTableSpec{ {name: "code_intel_events", keyColumns: []string{"event_id"}}, - {name: "schema_metadata", keyColumns: []string{"key"}}, + { + name: "schema_metadata", + keyColumns: []string{"key"}, + excludeVersionRow: true, + }, {name: "traces", keyColumns: []string{"trace_id"}}, {name: "findings", keyColumns: []string{"finding_id"}}, {name: "finding_occurrences", keyColumns: []string{"trace_id", "ordinal"}}, @@ -80,11 +86,13 @@ func migrationTableSpecs() []migrationTableSpec { name: "code_intel_fts", keyColumns: []string{"fts_id"}, logicalPrimaryKey: true, + deduplicateRows: true, }, { name: "code_intel_search_terms", keyColumns: []string{"fts_id", "term"}, logicalPrimaryKey: true, + deduplicateRows: true, }, } } @@ -132,9 +140,9 @@ func validateMigrationSchemaVersion(ctx context.Context, database *sql.DB) error return fmt.Errorf("parse code-intel migration schema version %q: %w", rawVersion, err) } - if version != schemaVersion { + if version < 1 || version > schemaVersion { return fmt.Errorf( - "%w: schema version is %d, expected %d", + "%w: schema version is %d, supported range is 1 through %d", errStoreMigrationIntegrity, version, schemaVersion, diff --git a/go/internal/codeintel/store_migration_test.go b/go/internal/codeintel/store_migration_test.go index adfdce0c..31c95b74 100644 --- a/go/internal/codeintel/store_migration_test.go +++ b/go/internal/codeintel/store_migration_test.go @@ -119,7 +119,7 @@ func TestMigrateStoreRejectsUnequalDuplicateRows(t *testing.T) { } } -func TestMigrateStorePreservesEqualLogicalKeyDuplicates(t *testing.T) { +func TestMigrateStorePreservesAndDeduplicatesEqualLogicalKeyRows(t *testing.T) { tests := []struct { name string sourceCopies int @@ -170,25 +170,45 @@ func TestMigrateStorePreservesEqualLogicalKeyDuplicates(t *testing.T) { t.Fatalf("migrate equal logical-key duplicates: %v", err) } - for _, tableName := range []string{ + lshEvidence := migrationTableEvidence(t, result.Manifest.Tables, "lsh_bands") + if lshEvidence.SourceRows != int64(test.sourceCopies) || + lshEvidence.ImportedRows != test.wantImported || + lshEvidence.MatchedRows != test.wantMatched || + lshEvidence.DestinationRows != test.wantDestinationRows { + t.Fatalf("unexpected lsh_bands evidence: %#v", lshEvidence) + } + assertMigrationTableRowCount( + t, + ctx, + destinationPath, "lsh_bands", + test.wantDestinationRows, + ) + + for _, tableName := range []string{ "code_intel_fts", "code_intel_search_terms", } { evidence := migrationTableEvidence(t, result.Manifest.Tables, tableName) + wantImported := min(test.wantImported, 1) + wantDestinationRows := min(test.wantDestinationRows, 1) + wantDeduplicated := int64(0) + if test.destinationCopies == 0 { + wantDeduplicated = int64(test.sourceCopies) - wantImported + } if evidence.SourceRows != int64(test.sourceCopies) || - evidence.ImportedRows != test.wantImported || + evidence.ImportedRows != wantImported || evidence.MatchedRows != test.wantMatched || - evidence.DestinationRows != test.wantDestinationRows { + evidence.DeduplicatedRows != wantDeduplicated || + evidence.DestinationRows != wantDestinationRows { t.Fatalf("unexpected %s evidence: %#v", tableName, evidence) } - assertMigrationTableRowCount( t, ctx, destinationPath, tableName, - test.wantDestinationRows, + wantDestinationRows, ) } }) @@ -222,9 +242,15 @@ func TestMigrateStoreRejectsConflictingLogicalKeyVariants(t *testing.T) { SourcePath: sourcePath, DestinationPath: destinationPath, }) - if err == nil || - !strings.Contains(err.Error(), "conflicting row variants") || - !strings.Contains(err.Error(), tableName) { + conflictingVariant := err != nil && + strings.Contains(err.Error(), "conflicting row variants") && + strings.Contains(err.Error(), tableName) + conflictingUpgrade := err != nil && tableName == "code_intel_fts" && + strings.Contains( + err.Error(), + "conflicting code intelligence FTS rows share identity", + ) + if !conflictingVariant && !conflictingUpgrade { t.Fatalf("expected %s conflict rejection, got %v", tableName, err) } @@ -454,8 +480,25 @@ func insertMigrationLogicalKeySupport( database *sql.DB, ) { t.Helper() + for _, indexName := range []string{ + "idx_code_intel_fts_id_unique", + "idx_code_intel_search_terms_unique", + } { + _, err := database.ExecContext(ctx, "DROP INDEX IF EXISTS "+indexName) + if err != nil { + t.Fatalf("drop v2 logical-key index %s: %v", indexName, err) + } + } _, err := database.ExecContext( + ctx, + "UPDATE schema_metadata SET value = '1' WHERE key = 'schema_version'", + ) + if err != nil { + t.Fatalf("mark logical-key fixture as schema v1: %v", err) + } + + _, err = database.ExecContext( ctx, `INSERT OR IGNORE INTO code_files( path, language, content_hash, size_bytes, line_count, indexed_at_utc @@ -523,9 +566,13 @@ func recordTestMigrationIdentity( repositoryRoot string, ) { t.Helper() - store := openMigrationTestStore(t, ctx, databasePath) + database, err := sql.Open("duckdb", databasePath) + if err != nil { + t.Fatalf("open test migration identity database: %v", err) + } + defer database.Close() - _, err := store.Database().ExecContext( + _, err = database.ExecContext( ctx, `INSERT OR REPLACE INTO schema_metadata(key, value) VALUES ('repository_identity', ?)`, @@ -534,8 +581,6 @@ func recordTestMigrationIdentity( if err != nil { t.Fatalf("record test migration identity: %v", err) } - - closeMigrationTestStore(t, store) } func assertMigrationCodeFileHash( diff --git a/go/internal/codeintel/write.go b/go/internal/codeintel/write.go index d90fbd88..0c5b5d2c 100644 --- a/go/internal/codeintel/write.go +++ b/go/internal/codeintel/write.go @@ -995,11 +995,22 @@ func upsertCodeFile( ) error { _, err := transaction.ExecContext( ctx, - `INSERT OR REPLACE INTO code_files( + `INSERT INTO code_files( path, language, content_hash, parser_name, parser_version, source_mtime_utc, deleted_at_utc, size_bytes, line_count, indexed_at_utc, stale_reason - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(path) DO UPDATE SET + language = excluded.language, + content_hash = excluded.content_hash, + parser_name = excluded.parser_name, + parser_version = excluded.parser_version, + source_mtime_utc = excluded.source_mtime_utc, + deleted_at_utc = excluded.deleted_at_utc, + size_bytes = excluded.size_bytes, + line_count = excluded.line_count, + indexed_at_utc = excluded.indexed_at_utc, + stale_reason = excluded.stale_reason`, file.Path, file.Language, file.ContentHash, @@ -1022,11 +1033,29 @@ func upsertCodeFile( func insertCodeChunk(ctx context.Context, transaction *sql.Tx, chunk CodeChunk) error { _, inlineErrO := transaction.ExecContext( ctx, - `INSERT OR REPLACE INTO code_chunks( + `INSERT INTO code_chunks( chunk_id, path, language, node_kind, symbol_kind, symbol_name, symbol_path, parent_symbol_path, parent_chunk_id, start_byte, end_byte, start_line, end_line, content_hash, normalized_hash, minhash_sig, search_text, raw_text - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(chunk_id) DO UPDATE SET + path = excluded.path, + language = excluded.language, + node_kind = excluded.node_kind, + symbol_kind = excluded.symbol_kind, + symbol_name = excluded.symbol_name, + symbol_path = excluded.symbol_path, + parent_symbol_path = excluded.parent_symbol_path, + parent_chunk_id = excluded.parent_chunk_id, + start_byte = excluded.start_byte, + end_byte = excluded.end_byte, + start_line = excluded.start_line, + end_line = excluded.end_line, + content_hash = excluded.content_hash, + normalized_hash = excluded.normalized_hash, + minhash_sig = excluded.minhash_sig, + search_text = excluded.search_text, + raw_text = excluded.raw_text`, chunk.ID, chunk.Path, chunk.Language, @@ -1065,11 +1094,21 @@ func insertCodeChunk(ctx context.Context, transaction *sql.Tx, chunk CodeChunk) func insertCodeEdge(ctx context.Context, transaction *sql.Tx, edge CodeEdge) error { _, inlineErrP := transaction.ExecContext( ctx, - `INSERT OR REPLACE INTO code_edges( + `INSERT INTO code_edges( edge_id, edge_kind, path, source_chunk_id, target_path, target_chunk_id, target_symbol_path, target_name, provenance_class, raw_text - ) VALUES (?, ?, ?, NULLIF(?, ''), ?, NULLIF(?, ''), ?, ?, ?, ?)`, + ) VALUES (?, ?, ?, NULLIF(?, ''), ?, NULLIF(?, ''), ?, ?, ?, ?) + ON CONFLICT(edge_id) DO UPDATE SET + edge_kind = excluded.edge_kind, + path = excluded.path, + source_chunk_id = excluded.source_chunk_id, + target_path = excluded.target_path, + target_chunk_id = excluded.target_chunk_id, + target_symbol_path = excluded.target_symbol_path, + target_name = excluded.target_name, + provenance_class = excluded.provenance_class, + raw_text = excluded.raw_text`, edge.ID, edge.Kind, edge.Path, @@ -1292,7 +1331,16 @@ func insertFTS(ctx context.Context, transaction *sql.Tx, row ftsRow) error { ctx, `INSERT INTO code_intel_fts( fts_id, kind, record_id, trace_id, policy_id, skill_id, path, message, search_text - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(fts_id) DO UPDATE SET + kind = excluded.kind, + record_id = excluded.record_id, + trace_id = excluded.trace_id, + policy_id = excluded.policy_id, + skill_id = excluded.skill_id, + path = excluded.path, + message = excluded.message, + search_text = excluded.search_text`, rowID, row.Kind, row.RecordID, @@ -1307,7 +1355,7 @@ func insertFTS(ctx context.Context, transaction *sql.Tx, row ftsRow) error { return fmt.Errorf("insert code intelligence FTS row: %w", err) } - return insertSearchTerms(ctx, transaction, rowID, row.SearchText) + return synchronizeSearchTerms(ctx, transaction, rowID, row.SearchText) } func ftsRowID(row ftsRow) string { @@ -1323,7 +1371,14 @@ func insertSearchTerms( for _, term := range searchTerms(text) { _, err := transaction.ExecContext( ctx, - `INSERT INTO code_intel_search_terms(term, fts_id) VALUES (?, ?)`, + `INSERT INTO code_intel_search_terms(term, fts_id) + SELECT ?, ? + WHERE NOT EXISTS ( + SELECT 1 FROM code_intel_search_terms + WHERE term = ? AND fts_id = ? + )`, + term, + rowID, term, rowID, ) @@ -1335,6 +1390,79 @@ func insertSearchTerms( return nil } +func synchronizeSearchTerms( + ctx context.Context, + transaction *sql.Tx, + rowID string, + text string, +) error { + desired := searchTerms(text) + desiredSet := make(map[string]bool, len(desired)) + + for _, term := range desired { + desiredSet[term] = true + } + + existing, err := existingSearchTerms(ctx, transaction, rowID) + if err != nil { + return err + } + + for _, term := range existing { + if desiredSet[term] { + continue + } + + _, err = transaction.ExecContext( + ctx, + "DELETE FROM code_intel_search_terms WHERE term = ? AND fts_id = ?", + term, + rowID, + ) + if err != nil { + return fmt.Errorf("delete stale code intelligence search term: %w", err) + } + } + + return insertSearchTerms(ctx, transaction, rowID, text) +} + +func existingSearchTerms( + ctx context.Context, + transaction *sql.Tx, + rowID string, +) ([]string, error) { + rows, err := transaction.QueryContext( + ctx, + "SELECT term FROM code_intel_search_terms WHERE fts_id = ?", + rowID, + ) + if err != nil { + return nil, fmt.Errorf("query existing code intelligence search terms: %w", err) + } + defer rows.Close() + + existing := []string{} + + for rows.Next() { + var term string + + err = rows.Scan(&term) + if err != nil { + return nil, fmt.Errorf("scan existing code intelligence search term: %w", err) + } + + existing = append(existing, term) + } + + err = rows.Err() + if err != nil { + return nil, fmt.Errorf("iterate existing code intelligence search terms: %w", err) + } + + return existing, nil +} + func remediationSearchText(remediation agentmsg.Remediation) string { return strings.Join(compactStrings([]string{ remediation.PolicyID, diff --git a/go/internal/hookrunnercli/export.go b/go/internal/hookrunnercli/export.go index 5b7ecab2..438ca96c 100644 --- a/go/internal/hookrunnercli/export.go +++ b/go/internal/hookrunnercli/export.go @@ -13,6 +13,14 @@ func Run(args []string) int { return 1 } + restoreCacheEnvironment, err := prepareHookProcessCacheEnvironment(repoRoot()) + if err != nil { + writef(os.Stderr, "FATAL: prepare hook cache environment: %v\n", err) + + return 1 + } + defer restoreCacheEnvironment() + cfg, err := loadConfig() if err != nil { writef(os.Stderr, "FATAL: %v\n", err) diff --git a/go/internal/hookrunnercli/external_tool.go b/go/internal/hookrunnercli/external_tool.go index 92d7c6da..2fecf4d8 100644 --- a/go/internal/hookrunnercli/external_tool.go +++ b/go/internal/hookrunnercli/external_tool.go @@ -317,6 +317,59 @@ func externalToolCacheEnv(root string) (externalToolCacheEnvironment, error) { }, nil } +// prepareHookProcessCacheEnvironment projects the consumer-owned cache roots +// onto the hook runner itself. Nested pre-commit languages can start before an +// individual external-tool request is constructed; setting these variables at +// the command boundary prevents uv, Go, Cargo, and linters from falling back +// to an unwritable operator cache. The returned closure restores the exact +// caller environment for in-process tests and embedded invocations. +func prepareHookProcessCacheEnvironment(root string) (func(), error) { + environment, err := externalToolCacheEnv(root) + if err != nil { + return nil, err + } + + type priorValue struct { + value string + existed bool + } + + prior := map[string]priorValue{} + + for _, item := range environment.items() { + name, value, found := strings.Cut(item, "=") + if !found { + continue + } + + previous, existed := os.LookupEnv(name) + prior[name] = priorValue{value: previous, existed: existed} + + setErr := os.Setenv(name, value) + if setErr != nil { + for restoreName, restoreValue := range prior { + if restoreValue.existed { + _ = os.Setenv(restoreName, restoreValue.value) + } else { + _ = os.Unsetenv(restoreName) + } + } + + return nil, fmt.Errorf("set hook process cache environment %s: %w", name, setErr) + } + } + + return func() { + for name, previous := range prior { + if previous.existed { + _ = os.Setenv(name, previous.value) + } else { + _ = os.Unsetenv(name) + } + } + }, nil +} + func (environment externalToolCacheEnvironment) overrides(name string) bool { return environment.value(name) != "" } diff --git a/go/internal/hookrunnercli/external_tool_internal_test.go b/go/internal/hookrunnercli/external_tool_internal_test.go index cfaa0bf5..02380622 100644 --- a/go/internal/hookrunnercli/external_tool_internal_test.go +++ b/go/internal/hookrunnercli/external_tool_internal_test.go @@ -10,8 +10,34 @@ import ( "slices" "strings" "testing" + + "blackcat.ca/coding-ethos/go/internal/testlock" ) +func TestPrepareHookProcessCacheEnvironmentSetsAndRestoresUVCache(t *testing.T) { + testlock.ProcessState(t, "hook-process-cache-environment") + + root := t.TempDir() + t.Setenv("UV_CACHE_DIR", "/previous/uv-cache") + restore, err := prepareHookProcessCacheEnvironment(root) + if err != nil { + t.Fatalf("prepareHookProcessCacheEnvironment: %v", err) + } + + want := filepath.Join(root, ".coding-ethos", "cache", "uv") + if got := os.Getenv("UV_CACHE_DIR"); got != want { + t.Fatalf("UV_CACHE_DIR = %q, want %q", got, want) + } + if info, statErr := os.Stat(want); statErr != nil || !info.IsDir() { + t.Fatalf("UV cache directory is not usable: info=%v error=%v", info, statErr) + } + + restore() + if got := os.Getenv("UV_CACHE_DIR"); got != "/previous/uv-cache" { + t.Fatalf("restored UV_CACHE_DIR = %q", got) + } +} + func TestExternalToolEnvRemovesGitHookLocalEnvironment(t *testing.T) { shimDir := t.TempDir() shimPath := filepath.Join(shimDir, "git") diff --git a/go/internal/hookrunnercli/git_hook.go b/go/internal/hookrunnercli/git_hook.go index 9aa9195f..a2290828 100644 --- a/go/internal/hookrunnercli/git_hook.go +++ b/go/internal/hookrunnercli/git_hook.go @@ -388,11 +388,6 @@ func hookGroupResultFilePath(path string) (string, bool) { return "", false } - tempDir, err := resolvedPath(os.TempDir()) - if err != nil { - return "", false - } - absolutePath, err := filepath.Abs(cleanPath) if err != nil { return "", false @@ -403,16 +398,46 @@ func hookGroupResultFilePath(path string) (string, bool) { return "", false } - relativePath, err := filepath.Rel(tempDir, resolvedTarget) - if err != nil || - relativePath == ".." || - strings.HasPrefix(relativePath, ".."+string(os.PathSeparator)) { + if !pathWithinTemporaryRoots(resolvedTarget) { return "", false } return absolutePath, true } +func pathWithinTemporaryRoots(path string) bool { + for _, root := range temporaryRoots() { + relativePath, err := filepath.Rel(root, path) + if err == nil && + relativePath != ".." && + !strings.HasPrefix(relativePath, ".."+string(os.PathSeparator)) { + return true + } + } + + return false +} + +func temporaryRoots() []string { + roots := []string{} + + for _, candidate := range []string{os.TempDir(), os.Getenv("GOTMPDIR")} { + candidate = strings.TrimSpace(candidate) + if candidate == "" { + continue + } + + resolved, err := resolvedPath(candidate) + if err != nil || slices.Contains(roots, resolved) { + continue + } + + roots = append(roots, resolved) + } + + return roots +} + func resolvedPath(path string) (string, error) { absolutePath, err := filepath.Abs(filepath.Clean(path)) if err != nil { diff --git a/go/internal/hookrunnercli/git_hook_internal_test.go b/go/internal/hookrunnercli/git_hook_internal_test.go index bbd39b50..c0dc8ba0 100644 --- a/go/internal/hookrunnercli/git_hook_internal_test.go +++ b/go/internal/hookrunnercli/git_hook_internal_test.go @@ -246,6 +246,25 @@ func TestHookGroupResultFileRoundTrip(t *testing.T) { } } +func TestHookGroupResultFilePathAcceptsGoTempDir(t *testing.T) { + goTempDir := filepath.Join(t.TempDir(), "go-temp") + if err := os.MkdirAll(goTempDir, 0o700); err != nil { + t.Fatalf("create Go temp dir: %v", err) + } + t.Setenv("GOTMPDIR", goTempDir) + + resultPath := filepath.Join(goTempDir, "result.json") + cleanPath, ok := hookGroupResultFilePath(resultPath) + if !ok || cleanPath != resultPath { + t.Fatalf( + "hookGroupResultFilePath() = %q, %t; want %q, true", + cleanPath, + ok, + resultPath, + ) + } +} + func TestReadHookGroupResultFileRejectsNonTempPath(t *testing.T) { t.Parallel() diff --git a/go/internal/hooks/gate_exit_status.go b/go/internal/hooks/gate_exit_status.go new file mode 100644 index 00000000..eaf9fc4e --- /dev/null +++ b/go/internal/hooks/gate_exit_status.go @@ -0,0 +1,638 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package hooks + +import ( + "fmt" + "path/filepath" + "slices" + "strings" + + "blackcat.ca/coding-ethos/go/internal/shellparse" +) + +const ( + gateExitStatusPolicyID = "shell.required_gate_exit_status" + maxGateShellDepth = 3 + preCommitExecutable = "pre-commit" + pytestExecutable = "pytest" + + gateBackgroundReason = "Required repository gates must return their " + + "terminal status, not run in the background." + gateFallbackReason = "Required repository gate failure cannot be hidden " + + "by a shell fallback." + gatePipelineReason = "Required repository gate pipelines must enable " + + "pipefail so the gate status remains authoritative." + gateSequenceReason = "Commands after a required repository gate must " + + "capture and return the gate's exact exit status." +) + +func requiredGateExitStatusRouteFor(event Event) InspectionRoute { + if event.HookEventName != eventPreToolUse || event.ToolName != toolBash { + return InspectionRoute{} + } + + reason, masked := maskedRequiredGateStatus(event.Command(), false, 0) + if !masked { + return InspectionRoute{} + } + + return InspectionRoute{ + BlockPolicyID: gateExitStatusPolicyID, + Reason: reason, + Block: true, + } +} + +type gateShell struct { + segments [][]string + operators []string +} + +func maskedRequiredGateStatus( + command string, + inheritedPipefail bool, + depth int, +) (string, bool) { + if depth > maxGateShellDepth || strings.TrimSpace(command) == "" { + return "", false + } + + parsed, err := parseGateShell(command) + if err != nil { + return "", false + } + + pipefail := inheritedPipefail || parsed.enablesPipefail() + + for index := range parsed.segments { + reason, masked := parsed.maskedSegmentStatus(index, pipefail, depth) + if masked { + return reason, true + } + } + + return "", false +} + +func (parsed gateShell) maskedSegmentStatus( + index int, + pipefail bool, + depth int, +) (string, bool) { + argv := gateExecutableArgv(parsed.segments[index]) + nested, nestedPipefail, nestedFound := nestedShellScript(argv) + + if nestedFound { + reason, masked := maskedRequiredGateStatus( + nested, + pipefail || nestedPipefail, + depth+1, + ) + if masked { + return reason, true + } + } + + if !requiredGateArgv(argv) { + return "", false + } + + reason := maskedGateOperatorReason(parsed.operators[index:], pipefail) + if reason != "" { + return reason, true + } + + if sequenceAfter(index, parsed.operators) && + !parsed.returnsStatusFrom(index) { + return gateSequenceReason, true + } + + return "", false +} + +func maskedGateOperatorReason(operators []string, pipefail bool) string { + for _, operator := range operators { + switch operator { + case "||": + return gateFallbackReason + case "&": + return gateBackgroundReason + case "|", "|&": + if !pipefail { + return gatePipelineReason + } + } + } + + return "" +} + +func parseGateShell(command string) (gateShell, error) { + fields, err := shellparse.ControlFields(command) + if err != nil { + return gateShell{}, fmt.Errorf("parse shell control fields: %w", err) + } + + parsed := gateShell{} + segment := []string{} + + for _, field := range fields { + if isShellControlToken(field) { + if len(segment) > 0 { + parsed.segments = append(parsed.segments, segment) + segment = nil + } + + parsed.operators = append(parsed.operators, field) + + continue + } + + segment = append(segment, field) + } + + if len(segment) > 0 { + parsed.segments = append(parsed.segments, segment) + } + + validTrailingBackground := len(parsed.operators) == len(parsed.segments) && + len(parsed.operators) > 0 && + parsed.operators[len(parsed.operators)-1] == "&" + + if len(parsed.segments) == 0 || + (len(parsed.operators)+1 != len(parsed.segments) && + !validTrailingBackground) { + return gateShell{}, nil + } + + return parsed, nil +} + +func (parsed gateShell) enablesPipefail() bool { + for _, segment := range parsed.segments { + if isPipefailCommand(gateExecutableArgv(segment)) { + return true + } + } + + return false +} + +func isPipefailCommand(argv []string) bool { + return len(argv) >= 3 && argv[0] == "set" && + argv[1] == "-o" && argv[2] == "pipefail" +} + +func sequenceAfter(gateIndex int, operators []string) bool { + return slices.Contains(operators[gateIndex:], ";") +} + +func (parsed gateShell) returnsStatusFrom(gateIndex int) bool { + relativeIndex := slices.Index(parsed.operators[gateIndex:], ";") + if relativeIndex < 0 { + return true + } + + sequenceIndex := gateIndex + relativeIndex + commandIndex := sequenceIndex + 1 + + if commandIndex >= len(parsed.segments) { + return true + } + + first := parsed.segments[commandIndex] + if directStatusExit(first, commandIndex, len(parsed.segments)) { + return true + } + + statusName, captured := capturedStatusName(first) + if !captured { + return false + } + + last := gateExecutableArgv(parsed.segments[len(parsed.segments)-1]) + if len(last) < 2 || last[0] != "exit" { + return false + } + + return last[1] == "$"+statusName || + last[1] == "${"+statusName+"}" +} + +func directStatusExit(first []string, commandIndex, segmentCount int) bool { + return len(first) >= 2 && first[0] == "exit" && first[1] == "$?" && + commandIndex == segmentCount-1 +} + +func capturedStatusName(first []string) (string, bool) { + if len(first) == 0 { + return "", false + } + + name, value, found := strings.Cut(first[0], "=") + + return name, found && name != "" && value == "$?" +} + +func gateExecutableArgv(segment []string) []string { + argv := gateExecutableFields(segment) + + for len(argv) > 0 { + unwrapped, changed := unwrapGateWrapper(argv) + if !changed { + return argv + } + + argv = unwrapped + } + + return argv +} + +func gateExecutableFields(segment []string) []string { + argv := make([]string, 0, len(segment)) + + for _, field := range segment { + if shellRedirectField(field) || + (len(argv) == 0 && shellAssignment(field)) { + continue + } + + argv = append(argv, field) + } + + return argv +} + +func unwrapGateWrapper(argv []string) ([]string, bool) { + switch filepath.Base(argv[0]) { + case tokenEnv: + return unwrapEnvArgv(argv[1:]), true + case tokenCommand, tokenExec, "nohup", "time": + return unwrapSimpleArgv(argv[1:]), true + case "nice": + return unwrapNiceArgv(argv[1:]), true + case "timeout": + return unwrapTimeoutArgv(argv[1:]), true + case "flock": + return unwrapFlockArgv(argv[1:]), true + case "cerun": + separator := slices.Index(argv, "--") + if separator < 0 { + return argv, false + } + + return argv[separator+1:], true + default: + return argv, false + } +} + +func shellRedirectField(field string) bool { + trimmed := strings.TrimLeft(field, "0123456789") + + return strings.HasPrefix(trimmed, ">") || + strings.HasPrefix(trimmed, "<") +} + +func unwrapEnvArgv(argv []string) []string { + for len(argv) > 0 { + if shellAssignment(argv[0]) { + argv = argv[1:] + + continue + } + + if envOptionConsumesValue(argv[0]) && len(argv) > 1 { + argv = argv[2:] + + continue + } + + if strings.HasPrefix(argv[0], "-") { + argv = argv[1:] + + continue + } + + break + } + + return argv +} + +func envOptionConsumesValue(argument string) bool { + return slices.Contains( + []string{ + "-u", "--unset", "-C", "--chdir", "-S", "--split-string", + }, + argument, + ) +} + +func unwrapSimpleArgv(argv []string) []string { + for len(argv) > 0 && strings.HasPrefix(argv[0], "-") { + argv = argv[1:] + } + + return argv +} + +func unwrapNiceArgv(argv []string) []string { + for len(argv) > 0 { + if slices.Contains( + []string{"-n", "--adjustment"}, + argv[0], + ) && len(argv) > 1 { + argv = argv[2:] + + continue + } + + if strings.HasPrefix(argv[0], "-") { + argv = argv[1:] + + continue + } + + break + } + + return argv +} + +func unwrapTimeoutArgv(argv []string) []string { + for len(argv) > 0 { + if timeoutOptionConsumesValue(argv[0]) && len(argv) > 1 { + argv = argv[2:] + + continue + } + + if strings.HasPrefix(argv[0], "-") { + argv = argv[1:] + + continue + } + + break + } + + return discardWrapperOperand(argv) +} + +func timeoutOptionConsumesValue(argument string) bool { + return slices.Contains( + []string{"-k", "--kill-after", "-s", "--signal"}, + argument, + ) +} + +func unwrapFlockArgv(argv []string) []string { + for len(argv) > 0 { + if flockOptionConsumesValue(argv[0]) && len(argv) > 1 { + argv = argv[2:] + + continue + } + + if strings.HasPrefix(argv[0], "-") { + argv = argv[1:] + + continue + } + + break + } + + return discardWrapperOperand(argv) +} + +func flockOptionConsumesValue(argument string) bool { + return slices.Contains( + []string{"-E", "--conflict-exit-code", "-w", "--wait"}, + argument, + ) +} + +func discardWrapperOperand(argv []string) []string { + if len(argv) == 0 { + return argv + } + + return argv[1:] +} + +func nestedShellScript(argv []string) (string, bool, bool) { + if len(argv) == 0 || !shellExecutable(argv[0]) { + return "", false, false + } + + pipefail := false + + for index := 1; index < len(argv); index++ { + if argv[index] == "-o" && index+1 < len(argv) && + argv[index+1] == "pipefail" { + pipefail = true + index++ + + continue + } + + if strings.HasPrefix(argv[index], "-") && + strings.Contains(argv[index], "c") && index+1 < len(argv) { + return argv[index+1], pipefail, true + } + } + + return "", pipefail, false +} + +func shellExecutable(argument string) bool { + return slices.Contains( + []string{"bash", "dash", "sh"}, + filepath.Base(argument), + ) +} + +func requiredGateArgv(argv []string) bool { + if len(argv) == 0 { + return false + } + + name := filepath.Base(argv[0]) + arguments := argv[1:] + + if name == "make" { + return slices.ContainsFunc(arguments, requiredMakeGateTarget) + } + + return requiredExecutableGate(name, arguments) +} + +func requiredExecutableGate(name string, arguments []string) bool { + switch name { + case "cargo": + return requiredCargoGate(arguments) + case "go": + return requiredGoGate(arguments) + case pythonExecutable, "python3": + return len(arguments) > 1 && arguments[0] == "-m" && + arguments[1] == pytestExecutable + case pytestExecutable, "ghprsq": + return true + case preCommitExecutable: + return commandOperation( + arguments, + map[string]bool{"--color": true}, + ) == "run" + case "uv": + return uvRunsPytest(arguments) + case tokenGit: + return gitArgvOperation(arguments) == gitCommitOperation + default: + return false + } +} + +func requiredMakeGateTarget(argument string) bool { + switch argument { + case "acceptance", "bench", "build", "check", "check-sync", + gitCommitOperation, + "go-e2e-test", "go-test", "heavy", "install", "lint", + "maint-rust-heavy", "mutants", "nextest", preCommitExecutable, + "purrdf-extractor-check", "reason", "reason-verify", "release", + "rust-coverage", "rust-gate", "rust-test", testOperation, "validate", + "wasm-parity": + return true + default: + return false + } +} + +func requiredGoGate(argv []string) bool { + return commandOperation(argv, map[string]bool{"-C": true}) == + testOperation +} + +func requiredCargoGate(argv []string) bool { + operation := commandOperation(argv, map[string]bool{ + "--color": true, + "--config": true, + "--manifest-path": true, + "--target-dir": true, + }) + + return slices.Contains( + []string{ + "bench", "build", "check", "clippy", "nextest", testOperation, + }, + operation, + ) +} + +func commandOperation( + argv []string, + optionsWithValues map[string]bool, +) string { + for index := 0; index < len(argv); index++ { + argument := argv[index] + name, _, hasInlineValue := strings.Cut(argument, "=") + + if optionsWithValues[name] && !hasInlineValue { + index++ + + continue + } + + if strings.HasPrefix(argument, "+") || + strings.HasPrefix(argument, "-") { + continue + } + + return argument + } + + return "" +} + +func uvRunsPytest(argv []string) bool { + operation := commandOperation(argv, map[string]bool{ + "--config-file": true, + "--directory": true, + "--project": true, + }) + if operation != "run" { + return false + } + + runIndex := slices.Index(argv, "run") + if runIndex < 0 { + return false + } + + operation = commandOperation(argv[runIndex+1:], map[string]bool{ + "--directory": true, + "--env-file": true, + "--index": true, + "--index-url": true, + "--python": true, + }) + + return filepath.Base(operation) == pytestExecutable +} + +func gitArgvOperation(argv []string) string { + for index := 0; index < len(argv); index++ { + argument := argv[index] + + if gitGlobalOptionConsumesValue(argument) { + index++ + + continue + } + + if strings.HasPrefix(argument, "-") { + continue + } + + return argument + } + + return "" +} + +func gitGlobalOptionConsumesValue(argument string) bool { + return slices.Contains( + []string{ + "-c", "-C", "--git-dir", "--work-tree", "--namespace", + "--config-env", + }, + argument, + ) +} + +func shellAssignment(value string) bool { + name, _, found := strings.Cut(value, "=") + if !found || name == "" { + return false + } + + for index, character := range name { + if validShellNameCharacter(index, character) { + continue + } + + return false + } + + return true +} + +func validShellNameCharacter(index int, character rune) bool { + return character == '_' || character >= 'a' && character <= 'z' || + character >= 'A' && character <= 'Z' || + index > 0 && character >= '0' && character <= '9' +} diff --git a/go/internal/hooks/gate_exit_status_test.go b/go/internal/hooks/gate_exit_status_test.go new file mode 100644 index 00000000..d96e440b --- /dev/null +++ b/go/internal/hooks/gate_exit_status_test.go @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package hooks + +import "testing" + +func TestRequiredGateExitStatusBlocksMaskedFailures(t *testing.T) { + t.Parallel() + + for _, command := range []string{ + "make check > gate.log 2>&1; echo EXIT_CODE=$? >> gate.log", + "make check | tail -100", + "make check || true", + "bash -c 'make check; echo done'", + "git -c core.useBuiltinFSMonitor=false commit -m verified | tee commit.log", + "cargo --locked test | tee cargo.log", + "go -C ./go test ./... | tee go.log", + "python3 -m pytest | tee pytest.log", + "env -u CI make check | tee make.log", + "nice make check | tee make.log", + "timeout 30s make check | tee make.log", + "flock /tmp/gate.lock make check | tee make.log", + "make go-test | tee go.log", + "make go-e2e-test | tee go-e2e.log", + "make lint | tee lint.log", + "make purrdf-extractor-check | tee purrdf.log", + } { + route := requiredGateExitStatusRouteFor(Event{ + HookEventName: eventPreToolUse, + ToolName: toolBash, + ToolInput: map[string]any{ + "command": command, + }, + }) + if !route.Block || route.BlockPolicyID != gateExitStatusPolicyID { + t.Fatalf("command %q route = %#v, want required-gate block", command, route) + } + } +} + +func TestRequiredGateExitStatusAllowsAuthoritativeStatus(t *testing.T) { + t.Parallel() + + for _, command := range []string{ + "make check", + "make check && echo gate-passed", + "bash -o pipefail -c 'make check | tail -100'", + "set -o pipefail; make check | tail -100", + "make check > gate.log 2>&1; status=$?; echo EXIT_CODE=$status >> gate.log; exit \"$status\"", + "rg -n 'make check' src", + } { + route := requiredGateExitStatusRouteFor(Event{ + HookEventName: eventPreToolUse, + ToolName: toolBash, + ToolInput: map[string]any{ + "command": command, + }, + }) + if route.Block { + t.Fatalf("authoritative command %q was blocked: %#v", command, route) + } + } +} diff --git a/go/internal/hooks/git_wrapper_enforcement.go b/go/internal/hooks/git_wrapper_enforcement.go index 229a0cb1..b52512cc 100644 --- a/go/internal/hooks/git_wrapper_enforcement.go +++ b/go/internal/hooks/git_wrapper_enforcement.go @@ -22,6 +22,10 @@ const ( tokenGit = "git" tokenCommand = "command" tokenEnv = "env" + tokenExec = "exec" + gitCommitOperation = "commit" + pythonExecutable = "python" + testOperation = "test" wrappedToolArgs = 2 cerunRunnerName = "cerun" wrapperRunnerName = "coding-ethos-run" @@ -494,7 +498,7 @@ func cerunAgentShellSegment(segment []string) bool { } switch args[0] { - case "git", "python", "lint": + case tokenGit, pythonExecutable, "lint": return len(args) > 1 case "--", "--rewrite", "--no-rewrite", "--check": return agentShellArgsHaveCommand(args) @@ -823,7 +827,7 @@ func shellCommandIsEvasiveGitInvocation(parsed shellparse.Command) bool { switch name { case shellToolName, "sh", "zsh", "dash": return shellExecArgumentReferencesGit(parsed) - case tokenCommand, tokenEnv, "eval", "alias", "exec": + case tokenCommand, tokenEnv, "eval", "alias", tokenExec: return true default: return pythonGitBypassCommand(name, parsed) @@ -885,7 +889,7 @@ func shellCommandUsesPathOverride(command shellparse.Command) bool { } } - if shellCommandName(command) == "env" { + if shellCommandName(command) == tokenEnv { for _, arg := range command.Argv[1:] { if strings.HasPrefix(arg, "PATH=") { return true diff --git a/go/internal/hooks/lint_tool_capture.go b/go/internal/hooks/lint_tool_capture.go index 589ebae3..98c080fc 100644 --- a/go/internal/hooks/lint_tool_capture.go +++ b/go/internal/hooks/lint_tool_capture.go @@ -237,7 +237,7 @@ func commandLintToolArgs(segment []string) (toolcatalog.CapturedTool, []string, } func envLintToolArgs(segment []string) (toolcatalog.CapturedTool, []string, bool) { - if filepath.Base(segment[0]) != "env" { + if filepath.Base(segment[0]) != tokenEnv { return toolcatalog.CapturedTool{}, nil, false } @@ -382,7 +382,8 @@ func firstMentionedCapturedTool(command string) toolcatalog.CapturedTool { func isPythonCommand(token string) bool { base := filepath.Base(token) - return base == "python" || base == "python3" || strings.HasPrefix(base, "python3.") + return base == pythonExecutable || base == "python3" || + strings.HasPrefix(base, "python3.") } func segmentMentionsUnmanagedLintTool(segment []string) toolcatalog.CapturedTool { @@ -416,7 +417,7 @@ func shellCommandUsesLintToolIndirection(parsed shellparse.Command) bool { switch shellCommandName(parsed) { case "bash", "sh", "zsh", "dash": return shellExecArgumentMentionsCapturedTool(parsed) - case "eval", "alias", "exec": + case "eval", "alias", tokenExec: return shellCommandArgMentionsCapturedTool(parsed) default: return shellPythonCommandMentionsCapturedTool(parsed) diff --git a/go/internal/hooks/normalizer_internal_test.go b/go/internal/hooks/normalizer_internal_test.go new file mode 100644 index 00000000..fb2e5068 --- /dev/null +++ b/go/internal/hooks/normalizer_internal_test.go @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package hooks + +import ( + "path/filepath" + "testing" +) + +func TestHookOutputNormalizerTreatsGoTempDirAsTemporary(t *testing.T) { + goTempDir := filepath.Join(t.TempDir(), "go-temp") + t.Setenv("GOTMPDIR", goTempDir) + + transcript := filepath.Join(goTempDir, "session", "transcript.jsonl") + got := hookOutputNormalizer("/repo").preserveLines(transcript) + if got != "/session/transcript.jsonl" { + t.Fatalf("normalized Go temp path = %q", got) + } +} diff --git a/go/internal/hooks/proxy_output.go b/go/internal/hooks/proxy_output.go index ff63b621..aeee5b0e 100644 --- a/go/internal/hooks/proxy_output.go +++ b/go/internal/hooks/proxy_output.go @@ -1300,7 +1300,7 @@ func inferGoDiagnosticTool(argv []string) string { } switch strings.TrimSpace(argv[index+1]) { - case "test": + case testOperation: return "go-test" case "vet": return "go-vet" diff --git a/go/internal/hooks/runner.go b/go/internal/hooks/runner.go index 0908f76d..8617000d 100644 --- a/go/internal/hooks/runner.go +++ b/go/internal/hooks/runner.go @@ -157,6 +157,7 @@ func routeToolUse(ctx InspectionContext) InspectionRoute { parallelToolBatchRouteFor, memoryRouteFor, malformedShellRouteFor, + requiredGateExitStatusRouteFor, shellFileToolRouteFor, gitWrapperRouteFor, lintToolRouteFor, @@ -733,6 +734,9 @@ func hookOutputNormalizer(cwd string) hookTextNormalizer { } roots = append(roots, hookTextReplacement{Old: os.TempDir(), New: ""}) + if goTempDir := strings.TrimSpace(os.Getenv("GOTMPDIR")); goTempDir != "" { + roots = append(roots, hookTextReplacement{Old: goTempDir, New: ""}) + } replacements := []hookTextReplacement{} diff --git a/go/internal/hooks/semantic_policy_injection.go b/go/internal/hooks/semantic_policy_injection.go index a1e557ac..9eaadb35 100644 --- a/go/internal/hooks/semantic_policy_injection.go +++ b/go/internal/hooks/semantic_policy_injection.go @@ -176,7 +176,7 @@ func semanticGitMutation(operation string) bool { "checkout", "cherry-pick", "clean", - "commit", + gitCommitOperation, "merge", "mv", "pull", diff --git a/go/internal/policy/bundle.go b/go/internal/policy/bundle.go index 01e47b06..29ac0c53 100644 --- a/go/internal/policy/bundle.go +++ b/go/internal/policy/bundle.go @@ -266,18 +266,19 @@ func examplePrinciples() map[string]Principle { func examplePolicies() map[string]Policy { return map[string]Policy{ - "python.conditional_imports": exampleConditionalImportPolicy(), - "python.functional_idioms": exampleFunctionalIdiomPolicy(), - "git.hook_bypass": exampleHookBypassPolicy(), - "git.history_rewrite_prevention": exampleHistoryRewritePreventionPolicy(), - "git.protected_submodule_update": exampleProtectedSubmoduleUpdatePolicy(), - "git.commit_attribution": exampleCommitAttributionPolicy(), - "git.commit_head_advanced": exampleCommitHeadPolicy(), - "git.edit_evasive_git_execution": exampleEditEvasiveGitExecutionPolicy(), - "filesystem.protected_path": exampleProtectedPathPolicy(), - "proxy.search_replace_edit": ProxySearchReplaceEditPolicy(), - "shell.malformed_command": exampleShellMalformedCommandPolicy(), - "shell.forbidden_strings": exampleShellForbiddenStringsPolicy(), + "python.conditional_imports": exampleConditionalImportPolicy(), + "python.functional_idioms": exampleFunctionalIdiomPolicy(), + "git.hook_bypass": exampleHookBypassPolicy(), + "git.history_rewrite_prevention": exampleHistoryRewritePreventionPolicy(), + "git.protected_submodule_update": exampleProtectedSubmoduleUpdatePolicy(), + "git.commit_attribution": exampleCommitAttributionPolicy(), + "git.commit_head_advanced": exampleCommitHeadPolicy(), + "git.edit_evasive_git_execution": exampleEditEvasiveGitExecutionPolicy(), + "filesystem.protected_path": exampleProtectedPathPolicy(), + "proxy.search_replace_edit": ProxySearchReplaceEditPolicy(), + "shell.malformed_command": exampleShellMalformedCommandPolicy(), + "shell.required_gate_exit_status": exampleShellRequiredGateExitStatusPolicy(), + "shell.forbidden_strings": exampleShellForbiddenStringsPolicy(), } } @@ -1082,6 +1083,32 @@ func exampleShellMalformedCommandPolicy() Policy { } } +func exampleShellRequiredGateExitStatusPolicy() Policy { + return Policy{ + ID: "shell.required_gate_exit_status", + Category: "shell", + Source: SourceRef{ + File: "config.yaml", + Path: "shell.required_gate_exit_status", + }, + PrincipleIDs: []string{ + "validation-at-the-gate", + "evidence-based-engineering-and-decision-quality", + }, + DefaultSeverity: "block", + SupportedModes: []string{"block", "record"}, + Message: "Required repository gates must return their own " + + "exact terminal status.", + Suggestion: "Run the gate directly, enable pipefail for pipelines, or capture " + + "and return the gate's exact status.", + DefenseLayers: hookRouteDefenseLayers(), + AppliesTo: AppliesTo{Tools: []string{"Bash"}}, + Evaluators: []Evaluator{ + {Kind: "external", Name: "shell.required_gate_exit_status"}, + }, + } +} + func exampleLinterDispatch() map[string][]string { return map[string][]string{ "files": {"python.conditional_imports", "python.functional_idioms"}, diff --git a/go/internal/policy/hook_route_policies.go b/go/internal/policy/hook_route_policies.go index cadc06c9..45c5ee5b 100644 --- a/go/internal/policy/hook_route_policies.go +++ b/go/internal/policy/hook_route_policies.go @@ -18,6 +18,14 @@ package policy // CEL engine does not make. Registering these does not move enforcement. func hookRoutePolicies(principles map[string]Principle) map[string]Policy { + policies := coreHookRoutePolicies(principles) + policy := requiredGateExitStatusRoutePolicy(principles) + policies[policy.ID] = policy + + return policies +} + +func coreHookRoutePolicies(principles map[string]Principle) map[string]Policy { return map[string]Policy{ "shell.file_tool_emulation": { ID: "shell.file_tool_emulation", @@ -99,6 +107,36 @@ func hookRoutePolicies(principles map[string]Principle) map[string]Policy { } } +func requiredGateExitStatusRoutePolicy( + principles map[string]Principle, +) Policy { + return Policy{ + ID: "shell.required_gate_exit_status", + Category: "shell", + Source: SourceRef{ + File: "config.yaml", + Path: "shell.required_gate_exit_status", + }, + PrincipleIDs: principleRefs( + principles, + "validation-at-the-gate", + "evidence-based-engineering-and-decision-quality", + ), + DefaultSeverity: "block", + SupportedModes: []string{"block", "record"}, + DefenseLayers: hookRouteDefenseLayers(), + Message: "Required repository gates must return their own exact " + + "terminal status.", + Suggestion: "Run the gate directly. If output must be filtered or " + + "logged, enable pipefail or capture the gate status immediately " + + "and exit with that exact value.", + AppliesTo: AppliesTo{Tools: []string{"Bash"}}, + Evaluators: []Evaluator{ + {Kind: "external", Name: "shell.required_gate_exit_status"}, + }, + } +} + // hookRouteDefenseLayers describes where these blocks actually happen. // // At the hook, before the tool runs, and nowhere else. The generic code layers diff --git a/go/internal/policygitcli/main.go b/go/internal/policygitcli/main.go index dd7e3a77..77af9fae 100644 --- a/go/internal/policygitcli/main.go +++ b/go/internal/policygitcli/main.go @@ -5,7 +5,6 @@ package policygitcli import ( "bytes" - "flag" "fmt" "io" "os" @@ -26,6 +25,18 @@ var ( errAdminApprovedRequired = apperror.StaticError( "admin-start-branch requires --admin-approved", ) + errWrapperBoolValue = apperror.StaticError( + "policy-git boolean option accepts only true or false", + ) + errWrapperOptionEmpty = apperror.StaticError( + "policy-git option requires a non-empty value", + ) + errWrapperOptionUnknown = apperror.StaticError( + "unknown policy-git option", + ) + errWrapperValueRequired = apperror.StaticError( + "policy-git option requires a value", + ) ) func run() error { @@ -33,32 +44,21 @@ func run() error { } func runWithArgs(args []string) error { - flags := flag.NewFlagSet("coding-ethos-git", flag.ExitOnError) - bundlePath := flags.String("bundle", "", "Path to policy-bundle.json") - realGit := flags.String("real-git", "", "Real git executable") - checkOnly := flags.Bool("check-only", false, "Check policy without executing git") - jsonOutput := flags.Bool("json", false, "Emit JSON result") - adminApproved := flags.Bool( - "admin-approved", - false, - "Allow admin-protected coding-ethos commits when process ancestry is approved", - ) - - err := flags.Parse(args) + parsed, err := parsePolicyGitArgs(args) if err != nil { return fmt.Errorf("parse flags: %w", err) } - if *bundlePath == "" { + if parsed.bundlePath == "" { return errBundleRequired } - bundle, err := readValidatedBundle(*bundlePath) + bundle, err := readValidatedBundle(parsed.bundlePath) if err != nil { return err } - argv := flags.Args() + argv := parsed.gitArgv cwd, err := os.Getwd() if err != nil { @@ -66,13 +66,13 @@ func runWithArgs(args []string) error { } if len(argv) > 0 && argv[0] == "admin-start-branch" { - return startAdminBranch(*realGit, cwd, argv[1:], *adminApproved) + return startAdminBranch(parsed.realGit, cwd, argv[1:], parsed.adminApproved) } - restoreRealGit := exposeRealGitForPolicyEvaluation(*realGit) + restoreRealGit := exposeRealGitForPolicyEvaluation(parsed.realGit) defer restoreRealGit() - options, err := gitOptions(argv, cwd, *adminApproved) + options, err := gitOptions(argv, cwd, parsed.adminApproved) if err != nil { return err } @@ -82,7 +82,7 @@ func runWithArgs(args []string) error { return fmt.Errorf("check git policy: %w", err) } - err = maybePrintJSON(*jsonOutput, result) + err = maybePrintJSON(parsed.jsonOutput, result) if err != nil { return err } @@ -93,13 +93,167 @@ func runWithArgs(args []string) error { return gitwrap.ExitCodeError{Code: blockedExitCode} } - if *checkOnly { - printAllowedCheck(*jsonOutput) + if parsed.checkOnly { + printAllowedCheck(parsed.jsonOutput) return nil } - return executeGitWithPostChecks(bundle, *realGit, options, *jsonOutput) + return executeGitWithPostChecks(bundle, parsed.realGit, options, parsed.jsonOutput) +} + +type policyGitArguments struct { + bundlePath string + realGit string + gitArgv []string + checkOnly bool + jsonOutput bool + adminApproved bool +} + +func parsePolicyGitArgs(args []string) (policyGitArguments, error) { + parsed := policyGitArguments{} + + for index := 0; index < len(args); { + argument := args[index] + + if argument == "--" { + parsed.gitArgv = append([]string(nil), args[index+1:]...) + + return parsed, nil + } + + if gitGlobalOptionStartsArgv(argument) || !strings.HasPrefix(argument, "-") { + parsed.gitArgv = append([]string(nil), args[index:]...) + + return parsed, nil + } + + nextIndex, err := parsed.consumeWrapperOption(args, index) + if err != nil { + return policyGitArguments{}, err + } + + index = nextIndex + } + + return parsed, nil +} + +func (parsed *policyGitArguments) consumeWrapperOption( + args []string, + index int, +) (int, error) { + argument := args[index] + name, value, hasValue := strings.Cut(argument, "=") + + switch name { + case "--bundle", "--real-git": + resolved, nextIndex, err := wrapperStringValue( + args, + index, + name, + value, + hasValue, + ) + if err != nil { + return 0, err + } + + parsed.setStringOption(name, resolved) + + return nextIndex, nil + case "--check-only", "--json", "--admin-approved": + boolValue, err := wrapperBoolValue(name, value, hasValue) + if err != nil { + return 0, err + } + + parsed.setBoolOption(name, boolValue) + + return index + 1, nil + default: + return 0, fmt.Errorf("%w %q", errWrapperOptionUnknown, argument) + } +} + +func wrapperStringValue( + args []string, + index int, + name string, + value string, + hasValue bool, +) (string, int, error) { + if hasValue { + if value == "" { + return "", 0, fmt.Errorf("%w: %s", errWrapperOptionEmpty, name) + } + + return value, index + 1, nil + } + + nextIndex := index + 1 + if nextIndex >= len(args) { + return "", 0, fmt.Errorf("%w: %s", errWrapperValueRequired, name) + } + + value = args[nextIndex] + if value == "" { + return "", 0, fmt.Errorf("%w: %s", errWrapperOptionEmpty, name) + } + + return value, nextIndex + 1, nil +} + +func (parsed *policyGitArguments) setStringOption(name, value string) { + if name == "--bundle" { + parsed.bundlePath = value + + return + } + + parsed.realGit = value +} + +func (parsed *policyGitArguments) setBoolOption(name string, value bool) { + switch name { + case "--check-only": + parsed.checkOnly = value + case "--json": + parsed.jsonOutput = value + case "--admin-approved": + parsed.adminApproved = value + } +} + +func wrapperBoolValue(name, value string, hasValue bool) (bool, error) { + if !hasValue || value == "true" { + return true, nil + } + + if value == "false" { + return false, nil + } + + return false, fmt.Errorf("%w: %s", errWrapperBoolValue, name) +} + +func gitGlobalOptionStartsArgv(argument string) bool { + if strings.HasPrefix(argument, "-c=") || strings.HasPrefix(argument, "-C=") { + return true + } + + name, _, _ := strings.Cut(argument, "=") + switch name { + case "-c", "-C", "-p", "-P", "--paginate", "--no-pager", "--no-replace-objects", + "--bare", "--git-dir", "--work-tree", "--namespace", "--super-prefix", + "--exec-path", "--html-path", "--man-path", "--info-path", "--config-env", + "--literal-pathspecs", "--glob-pathspecs", "--noglob-pathspecs", "--icase-pathspecs", + "--version", "--help": + return true + default: + return false + } } func exposeRealGitForPolicyEvaluation(path string) func() { diff --git a/go/internal/policygitcli/main_internal_test.go b/go/internal/policygitcli/main_internal_test.go index 201d8ba6..6187d5bf 100644 --- a/go/internal/policygitcli/main_internal_test.go +++ b/go/internal/policygitcli/main_internal_test.go @@ -67,6 +67,66 @@ func TestGitCommitReadsMessageFromStdin(t *testing.T) { } } +func TestParsePolicyGitArgsPreservesGitGlobalOptions(t *testing.T) { + t.Parallel() + + parsed, err := parsePolicyGitArgs([]string{ + "--bundle", "/policy/bundle.json", + "--real-git=/usr/bin/git", + "--check-only", + "-c", "core.useBuiltinFSMonitor=false", + "init", "--template=/tmp/hooks", + }) + if err != nil { + t.Fatalf("parsePolicyGitArgs: %v", err) + } + + if parsed.bundlePath != "/policy/bundle.json" || parsed.realGit != "/usr/bin/git" || + !parsed.checkOnly { + t.Fatalf("wrapper options = %#v", parsed) + } + want := "-c core.useBuiltinFSMonitor=false init --template=/tmp/hooks" + if got := strings.Join(parsed.gitArgv, " "); got != want { + t.Fatalf("git argv = %q, want %q", got, want) + } +} + +func TestParsePolicyGitArgsHonorsExplicitBoundary(t *testing.T) { + t.Parallel() + + parsed, err := parsePolicyGitArgs([]string{ + "--bundle=/policy/bundle.json", + "--json=false", + "--", + "--future-git-global", "value", "status", + }) + if err != nil { + t.Fatalf("parsePolicyGitArgs: %v", err) + } + if parsed.jsonOutput { + t.Fatal("--json=false was not retained") + } + if got := strings.Join( + parsed.gitArgv, + " ", + ); got != "--future-git-global value status" { + t.Fatalf("git argv = %q", got) + } +} + +func TestParsePolicyGitArgsRejectsUnknownWrapperOption(t *testing.T) { + t.Parallel() + + _, err := parsePolicyGitArgs([]string{ + "--bundle=/policy/bundle.json", + "--typo-wrapper-option", + "status", + }) + if err == nil || !strings.Contains(err.Error(), "unknown policy-git option") { + t.Fatalf("error = %v, want unknown wrapper option", err) + } +} + func TestGitOptionsForNonStdinCommand(t *testing.T) { t.Parallel() diff --git a/pre-commit/PRE-COMMIT.md b/pre-commit/PRE-COMMIT.md index 9418e5c6..77396139 100644 --- a/pre-commit/PRE-COMMIT.md +++ b/pre-commit/PRE-COMMIT.md @@ -166,18 +166,20 @@ Gemini `cachedContents` entries when the same batch corpus is reviewed by multiple prompts, and can run `standard`, `flex`, or `priority` requests from merged `config.yaml` plus `repo_config.yaml`. -The hook runtime is built into the checked-out `coding-ethos` repository: +The selected `coding-ethos` authority builds the hook runtime: - `bin/` contains built hook and policy binaries. - `build/policy/` contains the compiled policy bundle and source-hash metadata. -The old `.git/coding-ethos-hooks/` runtime cache is legacy. The current runtime -model is documented in `docs/HOOK_RUNTIME_BOOTSTRAP.md`: installed consumer -hooks are generated runner entrypoint scripts; all repo-discovery, build-repair, -and dispatch -behavior lives in compiled Go while binaries and compiled runtime files are -built and executed from the checked-out `coding-ethos` repository. +The current runtime model is documented in +`docs/HOOK_RUNTIME_BOOTSTRAP.md`: installed consumer hooks are generated runner +entrypoint scripts that dispatch to the stable Git-common +`.git/coding-ethos-hooks/` projection. `parent-install` atomically refreshes and +hash-verifies its compiled Go executables from the selected authority, while +`make build` refreshes the complete policy and toolchain projection. Hook +execution therefore does not depend on the lifetime or visibility of one +worktree checkout. The same wrapper also exposes local policy-runtime entrypoints: diff --git a/pre-commit/hooks/HOOKS.md b/pre-commit/hooks/HOOKS.md index aeb653ea..43113c19 100644 --- a/pre-commit/hooks/HOOKS.md +++ b/pre-commit/hooks/HOOKS.md @@ -5,12 +5,14 @@ Go-backed Git hooks for coding-ethos bundles. -Installed consumer repository shims are intentionally thin. They discover the -consumer repo, locate the checked-out `coding-ethos` bundle, repair missing -checkout-local runtime artifacts with `make -C build`, and -dispatch to binaries under `coding-ethos/bin/`. Policy selection and strict -policy freshness checks stay inside the `coding-ethos` checkout; lifecycle -hooks do not use a consumer `.git/coding-ethos-hooks` runtime cache. +Installed consumer repository shims are intentionally thin. They dispatch to +the compiled runner under the repository's stable Git-common +`.git/coding-ethos-hooks/bin/` projection. The selected `coding-ethos` +authority builds that projection; `parent-install` atomically refreshes and +hash-verifies its Go executables, and `make build` refreshes the complete policy +and toolchain runtime. Policy selection and strict freshness checks remain in +compiled Coding Ethos code rather than in the shim, and no hook points at a +worktree-local build path. The Go runner is the output-control layer for Git hooks. It supports `hooks.output_format` values of `auto`, `human`, `json`, and `toon`; `auto` From 0f2535b5d8e3b81867ad291f33626f7105c4e52e Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sun, 30 Aug 2026 23:36:53 -0600 Subject: [PATCH 05/16] fix(hooks): keep uv project environments writable --- docs/HOOK_RUNTIME_BOOTSTRAP.md | 3 ++ go/internal/hookrunnercli/external_tool.go | 15 ++++++++- .../external_tool_internal_test.go | 31 ++++++++++++++++--- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/docs/HOOK_RUNTIME_BOOTSTRAP.md b/docs/HOOK_RUNTIME_BOOTSTRAP.md index 282c8a40..5921ceaf 100644 --- a/docs/HOOK_RUNTIME_BOOTSTRAP.md +++ b/docs/HOOK_RUNTIME_BOOTSTRAP.md @@ -219,6 +219,9 @@ Bootstrap needs a few guardrails: - Keep authority build outputs under ignored `bin/` and `build/` directories. - Keep response, trace, and other transient repo-local caches under ignored `.coding-ethos/` paths, not under the Git common runtime. +- Hook-launched `uv` commands bind both their download cache and project + environment to the consumer-owned `.coding-ethos/cache/` tree. The installed + shared runtime remains read-only and never receives a generated `.venv`. - Install common-runtime executables with temporary-file sync plus atomic rename, and verify them by content rather than mtime. - Keep installed hook entrypoints stable and move versioned behavior into the diff --git a/go/internal/hookrunnercli/external_tool.go b/go/internal/hookrunnercli/external_tool.go index 2fecf4d8..ceff1d46 100644 --- a/go/internal/hookrunnercli/external_tool.go +++ b/go/internal/hookrunnercli/external_tool.go @@ -227,6 +227,7 @@ type externalToolCacheEnvironment struct { GoCache string GolangCILintDir string UVCache string + UVProjectEnv string // CargoTarget is per-repository build output, like GoCache. CargoTarget string // CargoHome and RustupHome are the operator's, not the repository's. Cargo @@ -291,9 +292,17 @@ func externalToolCacheEnv(root string) (externalToolCacheEnvironment, error) { goCache := filepath.Join(root, sandbox.SandboxGoCachePath) golangCILintDir := filepath.Join(root, sandbox.SandboxGolangCIPath) uvCache := filepath.Join(root, ".coding-ethos", "cache", "uv") + uvProjectEnv := filepath.Join(root, ".coding-ethos", "cache", "uv-project-env") cargoTarget := filepath.Join(root, ".coding-ethos", "cache", "cargo-target") - for _, dir := range []string{goTemp, goCache, golangCILintDir, uvCache, cargoTarget} { + for _, dir := range []string{ + goTemp, + goCache, + golangCILintDir, + uvCache, + uvProjectEnv, + cargoTarget, + } { err := os.MkdirAll(dir, externalToolCacheDirMode) if err != nil { return externalToolCacheEnvironment{}, fmt.Errorf( @@ -311,6 +320,7 @@ func externalToolCacheEnv(root string) (externalToolCacheEnvironment, error) { GoCache: goCache, GolangCILintDir: golangCILintDir, UVCache: uvCache, + UVProjectEnv: uvProjectEnv, CargoTarget: cargoTarget, CargoHome: cargoHome, RustupHome: rustupHome, @@ -383,6 +393,7 @@ func (environment externalToolCacheEnvironment) items() []string { "GOCACHE", "GOLANGCI_LINT_CACHE", "UV_CACHE_DIR", + "UV_PROJECT_ENVIRONMENT", "CARGO_TARGET_DIR", "CARGO_HOME", "RUSTUP_HOME", @@ -408,6 +419,8 @@ func (environment externalToolCacheEnvironment) value(name string) string { return environment.GolangCILintDir case "UV_CACHE_DIR": return environment.UVCache + case "UV_PROJECT_ENVIRONMENT": + return environment.UVProjectEnv case "CARGO_TARGET_DIR": return environment.CargoTarget case "CARGO_HOME": diff --git a/go/internal/hookrunnercli/external_tool_internal_test.go b/go/internal/hookrunnercli/external_tool_internal_test.go index 02380622..49206925 100644 --- a/go/internal/hookrunnercli/external_tool_internal_test.go +++ b/go/internal/hookrunnercli/external_tool_internal_test.go @@ -14,28 +14,42 @@ import ( "blackcat.ca/coding-ethos/go/internal/testlock" ) -func TestPrepareHookProcessCacheEnvironmentSetsAndRestoresUVCache(t *testing.T) { +func TestPrepareHookProcessCacheEnvironmentSetsAndRestoresUVEnvironment(t *testing.T) { testlock.ProcessState(t, "hook-process-cache-environment") root := t.TempDir() t.Setenv("UV_CACHE_DIR", "/previous/uv-cache") + t.Setenv("UV_PROJECT_ENVIRONMENT", "/previous/uv-project-environment") restore, err := prepareHookProcessCacheEnvironment(root) if err != nil { t.Fatalf("prepareHookProcessCacheEnvironment: %v", err) } - want := filepath.Join(root, ".coding-ethos", "cache", "uv") - if got := os.Getenv("UV_CACHE_DIR"); got != want { - t.Fatalf("UV_CACHE_DIR = %q, want %q", got, want) + wantCache := filepath.Join(root, ".coding-ethos", "cache", "uv") + if got := os.Getenv("UV_CACHE_DIR"); got != wantCache { + t.Fatalf("UV_CACHE_DIR = %q, want %q", got, wantCache) } - if info, statErr := os.Stat(want); statErr != nil || !info.IsDir() { + if info, statErr := os.Stat(wantCache); statErr != nil || !info.IsDir() { t.Fatalf("UV cache directory is not usable: info=%v error=%v", info, statErr) } + wantProjectEnv := filepath.Join(root, ".coding-ethos", "cache", "uv-project-env") + if got := os.Getenv("UV_PROJECT_ENVIRONMENT"); got != wantProjectEnv { + t.Fatalf("UV_PROJECT_ENVIRONMENT = %q, want %q", got, wantProjectEnv) + } + if info, statErr := os.Stat(wantProjectEnv); statErr != nil || !info.IsDir() { + t.Fatalf("UV project environment is not usable: info=%v error=%v", info, statErr) + } + restore() if got := os.Getenv("UV_CACHE_DIR"); got != "/previous/uv-cache" { t.Fatalf("restored UV_CACHE_DIR = %q", got) } + if got := os.Getenv( + "UV_PROJECT_ENVIRONMENT", + ); got != "/previous/uv-project-environment" { + t.Fatalf("restored UV_PROJECT_ENVIRONMENT = %q", got) + } } func TestExternalToolEnvRemovesGitHookLocalEnvironment(t *testing.T) { @@ -131,6 +145,13 @@ func TestExternalToolEnvRemovesGitHookLocalEnvironment(t *testing.T) { t.Fatalf("externalToolEnv did not set uv cache dir: %#v", env) } + if !slices.Contains( + env, + "UV_PROJECT_ENVIRONMENT="+filepath.Join(repo, ".coding-ethos/cache/uv-project-env"), + ) { + t.Fatalf("externalToolEnv did not set uv project environment: %#v", env) + } + for _, item := range env { if !strings.HasPrefix(item, "PATH=") { continue From 900d3df121e64d90cd6f7b3de5ab6e563d96f32a Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Mon, 31 Aug 2026 00:25:44 -0600 Subject: [PATCH 06/16] fix(hooks): seal uv project dependencies --- .gitignore | 1 + docs/HOOK_RUNTIME_BOOTSTRAP.md | 6 +- go/internal/hookrunnercli/external_tool.go | 5 + .../external_tool_internal_test.go | 12 + pre-commit/hooks/uv.lock | 1459 +++++++++++++++++ 5 files changed, 1481 insertions(+), 2 deletions(-) create mode 100644 pre-commit/hooks/uv.lock diff --git a/.gitignore b/.gitignore index 71e90a56..b7753e2b 100644 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,7 @@ ehthumbs.db *.swp *.swo *.lock +!pre-commit/hooks/uv.lock # Generated repo-root markdown and tool config files are intentionally versioned. # Do not ignore: diff --git a/docs/HOOK_RUNTIME_BOOTSTRAP.md b/docs/HOOK_RUNTIME_BOOTSTRAP.md index 5921ceaf..a1460776 100644 --- a/docs/HOOK_RUNTIME_BOOTSTRAP.md +++ b/docs/HOOK_RUNTIME_BOOTSTRAP.md @@ -220,8 +220,10 @@ Bootstrap needs a few guardrails: - Keep response, trace, and other transient repo-local caches under ignored `.coding-ethos/` paths, not under the Git common runtime. - Hook-launched `uv` commands bind both their download cache and project - environment to the consumer-owned `.coding-ethos/cache/` tree. The installed - shared runtime remains read-only and never receives a generated `.venv`. + environment to the consumer-owned `.coding-ethos/cache/` tree and use the + sealed project's committed lockfile in frozen mode. The installed shared + runtime remains read-only and never receives a generated `.venv` or a lockfile + rewrite. - Install common-runtime executables with temporary-file sync plus atomic rename, and verify them by content rather than mtime. - Keep installed hook entrypoints stable and move versioned behavior into the diff --git a/go/internal/hookrunnercli/external_tool.go b/go/internal/hookrunnercli/external_tool.go index ceff1d46..1588306f 100644 --- a/go/internal/hookrunnercli/external_tool.go +++ b/go/internal/hookrunnercli/external_tool.go @@ -228,6 +228,7 @@ type externalToolCacheEnvironment struct { GolangCILintDir string UVCache string UVProjectEnv string + UVFrozen string // CargoTarget is per-repository build output, like GoCache. CargoTarget string // CargoHome and RustupHome are the operator's, not the repository's. Cargo @@ -321,6 +322,7 @@ func externalToolCacheEnv(root string) (externalToolCacheEnvironment, error) { GolangCILintDir: golangCILintDir, UVCache: uvCache, UVProjectEnv: uvProjectEnv, + UVFrozen: "1", CargoTarget: cargoTarget, CargoHome: cargoHome, RustupHome: rustupHome, @@ -394,6 +396,7 @@ func (environment externalToolCacheEnvironment) items() []string { "GOLANGCI_LINT_CACHE", "UV_CACHE_DIR", "UV_PROJECT_ENVIRONMENT", + "UV_FROZEN", "CARGO_TARGET_DIR", "CARGO_HOME", "RUSTUP_HOME", @@ -421,6 +424,8 @@ func (environment externalToolCacheEnvironment) value(name string) string { return environment.UVCache case "UV_PROJECT_ENVIRONMENT": return environment.UVProjectEnv + case "UV_FROZEN": + return environment.UVFrozen case "CARGO_TARGET_DIR": return environment.CargoTarget case "CARGO_HOME": diff --git a/go/internal/hookrunnercli/external_tool_internal_test.go b/go/internal/hookrunnercli/external_tool_internal_test.go index 49206925..6d1a01eb 100644 --- a/go/internal/hookrunnercli/external_tool_internal_test.go +++ b/go/internal/hookrunnercli/external_tool_internal_test.go @@ -20,6 +20,7 @@ func TestPrepareHookProcessCacheEnvironmentSetsAndRestoresUVEnvironment(t *testi root := t.TempDir() t.Setenv("UV_CACHE_DIR", "/previous/uv-cache") t.Setenv("UV_PROJECT_ENVIRONMENT", "/previous/uv-project-environment") + t.Setenv("UV_FROZEN", "0") restore, err := prepareHookProcessCacheEnvironment(root) if err != nil { t.Fatalf("prepareHookProcessCacheEnvironment: %v", err) @@ -40,6 +41,9 @@ func TestPrepareHookProcessCacheEnvironmentSetsAndRestoresUVEnvironment(t *testi if info, statErr := os.Stat(wantProjectEnv); statErr != nil || !info.IsDir() { t.Fatalf("UV project environment is not usable: info=%v error=%v", info, statErr) } + if got := os.Getenv("UV_FROZEN"); got != "1" { + t.Fatalf("UV_FROZEN = %q, want %q", got, "1") + } restore() if got := os.Getenv("UV_CACHE_DIR"); got != "/previous/uv-cache" { @@ -50,6 +54,9 @@ func TestPrepareHookProcessCacheEnvironmentSetsAndRestoresUVEnvironment(t *testi ); got != "/previous/uv-project-environment" { t.Fatalf("restored UV_PROJECT_ENVIRONMENT = %q", got) } + if got := os.Getenv("UV_FROZEN"); got != "0" { + t.Fatalf("restored UV_FROZEN = %q", got) + } } func TestExternalToolEnvRemovesGitHookLocalEnvironment(t *testing.T) { @@ -74,6 +81,7 @@ func TestExternalToolEnvRemovesGitHookLocalEnvironment(t *testing.T) { t.Setenv("GIT_CONFIG_VALUE_0", "test@example.com") t.Setenv("CODING_ETHOS_REAL_GIT", "/tmp/hook-real-git") t.Setenv("GOCACHE", "/tmp/host-go-cache") + t.Setenv("UV_FROZEN", "0") t.Setenv("PATH", shimDir+string(os.PathListSeparator)+"/usr/bin") t.Setenv(consumerRootEnv, repo) t.Setenv(hookGroupChildEnv, hookPlanBoolTrue) @@ -152,6 +160,10 @@ func TestExternalToolEnvRemovesGitHookLocalEnvironment(t *testing.T) { t.Fatalf("externalToolEnv did not set uv project environment: %#v", env) } + if !slices.Contains(env, "UV_FROZEN=1") || slices.Contains(env, "UV_FROZEN=0") { + t.Fatalf("externalToolEnv did not freeze the sealed uv project: %#v", env) + } + for _, item := range env { if !strings.HasPrefix(item, "PATH=") { continue diff --git a/pre-commit/hooks/uv.lock b/pre-commit/hooks/uv.lock new file mode 100644 index 00000000..a75e95d2 --- /dev/null +++ b/pre-commit/hooks/uv.lock @@ -0,0 +1,1459 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[options] +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" + +[options.exclude-newer-package] +pip = "2026-04-26T21:00:06Z" +sqlfluff = "2026-05-15T00:00:00Z" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/9d/912fefab0e30aee6a3af8a62bbea4a81b29afa4ba2c973d31170620a26de/ast_serialize-0.3.0.tar.gz", hash = "sha256:1bc3ca09a63a021376527c4e938deedd11d11d675ce850e6f9c7487f5889992b", size = 60689, upload-time = "2026-04-30T23:24:48.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/57/a54d4de491d6cdd7a4e4b0952cc3ca9f60dcefa7b5fb48d6d492debe1649/ast_serialize-0.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a867927df59f76a18dc1d874a0b2c079b42c58972dca637905576deb0912e14", size = 1182966, upload-time = "2026-04-30T23:23:57.376Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/a5db014bb0f91b209236b57c429389e31290c0093532b8436d577699b2fa/ast_serialize-0.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a6fb063bf040abf8321e7b8113a0554eda445ffc508aa51287f8808886a5ae22", size = 1171316, upload-time = "2026-04-30T23:23:59.63Z" }, + { url = "https://files.pythonhosted.org/packages/15/59/fd55133e478c4326f60a11df02573bf7ccb2ac685810b50f1803d0f68053/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5075cd8482573d743586779e5f9b652a015e37d4e95132d7e5a9bc5c8f483d8f", size = 1232234, upload-time = "2026-04-30T23:24:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/cc/79/0ca1d26357ecb4a697d74d00b73ef3137f24c140424125393a0de820eb09/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41560b27794f4553b0f77811e9fb325b77db4a2b39018d437e09932275306e66", size = 1233437, upload-time = "2026-04-30T23:24:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/7078ec94dd6e124b8e028ac77016a4f13c83fa1c145790f2e68f3816998b/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b967c01ca74909c5d90e0fe4393401e2cc5da5ebd9a6262a19e45ffd3757dec8", size = 1440188, upload-time = "2026-04-30T23:24:04.717Z" }, + { url = "https://files.pythonhosted.org/packages/21/16/cca7195ef55a012f8013c3442afa91d287a0a36dcf88b480b262475135b3/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:424ebb8f46cd993f7cec4009d119312d8433dd90e6b0df0499cd2c91bdcc5af9", size = 1254211, upload-time = "2026-04-30T23:24:06.18Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/f3d4dfae67dee6580534361a6343367d34217e7d25cff858bd1d8f03b8ed/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14b1d566b56e2ee70b11fec1de7e0b94ec7cd83717ec7d189967841a361190e", size = 1255973, upload-time = "2026-04-30T23:24:07.772Z" }, + { url = "https://files.pythonhosted.org/packages/14/41/55fbfe02c42f40fbe3e74eda167d977d555ff720ce1abfa08515236efd88/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ba30b18735f047ec11103d1ab92f4789cf1fea1e0dc89b04a2f5a0632fd79de", size = 1298629, upload-time = "2026-04-30T23:24:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/28/36/7d2501cacc7989fb8504aa9da2a2022a174200a59d4e6639de4367a57fdd/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ea0754cb7b0f682ebb005ffb0d18f8d17993490d9c289863cd69cacc4ab8df", size = 1408435, upload-time = "2026-04-30T23:24:11.013Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/54e3b469c3fa0bf9cd532fa643d1d33b73303f8d70beac3e366b68dd64b7/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a0c5aa1073a5ba7b2abaa4b54abe8b8d75c4d1e2d54a2ff70b0ca6222fea5728", size = 1508174, upload-time = "2026-04-30T23:24:12.635Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/9b9621865b02c60539e26d9b114a312b4fa46aa703e33e79317174bfea21/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4e52650d834c1ea7791969a361de2c54c13b2fb4c519ec79445fa8b9021a147d", size = 1502354, upload-time = "2026-04-30T23:24:14.186Z" }, + { url = "https://files.pythonhosted.org/packages/34/dd/f138bc5c43b0c414fdd12eefe15677839323078b6e75301ad7f96cd26d45/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15bd6af3f136c61dae27805eb6b8f3269e85a545c4c27ffe9e530ead78d2b36d", size = 1450504, upload-time = "2026-04-30T23:24:16.076Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/97ef9e1c315601db74365955c8edd3292e3055500d6317602815dbdf08ae/ast_serialize-0.3.0-cp314-cp314t-win32.whl", hash = "sha256:d188bfe37b674b49708497683051d4b571366a668799c9b8e8a94513694969d9", size = 1058662, upload-time = "2026-04-30T23:24:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d6/e2c3483c31580fdb623f92ad38d2f856cde4b9205a3e6bd84760f3de7d82/ast_serialize-0.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5832c2fdf8f8a6cf682b4cfcf677f5eaf39b4ddbc490f5480cfccdd1e7ce8fa1", size = 1100349, upload-time = "2026-04-30T23:24:18.992Z" }, + { url = "https://files.pythonhosted.org/packages/ab/89/29abcb1fe18a429cda60c6e0bbd1d6e90499339842a2f548d7567542357e/ast_serialize-0.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:670f177188d128fb7f9f15b5ad0e1b553d22c34e3f584dcb83eb8077600437f0", size = 1072895, upload-time = "2026-04-30T23:24:20.706Z" }, + { url = "https://files.pythonhosted.org/packages/bc/93/72abad83966ed6235647c9f956417dc1e17e997696388521910e3d1fa3f4/ast_serialize-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ec2fafa5e4313cc8feed96e436ebe19ac7bc6fa41fbc2827e826c48b9e4c3a9", size = 1190024, upload-time = "2026-04-30T23:24:22.486Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/eb88584b2f0234e581762011208ca203252bf6c98e59b4769daa571f3576/ast_serialize-0.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef6d3c08b7b4cd29b48410338e134764a00e76d25841eb02c1084e868c888ecc", size = 1178633, upload-time = "2026-04-30T23:24:24.35Z" }, + { url = "https://files.pythonhosted.org/packages/56/51/cf1ec1ff3e616373d0dcbd5fad502e0029dc541f13ab642259762a7d127f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d841424f41b886e98044abc80769c14a956e6e5ccd5fb5b0d9f5ead72be18a4", size = 1241351, upload-time = "2026-04-30T23:24:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/68fcf50478cf1093f2d423f034ae06453122c8b415d8e21a44668eca485d/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d21453734ad39367ede5d37efe4f59f830ce1c09f432fc72a90e368f77a4a3e7", size = 1239582, upload-time = "2026-04-30T23:24:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c1/a6c9fa284eceb5fc6f21347e968445a051d7ca2c4d34e6a04314646dbcee/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5e110cdce2a347e1dd987529c88ef54d26f67848dce3eba1b3b2cc2cf085c94", size = 1448853, upload-time = "2026-04-30T23:24:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/23/5f/8ad3829a09e4e8c5328a53ce7d4711d660944e3e164c5f6abcc2c8f27167/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6e23a98e57560a055f5c4b68700a0fd5ce483d2814c23140b3638c7f5d1e61", size = 1262204, upload-time = "2026-04-30T23:24:31.482Z" }, + { url = "https://files.pythonhosted.org/packages/25/13/44aa28d97f10e25247e8576b5f6b2795d4fa1a80acc88acc942c508d06f7/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c9e763d70293d65ce1e1ea8c943140c68d0953f0268c7ee0998f2e07f77dd0", size = 1266458, upload-time = "2026-04-30T23:24:33.088Z" }, + { url = "https://files.pythonhosted.org/packages/d8/58/b3a8be3777cd3744324fd5cec0d80d37cd96fc7cbb0fb010e03dff1e870f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4388a1796c228f1ce5c391426f7d21a0003ad3b47f677dbeded9bd1a85c7209f", size = 1308700, upload-time = "2026-04-30T23:24:34.657Z" }, + { url = "https://files.pythonhosted.org/packages/13/03/f8312d6b57f5471a9dc7946f22b8798a1fc296d38c25766223aacadec42c/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5283cdcc0c64c3d8b9b688dc6aaa012d9c0cf1380a7f774a6bae6a1c01b3205a", size = 1416724, upload-time = "2026-04-30T23:24:36.562Z" }, + { url = "https://files.pythonhosted.org/packages/50/5d/13fc3789a7abac00559da2e2e9f386db4612aa1f84fc53d09bf714c37545/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ef88cc5842a5d7a6ac09dc0d5fc2c98f5d276c1f076f866d55047ce886785b", size = 1515441, upload-time = "2026-04-30T23:24:38.018Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b9/7ab43fc7a23b1f970281093228f5f79bed6edeed7a3e672bde6d7a832a58/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cc14bf402bdc0978594ecce783793de2c7470cd4f5cd7eb286ca97ed8ff7cba9", size = 1510522, upload-time = "2026-04-30T23:24:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/56/ec/d75fc2b788d319f1fad77c14156896f31afdfc68af85b505e5bdebcb9592/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11eae0cf1b7b3e0678133cc2daa974ea972caf02eb4b3aa062af6fa9acd52c57", size = 1460917, upload-time = "2026-04-30T23:24:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/95/74/f99c81193a2725911e1911ae567ed27c2f2419332c7f3537366f9d238cac/ast_serialize-0.3.0-cp39-abi3-win32.whl", hash = "sha256:2db3dd99de5e6a5a11d7dda73de8750eb6e5baaf25245adf7bdcfe64b6108ae2", size = 1067804, upload-time = "2026-04-30T23:24:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/16/81/76af00c47daa151e89f98ae21fbbcb2840aaa9f5766579c4da76a3c57188/ast_serialize-0.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:a2cd125adccf7969470621905d302750cd25951f22ea430d9a25b7be031e5549", size = 1105561, upload-time = "2026-04-30T23:24:44.578Z" }, + { url = "https://files.pythonhosted.org/packages/bd/46/d3ec57ad500f598d1554bd14ce4df615960549ab2844961bc4e1f5fbd174/ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a", size = 1077165, upload-time = "2026-04-30T23:24:46.377Z" }, +] + +[[package]] +name = "astroid" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0", size = 406358, upload-time = "2026-02-07T23:35:07.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", size = 276445, upload-time = "2026-02-07T23:35:05.344Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + +[package.optional-dependencies] +sarif = [ + { name = "jschema-to-python" }, + { name = "sarif-om" }, +] + +[[package]] +name = "black" +version = "26.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/58/0a9d9b1195c159d206000c541c3e05897e339be754f0e4d8b29445ab536e/black-26.5.0.tar.gz", hash = "sha256:5cbe4cc4037ffca34cdb0a6a9a046f104b262d0bd63c30fd4a88c7adc2049b1d", size = 677762, upload-time = "2026-05-16T17:57:12.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/0e/328992a8ce73c93605e7fe7325bcf38d3f1bc9b0118b514873699a5ed379/black-26.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2b64ce9841e8b8254c3d702ebccdaf5c520607df8aa4176f5732b7f9af1e6f6", size = 2003830, upload-time = "2026-05-16T18:01:12.853Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/0ded3f1c10306c0d4c5b112ec7c75bd323a199b96d9a0c61f4116ab985e8/black-26.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0a789a41b386f0f83711785f182f2977138ba9cc1f41ad0f6fbc8faac4d2639e", size = 1810249, upload-time = "2026-05-16T18:01:14.367Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/b5cf00e7d8e5b168bfc389e3b937b8d1250cfdda0c6c607f91dba0d5c2a7/black-26.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f69837f7e26d67b1d1e9d0ed49231a14a0469f266e44cd142873e0552f325395", size = 1879117, upload-time = "2026-05-16T18:01:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0e/01baec29dd65ecca6be69d721b90dfff473b0e49fb49bb1b5b3fa470ab9d/black-26.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:c5b08371561dae9c90391fe7f2138fe7fa495437d3bb134eb865839036e65784", size = 1486102, upload-time = "2026-05-16T18:01:17.78Z" }, + { url = "https://files.pythonhosted.org/packages/36/4b/6f9623c8cd5a3c6883318800e2073761fd9db1e859f594ee42e95c18fcd6/black-26.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:3968ce82ca0bd4914769518490d91a9b0ef2ff2fc68e2122d22b5915a0342eaa", size = 1286888, upload-time = "2026-05-16T18:01:19.275Z" }, + { url = "https://files.pythonhosted.org/packages/75/d1/40d151b65b659848001ec8b8226323a6f25ee535a2f9d441392e1d86933b/black-26.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea8a0c4505486c132c6640e4e108d25f41360a06d844db5a76477c3dbae1b616", size = 1998941, upload-time = "2026-05-16T18:01:20.788Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d1/991d741faf172502f17966ad8abb7e5b6ce06560855938000564dcf8e1f1/black-26.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2178a70e7c45fb85999b687d8326abceef1e7227463d5d7e07ef125c9fbb9c5c", size = 1810853, upload-time = "2026-05-16T18:01:22.369Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6c/6bb8ab3fa60074d5295162493482b4ed01c33dd19acf1754497fd506caed/black-26.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3ad14d7c24c40eafecf4fb212d9c01e7c7b2ab05c8646b351c93728f499c555", size = 1874114, upload-time = "2026-05-16T18:01:23.973Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3b/d9dc4206bbd9313d5c3761bd88e9bece5c85e909e5870c46bb7f835ecbcb/black-26.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:8ea767bae9c4f331ea9ad2e08895c951e600dffd550a42624d5210a908720b39", size = 1508463, upload-time = "2026-05-16T18:01:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/2c5fc4152fc3bf79aa498bce429581b87aca340da2fde92423c0b6ce74bd/black-26.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:d658f4ee6167797b08be07ee4bbf6045753ddabfc676c3cb0eec23752ca83eff", size = 1312669, upload-time = "2026-05-16T18:01:27.503Z" }, + { url = "https://files.pythonhosted.org/packages/14/c8/13da5c6a37b46a690199e0895c33a758ba4f2ec3cd81d1d72ebb373509a8/black-26.5.0-py3-none-any.whl", hash = "sha256:241f25bf59f5ca17f5121031e310e089b84cd22bb4eca47360099ea825544f17", size = 212907, upload-time = "2026-05-16T17:57:10.792Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "chardet" +version = "7.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/b6/9df434a8eeba2e6628c465a1dfa31034228ef79b26f76f46278f4ef7e49d/chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", size = 784800, upload-time = "2026-04-13T21:33:39.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", size = 873769, upload-time = "2026-04-13T21:33:14.002Z" }, + { url = "https://files.pythonhosted.org/packages/55/5f/25bdec773905bff0ff6cf35ca73b17bd05593b4f87bd8c5fa43705f7167d/chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", size = 853991, upload-time = "2026-04-13T21:33:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/a29380ee0b215d23d77733b5ad60c5c0c7969650e080c667acdf9462040d/chardet-7.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", size = 874024, upload-time = "2026-04-13T21:33:16.915Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", size = 887410, upload-time = "2026-04-13T21:33:18.368Z" }, + { url = "https://files.pythonhosted.org/packages/63/1c/44a9a9e0c59c185a5d307ceaeee8768afa1558f0a24f7a4b5fa11b67586b/chardet-7.4.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", size = 879269, upload-time = "2026-04-13T21:33:20.377Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b3/5d0e77ea774bd3224321c248880ea0c0379000ac5c2bb6d77609549de247/chardet-7.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131", size = 944155, upload-time = "2026-04-13T21:33:21.694Z" }, + { url = "https://files.pythonhosted.org/packages/70/a8/bf0811d859e13801279a2ae64f37a408027b282f2047bc0001c75dd356ad/chardet-7.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", size = 872887, upload-time = "2026-04-13T21:33:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", size = 853964, upload-time = "2026-04-13T21:33:24.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/17fa103ea9caf5d325a5e4051ab2ba65996fd66baa60b81ee41af1f54e10/chardet-7.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", size = 876006, upload-time = "2026-04-13T21:33:26.098Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", size = 887680, upload-time = "2026-04-13T21:33:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/94a3c673327392652ee8bdea9a45bc8a5f5365197a7387d68f0eed007115/chardet-7.4.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", size = 879865, upload-time = "2026-04-13T21:33:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", size = 939594, upload-time = "2026-04-13T21:33:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/33/e0/d06e42fd6f02a58e5e227e5106587751cb38adcff0aaf949add744b78b6e/chardet-7.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", size = 889714, upload-time = "2026-04-13T21:33:32.772Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ed/40d091954d48abea037baae6be8fb79905e5f78d34d12ea955132c7d8011/chardet-7.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", size = 872319, upload-time = "2026-04-13T21:33:34.427Z" }, + { url = "https://files.pythonhosted.org/packages/bb/77/82a46821dbfbdfe062710d2bf2ede13426304e3567a23c57d919c0c31630/chardet-7.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", size = 892021, upload-time = "2026-04-13T21:33:35.766Z" }, + { url = "https://files.pythonhosted.org/packages/49/57/42d30c562bda5b4a839766c1aad8d5856b798ad2a1c3247b72a679afec94/chardet-7.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", size = 902509, upload-time = "2026-04-13T21:33:37.096Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/0a40afdb50a0fe041ab95553b835a8160b6cf0e81edf2ae2fe9f5224cbf9/chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", size = 626562, upload-time = "2026-04-13T21:33:38.559Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "coding-ethos-hooks" +version = "0.3.0" +source = { editable = "." } +dependencies = [ + { name = "bandit", extra = ["sarif"] }, + { name = "black" }, + { name = "mypy" }, + { name = "mypy-extensions" }, + { name = "pandas-stubs" }, + { name = "pip" }, + { name = "pydantic" }, + { name = "pylint" }, + { name = "pyright" }, + { name = "pyupgrade" }, + { name = "pyyaml" }, + { name = "radon" }, + { name = "ruff" }, + { name = "sqlfluff" }, + { name = "tombi" }, + { name = "types-aiofiles" }, + { name = "types-beautifulsoup4" }, + { name = "types-decorator" }, + { name = "types-docker" }, + { name = "types-openpyxl" }, + { name = "types-pexpect" }, + { name = "types-pillow" }, + { name = "types-psutil" }, + { name = "types-psycopg2" }, + { name = "types-pyasn1" }, + { name = "types-pycurl" }, + { name = "types-pygments" }, + { name = "types-pymysql" }, + { name = "types-tqdm" }, + { name = "types-ujson" }, + { name = "types-xlrd" }, + { name = "uv" }, + { name = "vulture" }, + { name = "yamllint" }, +] + +[package.metadata] +requires-dist = [ + { name = "bandit", extras = ["sarif", "toml"], specifier = ">=1.8.6" }, + { name = "black", specifier = ">=26.1.0" }, + { name = "mypy", specifier = ">=1.19.1" }, + { name = "mypy-extensions", specifier = ">=1.1.0" }, + { name = "pandas-stubs", specifier = ">=3.0.0.260204" }, + { name = "pip", specifier = ">=26.1" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pylint", specifier = ">=4.0.4" }, + { name = "pyright", specifier = ">=1.1.408" }, + { name = "pyupgrade", specifier = ">=3.21.2" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "radon", specifier = ">=6.0.1" }, + { name = "ruff", specifier = ">=0.15.1" }, + { name = "sqlfluff", specifier = ">=4.2.0" }, + { name = "tombi", specifier = ">=0.6.12" }, + { name = "types-aiofiles", specifier = ">=25.1.0.20260508" }, + { name = "types-beautifulsoup4", specifier = ">=4.12.0.20250516" }, + { name = "types-decorator", specifier = ">=5.2.0.20260508" }, + { name = "types-docker", specifier = ">=7.1.0.20260409" }, + { name = "types-openpyxl", specifier = ">=3.1.5.20260508" }, + { name = "types-pexpect", specifier = ">=4.9.0.20260508" }, + { name = "types-pillow", specifier = ">=10.2.0.20240822" }, + { name = "types-psutil", specifier = ">=7.2.2.20260408" }, + { name = "types-psycopg2", specifier = ">=2.9.21.20260422" }, + { name = "types-pyasn1", specifier = ">=0.6.0.20260408" }, + { name = "types-pycurl", specifier = ">=7.45.7.20260408" }, + { name = "types-pygments", specifier = ">=2.20.0.20260508" }, + { name = "types-pymysql", specifier = ">=1.1.0.20260508" }, + { name = "types-tqdm", specifier = ">=4.67.3.20260508" }, + { name = "types-ujson", specifier = ">=5.10.0.20250822" }, + { name = "types-xlrd", specifier = ">=2.0.0.20260408" }, + { name = "uv", specifier = ">=0.10.4" }, + { name = "vulture", specifier = ">=2.14" }, + { name = "yamllint", specifier = ">=1.38.0" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, +] + +[[package]] +name = "diff-cover" +version = "10.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet" }, + { name = "jinja2" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/b4/eee71d1e338bc1f9bd3539b46b70e303dac061324b759c9a80fa3c96d90d/diff_cover-10.2.0.tar.gz", hash = "sha256:61bf83025f10510c76ef6a5820680cf61b9b974e8f81de70c57ac926fa63872a", size = 102473, upload-time = "2026-01-09T01:59:07.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2c/61eeb887055a37150db824b6bf830e821a736580769ac2fea4eadb0d613f/diff_cover-10.2.0-py3-none-any.whl", hash = "sha256:59c328595e0b8948617cc5269af9e484c86462e2844bfcafa3fb37f8fca0af87", size = 56748, upload-time = "2026-01-09T01:59:06.028Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jschema-to-python" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonpickle" }, + { name = "pbr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/7f/5ae3d97ddd86ec33323231d68453afd504041efcfd4f4dde993196606849/jschema_to_python-1.2.3.tar.gz", hash = "sha256:76ff14fe5d304708ccad1284e4b11f96a658949a31ee7faed9e0995279549b91", size = 10061, upload-time = "2019-10-05T20:02:39.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/9e/1b6819a87c3f59170406163ba17bc55b0abe18ae552f53d2b0a2025f9c63/jschema_to_python-1.2.3-py3-none-any.whl", hash = "sha256:8a703ca7604d42d74b2815eecf99a33359a8dccbb80806cce386d5e2dd992b05", size = 10400, upload-time = "2019-10-05T20:02:37.948Z" }, +] + +[[package]] +name = "jsonpickle" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/a6/d07afcfdef402900229bcca795f80506b207af13a838d4d99ad45abf530c/jsonpickle-4.1.1.tar.gz", hash = "sha256:f86e18f13e2b96c1c1eede0b7b90095bbb61d99fedc14813c44dc2f361dbbae1", size = 316885, upload-time = "2025-06-02T20:36:11.57Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/73/04df8a6fa66d43a9fd45c30f283cc4afff17da671886e451d52af60bdc7e/jsonpickle-4.1.1-py3-none-any.whl", hash = "sha256:bb141da6057898aa2438ff268362b126826c812a1721e31cf08a6e142910dc91", size = 47125, upload-time = "2025-06-02T20:36:08.647Z" }, +] + +[[package]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + +[[package]] +name = "mando" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/24/cd70d5ae6d35962be752feccb7dca80b5e0c2d450e995b16abd6275f3296/mando-0.7.1.tar.gz", hash = "sha256:18baa999b4b613faefb00eac4efadcf14f510b59b924b66e08289aa1de8c3500", size = 37868, upload-time = "2022-02-24T08:12:27.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl", hash = "sha256:26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a", size = 28149, upload-time = "2022-02-24T08:12:25.24Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +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" } +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" }, +] + +[[package]] +name = "pandas-stubs" +version = "3.0.0.260204" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/1d/297ff2c7ea50a768a2247621d6451abb2a07c0e9be7ca6d36ebe371658e5/pandas_stubs-3.0.0.260204.tar.gz", hash = "sha256:bf9294b76352effcffa9cb85edf0bed1339a7ec0c30b8e1ac3d66b4228f1fbc3", size = 109383, upload-time = "2026-02-04T15:17:17.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/2f/f91e4eee21585ff548e83358332d5632ee49f6b2dcd96cb5dca4e0468951/pandas_stubs-3.0.0.260204-py3-none-any.whl", hash = "sha256:5ab9e4d55a6e2752e9720828564af40d48c4f709e6a2c69b743014a6fcb6c241", size = 168540, upload-time = "2026-02-04T15:17:15.615Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pbr" +version = "7.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/ab/1de9a4f730edde1bdbbc2b8d19f8fa326f036b4f18b2f72cfbea7dc53c26/pbr-7.0.3.tar.gz", hash = "sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29", size = 135625, upload-time = "2025-11-03T17:04:56.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/db/61efa0d08a99f897ef98256b03e563092d36cc38dc4ebe4a85020fe40b31/pbr-7.0.3-py2.py3-none-any.whl", hash = "sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b", size = 131898, upload-time = "2025-11-03T17:04:54.875Z" }, +] + +[[package]] +name = "pip" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/7e/d2b04004e1068ad4fdfa2f227b839b5d03e602e47cdbbf49de71137c9546/pip-26.1.tar.gz", hash = "sha256:81e13ebcca3ffa8cc85e4deff5c27e1ee26dea0aa7fc2f294a073ac208806ff3", size = 1840316, upload-time = "2026-04-26T21:00:05.406Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl", hash = "sha256:4e8486d821d814b77319acb7b9e8bf5a4ee7590a643e7cb21029f209be8573c1", size = 1812804, upload-time = "2026-04-26T21:00:03.194Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pylint" +version = "4.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/74d9a8a68b8067efce8d07707fe6a236324ee1e7808d2eb3646ec8517c7d/pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c", size = 1572474, upload-time = "2026-02-20T09:07:33.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/4e/3aa27f74211522dba7e9cbc3e74de779c6d4b654c54e50a4840623be8014/pyright-1.1.409.tar.gz", hash = "sha256:986ee05beca9e077c165758ad123667c679e050059a2546aa02473930394bc93", size = 4430434, upload-time = "2026-04-23T11:02:03.799Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/6b/330d8ebae582b30c2959a1ef4c3bc344ebde48c2ff0c3f113c4710735e11/pyright-1.1.409-py3-none-any.whl", hash = "sha256:aa3ea228cab90c845c7a60d28db7a844c04315356392aa09fafcee98c8c22fb3", size = 6438161, upload-time = "2026-04-23T11:02:01.309Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pyupgrade" +version = "3.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tokenize-rt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/a1/dc63caaeed232b1c58eae1b7a75f262d64ab8435882f696ffa9b58c0c415/pyupgrade-3.21.2.tar.gz", hash = "sha256:1a361bea39deda78d1460f65d9dd548d3a36ff8171d2482298539b9dc11c9c06", size = 45455, upload-time = "2025-11-19T00:39:48.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8c/433dac11910989a90c40b10149d07ef7224232236971a562d3976790ec53/pyupgrade-3.21.2-py2.py3-none-any.whl", hash = "sha256:2ac7b95cbd176475041e4dfe8ef81298bd4654a244f957167bd68af37d52be9f", size = 62814, upload-time = "2025-11-19T00:39:46.958Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "radon" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "mando" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/6d/98e61600febf6bd929cf04154537c39dc577ce414bafbfc24a286c4fa76d/radon-6.0.1.tar.gz", hash = "sha256:d1ac0053943a893878940fedc8b19ace70386fc9c9bf0a09229a44125ebf45b5", size = 1874992, upload-time = "2023-03-26T06:24:38.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl", hash = "sha256:632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859", size = 52784, upload-time = "2023-03-26T06:24:33.949Z" }, +] + +[[package]] +name = "regex" +version = "2026.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, + { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, + { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, + { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, + { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, + { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, + { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, + { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, + { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, + { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, + { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, + { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, + { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, + { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, + { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, + { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" }, + { url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" }, + { url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" }, + { url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" }, + { url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" }, + { url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" }, + { url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" }, + { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, +] + +[[package]] +name = "sarif-om" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "pbr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/de/bbdd93fe456d4011500784657c5e4a31e3f4fcbb276255d4db1213aed78c/sarif_om-1.0.4.tar.gz", hash = "sha256:cd5f416b3083e00d402a92e449a7ff67af46f11241073eea0461802a3b5aef98", size = 28847, upload-time = "2019-10-05T20:11:23.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/7c/1d3d0467565aa8b3e77ab8712042a09dd1158056826f45783f3d2b34adf1/sarif_om-1.0.4-py3-none-any.whl", hash = "sha256:539ef47a662329b1c8502388ad92457425e95dc0aaaf995fe46f4984c4771911", size = 30193, upload-time = "2019-10-05T20:11:21.577Z" }, +] + +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sqlfluff" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet" }, + { name = "click" }, + { name = "colorama" }, + { name = "diff-cover" }, + { name = "jinja2" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "tblib" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/a1/3430aebc4fae35d7e466e793b5da2f36c2245af092311520dc1d3d3146d6/sqlfluff-4.2.1.tar.gz", hash = "sha256:32f43fbf6721e57f1a5a87d71df0d94b84ecba6ed65727266c7fa60991110fb9", size = 1013384, upload-time = "2026-05-14T21:15:37.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/55/8830f3204939cc965c72680bfae99b0f3fd6c16bffdaf79372ad0a3d1ca6/sqlfluff-4.2.1-py3-none-any.whl", hash = "sha256:ea84f196c41f45df40a851b0881cb3fbb660570e07acbbfd304ff4e9b893424d", size = 1002493, upload-time = "2026-05-14T21:15:35.582Z" }, +] + +[[package]] +name = "stevedore" +version = "5.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, +] + +[[package]] +name = "tblib" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8a/14c15ae154895cc131174f858c707790d416c444fc69f93918adfd8c4c0b/tblib-3.2.2.tar.gz", hash = "sha256:e9a652692d91bf4f743d4a15bc174c0b76afc750fe8c7b6d195cc1c1d6d2ccec", size = 35046, upload-time = "2025-11-12T12:21:16.572Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl", hash = "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", size = 12893, upload-time = "2025-11-12T12:21:14.407Z" }, +] + +[[package]] +name = "tokenize-rt" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/ed/8f07e893132d5051d86a553e749d5c89b2a4776eb3a579b72ed61f8559ca/tokenize_rt-6.2.0.tar.gz", hash = "sha256:8439c042b330c553fdbe1758e4a05c0ed460dbbbb24a606f11f0dee75da4cad6", size = 5476, upload-time = "2025-05-23T23:48:00.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl", hash = "sha256:a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44", size = 6004, upload-time = "2025-05-23T23:47:58.812Z" }, +] + +[[package]] +name = "tombi" +version = "0.11.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/8c/3dad08999de09f07941c473aa3cb80e54a1a3dc6213002421ec18165844a/tombi-0.11.5.tar.gz", hash = "sha256:16f58179b2a96f7e0150979182e0bba55d101762a7a16dc9c7daa3881ae864b1", size = 691093, upload-time = "2026-05-17T01:50:38.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/09/f98759bd844bbff7d7c335b4ef664e2c8f720d73f30dbfd322809a29a367/tombi-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aa2cb09621b36fdcfea06924510992a1ac36e73d5a83a4a0aaa60fef8ee83bbd", size = 10400173, upload-time = "2026-05-17T01:50:24.93Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/22d576daaf919aff631dfc77bba6f5c5333ce8def85d1916954d1b6bffc2/tombi-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e81c81cdfbd4c584ebfcf1e5ad906a46b40df7d3211d034dec68cfa35337d6d8", size = 10099584, upload-time = "2026-05-17T01:50:22.44Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/e668e0ea87a1fb2523cc22157ad871d319bf7b544d02dd392411bebb01fb/tombi-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:33981e93b75f26953dc1c6992e2877ebe0089785b08d9f2261942037cecfcbb7", size = 10408432, upload-time = "2026-05-17T01:50:09.073Z" }, + { url = "https://files.pythonhosted.org/packages/39/05/ca751dc7b86891273e95926ff26cb7bcb82d48a17bf02313dea258321cdc/tombi-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e73e88682003f6d1a42e7185b57f8030eeb6e919e24918fde5c300323a7638f5", size = 11738101, upload-time = "2026-05-17T01:50:17.32Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0c/8c214b74c208bdf7ce6bf37b78928993f1817227756f07547badf61ca13b/tombi-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0bf49b8aec6d5f7da397613dd63ec9a7bda935147f4ceec3dbd92527758b777", size = 11782947, upload-time = "2026-05-17T01:50:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/2c/86/68a0dbdc871828a2faeacba69badd1b030a956d1f585b88b4f2dd0debfff/tombi-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f3f31d4b8e3a902b0251cffd41cf16065e1122a9f93a27fb9776a846360abc7e", size = 10423667, upload-time = "2026-05-17T01:50:14.417Z" }, + { url = "https://files.pythonhosted.org/packages/45/80/340280e3a32b134d0447a866341f9c919c4a4166ed90b48ecfd512e6b3c1/tombi-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df631bdceb7d58615bebd89464450f501d949f80293f16e02d57c8ce3a05b370", size = 10860129, upload-time = "2026-05-17T01:50:19.975Z" }, + { url = "https://files.pythonhosted.org/packages/14/d1/9030fdb90e5a1293a5089576a04955b9c6bc698d63ffc67af996518576d0/tombi-0.11.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5b40294861111906d18a8f52a9ce10a97b7631249f2c2b7d496058f0b5dcb131", size = 10648091, upload-time = "2026-05-17T01:50:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/e6/51/cc0e8796c25215100842f520287720dd14f46c9c14c472f62422fbf9cb0f/tombi-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:daa220f608cb76798f0f1957aff9f036b42a2f1b4c4f6122db1e0e275460a7b7", size = 10673541, upload-time = "2026-05-17T01:50:27.459Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/1489b16322e27edb7788d8eb914278a2dab1528d21ea04321082a6e6b8ca/tombi-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:078887f8dbdcffcb8083f7348491ae4393dd481dc0f280361457c09bc379f159", size = 10458941, upload-time = "2026-05-17T01:50:29.972Z" }, + { url = "https://files.pythonhosted.org/packages/32/99/fb87b6c2de87d891fbc4c807f71c4b9382ae446a3b3e0de01d2fe0f3a3d4/tombi-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33e233c4d5e6ab6f5d4c7110b3c11cdd4e7fc909c0fda31c3527e223c6a4e0ad", size = 11135135, upload-time = "2026-05-17T01:50:32.914Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/751b9351cf63a4e4053d4593c89f821ee228e4d911d18ee9e2e30b629306/tombi-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5397f12c6d68be9e86a224359ffb82cc6700ba2e113f22cce839cc4cf2e965d3", size = 11099695, upload-time = "2026-05-17T01:50:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/7f3c2d4937db3472ac1c24a0a76f9871a278b05e2b38fdf303fa2e7173b0/tombi-0.11.5-py3-none-win32.whl", hash = "sha256:2419c220b543caace7b3561c7e2596ff104472fb88c729aec20d93c772b9ceca", size = 8458415, upload-time = "2026-05-17T01:50:43.235Z" }, + { url = "https://files.pythonhosted.org/packages/9f/fd/ae01728e35571a063101559cb3f67a6e206763bb8c99ea3e06d3822c70d0/tombi-0.11.5-py3-none-win_amd64.whl", hash = "sha256:1458c9756a02efe33224a803f69ed16f78f7c01a1e88be1ce18797295851916a", size = 9825141, upload-time = "2026-05-17T01:50:40.639Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "types-aiofiles" +version = "25.1.0.20260508" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/d9/60e8b26ad7e57eb3b58f3370b35f0d740dd0909079417e784f4e6c0f92f6/types_aiofiles-25.1.0.20260508.tar.gz", hash = "sha256:d26b07bb28f36c154c77d33982e506ee462044932d42c4eea6e78f69d1de5b84", size = 14851, upload-time = "2026-05-08T04:49:48.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/88/31d8fb1a3d0a96e1e41cfc58c67f48634e840e2598336887fa5be1dd9c82/types_aiofiles-25.1.0.20260508-py3-none-any.whl", hash = "sha256:c35d2be25a7e4b881da7f62ff3823db3770b2f704f31ac69681c227569e808cc", size = 14365, upload-time = "2026-05-08T04:49:47.302Z" }, +] + +[[package]] +name = "types-beautifulsoup4" +version = "4.12.0.20250516" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-html5lib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/d1/32b410f6d65eda94d3dfb0b3d0ca151f12cb1dc4cef731dcf7cbfd8716ff/types_beautifulsoup4-4.12.0.20250516.tar.gz", hash = "sha256:aa19dd73b33b70d6296adf92da8ab8a0c945c507e6fb7d5db553415cc77b417e", size = 16628, upload-time = "2025-05-16T03:09:09.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/79/d84de200a80085b32f12c5820d4fd0addcbe7ba6dce8c1c9d8605e833c8e/types_beautifulsoup4-4.12.0.20250516-py3-none-any.whl", hash = "sha256:5923399d4a1ba9cc8f0096fe334cc732e130269541d66261bb42ab039c0376ee", size = 16879, upload-time = "2025-05-16T03:09:09.051Z" }, +] + +[[package]] +name = "types-decorator" +version = "5.2.0.20260508" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/42/88861ae7467752fa2933cdccc562b63b29bbc000fe841c56c1f87958cf9b/types_decorator-5.2.0.20260508.tar.gz", hash = "sha256:83af4c212bdccf0cef593b210051beb451664a2ee304c96cae8eea25f98f04e6", size = 9212, upload-time = "2026-05-08T04:48:34.813Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/23/e975683499a548cd6e8522279e73e5ba501401fe417f2fe6babd52ab9b6a/types_decorator-5.2.0.20260508-py3-none-any.whl", hash = "sha256:7d5fb96ea6f959c1685c85fc76aaf186e4695a68e5cbd05dad9b10073e94be12", size = 8066, upload-time = "2026-05-08T04:48:34.011Z" }, +] + +[[package]] +name = "types-docker" +version = "7.1.0.20260512" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-paramiko" }, + { name = "types-requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/48/88f15e1ee1b561006b3c37ffe14d954a1fd15d1e87ed2d9206bb7ae82485/types_docker-7.1.0.20260512.tar.gz", hash = "sha256:d05ce1162267f769b26cbc8f38d7e9712e86816a37f06d4eab16279dbcf18456", size = 33799, upload-time = "2026-05-12T05:29:42.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/d3/0116c3b699d5060f77af1a5bc64cd776ec3d788b675d8e9e68899a317d0e/types_docker-7.1.0.20260512-py3-none-any.whl", hash = "sha256:cde75a5a299c504f13dfb681c3dffdb14501c2d261ebff9c9bb5cf0d89971506", size = 48123, upload-time = "2026-05-12T05:29:41.602Z" }, +] + +[[package]] +name = "types-docutils" +version = "0.22.3.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/49/48a386fe15539556de085b87a69568b028cca2fa4b92596a3d4f79ac6784/types_docutils-0.22.3.20260408.tar.gz", hash = "sha256:22d5d45e4e0d65a1bc8280987a73e28669bb1cc9d16b18d0afc91713d1be26da", size = 57383, upload-time = "2026-04-08T04:27:26.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/47/1667fda6e9fcb044f8fb797f6dc4367b88dc2ab40f1a035e387f5405e870/types_docutils-0.22.3.20260408-py3-none-any.whl", hash = "sha256:2545a86966022cdf1468d430b0007eba0837be77974a7f3fafa1b04a6815d531", size = 91981, upload-time = "2026-04-08T04:27:25.934Z" }, +] + +[[package]] +name = "types-html5lib" +version = "1.1.11.20260408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/59/914d00107c770e49fa57d4c4572e0371bbce14321385fd2ea3e06691b62d/types_html5lib-1.1.11.20260408.tar.gz", hash = "sha256:8a281aa367bc77dbc758358cd9bef79530f2d154eeed9b33705bb035a0dab9e4", size = 18316, upload-time = "2026-04-08T04:35:49.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/19/12d95e98e42e120522665ec6850b38df8d2c1cca94e21c4d7f8578acb64e/types_html5lib-1.1.11.20260408-py3-none-any.whl", hash = "sha256:d18dc4b90d6d6745585790b920db13ede43e1f8ff6ee1ac0ceb0dec4223a06fa", size = 24313, upload-time = "2026-04-08T04:35:48.679Z" }, +] + +[[package]] +name = "types-openpyxl" +version = "3.1.5.20260508" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/25/86379c5b2d83235984b2514168bf1c04deb54adbb9e0ab0a2abc3809dfa2/types_openpyxl-3.1.5.20260508.tar.gz", hash = "sha256:34f5c6398f9066bdad309661f300590fdd090846f76164002110a97327173d3d", size = 101398, upload-time = "2026-05-08T04:48:22.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/82/19cdc3d26f18c0848ad53ccc216e8bdcdd60a3c22c60cd47499f8e2dcd2d/types_openpyxl-3.1.5.20260508-py3-none-any.whl", hash = "sha256:603e41a524bb4f34e2ccfee63f5fe1f166d75387d36b1060183242f0958ec539", size = 165593, upload-time = "2026-05-08T04:48:20.834Z" }, +] + +[[package]] +name = "types-paramiko" +version = "4.0.0.20260408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/f5/2a556b03ba264508b6bc6a65131500265f210ff3ebf5d76dbe51b53c3979/types_paramiko-4.0.0.20260408.tar.gz", hash = "sha256:978191a2e11064fa4c7f9ada0fccf49159a17beb98b780310dd2c2d2b4106063", size = 29116, upload-time = "2026-04-08T04:35:04.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/e2/cf451598a6a8820139d021b2be08a836b9b905d744bcc73b72172e7e10b3/types_paramiko-4.0.0.20260408-py3-none-any.whl", hash = "sha256:350bf53edb4eb88181be68854d598e1cc3a8764fe905d49913025b86e831adbc", size = 38816, upload-time = "2026-04-08T04:35:03.503Z" }, +] + +[[package]] +name = "types-pexpect" +version = "4.9.0.20260508" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/e4/c6a3db57e1559ecd588cb8df809a6e29cbc9d8bd52b5357d0f6853cd5930/types_pexpect-4.9.0.20260508.tar.gz", hash = "sha256:1729c05eb86c3f2bf76652576fb9f632921b3392ed951a681f3b4457d78af198", size = 13513, upload-time = "2026-05-08T04:49:24.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/20/8592e3e3ceb6147101a8803cda4be5433da83a56dc562b8dc1c55e9ccbe3/types_pexpect-4.9.0.20260508-py3-none-any.whl", hash = "sha256:a215c07a3d317597b5dcf6ed6892c683a20c8488b2cd37001c885497928a8bdf", size = 17077, upload-time = "2026-05-08T04:49:22.811Z" }, +] + +[[package]] +name = "types-pillow" +version = "10.2.0.20240822" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/4a/4495264dddaa600d65d68bcedb64dcccf9d9da61adff51f7d2ffd8e4c9ce/types-Pillow-10.2.0.20240822.tar.gz", hash = "sha256:559fb52a2ef991c326e4a0d20accb3bb63a7ba8d40eb493e0ecb0310ba52f0d3", size = 35389, upload-time = "2024-08-22T02:32:48.15Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/23/e81a5354859831fcf54d488d33b80ba6133ea84f874a9c0ec40a4881e133/types_Pillow-10.2.0.20240822-py3-none-any.whl", hash = "sha256:d9dab025aba07aeb12fd50a6799d4eac52a9603488eca09d7662543983f16c5d", size = 54354, upload-time = "2024-08-22T02:32:46.664Z" }, +] + +[[package]] +name = "types-psutil" +version = "7.2.2.20260508" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/cc/5ac56357b08655ff93106d6391b72d50c47416a044041312550bcd806827/types_psutil-7.2.2.20260508.tar.gz", hash = "sha256:8cfd8339f5e898570f80486423e65d87558d89d0181bf723d20ac5e778fe218e", size = 26575, upload-time = "2026-05-08T04:46:48.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/44/4467583df75313c28abaadfab12f102fba9db7285323e75601acf936d996/types_psutil-7.2.2.20260508-py3-none-any.whl", hash = "sha256:b142452e0953f2d07dbdbb98d81f3a629f5906cc2d94bb7e34da0fba55fbab4a", size = 32777, upload-time = "2026-05-08T04:46:46.972Z" }, +] + +[[package]] +name = "types-psycopg2" +version = "2.9.21.20260509" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/9e/a78cffd63e14c45fc05664c42739b2b15938acff92ca59e22b22ced5695e/types_psycopg2-2.9.21.20260509.tar.gz", hash = "sha256:0422105f691a409e9d8048c2205aca9d694b70823248c6614393444017e9f088", size = 27215, upload-time = "2026-05-09T04:58:40.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/b3/f4d11df63430d7e14b2889caf73f6f6bf9147a51d866ce2da4c07d9f3c8b/types_psycopg2-2.9.21.20260509-py3-none-any.whl", hash = "sha256:69f6dae384bbea830dff23621936423035db152af901331e6f9c46f7c4f4b24f", size = 24940, upload-time = "2026-05-09T04:58:39.208Z" }, +] + +[[package]] +name = "types-pyasn1" +version = "0.6.0.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/c0/02f897fc8543f64fa6b1ca6a30d388e37c4ec2f761f469a2d9a29b89cdef/types_pyasn1-0.6.0.20260408.tar.gz", hash = "sha256:32dc90927adbe504fd2eee83ae30cf5ef934e5db0d1d94886071fed47eb50c8c", size = 17312, upload-time = "2026-04-08T04:27:16.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a5/473e06d5aaec3730aab5a9d40c2044e673c927412c24bd7f3fa0df7e95d3/types_pyasn1-0.6.0.20260408-py3-none-any.whl", hash = "sha256:ee7fbd98bce61193c5d4f8f7812fa53cddc5b8cc5ceb9fcda6eea539947c6d6b", size = 24044, upload-time = "2026-04-08T04:27:16.002Z" }, +] + +[[package]] +name = "types-pycurl" +version = "7.46.0.20260509" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/3e/9b7e3779b7baf42d650ccc9943f3fd620207446bbdd5103c6a2be3bf2fbb/types_pycurl-7.46.0.20260509.tar.gz", hash = "sha256:719d328744d0a0f1765c7a2eb3e3b081edc8ddec4ca668b61e82625a4519b359", size = 16277, upload-time = "2026-05-09T04:58:55.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/b9/9635032e42e92d466edbed314d1ddbaf0e0dedc81bed4083e09f44a71799/types_pycurl-7.46.0.20260509-py3-none-any.whl", hash = "sha256:9399a9e0c2682e8740a8027bbc76e4510380bc71165d27c9f6107e5042926334", size = 14351, upload-time = "2026-05-09T04:58:53.891Z" }, +] + +[[package]] +name = "types-pygments" +version = "2.20.0.20260508" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-docutils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/9c/b5c7f3d26108e88594d5d5fa0aaae7edab0ad3e90746cb212a1548f58820/types_pygments-2.20.0.20260508.tar.gz", hash = "sha256:6ec4e232784e427eea12e798fe28d5a28321023937795527a94582d5f56feed7", size = 21102, upload-time = "2026-05-08T04:50:34.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/e6/767672dd4df860550da0aade55ea5b4e6244b3f58edc209f0bb36dca55f2/types_pygments-2.20.0.20260508-py3-none-any.whl", hash = "sha256:42d9c45f3397a46bdb7bf718d9fe9c2d001c842ffa5afe748a4d7a5b5f0708fe", size = 28989, upload-time = "2026-05-08T04:50:33.354Z" }, +] + +[[package]] +name = "types-pymysql" +version = "1.1.0.20260508" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/1a/3dd6633ddc5fda8b81914d768b45378fd874ae5c60509c46a162e8593a9f/types_pymysql-1.1.0.20260508.tar.gz", hash = "sha256:49abf0c2f8d944384834544a8f25ea1b25018c3d815f7ec676f73d0b7fe0731c", size = 22399, upload-time = "2026-05-08T04:46:51.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/43/01afb22053493050209299ac830dfc2d2a0490b7c6b17fafc234a352bc5e/types_pymysql-1.1.0.20260508-py3-none-any.whl", hash = "sha256:a726e6f867e0a1027a297c89d7646e4d52b7db6ccd11b2a40780120257610c78", size = 23069, upload-time = "2026-05-08T04:46:50.802Z" }, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20260408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/6a/749dc53a54a3f35842c1f8197b3ca6b54af6d7458a1bfc75f6629b6da666/types_requests-2.33.0.20260408.tar.gz", hash = "sha256:95b9a86376807a216b2fb412b47617b202091c3ea7c078f47cc358d5528ccb7b", size = 23882, upload-time = "2026-04-08T04:34:49.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/b8/78fd6c037de4788c040fdd323b3369804400351b7827473920f6c1d03c10/types_requests-2.33.0.20260408-py3-none-any.whl", hash = "sha256:81f31d5ea4acb39f03be7bc8bed569ba6d5a9c5d97e89f45ac43d819b68ca50f", size = 20739, upload-time = "2026-04-08T04:34:48.325Z" }, +] + +[[package]] +name = "types-tqdm" +version = "4.67.3.20260508" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/d9/add71c78db72e934747f7467ffe7b8fa9f3e9fb38ffa5377d5dd390ac036/types_tqdm-4.67.3.20260508.tar.gz", hash = "sha256:9acfdd179bdf5cc81f7ce7353b5b85eb92b16667bba89ec6c187b5e7ce617986", size = 18141, upload-time = "2026-05-08T04:52:34.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/64/e66c98e951deb5985fbff40a11ba0a1f0528505e0734fa6c39534fc0b113/types_tqdm-4.67.3.20260508-py3-none-any.whl", hash = "sha256:0440759cc861a90c1cc98870f2c15ac633c0b6b14651dcafb83f98ab83bad0f4", size = 24546, upload-time = "2026-05-08T04:52:33.995Z" }, +] + +[[package]] +name = "types-ujson" +version = "5.10.0.20250822" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/bd/d372d44534f84864a96c19a7059d9b4d29db8541828b8b9dc3040f7a46d0/types_ujson-5.10.0.20250822.tar.gz", hash = "sha256:0a795558e1f78532373cf3f03f35b1f08bc60d52d924187b97995ee3597ba006", size = 8437, upload-time = "2025-08-22T03:02:19.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/f2/d812543c350674d8b3f6e17c8922248ee3bb752c2a76f64beb8c538b40cf/types_ujson-5.10.0.20250822-py3-none-any.whl", hash = "sha256:3e9e73a6dc62ccc03449d9ac2c580cd1b7a8e4873220db498f7dd056754be080", size = 7657, upload-time = "2025-08-22T03:02:18.699Z" }, +] + +[[package]] +name = "types-webencodings" +version = "0.5.0.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/d2/21567fac142315580ce3ee37d08a4e8819921dae833bbcd27b9f8b373799/types_webencodings-0.5.0.20260408.tar.gz", hash = "sha256:28c596619f367e43eee393d85f63e8d2fdb6874c654a8d441c37f8afe29c6d0d", size = 7504, upload-time = "2026-04-08T04:28:51.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/e4/f13be8f6d9a561166f7d963012d0ccc833e13aee3044c4f1a8fb1fee462a/types_webencodings-0.5.0.20260408-py3-none-any.whl", hash = "sha256:19a2afe5c22d9b1e880b49ff823c7b531f473a390fe47ac903c0bdb5cd677dd9", size = 8717, upload-time = "2026-04-08T04:28:50.943Z" }, +] + +[[package]] +name = "types-xlrd" +version = "2.0.0.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/41/0045814e0f0676e3526453a4e5beb4914aae354126a7de7225c6c2d7568e/types_xlrd-2.0.0.20260408.tar.gz", hash = "sha256:f86341825dcecc82c9eb4955308a9e7ea07d9d7290aca9a8f5373d7cdf172a2c", size = 13984, upload-time = "2026-04-08T04:29:05.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/3d/fa50879aad7e63d1b0d4fe6ea97db0e1a7141a223f6f8d8eafa9df5033a8/types_xlrd-2.0.0.20260408-py3-none-any.whl", hash = "sha256:a140966631a855d69d69aa8842526123f2aaa1f855338083e361561a266a6f5a", size = 16879, upload-time = "2026-04-08T04:29:05.143Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uv" +version = "0.12.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/b0/3085b844fe59aa319a3f94a5cca9938fffecc82705aa9c2762a749f7095c/uv-0.12.5.tar.gz", hash = "sha256:442a21d181faae21742aaaf6d2091a0d27755d3eac344061a9a00c90169b7524", size = 7101936, upload-time = "2026-08-14T19:56:57.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/4c/6412d4a618230db699118b362ec41c54795f93992b43c53e225bd0213501/uv-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:2bd62134e56af35b9cf017aaf8ae41a605d6501dd49afc35b70b544a45dd8354", size = 23310055, upload-time = "2026-08-14T19:55:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ec/d76387b388fa21620088b89b9c67f2596a707add585104e0cb5e8abf55f2/uv-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1a06c8bc4d43b5f6c1e3f2ae3d0f6455b07515f762516f95e52e6c0cbccedf15", size = 21401335, upload-time = "2026-08-14T19:55:55.371Z" }, + { url = "https://files.pythonhosted.org/packages/6d/bc/81ab953b7261ae6be40874b1f283a10873871e02eb353d354614dd8da96b/uv-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d87156bc174d94fae890bb7a261e2867140abb9fe1e9de81a5295e582fb9d0f5", size = 19290641, upload-time = "2026-08-14T19:55:58.998Z" }, + { url = "https://files.pythonhosted.org/packages/7d/13/07585043c10e648820bf826474dac46864ce6691da5dc52fee43c5c7523a/uv-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2d65b7b3bc3fd28678f62aa7fb5d90f106ad9782c1354af60b6cecdf9ea9ecd9", size = 22245569, upload-time = "2026-08-14T19:56:02.729Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6d/310f8f56f8d001b4000112a09d7b7de80fb2024a90208fabb9ddc457c123/uv-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:712624b62e25c84e5a10fc6aa144d8a81b685fdc067a54a7ca4367d75d2cf791", size = 22745152, upload-time = "2026-08-14T19:56:06.426Z" }, + { url = "https://files.pythonhosted.org/packages/92/da/7922b67eec5ee03e94333c5841b682c335033ee80acac17c3417bd752656/uv-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9656ac7a00fd4314980fb0f790df1c1f3fa9cbcf9af9c6f611b19448b9da687", size = 22787947, upload-time = "2026-08-14T19:56:10.149Z" }, + { url = "https://files.pythonhosted.org/packages/62/55/5dbaed832a4b36809ef8a07c8e56e9fee0dedb0aa0454f6d232b6e468f2c/uv-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:568485b44e848eb3693f85d6b00299ccd8fc4d26902030dbf24f549c276db9ca", size = 23367616, upload-time = "2026-08-14T19:56:13.768Z" }, + { url = "https://files.pythonhosted.org/packages/11/77/baf761d12bb66efb01706e3bbb5926ed0d13cb0a40539a661fcfffd46de4/uv-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd08c82831b0033330f8eeeb0d90f938a4d999f25569bee68a975c736142d795", size = 24586263, upload-time = "2026-08-14T19:56:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a8/76c1031c4834c959bb8a8059c9feabeaa77488ce8b6a3529d6d929ae81cf/uv-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:edd9ff6154b891146a342c143cd29b330ad97ac6a4b20ff4a99a20a4da84ceca", size = 24160655, upload-time = "2026-08-14T19:56:21.568Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/dacc9a0bc8604187a1ba954a3aef8329e4104eb0af772d2c3c634893bd9b/uv-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e195ccf1ed60c8bb24a6447ce306441a4181d54b602407e09bc56e963911c15", size = 23657089, upload-time = "2026-08-14T19:56:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/39/98/e8f9c071622f2cb4072d8b587d27b27d23cf0d3ebf8b3687f5af6030f587/uv-0.12.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:58abfb0f658b39a834307a11223bc170294ea214263b4c99ecc7663720d43544", size = 22379954, upload-time = "2026-08-14T19:56:28.789Z" }, + { url = "https://files.pythonhosted.org/packages/73/95/4c3f060e95f7cbe9177b4ab361f0cbfc4ae22e5a49b22e73eee9f0d0a6ca/uv-0.12.5-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:6ad2c455f1fe4d2962f6fd7ccb3b1f61c61856681c9d99f40e170b2074353fa3", size = 23318163, upload-time = "2026-08-14T19:56:32.504Z" }, + { url = "https://files.pythonhosted.org/packages/a0/96/ca0497ef8912ef48dbbc9982a8b4212260c34d56bfd0d45fe67b31942121/uv-0.12.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a05b497c2a948c8600f4c831a89852b4d2514b7f561074225cc9edd0cc4811e2", size = 23470437, upload-time = "2026-08-14T19:56:36.525Z" }, + { url = "https://files.pythonhosted.org/packages/60/e7/8bdc37669a6cd2b46a2ec08ccbb58c61395ec84a073e199f5a4a64bb998f/uv-0.12.5-py3-none-musllinux_1_1_i686.whl", hash = "sha256:7817f8e957960f9ddc452ea353f283c0d6393e2e31b400276485adced5b1f371", size = 22545803, upload-time = "2026-08-14T19:56:40.606Z" }, + { url = "https://files.pythonhosted.org/packages/37/cc/01e39e1dbeb838a6b3c26bf97c867d6f366459b22a38bea691af8c6c94c0/uv-0.12.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:dc14e4f81a99b585a891350c60d1ff4557d54cb3c3c81fa45fd4e0dd512ba752", size = 23874113, upload-time = "2026-08-14T19:56:44.193Z" }, + { url = "https://files.pythonhosted.org/packages/0a/38/9053599a73a351d1cd34195c7a48c1db4d4d51b57b543607fad7ecf9354c/uv-0.12.5-py3-none-win32.whl", hash = "sha256:39bb102766c95571781a7b4c611675ea213e08df5c680f3936279b3c0d1f6c3c", size = 20744641, upload-time = "2026-08-14T19:56:47.689Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f6/a9af9311c7f5640ca2bfcfdedb7aca37fa6d1d9f5c981fb50c5be02b7477/uv-0.12.5-py3-none-win_amd64.whl", hash = "sha256:455c3e57602e2141e66e2f0bf685898c9c5e5a70377d14c9a71554a3baf3ddbf", size = 21621812, upload-time = "2026-08-14T19:56:51.126Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fb/e1266399f755f97a0783de379f2fed6dae0a2a240db32fe5a2eb976fec8a/uv-0.12.5-py3-none-win_arm64.whl", hash = "sha256:bea86f27a027e0e3af908db4bdd4f1ceef3ca2bd47673b5ccca7f550e325b1b4", size = 20381876, upload-time = "2026-08-14T19:56:54.883Z" }, +] + +[[package]] +name = "vulture" +version = "2.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/3e/4d08c5903b2c0c70cad583c170cc4a663fc6a61e2ad00b711fcda61358cd/vulture-2.16.tar.gz", hash = "sha256:f8d9f6e2af03011664a3c6c240c9765b3f392917d3135fddca6d6a68d359f717", size = 52680, upload-time = "2026-03-25T14:41:27.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/f935130312330614811dae2ea9df3f395f6d63889eb6c2e68c14507152ee/vulture-2.16-py3-none-any.whl", hash = "sha256:6e0f1c312cef1c87856957e5c2ca9608834a7c794c2180477f30bf0e4cc58eee", size = 26993, upload-time = "2026-03-25T14:41:26.21Z" }, +] + +[[package]] +name = "yamllint" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathspec" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/a0/8fc2d68e132cf918f18273fdc8a1b8432b60d75ac12fdae4b0ef5c9d2e8d/yamllint-1.38.0.tar.gz", hash = "sha256:09e5f29531daab93366bb061e76019d5e91691ef0a40328f04c927387d1d364d", size = 142446, upload-time = "2026-01-13T07:47:53.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/92/aed08e68de6e6a3d7c2328ce7388072cd6affc26e2917197430b646aed02/yamllint-1.38.0-py3-none-any.whl", hash = "sha256:fc394a5b3be980a4062607b8fdddc0843f4fa394152b6da21722f5d59013c220", size = 68940, upload-time = "2026-01-13T07:47:51.343Z" }, +] From 2639b7419e9194281ae68e89927c327ac39d3c82 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Mon, 31 Aug 2026 00:45:50 -0600 Subject: [PATCH 07/16] fix(runtime): bound parent code-intel refresh --- go/cmd/coding-ethos-run/args.go | 2 ++ go/cmd/coding-ethos-run/main_test.go | 30 +++++++++++++++++++++ go/internal/hooklog/runner.go | 8 +++--- go/internal/hooklog/runner_internal_test.go | 3 +++ 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/go/cmd/coding-ethos-run/args.go b/go/cmd/coding-ethos-run/args.go index 2def0129..ad9501d5 100644 --- a/go/cmd/coding-ethos-run/args.go +++ b/go/cmd/coding-ethos-run/args.go @@ -268,6 +268,8 @@ func (paths runtimePaths) withCommandRoots(args []string) runtimePaths { repoRoot = flagValue(args[1:], "--root", paths.Root) case "runtime-policy": repoRoot = flagValue(args[1:], "--repo", paths.Root) + case "parent-install", "parent-check", "parent-lint": + repoRoot = flagValue(args[1:], "--repo", paths.Root) default: return paths } diff --git a/go/cmd/coding-ethos-run/main_test.go b/go/cmd/coding-ethos-run/main_test.go index aa4246e2..f5d43aa8 100644 --- a/go/cmd/coding-ethos-run/main_test.go +++ b/go/cmd/coding-ethos-run/main_test.go @@ -396,6 +396,36 @@ func TestWithCommandRootsUsesCommandSpecificRepositoryFlags(t *testing.T) { "/private/state", }, }, + { + name: "parent install", + args: []string{ + "parent-install", + "--repo", + "/repo", + "--state-root", + "/private/state", + }, + }, + { + name: "parent check", + args: []string{ + "parent-check", + "--repo", + "/repo", + "--state-root", + "/private/state", + }, + }, + { + name: "parent lint", + args: []string{ + "parent-lint", + "--repo", + "/repo", + "--state-root", + "/private/state", + }, + }, } { t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/go/internal/hooklog/runner.go b/go/internal/hooklog/runner.go index 38f202a9..0f0e1247 100644 --- a/go/internal/hooklog/runner.go +++ b/go/internal/hooklog/runner.go @@ -257,10 +257,10 @@ func autoPruneHookRuns(root string) error { } func shouldForceCodeIntelRefresh(command []string) bool { - return commandContains(command, "parent-install") || - commandContains(command, "parent-check") || - commandContains(command, "parent-lint") || - commandContains(command, "policy-lint") || + // Parent workflows refresh their explicit --repo target as part of the + // workflow itself. Repeating that work here both doubles maintenance and, + // before command-root resolution, could index the invocation repository. + return commandContains(command, "policy-lint") || commandContainsSequence(command, "git-hook", "pre-commit") || commandContainsSequence(command, "git-hook", "pre-push") || commandContainsSequence(command, "make", "pre-commit") || diff --git a/go/internal/hooklog/runner_internal_test.go b/go/internal/hooklog/runner_internal_test.go index 35030e40..725a8f32 100644 --- a/go/internal/hooklog/runner_internal_test.go +++ b/go/internal/hooklog/runner_internal_test.go @@ -12,6 +12,9 @@ func TestShouldForceCodeIntelRefreshSkipsManagedToolRuns(t *testing.T) { {"coding-ethos-run", "policy-tool", "go-test", "go"}, {"coding-ethos-run", "policy-tool", "ruff", "check", "pkg"}, {"coding-ethos-run", "policy-tool-group", "type_check"}, + {"coding-ethos-run", "parent-install", "--repo", "/repo"}, + {"coding-ethos-run", "parent-check", "--repo", "/repo"}, + {"coding-ethos-run", "parent-lint", "--repo", "/repo"}, } { if shouldForceCodeIntelRefresh(command) { t.Fatalf("shouldForceCodeIntelRefresh(%#v) = true, want false", command) From 13fa734d5bf5332629919662e467cb40099c66ea Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Mon, 31 Aug 2026 01:19:30 -0600 Subject: [PATCH 08/16] fix(runtime): keep parent artifact checks bounded --- README.md | 5 +++++ go/cmd/coding-ethos-run/parent_workflow.go | 18 ------------------ 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d48180a0..155aef40 100644 --- a/README.md +++ b/README.md @@ -936,6 +936,11 @@ projects byte-identical executables into the parent repository's stable common Git runtime. `parent-check` hashes both sides and fails if that projection is missing, non-executable, symlinked back to a retiring checkout, or stale. +Parent install and check are artifact workflows and do not perform a full +repository code-intel refresh. Lint and Git-hook workflows refresh code intel +when source analysis is actually part of the requested gate, so deploying or +checking the runtime cannot be delayed by an unrelated whole-repository scan. + When `parent-install` or `parent-lint` receives an external `--state-root`, it leaves the consumer checkout's tracked `.gitignore` unchanged. Other generated parent artifacts remain normal consumer surfaces; repo-local state retains the diff --git a/go/cmd/coding-ethos-run/parent_workflow.go b/go/cmd/coding-ethos-run/parent_workflow.go index 2b87b48d..a14d6947 100644 --- a/go/cmd/coding-ethos-run/parent_workflow.go +++ b/go/cmd/coding-ethos-run/parent_workflow.go @@ -20,7 +20,6 @@ import ( "blackcat.ca/coding-ethos/go/internal/agenthooks" "blackcat.ca/coding-ethos/go/internal/agentskills" "blackcat.ca/coding-ethos/go/internal/apperror" - "blackcat.ca/coding-ethos/go/internal/codeintel" "blackcat.ca/coding-ethos/go/internal/feedback" "blackcat.ca/coding-ethos/go/internal/geminiprompts" "blackcat.ca/coding-ethos/go/internal/hookoutput" @@ -289,10 +288,6 @@ func syncParentArtifacts( })) } - steps = append(steps, runParentStep("code_intel", func() error { - return refreshParentCodeIntel(options.Repo) - })) - return steps } @@ -345,22 +340,9 @@ func checkParentArtifacts( steps = append(steps, runParentStep("agent_hooks", func() error { return agenthooks.DoctorSettings(options.Repo, parentAgentHookCommand(paths)) })) - steps = append(steps, runParentStep("code_intel", func() error { - return refreshParentCodeIntel(options.Repo) - })) - return steps } -func refreshParentCodeIntel(repo string) error { - _, err := codeintel.RefreshRepository(context.Background(), repo, []string{"."}) - if err != nil { - return fmt.Errorf("refresh code-intel: %w", err) - } - - return nil -} - func syncParentPolicyBundle(paths runtimePaths, options parentWorkflowOptions) error { bundle, metadata, err := compileParentPolicyBundle(paths, options, "") if err != nil { From 3549929082418da1604a89a76bd7c9db7ad12c6c Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Mon, 31 Aug 2026 01:25:03 -0600 Subject: [PATCH 09/16] fix(runtime): satisfy parent workflow lint --- go/cmd/coding-ethos-run/parent_workflow.go | 1 + 1 file changed, 1 insertion(+) diff --git a/go/cmd/coding-ethos-run/parent_workflow.go b/go/cmd/coding-ethos-run/parent_workflow.go index a14d6947..77619275 100644 --- a/go/cmd/coding-ethos-run/parent_workflow.go +++ b/go/cmd/coding-ethos-run/parent_workflow.go @@ -340,6 +340,7 @@ func checkParentArtifacts( steps = append(steps, runParentStep("agent_hooks", func() error { return agenthooks.DoctorSettings(options.Repo, parentAgentHookCommand(paths)) })) + return steps } From 51aca249fa1212400e413128291fdec943b84c58 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Mon, 31 Aug 2026 09:31:55 -0600 Subject: [PATCH 10/16] fix(policy): unblock verified consumer commits --- README.md | 12 ++ go/internal/evaluators/git_staged_admin.go | 104 ++++++++++++++++++ .../evaluators/git_staged_admin_test.go | 87 ++++++++++++++- .../evaluators/shell_best_practices.go | 22 ++++ .../evaluators/shell_best_practices_test.go | 50 +++++++++ go/internal/policy/compiler_policies.go | 15 ++- 6 files changed, 283 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 155aef40..7ee9a8f2 100644 --- a/README.md +++ b/README.md @@ -1867,6 +1867,18 @@ bin/coding-ethos-run policy-git --admin-approved commit -F /tmp/msg The flag only changes `git.staged_admin_files` from block to record. It does not disable other policy and is invalid outside this repository. +In consumer repositories, an admin-classified file that is also a generated +tool-config surface may be committed by an agent only when its staged bytes +exactly match the output rendered from the active Coding Ethos policy. The +comparison is against the Git index, so restoring only the working-tree copy +cannot conceal a divergent staged config. Any hand-edited or stale config +remains admin-blocked. + +The optional shell common-helper convention is enforced only when the consumer +actually tracks a `common.sh` helper. A repository without that convention is +still checked for shebangs, strict mode, syntax, and unsafe shell constructs, +but is not told to source a nonexistent file. + Agents must not use `/usr/bin/git` or any other raw Git path for this workflow. ## Development diff --git a/go/internal/evaluators/git_staged_admin.go b/go/internal/evaluators/git_staged_admin.go index 18488115..48d0d236 100644 --- a/go/internal/evaluators/git_staged_admin.go +++ b/go/internal/evaluators/git_staged_admin.go @@ -4,6 +4,8 @@ package evaluators import ( + "crypto/sha256" + "encoding/hex" "fmt" "os" "path/filepath" @@ -12,6 +14,7 @@ import ( "blackcat.ca/coding-ethos/go/diagnostics" "blackcat.ca/coding-ethos/go/internal/policy" "blackcat.ca/coding-ethos/go/internal/shellquote" + "blackcat.ca/coding-ethos/go/internal/toolconfigs" ) func defaultAdminOnlyBasenames() []string { @@ -51,6 +54,32 @@ func EvaluateGitStagedAdminFiles( return nil, nil } + verifiedGenerated, blockedFiles, err := verifiedGeneratedAdminFiles( + context, + blockedFiles, + ) + if err != nil { + return nil, err + } + + if len(blockedFiles) == 0 { + decision := policy.NewDecision(recordDecision, policyDef) + decision.Severity = recordDecision + decision.Message = "Administrative staged files match generated tool policy." + decision.Evidence = stagedAdminEvidence(verifiedGenerated, context.Cwd) + decision.Evidence["verified_generated_files"] = append( + []string(nil), + verifiedGenerated..., + ) + decision.Diagnostics = verifiedGeneratedAdminDiagnostics( + policyDef, + verifiedGenerated, + decision, + ) + + return []policy.Decision{decision}, nil + } + mergeParent, inheritedFiles, divergentFiles := mergeParentAdminFiles( context.Cwd, blockedFiles, @@ -86,6 +115,60 @@ func EvaluateGitStagedAdminFiles( return []policy.Decision{decision}, nil } +func verifiedGeneratedAdminFiles( + context Context, + files []string, +) ([]string, []string, error) { + ethosRoot := stringOption(context.EvaluatorOptions, "ethos_root", "") + if ethosRoot == "" || context.Cwd == "" { + return nil, append([]string(nil), files...), nil + } + + artifacts, err := toolconfigs.StateArtifacts(ethosRoot, context.Cwd, "") + if err != nil { + return nil, nil, fmt.Errorf( + "render generated admin-file policy: %w", + err, + ) + } + + expected := make(map[string]string, len(artifacts)) + for _, artifact := range artifacts { + expected[filepath.ToSlash(filepath.Clean(artifact.Path))] = artifact.ExpectedSHA256 + } + + verified := make([]string, 0, len(files)) + blocked := make([]string, 0, len(files)) + + for _, file := range files { + normalized := filepath.ToSlash(filepath.Clean(file)) + + expectedHash, generated := expected[normalized] + if !generated { + blocked = append(blocked, file) + + continue + } + + staged, readErr := GitCommand(context.Cwd, "show", ":"+normalized).Output() + if readErr != nil || sha256String(staged) != expectedHash { + blocked = append(blocked, file) + + continue + } + + verified = append(verified, file) + } + + return verified, blocked, nil +} + +func sha256String(content []byte) string { + sum := sha256.Sum256(content) + + return "sha256:" + hex.EncodeToString(sum[:]) +} + func mergeParentAdminFiles(cwd string, files []string) (string, []string, []string) { divergent := append([]string(nil), files...) @@ -270,6 +353,27 @@ func inheritedAdminDiagnostics( return items } +func verifiedGeneratedAdminDiagnostics( + policyDef policy.Policy, + files []string, + decision policy.Decision, +) []diagnostics.Diagnostic { + items := make([]diagnostics.Diagnostic, 0, len(files)) + for _, file := range files { + items = append(items, diagnostics.Diagnostic{ + Tool: "git", + File: file, + PolicyID: policyDef.ID, + Severity: decision.Severity, + Message: "Administrative staged file matches generated tool policy.", + Advice: "Keep the generated tool configuration unchanged.", + PrincipleIDs: append([]string(nil), decision.PrincipleIDs...), + }) + } + + return items +} + func stagedFiles(cwd string) ([]string, error) { cmd := GitCommand(cwd, "diff", "--cached", "--name-only") diff --git a/go/internal/evaluators/git_staged_admin_test.go b/go/internal/evaluators/git_staged_admin_test.go index 4628f634..63965586 100644 --- a/go/internal/evaluators/git_staged_admin_test.go +++ b/go/internal/evaluators/git_staged_admin_test.go @@ -169,6 +169,81 @@ func TestEvaluateGitStagedAdminFilesRecordsWithAdminApproval(t *testing.T) { } } +func TestEvaluateGitStagedAdminFilesRecordsExactGeneratedConfig(t *testing.T) { + t.Parallel() + + ethosRoot, repo := syncedGeneratedConfigRepo(t) + initializeStagedAdminGitRepo(t, repo) + runGit(t, repo, "add", ".pylintrc") + + decisions, err := EvaluateGitStagedAdminFiles( + stagedAdminPolicy(), + Context{ + Argv: []string{"git", "commit", "-m", "generated config"}, + Cwd: repo, + EvaluatorOptions: map[string]any{ + "ethos_root": ethosRoot, + }, + }, + ) + if err != nil { + t.Fatalf("evaluate staged generated admin file: %v", err) + } + + if len(decisions) != 1 || decisions[0].Decision != recordDecision { + t.Fatalf("decision mismatch: %#v", decisions) + } + assertStringSliceEvidence( + t, + decisions[0].Evidence, + "verified_generated_files", + []string{".pylintrc"}, + ) +} + +func TestEvaluateGitStagedAdminFilesBlocksDivergentStagedGeneratedConfig( + t *testing.T, +) { + t.Parallel() + + ethosRoot, repo := syncedGeneratedConfigRepo(t) + initializeStagedAdminGitRepo(t, repo) + pylintPath := filepath.Join(repo, ".pylintrc") + expected, err := os.ReadFile(pylintPath) + if err != nil { + t.Fatalf("read generated Pylint config: %v", err) + } + if err = os.WriteFile( + pylintPath, + []byte("[MAIN]\nignore-patterns=.*\n"), + 0o600, + ); err != nil { + t.Fatalf("write divergent Pylint config: %v", err) + } + runGit(t, repo, "add", ".pylintrc") + if err = os.WriteFile(pylintPath, expected, 0o600); err != nil { + t.Fatalf("restore working-tree Pylint config: %v", err) + } + + decisions, err := EvaluateGitStagedAdminFiles( + stagedAdminPolicy(), + Context{ + Argv: []string{"git", "commit", "-m", "divergent config"}, + Cwd: repo, + EvaluatorOptions: map[string]any{ + "ethos_root": ethosRoot, + }, + }, + ) + if err != nil { + t.Fatalf("evaluate divergent staged generated admin file: %v", err) + } + + if len(decisions) != 1 || decisions[0].Decision != blockDecision { + t.Fatalf("decision mismatch: %#v", decisions) + } +} + func TestEvaluateGitStagedAdminFilesRecordsAdminFileInheritedFromMergeParent( t *testing.T, ) { @@ -295,9 +370,7 @@ func stagedAdminRepo(t *testing.T) string { t.Helper() repo := t.TempDir() - runGit(t, repo, "init") - runGit(t, repo, "config", "user.email", "test@example.com") - runGit(t, repo, "config", "user.name", "Test") + initializeStagedAdminGitRepo(t, repo) hookPath := filepath.Join(repo, "bin") @@ -320,6 +393,14 @@ func stagedAdminRepo(t *testing.T) string { return repo } +func initializeStagedAdminGitRepo(t *testing.T, repo string) { + t.Helper() + + runGit(t, repo, "init") + runGit(t, repo, "config", "user.email", "test@example.com") + runGit(t, repo, "config", "user.name", "Test") +} + func stagedAdminMergeRepo(t *testing.T) string { t.Helper() diff --git a/go/internal/evaluators/shell_best_practices.go b/go/internal/evaluators/shell_best_practices.go index c9e6ae72..f6f6c3e5 100644 --- a/go/internal/evaluators/shell_best_practices.go +++ b/go/internal/evaluators/shell_best_practices.go @@ -43,6 +43,9 @@ func EvaluateShellBestPractices( "require_common_for_prefixes", []string{"scripts/"}, ) + if !repositoryHasTrackedCommonShellHelper(context.Cwd) { + requireCommon = nil + } for _, file := range context.Files { if !looksLikeShellFile(file) { @@ -69,6 +72,25 @@ func EvaluateShellBestPractices( return nil, nil } +func repositoryHasTrackedCommonShellHelper(cwd string) bool { + if strings.TrimSpace(cwd) == "" { + return false + } + + output, err := GitCommand(cwd, "ls-files", "--cached").Output() + if err != nil { + return false + } + + for line := range strings.SplitSeq(string(output), "\n") { + if filepath.Base(strings.TrimSpace(line)) == "common.sh" { + return true + } + } + + return false +} + func looksLikeShellFile(path string) bool { base := filepath.Base(path) switch strings.ToLower(filepath.Ext(path)) { diff --git a/go/internal/evaluators/shell_best_practices_test.go b/go/internal/evaluators/shell_best_practices_test.go index 676c322e..3ebab1a4 100644 --- a/go/internal/evaluators/shell_best_practices_test.go +++ b/go/internal/evaluators/shell_best_practices_test.go @@ -88,6 +88,56 @@ func TestEvaluateShellBestPracticesBlocksInvalidShellSyntaxWithLocation(t *testi } } +func TestEvaluateShellBestPracticesRequiresOnlyExistingCommonHelper(t *testing.T) { + t.Parallel() + + repo := t.TempDir() + initializeStagedAdminGitRepo(t, repo) + scriptsDir := filepath.Join(repo, "scripts") + if err := os.MkdirAll(scriptsDir, 0o700); err != nil { + t.Fatalf("create scripts directory: %v", err) + } + + scriptPath := filepath.Join(scriptsDir, "work.sh") + content := []byte("#!/usr/bin/env bash\nset -euo pipefail\necho ok\n") + if err := os.WriteFile(scriptPath, content, 0o600); err != nil { + t.Fatalf("write script: %v", err) + } + + context := Context{ + Cwd: repo, + Files: []string{scriptPath}, + EvaluatorOptions: map[string]any{ + "require_common_for_prefixes": []any{scriptsDir + string(filepath.Separator)}, + }, + } + decisions, err := EvaluateShellBestPractices(shellBestPracticesPolicy(), context) + if err != nil { + t.Fatalf("evaluate without common helper: %v", err) + } + if len(decisions) != 0 { + t.Fatalf("missing common helper must disable convention: %#v", decisions) + } + + commonPath := filepath.Join(scriptsDir, "common.sh") + if err = os.WriteFile(commonPath, content, 0o600); err != nil { + t.Fatalf("write common helper: %v", err) + } + runGit(t, repo, "add", "scripts/common.sh") + + decisions, err = EvaluateShellBestPractices(shellBestPracticesPolicy(), context) + if err != nil { + t.Fatalf("evaluate with common helper: %v", err) + } + if len(decisions) != 1 { + t.Fatalf("existing common helper must activate convention: %#v", decisions) + } + if got := decisions[0].Diagnostics[0].Message; got != + "scripts/ shell files must source the repository common shell helpers" { + t.Fatalf("diagnostic message = %q", got) + } +} + func shellBestPracticesPolicy() policy.Policy { return policy.Policy{ ID: "shell.best_practices", diff --git a/go/internal/policy/compiler_policies.go b/go/internal/policy/compiler_policies.go index 5e5748e9..b42a78fe 100644 --- a/go/internal/policy/compiler_policies.go +++ b/go/internal/policy/compiler_policies.go @@ -42,7 +42,7 @@ func compilePolicies( ) (map[string]Policy, error) { policies := map[string]Policy{} addConfiguredPythonPolicies(policies, config, principles) - addGitPolicies(policies, config, principles) + addGitPolicies(policies, config, principles, configSourceRoot) addSyntaxPolicies(policies, config, principles) addShellPolicies(policies, config, principles) addProxyPolicies(policies, config) @@ -80,8 +80,9 @@ func addGitPolicies( policies map[string]Policy, config map[string]any, principles map[string]Principle, + configSourceRoot string, ) { - for _, policy := range gitPolicies(config, principles) { + for _, policy := range gitPolicies(config, principles, configSourceRoot) { policies[policy.ID] = policy } @@ -103,9 +104,13 @@ func addGitPolicies( } } -func gitPolicies(config map[string]any, principles map[string]Principle) []Policy { +func gitPolicies( + config map[string]any, + principles map[string]Principle, + configSourceRoot string, +) []Policy { return []Policy{ - gitStagedAdminPolicy(config, principles), + gitStagedAdminPolicy(config, principles, configSourceRoot), gitCommitHeadPolicy(principles), } } @@ -113,6 +118,7 @@ func gitPolicies(config map[string]any, principles map[string]Principle) []Polic func gitStagedAdminPolicy( config map[string]any, principles map[string]Principle, + configSourceRoot string, ) Policy { return Policy{ ID: "git.staged_admin_files", @@ -139,6 +145,7 @@ func gitStagedAdminPolicy( Kind: "git_state", Name: "git.staged_admin_files", Options: map[string]any{ + "ethos_root": configSourceRoot, "basenames": stringSliceAt( config, []string{"git", "staged_admin_files", "basenames"}, From ea10cf72c0626a8e95aa398a61077d314fb332da Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Mon, 31 Aug 2026 09:52:50 -0600 Subject: [PATCH 11/16] fix(memory): bound provider project keys --- go/internal/memories/memory.go | 26 +++++++++++++++++++++++++- go/internal/memories/memory_test.go | 19 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/go/internal/memories/memory.go b/go/internal/memories/memory.go index 74356c2b..348031ac 100644 --- a/go/internal/memories/memory.go +++ b/go/internal/memories/memory.go @@ -13,6 +13,7 @@ import ( "slices" "strings" "time" + "unicode/utf8" "go.yaml.in/yaml/v3" @@ -29,6 +30,7 @@ const ( defaultIndexName = "index.yaml" defaultLockName = ".lock" + maxProjectKeyLen = 240 fileMode = 0o600 dirMode = 0o700 ) @@ -631,7 +633,29 @@ func claudeProjectKey(root string) string { replacer := strings.NewReplacer("/", "-", "_", "-", ".", "-") - return replacer.Replace(key) + return boundedClaudeProjectKey(replacer.Replace(key)) +} + +func boundedClaudeProjectKey(key string) string { + if len(key) <= maxProjectKeyLen { + return key + } + + sum := sha256.Sum256([]byte(key)) + suffix := "-" + hex.EncodeToString(sum[:16]) + prefixLimit := maxProjectKeyLen - len(suffix) + cut := prefixLimit + + for cut > 0 && !utf8.ValidString(key[:cut]) { + cut-- + } + + prefix := strings.TrimRight(key[:cut], "-") + if prefix == "" { + prefix = "project" + } + + return prefix + suffix } func normalizedMemoryPath(root, path string) string { diff --git a/go/internal/memories/memory_test.go b/go/internal/memories/memory_test.go index 8540846f..194e1e24 100644 --- a/go/internal/memories/memory_test.go +++ b/go/internal/memories/memory_test.go @@ -192,6 +192,25 @@ func TestImportExistingForRootsKeepsDurableStateOutOfSourceRoot(t *testing.T) { } } +func TestImportExistingForRootsBoundsLongClaudeProjectKey(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + + sourceRoot := filepath.Join( + t.TempDir(), + strings.Repeat("authority-", 15), + strings.Repeat("sandbox-", 15), + ) + if err := os.MkdirAll(sourceRoot, 0o700); err != nil { + t.Fatalf("create long source root: %v", err) + } + + stateRoot := t.TempDir() + if _, err := memories.ImportExistingForRoots(sourceRoot, stateRoot); err != nil { + t.Fatalf("import with long Claude project key: %v", err) + } +} + func TestLoadSettingsMergesRepoOverride(t *testing.T) { t.Parallel() From e2af2ebe7f290f1062890e9b0446366635d6b7b2 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Mon, 31 Aug 2026 10:38:20 -0600 Subject: [PATCH 12/16] fix(hooks): harden generated consumer commits --- README.md | 8 ++ go/internal/generatedtrust/staged.go | 112 ++++++++++++++++ go/internal/githookcli/main.go | 10 +- go/internal/hooklog/runner.go | 14 +- go/internal/hooklog/runner_internal_test.go | 12 ++ go/internal/lint/runner.go | 69 +++++++++- go/internal/lint/runner_test.go | 134 ++++++++++++++++++++ go/internal/lintcli/main.go | 12 +- go/internal/mcp/server.go | 16 ++- 9 files changed, 361 insertions(+), 26 deletions(-) create mode 100644 go/internal/generatedtrust/staged.go diff --git a/README.md b/README.md index 7ee9a8f2..0ed61ef2 100644 --- a/README.md +++ b/README.md @@ -940,6 +940,14 @@ Parent install and check are artifact workflows and do not perform a full repository code-intel refresh. Lint and Git-hook workflows refresh code intel when source analysis is actually part of the requested gate, so deploying or checking the runtime cannot be delayed by an unrelated whole-repository scan. +Failed or policy-blocked hooks retain their trace evidence but do not perform a +whole-repository refresh, because no accepted source transition occurred. + +Consumer commits may include generated tool and provider configuration only +when the staged Git-index bytes exactly match output rendered by the active +Coding Ethos authority. That narrow trust record exempts the generated surface +from path-write guards while every content policy still runs; restoring a clean +working-tree copy cannot conceal divergent staged bytes. When `parent-install` or `parent-lint` receives an external `--state-root`, it leaves the consumer checkout's tracked `.gitignore` unchanged. Other generated diff --git a/go/internal/generatedtrust/staged.go b/go/internal/generatedtrust/staged.go new file mode 100644 index 00000000..6972ae57 --- /dev/null +++ b/go/internal/generatedtrust/staged.go @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +// Package generatedtrust verifies staged generated surfaces against the active +// Coding Ethos authority before path-protection policies consider exemptions. +package generatedtrust + +import ( + "crypto/sha256" + "encoding/hex" + "path/filepath" + "sort" + "strings" + + "blackcat.ca/coding-ethos/go/internal/agenthooks" + "blackcat.ca/coding-ethos/go/internal/evaluators" + "blackcat.ca/coding-ethos/go/internal/policy" + "blackcat.ca/coding-ethos/go/internal/shellquote" + "blackcat.ca/coding-ethos/go/internal/syncstate" + "blackcat.ca/coding-ethos/go/internal/toolconfigs" +) + +// ExactStagedFiles returns only files whose Git index bytes exactly match an +// artifact rendered by the active authority. Rendering and index-read errors +// fail closed by omitting the affected artifact. +func ExactStagedFiles(bundle policy.Bundle, cwd string, files []string) []string { + if strings.TrimSpace(cwd) == "" || len(files) == 0 { + return nil + } + + ethosRoot := generatedConfigEthosRoot(bundle) + if ethosRoot == "" { + return nil + } + + artifacts := generatedArtifacts(ethosRoot, cwd) + staged := normalizedPathSet(files) + trusted := map[string]bool{} + + for _, artifact := range artifacts { + path := filepath.ToSlash(filepath.Clean(artifact.Path)) + if !staged[path] || artifact.ExpectedSHA256 == "" { + continue + } + + content, err := evaluators.GitCommand(cwd, "show", ":"+path).Output() + if err == nil && sha256Bytes(content) == artifact.ExpectedSHA256 { + trusted[path] = true + } + } + + result := make([]string, 0, len(trusted)) + for path := range trusted { + result = append(result, path) + } + + sort.Strings(result) + + return result +} + +func generatedArtifacts(ethosRoot, repoRoot string) []syncstate.Artifact { + artifacts := []syncstate.Artifact{} + + toolArtifacts, err := toolconfigs.StateArtifacts(ethosRoot, repoRoot, "") + if err == nil { + artifacts = append(artifacts, toolArtifacts...) + } + + hookCommand := shellquote.Command( + filepath.Join(ethosRoot, "bin", "coding-ethos-run"), + "agent-hook", + ) + + hookArtifacts, err := agenthooks.StateArtifacts(repoRoot, hookCommand) + if err == nil { + artifacts = append(artifacts, hookArtifacts...) + } + + return artifacts +} + +func generatedConfigEthosRoot(bundle policy.Bundle) string { + policyDef, found := bundle.Policies["generated_config.freshness"] + if !found { + return "" + } + + for _, evaluator := range policyDef.Evaluators { + value, ok := evaluator.Options["ethos_root"].(string) + if ok && strings.TrimSpace(value) != "" { + return filepath.Clean(value) + } + } + + return "" +} + +func normalizedPathSet(files []string) map[string]bool { + paths := make(map[string]bool, len(files)) + for _, file := range files { + paths[filepath.ToSlash(filepath.Clean(file))] = true + } + + return paths +} + +func sha256Bytes(content []byte) string { + sum := sha256.Sum256(content) + + return "sha256:" + hex.EncodeToString(sum[:]) +} diff --git a/go/internal/githookcli/main.go b/go/internal/githookcli/main.go index 9091f950..5892f27b 100644 --- a/go/internal/githookcli/main.go +++ b/go/internal/githookcli/main.go @@ -19,6 +19,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/apperror" "blackcat.ca/coding-ethos/go/internal/evaluators" "blackcat.ca/coding-ethos/go/internal/feedback" + "blackcat.ca/coding-ethos/go/internal/generatedtrust" "blackcat.ca/coding-ethos/go/internal/gitwrap" "blackcat.ca/coding-ethos/go/internal/hookoutput" "blackcat.ca/coding-ethos/go/internal/hookrunnercli" @@ -493,10 +494,11 @@ func runHookPolicy( files []string, ) (lint.Result, error) { result, err := lint.Run(bundle, lint.Options{ - AdminApproved: adminApproved(cwd), - Scope: scope, - Cwd: cwd, - Files: files, + AdminApproved: adminApproved(cwd), + Scope: scope, + Cwd: cwd, + Files: files, + TrustedGeneratedFiles: generatedtrust.ExactStagedFiles(bundle, cwd, files), }) if err != nil { return lint.Result{}, fmt.Errorf("run hook policy: %w", err) diff --git a/go/internal/hooklog/runner.go b/go/internal/hooklog/runner.go index 0f0e1247..6539ffa5 100644 --- a/go/internal/hooklog/runner.go +++ b/go/internal/hooklog/runner.go @@ -167,7 +167,7 @@ func runWithStatus(options Options) (int, error) { return 1, metadataErr } - maintenanceErr := finishHookMaintenance(options, runDir) + maintenanceErr := finishHookMaintenance(options, runDir, status) return completedHookStatus(status, err, maintenanceErr) } @@ -199,8 +199,8 @@ func completedHookStatus(status int, runErr, maintenanceErr error) (int, error) return status, nil } -func finishHookMaintenance(options Options, runDir string) error { - err := refreshCodeIntelAfterRun(options, runDir) +func finishHookMaintenance(options Options, runDir string, status int) error { + err := refreshCodeIntelAfterRun(options, runDir, status) if err != nil { return err } @@ -214,8 +214,8 @@ func finishHookMaintenance(options Options, runDir string) error { return nil } -func refreshCodeIntelAfterRun(options Options, runDir string) error { - if shouldForceCodeIntelRefresh(options.Command) { +func refreshCodeIntelAfterRun(options Options, runDir string, status int) error { + if shouldRefreshRepository(options.Command, status) { _, err := codeintel.RefreshRepository( context.Background(), options.Root, @@ -247,6 +247,10 @@ func refreshCodeIntelAfterRun(options Options, runDir string) error { return nil } +func shouldRefreshRepository(command []string, status int) bool { + return status == 0 && shouldForceCodeIntelRefresh(command) +} + func autoPruneHookRuns(root string) error { err := outputsurface.AutoPruneSurface(context.Background(), root, "hook_runs", false) if err != nil { diff --git a/go/internal/hooklog/runner_internal_test.go b/go/internal/hooklog/runner_internal_test.go index 725a8f32..693eb238 100644 --- a/go/internal/hooklog/runner_internal_test.go +++ b/go/internal/hooklog/runner_internal_test.go @@ -37,3 +37,15 @@ func TestShouldForceCodeIntelRefreshKeepsHookAndRepoGates(t *testing.T) { } } } + +func TestShouldRefreshRepositorySkipsFailedHook(t *testing.T) { + t.Parallel() + + command := []string{"coding-ethos-run", "git-hook", "pre-commit"} + if !shouldRefreshRepository(command, 0) { + t.Fatal("successful pre-commit should refresh repository code-intel") + } + if shouldRefreshRepository(command, 2) { + t.Fatal("blocked pre-commit must not perform a full repository refresh") + } +} diff --git a/go/internal/lint/runner.go b/go/internal/lint/runner.go index cbb94bb7..df9a75ee 100644 --- a/go/internal/lint/runner.go +++ b/go/internal/lint/runner.go @@ -43,12 +43,13 @@ var ( ) type Options struct { - Command string - Cwd string - Scope string - Files []string - Argv []string - AdminApproved bool + Command string + Cwd string + Scope string + Files []string + Argv []string + TrustedGeneratedFiles []string + AdminApproved bool } func Run(bundle policy.Bundle, options Options) (Result, error) { @@ -70,6 +71,7 @@ func RunWithRegistry( return Result{}, err } + trustedGenerated := normalizedTrustedGeneratedFiles(options, scope) decisions := make([]policy.Decision, 0, len(policyIDs)) for _, policyID := range policyIDs { policyDef, ok := bundle.Policies[policyID] @@ -82,7 +84,15 @@ func RunWithRegistry( ) } - evaluated, err := evaluatePolicy(policyDef, scope, options, registry) + policyOptions := options + if policyAllowsExactGeneratedFiles(policyID) { + policyOptions.Files = withoutTrustedGeneratedFiles( + options.Files, + trustedGenerated, + ) + } + + evaluated, err := evaluatePolicy(policyDef, scope, policyOptions, registry) if err != nil { return Result{}, fmt.Errorf("evaluate policy %q: %w", policyID, err) } @@ -104,6 +114,51 @@ func RunWithRegistry( return EnrichResultWithSkills(result, bundle.Skills), nil } +func policyAllowsExactGeneratedFiles(policyID string) bool { + switch policyID { + case "agent_workspace.enforcement_point_write", + "filesystem.protected_path", + "shell.forbidden_strings": + return true + default: + return false + } +} + +func withoutTrustedGeneratedFiles(files []string, trusted map[string]bool) []string { + filtered := make([]string, 0, len(files)) + for _, file := range files { + if trusted[filepath.ToSlash(filepath.Clean(file))] { + continue + } + + filtered = append(filtered, file) + } + + return filtered +} + +func normalizedTrustedGeneratedFiles(options Options, scope string) map[string]bool { + trusted := map[string]bool{} + if scope != ScopeStaged { + return trusted + } + + staged := make(map[string]bool, len(options.Files)) + for _, file := range options.Files { + staged[filepath.ToSlash(filepath.Clean(file))] = true + } + + for _, file := range options.TrustedGeneratedFiles { + path := filepath.ToSlash(filepath.Clean(file)) + if staged[path] { + trusted[path] = true + } + } + + return trusted +} + func enrichDecisionDiagnostics( decisions []policy.Decision, evidenceMaps []diagnostics.EvidenceMap, diff --git a/go/internal/lint/runner_test.go b/go/internal/lint/runner_test.go index 29f73e05..a37f431d 100644 --- a/go/internal/lint/runner_test.go +++ b/go/internal/lint/runner_test.go @@ -5,13 +5,17 @@ package lint_test import ( "os" + "os/exec" "path/filepath" "strings" "testing" "blackcat.ca/coding-ethos/go/diagnostics" + "blackcat.ca/coding-ethos/go/internal/agenthooks" + "blackcat.ca/coding-ethos/go/internal/generatedtrust" . "blackcat.ca/coding-ethos/go/internal/lint" "blackcat.ca/coding-ethos/go/internal/policy" + "blackcat.ca/coding-ethos/go/internal/toolconfigs" ) const statusBlocked = "blocked" @@ -277,6 +281,125 @@ func TestRunLimitsForbiddenStringFileContentScanToAutomationSurfaces(t *testing. } } +func TestRunAllowsExactStagedGeneratedToolConfig(t *testing.T) { + t.Parallel() + + ethosRoot := repoRootForLintTest(t) + repo := initializedLintGitRepo(t) + + _, err := toolconfigs.Sync(ethosRoot, repo, "") + if err != nil { + t.Fatalf("sync generated tool configs: %v", err) + } + runLintGit(t, repo, "add", ".bandit.yml") + + bundle := compiledRepoLintBundle(t) + files := []string{".bandit.yml"} + result, err := Run(bundle, Options{ + Scope: ScopeStaged, + Cwd: repo, + Files: files, + TrustedGeneratedFiles: generatedtrust.ExactStagedFiles(bundle, repo, files), + }) + if err != nil { + t.Fatalf("run staged generated-config lint: %v", err) + } + if lintResultHasBlockingDecision(result, "filesystem.protected_path") || + lintResultHasBlockingDecision(result, "agent_workspace.enforcement_point_write") { + t.Fatalf("exact generated config was blocked: %#v", result.Decisions) + } +} + +func TestRunBlocksDivergentStagedGeneratedToolConfig(t *testing.T) { + t.Parallel() + + ethosRoot := repoRootForLintTest(t) + repo := initializedLintGitRepo(t) + + _, err := toolconfigs.Sync(ethosRoot, repo, "") + if err != nil { + t.Fatalf("sync generated tool configs: %v", err) + } + path := filepath.Join(repo, ".bandit.yml") + expected, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read generated tool config: %v", err) + } + if err = os.WriteFile(path, []byte("skips: [B101]\n"), 0o600); err != nil { + t.Fatalf("write divergent generated tool config: %v", err) + } + runLintGit(t, repo, "add", ".bandit.yml") + if err = os.WriteFile(path, expected, 0o600); err != nil { + t.Fatalf("restore generated tool config: %v", err) + } + + bundle := compiledRepoLintBundle(t) + files := []string{".bandit.yml"} + result, err := Run(bundle, Options{ + Scope: ScopeStaged, + Cwd: repo, + Files: files, + TrustedGeneratedFiles: generatedtrust.ExactStagedFiles(bundle, repo, files), + }) + if err != nil { + t.Fatalf("run divergent staged generated-config lint: %v", err) + } + if !lintResultHasBlockingDecision(result, "filesystem.protected_path") { + t.Fatalf("divergent generated config was not blocked: %#v", result.Decisions) + } +} + +func TestRunAllowsExactStagedGeneratedAgentConfig(t *testing.T) { + t.Parallel() + + ethosRoot := repoRootForLintTest(t) + repo := initializedLintGitRepo(t) + hookCommand := filepath.Join(ethosRoot, "bin", "coding-ethos-run") + " agent-hook" + + if err := agenthooks.SyncSettings(repo, hookCommand); err != nil { + t.Fatalf("sync generated agent settings: %v", err) + } + runLintGit(t, repo, "add", ".codex/config.toml") + + bundle := compiledRepoLintBundle(t) + files := []string{".codex/config.toml"} + result, err := Run(bundle, Options{ + Scope: ScopeStaged, + Cwd: repo, + Files: files, + TrustedGeneratedFiles: generatedtrust.ExactStagedFiles(bundle, repo, files), + }) + if err != nil { + t.Fatalf("run staged generated-agent lint: %v", err) + } + if lintResultHasBlockingDecision(result, "agent_workspace.enforcement_point_write") || + lintResultHasBlockingDecision(result, "shell.forbidden_strings") { + t.Fatalf("exact generated agent config was blocked: %#v", result.Decisions) + } +} + +func initializedLintGitRepo(t *testing.T) string { + t.Helper() + + repo := t.TempDir() + runLintGit(t, repo, "init") + runLintGit(t, repo, "config", "user.email", "test@example.com") + runLintGit(t, repo, "config", "user.name", "Test") + + return repo +} + +func runLintGit(t *testing.T, cwd string, args ...string) { + t.Helper() + + command := exec.Command("git", args...) + command.Dir = cwd + output, err := command.CombinedOutput() + if err != nil { + t.Fatalf("git %v: %v: %s", args, err, output) + } +} + func TestRunAcceptsCommitMessageScope(t *testing.T) { t.Parallel() @@ -362,6 +485,17 @@ func lintResultHasDecision(result Result, policyID string) bool { return false } +func lintResultHasBlockingDecision(result Result, policyID string) bool { + for _, decision := range result.Decisions { + if decision.PolicyID == policyID && + (decision.Decision == "block" || decision.Severity == "block") { + return true + } + } + + return false +} + func compiledRepoLintBundle(tb testing.TB) policy.Bundle { tb.Helper() diff --git a/go/internal/lintcli/main.go b/go/internal/lintcli/main.go index 1dbd9839..d2983f28 100644 --- a/go/internal/lintcli/main.go +++ b/go/internal/lintcli/main.go @@ -15,6 +15,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/codeintel" "blackcat.ca/coding-ethos/go/internal/evaluators" "blackcat.ca/coding-ethos/go/internal/feedback" + "blackcat.ca/coding-ethos/go/internal/generatedtrust" "blackcat.ca/coding-ethos/go/internal/hookoutput" "blackcat.ca/coding-ethos/go/internal/lint" "blackcat.ca/coding-ethos/go/internal/managedcapture" @@ -471,11 +472,12 @@ func runLintMode(config lintCLIConfig, bundle policy.Bundle) int { } result, err := lint.Run(bundle, lint.Options{ - Scope: config.scope.Value(), - Files: files, - Argv: parseArgv(*config.argvRaw), - Command: *config.command, - Cwd: *config.cwd, + Scope: config.scope.Value(), + Files: files, + Argv: parseArgv(*config.argvRaw), + Command: *config.command, + Cwd: *config.cwd, + TrustedGeneratedFiles: generatedtrust.ExactStagedFiles(bundle, *config.cwd, files), }) if err != nil { exitErr(err) diff --git a/go/internal/mcp/server.go b/go/internal/mcp/server.go index 4ef1e0fe..f8c06dc7 100644 --- a/go/internal/mcp/server.go +++ b/go/internal/mcp/server.go @@ -18,6 +18,7 @@ import ( "blackcat.ca/coding-ethos/go/diagnostics" "blackcat.ca/coding-ethos/go/internal/apperror" + "blackcat.ca/coding-ethos/go/internal/generatedtrust" "blackcat.ca/coding-ethos/go/internal/hookoutput" "blackcat.ca/coding-ethos/go/internal/hooks" "blackcat.ca/coding-ethos/go/internal/lint" @@ -432,11 +433,16 @@ func (server Server) checkLint(args json.RawMessage) (any, error) { } result, err := lint.Run(server.bundle, lint.Options{ - Command: input.Command, - Cwd: input.Cwd, - Scope: input.Scope, - Files: append([]string(nil), input.Files...), - Argv: append([]string(nil), input.Argv...), + Command: input.Command, + Cwd: input.Cwd, + Scope: input.Scope, + Files: append([]string(nil), input.Files...), + Argv: append([]string(nil), input.Argv...), + TrustedGeneratedFiles: generatedtrust.ExactStagedFiles( + server.bundle, + input.Cwd, + input.Files, + ), AdminApproved: input.AdminApproved, }) if err != nil { From 3810efd7c4872a24258c51f052c521730b9dcda5 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Mon, 31 Aug 2026 10:42:37 -0600 Subject: [PATCH 13/16] fix(lint): satisfy full-tree layout gate --- go/internal/lint/runner.go | 1 + 1 file changed, 1 insertion(+) diff --git a/go/internal/lint/runner.go b/go/internal/lint/runner.go index df9a75ee..8a34cc61 100644 --- a/go/internal/lint/runner.go +++ b/go/internal/lint/runner.go @@ -72,6 +72,7 @@ func RunWithRegistry( } trustedGenerated := normalizedTrustedGeneratedFiles(options, scope) + decisions := make([]policy.Decision, 0, len(policyIDs)) for _, policyID := range policyIDs { policyDef, ok := bundle.Policies[policyID] From 210c6d1b0a3070f66eda66eb023a788cd068f499 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Mon, 31 Aug 2026 12:15:41 -0600 Subject: [PATCH 14/16] fix(policy): allow synthetic sandbox homes --- config.yaml | 2 ++ go/internal/evaluators/file_guards.go | 38 +++++++++++++++++++++- go/internal/evaluators/file_guards_test.go | 35 ++++++++++++++++++++ go/internal/policy/compiler_policies.go | 5 +++ go/internal/policy/compiler_test.go | 9 +++++ repo_config.example.yaml | 2 ++ 6 files changed, 90 insertions(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index 805836fc..e44e876c 100644 --- a/config.yaml +++ b/config.yaml @@ -699,6 +699,8 @@ filesystem: - 'lbox-worktrees/[A-Za-z0-9._-]+' - '/tmp/tmp\.[A-Za-z0-9._-]+' literals: [] + allowed_patterns: + - '/home/agent/' exempt_prefixes: - .git/ license_header: diff --git a/go/internal/evaluators/file_guards.go b/go/internal/evaluators/file_guards.go index dd4cf051..6fecf233 100644 --- a/go/internal/evaluators/file_guards.go +++ b/go/internal/evaluators/file_guards.go @@ -251,6 +251,16 @@ func EvaluatePIIScrubber( return nil, err } + allowedPatterns, err := compiledPatterns( + context.EvaluatorOptions, + "allowed_patterns", + []string{`/home/agent/`}, + "allowed PII", + ) + if err != nil { + return nil, err + } + exemptPrefixes := stringSliceOption( context.EvaluatorOptions, "exempt_prefixes", @@ -268,8 +278,13 @@ func EvaluatePIIScrubber( found, err := scanGuardLines( path, func(lineNumber int, line string) ([]policy.Decision, bool) { + candidate := line + for _, allowed := range allowedPatterns { + candidate = allowed.ReplaceAllString(candidate, "") + } + for _, pattern := range patterns { - if pattern.MatchString(line) { + if pattern.MatchString(candidate) { return []policy.Decision{ fileGuardDecision( policyDef, @@ -483,6 +498,27 @@ func piiPatterns(options map[string]any) ([]*regexp.Regexp, error) { return patterns, nil } +func compiledPatterns( + options map[string]any, + key string, + defaults []string, + label string, +) ([]*regexp.Regexp, error) { + rawPatterns := stringSliceOption(options, key, defaults) + + patterns := make([]*regexp.Regexp, 0, len(rawPatterns)) + for _, raw := range rawPatterns { + pattern, err := regexp.Compile(raw) + if err != nil { + return nil, fmt.Errorf("compile %s pattern %q: %w", label, raw, err) + } + + patterns = append(patterns, pattern) + } + + return patterns, nil +} + func hasHiddenDirectoryComponent(path string) bool { normalized := filepath.ToSlash(path) normalized = strings.TrimPrefix(normalized, "./") diff --git a/go/internal/evaluators/file_guards_test.go b/go/internal/evaluators/file_guards_test.go index 4edbc7d9..f20a42a0 100644 --- a/go/internal/evaluators/file_guards_test.go +++ b/go/internal/evaluators/file_guards_test.go @@ -200,6 +200,41 @@ func TestEvaluatePIIScrubberBlocksConfiguredLiteral(t *testing.T) { } } +func TestEvaluatePIIScrubberAllowsSyntheticAgentHome(t *testing.T) { + t.Parallel() + + path := writeGuardTestFile(t, "sandbox.md", "PATH=/"+"home/agent/bin:/usr/bin\n") + decisions, err := EvaluatePIIScrubber( + fileGuardPolicy("repo.pii_scrubber"), + Context{Files: []string{path}}, + ) + if err != nil { + t.Fatalf("evaluate PII scrubber: %v", err) + } + if len(decisions) != 0 { + t.Fatalf("synthetic sandbox home should pass, got %#v", decisions) + } +} + +func TestEvaluatePIIScrubberStillBlocksRealHomeBesideSyntheticHome(t *testing.T) { + t.Parallel() + + path := writeGuardTestFile( + t, + "sandbox.md", + "sandbox: /"+"home/agent/bin host: /"+"home/example/private\n", + ) + decision := evaluateFileGuardPolicy( + t, + "repo.pii_scrubber", + EvaluatePIIScrubber, + Context{Files: []string{path}}, + ) + if decision.Diagnostics[0].Tool != piiToolName { + t.Fatalf("unexpected diagnostic: %#v", decision.Diagnostics) + } +} + func TestEvaluatePIIScrubberSkipsHiddenDirectories(t *testing.T) { t.Parallel() diff --git a/go/internal/policy/compiler_policies.go b/go/internal/policy/compiler_policies.go index b42a78fe..3cd76dc0 100644 --- a/go/internal/policy/compiler_policies.go +++ b/go/internal/policy/compiler_policies.go @@ -423,6 +423,11 @@ func piiScrubberOptions(config map[string]any) map[string]any { []string{"filesystem", "pii_scrubber", "literals"}, nil, ), + "allowed_patterns": stringSliceAt( + config, + []string{"filesystem", "pii_scrubber", "allowed_patterns"}, + []string{`/home/agent/`}, + ), "exempt_prefixes": stringSliceAt( config, []string{"filesystem", "pii_scrubber", "exempt_prefixes"}, diff --git a/go/internal/policy/compiler_test.go b/go/internal/policy/compiler_test.go index 21b81ac5..655f4cc1 100644 --- a/go/internal/policy/compiler_test.go +++ b/go/internal/policy/compiler_test.go @@ -1681,6 +1681,8 @@ filesystem: exempt_path_prefixes: [docs/plans/] required_ignores: paths: [.runtime/] + pii_scrubber: + allowed_patterns: ['/virtual/agent/'] shell: best_practices: require_common_for_prefixes: [bin/] @@ -1735,6 +1737,13 @@ func assertConfigBackedEvaluatorOptions(t *testing.T, bundle Bundle) { "required_ignore_paths", ".runtime/", ) + assertFirstOptionString( + t, + bundle, + "repo.pii_scrubber", + "allowed_patterns", + "/virtual/agent/", + ) assertFirstOptionString( t, bundle, diff --git a/repo_config.example.yaml b/repo_config.example.yaml index fbf9780d..a3bf0436 100644 --- a/repo_config.example.yaml +++ b/repo_config.example.yaml @@ -216,6 +216,8 @@ filesystem: - '/(home|Users)/[A-Za-z0-9._-]+/' - '/tmp/tmp\.[A-Za-z0-9._-]+' literals: [] + allowed_patterns: + - '/home/agent/' exempt_prefixes: - .git/ From 205a7e15121ba0bfc86024f7ac6d93bae2f35800 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Mon, 31 Aug 2026 17:52:35 -0600 Subject: [PATCH 15/16] fix(integration): close merge gate findings --- Makefile | 6 +- README.md | 6 +- docs/HOOK_RUNTIME_BOOTSTRAP.md | 19 +- go.work.sum | 12 + go/cmd/coding-ethos-run/dispatch.go | 57 +--- go/cmd/coding-ethos-run/main_test.go | 81 ++---- go/go.mod | 8 +- go/go.sum | 16 +- .../evaluators/shell_best_practices.go | 110 +++++++- .../evaluators/shell_best_practices_test.go | 116 ++++++++- go/internal/generatedtrust/staged.go | 39 ++- go/internal/hooklog/runner.go | 6 +- go/internal/hooklog/runner_test.go | 41 +-- go/internal/hookrunnercli/external_tool.go | 210 +++++++++++++-- .../external_tool_internal_test.go | 244 ++++++++++++++++-- go/internal/hookrunnercli/python_policies.go | 5 +- go/internal/hooks/gate_exit_status.go | 41 +-- go/internal/hooks/gate_exit_status_test.go | 20 ++ go/internal/lint/runner_test.go | 27 ++ go/internal/managedcapture/capture.go | 10 +- go/internal/managedcapture/capture_process.go | 2 +- go/internal/managedcapture/capture_test.go | 72 +++++- .../managedcapture/sandbox_cache_env.go | 21 ++ .../managedcapture/sandbox_cache_env_test.go | 45 ++++ go/internal/policygitcli/main.go | 2 +- .../policygitcli/main_internal_test.go | 10 + go/internal/sandbox/sandbox.go | 23 +- .../toolprotocol/actionlint_shellcheck.go | 46 ++++ .../actionlint_shellcheck_test.go | 74 ++++++ pre-commit/hooks/pyproject.toml | 4 +- pre-commit/hooks/uv.lock | 10 +- pyproject.toml | 2 +- uv.lock | 10 +- 33 files changed, 1137 insertions(+), 258 deletions(-) create mode 100644 go/internal/managedcapture/sandbox_cache_env_test.go create mode 100644 go/internal/toolprotocol/actionlint_shellcheck.go create mode 100644 go/internal/toolprotocol/actionlint_shellcheck_test.go diff --git a/Makefile b/Makefile index 75634d51..27b0c6fc 100644 --- a/Makefile +++ b/Makefile @@ -35,8 +35,12 @@ GOFMT ?= gofmt CARGO ?= cargo GO_BUILD_FLAGS ?= -trimpath -buildvcs=false GO_BUILD_CACHE_DIR ?= $(LOCAL_REPO_ROOT)/.coding-ethos/cache/go-build +GO_PATH_DIR ?= $(LOCAL_REPO_ROOT)/.coding-ethos/cache/go-path +GO_MODULE_CACHE_DIR ?= $(GO_PATH_DIR)/pkg/mod UV_CACHE_DIR ?= $(LOCAL_REPO_ROOT)/.coding-ethos/cache/uv export GOCACHE := $(GO_BUILD_CACHE_DIR) +export GOPATH := $(GO_PATH_DIR) +export GOMODCACHE := $(GO_MODULE_CACHE_DIR) export UV_CACHE_DIR empty := @@ -678,7 +682,7 @@ _sync-parent-hook-runtime: ensure-go go-tools-install policy-bundle-install @"$(GO_TOOLS_BIN_DIR)/coding-ethos-toolchain" install-git-shim \ --dest-dir "$(PARENT_HOOK_BIN_DIR)" \ --real-git "$(GIT)" \ - --runner "$(GO_HOOK)" + --runner "$(PARENT_HOOK_BIN_DIR)/coding-ethos-run" @"$(GO_TOOLS_BIN_DIR)/coding-ethos-lint" \ --install-shims \ --tools-bin-dir "$(PARENT_HOOK_BIN_DIR)" \ diff --git a/README.md b/README.md index 0ed61ef2..1207904a 100644 --- a/README.md +++ b/README.md @@ -945,8 +945,10 @@ whole-repository refresh, because no accepted source transition occurred. Consumer commits may include generated tool and provider configuration only when the staged Git-index bytes exactly match output rendered by the active -Coding Ethos authority. That narrow trust record exempts the generated surface -from path-write guards while every content policy still runs; restoring a clean +Coding Ethos authority and the path is present in the staged diff. That narrow +trust record exempts the generated surface from path-write guards and the +forbidden-string scan that would otherwise reject the authority's own generated +runtime commands; every other content policy still runs. Restoring a clean working-tree copy cannot conceal divergent staged bytes. When `parent-install` or `parent-lint` receives an external `--state-root`, it diff --git a/docs/HOOK_RUNTIME_BOOTSTRAP.md b/docs/HOOK_RUNTIME_BOOTSTRAP.md index a1460776..67459b86 100644 --- a/docs/HOOK_RUNTIME_BOOTSTRAP.md +++ b/docs/HOOK_RUNTIME_BOOTSTRAP.md @@ -219,11 +219,20 @@ Bootstrap needs a few guardrails: - Keep authority build outputs under ignored `bin/` and `build/` directories. - Keep response, trace, and other transient repo-local caches under ignored `.coding-ethos/` paths, not under the Git common runtime. -- Hook-launched `uv` commands bind both their download cache and project - environment to the consumer-owned `.coding-ethos/cache/` tree and use the - sealed project's committed lockfile in frozen mode. The installed shared - runtime remains read-only and never receives a generated `.venv` or a lockfile - rewrite. +- Hook and managed-capture Go commands use the consumer-owned + `.coding-ethos/cache/go-path/` as `GOPATH` and its `pkg/mod/` child as + `GOMODCACHE`. This keeps downloaded modules and Go's `pkg/sumdb` checksum + database writable inside the admitted repository cache instead of inheriting + an operator-owned host path. +- Hook-launched `uv` commands share the consumer-owned + `.coding-ethos/cache/uv/` download cache. Each active project, resolved from + an explicit `--project` path or the command working directory, receives a + distinct environment under `.coding-ethos/cache/uv-project-env/`. + `UV_PROJECT_ENVIRONMENT` and `UV_FROZEN` are scoped to that child invocation, + not exported across the hook process. Frozen mode is enabled only when the + command requests `--frozen` or the active project's `uv.lock` exists in + `HEAD`; unsealed projects remain writable. The installed shared runtime + remains read-only and never receives a generated `.venv` or lockfile rewrite. - Install common-runtime executables with temporary-file sync plus atomic rename, and verify them by content rather than mtime. - Keep installed hook entrypoints stable and move versioned behavior into the diff --git a/go.work.sum b/go.work.sum index ccd2ad95..0d7a58c0 100644 --- a/go.work.sum +++ b/go.work.sum @@ -11,6 +11,7 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/renameio/v2 v2.0.2/go.mod h1:OX+G6WHHpHq3NVj7cAOleLOwJfcQ1s3uUJQCrr78SWo= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/hamba/avro/v2 v2.30.0/go.mod h1:X6gDhYv6DQVAT56VqOKuW+PLnQrEQqGB9l1nhlMdAdQ= @@ -35,18 +36,29 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5/go.mod h1:LVehoXe41cL5SCVQilsV7Gg6BNG+Js6P9PhSbYTIUkQ= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= diff --git a/go/cmd/coding-ethos-run/dispatch.go b/go/cmd/coding-ethos-run/dispatch.go index fc423463..58b85b57 100644 --- a/go/cmd/coding-ethos-run/dispatch.go +++ b/go/cmd/coding-ethos-run/dispatch.go @@ -33,6 +33,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/realgit" "blackcat.ca/coding-ethos/go/internal/shellparse" "blackcat.ca/coding-ethos/go/internal/shellquote" + "blackcat.ca/coding-ethos/go/internal/toolprotocol" "blackcat.ca/coding-ethos/go/toolcatalog" ) @@ -1007,13 +1008,17 @@ func runAgentHooksCommand(paths runtimePaths, rest []string) { } func runPolicyTool(paths runtimePaths, rest []string) error { - return runPolicyToolForParent(paths, rest, parentExecutablePath()) + return runPolicyToolForProtocol( + paths, + rest, + os.Getenv(toolprotocol.ActionlintShellcheckEnv), + ) } -func runPolicyToolForParent( +func runPolicyToolForProtocol( paths runtimePaths, rest []string, - parentExecutable string, + protocolMarker string, ) error { if len(rest) == 0 { return apperror.StaticError("policy-tool requires a tool name") @@ -1021,7 +1026,11 @@ func runPolicyToolForParent( requirePolicyBundle(paths) - if actionlintShellcheckDependency(parentExecutable, rest[0], rest[1:]) { + if toolprotocol.IsActionlintShellcheckJSONStdin( + protocolMarker, + rest[0], + rest[1:], + ) { tool, found := toolcatalog.HookOwnedTool("shellcheck") if !found { return apperror.StaticError("managed shellcheck tool is not registered") @@ -1039,46 +1048,6 @@ func runPolicyToolForParent( return nil } -func parentExecutablePath() string { - if runtime.GOOS != linuxGOOS { - return "" - } - - path, err := os.Readlink("/proc/" + strconv.Itoa(os.Getppid()) + "/exe") - if err != nil { - return "" - } - - return path -} - -func actionlintShellcheckDependency( - parentExecutable string, - tool string, - args []string, -) bool { - if filepath.Base(parentExecutable) != "actionlint" || - tool != "shellcheck" || - len(args) == 0 || - args[len(args)-1] != "-" { - return false - } - - for index, arg := range args { - if (arg == "-f" || arg == "--format") && - index+1 < len(args) && - args[index+1] == "json" { - return true - } - - if arg == "-f=json" || arg == "--format=json" { - return true - } - } - - return false -} - func runMCP(paths runtimePaths, rest []string) { bundlePath := hookPolicyBundlePath(paths) requireRuntimeFile(bundlePath, "compiled policy bundle") diff --git a/go/cmd/coding-ethos-run/main_test.go b/go/cmd/coding-ethos-run/main_test.go index f5d43aa8..6fb3f717 100644 --- a/go/cmd/coding-ethos-run/main_test.go +++ b/go/cmd/coding-ethos-run/main_test.go @@ -27,6 +27,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/sharedlock" "blackcat.ca/coding-ethos/go/internal/shellquote" "blackcat.ca/coding-ethos/go/internal/testlock" + "blackcat.ca/coding-ethos/go/internal/toolprotocol" "blackcat.ca/coding-ethos/go/toolcatalog" ) @@ -2610,7 +2611,9 @@ func TestPolicyGitIgnoresArbitraryEnvRealGitExecutable(t *testing.T) { } } -func TestPolicyToolExecutesActionlintShellcheckDependencyRaw(t *testing.T) { +func TestPolicyToolUsesMarkedActionlintShellcheckProtocolWithoutParentPath( + t *testing.T, +) { paths := runtimeTestPaths(t) var calls []string paths.Executor = stubRuntimeOps{calls: &calls} @@ -2634,7 +2637,11 @@ func TestPolicyToolExecutesActionlintShellcheckDependencyRaw(t *testing.T) { "--shell", "bash", "-", } - err := runPolicyToolForParent(paths, args, "/managed/bin/actionlint") + err := runPolicyToolForProtocol( + paths, + args, + toolprotocol.ActionlintShellcheckJSONStdinV1, + ) if err != nil { t.Fatalf("run actionlint shellcheck dependency: %v", err) } @@ -2645,15 +2652,15 @@ func TestPolicyToolExecutesActionlintShellcheckDependencyRaw(t *testing.T) { } } -func TestPolicyToolCapturesShellcheckOutsideActionlint(t *testing.T) { +func TestPolicyToolCapturesShellcheckWithoutActionlintMarker(t *testing.T) { paths := runtimeTestPaths(t) var calls []string paths.Executor = stubRuntimeOps{calls: &calls} - err := runPolicyToolForParent( + err := runPolicyToolForProtocol( paths, []string{"shellcheck", "-f", "json", "-"}, - "/usr/bin/bash", + "", ) if err != nil { t.Fatalf("run direct shellcheck: %v", err) @@ -2665,63 +2672,6 @@ func TestPolicyToolCapturesShellcheckOutsideActionlint(t *testing.T) { } } -func TestActionlintShellcheckDependencyRequiresJSONStdinContract(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - parent string - tool string - args []string - want bool - }{ - { - name: "actionlint json stdin", - parent: "/managed/bin/actionlint", - tool: "shellcheck", - args: []string{"--norc", "-f", "json", "-"}, - want: true, - }, - { - name: "long json format", - parent: "/managed/bin/actionlint", - tool: "shellcheck", - args: []string{"--format=json", "-"}, - want: true, - }, - { - name: "wrong parent", - parent: "/usr/bin/bash", - tool: "shellcheck", - args: []string{"-f", "json", "-"}, - }, - { - name: "not stdin", - parent: "/managed/bin/actionlint", - tool: "shellcheck", - args: []string{"-f", "json", "script.sh"}, - }, - { - name: "not json", - parent: "/managed/bin/actionlint", - tool: "shellcheck", - args: []string{"-f", "gcc", "-"}, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - got := actionlintShellcheckDependency(test.parent, test.tool, test.args) - if got != test.want { - t.Fatalf("actionlintShellcheckDependency() = %v, want %v", got, test.want) - } - }) - } -} - func TestAgentShellNativeGitBindRequiresReadOnlyMountInfo(t *testing.T) { t.Parallel() @@ -3511,6 +3461,9 @@ func TestMakefileRoutesParentGitHooksThroughStableCommonRuntime(t *testing.T) { `$(call install_git_hooks,$(LOCAL_HOOKS_DIR),$(GO_HOOK))`, `$(call install_git_hooks,$(HOOKS_DIR),$(PARENT_HOOK_BIN_DIR)/coding-ethos-run)`, `_sync-git-hooks: ensure-go go-tools-install _sync-parent-hook-runtime`, + "--dest-dir \"$(PARENT_HOOK_BIN_DIR)\" \\\n" + + "\t\t--real-git \"$(GIT)\" \\\n" + + "\t\t--runner \"$(PARENT_HOOK_BIN_DIR)/coding-ethos-run\"", } { if !strings.Contains(makefile, want) { t.Fatalf("Makefile missing stable Git hook route %q", want) @@ -3523,6 +3476,10 @@ func TestMakefileRoutesParentGitHooksThroughStableCommonRuntime(t *testing.T) { ) { t.Fatal("Makefile routes parent Git hooks through a worktree-local runner") } + + if strings.Contains(makefile, `--runner "$(GO_HOOK)"`) { + t.Fatal("Makefile routes a parent shim through a worktree-local runner") + } } func fakeCIGit(t *testing.T, diffOutput string) string { diff --git a/go/go.mod b/go/go.mod index 4563a511..dfd27d8d 100644 --- a/go/go.mod +++ b/go/go.mod @@ -22,7 +22,7 @@ require ( github.com/tree-sitter/tree-sitter-typescript v0.23.2 github.com/yuin/goldmark v1.8.2 go.uber.org/zap v1.28.0 - golang.org/x/sys v0.46.0 + golang.org/x/sys v0.47.0 mvdan.cc/sh/v3 v3.13.1 ) @@ -43,10 +43,10 @@ require ( github.com/pierrec/lz4/v4 v4.1.25 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/mod v0.37.0 // indirect + golang.org/x/mod v0.40.0 // indirect golang.org/x/sync v0.22.0 // indirect - golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 // indirect - golang.org/x/tools v0.47.0 // indirect + golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 // indirect + golang.org/x/tools v0.49.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect ) diff --git a/go/go.sum b/go/go.sum index b4bac55e..d6ca0b16 100644 --- a/go/go.sum +++ b/go/go.sum @@ -118,18 +118,18 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= -golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 h1:ZUSxONxc981v7AW7QUg+I9WwZzSTTJ019ENBYr5pV/Q= +golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5/go.mod h1:LVehoXe41cL5SCVQilsV7Gg6BNG+Js6P9PhSbYTIUkQ= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= diff --git a/go/internal/evaluators/shell_best_practices.go b/go/internal/evaluators/shell_best_practices.go index f6f6c3e5..9a53381a 100644 --- a/go/internal/evaluators/shell_best_practices.go +++ b/go/internal/evaluators/shell_best_practices.go @@ -20,7 +20,11 @@ import ( ) var ( - shellStrictModePattern = regexp.MustCompile( + errShellHelperWorkingDirectoryRequired = errors.New( + "inspect tracked common shell helpers: repository working directory is required", + ) + errShellHelperPrefixOutsideRepository = errors.New("is outside the repository") + shellStrictModePattern = regexp.MustCompile( `(?m)^\s*set\s+-[euo]+\s*pipefail|^\s*set\s+-euo\s+pipefail`, ) shellCommonSourcePattern = regexp.MustCompile( @@ -38,12 +42,25 @@ func EvaluateShellBestPractices( policyDef policy.Policy, context Context, ) ([]policy.Decision, error) { + if len(context.Files) == 0 { + return nil, nil + } + requireCommon := stringSliceOption( context.EvaluatorOptions, "require_common_for_prefixes", []string{"scripts/"}, ) - if !repositoryHasTrackedCommonShellHelper(context.Cwd) { + + hasCommonHelper, err := repositoryHasTrackedCommonShellHelper( + context.Cwd, + requireCommon, + ) + if err != nil { + return nil, err + } + + if !hasCommonHelper { requireCommon = nil } @@ -72,23 +89,98 @@ func EvaluateShellBestPractices( return nil, nil } -func repositoryHasTrackedCommonShellHelper(cwd string) bool { +func repositoryHasTrackedCommonShellHelper( + cwd string, + requireCommonForPrefixes []string, +) (bool, error) { if strings.TrimSpace(cwd) == "" { - return false + return false, errShellHelperWorkingDirectoryRequired } - output, err := GitCommand(cwd, "ls-files", "--cached").Output() + helperPaths, err := configuredCommonShellHelperPaths( + cwd, + requireCommonForPrefixes, + ) if err != nil { - return false + return false, err + } + + if len(helperPaths) == 0 { + return false, nil + } + + args := []string{"ls-files", "--cached", "--"} + for _, helperPath := range helperPaths { + args = append(args, ":(literal)"+helperPath) + } + + output, err := GitCommand(cwd, args...).CombinedOutput() + if err != nil { + return false, fmt.Errorf( + "inspect tracked common shell helpers with git ls-files: %w: %s", + err, + strings.TrimSpace(string(output)), + ) } + configuredHelpers := stringSet(helperPaths) + for line := range strings.SplitSeq(string(output), "\n") { - if filepath.Base(strings.TrimSpace(line)) == "common.sh" { - return true + trackedPath := filepath.ToSlash(strings.TrimSpace(line)) + if configuredHelpers[trackedPath] { + return true, nil } } - return false + return false, nil +} + +func configuredCommonShellHelperPaths( + cwd string, + prefixes []string, +) ([]string, error) { + repositoryRoot, err := filepath.Abs(cwd) + if err != nil { + return nil, fmt.Errorf("resolve repository working directory: %w", err) + } + + helperPaths := make([]string, 0, len(prefixes)) + for _, prefix := range prefixes { + trimmed := strings.TrimSpace(prefix) + if trimmed == "" { + continue + } + + cleaned := filepath.Clean(trimmed) + if filepath.IsAbs(cleaned) { + cleaned, err = filepath.Rel(repositoryRoot, cleaned) + if err != nil { + return nil, fmt.Errorf( + "resolve common shell helper prefix %q: %w", + prefix, + err, + ) + } + } + + if cleaned == ".." || strings.HasPrefix( + cleaned, + ".."+string(filepath.Separator), + ) { + return nil, fmt.Errorf( + "common shell helper prefix %q %w", + prefix, + errShellHelperPrefixOutsideRepository, + ) + } + + helperPaths = append( + helperPaths, + filepath.ToSlash(filepath.Join(cleaned, "common.sh")), + ) + } + + return helperPaths, nil } func looksLikeShellFile(path string) bool { diff --git a/go/internal/evaluators/shell_best_practices_test.go b/go/internal/evaluators/shell_best_practices_test.go index 3ebab1a4..10368c21 100644 --- a/go/internal/evaluators/shell_best_practices_test.go +++ b/go/internal/evaluators/shell_best_practices_test.go @@ -6,6 +6,7 @@ package evaluators_test import ( "os" "path/filepath" + "strings" "testing" "blackcat.ca/coding-ethos/go/diagnostics" @@ -17,6 +18,7 @@ func TestEvaluateShellBestPracticesBlocksMissingStrictMode(t *testing.T) { t.Parallel() dir := t.TempDir() + initializeStagedAdminGitRepo(t, dir) path := filepath.Join(dir, "script.sh") @@ -31,7 +33,7 @@ func TestEvaluateShellBestPracticesBlocksMissingStrictMode(t *testing.T) { decisions, err := EvaluateShellBestPractices( shellBestPracticesPolicy(), - Context{Files: []string{path}}, + Context{Cwd: dir, Files: []string{path}}, ) if err != nil { t.Fatalf("evaluate shell best practices: %v", err) @@ -50,6 +52,7 @@ func TestEvaluateShellBestPracticesBlocksInvalidShellSyntaxWithLocation(t *testi t.Parallel() dir := t.TempDir() + initializeStagedAdminGitRepo(t, dir) path := filepath.Join(dir, "script.sh") content := "#!/usr/bin/env bash\nset -euo pipefail\necho 'unterminated\n" @@ -61,7 +64,7 @@ func TestEvaluateShellBestPracticesBlocksInvalidShellSyntaxWithLocation(t *testi decisions, err := EvaluateShellBestPractices( shellBestPracticesPolicy(), - Context{Files: []string{path}}, + Context{Cwd: dir, Files: []string{path}}, ) if err != nil { t.Fatalf("evaluate shell best practices: %v", err) @@ -88,6 +91,115 @@ func TestEvaluateShellBestPracticesBlocksInvalidShellSyntaxWithLocation(t *testi } } +func TestEvaluateShellBestPracticesRejectsMissingRepositoryContext(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + path := filepath.Join(dir, "script.sh") + content := []byte("#!/usr/bin/env bash\nset -euo pipefail\necho ok\n") + if err := os.WriteFile(path, content, 0o600); err != nil { + t.Fatalf("write script: %v", err) + } + + _, err := EvaluateShellBestPractices( + shellBestPracticesPolicy(), + Context{Files: []string{path}}, + ) + if err == nil { + t.Fatal("missing repository context must fail helper validation") + } + if !strings.Contains(err.Error(), "repository working directory is required") { + t.Fatalf("missing repository context error = %q", err) + } +} + +func TestEvaluateShellBestPracticesPropagatesGitInspectionFailure(t *testing.T) { + t.Parallel() + + repo := t.TempDir() + scriptsDir := filepath.Join(repo, "scripts") + if err := os.MkdirAll(scriptsDir, 0o700); err != nil { + t.Fatalf("create scripts directory: %v", err) + } + + scriptPath := filepath.Join(scriptsDir, "work.sh") + content := []byte("#!/usr/bin/env bash\nset -euo pipefail\necho ok\n") + if err := os.WriteFile(scriptPath, content, 0o600); err != nil { + t.Fatalf("write script: %v", err) + } + if err := os.WriteFile( + filepath.Join(repo, ".git"), + []byte("gitdir: missing\n"), + 0o600, + ); err != nil { + t.Fatalf("write invalid Git directory marker: %v", err) + } + + _, err := EvaluateShellBestPractices( + shellBestPracticesPolicy(), + Context{ + Cwd: repo, + Files: []string{scriptPath}, + EvaluatorOptions: map[string]any{ + "require_common_for_prefixes": []any{ + scriptsDir + string(filepath.Separator), + }, + }, + }, + ) + if err == nil { + t.Fatal("git inspection failure must fail helper validation") + } + if !strings.Contains(err.Error(), "git ls-files") { + t.Fatalf("git inspection error = %q", err) + } +} + +func TestEvaluateShellBestPracticesIgnoresUnrelatedCommonHelper(t *testing.T) { + t.Parallel() + + repo := t.TempDir() + initializeStagedAdminGitRepo(t, repo) + scriptsDir := filepath.Join(repo, "scripts") + fixturesDir := filepath.Join(repo, "fixtures") + if err := os.MkdirAll(scriptsDir, 0o700); err != nil { + t.Fatalf("create scripts directory: %v", err) + } + if err := os.MkdirAll(fixturesDir, 0o700); err != nil { + t.Fatalf("create fixtures directory: %v", err) + } + + content := []byte("#!/usr/bin/env bash\nset -euo pipefail\necho ok\n") + scriptPath := filepath.Join(scriptsDir, "work.sh") + if err := os.WriteFile(scriptPath, content, 0o600); err != nil { + t.Fatalf("write script: %v", err) + } + fixturePath := filepath.Join(fixturesDir, "common.sh") + if err := os.WriteFile(fixturePath, content, 0o600); err != nil { + t.Fatalf("write unrelated common helper: %v", err) + } + runGit(t, repo, "add", "fixtures/common.sh") + + decisions, err := EvaluateShellBestPractices( + shellBestPracticesPolicy(), + Context{ + Cwd: repo, + Files: []string{scriptPath}, + EvaluatorOptions: map[string]any{ + "require_common_for_prefixes": []any{ + scriptsDir + string(filepath.Separator), + }, + }, + }, + ) + if err != nil { + t.Fatalf("evaluate with unrelated common helper: %v", err) + } + if len(decisions) != 0 { + t.Fatalf("unrelated common helper must not activate convention: %#v", decisions) + } +} + func TestEvaluateShellBestPracticesRequiresOnlyExistingCommonHelper(t *testing.T) { t.Parallel() diff --git a/go/internal/generatedtrust/staged.go b/go/internal/generatedtrust/staged.go index 6972ae57..7a69ea13 100644 --- a/go/internal/generatedtrust/staged.go +++ b/go/internal/generatedtrust/staged.go @@ -6,6 +6,7 @@ package generatedtrust import ( + "bytes" "crypto/sha256" "encoding/hex" "path/filepath" @@ -34,12 +35,18 @@ func ExactStagedFiles(bundle policy.Bundle, cwd string, files []string) []string } artifacts := generatedArtifacts(ethosRoot, cwd) - staged := normalizedPathSet(files) + requested := normalizedPathSet(files) + + staged := stagedChangedPathSet(cwd) + if staged == nil { + return nil + } + trusted := map[string]bool{} for _, artifact := range artifacts { path := filepath.ToSlash(filepath.Clean(artifact.Path)) - if !staged[path] || artifact.ExpectedSHA256 == "" { + if !requested[path] || !staged[path] || artifact.ExpectedSHA256 == "" { continue } @@ -59,6 +66,34 @@ func ExactStagedFiles(bundle policy.Bundle, cwd string, files []string) []string return result } +func stagedChangedPathSet(cwd string) map[string]bool { + content, err := evaluators.GitCommand( + cwd, + "diff", + "--cached", + "--name-only", + "--diff-filter=ACMR", + "-z", + "--", + ).Output() + if err != nil { + return nil + } + + paths := map[string]bool{} + + for rawPath := range bytes.SplitSeq(content, []byte{0}) { + path := string(rawPath) + if path == "" { + continue + } + + paths[filepath.ToSlash(filepath.Clean(path))] = true + } + + return paths +} + func generatedArtifacts(ethosRoot, repoRoot string) []syncstate.Artifact { artifacts := []syncstate.Artifact{} diff --git a/go/internal/hooklog/runner.go b/go/internal/hooklog/runner.go index 6539ffa5..5b6f70fb 100644 --- a/go/internal/hooklog/runner.go +++ b/go/internal/hooklog/runner.go @@ -261,9 +261,9 @@ func autoPruneHookRuns(root string) error { } func shouldForceCodeIntelRefresh(command []string) bool { - // Parent workflows refresh their explicit --repo target as part of the - // workflow itself. Repeating that work here both doubles maintenance and, - // before command-root resolution, could index the invocation repository. + // Hooklog owns refreshes only for policy-lint, Git hooks, and the selected + // Make targets below. Parent-install, parent-check, and parent-lint do not + // refresh code intelligence and must not be described as doing so here. return commandContains(command, "policy-lint") || commandContainsSequence(command, "git-hook", "pre-commit") || commandContainsSequence(command, "git-hook", "pre-push") || diff --git a/go/internal/hooklog/runner_test.go b/go/internal/hooklog/runner_test.go index 28468aa4..713bd9bb 100644 --- a/go/internal/hooklog/runner_test.go +++ b/go/internal/hooklog/runner_test.go @@ -5,6 +5,7 @@ package hooklog_test import ( "bytes" + "context" "errors" "os" "path/filepath" @@ -14,6 +15,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/codeintel" . "blackcat.ca/coding-ethos/go/internal/hooklog" + "blackcat.ca/coding-ethos/go/internal/realgit" "blackcat.ca/coding-ethos/go/internal/testlock" ) @@ -174,14 +176,19 @@ func TestRunWritesDebugLogAndStderrWhenEnabled(t *testing.T) { } func TestRunChecksIgnoresWithoutIndex(t *testing.T) { - t.Parallel() + testlock.ProcessState(t, "hooklog-git-trace") root := t.TempDir() writeHookLogIgnore(t, root) logPath := filepath.Join(t.TempDir(), "git-args.log") - git := fakeGitWithLog(t, logPath) + t.Setenv("GIT_TRACE", logPath) - err := Run(Options{ + git, err := realgit.Resolve(context.Background(), "git") + if err != nil { + t.Fatalf("resolve real git: %v", err) + } + + err = Run(Options{ Stdin: strings.NewReader(""), Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}, @@ -524,7 +531,7 @@ func fakeGit(t *testing.T) string { dir := t.TempDir() path := filepath.Join(dir, "git") - script := fakeGitCheckIgnoreScript("") + script := fakeGitCheckIgnoreScript() writeExecutableTestFile(t, path, script) @@ -557,31 +564,9 @@ func fakeFailingGit(t *testing.T) string { return path } -func fakeGitWithLog(t *testing.T, logPath string) string { - t.Helper() - - dir := t.TempDir() - path := filepath.Join(dir, "git") - - script := fakeGitCheckIgnoreScript(logPath) - - writeExecutableTestFile(t, path, script) - - return path -} - -func shellQuoteForTest(value string) string { - return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'" -} - -func fakeGitCheckIgnoreScript(logPath string) string { - logLine := "" - if logPath != "" { - logLine = "printf '%s\\n' \"$*\" >> " + shellQuoteForTest(logPath) + "\n" - } - +func fakeGitCheckIgnoreScript() string { return `#!/usr/bin/env bash -` + logLine + `root="" +root="" target="" while [ "$#" -gt 0 ]; do if [ "$1" = "-C" ]; then diff --git a/go/internal/hookrunnercli/external_tool.go b/go/internal/hookrunnercli/external_tool.go index 1588306f..01c3ebad 100644 --- a/go/internal/hookrunnercli/external_tool.go +++ b/go/internal/hookrunnercli/external_tool.go @@ -6,6 +6,8 @@ package hookrunnercli import ( "bytes" "context" + "crypto/sha256" + "encoding/hex" "errors" "fmt" "os" @@ -70,7 +72,7 @@ func runExternalTool(request externalToolRequest) externalToolResult { ) defer cancel() - env, envErr := externalToolEnv(request.Env) + env, envErr := externalToolEnv(request) if envErr != nil { return externalToolResult{ ExitCode: 1, @@ -179,11 +181,18 @@ func externalToolCombinedOutput(stdout, stderr string) string { } } -func externalToolEnv(extra []string) ([]string, error) { - env := make([]string, 0, len(os.Environ())+len(extra)) +func externalToolEnv(request externalToolRequest) ([]string, error) { + env := make([]string, 0, len(os.Environ())+len(request.Env)) hasPath := false - cacheEnv, err := externalToolCacheEnv(repoRoot()) + root := repoRoot() + + cacheEnv, err := externalToolCacheEnv(root) + if err != nil { + return nil, err + } + + uvEnv, err := externalToolUVEnv(root, request.Command, request.Dir) if err != nil { return nil, err } @@ -202,7 +211,7 @@ func externalToolEnv(extra []string) ([]string, error) { continue } - if found && cacheEnv.overrides(name) { + if found && (cacheEnv.overrides(name) || externalToolUVScopeName(name)) { continue } @@ -218,17 +227,18 @@ func externalToolEnv(extra []string) ([]string, error) { env = append(env, "GIT_CONFIG_GLOBAL="+os.DevNull) env = append(env, "XDG_CONFIG_HOME="+os.DevNull) env = append(env, cacheEnv.items()...) + env = append(env, uvEnv...) - return append(env, extra...), nil + return append(env, request.Env...), nil } type externalToolCacheEnvironment struct { GoTemp string GoCache string + GoPath string + GoModCache string GolangCILintDir string UVCache string - UVProjectEnv string - UVFrozen string // CargoTarget is per-repository build output, like GoCache. CargoTarget string // CargoHome and RustupHome are the operator's, not the repository's. Cargo @@ -291,17 +301,19 @@ func externalToolCacheEnv(root string) (externalToolCacheEnvironment, error) { goTemp := filepath.Join(root, ".coding-ethos", "cache", "go-tmp") goCache := filepath.Join(root, sandbox.SandboxGoCachePath) + goPath := filepath.Join(root, sandbox.SandboxGoPath) + goModCache := filepath.Join(root, sandbox.SandboxGoModCachePath) golangCILintDir := filepath.Join(root, sandbox.SandboxGolangCIPath) uvCache := filepath.Join(root, ".coding-ethos", "cache", "uv") - uvProjectEnv := filepath.Join(root, ".coding-ethos", "cache", "uv-project-env") cargoTarget := filepath.Join(root, ".coding-ethos", "cache", "cargo-target") for _, dir := range []string{ goTemp, goCache, + goPath, + goModCache, golangCILintDir, uvCache, - uvProjectEnv, cargoTarget, } { err := os.MkdirAll(dir, externalToolCacheDirMode) @@ -319,10 +331,10 @@ func externalToolCacheEnv(root string) (externalToolCacheEnvironment, error) { return externalToolCacheEnvironment{ GoTemp: goTemp, GoCache: goCache, + GoPath: goPath, + GoModCache: goModCache, GolangCILintDir: golangCILintDir, UVCache: uvCache, - UVProjectEnv: uvProjectEnv, - UVFrozen: "1", CargoTarget: cargoTarget, CargoHome: cargoHome, RustupHome: rustupHome, @@ -331,10 +343,13 @@ func externalToolCacheEnv(root string) (externalToolCacheEnvironment, error) { // prepareHookProcessCacheEnvironment projects the consumer-owned cache roots // onto the hook runner itself. Nested pre-commit languages can start before an -// individual external-tool request is constructed; setting these variables at -// the command boundary prevents uv, Go, Cargo, and linters from falling back -// to an unwritable operator cache. The returned closure restores the exact -// caller environment for in-process tests and embedded invocations. +// individual external-tool request is constructed; setting universal cache +// variables at the command boundary prevents uv, Go, Cargo, and linters from +// falling back to an unwritable operator cache. GOPATH also keeps Go's checksum +// database inside the consumer cache; GOMODCACHE is its pkg/mod child. UV +// project environments and frozen mode remain scoped to individual uv child +// invocations. The returned closure restores the exact caller environment for +// in-process tests and embedded invocations. func prepareHookProcessCacheEnvironment(root string) (func(), error) { environment, err := externalToolCacheEnv(root) if err != nil { @@ -393,10 +408,10 @@ func (environment externalToolCacheEnvironment) items() []string { "TMPDIR", "GOTMPDIR", "GOCACHE", + "GOPATH", + "GOMODCACHE", "GOLANGCI_LINT_CACHE", "UV_CACHE_DIR", - "UV_PROJECT_ENVIRONMENT", - "UV_FROZEN", "CARGO_TARGET_DIR", "CARGO_HOME", "RUSTUP_HOME", @@ -418,14 +433,14 @@ func (environment externalToolCacheEnvironment) value(name string) string { return environment.GoTemp case "GOCACHE": return environment.GoCache + case "GOPATH": + return environment.GoPath + case "GOMODCACHE": + return environment.GoModCache case "GOLANGCI_LINT_CACHE": return environment.GolangCILintDir case "UV_CACHE_DIR": return environment.UVCache - case "UV_PROJECT_ENVIRONMENT": - return environment.UVProjectEnv - case "UV_FROZEN": - return environment.UVFrozen case "CARGO_TARGET_DIR": return environment.CargoTarget case "CARGO_HOME": @@ -437,6 +452,157 @@ func (environment externalToolCacheEnvironment) value(name string) string { } } +func externalToolUVEnv(root string, command []string, dir string) ([]string, error) { + project, active, err := activeUVProject(command, dir) + if err != nil || !active || strings.TrimSpace(root) == "" || root == "." { + return nil, err + } + + digest := sha256.Sum256([]byte(project)) + + projectEnv := filepath.Join( + root, + ".coding-ethos", + "cache", + "uv-project-env", + hex.EncodeToString(digest[:]), + ) + + err = os.MkdirAll(projectEnv, externalToolCacheDirMode) + if err != nil { + return nil, fmt.Errorf( + "create uv project environment %s: %w", + projectEnv, + err, + ) + } + + env := []string{"UV_PROJECT_ENVIRONMENT=" + projectEnv} + if uvInvocationRequestsFrozen(command) || uvProjectHasCommittedLock(project) { + env = append(env, "UV_FROZEN=1") + } + + return env, nil +} + +func activeUVProject(command []string, dir string) (string, bool, error) { + if len(command) == 0 || filepath.Base(command[0]) != "uv" || + slices.Contains(command[1:], "--no-project") { + return "", false, nil + } + + workingDir := strings.TrimSpace(dir) + if workingDir == "" { + var err error + + workingDir, err = os.Getwd() + if err != nil { + return "", false, fmt.Errorf("resolve uv command directory: %w", err) + } + } + + project := workingDir + + explicitProject, explicit := uvProjectArgument(command) + if explicit { + project = explicitProject + if !filepath.IsAbs(project) { + project = filepath.Join(workingDir, project) + } + } + + project, err := canonicalUVProjectPath(project) + if err != nil { + return "", false, err + } + + if !explicit { + project = discoverUVProjectRoot(project) + } + + return project, true, nil +} + +func uvProjectArgument(command []string) (string, bool) { + for index := 1; index < len(command); index++ { + if command[index] == "--project" && index+1 < len(command) { + return command[index+1], true + } + + value, found := strings.CutPrefix(command[index], "--project=") + if found && value != "" { + return value, true + } + } + + return "", false +} + +func canonicalUVProjectPath(project string) (string, error) { + absolute, err := filepath.Abs(project) + if err != nil { + return "", fmt.Errorf("resolve uv project path %s: %w", project, err) + } + + resolved, err := filepath.EvalSymlinks(absolute) + if err == nil { + return resolved, nil + } + + if !os.IsNotExist(err) { + return "", fmt.Errorf("resolve uv project symlinks %s: %w", absolute, err) + } + + return filepath.Clean(absolute), nil +} + +func discoverUVProjectRoot(start string) string { + for current := start; ; current = filepath.Dir(current) { + info, err := os.Stat(filepath.Join(current, "pyproject.toml")) + if err == nil && !info.IsDir() { + return current + } + + parent := filepath.Dir(current) + if parent == current { + return start + } + } +} + +func uvInvocationRequestsFrozen(command []string) bool { + return slices.Contains(command[1:], "--frozen") +} + +func uvProjectHasCommittedLock(project string) bool { + output, err := combinedGitOutputInRoot(project, "rev-parse", "--show-toplevel") + if err != nil { + return false + } + + gitRoot := strings.TrimSpace(string(output)) + lockPath := filepath.Join(project, "uv.lock") + + relative, err := filepath.Rel(gitRoot, lockPath) + if err != nil || relative == ".." || + strings.HasPrefix(relative, ".."+string(os.PathSeparator)) { + return false + } + + _, err = combinedGitOutputInRoot( + gitRoot, + "cat-file", + "-e", + "HEAD:"+filepath.ToSlash(relative), + ) + + return err == nil +} + +func externalToolUVScopeName(name string) bool { + return name == "UV_PROJECT_ENVIRONMENT" || name == "UV_FROZEN" +} + func externalToolPathWithoutGitShim(pathValue string) string { kept := []string{} diff --git a/go/internal/hookrunnercli/external_tool_internal_test.go b/go/internal/hookrunnercli/external_tool_internal_test.go index 6d1a01eb..636b595d 100644 --- a/go/internal/hookrunnercli/external_tool_internal_test.go +++ b/go/internal/hookrunnercli/external_tool_internal_test.go @@ -14,10 +14,12 @@ import ( "blackcat.ca/coding-ethos/go/internal/testlock" ) -func TestPrepareHookProcessCacheEnvironmentSetsAndRestoresUVEnvironment(t *testing.T) { +func TestPrepareHookProcessCacheEnvironmentScopesUniversalCaches(t *testing.T) { testlock.ProcessState(t, "hook-process-cache-environment") root := t.TempDir() + t.Setenv("GOPATH", "/previous/go-path") + t.Setenv("GOMODCACHE", "/previous/go-mod-cache") t.Setenv("UV_CACHE_DIR", "/previous/uv-cache") t.Setenv("UV_PROJECT_ENVIRONMENT", "/previous/uv-project-environment") t.Setenv("UV_FROZEN", "0") @@ -33,19 +35,40 @@ func TestPrepareHookProcessCacheEnvironmentSetsAndRestoresUVEnvironment(t *testi if info, statErr := os.Stat(wantCache); statErr != nil || !info.IsDir() { t.Fatalf("UV cache directory is not usable: info=%v error=%v", info, statErr) } + wantGoPath := filepath.Join(root, ".coding-ethos", "cache", "go-path") + wantGoModCache := filepath.Join(wantGoPath, "pkg", "mod") + for name, want := range map[string]string{ + "GOPATH": wantGoPath, + "GOMODCACHE": wantGoModCache, + } { + if got := os.Getenv(name); got != want { + t.Fatalf("%s = %q, want %q", name, got, want) + } + if info, statErr := os.Stat(want); statErr != nil || !info.IsDir() { + t.Fatalf("%s directory is not usable: info=%v error=%v", name, info, statErr) + } + } - wantProjectEnv := filepath.Join(root, ".coding-ethos", "cache", "uv-project-env") - if got := os.Getenv("UV_PROJECT_ENVIRONMENT"); got != wantProjectEnv { - t.Fatalf("UV_PROJECT_ENVIRONMENT = %q, want %q", got, wantProjectEnv) + if got := os.Getenv( + "UV_PROJECT_ENVIRONMENT", + ); got != "/previous/uv-project-environment" { + t.Fatalf("UV_PROJECT_ENVIRONMENT changed process-wide: %q", got) } - if info, statErr := os.Stat(wantProjectEnv); statErr != nil || !info.IsDir() { - t.Fatalf("UV project environment is not usable: info=%v error=%v", info, statErr) + if got := os.Getenv("UV_FROZEN"); got != "0" { + t.Fatalf("UV_FROZEN changed process-wide: %q", got) } - if got := os.Getenv("UV_FROZEN"); got != "1" { - t.Fatalf("UV_FROZEN = %q, want %q", got, "1") + projectEnvRoot := filepath.Join(root, ".coding-ethos", "cache", "uv-project-env") + if _, statErr := os.Stat(projectEnvRoot); !os.IsNotExist(statErr) { + t.Fatalf("process preparation created uv project environment: %v", statErr) } restore() + if got := os.Getenv("GOPATH"); got != "/previous/go-path" { + t.Fatalf("restored GOPATH = %q", got) + } + if got := os.Getenv("GOMODCACHE"); got != "/previous/go-mod-cache" { + t.Fatalf("restored GOMODCACHE = %q", got) + } if got := os.Getenv("UV_CACHE_DIR"); got != "/previous/uv-cache" { t.Fatalf("restored UV_CACHE_DIR = %q", got) } @@ -81,6 +104,9 @@ func TestExternalToolEnvRemovesGitHookLocalEnvironment(t *testing.T) { t.Setenv("GIT_CONFIG_VALUE_0", "test@example.com") t.Setenv("CODING_ETHOS_REAL_GIT", "/tmp/hook-real-git") t.Setenv("GOCACHE", "/tmp/host-go-cache") + t.Setenv("GOPATH", "/tmp/host-go-path") + t.Setenv("GOMODCACHE", "/tmp/host-go-mod-cache") + t.Setenv("UV_PROJECT_ENVIRONMENT", "/tmp/host-uv-project") t.Setenv("UV_FROZEN", "0") t.Setenv("PATH", shimDir+string(os.PathListSeparator)+"/usr/bin") t.Setenv(consumerRootEnv, repo) @@ -90,7 +116,11 @@ func TestExternalToolEnvRemovesGitHookLocalEnvironment(t *testing.T) { t.Setenv("CODING_ETHOS_SANDBOX_ACTIVE", "1") t.Setenv("CODING_ETHOS_SANDBOX_ROOT", repo) - env, err := externalToolEnv([]string{"KEEP_EXTRA=1"}) + env, err := externalToolEnv(externalToolRequest{ + Dir: repo, + Command: []string{"go", "test", "./..."}, + Env: []string{"KEEP_EXTRA=1"}, + }) if err != nil { t.Fatalf("externalToolEnv() returned error: %v", err) } @@ -132,6 +162,24 @@ func TestExternalToolEnvRemovesGitHookLocalEnvironment(t *testing.T) { t.Fatalf("externalToolEnv did not replace host Go cache: %#v", env) } + wantGoPath := filepath.Join(repo, ".coding-ethos", "cache", "go-path") + for name, want := range map[string]string{ + "GOPATH": wantGoPath, + "GOMODCACHE": filepath.Join(wantGoPath, "pkg", "mod"), + } { + if got := externalToolTestRequiredEnvValue(t, env, name); got != want { + t.Fatalf("externalToolEnv %s = %q, want %q", name, got, want) + } + } + for _, unwanted := range []string{ + "GOPATH=/tmp/host-go-path", + "GOMODCACHE=/tmp/host-go-mod-cache", + } { + if slices.Contains(env, unwanted) { + t.Fatalf("externalToolEnv kept host Go path %q: %#v", unwanted, env) + } + } + if !slices.Contains( env, "GOTMPDIR="+filepath.Join(repo, ".coding-ethos/cache/go-tmp"), @@ -153,15 +201,11 @@ func TestExternalToolEnvRemovesGitHookLocalEnvironment(t *testing.T) { t.Fatalf("externalToolEnv did not set uv cache dir: %#v", env) } - if !slices.Contains( - env, - "UV_PROJECT_ENVIRONMENT="+filepath.Join(repo, ".coding-ethos/cache/uv-project-env"), - ) { - t.Fatalf("externalToolEnv did not set uv project environment: %#v", env) + if _, found := externalToolTestEnvValue(env, "UV_PROJECT_ENVIRONMENT"); found { + t.Fatalf("externalToolEnv leaked uv project scope into non-uv tool: %#v", env) } - - if !slices.Contains(env, "UV_FROZEN=1") || slices.Contains(env, "UV_FROZEN=0") { - t.Fatalf("externalToolEnv did not freeze the sealed uv project: %#v", env) + if _, found := externalToolTestEnvValue(env, "UV_FROZEN"); found { + t.Fatalf("externalToolEnv leaked frozen mode into non-uv tool: %#v", env) } for _, item := range env { @@ -183,6 +227,146 @@ func TestExternalToolEnvRemovesGitHookLocalEnvironment(t *testing.T) { t.Fatalf("externalToolEnv omitted PATH: %#v", env) } +func TestExternalToolEnvIsolatesActiveUVProjects(t *testing.T) { + repo := t.TempDir() + firstProject := filepath.Join(repo, "first") + secondProject := filepath.Join(repo, "second") + for _, project := range []string{firstProject, secondProject} { + mustWriteTestFile( + t, + filepath.Join(project, "pyproject.toml"), + "[project]\nname = 'fixture'\n", + ) + } + + t.Chdir(repo) + t.Setenv(consumerRootEnv, repo) + + firstEnv, err := externalToolEnv(externalToolRequest{ + Dir: firstProject, + Command: []string{"uv", "run", "python", "-V"}, + }) + if err != nil { + t.Fatalf("first externalToolEnv: %v", err) + } + secondEnv, err := externalToolEnv(externalToolRequest{ + Dir: repo, + Command: []string{ + "uv", "run", "--project", "second", "python", "-V", + }, + }) + if err != nil { + t.Fatalf("second externalToolEnv: %v", err) + } + + firstPath := externalToolTestRequiredEnvValue( + t, + firstEnv, + "UV_PROJECT_ENVIRONMENT", + ) + secondPath := externalToolTestRequiredEnvValue( + t, + secondEnv, + "UV_PROJECT_ENVIRONMENT", + ) + if firstPath == secondPath { + t.Fatalf("distinct uv projects shared environment %q", firstPath) + } + wantRoot := filepath.Join(repo, ".coding-ethos", "cache", "uv-project-env") + for _, path := range []string{firstPath, secondPath} { + if !strings.HasPrefix(path, wantRoot+string(os.PathSeparator)) { + t.Fatalf("uv project environment %q is outside %q", path, wantRoot) + } + if info, statErr := os.Stat(path); statErr != nil || !info.IsDir() { + t.Fatalf("uv project environment is not usable: info=%v error=%v", info, statErr) + } + } + + firstCache := externalToolTestRequiredEnvValue(t, firstEnv, "UV_CACHE_DIR") + secondCache := externalToolTestRequiredEnvValue(t, secondEnv, "UV_CACHE_DIR") + if firstCache != secondCache { + t.Fatalf("uv projects did not share cache: %q != %q", firstCache, secondCache) + } +} + +func TestExternalToolEnvFreezesOnlySealedOrExplicitUVProjects(t *testing.T) { + repo := t.TempDir() + sealedProject := filepath.Join(repo, "sealed") + unsealedProject := filepath.Join(repo, "unsealed") + for _, project := range []string{sealedProject, unsealedProject} { + mustWriteTestFile( + t, + filepath.Join(project, "pyproject.toml"), + "[project]\nname = 'fixture'\n", + ) + } + mustWriteTestFile(t, filepath.Join(sealedProject, "uv.lock"), "version = 1\n") + + runGitTestCommandInDir(t, repo, "init", "--quiet") + runGitTestCommandInDir( + t, + repo, + "add", + "sealed/pyproject.toml", + "sealed/uv.lock", + "unsealed/pyproject.toml", + ) + runGitTestCommandInDir( + t, + repo, + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "-c", + "commit.gpgSign=false", + "commit", + "--quiet", + "--no-gpg-sign", + "-m", + "fixture", + ) + mustWriteTestFile(t, filepath.Join(unsealedProject, "uv.lock"), "version = 1\n") + + t.Chdir(repo) + t.Setenv(consumerRootEnv, repo) + + sealedEnv, err := externalToolEnv(externalToolRequest{ + Dir: sealedProject, + Command: []string{"uv", "run", "python", "-V"}, + }) + if err != nil { + t.Fatalf("sealed externalToolEnv: %v", err) + } + if got := externalToolTestRequiredEnvValue(t, sealedEnv, "UV_FROZEN"); got != "1" { + t.Fatalf("sealed UV_FROZEN = %q, want 1", got) + } + + unsealedEnv, err := externalToolEnv(externalToolRequest{ + Dir: unsealedProject, + Command: []string{"uv", "run", "python", "-V"}, + }) + if err != nil { + t.Fatalf("unsealed externalToolEnv: %v", err) + } + if value, found := externalToolTestEnvValue(unsealedEnv, "UV_FROZEN"); found { + t.Fatalf("unsealed UV_FROZEN = %q, want absent", value) + } + + explicitEnv, err := externalToolEnv(externalToolRequest{ + Dir: unsealedProject, + Command: []string{ + "uv", "run", "--frozen", "python", "-V", + }, + }) + if err != nil { + t.Fatalf("explicit frozen externalToolEnv: %v", err) + } + if got := externalToolTestRequiredEnvValue(t, explicitEnv, "UV_FROZEN"); got != "1" { + t.Fatalf("explicit UV_FROZEN = %q, want 1", got) + } +} + func TestExternalToolEnvAddsUsablePathWhenInheritedPathMissing(t *testing.T) { original, hadOriginal := os.LookupEnv("PATH") if err := os.Unsetenv("PATH"); err != nil { @@ -196,7 +380,9 @@ func TestExternalToolEnvAddsUsablePathWhenInheritedPathMissing(t *testing.T) { } }) - env, err := externalToolEnv(nil) + env, err := externalToolEnv(externalToolRequest{ + Command: []string{"go", "version"}, + }) if err != nil { t.Fatalf("externalToolEnv() returned error: %v", err) } @@ -219,6 +405,28 @@ func TestExternalToolEnvAddsUsablePathWhenInheritedPathMissing(t *testing.T) { t.Fatalf("externalToolEnv omitted PATH: %#v", env) } +func externalToolTestRequiredEnvValue(t *testing.T, env []string, name string) string { + t.Helper() + + value, found := externalToolTestEnvValue(env, name) + if !found { + t.Fatalf("external tool environment omitted %s: %#v", name, env) + } + + return value +} + +func externalToolTestEnvValue(env []string, name string) (string, bool) { + for _, item := range env { + itemName, value, found := strings.Cut(item, "=") + if found && itemName == name { + return value, true + } + } + + return "", false +} + func TestExternalToolCacheEnvFailsWhenCacheDirsCannotBeCreated(t *testing.T) { root := t.TempDir() if err := os.WriteFile( diff --git a/go/internal/hookrunnercli/python_policies.go b/go/internal/hookrunnercli/python_policies.go index 7cc62fac..2345f265 100644 --- a/go/internal/hookrunnercli/python_policies.go +++ b/go/internal/hookrunnercli/python_policies.go @@ -913,7 +913,10 @@ func runPytestCommand(settings pytestGateSettings) (pytestRunResult, error) { ) cmd.Dir = settings.ConsumerRoot - env, envErr := externalToolEnv(nil) + env, envErr := externalToolEnv(externalToolRequest{ + Dir: settings.ConsumerRoot, + Command: settings.TestCommand, + }) if envErr != nil { return result, envErr } diff --git a/go/internal/hooks/gate_exit_status.go b/go/internal/hooks/gate_exit_status.go index eaf9fc4e..b2f1aa89 100644 --- a/go/internal/hooks/gate_exit_status.go +++ b/go/internal/hooks/gate_exit_status.go @@ -24,6 +24,8 @@ const ( "by a shell fallback." gatePipelineReason = "Required repository gate pipelines must enable " + "pipefail so the gate status remains authoritative." + gateNestingReason = "Required repository gate inspection exceeded the " + + "supported shell nesting depth." gateSequenceReason = "Commands after a required repository gate must " + "capture and return the gate's exact exit status." ) @@ -55,7 +57,11 @@ func maskedRequiredGateStatus( inheritedPipefail bool, depth int, ) (string, bool) { - if depth > maxGateShellDepth || strings.TrimSpace(command) == "" { + if depth > maxGateShellDepth { + return gateNestingReason, true + } + + if strings.TrimSpace(command) == "" { return "", false } @@ -64,13 +70,17 @@ func maskedRequiredGateStatus( return "", false } - pipefail := inheritedPipefail || parsed.enablesPipefail() + pipefail := inheritedPipefail for index := range parsed.segments { reason, masked := parsed.maskedSegmentStatus(index, pipefail, depth) if masked { return reason, true } + + if isPipefailCommand(gateExecutableArgv(parsed.segments[index])) { + pipefail = true + } } return "", false @@ -170,16 +180,6 @@ func parseGateShell(command string) (gateShell, error) { return parsed, nil } -func (parsed gateShell) enablesPipefail() bool { - for _, segment := range parsed.segments { - if isPipefailCommand(gateExecutableArgv(segment)) { - return true - } - } - - return false -} - func isPipefailCommand(argv []string) bool { return len(argv) >= 3 && argv[0] == "set" && argv[1] == "-o" && argv[2] == "pipefail" @@ -441,8 +441,7 @@ func nestedShellScript(argv []string) (string, bool, bool) { continue } - if strings.HasPrefix(argv[index], "-") && - strings.Contains(argv[index], "c") && index+1 < len(argv) { + if shellCommandStringOption(argv[index]) && index+1 < len(argv) { return argv[index+1], pipefail, true } } @@ -450,6 +449,12 @@ func nestedShellScript(argv []string) (string, bool, bool) { return "", pipefail, false } +func shellCommandStringOption(argument string) bool { + return strings.HasPrefix(argument, "-") && + !strings.HasPrefix(argument, "--") && + strings.Contains(argument[1:], "c") +} + func shellExecutable(argument string) bool { return slices.Contains( []string{"bash", "dash", "sh"}, @@ -473,14 +478,16 @@ func requiredGateArgv(argv []string) bool { } func requiredExecutableGate(name string, arguments []string) bool { + if isPythonCommand(name) { + return len(arguments) > 1 && arguments[0] == "-m" && + arguments[1] == pytestExecutable + } + switch name { case "cargo": return requiredCargoGate(arguments) case "go": return requiredGoGate(arguments) - case pythonExecutable, "python3": - return len(arguments) > 1 && arguments[0] == "-m" && - arguments[1] == pytestExecutable case pytestExecutable, "ghprsq": return true case preCommitExecutable: diff --git a/go/internal/hooks/gate_exit_status_test.go b/go/internal/hooks/gate_exit_status_test.go index d96e440b..0acd9053 100644 --- a/go/internal/hooks/gate_exit_status_test.go +++ b/go/internal/hooks/gate_exit_status_test.go @@ -13,10 +13,12 @@ func TestRequiredGateExitStatusBlocksMaskedFailures(t *testing.T) { "make check | tail -100", "make check || true", "bash -c 'make check; echo done'", + "bash --norc -c 'make check || true'", "git -c core.useBuiltinFSMonitor=false commit -m verified | tee commit.log", "cargo --locked test | tee cargo.log", "go -C ./go test ./... | tee go.log", "python3 -m pytest | tee pytest.log", + "python3.13 -m pytest | tee pytest.log", "env -u CI make check | tee make.log", "nice make check | tee make.log", "timeout 30s make check | tee make.log", @@ -25,6 +27,7 @@ func TestRequiredGateExitStatusBlocksMaskedFailures(t *testing.T) { "make go-e2e-test | tee go-e2e.log", "make lint | tee lint.log", "make purrdf-extractor-check | tee purrdf.log", + "make check | tail -100 && set -o pipefail", } { route := requiredGateExitStatusRouteFor(Event{ HookEventName: eventPreToolUse, @@ -39,6 +42,23 @@ func TestRequiredGateExitStatusBlocksMaskedFailures(t *testing.T) { } } +func TestRequiredGateExitStatusFailsClosedBeyondShellDepth(t *testing.T) { + t.Parallel() + + reason, masked := maskedRequiredGateStatus( + "make check", + false, + maxGateShellDepth+1, + ) + if !masked || reason != gateNestingReason { + t.Fatalf( + "over-nested inspection = (%q, %t), want fail-closed nesting block", + reason, + masked, + ) + } +} + func TestRequiredGateExitStatusAllowsAuthoritativeStatus(t *testing.T) { t.Parallel() diff --git a/go/internal/lint/runner_test.go b/go/internal/lint/runner_test.go index a37f431d..cae6fd7c 100644 --- a/go/internal/lint/runner_test.go +++ b/go/internal/lint/runner_test.go @@ -310,6 +310,32 @@ func TestRunAllowsExactStagedGeneratedToolConfig(t *testing.T) { } } +func TestExactStagedGeneratedToolConfigRejectsUnchangedIndexEntry(t *testing.T) { + t.Parallel() + + ethosRoot := repoRootForLintTest(t) + repo := initializedLintGitRepo(t) + + _, err := toolconfigs.Sync(ethosRoot, repo, "") + if err != nil { + t.Fatalf("sync generated tool configs: %v", err) + } + runLintGit(t, repo, "add", ".bandit.yml") + runLintGit(t, repo, "commit", "-m", "test: establish generated baseline") + + otherPath := filepath.Join(repo, "other.txt") + if err = os.WriteFile(otherPath, []byte("staged change\n"), 0o600); err != nil { + t.Fatalf("write staged change: %v", err) + } + runLintGit(t, repo, "add", "other.txt") + + bundle := compiledRepoLintBundle(t) + trusted := generatedtrust.ExactStagedFiles(bundle, repo, []string{".bandit.yml"}) + if len(trusted) != 0 { + t.Fatalf("unchanged generated index entry was trusted: %#v", trusted) + } +} + func TestRunBlocksDivergentStagedGeneratedToolConfig(t *testing.T) { t.Parallel() @@ -385,6 +411,7 @@ func initializedLintGitRepo(t *testing.T) string { runLintGit(t, repo, "init") runLintGit(t, repo, "config", "user.email", "test@example.com") runLintGit(t, repo, "config", "user.name", "Test") + runLintGit(t, repo, "config", "commit.gpgsign", "false") return repo } diff --git a/go/internal/managedcapture/capture.go b/go/internal/managedcapture/capture.go index 5db596f6..2144633c 100644 --- a/go/internal/managedcapture/capture.go +++ b/go/internal/managedcapture/capture.go @@ -31,6 +31,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/policy" "blackcat.ca/coding-ethos/go/internal/processstatus" "blackcat.ca/coding-ethos/go/internal/sandbox" + "blackcat.ca/coding-ethos/go/internal/toolprotocol" "blackcat.ca/coding-ethos/go/toolcatalog" ) @@ -508,7 +509,11 @@ func capturedProcessArgv(plan sandbox.Plan) []string { return append([]string{plan.Executable}, plan.Args...) } -func capturedProcessEnv(environ []string, cacheEnv sandboxCacheEnvironment) []string { +func capturedProcessEnv( + environ []string, + cacheEnv sandboxCacheEnvironment, + tool string, +) []string { out := make([]string, 0, len(environ)) hasPath := false @@ -556,6 +561,9 @@ func capturedProcessEnv(environ []string, cacheEnv sandboxCacheEnvironment) []st } out = append(out, cacheEnv.items()...) + if tool == toolprotocol.ActionlintTool { + out = append(out, toolprotocol.ActionlintShellcheckEnvironment()) + } return out } diff --git a/go/internal/managedcapture/capture_process.go b/go/internal/managedcapture/capture_process.go index 0487c718..7d87d60b 100644 --- a/go/internal/managedcapture/capture_process.go +++ b/go/internal/managedcapture/capture_process.go @@ -169,7 +169,7 @@ func startCapturedOSProcess( argv, &os.ProcAttr{ Dir: request.Cwd, - Env: capturedProcessEnv(os.Environ(), cacheEnv), + Env: capturedProcessEnv(os.Environ(), cacheEnv, request.Tool), Files: files, Sys: capturedProcessSysProcAttr(cgroup, evidence), }, diff --git a/go/internal/managedcapture/capture_test.go b/go/internal/managedcapture/capture_test.go index a4002d14..50efecd8 100644 --- a/go/internal/managedcapture/capture_test.go +++ b/go/internal/managedcapture/capture_test.go @@ -24,6 +24,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/hookoutput" "blackcat.ca/coding-ethos/go/internal/policy" "blackcat.ca/coding-ethos/go/internal/sandbox" + "blackcat.ca/coding-ethos/go/internal/toolprotocol" "blackcat.ca/coding-ethos/go/toolcatalog" ) @@ -2060,11 +2061,11 @@ func TestRunCapturedToolLogsForcedStructuredFormats(t *testing.T) { "", io.Discard, ) + content := singleTraceContent(t, repo) if exitCode != 1 { - t.Fatalf("exit code = %d, want 1", exitCode) + t.Fatalf("exit code = %d, want 1; trace:\n%s", exitCode, content) } - content := singleTraceContent(t, repo) for _, want := range []string{ `"source_tool": "` + test.wantTool + `"`, `"file": "` + test.wantFile + `"`, @@ -2447,6 +2448,8 @@ func TestCapturedProcessEnvRemovesCodingEthosGitShimPath(t *testing.T) { "OTHER=value", "TMPDIR=/tmp/host", "GOCACHE=/tmp/go-cache", + "GOPATH=/tmp/go-path", + "GOMODCACHE=/tmp/go-mod-cache", "GOLANGCI_LINT_CACHE=/tmp/golangci-cache", "GOROOT=/tmp/go-root", "CGO_ENABLED=0", @@ -2456,13 +2459,15 @@ func TestCapturedProcessEnvRemovesCodingEthosGitShimPath(t *testing.T) { }, sandboxCacheEnvironment{ TempDir: "/repo/.coding-ethos/cache/sandbox-tmp", GoCache: "/repo/.coding-ethos/cache/go-build", + GoPath: "/repo/.coding-ethos/cache/go-path", + GoModCache: "/repo/.coding-ethos/cache/go-path/pkg/mod", GolangCILintDir: "/repo/.coding-ethos/cache/golangci-lint", GoRoot: "/repo/go-root", CGOEnabled: "1", CC: "/usr/bin/gcc", CompilerPath: "/usr/bin", Assembler: "/usr/bin/as", - }) + }, "ruff") if !capturedEnvPathContains(env, realDir) { t.Fatalf("captured env PATH = %#v, want entry %q", env, realDir) @@ -2479,6 +2484,8 @@ func TestCapturedProcessEnvRemovesCodingEthosGitShimPath(t *testing.T) { for _, want := range []string{ "GOCACHE=/repo/.coding-ethos/cache/go-build", + "GOPATH=/repo/.coding-ethos/cache/go-path", + "GOMODCACHE=/repo/.coding-ethos/cache/go-path/pkg/mod", "GOLANGCI_LINT_CACHE=/repo/.coding-ethos/cache/golangci-lint", "GOROOT=/repo/go-root", "CGO_ENABLED=1", @@ -2492,6 +2499,15 @@ func TestCapturedProcessEnvRemovesCodingEthosGitShimPath(t *testing.T) { } } + for _, inherited := range []string{ + "GOPATH=/tmp/go-path", + "GOMODCACHE=/tmp/go-mod-cache", + } { + if slices.Contains(env, inherited) { + t.Fatalf("captured env kept host Go path %q: %#v", inherited, env) + } + } + for _, blocked := range []string{ "CODE_ETHOS_CONSUMER_ROOT=/repo", "CODING_ETHOS_EXEC_STACK=coding-ethos-run", @@ -2520,10 +2536,58 @@ func capturedEnvPathContains(env []string, entry string) bool { return false } +func TestCapturedProcessEnvMarksOnlyManagedActionlint(t *testing.T) { + t.Parallel() + + inherited := []string{ + toolprotocol.ActionlintShellcheckEnv + "=untrusted", + "OTHER=value", + } + want := toolprotocol.ActionlintShellcheckEnvironment() + + actionlintEnv := capturedProcessEnv( + inherited, + sandboxCacheEnvironment{}, + toolprotocol.ActionlintTool, + ) + if count := countEnvironmentEntry(actionlintEnv, want); count != 1 { + t.Fatalf( + "managed actionlint protocol marker count = %d, want 1: %#v", + count, + actionlintEnv, + ) + } + + shellcheckEnv := capturedProcessEnv( + inherited, + sandboxCacheEnvironment{}, + toolprotocol.ShellcheckTool, + ) + for _, item := range shellcheckEnv { + if strings.HasPrefix(item, toolprotocol.ActionlintShellcheckEnv+"=") { + t.Fatalf( + "ordinary ShellCheck inherited actionlint protocol marker: %#v", + shellcheckEnv, + ) + } + } +} + +func countEnvironmentEntry(env []string, want string) int { + count := 0 + for _, item := range env { + if item == want { + count++ + } + } + + return count +} + func TestCapturedProcessEnvAddsUsablePathWhenInheritedPathMissing(t *testing.T) { t.Parallel() - env := capturedProcessEnv([]string{"OTHER=value"}, sandboxCacheEnvironment{}) + env := capturedProcessEnv([]string{"OTHER=value"}, sandboxCacheEnvironment{}, "ruff") for _, item := range env { name, value, ok := strings.Cut(item, "=") diff --git a/go/internal/managedcapture/sandbox_cache_env.go b/go/internal/managedcapture/sandbox_cache_env.go index 4c761ccc..1acef6c7 100644 --- a/go/internal/managedcapture/sandbox_cache_env.go +++ b/go/internal/managedcapture/sandbox_cache_env.go @@ -19,6 +19,8 @@ type sandboxCacheEnvironment struct { TempDir string RuntimeDir string GoCache string + GoPath string + GoModCache string GolangCILintDir string GoRoot string CGOEnabled string @@ -103,6 +105,8 @@ func (environment sandboxCacheEnvironment) value(name string) string { "TMPDIR": environment.TempDir, "XDG_RUNTIME_DIR": environment.RuntimeDir, "GOCACHE": environment.GoCache, + "GOPATH": environment.GoPath, + "GOMODCACHE": environment.GoModCache, "GOLANGCI_LINT_CACHE": environment.GolangCILintDir, "GOROOT": environment.GoRoot, "CGO_ENABLED": environment.CGOEnabled, @@ -120,6 +124,8 @@ func (environment sandboxCacheEnvironment) names() []string { "TMPDIR", "XDG_RUNTIME_DIR", "GOCACHE", + "GOPATH", + "GOMODCACHE", "GOLANGCI_LINT_CACHE", "GOROOT", "CGO_ENABLED", @@ -184,6 +190,19 @@ func sandboxCacheEnv( return sandboxCacheEnvironment{}, err } + goPath := filepath.Join(root, sandbox.SandboxGoPath) + goModCache := filepath.Join(root, sandbox.SandboxGoModCachePath) + + err = makeSandboxDir(goPath, "Go path") + if err != nil { + return sandboxCacheEnvironment{}, err + } + + err = makeSandboxDir(goModCache, "Go module cache") + if err != nil { + return sandboxCacheEnvironment{}, err + } + golangCILintDir := filepath.Join(root, sandbox.SandboxGolangCIPath) err = makeSandboxDir(golangCILintDir, "golangci-lint cache") @@ -201,6 +220,8 @@ func sandboxCacheEnv( RuntimeDir: runtimeDir, CleanupTemp: cleanupTemp, GoCache: goCache, + GoPath: goPath, + GoModCache: goModCache, GolangCILintDir: golangCILintDir, GoRoot: managedGoRoot(ctx), CGOEnabled: "1", diff --git a/go/internal/managedcapture/sandbox_cache_env_test.go b/go/internal/managedcapture/sandbox_cache_env_test.go new file mode 100644 index 00000000..5704315d --- /dev/null +++ b/go/internal/managedcapture/sandbox_cache_env_test.go @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package managedcapture + +import ( + "context" + "os" + "path/filepath" + "testing" + + "blackcat.ca/coding-ethos/go/internal/sandbox" +) + +func TestSandboxCacheEnvCreatesConsumerGoPath(t *testing.T) { + root := t.TempDir() + + environment, err := sandboxCacheEnv(context.Background(), captureRequest{ + Cwd: root, + TraceRoot: root, + Tool: "ruff", + }) + if err != nil { + t.Fatalf("sandboxCacheEnv: %v", err) + } + + wantGoPath := filepath.Join(root, sandbox.SandboxGoPath) + wantGoModCache := filepath.Join(root, sandbox.SandboxGoModCachePath) + if environment.GoPath != wantGoPath { + t.Fatalf("GOPATH = %q, want %q", environment.GoPath, wantGoPath) + } + if environment.GoModCache != wantGoModCache { + t.Fatalf("GOMODCACHE = %q, want %q", environment.GoModCache, wantGoModCache) + } + + for name, path := range map[string]string{ + "GOPATH": environment.GoPath, + "GOMODCACHE": environment.GoModCache, + } { + info, statErr := os.Stat(path) + if statErr != nil || !info.IsDir() { + t.Fatalf("%s directory is not usable: info=%v error=%v", name, info, statErr) + } + } +} diff --git a/go/internal/policygitcli/main.go b/go/internal/policygitcli/main.go index 77af9fae..c04e10ad 100644 --- a/go/internal/policygitcli/main.go +++ b/go/internal/policygitcli/main.go @@ -249,7 +249,7 @@ func gitGlobalOptionStartsArgv(argument string) bool { "--bare", "--git-dir", "--work-tree", "--namespace", "--super-prefix", "--exec-path", "--html-path", "--man-path", "--info-path", "--config-env", "--literal-pathspecs", "--glob-pathspecs", "--noglob-pathspecs", "--icase-pathspecs", - "--version", "--help": + "-v", "--version", "-h", "--help": return true default: return false diff --git a/go/internal/policygitcli/main_internal_test.go b/go/internal/policygitcli/main_internal_test.go index 6187d5bf..794cb6e4 100644 --- a/go/internal/policygitcli/main_internal_test.go +++ b/go/internal/policygitcli/main_internal_test.go @@ -91,6 +91,16 @@ func TestParsePolicyGitArgsPreservesGitGlobalOptions(t *testing.T) { } } +func TestGitGlobalOptionStartsArgvRecognizesShortMetaOptions(t *testing.T) { + t.Parallel() + + for _, option := range []string{"-h", "-v"} { + if !gitGlobalOptionStartsArgv(option) { + t.Fatalf("gitGlobalOptionStartsArgv(%q) = false, want true", option) + } + } +} + func TestParsePolicyGitArgsHonorsExplicitBoundary(t *testing.T) { t.Parallel() diff --git a/go/internal/sandbox/sandbox.go b/go/internal/sandbox/sandbox.go index b6ef5c9d..afa68fb4 100644 --- a/go/internal/sandbox/sandbox.go +++ b/go/internal/sandbox/sandbox.go @@ -25,16 +25,18 @@ const ( BackendNative = "native" - cgroupLineParts = 3 - toolFallbackName = "tool" - nativeSandboxBinary = "coding-ethos-sandbox" - nativeProbeTimeout = 10 - nativeProbeDirMode = 0o700 - nativeProbeFileMode = 0o700 - nativeProbeWriteMode = 0o600 - SandboxTempWritePath = ".coding-ethos/cache/sandbox-tmp" - SandboxGoCachePath = ".coding-ethos/cache/go-build" - SandboxGolangCIPath = ".coding-ethos/cache/golangci-lint" + cgroupLineParts = 3 + toolFallbackName = "tool" + nativeSandboxBinary = "coding-ethos-sandbox" + nativeProbeTimeout = 10 + nativeProbeDirMode = 0o700 + nativeProbeFileMode = 0o700 + nativeProbeWriteMode = 0o600 + SandboxTempWritePath = ".coding-ethos/cache/sandbox-tmp" + SandboxGoCachePath = ".coding-ethos/cache/go-build" + SandboxGoPath = ".coding-ethos/cache/go-path" + SandboxGoModCachePath = SandboxGoPath + "/pkg/mod" + SandboxGolangCIPath = ".coding-ethos/cache/golangci-lint" ) var ( @@ -673,6 +675,7 @@ func (request Request) evidence() Evidence { ) writePaths = append(writePaths, SandboxTempWritePath) writePaths = append(writePaths, SandboxGoCachePath) + writePaths = append(writePaths, SandboxGoPath) writePaths = append(writePaths, SandboxGolangCIPath) writePaths = append(writePaths, nativeSystemWritePaths()...) readPaths := append([]string(nil), request.Capabilities.ReadPaths...) diff --git a/go/internal/toolprotocol/actionlint_shellcheck.go b/go/internal/toolprotocol/actionlint_shellcheck.go new file mode 100644 index 00000000..08434745 --- /dev/null +++ b/go/internal/toolprotocol/actionlint_shellcheck.go @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +// Package toolprotocol defines explicit contracts between managed tools. +package toolprotocol + +const ( + // ActionlintTool identifies the managed actionlint process. + ActionlintTool = "actionlint" + // ShellcheckTool identifies actionlint's ShellCheck dependency. + ShellcheckTool = "shellcheck" + + // ActionlintShellcheckEnv marks ShellCheck requests made by managed actionlint. + ActionlintShellcheckEnv = "CODE_ETHOS_ACTIONLINT_SHELLCHECK_PROTOCOL" + // ActionlintShellcheckJSONStdinV1 identifies the raw JSON-on-stdin protocol. + ActionlintShellcheckJSONStdinV1 = "json-stdin-v1" +) + +// ActionlintShellcheckEnvironment returns the managed actionlint marker entry. +func ActionlintShellcheckEnvironment() string { + return ActionlintShellcheckEnv + "=" + ActionlintShellcheckJSONStdinV1 +} + +// IsActionlintShellcheckJSONStdin reports whether a request matches the raw protocol. +func IsActionlintShellcheckJSONStdin(marker, tool string, args []string) bool { + if marker != ActionlintShellcheckJSONStdinV1 || + tool != ShellcheckTool || + len(args) == 0 || + args[len(args)-1] != "-" { + return false + } + + for index, arg := range args { + if (arg == "-f" || arg == "--format") && + index+1 < len(args) && + args[index+1] == "json" { + return true + } + + if arg == "-f=json" || arg == "--format=json" { + return true + } + } + + return false +} diff --git a/go/internal/toolprotocol/actionlint_shellcheck_test.go b/go/internal/toolprotocol/actionlint_shellcheck_test.go new file mode 100644 index 00000000..9fd7f46e --- /dev/null +++ b/go/internal/toolprotocol/actionlint_shellcheck_test.go @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package toolprotocol + +import "testing" + +func TestIsActionlintShellcheckJSONStdin(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + marker string + tool string + args []string + want bool + }{ + { + name: "marked actionlint json stdin", + marker: ActionlintShellcheckJSONStdinV1, + tool: ShellcheckTool, + args: []string{"--norc", "-f", "json", "-x", "--shell", "bash", "-"}, + want: true, + }, + { + name: "marked long json format", + marker: ActionlintShellcheckJSONStdinV1, + tool: ShellcheckTool, + args: []string{"--format=json", "-"}, + want: true, + }, + { + name: "marker absent", + tool: ShellcheckTool, + args: []string{"-f", "json", "-"}, + }, + { + name: "unknown marker", + marker: "json-stdin-v2", + tool: ShellcheckTool, + args: []string{"-f", "json", "-"}, + }, + { + name: "wrong tool", + marker: ActionlintShellcheckJSONStdinV1, + tool: ActionlintTool, + args: []string{"-f", "json", "-"}, + }, + { + name: "not stdin", + marker: ActionlintShellcheckJSONStdinV1, + tool: ShellcheckTool, + args: []string{"-f", "json", "script.sh"}, + }, + { + name: "not json", + marker: ActionlintShellcheckJSONStdinV1, + tool: ShellcheckTool, + args: []string{"-f", "gcc", "-"}, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + got := IsActionlintShellcheckJSONStdin(test.marker, test.tool, test.args) + if got != test.want { + t.Fatalf("IsActionlintShellcheckJSONStdin() = %v, want %v", got, test.want) + } + }) + } +} diff --git a/pre-commit/hooks/pyproject.toml b/pre-commit/hooks/pyproject.toml index 459cb3a2..e2f9e9a0 100644 --- a/pre-commit/hooks/pyproject.toml +++ b/pre-commit/hooks/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ "mypy>=1.19.1", "mypy-extensions>=1.1.0", "pandas-stubs>=3.0.0.260204", - "pip>=26.1", + "pip>=26.2", "pydantic>=2.0.0", "pylint>=4.0.4", "pyright>=1.1.408", @@ -54,7 +54,7 @@ packages = ["coding_ethos_hooks"] [tool.uv] exclude-newer = "7 days" -exclude-newer-package = { pip = "2026-04-26T21:00:06Z", sqlfluff = "2026-05-15T00:00:00Z" } +exclude-newer-package = { pip = "2026-07-29T21:57:56Z", sqlfluff = "2026-05-15T00:00:00Z" } [tool.pyright] pythonVersion = "3.13" diff --git a/pre-commit/hooks/uv.lock b/pre-commit/hooks/uv.lock index a75e95d2..36c6d8a8 100644 --- a/pre-commit/hooks/uv.lock +++ b/pre-commit/hooks/uv.lock @@ -11,7 +11,7 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for exclude-newer-span = "P7D" [options.exclude-newer-package] -pip = "2026-04-26T21:00:06Z" +pip = "2026-07-29T21:57:56Z" sqlfluff = "2026-05-15T00:00:00Z" [[package]] @@ -257,7 +257,7 @@ requires-dist = [ { name = "mypy", specifier = ">=1.19.1" }, { name = "mypy-extensions", specifier = ">=1.1.0" }, { name = "pandas-stubs", specifier = ">=3.0.0.260204" }, - { name = "pip", specifier = ">=26.1" }, + { name = "pip", specifier = ">=26.2" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pylint", specifier = ">=4.0.4" }, { name = "pyright", specifier = ">=1.1.408" }, @@ -705,11 +705,11 @@ wheels = [ [[package]] name = "pip" -version = "26.1" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/7e/d2b04004e1068ad4fdfa2f227b839b5d03e602e47cdbbf49de71137c9546/pip-26.1.tar.gz", hash = "sha256:81e13ebcca3ffa8cc85e4deff5c27e1ee26dea0aa7fc2f294a073ac208806ff3", size = 1840316, upload-time = "2026-04-26T21:00:05.406Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/96/e6f8e9d9d7b9cc4457092712a7e919c3186aa2c2fa9ffed2c5d29cc947e8/pip-26.2.tar.gz", hash = "sha256:2d8542afcc84cdd8e846c2b36b2861fad1da376dd98f8e7113e9108a3c331690", size = 1848845, upload-time = "2026-07-29T21:57:56.407Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl", hash = "sha256:4e8486d821d814b77319acb7b9e8bf5a4ee7590a643e7cb21029f209be8573c1", size = 1812804, upload-time = "2026-04-26T21:00:03.194Z" }, + { url = "https://files.pythonhosted.org/packages/62/36/a3aed958d60531cb442b7ab4596cda7b3621cfb916f8ae1d6769795c7dc1/pip-26.2-py3-none-any.whl", hash = "sha256:931c303696af6fa3417112103b1cad26890e5a07eccb5b99783700e33f2b8aad", size = 1816475, upload-time = "2026-07-29T21:57:54.763Z" }, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 8db240b8..dfa8aea8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ packages = ["coding_ethos"] [tool.uv] exclude-newer = "7 days" -exclude-newer-package = { pip = "2026-05-31T17:33:57Z", sqlfluff = "2026-05-15T00:00:00Z" } +exclude-newer-package = { pip = "2026-07-29T21:57:56Z", sqlfluff = "2026-05-15T00:00:00Z" } managed = true [tool.uv.workspace] diff --git a/uv.lock b/uv.lock index a52bd661..8ddd9f5b 100644 --- a/uv.lock +++ b/uv.lock @@ -11,7 +11,7 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for exclude-newer-span = "P7D" [options.exclude-newer-package] -pip = "2026-05-31T17:33:57Z" +pip = "2026-07-29T21:57:56Z" sqlfluff = "2026-05-15T00:00:00Z" [manifest] @@ -294,7 +294,7 @@ requires-dist = [ { name = "mypy", specifier = ">=1.19.1" }, { name = "mypy-extensions", specifier = ">=1.1.0" }, { name = "pandas-stubs", specifier = ">=3.0.0.260204" }, - { name = "pip", specifier = ">=26.1" }, + { name = "pip", specifier = ">=26.2" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pylint", specifier = ">=4.0.4" }, { name = "pyright", specifier = ">=1.1.408" }, @@ -820,11 +820,11 @@ wheels = [ [[package]] name = "pip" -version = "26.1.2" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/96/e6f8e9d9d7b9cc4457092712a7e919c3186aa2c2fa9ffed2c5d29cc947e8/pip-26.2.tar.gz", hash = "sha256:2d8542afcc84cdd8e846c2b36b2861fad1da376dd98f8e7113e9108a3c331690", size = 1848845, upload-time = "2026-07-29T21:57:56.407Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, + { url = "https://files.pythonhosted.org/packages/62/36/a3aed958d60531cb442b7ab4596cda7b3621cfb916f8ae1d6769795c7dc1/pip-26.2-py3-none-any.whl", hash = "sha256:931c303696af6fa3417112103b1cad26890e5a07eccb5b99783700e33f2b8aad", size = 1816475, upload-time = "2026-07-29T21:57:54.763Z" }, ] [[package]] From 5a803de5b8a463ef74424475166ee62d7031c458 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Mon, 31 Aug 2026 18:02:51 -0600 Subject: [PATCH 16/16] fix(managedcapture): separate Go cache preparation --- .../managedcapture/sandbox_cache_env.go | 65 +++++++++++-------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/go/internal/managedcapture/sandbox_cache_env.go b/go/internal/managedcapture/sandbox_cache_env.go index 1acef6c7..92092a07 100644 --- a/go/internal/managedcapture/sandbox_cache_env.go +++ b/go/internal/managedcapture/sandbox_cache_env.go @@ -41,6 +41,13 @@ type sandboxCacheEnvironment struct { CleanupTemp bool } +type sandboxGoCachePaths struct { + Cache string + Path string + ModCache string + GolangCILint string +} + // rustHomes reports where Cargo and Rustup live, preferring the environment so // an installation moved off its default path is still found. func rustHomes() (string, string) { @@ -183,29 +190,7 @@ func sandboxCacheEnv( return sandboxCacheEnvironment{}, err } - goCache := filepath.Join(root, sandbox.SandboxGoCachePath) - - err = makeSandboxDir(goCache, "Go cache") - if err != nil { - return sandboxCacheEnvironment{}, err - } - - goPath := filepath.Join(root, sandbox.SandboxGoPath) - goModCache := filepath.Join(root, sandbox.SandboxGoModCachePath) - - err = makeSandboxDir(goPath, "Go path") - if err != nil { - return sandboxCacheEnvironment{}, err - } - - err = makeSandboxDir(goModCache, "Go module cache") - if err != nil { - return sandboxCacheEnvironment{}, err - } - - golangCILintDir := filepath.Join(root, sandbox.SandboxGolangCIPath) - - err = makeSandboxDir(golangCILintDir, "golangci-lint cache") + goCaches, err := prepareSandboxGoCachePaths(root) if err != nil { return sandboxCacheEnvironment{}, err } @@ -219,10 +204,10 @@ func sandboxCacheEnv( TempDir: tempDir, RuntimeDir: runtimeDir, CleanupTemp: cleanupTemp, - GoCache: goCache, - GoPath: goPath, - GoModCache: goModCache, - GolangCILintDir: golangCILintDir, + GoCache: goCaches.Cache, + GoPath: goCaches.Path, + GoModCache: goCaches.ModCache, + GolangCILintDir: goCaches.GolangCILint, GoRoot: managedGoRoot(ctx), CGOEnabled: "1", CC: cCompiler, @@ -235,6 +220,32 @@ func sandboxCacheEnv( }, nil } +func prepareSandboxGoCachePaths(root string) (sandboxGoCachePaths, error) { + paths := sandboxGoCachePaths{ + Cache: filepath.Join(root, sandbox.SandboxGoCachePath), + Path: filepath.Join(root, sandbox.SandboxGoPath), + ModCache: filepath.Join(root, sandbox.SandboxGoModCachePath), + GolangCILint: filepath.Join(root, sandbox.SandboxGolangCIPath), + } + + for _, dir := range []struct { + path string + what string + }{ + {paths.Cache, "Go cache"}, + {paths.Path, "Go path"}, + {paths.ModCache, "Go module cache"}, + {paths.GolangCILint, "golangci-lint cache"}, + } { + err := makeSandboxDir(dir.path, dir.what) + if err != nil { + return sandboxGoCachePaths{}, err + } + } + + return paths, nil +} + func resolvedManagedSubprocessGit(ctx context.Context) (string, error) { envGit := strings.TrimSpace(os.Getenv(evaluators.RealGitEnv)) if envGit != "" {