feature-loop-harness-cli 0.1.0

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 (40) hide show
  1. package/README.md +53 -0
  2. package/bin/flh.js +391 -0
  3. package/package.json +29 -0
  4. package/templates/default/.codex/config.toml +2 -0
  5. package/templates/default/.codex/hooks/user-prompt-submit.sh +5 -0
  6. package/templates/default/.codex/hooks.json +16 -0
  7. package/templates/default/.flh/docs/FEATURE_IMPLEMENTATION_PIPELINE.md +454 -0
  8. package/templates/default/.flh/docs/PROJECT_WORKFLOW.md +270 -0
  9. package/templates/default/.flh/docs/REVIEW_PATCH_PIPELINE.md +166 -0
  10. package/templates/default/.flh/hooks/user_prompt_submit.py +1440 -0
  11. package/templates/default/.flh/runtime/STATE.md +84 -0
  12. package/templates/default/.flh/scripts/pre_commit.py +674 -0
  13. package/templates/default/.flh/workflow/docs-spec.yml +134 -0
  14. package/templates/default/.flh/workflow/flow.yml +82 -0
  15. package/templates/default/.flh/workflow/request-patterns.yml +265 -0
  16. package/templates/default/.flh/workflow/state-actions.yml +117 -0
  17. package/templates/default/.flh/workflow/transition-guards.yml +57 -0
  18. package/templates/default/.husky/pre-commit +3 -0
  19. package/templates/default/AGENTS.md +44 -0
  20. package/templates/default/HARNESS_MANUAL.md +1105 -0
  21. package/templates/default/README.md +251 -0
  22. package/templates/default/docs/API.md +41 -0
  23. package/templates/default/docs/ARCHITECTURE.md +86 -0
  24. package/templates/default/docs/DB_SCHEMA.md +149 -0
  25. package/templates/default/docs/DESIGN.md +52 -0
  26. package/templates/default/docs/MVP.md +47 -0
  27. package/templates/default/docs/QUALITY_SCORE.md +54 -0
  28. package/templates/default/docs/docs-map.md +64 -0
  29. package/templates/default/docs/features/active/.gitkeep +1 -0
  30. package/templates/default/docs/features/backlog/.gitkeep +1 -0
  31. package/templates/default/docs/features/blocked/.gitkeep +1 -0
  32. package/templates/default/docs/features/done/.gitkeep +1 -0
  33. package/templates/default/docs/features/feature-index.md +21 -0
  34. package/templates/default/docs/features/postponed/.gitkeep +1 -0
  35. package/templates/default/docs/features/ready/.gitkeep +1 -0
  36. package/templates/default/docs/features/review/.gitkeep +1 -0
  37. package/templates/default/docs/source-layout.yml +33 -0
  38. package/templates/default/gitignore.template +9 -0
  39. package/templates/default/tests/hooks/test_pre_commit.py +659 -0
  40. package/templates/default/tests/hooks/test_user_prompt_submit.py +750 -0
