esaa-core 0.5.0b1__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.
- audit/amend_claude_md.py +35 -0
- audit/bump_version.py +40 -0
- audit/contract_consistency.py +149 -0
- audit/critical_findings.py +269 -0
- audit/document_future_templates.py +46 -0
- audit/eventstore_integrity.py +73 -0
- audit/rename_parcer.py +42 -0
- audit/schema_conformance.py +64 -0
- audit/traceability_and_report.py +118 -0
- esaa/__init__.py +17 -0
- esaa/__main__.py +4 -0
- esaa/adapters/__init__.py +5 -0
- esaa/adapters/base.py +17 -0
- esaa/adapters/http_llm.py +63 -0
- esaa/adapters/mock.py +103 -0
- esaa/bootstrap.py +72 -0
- esaa/cli.py +571 -0
- esaa/compat.py +27 -0
- esaa/conflicts.py +45 -0
- esaa/constants.py +39 -0
- esaa/dispatch.py +187 -0
- esaa/errors.py +19 -0
- esaa/file_effects.py +388 -0
- esaa/metrics.py +131 -0
- esaa/plugins.py +666 -0
- esaa/projector.py +340 -0
- esaa/reject_codes.py +201 -0
- esaa/runner_metrics.py +114 -0
- esaa/runtime_policy.py +152 -0
- esaa/scenarios.py +172 -0
- esaa/service.py +2767 -0
- esaa/snapshot.py +159 -0
- esaa/state_machine.py +108 -0
- esaa/store.py +254 -0
- esaa/templates/AGENT_CONTRACT.yaml +312 -0
- esaa/templates/ORCHESTRATOR_CONTRACT.yaml +285 -0
- esaa/templates/RUNTIME_POLICY.yaml +36 -0
- esaa/templates/agent_result.schema.json +254 -0
- esaa/templates/agents_swarm.yaml +49 -0
- esaa/templates/esaa-plugin.schema.json +65 -0
- esaa/templates/issues.schema.json +192 -0
- esaa/templates/lessons.schema.json +174 -0
- esaa/templates/roadmap.schema.json +363 -0
- esaa/utils.py +29 -0
- esaa/validator.py +122 -0
- esaa/vocabulary.py +47 -0
- esaa_core-0.5.0b1.dist-info/METADATA +942 -0
- esaa_core-0.5.0b1.dist-info/RECORD +52 -0
- esaa_core-0.5.0b1.dist-info/WHEEL +5 -0
- esaa_core-0.5.0b1.dist-info/entry_points.txt +2 -0
- esaa_core-0.5.0b1.dist-info/licenses/LICENSE +21 -0
- esaa_core-0.5.0b1.dist-info/top_level.txt +2 -0
audit/amend_claude_md.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""FIX-1541 — Amenda CLAUDE.md substituindo 'enforcement.mode=reject' por '{reject, require_field, require_step}'.
|
|
3
|
+
|
|
4
|
+
LES-0003 tem enforcement.mode=require_field, entao a redacao original (`mode=reject`)
|
|
5
|
+
e estreita demais. Esta amenda reflete o conjunto real de modos enforcaveis.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import argparse
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> int:
|
|
13
|
+
ap = argparse.ArgumentParser()
|
|
14
|
+
ap.add_argument("--root", default=".")
|
|
15
|
+
args = ap.parse_args()
|
|
16
|
+
target = Path(args.root) / ".claude/CLAUDE.md"
|
|
17
|
+
if not target.exists():
|
|
18
|
+
print("CLAUDE.md not found; skipping")
|
|
19
|
+
return 0
|
|
20
|
+
txt = target.read_text(encoding="utf-8")
|
|
21
|
+
old = "Trate cada lesson com `enforcement.mode=reject` como **constraint inviolável**"
|
|
22
|
+
new = "Trate cada lesson com `enforcement.mode` em {`reject`, `require_field`, `require_step`} como **constraint inviolável**"
|
|
23
|
+
if old in txt:
|
|
24
|
+
txt = txt.replace(old, new)
|
|
25
|
+
target.write_text(txt, encoding="utf-8")
|
|
26
|
+
print("amended")
|
|
27
|
+
elif new in txt:
|
|
28
|
+
print("already amended")
|
|
29
|
+
else:
|
|
30
|
+
print("trecho nao encontrado; revise manualmente")
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
raise SystemExit(main())
|
audit/bump_version.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""FIX-1551 — Atualiza arquivos fora da boundary impl (pyproject.toml, roadmap.schema.json)
|
|
3
|
+
para alinhar a 0.4.1.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import argparse, json, re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> int:
|
|
11
|
+
ap = argparse.ArgumentParser()
|
|
12
|
+
ap.add_argument("--root", default=".")
|
|
13
|
+
args = ap.parse_args()
|
|
14
|
+
root = Path(args.root)
|
|
15
|
+
|
|
16
|
+
py = root / "pyproject.toml"
|
|
17
|
+
if py.exists():
|
|
18
|
+
txt = py.read_text(encoding="utf-8")
|
|
19
|
+
txt2 = re.sub(r'(?m)^version\s*=\s*"0\.4\.0"', 'version = "0.4.1"', txt)
|
|
20
|
+
if txt != txt2:
|
|
21
|
+
py.write_text(txt2, encoding="utf-8")
|
|
22
|
+
print("pyproject.toml -> 0.4.1")
|
|
23
|
+
else:
|
|
24
|
+
print("pyproject.toml ja em 0.4.1 ou nao encontrado padrao")
|
|
25
|
+
|
|
26
|
+
rsj = root / ".roadmap/roadmap.schema.json"
|
|
27
|
+
if rsj.exists():
|
|
28
|
+
data = json.loads(rsj.read_text(encoding="utf-8"))
|
|
29
|
+
sv = data.get("properties", {}).get("meta", {}).get("properties", {}).get("schema_version", {})
|
|
30
|
+
if sv.get("const") == "0.4.0":
|
|
31
|
+
sv["const"] = "0.4.1"
|
|
32
|
+
rsj.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
33
|
+
print("roadmap.schema.json const -> 0.4.1")
|
|
34
|
+
else:
|
|
35
|
+
print(f"roadmap.schema.json const ja em {sv.get('const')}")
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""AUD-1001 — Verificador de consistência contrato/documento.
|
|
3
|
+
|
|
4
|
+
Audita drift de versão e divergência de vocabulário entre os artefatos
|
|
5
|
+
canônicos do ESAA. Read-only. Saída: JSON de findings.
|
|
6
|
+
|
|
7
|
+
Uso: python contract_consistency.py --root <repo_root>
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
import argparse, json, re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# M-04: vocabulario canonico de reject_codes vem de src/esaa/reject_codes.py.
|
|
14
|
+
# Importacao defensiva: se PYTHONPATH=src nao estiver setado, cai para fallback
|
|
15
|
+
# textual (mantem o checker funcional fora do dev environment).
|
|
16
|
+
try:
|
|
17
|
+
import sys as _sys
|
|
18
|
+
_sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src"))
|
|
19
|
+
from esaa.reject_codes import ALL_CODES as _RC_ALL, WORKFLOW_GATE_CODES as _RC_WG
|
|
20
|
+
DOC_REJECT_CODES = frozenset(_RC_WG)
|
|
21
|
+
ENGINE_ERROR_CODES = frozenset(_RC_ALL)
|
|
22
|
+
except Exception:
|
|
23
|
+
DOC_REJECT_CODES = frozenset({
|
|
24
|
+
"MISSING_CLAIM", "MISSING_COMPLETE", "MISSING_VERIFICATION",
|
|
25
|
+
"PRIOR_STATUS_MISMATCH", "LOCK_VIOLATION", "ACTION_COLLAPSE",
|
|
26
|
+
"IMMUTABLE_DONE_VIOLATION",
|
|
27
|
+
})
|
|
28
|
+
ENGINE_ERROR_CODES = frozenset({
|
|
29
|
+
"SCHEMA_INVALID", "UNKNOWN_ACTION", "WORKFLOW_GATE_VIOLATION", "BOUNDARY_VIOLATION",
|
|
30
|
+
"IMMUTABLE_DONE_VIOLATION", "LOCK_VIOLATION", "TASK_NOT_FOUND",
|
|
31
|
+
"DUPLICATE_TASK", "ISSUE_NOT_FOUND",
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
def main() -> int:
|
|
35
|
+
ap = argparse.ArgumentParser()
|
|
36
|
+
ap.add_argument("--root", default=".")
|
|
37
|
+
args = ap.parse_args()
|
|
38
|
+
root = Path(args.root)
|
|
39
|
+
findings = []
|
|
40
|
+
|
|
41
|
+
def declared_version(path: str, pattern: str) -> str | None:
|
|
42
|
+
p = root / path
|
|
43
|
+
if not p.exists():
|
|
44
|
+
return None
|
|
45
|
+
m = re.search(pattern, p.read_text(encoding="utf-8"))
|
|
46
|
+
return m.group(1) if m else None
|
|
47
|
+
|
|
48
|
+
protocol_versions = {
|
|
49
|
+
"AGENT_CONTRACT.yaml": declared_version(".roadmap/AGENT_CONTRACT.yaml", r'contract_version:\s*"([^"]+)"'),
|
|
50
|
+
"ORCHESTRATOR_CONTRACT.yaml": declared_version(".roadmap/ORCHESTRATOR_CONTRACT.yaml", r'contract_version:\s*"([^"]+)"'),
|
|
51
|
+
"agent_result.schema.json": declared_version(".roadmap/agent_result.schema.json", r'v(0\.\d\.\d)'),
|
|
52
|
+
"constants.py(SCHEMA_VERSION)": declared_version("src/esaa/constants.py", r'SCHEMA_VERSION\s*=\s*"([^"]+)"'),
|
|
53
|
+
"roadmap.schema.json(const)": declared_version(".roadmap/roadmap.schema.json", r'"const":\s*"(0\.\d\.\d)"'),
|
|
54
|
+
}
|
|
55
|
+
package_version = declared_version("pyproject.toml", r'version\s*=\s*"([^"]+)"')
|
|
56
|
+
distinct = {v for v in protocol_versions.values() if v}
|
|
57
|
+
if len(distinct) > 1:
|
|
58
|
+
findings.append({
|
|
59
|
+
"id": "R3", "severity": "high",
|
|
60
|
+
"title": "Version drift entre contratos e engine/projeções",
|
|
61
|
+
"evidence": protocol_versions,
|
|
62
|
+
"recommendation": "Migrar contratos, schemas e constants.py para a mesma versao de protocolo.",
|
|
63
|
+
})
|
|
64
|
+
if package_version and not package_version.startswith("0.5.0"):
|
|
65
|
+
findings.append({
|
|
66
|
+
"id": "R-PACKAGE-VERSION",
|
|
67
|
+
"severity": "low",
|
|
68
|
+
"title": "Versao do pacote nao esta na linha publica beta esperada",
|
|
69
|
+
"evidence": {"pyproject.toml": package_version, "protocol_versions": protocol_versions},
|
|
70
|
+
"recommendation": "Manter pacote 0.5.0b1 enquanto o protocolo permanece em 0.4.1.",
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
# Convenção de nome dos PARCER profiles
|
|
74
|
+
roadmap_dir = root / ".roadmap"
|
|
75
|
+
if roadmap_dir.exists():
|
|
76
|
+
profiles = [p.name for p in roadmap_dir.glob("PARCER_PROFILE*")]
|
|
77
|
+
dot = [n for n in profiles if n.startswith("PARCER_PROFILE.")]
|
|
78
|
+
underscore = [n for n in profiles if n.startswith("PARCER_PROFILE_")]
|
|
79
|
+
if dot and underscore:
|
|
80
|
+
findings.append({
|
|
81
|
+
"id": "R10", "severity": "low",
|
|
82
|
+
"title": "Convencao de nome de PARCER profile inconsistente (ponto vs underscore)",
|
|
83
|
+
"evidence": {"dot": dot, "underscore": underscore},
|
|
84
|
+
"recommendation": "Padronizar para um unico separador.",
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
# FIX-1812: runner.metrics deve estar em todas as fontes canonicas
|
|
88
|
+
rm_sources = {
|
|
89
|
+
"AGENT_CONTRACT.yaml(reserved)": "runner.metrics" in (root / ".roadmap/AGENT_CONTRACT.yaml").read_text(encoding="utf-8") if (root / ".roadmap/AGENT_CONTRACT.yaml").exists() else False,
|
|
90
|
+
"ORCHESTRATOR_CONTRACT.yaml(reserved)": "runner.metrics" in (root / ".roadmap/ORCHESTRATOR_CONTRACT.yaml").read_text(encoding="utf-8") if (root / ".roadmap/ORCHESTRATOR_CONTRACT.yaml").exists() else False,
|
|
91
|
+
"constants.py(CANONICAL_ACTIONS)": "runner.metrics" in (root / "src/esaa/constants.py").read_text(encoding="utf-8") if (root / "src/esaa/constants.py").exists() else False,
|
|
92
|
+
"AGENTS.md": "runner.metrics" in (root / "AGENTS.md").read_text(encoding="utf-8") if (root / "AGENTS.md").exists() else False,
|
|
93
|
+
".claude/CLAUDE.md": "runner.metrics" in (root / ".claude/CLAUDE.md").read_text(encoding="utf-8") if (root / ".claude/CLAUDE.md").exists() else False,
|
|
94
|
+
}
|
|
95
|
+
missing_rm = [k for k, v in rm_sources.items() if not v]
|
|
96
|
+
if missing_rm:
|
|
97
|
+
findings.append({
|
|
98
|
+
"id": "R-RUNNER-METRICS-DRIFT", "severity": "medium",
|
|
99
|
+
"title": "runner.metrics ausente em fonte canonica",
|
|
100
|
+
"evidence": {"missing_in": missing_rm},
|
|
101
|
+
"recommendation": "Adicionar runner.metrics a todas as fontes do vocabulario reservado.",
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
# M-05: prior_status enum drift entre contract YAML e schema JSON
|
|
105
|
+
try:
|
|
106
|
+
import yaml as _yaml
|
|
107
|
+
ag_path = root / ".roadmap/AGENT_CONTRACT.yaml"
|
|
108
|
+
sch_path = root / ".roadmap/agent_result.schema.json"
|
|
109
|
+
if ag_path.exists() and sch_path.exists():
|
|
110
|
+
ag = _yaml.safe_load(ag_path.read_text(encoding="utf-8")) or {}
|
|
111
|
+
sch = json.loads(sch_path.read_text(encoding="utf-8"))
|
|
112
|
+
yaml_allowed = set(
|
|
113
|
+
ag.get("output_contract", {})
|
|
114
|
+
.get("activity_event", {})
|
|
115
|
+
.get("prior_status", {})
|
|
116
|
+
.get("allowed_values", [])
|
|
117
|
+
)
|
|
118
|
+
schema_enum = set(
|
|
119
|
+
sch.get("properties", {})
|
|
120
|
+
.get("activity_event", {})
|
|
121
|
+
.get("properties", {})
|
|
122
|
+
.get("prior_status", {})
|
|
123
|
+
.get("enum", [])
|
|
124
|
+
)
|
|
125
|
+
if yaml_allowed != schema_enum:
|
|
126
|
+
findings.append({
|
|
127
|
+
"id": "R-PRIOR-STATUS-ENUM-DRIFT", "severity": "medium",
|
|
128
|
+
"title": "prior_status enum diverge entre AGENT_CONTRACT.yaml e agent_result.schema.json",
|
|
129
|
+
"evidence": {
|
|
130
|
+
"yaml_allowed_values": sorted(yaml_allowed),
|
|
131
|
+
"schema_enum": sorted(schema_enum),
|
|
132
|
+
"only_in_yaml": sorted(yaml_allowed - schema_enum),
|
|
133
|
+
"only_in_schema": sorted(schema_enum - yaml_allowed),
|
|
134
|
+
},
|
|
135
|
+
"recommendation": "Alinhar AGENT_CONTRACT.yaml#prior_status.allowed_values com agent_result.schema.json enum.",
|
|
136
|
+
})
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
findings.append({
|
|
139
|
+
"id": "R-PRIOR-STATUS-CHECK-FAILED", "severity": "low",
|
|
140
|
+
"title": "Falha ao executar check_prior_status_enum_drift",
|
|
141
|
+
"evidence": {"error": str(exc)[:120]},
|
|
142
|
+
"recommendation": "Verificar manualmente alinhamento entre AGENT_CONTRACT e agent_result.schema.",
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
print(json.dumps({"checker": "contract_consistency", "findings": findings}, indent=2, ensure_ascii=False))
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""AUD-1814 — Critical findings audit checker.
|
|
3
|
+
|
|
4
|
+
Detects the architectural findings tracked by the critical-fixes trail:
|
|
5
|
+
- R-RUNNER-METRICS-DRIFT: runner.metrics missing in any canonical source
|
|
6
|
+
- R-NO-BASELINE-LESSON-RESEED: service.init not emitting baseline lessons event
|
|
7
|
+
- R-REVIEW-ROLE-DRIFT: projector forces owner-only review (no _reviewer_role check)
|
|
8
|
+
- R-NON-SERIALIZABLE-APPEND: append_transactional missing from store
|
|
9
|
+
- R-FILE-EFFECT-ARTIFACTS: file_effects module missing
|
|
10
|
+
- R-HOTFIX-VALIDATION: validate_hotfix_request missing
|
|
11
|
+
- R-DONE-PRIOR-STATUS: agent_result.schema prior_status enum lacks 'done'
|
|
12
|
+
- R-ATOMIC-FILE-EFFECTS: file_effects.stage_file_updates missing
|
|
13
|
+
|
|
14
|
+
Usage: python src/audit/critical_findings.py --root <repo_root>
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read(p: Path) -> str:
|
|
26
|
+
return p.read_text(encoding="utf-8") if p.exists() else ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _yaml_safe_load(p: Path):
|
|
30
|
+
try:
|
|
31
|
+
import yaml
|
|
32
|
+
return yaml.safe_load(_read(p)) or {}
|
|
33
|
+
except Exception:
|
|
34
|
+
return {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_runner_metrics(root: Path) -> list[dict]:
|
|
38
|
+
findings = []
|
|
39
|
+
sources = {
|
|
40
|
+
"AGENT_CONTRACT.yaml": _yaml_safe_load(root / ".roadmap/AGENT_CONTRACT.yaml")
|
|
41
|
+
.get("vocabulary", {}).get("reserved_orchestrator_actions", []),
|
|
42
|
+
"ORCHESTRATOR_CONTRACT.yaml": _yaml_safe_load(root / ".roadmap/ORCHESTRATOR_CONTRACT.yaml")
|
|
43
|
+
.get("roles", {}).get("orchestrator", {}).get("reserved_actions", []),
|
|
44
|
+
"constants.py": "runner.metrics" in _read(root / "src/esaa/constants.py"),
|
|
45
|
+
}
|
|
46
|
+
missing = []
|
|
47
|
+
for name, value in sources.items():
|
|
48
|
+
if name == "constants.py":
|
|
49
|
+
if not value:
|
|
50
|
+
missing.append(name)
|
|
51
|
+
else:
|
|
52
|
+
if "runner.metrics" not in (value or []):
|
|
53
|
+
missing.append(name)
|
|
54
|
+
if missing:
|
|
55
|
+
findings.append({
|
|
56
|
+
"id": "R-RUNNER-METRICS-DRIFT",
|
|
57
|
+
"severity": "medium",
|
|
58
|
+
"title": "runner.metrics missing from canonical sources",
|
|
59
|
+
"evidence": {"missing_in": missing},
|
|
60
|
+
"recommendation": "Add runner.metrics to all reserved-action vocabularies.",
|
|
61
|
+
})
|
|
62
|
+
return findings
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def check_baseline_lessons_reseed(root: Path) -> list[dict]:
|
|
66
|
+
src = _read(root / "src/esaa/service.py")
|
|
67
|
+
findings = []
|
|
68
|
+
if "BASELINE_LESSONS" not in src:
|
|
69
|
+
findings.append({
|
|
70
|
+
"id": "R-NO-BASELINE-LESSON-RESEED",
|
|
71
|
+
"severity": "high",
|
|
72
|
+
"title": "service.py lacks BASELINE_LESSONS constant",
|
|
73
|
+
"evidence": {"file": "src/esaa/service.py"},
|
|
74
|
+
"recommendation": "Define BASELINE_LESSONS and emit baseline_reseed event in init.",
|
|
75
|
+
})
|
|
76
|
+
elif "baseline_reseed" not in src:
|
|
77
|
+
findings.append({
|
|
78
|
+
"id": "R-NO-BASELINE-LESSON-RESEED",
|
|
79
|
+
"severity": "high",
|
|
80
|
+
"title": "service.init not emitting baseline_reseed event",
|
|
81
|
+
"evidence": {"file": "src/esaa/service.py", "missing_marker": "baseline_reseed"},
|
|
82
|
+
"recommendation": "Append orchestrator.view.mutate with baseline_reseed in init.",
|
|
83
|
+
})
|
|
84
|
+
return findings
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check_review_role(root: Path) -> list[dict]:
|
|
88
|
+
src = _read(root / "src/esaa/projector.py")
|
|
89
|
+
findings = []
|
|
90
|
+
if "_reviewer_role" not in src:
|
|
91
|
+
findings.append({
|
|
92
|
+
"id": "R-REVIEW-ROLE-DRIFT",
|
|
93
|
+
"severity": "high",
|
|
94
|
+
"title": "projector._apply_review missing role-based authorization",
|
|
95
|
+
"evidence": {"file": "src/esaa/projector.py"},
|
|
96
|
+
"recommendation": "Honor _reviewer_role in payload to allow qa_role mode.",
|
|
97
|
+
})
|
|
98
|
+
return findings
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check_serializable_append(root: Path) -> list[dict]:
|
|
102
|
+
src = _read(root / "src/esaa/store.py")
|
|
103
|
+
findings = []
|
|
104
|
+
if "append_transactional" not in src:
|
|
105
|
+
findings.append({
|
|
106
|
+
"id": "R-NON-SERIALIZABLE-APPEND",
|
|
107
|
+
"severity": "critical",
|
|
108
|
+
"title": "store.append_transactional missing",
|
|
109
|
+
"evidence": {"file": "src/esaa/store.py"},
|
|
110
|
+
"recommendation": "Add lock-then-revalidate-then-write transactional API.",
|
|
111
|
+
})
|
|
112
|
+
return findings
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def check_file_effects_module(root: Path) -> list[dict]:
|
|
116
|
+
p = root / "src/esaa/file_effects.py"
|
|
117
|
+
findings = []
|
|
118
|
+
if not p.exists():
|
|
119
|
+
findings.append({
|
|
120
|
+
"id": "R-FILE-EFFECT-ARTIFACTS",
|
|
121
|
+
"severity": "high",
|
|
122
|
+
"title": "src/esaa/file_effects.py missing",
|
|
123
|
+
"evidence": {"expected_file": "src/esaa/file_effects.py"},
|
|
124
|
+
"recommendation": "Implement staging + content-addressed artifacts.",
|
|
125
|
+
})
|
|
126
|
+
return findings
|
|
127
|
+
src = _read(p)
|
|
128
|
+
required = ["stage_file_updates", "commit_staged", "discard_staged",
|
|
129
|
+
"compute_file_metadata", "write_artifact", "verify_artifact",
|
|
130
|
+
"read_artifact", "recover_file_effects"]
|
|
131
|
+
missing = [s for s in required if s not in src]
|
|
132
|
+
if missing:
|
|
133
|
+
findings.append({
|
|
134
|
+
"id": "R-FILE-EFFECT-ARTIFACTS",
|
|
135
|
+
"severity": "medium",
|
|
136
|
+
"title": "file_effects.py missing required functions",
|
|
137
|
+
"evidence": {"missing": missing},
|
|
138
|
+
"recommendation": "Implement: " + ", ".join(missing),
|
|
139
|
+
})
|
|
140
|
+
return findings
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def check_hotfix_validation(root: Path) -> list[dict]:
|
|
144
|
+
src = _read(root / "src/esaa/service.py")
|
|
145
|
+
findings = []
|
|
146
|
+
if "validate_hotfix_request" not in src:
|
|
147
|
+
findings.append({
|
|
148
|
+
"id": "R-HOTFIX-VALIDATION",
|
|
149
|
+
"severity": "medium",
|
|
150
|
+
"title": "service.validate_hotfix_request missing",
|
|
151
|
+
"evidence": {"file": "src/esaa/service.py"},
|
|
152
|
+
"recommendation": "Add validate_hotfix_request returning structured codes.",
|
|
153
|
+
})
|
|
154
|
+
return findings
|
|
155
|
+
expected_codes = ["HOTFIX_ISSUE_NOT_FOUND", "HOTFIX_TARGET_NOT_FOUND",
|
|
156
|
+
"HOTFIX_TARGET_NOT_DONE", "HOTFIX_SCOPE_INVALID"]
|
|
157
|
+
missing = [c for c in expected_codes if c not in src]
|
|
158
|
+
if missing:
|
|
159
|
+
findings.append({
|
|
160
|
+
"id": "R-HOTFIX-VALIDATION",
|
|
161
|
+
"severity": "low",
|
|
162
|
+
"title": "validate_hotfix_request missing error codes",
|
|
163
|
+
"evidence": {"missing_codes": missing},
|
|
164
|
+
"recommendation": "Emit all hotfix structured reject codes.",
|
|
165
|
+
})
|
|
166
|
+
return findings
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def check_done_in_prior_status(root: Path) -> list[dict]:
|
|
170
|
+
p = root / ".roadmap/agent_result.schema.json"
|
|
171
|
+
findings = []
|
|
172
|
+
if not p.exists():
|
|
173
|
+
return findings
|
|
174
|
+
data = json.loads(_read(p))
|
|
175
|
+
enum = (
|
|
176
|
+
data.get("properties", {})
|
|
177
|
+
.get("activity_event", {})
|
|
178
|
+
.get("properties", {})
|
|
179
|
+
.get("prior_status", {})
|
|
180
|
+
.get("enum", [])
|
|
181
|
+
)
|
|
182
|
+
if "done" not in enum:
|
|
183
|
+
findings.append({
|
|
184
|
+
"id": "R-DONE-PRIOR-STATUS",
|
|
185
|
+
"severity": "high",
|
|
186
|
+
"title": "agent_result.schema prior_status enum lacks 'done'",
|
|
187
|
+
"evidence": {"current_enum": enum},
|
|
188
|
+
"recommendation": "Add 'done' to enable issue.report on done tasks.",
|
|
189
|
+
})
|
|
190
|
+
return findings
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def check_plugin_dispatch_parity(root: Path) -> list[dict]:
|
|
194
|
+
src = _read(root / "src/esaa/service.py")
|
|
195
|
+
findings = []
|
|
196
|
+
required = ["tasks_with_planned_plugins", "_accept_agent_output", "task.create"]
|
|
197
|
+
missing = [marker for marker in required if marker not in src]
|
|
198
|
+
if missing:
|
|
199
|
+
findings.append({
|
|
200
|
+
"id": "R-PLUGIN-DISPATCH-DRIFT",
|
|
201
|
+
"severity": "high",
|
|
202
|
+
"title": "run path no longer appears to consume planned plugin tasks",
|
|
203
|
+
"evidence": {"missing_markers": missing, "file": "src/esaa/service.py"},
|
|
204
|
+
"recommendation": "Keep run/eligible on the same tasks_with_planned_plugins view and admit task.create before claim.",
|
|
205
|
+
})
|
|
206
|
+
return findings
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def check_dry_run_semantics(root: Path) -> list[dict]:
|
|
210
|
+
src = _read(root / "src/esaa/service.py")
|
|
211
|
+
findings = []
|
|
212
|
+
required = ['"status": "dry_run"', "would_append_events", "simulated_last_event_seq"]
|
|
213
|
+
missing = [marker for marker in required if marker not in src]
|
|
214
|
+
if missing:
|
|
215
|
+
findings.append({
|
|
216
|
+
"id": "R-DRY-RUN-AMBIGUOUS",
|
|
217
|
+
"severity": "medium",
|
|
218
|
+
"title": "dry-run responses may be ambiguous",
|
|
219
|
+
"evidence": {"missing_markers": missing, "file": "src/esaa/service.py"},
|
|
220
|
+
"recommendation": "Return status=dry_run and simulated append metadata for every dry-run command.",
|
|
221
|
+
})
|
|
222
|
+
return findings
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
CHECKS = [
|
|
226
|
+
("runner_metrics", check_runner_metrics),
|
|
227
|
+
("baseline_lessons_reseed", check_baseline_lessons_reseed),
|
|
228
|
+
("review_role", check_review_role),
|
|
229
|
+
("serializable_append", check_serializable_append),
|
|
230
|
+
("file_effects_module", check_file_effects_module),
|
|
231
|
+
("hotfix_validation", check_hotfix_validation),
|
|
232
|
+
("done_in_prior_status", check_done_in_prior_status),
|
|
233
|
+
("plugin_dispatch_parity", check_plugin_dispatch_parity),
|
|
234
|
+
("dry_run_semantics", check_dry_run_semantics),
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def run_checks(root: Path) -> dict:
|
|
239
|
+
all_findings = []
|
|
240
|
+
by_check = {}
|
|
241
|
+
for name, fn in CHECKS:
|
|
242
|
+
result = fn(root)
|
|
243
|
+
by_check[name] = len(result)
|
|
244
|
+
all_findings.extend(result)
|
|
245
|
+
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
246
|
+
all_findings.sort(key=lambda f: severity_order.get(f.get("severity"), 9))
|
|
247
|
+
return {
|
|
248
|
+
"checker": "critical_findings",
|
|
249
|
+
"total_findings": len(all_findings),
|
|
250
|
+
"by_check": by_check,
|
|
251
|
+
"by_severity": {
|
|
252
|
+
sev: sum(1 for f in all_findings if f.get("severity") == sev)
|
|
253
|
+
for sev in severity_order
|
|
254
|
+
},
|
|
255
|
+
"findings": all_findings,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def main() -> int:
|
|
260
|
+
ap = argparse.ArgumentParser()
|
|
261
|
+
ap.add_argument("--root", default=".")
|
|
262
|
+
args = ap.parse_args()
|
|
263
|
+
result = run_checks(Path(args.root))
|
|
264
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
265
|
+
return 0 if result["total_findings"] == 0 else 0
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
sys.exit(main())
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""FIX-1531 — Cria docs/spec/activity_future_templates.md documentando o arquivo."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
DOC = """# activity_future_templates.jsonl
|
|
8
|
+
|
|
9
|
+
## Status: experimental / nao consumido pelo runtime
|
|
10
|
+
|
|
11
|
+
O arquivo `.roadmap/activity_future_templates.jsonl` contem **templates** de
|
|
12
|
+
eventos hipoteticos previstos para evolucao futura do protocolo ESAA. Ele:
|
|
13
|
+
|
|
14
|
+
- **nao e lido** por `esaa.store.parse_event_store`;
|
|
15
|
+
- **nao afeta** projecoes (`roadmap.json`/`issues.json`/`lessons.json`);
|
|
16
|
+
- **nao afeta** `esaa verify` ou `esaa replay`;
|
|
17
|
+
- serve como **referencia de design** para acoes/eventos ainda nao implementados.
|
|
18
|
+
|
|
19
|
+
## Quando usar
|
|
20
|
+
|
|
21
|
+
Ao propor uma nova acao no vocabulario, adicione um template no arquivo (sem
|
|
22
|
+
event_seq, sem ts) ilustrando a estrutura esperada. Quando implementada, o
|
|
23
|
+
template e movido para o event store canonico com event_seq real, e a entrada
|
|
24
|
+
removida deste arquivo.
|
|
25
|
+
|
|
26
|
+
## Garantia de nao-contaminacao
|
|
27
|
+
|
|
28
|
+
O runtime so consome `.roadmap/activity.jsonl` (ver `EVENT_STORE_PATH` em
|
|
29
|
+
`src/esaa/constants.py`). Qualquer outro arquivo `.jsonl` em `.roadmap/` e
|
|
30
|
+
ignorado.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> int:
|
|
35
|
+
ap = argparse.ArgumentParser()
|
|
36
|
+
ap.add_argument("--root", default=".")
|
|
37
|
+
args = ap.parse_args()
|
|
38
|
+
target = Path(args.root) / "docs/spec/activity_future_templates.md"
|
|
39
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
target.write_text(DOC, encoding="utf-8")
|
|
41
|
+
print(f"wrote: {target}")
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""AUD-1201 — Integridade do event store e replay de projecao.
|
|
3
|
+
|
|
4
|
+
Checa monotonicidade/gaps de event_seq, append-only e reprodutibilidade das
|
|
5
|
+
projecoes por replay (em especial lessons.json). Read-only.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import argparse, json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
def parse_events(path: Path):
|
|
12
|
+
events = []
|
|
13
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
14
|
+
line = line.strip()
|
|
15
|
+
if line:
|
|
16
|
+
events.append(json.loads(line))
|
|
17
|
+
return events
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
ap = argparse.ArgumentParser()
|
|
21
|
+
ap.add_argument("--root", default=".")
|
|
22
|
+
args = ap.parse_args()
|
|
23
|
+
root = Path(args.root)
|
|
24
|
+
findings = []
|
|
25
|
+
|
|
26
|
+
store = root / ".roadmap/activity.jsonl"
|
|
27
|
+
if not store.exists():
|
|
28
|
+
print(json.dumps({"checker": "eventstore_integrity", "findings": [
|
|
29
|
+
{"id": "ERR", "severity": "high", "title": "event store ausente"}]}, indent=2))
|
|
30
|
+
return 0
|
|
31
|
+
events = parse_events(store)
|
|
32
|
+
|
|
33
|
+
seqs = [e["event_seq"] for e in events]
|
|
34
|
+
if seqs != sorted(seqs):
|
|
35
|
+
findings.append({"id": "R-order", "severity": "high",
|
|
36
|
+
"title": "event_seq fora de ordem", "evidence": {"seqs": seqs}})
|
|
37
|
+
gaps = [i for i in range(1, len(seqs)) if seqs[i] != seqs[i-1] + 1]
|
|
38
|
+
if gaps:
|
|
39
|
+
findings.append({"id": "R-gap", "severity": "high",
|
|
40
|
+
"title": "gaps em event_seq", "evidence": {"positions": gaps}})
|
|
41
|
+
|
|
42
|
+
# lessons reproducibility: lessons so nascem de issue.report(category=process,subtype=lesson)
|
|
43
|
+
lesson_sources = [e for e in events
|
|
44
|
+
if e["action"] == "issue.report"
|
|
45
|
+
and e.get("payload", {}).get("category") == "process"
|
|
46
|
+
and e.get("payload", {}).get("subtype") == "lesson"]
|
|
47
|
+
stored = root / ".roadmap/lessons.json"
|
|
48
|
+
stored_lessons = json.loads(stored.read_text(encoding="utf-8")).get("lessons", []) if stored.exists() else []
|
|
49
|
+
if stored_lessons and not lesson_sources:
|
|
50
|
+
findings.append({
|
|
51
|
+
"id": "R1", "severity": "critical",
|
|
52
|
+
"title": "lessons.json NAO e reconstruivel por replay",
|
|
53
|
+
"evidence": {"stored_lessons": [l["lesson_id"] for l in stored_lessons],
|
|
54
|
+
"lesson_creating_events": 0},
|
|
55
|
+
"recommendation": "Modelar lessons como eventos (issue.report/lesson) ou outra acao versionada; senao 'esaa project' apaga as lessons.",
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
# schema_version misto nos eventos
|
|
59
|
+
ev_versions = sorted({e.get("schema_version") for e in events})
|
|
60
|
+
if len(ev_versions) > 1:
|
|
61
|
+
findings.append({
|
|
62
|
+
"id": "R3b", "severity": "medium",
|
|
63
|
+
"title": "schema_version misto no event store",
|
|
64
|
+
"evidence": {"versions": ev_versions},
|
|
65
|
+
"recommendation": "make_event usa constante fixa e regride eventos novos para a versao antiga.",
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
print(json.dumps({"checker": "eventstore_integrity",
|
|
69
|
+
"events": len(events), "findings": findings}, indent=2, ensure_ascii=False))
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
raise SystemExit(main())
|
audit/rename_parcer.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""FIX-1521 — Rename PARCER_PROFILE_agent-docs.yaml para PARCER_PROFILE.agent-docs.yaml.
|
|
3
|
+
|
|
4
|
+
Adota convencao '.' (4 dos 5 profiles ja usam). Atualiza referencias textuais
|
|
5
|
+
em CLAUDE.md e AGENTS.md. O evento 7 do event store permanece imutavel com o
|
|
6
|
+
nome antigo (registro historico valido).
|
|
7
|
+
|
|
8
|
+
Uso: python src/audit/rename_parcer.py --root <repo_root>
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
import argparse, re, shutil
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> int:
|
|
16
|
+
ap = argparse.ArgumentParser()
|
|
17
|
+
ap.add_argument("--root", default=".")
|
|
18
|
+
args = ap.parse_args()
|
|
19
|
+
root = Path(args.root)
|
|
20
|
+
old = root / ".roadmap/PARCER_PROFILE_agent-docs.yaml"
|
|
21
|
+
new = root / ".roadmap/PARCER_PROFILE.agent-docs.yaml"
|
|
22
|
+
if old.exists() and not new.exists():
|
|
23
|
+
shutil.move(str(old), str(new))
|
|
24
|
+
print(f"renamed: {old.name} -> {new.name}")
|
|
25
|
+
elif new.exists():
|
|
26
|
+
print("already renamed")
|
|
27
|
+
else:
|
|
28
|
+
print("source file not found; skipping")
|
|
29
|
+
|
|
30
|
+
for ref in (root / ".claude/CLAUDE.md", root / "AGENTS.md"):
|
|
31
|
+
if not ref.exists():
|
|
32
|
+
continue
|
|
33
|
+
txt = ref.read_text(encoding="utf-8")
|
|
34
|
+
new_txt = re.sub(r"PARCER_PROFILE_agent-docs\.yaml", "PARCER_PROFILE.agent-docs.yaml", txt)
|
|
35
|
+
if new_txt != txt:
|
|
36
|
+
ref.write_text(new_txt, encoding="utf-8")
|
|
37
|
+
print(f"updated refs in {ref}")
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
raise SystemExit(main())
|