pdl-taskmaster 2.4.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.
- pdl_taskmaster/__init__.py +6 -0
- pdl_taskmaster/contracts/AUTHORITY_MAP.json +19 -0
- pdl_taskmaster/contracts/CALIBRATION.json +7 -0
- pdl_taskmaster/contracts/CONTRACT_MANIFEST.json +231 -0
- pdl_taskmaster/contracts/EXECUTION_CONTRACT.json +615 -0
- pdl_taskmaster/contracts/IMPLEMENTATION_BOUNDARY.json +32 -0
- pdl_taskmaster/contracts/VERIFICATION_CONTRACT.json +379 -0
- pdl_taskmaster/contracts/standards/ARTIFACT_STANDARD.md +7 -0
- pdl_taskmaster/contracts/standards/AUTHORITY_STANDARD.md +17 -0
- pdl_taskmaster/contracts/standards/CONFORMANCE_STANDARD.md +15 -0
- pdl_taskmaster/contracts/standards/CONTEXT_STANDARD.md +15 -0
- pdl_taskmaster/contracts/standards/EXECUTION_STANDARD.md +14 -0
- pdl_taskmaster/contracts/standards/HANDOFF_STANDARD.md +21 -0
- pdl_taskmaster/contracts/standards/INSTALLATION_STANDARD.md +17 -0
- pdl_taskmaster/contracts/standards/PDL_STANDARD.md +19 -0
- pdl_taskmaster/contracts/standards/PROMPT_STANDARD.md +13 -0
- pdl_taskmaster/contracts/standards/PROTOCOL_STANDARD.md +17 -0
- pdl_taskmaster/contracts/standards/RESPONSE_PLAN_STANDARD.md +24 -0
- pdl_taskmaster/contracts/standards/RESULT_STANDARD.md +35 -0
- pdl_taskmaster/contracts/standards/REVIEW_STANDARD.md +31 -0
- pdl_taskmaster/contracts/standards/SEMANTIC_INPUT_STANDARD.md +15 -0
- pdl_taskmaster/contracts/standards/TASK_SEMANTICS_STANDARD.md +9 -0
- pdl_taskmaster/controller/__init__.py +0 -0
- pdl_taskmaster/controller/mechanical_controller.py +542 -0
- pdl_taskmaster/controller/schemas/activation_decision.schema.json +34 -0
- pdl_taskmaster/controller/schemas/artifact_body.schema.json +14 -0
- pdl_taskmaster/controller/schemas/artifact_review_facts.schema.json +91 -0
- pdl_taskmaster/controller/schemas/bootstrap_analysis.schema.json +60 -0
- pdl_taskmaster/controller/schemas/execution_draft.schema.json +47 -0
- pdl_taskmaster/controller/schemas/execution_input_decision.schema.json +43 -0
- pdl_taskmaster/controller/schemas/execution_outcome.schema.json +201 -0
- pdl_taskmaster/controller/schemas/plan_artifact.schema.json +14 -0
- pdl_taskmaster/controller/schemas/prompt_artifact.schema.json +63 -0
- pdl_taskmaster/controller/schemas/protocol_discussion.schema.json +8 -0
- pdl_taskmaster/controller/schemas/result_ir_repair.schema.json +140 -0
- pdl_taskmaster/eval/__init__.py +1 -0
- pdl_taskmaster/eval/build_adversarial_battery.py +351 -0
- pdl_taskmaster/eval/compare_eval_runs.py +251 -0
- pdl_taskmaster/eval/control_api_call.py +428 -0
- pdl_taskmaster/eval/export_rlcd_dataset.py +799 -0
- pdl_taskmaster/eval/fidelity_scan.py +268 -0
- pdl_taskmaster/eval/gen_fidelity_battery.py +457 -0
- pdl_taskmaster/eval/leak_scan.py +260 -0
- pdl_taskmaster/eval/rerecord_fixtures.py +139 -0
- pdl_taskmaster/eval/run_canary_shape_probe.py +229 -0
- pdl_taskmaster/eval/run_dual_gate_probe.py +404 -0
- pdl_taskmaster/eval/run_qualified_batch.py +841 -0
- pdl_taskmaster/eval/summarize_run.py +194 -0
- pdl_taskmaster/host/__init__.py +1 -0
- pdl_taskmaster/host/app.py +164 -0
- pdl_taskmaster/host/cli.py +100 -0
- pdl_taskmaster/host/repl.py +866 -0
- pdl_taskmaster/observation/__init__.py +1 -0
- pdl_taskmaster/observation/cli.py +47 -0
- pdl_taskmaster/observation/observed_session.py +219 -0
- pdl_taskmaster/observation/parsers.py +66 -0
- pdl_taskmaster/observation/records.py +58 -0
- pdl_taskmaster/observation/session.py +3 -0
- pdl_taskmaster/observation/sink.py +3 -0
- pdl_taskmaster/observation/sinks.py +20 -0
- pdl_taskmaster/providers/README.md +66 -0
- pdl_taskmaster/providers/__init__.py +1 -0
- pdl_taskmaster/providers/api_worker.py +411 -0
- pdl_taskmaster/providers/base.py +26 -0
- pdl_taskmaster/providers/codex_worker.py +328 -0
- pdl_taskmaster/providers/fixtures.py +157 -0
- pdl_taskmaster/providers/live_stub.py +66 -0
- pdl_taskmaster/providers/recorded.py +120 -0
- pdl_taskmaster/py.typed +1 -0
- pdl_taskmaster/runtime/__init__.py +0 -0
- pdl_taskmaster/runtime/context_compiler.py +151 -0
- pdl_taskmaster/runtime/model_classification.py +290 -0
- pdl_taskmaster/runtime/normative_store.py +157 -0
- pdl_taskmaster/runtime/operation_bridge.py +336 -0
- pdl_taskmaster/runtime/presentation.py +18 -0
- pdl_taskmaster/runtime/quarantine.py +91 -0
- pdl_taskmaster/runtime/result_ir.py +401 -0
- pdl_taskmaster/runtime/semantic_observer.py +40 -0
- pdl_taskmaster/runtime/session_engine.py +1168 -0
- pdl_taskmaster/runtime/standard_registry.py +55 -0
- pdl_taskmaster/runtime/wire_payloads.py +462 -0
- pdl_taskmaster/runtime/worker-bootstrap.txt +4 -0
- pdl_taskmaster/runtime/workspace.py +602 -0
- pdl_taskmaster/tracking/__init__.py +1 -0
- pdl_taskmaster/tracking/log_live_session.py +98 -0
- pdl_taskmaster/tracking/mlflow_sink.py +58 -0
- pdl_taskmaster/verify/verify_repl_baseline.py +299 -0
- pdl_taskmaster-2.4.0.dist-info/METADATA +155 -0
- pdl_taskmaster-2.4.0.dist-info/RECORD +92 -0
- pdl_taskmaster-2.4.0.dist-info/WHEEL +4 -0
- pdl_taskmaster-2.4.0.dist-info/entry_points.txt +2 -0
- pdl_taskmaster-2.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""Result Pseudocode decomposition IR (ADR-0009 prototype).
|
|
2
|
+
|
|
3
|
+
Structured result artifact emitted by the EXECUTE operation alongside the
|
|
4
|
+
native deliverable: the result is decomposed per file, reconciled against
|
|
5
|
+
the mechanically derived requirement IDs of the confirmed Prompt Pseudocode,
|
|
6
|
+
and grounded in execution-evidence citations that the controller validates
|
|
7
|
+
mechanically. Model-asserted content is never trusted:
|
|
8
|
+
|
|
9
|
+
- every cited path MUST resolve inside the workspace;
|
|
10
|
+
- every cited section marker MUST appear verbatim in the cited artifact;
|
|
11
|
+
- every quoted observation MUST appear verbatim in the cited artifact;
|
|
12
|
+
- every requirement ID derived from the confirmed prompt body MUST be
|
|
13
|
+
reconciled exactly once.
|
|
14
|
+
|
|
15
|
+
Zero external dependencies (stdlib only), per the harness constraint.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# Commitment verbs: prompt-body lines opening with one of these are
|
|
25
|
+
# mechanically promoted to requirement IDs (R1..Rn, in order).
|
|
26
|
+
_COMMITMENT_VERB = re.compile(
|
|
27
|
+
r"^(?:IMPLEMENT|DEFINE|GENERATE|DELIVER|ENSURE|PRESERVE|REMOVE|UPDATE|"
|
|
28
|
+
r"MAINTAIN|FIX|SCAN|ALIGN|ELIMINATE|COMPARE|APPLY|MODIFY|LOCATE|CREATE|"
|
|
29
|
+
r"VERIFY|REWRITE|REBUILD|PRESENT|ADD)\b"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_VALID_STATUS = {"satisfied", "partial", "open"}
|
|
33
|
+
|
|
34
|
+
RESULT_IR_INSTRUCTIONS_TEMPLATE = """The response MUST end with a fenced ```json block containing the Result IR object, exactly this shape:
|
|
35
|
+
{{"files": [{{"filename": "<name>.py", "satisfies": ["R<n>", ...], "evidence": {{"path": "<workspace-relative path of the artifact this file's content is grounded in>", "section": "<verbatim section marker inside that artifact, or omitted>"}}}}], "reconciliation": [{{"requirement": "R<n>", "status": "satisfied|partial|open", "evidence": {{"path": "...", "section": "...", "observed": "<verbatim quote from the cited artifact supporting this status>"}}}}, ...], "open_defects": [{{"id": "D<n>", "description": "<defect>", "evidence": {{"path": "...", "observed": "<verbatim quote>"}}}}]}}
|
|
36
|
+
Rules: cite ONLY the artifacts listed under AVAILABLE EVIDENCE PATHS below; every "observed" string MUST be copied verbatim from the cited artifact; every requirement ID MUST appear in "reconciliation" exactly once; do not invent paths, sections, quotes, or requirement IDs."""
|
|
37
|
+
|
|
38
|
+
_EVIDENCE_KEYS = ("path", "section", "observed")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def derive_requirements(prompt_body: str) -> list[str]:
|
|
42
|
+
"""Mechanically derive requirement IDs (R1..Rn) from the confirmed prompt."""
|
|
43
|
+
reqs: list[str] = []
|
|
44
|
+
for line in (prompt_body or "").splitlines():
|
|
45
|
+
s = line.strip()
|
|
46
|
+
if s and _COMMITMENT_VERB.match(s):
|
|
47
|
+
reqs.append(s)
|
|
48
|
+
return reqs
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def render_instructions(
|
|
52
|
+
requirements: list[str],
|
|
53
|
+
repo_root: str | Path | None = None,
|
|
54
|
+
evidence_paths: list[str] | None = None,
|
|
55
|
+
) -> str:
|
|
56
|
+
numbered = "\n".join(f"R{i}: {r}" for i, r in enumerate(requirements, 1))
|
|
57
|
+
paths = "\n".join(f"- {p}" for p in (evidence_paths or ["execution://body"]))
|
|
58
|
+
return (
|
|
59
|
+
"CONFIRMED REQUIREMENTS (mechanically derived; reconcile EVERY ID):\n"
|
|
60
|
+
+ (numbered or "R1: (no commitment lines detected)")
|
|
61
|
+
+ "\n\n"
|
|
62
|
+
+ load_standard_instructions(repo_root).replace(
|
|
63
|
+
"{evidence_paths}", paths or "- execution://body"
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_standard_instructions(repo_root: str | Path | None = None) -> str:
|
|
69
|
+
"""ADR-0009: the controller imports the Result IR standard at the EXECUTE
|
|
70
|
+
stage and renders its instruction block verbatim. Single source of truth
|
|
71
|
+
is contracts/standards/RESULT_STANDARD.md."""
|
|
72
|
+
root = Path(repo_root) if repo_root else Path(__file__).resolve().parents[3]
|
|
73
|
+
standard = root / "contracts" / "standards" / "RESULT_STANDARD.md"
|
|
74
|
+
text = standard.read_text(encoding="utf-8")
|
|
75
|
+
m = re.search(
|
|
76
|
+
r"<!-- RESULT-IR:INSTRUCTIONS.*?-->\s*\n(.*?)\n?<!-- /RESULT-IR:INSTRUCTIONS -->",
|
|
77
|
+
text,
|
|
78
|
+
re.S,
|
|
79
|
+
)
|
|
80
|
+
if not m:
|
|
81
|
+
raise ValueError("result_standard_instructions_missing")
|
|
82
|
+
return m.group(1).strip()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def extract_result_ir(body: str) -> dict | None:
|
|
86
|
+
"""Extract the Result IR from an execution body: the LAST fenced ```json
|
|
87
|
+
block, or — as models sometimes emit it unfenced — a trailing raw JSON
|
|
88
|
+
object starting with {\"files\"."""
|
|
89
|
+
body = body or ""
|
|
90
|
+
blocks = re.findall(r"```json\s*\n(.*?)```", body, re.S)
|
|
91
|
+
for candidate in reversed(blocks):
|
|
92
|
+
try:
|
|
93
|
+
obj = json.loads(candidate)
|
|
94
|
+
except json.JSONDecodeError:
|
|
95
|
+
continue
|
|
96
|
+
if isinstance(obj, dict):
|
|
97
|
+
return obj
|
|
98
|
+
idx = body.rfind('{"files"')
|
|
99
|
+
if idx >= 0:
|
|
100
|
+
try:
|
|
101
|
+
obj, _ = json.JSONDecoder().raw_decode(body[idx:])
|
|
102
|
+
except json.JSONDecodeError:
|
|
103
|
+
return None
|
|
104
|
+
if isinstance(obj, dict):
|
|
105
|
+
return obj
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _resolve_evidence(
|
|
110
|
+
ev: object,
|
|
111
|
+
workspace_path: Path,
|
|
112
|
+
errors: list[str],
|
|
113
|
+
where: str,
|
|
114
|
+
execution_body: str | None = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
if not isinstance(ev, dict) or "path" not in ev:
|
|
117
|
+
errors.append(f"{where}: evidence object with 'path' required")
|
|
118
|
+
return
|
|
119
|
+
raw = str(ev["path"]).replace("\\", "/")
|
|
120
|
+
if raw == "execution://body":
|
|
121
|
+
# Reserved self-reference (RS-04): resolves to the current execution
|
|
122
|
+
# body — the just-produced deliverable, not yet on disk.
|
|
123
|
+
if execution_body is None:
|
|
124
|
+
errors.append(f"{where}: execution://body cited but no execution body available")
|
|
125
|
+
return
|
|
126
|
+
content = execution_body
|
|
127
|
+
else:
|
|
128
|
+
candidate = (workspace_path / raw).resolve()
|
|
129
|
+
try:
|
|
130
|
+
candidate.relative_to(workspace_path.resolve())
|
|
131
|
+
except ValueError:
|
|
132
|
+
errors.append(f"{where}: evidence path escapes workspace: {raw}")
|
|
133
|
+
return
|
|
134
|
+
if not candidate.is_file():
|
|
135
|
+
errors.append(f"{where}: evidence path does not resolve: {raw}")
|
|
136
|
+
return
|
|
137
|
+
content = candidate.read_text(encoding="utf-8", errors="replace")
|
|
138
|
+
section = ev.get("section")
|
|
139
|
+
if section and str(section) not in content:
|
|
140
|
+
errors.append(f"{where}: cited section marker not found in artifact: {section!r}")
|
|
141
|
+
observed = ev.get("observed")
|
|
142
|
+
if observed and str(observed).strip() not in content:
|
|
143
|
+
errors.append(
|
|
144
|
+
f"{where}: cited observation is not a verbatim substring of the artifact: {str(observed)[:80]!r}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def validate_result_ir(
|
|
149
|
+
ir: object,
|
|
150
|
+
workspace_path: str | Path,
|
|
151
|
+
requirements: list[str],
|
|
152
|
+
execution_body: str | None = None,
|
|
153
|
+
) -> tuple[list[str], dict]:
|
|
154
|
+
"""Mechanically validate a Result IR against the workspace filesystem.
|
|
155
|
+
|
|
156
|
+
Returns (errors, normalized_ir). Empty errors means every structural,
|
|
157
|
+
coverage, and evidence-citation constraint held.
|
|
158
|
+
"""
|
|
159
|
+
errors: list[str] = []
|
|
160
|
+
ws = Path(workspace_path).resolve()
|
|
161
|
+
if not isinstance(ir, dict):
|
|
162
|
+
return ["Result IR must be a JSON object"], {}
|
|
163
|
+
|
|
164
|
+
files = ir.get("files")
|
|
165
|
+
recon = ir.get("reconciliation")
|
|
166
|
+
defects = ir.get("open_defects", [])
|
|
167
|
+
if not isinstance(files, list) or not files:
|
|
168
|
+
errors.append("'files' must be a non-empty array")
|
|
169
|
+
if not isinstance(recon, list) or not recon:
|
|
170
|
+
errors.append("'reconciliation' must be a non-empty array")
|
|
171
|
+
if not isinstance(defects, list):
|
|
172
|
+
errors.append("'open_defects' must be an array when present")
|
|
173
|
+
|
|
174
|
+
# Requirement coverage: every derived ID reconciled exactly once.
|
|
175
|
+
seen: dict[str, int] = {}
|
|
176
|
+
if isinstance(recon, list):
|
|
177
|
+
for i, entry in enumerate(recon, 1):
|
|
178
|
+
if not isinstance(entry, dict):
|
|
179
|
+
errors.append(f"reconciliation[{i}]: must be an object")
|
|
180
|
+
continue
|
|
181
|
+
rid = str(entry.get("requirement", "")).strip()
|
|
182
|
+
seen[rid] = seen.get(rid, 0) + 1
|
|
183
|
+
status = str(entry.get("status", "")).strip().lower()
|
|
184
|
+
if status not in _VALID_STATUS:
|
|
185
|
+
errors.append(f"reconciliation[{i}] ({rid}): invalid status {status!r}")
|
|
186
|
+
if rid not in {f"R{j}" for j in range(1, len(requirements) + 1)}:
|
|
187
|
+
errors.append(f"reconciliation[{i}]: unknown requirement ID {rid!r}")
|
|
188
|
+
_resolve_evidence(entry.get("evidence"), ws, errors, f"reconciliation[{i}] ({rid})", execution_body)
|
|
189
|
+
total = {f"R{j}" for j in range(1, len(requirements) + 1)}
|
|
190
|
+
for rid in sorted(total - set(seen)):
|
|
191
|
+
errors.append(f"requirement {rid} is not reconciled")
|
|
192
|
+
for rid, n in sorted(seen.items()):
|
|
193
|
+
if n > 1 and rid in total:
|
|
194
|
+
errors.append(f"requirement {rid} reconciled {n} times (expected exactly once)")
|
|
195
|
+
|
|
196
|
+
if isinstance(files, list):
|
|
197
|
+
for i, f in enumerate(files, 1):
|
|
198
|
+
if not isinstance(f, dict) or not str(f.get("filename", "")).strip():
|
|
199
|
+
errors.append(f"files[{i}]: 'filename' required")
|
|
200
|
+
continue
|
|
201
|
+
_resolve_evidence(
|
|
202
|
+
f.get("evidence"), ws, errors, f"files[{i}] ({f.get('filename')})", execution_body
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if isinstance(defects, list):
|
|
206
|
+
for i, d in enumerate(defects, 1):
|
|
207
|
+
if not isinstance(d, dict) or not str(d.get("description", "")).strip():
|
|
208
|
+
errors.append(f"open_defects[{i}]: 'description' required")
|
|
209
|
+
continue
|
|
210
|
+
_resolve_evidence(d.get("evidence"), ws, errors, f"open_defects[{i}]", execution_body)
|
|
211
|
+
|
|
212
|
+
return errors, (ir if not errors else {})
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def render_prior_ir_section(ir: object) -> str:
|
|
216
|
+
"""Render a validated prior Result IR for injection into the next epoch."""
|
|
217
|
+
try:
|
|
218
|
+
body = json.dumps(ir, indent=2, ensure_ascii=False)
|
|
219
|
+
except (TypeError, ValueError):
|
|
220
|
+
return ""
|
|
221
|
+
return (
|
|
222
|
+
"\n\n## PRIOR RESULT IR (mechanically validated by the host; "
|
|
223
|
+
"reconciliation statuses and open defects are authoritative)\n"
|
|
224
|
+
+ body
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def render_execution_brief(ir: object | None, requirements: list[str]) -> str:
|
|
229
|
+
"""Host-side execution brief (ADR-0009 experiment): entity-dense steering
|
|
230
|
+
assembled mechanically from the prior validated IR — open defects as the
|
|
231
|
+
authoritative work list, delivery discipline to suppress cross-file churn.
|
|
232
|
+
Mirrors the entity-extraction purpose of DRAFT_PROMPT at the execute
|
|
233
|
+
boundary without an additional worker call."""
|
|
234
|
+
lines = ["\n## EXECUTION BRIEF (host-assembled; authoritative)"]
|
|
235
|
+
defects = (ir or {}).get("open_defects") or []
|
|
236
|
+
open_reqs = [
|
|
237
|
+
r for r in ((ir or {}).get("reconciliation") or [])
|
|
238
|
+
if isinstance(r, dict) and r.get("status") in {"partial", "open"}
|
|
239
|
+
]
|
|
240
|
+
if defects:
|
|
241
|
+
lines.append("OPEN DEFECTS (fix these and only these):")
|
|
242
|
+
for d in defects:
|
|
243
|
+
lines.append(f"- {d.get('id')}: {d.get('description')}")
|
|
244
|
+
elif open_reqs:
|
|
245
|
+
lines.append("UNRESOLVED REQUIREMENTS:")
|
|
246
|
+
for r in open_reqs:
|
|
247
|
+
lines.append(f"- {r.get('requirement')}: {r.get('status')}")
|
|
248
|
+
else:
|
|
249
|
+
lines.append("NO OPEN DEFECTS recorded in the prior Result IR; if the task is a fix task, "
|
|
250
|
+
"the defect description is in the user message — fix exactly that.")
|
|
251
|
+
lines.append(
|
|
252
|
+
"DELIVERY DISCIPLINE: change only what the listed defects require; every file "
|
|
253
|
+
"section MUST contain only code (no prose, no requirement echoes, no fence markers "
|
|
254
|
+
"inside sections); reproduce all other files byte-identical; never weaken or "
|
|
255
|
+
"hardcode tests to force a pass."
|
|
256
|
+
)
|
|
257
|
+
if requirements:
|
|
258
|
+
lines.append("REQUIREMENT LEDGER: " + "; ".join(
|
|
259
|
+
f"R{i}: {r.splitlines()[0][:80]}" for i, r in enumerate(requirements, 1)
|
|
260
|
+
))
|
|
261
|
+
return "\n".join(lines)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def load_ir_from_deliverable(deliverable_text: str | None) -> dict | None:
|
|
265
|
+
"""Recover the embedded Result IR from a prior deliverable (restore/chaining)."""
|
|
266
|
+
if not deliverable_text:
|
|
267
|
+
return None
|
|
268
|
+
ir = extract_result_ir(deliverable_text)
|
|
269
|
+
return ir if isinstance(ir, dict) and ir.get("reconciliation") else None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def artifact_sha256(path: str | Path) -> str:
|
|
273
|
+
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _norm(text: str) -> str:
|
|
277
|
+
"""Whitespace-normalized form for verbatim entity matching (content, not
|
|
278
|
+
layout, is the 4B check)."""
|
|
279
|
+
return " ".join((text or "").split())
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Typed entity pipeline (ADR-0009 / TRD-0003 RS-02 extension): entities are
|
|
284
|
+
# parsed and classified at DRAFT_EXECUTE time; the host applies
|
|
285
|
+
# kind-appropriate mechanical checks instead of heuristic string filters.
|
|
286
|
+
|
|
287
|
+
_ENTITY_KINDS = {
|
|
288
|
+
"delivery_marker": {"brief": "line", "deliverable": "line"},
|
|
289
|
+
"api_signature": {"brief": "normalized", "deliverable": "normalized"},
|
|
290
|
+
"constant": {"brief": "normalized", "deliverable": "normalized"},
|
|
291
|
+
"wire_format": {"brief": "normalized", "deliverable": "normalized"},
|
|
292
|
+
"threshold": {"brief": "normalized", "deliverable": "normalized"},
|
|
293
|
+
"meta": {"brief": "normalized", "deliverable": None},
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def parse_typed_entities(raw_entities: list) -> list[dict]:
|
|
298
|
+
"""Normalize raw entity entries (dicts per schema, or legacy plain strings)
|
|
299
|
+
into typed records. Untyped strings become kind 'meta' (brief-only) so
|
|
300
|
+
legacy emission degrades gracefully instead of triggering false retries."""
|
|
301
|
+
typed = []
|
|
302
|
+
for e in raw_entities or []:
|
|
303
|
+
if isinstance(e, dict) and str(e.get("value", "")).strip():
|
|
304
|
+
kind = str(e.get("kind", "meta")).strip()
|
|
305
|
+
if kind not in _ENTITY_KINDS:
|
|
306
|
+
kind = "meta"
|
|
307
|
+
typed.append({
|
|
308
|
+
"kind": kind,
|
|
309
|
+
"value": str(e["value"]),
|
|
310
|
+
"name": e.get("name"),
|
|
311
|
+
"struct_format": e.get("struct_format"),
|
|
312
|
+
"declared_size": e.get("declared_size"),
|
|
313
|
+
})
|
|
314
|
+
elif isinstance(e, str) and e.strip():
|
|
315
|
+
typed.append({"kind": "meta", "value": e.strip(),
|
|
316
|
+
"name": None, "struct_format": None, "declared_size": None})
|
|
317
|
+
return typed
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def arithmetic_checks(typed_entities: list[dict]) -> list[str]:
|
|
321
|
+
"""Mechanical arithmetic validation over typed entities (host-side, stdlib).
|
|
322
|
+
|
|
323
|
+
- wire_format: struct.calcsize(struct_format) MUST equal declared_size
|
|
324
|
+
(catches native-alignment vs declared-size drift, e.g. HEADER_SIZE=16
|
|
325
|
+
with a 20-byte '4s Q I' native layout).
|
|
326
|
+
- constant with name MAGIC-like byte literal: the literal length MUST fit
|
|
327
|
+
the wire_format's leading string-field width when both are declared
|
|
328
|
+
(catches b'WAL01' truncated by a '4s' field).
|
|
329
|
+
"""
|
|
330
|
+
import struct as _struct
|
|
331
|
+
errors: list[str] = []
|
|
332
|
+
wire = [e for e in typed_entities if e["kind"] == "wire_format"]
|
|
333
|
+
for e in wire:
|
|
334
|
+
fmt, declared = e.get("struct_format"), e.get("declared_size")
|
|
335
|
+
if not fmt or declared is None:
|
|
336
|
+
continue
|
|
337
|
+
try:
|
|
338
|
+
actual = _struct.calcsize(fmt)
|
|
339
|
+
except struct.error as exc:
|
|
340
|
+
errors.append(f"wire_format {fmt!r}: invalid struct format ({exc})")
|
|
341
|
+
continue
|
|
342
|
+
if actual != declared:
|
|
343
|
+
errors.append(
|
|
344
|
+
f"wire_format arithmetic mismatch: calcsize({fmt!r}) = {actual} "
|
|
345
|
+
f"but declared_size = {declared}"
|
|
346
|
+
)
|
|
347
|
+
consts = [e for e in typed_entities if e["kind"] == "constant" and e.get("name")]
|
|
348
|
+
wires = [e for e in typed_entities if e["kind"] == "wire_format" and e.get("struct_format")]
|
|
349
|
+
for c in consts:
|
|
350
|
+
val = c["value"]
|
|
351
|
+
if not (val.startswith(("b'", 'b"')) and val.endswith(("'", '"'))):
|
|
352
|
+
continue
|
|
353
|
+
try:
|
|
354
|
+
import ast as _ast
|
|
355
|
+
literal = _ast.literal_eval(val)
|
|
356
|
+
except (ValueError, SyntaxError):
|
|
357
|
+
continue
|
|
358
|
+
if not isinstance(literal, (bytes, bytearray)):
|
|
359
|
+
continue
|
|
360
|
+
for w in wires:
|
|
361
|
+
m = re.search(r"(\d+)s", str(w["struct_format"]))
|
|
362
|
+
if not m:
|
|
363
|
+
continue
|
|
364
|
+
width = int(m.group(1))
|
|
365
|
+
if len(literal) > width:
|
|
366
|
+
errors.append(
|
|
367
|
+
f"constant {c['name']} = {val} is {len(literal)} bytes but wire_format "
|
|
368
|
+
f"{w['struct_format']!r} allots a {width}-byte field (truncation on write)"
|
|
369
|
+
)
|
|
370
|
+
return errors
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def entity_enforcement_misses(
|
|
374
|
+
typed_entities: list[dict],
|
|
375
|
+
brief_body: str,
|
|
376
|
+
deliverable_body: str | None,
|
|
377
|
+
) -> tuple[list[str], list[str]]:
|
|
378
|
+
"""Apply per-kind enforcement. Returns (brief_misses, deliverable_misses)
|
|
379
|
+
with kind-tagged strings. meta entities are never checked against the
|
|
380
|
+
deliverable; delivery markers are checked line-verbatim."""
|
|
381
|
+
brief_misses: list[str] = []
|
|
382
|
+
deliv_misses: list[str] = []
|
|
383
|
+
norm_brief, norm_deliv = _norm(brief_body), _norm(deliverable_body or "")
|
|
384
|
+
deliv_lines = set((deliverable_body or "").splitlines())
|
|
385
|
+
for e in typed_entities:
|
|
386
|
+
kind, val = e["kind"], e["value"]
|
|
387
|
+
rule = _ENTITY_KINDS[kind]
|
|
388
|
+
if rule["brief"] == "line":
|
|
389
|
+
if val not in brief_body:
|
|
390
|
+
brief_misses.append(f"{kind}:{val}")
|
|
391
|
+
elif _norm(val) not in norm_brief:
|
|
392
|
+
brief_misses.append(f"{kind}:{val}")
|
|
393
|
+
target = rule["deliverable"]
|
|
394
|
+
if target is None:
|
|
395
|
+
continue
|
|
396
|
+
if target == "line":
|
|
397
|
+
if val not in deliv_lines and val not in (deliverable_body or ""):
|
|
398
|
+
deliv_misses.append(f"{kind}:{val}")
|
|
399
|
+
elif _norm(val) not in norm_deliv:
|
|
400
|
+
deliv_misses.append(f"{kind}:{val}")
|
|
401
|
+
return brief_misses, deliv_misses
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ObservationSpec:
|
|
11
|
+
check_id: str
|
|
12
|
+
requirements: tuple[str, ...]
|
|
13
|
+
resolver: str
|
|
14
|
+
operations: tuple[str, ...]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SemanticObserverRegistry:
|
|
18
|
+
"""Read-only registry for declared semantic checks.
|
|
19
|
+
|
|
20
|
+
This module does not invoke a model, retry candidate work, mutate artifacts,
|
|
21
|
+
or gate the baseline Condition C candidate path.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, repo_root: str | Path):
|
|
25
|
+
root = Path(repo_root)
|
|
26
|
+
contract = json.loads((root / "contracts" / "VERIFICATION_CONTRACT.json").read_text(encoding="utf-8"))
|
|
27
|
+
self.mode = contract["baseline_profile"]["S"]
|
|
28
|
+
self._checks = tuple(
|
|
29
|
+
ObservationSpec(
|
|
30
|
+
check_id=item["id"],
|
|
31
|
+
requirements=tuple(item["requirements"]),
|
|
32
|
+
resolver=item["resolver"],
|
|
33
|
+
operations=tuple(item["operations"]),
|
|
34
|
+
)
|
|
35
|
+
for item in contract["checks"]
|
|
36
|
+
if item["class"] == "S"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def for_operation(self, operation: str) -> tuple[ObservationSpec, ...]:
|
|
40
|
+
return tuple(spec for spec in self._checks if operation in spec.operations)
|