brainfactory 0.1.0__tar.gz → 0.2.0__tar.gz
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-0.1.0 → brainfactory-0.2.0}/PKG-INFO +2 -2
- {brainfactory-0.1.0 → brainfactory-0.2.0}/README.md +1 -1
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/__init__.py +1 -1
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/apply.py +2 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/capabilities.py +89 -2
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/cli.py +19 -7
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/inspect.py +74 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory.egg-info/PKG-INFO +2 -2
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory.egg-info/SOURCES.txt +2 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_apply.py +9 -0
- brainfactory-0.2.0/tests/test_capabilities.py +165 -0
- brainfactory-0.2.0/tests/test_operating_standards.py +40 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/__main__.py +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/docsmesh.py +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/emit.py +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/manifest.py +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/mcpserver.py +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/substitute.py +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/upgrade.py +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory.egg-info/dependency_links.txt +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory.egg-info/entry_points.txt +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory.egg-info/requires.txt +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory.egg-info/top_level.txt +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/pyproject.toml +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/setup.cfg +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_adapters.py +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_emit.py +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_manifest.py +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_mcpserver.py +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_substitute.py +0 -0
- {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_upgrade.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brainfactory
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: The executable onboarding engine for Brain Factory — provision, adopt, and upgrade project 'brain' repositories.
|
|
5
5
|
Author: Izak Laubscher
|
|
6
6
|
License: MIT
|
|
@@ -40,7 +40,7 @@ pip install brainfactory
|
|
|
40
40
|
pipx install "brainfactory[schema]"
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
To install the latest unreleased build straight from the repository:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
pipx install "git+https://github.com/izakl/brainforge#subdirectory=brain-factory/adapters/python"
|
|
@@ -18,7 +18,7 @@ pip install brainfactory
|
|
|
18
18
|
pipx install "brainfactory[schema]"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
To install the latest unreleased build straight from the repository:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
pipx install "git+https://github.com/izakl/brainforge#subdirectory=brain-factory/adapters/python"
|
|
@@ -34,6 +34,8 @@ MODULE_TEMPLATE_PATHS: dict[str, tuple[str, ...]] = {
|
|
|
34
34
|
"operating-contract": (
|
|
35
35
|
"00-governance/OPERATING-CONTRACT.md.tmpl",
|
|
36
36
|
"AGENTS.md.tmpl",
|
|
37
|
+
"CLAUDE.md.tmpl",
|
|
38
|
+
".github/copilot-instructions.md.tmpl",
|
|
37
39
|
),
|
|
38
40
|
"session-ritual-hooks": ("03-templates/agent-commands/hooks",),
|
|
39
41
|
"continuity-log": (
|
|
@@ -17,6 +17,12 @@ Three modes:
|
|
|
17
17
|
no matching row in the committed ``CAPABILITIES.md`` — "a feature cannot land
|
|
18
18
|
without the brain growing with it".
|
|
19
19
|
|
|
20
|
+
Command discovery is guarded: :func:`build_model` raises
|
|
21
|
+
:class:`CommandNamingError` if any command directory's base name already starts
|
|
22
|
+
with ``<prefix>-`` (the ``ap-ap-*`` double-prefix bug), so ``--write``,
|
|
23
|
+
``--check`` and the intent gate all fail rather than silently emitting a doubled
|
|
24
|
+
invocation. :func:`find_prefixed_command_dirs` is the read-only companion.
|
|
25
|
+
|
|
20
26
|
Standard library only. Typed. Same code style as ``inspect.py``.
|
|
21
27
|
"""
|
|
22
28
|
|
|
@@ -63,6 +69,45 @@ class CommandInfo:
|
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
|
|
72
|
+
class CommandNamingError(ValueError):
|
|
73
|
+
"""A command directory base name redundantly starts with the command prefix.
|
|
74
|
+
|
|
75
|
+
The generator derives a command's invocation as ``<prefix>-<base>`` (see
|
|
76
|
+
:meth:`CommandInfo.invocation`). If the directory base *already* starts with
|
|
77
|
+
``<prefix>-`` the invocation doubles the prefix — e.g. prefix ``ap`` and a
|
|
78
|
+
directory ``ap-foo`` yield ``ap-ap-foo``. Command directories must use BARE
|
|
79
|
+
names; this error stops such a map from ever being generated (it fails
|
|
80
|
+
``--check``, ``--write`` and the intent gate) so the bug can never silently
|
|
81
|
+
ship.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, offenders: list[CommandInfo], prefix: str) -> None:
|
|
85
|
+
self.offenders = list(offenders)
|
|
86
|
+
self.prefix = prefix
|
|
87
|
+
super().__init__(self._render(self.offenders, prefix))
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _render(offenders: list[CommandInfo], prefix: str) -> str:
|
|
91
|
+
n = len(offenders)
|
|
92
|
+
noun = "directory" if n == 1 else "directories"
|
|
93
|
+
verb = "is" if n == 1 else "are"
|
|
94
|
+
lines = [
|
|
95
|
+
f"{n} command {noun} {verb} mis-named with a redundant "
|
|
96
|
+
f"'{prefix}-' prefix.",
|
|
97
|
+
"The generator invokes a command as '<prefix>-<base>', so a "
|
|
98
|
+
f"directory whose name already starts with '{prefix}-' doubles the "
|
|
99
|
+
f"prefix (e.g. '{prefix}-{prefix}-...').",
|
|
100
|
+
"Rename each directory to a BARE name:",
|
|
101
|
+
]
|
|
102
|
+
for c in sorted(offenders, key=lambda o: (o.layer, o.base)):
|
|
103
|
+
bare = c.base[len(prefix) + 1:] or "<name>"
|
|
104
|
+
lines.append(
|
|
105
|
+
f" - {c.layer}/{c.base} -> {c.layer}/{bare} "
|
|
106
|
+
f"(now invokes '{prefix}-{c.base}'; should be '{prefix}-{bare}')"
|
|
107
|
+
)
|
|
108
|
+
return "\n".join(lines)
|
|
109
|
+
|
|
110
|
+
|
|
66
111
|
@dataclass
|
|
67
112
|
class AppRepo:
|
|
68
113
|
"""One coordinated app repo from the manifest."""
|
|
@@ -176,6 +221,40 @@ def _discover_commands(brain: Path, layer_rel: str, layer: str) -> list[CommandI
|
|
|
176
221
|
return found
|
|
177
222
|
|
|
178
223
|
|
|
224
|
+
def _prefixed_offenders(
|
|
225
|
+
commands: list[CommandInfo], prefix: str,
|
|
226
|
+
) -> list[CommandInfo]:
|
|
227
|
+
"""Command dirs whose base name already starts with ``<prefix>-``.
|
|
228
|
+
|
|
229
|
+
These are the double-prefix bug: the generator would render their invocation
|
|
230
|
+
as ``<prefix>-<prefix>-...``. An empty prefix disables the check.
|
|
231
|
+
"""
|
|
232
|
+
if not prefix:
|
|
233
|
+
return []
|
|
234
|
+
token = f"{prefix}-"
|
|
235
|
+
return [c for c in commands if c.base.startswith(token)]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def find_prefixed_command_dirs(
|
|
239
|
+
brain: str | Path, prefix: str | None = None,
|
|
240
|
+
) -> list[CommandInfo]:
|
|
241
|
+
"""Return command dirs whose base redundantly starts with ``<prefix>-``.
|
|
242
|
+
|
|
243
|
+
Read-only companion to the guard baked into :func:`build_model`: it never
|
|
244
|
+
raises, so callers (e.g. the hub self-check) can report *all* offenders at
|
|
245
|
+
once. ``prefix`` defaults to the brain manifest's ``command_prefix`` (or
|
|
246
|
+
``"cmd"`` when unset), matching :func:`build_model`.
|
|
247
|
+
"""
|
|
248
|
+
root = Path(brain).resolve()
|
|
249
|
+
if prefix is None:
|
|
250
|
+
prefix = _read_manifest(root).get("command_prefix") or "cmd"
|
|
251
|
+
commands = [
|
|
252
|
+
*_discover_commands(root, _CORE_REL, "core"),
|
|
253
|
+
*_discover_commands(root, _EXT_REL, "extensions"),
|
|
254
|
+
]
|
|
255
|
+
return _prefixed_offenders(commands, prefix)
|
|
256
|
+
|
|
257
|
+
|
|
179
258
|
def build_model(brain: str | Path) -> CapabilitiesModel:
|
|
180
259
|
"""Scan a brain directory and assemble the capabilities model from code."""
|
|
181
260
|
root = Path(brain).resolve()
|
|
@@ -194,13 +273,21 @@ def build_model(brain: str | Path) -> CapabilitiesModel:
|
|
|
194
273
|
if isinstance(entry, dict) and entry.get("name"):
|
|
195
274
|
app_repos.append(AppRepo(name=entry["name"], role=entry.get("role", "")))
|
|
196
275
|
|
|
276
|
+
core_commands = _discover_commands(root, _CORE_REL, "core")
|
|
277
|
+
extension_commands = _discover_commands(root, _EXT_REL, "extensions")
|
|
278
|
+
|
|
279
|
+
offenders = _prefixed_offenders(
|
|
280
|
+
[*core_commands, *extension_commands], command_prefix)
|
|
281
|
+
if offenders:
|
|
282
|
+
raise CommandNamingError(offenders, command_prefix)
|
|
283
|
+
|
|
197
284
|
return CapabilitiesModel(
|
|
198
285
|
project_name=project_name,
|
|
199
286
|
command_prefix=command_prefix,
|
|
200
287
|
framework_version=framework_version,
|
|
201
288
|
platforms=platforms,
|
|
202
|
-
core_commands=
|
|
203
|
-
extension_commands=
|
|
289
|
+
core_commands=core_commands,
|
|
290
|
+
extension_commands=extension_commands,
|
|
204
291
|
app_repos=app_repos,
|
|
205
292
|
)
|
|
206
293
|
|
|
@@ -104,12 +104,18 @@ def cmd_adopt(args: argparse.Namespace) -> int:
|
|
|
104
104
|
|
|
105
105
|
def cmd_capabilities(args: argparse.Namespace) -> int:
|
|
106
106
|
brain = args.brain or "."
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
try:
|
|
108
|
+
if args.write:
|
|
109
|
+
path = capabilities_mod.write(brain)
|
|
110
|
+
print(f"Wrote capabilities map: {path}")
|
|
111
|
+
return 0
|
|
112
|
+
# Default (and --check): regenerate in-memory and diff.
|
|
113
|
+
result = capabilities_mod.check(brain)
|
|
114
|
+
except capabilities_mod.CommandNamingError as exc:
|
|
115
|
+
print("capabilities: FAIL — command directory naming error (refusing to "
|
|
116
|
+
"emit a doubled invocation).\n")
|
|
117
|
+
print(exc)
|
|
118
|
+
return 1
|
|
113
119
|
if args.json:
|
|
114
120
|
Path(args.json).write_text(
|
|
115
121
|
json.dumps(result.to_dict(), indent=2) + "\n", encoding="utf-8")
|
|
@@ -140,7 +146,13 @@ def cmd_docs_mesh(args: argparse.Namespace) -> int:
|
|
|
140
146
|
|
|
141
147
|
def cmd_intent_gate(args: argparse.Namespace) -> int:
|
|
142
148
|
brain = args.brain or "."
|
|
143
|
-
|
|
149
|
+
try:
|
|
150
|
+
result = capabilities_mod.intent_gate(brain)
|
|
151
|
+
except capabilities_mod.CommandNamingError as exc:
|
|
152
|
+
print("intent-gate: FAIL — command directory naming error (refusing to "
|
|
153
|
+
"emit a doubled invocation).\n")
|
|
154
|
+
print(exc)
|
|
155
|
+
return 1
|
|
144
156
|
if args.json:
|
|
145
157
|
Path(args.json).write_text(
|
|
146
158
|
json.dumps(result.to_dict(), indent=2) + "\n", encoding="utf-8")
|
|
@@ -44,6 +44,21 @@ _EXTERNAL_SOT_SIGNALS = (
|
|
|
44
44
|
"source of truth",
|
|
45
45
|
)
|
|
46
46
|
|
|
47
|
+
# Runtime/docs drift signals (inspect-first). The first codified check targets
|
|
48
|
+
# PostgreSQL version mismatch between runtime config and docs.
|
|
49
|
+
_PG_DOC_RE = re.compile(r"\bpostgres(?:ql)?(?:\s+version)?\s*(\d{1,2})\b",
|
|
50
|
+
re.IGNORECASE)
|
|
51
|
+
_PG_IMAGE_RE = re.compile(r"\bpostgres:(\d{1,2})(?:[.-][^\s\"']*)?\b",
|
|
52
|
+
re.IGNORECASE)
|
|
53
|
+
_RUNTIME_CONFIG_GLOBS = (
|
|
54
|
+
"docker-compose.yml",
|
|
55
|
+
"docker-compose.yaml",
|
|
56
|
+
"compose.yml",
|
|
57
|
+
"compose.yaml",
|
|
58
|
+
"docker/**/*.yml",
|
|
59
|
+
"docker/**/*.yaml",
|
|
60
|
+
)
|
|
61
|
+
|
|
47
62
|
|
|
48
63
|
@dataclass
|
|
49
64
|
class ModuleFinding:
|
|
@@ -247,6 +262,12 @@ def inspect_repo(repo_path: str | Path) -> InspectionResult:
|
|
|
247
262
|
f_cont.action = "augment"
|
|
248
263
|
modules.append(f_cont)
|
|
249
264
|
|
|
265
|
+
# Runtime/docs drift risk scan (currently PostgreSQL-focused).
|
|
266
|
+
drift_hits = _scan_runtime_docs_drift(files, root)
|
|
267
|
+
if drift_hits:
|
|
268
|
+
for hit in drift_hits:
|
|
269
|
+
risks.append(hit)
|
|
270
|
+
|
|
250
271
|
# --- capabilities-map -----------------------------------------------------
|
|
251
272
|
# CAPABILITIES.md or an inventory doc.
|
|
252
273
|
cap = _collect(files, root, name_globs=("CAPABILITIES.md", "CAPABILITIES*.md",
|
|
@@ -353,6 +374,59 @@ def _scan_external_sot(files: list[Path], root: Path) -> list[dict[str, str]]:
|
|
|
353
374
|
return hits
|
|
354
375
|
|
|
355
376
|
|
|
377
|
+
def _scan_runtime_docs_drift(files: list[Path], root: Path) -> list[dict[str, str]]:
|
|
378
|
+
"""Detect runtime-vs-doc version drift discovered during inspect-first audits."""
|
|
379
|
+
runtime_versions: dict[str, str] = {}
|
|
380
|
+
docs_versions: dict[str, str] = {}
|
|
381
|
+
|
|
382
|
+
for f in files:
|
|
383
|
+
rel = _rel(f, root).replace(os.sep, "/")
|
|
384
|
+
lower = rel.lower()
|
|
385
|
+
try:
|
|
386
|
+
text = f.read_text(encoding="utf-8", errors="ignore")
|
|
387
|
+
except OSError:
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
is_runtime_config = any(
|
|
391
|
+
_match_any(lower, f.name.lower(), (pat,)) for pat in _RUNTIME_CONFIG_GLOBS
|
|
392
|
+
)
|
|
393
|
+
if is_runtime_config:
|
|
394
|
+
for m in _PG_IMAGE_RE.finditer(text):
|
|
395
|
+
runtime_versions[m.group(1)] = rel
|
|
396
|
+
|
|
397
|
+
if f.suffix.lower() in (".md", ".txt", ".rst"):
|
|
398
|
+
for m in _PG_DOC_RE.finditer(text):
|
|
399
|
+
docs_versions[m.group(1)] = rel
|
|
400
|
+
|
|
401
|
+
if not runtime_versions or not docs_versions:
|
|
402
|
+
return []
|
|
403
|
+
|
|
404
|
+
runtime_set = set(runtime_versions.keys())
|
|
405
|
+
docs_set = set(docs_versions.keys())
|
|
406
|
+
if runtime_set == docs_set:
|
|
407
|
+
return []
|
|
408
|
+
|
|
409
|
+
runtime_text = ", ".join(sorted(runtime_set))
|
|
410
|
+
docs_text = ", ".join(sorted(docs_set))
|
|
411
|
+
evidence = (
|
|
412
|
+
f"runtime config: {', '.join(sorted(set(runtime_versions.values()))[:4])}; "
|
|
413
|
+
f"docs: {', '.join(sorted(set(docs_versions.values()))[:4])}"
|
|
414
|
+
)
|
|
415
|
+
detail = (
|
|
416
|
+
"Detected runtime/docs PostgreSQL version drift: "
|
|
417
|
+
f"runtime config indicates {runtime_text}, docs indicate {docs_text}."
|
|
418
|
+
)
|
|
419
|
+
return [{
|
|
420
|
+
"kind": "runtime-config-drift",
|
|
421
|
+
"severity": "high",
|
|
422
|
+
"evidence": evidence,
|
|
423
|
+
"detail": detail,
|
|
424
|
+
"recommended_action": (
|
|
425
|
+
"Reconcile docs and runtime config to one approved version before apply."
|
|
426
|
+
),
|
|
427
|
+
}]
|
|
428
|
+
|
|
429
|
+
|
|
356
430
|
def _detect_platforms(files: list[Path], root: Path) -> dict[str, Any]:
|
|
357
431
|
"""Capture platform signals: *.ps1 vs *.sh, workflows."""
|
|
358
432
|
ps1 = _collect(files, root, name_globs=("*.ps1",))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brainfactory
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: The executable onboarding engine for Brain Factory — provision, adopt, and upgrade project 'brain' repositories.
|
|
5
5
|
Author: Izak Laubscher
|
|
6
6
|
License: MIT
|
|
@@ -40,7 +40,7 @@ pip install brainfactory
|
|
|
40
40
|
pipx install "brainfactory[schema]"
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
To install the latest unreleased build straight from the repository:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
pipx install "git+https://github.com/izakl/brainforge#subdirectory=brain-factory/adapters/python"
|
|
@@ -20,8 +20,10 @@ brainfactory.egg-info/requires.txt
|
|
|
20
20
|
brainfactory.egg-info/top_level.txt
|
|
21
21
|
tests/test_adapters.py
|
|
22
22
|
tests/test_apply.py
|
|
23
|
+
tests/test_capabilities.py
|
|
23
24
|
tests/test_emit.py
|
|
24
25
|
tests/test_manifest.py
|
|
25
26
|
tests/test_mcpserver.py
|
|
27
|
+
tests/test_operating_standards.py
|
|
26
28
|
tests/test_substitute.py
|
|
27
29
|
tests/test_upgrade.py
|
|
@@ -21,6 +21,15 @@ class ProvisionTest(unittest.TestCase):
|
|
|
21
21
|
agents = (dest / "AGENTS.md").read_text(encoding="utf-8")
|
|
22
22
|
self.assertIn("Acme brain", agents)
|
|
23
23
|
self.assertNotIn("{{", agents)
|
|
24
|
+
claude = (dest / "CLAUDE.md").read_text(encoding="utf-8")
|
|
25
|
+
copilot = (dest / ".github" / "copilot-instructions.md").read_text(
|
|
26
|
+
encoding="utf-8")
|
|
27
|
+
self.assertIn("SYNC-LATEST-FIRST STANDARD", agents)
|
|
28
|
+
self.assertIn("CLEANUP-NO-STALE-STATE STANDARD", agents)
|
|
29
|
+
self.assertIn("SYNC-LATEST-FIRST STANDARD", claude)
|
|
30
|
+
self.assertIn("CLEANUP-NO-STALE-STATE STANDARD", claude)
|
|
31
|
+
self.assertIn("SYNC-LATEST-FIRST STANDARD", copilot)
|
|
32
|
+
self.assertIn("CLEANUP-NO-STALE-STATE STANDARD", copilot)
|
|
24
33
|
|
|
25
34
|
def test_provision_refuses_nonempty_dest(self):
|
|
26
35
|
with tempfile.TemporaryDirectory() as d:
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Tests for the capabilities generator's command-dir naming guard.
|
|
2
|
+
|
|
3
|
+
The generator derives a command's invocation as ``<prefix>-<base>``. A command
|
|
4
|
+
directory whose base already starts with ``<prefix>-`` doubles the prefix (the
|
|
5
|
+
``ap-ap-*`` bug that bit the Contoso brain). :func:`build_model` must refuse such
|
|
6
|
+
a brain so ``--check``, ``--write`` and the intent gate all fail rather than
|
|
7
|
+
silently emitting a doubled invocation. These tests prove the guard catches the
|
|
8
|
+
offense, stays silent on correctly bare directories, and that the hub's own
|
|
9
|
+
shipped ``brain-template`` command dirs are clean.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
import unittest
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from brainfactory import capabilities as cap
|
|
23
|
+
|
|
24
|
+
ADAPTERS = Path(__file__).resolve().parents[2] # .../brain-factory/adapters
|
|
25
|
+
PYDIR = ADAPTERS / "python"
|
|
26
|
+
BRAIN_TEMPLATE = ADAPTERS.parent / "brain-template" # .../brain-factory/brain-template
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _make_brain(root: Path, prefix: str,
|
|
30
|
+
dirs: list[tuple[str, str]], *, manifest: bool = True) -> Path:
|
|
31
|
+
"""Build a minimal brain: a manifest plus one SKILL.md per command dir."""
|
|
32
|
+
brain = root / "brain"
|
|
33
|
+
brain.mkdir()
|
|
34
|
+
if manifest:
|
|
35
|
+
(brain / "brain.manifest.json").write_text(
|
|
36
|
+
json.dumps({"command_prefix": prefix, "project": {"name": "T"}}),
|
|
37
|
+
encoding="utf-8")
|
|
38
|
+
for layer, name in dirs:
|
|
39
|
+
cmd = brain / "03-templates" / "agent-commands" / layer / name
|
|
40
|
+
cmd.mkdir(parents=True)
|
|
41
|
+
(cmd / "SKILL.md").write_text(
|
|
42
|
+
"---\ndescription: demo\n---\nbody\n", encoding="utf-8")
|
|
43
|
+
return brain
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_cli(args: list[str], cwd: str) -> subprocess.CompletedProcess:
|
|
47
|
+
env = dict(os.environ)
|
|
48
|
+
prev = env.get("PYTHONPATH")
|
|
49
|
+
env["PYTHONPATH"] = str(PYDIR) + (os.pathsep + prev if prev else "")
|
|
50
|
+
return subprocess.run(
|
|
51
|
+
[sys.executable, "-m", "brainfactory", *args],
|
|
52
|
+
capture_output=True, text=True, env=env, cwd=cwd)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class NamingGuardModelTest(unittest.TestCase):
|
|
56
|
+
def test_build_model_raises_on_prefixed_core_dir(self):
|
|
57
|
+
with tempfile.TemporaryDirectory() as d:
|
|
58
|
+
brain = _make_brain(Path(d), "ap",
|
|
59
|
+
[("core", "ap-foo"), ("core", "bar")])
|
|
60
|
+
with self.assertRaises(cap.CommandNamingError) as ctx:
|
|
61
|
+
cap.build_model(brain)
|
|
62
|
+
msg = str(ctx.exception)
|
|
63
|
+
self.assertIn("ap-foo", msg)
|
|
64
|
+
self.assertIn("ap-ap-foo", msg) # names the doubled invocation
|
|
65
|
+
self.assertIn("core/foo", msg) # actionable bare rename
|
|
66
|
+
self.assertEqual([c.base for c in ctx.exception.offenders], ["ap-foo"])
|
|
67
|
+
self.assertEqual(ctx.exception.prefix, "ap")
|
|
68
|
+
|
|
69
|
+
def test_build_model_raises_on_prefixed_extension_dir(self):
|
|
70
|
+
with tempfile.TemporaryDirectory() as d:
|
|
71
|
+
brain = _make_brain(Path(d), "ap", [("extensions", "ap-deploy")])
|
|
72
|
+
with self.assertRaises(cap.CommandNamingError):
|
|
73
|
+
cap.build_model(brain)
|
|
74
|
+
|
|
75
|
+
def test_build_model_passes_on_bare_dirs(self):
|
|
76
|
+
with tempfile.TemporaryDirectory() as d:
|
|
77
|
+
brain = _make_brain(Path(d), "ap",
|
|
78
|
+
[("core", "foo"), ("extensions", "bar")])
|
|
79
|
+
model = cap.build_model(brain)
|
|
80
|
+
invs = [c.invocation("ap")
|
|
81
|
+
for c in model.core_commands + model.extension_commands]
|
|
82
|
+
self.assertEqual(sorted(invs), ["ap-bar", "ap-foo"])
|
|
83
|
+
|
|
84
|
+
def test_substring_prefix_is_not_flagged(self):
|
|
85
|
+
# Only a leading "<prefix>-" is the bug; a base that merely starts with
|
|
86
|
+
# the prefix letters (or contains them) must be allowed.
|
|
87
|
+
with tempfile.TemporaryDirectory() as d:
|
|
88
|
+
brain = _make_brain(Path(d), "ap",
|
|
89
|
+
[("core", "apply"), ("core", "map-ap")])
|
|
90
|
+
model = cap.build_model(brain) # must NOT raise
|
|
91
|
+
self.assertEqual(len(model.core_commands), 2)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class NamingGuardCheckWriteTest(unittest.TestCase):
|
|
95
|
+
def test_check_raises_and_write_refuses_to_emit(self):
|
|
96
|
+
with tempfile.TemporaryDirectory() as d:
|
|
97
|
+
brain = _make_brain(Path(d), "ap", [("core", "ap-foo")])
|
|
98
|
+
with self.assertRaises(cap.CommandNamingError):
|
|
99
|
+
cap.check(brain)
|
|
100
|
+
with self.assertRaises(cap.CommandNamingError):
|
|
101
|
+
cap.write(brain)
|
|
102
|
+
# write must NOT have produced the (doubled) map.
|
|
103
|
+
self.assertFalse((brain / "01-docs" / "CAPABILITIES.md").exists())
|
|
104
|
+
|
|
105
|
+
def test_intent_gate_raises_on_prefixed_dir(self):
|
|
106
|
+
with tempfile.TemporaryDirectory() as d:
|
|
107
|
+
brain = _make_brain(Path(d), "ap", [("core", "ap-foo")])
|
|
108
|
+
with self.assertRaises(cap.CommandNamingError):
|
|
109
|
+
cap.intent_gate(brain)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class FindPrefixedCommandDirsTest(unittest.TestCase):
|
|
113
|
+
def test_reports_all_offenders_without_raising(self):
|
|
114
|
+
with tempfile.TemporaryDirectory() as d:
|
|
115
|
+
brain = _make_brain(Path(d), "ap",
|
|
116
|
+
[("core", "ap-foo"), ("extensions", "ap-bar"),
|
|
117
|
+
("core", "ok")])
|
|
118
|
+
offenders = cap.find_prefixed_command_dirs(brain) # prefix from manifest
|
|
119
|
+
self.assertEqual(sorted(c.base for c in offenders),
|
|
120
|
+
["ap-bar", "ap-foo"])
|
|
121
|
+
|
|
122
|
+
def test_explicit_prefix_overrides_manifest(self):
|
|
123
|
+
with tempfile.TemporaryDirectory() as d:
|
|
124
|
+
brain = _make_brain(Path(d), "ap", [("core", "zz-foo")])
|
|
125
|
+
self.assertEqual(cap.find_prefixed_command_dirs(brain), [])
|
|
126
|
+
hits = cap.find_prefixed_command_dirs(brain, prefix="zz")
|
|
127
|
+
self.assertEqual([c.base for c in hits], ["zz-foo"])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class HubTemplateCleanTest(unittest.TestCase):
|
|
131
|
+
def test_brain_template_command_dirs_are_bare(self):
|
|
132
|
+
# Hub self-guard: the shipped brain-template command dirs must be bare
|
|
133
|
+
# against the example manifest's command_prefix (same check CI runs).
|
|
134
|
+
example = BRAIN_TEMPLATE / "brain.manifest.example.json"
|
|
135
|
+
prefix = json.loads(example.read_text(encoding="utf-8")).get("command_prefix")
|
|
136
|
+
self.assertTrue(prefix, "example manifest is missing command_prefix")
|
|
137
|
+
offenders = cap.find_prefixed_command_dirs(BRAIN_TEMPLATE, prefix)
|
|
138
|
+
self.assertEqual(
|
|
139
|
+
offenders, [],
|
|
140
|
+
"brain-template command dirs must be bare, found: "
|
|
141
|
+
+ ", ".join(f"{c.layer}/{c.base}" for c in offenders))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class NamingGuardCliTest(unittest.TestCase):
|
|
145
|
+
def test_cli_check_exits_1_with_clean_message(self):
|
|
146
|
+
with tempfile.TemporaryDirectory() as d:
|
|
147
|
+
brain = _make_brain(Path(d), "ap", [("core", "ap-foo")])
|
|
148
|
+
r = _run_cli(["capabilities", "--brain", str(brain), "--check"], cwd=d)
|
|
149
|
+
self.assertEqual(r.returncode, 1, r.stderr)
|
|
150
|
+
self.assertIn("naming error", r.stdout.lower())
|
|
151
|
+
self.assertIn("ap-ap-foo", r.stdout)
|
|
152
|
+
self.assertNotIn("Traceback", r.stderr) # clean failure, not a crash
|
|
153
|
+
|
|
154
|
+
def test_cli_write_then_check_exit_0_on_bare(self):
|
|
155
|
+
with tempfile.TemporaryDirectory() as d:
|
|
156
|
+
brain = _make_brain(Path(d), "ap", [("core", "foo")])
|
|
157
|
+
w = _run_cli(["capabilities", "--brain", str(brain), "--write"], cwd=d)
|
|
158
|
+
self.assertEqual(w.returncode, 0, w.stderr)
|
|
159
|
+
self.assertTrue((brain / "01-docs" / "CAPABILITIES.md").is_file())
|
|
160
|
+
r = _run_cli(["capabilities", "--brain", str(brain), "--check"], cwd=d)
|
|
161
|
+
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
unittest.main()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Regression checks for permanent operating standards across toolchains."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import unittest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
ANCHORS = (
|
|
9
|
+
"SYNC-LATEST-FIRST STANDARD",
|
|
10
|
+
"CLEANUP-NO-STALE-STATE STANDARD",
|
|
11
|
+
"CONTINUITY-CAPTURE / BRAIN-MEMORY WRITEBACK STANDARD",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
REQUIRED_FILES = (
|
|
15
|
+
"AGENTS.md",
|
|
16
|
+
".github/copilot-instructions.md",
|
|
17
|
+
"CLAUDE.md",
|
|
18
|
+
"brain-factory/brain-template/AGENTS.md.tmpl",
|
|
19
|
+
"brain-factory/brain-template/.github/copilot-instructions.md.tmpl",
|
|
20
|
+
"brain-factory/brain-template/CLAUDE.md.tmpl",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OperatingStandardsPresenceTest(unittest.TestCase):
|
|
25
|
+
def test_anchors_exist_in_all_required_files(self):
|
|
26
|
+
repo_root = Path(__file__).resolve().parents[4]
|
|
27
|
+
for rel in REQUIRED_FILES:
|
|
28
|
+
path = repo_root / rel
|
|
29
|
+
self.assertTrue(path.is_file(), f"missing required file: {rel}")
|
|
30
|
+
content = path.read_text(encoding="utf-8")
|
|
31
|
+
for anchor in ANCHORS:
|
|
32
|
+
self.assertIn(
|
|
33
|
+
anchor,
|
|
34
|
+
content,
|
|
35
|
+
f"{rel} missing required operating standard anchor: {anchor}",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|