devflow-cli 8.5.1__py3-none-any.whl → 9.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.
- devflow_cli/__init__.py +1 -1
- devflow_cli/commands/adaptive.py +68 -63
- devflow_cli/commands/feature.py +22 -0
- 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 +95 -141
- devflow_cli/core/adaptive_workflow.py +4 -2
- devflow_cli/core/automatic_evidence.py +1 -1
- devflow_cli/core/contract_schemas.py +43 -3
- devflow_cli/core/feedback.py +2 -0
- devflow_cli/core/remediation.py +67 -2
- devflow_cli/core/worklog.py +37 -10
- devflow_cli/schemas/{activity-result-v1.schema.json → activity-result-v2.schema.json} +3 -3
- {devflow_cli-8.5.1.dist-info → devflow_cli-9.0.1.dist-info}/METADATA +10 -1
- {devflow_cli-8.5.1.dist-info → devflow_cli-9.0.1.dist-info}/RECORD +19 -19
- {devflow_cli-8.5.1.dist-info → devflow_cli-9.0.1.dist-info}/WHEEL +0 -0
- {devflow_cli-8.5.1.dist-info → devflow_cli-9.0.1.dist-info}/entry_points.txt +0 -0
- {devflow_cli-8.5.1.dist-info → devflow_cli-9.0.1.dist-info}/licenses/LICENSE +0 -0
devflow_cli/__init__.py
CHANGED
devflow_cli/commands/adaptive.py
CHANGED
|
@@ -76,7 +76,11 @@ from devflow_cli.core.validation_approvals import (
|
|
|
76
76
|
pending_approvals,
|
|
77
77
|
pending_plan_approval,
|
|
78
78
|
)
|
|
79
|
-
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
|
+
)
|
|
80
84
|
from devflow_cli.core.operational import ExitCode, exit_code_for_run_reason
|
|
81
85
|
from devflow_cli.core.result_template import (
|
|
82
86
|
remove_result_template,
|
|
@@ -196,6 +200,7 @@ def start(feature: str = typer.Argument(..., help="ID ou dossier de la feature")
|
|
|
196
200
|
fail("workflow-state.json existe deja ; le workflow ne sera pas ecrase.")
|
|
197
201
|
console.print(f" Suite : devflow adaptive status {feature}")
|
|
198
202
|
raise typer.Exit(int(ExitCode.CONCURRENT_CHANGE))
|
|
203
|
+
check_feature_files(feature_dir)
|
|
199
204
|
try:
|
|
200
205
|
assessment = _assessment(feature_dir)
|
|
201
206
|
validate_need_statement(feature_dir / NEED_STATEMENT)
|
|
@@ -483,7 +488,7 @@ def run_adaptive(
|
|
|
483
488
|
}
|
|
484
489
|
_write_json_atomic(feature_dir / "agent-execution.json", audit_payload)
|
|
485
490
|
except (OSError, ValueError) as exc:
|
|
486
|
-
return AgentActivityExecution(1, stderr=str(exc))
|
|
491
|
+
return AgentActivityExecution(1, stderr=str(exc), agent=agent)
|
|
487
492
|
return AgentActivityExecution(
|
|
488
493
|
execution.returncode,
|
|
489
494
|
output,
|
|
@@ -533,6 +538,8 @@ def run_adaptive(
|
|
|
533
538
|
recovery = _stale_preflight_recovery(current_state)
|
|
534
539
|
if outcome.reason == "artifact-files-mismatch":
|
|
535
540
|
recovery = _artifact_mismatch_recovery(current_state, outcome, agent)
|
|
541
|
+
if outcome.reason == "imputed-input":
|
|
542
|
+
recovery = _imputed_input_recovery(current_state, outcome, agent)
|
|
536
543
|
if json_output:
|
|
537
544
|
_print_execution_json(
|
|
538
545
|
current_state.issueId, outcome.activity, False, outcome.reason,
|
|
@@ -721,6 +728,14 @@ def reconcile_adaptive(
|
|
|
721
728
|
state, outcome, _attempt_agent(feature_dir, outcome.activity)
|
|
722
729
|
)
|
|
723
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
|
+
)
|
|
724
739
|
if (
|
|
725
740
|
outcome.reason == "reconcile-refused"
|
|
726
741
|
and activity_attempt_file(feature_dir, outcome.activity).is_file()
|
|
@@ -1336,7 +1351,7 @@ def _operational_status(
|
|
|
1336
1351
|
) -> dict:
|
|
1337
1352
|
result_file, result = _latest_activity_result(feature_dir, state.currentActivity)
|
|
1338
1353
|
blockers = result.get("blockers", []) if isinstance(result, dict) else []
|
|
1339
|
-
risks = latest_remaining_risks(feature_dir, state
|
|
1354
|
+
risks = latest_remaining_risks(feature_dir, state)
|
|
1340
1355
|
if not isinstance(blockers, list):
|
|
1341
1356
|
blockers = []
|
|
1342
1357
|
stale: set[str] = set()
|
|
@@ -1610,10 +1625,41 @@ def _stale_preflight_recovery(state: AdaptiveWorkflowState) -> dict:
|
|
|
1610
1625
|
ARTIFACT_FILE_CAUSES = {
|
|
1611
1626
|
"during-agent": "modifie pendant l'agent",
|
|
1612
1627
|
"before-launch": "modifie avant le lancement de l'agent",
|
|
1613
|
-
"agent-input": "entree du besoin modifiee par un agent de cette activite",
|
|
1614
1628
|
}
|
|
1615
1629
|
|
|
1616
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
|
+
|
|
1617
1663
|
def _artifact_mismatch_recovery(
|
|
1618
1664
|
state: AdaptiveWorkflowState, outcome: AdaptiveRunOutcome, agent: str
|
|
1619
1665
|
) -> dict:
|
|
@@ -1638,34 +1684,6 @@ def _artifact_mismatch_recovery(
|
|
|
1638
1684
|
"purpose": "relancer l'agent pour qu'il ecrive l'artefact",
|
|
1639
1685
|
}],
|
|
1640
1686
|
}
|
|
1641
|
-
if causes == {"agent-input"}:
|
|
1642
|
-
# Refus avant l'agent : ni reconcile ni reopen ne leve l'imputation.
|
|
1643
|
-
run = f"devflow adaptive run {issue}" + (
|
|
1644
|
-
f" --agent {shlex.quote(agent)}" if agent else ""
|
|
1645
|
-
)
|
|
1646
|
-
return {
|
|
1647
|
-
"kind": "artifact-files-mismatch",
|
|
1648
|
-
"summary": (
|
|
1649
|
-
"Entree du besoin modifiee par un agent de cette activite et pas remise "
|
|
1650
|
-
"en etat : l'agent n'a pas ete lance. Deux sorties : remettre l'entree "
|
|
1651
|
-
"dans son etat de base, ou abandonner la tentative (l'entree modifiee "
|
|
1652
|
-
"est gardee, a verifier)."
|
|
1653
|
-
),
|
|
1654
|
-
"files": files,
|
|
1655
|
-
"commands": [
|
|
1656
|
-
{
|
|
1657
|
-
"command": run,
|
|
1658
|
-
"purpose": "apres remise en etat de l'entree dans son etat de base",
|
|
1659
|
-
},
|
|
1660
|
-
{
|
|
1661
|
-
"command": f"devflow adaptive discard-attempt {issue} --reason MOTIF",
|
|
1662
|
-
"purpose": (
|
|
1663
|
-
"sinon, garder l'entree modifiee : abandon audite de la "
|
|
1664
|
-
"tentative, puis devflow adaptive run"
|
|
1665
|
-
),
|
|
1666
|
-
},
|
|
1667
|
-
],
|
|
1668
|
-
}
|
|
1669
1687
|
commands = [{
|
|
1670
1688
|
"command": f"devflow adaptive reconcile {issue}",
|
|
1671
1689
|
"purpose": (
|
|
@@ -1677,7 +1695,6 @@ def _artifact_mismatch_recovery(
|
|
|
1677
1695
|
previous = state.activities[index - 1] if index > 0 else ""
|
|
1678
1696
|
if (
|
|
1679
1697
|
"before-launch" in causes
|
|
1680
|
-
and "agent-input" not in causes
|
|
1681
1698
|
and previous not in {"", "assess"}
|
|
1682
1699
|
and _reopenable(state, previous)
|
|
1683
1700
|
):
|
|
@@ -1688,14 +1705,6 @@ def _artifact_mismatch_recovery(
|
|
|
1688
1705
|
"l'activite precedente pour repartir d'une base fraiche"
|
|
1689
1706
|
),
|
|
1690
1707
|
})
|
|
1691
|
-
if "agent-input" in causes:
|
|
1692
|
-
commands.append({
|
|
1693
|
-
"command": f"devflow adaptive discard-attempt {issue} --reason MOTIF",
|
|
1694
|
-
"purpose": (
|
|
1695
|
-
"sinon, garder l'entree du besoin modifiee par l'agent : abandon "
|
|
1696
|
-
"audite de la tentative, puis devflow adaptive run"
|
|
1697
|
-
),
|
|
1698
|
-
})
|
|
1699
1708
|
return {
|
|
1700
1709
|
"kind": "artifact-files-mismatch",
|
|
1701
1710
|
"summary": (
|
|
@@ -1724,9 +1733,8 @@ def _print_recovery_steps(recovery: dict) -> None:
|
|
|
1724
1733
|
# soft_wrap : la commande a copier reste sur une seule ligne.
|
|
1725
1734
|
console.print(f" {recovery['summary']}", markup=False, soft_wrap=True)
|
|
1726
1735
|
for item in recovery.get("files", []):
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
)
|
|
1736
|
+
label = f" : {item['label']}" if "label" in item else ""
|
|
1737
|
+
console.print(f" {item['path']}{label}", markup=False, soft_wrap=True)
|
|
1730
1738
|
for step in recovery["commands"]:
|
|
1731
1739
|
console.print(
|
|
1732
1740
|
f" {step['command']} # {step['purpose']}", markup=False, soft_wrap=True
|
|
@@ -1782,25 +1790,22 @@ def _print_run_recovery(
|
|
|
1782
1790
|
" Les entrees du besoin ont change pendant l'execution. "
|
|
1783
1791
|
"Verifiez-les avant une nouvelle tentative."
|
|
1784
1792
|
)
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
)
|
|
1802
|
-
return
|
|
1803
|
-
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
|
+
)
|
|
1804
1809
|
return
|
|
1805
1810
|
if reason == "agent-running":
|
|
1806
1811
|
console.print(" Un agent execute deja cette activite : attendez sa fin.")
|
devflow_cli/commands/feature.py
CHANGED
|
@@ -58,6 +58,27 @@ def _ignore_agent_traces(feature_dir) -> None:
|
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
LOCK_IGNORE_RULES = (".workflow-state.lock", ".agent-run.lock", ".feature-lock")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _ignore_cli_locks(specs_root) -> None:
|
|
65
|
+
"""Exclut du depot les verrous de la CLI, a toute profondeur sous les specs."""
|
|
66
|
+
gitignore = specs_root / ".gitignore"
|
|
67
|
+
content = gitignore.read_text(encoding="utf-8") if gitignore.is_file() else ""
|
|
68
|
+
present = content.splitlines()
|
|
69
|
+
missing = [rule for rule in LOCK_IGNORE_RULES if rule not in present]
|
|
70
|
+
if not missing:
|
|
71
|
+
return
|
|
72
|
+
prefix = "" if not content or content.endswith("\n") else "\n"
|
|
73
|
+
gitignore.write_text(
|
|
74
|
+
content
|
|
75
|
+
+ prefix
|
|
76
|
+
+ "# Verrous de la CLI devflow : fichiers d'execution, jamais suivis.\n"
|
|
77
|
+
+ "".join(f"{rule}\n" for rule in missing),
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
61
82
|
def feature(
|
|
62
83
|
issue_id: str = typer.Argument(..., help="Identifiant de l'issue (ex: KS-123)"),
|
|
63
84
|
short_name: Optional[str] = typer.Option(None, "--short-name", help="Nom court pour le repertoire (ex: user-auth)"),
|
|
@@ -107,6 +128,7 @@ def feature(
|
|
|
107
128
|
feature_dir.mkdir(parents=True)
|
|
108
129
|
ok(f"Dossier {feature_dir} cree")
|
|
109
130
|
_ignore_agent_traces(feature_dir)
|
|
131
|
+
_ignore_cli_locks(specs_root)
|
|
110
132
|
|
|
111
133
|
console.print()
|
|
112
134
|
console.print(f"Feature [bold]{issue_id}[/bold] initialisee :")
|
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(),
|