guardian-framework 0.1.163 → 0.1.165

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.
package/dist/cli.js CHANGED
@@ -3206,7 +3206,7 @@ __export(exports_package, {
3206
3206
  bin: () => bin,
3207
3207
  author: () => author
3208
3208
  });
3209
- var name = "guardian-framework", version = "0.1.163", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
3209
+ var name = "guardian-framework", version = "0.1.165", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
3210
3210
  var init_package = __esm(() => {
3211
3211
  exports = {
3212
3212
  ".": {
package/dist/exports.js CHANGED
@@ -1991,7 +1991,7 @@ __export(exports_package, {
1991
1991
  bin: () => bin,
1992
1992
  author: () => author
1993
1993
  });
1994
- var name = "guardian-framework", version = "0.1.163", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
1994
+ var name = "guardian-framework", version = "0.1.165", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
1995
1995
  var init_package = __esm(() => {
1996
1996
  exports = {
1997
1997
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardian-framework",
3
- "version": "0.1.163",
3
+ "version": "0.1.165",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -204,6 +204,16 @@ export function generateEpicTestFiles(options: TddGenerateOptions): string[] {
204
204
 
205
205
  mkdirSync(testDir, { recursive: true });
206
206
 
207
+ // Python: a unique __init__.py per test package. Without it pytest's
208
+ // default "prepend" import mode imports test modules by BASENAME, so
209
+ // <module>/domain-services/test_domain-services.py collides with
210
+ // <other-module>/domain-services/test_domain-services.py and the whole
211
+ // suite fails to collect once two modules share a component name.
212
+ if (language === "python") {
213
+ const initPath = join(testDir, "__init__.py");
214
+ if (!existsSync(initPath)) writeFileSync(initPath, "", "utf-8");
215
+ }
216
+
207
217
  const fileName = getTestFileName(componentId, language);
208
218
  const filePath = join(testDir, fileName);
209
219
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-11-13T20:44:26Z",
2
+ "timestamp": "2026-11-13T21:39:58Z",
3
3
  "mode": "all",
4
4
  "stages_run": [],
5
5
  "summary": {
@@ -16,13 +16,14 @@
16
16
  # freeze.sh status [--root DIR]
17
17
  # freeze.sh list [--root DIR]
18
18
  # freeze.sh next [--root DIR] # next module eligible to freeze, in order
19
+ # freeze.sh unfreeze --module <m> --reason "<why>" [--root DIR]
19
20
  #
20
21
  # Exit: 0 pass · 1 verification failed · 2 usage/state error
21
22
  # ============================================================================
22
23
  set -uo pipefail
23
24
 
24
25
  CMD="${1:-}"; shift || true
25
- MODULE=""; ROOT_OPT=""; BASE="main"; JSON_OUT=0; COMMIT=""
26
+ MODULE=""; ROOT_OPT=""; BASE="main"; JSON_OUT=0; COMMIT=""; REASON=""
26
27
 
27
28
  while [ $# -gt 0 ]; do
28
29
  case "$1" in
@@ -31,6 +32,7 @@ while [ $# -gt 0 ]; do
31
32
  --base) BASE="${2:-main}"; shift 2 ;;
32
33
  --commit) COMMIT="${2:-}"; shift 2 ;;
33
34
  --json) JSON_OUT=1; shift ;;
35
+ --reason) REASON="${2:-}"; shift 2 ;;
34
36
  *) echo "unknown argument: $1" >&2; exit 2 ;;
35
37
  esac
36
38
  done
@@ -54,15 +56,15 @@ usage() {
54
56
  }
55
57
 
56
58
  case "$CMD" in
57
- verify | record | status | list | next) ;;
59
+ verify | record | status | list | next | unfreeze) ;;
58
60
  *) usage; exit 2 ;;
59
61
  esac
60
62
 
61
- exec python3 - "$CMD" "$ROOT" "$GIT_TOP" "$STATE" "$MODULE" "$BASE" "$JSON_OUT" "$COMMIT" <<'PY'
63
+ exec python3 - "$CMD" "$ROOT" "$GIT_TOP" "$STATE" "$MODULE" "$BASE" "$JSON_OUT" "$COMMIT" "$REASON" <<'PY'
62
64
  import json, os, re, subprocess, sys
63
65
  from datetime import datetime, timezone
64
66
 
65
- cmd, root, git_top, state_path, module, base, json_out, commit = sys.argv[1:9]
67
+ cmd, root, git_top, state_path, module, base, json_out, commit, reason = sys.argv[1:10]
66
68
  json_out = json_out == "1"
67
69
 
68
70
  MODS = os.path.join(root, ".pi", "architecture", "modules")
@@ -232,6 +234,23 @@ def classify(code):
232
234
 
233
235
  AC_ID = re.compile(r"\b[A-Z]{1,3}-\d{1,3}\b")
234
236
 
237
+ def find_all_test_basenames():
238
+ """basename -> [paths] for every test file in the project."""
239
+ out = {}
240
+ for base_dir in ("tests",):
241
+ base_path = os.path.join(root, base_dir)
242
+ if not os.path.isdir(base_path):
243
+ continue
244
+ for dp, _dn, fs in os.walk(base_path):
245
+ if "__pycache__" in dp:
246
+ continue
247
+ for f in fs:
248
+ stem = f.rsplit(".", 1)[0]
249
+ if stem.startswith("test_") or stem.endswith("_test"):
250
+ out.setdefault(f, []).append(os.path.join(dp, f))
251
+ return out
252
+
253
+
235
254
  def find_tests(slug):
236
255
  """Test files for the module. `tests/` wins; `src/` is a fallback only for
237
256
  projects that colocate tests, so source files are never counted as suites."""
@@ -391,6 +410,50 @@ def verify(slug):
391
410
  checks.append(("red state", ok_red, f"{status}: {reason}"))
392
411
  ok_all &= ok_red
393
412
 
413
+ # 3b. static types — a frozen contract that does not type-check is a defect.
414
+ # This is what caught `Invoice.record` shadowing `AggregateRoot.record`
415
+ # (a classmethod factory hiding the event recorder) inside a frozen module.
416
+ pyproject = read(os.path.join(root, "pyproject.toml"))
417
+ has_mypy_cfg = ("[tool.mypy]" in pyproject
418
+ or os.path.exists(os.path.join(root, "mypy.ini"))
419
+ or "[mypy]" in read(os.path.join(root, "setup.cfg")))
420
+ mypy = os.path.join(root, ".venv", "bin", "mypy")
421
+ # dedupe: for a slug without hyphens both candidates are the same path, and
422
+ # passing it twice makes mypy report "Duplicate module named …"
423
+ pkg_dirs = sorted({d for d in (os.path.join(root, "src", slug),
424
+ os.path.join(root, "src", slug.replace("-", "_")))
425
+ if os.path.isdir(d)})
426
+ if has_mypy_cfg and pkg_dirs and (os.path.exists(mypy) or os.path.exists("/usr/bin/env")):
427
+ try:
428
+ tc = subprocess.run([mypy if os.path.exists(mypy) else "mypy", *pkg_dirs],
429
+ cwd=root, capture_output=True, text=True, timeout=600)
430
+ ok_tc = tc.returncode == 0
431
+ tail = [l for l in (tc.stdout or "").splitlines() if ": error:" in l][:3]
432
+ checks.append(("type check", ok_tc,
433
+ "mypy clean" if ok_tc else "mypy errors: " + " | ".join(tail)))
434
+ ok_all &= ok_tc
435
+ except Exception as e:
436
+ checks.append(("type check", True, f"skipped ({e})"))
437
+
438
+ # 3c. test isolation: duplicate test basenames across packages break pytest
439
+ # collection entirely (prepend import mode imports by basename), which
440
+ # silently hides every suite behind "2 errors during collection".
441
+ if tests:
442
+ by_base = {}
443
+ for t in tests:
444
+ by_base.setdefault(os.path.basename(t), []).append(t)
445
+ for base, paths in find_all_test_basenames().items():
446
+ if len(paths) < 2:
447
+ continue
448
+ dirs = {os.path.dirname(p) for p in paths}
449
+ isolated = all(os.path.exists(os.path.join(d, "__init__.py")) for d in dirs)
450
+ if not isolated:
451
+ checks.append(("test isolation", False,
452
+ f"'{base}' exists in {len(dirs)} packages without __init__.py — "
453
+ f"pytest collection collides; add __init__.py or set import-mode=importlib"))
454
+ ok_all = False
455
+ break
456
+
394
457
  # 4b. recorded evidence must still hold (spec: a contract change means
395
458
  # the module is no longer frozen and must be re-frozen).
396
459
  st_rec = load_state()
@@ -519,6 +582,31 @@ if cmd == "next":
519
582
  print("no eligible module: every module is frozen/implemented or blocked by an unfrozen dependency")
520
583
  raise SystemExit(1)
521
584
 
585
+ if cmd == "unfreeze":
586
+ if not module:
587
+ print("usage: freeze.sh unfreeze --module <m> --reason \"why\"", file=sys.stderr)
588
+ raise SystemExit(2)
589
+ if not reason:
590
+ print("refusing to unfreeze without a reason: a contract change must be recorded",
591
+ file=sys.stderr)
592
+ raise SystemExit(2)
593
+ slug = slugify(module)
594
+ st = load_state()
595
+ e = entry(st, slug)
596
+ if not e:
597
+ print(f"{slug} is not recorded — nothing to unfreeze", file=sys.stderr)
598
+ raise SystemExit(2)
599
+ history = e.get("history") or []
600
+ if e.get("evidence"):
601
+ history.append({"evidence": e["evidence"], "state": e.get("state"),
602
+ "unfrozenAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
603
+ "reason": reason})
604
+ set_entry(st, slug, state="unfrozen", contractFrozen=False, evidence=None, history=history)
605
+ save_state(st)
606
+ print(f"unfrozen: {slug} — {reason}")
607
+ print("Re-run Phase A for the changed contract, then record again.")
608
+ raise SystemExit(0)
609
+
522
610
  # ── commands ────────────────────────────────────────────────────────────────
523
611
  if cmd == "list":
524
612
  st = load_state()