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.
- brainfactory/__init__.py +25 -0
- brainfactory/__main__.py +10 -0
- brainfactory/apply.py +397 -0
- brainfactory/capabilities.py +403 -0
- brainfactory/cli.py +304 -0
- brainfactory/docsmesh.py +359 -0
- brainfactory/emit.py +180 -0
- brainfactory/inspect.py +441 -0
- brainfactory/manifest.py +251 -0
- brainfactory/mcpserver.py +236 -0
- brainfactory/substitute.py +126 -0
- brainfactory/upgrade.py +281 -0
- brainfactory-0.1.0.dist-info/METADATA +63 -0
- brainfactory-0.1.0.dist-info/RECORD +17 -0
- brainfactory-0.1.0.dist-info/WHEEL +5 -0
- brainfactory-0.1.0.dist-info/entry_points.txt +2 -0
- brainfactory-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|