guardian-framework 0.1.162 → 0.1.164

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.162", 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.164", 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.162", 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.164", 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.162",
3
+ "version": "0.1.164",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-11-13T20:21:08Z",
2
+ "timestamp": "2026-11-13T21:11:04Z",
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")
@@ -245,6 +247,13 @@ def find_tests(slug):
245
247
  if not re.search(r"\.(py|ts|rs|go|java)$", f):
246
248
  continue
247
249
  low = os.path.join(dp, f).lower()
250
+ if "__pycache__" in low:
251
+ continue
252
+ # Only actual test files: conftest.py, _fixtures.py and
253
+ # __init__.py are support, not suites (they made counts lie).
254
+ stem = f.rsplit(".", 1)[0]
255
+ if not (stem.startswith("test_") or stem.endswith("_test") or stem in ("tests", "test")):
256
+ continue
248
257
  if slug in low or slug.replace("-", "") in low.replace("-", ""):
249
258
  hits.append(os.path.join(dp, f))
250
259
  if hits:
@@ -337,6 +346,8 @@ def verify(slug):
337
346
  sig_files = []
338
347
  for d in src_dirs:
339
348
  for dp, _dn, fs in os.walk(d):
349
+ if "__pycache__" in dp:
350
+ continue
340
351
  sig_files += [os.path.join(dp, f) for f in fs if re.search(r"\.(py|ts|java|rs|go)$", f)]
341
352
  ok_sigs = bool(sig_files)
342
353
  checks.append(("frozen signatures", ok_sigs, f"{len(sig_files)} source file(s)" if ok_sigs else f"no src/{slug}/"))
@@ -382,6 +393,87 @@ def verify(slug):
382
393
  checks.append(("red state", ok_red, f"{status}: {reason}"))
383
394
  ok_all &= ok_red
384
395
 
396
+ # 3b. static types — a frozen contract that does not type-check is a defect.
397
+ # This is what caught `Invoice.record` shadowing `AggregateRoot.record`
398
+ # (a classmethod factory hiding the event recorder) inside a frozen module.
399
+ pyproject = read(os.path.join(root, "pyproject.toml"))
400
+ has_mypy_cfg = ("[tool.mypy]" in pyproject
401
+ or os.path.exists(os.path.join(root, "mypy.ini"))
402
+ or "[mypy]" in read(os.path.join(root, "setup.cfg")))
403
+ mypy = os.path.join(root, ".venv", "bin", "mypy")
404
+ # dedupe: for a slug without hyphens both candidates are the same path, and
405
+ # passing it twice makes mypy report "Duplicate module named …"
406
+ pkg_dirs = sorted({d for d in (os.path.join(root, "src", slug),
407
+ os.path.join(root, "src", slug.replace("-", "_")))
408
+ if os.path.isdir(d)})
409
+ if has_mypy_cfg and pkg_dirs and (os.path.exists(mypy) or os.path.exists("/usr/bin/env")):
410
+ try:
411
+ tc = subprocess.run([mypy if os.path.exists(mypy) else "mypy", *pkg_dirs],
412
+ cwd=root, capture_output=True, text=True, timeout=600)
413
+ ok_tc = tc.returncode == 0
414
+ tail = [l for l in (tc.stdout or "").splitlines() if ": error:" in l][:3]
415
+ checks.append(("type check", ok_tc,
416
+ "mypy clean" if ok_tc else "mypy errors: " + " | ".join(tail)))
417
+ ok_all &= ok_tc
418
+ except Exception as e:
419
+ checks.append(("type check", True, f"skipped ({e})"))
420
+
421
+ # 4b. recorded evidence must still hold (spec: a contract change means
422
+ # the module is no longer frozen and must be re-frozen).
423
+ st_rec = load_state()
424
+ ent = entry(st_rec, slug)
425
+ if ent.get("state") == "frozen" and (ent.get("evidence") or {}).get("commit"):
426
+ ev = ent["evidence"]
427
+ sha = ev["commit"]
428
+ base_ref = ev.get("branch") or base
429
+ # realpath both sides: on macOS /tmp is a symlink to /private/tmp, and a
430
+ # symlinked project dir made relpath produce ../../../tmp/... so git saw
431
+ # a path outside the repo and reported "no changes".
432
+ rel = os.path.relpath(os.path.realpath(root), os.path.realpath(git_top))
433
+ prefix = "" if rel == "." else f"{rel}/"
434
+
435
+ on_base = subprocess.run(
436
+ ["git", "-C", git_top, "merge-base", "--is-ancestor", sha, base_ref],
437
+ capture_output=True).returncode == 0
438
+ checks.append(("recorded commit", on_base,
439
+ f"{sha[:8]} is an ancestor of {base_ref}" if on_base
440
+ else f"{sha[:8]} is NOT an ancestor of {base_ref}"))
441
+ ok_all &= on_base
442
+
443
+ missing = [p for p in (ev.get("contracts") or []) if not os.path.exists(os.path.join(root, p))]
444
+ checks.append(("evidence files", not missing,
445
+ "recorded contract/test files exist" if not missing
446
+ else "missing: " + ", ".join(missing)))
447
+ ok_all &= not missing
448
+
449
+ def changed_since(paths):
450
+ """Files changed since the freeze — committed OR uncommitted.
451
+
452
+ `git diff <sha> -- <paths>` compares the recorded commit against the
453
+ WORKING TREE, so it catches both later commits and local edits.
454
+ Diffing commit-to-commit silently ignored uncommitted tampering.
455
+ """
456
+ if not paths:
457
+ return []
458
+ args = ["git", "-C", git_top, "diff", "--name-only", sha, "--"]
459
+ args += [f"{prefix}{p}" for p in paths]
460
+ out = subprocess.run(args, capture_output=True, text=True).stdout.strip()
461
+ if os.environ.get("GUARDIAN_FREEZE_DEBUG"):
462
+ print(f"DEBUG changed_since args={args} out={out!r}", file=sys.stderr)
463
+ return [l for l in out.splitlines() if l.strip()]
464
+
465
+ contract_changes = changed_since(ev.get("contracts") or [])
466
+ checks.append(("contracts unchanged", not contract_changes,
467
+ "contracts unchanged since the freeze" if not contract_changes
468
+ else "changed after the freeze — re-freeze: " + ", ".join(contract_changes)))
469
+ ok_all &= not contract_changes
470
+
471
+ test_changes = changed_since(ev.get("tests") or [])
472
+ checks.append(("tests unchanged", not test_changes,
473
+ "frozen test files unchanged" if not test_changes
474
+ else "tests changed after the freeze — re-freeze: " + ", ".join(test_changes[:3])))
475
+ ok_all &= not test_changes
476
+
385
477
  # 5. dependencies frozen/implemented
