mindslice-forge 0.1.0__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.
@@ -0,0 +1,419 @@
1
+ """The forge pipeline: profile -> plan -> split -> extract (chunk by chunk) -> finalize.
2
+
3
+ Every stage has the same shape: an agent session does the work, a check decides whether it is
4
+ sound, and repair sessions fix what the check reports. A finished stage is committed in the
5
+ workspace and recorded in ``state.json``, so running the same command again continues from there.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ import secrets
13
+ import shlex
14
+ import shutil
15
+ import sys
16
+ import time
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Callable
20
+
21
+ from . import ForgeError, checkpoint, engine, harness, lookup, profile, split
22
+ from . import state as progress
23
+
24
+ PROMPTS = Path(__file__).parent / "prompts"
25
+ NO_PROMPT = "(none given: build a faithful, general-purpose knowledge base of the sources)"
26
+ SPLIT_NEEDED = "The sources are too large to read in one session, so you must also write the splitter."
27
+ SPLIT_NOT_NEEDED = "The sources fit in one session and will be read as a single chunk: do NOT write a splitter."
28
+ MAX_ISSUES_SHOWN = 50
29
+ PROBE_TIMEOUT = 300 # seconds: the harness check is a task of a few tool calls
30
+
31
+ Check = Callable[[], list[dict]]
32
+
33
+
34
+ @dataclass
35
+ class Job:
36
+ sources: list[Path]
37
+ dicts: list[Path]
38
+ prompt: str
39
+ out: Path
40
+ harness: list[str] # command template, see harness.template()
41
+ sample: int | None = None
42
+ chunk_size: int = 100_000
43
+ timeout: int = 3600
44
+ max_repairs: int = 2
45
+ from_stage: str | None = None
46
+ dry_run: bool = False
47
+
48
+ @property
49
+ def work(self) -> Path:
50
+ return lookup.forge_dir(self.out)
51
+
52
+ @property
53
+ def chunks(self) -> Path:
54
+ return self.work / "chunks"
55
+
56
+ @property
57
+ def state_file(self) -> Path:
58
+ return self.work / "state.json"
59
+
60
+
61
+ def log(message: str) -> None:
62
+ print(message, flush=True)
63
+
64
+
65
+ def run(job: Job) -> None:
66
+ sources, not_text = profile.scan(job.sources)
67
+ if not sources:
68
+ raise ForgeError("no text files found in the sources")
69
+ inventory = profile.render(sources, not_text)
70
+ needs_split = sum(s.chars for s in sources) > job.chunk_size
71
+ if job.dry_run:
72
+ log(inventory)
73
+ log(f"Reference frame:\n{bullets(job.dicts)}\n")
74
+ log(f"Workspace: {job.out}\nForge directory: {job.work}")
75
+ log(f"Splitting: {'needed' if needs_split else 'not needed'} at {job.chunk_size} characters per chunk")
76
+ log(f"An agent session must reach:\n{reach(job)}")
77
+ log(f"Harness: {shlex.join(job.harness) if job.harness else '(no --harness-cmd given)'}")
78
+ return
79
+
80
+ engine.version()
81
+ state = start(job, sources)
82
+ (job.work / "profile.md").write_text(inventory, encoding="utf-8")
83
+ if not state["plan"]:
84
+ plan(job, state, inventory, needs_split)
85
+ if not state["split"]:
86
+ split_sources(job, state, sources, needs_split)
87
+
88
+ chunks = split.load(job.chunks)
89
+ selected = chunks[:job.sample] if job.sample else chunks
90
+ for index, chunk in enumerate(selected):
91
+ if chunk["id"] not in state["chunks_done"]:
92
+ previous = selected[index - 1]["id"] if index else None
93
+ extract(job, state, chunk, f"{index + 1}/{len(chunks)}", previous)
94
+ if not state["finalized"]:
95
+ finalize(job, state, len(chunks))
96
+
97
+ report = engine.validate(job.out)
98
+ if report["errors"]:
99
+ raise ForgeError("the finished workspace does not validate:\n" + format_issues(report["errors"]))
100
+ log(f"\nWorkspace ready: {job.out}")
101
+ log(f" {report['stats']['cards']} cards, {len(state['chunks_done'])} of {len(chunks)} chunks processed, "
102
+ f"{len(report['warnings'])} validation warning(s)")
103
+ log(f" Report: {job.work / 'report.md'}")
104
+ log(f" Per-chunk changes: git -C {job.out} log --stat")
105
+ log(f" Next: mindslice -w {job.out} doctor")
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Starting and resuming
110
+ # ---------------------------------------------------------------------------
111
+
112
+
113
+ def start(job: Job, sources: list[profile.Source]) -> dict:
114
+ """Load the state of an earlier run, or create the workspace for a new one."""
115
+ files = [s.path for s in sources] + profile.collect(job.dicts)
116
+ fingerprint = progress.fingerprint(files, job.prompt, job.chunk_size)
117
+ state = progress.load(job.state_file)
118
+ if state is None:
119
+ if job.from_stage:
120
+ raise ForgeError(f"--from needs an earlier run, and {job.work} has none")
121
+ if job.out.exists() and any(job.out.iterdir()):
122
+ raise ForgeError(f"{job.out} already exists and was not created by the forge; choose another --output")
123
+ job.work.mkdir(parents=True, exist_ok=True)
124
+ engine.init(job.out)
125
+ checkpoint.init(job.out)
126
+ checkpoint.commit(job.out, "empty workspace", tag="forge-init")
127
+ state = progress.new(fingerprint)
128
+ elif job.from_stage:
129
+ rewind(job, state)
130
+ state["fingerprint"] = fingerprint
131
+ elif state["fingerprint"] != fingerprint:
132
+ raise ForgeError("the sources, dict, prompt or chunk size changed since this output was started.\n"
133
+ "Redo the affected stages with --from plan|split|extract, or choose another --output.")
134
+ else:
135
+ checkpoint.rollback(job.out) # drop whatever an interrupted session left behind
136
+ progress.save(job.state_file, state)
137
+ return state
138
+
139
+
140
+ def rewind(job: Job, state: dict) -> None:
141
+ """--from STAGE: return the workspace and the forge directory to how they were before that stage."""
142
+ stage = job.from_stage if state["plan"] else "plan" # nothing exists yet after an unfinished plan
143
+ progress.rewind(state, stage)
144
+ if stage == "finalize":
145
+ checkpoint.rollback(job.out)
146
+ return
147
+ checkpoint.rollback(job.out, "forge-init" if stage == "plan" else "forge-plan")
148
+ stale = {"plan": ("plan.md", "split.py", "chunks", "ledger", "handoff", "lookups.jsonl"),
149
+ "split": ("chunks", "ledger", "handoff", "lookups.jsonl"),
150
+ "extract": ("ledger", "handoff", "lookups.jsonl")}[stage]
151
+ for name in stale:
152
+ path = job.work / name
153
+ shutil.rmtree(path) if path.is_dir() else path.unlink(missing_ok=True)
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # Stages
158
+ # ---------------------------------------------------------------------------
159
+
160
+
161
+ def plan(job: Job, state: dict, inventory: str, needs_split: bool) -> None:
162
+ log("[plan] the agent studies the sources and writes the build plan")
163
+ prompt = render("plan", profile=inventory, dicts=bullets(job.dicts), user_prompt=job.prompt or NO_PROMPT,
164
+ plan_file=job.work / "plan.md", slices_dir=job.out / "slices",
165
+ split_note=SPLIT_NEEDED if needs_split else SPLIT_NOT_NEEDED,
166
+ split_file=job.work / "split.py", chunk_size=job.chunk_size)
167
+ run_step(job, "plan", prompt, lambda: check_plan(job, needs_split))
168
+ aliases = engine.mount_slices(job.out)
169
+ checkpoint.commit(job.out, "plan", tag="forge-plan")
170
+ state["plan"] = True
171
+ progress.save(job.state_file, state)
172
+ log(f"[plan] done; slices: {', '.join(aliases)}")
173
+
174
+
175
+ def check_plan(job: Job, needs_split: bool) -> list[dict]:
176
+ issues = []
177
+ if not has_text(job.work / "plan.md"):
178
+ issues.append(problem("plan.md was not written", job.work / "plan.md"))
179
+ if needs_split and not has_text(job.work / "split.py"):
180
+ issues.append(problem("split.py was not written", job.work / "split.py"))
181
+ slices = sorted((job.out / "slices").glob("*.slice"))
182
+ if not slices:
183
+ issues.append(problem("no <name>.slice directory was created", job.out / "slices"))
184
+ for directory in slices:
185
+ if not has_text(directory / "slice.toml"):
186
+ issues.append(problem("slice.toml is missing", directory / "slice.toml"))
187
+ issues += engine.issues(engine.validate(job.out, directory))
188
+ return issues
189
+
190
+
191
+ def split_sources(job: Job, state: dict, sources: list[profile.Source], needs_split: bool) -> None:
192
+ if needs_split:
193
+ log("[split] running the agent-written splitter")
194
+ settle(job, "split", lambda: check_split(job, sources), task="plan")
195
+ else:
196
+ split.single_chunk(job.chunks, sources)
197
+ chunks = split.load(job.chunks)
198
+ state["split"] = True
199
+ progress.save(job.state_file, state)
200
+ log(f"[split] {len(chunks)} chunk(s): {chunks[0].get('title', '')} ... {chunks[-1].get('title', '')}")
201
+
202
+
203
+ def check_split(job: Job, sources: list[profile.Source]) -> list[dict]:
204
+ script = job.work / "split.py"
205
+ failure = split.run_splitter(script, job.chunks, sources, job.chunk_size)
206
+ errors = [failure] if failure else split.verify(job.chunks, sources, job.chunk_size)
207
+ return [problem(error, script) for error in errors]
208
+
209
+
210
+ def extract(job: Job, state: dict, chunk: dict, position: str, previous: str | None) -> None:
211
+ step, title = f"extract-{chunk['id']}", chunk.get("title", "")
212
+ log(f"[{step}] chunk {position}: {title}")
213
+ handoff = job.work / "handoff"
214
+ prompt = render("extract", plan_file=job.work / "plan.md", user_prompt=job.prompt or NO_PROMPT,
215
+ dicts=bullets(job.dicts), chunk_file=job.chunks / chunk["file"], chunk_title=title,
216
+ chunk_parts=parts_list(chunk), lookup=lookup_command(job),
217
+ position=position, out=job.out, slices_dir=job.out / "slices",
218
+ previous_handoff=handoff / f"{previous}.md" if previous else "(none: this is the first chunk)",
219
+ handoff_file=handoff / f"{chunk['id']}.md", ledger_file=job.work / "ledger" / f"{chunk['id']}.md")
220
+ run_step(job, step, prompt, lambda: check_chunk(job, chunk), check_lookups=True)
221
+ checkpoint.commit(job.out, f"chunk {chunk['id']}: {title}")
222
+ state["chunks_done"].append(chunk["id"])
223
+ state["finalized"] = False
224
+ progress.save(job.state_file, state)
225
+
226
+
227
+ def check_chunk(job: Job, chunk: dict) -> list[dict]:
228
+ notes = [job.work / "ledger" / f"{chunk['id']}.md", job.work / "handoff" / f"{chunk['id']}.md"]
229
+ issues = [problem(f"{path.parent.name}/{path.name} was not written", path) for path in notes if not has_text(path)]
230
+ return issues + engine.issues(engine.validate(job.out), checkpoint.changed(job.out))
231
+
232
+
233
+ def not_looked_up(job: Job, queries: list[str]) -> list[dict]:
234
+ """Every new card in this step needs a successful lookup naming it."""
235
+ slices = (job.out / "slices").resolve()
236
+ overviews = {directory / "overview.md" for directory in slices.glob("*.slice")}
237
+ new_cards = sorted(path for path in checkpoint.created(job.out)
238
+ if slices in path.parents and path.suffix == ".md" and path not in overviews)
239
+ return [{"severity": "error", "code": "not_looked_up", "file": str(card),
240
+ "message": "this card was created without looking up its title or an alias. Look it up now, with the "
241
+ f"command {lookup_command(job)}; if the workspace already has a card for the same thing, "
242
+ "merge the two into the one that existed first"}
243
+ for card in new_cards if not lookup.covered(engine.card_names(card), queries)]
244
+
245
+
246
+ def finalize(job: Job, state: dict, total: int) -> None:
247
+ log("[finalize] overviews, remaining problems, retrieval checks and the report")
248
+ done = len(state["chunks_done"])
249
+ coverage = f"{done} of {total} chunks" + ("" if done == total else " (a partial, sample build)")
250
+ prompt = render("finalize", plan_file=job.work / "plan.md", user_prompt=job.prompt or NO_PROMPT,
251
+ out=job.out, lookup=lookup_command(job), slices_dir=job.out / "slices", ledger_dir=job.work / "ledger",
252
+ report_file=job.work / "report.md", coverage=coverage,
253
+ profile_file=job.work / "profile.md", manifest_file=job.chunks / "manifest.json",
254
+ issues=format_issues(engine.issues(engine.validate(job.out))) or "(none)")
255
+ run_step(job, "finalize", prompt, lambda: check_final(job), check_lookups=True)
256
+ checkpoint.commit(job.out, "finalize")
257
+ state["finalized"] = True
258
+ progress.save(job.state_file, state)
259
+
260
+
261
+ def check_final(job: Job) -> list[dict]:
262
+ required = [d / "overview.md" for d in sorted((job.out / "slices").glob("*.slice"))] + [job.work / "report.md"]
263
+ issues = [problem(f"{path.name} was not written", path) for path in required if not has_text(path)]
264
+ return issues + engine.issues(engine.validate(job.out))
265
+
266
+
267
+ # ---------------------------------------------------------------------------
268
+ # Sessions, checks and repairs
269
+ # ---------------------------------------------------------------------------
270
+
271
+
272
+ def run_step(job: Job, step: str, prompt: str, check: Check, *, check_lookups: bool = False) -> None:
273
+ """Run the step's agent session, retrying from a clean workspace if it fails, then settle its result."""
274
+ for delay in (*harness.BACKOFF, None):
275
+ earlier = len(lookup.queries(job.out)) if check_lookups else 0
276
+ if session(job, step, prompt):
277
+ break
278
+ checkpoint.rollback(job.out)
279
+ if delay is None:
280
+ raise ForgeError(f"{step}: the agent session failed {len(harness.BACKOFF) + 1} times; "
281
+ f"see {job.work / 'logs' / (step + '.log')}")
282
+ log(f"[{step}] the session failed; retrying in {delay}s")
283
+ time.sleep(delay)
284
+
285
+ def checked() -> list[dict]:
286
+ issues = check()
287
+ if check_lookups:
288
+ issues += not_looked_up(job, lookup.queries(job.out)[earlier:])
289
+ return issues
290
+
291
+ settle(job, step, checked, task=step)
292
+
293
+
294
+ def settle(job: Job, step: str, check: Check, task: str) -> None:
295
+ """Run repair sessions until ``check`` is satisfied. Errors left after the last repair are fatal;
296
+ warnings are not, and end up in the final report. ``task`` names the prompt the work was done under."""
297
+ for attempt in range(job.max_repairs + 1):
298
+ issues = check()
299
+ if not issues:
300
+ return
301
+ if attempt < job.max_repairs:
302
+ log(f"[{step}] {len(issues)} problem(s) found; repair session {attempt + 1}")
303
+ session(job, f"{step}.repair{attempt + 1}",
304
+ render("repair", task_file=job.work / "prompts" / f"{task}.md", issues=format_issues(issues)))
305
+ errors = [issue for issue in issues if issue["severity"] == "error"]
306
+ if errors:
307
+ raise ForgeError(f"{step}: problems remain after {job.max_repairs} repair session(s):\n{format_issues(errors)}\n"
308
+ f"The failed attempt is left in place for inspection; "
309
+ f"running the same command again discards it and retries this step.")
310
+ log(f"[{step}] {len(issues)} warning(s) left for the final report")
311
+
312
+
313
+ def session(job: Job, name: str, prompt: str) -> bool:
314
+ """One fresh agent session. The rendered prompt and the session log are kept in the forge directory."""
315
+ check_harness(job)
316
+ prompt_file = job.work / "prompts" / f"{name}.md"
317
+ prompt_file.parent.mkdir(parents=True, exist_ok=True)
318
+ prompt_file.write_text(prompt, encoding="utf-8")
319
+ cmd = harness.command(job.harness, prompt_file, job.work)
320
+ checkpoint.begin(job.out)
321
+ return harness.run(cmd, job.work, job.work / "logs" / f"{name}.log", job.timeout)
322
+
323
+
324
+ def check_harness(job: Job) -> None:
325
+ """Before the first session with a harness command, prove with a short one that it works.
326
+
327
+ The forge does not know the harness, so a command that cannot do the work must fail here, in
328
+ seconds and with the reason, not after a long step and its retries. The command that passed
329
+ is recorded in the forge directory, and is not checked again."""
330
+ record = job.work / "harness.json"
331
+ if record.exists() and json.loads(record.read_text(encoding="utf-8")) == job.harness:
332
+ return
333
+ log("[harness] checking the harness command with one short session")
334
+ engine_version, source = engine.version(), profile.scan(job.sources)[0][0].path
335
+ token, answer_file = secrets.token_hex(4), job.out / "forge-probe.txt"
336
+ prompt_file, log_file = job.work / "prompts" / "probe.md", job.work / "logs" / "probe.log"
337
+ prompt_file.parent.mkdir(parents=True, exist_ok=True)
338
+ prompt_file.write_text(render("probe", source=source, probe_file=answer_file, token=token), encoding="utf-8")
339
+ cmd = harness.command(job.harness, prompt_file, job.work)
340
+ timeout = min(job.timeout, PROBE_TIMEOUT)
341
+ checkpoint.begin(job.out)
342
+ try: # no step is unfinished when a run reaches its first session, so the rollback only removes the answer
343
+ finished = harness.run(cmd, job.work, log_file, timeout)
344
+ answer = answer_file.read_text(encoding="utf-8", errors="replace") if answer_file.is_file() else None
345
+ finally:
346
+ checkpoint.rollback(job.out)
347
+
348
+ problems = []
349
+ if not finished:
350
+ problems.append(f"the command did not end with exit status 0 within {timeout}s")
351
+ if answer is None:
352
+ problems.append(f"the session did not write {answer_file}: the agent must receive its task, use its tools "
353
+ "without asking for permission, and be able to write in the workspace")
354
+ else:
355
+ if token not in answer:
356
+ problems.append(f"the session wrote {answer_file} without the code from its prompt")
357
+ if engine_version not in answer:
358
+ problems.append(f"`mindslice --version` did not print `{engine_version}` in the session: "
359
+ "the agent's shell needs the same `mindslice` on its PATH")
360
+ if str(source.stat().st_size) not in answer.replace(",", ""):
361
+ problems.append(f"the session did not find the size of {source}: the agent must be able to read the sources")
362
+ if problems:
363
+ tail = log_file.read_text(encoding="utf-8", errors="replace").splitlines()[-12:]
364
+ raise ForgeError("the harness command cannot run this build:\n" + "\n".join(f"- {p}" for p in problems)
365
+ + f"\nCommand: {shlex.join(cmd)}\nThe end of the session log, {log_file}:\n"
366
+ + "\n".join(f" {line[:300]}" for line in tail)
367
+ + "\n`mindslice-forge --help` says what the command must do.")
368
+ record.write_text(json.dumps(job.harness, ensure_ascii=False) + "\n", encoding="utf-8")
369
+ log("[harness] the command works")
370
+
371
+
372
+ # ---------------------------------------------------------------------------
373
+ # Small helpers
374
+ # ---------------------------------------------------------------------------
375
+
376
+
377
+ def render(name: str, **values: object) -> str:
378
+ """Fill the ``{{placeholders}}`` of a prompt template; ``{{card_format}}`` is always available."""
379
+ values.setdefault("card_format", (PROMPTS / "card-format.md").read_text(encoding="utf-8"))
380
+ template = (PROMPTS / f"{name}.md").read_text(encoding="utf-8")
381
+ return re.sub(r"\{\{(\w+)\}\}", lambda match: str(values[match.group(1)]), template)
382
+
383
+
384
+ def problem(message: str, file: Path) -> dict:
385
+ """A forge-detected error, in the shape of an engine validation issue."""
386
+ return {"severity": "error", "code": "forge", "message": message, "file": str(file)}
387
+
388
+
389
+ def format_issues(issues: list[dict]) -> str:
390
+ lines = [f"- {i['severity']} [{i['code']}] {i.get('file') or i.get('address', '')}: {i['message']}"
391
+ for i in issues[:MAX_ISSUES_SHOWN]]
392
+ if len(issues) > MAX_ISSUES_SHOWN:
393
+ lines.append(f"- ... and {len(issues) - MAX_ISSUES_SHOWN} more of the same kinds")
394
+ return "\n".join(lines)
395
+
396
+
397
+ def lookup_command(job: Job) -> str:
398
+ """The command line an agent uses to search the workspace, with the search recorded."""
399
+ return shlex.join([sys.executable, lookup.__file__, str(job.out)])
400
+
401
+
402
+ def parts_list(chunk: dict) -> str:
403
+ """Which source lines a chunk holds: a chunk can span several files, and a file several chunks."""
404
+ return "\n".join(f"- `{p['source']}`, lines {p['start_line']}-{p['end_line']}" for p in chunk["parts"]) or "(none)"
405
+
406
+
407
+ def reach(job: Job) -> str:
408
+ """The directories a session of the harness command works in, for whoever composes that command."""
409
+ read = dict.fromkeys(p if p.is_dir() else p.parent for p in [*job.sources, *job.dicts])
410
+ return "\n".join([f"- `{job.work}` (its working directory; read and write)", f"- `{job.out}` (read and write)",
411
+ *(f"- `{directory}` (read)" for directory in read)])
412
+
413
+
414
+ def bullets(paths: list[Path]) -> str:
415
+ return "\n".join(f"- `{p}`" for p in paths) or "(none)"
416
+
417
+
418
+ def has_text(path: Path) -> bool:
419
+ return path.is_file() and path.stat().st_size > 0
@@ -0,0 +1,83 @@
1
+ """Source inventory: which text files there are, how they are encoded and how large they are.
2
+
3
+ Nothing here looks at the *structure* of a document; finding chapters and sections is the agent's job.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import codecs
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ from . import ForgeError
13
+
14
+
15
+ @dataclass
16
+ class Source:
17
+ path: Path
18
+ encoding: str
19
+ lines: int
20
+ chars: int
21
+
22
+
23
+ def collect(paths: list[Path]) -> list[Path]:
24
+ """The files named by ``paths``: a file is itself, a directory is every non-hidden file below it."""
25
+ files: list[Path] = []
26
+ for path in paths:
27
+ if path.is_file():
28
+ files.append(path)
29
+ elif path.is_dir():
30
+ files += sorted(p for p in path.rglob("*")
31
+ if p.is_file() and not any(part.startswith(".") for part in p.relative_to(path).parts))
32
+ else:
33
+ raise ForgeError(f"no such file or directory: {path}")
34
+ return list(dict.fromkeys(files))
35
+
36
+
37
+ def detect_encoding(data: bytes) -> str | None:
38
+ """The encoding that decodes ``data``, or None when it is not text."""
39
+ if data.startswith((codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE)):
40
+ return "utf-16"
41
+ if b"\x00" in data[:8192]:
42
+ return None
43
+ for encoding in ("utf-8-sig", "gb18030"):
44
+ try:
45
+ data.decode(encoding)
46
+ return encoding
47
+ except UnicodeDecodeError:
48
+ continue
49
+ return None
50
+
51
+
52
+ def read_lines(path: Path, encoding: str) -> list[str]:
53
+ """The file's lines without their terminators. Line N of a source is ``read_lines(...)[N - 1]``;
54
+ this is what Python's ``open(path, encoding=encoding)`` iterates over."""
55
+ text = path.read_bytes().decode(encoding).replace("\r\n", "\n").replace("\r", "\n")
56
+ lines = text.split("\n")
57
+ if lines and lines[-1] == "":
58
+ lines.pop()
59
+ return lines
60
+
61
+
62
+ def scan(paths: list[Path]) -> tuple[list[Source], list[Path]]:
63
+ """Profile every file under ``paths``: (text sources, files skipped because they are not text)."""
64
+ sources, skipped = [], []
65
+ for path in collect(paths):
66
+ encoding = detect_encoding(path.read_bytes())
67
+ if encoding is None:
68
+ skipped.append(path)
69
+ continue
70
+ lines = read_lines(path, encoding)
71
+ sources.append(Source(path, encoding, len(lines), sum(map(len, lines))))
72
+ return sources, skipped
73
+
74
+
75
+ def render(sources: list[Source], skipped: list[Path]) -> str:
76
+ """The inventory as Markdown, for the agent and for the user."""
77
+ out = ["| source | encoding | lines | characters |", "| --- | --- | ---: | ---: |"]
78
+ out += [f"| `{s.path}` | {s.encoding} | {s.lines} | {s.chars} |" for s in sources]
79
+ out.append(f"\nTotal: {len(sources)} file(s), {sum(s.chars for s in sources)} characters.")
80
+ if skipped:
81
+ out.append("\nNot text, left out of the build:")
82
+ out += [f"- `{p}`" for p in skipped]
83
+ return "\n".join(out) + "\n"
@@ -0,0 +1,76 @@
1
+ # Card format and retrieval vocabulary
2
+
3
+ ```markdown
4
+ +++
5
+ title = "Python Backend Error Handling Conventions"
6
+ aliases = ["error-handling", "exception-policy"]
7
+ tags = ["python", "backend", "reliability"]
8
+ extends = [] # @alias/path or slice-relative path
9
+ supersedes = [] # @alias/path or slice-relative path
10
+ +++
11
+
12
+ All backend services raise typed AppError subclasses and translate them to RFC 7807 only at the HTTP boundary.
13
+
14
+ - Define domain exceptions in `core/errors.py` inheriting from `AppError`.
15
+ - Map `AppError` subclasses to status codes only in the exception middleware (see [[architecture/http-middleware]]).
16
+ ```
17
+
18
+ Rules:
19
+
20
+ 1. **One focused sub-topic per card**: answer one independently useful question. Around 5-15
21
+ statements is a guideline, not a quota; a short fact needs no padding. `validate()` warns past
22
+ 50 lines. Split by meaning while preserving prerequisites, exceptions and ordered procedures;
23
+ use numbered steps, tables or code blocks when those represent the knowledge more faithfully.
24
+ 2. **Summary paragraph** directly under the closing `+++`: 1-2 sentences, <= 240 chars.
25
+ It is what `search()` shows, so make it a high-signal abstract, not a heading.
26
+ 3. **Bullets are atomic, not stripped**: one fact/rule per `- ` line, self-contained, greppable,
27
+ diffable. Keep the concrete values, names, dates, conditions and the source's own wording:
28
+ search matches only words the card contains.
29
+ 4. **Frontmatter is flat TOML**: only strings and lists of strings. `title` is weighted highest
30
+ in search; `aliases` are synonyms (also usable as `[[alias]]` link targets, see below); `tags`
31
+ are facets.
32
+ 5. **Paths are addresses**: `architecture/async-patterns.md` is `architecture/async-patterns`.
33
+ Use lowercase-kebab-case, group by topic folders, keep paths stable (links depend on them).
34
+ **Name a card by the durable topic or slot it fills** (`team/lead`, `subscriptions/streaming`),
35
+ never by the value it holds today (`team/lead-sarah-chen`, `subscriptions/xelvoria`). Ids and
36
+ `[[links]]` outlive values: a link to a value-named card keeps stating the old value after it
37
+ changes. When maintaining an existing overlay requires a separate replacement id, use a stable
38
+ qualifier such as the change date (`team/lead-2026-11`), never the new value. Link to the slot
39
+ (`[[team/lead]]`): `read()` follows `supersedes` from it to the current card.
40
+ 6. **Links**: `[[path]]` or `[[path|label]]`. Without an `@alias/` prefix a link resolves *only
41
+ inside the same slice* - never into other mounts. Use `[[@persona/tone]]` to reach across slices
42
+ and declare that slice in `slice.toml` `dependencies`.
43
+ 7. Never put secrets, tokens, machine-specific absolute paths or session noise in cards.
44
+ 8. **`load = "always"`** (optional, after `title`) marks a card every session reads at start, with
45
+ no query: an agent persona, the user's reply language, standing instructions. Use it sparingly
46
+ (it costs context in every session) and keep such cards short. Everything else is the default,
47
+ `load = "as-needed"`: found by search. Changes that touch an always-loaded card (editing,
48
+ deleting, extending or superseding it, or marking a card always) are staged for the user's
49
+ approval by default when made through `write`; the user approves with `mindslice apply`.
50
+ Direct authoring of a new slice does not pass through that review buffer; do not infer standing
51
+ instructions from source documents just because they contain imperative wording.
52
+
53
+ ## Aliases: the words someone will search with
54
+
55
+ `search()` is lexical: a card is found only by words it contains (stemmed, case-folded; Chinese,
56
+ Japanese and other unspaced scripts match by character pairs). Someone who has never read the card
57
+ searches with *their* words, not yours. `aliases` are indexed just below the title, so they are the
58
+ place to put the other words for the same thing. Aim for 3-8 useful short aliases per card,
59
+ without padding when fewer suffice:
60
+
61
+ - **Synonyms and near-synonyms** of the title's key terms (`exception policy` for error handling).
62
+ - **The asker's phrasing**: the symptom, task or goal that sends someone to this card, as they
63
+ would say it (`500 errors`, `what to return when a request fails`), including colloquial wording.
64
+ - **Abbreviations and expansions** both ways, and spelling variants (`RFC 7807`, `problem details`).
65
+ - **Former names** of renamed things, and names from tools people know (`error middleware`).
66
+ - **The team's other working language**, if it has one: the terms a colleague would use for this
67
+ topic in that language (`错误处理` on an English card, `error handling` on a Chinese one).
68
+
69
+ Avoid:
70
+
71
+ - Words already in the title or summary: they are indexed already.
72
+ - Generic words that fit most cards (`process`, `policy`, `guide`, `team`, `规范`, `流程`): they make
73
+ unrelated cards match.
74
+ - An alias another card in the same slice already uses (`validate()` warns), or one that equals
75
+ another card's path: aliases are also link and `read()` targets.
76
+