sanityops-cli 0.1.8__tar.gz → 0.1.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/CHANGELOG.md +20 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/PKG-INFO +1 -1
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/pyproject.toml +1 -1
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/__init__.py +1 -1
- sanityops_cli-0.1.9/src/sanityops_cli/code_contract_checker/__init__.py +35 -0
- sanityops_cli-0.1.9/src/sanityops_cli/code_contract_checker/checker.py +484 -0
- sanityops_cli-0.1.9/src/sanityops_cli/code_contract_checker/markdown_reporter.py +191 -0
- sanityops_cli-0.1.9/src/sanityops_cli/code_contract_checker/models.py +202 -0
- sanityops_cli-0.1.9/src/sanityops_cli/code_contract_checker/renderer.py +183 -0
- sanityops_cli-0.1.9/src/sanityops_cli/code_contract_checker/rules/__init__.py +55 -0
- sanityops_cli-0.1.9/src/sanityops_cli/code_contract_checker/rules/qc0_baseline.py +489 -0
- sanityops_cli-0.1.9/src/sanityops_cli/code_contract_checker/rules/qc1_syntax.py +401 -0
- sanityops_cli-0.1.9/src/sanityops_cli/code_contract_checker/rules/qc2_resource.py +345 -0
- sanityops_cli-0.1.9/src/sanityops_cli/code_contract_checker/rules/qc3_pattern.py +337 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/commands/inspect.py +67 -10
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/defect_checker/checker.py +9 -2
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/defect_checker/markdown_reporter.py +167 -20
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/defect_checker/renderer.py +39 -1
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/utils/artifact_packer.py +34 -17
- sanityops_cli-0.1.9/tests/unit/code_contract_checker/test_checker.py +165 -0
- sanityops_cli-0.1.9/tests/unit/code_contract_checker/test_renderer.py +73 -0
- sanityops_cli-0.1.9/tests/unit/code_contract_checker/test_rules.py +328 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/commands/test_inspect.py +106 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/defect_checker/test_markdown_reporter.py +49 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/defect_checker/test_renderer.py +65 -1
- sanityops_cli-0.1.9/tests/unit/progress/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/utils/test_artifact_packer.py +63 -4
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/.github/dependabot.yml +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/.github/scripts/pr_review_bot.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/.github/workflows/ai-review.yml +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/.github/workflows/ci.yml +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/.github/workflows/release.yml +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/.gitignore +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/CODE_OF_CONDUCT.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/CONTRIBUTING.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/LICENSE +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/NOTICE +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/README.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/SECURITY.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/plans/2026-08-13-model-config-comments-in-init-template.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/plans/2026-08-14-loading-animations.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/plans/2026-08-14-skills-files-banner-readme.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/plans/2026-08-28-copyright-compliance.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/plans/2026-09-08-config-command.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/plans/2026-09-20-cross-artifact-rendering.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/plans/2026-09-20-help-panel-migration.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/plans/2026-09-20-repair-feature.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/specs/2026-08-13-model-config-comments-in-init-template-design.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/specs/2026-08-14-loading-animations-design.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/specs/2026-08-14-skills-files-banner-readme-design.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/specs/2026-08-28-copyright-compliance-design.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/specs/2026-09-08-config-command-design.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/specs/2026-09-20-cross-artifact-rendering-design.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/specs/2026-09-20-help-panel-migration-design.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/docs/superpowers/specs/2026-09-20-repair-feature-design.md +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/entry.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/release.sh +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/scripts/generate-checksums.ps1 +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/scripts/install.ps1 +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/scripts/install.sh +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/repair_agent/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/repair_agent/agent.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/repair_agent/prompts.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/repair_agent/tools/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/repair_agent/tools/store_repairs_tool.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/scanner_agent/agent.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/scanner_agent/hooks/progress_hook.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/scanner_agent/models/finding.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/scanner_agent/prompts.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/scanner_agent/tools/grep_tool.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/scanner_agent/tools/listfiles_tool.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/scanner_agent/tools/readfile_tool.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/agents/scanner_agent/tools/storefindings_tool.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/api/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/api/client.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/commands/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/commands/config.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/commands/init.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/constants/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/constants/config_defaults.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/constants/exit_codes.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/defect_checker/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/defect_checker/llm_config.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/exceptions/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/exceptions/api_exceptions.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/exceptions/base_exceptions.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/help_panel.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/logging/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/logging/logger.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/main.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/progress/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/progress/tracker.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/renderers/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/renderers/command_renderer/inspect_command_renderer.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/templates/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/templates/inspect_config.yaml +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/utils/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/utils/config_loader.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/utils/config_resolver.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/src/sanityops_cli/utils/validators.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/commands/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/commands/test_config.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/commands/test_init.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/commands/test_inspect_repair.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/test_help_panel.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/agents/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/agents/repair_agent/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/agents/repair_agent/test_agent.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/agents/repair_agent/test_store_repairs_tool.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/agents/scanner_agent/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/agents/scanner_agent/models/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/agents/scanner_agent/models/test_finding.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/agents/scanner_agent/test_agent.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/agents/scanner_agent/test_deterministic_capture.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/agents/scanner_agent/test_progress_hook.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/agents/scanner_agent/test_prompts.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/api/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/api/test_client.py +0 -0
- {sanityops_cli-0.1.8/tests/unit/defect_checker → sanityops_cli-0.1.9/tests/unit/code_contract_checker}/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/commands/test_version.py +0 -0
- {sanityops_cli-0.1.8/tests/unit/logging → sanityops_cli-0.1.9/tests/unit/defect_checker}/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/defect_checker/test_checker.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/defect_checker/test_llm_config.py +0 -0
- {sanityops_cli-0.1.8/tests/unit/progress → sanityops_cli-0.1.9/tests/unit/logging}/__init__.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/logging/test_logger.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/progress/test_tracker.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/unit/utils/test_config_loader.py +0 -0
- {sanityops_cli-0.1.8 → sanityops_cli-0.1.9}/tests/utils/test_config_resolver.py +0 -0
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Code Contract (QC) lint check before LLM analysis: AST-based static inspection
|
|
13
|
+
of embedded code blocks and Python source files (syntax, undefined names,
|
|
14
|
+
resource leaks, code patterns) with `--skip-qc-lint` to opt out
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Breaking**: tool schema uploads now emit the provider-neutral flat format
|
|
19
|
+
`{name, description, parameters}` in both upload paths (push and inspect).
|
|
20
|
+
The OpenAI `{type, function}` wrapper is stripped during packing; the
|
|
21
|
+
backend converts to the selected provider format when a model endpoint is set.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Failed module inspections now render a CAUTION callout instead of a
|
|
26
|
+
misleading "No defects found" in markdown reports
|
|
27
|
+
- Duplicate defects reported under both `$.inputSchema` and `$.parameters`
|
|
28
|
+
are deduplicated in terminal and markdown output
|
|
29
|
+
|
|
10
30
|
## [0.0.8] - 2026-09-21
|
|
11
31
|
|
|
12
32
|
### Added
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright 2026 zipsonken
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Code Contract inspection module for Logic Artifacts."""
|
|
16
|
+
|
|
17
|
+
from .models import (
|
|
18
|
+
RULE_DEFINITIONS,
|
|
19
|
+
GateResult,
|
|
20
|
+
QCArtifactResult,
|
|
21
|
+
QCDefect,
|
|
22
|
+
QCInspectionResult,
|
|
23
|
+
QCRuleID,
|
|
24
|
+
Severity,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"QCArtifactResult",
|
|
29
|
+
"QCDefect",
|
|
30
|
+
"QCInspectionResult",
|
|
31
|
+
"QCRuleID",
|
|
32
|
+
"RULE_DEFINITIONS",
|
|
33
|
+
"Severity",
|
|
34
|
+
"GateResult",
|
|
35
|
+
]
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# Copyright 2026 zipsonken
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Main Code Contract inspector for Logic Artifacts."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import re
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from time import perf_counter
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
import yaml
|
|
28
|
+
|
|
29
|
+
from .models import (
|
|
30
|
+
QCArtifactResult,
|
|
31
|
+
QCInspectionResult,
|
|
32
|
+
)
|
|
33
|
+
from .rules import (
|
|
34
|
+
check_qc_0_1_syntax_parsability,
|
|
35
|
+
check_qc_0_3_structural_completeness,
|
|
36
|
+
check_qc_1_1_undefined_name,
|
|
37
|
+
check_qc_1_4_type_mismatch,
|
|
38
|
+
check_qc_2_1_unclosed_file_handle,
|
|
39
|
+
check_qc_2_2_unreleased_network,
|
|
40
|
+
check_qc_2_3_missing_exception_handling,
|
|
41
|
+
check_qc_3_1_unused_import,
|
|
42
|
+
check_qc_3_2_missing_import,
|
|
43
|
+
check_qc_3_3_hardcoded_demo_value,
|
|
44
|
+
extract_code_blocks,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
#: Maximum nesting depth when recursing into nested tool parameter schemas.
|
|
50
|
+
#: Prevents stack overflow on malformed or adversarial deeply-nested schemas.
|
|
51
|
+
MAX_TOOL_SCHEMA_DEPTH = 10
|
|
52
|
+
|
|
53
|
+
#: All QC checks applied to every Python code snippet, in order.
|
|
54
|
+
#: Adding a new rule here automatically applies it to both embedded
|
|
55
|
+
#: code blocks and whole Python source files.
|
|
56
|
+
#: Note: QC-0.2 (language declaration) is intentionally absent here - it
|
|
57
|
+
#: operates on the fence marker rather than the code body, so it is run
|
|
58
|
+
#: inside extract_code_blocks during block discovery instead.
|
|
59
|
+
_QC_PYTHON_CHECKS = (
|
|
60
|
+
check_qc_0_1_syntax_parsability,
|
|
61
|
+
check_qc_0_3_structural_completeness,
|
|
62
|
+
check_qc_1_1_undefined_name,
|
|
63
|
+
check_qc_1_4_type_mismatch,
|
|
64
|
+
check_qc_2_1_unclosed_file_handle,
|
|
65
|
+
check_qc_2_2_unreleased_network,
|
|
66
|
+
check_qc_2_3_missing_exception_handling,
|
|
67
|
+
check_qc_3_1_unused_import,
|
|
68
|
+
check_qc_3_2_missing_import,
|
|
69
|
+
check_qc_3_3_hardcoded_demo_value,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class CodeContractChecker:
|
|
74
|
+
"""Inspects Logic Artifacts for Code Contract violations.
|
|
75
|
+
|
|
76
|
+
This checker runs WITHOUT LLM - it uses pure AST parsing and
|
|
77
|
+
pattern matching to detect code defects in embedded code snippets.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, verbose: bool = False):
|
|
81
|
+
"""Initialize the checker.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
verbose: Enable verbose output.
|
|
85
|
+
"""
|
|
86
|
+
self.verbose = verbose
|
|
87
|
+
|
|
88
|
+
def check_from_paths(
|
|
89
|
+
self,
|
|
90
|
+
prompts: list[str],
|
|
91
|
+
tools: list[str],
|
|
92
|
+
skills: list[str],
|
|
93
|
+
) -> QCInspectionResult:
|
|
94
|
+
"""Inspect artifacts from file paths.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
prompts: List of prompt file paths.
|
|
98
|
+
tools: List of tool schema file paths.
|
|
99
|
+
skills: List of skill file paths.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
QCInspectionResult with all defects found.
|
|
103
|
+
"""
|
|
104
|
+
start_time = perf_counter()
|
|
105
|
+
# Single timestamp for the whole run so all artifacts in one
|
|
106
|
+
# inspection share a consistent checked_at value.
|
|
107
|
+
checked_at = datetime.now().isoformat()
|
|
108
|
+
result = QCInspectionResult()
|
|
109
|
+
|
|
110
|
+
# Check prompts
|
|
111
|
+
for path in prompts:
|
|
112
|
+
artifact_result = self._check_prompt(path, checked_at)
|
|
113
|
+
result.prompts.append(artifact_result)
|
|
114
|
+
|
|
115
|
+
# Check tools
|
|
116
|
+
for path in tools:
|
|
117
|
+
artifact_result = self._check_tool(path, checked_at)
|
|
118
|
+
result.tools.append(artifact_result)
|
|
119
|
+
|
|
120
|
+
# Check skills
|
|
121
|
+
for path in skills:
|
|
122
|
+
artifact_result = self._check_skill(path, checked_at)
|
|
123
|
+
result.skills.append(artifact_result)
|
|
124
|
+
|
|
125
|
+
result.inspection_time_seconds = perf_counter() - start_time
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
def _read_artifact(self, path: str) -> str | None:
|
|
129
|
+
"""Read an artifact file, returning None on read failure."""
|
|
130
|
+
try:
|
|
131
|
+
return Path(path).read_text(encoding="utf-8")
|
|
132
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
133
|
+
# OSError covers FileNotFoundError, PermissionError,
|
|
134
|
+
# IsADirectoryError and other path-related failures;
|
|
135
|
+
# UnicodeDecodeError (a ValueError) covers binary files.
|
|
136
|
+
logger.debug("Could not read %s: %s", path, e)
|
|
137
|
+
if self.verbose:
|
|
138
|
+
logger.warning("Could not read %s: %s", path, e)
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
def _check_prompt(self, path: str, checked_at: str) -> QCArtifactResult:
|
|
142
|
+
"""Check a prompt file for code contract violations.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
path: Absolute path to the prompt file.
|
|
146
|
+
checked_at: ISO timestamp shared across the inspection run.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
QCArtifactResult with any defects found.
|
|
150
|
+
"""
|
|
151
|
+
artifact_result = QCArtifactResult(
|
|
152
|
+
artifact_type="prompt",
|
|
153
|
+
artifact_path=path,
|
|
154
|
+
artifact_name=Path(path).name,
|
|
155
|
+
checked_at=checked_at,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
content = self._read_artifact(path)
|
|
159
|
+
if content is None:
|
|
160
|
+
return artifact_result
|
|
161
|
+
|
|
162
|
+
# Extract and check code blocks
|
|
163
|
+
code_blocks = extract_code_blocks(content, artifact_result, location_prefix="$.content")
|
|
164
|
+
|
|
165
|
+
for idx, (code, language, line_number) in enumerate(code_blocks):
|
|
166
|
+
location = f"$.content.code_blocks[{idx}]"
|
|
167
|
+
self._run_qc_checks(code, language, artifact_result, location, line_number)
|
|
168
|
+
|
|
169
|
+
return artifact_result
|
|
170
|
+
|
|
171
|
+
def _check_tool(self, path: str, checked_at: str) -> QCArtifactResult:
|
|
172
|
+
"""Check a tool schema file for code contract violations.
|
|
173
|
+
|
|
174
|
+
Code snippets can appear in:
|
|
175
|
+
- description field
|
|
176
|
+
- parameters.properties.*.description
|
|
177
|
+
- parameters.properties.*.default (for code-like values)
|
|
178
|
+
- parameters.properties.*.examples
|
|
179
|
+
|
|
180
|
+
For Python source files (.py), the entire file is checked directly.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
path: Absolute path to the tool schema file.
|
|
184
|
+
checked_at: ISO timestamp shared across the inspection run.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
QCArtifactResult with any defects found.
|
|
188
|
+
"""
|
|
189
|
+
artifact_result = QCArtifactResult(
|
|
190
|
+
artifact_type="tool",
|
|
191
|
+
artifact_path=path,
|
|
192
|
+
artifact_name=Path(path).stem,
|
|
193
|
+
checked_at=checked_at,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
content = self._read_artifact(path)
|
|
197
|
+
if content is None:
|
|
198
|
+
return artifact_result
|
|
199
|
+
|
|
200
|
+
# Handle Python source files directly
|
|
201
|
+
if Path(path).suffix.lower() == ".py":
|
|
202
|
+
# For .py files, check the entire file as Python code
|
|
203
|
+
self._run_qc_checks(content, "python", artifact_result, "$.content", line_offset=0)
|
|
204
|
+
return artifact_result
|
|
205
|
+
|
|
206
|
+
# Parse JSON or YAML
|
|
207
|
+
try:
|
|
208
|
+
if Path(path).suffix.lower() == ".json":
|
|
209
|
+
data = json.loads(content)
|
|
210
|
+
else:
|
|
211
|
+
data = yaml.safe_load(content)
|
|
212
|
+
except (json.JSONDecodeError, yaml.YAMLError) as e:
|
|
213
|
+
# Not valid JSON/YAML - try to extract code blocks from raw content.
|
|
214
|
+
# Log at DEBUG so a fallback stays explainable without spamming
|
|
215
|
+
# normal output.
|
|
216
|
+
logger.debug(
|
|
217
|
+
"Could not parse %s as JSON/YAML, falling back to code blocks: %s", path, e
|
|
218
|
+
)
|
|
219
|
+
code_blocks = extract_code_blocks(content, artifact_result, location_prefix="$.content")
|
|
220
|
+
self._check_code_blocks(code_blocks, artifact_result, location_prefix="$.content")
|
|
221
|
+
return artifact_result
|
|
222
|
+
|
|
223
|
+
# Check description field
|
|
224
|
+
description = data.get("description", "")
|
|
225
|
+
if description and isinstance(description, str):
|
|
226
|
+
code_blocks = extract_code_blocks(
|
|
227
|
+
description, artifact_result, location_prefix="$.description"
|
|
228
|
+
)
|
|
229
|
+
self._check_code_blocks(code_blocks, artifact_result, location_prefix="$.description")
|
|
230
|
+
|
|
231
|
+
# Check parameter descriptions and examples
|
|
232
|
+
parameters = data.get("parameters", {})
|
|
233
|
+
if isinstance(parameters, dict):
|
|
234
|
+
self._check_tool_parameters(parameters, artifact_result, "$.parameters")
|
|
235
|
+
|
|
236
|
+
return artifact_result
|
|
237
|
+
|
|
238
|
+
def _run_qc_checks(
|
|
239
|
+
self,
|
|
240
|
+
code: str,
|
|
241
|
+
language: str,
|
|
242
|
+
artifact_result: QCArtifactResult,
|
|
243
|
+
location: str,
|
|
244
|
+
line_offset: int,
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Apply every QC check to a single Python code snippet.
|
|
247
|
+
|
|
248
|
+
Shared by embedded code blocks and whole Python source files.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
code: The code snippet to check.
|
|
252
|
+
language: The declared language.
|
|
253
|
+
artifact_result: The artifact result to add defects to.
|
|
254
|
+
location: JSONPath-like location of the code.
|
|
255
|
+
line_offset: Line offset added to each AST node's own line number
|
|
256
|
+
to produce the absolute line in the source. Pass 0 for whole
|
|
257
|
+
files; for embedded blocks pass the block's starting line.
|
|
258
|
+
"""
|
|
259
|
+
for check in _QC_PYTHON_CHECKS:
|
|
260
|
+
check(code, language, artifact_result, location, line_offset)
|
|
261
|
+
|
|
262
|
+
def _check_tool_parameters(
|
|
263
|
+
self,
|
|
264
|
+
parameters: dict[str, Any],
|
|
265
|
+
artifact_result: QCArtifactResult,
|
|
266
|
+
location_prefix: str,
|
|
267
|
+
*,
|
|
268
|
+
depth: int = 0,
|
|
269
|
+
max_depth: int = MAX_TOOL_SCHEMA_DEPTH,
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Recursively check tool parameters for code snippets.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
parameters: The parameters dict to check.
|
|
275
|
+
artifact_result: The artifact result to add defects to.
|
|
276
|
+
location_prefix: Location prefix for defects.
|
|
277
|
+
depth: Current recursion depth.
|
|
278
|
+
max_depth: Maximum nesting depth before stopping (prevents stack overflow
|
|
279
|
+
on malformed or adversarial deeply-nested schemas).
|
|
280
|
+
"""
|
|
281
|
+
if depth >= max_depth:
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
properties = parameters.get("properties", {})
|
|
285
|
+
if not isinstance(properties, dict):
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
for prop_name, prop_def in properties.items():
|
|
289
|
+
if not isinstance(prop_def, dict):
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
prop_location = f"{location_prefix}.properties.{prop_name}"
|
|
293
|
+
|
|
294
|
+
# Check description
|
|
295
|
+
description = prop_def.get("description", "")
|
|
296
|
+
if description and isinstance(description, str):
|
|
297
|
+
code_blocks = extract_code_blocks(
|
|
298
|
+
description, artifact_result, location_prefix=f"{prop_location}.description"
|
|
299
|
+
)
|
|
300
|
+
self._check_code_blocks(
|
|
301
|
+
code_blocks, artifact_result, location_prefix=f"{prop_location}.description"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Check default value if it looks like code
|
|
305
|
+
default = prop_def.get("default")
|
|
306
|
+
if isinstance(default, str) and "\n" in default:
|
|
307
|
+
# Multi-line default might be code
|
|
308
|
+
code_blocks = extract_code_blocks(
|
|
309
|
+
default, artifact_result, location_prefix=f"{prop_location}.default"
|
|
310
|
+
)
|
|
311
|
+
self._check_code_blocks(
|
|
312
|
+
code_blocks, artifact_result, location_prefix=f"{prop_location}.default"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Check examples
|
|
316
|
+
examples = prop_def.get("examples", [])
|
|
317
|
+
if isinstance(examples, list):
|
|
318
|
+
for idx, example in enumerate(examples):
|
|
319
|
+
if isinstance(example, str) and "\n" in example:
|
|
320
|
+
code_blocks = extract_code_blocks(
|
|
321
|
+
example,
|
|
322
|
+
artifact_result,
|
|
323
|
+
location_prefix=f"{prop_location}.examples[{idx}]",
|
|
324
|
+
)
|
|
325
|
+
self._check_code_blocks(
|
|
326
|
+
code_blocks,
|
|
327
|
+
artifact_result,
|
|
328
|
+
location_prefix=f"{prop_location}.examples[{idx}]",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Check nested object properties
|
|
332
|
+
if prop_def.get("type") == "object":
|
|
333
|
+
nested_props = prop_def.get("properties", {})
|
|
334
|
+
if isinstance(nested_props, dict):
|
|
335
|
+
self._check_tool_parameters(
|
|
336
|
+
prop_def,
|
|
337
|
+
artifact_result,
|
|
338
|
+
prop_location,
|
|
339
|
+
depth=depth + 1,
|
|
340
|
+
max_depth=max_depth,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Check items for array types
|
|
344
|
+
items = prop_def.get("items", {})
|
|
345
|
+
if isinstance(items, dict) and items.get("type") == "object":
|
|
346
|
+
self._check_tool_parameters(
|
|
347
|
+
items,
|
|
348
|
+
artifact_result,
|
|
349
|
+
f"{prop_location}.items",
|
|
350
|
+
depth=depth + 1,
|
|
351
|
+
max_depth=max_depth,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def _check_skill(self, path: str, checked_at: str) -> QCArtifactResult:
|
|
355
|
+
"""Check a skill file for code contract violations.
|
|
356
|
+
|
|
357
|
+
Code snippets can appear in:
|
|
358
|
+
- description field (frontmatter)
|
|
359
|
+
- section content (markdown body)
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
path: Absolute path to the skill file.
|
|
363
|
+
checked_at: ISO timestamp shared across the inspection run.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
QCArtifactResult with any defects found.
|
|
367
|
+
"""
|
|
368
|
+
artifact_result = QCArtifactResult(
|
|
369
|
+
artifact_type="skill",
|
|
370
|
+
artifact_path=path,
|
|
371
|
+
artifact_name=Path(path).stem,
|
|
372
|
+
checked_at=checked_at,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
content = self._read_artifact(path)
|
|
376
|
+
if content is None:
|
|
377
|
+
return artifact_result
|
|
378
|
+
|
|
379
|
+
# Parse frontmatter
|
|
380
|
+
frontmatter, body = self._parse_frontmatter(content)
|
|
381
|
+
|
|
382
|
+
# Check description in frontmatter
|
|
383
|
+
description = frontmatter.get("description", "")
|
|
384
|
+
if description and isinstance(description, str):
|
|
385
|
+
code_blocks = extract_code_blocks(
|
|
386
|
+
description, artifact_result, location_prefix="$.frontmatter.description"
|
|
387
|
+
)
|
|
388
|
+
self._check_code_blocks(
|
|
389
|
+
code_blocks, artifact_result, location_prefix="$.frontmatter.description"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Check markdown body sections
|
|
393
|
+
sections = self._parse_markdown_sections(body)
|
|
394
|
+
for section_title, section_content in sections:
|
|
395
|
+
safe_title = section_title.replace(".", "_").replace("[", "_")
|
|
396
|
+
code_blocks = extract_code_blocks(
|
|
397
|
+
section_content,
|
|
398
|
+
artifact_result,
|
|
399
|
+
location_prefix=f"$.sections.{safe_title}",
|
|
400
|
+
)
|
|
401
|
+
self._check_code_blocks(
|
|
402
|
+
code_blocks, artifact_result, location_prefix=f"$.sections.{safe_title}"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
return artifact_result
|
|
406
|
+
|
|
407
|
+
def _parse_frontmatter(self, content: str) -> tuple[dict[str, Any], str]:
|
|
408
|
+
"""Parse YAML frontmatter from markdown content.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
content: The full file content.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
Tuple of (frontmatter_dict, body_content).
|
|
415
|
+
"""
|
|
416
|
+
if not content.startswith("---"):
|
|
417
|
+
return {}, content
|
|
418
|
+
|
|
419
|
+
# Find the closing ---
|
|
420
|
+
end_idx = content.find("\n---", 4)
|
|
421
|
+
if end_idx == -1:
|
|
422
|
+
return {}, content
|
|
423
|
+
|
|
424
|
+
frontmatter_str = content[4:end_idx]
|
|
425
|
+
body = content[end_idx + 5 :].lstrip()
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
frontmatter = yaml.safe_load(frontmatter_str) or {}
|
|
429
|
+
except yaml.YAMLError:
|
|
430
|
+
frontmatter = {}
|
|
431
|
+
|
|
432
|
+
return frontmatter, body
|
|
433
|
+
|
|
434
|
+
def _parse_markdown_sections(self, content: str) -> list[tuple[str, str]]:
|
|
435
|
+
"""Parse markdown into sections by headings of any level.
|
|
436
|
+
|
|
437
|
+
Every ATX heading (``#`` through ``######``) starts a new section so
|
|
438
|
+
code blocks under ``###``/``####`` headings are attributed to their
|
|
439
|
+
own section rather than their parent ``##`` block.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
content: Markdown content.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
List of (section_title, section_content) tuples.
|
|
446
|
+
"""
|
|
447
|
+
heading_re = re.compile(r"^(#{1,6})\s+(.*)$")
|
|
448
|
+
sections: list[tuple[str, str]] = []
|
|
449
|
+
current_title = ""
|
|
450
|
+
current_content: list[str] = []
|
|
451
|
+
|
|
452
|
+
for line in content.split("\n"):
|
|
453
|
+
match = heading_re.match(line)
|
|
454
|
+
if match:
|
|
455
|
+
# Save previous section
|
|
456
|
+
if current_content:
|
|
457
|
+
sections.append((current_title, "\n".join(current_content)))
|
|
458
|
+
current_title = match.group(2).strip()
|
|
459
|
+
current_content = []
|
|
460
|
+
else:
|
|
461
|
+
current_content.append(line)
|
|
462
|
+
|
|
463
|
+
# Save last section
|
|
464
|
+
if current_content:
|
|
465
|
+
sections.append((current_title, "\n".join(current_content)))
|
|
466
|
+
|
|
467
|
+
return sections
|
|
468
|
+
|
|
469
|
+
def _check_code_blocks(
|
|
470
|
+
self,
|
|
471
|
+
code_blocks: list[tuple[str, str, int]],
|
|
472
|
+
artifact_result: QCArtifactResult,
|
|
473
|
+
location_prefix: str = "$",
|
|
474
|
+
) -> None:
|
|
475
|
+
"""Apply all QC checks to a list of code blocks.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
code_blocks: List of (code, language, line_number) tuples.
|
|
479
|
+
artifact_result: The artifact result to add defects to.
|
|
480
|
+
location_prefix: Prefix for defect locations.
|
|
481
|
+
"""
|
|
482
|
+
for idx, (code, language, line_number) in enumerate(code_blocks):
|
|
483
|
+
location = f"{location_prefix}.code_blocks[{idx}]"
|
|
484
|
+
self._run_qc_checks(code, language, artifact_result, location, line_number)
|