2"""Reject unmanaged run-path literals so a layout change cannot leave prose stale."""
4from __future__
import annotations
10from pathlib
import Path
12sys.path.insert(0, str(Path(__file__).resolve().parent))
13from repo_files
import enumerate_repository_files
16REPO_ROOT = Path(__file__).resolve().parents[2]
17TOPOLOGY = REPO_ROOT /
"tests" /
"tooling" /
"artifact_topology.json"
18SKIP_DIRS = {
".git",
"docs_build",
"obj",
"bin",
"stubs",
"runs",
"studies",
"__pycache__",
".pytest_cache"}
21HISTORICAL = {
"docs/CHANGELOG.md"}
31RUN_OWNED =
"config|logs|output|scheduler|visualization|checkpoints"
35 r"(?<![\w./<])runs/(?:<[^>]+>|\$\{[^}]+\}|[A-Za-z0-9_.*-]+)/(" + RUN_OWNED +
r")\b")
40PLACEHOLDER_ROOT = re.compile(
41 r"<(?:run_dir|run|RUN_DIR)>/(" + RUN_OWNED +
r")\b")
51BARE_PREFIX = re.compile(
52 r"(?<![\w./<>-])(" + RUN_OWNED +
r")/(?![A-Za-z0-9_./*-]*[<>])[A-Za-z0-9_./*-]*")
58REPO_OWNED = re.compile(
r"<repo>/[A-Za-z0-9_][A-Za-z0-9_./*<>-]*")
64WORKSPACE_OWNED = re.compile(
r"<workspace>/[A-Za-z0-9_][A-Za-z0-9_./*<>-]*")
70REPO_CONFIG_SUBDIRS = (
"guide.md",
"build",
"grids",
"initial_conditions",
"monitors",
71 "postprocessors",
"profiles",
"runtime",
"schedulers",
"solvers",
73NOT_RUN_OWNED_PREFIXES = re.compile(
74 r"(?<![\w./<>-])config/(?:" +
"|".join(
75 name.replace(
".",
r"\.")
for name
in REPO_CONFIG_SUBDIRS) +
r")\b")
87 @brief Markdown files the current commit carries.
88 @return Sorted Markdown paths.
90 found = enumerate_repository_files(REPO_ROOT,
".md", frozenset(SKIP_DIRS))
92 return sorted(path
for path
in REPO_ROOT.rglob(
"*.md")
if path.is_file())
98 @brief Whether a line sits inside a fenced code block.
99 @param[in] lines All lines of the file.
100 @param[in] index Zero-based line index.
101 @return True when the line is inside a fence.
103 fences = sum(1
for line
in lines[:index]
if line.lstrip().startswith(
"```"))
104 return fences % 2 == 1
109 @brief Fail when narrative prose hardcodes a run-relative path.
110 @return Process status code.
112 contract = json.loads(TOPOLOGY.read_text(encoding=
"utf-8"))
113 identities = [a[
"id"]
for a
in contract[
"artifacts"]]
114 violations: list = []
117 if str(path.relative_to(REPO_ROOT))
in HISTORICAL:
119 text = path.read_text(encoding=
"utf-8", errors=
"replace")
120 lines = text.splitlines()
122 for number, line
in enumerate(lines):
123 if in_code_block(lines, number)
or any(token
in line
for token
in ALLOWED_CONTEXTS):
125 probe = NOT_RUN_OWNED_PREFIXES.sub(
"", line)
130 probe = REPO_OWNED.sub(
"", probe)
131 probe = WORKSPACE_OWNED.sub(
"", probe)
132 if MANAGED.search(line):
133 found, advice =
"unmanaged run-path literal", (
134 "Use logical notation such as `<run.config>`, or move the concrete "
135 "path into a runnable command block.")
136 elif PLACEHOLDER_ROOT.search(line):
137 found, advice =
"run-owned directory named under an unresolved run root", (
138 "`<run_dir>/logs/...` names a subdirectory without naming the "
139 "contract that fixes it. Use the logical identity - "
140 "`<run.runtime_logs>/...` - which artifact_topology.json maps.")
141 elif BARE_PREFIX.search(probe):
142 match = BARE_PREFIX.search(probe)
143 found, advice = f
"bare run-owned prefix `{match.group(0)}`", (
144 "A bare `logs/...` or `config/...` does not say which owner is "
145 "meant. Use the run's logical identity - `<run.runtime_logs>/...`, "
146 "`<run.solver_output>/...` - or name the other owner explicitly "
147 "with `<repo>/logs/...` or `<workspace>/config/...`.")
151 f
"{path.relative_to(REPO_ROOT)}:{number + 1}: {found}\n"
152 f
" {line.strip()[:96]}\n"
156 print(
"Unmanaged run-path literals in narrative prose:", file=sys.stderr)
157 for violation
in violations:
158 print(f
" {violation}", file=sys.stderr)
160 f
"\nLogical identities are declared in tests/tooling/artifact_topology.json "
161 f
"({len(identities)} identities). Narrative pages should refer to those, so a layout "
162 f
"change is a contract change rather than a prose hunt.",
166 print(f
"Path-literal audit passed: {scanned} Markdown files, no unmanaged run-path literals in prose.")
170if __name__ ==
"__main__":
171 raise SystemExit(
main())
int main()
Fail when narrative prose hardcodes a run-relative path.
list tracked_markdown()
Markdown files the current commit carries.
bool in_code_block(list lines, int index)
Whether a line sits inside a fenced code block.