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.
Files changed (31) hide show
  1. {brainfactory-0.1.0 → brainfactory-0.2.0}/PKG-INFO +2 -2
  2. {brainfactory-0.1.0 → brainfactory-0.2.0}/README.md +1 -1
  3. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/__init__.py +1 -1
  4. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/apply.py +2 -0
  5. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/capabilities.py +89 -2
  6. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/cli.py +19 -7
  7. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/inspect.py +74 -0
  8. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory.egg-info/PKG-INFO +2 -2
  9. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory.egg-info/SOURCES.txt +2 -0
  10. {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_apply.py +9 -0
  11. brainfactory-0.2.0/tests/test_capabilities.py +165 -0
  12. brainfactory-0.2.0/tests/test_operating_standards.py +40 -0
  13. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/__main__.py +0 -0
  14. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/docsmesh.py +0 -0
  15. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/emit.py +0 -0
  16. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/manifest.py +0 -0
  17. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/mcpserver.py +0 -0
  18. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/substitute.py +0 -0
  19. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory/upgrade.py +0 -0
  20. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory.egg-info/dependency_links.txt +0 -0
  21. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory.egg-info/entry_points.txt +0 -0
  22. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory.egg-info/requires.txt +0 -0
  23. {brainfactory-0.1.0 → brainfactory-0.2.0}/brainfactory.egg-info/top_level.txt +0 -0
  24. {brainfactory-0.1.0 → brainfactory-0.2.0}/pyproject.toml +0 -0
  25. {brainfactory-0.1.0 → brainfactory-0.2.0}/setup.cfg +0 -0
  26. {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_adapters.py +0 -0
  27. {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_emit.py +0 -0
  28. {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_manifest.py +0 -0
  29. {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_mcpserver.py +0 -0
  30. {brainfactory-0.1.0 → brainfactory-0.2.0}/tests/test_substitute.py +0 -0
  31. {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.1.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
- Until the package is published to PyPI, install straight from the repo:
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
- Until the package is published to PyPI, install straight from the repo:
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"
@@ -22,4 +22,4 @@ __all__ = [
22
22
  "apply",
23
23
  ]
24
24
 
25
- __version__ = "0.1.0"
25
+ __version__ = "0.2.0"
@@ -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=_discover_commands(root, _CORE_REL, "core"),
203
- extension_commands=_discover_commands(root, _EXT_REL, "extensions"),
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
- if args.write:
108
- path = capabilities_mod.write(brain)
109
- print(f"Wrote capabilities map: {path}")
110
- return 0
111
- # Default (and --check): regenerate in-memory and diff.
112
- result = capabilities_mod.check(brain)
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
- result = capabilities_mod.intent_gate(brain)
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.1.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
- Until the package is published to PyPI, install straight from the repo:
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