design-playbook 0.20.1 → 0.20.2
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.
- package/examples/export-entry/run/evidence/L6.1-usertest-notes.md +8 -13
- package/examples/export-entry/run/evidence/manifest.jsonl +0 -1
- package/examples/export-entry/run/point-back.md +2 -2
- package/package.json +1 -1
- package/scripts/g8_run_registry.py +14 -4
- package/scripts/run_facts.py +113 -1
- package/scripts/run_status.py +38 -111
- package/scripts/validate_run.py +46 -57
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Synthetic user-test example — not evidence
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Schema/example material only. No real human-subject session occurred, no
|
|
4
|
+
participant observations or personal data are represented, and this file is
|
|
5
|
+
not registered in `manifest.jsonl`.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
anonymisation / retention record was completed, so the manifest entry
|
|
11
|
-
carries `method: user-test` with `population` but no `ethics` key. Per the
|
|
12
|
-
method-semantics gate this entry is quarantined as blocked evidence: it
|
|
13
|
-
cannot support any judgment (the L6.1 pass rests on
|
|
14
|
-
`L6.1-export-trace.json`, not on these notes). Complete the ethics record
|
|
15
|
-
in a newer manifest entry to make the session citable.
|
|
7
|
+
The scenario illustrates what a user-test note might look like during method
|
|
8
|
+
semantics testing. It must not support a run judgment or any claim about user
|
|
9
|
+
behavior. Tests that exercise missing `ethics` construct their own temporary
|
|
10
|
+
manifest entry rather than treating this file as evidence.
|
|
@@ -5,4 +5,3 @@
|
|
|
5
5
|
{"criterion": "L6.2", "capture": {"type": "a11y tree", "provider": "manual", "state": "cap-blocked-r2", "actions": ["select over-cap range", "open export dialog (post R4 fix)"], "schemaVersion": 1, "request": {"schemaVersion": 1, "viewport": {"width": 1280, "height": 800, "devicePixelRatio": 1.0, "colorScheme": "light"}, "freeze": {"enabled": true, "waitFonts": true, "networkIdle": false}}}, "artifact": "L6.2-a11y-tree-r2.json", "observed_state": "cap-blocked-r2", "result": "captured", "ts": "2026-08-14T11:04:30Z", "request": {"schemaVersion": 1, "viewport": {"width": 1280, "height": 800, "devicePixelRatio": 1.0, "colorScheme": "light"}, "freeze": {"enabled": true, "waitFonts": true, "networkIdle": false}}, "method": "runtime-observation", "observation": "toast 节点带 role=alert 与可读名称(含超限数值 200,000)", "interpretation": "R4 修复后的 a11y 附接证据,印证 L6.2 pass(ADR-0016 附接到既有 user-risk 判据)", "scope": "单次运行, viewport 1280x800, 数据集 week-2026-32", "population": null, "ethics": null}
|
|
6
6
|
{"criterion": "L6.2", "capture": {"type": "screenshot", "provider": "manual", "state": "cap-blocked-r2", "actions": ["select over-cap range", "open export dialog (post R4 fix)"], "schemaVersion": 1, "request": {"schemaVersion": 1, "viewport": {"width": 1280, "height": 800, "devicePixelRatio": 1.0, "colorScheme": "light"}, "freeze": {"enabled": true, "waitFonts": true, "networkIdle": false}}}, "artifact": "L6.2-cap-error-r2.png", "observed_state": "cap-blocked-r2", "result": "captured", "ts": "2026-08-14T11:05:00Z", "request": {"schemaVersion": 1, "viewport": {"width": 1280, "height": 800, "devicePixelRatio": 1.0, "colorScheme": "light"}, "freeze": {"enabled": true, "waitFonts": true, "networkIdle": false}}, "method": "runtime-observation", "observation": "提示含「超出 200,000 行上限」与「按周导出」收窄建议(重评 r2)", "interpretation": "满足 c2 的 Then 子句(剩余量与收窄建议);依赖 assumed 字段 export.row_cap 成立", "scope": "单次运行, viewport 1280x800, 数据集 week-2026-32", "population": null, "ethics": null}
|
|
7
7
|
{"criterion": "L6.3", "capture": {"type": "interaction trace", "provider": "manual", "state": "return-visible", "actions": ["start export", "navigate away", "navigate back", "read progress and result"], "schemaVersion": 1, "request": {"schemaVersion": 1, "viewport": {"width": 1280, "height": 800, "devicePixelRatio": 1.0, "colorScheme": "light"}, "freeze": {"enabled": true, "waitFonts": true, "networkIdle": false}}}, "artifact": "L6.3-return-trace.json", "observed_state": "return-visible", "result": "captured", "ts": "2026-08-14T11:10:00Z", "request": {"schemaVersion": 1, "viewport": {"width": 1280, "height": 800, "devicePixelRatio": 1.0, "colorScheme": "light"}, "freeze": {"enabled": true, "waitFonts": true, "networkIdle": false}}, "method": "runtime-observation", "observation": "导出中离开 main-list 后返回,exporting 进度态可见,完成后结果可获知(R5 修订 capture plan 后重采)", "interpretation": "满足 c3 的 Then 子句(进度与结果仍可获知);首轮跨导航 session 丢失记 blocked 已按 invalidated 块登记", "scope": "单次运行, viewport 1280x800, 数据集 week-2026-32", "population": null, "ethics": null}
|
|
8
|
-
{"criterion": "L6.1", "capture": {"type": "session notes", "provider": "manual", "state": "usertest-round1", "actions": ["facilitate weekly-report export task", "record participant behavior"], "schemaVersion": 1, "request": {"schemaVersion": 1, "viewport": {"width": 1280, "height": 800, "devicePixelRatio": 1.0, "colorScheme": "light"}, "freeze": {"enabled": true, "waitFonts": true, "networkIdle": false}}}, "artifact": "L6.1-usertest-notes.md", "observed_state": "usertest-round1", "result": "captured", "ts": "2026-08-14T10:50:00Z", "request": {"schemaVersion": 1, "viewport": {"width": 1280, "height": 800, "devicePixelRatio": 1.0, "colorScheme": "light"}, "freeze": {"enabled": true, "waitFonts": true, "networkIdle": false}}, "method": "user-test", "observation": "3 名运营参与者完成周报导出任务;其中 2 人先在全局工具栏寻找导出入口,经提示后使用行内入口", "interpretation": null, "scope": "单次会话, 3 名运营参与者, 便利抽样", "population": "3 名运营角色参与者(周报任务,便利抽样)", "ethics": null}
|
|
@@ -67,7 +67,7 @@ face: subjective
|
|
|
67
67
|
basis: agent-judgment
|
|
68
68
|
confidence: low
|
|
69
69
|
disposition: advisory
|
|
70
|
-
evidence: rendered 走查(agent-judgment, method=expert-review
|
|
70
|
+
evidence: rendered 走查(agent-judgment, method=expert-review);非用户证据——evidence/L6.1-usertest-notes.md 仅为合成 schema 示例,未登记到 Manifest,不能支持判断
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
## Positive findings
|
|
@@ -107,7 +107,7 @@ evidence: evidence/L6.1-export-trace.json(跨层:交互轨迹 + 度量计
|
|
|
107
107
|
## Limitations statement
|
|
108
108
|
|
|
109
109
|
- 判断类 advisory:术语适配(task-organization 主观面,agent-judgment 非用户证据,confidence=low,advisory 不阻 verdict)
|
|
110
|
-
- 用户代表性:本 run
|
|
110
|
+
- 用户代表性:本 run 无真实 user-test evidence;合成 notes 未登记到 Manifest,全部结论不构成任何「用户会」断言
|
|
111
111
|
- pass 范围:L6.1/L6.2/L6.3 pass 限单 viewport 1280x800 / 单数据集 week-2026-32 / 单次运行
|
|
112
112
|
- assumed 依赖:L6.2 pass 依赖 export.row_cap 假设成立
|
|
113
113
|
- 机器面证明声明与事实一致,不证明体验良好
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "design-playbook",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
4
4
|
"description": "Design I/O for coding agents: controllable UI generation via declarations (spec/domain/craft/design/components/template) and contracts (skill/evaluator). Use for product UI—console, dashboard, agent-ops, CJK-first apps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -37,7 +37,10 @@ if str(_PKG_ROOT) not in sys.path:
|
|
|
37
37
|
from design_playbook.scripts import rules_registry # noqa: E402
|
|
38
38
|
from design_playbook.scripts._diagnostics import Finding, finding # noqa: E402
|
|
39
39
|
from design_playbook.scripts.rules_registry import RegistryError # noqa: E402
|
|
40
|
-
from design_playbook.scripts.run_profile import
|
|
40
|
+
from design_playbook.scripts.run_profile import ( # noqa: E402
|
|
41
|
+
parse_run_profile,
|
|
42
|
+
validate_run_profile,
|
|
43
|
+
)
|
|
41
44
|
|
|
42
45
|
REGISTRY_PATH = (
|
|
43
46
|
Path(__file__).resolve().parents[1] / "skills" / "design-playbook"
|
|
@@ -144,9 +147,16 @@ def main(argv: list[str]) -> int:
|
|
|
144
147
|
try:
|
|
145
148
|
profile = parse_run_profile(
|
|
146
149
|
plan_path.read_text(encoding="utf-8"))
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
except (OSError, UnicodeError) as exc:
|
|
151
|
+
print(f"G8 INVALID: cannot read {plan_path}: {exc}", file=sys.stderr)
|
|
152
|
+
return 2
|
|
153
|
+
profile_errors = validate_run_profile(profile) if profile is not None else []
|
|
154
|
+
if profile_errors:
|
|
155
|
+
print("G8 INVALID:")
|
|
156
|
+
for error in profile_errors:
|
|
157
|
+
print(f" FAIL run-profile invalid: {error}")
|
|
158
|
+
return 1
|
|
159
|
+
tier = profile.tier if profile is not None else None
|
|
150
160
|
findings = check_g8_run(craft_text, entries, tier)
|
|
151
161
|
if not findings:
|
|
152
162
|
print("G8 OK: craft audit rows satisfy the run-level registry gate")
|
package/scripts/run_facts.py
CHANGED
|
@@ -13,6 +13,12 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
from design_playbook.mcp.evidence.ledger_syntax import LedgerFacts, parse_ledger
|
|
15
15
|
from design_playbook.mcp.preview.integrity import PreviewSnapshot, inspect_preview
|
|
16
|
+
from design_playbook.scripts.dd_entries import DDEntry, parse_dd_entries
|
|
17
|
+
from design_playbook.scripts.run_profile import RunProfile, parse_run_profile
|
|
18
|
+
from design_playbook.scripts.shaping_log import (
|
|
19
|
+
ShapingLogError,
|
|
20
|
+
parse_shaping_log,
|
|
21
|
+
)
|
|
16
22
|
from design_playbook.scripts.verdict_syntax import VerdictFacts, parse_verdict
|
|
17
23
|
|
|
18
24
|
|
|
@@ -38,6 +44,10 @@ class RunFacts:
|
|
|
38
44
|
evidence_dir: Path | None
|
|
39
45
|
spec_text: str
|
|
40
46
|
pointback_text: str
|
|
47
|
+
plan_text: str
|
|
48
|
+
plan_fill_artifacts: tuple[str, ...]
|
|
49
|
+
craft_guard_exists: bool
|
|
50
|
+
craft_guard_text: str
|
|
41
51
|
ledger: LedgerFacts
|
|
42
52
|
verdict: VerdictFacts
|
|
43
53
|
preview: PreviewSnapshot | None
|
|
@@ -46,6 +56,11 @@ class RunFacts:
|
|
|
46
56
|
_baseline_text: str | None
|
|
47
57
|
baseline_state_error: str | None
|
|
48
58
|
read_errors: tuple[ArtifactReadFact, ...]
|
|
59
|
+
run_profile: RunProfile | None = None
|
|
60
|
+
decision_report_text: str = ""
|
|
61
|
+
decision_entries: tuple[DDEntry, ...] = ()
|
|
62
|
+
shaping_events: tuple[dict[str, Any], ...] | None = None
|
|
63
|
+
shaping_error: str | None = None
|
|
49
64
|
|
|
50
65
|
@property
|
|
51
66
|
def manifest_entries(self) -> tuple[dict[str, Any], ...]:
|
|
@@ -60,6 +75,19 @@ class RunFacts:
|
|
|
60
75
|
return json.loads(self._baseline_text)
|
|
61
76
|
|
|
62
77
|
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class _OptionalRunFacts:
|
|
80
|
+
plan_text: str = ""
|
|
81
|
+
run_profile: RunProfile | None = None
|
|
82
|
+
decision_report_text: str = ""
|
|
83
|
+
decision_entries: tuple[DDEntry, ...] = ()
|
|
84
|
+
shaping_events: tuple[dict[str, Any], ...] | None = None
|
|
85
|
+
shaping_error: str | None = None
|
|
86
|
+
craft_guard_exists: bool = False
|
|
87
|
+
craft_guard_text: str = ""
|
|
88
|
+
read_errors: tuple[ArtifactReadFact, ...] = ()
|
|
89
|
+
|
|
90
|
+
|
|
63
91
|
def _read_manifest(
|
|
64
92
|
evidence_dir: Path | None,
|
|
65
93
|
) -> tuple[tuple[str, ...], tuple[ArtifactReadFact, ...]]:
|
|
@@ -155,6 +183,78 @@ def _existing_paths(run_root: Path | None) -> frozenset[str]:
|
|
|
155
183
|
return frozenset(paths)
|
|
156
184
|
|
|
157
185
|
|
|
186
|
+
def _plan_fill_artifacts(
|
|
187
|
+
run_root: Path | None,
|
|
188
|
+
plan_text: str,
|
|
189
|
+
) -> tuple[str, ...]:
|
|
190
|
+
"""Capture existing fill declarations while the run snapshot is loaded."""
|
|
191
|
+
if run_root is None:
|
|
192
|
+
return ()
|
|
193
|
+
found: list[str] = []
|
|
194
|
+
fenced = False
|
|
195
|
+
for line in plan_text.splitlines():
|
|
196
|
+
if line.lstrip().startswith("```"):
|
|
197
|
+
fenced = not fenced
|
|
198
|
+
continue
|
|
199
|
+
if fenced or not line.startswith("fill:"):
|
|
200
|
+
continue
|
|
201
|
+
declared = line[5:].strip().split()[0].rstrip(",") if line[5:].strip() else ""
|
|
202
|
+
if not declared:
|
|
203
|
+
continue
|
|
204
|
+
candidate = Path(declared)
|
|
205
|
+
bases = (
|
|
206
|
+
[candidate] if candidate.is_absolute()
|
|
207
|
+
else [run_root / candidate, Path.cwd() / candidate]
|
|
208
|
+
)
|
|
209
|
+
if any(base.is_file() for base in bases) and declared not in found:
|
|
210
|
+
found.append(declared)
|
|
211
|
+
return tuple(found)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _read_optional_run_facts(run_root: Path | None) -> _OptionalRunFacts:
|
|
215
|
+
"""Load optional vNext artifacts into one immutable snapshot."""
|
|
216
|
+
if run_root is None:
|
|
217
|
+
return _OptionalRunFacts()
|
|
218
|
+
|
|
219
|
+
plan_text, plan_error = _read_text("plan", run_root / "plan.md")
|
|
220
|
+
profile = parse_run_profile(plan_text) if plan_text else None
|
|
221
|
+
|
|
222
|
+
entries: tuple[DDEntry, ...] = ()
|
|
223
|
+
report_text = ""
|
|
224
|
+
report = run_root / "decision-report.md"
|
|
225
|
+
report_text, report_error = _read_text("decision_report", report)
|
|
226
|
+
if report_text:
|
|
227
|
+
entries = tuple(parse_dd_entries(report_text))
|
|
228
|
+
|
|
229
|
+
shaping_events: tuple[dict[str, Any], ...] | None = None
|
|
230
|
+
shaping_error: str | None = None
|
|
231
|
+
shaping_log = run_root / "shaping" / "shaping-log.jsonl"
|
|
232
|
+
try:
|
|
233
|
+
if shaping_log.is_file():
|
|
234
|
+
shaping_events = tuple(
|
|
235
|
+
parse_shaping_log(shaping_log.read_text(encoding="utf-8"))
|
|
236
|
+
)
|
|
237
|
+
except (OSError, UnicodeError, ShapingLogError) as exc:
|
|
238
|
+
shaping_events = None
|
|
239
|
+
shaping_error = str(exc)
|
|
240
|
+
craft_guard_text, craft_error = _read_text(
|
|
241
|
+
"craft_guard", run_root / "craft-guard.md"
|
|
242
|
+
)
|
|
243
|
+
craft_guard_exists = craft_error is None or craft_error.code != "missing"
|
|
244
|
+
errors = tuple(error for error in (plan_error, report_error, craft_error) if error)
|
|
245
|
+
return _OptionalRunFacts(
|
|
246
|
+
plan_text=plan_text,
|
|
247
|
+
run_profile=profile,
|
|
248
|
+
decision_report_text=report_text,
|
|
249
|
+
decision_entries=entries,
|
|
250
|
+
shaping_events=shaping_events,
|
|
251
|
+
shaping_error=shaping_error,
|
|
252
|
+
craft_guard_exists=craft_guard_exists,
|
|
253
|
+
craft_guard_text=craft_guard_text,
|
|
254
|
+
read_errors=errors,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
158
258
|
def capture_run_facts(
|
|
159
259
|
*,
|
|
160
260
|
spec_path: Path | None = None,
|
|
@@ -189,6 +289,7 @@ def capture_run_facts(
|
|
|
189
289
|
baseline_text, baseline_error = _read_baseline(run_root)
|
|
190
290
|
manifest_lines, manifest_errors = _read_manifest(evidence_dir)
|
|
191
291
|
preview = inspect_preview(preview_dir) if preview_dir is not None else None
|
|
292
|
+
optional = _read_optional_run_facts(run_root)
|
|
192
293
|
return RunFacts(
|
|
193
294
|
run_root=run_root,
|
|
194
295
|
spec_path=spec_path,
|
|
@@ -197,6 +298,10 @@ def capture_run_facts(
|
|
|
197
298
|
evidence_dir=evidence_dir,
|
|
198
299
|
spec_text=spec_text,
|
|
199
300
|
pointback_text=pointback_text,
|
|
301
|
+
plan_text=optional.plan_text,
|
|
302
|
+
plan_fill_artifacts=_plan_fill_artifacts(run_root, optional.plan_text),
|
|
303
|
+
craft_guard_exists=optional.craft_guard_exists,
|
|
304
|
+
craft_guard_text=optional.craft_guard_text,
|
|
200
305
|
ledger=parse_ledger(pointback_text),
|
|
201
306
|
verdict=parse_verdict(pointback_text),
|
|
202
307
|
preview=preview,
|
|
@@ -208,5 +313,12 @@ def capture_run_facts(
|
|
|
208
313
|
error
|
|
209
314
|
for error in (spec_error, pointback_error)
|
|
210
315
|
if error is not None
|
|
211
|
-
) + manifest_errors
|
|
316
|
+
) + manifest_errors + tuple(
|
|
317
|
+
error for error in optional.read_errors if error.code != "missing"
|
|
318
|
+
),
|
|
319
|
+
run_profile=optional.run_profile,
|
|
320
|
+
decision_report_text=optional.decision_report_text,
|
|
321
|
+
decision_entries=optional.decision_entries,
|
|
322
|
+
shaping_events=optional.shaping_events,
|
|
323
|
+
shaping_error=optional.shaping_error,
|
|
212
324
|
)
|
package/scripts/run_status.py
CHANGED
|
@@ -11,7 +11,6 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import argparse
|
|
13
13
|
import json
|
|
14
|
-
import re
|
|
15
14
|
import sys
|
|
16
15
|
from dataclasses import dataclass
|
|
17
16
|
from pathlib import Path
|
|
@@ -43,22 +42,13 @@ else:
|
|
|
43
42
|
# its status decision from the shared canonical value.
|
|
44
43
|
from design_playbook.scripts.stages import STAGES, STAGES_BY_KEY # noqa: E402
|
|
45
44
|
from design_playbook.scripts.run_facts import RunFacts, capture_run_facts # noqa: E402
|
|
46
|
-
from design_playbook.scripts.
|
|
47
|
-
from design_playbook.scripts.shaping_log import ( # noqa: E402
|
|
48
|
-
ShapingLogError,
|
|
49
|
-
load_shaping_facts,
|
|
50
|
-
queue_state,
|
|
51
|
-
)
|
|
45
|
+
from design_playbook.scripts.shaping_log import queue_state # noqa: E402
|
|
52
46
|
|
|
53
47
|
# vNext S4 re-entry narration (loop-prototype 7.1): repair rounds, route
|
|
54
48
|
# hit counts, dd supersedes / stale reviews, derived escalation signals,
|
|
55
49
|
# and the close_reason terminal narration, all read from artifacts the
|
|
56
50
|
# gates already consume (additive; plain runs report empty faces).
|
|
57
|
-
from design_playbook.scripts.dd_entries import
|
|
58
|
-
DD_HEADING,
|
|
59
|
-
dd_refs_in_pointback,
|
|
60
|
-
parse_dd_entries,
|
|
61
|
-
)
|
|
51
|
+
from design_playbook.scripts.dd_entries import dd_refs_in_pointback # noqa: E402
|
|
62
52
|
from design_playbook.scripts.escalation_signals import ( # noqa: E402
|
|
63
53
|
EscalationSignal,
|
|
64
54
|
collect_signals,
|
|
@@ -118,25 +108,16 @@ class RepairNarration:
|
|
|
118
108
|
|
|
119
109
|
|
|
120
110
|
def _repair_narration(
|
|
121
|
-
pointback_text: str,
|
|
122
|
-
upgrades: tuple[str, ...]
|
|
111
|
+
pointback_text: str,
|
|
112
|
+
upgrades: tuple[str, ...],
|
|
113
|
+
decision_entries: tuple,
|
|
114
|
+
) -> RepairNarration:
|
|
123
115
|
"""Derive the S4 re-entry faces from artifacts in the run root."""
|
|
124
116
|
rounds = parse_round_facts(pointback_text).max_rounds
|
|
125
117
|
routes = tuple(sorted(route_hits(pointback_text).items()))
|
|
126
118
|
dd_supersedes = len(dd_refs_in_pointback(pointback_text))
|
|
127
|
-
stale_reviews =
|
|
128
|
-
dd_explore =
|
|
129
|
-
report = run_root / "decision-report.md"
|
|
130
|
-
if report.is_file():
|
|
131
|
-
try:
|
|
132
|
-
text = report.read_text(encoding="utf-8")
|
|
133
|
-
except (OSError, UnicodeError):
|
|
134
|
-
text = ""
|
|
135
|
-
if text and DD_HEADING.search(text):
|
|
136
|
-
entries = parse_dd_entries(text)
|
|
137
|
-
stale_reviews = sum(
|
|
138
|
-
1 for entry in entries if entry.stale_review)
|
|
139
|
-
dd_explore = any(entry.tier == "explore" for entry in entries)
|
|
119
|
+
stale_reviews = sum(1 for entry in decision_entries if entry.stale_review)
|
|
120
|
+
dd_explore = any(entry.tier == "explore" for entry in decision_entries)
|
|
140
121
|
signals = list(collect_signals(pointback_text, dd_explore=dd_explore))
|
|
141
122
|
signals.extend(recorded_regrades(upgrades))
|
|
142
123
|
return RepairNarration(
|
|
@@ -149,47 +130,36 @@ def _repair_narration(
|
|
|
149
130
|
)
|
|
150
131
|
|
|
151
132
|
|
|
152
|
-
def inspect_vnext(
|
|
153
|
-
|
|
133
|
+
def inspect_vnext(
|
|
134
|
+
run_root: Path,
|
|
135
|
+
run_facts: RunFacts | None = None,
|
|
136
|
+
) -> VnextNarration:
|
|
137
|
+
"""Project vNext facts from one immutable run snapshot."""
|
|
138
|
+
facts = run_facts or capture_run_facts(run_root=run_root)
|
|
154
139
|
tier = None
|
|
155
140
|
confirmed_by = None
|
|
156
141
|
skipped: tuple[tuple[str, str], ...] = ()
|
|
157
142
|
upgrades: tuple[str, ...] = ()
|
|
158
|
-
|
|
159
|
-
if
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
profile = None
|
|
165
|
-
if profile is not None:
|
|
166
|
-
tier = profile.tier or None
|
|
167
|
-
confirmed_by = profile.confirmed_by or None
|
|
168
|
-
skipped = profile.skipped
|
|
169
|
-
upgrades = profile.upgrades
|
|
143
|
+
profile = facts.run_profile
|
|
144
|
+
if profile is not None:
|
|
145
|
+
tier = profile.tier or None
|
|
146
|
+
confirmed_by = profile.confirmed_by or None
|
|
147
|
+
skipped = profile.skipped
|
|
148
|
+
upgrades = profile.upgrades
|
|
170
149
|
shaping: str | None = None
|
|
171
|
-
|
|
172
|
-
session = load_shaping_facts(run_root)
|
|
173
|
-
except (ShapingLogError, OSError, UnicodeError):
|
|
150
|
+
if facts.shaping_error is not None:
|
|
174
151
|
shaping = "unreadable"
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
pb_text = ""
|
|
182
|
-
if pointback.is_file():
|
|
183
|
-
try:
|
|
184
|
-
pb_text = pointback.read_text(encoding="utf-8")
|
|
185
|
-
except (OSError, UnicodeError):
|
|
186
|
-
pb_text = ""
|
|
187
|
-
six_block = "## Coverage statement" in pb_text
|
|
188
|
-
invalidated = "\ninvalidated:" in pb_text or pb_text.startswith(
|
|
189
|
-
"invalidated:")
|
|
152
|
+
elif facts.shaping_events is not None:
|
|
153
|
+
shaping = queue_state(list(facts.shaping_events))
|
|
154
|
+
pb_text = facts.pointback_text
|
|
155
|
+
six_block = "## Coverage statement" in pb_text
|
|
156
|
+
invalidated = "\ninvalidated:" in pb_text or pb_text.startswith(
|
|
157
|
+
"invalidated:")
|
|
190
158
|
repair = None
|
|
191
159
|
if pb_text:
|
|
192
|
-
repair = _repair_narration(
|
|
160
|
+
repair = _repair_narration(
|
|
161
|
+
pb_text, upgrades, facts.decision_entries
|
|
162
|
+
)
|
|
193
163
|
return VnextNarration(
|
|
194
164
|
tier=tier, confirmed_by=confirmed_by, skipped=skipped,
|
|
195
165
|
upgrades=upgrades, shaping=shaping,
|
|
@@ -206,47 +176,7 @@ class StageState:
|
|
|
206
176
|
evidence: list[str]
|
|
207
177
|
|
|
208
178
|
|
|
209
|
-
#
|
|
210
|
-
# of the run root. When plan.md registers those paths as ``fill:`` field
|
|
211
|
-
# lines, the fill stage is also judged on their existence — one path per
|
|
212
|
-
# line, run-root-relative or host-project-relative (the orchestrating cwd).
|
|
213
|
-
# Only unfenced column-0 field lines are declarations; fenced blocks are
|
|
214
|
-
# prose/examples and are never read as declarations.
|
|
215
|
-
PLAN_FILL_LINE = re.compile(r"^fill:[ \t]*(\S+)")
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def _plan_fill_artifacts(run_root: Path) -> list[str]:
|
|
219
|
-
"""Declared fill artifact paths from plan.md that exist on disk.
|
|
220
|
-
|
|
221
|
-
Fenced code blocks (```` ``` ````) are skipped while scanning: an
|
|
222
|
-
example or prose block citing ``fill: spec.md`` is not a declaration
|
|
223
|
-
(fail-closed — the fill stage stays unchecked rather than counting a
|
|
224
|
-
narrated example).
|
|
225
|
-
"""
|
|
226
|
-
try:
|
|
227
|
-
text = (run_root / "plan.md").read_text(encoding="utf-8")
|
|
228
|
-
except (OSError, UnicodeError):
|
|
229
|
-
return []
|
|
230
|
-
found: list[str] = []
|
|
231
|
-
fenced = False
|
|
232
|
-
for line in text.splitlines():
|
|
233
|
-
if line.lstrip().startswith("```"):
|
|
234
|
-
fenced = not fenced
|
|
235
|
-
continue
|
|
236
|
-
if fenced:
|
|
237
|
-
continue
|
|
238
|
-
match = PLAN_FILL_LINE.match(line)
|
|
239
|
-
if match is None:
|
|
240
|
-
continue
|
|
241
|
-
declared = match.group(1).strip().rstrip(",")
|
|
242
|
-
candidate = Path(declared)
|
|
243
|
-
bases = (
|
|
244
|
-
[candidate] if candidate.is_absolute()
|
|
245
|
-
else [run_root / candidate, Path.cwd() / candidate]
|
|
246
|
-
)
|
|
247
|
-
if any(base.is_file() for base in bases) and declared not in found:
|
|
248
|
-
found.append(declared)
|
|
249
|
-
return found
|
|
179
|
+
# Plan fill declarations are captured by RunFacts; status only projects them.
|
|
250
180
|
|
|
251
181
|
|
|
252
182
|
def inspect_run(
|
|
@@ -255,7 +185,7 @@ def inspect_run(
|
|
|
255
185
|
) -> list[StageState]:
|
|
256
186
|
facts = run_facts or capture_run_facts(run_root=run_root)
|
|
257
187
|
snapshot = preview_snapshot or facts.preview or inspect_preview(run_root / "preview")
|
|
258
|
-
plan_fills =
|
|
188
|
+
plan_fills = list(facts.plan_fill_artifacts)
|
|
259
189
|
states: list[StageState] = []
|
|
260
190
|
for stage in STAGES:
|
|
261
191
|
if stage.key == "preview":
|
|
@@ -424,24 +354,21 @@ def render(run_root: Path, *, as_json: bool) -> int:
|
|
|
424
354
|
print(f"RUN STATUS ERROR: not a directory: {run_root}", file=sys.stderr)
|
|
425
355
|
return 2
|
|
426
356
|
facts = capture_run_facts(run_root=run_root)
|
|
427
|
-
|
|
428
|
-
(
|
|
429
|
-
error
|
|
430
|
-
for error in facts.read_errors
|
|
431
|
-
if error.artifact == "point_back" and error.code != "missing"
|
|
432
|
-
),
|
|
357
|
+
read_error = next(
|
|
358
|
+
(error for error in facts.read_errors if error.code != "missing"),
|
|
433
359
|
None,
|
|
434
360
|
)
|
|
435
|
-
if
|
|
361
|
+
if read_error is not None:
|
|
436
362
|
print(
|
|
437
|
-
f"RUN STATUS ERROR: cannot read
|
|
363
|
+
f"RUN STATUS ERROR: cannot read {read_error.path.name}: "
|
|
364
|
+
f"{read_error.message}",
|
|
438
365
|
file=sys.stderr,
|
|
439
366
|
)
|
|
440
367
|
return 2
|
|
441
368
|
snapshot = facts.preview or inspect_preview(run_root / "preview")
|
|
442
369
|
states = inspect_run(run_root, snapshot, facts)
|
|
443
370
|
action = next_action(states, run_root, snapshot, facts)
|
|
444
|
-
vnext = inspect_vnext(run_root)
|
|
371
|
+
vnext = inspect_vnext(run_root, facts)
|
|
445
372
|
payload = {
|
|
446
373
|
"run_root": str(run_root),
|
|
447
374
|
"stages": [
|
package/scripts/validate_run.py
CHANGED
|
@@ -144,7 +144,7 @@ from design_playbook.scripts.shaping_log import ( # noqa: E402
|
|
|
144
144
|
from design_playbook.scripts.dd_entries import DD_HEADING # noqa: E402
|
|
145
145
|
from design_playbook.scripts.g10_design_decisions import check_g10 # noqa: E402
|
|
146
146
|
from design_playbook.scripts.repair_rounds import check_rounds # noqa: E402
|
|
147
|
-
from design_playbook.scripts.run_profile import
|
|
147
|
+
from design_playbook.scripts.run_profile import validate_run_profile # noqa: E402
|
|
148
148
|
|
|
149
149
|
# vNext S3 gates: method-semantics keys (G6-adjacent), interaction-track
|
|
150
150
|
# dimension annotations (G2-adjacent), sampling-matrix gaps (G11), and the
|
|
@@ -198,6 +198,17 @@ def run(
|
|
|
198
198
|
spec_path=Path(spec_path), pointback_path=Path(pb_path),
|
|
199
199
|
preview_dir=pd, evidence_dir=ed, run_root=rr,
|
|
200
200
|
)
|
|
201
|
+
profile = facts.run_profile
|
|
202
|
+
if profile is not None:
|
|
203
|
+
for error in validate_run_profile(profile):
|
|
204
|
+
errs.append(finding(
|
|
205
|
+
"G12.run_profile",
|
|
206
|
+
f"run-profile invalid: {error}",
|
|
207
|
+
owner="plan.md#run-profile",
|
|
208
|
+
expected="supported v1 run-profile with valid tier and confirmation",
|
|
209
|
+
actual=error,
|
|
210
|
+
repair="Fix the run-profile block before running vNext gates",
|
|
211
|
+
))
|
|
201
212
|
operational_errors = tuple(
|
|
202
213
|
error for error in facts.read_errors if error.artifact != "manifest"
|
|
203
214
|
)
|
|
@@ -232,7 +243,10 @@ def run(
|
|
|
232
243
|
# vNext S6: the effective tier P3 makes the matrix block itself
|
|
233
244
|
# mandatory (loop-prototype 1.2 "sampling matrix fully executed").
|
|
234
245
|
errs += check_sampling_matrix(
|
|
235
|
-
pointback_text, spec_text, evidence_dir=ed, tier=
|
|
246
|
+
pointback_text, spec_text, evidence_dir=ed, tier=(
|
|
247
|
+
effective_tier(profile.tier, profile.upgrades)
|
|
248
|
+
if profile is not None else None
|
|
249
|
+
))
|
|
236
250
|
# G2 dimensions (vNext S3): dimension/face/basis annotations on findings
|
|
237
251
|
# — subjective faces are judgment class (advisory only, source declared).
|
|
238
252
|
errs += check_dimensions(pointback_text)
|
|
@@ -309,20 +323,23 @@ def run(
|
|
|
309
323
|
sd = Path(shaping_dir) if shaping_dir else (
|
|
310
324
|
rr / "shaping" if rr is not None else None
|
|
311
325
|
)
|
|
312
|
-
shaping_events: list[dict] | None =
|
|
326
|
+
shaping_events: list[dict] | None = (
|
|
327
|
+
list(facts.shaping_events) if facts.shaping_events is not None else None
|
|
328
|
+
)
|
|
313
329
|
if sd is not None and (sd / Path(SHAPING_LOG).name).is_file():
|
|
314
330
|
errs += check_g9(
|
|
315
331
|
sd,
|
|
316
332
|
project_dir=Path(contract_project) if contract_project else None,
|
|
317
333
|
run_dir=Path(contract_run) if contract_run else rr,
|
|
318
334
|
)
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
335
|
+
# Explicit --shaping-dir may point outside the captured run root.
|
|
336
|
+
# Default discovery already parsed the same log into RunFacts.
|
|
337
|
+
if shaping_dir is not None:
|
|
338
|
+
try:
|
|
339
|
+
shaping_events = parse_shaping_log(
|
|
340
|
+
(sd / SHAPING_LOG).read_text(encoding="utf-8"))
|
|
341
|
+
except (OSError, UnicodeError, ShapingLogError):
|
|
342
|
+
shaping_events = None
|
|
326
343
|
|
|
327
344
|
# G10 (conditional): a decision report carrying DD entry blocks engages
|
|
328
345
|
# the design-decision gate (top block stays verbatim; reports without
|
|
@@ -330,10 +347,10 @@ def run(
|
|
|
330
347
|
dr_g10 = dr if dr is not None else (
|
|
331
348
|
rr / "decision-report.md" if rr is not None else None
|
|
332
349
|
)
|
|
333
|
-
report_text = ""
|
|
334
|
-
if
|
|
350
|
+
report_text = facts.decision_report_text if dr is None else ""
|
|
351
|
+
if dr is not None and dr.is_file():
|
|
335
352
|
try:
|
|
336
|
-
report_text =
|
|
353
|
+
report_text = dr.read_text(encoding="utf-8")
|
|
337
354
|
except (OSError, UnicodeError):
|
|
338
355
|
report_text = ""
|
|
339
356
|
if report_text and DD_HEADING.search(report_text):
|
|
@@ -343,18 +360,19 @@ def run(
|
|
|
343
360
|
preview_dir=pd,
|
|
344
361
|
shaping_events=shaping_events,
|
|
345
362
|
pointback_text=pointback_text,
|
|
346
|
-
baseline_state=
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
363
|
+
baseline_state=facts.baseline_state,
|
|
364
|
+
run_profile_tier=(
|
|
365
|
+
effective_tier(profile.tier, profile.upgrades)
|
|
366
|
+
if profile is not None else None
|
|
367
|
+
),
|
|
350
368
|
)
|
|
351
369
|
|
|
352
370
|
# G8 run level (vNext S3): a craft-guard.md in the run root is checked
|
|
353
371
|
# against the registry (shared parser). P2/P3 demand one audit row per
|
|
354
372
|
# advisory entry; P1 and tier-less legacy runs keep the subset freedom.
|
|
355
|
-
if
|
|
373
|
+
if facts.craft_guard_exists:
|
|
356
374
|
try:
|
|
357
|
-
craft_text =
|
|
375
|
+
craft_text = facts.craft_guard_text
|
|
358
376
|
registry_entries, _ = load_registry()
|
|
359
377
|
except (OSError, UnicodeError):
|
|
360
378
|
errs.append(finding(
|
|
@@ -367,13 +385,15 @@ def run(
|
|
|
367
385
|
repair="Restore the files or drop the audit log",
|
|
368
386
|
))
|
|
369
387
|
else:
|
|
370
|
-
errs += check_g8_run(craft_text, registry_entries,
|
|
388
|
+
errs += check_g8_run(craft_text, registry_entries, (
|
|
389
|
+
effective_tier(profile.tier, profile.upgrades)
|
|
390
|
+
if profile is not None else None
|
|
391
|
+
))
|
|
371
392
|
|
|
372
393
|
# G12 tier boundary + escalation signals (vNext S4): fires when plan.md
|
|
373
394
|
# carries a run-profile block; legacy runs without the block are not
|
|
374
395
|
# re-checked. The contract diff basis is the G7 bind snapshot; without
|
|
375
396
|
# contract paths the route / decision / blocking faces still fire.
|
|
376
|
-
profile = _plan_profile(rr)
|
|
377
397
|
if profile is not None:
|
|
378
398
|
touch = None
|
|
379
399
|
bound_criteria = None
|
|
@@ -386,10 +406,13 @@ def run(
|
|
|
386
406
|
touch = contract_touch(bound, effective)
|
|
387
407
|
bound_criteria = sum(
|
|
388
408
|
1 for path in bound if CRITERION_PATH.match(path))
|
|
389
|
-
dd_explore =
|
|
409
|
+
dd_explore = any(
|
|
410
|
+
entry.tier == "explore" for entry in facts.decision_entries
|
|
411
|
+
) if dr is None else bool(
|
|
390
412
|
report_text and DD_HEADING.search(report_text)
|
|
391
413
|
and any(entry.tier == "explore"
|
|
392
|
-
for entry in parse_dd_entries(report_text))
|
|
414
|
+
for entry in parse_dd_entries(report_text))
|
|
415
|
+
)
|
|
393
416
|
g12_errs, g12_warns, _signals = check_g12(
|
|
394
417
|
profile,
|
|
395
418
|
pointback_text=pointback_text,
|
|
@@ -403,40 +426,6 @@ def run(
|
|
|
403
426
|
return errs, warns
|
|
404
427
|
|
|
405
428
|
|
|
406
|
-
def _load_json_tolerant(path: Path) -> dict | None:
|
|
407
|
-
try:
|
|
408
|
-
import json
|
|
409
|
-
|
|
410
|
-
data = json.loads(path.read_text(encoding="utf-8"))
|
|
411
|
-
except (OSError, UnicodeError, ValueError):
|
|
412
|
-
return None
|
|
413
|
-
return data if isinstance(data, dict) else None
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
def _plan_profile(run_root: Path | None):
|
|
417
|
-
"""The parsed run-profile block of a run root; None when absent."""
|
|
418
|
-
if run_root is None:
|
|
419
|
-
return None
|
|
420
|
-
plan = run_root / "plan.md"
|
|
421
|
-
try:
|
|
422
|
-
return parse_run_profile(plan.read_text(encoding="utf-8"))
|
|
423
|
-
except (OSError, UnicodeError):
|
|
424
|
-
return None
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
def _plan_tier(run_root: Path | None) -> str | None:
|
|
428
|
-
"""The run's effective tier: declared tier plus recorded S4 upgrades.
|
|
429
|
-
|
|
430
|
-
G8/G10 consume this; after an escalation (run-profile upgrades event)
|
|
431
|
-
the run walks the new tier's obligations, so the effective tier — not
|
|
432
|
-
the intake declaration — is the gate input.
|
|
433
|
-
"""
|
|
434
|
-
profile = _plan_profile(run_root)
|
|
435
|
-
if profile is None:
|
|
436
|
-
return None
|
|
437
|
-
return effective_tier(profile.tier, profile.upgrades)
|
|
438
|
-
|
|
439
|
-
|
|
440
429
|
def _parse_args(argv: list[str]) -> argparse.Namespace:
|
|
441
430
|
parser = argparse.ArgumentParser(
|
|
442
431
|
prog="validate_run.py",
|