bazis-cli 2.5.0__tar.gz → 2.6.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 (38) hide show
  1. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/PKG-INFO +5 -4
  2. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/README.md +3 -2
  3. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis/contrib/cli/_version.py +3 -3
  4. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis_cli.egg-info/PKG-INFO +5 -4
  5. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis_cli.egg-info/SOURCES.txt +1 -0
  6. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis_cli.egg-info/requires.txt +1 -1
  7. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis_cli.egg-info/scm_file_list.json +1 -0
  8. bazis_cli-2.6.0/bazis_cli.egg-info/scm_version.json +8 -0
  9. bazis_cli-2.6.0/docs/releases/2.6.0.md +15 -0
  10. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/evals/grade.py +57 -4
  11. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/pyproject.toml +1 -1
  12. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/tests/test_cli.py +12 -5
  13. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/tests/test_evals.py +79 -5
  14. bazis_cli-2.5.0/bazis_cli.egg-info/scm_version.json +0 -8
  15. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/.dockerignore +0 -0
  16. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/.github/workflows/build-publish.yml +0 -0
  17. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/.github/workflows/release.yml +0 -0
  18. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/.github/workflows/tests.yml +0 -0
  19. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/.github/workflows/update-licenses.yml +0 -0
  20. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/.gitignore +0 -0
  21. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/CLAUDE.md +0 -0
  22. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis/contrib/cli/AGENTS.md +0 -0
  23. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis/contrib/cli/__init__.py +0 -0
  24. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis/contrib/cli/agent.py +0 -0
  25. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis/contrib/cli/bazis_manifest.toml +0 -0
  26. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis/contrib/cli/main.py +0 -0
  27. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis_cli.egg-info/dependency_links.txt +0 -0
  28. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis_cli.egg-info/entry_points.txt +0 -0
  29. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/bazis_cli.egg-info/top_level.txt +0 -0
  30. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/docs/releases/2.4.0.md +0 -0
  31. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/docs/releases/2.4.1.md +0 -0
  32. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/docs/releases/2.5.0.md +0 -0
  33. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/evals/__init__.py +0 -0
  34. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/evals/run.py +0 -0
  35. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/evals/tasks.toml +0 -0
  36. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/setup.cfg +0 -0
  37. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/tests/__init__.py +0 -0
  38. {bazis_cli-2.5.0 → bazis_cli-2.6.0}/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.0
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.0'
22
+ __version_tuple__ = version_tuple = (2, 6, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g7cba9682b'
24
+ __commit_id__ = commit_id = 'g04ed6b75d'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazis-cli
3
- Version: 2.5.0
3
+ Version: 2.6.0
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,7 @@ 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
27
28
  evals/__init__.py
28
29
  evals/grade.py
29
30
  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,7 @@
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",
19
20
  "evals/__init__.py",
20
21
  "evals/grade.py",
21
22
  "evals/run.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.6.0",
3
+ "distance": 0,
4
+ "node": "g04ed6b75d74ecf9bbfacf5be0bd136fda8106bf7",
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.
@@ -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,17 @@ BAZIS_CHECK_PREFIXES = (
49
51
  'async_request.', 'bg.', 'front.',
50
52
  )
51
53
  TIMEOUT = 900
54
+ #: the TSX of `frontend/src/` that a product does not write: the generated screens and the
55
+ #: template (its entry, its app and its login), besides the assets that bazis-front copied
56
+ #: (the files of `assets` in its lock, among them the shadcn components of `components/ui/`)
57
+ NOT_WRITTEN = ('bazis/', 'screens/generated/', 'main.tsx', 'app/', 'screens/login/')
58
+ #: prints `custom` and `extend` of the specs of the screens by their ids, as JSON (run with the
59
+ #: Python of the project, which has PyYAML with bazis-front)
60
+ SCREEN_SPECS = """\
61
+ import json, pathlib, yaml
62
+ specs = [yaml.safe_load(path.read_text(encoding='utf-8')) for path in pathlib.Path('spec/screens').glob('*.yaml')]
63
+ print(json.dumps({it['id']: {key: it[key] for key in ('custom', 'extend') if key in it} for it in specs}))
64
+ """
52
65
 
53
66
 
54
67
  @dataclass
@@ -278,12 +291,14 @@ def frontend_checks(project: Path, env: dict, min_scenarios: int) -> list[Check]
278
291
  stale = [
279
292
  done for done in (
280
293
  run(project, ['manage.py', 'bazis_front', command, '--check'], env)
281
- for command in ('design', 'e2e')
294
+ for command in ('design', 'screens', 'e2e', 'shots')
282
295
  ) if done.returncode
283
296
  ]
284
297
  checks.append(Check(
285
- 'theme and end-to-end tests generated', not stale, '\n'.join(tail(d, 4) for d in stale),
298
+ 'theme, screens, end-to-end tests and screenshots generated', not stale,
299
+ '\n'.join(tail(d, 4) for d in stale),
286
300
  ))
301
+ checks.append(written_screens(project, env))
287
302
 
288
303
  build = npm(frontend, ['run', 'build'], env)
289
304
  checks.append(Check('frontend builds', build.returncode == 0, tail(build, 8)))
@@ -298,6 +313,44 @@ def frontend_checks(project: Path, env: dict, min_scenarios: int) -> list[Check]
298
313
  return checks
299
314
 
300
315
 
316
+ def written_screens(project: Path, env: dict) -> Check:
317
+ """
318
+ The screens are generated from their specs: the TSX that the product writes in
319
+ `frontend/src/` (not NOT_WRITTEN) is only the code of a custom screen (`custom`: its
320
+ directory `screens/<id>/`, or the module it names) or of an extended one (`extend`: its
321
+ directory). The detail counts the lines of that TSX, a measure of the hand-written code.
322
+ """
323
+ title = 'no TSX of its own but that of the custom and extended screens'
324
+ specs = run(project, ['-c', SCREEN_SPECS], env)
325
+ screens = mcp_project.parse_json(specs.stdout)
326
+ if screens is None:
327
+ return Check(title, False, 'the specs of the screens: ' + tail(specs, 4))
328
+ allowed = []
329
+ for id_, spec in screens.items():
330
+ custom = spec.get('custom')
331
+ if custom is True or spec.get('extend'):
332
+ allowed.append(f'screens/{id_}/')
333
+ elif custom:
334
+ module = custom['module'].removeprefix('@/')
335
+ allowed += [f'{module}.tsx', f'{module}/']
336
+ source = project / 'frontend' / 'src'
337
+ lock = json.loads((project / 'frontend' / 'bazis-front.lock.json').read_text(encoding='utf-8'))
338
+ copied = {
339
+ file.removeprefix('src/') for asset in lock['assets'].values() for file in asset.get('files', ())
340
+ }
341
+ written = {
342
+ name: path for path in sorted(source.rglob('*.tsx'))
343
+ if not (name := path.relative_to(source).as_posix()).startswith(NOT_WRITTEN)
344
+ and name not in copied
345
+ }
346
+ lines = sum(len(path.read_text(encoding='utf-8').splitlines()) for path in written.values())
347
+ other = [name for name in written if not name.startswith(tuple(allowed))]
348
+ detail = f'lines of TSX written by hand: {lines}'
349
+ if other:
350
+ detail += '; not of a custom or extended screen: ' + ', '.join(other)
351
+ return Check(title, not other, detail)
352
+
353
+
301
354
  def project_package(project: Path) -> str:
302
355
  """
303
356
  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
 
@@ -337,8 +337,11 @@ def test_add_to_a_product_with_a_frontend(tmp_path, fake_agent, capsys):
337
337
  assert 'package_guide("bazis-statusy")' in backend
338
338
  # a project made before the ports gets them
339
339
  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']
340
+ # the screens generated again, then the whole round of the final checks (bazis-mcp 2.6)
341
+ frontend = ' '.join(frontend.split())
342
+ order = ['spec/', 'bazis_front contract', 'front_check', 'bazis_front screens',
343
+ 'bazis_front e2e', 'e2e_data', 'the whole round of the final checks of the rules',
344
+ 'npm run build', 'bazis_front screens --check']
342
345
  positions = [frontend.index(text) for text in order]
343
346
  assert positions == sorted(positions)
344
347
  assert FRONTEND_RULES in options.system_prompt['append']
@@ -359,7 +362,7 @@ def test_add_the_frontend(tmp_path, fake_agent):
359
362
  assert '"Building the frontend of a product"' in prompt
360
363
  assert 'the final checks of the rules' in prompt
361
364
  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
365
+ assert f'requirement `{scaffold.FRONTEND_REQUIREMENT}`' in prompt
363
366
  assert FRONTEND_RULES in options.system_prompt['append']
364
367
 
365
368
 
@@ -372,7 +375,7 @@ def test_add_the_frontend_from_a_build(tmp_path, fake_agent, monkeypatch):
372
375
 
373
376
  prompt, _ = fake_agent[0]
374
377
  assert 'requirement `/wheels/bazis_front-0.1.0.whl`' in prompt
375
- assert 'bazis-front>=0.1.0' not in prompt
378
+ assert scaffold.FRONTEND_REQUIREMENT not in prompt
376
379
 
377
380
 
378
381
  def test_add_without_node(tmp_path, fake_agent, monkeypatch, capsys):
@@ -505,7 +508,11 @@ def test_change(tmp_path, fake_agent, sessions, capsys):
505
508
  assert main.main(['change', 'Add the authors', *project, '--budget', '2']) == 0
506
509
  prompt, options = fake_agent[-1]
507
510
  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'
511
+ # the request, the final checks once, and the headings of the summary (bazis-mcp 2.6)
512
+ assert prompt == (
513
+ 'Add the authors\n\nAfter the change, run the final checks of the rules once.\n\n'
514
+ f'{prompts.SUMMARY}\n'
515
+ )
509
516
 
510
517
  assert main.main(['change', 'Add the authors', *project, '--new-session']) == 0
511
518
  assert fake_agent[-1][1].resume is None
@@ -17,6 +17,8 @@ import os
17
17
  import subprocess
18
18
  import time
19
19
 
20
+ import pytest
21
+
20
22
  from bazis.contrib.mcp import catalog, scaffold
21
23
 
22
24
  from evals import grade, run
@@ -111,8 +113,6 @@ def test_summary(tmp_path):
111
113
 
112
114
 
113
115
  def test_running_needs_a_budget():
114
- import pytest
115
-
116
116
  with pytest.raises(SystemExit, match='budget'):
117
117
  run.main_cli(['--task', 'tracker'])
118
118
 
@@ -125,7 +125,7 @@ def product(tmp_path, scenarios: int):
125
125
  scaffold.write_files(tmp_path, 'desk', frontend=True)
126
126
  generated = tmp_path / 'frontend' / 'e2e' / 'generated'
127
127
  generated.mkdir(parents=True)
128
- (tmp_path / 'frontend' / 'bazis-front.lock.json').write_text('{}')
128
+ (tmp_path / 'frontend' / 'bazis-front.lock.json').write_text('{"assets": {}}')
129
129
  for index in range(scenarios):
130
130
  (generated / f'scenario-{index}.spec.ts').write_text('')
131
131
  (generated / 'product.ts').write_text('')
@@ -139,6 +139,8 @@ def test_frontend_checks(tmp_path, monkeypatch):
139
139
 
140
140
  def fake_run(project, args, env):
141
141
  commands.append(args)
142
+ if args[0] == '-c':
143
+ return done(0, '{}')
142
144
  if args[2] == 'check':
143
145
  return done(1, json.dumps({'contract': True, 'errors': 1, 'warnings': 1, 'issues': issues}))
144
146
  return done(1 if args[2] == 'e2e' else 0)
@@ -153,10 +155,82 @@ def test_frontend_checks(tmp_path, monkeypatch):
153
155
  assert checks['specs valid against the contract'].detail == 'P019 spec/product.yaml#/roles/0'
154
156
  assert not checks['at least 3 scenarios'].passed
155
157
  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
158
+ assert not checks['theme, screens, end-to-end tests and screenshots generated'].passed
159
+ assert checks['no TSX of its own but that of the custom and extended screens'].passed
157
160
  assert checks['frontend builds'].passed
158
161
  assert not checks['frontend lint and component tests pass'].passed
159
- assert ['manage.py', 'bazis_front', 'design', '--check'] in commands
162
+ for command in ('design', 'screens', 'e2e', 'shots'):
163
+ assert ['manage.py', 'bazis_front', command, '--check'] in commands
164
+
165
+
166
+ def test_written_screens(tmp_path, monkeypatch):
167
+ """
168
+ The TSX that a product writes is only that of its custom and extended screens: not of a
169
+ screen generated from its spec, nor elsewhere in `src/`; the copies, the generated
170
+ screens and the template are not its own. The lines written are counted.
171
+ """
172
+ screens = {
173
+ 'loan-list': {'custom': True},
174
+ 'book-card': {'extend': True},
175
+ 'reader-list': {'custom': {'module': '@/screens/readers', 'export': 'ReaderListScreen'}},
176
+ 'book-list': {},
177
+ }
178
+ monkeypatch.setattr(grade, 'run', lambda project, args, env: done(0, json.dumps(screens)))
179
+ source = tmp_path / 'frontend' / 'src'
180
+ files = {
181
+ # the custom and extended screens, their own files too
182
+ 'screens/loan-list/index.tsx': 3, 'screens/loan-list/parts.tsx': 2,
183
+ 'screens/book-card/extend.tsx': 4, 'screens/readers.tsx': 1,
184
+ # not the product's
185
+ 'screens/generated/book-list.tsx': 50, 'bazis/ui/list/index.tsx': 50,
186
+ 'app/router.tsx': 50, 'main.tsx': 50, 'screens/login/index.tsx': 50,
187
+ 'screens/book-list/index.tsx': 5, 'components/badge.tsx': 6,
188
+ 'screens/loan-list-extra/index.tsx': 7, 'i18n/en.ts': 50,
189
+ # the screen of a custom module elsewhere has no directory of its own
190
+ 'screens/reader-list/index.tsx': 8,
191
+ }
192
+ for name, lines in files.items():
193
+ (source / name).parent.mkdir(parents=True, exist_ok=True)
194
+ (source / name).write_text('x\n' * lines)
195
+ # the shadcn components that bazis-front copied are in its lock: not the product's
196
+ (source / 'components' / 'ui').mkdir(parents=True)
197
+ (source / 'components' / 'ui' / 'button.tsx').write_text('x\n' * 100)
198
+ (source.parent / 'bazis-front.lock.json').write_text(json.dumps({
199
+ # an entry of an old dev-lock has no `files`
200
+ 'assets': {'button': {'files': {'src/components/ui/button.tsx': 'sha256:0'}}, 'old': {}},
201
+ }))
202
+
203
+ check = grade.written_screens(tmp_path, {})
204
+
205
+ assert not check.passed and check.name.startswith('no TSX of its own')
206
+ assert check.detail == (
207
+ 'lines of TSX written by hand: 36; not of a custom or extended screen: '
208
+ 'components/badge.tsx, screens/book-list/index.tsx, screens/loan-list-extra/index.tsx, '
209
+ 'screens/reader-list/index.tsx'
210
+ )
211
+
212
+ for name in ('screens/book-list/index.tsx', 'components/badge.tsx', 'screens/loan-list-extra/index.tsx',
213
+ 'screens/reader-list/index.tsx'):
214
+ (source / name).unlink()
215
+ check = grade.written_screens(tmp_path, {})
216
+ assert check.passed and check.detail == 'lines of TSX written by hand: 10'
217
+
218
+
219
+ def test_written_screens_reads_the_specs(tmp_path):
220
+ """
221
+ The specs of the screens are read with the Python of the project (PyYAML of bazis-front).
222
+ """
223
+ pytest.importorskip('yaml')
224
+ (tmp_path / 'spec' / 'screens').mkdir(parents=True)
225
+ (tmp_path / 'spec' / 'screens' / 'loan-list.yaml').write_text('id: loan-list\ncustom: true\n')
226
+ (tmp_path / 'spec' / 'screens' / 'book-list.yaml').write_text('id: book-list\nprimitive: list\n')
227
+ (tmp_path / 'frontend' / 'src' / 'screens' / 'loan-list').mkdir(parents=True)
228
+ (tmp_path / 'frontend' / 'src' / 'screens' / 'loan-list' / 'index.tsx').write_text('x\n')
229
+ (tmp_path / 'frontend' / 'bazis-front.lock.json').write_text('{"assets": {}}')
230
+
231
+ check = grade.written_screens(tmp_path, {**os.environ})
232
+
233
+ assert check.passed and check.detail == 'lines of TSX written by hand: 1'
160
234
 
161
235
 
162
236
  def test_frontend_checks_without_a_frontend(tmp_path):
@@ -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