superlab 0.1.63 → 0.1.64
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/lib/auto_state.cjs +3 -0
- package/lib/i18n.cjs +6 -4
- package/lib/install.cjs +1 -1
- package/lib/rule_preflight.cjs +49 -1
- package/package-assets/shared/lab/.managed/scripts/paper_topology.py +91 -0
- package/package-assets/shared/lab/.managed/scripts/render_rule_preflight.py +115 -0
- package/package-assets/shared/lab/.managed/scripts/validate_manuscript_delivery.py +2 -0
- package/package-assets/shared/lab/.managed/scripts/validate_paper_topology.py +83 -0
- package/package-assets/shared/lab/.managed/scripts/validate_rule_preflight.py +183 -0
- package/package-assets/shared/lab/.managed/scripts/validate_section_draft.py +28 -12
- package/package-assets/shared/lab/.managed/templates/iteration-report.md +1 -0
- package/package-assets/shared/lab/.managed/templates/write-iteration.md +1 -0
- package/package-assets/shared/lab/context/auto-status.md +1 -0
- package/package-assets/shared/skills/lab/SKILL.md +2 -0
- package/package-assets/shared/skills/lab/stages/auto.md +1 -1
- package/package-assets/shared/skills/lab/stages/write.md +4 -3
- package/package.json +1 -1
package/lib/auto_state.cjs
CHANGED
|
@@ -99,6 +99,7 @@ function parseAutoStatus(targetDir) {
|
|
|
99
99
|
resolvedStage: extractValue(text, ["Resolved stage", "解析后的阶段"]),
|
|
100
100
|
resolvedMode: extractValue(text, ["Resolved mode", "解析后的模式"]),
|
|
101
101
|
resolvedTarget: extractValue(text, ["Resolved target", "解析后的目标"]),
|
|
102
|
+
preflightStamp: extractValue(text, ["Preflight stamp", "预检签名"]),
|
|
102
103
|
overrideReason: extractValue(text, ["Override reason, if any", "如有覆盖理由"]),
|
|
103
104
|
status: extractValue(text, ["Status", "状态"]),
|
|
104
105
|
currentStage: extractValue(text, ["Current stage", "当前阶段"]),
|
|
@@ -155,6 +156,7 @@ function renderAutoStatus(status, { lang = "en" } = {}) {
|
|
|
155
156
|
- 解析后的阶段: ${status.resolvedStage || ""}
|
|
156
157
|
- 解析后的模式: ${status.resolvedMode || ""}
|
|
157
158
|
- 解析后的目标: ${status.resolvedTarget || ""}
|
|
159
|
+
- 预检签名: ${status.preflightStamp || ""}
|
|
158
160
|
- 如有覆盖理由: ${status.overrideReason || ""}
|
|
159
161
|
|
|
160
162
|
## 运行状态
|
|
@@ -194,6 +196,7 @@ function renderAutoStatus(status, { lang = "en" } = {}) {
|
|
|
194
196
|
- Resolved stage: ${status.resolvedStage || ""}
|
|
195
197
|
- Resolved mode: ${status.resolvedMode || ""}
|
|
196
198
|
- Resolved target: ${status.resolvedTarget || ""}
|
|
199
|
+
- Preflight stamp: ${status.preflightStamp || ""}
|
|
197
200
|
- Override reason, if any: ${status.overrideReason || ""}
|
|
198
201
|
|
|
199
202
|
## Runtime State
|
package/lib/i18n.cjs
CHANGED
|
@@ -2082,9 +2082,10 @@ ZH_CONTENT[path.join(".codex", "skills", "lab", "stages", "write.md")] = `# \`/l
|
|
|
2082
2082
|
## 规则预检
|
|
2083
2083
|
|
|
2084
2084
|
- 起草前先读取 \`.lab/.managed/rule-manifest.json\`。
|
|
2085
|
-
-
|
|
2086
|
-
- \`Rule Preflight\` 至少记录:Rule source file、Rule source revision、Project version、Resolved stage、Resolved mode、Resolved target,以及任何 override reason。
|
|
2085
|
+
- 先用 \`.lab/.managed/scripts/render_rule_preflight.py\` 生成 write iteration 里的 \`Rule Preflight\`,再改 prose;不要凭记忆手填。
|
|
2086
|
+
- \`Rule Preflight\` 至少记录:Rule source file、Rule source revision、Project version、Resolved stage、Resolved mode、Resolved target、Preflight stamp,以及任何 override reason。
|
|
2087
2087
|
- 如果已安装的 write 规则和当前轮次行为不一致,就先修正目标层或补充有效 override 理由,再继续编辑。
|
|
2088
|
+
- 在 draft 模式下,\`Rule Preflight\` 不一致和 paper topology 不一致都属于 blocker,不是可以带着 warning 继续润色的问题。
|
|
2088
2089
|
|
|
2089
2090
|
## 小步写作规则
|
|
2090
2091
|
|
|
@@ -2393,7 +2394,8 @@ description: 严格研究工作流,覆盖 idea、data、auto、framing、spec
|
|
|
2393
2394
|
- 使用 \`.lab/config/workflow.json\` 作为全局约束,统一管理 workflow language、paper language、paper format、results_root、figures_root、deliverables_root、paper_template_root 和 paper-language 的最终定稿决定。
|
|
2394
2395
|
- 工作流中间工件默认跟随安装语言。
|
|
2395
2396
|
- 每个 \`/lab:*\` stage 开始前,都要先读取 \`.lab/.managed/rule-manifest.json\` 并完成一段 \`Rule Preflight\`。
|
|
2396
|
-
- 每个 stage round 的受管工件都要先记录:Rule source file、Rule source revision、Project version、Resolved stage、Resolved mode、Resolved target,以及任何 override reason。
|
|
2397
|
+
- 每个 stage round 的受管工件都要先记录:Rule source file、Rule source revision、Project version、Resolved stage、Resolved mode、Resolved target、Preflight stamp,以及任何 override reason。
|
|
2398
|
+
- 这段 \`Rule Preflight\` 应通过受管脚本生成,而不是靠记忆手填。
|
|
2397
2399
|
- 如果 \`Rule Preflight\` 缺失、过期或和实际行为冲突,就把它视为 stage-contract failure,而不是继续润色或继续推进。
|
|
2398
2400
|
- 项目里已安装的规则优先于模型记忆;如果记忆里的旧做法和 \`.lab/.managed/rule-manifest.json\` 记录的规则冲突,以项目里安装的规则为准。
|
|
2399
2401
|
- 最终论文默认输出为 LaTeX,论文语言与工作流语言分开决定。
|
|
@@ -2878,7 +2880,7 @@ ZH_CONTENT[path.join(".codex", "skills", "lab", "stages", "auto.md")] = `# \`/la
|
|
|
2878
2880
|
## 规则预检
|
|
2879
2881
|
|
|
2880
2882
|
- 启动 auto 前先读取 \`.lab/.managed/rule-manifest.json\`。
|
|
2881
|
-
- 可见的 \`Auto preflight\` 块里除了契约字段外,还要写明:Rule source file、Rule source revision、Project version、Resolved stage、Resolved mode、Resolved target,以及任何 override reason。
|
|
2883
|
+
- 可见的 \`Auto preflight\` 块里除了契约字段外,还要写明:Rule source file、Rule source revision、Project version、Resolved stage、Resolved mode、Resolved target、Preflight stamp,以及任何 override reason。
|
|
2882
2884
|
- 这些 \`Rule Preflight\` 字段也要保存在 \`.lab/context/auto-status.md\`。
|
|
2883
2885
|
- 如果已安装的 auto 规则和当前 campaign 行为不一致,就先修正契约或补充有效 override 理由,再启动 loop。
|
|
2884
2886
|
|
package/lib/install.cjs
CHANGED
|
@@ -7,7 +7,7 @@ const { getLocalizedContent } = require("./i18n.cjs");
|
|
|
7
7
|
|
|
8
8
|
const REPO_ROOT = path.resolve(__dirname, "..");
|
|
9
9
|
const PACKAGE_VERSION = require(path.join(REPO_ROOT, "package.json")).version;
|
|
10
|
-
const LAYOUT_VERSION =
|
|
10
|
+
const LAYOUT_VERSION = 2;
|
|
11
11
|
const SUPERLAB_START = "<!-- superlab:start -->";
|
|
12
12
|
const SUPERLAB_END = "<!-- superlab:end -->";
|
|
13
13
|
|
package/lib/rule_preflight.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require("node:fs");
|
|
2
2
|
const path = require("node:path");
|
|
3
|
+
const crypto = require("node:crypto");
|
|
3
4
|
|
|
4
5
|
function ruleManifestPath(targetDir) {
|
|
5
6
|
return path.join(targetDir, ".lab", ".managed", "rule-manifest.json");
|
|
@@ -33,7 +34,7 @@ function buildRulePreflight({
|
|
|
33
34
|
overrideReason = "",
|
|
34
35
|
} = {}) {
|
|
35
36
|
const entry = resolveStageRuleEntry(targetDir, stage) || {};
|
|
36
|
-
|
|
37
|
+
const preflight = {
|
|
37
38
|
ruleSourceFile: entry.ruleSourceFile || "",
|
|
38
39
|
ruleSourceRevision: entry.ruleSourceRevision || "",
|
|
39
40
|
projectVersion: entry.packageVersion || "",
|
|
@@ -42,11 +43,58 @@ function buildRulePreflight({
|
|
|
42
43
|
resolvedTarget: target || "",
|
|
43
44
|
overrideReason: overrideReason || "",
|
|
44
45
|
};
|
|
46
|
+
return {
|
|
47
|
+
...preflight,
|
|
48
|
+
preflightStamp: computeRulePreflightStamp(preflight),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function computeRulePreflightStamp(preflight = {}) {
|
|
53
|
+
const payload = {
|
|
54
|
+
ruleSourceFile: preflight.ruleSourceFile || "",
|
|
55
|
+
ruleSourceRevision: preflight.ruleSourceRevision || "",
|
|
56
|
+
projectVersion: preflight.projectVersion || "",
|
|
57
|
+
resolvedStage: preflight.resolvedStage || "",
|
|
58
|
+
resolvedMode: preflight.resolvedMode || "",
|
|
59
|
+
resolvedTarget: preflight.resolvedTarget || "",
|
|
60
|
+
overrideReason: preflight.overrideReason || "",
|
|
61
|
+
};
|
|
62
|
+
return crypto.createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderRulePreflightSection(preflight = {}, { lang = "en" } = {}) {
|
|
66
|
+
if (lang === "zh") {
|
|
67
|
+
return `## 规则预检
|
|
68
|
+
|
|
69
|
+
- 规则来源文件: ${preflight.ruleSourceFile || ""}
|
|
70
|
+
- 规则来源修订: ${preflight.ruleSourceRevision || ""}
|
|
71
|
+
- 项目版本: ${preflight.projectVersion || ""}
|
|
72
|
+
- 解析后的阶段: ${preflight.resolvedStage || ""}
|
|
73
|
+
- 解析后的模式: ${preflight.resolvedMode || ""}
|
|
74
|
+
- 解析后的目标: ${preflight.resolvedTarget || ""}
|
|
75
|
+
- 预检签名: ${preflight.preflightStamp || ""}
|
|
76
|
+
- 如有覆盖理由: ${preflight.overrideReason || ""}
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return `## Rule Preflight
|
|
81
|
+
|
|
82
|
+
- Rule source file: ${preflight.ruleSourceFile || ""}
|
|
83
|
+
- Rule source revision: ${preflight.ruleSourceRevision || ""}
|
|
84
|
+
- Project version: ${preflight.projectVersion || ""}
|
|
85
|
+
- Resolved stage: ${preflight.resolvedStage || ""}
|
|
86
|
+
- Resolved mode: ${preflight.resolvedMode || ""}
|
|
87
|
+
- Resolved target: ${preflight.resolvedTarget || ""}
|
|
88
|
+
- Preflight stamp: ${preflight.preflightStamp || ""}
|
|
89
|
+
- Override reason, if any: ${preflight.overrideReason || ""}
|
|
90
|
+
`;
|
|
45
91
|
}
|
|
46
92
|
|
|
47
93
|
module.exports = {
|
|
48
94
|
buildRulePreflight,
|
|
95
|
+
computeRulePreflightStamp,
|
|
49
96
|
readRuleManifest,
|
|
97
|
+
renderRulePreflightSection,
|
|
50
98
|
resolveStageRuleEntry,
|
|
51
99
|
ruleManifestPath,
|
|
52
100
|
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
DEPRECATION_MARKERS = ("archived", "deprecated", "legacy", "out-of-band", "归档", "废弃", "旧层", "旁路", "历史")
|
|
9
|
+
PATH_TOKEN_PATTERN = re.compile(
|
|
10
|
+
r"(/[^ \t\n\r)]+|(?:[A-Za-z0-9_.-]+/)+[A-Za-z0-9_.\-/]+)"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def find_project_root(start_path: Path) -> Path | None:
|
|
15
|
+
for parent in (start_path, *start_path.parents):
|
|
16
|
+
workflow_config = parent / ".lab" / "config" / "workflow.json"
|
|
17
|
+
if workflow_config.exists():
|
|
18
|
+
return parent
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_workflow_config(project_root: Path | None) -> dict:
|
|
23
|
+
if project_root is None:
|
|
24
|
+
return {}
|
|
25
|
+
workflow_config = project_root / ".lab" / "config" / "workflow.json"
|
|
26
|
+
if not workflow_config.exists():
|
|
27
|
+
return {}
|
|
28
|
+
try:
|
|
29
|
+
return json.loads(workflow_config.read_text(encoding="utf-8"))
|
|
30
|
+
except json.JSONDecodeError:
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_paper_topology(project_root: Path, workflow_config: dict | None = None) -> dict:
|
|
35
|
+
config = workflow_config or load_workflow_config(project_root)
|
|
36
|
+
deliverables_root = (project_root / config.get("deliverables_root", "docs/research")).resolve()
|
|
37
|
+
canonical_root = (deliverables_root / "paper").resolve()
|
|
38
|
+
workflow_language = config.get("workflow_language", "")
|
|
39
|
+
paper_language = config.get("paper_language", "")
|
|
40
|
+
workflow_language_active = bool(workflow_language and paper_language and workflow_language != paper_language)
|
|
41
|
+
workflow_root = (canonical_root / "workflow-language").resolve() if workflow_language_active else None
|
|
42
|
+
return {
|
|
43
|
+
"project_root": project_root.resolve(),
|
|
44
|
+
"deliverables_root": deliverables_root,
|
|
45
|
+
"canonical_root": canonical_root,
|
|
46
|
+
"workflow_root": workflow_root,
|
|
47
|
+
"workflow_language_active": workflow_language_active,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def normalize_path(path_value: str | Path, project_root: Path) -> Path:
|
|
52
|
+
path_obj = Path(path_value)
|
|
53
|
+
return path_obj.resolve() if path_obj.is_absolute() else (project_root / path_obj).resolve()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_within(candidate: Path, root: Path | None) -> bool:
|
|
57
|
+
if root is None:
|
|
58
|
+
return False
|
|
59
|
+
try:
|
|
60
|
+
candidate.relative_to(root)
|
|
61
|
+
return True
|
|
62
|
+
except ValueError:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def classify_paper_path(path_value: str | Path, topology: dict) -> tuple[str, Path]:
|
|
67
|
+
candidate = normalize_path(path_value, topology["project_root"])
|
|
68
|
+
if topology["workflow_language_active"] and is_within(candidate, topology["workflow_root"]):
|
|
69
|
+
return "active-workflow-language", candidate
|
|
70
|
+
if is_within(candidate, topology["canonical_root"]):
|
|
71
|
+
return "active-canonical", candidate
|
|
72
|
+
|
|
73
|
+
parts = candidate.parts
|
|
74
|
+
if any(part.startswith("review_") or part.startswith("translation_") or part.startswith("sections_") for part in parts):
|
|
75
|
+
return "legacy-paper-layer", candidate
|
|
76
|
+
if "deliverables" in parts and ("sections" in parts or "workflow-language" in parts):
|
|
77
|
+
return "legacy-paper-layer", candidate
|
|
78
|
+
if "docs" in parts and "lab" in parts and "paper" in parts:
|
|
79
|
+
return "legacy-paper-layer", candidate
|
|
80
|
+
return "out-of-band", candidate
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def line_is_deprecated(line: str) -> bool:
|
|
84
|
+
lowered = line.lower()
|
|
85
|
+
return any(marker in lowered for marker in DEPRECATION_MARKERS)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def iter_path_tokens(text: str):
|
|
89
|
+
for line_number, line in enumerate(text.splitlines(), start=1):
|
|
90
|
+
for match in PATH_TOKEN_PATTERN.finditer(line):
|
|
91
|
+
yield line_number, line, match.group(1)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from validate_rule_preflight import compute_preflight_stamp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_args():
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
description="Render or inject a machine-generated Rule Preflight block for a managed lab stage artifact."
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("--artifact", required=True, help="Markdown artifact to update in place")
|
|
17
|
+
parser.add_argument("--stage", required=True, help="Stage name, e.g. write or auto")
|
|
18
|
+
parser.add_argument("--project-root", required=True, help="Project root containing .lab/.managed/rule-manifest.json")
|
|
19
|
+
parser.add_argument("--mode", required=True, help="Resolved mode for this round")
|
|
20
|
+
parser.add_argument("--target", required=True, help="Resolved target for this round")
|
|
21
|
+
parser.add_argument("--override-reason", default="", help="Optional override reason")
|
|
22
|
+
parser.add_argument("--lang", choices=("en", "zh"), default="en", help="Render language for the section")
|
|
23
|
+
return parser.parse_args()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_manifest(project_root: Path, stage: str) -> dict:
|
|
27
|
+
manifest_path = project_root / ".lab" / ".managed" / "rule-manifest.json"
|
|
28
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
29
|
+
stage_entry = (manifest.get("stages") or {}).get(stage)
|
|
30
|
+
if not stage_entry:
|
|
31
|
+
raise SystemExit(f"stage `{stage}` is missing from {manifest_path}")
|
|
32
|
+
return {
|
|
33
|
+
"rule_source_file": stage_entry.get("rule_source_file", ""),
|
|
34
|
+
"rule_source_revision": stage_entry.get("rule_source_revision", ""),
|
|
35
|
+
"project_version": manifest.get("package_version", ""),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def render_preflight_block(values: dict[str, str], lang: str) -> str:
|
|
40
|
+
values = dict(values)
|
|
41
|
+
values["preflight_stamp"] = compute_preflight_stamp(values)
|
|
42
|
+
if lang == "zh":
|
|
43
|
+
lines = [
|
|
44
|
+
"## 规则预检",
|
|
45
|
+
"",
|
|
46
|
+
f"- 规则来源文件: {values['rule_source_file']}",
|
|
47
|
+
f"- 规则来源修订: {values['rule_source_revision']}",
|
|
48
|
+
f"- 项目版本: {values['project_version']}",
|
|
49
|
+
f"- 解析后的阶段: {values['resolved_stage']}",
|
|
50
|
+
f"- 解析后的模式: {values['resolved_mode']}",
|
|
51
|
+
f"- 解析后的目标: {values['resolved_target']}",
|
|
52
|
+
f"- 预检签名: {values['preflight_stamp']}",
|
|
53
|
+
f"- 如有覆盖理由: {values['override_reason']}",
|
|
54
|
+
"",
|
|
55
|
+
]
|
|
56
|
+
else:
|
|
57
|
+
lines = [
|
|
58
|
+
"## Rule Preflight",
|
|
59
|
+
"",
|
|
60
|
+
f"- Rule source file: {values['rule_source_file']}",
|
|
61
|
+
f"- Rule source revision: {values['rule_source_revision']}",
|
|
62
|
+
f"- Project version: {values['project_version']}",
|
|
63
|
+
f"- Resolved stage: {values['resolved_stage']}",
|
|
64
|
+
f"- Resolved mode: {values['resolved_mode']}",
|
|
65
|
+
f"- Resolved target: {values['resolved_target']}",
|
|
66
|
+
f"- Preflight stamp: {values['preflight_stamp']}",
|
|
67
|
+
f"- Override reason, if any: {values['override_reason']}",
|
|
68
|
+
"",
|
|
69
|
+
]
|
|
70
|
+
return "\n".join(lines)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def replace_or_insert_rule_preflight(text: str, block: str) -> str:
|
|
74
|
+
pattern = re.compile(r"^##\s+(?:Rule Preflight|规则预检)\s*$", flags=re.MULTILINE)
|
|
75
|
+
match = pattern.search(text)
|
|
76
|
+
if match:
|
|
77
|
+
start = match.start()
|
|
78
|
+
next_heading = re.search(r"^##\s+", text[match.end():], flags=re.MULTILINE)
|
|
79
|
+
end = match.end() + next_heading.start() if next_heading else len(text)
|
|
80
|
+
prefix = text[:start].rstrip() + "\n\n"
|
|
81
|
+
suffix = text[end:].lstrip("\n")
|
|
82
|
+
return prefix + block.rstrip() + ("\n\n" + suffix if suffix else "\n")
|
|
83
|
+
|
|
84
|
+
title_match = re.search(r"^# .*$", text, flags=re.MULTILINE)
|
|
85
|
+
if not title_match:
|
|
86
|
+
return block.rstrip() + "\n\n" + text.lstrip()
|
|
87
|
+
insert_at = title_match.end()
|
|
88
|
+
prefix = text[:insert_at].rstrip() + "\n\n"
|
|
89
|
+
suffix = text[insert_at:].lstrip("\n")
|
|
90
|
+
return prefix + block.rstrip() + ("\n\n" + suffix if suffix else "\n")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main():
|
|
94
|
+
args = parse_args()
|
|
95
|
+
artifact_path = Path(args.artifact)
|
|
96
|
+
project_root = Path(args.project_root)
|
|
97
|
+
values = read_manifest(project_root, args.stage)
|
|
98
|
+
values.update(
|
|
99
|
+
{
|
|
100
|
+
"resolved_stage": args.stage,
|
|
101
|
+
"resolved_mode": args.mode,
|
|
102
|
+
"resolved_target": args.target,
|
|
103
|
+
"override_reason": args.override_reason,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
block = render_preflight_block(values, args.lang)
|
|
107
|
+
original = artifact_path.read_text(encoding="utf-8") if artifact_path.exists() else ""
|
|
108
|
+
updated = replace_or_insert_rule_preflight(original, block)
|
|
109
|
+
artifact_path.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
artifact_path.write_text(updated, encoding="utf-8")
|
|
111
|
+
print("rule preflight written")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
main()
|
|
@@ -5,6 +5,7 @@ import re
|
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from validate_rule_preflight import validate_rule_preflight
|
|
8
|
+
from validate_paper_topology import validate_topology_artifacts
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
ABSOLUTE_PATH_MARKERS = ("/Users/", "/home/", "/tmp/", "/private/tmp/")
|
|
@@ -609,6 +610,7 @@ def check_latest_write_iteration_preflight(paper_dir: Path, issues: list[str]):
|
|
|
609
610
|
issues.append("missing latest write iteration needed for rule preflight validation")
|
|
610
611
|
return
|
|
611
612
|
issues.extend(validate_rule_preflight(latest_iteration, "write", project_root=project_root))
|
|
613
|
+
issues.extend(validate_topology_artifacts(project_root))
|
|
612
614
|
|
|
613
615
|
|
|
614
616
|
def main():
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from paper_topology import (
|
|
9
|
+
classify_paper_path,
|
|
10
|
+
find_project_root,
|
|
11
|
+
iter_path_tokens,
|
|
12
|
+
line_is_deprecated,
|
|
13
|
+
resolve_paper_topology,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_args():
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
description="Validate that plan/context artifacts point at the active paper topology instead of legacy side layers."
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument("--project-root", required=True, help="Project root containing .lab/config/workflow.json")
|
|
22
|
+
return parser.parse_args()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_text(path: Path) -> str:
|
|
26
|
+
return path.read_text(encoding="utf-8")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_file(file_path: Path, topology: dict, issues: list[str]):
|
|
30
|
+
if not file_path.exists():
|
|
31
|
+
return
|
|
32
|
+
text = read_text(file_path)
|
|
33
|
+
current_heading_deprecated = False
|
|
34
|
+
for line_number, line in enumerate(text.splitlines(), start=1):
|
|
35
|
+
stripped = line.strip()
|
|
36
|
+
if stripped.startswith("#"):
|
|
37
|
+
current_heading_deprecated = line_is_deprecated(stripped)
|
|
38
|
+
for _, _, token in iter_path_tokens(line):
|
|
39
|
+
role, normalized_path = classify_paper_path(token, topology)
|
|
40
|
+
if role in {"active-canonical", "active-workflow-language"}:
|
|
41
|
+
continue
|
|
42
|
+
if current_heading_deprecated or line_is_deprecated(line):
|
|
43
|
+
continue
|
|
44
|
+
if role == "legacy-paper-layer":
|
|
45
|
+
issues.append(
|
|
46
|
+
f"{file_path}:line {line_number} treats legacy-paper-layer path as active topology: {token} -> {normalized_path}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def validate_topology_artifacts(project_root: Path) -> list[str]:
|
|
51
|
+
topology = resolve_paper_topology(project_root)
|
|
52
|
+
files_to_check = [
|
|
53
|
+
project_root / ".lab" / "writing" / "plan.md",
|
|
54
|
+
topology["deliverables_root"] / "artifact-status.md",
|
|
55
|
+
project_root / ".lab" / "context" / "evidence-index.md",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
issues: list[str] = []
|
|
59
|
+
for file_path in files_to_check:
|
|
60
|
+
validate_file(file_path, topology, issues)
|
|
61
|
+
return issues
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
args = parse_args()
|
|
66
|
+
project_root = find_project_root(Path(args.project_root).resolve())
|
|
67
|
+
if project_root is None:
|
|
68
|
+
print("could not find .lab/config/workflow.json for project root", file=sys.stderr)
|
|
69
|
+
return 1
|
|
70
|
+
|
|
71
|
+
issues = validate_topology_artifacts(project_root)
|
|
72
|
+
|
|
73
|
+
if issues:
|
|
74
|
+
for issue in issues:
|
|
75
|
+
print(issue, file=sys.stderr)
|
|
76
|
+
return 1
|
|
77
|
+
|
|
78
|
+
print("paper topology is consistent")
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
SECTION_PATTERNS = (r"^##\s+Rule Preflight\s*$", r"^##\s+规则预检\s*$")
|
|
11
|
+
FIELD_LABELS = {
|
|
12
|
+
"rule_source_file": ("Rule source file", "规则来源文件"),
|
|
13
|
+
"rule_source_revision": ("Rule source revision", "规则来源修订"),
|
|
14
|
+
"project_version": ("Project version", "项目版本"),
|
|
15
|
+
"resolved_stage": ("Resolved stage", "解析后的阶段"),
|
|
16
|
+
"resolved_mode": ("Resolved mode", "解析后的模式"),
|
|
17
|
+
"resolved_target": ("Resolved target", "解析后的目标"),
|
|
18
|
+
"preflight_stamp": ("Preflight stamp", "预检签名"),
|
|
19
|
+
"override_reason": ("Override reason, if any", "Override 理由(如有)"),
|
|
20
|
+
}
|
|
21
|
+
BASE_REQUIRED_FIELD_KEYS = (
|
|
22
|
+
"rule_source_file",
|
|
23
|
+
"rule_source_revision",
|
|
24
|
+
"project_version",
|
|
25
|
+
"resolved_stage",
|
|
26
|
+
"resolved_mode",
|
|
27
|
+
"resolved_target",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_args():
|
|
32
|
+
parser = argparse.ArgumentParser(
|
|
33
|
+
description="Validate that a managed stage artifact records a consistent Rule Preflight block."
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument("--artifact", required=True, help="Path to the managed stage artifact")
|
|
36
|
+
parser.add_argument("--stage", required=True, help="Stage name, e.g. write or auto")
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--project-root",
|
|
39
|
+
required=False,
|
|
40
|
+
help="Optional project root; if omitted, infer it by walking upward for .lab/config/workflow.json",
|
|
41
|
+
)
|
|
42
|
+
return parser.parse_args()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read_text(path: Path) -> str:
|
|
46
|
+
return path.read_text(encoding="utf-8")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def find_project_root(start_path: Path) -> Path | None:
|
|
50
|
+
for parent in (start_path.parent, *start_path.parents):
|
|
51
|
+
workflow_config = parent / ".lab" / "config" / "workflow.json"
|
|
52
|
+
if workflow_config.exists():
|
|
53
|
+
return parent
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def extract_section_body(text: str, patterns: tuple[str, ...]) -> str:
|
|
58
|
+
for pattern in patterns:
|
|
59
|
+
match = re.search(pattern, text, flags=re.MULTILINE)
|
|
60
|
+
if not match:
|
|
61
|
+
continue
|
|
62
|
+
start = match.end()
|
|
63
|
+
next_heading = re.search(r"^##\s+", text[start:], flags=re.MULTILINE)
|
|
64
|
+
end = start + next_heading.start() if next_heading else len(text)
|
|
65
|
+
return text[start:end].strip()
|
|
66
|
+
return ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def extract_field_value(body: str, labels: tuple[str, ...]) -> str | None:
|
|
70
|
+
for label in labels:
|
|
71
|
+
pattern = re.compile(
|
|
72
|
+
rf"^\s*(?:-|\d+\.)\s*{re.escape(label)}[::][ \t]*([^\n]+?)\s*$",
|
|
73
|
+
flags=re.MULTILINE,
|
|
74
|
+
)
|
|
75
|
+
for match in pattern.finditer(body):
|
|
76
|
+
return match.group(1).strip()
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def has_meaningful_value(value: str | None) -> bool:
|
|
81
|
+
if value is None:
|
|
82
|
+
return False
|
|
83
|
+
normalized = value.strip().lower()
|
|
84
|
+
return normalized not in {"", "-", "n/a", "na", "none", "null", "todo", "tbd"}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def compute_preflight_stamp(values: dict[str, str | None]) -> str:
|
|
88
|
+
payload = {
|
|
89
|
+
"ruleSourceFile": values.get("rule_source_file") or "",
|
|
90
|
+
"ruleSourceRevision": values.get("rule_source_revision") or "",
|
|
91
|
+
"projectVersion": values.get("project_version") or "",
|
|
92
|
+
"resolvedStage": values.get("resolved_stage") or "",
|
|
93
|
+
"resolvedMode": values.get("resolved_mode") or "",
|
|
94
|
+
"resolvedTarget": values.get("resolved_target") or "",
|
|
95
|
+
"overrideReason": values.get("override_reason") or "",
|
|
96
|
+
}
|
|
97
|
+
return hashlib.sha256(json.dumps(payload, ensure_ascii=False).encode("utf-8")).hexdigest()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def validate_rule_preflight(artifact_path: Path, stage: str, project_root: Path | None = None) -> list[str]:
|
|
101
|
+
issues: list[str] = []
|
|
102
|
+
if project_root is None:
|
|
103
|
+
project_root = find_project_root(artifact_path)
|
|
104
|
+
if project_root is None:
|
|
105
|
+
issues.append("could not infer project root for rule preflight validation")
|
|
106
|
+
return issues
|
|
107
|
+
|
|
108
|
+
manifest_path = project_root / ".lab" / ".managed" / "rule-manifest.json"
|
|
109
|
+
if not manifest_path.exists():
|
|
110
|
+
issues.append(f"missing managed rule manifest: {manifest_path.as_posix()}")
|
|
111
|
+
return issues
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
manifest = json.loads(read_text(manifest_path))
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
issues.append(f"managed rule manifest is invalid JSON: {manifest_path.as_posix()}")
|
|
117
|
+
return issues
|
|
118
|
+
|
|
119
|
+
stage_entry = (manifest.get("stages") or {}).get(stage)
|
|
120
|
+
if not stage_entry:
|
|
121
|
+
issues.append(f"managed rule manifest does not define stage `{stage}`")
|
|
122
|
+
return issues
|
|
123
|
+
|
|
124
|
+
if not artifact_path.exists():
|
|
125
|
+
issues.append(f"artifact does not exist: {artifact_path.as_posix()}")
|
|
126
|
+
return issues
|
|
127
|
+
|
|
128
|
+
body = extract_section_body(read_text(artifact_path), SECTION_PATTERNS)
|
|
129
|
+
if not body:
|
|
130
|
+
issues.append("missing Rule Preflight section")
|
|
131
|
+
return issues
|
|
132
|
+
|
|
133
|
+
values = {
|
|
134
|
+
key: extract_field_value(body, labels)
|
|
135
|
+
for key, labels in FIELD_LABELS.items()
|
|
136
|
+
}
|
|
137
|
+
required_field_keys = list(BASE_REQUIRED_FIELD_KEYS)
|
|
138
|
+
if int(manifest.get("layout_version") or 0) >= 2:
|
|
139
|
+
required_field_keys.append("preflight_stamp")
|
|
140
|
+
missing = [key for key in required_field_keys if not has_meaningful_value(values[key])]
|
|
141
|
+
if missing:
|
|
142
|
+
pretty = ", ".join(FIELD_LABELS[key][0] for key in missing)
|
|
143
|
+
issues.append(f"missing required rule preflight fields: {pretty}")
|
|
144
|
+
return issues
|
|
145
|
+
|
|
146
|
+
if values["rule_source_file"] != stage_entry.get("rule_source_file"):
|
|
147
|
+
issues.append(
|
|
148
|
+
f"rule preflight source file mismatch: expected {stage_entry.get('rule_source_file')}, got {values['rule_source_file']}"
|
|
149
|
+
)
|
|
150
|
+
if values["rule_source_revision"] != stage_entry.get("rule_source_revision"):
|
|
151
|
+
issues.append(
|
|
152
|
+
"rule preflight source revision mismatch"
|
|
153
|
+
)
|
|
154
|
+
if values["project_version"] != manifest.get("package_version"):
|
|
155
|
+
issues.append(
|
|
156
|
+
f"rule preflight project version mismatch: expected {manifest.get('package_version')}, got {values['project_version']}"
|
|
157
|
+
)
|
|
158
|
+
if values["resolved_stage"].strip().lower() != stage.strip().lower():
|
|
159
|
+
issues.append(
|
|
160
|
+
f"rule preflight stage mismatch: expected {stage}, got {values['resolved_stage']}"
|
|
161
|
+
)
|
|
162
|
+
if int(manifest.get("layout_version") or 0) >= 2:
|
|
163
|
+
expected_stamp = compute_preflight_stamp(values)
|
|
164
|
+
if values["preflight_stamp"] != expected_stamp:
|
|
165
|
+
issues.append("rule preflight stamp mismatch")
|
|
166
|
+
return issues
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main():
|
|
170
|
+
args = parse_args()
|
|
171
|
+
artifact_path = Path(args.artifact)
|
|
172
|
+
project_root = Path(args.project_root) if args.project_root else None
|
|
173
|
+
issues = validate_rule_preflight(artifact_path, args.stage, project_root=project_root)
|
|
174
|
+
if not issues:
|
|
175
|
+
print("rule preflight is valid")
|
|
176
|
+
return 0
|
|
177
|
+
for issue in issues:
|
|
178
|
+
print(issue, file=sys.stderr)
|
|
179
|
+
return 1
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
raise SystemExit(main())
|
|
@@ -10,6 +10,7 @@ from paper_topology import (
|
|
|
10
10
|
load_workflow_config,
|
|
11
11
|
resolve_paper_topology,
|
|
12
12
|
)
|
|
13
|
+
from validate_paper_topology import validate_topology_artifacts
|
|
13
14
|
from validate_rule_preflight import validate_rule_preflight
|
|
14
15
|
|
|
15
16
|
|
|
@@ -424,6 +425,13 @@ def check_write_rule_preflight(section_path: Path, issues: list[str]):
|
|
|
424
425
|
issues.extend(validate_rule_preflight(latest_iteration, "write", project_root=project_root))
|
|
425
426
|
|
|
426
427
|
|
|
428
|
+
def check_active_paper_topology(section_path: Path, issues: list[str]):
|
|
429
|
+
project_root = find_project_root(section_path)
|
|
430
|
+
if project_root is None:
|
|
431
|
+
return
|
|
432
|
+
issues.extend(validate_topology_artifacts(project_root))
|
|
433
|
+
|
|
434
|
+
|
|
427
435
|
def check_abstract(text: str, issues: list[str]):
|
|
428
436
|
numbers = re.findall(r"\b\d+(?:\.\d+)?\b", text)
|
|
429
437
|
if len(numbers) > 6:
|
|
@@ -550,25 +558,33 @@ def main():
|
|
|
550
558
|
return 1
|
|
551
559
|
|
|
552
560
|
text = read_text(section_path)
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
check_write_rule_preflight(section_path,
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
561
|
+
blocking_issues: list[str] = []
|
|
562
|
+
warning_issues: list[str] = []
|
|
563
|
+
check_write_rule_preflight(section_path, blocking_issues)
|
|
564
|
+
check_active_paper_topology(section_path, blocking_issues)
|
|
565
|
+
check_paper_topology_targeting(section_path, blocking_issues)
|
|
566
|
+
check_workflow_language_targeting(section_path, blocking_issues)
|
|
567
|
+
check_common_section_gate_risks(text, warning_issues)
|
|
568
|
+
check_section_style_policy(text, args.section, warning_issues)
|
|
569
|
+
SECTION_CHECKS[args.section](text, warning_issues)
|
|
570
|
+
check_neighbor_asset_files(args.section, section_path, warning_issues)
|
|
571
|
+
|
|
572
|
+
if not blocking_issues and not warning_issues:
|
|
563
573
|
print("section draft is valid")
|
|
564
574
|
return 0
|
|
565
575
|
|
|
566
576
|
if args.mode == "draft":
|
|
567
|
-
|
|
577
|
+
if blocking_issues:
|
|
578
|
+
for issue in blocking_issues:
|
|
579
|
+
print(issue, file=sys.stderr)
|
|
580
|
+
for issue in warning_issues:
|
|
581
|
+
print(f"WARNING: {issue}")
|
|
582
|
+
return 1
|
|
583
|
+
for issue in warning_issues:
|
|
568
584
|
print(f"WARNING: {issue}")
|
|
569
585
|
return 0
|
|
570
586
|
|
|
571
|
-
for issue in
|
|
587
|
+
for issue in [*blocking_issues, *warning_issues]:
|
|
572
588
|
print(issue, file=sys.stderr)
|
|
573
589
|
return 1
|
|
574
590
|
|
|
@@ -37,7 +37,9 @@ Use this skill when the user invokes `/lab:*` or asks for the structured researc
|
|
|
37
37
|
- `Resolved stage`
|
|
38
38
|
- `Resolved mode`
|
|
39
39
|
- `Resolved target`
|
|
40
|
+
- `Preflight stamp`
|
|
40
41
|
- `Override reason, if any`
|
|
42
|
+
- Generate the `Rule Preflight` block from `.lab/.managed/rule-manifest.json` with the managed preflight renderer instead of handwriting it from memory.
|
|
41
43
|
- Treat missing, stale, or contradictory `Rule Preflight` data as a stage-contract failure.
|
|
42
44
|
- Project-installed rules take priority over model memory. If remembered patterns conflict with the installed rule source, follow the installed source recorded in `.lab/.managed/rule-manifest.json`.
|
|
43
45
|
- Final paper output should default to LaTeX, and its manuscript language should be decided separately from the workflow language.
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
## Rule Preflight
|
|
30
30
|
|
|
31
31
|
- Read `.lab/.managed/rule-manifest.json` before arming auto mode.
|
|
32
|
-
- The visible `Auto preflight` summary must also record the installed rule source file, rule source revision, project version, resolved stage, resolved mode, resolved target, and any override reason.
|
|
32
|
+
- The visible `Auto preflight` summary must also record the installed rule source file, rule source revision, project version, resolved stage, resolved mode, resolved target, a machine-generated preflight stamp, and any override reason.
|
|
33
33
|
- Keep the same `Rule Preflight` fields in `.lab/context/auto-status.md` while the campaign is live.
|
|
34
34
|
- If the installed auto rule and the current campaign behavior disagree, stop and fix the contract or record a valid override reason before launching the loop.
|
|
35
35
|
|
|
@@ -33,9 +33,10 @@
|
|
|
33
33
|
## Rule Preflight
|
|
34
34
|
|
|
35
35
|
- Read `.lab/.managed/rule-manifest.json` before drafting.
|
|
36
|
-
-
|
|
37
|
-
- The `Rule Preflight` block must record the installed rule source file, rule source revision, project version, resolved stage, resolved mode, resolved target, and any override reason.
|
|
36
|
+
- Write the `Rule Preflight` block with `.lab/.managed/scripts/render_rule_preflight.py` before revising prose; do not hand-fill it from memory.
|
|
37
|
+
- The `Rule Preflight` block must record the installed rule source file, rule source revision, project version, resolved stage, resolved mode, resolved target, a machine-generated preflight stamp, and any override reason.
|
|
38
38
|
- If the installed write rule and the current round behavior disagree, fix the targeting or record a valid override reason before further editing.
|
|
39
|
+
- In draft mode, rule-preflight mismatches and paper-topology mismatches are blockers, not polish warnings.
|
|
39
40
|
|
|
40
41
|
## Context Write Set
|
|
41
42
|
|
|
@@ -156,7 +157,7 @@ Run these on every round:
|
|
|
156
157
|
- record what each figure or analysis asset should show and why the reader needs it
|
|
157
158
|
- record which citation anchors must appear in the section and why each anchor matters
|
|
158
159
|
- Before drafting `introduction`, `method`, `experiments`, `related work`, or `conclusion`, run `.lab/.managed/scripts/validate_paper_plan.py --paper-plan .lab/writing/plan.md`.
|
|
159
|
-
- Before drafting `introduction`, `method`, `experiments`, `related work`, or `conclusion`, also run `.lab/.managed/scripts/validate_paper_topology.py --project-root .` so plan/context files cannot keep presenting legacy layers as the active paper topology.
|
|
160
|
+
- Before drafting `introduction`, `method`, `experiments`, `related work`, or `conclusion`, also run `.lab/.managed/scripts/validate_paper_topology.py --project-root .` so plan/context files cannot keep presenting legacy layers as the active paper topology. If that validator fails, do not mark the topology as repaired and do not continue section polish.
|
|
160
161
|
- When the repository workflow config is available, the paper-plan validator also checks that `.lab/writing/plan.md` stays in `workflow_language` instead of silently drifting into another language.
|
|
161
162
|
- If the paper-plan validator fails, stop and fill `.lab/writing/plan.md` first instead of drafting prose.
|
|
162
163
|
- During ordinary draft rounds, run `.lab/.managed/scripts/validate_section_draft.py --section <section> --section-file <section-file> --mode draft` and `.lab/.managed/scripts/validate_paper_claims.py --section-file <section-file> --mode draft` after revising the active section.
|