devflow-cli 8.5.0__py3-none-any.whl → 9.0.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.
- devflow_cli/__init__.py +1 -1
- devflow_cli/commands/adaptive.py +78 -68
- devflow_cli/commands/status.py +1 -1
- devflow_cli/core/activity_base.py +96 -7
- devflow_cli/core/activity_contracts.py +3 -2
- devflow_cli/core/adaptive_runner.py +96 -142
- devflow_cli/core/adaptive_workflow.py +99 -48
- devflow_cli/core/automatic_evidence.py +5 -1
- devflow_cli/core/contract_schemas.py +43 -3
- devflow_cli/core/feedback.py +2 -0
- devflow_cli/core/remediation.py +109 -14
- devflow_cli/core/worklog.py +10 -4
- devflow_cli/schemas/{activity-result-v1.schema.json → activity-result-v2.schema.json} +3 -3
- {devflow_cli-8.5.0.dist-info → devflow_cli-9.0.0.dist-info}/METADATA +10 -1
- {devflow_cli-8.5.0.dist-info → devflow_cli-9.0.0.dist-info}/RECORD +18 -18
- {devflow_cli-8.5.0.dist-info → devflow_cli-9.0.0.dist-info}/WHEEL +0 -0
- {devflow_cli-8.5.0.dist-info → devflow_cli-9.0.0.dist-info}/entry_points.txt +0 -0
- {devflow_cli-8.5.0.dist-info → devflow_cli-9.0.0.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py
CHANGED
devflow_cli/commands/adaptive.py
CHANGED
|
@@ -17,6 +17,7 @@ from devflow_cli import VERSION
|
|
|
17
17
|
from devflow_cli.adapters.linear import sync_activity_status
|
|
18
18
|
from devflow_cli.core.adaptive_workflow import (
|
|
19
19
|
AdaptiveWorkflowState,
|
|
20
|
+
adaptive_state_violation,
|
|
20
21
|
complete_activity,
|
|
21
22
|
create_adaptive_workflow,
|
|
22
23
|
read_adaptive_state,
|
|
@@ -75,7 +76,11 @@ from devflow_cli.core.validation_approvals import (
|
|
|
75
76
|
pending_approvals,
|
|
76
77
|
pending_plan_approval,
|
|
77
78
|
)
|
|
78
|
-
from devflow_cli.core.contract_schemas import
|
|
79
|
+
from devflow_cli.core.contract_schemas import (
|
|
80
|
+
LegacySchemaError,
|
|
81
|
+
check_feature_files,
|
|
82
|
+
diagnose_contract_file,
|
|
83
|
+
)
|
|
79
84
|
from devflow_cli.core.operational import ExitCode, exit_code_for_run_reason
|
|
80
85
|
from devflow_cli.core.result_template import (
|
|
81
86
|
remove_result_template,
|
|
@@ -148,8 +153,12 @@ def _state(
|
|
|
148
153
|
raise typer.Exit(LEGACY_SCHEMA_EXIT_CODE) from exc
|
|
149
154
|
if state is None:
|
|
150
155
|
diagnostic = diagnose_contract_file("workflow-state", state_file)
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
violation = None if diagnostic else adaptive_state_violation(state_file)
|
|
157
|
+
detail = diagnostic or (
|
|
158
|
+
"workflow-state.json respecte le schema mais viole le verrou "
|
|
159
|
+
f"{violation}."
|
|
160
|
+
if violation
|
|
161
|
+
else "workflow-state.json respecte le schema mais ses invariants "
|
|
153
162
|
"métier sont incoherents."
|
|
154
163
|
)
|
|
155
164
|
if json_output:
|
|
@@ -191,6 +200,7 @@ def start(feature: str = typer.Argument(..., help="ID ou dossier de la feature")
|
|
|
191
200
|
fail("workflow-state.json existe deja ; le workflow ne sera pas ecrase.")
|
|
192
201
|
console.print(f" Suite : devflow adaptive status {feature}")
|
|
193
202
|
raise typer.Exit(int(ExitCode.CONCURRENT_CHANGE))
|
|
203
|
+
check_feature_files(feature_dir)
|
|
194
204
|
try:
|
|
195
205
|
assessment = _assessment(feature_dir)
|
|
196
206
|
validate_need_statement(feature_dir / NEED_STATEMENT)
|
|
@@ -198,10 +208,10 @@ def start(feature: str = typer.Argument(..., help="ID ou dossier de la feature")
|
|
|
198
208
|
enter_current_activity(
|
|
199
209
|
state, project_root_for_feature(feature_dir), feature_dir
|
|
200
210
|
)
|
|
211
|
+
write_adaptive_state(state_file, state)
|
|
201
212
|
except (OSError, ValueError) as exc:
|
|
202
213
|
fail(f"Impossible de demarrer le workflow : {exc}")
|
|
203
214
|
raise typer.Exit(int(ExitCode.INVALID_CONTRACT)) from exc
|
|
204
|
-
write_adaptive_state(state_file, state)
|
|
205
215
|
safe_refresh_worklog(feature_dir)
|
|
206
216
|
ok(
|
|
207
217
|
f"Workflow {state.profile} demarre. "
|
|
@@ -256,11 +266,11 @@ def complete(
|
|
|
256
266
|
enter_current_activity(
|
|
257
267
|
state, project_root_for_feature(feature_dir), feature_dir
|
|
258
268
|
)
|
|
269
|
+
write_adaptive_state(feature_dir / "workflow-state.json", state)
|
|
259
270
|
except (OSError, ValueError) as exc:
|
|
260
271
|
fail(str(exc))
|
|
261
272
|
console.print(f" Suite : devflow adaptive status {state.issueId}")
|
|
262
273
|
raise typer.Exit(int(ExitCode.BLOCKED)) from exc
|
|
263
|
-
write_adaptive_state(feature_dir / "workflow-state.json", state)
|
|
264
274
|
safe_refresh_worklog(feature_dir)
|
|
265
275
|
if activity == "done":
|
|
266
276
|
sync_activity_status(state.issueId, "done", run_progress.progress_stream(False))
|
|
@@ -284,10 +294,10 @@ def evidence(
|
|
|
284
294
|
state = _state(feature_dir)
|
|
285
295
|
try:
|
|
286
296
|
record_evidence(state, evidence_id, report or "")
|
|
297
|
+
write_adaptive_state(feature_dir / "workflow-state.json", state)
|
|
287
298
|
except ValueError as exc:
|
|
288
299
|
fail(str(exc))
|
|
289
300
|
raise typer.Exit(int(ExitCode.BLOCKED)) from exc
|
|
290
|
-
write_adaptive_state(feature_dir / "workflow-state.json", state)
|
|
291
301
|
safe_refresh_worklog(feature_dir)
|
|
292
302
|
ok(f"Preuve '{evidence_id}' enregistree.")
|
|
293
303
|
warn("Source manuelle : cette preuve ne valide pas les gates du workflow.")
|
|
@@ -478,7 +488,7 @@ def run_adaptive(
|
|
|
478
488
|
}
|
|
479
489
|
_write_json_atomic(feature_dir / "agent-execution.json", audit_payload)
|
|
480
490
|
except (OSError, ValueError) as exc:
|
|
481
|
-
return AgentActivityExecution(1, stderr=str(exc))
|
|
491
|
+
return AgentActivityExecution(1, stderr=str(exc), agent=agent)
|
|
482
492
|
return AgentActivityExecution(
|
|
483
493
|
execution.returncode,
|
|
484
494
|
output,
|
|
@@ -528,6 +538,8 @@ def run_adaptive(
|
|
|
528
538
|
recovery = _stale_preflight_recovery(current_state)
|
|
529
539
|
if outcome.reason == "artifact-files-mismatch":
|
|
530
540
|
recovery = _artifact_mismatch_recovery(current_state, outcome, agent)
|
|
541
|
+
if outcome.reason == "imputed-input":
|
|
542
|
+
recovery = _imputed_input_recovery(current_state, outcome, agent)
|
|
531
543
|
if json_output:
|
|
532
544
|
_print_execution_json(
|
|
533
545
|
current_state.issueId, outcome.activity, False, outcome.reason,
|
|
@@ -716,6 +728,14 @@ def reconcile_adaptive(
|
|
|
716
728
|
state, outcome, _attempt_agent(feature_dir, outcome.activity)
|
|
717
729
|
)
|
|
718
730
|
)
|
|
731
|
+
if outcome.reason == "imputed-input":
|
|
732
|
+
console.print(" Reprise guidee :")
|
|
733
|
+
_print_recovery_steps(
|
|
734
|
+
_imputed_input_recovery(
|
|
735
|
+
state, outcome, _attempt_agent(feature_dir, outcome.activity),
|
|
736
|
+
reconcile=True,
|
|
737
|
+
)
|
|
738
|
+
)
|
|
719
739
|
if (
|
|
720
740
|
outcome.reason == "reconcile-refused"
|
|
721
741
|
and activity_attempt_file(feature_dir, outcome.activity).is_file()
|
|
@@ -903,11 +923,11 @@ def reopen(
|
|
|
903
923
|
enter_current_activity(
|
|
904
924
|
state, project_root_for_feature(feature_dir), feature_dir
|
|
905
925
|
)
|
|
926
|
+
write_adaptive_state(feature_dir / "workflow-state.json", state)
|
|
906
927
|
except (OSError, ValueError) as exc:
|
|
907
928
|
fail(str(exc))
|
|
908
929
|
console.print(f" Suite : devflow adaptive status {state.issueId}")
|
|
909
930
|
raise typer.Exit(int(ExitCode.BLOCKED)) from exc
|
|
910
|
-
write_adaptive_state(feature_dir / "workflow-state.json", state)
|
|
911
931
|
safe_refresh_worklog(feature_dir)
|
|
912
932
|
ok(
|
|
913
933
|
f"Workflow rouvert sur '{activity}' (audit #{entry['sequence']}, "
|
|
@@ -1605,10 +1625,41 @@ def _stale_preflight_recovery(state: AdaptiveWorkflowState) -> dict:
|
|
|
1605
1625
|
ARTIFACT_FILE_CAUSES = {
|
|
1606
1626
|
"during-agent": "modifie pendant l'agent",
|
|
1607
1627
|
"before-launch": "modifie avant le lancement de l'agent",
|
|
1608
|
-
"agent-input": "entree du besoin modifiee par un agent de cette activite",
|
|
1609
1628
|
}
|
|
1610
1629
|
|
|
1611
1630
|
|
|
1631
|
+
def _imputed_input_recovery(
|
|
1632
|
+
state: AdaptiveWorkflowState, outcome: AdaptiveRunOutcome, agent: str,
|
|
1633
|
+
*, reconcile: bool = False,
|
|
1634
|
+
) -> dict:
|
|
1635
|
+
"""Reprise guidee d'un refus `imputed-input` : remise en etat ou abandon audite."""
|
|
1636
|
+
issue = shlex.quote(state.issueId)
|
|
1637
|
+
not_done = "reconcile refuse" if reconcile else "l'agent n'a pas ete lance"
|
|
1638
|
+
return {
|
|
1639
|
+
"kind": "imputed-input",
|
|
1640
|
+
"summary": (
|
|
1641
|
+
"Entree du besoin modifiee par un agent de cette activite et pas remise "
|
|
1642
|
+
f"en etat : {not_done}. Deux sorties : remettre l'entree dans son etat de "
|
|
1643
|
+
"base, ou abandonner la tentative (l'entree modifiee est gardee, a verifier)."
|
|
1644
|
+
),
|
|
1645
|
+
"files": [{"path": path} for path in outcome.imputedInputs],
|
|
1646
|
+
"commands": [
|
|
1647
|
+
{
|
|
1648
|
+
"command": f"devflow adaptive run {issue}"
|
|
1649
|
+
+ (f" --agent {shlex.quote(agent)}" if agent else ""),
|
|
1650
|
+
"purpose": "apres remise en etat de l'entree dans son etat de base",
|
|
1651
|
+
},
|
|
1652
|
+
{
|
|
1653
|
+
"command": f"devflow adaptive discard-attempt {issue} --reason MOTIF",
|
|
1654
|
+
"purpose": (
|
|
1655
|
+
"sinon, garder l'entree modifiee : abandon audite de la "
|
|
1656
|
+
"tentative, puis devflow adaptive run"
|
|
1657
|
+
),
|
|
1658
|
+
},
|
|
1659
|
+
],
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
|
|
1612
1663
|
def _artifact_mismatch_recovery(
|
|
1613
1664
|
state: AdaptiveWorkflowState, outcome: AdaptiveRunOutcome, agent: str
|
|
1614
1665
|
) -> dict:
|
|
@@ -1633,34 +1684,6 @@ def _artifact_mismatch_recovery(
|
|
|
1633
1684
|
"purpose": "relancer l'agent pour qu'il ecrive l'artefact",
|
|
1634
1685
|
}],
|
|
1635
1686
|
}
|
|
1636
|
-
if causes == {"agent-input"}:
|
|
1637
|
-
# Refus avant l'agent : ni reconcile ni reopen ne leve l'imputation.
|
|
1638
|
-
run = f"devflow adaptive run {issue}" + (
|
|
1639
|
-
f" --agent {shlex.quote(agent)}" if agent else ""
|
|
1640
|
-
)
|
|
1641
|
-
return {
|
|
1642
|
-
"kind": "artifact-files-mismatch",
|
|
1643
|
-
"summary": (
|
|
1644
|
-
"Entree du besoin modifiee par un agent de cette activite et pas remise "
|
|
1645
|
-
"en etat : l'agent n'a pas ete lance. Deux sorties : remettre l'entree "
|
|
1646
|
-
"dans son etat de base, ou abandonner la tentative (l'entree modifiee "
|
|
1647
|
-
"est gardee, a verifier)."
|
|
1648
|
-
),
|
|
1649
|
-
"files": files,
|
|
1650
|
-
"commands": [
|
|
1651
|
-
{
|
|
1652
|
-
"command": run,
|
|
1653
|
-
"purpose": "apres remise en etat de l'entree dans son etat de base",
|
|
1654
|
-
},
|
|
1655
|
-
{
|
|
1656
|
-
"command": f"devflow adaptive discard-attempt {issue} --reason MOTIF",
|
|
1657
|
-
"purpose": (
|
|
1658
|
-
"sinon, garder l'entree modifiee : abandon audite de la "
|
|
1659
|
-
"tentative, puis devflow adaptive run"
|
|
1660
|
-
),
|
|
1661
|
-
},
|
|
1662
|
-
],
|
|
1663
|
-
}
|
|
1664
1687
|
commands = [{
|
|
1665
1688
|
"command": f"devflow adaptive reconcile {issue}",
|
|
1666
1689
|
"purpose": (
|
|
@@ -1672,7 +1695,6 @@ def _artifact_mismatch_recovery(
|
|
|
1672
1695
|
previous = state.activities[index - 1] if index > 0 else ""
|
|
1673
1696
|
if (
|
|
1674
1697
|
"before-launch" in causes
|
|
1675
|
-
and "agent-input" not in causes
|
|
1676
1698
|
and previous not in {"", "assess"}
|
|
1677
1699
|
and _reopenable(state, previous)
|
|
1678
1700
|
):
|
|
@@ -1683,14 +1705,6 @@ def _artifact_mismatch_recovery(
|
|
|
1683
1705
|
"l'activite precedente pour repartir d'une base fraiche"
|
|
1684
1706
|
),
|
|
1685
1707
|
})
|
|
1686
|
-
if "agent-input" in causes:
|
|
1687
|
-
commands.append({
|
|
1688
|
-
"command": f"devflow adaptive discard-attempt {issue} --reason MOTIF",
|
|
1689
|
-
"purpose": (
|
|
1690
|
-
"sinon, garder l'entree du besoin modifiee par l'agent : abandon "
|
|
1691
|
-
"audite de la tentative, puis devflow adaptive run"
|
|
1692
|
-
),
|
|
1693
|
-
})
|
|
1694
1708
|
return {
|
|
1695
1709
|
"kind": "artifact-files-mismatch",
|
|
1696
1710
|
"summary": (
|
|
@@ -1719,9 +1733,8 @@ def _print_recovery_steps(recovery: dict) -> None:
|
|
|
1719
1733
|
# soft_wrap : la commande a copier reste sur une seule ligne.
|
|
1720
1734
|
console.print(f" {recovery['summary']}", markup=False, soft_wrap=True)
|
|
1721
1735
|
for item in recovery.get("files", []):
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
)
|
|
1736
|
+
label = f" : {item['label']}" if "label" in item else ""
|
|
1737
|
+
console.print(f" {item['path']}{label}", markup=False, soft_wrap=True)
|
|
1725
1738
|
for step in recovery["commands"]:
|
|
1726
1739
|
console.print(
|
|
1727
1740
|
f" {step['command']} # {step['purpose']}", markup=False, soft_wrap=True
|
|
@@ -1777,25 +1790,22 @@ def _print_run_recovery(
|
|
|
1777
1790
|
" Les entrees du besoin ont change pendant l'execution. "
|
|
1778
1791
|
"Verifiez-les avant une nouvelle tentative."
|
|
1779
1792
|
)
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
)
|
|
1797
|
-
return
|
|
1798
|
-
console.print(f" devflow adaptive run {issue_id} --agent {agent}")
|
|
1793
|
+
console.print(
|
|
1794
|
+
" La relance reste refusee tant que l'entree "
|
|
1795
|
+
"n'est pas remise dans son etat de base, ou la tentative abandonnee :",
|
|
1796
|
+
markup=False, soft_wrap=True,
|
|
1797
|
+
)
|
|
1798
|
+
issue, agent_arg = shlex.quote(issue_id), shlex.quote(agent)
|
|
1799
|
+
console.print(
|
|
1800
|
+
f" devflow adaptive discard-attempt {issue} --reason MOTIF"
|
|
1801
|
+
" # garder l'entree modifiee : abandon audite",
|
|
1802
|
+
markup=False, soft_wrap=True,
|
|
1803
|
+
)
|
|
1804
|
+
console.print(
|
|
1805
|
+
f" devflow adaptive run {issue} --agent {agent_arg}"
|
|
1806
|
+
" # apres remise en etat de l'entree, ou apres l'abandon",
|
|
1807
|
+
markup=False, soft_wrap=True,
|
|
1808
|
+
)
|
|
1799
1809
|
return
|
|
1800
1810
|
if reason == "agent-running":
|
|
1801
1811
|
console.print(" Un agent execute deja cette activite : attendez sa fin.")
|
devflow_cli/commands/status.py
CHANGED
|
@@ -8,6 +8,7 @@ la declaration de l'agent ne produit qu'un avertissement.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import copy
|
|
11
|
+
import json
|
|
11
12
|
import re
|
|
12
13
|
import subprocess
|
|
13
14
|
from dataclasses import dataclass
|
|
@@ -16,6 +17,7 @@ from pathlib import Path
|
|
|
16
17
|
from typing import TYPE_CHECKING, Any, Iterable
|
|
17
18
|
|
|
18
19
|
from devflow_cli.adapters.linear import environment_without_linear_key
|
|
20
|
+
from devflow_cli.core.contract_schemas import ATTEMPT_SCHEMA_VERSION
|
|
19
21
|
from devflow_cli.core.worktree_snapshot import (
|
|
20
22
|
_fingerprint,
|
|
21
23
|
_git_paths,
|
|
@@ -24,7 +26,7 @@ from devflow_cli.core.worktree_snapshot import (
|
|
|
24
26
|
snapshot_worktree,
|
|
25
27
|
untracked_paths,
|
|
26
28
|
)
|
|
27
|
-
from devflow_cli.utils.paths import project_root_for_feature
|
|
29
|
+
from devflow_cli.utils.paths import activity_attempt_file, project_root_for_feature
|
|
28
30
|
|
|
29
31
|
if TYPE_CHECKING:
|
|
30
32
|
from devflow_cli.core.activity_contracts import ActivityContract, ActivityResult
|
|
@@ -49,6 +51,14 @@ RUNTIME_FILE_NAMES = frozenset({
|
|
|
49
51
|
RUNTIME_FILE_SUFFIXES = ("-result.json", "-attempt.json", ".raw.txt")
|
|
50
52
|
RUNTIME_DIRECTORIES = ("attempts", "evidence")
|
|
51
53
|
|
|
54
|
+
# Fiche de tentative (CT-04) : cles exactes, validees par ``attempt_violation`` seule.
|
|
55
|
+
ATTEMPT_KEYS = frozenset({
|
|
56
|
+
"schemaVersion", "contractId", "activity", "agent", "reason", "recordedAt",
|
|
57
|
+
"head", "branch", "rawOutputSha256", "observedFiles", "executionFiles",
|
|
58
|
+
"changedInputs",
|
|
59
|
+
})
|
|
60
|
+
RAW_OUTPUT_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
|
|
61
|
+
|
|
52
62
|
OBSERVATION_SCHEMA: dict[str, Any] = {
|
|
53
63
|
"type": "object",
|
|
54
64
|
"required": ["baseRecordedAt", "baseHead", "observedAt", "head"],
|
|
@@ -61,6 +71,7 @@ OBSERVATION_SCHEMA: dict[str, Any] = {
|
|
|
61
71
|
"type": "array", "items": {"type": "string", "minLength": 1}
|
|
62
72
|
},
|
|
63
73
|
"warning": {"type": "string", "minLength": 1},
|
|
74
|
+
"amendmentWarning": {"type": "string", "minLength": 1},
|
|
64
75
|
},
|
|
65
76
|
"additionalProperties": False,
|
|
66
77
|
"dependentRequired": {
|
|
@@ -378,7 +389,8 @@ def recorded_observation(
|
|
|
378
389
|
"""Bloc ``observation`` ecrit par la CLI seule dans le resultat enregistre.
|
|
379
390
|
|
|
380
391
|
``exempted`` : entrees du besoin amendees avant l'agent, hors ecart de
|
|
381
|
-
declaration ; ``amendment_warning`` les nomme dans la cle ``
|
|
392
|
+
declaration ; ``amendment_warning`` les nomme dans la cle ``amendmentWarning``,
|
|
393
|
+
distincte de ``warning`` (ecart de declaration seul).
|
|
382
394
|
"""
|
|
383
395
|
observed = sorted(set(observation.sinceBase) - set(exempted))
|
|
384
396
|
block: dict[str, Any] = {
|
|
@@ -387,17 +399,94 @@ def recorded_observation(
|
|
|
387
399
|
"observedAt": observation.observedAt,
|
|
388
400
|
"head": observation.head,
|
|
389
401
|
}
|
|
390
|
-
warning =
|
|
391
|
-
message
|
|
392
|
-
for message in (declaration_warning(declared, observed), amendment_warning)
|
|
393
|
-
if message
|
|
394
|
-
)
|
|
402
|
+
warning = declaration_warning(declared, observed)
|
|
395
403
|
if warning:
|
|
396
404
|
block["declaredChangedFiles"] = list(declared)
|
|
397
405
|
block["warning"] = warning
|
|
406
|
+
if amendment_warning:
|
|
407
|
+
block["amendmentWarning"] = amendment_warning
|
|
398
408
|
return block
|
|
399
409
|
|
|
400
410
|
|
|
411
|
+
def attempt_violation(attempt: object) -> str | None:
|
|
412
|
+
"""Premier invariant A1 a A10 viole par une fiche de tentative, ou None (CT-04).
|
|
413
|
+
|
|
414
|
+
Seule validation de la fiche : l'ecriture et la lecture l'appellent.
|
|
415
|
+
"""
|
|
416
|
+
prefix = "fiche de tentative : "
|
|
417
|
+
if not isinstance(attempt, dict):
|
|
418
|
+
return prefix + "objet attendu"
|
|
419
|
+
missing = sorted(ATTEMPT_KEYS - set(attempt))
|
|
420
|
+
if missing:
|
|
421
|
+
return prefix + "cles manquantes : " + ", ".join(missing)
|
|
422
|
+
extra = sorted(set(attempt) - ATTEMPT_KEYS)
|
|
423
|
+
if extra:
|
|
424
|
+
return prefix + "cles en trop : " + ", ".join(extra)
|
|
425
|
+
version = attempt["schemaVersion"]
|
|
426
|
+
if isinstance(version, bool) or version != ATTEMPT_SCHEMA_VERSION:
|
|
427
|
+
return prefix + f"schemaVersion {ATTEMPT_SCHEMA_VERSION} attendu"
|
|
428
|
+
for key in ("contractId", "activity", "recordedAt", "head", "branch"):
|
|
429
|
+
if not isinstance(attempt[key], str):
|
|
430
|
+
return prefix + f"{key} doit etre une chaine"
|
|
431
|
+
for key in ("contractId", "activity", "agent", "reason", "recordedAt"):
|
|
432
|
+
if not isinstance(attempt[key], str) or not attempt[key]:
|
|
433
|
+
return prefix + f"{key} vide"
|
|
434
|
+
raw_sha = attempt["rawOutputSha256"]
|
|
435
|
+
if not isinstance(raw_sha, str) or not RAW_OUTPUT_SHA256_PATTERN.fullmatch(raw_sha):
|
|
436
|
+
return prefix + "rawOutputSha256 invalide"
|
|
437
|
+
observed = attempt["observedFiles"]
|
|
438
|
+
if not isinstance(observed, dict) or not all(
|
|
439
|
+
isinstance(path, str) and isinstance(fingerprint, str)
|
|
440
|
+
for path, fingerprint in observed.items()
|
|
441
|
+
):
|
|
442
|
+
return prefix + "observedFiles invalide"
|
|
443
|
+
execution_files = attempt["executionFiles"]
|
|
444
|
+
if not isinstance(execution_files, list) or not all(
|
|
445
|
+
isinstance(item, str) for item in execution_files
|
|
446
|
+
):
|
|
447
|
+
return prefix + "executionFiles invalide"
|
|
448
|
+
changed = attempt["changedInputs"]
|
|
449
|
+
if (
|
|
450
|
+
not isinstance(changed, list)
|
|
451
|
+
or not all(isinstance(item, str) and item for item in changed)
|
|
452
|
+
or changed != sorted(set(changed))
|
|
453
|
+
):
|
|
454
|
+
return prefix + "changedInputs invalide"
|
|
455
|
+
if attempt["head"] == "" and attempt["branch"] == "" and observed == {} and not changed:
|
|
456
|
+
return prefix + "fiche minimale incoherente"
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def imputed_inputs(
|
|
461
|
+
feature_dir: Path,
|
|
462
|
+
feature_path: str,
|
|
463
|
+
activity: str,
|
|
464
|
+
contract_id: str,
|
|
465
|
+
since_base: dict[str, str],
|
|
466
|
+
) -> list[str]:
|
|
467
|
+
"""Entrees que la fiche de tentative impute a un agent de l'activite (CT-01).
|
|
468
|
+
|
|
469
|
+
Seules comptent celles qui different encore de la base : une entree remise en
|
|
470
|
+
etat n'est plus imputee. Une fiche absente, illisible, invalide, d'une autre
|
|
471
|
+
activite ou d'un autre contrat n'impute rien. Aucune ecriture.
|
|
472
|
+
"""
|
|
473
|
+
try:
|
|
474
|
+
attempt = json.loads(
|
|
475
|
+
activity_attempt_file(feature_dir, activity).read_text(encoding="utf-8")
|
|
476
|
+
)
|
|
477
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
478
|
+
return []
|
|
479
|
+
if attempt_violation(attempt) is not None:
|
|
480
|
+
return []
|
|
481
|
+
if attempt["activity"] != activity or attempt["contractId"] != contract_id:
|
|
482
|
+
return []
|
|
483
|
+
return sorted(
|
|
484
|
+
name
|
|
485
|
+
for name in attempt["changedInputs"]
|
|
486
|
+
if f"{feature_path}/{name}" in since_base
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
|
|
401
490
|
def parse_recorded_result(
|
|
402
491
|
payload: object, contract: "ActivityContract"
|
|
403
492
|
) -> tuple["ActivityResult", dict[str, Any]]:
|
|
@@ -19,6 +19,7 @@ from devflow_cli.core.adaptive_workflow import (
|
|
|
19
19
|
from devflow_cli.core.assessment import ASSESSMENT_SCHEMA_VERSION, compute_route
|
|
20
20
|
from devflow_cli.core.contract_schemas import (
|
|
21
21
|
activity_result_schema,
|
|
22
|
+
contract_schema_version,
|
|
22
23
|
validate_contract_payload,
|
|
23
24
|
)
|
|
24
25
|
from devflow_cli.core.validators import ARTIFACT_RULES
|
|
@@ -573,7 +574,7 @@ def parse_activity_result(payload: dict[str, Any], contract: ActivityContract) -
|
|
|
573
574
|
expected_fields.add("validationPlan")
|
|
574
575
|
if expected_fields != set(payload):
|
|
575
576
|
raise ValueError("champs du resultat manquants ou inattendus")
|
|
576
|
-
if payload.get("schemaVersion") !=
|
|
577
|
+
if payload.get("schemaVersion") != contract_schema_version("activity-result"):
|
|
577
578
|
raise ValueError("schema de resultat non supporte")
|
|
578
579
|
if payload.get("contractId") != contract.contractId:
|
|
579
580
|
raise ValueError("contractId du resultat incorrect")
|
|
@@ -678,7 +679,7 @@ def parse_activity_result(payload: dict[str, Any], contract: ActivityContract) -
|
|
|
678
679
|
)
|
|
679
680
|
|
|
680
681
|
return ActivityResult(
|
|
681
|
-
schemaVersion=
|
|
682
|
+
schemaVersion=contract_schema_version("activity-result"),
|
|
682
683
|
contractId=contract.contractId,
|
|
683
684
|
status=status,
|
|
684
685
|
summary=summary.strip(),
|