386
478
  bad_deps = []
387
479
  st = load_state()
@@ -454,6 +546,31 @@ if cmd == "next":
454
546
  print("no eligible module: every module is frozen/implemented or blocked by an unfrozen dependency")
455
547
  raise SystemExit(1)
456
548
 
549
+ if cmd == "unfreeze":
550
+ if not module:
551
+ print("usage: freeze.sh unfreeze --module <m> --reason \"why\"", file=sys.stderr)
552
+ raise SystemExit(2)
553
+ if not reason:
554
+ print("refusing to unfreeze without a reason: a contract change must be recorded",
555
+ file=sys.stderr)
556
+ raise SystemExit(2)
557
+ slug = slugify(module)
558
+ st = load_state()
559
+ e = entry(st, slug)
560
+ if not e:
561
+ print(f"{slug} is not recorded — nothing to unfreeze", file=sys.stderr)
562
+ raise SystemExit(2)
563
+ history = e.get("history") or []
564
+ if e.get("evidence"):
565
+ history.append({"evidence": e["evidence"], "state": e.get("state"),
566
+ "unfrozenAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
567
+ "reason": reason})
568
+ set_entry(st, slug, state="unfrozen", contractFrozen=False, evidence=None, history=history)
569
+ save_state(st)
570
+ print(f"unfrozen: {slug} — {reason}")
571
+ print("Re-run Phase A for the changed contract, then record again.")
572
+ raise SystemExit(0)
573
+
457
574
  # ── commands ────────────────────────────────────────────────────────────────
458
575
  if cmd == "list":
459
576
  st = load_state()
@@ -472,8 +589,8 @@ if cmd == "list":
472
589
 
473
590
  if cmd == "status":
474
591
  if not module:
475
- print("usage: freeze.sh status --module <m>", file=sys.stderr)
476
- raise SystemExit(2)
592
+ # no module given: same as list
593
+ cmd = "list"
477
594
  slug = slugify(module)
478
595
  st = load_state()
479
596
  e = entry(st, slug)