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 +1 -1
- package/dist/exports.js +1 -1
- package/package.json +1 -1
- package/templates/pi/preflight_report.json +1 -1
- package/templates/pi/scripts/freeze.sh +123 -6
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.
|
|
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.
|
|
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
|
@@ -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:
|
|
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
|
-
|
|
476
|
-
|
|
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)
|