agentic-project-kit 1.0.1__py3-none-any.whl
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.
- agentic_project_kit/__init__.py +1 -0
- agentic_project_kit/absolute_path_portability_audit.py +241 -0
- agentic_project_kit/access_level_policy.py +14 -0
- agentic_project_kit/access_levels.py +22 -0
- agentic_project_kit/action_registry.py +195 -0
- agentic_project_kit/action_specs.py +286 -0
- agentic_project_kit/agent_command_runner.py +516 -0
- agentic_project_kit/audit_test_matrix.py +36 -0
- agentic_project_kit/audit_test_matrix_cli.py +43 -0
- agentic_project_kit/chat_bootloader.py +257 -0
- agentic_project_kit/chat_entrypoint_contract.py +297 -0
- agentic_project_kit/checks.py +511 -0
- agentic_project_kit/ci_readiness.py +211 -0
- agentic_project_kit/cli.py +147 -0
- agentic_project_kit/cli_commands/__init__.py +1 -0
- agentic_project_kit/cli_commands/absolute_path_audit.py +25 -0
- agentic_project_kit/cli_commands/actions.py +21 -0
- agentic_project_kit/cli_commands/artifact_gc.py +240 -0
- agentic_project_kit/cli_commands/boot.py +55 -0
- agentic_project_kit/cli_commands/chat.py +34 -0
- agentic_project_kit/cli_commands/checks.py +305 -0
- agentic_project_kit/cli_commands/cockpit.py +103 -0
- agentic_project_kit/cli_commands/command_taxonomy.py +25 -0
- agentic_project_kit/cli_commands/commands.py +129 -0
- agentic_project_kit/cli_commands/dev.py +23 -0
- agentic_project_kit/cli_commands/direction.py +92 -0
- agentic_project_kit/cli_commands/doc_currency_audit.py +25 -0
- agentic_project_kit/cli_commands/doc_registry.py +126 -0
- agentic_project_kit/cli_commands/docs.py +223 -0
- agentic_project_kit/cli_commands/dpa.py +1338 -0
- agentic_project_kit/cli_commands/evidence.py +251 -0
- agentic_project_kit/cli_commands/github.py +34 -0
- agentic_project_kit/cli_commands/governance.py +14 -0
- agentic_project_kit/cli_commands/gui.py +23 -0
- agentic_project_kit/cli_commands/gui_readiness_gate.py +27 -0
- agentic_project_kit/cli_commands/handoff.py +183 -0
- agentic_project_kit/cli_commands/human_workflows.py +430 -0
- agentic_project_kit/cli_commands/init.py +132 -0
- agentic_project_kit/cli_commands/instruction.py +75 -0
- agentic_project_kit/cli_commands/mutation_lock_audit.py +24 -0
- agentic_project_kit/cli_commands/ns_legacy_audit.py +25 -0
- agentic_project_kit/cli_commands/pass_already_done.py +66 -0
- agentic_project_kit/cli_commands/patch_failure_discipline_audit.py +26 -0
- agentic_project_kit/cli_commands/patch_preflight.py +37 -0
- agentic_project_kit/cli_commands/path_literal_audit.py +39 -0
- agentic_project_kit/cli_commands/patterns.py +40 -0
- agentic_project_kit/cli_commands/planning_docs_audit.py +25 -0
- agentic_project_kit/cli_commands/pr.py +133 -0
- agentic_project_kit/cli_commands/pr_hygiene.py +27 -0
- agentic_project_kit/cli_commands/profiles.py +26 -0
- agentic_project_kit/cli_commands/program_redundancy_audit.py +25 -0
- agentic_project_kit/cli_commands/project_direction.py +25 -0
- agentic_project_kit/cli_commands/release.py +392 -0
- agentic_project_kit/cli_commands/release_publish_orchestration.py +36 -0
- agentic_project_kit/cli_commands/remote_branch_hygiene.py +152 -0
- agentic_project_kit/cli_commands/remote_next.py +57 -0
- agentic_project_kit/cli_commands/removed_ns_commands.py +65 -0
- agentic_project_kit/cli_commands/rule_registry.py +129 -0
- agentic_project_kit/cli_commands/rules.py +222 -0
- agentic_project_kit/cli_commands/scaffold.py +46 -0
- agentic_project_kit/cli_commands/slice.py +21 -0
- agentic_project_kit/cli_commands/standard_gates_audit_suite.py +27 -0
- agentic_project_kit/cli_commands/state.py +51 -0
- agentic_project_kit/cli_commands/status_current_state_audit.py +30 -0
- agentic_project_kit/cli_commands/todo.py +53 -0
- agentic_project_kit/cli_commands/transfer.py +19 -0
- agentic_project_kit/cli_commands/transfer_context_flow.py +398 -0
- agentic_project_kit/cli_commands/transfer_context_helpers.py +398 -0
- agentic_project_kit/cli_commands/transfer_diagnostics.py +767 -0
- agentic_project_kit/cli_commands/transfer_evidence_flow.py +368 -0
- agentic_project_kit/cli_commands/transfer_handoff_flow.py +594 -0
- agentic_project_kit/cli_commands/transfer_order_contract.py +74 -0
- agentic_project_kit/cli_commands/transfer_post_merge_complete.py +442 -0
- agentic_project_kit/cli_commands/transfer_post_merge_settle.py +171 -0
- agentic_project_kit/cli_commands/transfer_pr_closeout_complete.py +124 -0
- agentic_project_kit/cli_commands/transfer_pr_create_flow.py +743 -0
- agentic_project_kit/cli_commands/transfer_pr_merge_flow.py +547 -0
- agentic_project_kit/cli_commands/transfer_repo_after_pr.py +160 -0
- agentic_project_kit/cli_commands/transfer_repo_pre_pr.py +664 -0
- agentic_project_kit/cli_commands/transfer_runner_flow.py +131 -0
- agentic_project_kit/cli_commands/transfer_runtime_initial.py +194 -0
- agentic_project_kit/cli_commands/transfer_shared.py +274 -0
- agentic_project_kit/cli_commands/validation.py +200 -0
- agentic_project_kit/cli_commands/version.py +27 -0
- agentic_project_kit/cli_commands/work_orders.py +219 -0
- agentic_project_kit/cli_commands/workflow.py +471 -0
- agentic_project_kit/cli_commands/workflow_guard.py +26 -0
- agentic_project_kit/cli_commands/workspace.py +252 -0
- agentic_project_kit/cockpit.py +608 -0
- agentic_project_kit/cockpit_readiness.py +67 -0
- agentic_project_kit/command_authority_audit.py +192 -0
- agentic_project_kit/command_inbox_check.py +103 -0
- agentic_project_kit/command_manifest.py +591 -0
- agentic_project_kit/command_queue_contract.py +161 -0
- agentic_project_kit/command_selector.py +245 -0
- agentic_project_kit/command_taxonomy.py +264 -0
- agentic_project_kit/commit_guard.py +67 -0
- agentic_project_kit/communication_artifact_gc.py +495 -0
- agentic_project_kit/communication_rule_context.py +437 -0
- agentic_project_kit/communication_state.py +115 -0
- agentic_project_kit/composed_wrapper_invariants.py +97 -0
- agentic_project_kit/composite_short_circuit.py +140 -0
- agentic_project_kit/contract.py +251 -0
- agentic_project_kit/control_state.py +83 -0
- agentic_project_kit/doc_currency_audit.py +536 -0
- agentic_project_kit/doc_lifecycle.py +1179 -0
- agentic_project_kit/doc_lifecycle_signals.py +446 -0
- agentic_project_kit/doc_lifecycle_sweep.py +639 -0
- agentic_project_kit/doc_mesh.py +491 -0
- agentic_project_kit/doctor.py +379 -0
- agentic_project_kit/documentation_registry.py +1122 -0
- agentic_project_kit/documentation_system_audit.py +310 -0
- agentic_project_kit/dpa_current_handoff_lifecycle.py +549 -0
- agentic_project_kit/dpa_dp2_decision_readiness.py +362 -0
- agentic_project_kit/dpa_dp3_dp4_adjudication.py +604 -0
- agentic_project_kit/dpa_dp5_block_new_gate.py +294 -0
- agentic_project_kit/dpa_dp5_stage_adoption.py +615 -0
- agentic_project_kit/dpa_dp5_strict_gate.py +187 -0
- agentic_project_kit/dpa_final_closeout.py +627 -0
- agentic_project_kit/dpa_fixture_evidence.py +680 -0
- agentic_project_kit/dpa_maintainer_record_check.py +711 -0
- agentic_project_kit/dpa_post_dp2_scope_assessment.py +734 -0
- agentic_project_kit/dpa_probe_002_readiness.py +509 -0
- agentic_project_kit/dpa_probe_003_readiness.py +417 -0
- agentic_project_kit/dpa_probe_004_readiness.py +437 -0
- agentic_project_kit/dpa_readiness.py +505 -0
- agentic_project_kit/dpa_readonly_probe_execution.py +500 -0
- agentic_project_kit/dpa_registry_contracts.py +312 -0
- agentic_project_kit/dpa_renderer_readiness.py +407 -0
- agentic_project_kit/dpa_repo_adoption_assessment.py +574 -0
- agentic_project_kit/dpa_stable_readiness.py +743 -0
- agentic_project_kit/dpa_successor_projection.py +168 -0
- agentic_project_kit/dpa_workspace_init_projection.py +93 -0
- agentic_project_kit/dpa_wrt_ch001_evidence.py +327 -0
- agentic_project_kit/entrypoint_slice_runner.py +153 -0
- agentic_project_kit/evidence_clean.py +152 -0
- agentic_project_kit/evidence_commit_paths.py +214 -0
- agentic_project_kit/evidence_finalize_log.py +204 -0
- agentic_project_kit/evidence_guard.py +114 -0
- agentic_project_kit/evidence_inspector.py +507 -0
- agentic_project_kit/evidence_state_contract.py +99 -0
- agentic_project_kit/execution_mode_state.py +148 -0
- agentic_project_kit/final_summary_contract.py +85 -0
- agentic_project_kit/finalize_guard.py +139 -0
- agentic_project_kit/gatekeeper_core.py +232 -0
- agentic_project_kit/github.py +25 -0
- agentic_project_kit/governance.py +59 -0
- agentic_project_kit/gui_action_execution.py +105 -0
- agentic_project_kit/gui_action_renderer.py +81 -0
- agentic_project_kit/gui_action_run_intent.py +98 -0
- agentic_project_kit/gui_action_views.py +195 -0
- agentic_project_kit/gui_activity_log.py +76 -0
- agentic_project_kit/gui_button_catalog.py +605 -0
- agentic_project_kit/gui_cockpit.py +365 -0
- agentic_project_kit/gui_cockpit_actions.py +695 -0
- agentic_project_kit/gui_cockpit_common.py +172 -0
- agentic_project_kit/gui_cockpit_header.py +501 -0
- agentic_project_kit/gui_cockpit_sidebar.py +387 -0
- agentic_project_kit/gui_cockpit_task.py +748 -0
- agentic_project_kit/gui_command_projection.py +355 -0
- agentic_project_kit/gui_communication_modes.py +130 -0
- agentic_project_kit/gui_dry_run.py +219 -0
- agentic_project_kit/gui_gatekeeper_status.py +259 -0
- agentic_project_kit/gui_layout_plan.py +148 -0
- agentic_project_kit/gui_open_folder.py +18 -0
- agentic_project_kit/gui_output_status_panel.py +61 -0
- agentic_project_kit/gui_panel_state.py +47 -0
- agentic_project_kit/gui_presenter.py +62 -0
- agentic_project_kit/gui_readiness_gate.py +206 -0
- agentic_project_kit/gui_release_flow.py +59 -0
- agentic_project_kit/gui_task_editor.py +1077 -0
- agentic_project_kit/gui_tk_widgets.py +177 -0
- agentic_project_kit/gui_tkinter_renderer.py +87 -0
- agentic_project_kit/gui_tkinter_shell.py +1176 -0
- agentic_project_kit/gui_transfer_contract.py +28 -0
- agentic_project_kit/gui_viewmodel.py +698 -0
- agentic_project_kit/gui_window_guard.py +46 -0
- agentic_project_kit/handoff_freshness.py +390 -0
- agentic_project_kit/handoff_prompt.py +180 -0
- agentic_project_kit/handoff_state.py +254 -0
- agentic_project_kit/instruction_lint.py +455 -0
- agentic_project_kit/llm_context_carriers.py +66 -0
- agentic_project_kit/llm_execution_context.py +285 -0
- agentic_project_kit/local_command_stack.py +133 -0
- agentic_project_kit/local_feature_gate.py +82 -0
- agentic_project_kit/local_garbage_collector.py +269 -0
- agentic_project_kit/models.py +18 -0
- agentic_project_kit/mutation_lock_audit.py +421 -0
- agentic_project_kit/next_turn_evidence.py +253 -0
- agentic_project_kit/next_turn_merge_if_green.py +459 -0
- agentic_project_kit/next_turn_pr_status.py +255 -0
- agentic_project_kit/next_turn_result.py +183 -0
- agentic_project_kit/next_turn_runner.py +159 -0
- agentic_project_kit/next_turn_slot.py +107 -0
- agentic_project_kit/next_turn_status.py +274 -0
- agentic_project_kit/ns_legacy_reference_audit.py +258 -0
- agentic_project_kit/ns_slice_runner.py +16 -0
- agentic_project_kit/operational_handoff_projection.py +124 -0
- agentic_project_kit/output_contract.py +80 -0
- agentic_project_kit/output_repair.py +61 -0
- agentic_project_kit/pass_already_done.py +216 -0
- agentic_project_kit/patch_artifact_preflight.py +147 -0
- agentic_project_kit/patch_cycle_workflow.py +440 -0
- agentic_project_kit/patch_failure_discipline_audit.py +223 -0
- agentic_project_kit/patch_preflight.py +215 -0
- agentic_project_kit/path_literal_audit.py +612 -0
- agentic_project_kit/patterns.py +99 -0
- agentic_project_kit/planning_docs_consolidation_audit.py +313 -0
- agentic_project_kit/portability_shell_gate.py +40 -0
- agentic_project_kit/post_merge_handoff_refresh.py +56 -0
- agentic_project_kit/post_release.py +252 -0
- agentic_project_kit/post_release_closeout.py +616 -0
- agentic_project_kit/pr_cleanup.py +172 -0
- agentic_project_kit/pr_closeout.py +50 -0
- agentic_project_kit/pr_closeout_complete.py +344 -0
- agentic_project_kit/pr_create_or_skip.py +71 -0
- agentic_project_kit/pr_hygiene.py +210 -0
- agentic_project_kit/program_redundancy_audit.py +251 -0
- agentic_project_kit/project_direction.py +840 -0
- agentic_project_kit/protected_change_planner.py +194 -0
- agentic_project_kit/release.py +401 -0
- agentic_project_kit/release_doi_safety.py +24 -0
- agentic_project_kit/release_gate_core.py +174 -0
- agentic_project_kit/release_metadata_authority_gate.py +328 -0
- agentic_project_kit/release_metadata_prep.py +43 -0
- agentic_project_kit/release_notes.py +823 -0
- agentic_project_kit/release_prep_core.py +204 -0
- agentic_project_kit/release_prepare.py +369 -0
- agentic_project_kit/release_process_guardrails.py +247 -0
- agentic_project_kit/release_publish_core.py +114 -0
- agentic_project_kit/release_publish_orchestration.py +469 -0
- agentic_project_kit/release_state.py +552 -0
- agentic_project_kit/release_verify_core.py +175 -0
- agentic_project_kit/remote_branch_hygiene.py +223 -0
- agentic_project_kit/remote_next.py +89 -0
- agentic_project_kit/remote_next_closeout.py +101 -0
- agentic_project_kit/remote_next_order_contract.py +391 -0
- agentic_project_kit/removed_ns_commands.py +458 -0
- agentic_project_kit/removed_source_audit.py +225 -0
- agentic_project_kit/rendering.py +13 -0
- agentic_project_kit/repair_report.py +79 -0
- agentic_project_kit/repo_identity.py +59 -0
- agentic_project_kit/result_report_classifier.py +57 -0
- agentic_project_kit/rule_ack.py +105 -0
- agentic_project_kit/rule_preservation.py +58 -0
- agentic_project_kit/rule_refresh.py +174 -0
- agentic_project_kit/rule_registry_registration.py +322 -0
- agentic_project_kit/rule_registry_report.py +206 -0
- agentic_project_kit/rule_registry_validator.py +704 -0
- agentic_project_kit/rule_snapshot.py +98 -0
- agentic_project_kit/rule_source_validator.py +133 -0
- agentic_project_kit/run_summary.py +68 -0
- agentic_project_kit/run_summary_renderer.py +238 -0
- agentic_project_kit/runtime_validator.py +68 -0
- agentic_project_kit/safe_file_replace.py +35 -0
- agentic_project_kit/safe_patch.py +47 -0
- agentic_project_kit/safe_push.py +218 -0
- agentic_project_kit/safe_remove_diagnostic.py +51 -0
- agentic_project_kit/scaffold.py +69 -0
- agentic_project_kit/site_claims.py +444 -0
- agentic_project_kit/site_generator.py +1140 -0
- agentic_project_kit/slice_gate.py +225 -0
- agentic_project_kit/slice_runner.py +75 -0
- agentic_project_kit/standard_gates_audit_suite.py +167 -0
- agentic_project_kit/state_freshness.py +135 -0
- agentic_project_kit/status_current_state_audit.py +762 -0
- agentic_project_kit/successor_handoff_package.py +1763 -0
- agentic_project_kit/templates.py +292 -0
- agentic_project_kit/terminal_block_guard.py +166 -0
- agentic_project_kit/terminal_logging.py +275 -0
- agentic_project_kit/todo.py +97 -0
- agentic_project_kit/transfer_closeout.py +136 -0
- agentic_project_kit/transfer_continue.py +215 -0
- agentic_project_kit/transfer_local_runner.py +101 -0
- agentic_project_kit/transfer_operation_monitor.py +236 -0
- agentic_project_kit/transfer_post_merge_lifecycle.py +356 -0
- agentic_project_kit/transfer_post_merge_settle.py +383 -0
- agentic_project_kit/transfer_pr_actions.py +164 -0
- agentic_project_kit/transfer_remote_next.py +959 -0
- agentic_project_kit/transfer_repo_actions.py +2253 -0
- agentic_project_kit/transfer_runner.py +347 -0
- agentic_project_kit/transfer_safety_context.py +291 -0
- agentic_project_kit/transfer_state.py +483 -0
- agentic_project_kit/transfer_uplink.py +471 -0
- agentic_project_kit/transfer_workflow_next.py +148 -0
- agentic_project_kit/typed_work_order_evidence.py +58 -0
- agentic_project_kit/typed_work_order_queue.py +166 -0
- agentic_project_kit/typed_work_order_runner.py +168 -0
- agentic_project_kit/work_cycle.py +317 -0
- agentic_project_kit/work_discard_changes.py +129 -0
- agentic_project_kit/work_order_patch.py +199 -0
- agentic_project_kit/work_order_runner.py +120 -0
- agentic_project_kit/work_order_uploader.py +220 -0
- agentic_project_kit/work_order_validator.py +185 -0
- agentic_project_kit/work_orders.py +246 -0
- agentic_project_kit/workflow_guard.py +262 -0
- agentic_project_kit/workflow_summary_runner.py +124 -0
- agentic_project_kit/workspace.py +665 -0
- agentic_project_kit/workspace_adopt.py +471 -0
- agentic_project_kit/workspace_dpa_intake.py +406 -0
- agentic_project_kit/workspace_init.py +487 -0
- agentic_project_kit/workspace_lock.py +129 -0
- agentic_project_kit/workspace_remove.py +381 -0
- agentic_project_kit/workspace_upgrade.py +324 -0
- agentic_project_kit/wrapper_live_status.py +104 -0
- agentic_project_kit-1.0.1.dist-info/METADATA +724 -0
- agentic_project_kit-1.0.1.dist-info/RECORD +310 -0
- agentic_project_kit-1.0.1.dist-info/WHEEL +4 -0
- agentic_project_kit-1.0.1.dist-info/entry_points.txt +3 -0
- agentic_project_kit-1.0.1.dist-info/licenses/LICENSE +10 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from agentic_project_kit.rule_source_validator import (
|
|
9
|
+
RuleSourceValidationResult,
|
|
10
|
+
validate_rule_sources,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class RuleSourceDigest:
|
|
16
|
+
path: str
|
|
17
|
+
sha256: str
|
|
18
|
+
size_bytes: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class DerivedRuleSnapshot:
|
|
23
|
+
schema_version: int
|
|
24
|
+
snapshot_id: str
|
|
25
|
+
sources_total: int
|
|
26
|
+
source_digests: tuple[RuleSourceDigest, ...]
|
|
27
|
+
validation: RuleSourceValidationResult
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def is_valid(self) -> bool:
|
|
31
|
+
return self.validation.is_valid
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def fail_closed(self) -> bool:
|
|
35
|
+
return self.validation.fail_closed
|
|
36
|
+
|
|
37
|
+
def as_json_data(self) -> dict[str, object]:
|
|
38
|
+
return {
|
|
39
|
+
"schema_version": self.schema_version,
|
|
40
|
+
"snapshot_id": self.snapshot_id,
|
|
41
|
+
"sources_total": self.sources_total,
|
|
42
|
+
"is_valid": self.is_valid,
|
|
43
|
+
"fail_closed": self.fail_closed,
|
|
44
|
+
"source_digests": [
|
|
45
|
+
{
|
|
46
|
+
"path": digest.path,
|
|
47
|
+
"sha256": digest.sha256,
|
|
48
|
+
"size_bytes": digest.size_bytes,
|
|
49
|
+
}
|
|
50
|
+
for digest in self.source_digests
|
|
51
|
+
],
|
|
52
|
+
"validation": self.validation.as_json_data(),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _file_digest(path: Path) -> tuple[str, int]:
|
|
57
|
+
data = path.read_bytes()
|
|
58
|
+
return hashlib.sha256(data).hexdigest(), len(data)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _snapshot_id(
|
|
62
|
+
source_digests: tuple[RuleSourceDigest, ...], validation: RuleSourceValidationResult
|
|
63
|
+
) -> str:
|
|
64
|
+
payload = {
|
|
65
|
+
"schema_version": 1,
|
|
66
|
+
"source_digests": [
|
|
67
|
+
{
|
|
68
|
+
"path": digest.path,
|
|
69
|
+
"sha256": digest.sha256,
|
|
70
|
+
"size_bytes": digest.size_bytes,
|
|
71
|
+
}
|
|
72
|
+
for digest in source_digests
|
|
73
|
+
],
|
|
74
|
+
"blocking_reasons": list(validation.blocking_reasons),
|
|
75
|
+
}
|
|
76
|
+
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
77
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_derived_rule_snapshot(root: str | Path = ".") -> DerivedRuleSnapshot:
|
|
81
|
+
root_path = Path(root)
|
|
82
|
+
validation = validate_rule_sources(root_path)
|
|
83
|
+
|
|
84
|
+
digests: list[RuleSourceDigest] = []
|
|
85
|
+
for source in validation.source_paths:
|
|
86
|
+
path = root_path / source
|
|
87
|
+
if path.exists() and path.is_file():
|
|
88
|
+
digest, size_bytes = _file_digest(path)
|
|
89
|
+
digests.append(RuleSourceDigest(path=source, sha256=digest, size_bytes=size_bytes))
|
|
90
|
+
|
|
91
|
+
source_digests = tuple(digests)
|
|
92
|
+
return DerivedRuleSnapshot(
|
|
93
|
+
schema_version=1,
|
|
94
|
+
snapshot_id=_snapshot_id(source_digests, validation),
|
|
95
|
+
sources_total=validation.sources_total,
|
|
96
|
+
source_digests=source_digests,
|
|
97
|
+
validation=validation,
|
|
98
|
+
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from agentic_project_kit.chat_bootloader import MANDATORY_BOOT_SOURCES
|
|
9
|
+
from agentic_project_kit.handoff_prompt import MANDATORY_SUCCESSOR_CHAT_SOURCES
|
|
10
|
+
from agentic_project_kit.handoff_state import load_handoff_state, validate_handoff_state
|
|
11
|
+
from agentic_project_kit.rule_refresh import COMMUNICATION_RULE_SOURCES, HANDOFF_RULE_SOURCES
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
CANONICAL_YAML_RULE_SOURCES = (
|
|
15
|
+
".agentic/compiled_agent_context.yaml",
|
|
16
|
+
".agentic/handoff_state.yaml",
|
|
17
|
+
".agentic/rule_mechanism_inventory.yaml",
|
|
18
|
+
".agentic/rule_migrations.yaml",
|
|
19
|
+
".agentic/rule_preservation.yaml",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class RuleSourceValidationResult:
|
|
25
|
+
source_paths: tuple[str, ...]
|
|
26
|
+
missing_required_paths: tuple[str, ...]
|
|
27
|
+
yaml_parse_error_paths: tuple[str, ...]
|
|
28
|
+
handoff_state_errors: tuple[str, ...]
|
|
29
|
+
blocking_reasons: tuple[str, ...]
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def sources_total(self) -> int:
|
|
33
|
+
return len(self.source_paths)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def is_valid(self) -> bool:
|
|
37
|
+
return not self.blocking_reasons
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def fail_closed(self) -> bool:
|
|
41
|
+
return bool(self.blocking_reasons)
|
|
42
|
+
|
|
43
|
+
def as_json_data(self) -> dict[str, object]:
|
|
44
|
+
return {
|
|
45
|
+
"schema_version": 1,
|
|
46
|
+
"sources_total": self.sources_total,
|
|
47
|
+
"is_valid": self.is_valid,
|
|
48
|
+
"fail_closed": self.fail_closed,
|
|
49
|
+
"source_paths": list(self.source_paths),
|
|
50
|
+
"missing_required_paths": list(self.missing_required_paths),
|
|
51
|
+
"yaml_parse_error_paths": list(self.yaml_parse_error_paths),
|
|
52
|
+
"handoff_state_errors": list(self.handoff_state_errors),
|
|
53
|
+
"blocking_reasons": list(self.blocking_reasons),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def canonical_rule_source_paths() -> tuple[str, ...]:
|
|
58
|
+
"""Return the de-duplicated canonical rule-source set used for read-only validation."""
|
|
59
|
+
|
|
60
|
+
ordered: list[str] = []
|
|
61
|
+
for collection in (
|
|
62
|
+
MANDATORY_BOOT_SOURCES,
|
|
63
|
+
tuple(MANDATORY_SUCCESSOR_CHAT_SOURCES),
|
|
64
|
+
COMMUNICATION_RULE_SOURCES,
|
|
65
|
+
HANDOFF_RULE_SOURCES,
|
|
66
|
+
CANONICAL_YAML_RULE_SOURCES,
|
|
67
|
+
):
|
|
68
|
+
for source in collection:
|
|
69
|
+
if source == "relevant source files and tests for the requested slice":
|
|
70
|
+
continue
|
|
71
|
+
if source not in ordered:
|
|
72
|
+
ordered.append(source)
|
|
73
|
+
return tuple(ordered)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def validate_rule_sources(root: str | Path = ".") -> RuleSourceValidationResult:
|
|
77
|
+
"""Validate canonical rule sources without writing files or generating snapshots."""
|
|
78
|
+
|
|
79
|
+
root_path = Path(root)
|
|
80
|
+
source_paths = canonical_rule_source_paths()
|
|
81
|
+
missing_required_paths: list[str] = []
|
|
82
|
+
yaml_parse_error_paths: list[str] = []
|
|
83
|
+
handoff_state_errors: list[str] = []
|
|
84
|
+
blocking_reasons: list[str] = []
|
|
85
|
+
|
|
86
|
+
for source in source_paths:
|
|
87
|
+
path = root_path / source
|
|
88
|
+
if not path.exists() or not path.is_file():
|
|
89
|
+
missing_required_paths.append(source)
|
|
90
|
+
blocking_reasons.append(f"missing required rule source: {source}")
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
if path.suffix in {".yaml", ".yml"}:
|
|
94
|
+
try:
|
|
95
|
+
yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
96
|
+
except yaml.YAMLError as exc:
|
|
97
|
+
yaml_parse_error_paths.append(source)
|
|
98
|
+
blocking_reasons.append(f"yaml parse error in rule source: {source}: {exc}")
|
|
99
|
+
|
|
100
|
+
handoff_path = root_path / ".agentic/handoff_state.yaml"
|
|
101
|
+
if handoff_path.exists() and handoff_path.is_file():
|
|
102
|
+
try:
|
|
103
|
+
handoff_state_errors.extend(validate_handoff_state(load_handoff_state(handoff_path)))
|
|
104
|
+
except (OSError, ValueError, yaml.YAMLError) as exc:
|
|
105
|
+
handoff_state_errors.append(str(exc))
|
|
106
|
+
for error in handoff_state_errors:
|
|
107
|
+
blocking_reasons.append(f"handoff_state invalid: {error}")
|
|
108
|
+
|
|
109
|
+
return RuleSourceValidationResult(
|
|
110
|
+
source_paths=source_paths,
|
|
111
|
+
missing_required_paths=tuple(missing_required_paths),
|
|
112
|
+
yaml_parse_error_paths=tuple(yaml_parse_error_paths),
|
|
113
|
+
handoff_state_errors=tuple(handoff_state_errors),
|
|
114
|
+
blocking_reasons=tuple(blocking_reasons),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def render_rule_source_validation(result: RuleSourceValidationResult) -> str:
|
|
119
|
+
lines = [
|
|
120
|
+
"RULE_SOURCE_VALIDATION",
|
|
121
|
+
f"sources_total={result.sources_total}",
|
|
122
|
+
f"is_valid={result.is_valid}",
|
|
123
|
+
f"fail_closed={result.fail_closed}",
|
|
124
|
+
]
|
|
125
|
+
for path in result.missing_required_paths:
|
|
126
|
+
lines.append(f"missing_required_path={path}")
|
|
127
|
+
for path in result.yaml_parse_error_paths:
|
|
128
|
+
lines.append(f"yaml_parse_error_path={path}")
|
|
129
|
+
for error in result.handoff_state_errors:
|
|
130
|
+
lines.append(f"handoff_state_error={error}")
|
|
131
|
+
for reason in result.blocking_reasons:
|
|
132
|
+
lines.append(f"blocking_reason={reason}")
|
|
133
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class WorkResult(StrEnum):
|
|
8
|
+
PASS = "PASS"
|
|
9
|
+
FAIL = "FAIL"
|
|
10
|
+
PENDING = "PENDING"
|
|
11
|
+
HARD_FAIL = "HARD-FAIL"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EvidenceResult(StrEnum):
|
|
15
|
+
PASS = "PASS"
|
|
16
|
+
FAIL = "FAIL"
|
|
17
|
+
PARTIAL = "PARTIAL"
|
|
18
|
+
CHAT_ONLY = "CHAT_ONLY"
|
|
19
|
+
NOT_REQUIRED = "NOT_REQUIRED"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RemoteEvidence(StrEnum):
|
|
23
|
+
PASS = "PASS"
|
|
24
|
+
FAIL = "FAIL"
|
|
25
|
+
PARTIAL = "PARTIAL"
|
|
26
|
+
NOT_REQUIRED = "NOT_REQUIRED"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NextChatReply(StrEnum):
|
|
30
|
+
D = "d"
|
|
31
|
+
F = "f"
|
|
32
|
+
PASTE_OUTPUT = "paste-output"
|
|
33
|
+
CONTINUE = "continue"
|
|
34
|
+
STOP = "stop"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class RunSummary:
|
|
39
|
+
work_result: WorkResult
|
|
40
|
+
evidence_result: EvidenceResult
|
|
41
|
+
overall_result: WorkResult
|
|
42
|
+
remote_evidence: RemoteEvidence
|
|
43
|
+
terminal_log: str = "NONE"
|
|
44
|
+
command_report: str = "NONE"
|
|
45
|
+
next_chat_reply: NextChatReply = NextChatReply.D
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def render_run_summary(summary: RunSummary) -> str:
|
|
49
|
+
lines = [
|
|
50
|
+
"================================================================",
|
|
51
|
+
"SUMMARY",
|
|
52
|
+
f"WORK RESULT: {summary.work_result.value}",
|
|
53
|
+
f"EVIDENCE RESULT: {summary.evidence_result.value}",
|
|
54
|
+
f"OVERALL RESULT: {summary.overall_result.value}",
|
|
55
|
+
f"REMOTE_EVIDENCE: {summary.remote_evidence.value}",
|
|
56
|
+
f"terminal_log={summary.terminal_log}",
|
|
57
|
+
f"command_report={summary.command_report}",
|
|
58
|
+
f"NEXT_CHAT_REPLY: {summary.next_chat_reply.value}",
|
|
59
|
+
f"### RESULT: {summary.overall_result.value} ###",
|
|
60
|
+
"================================================================",
|
|
61
|
+
]
|
|
62
|
+
return "\n".join(lines)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def summary_for_status(status: int, terminal_log: str = "NONE", command_report: str = "NONE", remote_evidence: RemoteEvidence = RemoteEvidence.PARTIAL) -> RunSummary:
|
|
66
|
+
if status == 0:
|
|
67
|
+
return RunSummary(WorkResult.PASS, EvidenceResult.PASS, WorkResult.PASS, remote_evidence, terminal_log, command_report, NextChatReply.D)
|
|
68
|
+
return RunSummary(WorkResult.FAIL, EvidenceResult.PARTIAL, WorkResult.FAIL, remote_evidence, terminal_log, command_report, NextChatReply.F)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
VALID_RESULTS = {"PASS", "FAIL", "PENDING", "HARD-FAIL", "NO-COMMAND"}
|
|
12
|
+
VALID_EVIDENCE_RESULTS = {"PASS", "FAIL", "PARTIAL", "CHAT_ONLY", "NOT_REQUIRED"}
|
|
13
|
+
VALID_REMOTE_EVIDENCE_RESULTS = {"PASS", "FAIL", "PARTIAL", "NOT_REQUIRED"}
|
|
14
|
+
VALID_CHAT_REPLIES = {"d", "f", "w", "paste-output", "stop"}
|
|
15
|
+
LEGACY_SUMMARY_TOKENS = ("WORK RESULT:", "EVIDENCE RESULT:", "OVERALL RESULT:", "NEXT_CHAT_REPLY:")
|
|
16
|
+
REQUIRED_RENDERED_TOKENS = (
|
|
17
|
+
"SUMMARY COMM-",
|
|
18
|
+
"SLICE\n",
|
|
19
|
+
"EXECUTION\n",
|
|
20
|
+
"RESULT\n",
|
|
21
|
+
"REMOTE\n",
|
|
22
|
+
"EVIDENCE FILES\n",
|
|
23
|
+
"INTERPRETATION\n",
|
|
24
|
+
"NEXT\n",
|
|
25
|
+
" WORK: ",
|
|
26
|
+
" EVIDENCE: ",
|
|
27
|
+
" OVERALL: ",
|
|
28
|
+
" REMOTE_EVIDENCE: ",
|
|
29
|
+
" terminal_log: ",
|
|
30
|
+
" command_report: ",
|
|
31
|
+
" CHAT_REPLY: ",
|
|
32
|
+
"### RESULT: ",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class SummaryPayload:
|
|
38
|
+
comm_id: str = "COMM-PENDING"
|
|
39
|
+
comm_header: str = ""
|
|
40
|
+
slice: str = ""
|
|
41
|
+
slice_name: str = ""
|
|
42
|
+
scope: str = ""
|
|
43
|
+
branch: str = "current"
|
|
44
|
+
origin: str = "local"
|
|
45
|
+
state_mode: str = "local"
|
|
46
|
+
mode_check: str = "not_recorded"
|
|
47
|
+
work: str = "PENDING"
|
|
48
|
+
evidence: str = "CHAT_ONLY"
|
|
49
|
+
overall: str = "PENDING"
|
|
50
|
+
remote_evidence: str = "NOT_REQUIRED"
|
|
51
|
+
pr: str = "NONE"
|
|
52
|
+
head_sha: str = "NONE"
|
|
53
|
+
ci: str = "NONE"
|
|
54
|
+
merge: str = "NONE"
|
|
55
|
+
terminal_log: str = "NONE"
|
|
56
|
+
terminal_log_remote: str = "NONE"
|
|
57
|
+
terminal_log_local: str = "NONE"
|
|
58
|
+
command_report: str = "NONE"
|
|
59
|
+
interpretation: str = "No interpretation recorded."
|
|
60
|
+
safe_step: str = "not_recorded"
|
|
61
|
+
next: str = ""
|
|
62
|
+
chat_reply: str = ""
|
|
63
|
+
timestamp: str = ""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
RunSummaryPayload = SummaryPayload
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _payload_from_mapping(data: dict[str, Any]) -> SummaryPayload:
|
|
70
|
+
normalized = {str(key).replace("-", "_"): value for key, value in data.items()}
|
|
71
|
+
if "next_safe_step" in normalized and "safe_step" not in normalized:
|
|
72
|
+
normalized["safe_step"] = normalized["next_safe_step"]
|
|
73
|
+
allowed = SummaryPayload.__dataclass_fields__.keys()
|
|
74
|
+
return SummaryPayload(**{key: str(normalized[key]) for key in allowed if key in normalized and normalized[key] is not None})
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _selected_chat_reply(payload: SummaryPayload) -> str:
|
|
78
|
+
return payload.chat_reply or payload.next or ""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_summary_data(data: SummaryPayload | dict[str, Any]) -> list[str]:
|
|
82
|
+
payload = data if isinstance(data, SummaryPayload) else _payload_from_mapping(data)
|
|
83
|
+
findings: list[str] = []
|
|
84
|
+
terminal_log = payload.terminal_log.strip()
|
|
85
|
+
if not terminal_log or terminal_log == "NONE":
|
|
86
|
+
findings.append("missing field: terminal_log")
|
|
87
|
+
if payload.work not in VALID_RESULTS:
|
|
88
|
+
findings.append("invalid field: work")
|
|
89
|
+
if payload.overall not in VALID_RESULTS:
|
|
90
|
+
findings.append("invalid field: overall")
|
|
91
|
+
if payload.evidence not in VALID_EVIDENCE_RESULTS:
|
|
92
|
+
findings.append("invalid field: evidence")
|
|
93
|
+
if payload.remote_evidence not in VALID_REMOTE_EVIDENCE_RESULTS:
|
|
94
|
+
findings.append("invalid field: remote_evidence")
|
|
95
|
+
next_reply = _selected_chat_reply(payload)
|
|
96
|
+
if next_reply not in VALID_CHAT_REPLIES:
|
|
97
|
+
findings.append("invalid field: chat_reply")
|
|
98
|
+
if payload.overall == "PASS" and payload.work != "PASS":
|
|
99
|
+
findings.append("invalid pass: work is not PASS")
|
|
100
|
+
if payload.overall == "PASS" and payload.evidence in {"FAIL", "PARTIAL", "CHAT_ONLY"}:
|
|
101
|
+
findings.append("invalid pass: evidence is not complete")
|
|
102
|
+
if payload.overall == "PASS" and payload.remote_evidence not in {"PASS", "NOT_REQUIRED"}:
|
|
103
|
+
findings.append("invalid pass: remote evidence is not complete")
|
|
104
|
+
if payload.overall == "PASS" and next_reply != "d":
|
|
105
|
+
findings.append("invalid pass: chat_reply must be d")
|
|
106
|
+
if payload.overall in {"FAIL", "HARD-FAIL"} and next_reply == "d":
|
|
107
|
+
findings.append("invalid failure: chat_reply must not be d")
|
|
108
|
+
if payload.overall == "PENDING" and next_reply in {"d", "f"}:
|
|
109
|
+
findings.append("invalid pending: chat_reply must be w, paste-output, or stop")
|
|
110
|
+
if payload.remote_evidence == "PASS" and not payload.terminal_log_remote.startswith("docs/reports/terminal/"):
|
|
111
|
+
findings.append("invalid remote evidence: missing remote terminal log")
|
|
112
|
+
return findings
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def validate_rendered_summary_text(text: str) -> list[str]:
|
|
116
|
+
findings: list[str] = []
|
|
117
|
+
for token in LEGACY_SUMMARY_TOKENS:
|
|
118
|
+
if token in text:
|
|
119
|
+
findings.append(f"legacy summary token: {token}")
|
|
120
|
+
for token in REQUIRED_RENDERED_TOKENS:
|
|
121
|
+
if token not in text:
|
|
122
|
+
findings.append(f"missing summary token: {token.strip()}")
|
|
123
|
+
summary_headers = [line for line in text.splitlines() if line.startswith("SUMMARY COMM-")]
|
|
124
|
+
if len(summary_headers) != 1:
|
|
125
|
+
findings.append(f"invalid summary count: {len(summary_headers)}")
|
|
126
|
+
result_values = [line.split(":", 1)[1].strip() for line in text.splitlines() if line.strip().startswith("OVERALL:")]
|
|
127
|
+
marker_values = [line.replace("### RESULT:", "").replace("###", "").strip() for line in text.splitlines() if line.startswith("### RESULT:")]
|
|
128
|
+
if len(marker_values) != 1:
|
|
129
|
+
findings.append(f"invalid result marker count: {len(marker_values)}")
|
|
130
|
+
if result_values and marker_values and result_values[-1] != marker_values[-1]:
|
|
131
|
+
findings.append("contradictory summary marker")
|
|
132
|
+
for bad in (" REMOTE_EVIDENCE: NONE", " REMOTE_EVIDENCE: CHAT_ONLY", " REMOTE_EVIDENCE: PENDING"):
|
|
133
|
+
if bad in text:
|
|
134
|
+
findings.append("invalid rendered remote evidence value")
|
|
135
|
+
return findings
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _header(payload: SummaryPayload) -> str:
|
|
139
|
+
if payload.comm_header.strip():
|
|
140
|
+
return payload.comm_header
|
|
141
|
+
timestamp = payload.timestamp or datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S %z")
|
|
142
|
+
return f"SUMMARY {payload.comm_id} | {timestamp}"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def render_summary(payload: SummaryPayload | dict[str, Any]) -> str:
|
|
146
|
+
if isinstance(payload, dict):
|
|
147
|
+
payload = _payload_from_mapping(payload)
|
|
148
|
+
findings = validate_summary_data(payload)
|
|
149
|
+
if findings:
|
|
150
|
+
raise ValueError("; ".join(findings))
|
|
151
|
+
slice_name = payload.slice or payload.slice_name or "UNKNOWN"
|
|
152
|
+
next_reply = _selected_chat_reply(payload)
|
|
153
|
+
lines = [
|
|
154
|
+
"================================================================",
|
|
155
|
+
_header(payload),
|
|
156
|
+
"",
|
|
157
|
+
"SLICE",
|
|
158
|
+
f" NAME: {slice_name}",
|
|
159
|
+
f" SCOPE: {payload.scope}",
|
|
160
|
+
f" BRANCH: {payload.branch}",
|
|
161
|
+
"",
|
|
162
|
+
"EXECUTION",
|
|
163
|
+
f" ORIGIN: {payload.origin}",
|
|
164
|
+
f" STATE_MODE: {payload.state_mode}",
|
|
165
|
+
f" MODE_CHECK: {payload.mode_check}",
|
|
166
|
+
" SWITCH_HINT: ./ns mode-write local|remote && ./ns mode-check local|remote",
|
|
167
|
+
"",
|
|
168
|
+
"RESULT",
|
|
169
|
+
f" WORK: {payload.work}",
|
|
170
|
+
f" EVIDENCE: {payload.evidence}",
|
|
171
|
+
f" OVERALL: {payload.overall}",
|
|
172
|
+
"",
|
|
173
|
+
"REMOTE",
|
|
174
|
+
f" REMOTE_EVIDENCE: {payload.remote_evidence}",
|
|
175
|
+
f" PR: {payload.pr}",
|
|
176
|
+
f" HEAD_SHA: {payload.head_sha}",
|
|
177
|
+
f" CI: {payload.ci}",
|
|
178
|
+
f" MERGE: {payload.merge}",
|
|
179
|
+
"",
|
|
180
|
+
"EVIDENCE FILES",
|
|
181
|
+
f" terminal_log: {payload.terminal_log}",
|
|
182
|
+
f" terminal_log_remote: {payload.terminal_log_remote}",
|
|
183
|
+
f" terminal_log_local: {payload.terminal_log_local}",
|
|
184
|
+
f" command_report: {payload.command_report}",
|
|
185
|
+
"",
|
|
186
|
+
"INTERPRETATION",
|
|
187
|
+
f" {payload.interpretation}",
|
|
188
|
+
"",
|
|
189
|
+
"NEXT",
|
|
190
|
+
f" SAFE_STEP: {payload.safe_step}",
|
|
191
|
+
f" CHAT_REPLY: {next_reply}",
|
|
192
|
+
"",
|
|
193
|
+
f"### RESULT: {payload.overall} ###",
|
|
194
|
+
"================================================================",
|
|
195
|
+
]
|
|
196
|
+
rendered = "\n".join(lines)
|
|
197
|
+
rendered_findings = validate_rendered_summary_text(rendered)
|
|
198
|
+
if rendered_findings:
|
|
199
|
+
raise ValueError("; ".join(rendered_findings))
|
|
200
|
+
return rendered
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
render = render_summary
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _load_json_argument(value: str) -> dict[str, Any]:
|
|
207
|
+
candidate = Path(value)
|
|
208
|
+
if candidate.exists():
|
|
209
|
+
return json.loads(candidate.read_text(encoding="utf-8"))
|
|
210
|
+
return json.loads(value)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
214
|
+
parser = argparse.ArgumentParser(prog="run-summary-renderer")
|
|
215
|
+
parser.add_argument("--json", default=None)
|
|
216
|
+
for field in SummaryPayload.__dataclass_fields__:
|
|
217
|
+
parser.add_argument("--" + field.replace("_", "-"), dest=field, default=None)
|
|
218
|
+
return parser
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def main(argv: list[str] | None = None) -> int:
|
|
222
|
+
parser = build_parser()
|
|
223
|
+
args = parser.parse_args(argv)
|
|
224
|
+
if args.json:
|
|
225
|
+
data = _load_json_argument(args.json)
|
|
226
|
+
else:
|
|
227
|
+
data = {field: getattr(args, field) for field in SummaryPayload.__dataclass_fields__ if getattr(args, field) is not None}
|
|
228
|
+
if data.get("terminal_log", "NONE") == "NONE":
|
|
229
|
+
data["terminal_log"] = data.get("terminal_log_remote") or data.get("terminal_log_local") or "NONE"
|
|
230
|
+
try:
|
|
231
|
+
print(render_summary(_payload_from_mapping(data)))
|
|
232
|
+
except ValueError as exc:
|
|
233
|
+
parser.error(str(exc))
|
|
234
|
+
return 0
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Runtime validation primitives for generated governance artifacts.
|
|
2
|
+
|
|
3
|
+
This module intentionally starts small: it provides deterministic, auditable
|
|
4
|
+
validation results without performing repair or invoking any model.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ValidationSeverity(str, Enum):
|
|
15
|
+
"""Severity for runtime validation findings."""
|
|
16
|
+
|
|
17
|
+
ERROR = "error"
|
|
18
|
+
WARNING = "warning"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ValidationFinding:
|
|
23
|
+
"""Single validation finding with stable machine-readable fields."""
|
|
24
|
+
|
|
25
|
+
code: str
|
|
26
|
+
message: str
|
|
27
|
+
severity: ValidationSeverity = ValidationSeverity.ERROR
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class ValidationReport:
|
|
32
|
+
findings: tuple[ValidationFinding, ...] = ()
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def ok(self) -> bool:
|
|
36
|
+
return not any(finding.severity is ValidationSeverity.ERROR for finding in self.findings)
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict[str, Any]:
|
|
39
|
+
"""Return a deterministic JSON-safe representation."""
|
|
40
|
+
return {
|
|
41
|
+
"ok": self.ok,
|
|
42
|
+
"findings": [
|
|
43
|
+
{
|
|
44
|
+
"severity": finding.severity.value,
|
|
45
|
+
"code": finding.code,
|
|
46
|
+
"message": finding.message,
|
|
47
|
+
}
|
|
48
|
+
for finding in self.findings
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def validate_required_sections(text: str, required_sections: tuple[str, ...]) -> ValidationReport:
|
|
53
|
+
"""Validate that required section markers are present in text.
|
|
54
|
+
|
|
55
|
+
The check is intentionally literal and deterministic. This makes it suitable
|
|
56
|
+
as a first runtime skeleton for generated output contracts and governance
|
|
57
|
+
wrapper artifacts.
|
|
58
|
+
"""
|
|
59
|
+
findings: list[ValidationFinding] = []
|
|
60
|
+
for section in required_sections:
|
|
61
|
+
if section not in text:
|
|
62
|
+
findings.append(
|
|
63
|
+
ValidationFinding(
|
|
64
|
+
code="missing_required_section",
|
|
65
|
+
message=f"Missing required section: {section}",
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
return ValidationReport(findings=tuple(findings))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import py_compile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SafeReplaceError(RuntimeError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_replacement(target: Path, replacement: Path) -> None:
|
|
13
|
+
if not replacement.exists():
|
|
14
|
+
raise SafeReplaceError(f"replacement does not exist: {replacement}")
|
|
15
|
+
if not replacement.is_file():
|
|
16
|
+
raise SafeReplaceError(f"replacement is not a file: {replacement}")
|
|
17
|
+
if replacement.is_symlink():
|
|
18
|
+
raise SafeReplaceError(f"replacement must not be a symlink: {replacement}")
|
|
19
|
+
if target.suffix == ".py":
|
|
20
|
+
try:
|
|
21
|
+
py_compile.compile(str(replacement), doraise=True)
|
|
22
|
+
except py_compile.PyCompileError as exc:
|
|
23
|
+
raise SafeReplaceError(f"replacement failed py_compile: {replacement}") from exc
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def safe_replace(target: Path | str, replacement: Path | str) -> Path:
|
|
27
|
+
target_path = Path(target)
|
|
28
|
+
replacement_path = Path(replacement)
|
|
29
|
+
if target_path.is_symlink():
|
|
30
|
+
raise SafeReplaceError(f"target must not be a symlink: {target_path}")
|
|
31
|
+
if not target_path.parent.exists():
|
|
32
|
+
raise SafeReplaceError(f"target parent does not exist: {target_path.parent}")
|
|
33
|
+
validate_replacement(target_path, replacement_path)
|
|
34
|
+
os.replace(replacement_path, target_path)
|
|
35
|
+
return target_path
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import shlex
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
FORBIDDEN_FRAGMENTS = (
|
|
9
|
+
"<<",
|
|
10
|
+
"python -c",
|
|
11
|
+
"python3 -c",
|
|
12
|
+
"${",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class SafePatchScript:
|
|
18
|
+
"""A small shell script that writes a file line by line with printf."""
|
|
19
|
+
|
|
20
|
+
target: Path
|
|
21
|
+
content: str
|
|
22
|
+
|
|
23
|
+
def render(self) -> str:
|
|
24
|
+
target = shlex.quote(str(self.target))
|
|
25
|
+
lines = ["#!/usr/bin/env sh", "set -eu", f": > {target}"]
|
|
26
|
+
for line in self.content.splitlines():
|
|
27
|
+
quoted = shlex.quote(line)
|
|
28
|
+
lines.append(f"printf '%s\\n' {quoted} >> {target}")
|
|
29
|
+
return "\n".join(lines) + "\n"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def render_safe_file_writer(target: str | Path, content: str) -> str:
|
|
33
|
+
"""Render a quoting-safe shell script for deterministic file replacement."""
|
|
34
|
+
|
|
35
|
+
script = SafePatchScript(Path(target), content).render()
|
|
36
|
+
validate_safe_patch_script(script)
|
|
37
|
+
return script
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_safe_patch_script(script: str) -> None:
|
|
41
|
+
"""Reject recurring unsafe patch-script patterns before they reach the terminal."""
|
|
42
|
+
|
|
43
|
+
for fragment in FORBIDDEN_FRAGMENTS:
|
|
44
|
+
if fragment in script:
|
|
45
|
+
raise ValueError(f"unsafe patch script fragment detected: {fragment}")
|
|
46
|
+
if "cat >" in script or "cat << " in script:
|
|
47
|
+
raise ValueError("unsafe cat-based patch script detected")
|