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.
@@ -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
- - write iteration artifact 里先写 \`Rule Preflight\`,再改 prose
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 = 1;
10
+ const LAYOUT_VERSION = 2;
11
11
  const SUPERLAB_START = "<!-- superlab:start -->";
12
12
  const SUPERLAB_END = "<!-- superlab:end -->";
13
13
 
@@ -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
- return {
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
- issues: list[str] = []
554
- check_common_section_gate_risks(text, issues)
555
- check_write_rule_preflight(section_path, issues)
556
- check_paper_topology_targeting(section_path, issues)
557
- check_workflow_language_targeting(section_path, issues)
558
- check_section_style_policy(text, args.section, issues)
559
- SECTION_CHECKS[args.section](text, issues)
560
- check_neighbor_asset_files(args.section, section_path, issues)
561
-
562
- if not issues:
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
- for issue in issues:
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 issues:
587
+ for issue in [*blocking_issues, *warning_issues]:
572
588
  print(issue, file=sys.stderr)
573
589
  return 1
574
590
 
@@ -8,6 +8,7 @@
8
8
  - Resolved stage:
9
9
  - Resolved mode:
10
10
  - Resolved target:
11
+ - Preflight stamp:
11
12
  - Override reason, if any:
12
13
 
13
14
  ## Round
@@ -8,6 +8,7 @@
8
8
  - Resolved stage:
9
9
  - Resolved mode:
10
10
  - Resolved target:
11
+ - Preflight stamp:
11
12
  - Override reason, if any:
12
13
 
13
14
  ## Round
@@ -8,6 +8,7 @@
8
8
  - Resolved stage:
9
9
  - Resolved mode:
10
10
  - Resolved target:
11
+ - Preflight stamp:
11
12
  - Override reason, if any:
12
13
 
13
14
  ## Runtime State
@@ -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
- - Record a `Rule Preflight` block in the write-iteration artifact before revising prose.
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlab",
3
- "version": "0.1.63",
3
+ "version": "0.1.64",
4
4
  "description": "Strict /lab research workflow installer for Codex and Claude",
5
5
  "keywords": [
6
6
  "codex",