bazis-cli 2.5.0__tar.gz → 2.6.1__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 (39) hide show
  1. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/PKG-INFO +5 -4
  2. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/README.md +3 -2
  3. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis/contrib/cli/_version.py +3 -3
  4. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/PKG-INFO +5 -4
  5. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/SOURCES.txt +2 -0
  6. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/requires.txt +1 -1
  7. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/scm_file_list.json +2 -0
  8. bazis_cli-2.6.1/bazis_cli.egg-info/scm_version.json +8 -0
  9. bazis_cli-2.6.1/docs/releases/2.6.0.md +15 -0
  10. bazis_cli-2.6.1/docs/releases/2.6.1.md +9 -0
  11. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/evals/grade.py +127 -26
  12. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/pyproject.toml +1 -1
  13. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/tests/test_cli.py +34 -5
  14. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/tests/test_evals.py +161 -5
  15. bazis_cli-2.5.0/bazis_cli.egg-info/scm_version.json +0 -8
  16. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/.dockerignore +0 -0
  17. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/.github/workflows/build-publish.yml +0 -0
  18. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/.github/workflows/release.yml +0 -0
  19. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/.github/workflows/tests.yml +0 -0
  20. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/.github/workflows/update-licenses.yml +0 -0
  21. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/.gitignore +0 -0
  22. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/CLAUDE.md +0 -0
  23. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis/contrib/cli/AGENTS.md +0 -0
  24. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis/contrib/cli/__init__.py +0 -0
  25. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis/contrib/cli/agent.py +0 -0
  26. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis/contrib/cli/bazis_manifest.toml +0 -0
  27. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis/contrib/cli/main.py +0 -0
  28. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/dependency_links.txt +0 -0
  29. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/entry_points.txt +0 -0
  30. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/bazis_cli.egg-info/top_level.txt +0 -0
  31. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/docs/releases/2.4.0.md +0 -0
  32. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/docs/releases/2.4.1.md +0 -0
  33. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/docs/releases/2.5.0.md +0 -0
  34. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/evals/__init__.py +0 -0
  35. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/evals/run.py +0 -0
  36. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/evals/tasks.toml +0 -0
  37. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/setup.cfg +0 -0
  38. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/tests/__init__.py +0 -0
  39. {bazis_cli-2.5.0 → bazis_cli-2.6.1}/tests/conftest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazis-cli
3
- Version: 2.5.0
3
+ Version: 2.6.1
4
4
  Summary: The bazis command: creates, extends and audits Bazis products, backend and frontend, with Claude (Claude Agent SDK).
5
5
  Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
6
6
  Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
@@ -19,7 +19,7 @@ Classifier: Framework :: FastAPI
19
19
  Requires-Python: >=3.12
20
20
  Description-Content-Type: text/markdown
21
21
  Requires-Dist: bazis>=2.16.0
22
- Requires-Dist: bazis-mcp>=2.5.0
22
+ Requires-Dist: bazis-mcp>=2.6.0
23
23
  Requires-Dist: claude-agent-sdk>=0.2.163
24
24
  Provides-Extra: test
25
25
  Requires-Dist: pytest>=8; extra == "test"
