sourcebound 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clean_docs/__init__.py +26 -0
- clean_docs/__main__.py +3 -0
- clean_docs/accessibility.py +182 -0
- clean_docs/adapters/__init__.py +1 -0
- clean_docs/adapters/event_capture.py +56 -0
- clean_docs/adapters/mdx_dependencies.json +714 -0
- clean_docs/adapters/mdx_parser.mjs +29992 -0
- clean_docs/applicability.py +330 -0
- clean_docs/audit.py +1120 -0
- clean_docs/bootstrap.py +507 -0
- clean_docs/capabilities.py +200 -0
- clean_docs/changed.py +452 -0
- clean_docs/claims.py +840 -0
- clean_docs/cli.py +1612 -0
- clean_docs/context.py +307 -0
- clean_docs/corpus.py +377 -0
- clean_docs/demo.py +369 -0
- clean_docs/doctor.py +184 -0
- clean_docs/emit/__init__.py +4 -0
- clean_docs/emit/llms_txt.py +102 -0
- clean_docs/emit/stepwise.py +168 -0
- clean_docs/engine.py +324 -0
- clean_docs/errors.py +20 -0
- clean_docs/evaluation.py +867 -0
- clean_docs/execution.py +138 -0
- clean_docs/explain.py +123 -0
- clean_docs/extractors/__init__.py +19 -0
- clean_docs/extractors/command.py +51 -0
- clean_docs/extractors/inventory.py +176 -0
- clean_docs/extractors/json_pointer.py +84 -0
- clean_docs/extractors/python_literal.py +104 -0
- clean_docs/extractors/static.py +111 -0
- clean_docs/feedback.py +1390 -0
- clean_docs/impact.py +1624 -0
- clean_docs/improvements.py +1178 -0
- clean_docs/inventory.py +474 -0
- clean_docs/isolation.py +157 -0
- clean_docs/manifest.py +898 -0
- clean_docs/mdx.py +272 -0
- clean_docs/migration.py +121 -0
- clean_docs/models.py +194 -0
- clean_docs/outcomes.py +296 -0
- clean_docs/performance.py +123 -0
- clean_docs/phrasing.py +448 -0
- clean_docs/plugins.py +249 -0
- clean_docs/policy.py +536 -0
- clean_docs/projections.py +255 -0
- clean_docs/regions.py +75 -0
- clean_docs/release.py +232 -0
- clean_docs/renderers.py +57 -0
- clean_docs/residue.py +311 -0
- clean_docs/review_contracts.py +862 -0
- clean_docs/review_ledger.py +297 -0
- clean_docs/review_limits.py +9 -0
- clean_docs/sensitivity.py +602 -0
- clean_docs/snapshot.py +212 -0
- clean_docs/standard.py +281 -0
- clean_docs/standards/default.json +309 -0
- clean_docs/standards/exemplars.md +87 -0
- clean_docs/standards/v0-migrations.json +26 -0
- clean_docs/symbols.py +47 -0
- clean_docs/templates.py +96 -0
- clean_docs/verdict.py +1397 -0
- clean_docs/visuals.py +346 -0
- clean_docs/write_gate.py +170 -0
- sourcebound-1.2.1.dist-info/LICENSE +21 -0
- sourcebound-1.2.1.dist-info/METADATA +109 -0
- sourcebound-1.2.1.dist-info/RECORD +71 -0
- sourcebound-1.2.1.dist-info/WHEEL +5 -0
- sourcebound-1.2.1.dist-info/entry_points.txt +2 -0
- sourcebound-1.2.1.dist-info/top_level.txt +1 -0
clean_docs/snapshot.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ntpath
|
|
4
|
+
import posixpath
|
|
5
|
+
import subprocess
|
|
6
|
+
import tarfile
|
|
7
|
+
import tempfile
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
from fnmatch import fnmatch
|
|
12
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
13
|
+
|
|
14
|
+
from clean_docs.errors import ExtractionError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _archive_path_is_safe(value: str) -> bool:
|
|
18
|
+
posix_path = PurePosixPath(value)
|
|
19
|
+
windows_path = PureWindowsPath(value)
|
|
20
|
+
return not (
|
|
21
|
+
posix_path.is_absolute()
|
|
22
|
+
or windows_path.drive
|
|
23
|
+
or windows_path.root
|
|
24
|
+
or ".." in posix_path.parts
|
|
25
|
+
or ".." in windows_path.parts
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _symlink_target_is_safe(member_name: str, link_name: str) -> bool:
|
|
30
|
+
posix_link = PurePosixPath(link_name)
|
|
31
|
+
windows_link = PureWindowsPath(link_name)
|
|
32
|
+
if (
|
|
33
|
+
posix_link.is_absolute()
|
|
34
|
+
or windows_link.drive
|
|
35
|
+
or windows_link.root
|
|
36
|
+
or ("\\" in link_name and ".." in windows_link.parts)
|
|
37
|
+
):
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
posix_target = posixpath.normpath(
|
|
41
|
+
(PurePosixPath(member_name).parent / posix_link).as_posix()
|
|
42
|
+
)
|
|
43
|
+
windows_target = ntpath.normpath(
|
|
44
|
+
str(PureWindowsPath(member_name).parent / windows_link)
|
|
45
|
+
)
|
|
46
|
+
return _archive_path_is_safe(posix_target) and _archive_path_is_safe(
|
|
47
|
+
windows_target
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _member_is_safe(member: tarfile.TarInfo) -> bool:
|
|
52
|
+
if not _archive_path_is_safe(member.name) or member.islnk():
|
|
53
|
+
return False
|
|
54
|
+
if not member.issym():
|
|
55
|
+
return True
|
|
56
|
+
return _symlink_target_is_safe(member.name, member.linkname)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class RepositorySnapshot:
|
|
61
|
+
root: Path
|
|
62
|
+
ref: str | None = None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def label(self) -> str:
|
|
66
|
+
if self.ref is None:
|
|
67
|
+
return "WORKTREE"
|
|
68
|
+
proc = self._git("rev-parse", "--verify", f"{self.ref}^{{commit}}")
|
|
69
|
+
return proc.stdout.strip()
|
|
70
|
+
|
|
71
|
+
def read_text(self, path: Path) -> str:
|
|
72
|
+
if path.is_absolute() or ".." in path.parts:
|
|
73
|
+
raise ExtractionError(f"source path escapes repository: {path}")
|
|
74
|
+
if self.ref is None:
|
|
75
|
+
target = self.root / path
|
|
76
|
+
try:
|
|
77
|
+
return target.read_text(encoding="utf-8")
|
|
78
|
+
except OSError as exc:
|
|
79
|
+
raise ExtractionError(f"cannot read source {path}: {exc}") from exc
|
|
80
|
+
proc = self._git("show", f"{self.ref}:{path.as_posix()}")
|
|
81
|
+
return proc.stdout
|
|
82
|
+
|
|
83
|
+
def matching_files(self, pattern: str) -> list[Path]:
|
|
84
|
+
candidate = Path(pattern)
|
|
85
|
+
if candidate.is_absolute() or ".." in candidate.parts:
|
|
86
|
+
raise ExtractionError(f"source glob escapes repository: {pattern}")
|
|
87
|
+
if self.ref is None:
|
|
88
|
+
return sorted(
|
|
89
|
+
path.relative_to(self.root)
|
|
90
|
+
for path in self.root.glob(pattern)
|
|
91
|
+
if path.is_file() and self.root in path.resolve().parents
|
|
92
|
+
)
|
|
93
|
+
proc = self._git("ls-tree", "-r", "--name-only", self.ref)
|
|
94
|
+
return [Path(path) for path in proc.stdout.splitlines() if fnmatch(path, pattern)]
|
|
95
|
+
|
|
96
|
+
@contextmanager
|
|
97
|
+
def materialized_root(
|
|
98
|
+
self,
|
|
99
|
+
*,
|
|
100
|
+
paths: tuple[Path, ...] = (),
|
|
101
|
+
) -> Iterator[Path]:
|
|
102
|
+
if self.ref is None:
|
|
103
|
+
yield self.root
|
|
104
|
+
return
|
|
105
|
+
archive_paths: list[str] = []
|
|
106
|
+
for path in paths:
|
|
107
|
+
if path == Path("."):
|
|
108
|
+
continue
|
|
109
|
+
if path.is_absolute() or ".." in path.parts:
|
|
110
|
+
raise ExtractionError(
|
|
111
|
+
f"snapshot path escapes repository: {path}"
|
|
112
|
+
)
|
|
113
|
+
archive_paths.append(path.as_posix().strip("/"))
|
|
114
|
+
archive_paths = self._include_symlink_targets(archive_paths)
|
|
115
|
+
with tempfile.TemporaryDirectory(prefix="sourcebound-snapshot-") as temporary:
|
|
116
|
+
destination = Path(temporary)
|
|
117
|
+
archive = destination / "snapshot.tar"
|
|
118
|
+
arguments = [
|
|
119
|
+
"archive",
|
|
120
|
+
"--format=tar",
|
|
121
|
+
f"--output={archive}",
|
|
122
|
+
self.label,
|
|
123
|
+
]
|
|
124
|
+
if archive_paths:
|
|
125
|
+
arguments.extend(("--", *archive_paths))
|
|
126
|
+
self._git(*arguments)
|
|
127
|
+
with tarfile.open(archive) as handle:
|
|
128
|
+
for member in handle.getmembers():
|
|
129
|
+
if not _member_is_safe(member):
|
|
130
|
+
raise ExtractionError(f"unsafe path in repository snapshot: {member.name}")
|
|
131
|
+
handle.extractall(destination)
|
|
132
|
+
archive.unlink()
|
|
133
|
+
yield destination
|
|
134
|
+
|
|
135
|
+
def _include_symlink_targets(self, paths: list[str]) -> list[str]:
|
|
136
|
+
if not paths:
|
|
137
|
+
return paths
|
|
138
|
+
selected = set(paths)
|
|
139
|
+
pending = list(paths)
|
|
140
|
+
while pending:
|
|
141
|
+
candidate = pending.pop()
|
|
142
|
+
entries = self._git(
|
|
143
|
+
"ls-tree",
|
|
144
|
+
"-r",
|
|
145
|
+
"-z",
|
|
146
|
+
self.label,
|
|
147
|
+
"--",
|
|
148
|
+
candidate,
|
|
149
|
+
).stdout
|
|
150
|
+
for entry in entries.split("\0"):
|
|
151
|
+
if not entry:
|
|
152
|
+
continue
|
|
153
|
+
header, separator, name = entry.partition("\t")
|
|
154
|
+
if not separator or not header.startswith("120000 "):
|
|
155
|
+
continue
|
|
156
|
+
link = self._git(
|
|
157
|
+
"show",
|
|
158
|
+
f"{self.label}:{name}",
|
|
159
|
+
).stdout.rstrip("\n")
|
|
160
|
+
link_path = PurePosixPath(link)
|
|
161
|
+
if link_path.is_absolute():
|
|
162
|
+
raise ExtractionError(
|
|
163
|
+
f"selected snapshot contains an absolute symlink: {name}"
|
|
164
|
+
)
|
|
165
|
+
target = PurePosixPath(
|
|
166
|
+
posixpath.normpath(
|
|
167
|
+
(PurePosixPath(name).parent / link_path).as_posix()
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
if target.is_absolute() or ".." in target.parts:
|
|
171
|
+
raise ExtractionError(
|
|
172
|
+
f"selected snapshot symlink escapes repository: {name}"
|
|
173
|
+
)
|
|
174
|
+
target_name = target.as_posix()
|
|
175
|
+
if target_name == ".":
|
|
176
|
+
raise ExtractionError(
|
|
177
|
+
"selected snapshot symlink expands scope to "
|
|
178
|
+
f"repository root: {name}"
|
|
179
|
+
)
|
|
180
|
+
if any(
|
|
181
|
+
target_name == selected_path
|
|
182
|
+
or target_name.startswith(selected_path + "/")
|
|
183
|
+
for selected_path in selected
|
|
184
|
+
):
|
|
185
|
+
continue
|
|
186
|
+
target_entry = self._git(
|
|
187
|
+
"ls-tree",
|
|
188
|
+
self.label,
|
|
189
|
+
"--",
|
|
190
|
+
target_name,
|
|
191
|
+
).stdout
|
|
192
|
+
if not target_entry:
|
|
193
|
+
continue
|
|
194
|
+
selected.add(target_name)
|
|
195
|
+
pending.append(target_name)
|
|
196
|
+
return sorted(selected)
|
|
197
|
+
|
|
198
|
+
def _git(self, *args: str) -> subprocess.CompletedProcess[str]:
|
|
199
|
+
try:
|
|
200
|
+
proc = subprocess.run(
|
|
201
|
+
["git", "-C", str(self.root), *args],
|
|
202
|
+
text=True,
|
|
203
|
+
capture_output=True,
|
|
204
|
+
timeout=30,
|
|
205
|
+
check=False,
|
|
206
|
+
)
|
|
207
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
208
|
+
raise ExtractionError(f"git {' '.join(args)} failed: {exc}") from exc
|
|
209
|
+
if proc.returncode != 0:
|
|
210
|
+
message = proc.stderr.strip() or proc.stdout.strip() or "unknown git error"
|
|
211
|
+
raise ExtractionError(f"git {' '.join(args)} failed: {message}")
|
|
212
|
+
return proc
|
clean_docs/standard.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from clean_docs.errors import ConfigurationError
|
|
11
|
+
|
|
12
|
+
PACK_VERSION = 1
|
|
13
|
+
DEFAULT_PROFILE = "sourcebound-default"
|
|
14
|
+
HEADING_RE = re.compile(r"^(#{2,4})\s+(.+?)\s*$")
|
|
15
|
+
CHECK_RE = re.compile(r"^- \[ \]\s+(.+)$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _digest(text: str) -> str:
|
|
19
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _pack_digest(pack: dict[str, Any]) -> str:
|
|
23
|
+
content = {key: value for key, value in pack.items() if key != "pack_sha256"}
|
|
24
|
+
canonical = json.dumps(content, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
25
|
+
return _digest(canonical)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _exemplar_asset(standard: Path) -> tuple[str, str]:
|
|
29
|
+
local = standard.parent / "src/clean_docs/standards/exemplars.md"
|
|
30
|
+
if local.exists():
|
|
31
|
+
text = local.read_text(encoding="utf-8")
|
|
32
|
+
else:
|
|
33
|
+
resource = resources.files("clean_docs").joinpath("standards/exemplars.md")
|
|
34
|
+
with resources.as_file(resource) as path:
|
|
35
|
+
text = path.read_text(encoding="utf-8")
|
|
36
|
+
return text, _digest(text)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _headings(lines: list[str]) -> list[dict[str, Any]]:
|
|
40
|
+
result = []
|
|
41
|
+
for line_number, line in enumerate(lines, start=1):
|
|
42
|
+
match = HEADING_RE.match(line)
|
|
43
|
+
if match:
|
|
44
|
+
result.append({
|
|
45
|
+
"level": len(match.group(1)),
|
|
46
|
+
"title": match.group(2),
|
|
47
|
+
"line": line_number,
|
|
48
|
+
})
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _checklist(lines: list[str]) -> list[str]:
|
|
53
|
+
start = next(
|
|
54
|
+
(index for index, line in enumerate(lines) if line.startswith("## 8. Pre-publish checklist")),
|
|
55
|
+
None,
|
|
56
|
+
)
|
|
57
|
+
if start is None:
|
|
58
|
+
raise ConfigurationError("standard is missing the pre-publish checklist")
|
|
59
|
+
checks: list[str] = []
|
|
60
|
+
current: list[str] = []
|
|
61
|
+
for line in lines[start + 1:]:
|
|
62
|
+
match = CHECK_RE.match(line)
|
|
63
|
+
if match:
|
|
64
|
+
if current:
|
|
65
|
+
checks.append(" ".join(current))
|
|
66
|
+
current = [match.group(1).strip()]
|
|
67
|
+
elif current and line.startswith(" "):
|
|
68
|
+
current.append(line.strip())
|
|
69
|
+
elif current and not line.strip():
|
|
70
|
+
checks.append(" ".join(current))
|
|
71
|
+
current = []
|
|
72
|
+
if current:
|
|
73
|
+
checks.append(" ".join(current))
|
|
74
|
+
if not checks:
|
|
75
|
+
raise ConfigurationError("standard pre-publish checklist is empty")
|
|
76
|
+
return checks
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _mechanical_policy(text: str, checks: list[str]) -> dict[str, Any]:
|
|
80
|
+
length = re.search(
|
|
81
|
+
r"README pages over (?P<readme>\d+) lines and guides over (?P<guide>\d+) lines",
|
|
82
|
+
text,
|
|
83
|
+
)
|
|
84
|
+
section = re.search(r"A section over (?P<section>\d+) lines", text)
|
|
85
|
+
preamble = re.search(r"first (?P<preamble>\d+) lines", text)
|
|
86
|
+
nominalizations = re.search(
|
|
87
|
+
r"sentence with (?P<nominalizations>\w+) or more abstraction-suffix", text
|
|
88
|
+
)
|
|
89
|
+
qualifiers = re.search(r"gets at most (?P<qualifiers>\w+) `may`", text)
|
|
90
|
+
if not all((length, section, preamble, nominalizations, qualifiers)):
|
|
91
|
+
raise ConfigurationError(
|
|
92
|
+
"standard is missing page budgets, preamble window, or register thresholds"
|
|
93
|
+
)
|
|
94
|
+
number_words = {
|
|
95
|
+
"zero": 0,
|
|
96
|
+
"one": 1,
|
|
97
|
+
"two": 2,
|
|
98
|
+
"three": 3,
|
|
99
|
+
"four": 4,
|
|
100
|
+
"five": 5,
|
|
101
|
+
}
|
|
102
|
+
assert length is not None
|
|
103
|
+
assert section is not None
|
|
104
|
+
assert preamble is not None
|
|
105
|
+
assert nominalizations is not None
|
|
106
|
+
assert qualifiers is not None
|
|
107
|
+
booster_check = next((check for check in checks if "booster adjectives" in check), None)
|
|
108
|
+
if booster_check is None:
|
|
109
|
+
raise ConfigurationError("standard is missing the booster-adjective check")
|
|
110
|
+
boosters = re.findall(r"`([^`]+)`", booster_check)
|
|
111
|
+
if len(boosters) < 3:
|
|
112
|
+
raise ConfigurationError("standard booster-adjective check has no usable registry")
|
|
113
|
+
return {
|
|
114
|
+
"readme_max_lines": int(length.group("readme")),
|
|
115
|
+
"guide_max_lines": int(length.group("guide")),
|
|
116
|
+
"section_max_lines": int(section.group("section")),
|
|
117
|
+
"preamble_window_lines": int(preamble.group("preamble")),
|
|
118
|
+
"nominalization_threshold": number_words[nominalizations.group("nominalizations")],
|
|
119
|
+
"nominalization_allowlist": [
|
|
120
|
+
"documentation",
|
|
121
|
+
"application",
|
|
122
|
+
"section",
|
|
123
|
+
"configuration",
|
|
124
|
+
],
|
|
125
|
+
"sentence_variance_min_words": 15,
|
|
126
|
+
"sentence_variance_max_words": 35,
|
|
127
|
+
"qualifier_threshold": number_words[qualifiers.group("qualifiers")],
|
|
128
|
+
"significance_phrases": [
|
|
129
|
+
"exactly the",
|
|
130
|
+
"the very",
|
|
131
|
+
"this demonstrates",
|
|
132
|
+
"deliberately",
|
|
133
|
+
"is itself",
|
|
134
|
+
"which is precisely",
|
|
135
|
+
],
|
|
136
|
+
"prohibited_boosters": boosters,
|
|
137
|
+
"require_grounded_facts": any("factual claim" in check for check in checks),
|
|
138
|
+
"require_definition_first": any("first screen defines" in check for check in checks),
|
|
139
|
+
"require_one_job": any("one job" in check for check in checks),
|
|
140
|
+
"require_purpose_contract": any("BLUF purpose contract" in check for check in checks),
|
|
141
|
+
"require_preamble_contract": any(
|
|
142
|
+
"first 15 lines contain the purpose" in check for check in checks
|
|
143
|
+
),
|
|
144
|
+
"require_readme_routes": any("README routes decisions" in check for check in checks),
|
|
145
|
+
"require_depth_links": any(
|
|
146
|
+
"explanatory section over 80 words" in check for check in checks
|
|
147
|
+
),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _style_contract(text: str, checks: list[str]) -> dict[str, Any]:
|
|
152
|
+
normalized = " ".join(text.split())
|
|
153
|
+
required_phrases = {
|
|
154
|
+
"second_person": "Second person + imperative",
|
|
155
|
+
"system_actor": "Name the system as an actor",
|
|
156
|
+
"informative_clauses": "Every clause adds information",
|
|
157
|
+
"concrete_verbs": "Plain, concrete verbs",
|
|
158
|
+
"direct_facts": "State facts without hedging",
|
|
159
|
+
"senior_colleague": "helpful senior colleague",
|
|
160
|
+
"bounded_personality": "Personality has a budget",
|
|
161
|
+
"bluf": "BLUF purpose contract",
|
|
162
|
+
}
|
|
163
|
+
missing = [name for name, phrase in required_phrases.items() if phrase not in normalized]
|
|
164
|
+
if missing:
|
|
165
|
+
raise ConfigurationError(
|
|
166
|
+
"standard is missing required style trait(s): " + ", ".join(missing)
|
|
167
|
+
)
|
|
168
|
+
if not any("BLUF purpose contract" in check for check in checks):
|
|
169
|
+
raise ConfigurationError("standard checklist is missing the BLUF purpose contract")
|
|
170
|
+
if not any("subject-derived memorable element" in check for check in checks):
|
|
171
|
+
raise ConfigurationError("standard checklist is missing bounded personality review")
|
|
172
|
+
return {
|
|
173
|
+
"precedence": [
|
|
174
|
+
"truth and honesty",
|
|
175
|
+
"grounding",
|
|
176
|
+
"reader budget",
|
|
177
|
+
"register",
|
|
178
|
+
"warmth",
|
|
179
|
+
],
|
|
180
|
+
"voice": {
|
|
181
|
+
"register": "helpful senior colleague",
|
|
182
|
+
"reader_actions": "second person and imperative",
|
|
183
|
+
"system_behavior": "name the system as an actor and state behavior as fact",
|
|
184
|
+
"sentence_shape": "split claims that need separate evidence or differ in scope",
|
|
185
|
+
"verbs": "plain and concrete",
|
|
186
|
+
"certainty": "direct facts; mark genuine uncertainty explicitly",
|
|
187
|
+
"contractions": "allowed",
|
|
188
|
+
},
|
|
189
|
+
"purpose_contract": {
|
|
190
|
+
"begin_marker": "<!-- sourcebound:purpose -->",
|
|
191
|
+
"end_marker": "<!-- sourcebound:end purpose -->",
|
|
192
|
+
"position": "first meaningful block after the H1",
|
|
193
|
+
"mechanical": [
|
|
194
|
+
"exactly one marked purpose block",
|
|
195
|
+
"purpose block precedes body content",
|
|
196
|
+
"purpose prose does not restate the H1",
|
|
197
|
+
],
|
|
198
|
+
"judgment": [
|
|
199
|
+
"defines the project-specific subject and intended operator",
|
|
200
|
+
"names the consequential failure or decision the page addresses",
|
|
201
|
+
"states the authority boundary and a falsifiable resulting capability",
|
|
202
|
+
"uses authored language grounded in the implementation and cited sources",
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def compile_standard(path: Path) -> dict[str, Any]:
|
|
209
|
+
try:
|
|
210
|
+
text = path.read_text(encoding="utf-8")
|
|
211
|
+
except OSError as exc:
|
|
212
|
+
raise ConfigurationError(f"cannot read standard {path}: {exc}") from exc
|
|
213
|
+
lines = text.splitlines()
|
|
214
|
+
headings = _headings(lines)
|
|
215
|
+
titles = [heading["title"] for heading in headings]
|
|
216
|
+
required = {
|
|
217
|
+
"voice": "2. Voice at the sentence level",
|
|
218
|
+
"document": "3. How to explain something technical simply (the actual techniques)",
|
|
219
|
+
"corpus": "6. Beyond the single doc: does it earn its existence?",
|
|
220
|
+
"grounding": "7. Grounding: the doc must be true to the code (and stay true)",
|
|
221
|
+
}
|
|
222
|
+
missing = [title for title in required.values() if title not in titles]
|
|
223
|
+
if missing:
|
|
224
|
+
raise ConfigurationError(f"standard is missing required tier heading(s): {', '.join(missing)}")
|
|
225
|
+
checks = _checklist(lines)
|
|
226
|
+
style = _style_contract(text, checks)
|
|
227
|
+
exemplars, exemplar_sha256 = _exemplar_asset(path)
|
|
228
|
+
pack: dict[str, Any] = {
|
|
229
|
+
"pack_version": PACK_VERSION,
|
|
230
|
+
"profile": DEFAULT_PROFILE,
|
|
231
|
+
"source": {
|
|
232
|
+
"name": path.name,
|
|
233
|
+
"sha256": _digest(text),
|
|
234
|
+
},
|
|
235
|
+
"tiers": required,
|
|
236
|
+
"headings": headings,
|
|
237
|
+
"checklist": checks,
|
|
238
|
+
"policy": _mechanical_policy(text, checks),
|
|
239
|
+
"style": style,
|
|
240
|
+
"generation": {
|
|
241
|
+
"instructions": text,
|
|
242
|
+
"constraint": "Phrase only the supplied evidence and preserve its scope.",
|
|
243
|
+
"voice": style["voice"],
|
|
244
|
+
"purpose_contract": style["purpose_contract"],
|
|
245
|
+
"precedence": style["precedence"],
|
|
246
|
+
"exemplars": exemplars,
|
|
247
|
+
"exemplars_sha256": exemplar_sha256,
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
pack["pack_sha256"] = _pack_digest(pack)
|
|
251
|
+
return pack
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def write_pack(pack: dict[str, Any], path: Path) -> None:
|
|
255
|
+
from clean_docs.regions import atomic_write
|
|
256
|
+
|
|
257
|
+
atomic_write(path, json.dumps(pack, indent=2, ensure_ascii=False) + "\n")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def load_pack(path: Path) -> dict[str, Any]:
|
|
261
|
+
try:
|
|
262
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
263
|
+
except OSError as exc:
|
|
264
|
+
raise ConfigurationError(f"cannot read policy pack {path}: {exc}") from exc
|
|
265
|
+
except json.JSONDecodeError as exc:
|
|
266
|
+
raise ConfigurationError(f"invalid policy pack {path}: {exc}") from exc
|
|
267
|
+
if not isinstance(raw, dict) or raw.get("pack_version") != PACK_VERSION:
|
|
268
|
+
raise ConfigurationError(f"unsupported policy pack: {path}")
|
|
269
|
+
if raw.get("pack_sha256") != _pack_digest(raw):
|
|
270
|
+
raise ConfigurationError(f"policy pack integrity check failed: {path}")
|
|
271
|
+
return raw
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def load_default_pack() -> dict[str, Any]:
|
|
275
|
+
resource = resources.files("clean_docs").joinpath("standards/default.json")
|
|
276
|
+
with resources.as_file(resource) as path:
|
|
277
|
+
return load_pack(path)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def pack_matches_standard(standard: Path, pack_path: Path) -> bool:
|
|
281
|
+
return compile_standard(standard) == load_pack(pack_path)
|