@@ -0,0 +1,659 @@
1
+ import contextlib
2
+ import io
3
+ import importlib.util
4
+ import json
5
+ import tempfile
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+
10
+ SCRIPT_PATH = Path(__file__).resolve().parents[2] / ".flh/scripts/pre_commit.py"
11
+
12
+
13
+ def load_module():
14
+ spec = importlib.util.spec_from_file_location("pre_commit", SCRIPT_PATH)
15
+ module = importlib.util.module_from_spec(spec)
16
+ assert spec.loader is not None
17
+ spec.loader.exec_module(module)
18
+ return module
19
+
20
+
21
+ class PreCommitScriptTest(unittest.TestCase):
22
+ def setUp(self):
23
+ self.module = load_module()
24
+
25
+ def make_temp_root(self):
26
+ tmpdir = tempfile.TemporaryDirectory()
27
+ self.addCleanup(tmpdir.cleanup)
28
+ root = Path(tmpdir.name)
29
+ self.module.ROOT = root
30
+ self.module.STATE_PATH = root / ".flh/runtime/STATE.md"
31
+ self.module.SOURCE_LAYOUT_PATH = root / "docs/source-layout.yml"
32
+ return root
33
+
34
+ def write_source_layout(
35
+ self,
36
+ root,
37
+ *,
38
+ status="completed",
39
+ package_manager="npm",
40
+ package=True,
41
+ ):
42
+ package_value = "true" if package else "false"
43
+ target = root / "docs/source-layout.yml"
44
+ target.parent.mkdir(parents=True, exist_ok=True)
45
+ target.write_text(
46
+ "\n".join(
47
+ [
48
+ "version: 1",
49
+ f"status: {status}",
50
+ "project:",
51
+ f" package_manager: {package_manager}",
52
+ "source_roots:",
53
+ " backend:",
54
+ " path: app/be",
55
+ f" package: {package_value}",
56
+ "",
57
+ ]
58
+ ),
59
+ encoding="utf-8",
60
+ )
61
+
62
+ def write_package_json(self, root, path, data):
63
+ target = root / path
64
+ target.parent.mkdir(parents=True, exist_ok=True)
65
+ target.write_text(json.dumps(data), encoding="utf-8")
66
+
67
+ def stub_git(self, branch, staged_files):
68
+ def fake_run_git(args):
69
+ if args == ["branch", "--show-current"]:
70
+ return branch
71
+ if args == ["diff", "--cached", "--name-only"]:
72
+ return "\n".join(staged_files)
73
+ raise AssertionError(f"Unexpected git args: {args}")
74
+
75
+ self.module.run_git = fake_run_git
76
+
77
+ def stub_package_manager_available(self):
78
+ self.module.shutil = type(
79
+ "FakeShutil",
80
+ (),
81
+ {"which": staticmethod(lambda _pm: "/usr/bin/tool")},
82
+ )
83
+
84
+ def run_main(self):
85
+ output = io.StringIO()
86
+ with contextlib.redirect_stdout(output):
87
+ result = self.module.main()
88
+ return result, output.getvalue()
89
+
90
+ def test_package_manager_normalizes_versions(self):
91
+ self.assertEqual(self.module.normalize_package_manager("pnpm@9.0.0"), "pnpm")
92
+ self.assertEqual(self.module.normalize_package_manager("npm@10.0.0"), "npm")
93
+ self.assertEqual(self.module.normalize_package_manager("yarn"), "yarn")
94
+
95
+ def test_branch_kind_classifies_source_commit_branches(self):
96
+ scenarios = [
97
+ ("main", "main"),
98
+ ("master", "main"),
99
+ ("feat/login", "implementation"),
100
+ ("fix/button", "implementation"),
101
+ ("refactor/core", "implementation"),
102
+ ("chore/docs", "other"),
103
+ ]
104
+
105
+ for branch, expected in scenarios:
106
+ with self.subTest(branch=branch):
107
+ self.assertEqual(self.module.branch_kind(branch), expected)
108
+
109
+ def test_main_blocks_source_changes_on_other_branch(self):
110
+ root = self.make_temp_root()
111
+ self.write_source_layout(root)
112
+ self.stub_git("chore/source-edit", ["app/be/src/index.ts"])
113
+
114
+ result, _output = self.run_main()
115
+
116
+ self.assertEqual(result, 1)
117
+
118
+ def test_main_allows_docs_harness_only_without_running_commands(self):
119
+ self.make_temp_root()
120
+ self.stub_git(
121
+ "chore/docs",
122
+ [
123
+ "README.md",
124
+ "docs/MVP.md",
125
+ ".flh/runtime/STATE.md",
126
+ "tests/hooks/test_pre_commit.py",
127
+ ],
128
+ )
129
+ commands = []
130
+ self.module.run_command = commands.append
131
+
132
+ result, _output = self.run_main()
133
+
134
+ self.assertEqual(result, 0)
135
+ self.assertEqual(commands, [])
136
+
137
+ def test_source_root_file_wins_over_root_file_name(self):
138
+ source_layout = {
139
+ "source_roots": {
140
+ "backend": {
141
+ "path": "app/be",
142
+ "package": True,
143
+ }
144
+ }
145
+ }
146
+ roots = self.module.source_roots(source_layout)
147
+
148
+ self.assertTrue(self.module.is_source_candidate("app/be/package.json", roots))
149
+ self.assertEqual(
150
+ self.module.match_source_root("app/be/package.json", roots)["path"],
151
+ "app/be",
152
+ )
153
+ self.assertTrue(self.module.is_doc_harness_file("package.json"))
154
+
155
+ def test_nested_source_root_prefers_longest_path(self):
156
+ source_layout = {
157
+ "source_roots": {
158
+ "app_root": {
159
+ "path": "app",
160
+ "package": False,
161
+ },
162
+ "backend": {
163
+ "path": "app/be",
164
+ "package": True,
165
+ },
166
+ }
167
+ }
168
+ roots = self.module.source_roots(source_layout)
169
+
170
+ matched = self.module.match_source_root("app/be/src/user.ts", roots)
171
+
172
+ self.assertEqual(matched["key"], "backend")
173
+ self.assertEqual(matched["path"], "app/be")
174
+
175
+ def test_feature_directory_exists_requires_child_directory(self):
176
+ root = self.make_temp_root()
177
+ active = root / "docs/features/active"
178
+ active.mkdir(parents=True)
179
+
180
+ self.assertFalse(self.module.feature_directory_exists("active"))
181
+
182
+ (active / "FEAT-001-login").mkdir()
183
+
184
+ self.assertTrue(self.module.feature_directory_exists("active"))
185
+ self.assertFalse(self.module.feature_directory_exists("review"))
186
+
187
+ def test_main_blocks_source_changes_without_active_or_review_feature(self):
188
+ root = self.make_temp_root()
189
+ self.write_source_layout(root)
190
+ self.stub_git("feat/login", ["app/be/src/index.ts"])
191
+ self.module.feature_directory_exists = lambda _kind: False
192
+ commands = []
193
+ self.module.run_command = commands.append
194
+
195
+ result, _output = self.run_main()
196
+
197
+ self.assertEqual(result, 1)
198
+ self.assertEqual(commands, [])
199
+
200
+ def test_main_accepts_review_feature_directory_for_source_changes(self):
201
+ root = self.make_temp_root()
202
+ self.write_source_layout(root, package=False)
203
+ self.stub_git("fix/review-patch", ["app/be/src/index.ts"])
204
+ self.stub_package_manager_available()
205
+ self.module.feature_directory_exists = lambda kind: kind == "review"
206
+ commands = []
207
+ self.module.run_command = commands.append
208
+
209
+ result, _output = self.run_main()
210
+
211
+ self.assertEqual(result, 0)
212
+ self.assertEqual(commands, [["npx", "lint-staged"]])
213
+
214
+ def test_unknown_files_excludes_source_and_harness_files(self):
215
+ source_layout = {
216
+ "source_roots": {
217
+ "backend": {
218
+ "path": "app/be",
219
+ "package": True,
220
+ }
221
+ }
222
+ }
223
+ roots = self.module.source_roots(source_layout)
224
+
225
+ unknown = self.module.unknown_files(
226
+ [
227
+ "docs/ARCHITECTURE.md",
228
+ ".flh/runtime/STATE.md",
229
+ "app/be/src/index.ts",
230
+ "HARNESS_MANUAL.md",
231
+ ],
232
+ roots,
233
+ )
234
+
235
+ self.assertEqual(unknown, ["HARNESS_MANUAL.md"])
236
+
237
+ def test_main_blocks_source_when_source_layout_is_not_completed(self):
238
+ root = self.make_temp_root()
239
+ self.write_source_layout(root, status="draft")
240
+ self.stub_git("feat/login", ["app/be/src/index.ts"])
241
+
242
+ result, _output = self.run_main()
243
+
244
+ self.assertEqual(result, 1)
245
+
246
+ def test_main_blocks_source_candidate_outside_configured_roots(self):
247
+ root = self.make_temp_root()
248
+ self.write_source_layout(root)
249
+ self.stub_git("feat/login", ["app/unknown/src/index.ts"])
250
+
251
+ result, _output = self.run_main()
252
+
253
+ self.assertEqual(result, 1)
254
+
255
+ def test_main_blocks_root_package_manager_conflict(self):
256
+ root = self.make_temp_root()
257
+ self.write_source_layout(root, package_manager="npm")
258
+ self.write_package_json(root, "package.json", {"packageManager": "pnpm@9.0.0"})
259
+ self.stub_git("feat/login", ["app/be/src/index.ts"])
260
+ self.stub_package_manager_available()
261
+ self.module.feature_directory_exists = lambda kind: kind == "active"
262
+
263
+ result, _output = self.run_main()
264
+
265
+ self.assertEqual(result, 1)
266
+
267
+ def test_main_runs_affected_package_scripts_and_lint_staged(self):
268
+ root = self.make_temp_root()
269
+ self.write_source_layout(root)
270
+ self.write_package_json(
271
+ root,
272
+ "app/be/package.json",
273
+ {
274
+ "packageManager": "npm@10.0.0",
275
+ "scripts": {
276
+ "lint": "eslint .",
277
+ "typecheck": "tsc --noEmit",
278
+ "test": "vitest run",
279
+ },
280
+ },
281
+ )
282
+ self.stub_git("feat/login", ["app/be/src/index.ts"])
283
+ self.stub_package_manager_available()
284
+ self.module.feature_directory_exists = lambda kind: kind == "active"
285
+ commands = []
286
+ self.module.run_command = commands.append
287
+
288
+ result, _output = self.run_main()
289
+
290
+ self.assertEqual(result, 0)
291
+ self.assertEqual(
292
+ commands,
293
+ [
294
+ ["npm", "--prefix", "app/be", "run", "lint"],
295
+ ["npm", "--prefix", "app/be", "run", "typecheck"],
296
+ ["npm", "--prefix", "app/be", "run", "test"],
297
+ ["npx", "lint-staged"],
298
+ ],
299
+ )
300
+
301
+ def test_state_frontmatter_ignores_body_approval_example(self):
302
+ text = "\n".join(
303
+ [
304
+ "---",
305
+ "current_state: FEATURE_IMPLEMENTATION",
306
+ "approvals: {}",
307
+ "---",
308
+ "",
309
+ "```yaml",
310
+ "approvals:",
311
+ " source_scaffold:",
312
+ " created: true",
313
+ "```",
314
+ ]
315
+ )
316
+
317
+ frontmatter = self.module.parse_frontmatter(text)
318
+
319
+ self.assertEqual(frontmatter["current_state"], "FEATURE_IMPLEMENTATION")
320
+ self.assertFalse(self.module.has_source_scaffold_approval(frontmatter))
321
+
322
+ def test_scaffold_exception_allows_root_extra_files_from_source_layout(self):
323
+ source_layout = {
324
+ "project": {
325
+ "scaffold_extra_root_files": ["turbo.json"],
326
+ },
327
+ "source_roots": {
328
+ "backend": {
329
+ "path": "app/be",
330
+ "package": True,
331
+ }
332
+ },
333
+ }
334
+ state = {
335
+ "current_state": "FEATURE_IMPLEMENTATION",
336
+ "approvals": {
337
+ "source_scaffold": {
338
+ "created": True,
339
+ }
340
+ },
341
+ }
342
+ committed_state = {
343
+ "current_state": "FEATURE_IMPLEMENTATION",
344
+ "approvals": {},
345
+ }
346
+ roots = self.module.source_roots(source_layout)
347
+ source_root = self.module.match_source_root("app/be/src/index.ts", roots)
348
+
349
+ allowed, reason = self.module.scaffold_exception_allowed(
350
+ ["app/be/src/index.ts", "turbo.json", ".flh/runtime/STATE.md"],
351
+ [("app/be/src/index.ts", source_root)],
352
+ source_layout,
353
+ state,
354
+ committed_state,
355
+ )
356
+
357
+ self.assertTrue(allowed, reason)
358
+
359
+ def test_scaffold_exception_blocks_feature_code(self):
360
+ source_layout = {
361
+ "project": {
362
+ "scaffold_extra_root_files": [],
363
+ },
364
+ "source_roots": {
365
+ "backend": {
366
+ "path": "app/be",
367
+ "package": True,
368
+ }
369
+ },
370
+ }
371
+ state = {
372
+ "current_state": "FEATURE_IMPLEMENTATION",
373
+ "approvals": {
374
+ "source_scaffold": {
375
+ "created": True,
376
+ }
377
+ },
378
+ }
379
+ committed_state = {
380
+ "current_state": "FEATURE_IMPLEMENTATION",
381
+ "approvals": {},
382
+ }
383
+ roots = self.module.source_roots(source_layout)
384
+ source_root = self.module.match_source_root("app/be/src/features/login.ts", roots)
385
+
386
+ allowed, _reason = self.module.scaffold_exception_allowed(
387
+ ["app/be/src/features/login.ts"],
388
+ [("app/be/src/features/login.ts", source_root)],
389
+ source_layout,
390
+ state,
391
+ committed_state,
392
+ )
393
+
394
+ self.assertFalse(allowed)
395
+
396
+ def test_scaffold_exception_requires_current_state_approval(self):
397
+ source_layout = {
398
+ "project": {
399
+ "scaffold_extra_root_files": [],
400
+ },
401
+ "source_roots": {
402
+ "backend": {
403
+ "path": "app/be",
404
+ "package": True,
405
+ }
406
+ },
407
+ }
408
+ state = {
409
+ "current_state": "FEATURE_IMPLEMENTATION",
410
+ "approvals": {},
411
+ }
412
+ committed_state = {
413
+ "current_state": "FEATURE_IMPLEMENTATION",
414
+ "approvals": {},
415
+ }
416
+ roots = self.module.source_roots(source_layout)
417
+ source_root = self.module.match_source_root("app/be/src/index.ts", roots)
418
+
419
+ allowed, reason = self.module.scaffold_exception_allowed(
420
+ ["app/be/src/index.ts"],
421
+ [("app/be/src/index.ts", source_root)],
422
+ source_layout,
423
+ state,
424
+ committed_state,
425
+ )
426
+
427
+ self.assertFalse(allowed)
428
+ self.assertIn("approval", reason)
429
+
430
+ def test_main_allows_scaffold_baseline_exception_on_main(self):
431
+ root = self.make_temp_root()
432
+ self.write_source_layout(root)
433
+ self.stub_git(
434
+ "main",
435
+ ["app/be/src/index.ts", ".flh/runtime/STATE.md"],
436
+ )
437
+ self.module.load_state = lambda: {
438
+ "current_state": "FEATURE_IMPLEMENTATION",
439
+ "approvals": {
440
+ "source_scaffold": {
441
+ "created": True,
442
+ }
443
+ },
444
+ }
445
+ self.module.load_committed_state = lambda: {
446
+ "current_state": "FEATURE_IMPLEMENTATION",
447
+ "approvals": {},
448
+ }
449
+ commands = []
450
+ self.module.run_command = commands.append
451
+
452
+ result, _output = self.run_main()
453
+
454
+ self.assertEqual(result, 0)
455
+ self.assertEqual(commands, [])
456
+
457
+ def test_database_baseline_exception_allows_prisma_baseline_files(self):
458
+ source_layout = {
459
+ "project": {
460
+ "scaffold_extra_root_files": [],
461
+ },
462
+ "source_roots": {
463
+ "backend": {
464
+ "path": "app/be",
465
+ "package": True,
466
+ }
467
+ },
468
+ }
469
+ state = {
470
+ "current_state": "FEATURE_IMPLEMENTATION",
471
+ "approvals": {
472
+ "source_scaffold": {
473
+ "created": True,
474
+ },
475
+ "database_baseline": {
476
+ "required": True,
477
+ "verified": True,
478
+ },
479
+ },
480
+ }
481
+ committed_state = {
482
+ "current_state": "FEATURE_IMPLEMENTATION",
483
+ "approvals": {
484
+ "source_scaffold": {
485
+ "created": True,
486
+ },
487
+ },
488
+ }
489
+ roots = self.module.source_roots(source_layout)
490
+ source_root = self.module.match_source_root(
491
+ "app/be/prisma/schema.prisma",
492
+ roots,
493
+ )
494
+
495
+ allowed, reason = self.module.database_baseline_exception_allowed(
496
+ ["app/be/prisma/schema.prisma", ".flh/runtime/STATE.md"],
497
+ [("app/be/prisma/schema.prisma", source_root)],
498
+ source_layout,
499
+ state,
500
+ committed_state,
501
+ )
502
+
503
+ self.assertTrue(allowed, reason)
504
+
505
+ def test_main_allows_database_baseline_exception_on_main(self):
506
+ root = self.make_temp_root()
507
+ self.write_source_layout(root)
508
+ self.stub_git(
509
+ "main",
510
+ ["app/be/prisma/schema.prisma", ".flh/runtime/STATE.md"],
511
+ )
512
+ self.module.load_state = lambda: {
513
+ "current_state": "FEATURE_IMPLEMENTATION",
514
+ "approvals": {
515
+ "source_scaffold": {
516
+ "created": True,
517
+ },
518
+ "database_baseline": {
519
+ "required": True,
520
+ "verified": True,
521
+ },
522
+ },
523
+ }
524
+ self.module.load_committed_state = lambda: {
525
+ "current_state": "FEATURE_IMPLEMENTATION",
526
+ "approvals": {
527
+ "source_scaffold": {
528
+ "created": True,
529
+ },
530
+ },
531
+ }
532
+ commands = []
533
+ self.module.run_command = commands.append
534
+
535
+ result, _output = self.run_main()
536
+
537
+ self.assertEqual(result, 0)
538
+ self.assertEqual(commands, [])
539
+
540
+ def test_database_baseline_approval_allows_no_database_skip(self):
541
+ state = {
542
+ "approvals": {
543
+ "database_baseline": {
544
+ "required": False,
545
+ "skipped": True,
546
+ }
547
+ }
548
+ }
549
+
550
+ self.assertTrue(self.module.has_database_baseline_approval(state))
551
+
552
+ def test_database_baseline_approval_rejects_legacy_verified_without_required(self):
553
+ state = {
554
+ "approvals": {
555
+ "database_baseline": {
556
+ "verified": True,
557
+ }
558
+ }
559
+ }
560
+
561
+ self.assertFalse(self.module.has_database_baseline_approval(state))
562
+
563
+ def test_database_baseline_exception_rejects_no_database_skip_with_source_files(self):
564
+ source_layout = {
565
+ "project": {
566
+ "scaffold_extra_root_files": [],
567
+ },
568
+ "source_roots": {
569
+ "backend": {
570
+ "path": "app/be",
571
+ "package": True,
572
+ }
573
+ },
574
+ }
575
+ state = {
576
+ "current_state": "FEATURE_IMPLEMENTATION",
577
+ "approvals": {
578
+ "source_scaffold": {
579
+ "created": True,
580
+ },
581
+ "database_baseline": {
582
+ "required": False,
583
+ "skipped": True,
584
+ },
585
+ },
586
+ }
587
+ committed_state = {
588
+ "current_state": "FEATURE_IMPLEMENTATION",
589
+ "approvals": {
590
+ "source_scaffold": {
591
+ "created": True,
592
+ },
593
+ },
594
+ }
595
+ roots = self.module.source_roots(source_layout)
596
+ source_root = self.module.match_source_root(
597
+ "app/be/prisma/schema.prisma",
598
+ roots,
599
+ )
600
+
601
+ allowed, reason = self.module.database_baseline_exception_allowed(
602
+ ["app/be/prisma/schema.prisma", ".flh/runtime/STATE.md"],
603
+ [("app/be/prisma/schema.prisma", source_root)],
604
+ source_layout,
605
+ state,
606
+ committed_state,
607
+ )
608
+
609
+ self.assertFalse(allowed)
610
+ self.assertIn("skip approval", reason)
611
+
612
+ def test_database_baseline_exception_blocks_feature_code(self):
613
+ source_layout = {
614
+ "project": {
615
+ "scaffold_extra_root_files": [],
616
+ },
617
+ "source_roots": {
618
+ "backend": {
619
+ "path": "app/be",
620
+ "package": True,
621
+ }
622
+ },
623
+ }
624
+ state = {
625
+ "current_state": "FEATURE_IMPLEMENTATION",
626
+ "approvals": {
627
+ "source_scaffold": {
628
+ "created": True,
629
+ },
630
+ "database_baseline": {
631
+ "required": True,
632
+ "verified": True,
633
+ },
634
+ },
635
+ }
636
+ committed_state = {
637
+ "current_state": "FEATURE_IMPLEMENTATION",
638
+ "approvals": {
639
+ "source_scaffold": {
640
+ "created": True,
641
+ },
642
+ },
643
+ }
644
+ roots = self.module.source_roots(source_layout)
645
+ source_root = self.module.match_source_root("app/be/src/db.ts", roots)
646
+
647
+ allowed, _reason = self.module.database_baseline_exception_allowed(
648
+ ["app/be/src/db.ts", ".flh/runtime/STATE.md"],
649
+ [("app/be/src/db.ts", source_root)],
650
+ source_layout,
651
+ state,
652
+ committed_state,
653
+ )
654
+
655
+ self.assertFalse(allowed)
656
+
657
+
658
+ if __name__ == "__main__":
659
+ unittest.main()