@@ -82,8 +82,9 @@ bazis change # the changes typed one after another; an empty line ends
82
82
  [bazis-front](https://github.com/ecofuture-tech/bazis-front): the specs of the product
83
83
  (`spec/`: roles, entities, workflows, access, scenarios, screens, design), the backend
84
84
  that satisfies them, the contract exported from it (`contract/`), a React + TypeScript
85
- frontend (`frontend/`) with the screens composed from the components of bazis-front,
86
- and the end-to-end tests of the scenarios run against the backend. It needs Node.js
85
+ frontend (`frontend/`) with the screens generated from their specs (`bazis_front
86
+ screens`), the end-to-end tests of the scenarios run against the backend and the
87
+ screenshots of the screens looked at by the agent. It needs Node.js
87
88
  22.12 or newer with npm, and PostgreSQL for the contract and the end-to-end tests;
88
89
  `--no-frontend` builds only the backend.
89
90
  - The product is in the language of its description and in English, the language of the
@@ -54,8 +54,9 @@ bazis change # the changes typed one after another; an empty line ends
54
54
  [bazis-front](https://github.com/ecofuture-tech/bazis-front): the specs of the product
55
55
  (`spec/`: roles, entities, workflows, access, scenarios, screens, design), the backend
56
56
  that satisfies them, the contract exported from it (`contract/`), a React + TypeScript
57
- frontend (`frontend/`) with the screens composed from the components of bazis-front,
58
- and the end-to-end tests of the scenarios run against the backend. It needs Node.js
57
+ frontend (`frontend/`) with the screens generated from their specs (`bazis_front
58
+ screens`), the end-to-end tests of the scenarios run against the backend and the
59
+ screenshots of the screens looked at by the agent. It needs Node.js
59
60
  22.12 or newer with npm, and PostgreSQL for the contract and the end-to-end tests;
60
61
  `--no-frontend` builds only the backend.
61
62
  - The product is in the language of its description and in English, the language of the
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.5.0'
22
- __version_tuple__ = version_tuple = (2, 5, 0)
21
+ __version__ = version = '2.6.1'
22
+ __version_tuple__ = version_tuple = (2, 6, 1)
23
23
 
24
- __commit_id__ = commit_id = 'g7cba9682b'
24
+ __commit_id__ = commit_id = 'g27dcdfabb'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazis-cli
3
- Version: 2.5.0
3
+ Version: 2.6.1
4
4
  Summary: The bazis command: creates, extends and audits Bazis products, backend and frontend, with Claude (Claude Agent SDK).
5
5
  Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
6
6
  Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
@@ -19,7 +19,7 @@ Classifier: Framework :: FastAPI
19
19
  Requires-Python: >=3.12
20
20
  Description-Content-Type: text/markdown
21
21
  Requires-Dist: bazis>=2.16.0
22
- Requires-Dist: bazis-mcp>=2.5.0
22
+ Requires-Dist: bazis-mcp>=2.6.0
23
23
  Requires-Dist: claude-agent-sdk>=0.2.163
24
24
  Provides-Extra: test
25
25
  Requires-Dist: pytest>=8; extra == "test"
@@ -82,8 +82,9 @@ bazis change # the changes typed one after another; an empty line ends
82
82
  [bazis-front](https://github.com/ecofuture-tech/bazis-front): the specs of the product
83
83
  (`spec/`: roles, entities, workflows, access, scenarios, screens, design), the backend
84
84
  that satisfies them, the contract exported from it (`contract/`), a React + TypeScript
85
- frontend (`frontend/`) with the screens composed from the components of bazis-front,
86
- and the end-to-end tests of the scenarios run against the backend. It needs Node.js
85
+ frontend (`frontend/`) with the screens generated from their specs (`bazis_front
86
+ screens`), the end-to-end tests of the scenarios run against the backend and the
87
+ screenshots of the screens looked at by the agent. It needs Node.js
87
88
  22.12 or newer with npm, and PostgreSQL for the contract and the end-to-end tests;
88
89
  `--no-frontend` builds only the backend.
89
90
  - The product is in the language of its description and in English, the language of the
@@ -24,6 +24,8 @@ bazis_cli.egg-info/top_level.txt
24
24
  docs/releases/2.4.0.md
25
25
  docs/releases/2.4.1.md
26
26
  docs/releases/2.5.0.md
27
+ docs/releases/2.6.0.md
28
+ docs/releases/2.6.1.md
27
29
  evals/__init__.py
28
30
  evals/grade.py
29
31
  evals/run.py
@@ -1,5 +1,5 @@
1
1
  bazis>=2.16.0
2
- bazis-mcp>=2.5.0
2
+ bazis-mcp>=2.6.0
3
3
  claude-agent-sdk>=0.2.163
4
4
 
5
5
  [dev]
@@ -16,6 +16,8 @@
16
16
  "docs/releases/2.4.0.md",
17
17
  "docs/releases/2.4.1.md",
18
18
  "docs/releases/2.5.0.md",
19
+ "docs/releases/2.6.0.md",
20
+ "docs/releases/2.6.1.md",
19
21
  "evals/__init__.py",
20
22
  "evals/grade.py",
21
23
  "evals/run.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.6.1",
3
+ "distance": 0,
4
+ "node": "g27dcdfabbf66447ef51bf37bdeb01b5ad7ce85f9",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-18"
8
+ }
@@ -0,0 +1,15 @@
1
+ # bazis-cli 2.6.0
2
+
3
+ ## Changes
4
+
5
+ - bazis-cli requires bazis-mcp 2.6.0 (`bazis-mcp>=2.6.0`): the tasks and the rules of
6
+ bazis-front 0.3.0. A new product requires bazis-front 0.3.0; its screens are generated
7
+ from their specs (`bazis_front screens`), with an extension (`extend`) or a custom screen
8
+ (`custom`) only for what a spec cannot say, and its screenshots (`bazis_front shots`)
9
+ are taken and looked at, at most three rounds. A change of a product generates its
10
+ screens again and ends with the whole round of the final checks.
11
+ - The evals grade the screens of a product: `bazis_front screens --check` and `bazis_front
12
+ shots --check` (a comparison with the specs, the screenshots not taken by the grader)
13
+ with `design` and `e2e`, and no TSX of the product in `frontend/src/` but the code of its
14
+ custom and extended screens (the copies, the generated screens and the template are not
15
+ its own); the detail counts the lines of TSX written by hand.
@@ -0,0 +1,9 @@
1
+ # bazis-cli 2.6.1
2
+
3
+ ## Changes
4
+
5
+ - The evals grader keeps the settings of the machine (the database and GDAL) and creates
6
+ the database on the server of the project.
7
+ - The check that the migrations apply does not pass silently: when the database server is
8
+ unknown or createdb is missing, the check fails.
9
+ - The test of the requirements to the latest releases of bazis and bazis-mcp is restored.
@@ -18,14 +18,16 @@ Python of the project (`.venv`) against PostgreSQL and Redis of `.env`. It does
18
18
  on the names the agent chose: it checks that the project loads and passes its checks, has
19
19
  the expected Bazis packages, complete migrations that apply, models and routes of its own,
20
20
  and tests that pass. A product with a frontend (`frontend = true`) also needs specs valid
21
- against a fresh contract, generated files that are not stale, a frontend that builds and
22
- passes its lint and component tests, and end-to-end tests of its scenarios that pass
21
+ against a fresh contract, generated files that are not stale, screens generated from their
22
+ specs (no TSX of its own but that of its custom and extended screens), a frontend that
23
+ builds and passes its lint and component tests, and end-to-end tests of its scenarios that pass
23
24
  against the backend with the test data of `manage.py e2e_data` (Node.js is needed). A task
24
25
  with a `language` needs the product in that language and in English, with its catalog.
25
26
  The grader runs on POSIX only (macOS, Linux): it stops the servers and the commands it
26
27
  starts by their process group.
27
28
  """
28
29
 
30
+ import json
29
31
  import os
30
32
  import re
31
33
  import secrets
@@ -49,6 +51,32 @@ BAZIS_CHECK_PREFIXES = (
49
51
  'async_request.', 'bg.', 'front.',
50
52
  )
51
53
  TIMEOUT = 900
54
+ #: the settings of the machine that the agent was given and the project must keep when it is
55
+ #: graded: the PostgreSQL and Redis servers and the GDAL and GEOS libraries. The other `BS_*`
56
+ #: variables are those of the project (its `.env`), the name of the database is the grader's
57
+ MACHINE_SETTINGS = (
58
+ 'BS_DATABASES__DEFAULT__HOST', 'BS_DATABASES__DEFAULT__PORT', 'BS_DATABASES__DEFAULT__USER',
59
+ 'BS_DATABASES__DEFAULT__PASSWORD', 'BS_CACHES__DEFAULT__LOCATION',
60
+ 'BS_GDAL_LIBRARY_PATH', 'BS_GEOS_LIBRARY_PATH',
61
+ )
62
+ #: run by `manage.py shell` of the project: prints the server of its database as JSON
63
+ DATABASE_SERVER = """\
64
+ import json
65
+ from django.conf import settings
66
+ db = settings.DATABASES['default']
67
+ print(json.dumps({key: db.get(key.upper()) or None for key in ('host', 'port', 'user', 'password')}))
68
+ """
69
+ #: the TSX of `frontend/src/` that a product does not write: the generated screens and the
70
+ #: template (its entry, its app and its login), besides the assets that bazis-front copied
71
+ #: (the files of `assets` in its lock, among them the shadcn components of `components/ui/`)
72
+ NOT_WRITTEN = ('bazis/', 'screens/generated/', 'main.tsx', 'app/', 'screens/login/')
73
+ #: prints `custom` and `extend` of the specs of the screens by their ids, as JSON (run with the
74
+ #: Python of the project, which has PyYAML with bazis-front)
75
+ SCREEN_SPECS = """\
76
+ import json, pathlib, yaml
77
+ specs = [yaml.safe_load(path.read_text(encoding='utf-8')) for path in pathlib.Path('spec/screens').glob('*.yaml')]
78
+ print(json.dumps({it['id']: {key: it[key] for key in ('custom', 'extend') if key in it} for it in specs}))
79
+ """
52
80
 
53
81
 
54
82
  @dataclass
@@ -119,13 +147,27 @@ def local_apps(project: Path) -> set[str]:
119
147
 
120
148
  def project_env(database: str) -> dict:
121
149
  """
122
- The environment of the project: its own .env and project.env, a database of its own.
150
+ The environment of the project: its own .env and project.env and the settings of the
151
+ machine (`MACHINE_SETTINGS`), a database of its own.
123
152
  """
124
- env = {k: v for k, v in os.environ.items() if not k.startswith(('BS_', 'DJANGO_'))}
153
+ env = {
154
+ k: v for k, v in os.environ.items()
155
+ if k in MACHINE_SETTINGS or not k.startswith(('BS_', 'DJANGO_'))
156
+ }
125
157
  env['BS_DATABASES__DEFAULT__NAME'] = database
126
158
  return env
127
159
 
128
160
 
161
+ def postgres_command(program: str, server: dict, *args: str) -> list[str]:
162
+ """
163
+ A PostgreSQL client command (`createdb`, `dropdb`) for the server of the project.
164
+ """
165
+ command = [program, '-h', server['host'] or 'localhost', '-U', server['user'] or 'postgres']
166
+ if server['port']:
167
+ command += ['-p', str(server['port'])]
168
+ return [*command, *args]
169
+
170
+
129
171
  def package_warnings(messages: list[dict]) -> list[str]:
130
172
  """
131
173
  The ids of the warnings of the Bazis packages among the messages of `bazis_doctor`; the
@@ -183,26 +225,7 @@ def grade(project: Path, task: dict) -> list[Check]:
183
225
 
184
226
  if task.get('frontend'):
185
227
  checks += frontend_checks(project, env, task.get('min_scenarios', 1))
186
- if shutil.which('createdb'):
187
- created = subprocess.run(
188
- ['createdb', '-h', 'localhost', '-U', 'postgres', database],
189
- env={**env, 'PGPASSWORD': 'postgres'}, capture_output=True, text=True,
190
- )
191
- try:
192
- migrate = run(project, ['manage.py', 'migrate', '--noinput'], env)
193
- migrated = created.returncode == 0 and migrate.returncode == 0
194
- checks.append(Check(
195
- 'migrations apply', migrated, created.stderr.strip() or tail(migrate, 6),
196
- ))
197
- if task.get('frontend'):
198
- checks += frontend_database_checks(project, env, migrated)
199
- finally:
200
- subprocess.run(
201
- ['dropdb', '-h', 'localhost', '-U', 'postgres', '--if-exists', '--force', database],
202
- env={**env, 'PGPASSWORD': 'postgres'}, capture_output=True,
203
- )
204
- elif task.get('frontend'):
205
- checks += frontend_database_checks(project, env, False, 'no PostgreSQL (createdb)')
228
+ checks += database_checks(project, env, database, bool(task.get('frontend')))
206
229
 
207
230
  collected = run(project, ['-m', 'pytest', '--collect-only', '-q', '-p', 'no:cacheprovider'], env)
208
231
  count = re.search(r'(\d+) tests? collected', collected.stdout)
@@ -216,6 +239,44 @@ def grade(project: Path, task: dict) -> list[Check]:
216
239
  return checks
217
240
 
218
241
 
242
+ def database_checks(project: Path, env: dict, database: str, frontend: bool) -> list[Check]:
243
+ """
244
+ The checks that need the database of the grading: the migrations apply to a database
245
+ created on the server of the project (dropped afterwards), then the checks of the
246
+ frontend on it. Without `createdb` or the server of the project they fail with the
247
+ reason instead of dropping out of the result.
248
+ """
249
+ server = None
250
+ if shutil.which('createdb'):
251
+ probe = run(project, ['manage.py', 'shell', '-c', DATABASE_SERVER], env)
252
+ server = mcp_project.parse_json(probe.stdout)
253
+ reason = 'server of the database: ' + (tail(probe, 6) or 'no output')
254
+ else:
255
+ reason = 'no PostgreSQL (createdb)'
256
+ if not server:
257
+ checks = [Check('migrations apply', False, reason)]
258
+ if frontend:
259
+ checks += frontend_database_checks(project, env, False, reason)
260
+ return checks
261
+ client_env = {**env, 'PGPASSWORD': server['password'] or ''}
262
+ created = subprocess.run(
263
+ postgres_command('createdb', server, database),
264
+ env=client_env, capture_output=True, text=True,
265
+ )
266
+ try:
267
+ migrate = run(project, ['manage.py', 'migrate', '--noinput'], env)
268
+ migrated = created.returncode == 0 and migrate.returncode == 0
269
+ checks = [Check('migrations apply', migrated, created.stderr.strip() or tail(migrate, 6))]
270
+ if frontend:
271
+ checks += frontend_database_checks(project, env, migrated)
272
+ return checks
273
+ finally:
274
+ subprocess.run(
275
+ postgres_command('dropdb', server, '--if-exists', '--force', database),
276
+ env=client_env, capture_output=True,
277
+ )
278
+
279
+
219
280
  def language_check(project: Path, info: dict, language: str, env: dict) -> Check:
220
281
  """
221
282
  The product is in the language of its description and in English (`LANGUAGES`), the
@@ -278,12 +339,14 @@ def frontend_checks(project: Path, env: dict, min_scenarios: int) -> list[Check]
278
339
  stale = [
279
340
  done for done in (
280
341
  run(project, ['manage.py', 'bazis_front', command, '--check'], env)
281
- for command in ('design', 'e2e')
342
+ for command in ('design', 'screens', 'e2e', 'shots')
282
343
  ) if done.returncode
283
344
  ]
284
345
  checks.append(Check(
285
- 'theme and end-to-end tests generated', not stale, '\n'.join(tail(d, 4) for d in stale),
346
+ 'theme, screens, end-to-end tests and screenshots generated', not stale,
347
+ '\n'.join(tail(d, 4) for d in stale),
286
348
  ))
349
+ checks.append(written_screens(project, env))
287
350
 
288
351
  build = npm(frontend, ['run', 'build'], env)
289
352
  checks.append(Check('frontend builds', build.returncode == 0, tail(build, 8)))
@@ -298,6 +361,44 @@ def frontend_checks(project: Path, env: dict, min_scenarios: int) -> list[Check]
298
361
  return checks
299
362
 
300
363
 
364
+ def written_screens(project: Path, env: dict) -> Check:
365
+ """
366
+ The screens are generated from their specs: the TSX that the product writes in
367
+ `frontend/src/` (not NOT_WRITTEN) is only the code of a custom screen (`custom`: its
368
+ directory `screens/<id>/`, or the module it names) or of an extended one (`extend`: its
369
+ directory). The detail counts the lines of that TSX, a measure of the hand-written code.
370
+ """
371
+ title = 'no TSX of its own but that of the custom and extended screens'
372
+ specs = run(project, ['-c', SCREEN_SPECS], env)
373
+ screens = mcp_project.parse_json(specs.stdout)
374
+ if screens is None:
375
+ return Check(title, False, 'the specs of the screens: ' + tail(specs, 4))
376
+ allowed = []
377
+ for id_, spec in screens.items():
378
+ custom = spec.get('custom')
379
+ if custom is True or spec.get('extend'):
380
+ allowed.append(f'screens/{id_}/')
381
+ elif custom:
382
+ module = custom['module'].removeprefix('@/')
383
+ allowed += [f'{module}.tsx', f'{module}/']
384
+ source = project / 'frontend' / 'src'
385
+ lock = json.loads((project / 'frontend' / 'bazis-front.lock.json').read_text(encoding='utf-8'))
386
+ copied = {
387
+ file.removeprefix('src/') for asset in lock['assets'].values() for file in asset.get('files', ())
388
+ }
389
+ written = {
390
+ name: path for path in sorted(source.rglob('*.tsx'))
391
+ if not (name := path.relative_to(source).as_posix()).startswith(NOT_WRITTEN)
392
+ and name not in copied
393
+ }
394
+ lines = sum(len(path.read_text(encoding='utf-8').splitlines()) for path in written.values())
395
+ other = [name for name in written if not name.startswith(tuple(allowed))]
396
+ detail = f'lines of TSX written by hand: {lines}'
397
+ if other:
398
+ detail += '; not of a custom or extended screen: ' + ', '.join(other)
399
+ return Check(title, not other, detail)
400
+
401
+
301
402
  def project_package(project: Path) -> str:
302
403
  """
303
404
  The project package, from DJANGO_SETTINGS_MODULE of manage.py.
@@ -37,7 +37,7 @@ dependencies = [
37
37
  # the settings of a project without .venv (--no-venv), the languages of Django and the
38
38
  # modules a project package must not hide are those of the Python of the CLI
39
39
  "bazis>=2.16.0",
40
- "bazis-mcp>=2.5.0",
40
+ "bazis-mcp>=2.6.0",
41
41
  "claude-agent-sdk>=0.2.163",
42
42
  ]
43
43
 
@@ -15,6 +15,8 @@
15
15
  import io
16
16
  import json
17
17
  import sys
18
+ import tomllib
19
+ from importlib import metadata
18
20
  from pathlib import Path
19
21
  from types import SimpleNamespace
20
22
 
@@ -337,8 +339,11 @@ def test_add_to_a_product_with_a_frontend(tmp_path, fake_agent, capsys):
337
339
  assert 'package_guide("bazis-statusy")' in backend
338
340
  # a project made before the ports gets them
339
341
  assert prompts.ports_text(scaffold.project_ports(tmp_path)) in frontend
340
- order = ['spec/', 'bazis_front contract', 'front_check', 'front_catalog', 'bazis_front add',
341
- 'bazis_front e2e', 'e2e_data', 'the final checks of the rules']
342
+ # the screens generated again, then the whole round of the final checks (bazis-mcp 2.6)
343
+ frontend = ' '.join(frontend.split())
344
+ order = ['spec/', 'bazis_front contract', 'front_check', 'bazis_front screens',
345
+ 'bazis_front e2e', 'e2e_data', 'the whole round of the final checks of the rules',
346
+ 'npm run build', 'bazis_front screens --check']
342
347
  positions = [frontend.index(text) for text in order]
343
348
  assert positions == sorted(positions)
344
349
  assert FRONTEND_RULES in options.system_prompt['append']
@@ -359,7 +364,7 @@ def test_add_the_frontend(tmp_path, fake_agent):
359
364
  assert '"Building the frontend of a product"' in prompt
360
365
  assert 'the final checks of the rules' in prompt
361
366
  assert 'the specs describe the backend and what the project needs it for' in prompt
362
- assert 'requirement `bazis-front>=0.1.0`' in prompt
367
+ assert f'requirement `{scaffold.FRONTEND_REQUIREMENT}`' in prompt
363
368
  assert FRONTEND_RULES in options.system_prompt['append']
364
369
 
365
370
 
@@ -372,7 +377,7 @@ def test_add_the_frontend_from_a_build(tmp_path, fake_agent, monkeypatch):
372
377
 
373
378
  prompt, _ = fake_agent[0]
374
379
  assert 'requirement `/wheels/bazis_front-0.1.0.whl`' in prompt
375
- assert 'bazis-front>=0.1.0' not in prompt
380
+ assert scaffold.FRONTEND_REQUIREMENT not in prompt
376
381
 
377
382
 
378
383
  def test_add_without_node(tmp_path, fake_agent, monkeypatch, capsys):
@@ -505,7 +510,11 @@ def test_change(tmp_path, fake_agent, sessions, capsys):
505
510
  assert main.main(['change', 'Add the authors', *project, '--budget', '2']) == 0
506
511
  prompt, options = fake_agent[-1]
507
512
  assert options.resume == 's' and options.max_budget_usd == 2
508
- assert prompt == 'Add the authors\n\nAfter the change, run the final checks of the rules once.\n'
513
+ # the request, the final checks once, and the headings of the summary (bazis-mcp 2.6)
514
+ assert prompt == (
515
+ 'Add the authors\n\nAfter the change, run the final checks of the rules once.\n\n'
516
+ f'{prompts.SUMMARY}\n'
517
+ )
509
518
 
510
519
  assert main.main(['change', 'Add the authors', *project, '--new-session']) == 0
511
520
  assert fake_agent[-1][1].resume is None
@@ -608,3 +617,23 @@ def test_change_without_a_terminal(tmp_path, fake_agent, monkeypatch, capsys):
608
617
  assert 'bazis change "..."' in capsys.readouterr().err
609
618
  assert fake_agent == []
610
619
 
620
+
621
+ def test_requirements_are_the_latest_releases():
622
+ """
623
+ The CLI requires at least the releases that bazis-mcp describes: its catalog of the
624
+ released packages gives bazis, and the installed bazis-mcp is the release of that catalog.
625
+ """
626
+ from bazis.contrib.mcp import catalog
627
+
628
+ def version(text):
629
+ return tuple(int(part) for part in text.split('.')[:3])
630
+
631
+ installed = metadata.version('bazis-mcp')
632
+ if 'dev' in installed:
633
+ pytest.skip(f'bazis-mcp {installed} is not a release')
634
+ pyproject = tomllib.loads((Path(__file__).parent.parent / 'pyproject.toml').read_text())
635
+ released = {**{name: entry['version'] for name, entry in catalog.catalog().items()}, 'bazis-mcp': installed}
636
+ pins = dict(it.split('>=') for it in pyproject['project']['dependencies'] if it.startswith('bazis'))
637
+ assert pins.keys() == {'bazis', 'bazis-mcp'}
638
+ for name, minimum in pins.items():
639
+ assert version(minimum) >= version(released[name]), f'{name}>={minimum}, released {released[name]}'
@@ -12,10 +12,16 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import contextlib
16
+ import io
15
17
  import json
16
18
  import os
17
19
  import subprocess
20
+ import sys
18
21
  import time
22
+ import types
23
+
24
+ import pytest
19
25
 
20
26
  from bazis.contrib.mcp import catalog, scaffold
21
27
 
@@ -96,11 +102,45 @@ def test_local_apps(tmp_path):
96
102
  def test_project_env_isolates_the_database(monkeypatch):
97
103
  monkeypatch.setenv('BS_DEBUG', 'false')
98
104
  monkeypatch.setenv('DJANGO_SETTINGS_MODULE', 'other.settings')
105
+ monkeypatch.setenv('BS_DATABASES__DEFAULT__NAME', 'other')
99
106
  env = grade.project_env('eval_x')
100
107
  assert env['BS_DATABASES__DEFAULT__NAME'] == 'eval_x'
101
108
  assert 'BS_DEBUG' not in env and 'DJANGO_SETTINGS_MODULE' not in env
102
109
 
103
110
 
111
+ def test_project_env_keeps_the_settings_of_the_machine(monkeypatch):
112
+ machine = {
113
+ 'BS_DATABASES__DEFAULT__PORT': '55910', 'BS_GDAL_LIBRARY_PATH': '/opt/lib/libgdal.dylib',
114
+ 'BS_GEOS_LIBRARY_PATH': '/opt/lib/libgeos_c.dylib', 'PGHOST': 'db', 'DYLD_LIBRARY_PATH': '/opt/lib',
115
+ }
116
+ for key, value in machine.items():
117
+ monkeypatch.setenv(key, value)
118
+ assert machine.items() <= grade.project_env('eval_x').items()
119
+
120
+
121
+ def test_database_server_of_the_project(monkeypatch):
122
+ settings = types.SimpleNamespace(DATABASES={'default': {
123
+ 'HOST': 'db', 'PORT': '55910', 'USER': 'owner', 'PASSWORD': 'secret', 'NAME': 'x',
124
+ }})
125
+ monkeypatch.setitem(sys.modules, 'django', types.ModuleType('django'))
126
+ monkeypatch.setitem(sys.modules, 'django.conf', types.SimpleNamespace(settings=settings))
127
+ output = io.StringIO()
128
+ with contextlib.redirect_stdout(output):
129
+ exec(grade.DATABASE_SERVER, {})
130
+ server = json.loads(output.getvalue())
131
+ assert server == {'host': 'db', 'port': '55910', 'user': 'owner', 'password': 'secret'}
132
+ assert grade.postgres_command('createdb', server, 'eval_x') == [
133
+ 'createdb', '-h', 'db', '-U', 'owner', '-p', '55910', 'eval_x',
134
+ ]
135
+
136
+
137
+ def test_postgres_command_defaults():
138
+ server = {'host': None, 'port': None, 'user': None, 'password': None}
139
+ assert grade.postgres_command('dropdb', server, '--force', 'x') == [
140
+ 'dropdb', '-h', 'localhost', '-U', 'postgres', '--force', 'x',
141
+ ]
142
+
143
+
104
144
  def test_summary(tmp_path):
105
145
  checks = [grade.Check('project loads', True), grade.Check('tests pass', False, 'boom\n1 failed')]
106
146
  result = {'task': 'tracker', 'seconds': 120, 'cost_usd': 1.5, 'turns': 30,
@@ -111,8 +151,6 @@ def test_summary(tmp_path):
111
151
 
112
152
 
113
153
  def test_running_needs_a_budget():
114
- import pytest
115
-
116
154
  with pytest.raises(SystemExit, match='budget'):
117
155
  run.main_cli(['--task', 'tracker'])
118
156
 
@@ -125,7 +163,7 @@ def product(tmp_path, scenarios: int):
125
163
  scaffold.write_files(tmp_path, 'desk', frontend=True)
126
164
  generated = tmp_path / 'frontend' / 'e2e' / 'generated'
127
165
  generated.mkdir(parents=True)
128
- (tmp_path / 'frontend' / 'bazis-front.lock.json').write_text('{}')
166
+ (tmp_path / 'frontend' / 'bazis-front.lock.json').write_text('{"assets": {}}')
129
167
  for index in range(scenarios):
130
168
  (generated / f'scenario-{index}.spec.ts').write_text('')
131
169
  (generated / 'product.ts').write_text('')
@@ -139,6 +177,8 @@ def test_frontend_checks(tmp_path, monkeypatch):
139
177
 
140
178
  def fake_run(project, args, env):
141
179
  commands.append(args)
180
+ if args[0] == '-c':
181
+ return done(0, '{}')
142
182
  if args[2] == 'check':
143
183
  return done(1, json.dumps({'contract': True, 'errors': 1, 'warnings': 1, 'issues': issues}))
144
184
  return done(1 if args[2] == 'e2e' else 0)
@@ -153,10 +193,82 @@ def test_frontend_checks(tmp_path, monkeypatch):
153
193
  assert checks['specs valid against the contract'].detail == 'P019 spec/product.yaml#/roles/0'
154
194
  assert not checks['at least 3 scenarios'].passed
155
195
  assert checks['at least 3 scenarios'].detail == 'scenario-0.spec, scenario-1.spec'
156
- assert not checks['theme and end-to-end tests generated'].passed
196
+ assert not checks['theme, screens, end-to-end tests and screenshots generated'].passed
197
+ assert checks['no TSX of its own but that of the custom and extended screens'].passed
157
198
  assert checks['frontend builds'].passed
158
199
  assert not checks['frontend lint and component tests pass'].passed
159
- assert ['manage.py', 'bazis_front', 'design', '--check'] in commands
200
+ for command in ('design', 'screens', 'e2e', 'shots'):
201
+ assert ['manage.py', 'bazis_front', command, '--check'] in commands
202
+
203
+
204
+ def test_written_screens(tmp_path, monkeypatch):
205
+ """
206
+ The TSX that a product writes is only that of its custom and extended screens: not of a
207
+ screen generated from its spec, nor elsewhere in `src/`; the copies, the generated
208
+ screens and the template are not its own. The lines written are counted.
209
+ """
210
+ screens = {
211
+ 'loan-list': {'custom': True},
212
+ 'book-card': {'extend': True},
213
+ 'reader-list': {'custom': {'module': '@/screens/readers', 'export': 'ReaderListScreen'}},
214
+ 'book-list': {},
215
+ }
216
+ monkeypatch.setattr(grade, 'run', lambda project, args, env: done(0, json.dumps(screens)))
217
+ source = tmp_path / 'frontend' / 'src'
218
+ files = {
219
+ # the custom and extended screens, their own files too
220
+ 'screens/loan-list/index.tsx': 3, 'screens/loan-list/parts.tsx': 2,
221
+ 'screens/book-card/extend.tsx': 4, 'screens/readers.tsx': 1,
222
+ # not the product's
223
+ 'screens/generated/book-list.tsx': 50, 'bazis/ui/list/index.tsx': 50,
224
+ 'app/router.tsx': 50, 'main.tsx': 50, 'screens/login/index.tsx': 50,
225
+ 'screens/book-list/index.tsx': 5, 'components/badge.tsx': 6,
226
+ 'screens/loan-list-extra/index.tsx': 7, 'i18n/en.ts': 50,
227
+ # the screen of a custom module elsewhere has no directory of its own
228
+ 'screens/reader-list/index.tsx': 8,
229
+ }
230
+ for name, lines in files.items():
231
+ (source / name).parent.mkdir(parents=True, exist_ok=True)
232
+ (source / name).write_text('x\n' * lines)
233
+ # the shadcn components that bazis-front copied are in its lock: not the product's
234
+ (source / 'components' / 'ui').mkdir(parents=True)
235
+ (source / 'components' / 'ui' / 'button.tsx').write_text('x\n' * 100)
236
+ (source.parent / 'bazis-front.lock.json').write_text(json.dumps({
237
+ # an entry of an old dev-lock has no `files`
238
+ 'assets': {'button': {'files': {'src/components/ui/button.tsx': 'sha256:0'}}, 'old': {}},
239
+ }))
240
+
241
+ check = grade.written_screens(tmp_path, {})
242
+
243
+ assert not check.passed and check.name.startswith('no TSX of its own')
244
+ assert check.detail == (
245
+ 'lines of TSX written by hand: 36; not of a custom or extended screen: '
246
+ 'components/badge.tsx, screens/book-list/index.tsx, screens/loan-list-extra/index.tsx, '
247
+ 'screens/reader-list/index.tsx'
248
+ )
249
+
250
+ for name in ('screens/book-list/index.tsx', 'components/badge.tsx', 'screens/loan-list-extra/index.tsx',
251
+ 'screens/reader-list/index.tsx'):
252
+ (source / name).unlink()
253
+ check = grade.written_screens(tmp_path, {})
254
+ assert check.passed and check.detail == 'lines of TSX written by hand: 10'
255
+
256
+
257
+ def test_written_screens_reads_the_specs(tmp_path):
258
+ """
259
+ The specs of the screens are read with the Python of the project (PyYAML of bazis-front).
260
+ """
261
+ pytest.importorskip('yaml')
262
+ (tmp_path / 'spec' / 'screens').mkdir(parents=True)
263
+ (tmp_path / 'spec' / 'screens' / 'loan-list.yaml').write_text('id: loan-list\ncustom: true\n')
264
+ (tmp_path / 'spec' / 'screens' / 'book-list.yaml').write_text('id: book-list\nprimitive: list\n')
265
+ (tmp_path / 'frontend' / 'src' / 'screens' / 'loan-list').mkdir(parents=True)
266
+ (tmp_path / 'frontend' / 'src' / 'screens' / 'loan-list' / 'index.tsx').write_text('x\n')
267
+ (tmp_path / 'frontend' / 'bazis-front.lock.json').write_text('{"assets": {}}')
268
+
269
+ check = grade.written_screens(tmp_path, {**os.environ})
270
+
271
+ assert check.passed and check.detail == 'lines of TSX written by hand: 1'
160
272
 
161
273
 
162
274
  def test_frontend_checks_without_a_frontend(tmp_path):
@@ -173,6 +285,50 @@ def test_frontend_database_checks_without_a_database(tmp_path):
173
285
  ]
174
286
 
175
287
 
288
+ def probe_stub(monkeypatch, output: str, returncode: int = 0):
289
+ monkeypatch.setattr(grade.shutil, 'which', lambda name: '/usr/bin/createdb')
290
+ monkeypatch.setattr(grade, 'run', lambda project, args, env: done(returncode, output))
291
+
292
+
293
+ @pytest.mark.parametrize('frontend', [False, True])
294
+ @pytest.mark.parametrize('output', ['Traceback: no module', 'not json'])
295
+ def test_database_checks_fail_when_the_server_is_unknown(tmp_path, monkeypatch, frontend, output):
296
+ probe_stub(monkeypatch, output, 1)
297
+ project = product(tmp_path, 1)
298
+ checks = grade.database_checks(project, {}, 'eval_x', frontend)
299
+ reason = 'server of the database: ' + output
300
+ expected = [('migrations apply', False, reason)]
301
+ if frontend:
302
+ expected += [('contract fresh', False, reason), ('end-to-end tests pass', False, reason)]
303
+ assert [(c.name, c.passed, c.detail) for c in checks] == expected
304
+
305
+
306
+ @pytest.mark.parametrize('frontend', [False, True])
307
+ def test_database_checks_without_createdb(tmp_path, monkeypatch, frontend):
308
+ monkeypatch.setattr(grade.shutil, 'which', lambda name: None)
309
+ monkeypatch.setattr(grade, 'run', lambda *a: pytest.fail('the server must not be probed'))
310
+ checks = grade.database_checks(product(tmp_path, 1), {}, 'eval_x', frontend)
311
+ assert [(c.name, c.passed, c.detail) for c in checks][0] == (
312
+ 'migrations apply', False, 'no PostgreSQL (createdb)',
313
+ )
314
+ assert len(checks) == (3 if frontend else 1)
315
+
316
+
317
+ def test_database_checks_create_migrate_and_drop(tmp_path, monkeypatch):
318
+ server = {'host': 'db', 'port': None, 'user': 'owner', 'password': 'secret'}
319
+ probe_stub(monkeypatch, json.dumps(server))
320
+ commands = []
321
+
322
+ def fake_subprocess(command, **kwargs):
323
+ commands.append(command[0])
324
+ return subprocess.CompletedProcess(command, 0, '', '')
325
+
326
+ monkeypatch.setattr(grade.subprocess, 'run', fake_subprocess)
327
+ checks = grade.database_checks(tmp_path, {}, 'eval_x', False)
328
+ assert [(c.name, c.passed) for c in checks] == [('migrations apply', True)]
329
+ assert commands == ['createdb', 'dropdb']
330
+
331
+
176
332
  def test_end_to_end_needs_the_test_data(tmp_path, monkeypatch):
177
333
  monkeypatch.setattr(grade, 'run', lambda project, args, env: done(1, 'Unknown command: e2e_data'))
178
334
  check = grade.end_to_end(product(tmp_path, 1), {})
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.5.0",
3
- "distance": 0,
4
- "node": "g7cba9682b5505940f5ee643f9a2af645105afad7",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-10-16"
8
- }
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