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,743 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agentic_project_kit.dpa_dp5_strict_gate import evaluate_dp5_strict_gate
|
|
10
|
+
from agentic_project_kit.dpa_final_closeout import (
|
|
11
|
+
DEFAULT_DPA_FINAL_CLOSEOUT_RECORD_PATH,
|
|
12
|
+
VALID_STATUS as VALID_FINAL_CLOSEOUT_STATUS,
|
|
13
|
+
evaluate_dpa_final_closeout_record,
|
|
14
|
+
)
|
|
15
|
+
from agentic_project_kit.dpa_post_dp2_scope_assessment import (
|
|
16
|
+
evaluate_post_dp2_scope_assessment,
|
|
17
|
+
)
|
|
18
|
+
from agentic_project_kit.dpa_probe_002_readiness import (
|
|
19
|
+
evaluate_probe_002_lifecycle_readiness,
|
|
20
|
+
)
|
|
21
|
+
from agentic_project_kit.dpa_probe_003_readiness import (
|
|
22
|
+
evaluate_probe_003_workflow_readiness,
|
|
23
|
+
)
|
|
24
|
+
from agentic_project_kit.dpa_probe_004_readiness import (
|
|
25
|
+
evaluate_probe_004_migration_readiness,
|
|
26
|
+
)
|
|
27
|
+
from agentic_project_kit.dpa_readiness import evaluate_dpa_readiness
|
|
28
|
+
from agentic_project_kit.dpa_renderer_readiness import evaluate_renderer_probe_readiness
|
|
29
|
+
from agentic_project_kit.workspace import load_workspace
|
|
30
|
+
|
|
31
|
+
DEFAULT_DPA_STABLE_PROMOTION_RECORD_PATH = Path(
|
|
32
|
+
"docs/architecture/evidence/dpa/assessment/DPA_STABLE_PROMOTION_RECORD_20260801.json"
|
|
33
|
+
)
|
|
34
|
+
DPA_STABLE_PROMOTION_MODEL = "dpa-stable-promotion-v1"
|
|
35
|
+
READY_FOR_STABLE_PROMOTION_STATUS = "READY_FOR_STABLE_DPA_PROMOTION"
|
|
36
|
+
VALID_STABLE_PROMOTION_STATUS = "VALID_DPA_STABLE_PROMOTION_RECORD"
|
|
37
|
+
BLOCKED_STATUS = "BLOCKED_FOR_STABLE_DPA_PROMOTION"
|
|
38
|
+
INVALID_STATUS = "INVALID_DPA_STABLE_PROMOTION_RECORD"
|
|
39
|
+
PROMOTION_RECORD_STATUS = "DPA_STABLE_PROMOTION_RECORDED"
|
|
40
|
+
PROMOTION_DECISION_TOKEN = "DPA_STABLE_PROMOTION_AUTHORIZED"
|
|
41
|
+
STABLE_SCOPE_ID = "DPA_STABLE_ACCEPTED_KIT_SCOPE"
|
|
42
|
+
STABLE_CLAIM_SCOPE = "accepted-kit-dpa-dp1-dp5-stable-implementation-scope"
|
|
43
|
+
EXTERNAL_REPO_STATUS = "DPA_CAPABLE_WITH_FRESH_PER_REPO_ASSESSMENT"
|
|
44
|
+
EVIDENCE_OUTPUT_ROOT_PARTS = ("evidence", "dpa", "assessment")
|
|
45
|
+
|
|
46
|
+
SPEC_FILES: tuple[tuple[str, str], ...] = (
|
|
47
|
+
("DPA-000", "docs/architecture/dpa/specs/DPA-000-VISION.md"),
|
|
48
|
+
("DPA-100", "docs/architecture/dpa/specs/DPA-100-FOUNDATIONS.md"),
|
|
49
|
+
("DPA-200", "docs/architecture/dpa/specs/DPA-200-DOCUMENT-MODEL.md"),
|
|
50
|
+
("DPA-200-FORM-MATRIX", "docs/architecture/dpa/specs/DPA-200-DOCUMENT-FORM-MATRIX.md"),
|
|
51
|
+
("DPA-300", "docs/architecture/dpa/specs/DPA-300-REGISTRY-LIFECYCLE-INTEGRATION.md"),
|
|
52
|
+
("DPA-400", "docs/architecture/dpa/specs/DPA-400-RENDERER-CONTRACT.md"),
|
|
53
|
+
("DPA-500", "docs/architecture/dpa/specs/DPA-500-FRESHNESS-AND-GATES.md"),
|
|
54
|
+
("DPA-600", "docs/architecture/dpa/specs/DPA-600-CONCURRENCY.md"),
|
|
55
|
+
("DPA-700", "docs/architecture/dpa/specs/DPA-700-MIGRATION.md"),
|
|
56
|
+
("DPA-800", "docs/architecture/dpa/specs/DPA-800-DP1-DP5.md"),
|
|
57
|
+
("DPA-900", "docs/architecture/dpa/specs/DPA-900-FUTURE.md"),
|
|
58
|
+
)
|
|
59
|
+
BASELINE_STABLE_SPEC_IDS = frozenset({"DPA-000", "DPA-100"})
|
|
60
|
+
PROMOTED_SPEC_IDS = tuple(spec_id for spec_id, _path in SPEC_FILES)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class DpaStableReadinessFinding:
|
|
65
|
+
code: str
|
|
66
|
+
message: str
|
|
67
|
+
path: str
|
|
68
|
+
|
|
69
|
+
def as_dict(self) -> dict[str, str]:
|
|
70
|
+
return {"code": self.code, "message": self.message, "path": self.path}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class DpaSpecStatus:
|
|
75
|
+
spec_id: str
|
|
76
|
+
path: str
|
|
77
|
+
status: str
|
|
78
|
+
present: bool
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def stable(self) -> bool:
|
|
82
|
+
return self.present and self.status == "stable"
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def promotable(self) -> bool:
|
|
86
|
+
return self.present and self.status in {"review-ready", "stable"}
|
|
87
|
+
|
|
88
|
+
def as_dict(self) -> dict[str, Any]:
|
|
89
|
+
return {
|
|
90
|
+
"id": self.spec_id,
|
|
91
|
+
"path": self.path,
|
|
92
|
+
"status": self.status,
|
|
93
|
+
"present": self.present,
|
|
94
|
+
"stable": self.stable,
|
|
95
|
+
"promotable": self.promotable,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class DpaStableReadinessResult:
|
|
101
|
+
root: str
|
|
102
|
+
current_validation_ref: str
|
|
103
|
+
promotion_record_path: str
|
|
104
|
+
promotion_record_present: bool
|
|
105
|
+
record_validation_ref: str
|
|
106
|
+
record_status: str
|
|
107
|
+
decision_token: str
|
|
108
|
+
dp2_status: str
|
|
109
|
+
dp2_implementation_percent: int
|
|
110
|
+
post_dp2_status: str
|
|
111
|
+
strict_gate_status: str
|
|
112
|
+
final_closeout_status: str
|
|
113
|
+
kit_wide_dpa_conformance_claimed: bool
|
|
114
|
+
stable_dpa_claimed: bool
|
|
115
|
+
external_repo_conformance_claimed: bool
|
|
116
|
+
probe_statuses: dict[str, str]
|
|
117
|
+
spec_statuses: tuple[DpaSpecStatus, ...]
|
|
118
|
+
findings: tuple[DpaStableReadinessFinding, ...]
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def ready_for_promotion(self) -> bool:
|
|
122
|
+
return not self.promotion_record_present and not self.findings
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def stable_promoted(self) -> bool:
|
|
126
|
+
return self.promotion_record_present and not self.findings
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def ok(self) -> bool:
|
|
130
|
+
return self.ready_for_promotion or self.stable_promoted
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def result_status(self) -> str:
|
|
134
|
+
if self.promotion_record_present:
|
|
135
|
+
return VALID_STABLE_PROMOTION_STATUS if not self.findings else INVALID_STATUS
|
|
136
|
+
return READY_FOR_STABLE_PROMOTION_STATUS if not self.findings else BLOCKED_STATUS
|
|
137
|
+
|
|
138
|
+
def as_dict(self) -> dict[str, Any]:
|
|
139
|
+
return {
|
|
140
|
+
"schema_version": 1,
|
|
141
|
+
"kind": "dpa_stable_readiness_check",
|
|
142
|
+
"promotion_model": DPA_STABLE_PROMOTION_MODEL,
|
|
143
|
+
"result_status": self.result_status,
|
|
144
|
+
"current_validation_ref": self.current_validation_ref,
|
|
145
|
+
"promotion_record_path": self.promotion_record_path,
|
|
146
|
+
"promotion_record_present": self.promotion_record_present,
|
|
147
|
+
"record_validation_ref": self.record_validation_ref,
|
|
148
|
+
"record_status": self.record_status,
|
|
149
|
+
"decision_token": self.decision_token,
|
|
150
|
+
"dp2_status": self.dp2_status,
|
|
151
|
+
"dp2_implementation_percent": self.dp2_implementation_percent,
|
|
152
|
+
"post_dp2_status": self.post_dp2_status,
|
|
153
|
+
"strict_gate_status": self.strict_gate_status,
|
|
154
|
+
"final_closeout_status": self.final_closeout_status,
|
|
155
|
+
"probe_statuses": self.probe_statuses,
|
|
156
|
+
"spec_statuses": [item.as_dict() for item in self.spec_statuses],
|
|
157
|
+
"finding_count": len(self.findings),
|
|
158
|
+
"findings": [finding.as_dict() for finding in self.findings],
|
|
159
|
+
"foreign_repo_management": {
|
|
160
|
+
"status": EXTERNAL_REPO_STATUS,
|
|
161
|
+
"automatic_external_repo_conformance_claimed": (
|
|
162
|
+
self.external_repo_conformance_claimed
|
|
163
|
+
),
|
|
164
|
+
"requires_fresh_per_repo_inventory": True,
|
|
165
|
+
"requires_fresh_per_repo_dpa_600_700_evidence": True,
|
|
166
|
+
"requires_maintainer_authorized_scope": True,
|
|
167
|
+
},
|
|
168
|
+
"claims": {
|
|
169
|
+
"kit_wide_dpa_conformance_claimed": self.kit_wide_dpa_conformance_claimed,
|
|
170
|
+
"stable_dpa_claimed": self.stable_dpa_claimed,
|
|
171
|
+
"production_mutation_performed": False,
|
|
172
|
+
"generated_outputs_manually_patched": False,
|
|
173
|
+
"external_repo_conformance_claimed": self.external_repo_conformance_claimed,
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def evaluate_dpa_stable_readiness(
|
|
179
|
+
root: Path | str = ".",
|
|
180
|
+
*,
|
|
181
|
+
record_path: Path | str = DEFAULT_DPA_STABLE_PROMOTION_RECORD_PATH,
|
|
182
|
+
validation_ref: str | None = None,
|
|
183
|
+
) -> DpaStableReadinessResult:
|
|
184
|
+
base = Path(root).resolve()
|
|
185
|
+
current_ref = validation_ref or _git_head(base)
|
|
186
|
+
promotion_path = _resolve_under_root(base, record_path)
|
|
187
|
+
display_path = _display_path(promotion_path, base)
|
|
188
|
+
spec_statuses = tuple(_spec_status(base, spec_id, path) for spec_id, path in SPEC_FILES)
|
|
189
|
+
findings: list[DpaStableReadinessFinding] = []
|
|
190
|
+
|
|
191
|
+
_validate_spec_prerequisites(spec_statuses, findings)
|
|
192
|
+
|
|
193
|
+
dp2 = evaluate_dpa_readiness(base)
|
|
194
|
+
if not dp2.dp2_ready:
|
|
195
|
+
_finding(findings, "dp2-not-authorized", "DP2 readiness must remain authorized", dp2.path)
|
|
196
|
+
if dp2.implementation_percent != 100:
|
|
197
|
+
_finding(
|
|
198
|
+
findings,
|
|
199
|
+
"dp2-percent-not-100",
|
|
200
|
+
"DP2 selected-scope implementation percent must remain 100",
|
|
201
|
+
dp2.path,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
post_dp2 = evaluate_post_dp2_scope_assessment(base, validation_ref=current_ref)
|
|
205
|
+
if not post_dp2.final_closeout_ready or post_dp2.blocker_count != 0:
|
|
206
|
+
_finding(
|
|
207
|
+
findings,
|
|
208
|
+
"post-dp2-not-closeout-ready",
|
|
209
|
+
"Post-DP2 DP3-DP5 assessment must remain ready for final closeout",
|
|
210
|
+
post_dp2.readiness_record,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
strict_gate = evaluate_dp5_strict_gate(base, validation_ref=current_ref)
|
|
214
|
+
if not strict_gate.ok:
|
|
215
|
+
_finding(
|
|
216
|
+
findings,
|
|
217
|
+
"dp5-strict-gate-not-pass",
|
|
218
|
+
"DP5 strict gate must pass with zero configured blockers",
|
|
219
|
+
display_path,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
final_closeout = evaluate_dpa_final_closeout_record(base, validation_ref=current_ref)
|
|
223
|
+
if not final_closeout.ok:
|
|
224
|
+
_finding(
|
|
225
|
+
findings,
|
|
226
|
+
"final-closeout-invalid",
|
|
227
|
+
"Final DP1-DP5 closeout record must be structurally valid",
|
|
228
|
+
final_closeout.record_path,
|
|
229
|
+
)
|
|
230
|
+
if final_closeout.kit_wide_dpa_conformance_claimed is not True:
|
|
231
|
+
_finding(
|
|
232
|
+
findings,
|
|
233
|
+
"kit-wide-conformance-not-claimed-by-closeout",
|
|
234
|
+
"Final closeout must own the bounded Kit-wide DPA conformance claim",
|
|
235
|
+
final_closeout.record_path,
|
|
236
|
+
)
|
|
237
|
+
if final_closeout.stable_dpa_claimed is not False:
|
|
238
|
+
_finding(
|
|
239
|
+
findings,
|
|
240
|
+
"final-closeout-overclaims-stable",
|
|
241
|
+
"Final closeout must leave stable DPA to the Stable Promotion record",
|
|
242
|
+
final_closeout.record_path,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
probe_statuses = _probe_statuses(base, current_ref, findings)
|
|
246
|
+
|
|
247
|
+
record_data: dict[str, Any] = {}
|
|
248
|
+
if promotion_path.exists():
|
|
249
|
+
record_data = _load_promotion_record(promotion_path, base, findings)
|
|
250
|
+
if record_data:
|
|
251
|
+
_validate_promotion_record(record_data, base, display_path, spec_statuses, findings)
|
|
252
|
+
|
|
253
|
+
claims = _mapping(record_data.get("claims"))
|
|
254
|
+
external_scope = _mapping(record_data.get("external_repo_management"))
|
|
255
|
+
|
|
256
|
+
return DpaStableReadinessResult(
|
|
257
|
+
root=base.as_posix(),
|
|
258
|
+
current_validation_ref=current_ref,
|
|
259
|
+
promotion_record_path=display_path,
|
|
260
|
+
promotion_record_present=promotion_path.exists(),
|
|
261
|
+
record_validation_ref=str(record_data.get("validation_ref", "")),
|
|
262
|
+
record_status=str(record_data.get("status", "")),
|
|
263
|
+
decision_token=str(record_data.get("decision_token", "")),
|
|
264
|
+
dp2_status=dp2.status,
|
|
265
|
+
dp2_implementation_percent=dp2.implementation_percent,
|
|
266
|
+
post_dp2_status=post_dp2.kit_wide_dpa_status,
|
|
267
|
+
strict_gate_status=strict_gate.result_status,
|
|
268
|
+
final_closeout_status=final_closeout.result_status,
|
|
269
|
+
kit_wide_dpa_conformance_claimed=claims.get("kit_wide_dpa_conformance_claimed") is True,
|
|
270
|
+
stable_dpa_claimed=claims.get("stable_dpa_claimed") is True,
|
|
271
|
+
external_repo_conformance_claimed=(
|
|
272
|
+
claims.get("external_repo_conformance_claimed") is True
|
|
273
|
+
or external_scope.get("automatic_external_repo_conformance_claimed") is True
|
|
274
|
+
),
|
|
275
|
+
probe_statuses=probe_statuses,
|
|
276
|
+
spec_statuses=spec_statuses,
|
|
277
|
+
findings=tuple(findings),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def render_dpa_stable_readiness(result: DpaStableReadinessResult) -> str:
|
|
282
|
+
payload = result.as_dict()
|
|
283
|
+
lines = [
|
|
284
|
+
"DPA_STABLE_READINESS_CHECK",
|
|
285
|
+
f"STATUS={payload['result_status']}",
|
|
286
|
+
f"CURRENT_VALIDATION_REF={payload['current_validation_ref']}",
|
|
287
|
+
f"PROMOTION_RECORD={payload['promotion_record_path']}",
|
|
288
|
+
f"PROMOTION_RECORD_PRESENT={str(payload['promotion_record_present']).lower()}",
|
|
289
|
+
f"RECORD_VALIDATION_REF={payload['record_validation_ref']}",
|
|
290
|
+
f"RECORD_STATUS={payload['record_status']}",
|
|
291
|
+
f"DECISION_TOKEN={payload['decision_token']}",
|
|
292
|
+
f"DP2_STATUS={payload['dp2_status']}",
|
|
293
|
+
f"DP2_IMPLEMENTATION_PERCENT={payload['dp2_implementation_percent']}",
|
|
294
|
+
f"POST_DP2_STATUS={payload['post_dp2_status']}",
|
|
295
|
+
f"STRICT_GATE_STATUS={payload['strict_gate_status']}",
|
|
296
|
+
f"FINAL_CLOSEOUT_STATUS={payload['final_closeout_status']}",
|
|
297
|
+
f"KIT_WIDE_DPA_CONFORMANCE_CLAIMED={str(payload['claims']['kit_wide_dpa_conformance_claimed']).lower()}",
|
|
298
|
+
f"STABLE_DPA_CLAIMED={str(payload['claims']['stable_dpa_claimed']).lower()}",
|
|
299
|
+
f"EXTERNAL_REPO_CONFORMANCE_CLAIMED={str(payload['claims']['external_repo_conformance_claimed']).lower()}",
|
|
300
|
+
f"FINDINGS={payload['finding_count']}",
|
|
301
|
+
]
|
|
302
|
+
for spec in payload["spec_statuses"]:
|
|
303
|
+
lines.append(f"SPEC={spec['id']}|status={spec['status']}|stable={str(spec['stable']).lower()}")
|
|
304
|
+
for probe_id, status in payload["probe_statuses"].items():
|
|
305
|
+
lines.append(f"PROBE={probe_id}|status={status}")
|
|
306
|
+
for finding in result.findings:
|
|
307
|
+
lines.append(f"FINDING={finding.code}|path={finding.path}|{finding.message}")
|
|
308
|
+
return "\n".join(lines) + "\n"
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def write_dpa_stable_readiness_json(
|
|
312
|
+
result: DpaStableReadinessResult,
|
|
313
|
+
root: Path | str,
|
|
314
|
+
output: Path | str,
|
|
315
|
+
*,
|
|
316
|
+
execute: bool,
|
|
317
|
+
) -> dict[str, Any]:
|
|
318
|
+
base = Path(root).resolve()
|
|
319
|
+
output_path = _resolve_under_root(base, output)
|
|
320
|
+
relative = output_path.relative_to(base)
|
|
321
|
+
evidence_root = _evidence_output_root(base)
|
|
322
|
+
if evidence_root not in (output_path, *output_path.parents):
|
|
323
|
+
return {
|
|
324
|
+
"result_status": "BLOCK",
|
|
325
|
+
"reason": "output_outside_dpa_assessment_evidence_root",
|
|
326
|
+
"output_path": relative.as_posix(),
|
|
327
|
+
"written": False,
|
|
328
|
+
}
|
|
329
|
+
rendered = json.dumps(result.as_dict(), indent=2, sort_keys=True) + "\n"
|
|
330
|
+
changed = True
|
|
331
|
+
if output_path.exists():
|
|
332
|
+
changed = output_path.read_text(encoding="utf-8") != rendered
|
|
333
|
+
if execute:
|
|
334
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
335
|
+
output_path.write_text(rendered, encoding="utf-8")
|
|
336
|
+
return {
|
|
337
|
+
"result_status": "PASS",
|
|
338
|
+
"output_path": relative.as_posix(),
|
|
339
|
+
"changed": changed,
|
|
340
|
+
"written": bool(execute),
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _validate_spec_prerequisites(
|
|
345
|
+
specs: tuple[DpaSpecStatus, ...],
|
|
346
|
+
findings: list[DpaStableReadinessFinding],
|
|
347
|
+
) -> None:
|
|
348
|
+
for spec in specs:
|
|
349
|
+
if not spec.present:
|
|
350
|
+
_finding(findings, "spec-missing", f"{spec.spec_id} spec file is missing", spec.path)
|
|
351
|
+
continue
|
|
352
|
+
if spec.spec_id in BASELINE_STABLE_SPEC_IDS and not spec.stable:
|
|
353
|
+
_finding(
|
|
354
|
+
findings,
|
|
355
|
+
"baseline-spec-not-stable",
|
|
356
|
+
f"{spec.spec_id} must already be stable before DPA stable promotion",
|
|
357
|
+
spec.path,
|
|
358
|
+
)
|
|
359
|
+
elif not spec.promotable:
|
|
360
|
+
_finding(
|
|
361
|
+
findings,
|
|
362
|
+
"spec-not-promotable",
|
|
363
|
+
f"{spec.spec_id} must be review-ready or stable before DPA stable promotion",
|
|
364
|
+
spec.path,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _validate_promotion_record(
|
|
369
|
+
data: dict[str, Any],
|
|
370
|
+
root: Path,
|
|
371
|
+
path: str,
|
|
372
|
+
specs: tuple[DpaSpecStatus, ...],
|
|
373
|
+
findings: list[DpaStableReadinessFinding],
|
|
374
|
+
) -> None:
|
|
375
|
+
if data.get("schema_version") != 1:
|
|
376
|
+
_finding(findings, "schema-version-invalid", "schema_version must be 1", path)
|
|
377
|
+
if data.get("kind") != "dpa_stable_promotion_record":
|
|
378
|
+
_finding(findings, "kind-invalid", "kind must be dpa_stable_promotion_record", path)
|
|
379
|
+
if data.get("status") != PROMOTION_RECORD_STATUS:
|
|
380
|
+
_finding(findings, "status-invalid", f"status must be {PROMOTION_RECORD_STATUS}", path)
|
|
381
|
+
if data.get("decision_token") != PROMOTION_DECISION_TOKEN:
|
|
382
|
+
_finding(
|
|
383
|
+
findings,
|
|
384
|
+
"decision-token-invalid",
|
|
385
|
+
f"decision_token must be {PROMOTION_DECISION_TOKEN}",
|
|
386
|
+
path,
|
|
387
|
+
)
|
|
388
|
+
validation_ref = str(data.get("validation_ref", "")).strip()
|
|
389
|
+
if not validation_ref:
|
|
390
|
+
_finding(findings, "validation-ref-missing", "validation_ref must record an exact ref", path)
|
|
391
|
+
elif _git_ref_exists(root, validation_ref) is False:
|
|
392
|
+
_finding(
|
|
393
|
+
findings,
|
|
394
|
+
"validation-ref-unresolvable",
|
|
395
|
+
f"validation_ref is not a known commit: {validation_ref}",
|
|
396
|
+
path,
|
|
397
|
+
)
|
|
398
|
+
if not str(data.get("maintainer", "")).strip():
|
|
399
|
+
_finding(findings, "maintainer-missing", "maintainer authorization text must be recorded", path)
|
|
400
|
+
|
|
401
|
+
scope = _mapping(data.get("stable_scope"))
|
|
402
|
+
criteria = _mapping(data.get("criteria"))
|
|
403
|
+
rollback = _mapping(data.get("rollback"))
|
|
404
|
+
claims = _mapping(data.get("claims"))
|
|
405
|
+
external_scope = _mapping(data.get("external_repo_management"))
|
|
406
|
+
|
|
407
|
+
_validate_scope(scope, root, path, specs, findings)
|
|
408
|
+
_validate_criteria(criteria, path, findings)
|
|
409
|
+
_validate_rollback(rollback, root, path, findings)
|
|
410
|
+
_validate_claims(claims, path, findings)
|
|
411
|
+
_validate_external_repo_scope(external_scope, path, findings)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _validate_scope(
|
|
415
|
+
scope: dict[str, Any],
|
|
416
|
+
root: Path,
|
|
417
|
+
path: str,
|
|
418
|
+
specs: tuple[DpaSpecStatus, ...],
|
|
419
|
+
findings: list[DpaStableReadinessFinding],
|
|
420
|
+
) -> None:
|
|
421
|
+
if scope.get("id") != STABLE_SCOPE_ID:
|
|
422
|
+
_finding(findings, "stable-scope-id-invalid", f"stable_scope.id must be {STABLE_SCOPE_ID}", path)
|
|
423
|
+
_require_existing_paths(scope.get("evidence"), root, findings, path, "stable-scope-evidence-missing")
|
|
424
|
+
final_closeout_record = str(scope.get("final_closeout_record", "")).strip()
|
|
425
|
+
if final_closeout_record != DEFAULT_DPA_FINAL_CLOSEOUT_RECORD_PATH.as_posix():
|
|
426
|
+
_finding(
|
|
427
|
+
findings,
|
|
428
|
+
"final-closeout-record-path-invalid",
|
|
429
|
+
"stable_scope.final_closeout_record must point at the final closeout record",
|
|
430
|
+
path,
|
|
431
|
+
)
|
|
432
|
+
_load_stable_readiness_evidence(scope.get("stable_readiness_evidence"), root, path, findings)
|
|
433
|
+
|
|
434
|
+
spec_map = {spec.spec_id: spec for spec in specs}
|
|
435
|
+
promoted_specs = _promoted_spec_map(scope.get("promoted_specs"), path, findings)
|
|
436
|
+
for spec_id in PROMOTED_SPEC_IDS:
|
|
437
|
+
promoted = promoted_specs.get(spec_id)
|
|
438
|
+
actual = spec_map.get(spec_id)
|
|
439
|
+
if promoted is None:
|
|
440
|
+
_finding(findings, "promoted-spec-missing", f"stable_scope.promoted_specs missing {spec_id}", path)
|
|
441
|
+
continue
|
|
442
|
+
if promoted.get("path") != actual.path:
|
|
443
|
+
_finding(
|
|
444
|
+
findings,
|
|
445
|
+
"promoted-spec-path-invalid",
|
|
446
|
+
f"stable_scope.promoted_specs path mismatch for {spec_id}",
|
|
447
|
+
f"{path}:stable_scope.promoted_specs.{spec_id}",
|
|
448
|
+
)
|
|
449
|
+
if promoted.get("to_status") != "stable":
|
|
450
|
+
_finding(
|
|
451
|
+
findings,
|
|
452
|
+
"promoted-spec-to-status-invalid",
|
|
453
|
+
f"{spec_id} to_status must be stable",
|
|
454
|
+
f"{path}:stable_scope.promoted_specs.{spec_id}",
|
|
455
|
+
)
|
|
456
|
+
if not _string_list(promoted.get("evidence")):
|
|
457
|
+
_finding(
|
|
458
|
+
findings,
|
|
459
|
+
"promoted-spec-evidence-missing",
|
|
460
|
+
f"{spec_id} promotion evidence must be listed",
|
|
461
|
+
f"{path}:stable_scope.promoted_specs.{spec_id}",
|
|
462
|
+
)
|
|
463
|
+
if actual and not actual.stable:
|
|
464
|
+
_finding(
|
|
465
|
+
findings,
|
|
466
|
+
"spec-not-stable-after-promotion",
|
|
467
|
+
f"{spec_id} header status must be stable after promotion",
|
|
468
|
+
actual.path,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _validate_criteria(
|
|
473
|
+
criteria: dict[str, Any],
|
|
474
|
+
path: str,
|
|
475
|
+
findings: list[DpaStableReadinessFinding],
|
|
476
|
+
) -> None:
|
|
477
|
+
expected = {
|
|
478
|
+
"dpa_000_to_dpa_900_stable": True,
|
|
479
|
+
"dp2_selected_scope_implementation_percent": 100,
|
|
480
|
+
"final_closeout_result_status": VALID_FINAL_CLOSEOUT_STATUS,
|
|
481
|
+
"post_dp2_kit_wide_dpa_status": "READY_FOR_FINAL_CLOSEOUT_RECORD",
|
|
482
|
+
"strict_gate_result_status": "PASS",
|
|
483
|
+
"stable_readiness_result_status": READY_FOR_STABLE_PROMOTION_STATUS,
|
|
484
|
+
"blocker_count": 0,
|
|
485
|
+
"warning_count": 0,
|
|
486
|
+
}
|
|
487
|
+
for key, value in expected.items():
|
|
488
|
+
if criteria.get(key) != value:
|
|
489
|
+
_finding(findings, f"criteria-{key.replace('_', '-')}-invalid", f"criteria.{key} must be {value!r}", path)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _validate_rollback(
|
|
493
|
+
rollback: dict[str, Any],
|
|
494
|
+
root: Path,
|
|
495
|
+
path: str,
|
|
496
|
+
findings: list[DpaStableReadinessFinding],
|
|
497
|
+
) -> None:
|
|
498
|
+
if rollback.get("tested_or_adjudicated") is not True:
|
|
499
|
+
_finding(findings, "rollback-not-adjudicated", "rollback must be tested or adjudicated", path)
|
|
500
|
+
if rollback.get("production_mutation_required") is not False:
|
|
501
|
+
_finding(findings, "rollback-production-mutation-invalid", "rollback must not require production mutation", path)
|
|
502
|
+
_require_existing_paths(rollback.get("evidence"), root, findings, path, "rollback-evidence-missing")
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _validate_claims(
|
|
506
|
+
claims: dict[str, Any],
|
|
507
|
+
path: str,
|
|
508
|
+
findings: list[DpaStableReadinessFinding],
|
|
509
|
+
) -> None:
|
|
510
|
+
required_true = ("kit_wide_dpa_conformance_claimed", "stable_dpa_claimed")
|
|
511
|
+
for field in required_true:
|
|
512
|
+
if claims.get(field) is not True:
|
|
513
|
+
_finding(findings, "claim-invalid", f"claims.{field} must be true", path)
|
|
514
|
+
required_false = (
|
|
515
|
+
"production_mutation_performed",
|
|
516
|
+
"generated_outputs_manually_patched",
|
|
517
|
+
"external_repo_conformance_claimed",
|
|
518
|
+
)
|
|
519
|
+
for field in required_false:
|
|
520
|
+
if claims.get(field) is not False:
|
|
521
|
+
_finding(findings, "false-claim-invalid", f"claims.{field} must be false", path)
|
|
522
|
+
if claims.get("claim_scope") != STABLE_CLAIM_SCOPE:
|
|
523
|
+
_finding(findings, "claim-scope-invalid", f"claims.claim_scope must be {STABLE_CLAIM_SCOPE}", path)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _validate_external_repo_scope(
|
|
527
|
+
external_scope: dict[str, Any],
|
|
528
|
+
path: str,
|
|
529
|
+
findings: list[DpaStableReadinessFinding],
|
|
530
|
+
) -> None:
|
|
531
|
+
if external_scope.get("status") != EXTERNAL_REPO_STATUS:
|
|
532
|
+
_finding(
|
|
533
|
+
findings,
|
|
534
|
+
"external-repo-status-invalid",
|
|
535
|
+
f"external_repo_management.status must be {EXTERNAL_REPO_STATUS}",
|
|
536
|
+
path,
|
|
537
|
+
)
|
|
538
|
+
if external_scope.get("automatic_external_repo_conformance_claimed") is not False:
|
|
539
|
+
_finding(
|
|
540
|
+
findings,
|
|
541
|
+
"external-repo-overclaim",
|
|
542
|
+
"external repo conformance must not be claimed without per-repo evidence",
|
|
543
|
+
path,
|
|
544
|
+
)
|
|
545
|
+
for key in (
|
|
546
|
+
"requires_fresh_per_repo_inventory",
|
|
547
|
+
"requires_fresh_per_repo_dpa_600_700_evidence",
|
|
548
|
+
"requires_maintainer_authorized_scope",
|
|
549
|
+
):
|
|
550
|
+
if external_scope.get(key) is not True:
|
|
551
|
+
_finding(findings, "external-repo-guardrail-missing", f"{key} must be true", path)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _probe_statuses(
|
|
555
|
+
root: Path,
|
|
556
|
+
validation_ref: str,
|
|
557
|
+
findings: list[DpaStableReadinessFinding],
|
|
558
|
+
) -> dict[str, str]:
|
|
559
|
+
probes = {
|
|
560
|
+
"PROBE-002": evaluate_probe_002_lifecycle_readiness(root, validation_ref=validation_ref),
|
|
561
|
+
"RENDERER": evaluate_renderer_probe_readiness(root, validation_ref=validation_ref),
|
|
562
|
+
"PROBE-003": evaluate_probe_003_workflow_readiness(root, validation_ref=validation_ref),
|
|
563
|
+
"PROBE-004": evaluate_probe_004_migration_readiness(root, validation_ref=validation_ref),
|
|
564
|
+
}
|
|
565
|
+
statuses: dict[str, str] = {}
|
|
566
|
+
for probe_id, result in probes.items():
|
|
567
|
+
statuses[probe_id] = str(result.result_status)
|
|
568
|
+
if not result.full_evidence_satisfied:
|
|
569
|
+
_finding(
|
|
570
|
+
findings,
|
|
571
|
+
"probe-full-evidence-not-satisfied",
|
|
572
|
+
f"{probe_id} full evidence must remain satisfied for stable promotion",
|
|
573
|
+
result.readiness_record,
|
|
574
|
+
)
|
|
575
|
+
return statuses
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _load_promotion_record(
|
|
579
|
+
path: Path,
|
|
580
|
+
root: Path,
|
|
581
|
+
findings: list[DpaStableReadinessFinding],
|
|
582
|
+
) -> dict[str, Any]:
|
|
583
|
+
display_path = _display_path(path, root)
|
|
584
|
+
try:
|
|
585
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
586
|
+
except json.JSONDecodeError as exc:
|
|
587
|
+
_finding(findings, "promotion-record-invalid-json", f"record JSON is invalid: {exc}", display_path)
|
|
588
|
+
return {}
|
|
589
|
+
if not isinstance(data, dict):
|
|
590
|
+
_finding(findings, "promotion-record-not-object", "record root must be a JSON object", display_path)
|
|
591
|
+
return {}
|
|
592
|
+
return data
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _load_stable_readiness_evidence(
|
|
596
|
+
value: Any,
|
|
597
|
+
root: Path,
|
|
598
|
+
path: str,
|
|
599
|
+
findings: list[DpaStableReadinessFinding],
|
|
600
|
+
) -> None:
|
|
601
|
+
candidate = str(value or "").strip()
|
|
602
|
+
if not candidate:
|
|
603
|
+
_finding(findings, "stable-readiness-evidence-missing", "stable readiness evidence path is missing", path)
|
|
604
|
+
return
|
|
605
|
+
evidence_path = root / candidate
|
|
606
|
+
if not evidence_path.exists():
|
|
607
|
+
_finding(findings, "stable-readiness-evidence-missing", f"missing evidence path: {candidate}", path)
|
|
608
|
+
return
|
|
609
|
+
try:
|
|
610
|
+
payload = json.loads(evidence_path.read_text(encoding="utf-8"))
|
|
611
|
+
except json.JSONDecodeError as exc:
|
|
612
|
+
_finding(findings, "stable-readiness-evidence-invalid-json", f"evidence JSON is invalid: {exc}", candidate)
|
|
613
|
+
return
|
|
614
|
+
if not isinstance(payload, dict):
|
|
615
|
+
_finding(findings, "stable-readiness-evidence-invalid", "evidence JSON must be an object", candidate)
|
|
616
|
+
return
|
|
617
|
+
if payload.get("kind") != "dpa_stable_readiness_check":
|
|
618
|
+
_finding(findings, "stable-readiness-evidence-kind-invalid", "evidence kind is invalid", candidate)
|
|
619
|
+
if payload.get("result_status") != READY_FOR_STABLE_PROMOTION_STATUS:
|
|
620
|
+
_finding(
|
|
621
|
+
findings,
|
|
622
|
+
"stable-readiness-evidence-status-invalid",
|
|
623
|
+
f"evidence result_status must be {READY_FOR_STABLE_PROMOTION_STATUS}",
|
|
624
|
+
candidate,
|
|
625
|
+
)
|
|
626
|
+
if payload.get("finding_count") != 0:
|
|
627
|
+
_finding(findings, "stable-readiness-evidence-findings-present", "evidence must have zero findings", candidate)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _promoted_spec_map(
|
|
631
|
+
value: Any,
|
|
632
|
+
path: str,
|
|
633
|
+
findings: list[DpaStableReadinessFinding],
|
|
634
|
+
) -> dict[str, dict[str, Any]]:
|
|
635
|
+
if not isinstance(value, list) or not value:
|
|
636
|
+
_finding(findings, "promoted-specs-missing", "stable_scope.promoted_specs must be a non-empty list", path)
|
|
637
|
+
return {}
|
|
638
|
+
promoted: dict[str, dict[str, Any]] = {}
|
|
639
|
+
for index, item in enumerate(value):
|
|
640
|
+
entry = _mapping(item)
|
|
641
|
+
spec_id = str(entry.get("id", "")).strip()
|
|
642
|
+
if not spec_id:
|
|
643
|
+
_finding(findings, "promoted-spec-id-missing", "promoted spec id is missing", f"{path}:promoted_specs[{index}]")
|
|
644
|
+
continue
|
|
645
|
+
if spec_id in promoted:
|
|
646
|
+
_finding(findings, "promoted-spec-duplicate", f"promoted spec is duplicated: {spec_id}", path)
|
|
647
|
+
promoted[spec_id] = entry
|
|
648
|
+
return promoted
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _spec_status(root: Path, spec_id: str, path: str) -> DpaSpecStatus:
|
|
652
|
+
candidate = root / path
|
|
653
|
+
if not candidate.exists():
|
|
654
|
+
return DpaSpecStatus(spec_id=spec_id, path=path, status="MISSING", present=False)
|
|
655
|
+
for line in candidate.read_text(encoding="utf-8").splitlines()[:40]:
|
|
656
|
+
if line.startswith("Status:"):
|
|
657
|
+
return DpaSpecStatus(
|
|
658
|
+
spec_id=spec_id,
|
|
659
|
+
path=path,
|
|
660
|
+
status=line.removeprefix("Status:").strip(),
|
|
661
|
+
present=True,
|
|
662
|
+
)
|
|
663
|
+
return DpaSpecStatus(spec_id=spec_id, path=path, status="MISSING_STATUS", present=True)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _require_existing_paths(
|
|
667
|
+
value: Any,
|
|
668
|
+
root: Path,
|
|
669
|
+
findings: list[DpaStableReadinessFinding],
|
|
670
|
+
path: str,
|
|
671
|
+
code: str,
|
|
672
|
+
) -> None:
|
|
673
|
+
paths = _string_list(value)
|
|
674
|
+
if not paths:
|
|
675
|
+
_finding(findings, code, "evidence paths must be a non-empty string list", path)
|
|
676
|
+
return
|
|
677
|
+
for evidence_path in paths:
|
|
678
|
+
if not (root / evidence_path).exists():
|
|
679
|
+
_finding(findings, code, f"missing evidence path: {evidence_path}", path)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _mapping(value: Any) -> dict[str, Any]:
|
|
683
|
+
return value if isinstance(value, dict) else {}
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _string_list(value: Any) -> list[str]:
|
|
687
|
+
if not isinstance(value, list):
|
|
688
|
+
return []
|
|
689
|
+
return [item for item in value if isinstance(item, str) and item.strip()]
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _finding(
|
|
693
|
+
findings: list[DpaStableReadinessFinding],
|
|
694
|
+
code: str,
|
|
695
|
+
message: str,
|
|
696
|
+
path: str,
|
|
697
|
+
) -> None:
|
|
698
|
+
findings.append(DpaStableReadinessFinding(code=code, message=message, path=path))
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _git_head(root: Path) -> str:
|
|
702
|
+
completed = subprocess.run(
|
|
703
|
+
["git", "rev-parse", "HEAD"],
|
|
704
|
+
cwd=root,
|
|
705
|
+
text=True,
|
|
706
|
+
capture_output=True,
|
|
707
|
+
check=False,
|
|
708
|
+
)
|
|
709
|
+
if completed.returncode != 0:
|
|
710
|
+
return "UNKNOWN"
|
|
711
|
+
return completed.stdout.strip()
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _git_ref_exists(root: Path, ref: str) -> bool | None:
|
|
715
|
+
if not (root / ".git").exists():
|
|
716
|
+
return None
|
|
717
|
+
completed = subprocess.run(
|
|
718
|
+
["git", "rev-parse", "--verify", f"{ref}^{{commit}}"],
|
|
719
|
+
cwd=root,
|
|
720
|
+
text=True,
|
|
721
|
+
capture_output=True,
|
|
722
|
+
check=False,
|
|
723
|
+
)
|
|
724
|
+
return completed.returncode == 0
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _resolve_under_root(root: Path, path: Path | str) -> Path:
|
|
728
|
+
candidate = Path(path)
|
|
729
|
+
if candidate.is_absolute():
|
|
730
|
+
return candidate
|
|
731
|
+
return root / candidate
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _display_path(path: Path, root: Path) -> str:
|
|
735
|
+
try:
|
|
736
|
+
return path.relative_to(root).as_posix()
|
|
737
|
+
except ValueError:
|
|
738
|
+
return path.as_posix()
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _evidence_output_root(root: Path) -> Path:
|
|
742
|
+
ws = load_workspace(root, suppress_legacy_profile_warning=True)
|
|
743
|
+
return ws.architecture_file(Path(*EVIDENCE_OUTPUT_ROOT_PARTS)).resolve()
|