brainfactory 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,403 @@
1
+ """Capabilities generator + intent gate (the self-extending brain — derived layer).
2
+
3
+ The capabilities map (``01-docs/CAPABILITIES.md``) is a DERIVED artifact: it is
4
+ regenerated from code so it cannot drift. Truth comes from:
5
+
6
+ - the brain's command set under ``03-templates/agent-commands/core/<base>/`` and
7
+ ``.../extensions/<base>/`` (a command is a directory containing a ``SKILL.md``
8
+ or ``SKILL.md.tmpl``); the description is read from the SKILL frontmatter;
9
+ - the ``brain.manifest.json`` for the command prefix, platforms,
10
+ framework_version, and the coordinated ``app_repos``.
11
+
12
+ Three modes:
13
+ - ``--write`` : regenerate and write the file (what the capabilities command runs).
14
+ - ``--check`` : regenerate in-memory and diff against the on-disk file; non-zero
15
+ exit if they differ (what CI runs to forbid drift).
16
+ - the intent gate (:func:`intent_gate`) FAILS if a command directory exists with
17
+ no matching row in the committed ``CAPABILITIES.md`` — "a feature cannot land
18
+ without the brain growing with it".
19
+
20
+ Standard library only. Typed. Same code style as ``inspect.py``.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import difflib
26
+ import json
27
+ from dataclasses import dataclass, field
28
+ from datetime import date
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ # Where commands live inside a brain.
33
+ _CORE_REL = "03-templates/agent-commands/core"
34
+ _EXT_REL = "03-templates/agent-commands/extensions"
35
+ # The generated capabilities map, relative to the brain root.
36
+ _CAPABILITIES_REL = "01-docs/CAPABILITIES.md"
37
+ # The brain manifest, relative to the brain root.
38
+ _MANIFEST_REL = "brain.manifest.json"
39
+
40
+ # Banner that marks the file as generated. Used both when writing and as a
41
+ # sanity marker; kept short so it survives reflow.
42
+ _BANNER = "GENERATED — do not hand-edit"
43
+
44
+
45
+ @dataclass
46
+ class CommandInfo:
47
+ """One command discovered under core/ or extensions/."""
48
+
49
+ base: str # directory name, e.g. "sync"
50
+ layer: str # "core" | "extensions"
51
+ description: str # from SKILL frontmatter (or "" if none)
52
+ skill_path: str # relative-to-brain path of the SKILL file
53
+
54
+ def invocation(self, prefix: str) -> str:
55
+ return f"{prefix}-{self.base}"
56
+
57
+ def to_dict(self) -> dict[str, Any]:
58
+ return {
59
+ "base": self.base,
60
+ "layer": self.layer,
61
+ "description": self.description,
62
+ "skill_path": self.skill_path,
63
+ }
64
+
65
+
66
+ @dataclass
67
+ class AppRepo:
68
+ """One coordinated app repo from the manifest."""
69
+
70
+ name: str
71
+ role: str = ""
72
+
73
+ def to_dict(self) -> dict[str, Any]:
74
+ return {"name": self.name, "role": self.role}
75
+
76
+
77
+ @dataclass
78
+ class CapabilitiesModel:
79
+ """Everything the capabilities map is rendered from."""
80
+
81
+ project_name: str
82
+ command_prefix: str
83
+ framework_version: str
84
+ platforms: list[str]
85
+ core_commands: list[CommandInfo]
86
+ extension_commands: list[CommandInfo]
87
+ app_repos: list[AppRepo] = field(default_factory=list)
88
+
89
+ def to_dict(self) -> dict[str, Any]:
90
+ return {
91
+ "project_name": self.project_name,
92
+ "command_prefix": self.command_prefix,
93
+ "framework_version": self.framework_version,
94
+ "platforms": self.platforms,
95
+ "core_commands": [c.to_dict() for c in self.core_commands],
96
+ "extension_commands": [c.to_dict() for c in self.extension_commands],
97
+ "app_repos": [a.to_dict() for a in self.app_repos],
98
+ }
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # Discovery
103
+ # ---------------------------------------------------------------------------
104
+
105
+ def _read_manifest(brain: Path) -> dict[str, Any]:
106
+ """Read brain.manifest.json if present; return {} otherwise."""
107
+ path = brain / _MANIFEST_REL
108
+ if not path.is_file():
109
+ return {}
110
+ try:
111
+ return json.loads(path.read_text(encoding="utf-8"))
112
+ except (OSError, ValueError):
113
+ return {}
114
+
115
+
116
+ def _skill_file(cmd_dir: Path) -> Path | None:
117
+ """Return the SKILL file for a command dir, preferring the rendered form.
118
+
119
+ A command directory qualifies if it contains ``SKILL.md`` (a provisioned
120
+ brain) or ``SKILL.md.tmpl`` (the un-rendered template).
121
+ """
122
+ rendered = cmd_dir / "SKILL.md"
123
+ if rendered.is_file():
124
+ return rendered
125
+ tmpl = cmd_dir / "SKILL.md.tmpl"
126
+ if tmpl.is_file():
127
+ return tmpl
128
+ return None
129
+
130
+
131
+ def _parse_frontmatter_description(skill_path: Path) -> str:
132
+ """Extract the ``description:`` field from a SKILL file's YAML frontmatter.
133
+
134
+ The frontmatter is a leading ``---`` fenced block. Only the single-line
135
+ ``description:`` key is read (no third-party YAML parser). Returns "" if
136
+ absent. Surrounding quotes are stripped.
137
+ """
138
+ try:
139
+ text = skill_path.read_text(encoding="utf-8")
140
+ except OSError:
141
+ return ""
142
+ lines = text.splitlines()
143
+ if not lines or lines[0].strip() != "---":
144
+ return ""
145
+ for line in lines[1:]:
146
+ stripped = line.strip()
147
+ if stripped == "---":
148
+ break
149
+ if stripped.lower().startswith("description:"):
150
+ value = stripped.split(":", 1)[1].strip()
151
+ if len(value) >= 2 and value[0] in "\"'" and value[-1] == value[0]:
152
+ value = value[1:-1]
153
+ return value
154
+ return ""
155
+
156
+
157
+ def _discover_commands(brain: Path, layer_rel: str, layer: str) -> list[CommandInfo]:
158
+ """Discover commands under a layer dir. Sorted by base name for determinism."""
159
+ layer_dir = brain / layer_rel
160
+ if not layer_dir.is_dir():
161
+ return []
162
+ found: list[CommandInfo] = []
163
+ for cmd_dir in sorted(layer_dir.iterdir(), key=lambda p: p.name):
164
+ if not cmd_dir.is_dir():
165
+ continue
166
+ skill = _skill_file(cmd_dir)
167
+ if skill is None:
168
+ continue
169
+ rel = skill.relative_to(brain).as_posix()
170
+ found.append(CommandInfo(
171
+ base=cmd_dir.name,
172
+ layer=layer,
173
+ description=_parse_frontmatter_description(skill),
174
+ skill_path=rel,
175
+ ))
176
+ return found
177
+
178
+
179
+ def build_model(brain: str | Path) -> CapabilitiesModel:
180
+ """Scan a brain directory and assemble the capabilities model from code."""
181
+ root = Path(brain).resolve()
182
+ if not root.is_dir():
183
+ raise NotADirectoryError(f"Brain directory not found: {root}")
184
+
185
+ manifest = _read_manifest(root)
186
+ project = manifest.get("project", {}) if isinstance(manifest, dict) else {}
187
+ project_name = project.get("name") or root.name
188
+ command_prefix = manifest.get("command_prefix") or "cmd"
189
+ framework_version = manifest.get("framework_version") or "unknown"
190
+ platforms = list(manifest.get("platforms") or [])
191
+
192
+ app_repos: list[AppRepo] = []
193
+ for entry in manifest.get("app_repos", []) or []:
194
+ if isinstance(entry, dict) and entry.get("name"):
195
+ app_repos.append(AppRepo(name=entry["name"], role=entry.get("role", "")))
196
+
197
+ return CapabilitiesModel(
198
+ project_name=project_name,
199
+ command_prefix=command_prefix,
200
+ framework_version=framework_version,
201
+ platforms=platforms,
202
+ core_commands=_discover_commands(root, _CORE_REL, "core"),
203
+ extension_commands=_discover_commands(root, _EXT_REL, "extensions"),
204
+ app_repos=app_repos,
205
+ )
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # Rendering
210
+ # ---------------------------------------------------------------------------
211
+
212
+ def _escape_cell(text: str) -> str:
213
+ """Make a string safe inside a markdown table cell."""
214
+ return text.replace("|", "\\|").replace("\n", " ").strip()
215
+
216
+
217
+ def _render_command_table(commands: list[CommandInfo], prefix: str) -> list[str]:
218
+ lines = ["| Command | Description |", "| --- | --- |"]
219
+ for cmd in commands:
220
+ desc = _escape_cell(cmd.description) or "_(no description)_"
221
+ lines.append(f"| `{cmd.invocation(prefix)}` | {desc} |")
222
+ return lines
223
+
224
+
225
+ def render_markdown(model: CapabilitiesModel, generated_on: str | None = None) -> str:
226
+ """Render a deterministic CAPABILITIES.md from the model.
227
+
228
+ The only date is the ``Last generated`` line; everything else is derived
229
+ purely from the scanned inputs so two runs over the same brain (on the same
230
+ day) are byte-identical.
231
+ """
232
+ stamp = generated_on or date.today().isoformat()
233
+ m = model
234
+ lines: list[str] = []
235
+ lines.append(f"<!-- {_BANNER}. Regenerate with `{m.command_prefix}-capabilities` "
236
+ "(brainfactory capabilities --write). -->")
237
+ lines.append("")
238
+ lines.append(f"# {m.project_name} — Capabilities map")
239
+ lines.append("")
240
+ lines.append(f"> {_BANNER}. Truth comes from code; this file is regenerated by "
241
+ f"`{m.command_prefix}-capabilities` and cannot drift. Hand edits are "
242
+ "overwritten.")
243
+ lines.append(">")
244
+ lines.append(f"> Last generated: {stamp}")
245
+ lines.append("")
246
+ lines.append(f"- Command prefix: `{m.command_prefix}`")
247
+ lines.append(f"- Framework version: `{m.framework_version}`")
248
+ plats = ", ".join(f"`{p}`" for p in m.platforms) if m.platforms else "_none declared_"
249
+ lines.append(f"- Platforms: {plats}")
250
+ lines.append(f"- Commands: {len(m.core_commands)} core, "
251
+ f"{len(m.extension_commands)} extension")
252
+ lines.append("")
253
+
254
+ lines.append("## Commands")
255
+ lines.append("")
256
+ lines.append("### Core (hub-owned)")
257
+ lines.append("")
258
+ if m.core_commands:
259
+ lines.extend(_render_command_table(m.core_commands, m.command_prefix))
260
+ else:
261
+ lines.append("_No core commands found._")
262
+ lines.append("")
263
+
264
+ lines.append("### Extensions (project-owned)")
265
+ lines.append("")
266
+ if m.extension_commands:
267
+ lines.extend(_render_command_table(m.extension_commands, m.command_prefix))
268
+ else:
269
+ lines.append("_No extension commands._")
270
+ lines.append("")
271
+
272
+ lines.append("## App repos")
273
+ lines.append("")
274
+ if m.app_repos:
275
+ lines.append("| Repo | Role |")
276
+ lines.append("| --- | --- |")
277
+ for repo in m.app_repos:
278
+ role = _escape_cell(repo.role) or "—"
279
+ lines.append(f"| `{_escape_cell(repo.name)}` | {role} |")
280
+ else:
281
+ lines.append("_No app repos declared in `brain.manifest.json`._")
282
+ lines.append("")
283
+ return "\n".join(lines) + "\n"
284
+
285
+
286
+ def generate(brain: str | Path, generated_on: str | None = None) -> str:
287
+ """Convenience: build the model and render the markdown."""
288
+ return render_markdown(build_model(brain), generated_on=generated_on)
289
+
290
+
291
+ # ---------------------------------------------------------------------------
292
+ # Check (anti-drift diff)
293
+ # ---------------------------------------------------------------------------
294
+
295
+ @dataclass
296
+ class CheckResult:
297
+ """Outcome of a ``--check`` (or intent-gate) run."""
298
+
299
+ brain: str
300
+ ok: bool
301
+ missing_file: bool = False
302
+ diff: str = ""
303
+ # Intent-gate findings: command dirs with no row in the committed map.
304
+ ungated: list[str] = field(default_factory=list)
305
+
306
+ def to_dict(self) -> dict[str, Any]:
307
+ return {
308
+ "brain": self.brain,
309
+ "ok": self.ok,
310
+ "missing_file": self.missing_file,
311
+ "diff": self.diff,
312
+ "ungated": self.ungated,
313
+ }
314
+
315
+
316
+ def _strip_generated_line(text: str) -> str:
317
+ """Drop the ``Last generated:`` line so a stale date alone is not a diff.
318
+
319
+ The capabilities command stamps the current date on write; we compare the
320
+ substance, not the timestamp. CI still re-writes on a real change.
321
+ """
322
+ out = []
323
+ for line in text.splitlines():
324
+ if line.strip().lower().startswith("> last generated:"):
325
+ continue
326
+ out.append(line)
327
+ return "\n".join(out)
328
+
329
+
330
+ def check(brain: str | Path) -> CheckResult:
331
+ """Regenerate in-memory and diff against the on-disk CAPABILITIES.md."""
332
+ root = Path(brain).resolve()
333
+ cap_path = root / _CAPABILITIES_REL
334
+ expected = generate(root)
335
+
336
+ if not cap_path.is_file():
337
+ return CheckResult(brain=str(root), ok=False, missing_file=True,
338
+ diff=f"{_CAPABILITIES_REL} does not exist")
339
+
340
+ on_disk = cap_path.read_text(encoding="utf-8")
341
+ exp_cmp = _strip_generated_line(expected)
342
+ disk_cmp = _strip_generated_line(on_disk)
343
+ if exp_cmp == disk_cmp:
344
+ return CheckResult(brain=str(root), ok=True)
345
+
346
+ diff = "\n".join(difflib.unified_diff(
347
+ disk_cmp.splitlines(),
348
+ exp_cmp.splitlines(),
349
+ fromfile=f"a/{_CAPABILITIES_REL} (on disk)",
350
+ tofile=f"b/{_CAPABILITIES_REL} (regenerated)",
351
+ lineterm="",
352
+ ))
353
+ return CheckResult(brain=str(root), ok=False, diff=diff)
354
+
355
+
356
+ def write(brain: str | Path) -> Path:
357
+ """Regenerate and write CAPABILITIES.md. Returns the path written."""
358
+ root = Path(brain).resolve()
359
+ cap_path = root / _CAPABILITIES_REL
360
+ cap_path.parent.mkdir(parents=True, exist_ok=True)
361
+ cap_path.write_text(generate(root), encoding="utf-8")
362
+ return cap_path
363
+
364
+
365
+ # ---------------------------------------------------------------------------
366
+ # Intent gate
367
+ # ---------------------------------------------------------------------------
368
+
369
+ def intent_gate(brain: str | Path) -> CheckResult:
370
+ """Fail if any command dir has no matching row in the committed map.
371
+
372
+ This enforces the self-extending-brain rule: a feature cannot land without
373
+ the brain growing with it. The gate reads the *committed* CAPABILITIES.md
374
+ (not a regeneration) and verifies that every discovered command's
375
+ invocation (``<prefix>-<base>``) appears in it. A missing map fails the gate.
376
+ """
377
+ root = Path(brain).resolve()
378
+ model = build_model(root)
379
+ cap_path = root / _CAPABILITIES_REL
380
+
381
+ if not cap_path.is_file():
382
+ all_cmds = [c.invocation(model.command_prefix)
383
+ for c in (*model.core_commands, *model.extension_commands)]
384
+ return CheckResult(brain=str(root), ok=False, missing_file=True,
385
+ ungated=all_cmds,
386
+ diff=f"{_CAPABILITIES_REL} does not exist; "
387
+ f"{len(all_cmds)} command(s) ungated")
388
+
389
+ committed = cap_path.read_text(encoding="utf-8")
390
+ ungated: list[str] = []
391
+ for cmd in (*model.core_commands, *model.extension_commands):
392
+ token = f"`{cmd.invocation(model.command_prefix)}`"
393
+ if token not in committed:
394
+ ungated.append(cmd.invocation(model.command_prefix))
395
+
396
+ if ungated:
397
+ return CheckResult(
398
+ brain=str(root), ok=False, ungated=ungated,
399
+ diff="Command directories without a row in "
400
+ f"{_CAPABILITIES_REL}: " + ", ".join(ungated)
401
+ + f"\nRun the capabilities generator (--write) to grow the brain.",
402
+ )
403
+ return CheckResult(brain=str(root), ok=True)