context-rail 0.1.0__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.
- context_rail/__init__.py +3 -0
- context_rail/__main__.py +4 -0
- context_rail/autonomy/__init__.py +85 -0
- context_rail/autonomy/_contracts.py +487 -0
- context_rail/autonomy/_drift.py +173 -0
- context_rail/autonomy/_evidence_harvest.py +210 -0
- context_rail/autonomy/_intent_match.py +145 -0
- context_rail/autonomy/_pdca.py +783 -0
- context_rail/autonomy/_rails.py +909 -0
- context_rail/autonomy/_runs.py +947 -0
- context_rail/autonomy/_runtime.py +194 -0
- context_rail/autonomy.py +6 -0
- context_rail/brainstorm/__init__.py +156 -0
- context_rail/brainstorm/_phase_drafts.py +278 -0
- context_rail/brainstorm/_plan_drafts.py +491 -0
- context_rail/brainstorm/drafts.py +34 -0
- context_rail/brainstorm_artifacts.py +160 -0
- context_rail/brainstorm_conductor.py +440 -0
- context_rail/brainstorm_core.py +459 -0
- context_rail/brainstorm_synthesis.py +40 -0
- context_rail/cli.py +661 -0
- context_rail/context_pack.py +314 -0
- context_rail/db.py +432 -0
- context_rail/db_schema/__init__.py +19 -0
- context_rail/db_schema/_gates.py +78 -0
- context_rail/db_schema/_schema.py +763 -0
- context_rail/db_schema.py +2 -0
- context_rail/focus.py +306 -0
- context_rail/gates.py +117 -0
- context_rail/rules.py +201 -0
- context_rail/server/__init__.py +289 -0
- context_rail/server/_imports.py +165 -0
- context_rail/server/_tool_surface.py +60 -0
- context_rail/server/tools_artifacts.py +154 -0
- context_rail/server/tools_autonomy.py +122 -0
- context_rail/server/tools_brainstorm.py +192 -0
- context_rail/server/tools_categories.py +103 -0
- context_rail/server/tools_configure.py +69 -0
- context_rail/server/tools_context.py +1212 -0
- context_rail/server/tools_decisions.py +142 -0
- context_rail/server/tools_find.py +201 -0
- context_rail/server/tools_focus.py +313 -0
- context_rail/server/tools_gates.py +194 -0
- context_rail/server/tools_intent.py +67 -0
- context_rail/server/tools_lifecycle.py +194 -0
- context_rail/server/tools_notes.py +333 -0
- context_rail/server/tools_orient.py +421 -0
- context_rail/server/tools_phase_brainstorm.py +184 -0
- context_rail/server/tools_plan_review.py +520 -0
- context_rail/server/tools_planning.py +463 -0
- context_rail/server/tools_projects.py +492 -0
- context_rail/server/tools_retro.py +243 -0
- context_rail/server/tools_risks.py +107 -0
- context_rail/server/tools_roadmap.py +434 -0
- context_rail/server/tools_rules.py +204 -0
- context_rail/server/tools_runtime.py +25 -0
- context_rail/server/tools_tasks.py +906 -0
- context_rail/server.py +8 -0
- context_rail/web.py +350 -0
- context_rail/web_api.py +74 -0
- context_rail/web_autonomy_api.py +109 -0
- context_rail/web_core.py +59 -0
- context_rail/web_dist/assets/AutonomyRailPanel-CbhEF78e.js +16 -0
- context_rail/web_dist/assets/Badge-DLfxmmCr.js +1 -0
- context_rail/web_dist/assets/BrainstormTemplate-CHDOfvGo.js +1 -0
- context_rail/web_dist/assets/Button-B1Pa9rKn.js +1 -0
- context_rail/web_dist/assets/CalendarTemplate-DeIunyja.js +1 -0
- context_rail/web_dist/assets/DashboardTemplate-Car4j8De.js +1 -0
- context_rail/web_dist/assets/DecisionsTemplate-4MnmyHue.js +1 -0
- context_rail/web_dist/assets/DepsArtifactsTemplate-D-5DrBVK.js +1 -0
- context_rail/web_dist/assets/Divider-D4ZEQqYw.js +1 -0
- context_rail/web_dist/assets/Dot-DXSDh1s8.js +1 -0
- context_rail/web_dist/assets/EntityDetailPanel-Be5U9SXA.js +1 -0
- context_rail/web_dist/assets/ExecuteTemplate-BYJIQwUU.js +1 -0
- context_rail/web_dist/assets/KanbanBoardTemplate-t-ZciXC6.js +1 -0
- context_rail/web_dist/assets/PageHeader-R2cGDHn_.js +1 -0
- context_rail/web_dist/assets/ProjectsTemplate-CNe-Wyat.js +1 -0
- context_rail/web_dist/assets/ReviewTemplate-BzBmmZ8i.js +4 -0
- context_rail/web_dist/assets/RisksTemplate-DsjhCA0v.js +1 -0
- context_rail/web_dist/assets/SectionCard-CaEXeRuy.js +1 -0
- context_rail/web_dist/assets/SettingsTemplate-K2LGnXE6.js +1 -0
- context_rail/web_dist/assets/Sidebar-Dpbs2xk9.js +146 -0
- context_rail/web_dist/assets/StatusPill-DjACmQdp.js +1 -0
- context_rail/web_dist/assets/StructureTemplate-tHzD7-8T.js +1 -0
- context_rail/web_dist/assets/TaskDetailPanel-Bn7EbFIK.js +1 -0
- context_rail/web_dist/assets/Text-BIsiCk07.js +1 -0
- context_rail/web_dist/assets/TimelineTemplate-CZ4ptEB1.js +1 -0
- context_rail/web_dist/assets/TrashTemplate-D5Hzlygu.js +1 -0
- context_rail/web_dist/assets/calendar-CL12sWTL.js +1 -0
- context_rail/web_dist/assets/charts-BCwg4OMJ.js +67 -0
- context_rail/web_dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
- context_rail/web_dist/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
- context_rail/web_dist/assets/index-BWbvE675.css +1 -0
- context_rail/web_dist/assets/index-CbFjhbCx.js +2 -0
- context_rail/web_dist/assets/react-RRCjlvKK.js +67 -0
- context_rail/web_dist/assets/sortable.esm-BNXrI9dD.js +5 -0
- context_rail/web_dist/index.html +14 -0
- context_rail/web_entities.py +471 -0
- context_rail/web_flow.py +344 -0
- context_rail/web_focus_api.py +265 -0
- context_rail/web_projects.py +372 -0
- context_rail-0.1.0.dist-info/METADATA +437 -0
- context_rail-0.1.0.dist-info/RECORD +151 -0
- context_rail-0.1.0.dist-info/WHEEL +5 -0
- context_rail-0.1.0.dist-info/entry_points.txt +2 -0
- context_rail-0.1.0.dist-info/licenses/LICENSE +21 -0
- context_rail-0.1.0.dist-info/top_level.txt +1 -0
context_rail/__init__.py
ADDED
context_rail/__main__.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Context Rail autonomy subpackage — contracts, rail checks, PDCA cycles, and run orchestration."""
|
|
2
|
+
|
|
3
|
+
from ._runtime import (
|
|
4
|
+
Blocker,
|
|
5
|
+
BlockerLayer,
|
|
6
|
+
BlockerSeverity,
|
|
7
|
+
RuntimeCheckReport,
|
|
8
|
+
runtime_check,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from ._contracts import (
|
|
12
|
+
CONTRACT_FIELDS,
|
|
13
|
+
_contract_digest,
|
|
14
|
+
_json_dumps,
|
|
15
|
+
_json_load,
|
|
16
|
+
_project_exists,
|
|
17
|
+
_row_contract,
|
|
18
|
+
_row_cycle,
|
|
19
|
+
_snapshot_from_draft,
|
|
20
|
+
_task_row,
|
|
21
|
+
active_plan_contract,
|
|
22
|
+
apply_contract_defaults,
|
|
23
|
+
apply_contract_edits,
|
|
24
|
+
contract_review_lines,
|
|
25
|
+
create_plan_contract,
|
|
26
|
+
default_autonomy_policy,
|
|
27
|
+
default_drift_rules,
|
|
28
|
+
default_evidence_rules,
|
|
29
|
+
default_quality_contract,
|
|
30
|
+
default_rail_check_policy,
|
|
31
|
+
default_stop_rules,
|
|
32
|
+
)
|
|
33
|
+
from ._rails import (
|
|
34
|
+
_completed_task_count,
|
|
35
|
+
_create_default_rail_policies,
|
|
36
|
+
_pending_due_checks,
|
|
37
|
+
_policy_row,
|
|
38
|
+
_rail_cursor_snapshot,
|
|
39
|
+
_record_due_rail_checks,
|
|
40
|
+
build_rail_check_packet,
|
|
41
|
+
collect_rail_context,
|
|
42
|
+
create_agent_rail_policy,
|
|
43
|
+
create_default_rail_policies,
|
|
44
|
+
detect_due_rail_checks,
|
|
45
|
+
evaluate_rail_check,
|
|
46
|
+
list_rail_policies,
|
|
47
|
+
record_rail_check,
|
|
48
|
+
resolve_rail_policies,
|
|
49
|
+
)
|
|
50
|
+
from ._drift import (
|
|
51
|
+
compute_plan_drift,
|
|
52
|
+
detect_decision_conflicts,
|
|
53
|
+
)
|
|
54
|
+
from ._pdca import (
|
|
55
|
+
_active_run_for_task,
|
|
56
|
+
_cycle_evidence,
|
|
57
|
+
_cycle_for_task,
|
|
58
|
+
attach_activity_to_active_cycle,
|
|
59
|
+
check_cycle,
|
|
60
|
+
handle_task_blocked,
|
|
61
|
+
handle_task_done,
|
|
62
|
+
precheck_task_done,
|
|
63
|
+
record_do,
|
|
64
|
+
record_evidence,
|
|
65
|
+
)
|
|
66
|
+
from ._runs import (
|
|
67
|
+
_active_cycle_for_task,
|
|
68
|
+
_parse_sqlite_datetime,
|
|
69
|
+
_row_run,
|
|
70
|
+
_select_next_task,
|
|
71
|
+
active_autonomy_run,
|
|
72
|
+
autonomy_gate,
|
|
73
|
+
autonomy_next,
|
|
74
|
+
autonomy_record,
|
|
75
|
+
autonomy_status,
|
|
76
|
+
build_execution_packet,
|
|
77
|
+
ensure_pdca_cycle_for_task,
|
|
78
|
+
execution_packet_for_task,
|
|
79
|
+
latest_autonomy_run,
|
|
80
|
+
lease_limit_report,
|
|
81
|
+
phase_gate_report,
|
|
82
|
+
resume_autonomy_run,
|
|
83
|
+
start_autonomy_run,
|
|
84
|
+
stop_run_for_user,
|
|
85
|
+
)
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
"""Plan contract defaults, editing, persistence, and snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sqlite3
|
|
9
|
+
import uuid
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..db import get_db, init_db, load_state, log_activity, save_state, transaction
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _create_default_rail_policies(conn: sqlite3.Connection, project_id: str, contract: dict[str, Any]) -> list[dict[str, Any]]:
|
|
17
|
+
from ._rails import _create_default_rail_policies as _impl
|
|
18
|
+
return _impl(conn, project_id, contract)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
CONTRACT_FIELDS = {
|
|
22
|
+
"quality_contract",
|
|
23
|
+
"autonomy_policy",
|
|
24
|
+
"rail_check_policy",
|
|
25
|
+
"stop_rules",
|
|
26
|
+
"drift_rules",
|
|
27
|
+
"evidence_rules",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def default_quality_contract() -> dict[str, Any]:
|
|
32
|
+
return {
|
|
33
|
+
"good_feels_like": [
|
|
34
|
+
"Goal-first, not task-first",
|
|
35
|
+
"Dense but readable",
|
|
36
|
+
"Clear next action, blockers, and review state",
|
|
37
|
+
],
|
|
38
|
+
"reference_examples": [],
|
|
39
|
+
"non_negotiables": [
|
|
40
|
+
"No hidden approval state",
|
|
41
|
+
"No task-by-task user babysitting",
|
|
42
|
+
"User-visible gates for meaningful direction changes",
|
|
43
|
+
],
|
|
44
|
+
"acceptance_evidence": [
|
|
45
|
+
"tests_pass_or_gap_recorded",
|
|
46
|
+
"plan_alignment_checked",
|
|
47
|
+
],
|
|
48
|
+
"quality_floor": [
|
|
49
|
+
"No broken primary flow",
|
|
50
|
+
"No invisible next action",
|
|
51
|
+
"No unchecked plan drift",
|
|
52
|
+
],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def default_autonomy_policy() -> dict[str, Any]:
|
|
57
|
+
return {
|
|
58
|
+
"codex_can_decide": [
|
|
59
|
+
"Implementation details inside approved scope",
|
|
60
|
+
"Small bug fixes",
|
|
61
|
+
"Documentation and cleanup",
|
|
62
|
+
"Tests and verification steps",
|
|
63
|
+
],
|
|
64
|
+
"codex_must_stop_for": [
|
|
65
|
+
"Product direction change",
|
|
66
|
+
"Major UX direction change",
|
|
67
|
+
"Architecture change",
|
|
68
|
+
"Scope expansion",
|
|
69
|
+
"Missing required quality evidence",
|
|
70
|
+
"Conflicting decisions",
|
|
71
|
+
],
|
|
72
|
+
"max_cycles_without_user": 10,
|
|
73
|
+
"phase_gate_required": True,
|
|
74
|
+
"lease": {
|
|
75
|
+
"scope": "phase",
|
|
76
|
+
"max_cycles": 10,
|
|
77
|
+
"max_minutes": 120,
|
|
78
|
+
"visible_gate": "phase",
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def default_rail_check_policy() -> dict[str, Any]:
|
|
84
|
+
return {
|
|
85
|
+
"default_every_completed_tasks": 5,
|
|
86
|
+
"also_check_on": [
|
|
87
|
+
"path_complete",
|
|
88
|
+
"goal_progress",
|
|
89
|
+
"goal_complete",
|
|
90
|
+
"before_phase_change",
|
|
91
|
+
"agent_lane_violation",
|
|
92
|
+
"decision_changed",
|
|
93
|
+
"evidence_missing",
|
|
94
|
+
"dependency_risk",
|
|
95
|
+
"scope_drift",
|
|
96
|
+
"quality_drift",
|
|
97
|
+
],
|
|
98
|
+
"user_defined_paths": [],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def default_stop_rules() -> list[dict[str, Any]]:
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
"id": "scope_expansion",
|
|
106
|
+
"severity": "hard",
|
|
107
|
+
"condition": "new_task_outside_plan",
|
|
108
|
+
"message": "New work is outside the approved plan contract.",
|
|
109
|
+
"required_action": "user_gate",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "missing_required_evidence",
|
|
113
|
+
"severity": "hard",
|
|
114
|
+
"condition": "required_evidence_missing",
|
|
115
|
+
"message": "Required quality evidence is missing.",
|
|
116
|
+
"required_action": "collect_evidence_or_user_gate",
|
|
117
|
+
},
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def default_drift_rules() -> list[dict[str, Any]]:
|
|
122
|
+
return [
|
|
123
|
+
{"id": "unplanned_inside_phase", "score": 1, "condition": "unplanned_task_inside_current_phase"},
|
|
124
|
+
{"id": "unplanned_outside_phase", "score": 3, "condition": "unplanned_task_outside_current_phase"},
|
|
125
|
+
{"id": "direction_changed", "score": 5, "condition": "product_or_architecture_direction_changed"},
|
|
126
|
+
{"id": "non_negotiable_violated", "score": 6, "condition": "quality_contract_non_negotiable_violated"},
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def default_evidence_rules() -> list[dict[str, Any]]:
|
|
131
|
+
return [
|
|
132
|
+
{"id": "plan_alignment", "scope": "cycle", "required": True, "kind": "manual_check",
|
|
133
|
+
"hint": "summary must reference a verifiable artifact (path or URL)"},
|
|
134
|
+
{"id": "tests_or_reason", "scope": "phase", "required": True, "kind": "test",
|
|
135
|
+
"hint": "evidence must include metadata.falsifier (>=15 chars)"},
|
|
136
|
+
{"id": "phase_report", "scope": "phase", "required": True, "kind": "manual_check",
|
|
137
|
+
"hint": "summary must reference a verifiable artifact"},
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def apply_contract_defaults(draft: dict[str, Any]) -> dict[str, Any]:
|
|
142
|
+
"""Ensure a draft contains the user-reviewable autonomy contract fields."""
|
|
143
|
+
draft.setdefault("quality_contract", default_quality_contract())
|
|
144
|
+
draft.setdefault("autonomy_policy", default_autonomy_policy())
|
|
145
|
+
draft.setdefault("rail_check_policy", default_rail_check_policy())
|
|
146
|
+
draft.setdefault("stop_rules", default_stop_rules())
|
|
147
|
+
draft.setdefault("drift_rules", default_drift_rules())
|
|
148
|
+
draft.setdefault("evidence_rules", default_evidence_rules())
|
|
149
|
+
return draft
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def apply_contract_edits(draft: dict[str, Any], edits: dict[str, Any]) -> dict[str, Any]:
|
|
153
|
+
"""Apply review-time edits to contract fields."""
|
|
154
|
+
for field in CONTRACT_FIELDS:
|
|
155
|
+
if field in edits:
|
|
156
|
+
draft[field] = edits[field]
|
|
157
|
+
return apply_contract_defaults(draft)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def contract_review_lines(draft: dict[str, Any]) -> list[str]:
|
|
161
|
+
"""Return compact human-readable lines for contract fields in plan review."""
|
|
162
|
+
draft = apply_contract_defaults(draft)
|
|
163
|
+
quality = draft.get("quality_contract") or {}
|
|
164
|
+
autonomy = draft.get("autonomy_policy") or {}
|
|
165
|
+
rail = draft.get("rail_check_policy") or {}
|
|
166
|
+
lease = autonomy.get("lease") or {}
|
|
167
|
+
return [
|
|
168
|
+
"",
|
|
169
|
+
"Autonomy Contract:",
|
|
170
|
+
f" Quality floor: {len(quality.get('quality_floor') or [])} item(s)",
|
|
171
|
+
f" Non-negotiables: {len(quality.get('non_negotiables') or [])} item(s)",
|
|
172
|
+
f" Can decide: {len(autonomy.get('codex_can_decide') or [])} item(s)",
|
|
173
|
+
f" Must stop for: {len(autonomy.get('codex_must_stop_for') or [])} item(s)",
|
|
174
|
+
f" Lease: {lease.get('scope', 'phase')} / {lease.get('max_cycles', autonomy.get('max_cycles_without_user', 0))} cycle(s)",
|
|
175
|
+
f" Rail-check: every {rail.get('default_every_completed_tasks', 5)} completed task(s), "
|
|
176
|
+
f"{len(rail.get('also_check_on') or [])} trigger(s)",
|
|
177
|
+
f" Evidence rules: {len(draft.get('evidence_rules') or [])} item(s)",
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _json_dumps(value: Any) -> str:
|
|
182
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _json_load(value: Any, fallback: Any) -> Any:
|
|
186
|
+
if value in (None, ""):
|
|
187
|
+
return fallback
|
|
188
|
+
if isinstance(value, (dict, list)):
|
|
189
|
+
return value
|
|
190
|
+
try:
|
|
191
|
+
return json.loads(value)
|
|
192
|
+
except (TypeError, json.JSONDecodeError):
|
|
193
|
+
return fallback
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _row_cycle(row: sqlite3.Row | None) -> dict[str, Any] | None:
|
|
197
|
+
if not row:
|
|
198
|
+
return None
|
|
199
|
+
data = dict(row)
|
|
200
|
+
for field, fallback in (
|
|
201
|
+
("plan_step", {}),
|
|
202
|
+
("do_log", {}),
|
|
203
|
+
("check_report", {}),
|
|
204
|
+
("act_log", {}),
|
|
205
|
+
("evidence_summary", {}),
|
|
206
|
+
):
|
|
207
|
+
data[field] = _json_load(data.get(field), fallback)
|
|
208
|
+
return data
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _task_row(conn: sqlite3.Connection, task_id: str) -> dict[str, Any] | None:
|
|
212
|
+
row = conn.execute(
|
|
213
|
+
"SELECT * FROM tasks WHERE id = ? AND deleted_at IS NULL",
|
|
214
|
+
(task_id,),
|
|
215
|
+
).fetchone()
|
|
216
|
+
return dict(row) if row else None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _contract_digest(contract: dict[str, Any]) -> str:
|
|
220
|
+
blob = _json_dumps({
|
|
221
|
+
"goal": contract.get("goal", ""),
|
|
222
|
+
"quality_contract": contract.get("quality_contract", {}),
|
|
223
|
+
"autonomy_policy": contract.get("autonomy_policy", {}),
|
|
224
|
+
"rail_check_policy": contract.get("rail_check_policy", {}),
|
|
225
|
+
"stop_rules": contract.get("stop_rules", []),
|
|
226
|
+
"drift_rules": contract.get("drift_rules", []),
|
|
227
|
+
"evidence_rules": contract.get("evidence_rules", []),
|
|
228
|
+
"plan_snapshot": contract.get("plan_snapshot", {}),
|
|
229
|
+
})
|
|
230
|
+
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _snapshot_from_draft(draft: dict[str, Any]) -> dict[str, Any]:
|
|
234
|
+
phases = []
|
|
235
|
+
goals = []
|
|
236
|
+
tasks = []
|
|
237
|
+
for phase in draft.get("phases", []) or []:
|
|
238
|
+
phases.append({
|
|
239
|
+
"id": phase.get("id", ""),
|
|
240
|
+
"name": phase.get("name", ""),
|
|
241
|
+
"sort_order": phase.get("sort_order", 0),
|
|
242
|
+
})
|
|
243
|
+
for goal in phase.get("goals", []) or []:
|
|
244
|
+
goals.append({
|
|
245
|
+
"id": goal.get("id", ""),
|
|
246
|
+
"phase_id": phase.get("id", ""),
|
|
247
|
+
"title": goal.get("title", ""),
|
|
248
|
+
"priority": goal.get("priority", 0),
|
|
249
|
+
})
|
|
250
|
+
for task in goal.get("tasks", []) or []:
|
|
251
|
+
tasks.append({
|
|
252
|
+
"id": task.get("id", ""),
|
|
253
|
+
"phase_id": phase.get("id", ""),
|
|
254
|
+
"goal_id": goal.get("id", ""),
|
|
255
|
+
"title": task.get("title", ""),
|
|
256
|
+
"priority": task.get("priority", 0),
|
|
257
|
+
})
|
|
258
|
+
for task in phase.get("unlinked_tasks", []) or []:
|
|
259
|
+
tasks.append({
|
|
260
|
+
"id": task.get("id", ""),
|
|
261
|
+
"phase_id": phase.get("id", ""),
|
|
262
|
+
"goal_id": task.get("goal_id") or "",
|
|
263
|
+
"title": task.get("title", ""),
|
|
264
|
+
"priority": task.get("priority", 0),
|
|
265
|
+
})
|
|
266
|
+
snapshot: dict[str, Any] = {
|
|
267
|
+
"scope": draft.get("_scope", "project"),
|
|
268
|
+
"level": draft.get("_level") or draft.get("level", ""),
|
|
269
|
+
"phases": phases,
|
|
270
|
+
"goals": goals,
|
|
271
|
+
"tasks": tasks,
|
|
272
|
+
"risks": draft.get("risks", []) or [],
|
|
273
|
+
"decisions": draft.get("decisions", []) or [],
|
|
274
|
+
}
|
|
275
|
+
if draft.get("markdown"):
|
|
276
|
+
snapshot["markdown"] = draft.get("markdown", "")
|
|
277
|
+
return snapshot
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _project_exists(conn: sqlite3.Connection, project_id: str) -> bool:
|
|
281
|
+
row = conn.execute(
|
|
282
|
+
"SELECT id FROM projects WHERE id = ? AND COALESCE(deleted_at, '') = ''",
|
|
283
|
+
(project_id,),
|
|
284
|
+
).fetchone()
|
|
285
|
+
return row is not None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _row_contract(row: sqlite3.Row | None) -> dict[str, Any] | None:
|
|
289
|
+
if not row:
|
|
290
|
+
return None
|
|
291
|
+
data = dict(row)
|
|
292
|
+
data["quality_contract"] = _json_load(data.get("quality_contract"), {})
|
|
293
|
+
data["autonomy_policy"] = _json_load(data.get("autonomy_policy"), {})
|
|
294
|
+
data["rail_check_policy"] = _json_load(data.get("rail_check_policy"), {})
|
|
295
|
+
data["stop_rules"] = _json_load(data.get("stop_rules"), [])
|
|
296
|
+
data["drift_rules"] = _json_load(data.get("drift_rules"), [])
|
|
297
|
+
data["evidence_rules"] = _json_load(data.get("evidence_rules"), [])
|
|
298
|
+
data["plan_snapshot"] = _json_load(data.get("plan_snapshot"), {})
|
|
299
|
+
return data
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def create_plan_contract(
|
|
303
|
+
project_id: str,
|
|
304
|
+
draft: dict[str, Any],
|
|
305
|
+
approved_by: str = "",
|
|
306
|
+
source_type: str = "plan_review",
|
|
307
|
+
source_id: str = "",
|
|
308
|
+
) -> dict[str, Any] | None:
|
|
309
|
+
"""Convert an approved draft into a durable plan contract.
|
|
310
|
+
|
|
311
|
+
Returns None when the project row is absent. That keeps legacy unit tests
|
|
312
|
+
with monkeypatched state from failing, while real approvals still persist.
|
|
313
|
+
"""
|
|
314
|
+
conn = get_db()
|
|
315
|
+
init_db(conn)
|
|
316
|
+
draft = apply_contract_defaults(dict(draft))
|
|
317
|
+
try:
|
|
318
|
+
if not _project_exists(conn, project_id):
|
|
319
|
+
return None
|
|
320
|
+
latest = conn.execute(
|
|
321
|
+
"SELECT COALESCE(MAX(version), 0) as version FROM plan_contracts WHERE project_id = ?",
|
|
322
|
+
(project_id,),
|
|
323
|
+
).fetchone()
|
|
324
|
+
version = int(latest["version"] or 0) + 1
|
|
325
|
+
contract = {
|
|
326
|
+
"id": str(uuid.uuid4()),
|
|
327
|
+
"project_id": project_id,
|
|
328
|
+
"source_type": source_type,
|
|
329
|
+
"source_id": source_id,
|
|
330
|
+
"title": draft.get("title") or draft.get("phase_name") or draft.get("goal") or "Approved plan",
|
|
331
|
+
"goal": draft.get("goal", ""),
|
|
332
|
+
"version": version,
|
|
333
|
+
"approved_by": approved_by,
|
|
334
|
+
"quality_contract": draft.get("quality_contract", {}),
|
|
335
|
+
"autonomy_policy": draft.get("autonomy_policy", {}),
|
|
336
|
+
"rail_check_policy": draft.get("rail_check_policy", {}),
|
|
337
|
+
"stop_rules": draft.get("stop_rules", []),
|
|
338
|
+
"drift_rules": draft.get("drift_rules", []),
|
|
339
|
+
"evidence_rules": draft.get("evidence_rules", []),
|
|
340
|
+
"plan_snapshot": draft.get("plan_snapshot") or _snapshot_from_draft(draft),
|
|
341
|
+
}
|
|
342
|
+
contract["digest"] = _contract_digest(contract)
|
|
343
|
+
with transaction(conn):
|
|
344
|
+
conn.execute(
|
|
345
|
+
"""UPDATE plan_contracts
|
|
346
|
+
SET status = 'superseded', updated_at = datetime('now')
|
|
347
|
+
WHERE project_id = ? AND status = 'active' AND deleted_at IS NULL""",
|
|
348
|
+
(project_id,),
|
|
349
|
+
)
|
|
350
|
+
conn.execute(
|
|
351
|
+
"""INSERT INTO plan_contracts (
|
|
352
|
+
id, project_id, source_type, source_id, title, goal, status,
|
|
353
|
+
version, approved_by, approved_at, quality_contract,
|
|
354
|
+
autonomy_policy, rail_check_policy, stop_rules, drift_rules,
|
|
355
|
+
evidence_rules, plan_snapshot, digest
|
|
356
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
357
|
+
(
|
|
358
|
+
contract["id"],
|
|
359
|
+
project_id,
|
|
360
|
+
source_type,
|
|
361
|
+
source_id,
|
|
362
|
+
contract["title"],
|
|
363
|
+
contract["goal"],
|
|
364
|
+
version,
|
|
365
|
+
approved_by,
|
|
366
|
+
_json_dumps(contract["quality_contract"]),
|
|
367
|
+
_json_dumps(contract["autonomy_policy"]),
|
|
368
|
+
_json_dumps(contract["rail_check_policy"]),
|
|
369
|
+
_json_dumps(contract["stop_rules"]),
|
|
370
|
+
_json_dumps(contract["drift_rules"]),
|
|
371
|
+
_json_dumps(contract["evidence_rules"]),
|
|
372
|
+
_json_dumps(contract["plan_snapshot"]),
|
|
373
|
+
contract["digest"],
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
policies = _create_default_rail_policies(conn, project_id, contract)
|
|
377
|
+
log_activity(
|
|
378
|
+
conn,
|
|
379
|
+
"project",
|
|
380
|
+
project_id,
|
|
381
|
+
"plan_contract_approved",
|
|
382
|
+
{"contract_id": contract["id"], "version": version, "rail_policies": [p["id"] for p in policies]},
|
|
383
|
+
)
|
|
384
|
+
state = load_state()
|
|
385
|
+
state["active_project_id"] = project_id
|
|
386
|
+
state["active_plan_contract_id"] = contract["id"]
|
|
387
|
+
if policies:
|
|
388
|
+
state["active_rail_policy_id"] = policies[0]["id"]
|
|
389
|
+
save_state(state)
|
|
390
|
+
return active_plan_contract(project_id)
|
|
391
|
+
finally:
|
|
392
|
+
conn.close()
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def active_plan_contract(project_id: str) -> dict[str, Any] | None:
|
|
396
|
+
conn = get_db()
|
|
397
|
+
init_db(conn)
|
|
398
|
+
try:
|
|
399
|
+
row = conn.execute(
|
|
400
|
+
"""SELECT * FROM plan_contracts
|
|
401
|
+
WHERE project_id = ? AND status = 'active' AND deleted_at IS NULL
|
|
402
|
+
ORDER BY version DESC, created_at DESC LIMIT 1""",
|
|
403
|
+
(project_id,),
|
|
404
|
+
).fetchone()
|
|
405
|
+
return _row_contract(row)
|
|
406
|
+
finally:
|
|
407
|
+
conn.close()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# ---------------------------------------------------------------------
|
|
411
|
+
# Task title validation (NCR-1)
|
|
412
|
+
# ---------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
# Common English stopwords + frequent imperative verbs that happen to look
|
|
415
|
+
# like code identifiers (PascalCase or lowercase) but aren't. Without this
|
|
416
|
+
# filter, "Wire the new module to the editor" would extract "Wire", "new",
|
|
417
|
+
# "the", "module" — three of which are noise.
|
|
418
|
+
_TITLE_STOPWORDS: frozenset[str] = frozenset({
|
|
419
|
+
# articles, prepositions, conjunctions, pronouns
|
|
420
|
+
"the", "a", "an", "to", "and", "or", "of", "in", "on", "at", "for",
|
|
421
|
+
"with", "from", "by", "is", "are", "was", "were", "be", "been", "being",
|
|
422
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
423
|
+
"should", "may", "might", "must", "shall", "can",
|
|
424
|
+
"this", "that", "these", "those", "i", "you", "we", "they",
|
|
425
|
+
"he", "she", "it", "its", "their", "our", "your", "my",
|
|
426
|
+
# adverbs / quantifiers
|
|
427
|
+
"new", "old", "first", "last", "next", "prev", "previous",
|
|
428
|
+
"all", "any", "some", "no", "not", "only",
|
|
429
|
+
# common imperative verbs (lowercase form)
|
|
430
|
+
"wire", "add", "fix", "make", "update", "implement", "create", "build",
|
|
431
|
+
"test", "tests", "verify", "check", "run", "remove", "delete", "drop",
|
|
432
|
+
"migrate", "rename", "refactor", "clean", "cleanup",
|
|
433
|
+
"ship", "merge", "split", "merge", "land", "deploy",
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _extract_code_nouns(title: str) -> list[str]:
|
|
438
|
+
"""Extract likely code-entity nouns from a task title.
|
|
439
|
+
|
|
440
|
+
Returns PascalCase (``[A-Z][a-zA-Z0-9]+``) and snake_case
|
|
441
|
+
(``[a-z][a-z0-9]*(_[a-z0-9]+)+``) tokens that aren't English stopwords.
|
|
442
|
+
Order is preserved, duplicates are removed (first occurrence wins).
|
|
443
|
+
"""
|
|
444
|
+
pascal = re.findall(r"\b[A-Z][a-zA-Z0-9]+\b", title)
|
|
445
|
+
# Non-capturing group so re.findall returns the full match, not the last
|
|
446
|
+
# captured underscore-prefixed segment.
|
|
447
|
+
snake = re.findall(r"\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b", title)
|
|
448
|
+
seen: set[str] = set()
|
|
449
|
+
result: list[str] = []
|
|
450
|
+
for n in pascal + snake:
|
|
451
|
+
if n.lower() in _TITLE_STOPWORDS:
|
|
452
|
+
continue
|
|
453
|
+
if n in seen:
|
|
454
|
+
continue
|
|
455
|
+
seen.add(n)
|
|
456
|
+
result.append(n)
|
|
457
|
+
return result
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _find_unresolved_refs(title: str, src_root: str) -> dict[str, list[str]]:
|
|
461
|
+
"""Check which code-noun tokens in ``title`` don't match anything in ``src_root``.
|
|
462
|
+
|
|
463
|
+
Returns ``{"resolved": [...], "unresolved": [...]}``. When ``src_root``
|
|
464
|
+
does not exist, both lists are empty (caller decides policy).
|
|
465
|
+
"""
|
|
466
|
+
nouns = _extract_code_nouns(title)
|
|
467
|
+
src_path = Path(src_root)
|
|
468
|
+
if not src_path.exists():
|
|
469
|
+
return {"resolved": [], "unresolved": []}
|
|
470
|
+
# Build set of identifier-like tokens harvested from filenames under src/.
|
|
471
|
+
found: set[str] = set()
|
|
472
|
+
for p in src_path.rglob("*"):
|
|
473
|
+
if not p.is_file():
|
|
474
|
+
continue
|
|
475
|
+
stem = p.stem
|
|
476
|
+
if stem:
|
|
477
|
+
found.add(stem)
|
|
478
|
+
for token in re.findall(r"[A-Z][a-zA-Z0-9]+|[a-z][a-z0-9]*(?:_[a-z0-9]+)+", stem):
|
|
479
|
+
found.add(token)
|
|
480
|
+
resolved: list[str] = []
|
|
481
|
+
unresolved: list[str] = []
|
|
482
|
+
for n in nouns:
|
|
483
|
+
if n in found:
|
|
484
|
+
resolved.append(n)
|
|
485
|
+
else:
|
|
486
|
+
unresolved.append(n)
|
|
487
|
+
return {"resolved": resolved, "unresolved": unresolved}
|