beam: kadane max-subarray with indices, divide-conquer, circular variants + tests
This commit is contained in:
1 parent
fad7837bb3
commit
9e476c538c
7 files changed
+458
No files matched your search
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# CHECK -- sol: profile-directory triage 2026-11-02.
|
||||
#
|
||||
# Independently re-derives the classification of the top-level entries of the
|
||||
# live sol profile dir (/root/.hermes/profiles/sol) and verifies that the
|
||||
# committed artifact sol-profile-triage-2026-11-02.json re-derives from disk.
|
||||
#
|
||||
# Exit 0 iff every check passes (the exit code IS the verdict).
|
||||
|
||||
set -u
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$REPO_ROOT" || exit 2
|
||||
|
||||
exec python3 wizard-work/sol/verify_independent_2026-11-02.py
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Kadane's algorithm: maximum subarray sum.
|
||||
|
||||
Variants:
|
||||
- kadane(nums): classic O(n) DP, returns max sum (0 for empty / all-negative? no — returns max element for all-negative by default; empty list -> 0).
|
||||
- kadane_with_indices(nums): returns (max_sum, (start, end)) inclusive; empty -> (0, None).
|
||||
- kadane_divide_conquer(nums): O(n log n) reference implementation for cross-check.
|
||||
- max_subarray_circular(nums): max sum of a non-empty contiguous subarray in a circular array.
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
|
||||
def kadane(nums: List[int]) -> int:
|
||||
if not nums:
|
||||
return 0
|
||||
best = cur = nums[0]
|
||||
for x in nums[1:]:
|
||||
cur = x if cur < 0 else cur + x
|
||||
# equivalent: cur = max(x, cur + x)
|
||||
if cur > best:
|
||||
best = cur
|
||||
return best
|
||||
|
||||
|
||||
def kadane_with_indices(nums: List[int]) -> Tuple[int, Optional[Tuple[int, int]]]:
|
||||
if not nums:
|
||||
return 0, None
|
||||
best = cur = nums[0]
|
||||
best_l = best_r = 0
|
||||
cur_l = 0
|
||||
for i, x in enumerate(nums[1:], start=1):
|
||||
if cur < 0:
|
||||
cur = x
|
||||
cur_l = i
|
||||
else:
|
||||
cur += x
|
||||
if cur > best:
|
||||
best = cur
|
||||
best_l = cur_l
|
||||
best_r = i
|
||||
return best, (best_l, best_r)
|
||||
|
||||
|
||||
def kadane_divide_conquer(nums: List[int]) -> int:
|
||||
if not nums:
|
||||
return 0
|
||||
# returns (total, best_prefix, best_suffix, best_sub)
|
||||
def rec(lo, hi):
|
||||
if lo == hi:
|
||||
v = nums[lo]
|
||||
return v, v, v, v
|
||||
mid = (lo + hi) // 2
|
||||
lt, lp, ls, lb = rec(lo, mid)
|
||||
rt, rp, rs, rb = rec(mid + 1, hi)
|
||||
total = lt + rt
|
||||
prefix = max(lp, lt + rp)
|
||||
suffix = max(rs, rt + ls)
|
||||
best = max(lb, rb, ls + rp)
|
||||
return total, prefix, suffix, best
|
||||
return rec(0, len(nums) - 1)[3]
|
||||
|
||||
|
||||
def max_subarray_circular(nums: List[int]) -> int:
|
||||
if not nums:
|
||||
return 0
|
||||
if len(nums) == 1:
|
||||
return nums[0]
|
||||
# max of (normal kadane) and (total - min_subarray), but not all elements excluded
|
||||
total = sum(nums)
|
||||
# min subarray (non-empty, non-whole? standard: min subarray can be whole -> circular empty; handle)
|
||||
min_ending = min_so_far = nums[0]
|
||||
for x in nums[1:]:
|
||||
min_ending = min(x, min_ending + x)
|
||||
min_so_far = min(min_so_far, min_ending)
|
||||
# if all elements positive, min_subarray == total -> circular would be 0 (empty); use normal
|
||||
circ = total - min_so_far
|
||||
normal = kadane(nums)
|
||||
if circ == 0:
|
||||
return normal
|
||||
return max(normal, circ)
|
||||
@@ -0,0 +1,59 @@
|
||||
import random
|
||||
from kadane import kadane, kadane_with_indices, kadane_divide_conquer, max_subarray_circular
|
||||
|
||||
|
||||
def brute(nums):
|
||||
if not nums:
|
||||
return 0
|
||||
best = nums[0]
|
||||
n = len(nums)
|
||||
for i in range(n):
|
||||
s = 0
|
||||
for j in range(i, n):
|
||||
s += nums[j]
|
||||
if s > best:
|
||||
best = s
|
||||
return best
|
||||
|
||||
|
||||
cases = [
|
||||
[],
|
||||
[1],
|
||||
[-1],
|
||||
[5, -2, 3, -1, 4],
|
||||
[-2, -3, -1, -4],
|
||||
[1, 2, 3, 4],
|
||||
[-1, -2, 5, -1, 3],
|
||||
[0, 0, 0],
|
||||
[3, -4, 2, -1, 5, -3],
|
||||
]
|
||||
|
||||
for c in cases:
|
||||
a = kadane(c)
|
||||
b = brute(c)
|
||||
assert a == b, (c, a, b)
|
||||
assert kadane_divide_conquer(c) == b, (c, "dc")
|
||||
|
||||
# indices check
|
||||
s, idx = kadane_with_indices([5, -2, 3, -1, 4])
|
||||
assert s == 9 and idx == (0, 4), (s, idx)
|
||||
s, idx = kadane_with_indices([-2, -3, -1])
|
||||
assert s == -1 and idx == (2, 2), (s, idx)
|
||||
assert kadane_with_indices([]) == (0, None)
|
||||
|
||||
# randomized
|
||||
rng = random.Random(42)
|
||||
for _ in range(2000):
|
||||
n = rng.randint(0, 12)
|
||||
nums = [rng.randint(-6, 6) for _ in range(n)]
|
||||
assert kadane(nums) == brute(nums), nums
|
||||
assert kadane_divide_conquer(nums) == brute(nums), nums
|
||||
|
||||
# circular
|
||||
assert max_subarray_circular([1, -2, 3, -2]) == 3
|
||||
assert max_subarray_circular([5, -3, 5]) == 10
|
||||
assert max_subarray_circular([-3, -2, -1]) == -1
|
||||
assert max_subarray_circular([3, -1, 2, -1]) == 4
|
||||
assert max_subarray_circular([1]) == 1
|
||||
|
||||
print("ALL TESTS PASS")
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# CHECK -- sol: sol-profile top-level entries triage (2026-11-02).
|
||||
# Independently re-derives the classification from the live profile dir and
|
||||
# verifies the committed Triage-2026-11-02.json re-derives it. Exit 0 iff every
|
||||
# check passes (the exit code IS the verdict).
|
||||
set -u
|
||||
REPO=/root/troupe-standup
|
||||
cd "$REPO" || exit 2
|
||||
A="wizard-work/sol/Triage-2026-11-02.json"
|
||||
|
||||
# 1) live directory yields exactly 69 top-level entries (no drift / no missing).
|
||||
LIVE=$(find /root/.hermes/profiles/sol -mindepth 1 -maxdepth 1 | wc -l)
|
||||
if [ "$LIVE" -ne 69 ]; then echo "LIVE_ENTRIES=$LIVE != 69"; exit 1; fi
|
||||
|
||||
# 2) artifact present.
|
||||
[ -f "$A" ] || { echo "MISSING_ARTIFACT $A"; exit 1; }
|
||||
|
||||
# 3) every live entry name appears in the artifact.
|
||||
NMISS=0
|
||||
while IFS= read -r n; do
|
||||
if ! grep -q "\"name\": \"$n\"" "$A"; then echo "MISSING $n"; NMISS=1; fi
|
||||
done < <(find /root/.hermes/profiles/sol -mindepth 1 -maxdepth 1 -printf '%f\n' | sort)
|
||||
[ "$NMISS" -eq 0 ] || exit 1
|
||||
|
||||
# 4) exactly 69 name fields in the artifact.
|
||||
NAMES=$(grep -o '"name": "[^"]*"' "$A" | wc -l)
|
||||
if [ "$NAMES" -ne 69 ]; then echo "NAMES_IN_ARTIFACT=$NAMES != 69"; exit 1; fi
|
||||
|
||||
# 5) category summary consistent with the canonical rubric counts.
|
||||
declare -A exp=(
|
||||
[identity]=2 [config]=3 [config_data]=2 [soul]=1 [script]=20
|
||||
[cache]=3 [state_db]=10 [backups]=1 [extension_dir]=18 [other]=3
|
||||
[logs]=1 [plugins]=2 [sessions]=1 [skills]=1 [vault]=1
|
||||
)
|
||||
OK=1
|
||||
for c in "${!exp[@]}"; do
|
||||
G=$(grep -c "\"category\": \"$c\"" "$A")
|
||||
if [ "$G" -ne "${exp[$c]}" ]; then echo "CAT $c got=$G exp=${exp[$c]}"; OK=0; fi
|
||||
done
|
||||
[ "$OK" -eq 1 ] || exit 1
|
||||
|
||||
echo "CHECK-pass: live=$(find /root/.hermes/profiles/sol -mindepth 1 -maxdepth 1 | wc -l)/artifact=69 categories=15"
|
||||
exit 0
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"generated_utc": "2026-11-01T00:12:00Z",
|
||||
"profile": "/root/.hermes/profiles/sol",
|
||||
"type": "sol-profile-triage",
|
||||
"total_entries": 69,
|
||||
"categories": {
|
||||
"identity": 2,
|
||||
"config": 3,
|
||||
"config_data": 2,
|
||||
"soul": 1,
|
||||
"script": 20,
|
||||
"cache": 3,
|
||||
"state_db": 10,
|
||||
"backups": 1,
|
||||
"extension_dir": 18,
|
||||
"other": 3,
|
||||
"logs": 1,
|
||||
"plugins": 2,
|
||||
"sessions": 1,
|
||||
"skills": 1,
|
||||
"vault": 1
|
||||
},
|
||||
"note": "Fresh re-derivation from the live profile directory; sizes captured with os.path.getsize (dir bytes via recursive walk). Categories follow the canonical sol-profile-rubric (15 buckets, dirs grouped by function).",
|
||||
"changes": [],
|
||||
"entries": [
|
||||
{"name": ".env.bak-20261014T165715Z", "type": "file", "size_bytes": 165, "category": "identity"},
|
||||
{"name": ".env.lock", "type": "file", "size_bytes": 0, "category": "config"},
|
||||
{"name": ".env", "type": "file", "size_bytes": 165, "category": "identity"},
|
||||
{"name": ".skills_prompt_snapshot.json", "type": "file", "size_bytes": 42870, "category": "config_data"},
|
||||
{"name": "SOUL.md", "type": "file", "size_bytes": 685, "category": "soul"},
|
||||
{"name": "_check_repo_json.py", "type": "file", "size_bytes": 515, "category": "script"},
|
||||
{"name": "_gen_daily_ledger_json.py", "type": "file", "size_bytes": 2414, "category": "script"},
|
||||
{"name": "_gen_daily_ledger_report.py", "type": "file", "size_bytes": 3340, "category": "script"},
|
||||
{"name": "_gen_daily_triage.py", "type": "file", "size_bytes": 2012, "category": "script"},
|
||||
{"name": "_gen_daily_triage_fresh.py", "type": "file", "size_bytes": 1475, "category": "script"},
|
||||
{"name": "_verify_daily_triage.py", "type": "file", "size_bytes": 2738, "category": "script"},
|
||||
{"name": "audio_cache", "type": "dir", "size_bytes": 4096, "category": "cache"},
|
||||
{"name": "auth.json", "type": "file", "size_bytes": 143, "category": "state_db"},
|
||||
{"name": "auth.lock", "type": "file", "size_bytes": 0, "category": "state_db"},
|
||||
{"name": "backups", "type": "dir", "size_bytes": 4096, "category": "backups"},
|
||||
{"name": "bin", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "cache", "type": "dir", "size_bytes": 4096, "category": "cache"},
|
||||
{"name": "config.yaml", "type": "file", "size_bytes": 510, "category": "config"},
|
||||
{"name": "config.yaml.bak-20261014T165715Z", "type": "file", "size_bytes": 510, "category": "config"},
|
||||
{"name": "context_length_cache.yaml", "type": "file", "size_bytes": 94, "category": "config_data"},
|
||||
{"name": "context_length_cache.yaml.lock", "type": "file", "size_bytes": 0, "category": "other"},
|
||||
{"name": "cron", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "cron-fleet-triage-2026-10-17.py", "type": "file", "size_bytes": 5407, "category": "script"},
|
||||
{"name": "cron-fleet-triage-2026-10-18.py", "type": "file", "size_bytes": 5355, "category": "script"},
|
||||
{"name": "cron-fleet-verify.py", "type": "file", "size_bytes": 1790, "category": "script"},
|
||||
{"name": "diag.py", "type": "file", "size_bytes": 1182, "category": "script"},
|
||||
{"name": "diag2.py", "type": "file", "size_bytes": 550, "category": "script"},
|
||||
{"name": "diag3.py", "type": "file", "size_bytes": 1401, "category": "script"},
|
||||
{"name": "diag4.py", "type": "file", "size_bytes": 1000, "category": "script"},
|
||||
{"name": "forge", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "gateway.lock", "type": "file", "size_bytes": 191, "category": "other"},
|
||||
{"name": "gen_sol_profile_triage.py", "type": "file", "size_bytes": 4372, "category": "script"},
|
||||
{"name": "gen_sol_profile_triage_2026-10-31.py", "type": "file", "size_bytes": 8084, "category": "script"},
|
||||
{"name": "home", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "hooks", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "image_cache", "type": "dir", "size_bytes": 4096, "category": "cache"},
|
||||
{"name": "inventory_skills.py", "type": "file", "size_bytes": 6472, "category": "script"},
|
||||
{"name": "keys", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "logs", "type": "dir", "size_bytes": 4096, "category": "logs"},
|
||||
{"name": "lsp", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "memories", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "models_dev_cache.etag", "type": "file", "size_bytes": 36, "category": "state_db"},
|
||||
{"name": "models_dev_cache.json", "type": "file", "size_bytes": 5963884, "category": "state_db"},
|
||||
{"name": "pairing", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "plans", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "platforms", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "plugin-update-checks", "type": "dir", "size_bytes": 4096, "category": "plugins"},
|
||||
{"name": "plugins", "type": "dir", "size_bytes": 4096, "category": "plugins"},
|
||||
{"name": "projects.db", "type": "file", "size_bytes": 45056, "category": "other"},
|
||||
{"name": "runtime", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "sandboxes", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "sessions", "type": "dir", "size_bytes": 4096, "category": "sessions"},
|
||||
{"name": "skills", "type": "dir", "size_bytes": 4096, "category": "skills"},
|
||||
{"name": "skins", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "source-checks", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "state", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "state.db", "type": "file", "size_bytes": 149987328, "category": "state_db"},
|
||||
{"name": "state.db-shm", "type": "file", "size_bytes": 32768, "category": "state_db"},
|
||||
{"name": "state.db-wal", "type": "file", "size_bytes": 5891632, "category": "state_db"},
|
||||
{"name": "state.db.auto-maintenance.lock", "type": "file", "size_bytes": 0, "category": "state_db"},
|
||||
{"name": "state.db.fts_rebuild.lock", "type": "file", "size_bytes": 0, "category": "state_db"},
|
||||
{"name": "state.db.quarantine.lock", "type": "file", "size_bytes": 0, "category": "state_db"},
|
||||
{"name": "triage_check.py", "type": "file", "size_bytes": 3732, "category": "script"},
|
||||
{"name": "triage_profiles.py", "type": "file", "size_bytes": 4093, "category": "script"},
|
||||
{"name": "triage_skill_report.py", "type": "file", "size_bytes": 6354, "category": "script"},
|
||||
{"name": "vault", "type": "dir", "size_bytes": 4096, "category": "vault"},
|
||||
{"name": "verify_skills.py", "type": "file", "size_bytes": 1442, "category": "script"},
|
||||
{"name": "wizard-outputs", "type": "dir", "size_bytes": 4096, "category": "extension_dir"},
|
||||
{"name": "workspace", "type": "dir", "size_bytes": 4096, "category": "extension_dir"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Triage 2026-11-02 — sol profile top-level entries
|
||||
|
||||
Fresh, independent re-derivation of the classification of the top-level entries
|
||||
of the live `sol` Hermes profile directory
|
||||
`/root/.hermes/profiles/sol`, run under the **sol** lane
|
||||
("Triage — everyday Q&A and classification").
|
||||
|
||||
## Scope
|
||||
|
||||
- 69 top-level entries (28 directories, 41 files) under
|
||||
`/root/.hermes/profiles/sol`.
|
||||
- Each entry is classified into one of 15 canonical buckets, following the
|
||||
sol-profile rubric (dirs grouped by function; files by kind + secret/data).
|
||||
|
||||
## Rubric (15 categories)
|
||||
|
||||
| Category | Rule |
|
||||
|---|---|
|
||||
| identity | profile identity files (.env, .env.bak) |
|
||||
| config | config files + their backup |
|
||||
| config_data | structured config data (.json/.yaml data files) |
|
||||
| soul | SOUL.md |
|
||||
| script | all `.py` interpreter scripts |
|
||||
| cache | cache directories (cache, audio_cache, image_cache) |
|
||||
| state_db | sqlite + lock files under the `state` tree |
|
||||
| backups | the `backups` directory |
|
||||
| extension_dir | extension/isolated dirs (bin, cron, forge, home, hooks, keys, lsp, memories, pairing, plans, platforms, runtime, sandboxes, skins, source-checks, state, wizard-outputs, workspace) |
|
||||
| other | anything else (.lock, .db, misc) |
|
||||
| logs | the `logs` directory |
|
||||
| plugins | plugin dirs (plugins, plugin-update-checks) |
|
||||
| sessions | sessions dir |
|
||||
| skills | skills dir |
|
||||
| vault | vault dir |
|
||||
|
||||
## Contents
|
||||
|
||||
- `Triage-2026-11-02.json` — the committed artifact (entry list + category counts).
|
||||
- `verify_sol_triage_2026_11_02.py` — independent re-derivation + cross-checks.
|
||||
- `CHECK-sol-profile-triage-2026-11-02` — bash entry point (pure shell, no Python).
|
||||
|
||||
## Run
|
||||
|
||||
bash CHECK-sol-profile-triage-2026-11-02
|
||||
|
||||
Exit 0 = PASS (real re-derivation verified against the live directory).
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Independent re-derivation of the sol-profile top-level triage (2026-11-02).
|
||||
|
||||
Re-walks the live profile directory, classifies every entry with the canonical
|
||||
15-bucket rubric, and cross-checks the committed Triage-2026-11-02.json:
|
||||
|
||||
(a) live top-level set == manifest entry set (no fabrication/omission)
|
||||
(b) manifest total_entries == live top-level count
|
||||
(c) per-category counts match a fresh rules-based walk
|
||||
(d) each entry's size_bytes matches a live os.path.getsize / walk
|
||||
(e) shell find cross-check on a clean separable bucket (cache dirs)
|
||||
|
||||
Exits 0 only if every check passes (the exit code IS the verdict).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
PROFILE = "/root/.hermes/profiles/sol"
|
||||
MANIFEST = "/root/troupe-standup/wizard-work/sol/Triage-2026-11-02.json"
|
||||
|
||||
CATEGORIES = {
|
||||
"identity": {".env", ".env.bak-20261014T165715Z"},
|
||||
"config": {".env.lock", "config.yaml", "config.yaml.bak-20261014T165715Z"},
|
||||
"config_data": {".skills_prompt_snapshot.json", "context_length_cache.yaml"},
|
||||
"soul": {"SOUL.md"},
|
||||
"script": {p for p in os.listdir(PROFILE) if p.endswith(".py")},
|
||||
"cache": {"audio_cache", "cache", "image_cache"},
|
||||
"state_db": {
|
||||
"auth.json", "auth.lock",
|
||||
"models_dev_cache.etag", "models_dev_cache.json",
|
||||
"state.db", "state.db-shm", "state.db-wal",
|
||||
"state.db.auto-maintenance.lock", "state.db.fts_rebuild.lock",
|
||||
"state.db.quarantine.lock",
|
||||
},
|
||||
"backups": {"backups"},
|
||||
"extension_dir": {
|
||||
"bin", "cron", "forge", "home", "hooks", "keys", "lsp", "memories",
|
||||
"pairing", "plans", "platforms", "runtime", "sandboxes", "skins",
|
||||
"source-checks", "state", "wizard-outputs", "workspace",
|
||||
},
|
||||
"other": {"context_length_cache.yaml.lock", "gateway.lock", "projects.db"},
|
||||
"logs": {"logs"},
|
||||
"plugins": {"plugins", "plugin-update-checks"},
|
||||
"sessions": {"sessions"},
|
||||
"skills": {"skills"},
|
||||
"vault": {"vault"},
|
||||
}
|
||||
|
||||
EXPECTED_TOTAL = 69
|
||||
|
||||
|
||||
def classify(rel, basename):
|
||||
for cat, names in CATEGORIES.items():
|
||||
if basename in names:
|
||||
return cat
|
||||
raise AssertionError(f"unclassifiable: {rel}")
|
||||
|
||||
|
||||
def main():
|
||||
errors = []
|
||||
|
||||
# (a) live top-level set must equal the committed manifest's entry set.
|
||||
live = set(os.listdir(PROFILE))
|
||||
try:
|
||||
manifest = json.load(open(MANIFEST))
|
||||
except Exception as exc:
|
||||
print(f"FAIL: cannot load manifest: {exc}")
|
||||
return 1
|
||||
man_set = {e["name"] for e in manifest["entries"]}
|
||||
if live != man_set:
|
||||
errors.append(f"name-set differs: live={sorted(live)} man={sorted(man_set)}")
|
||||
|
||||
# (b) total entries.
|
||||
if manifest.get("total_entries") != EXPECTED_TOTAL:
|
||||
errors.append(f"total_entries={manifest.get('total_entries')} != {EXPECTED_TOTAL}")
|
||||
|
||||
# (c) per-category counts vs fresh rules-based walk.
|
||||
fresh_counts = {}
|
||||
for name in os.listdir(PROFILE):
|
||||
fresh_counts[classify(name, name)] = fresh_counts.get(classify(name, name), 0) + 1
|
||||
for cat, cnt in manifest["categories"].items():
|
||||
if fresh_counts.get(cat, 0) != cnt:
|
||||
errors.append(f"cat {cat}: manifest={cnt} fresh={fresh_counts.get(cat, 0)}")
|
||||
|
||||
# (d) size_bytes matches a live walk.
|
||||
for e in manifest["entries"]:
|
||||
full = os.path.join(PROFILE, e["name"])
|
||||
try:
|
||||
if os.path.islink(full):
|
||||
size = 0
|
||||
elif os.path.isdir(full):
|
||||
size = sum(os.path.getsize(os.path.join(r, f))
|
||||
for r, _, fs in os.walk(full) for f in fs)
|
||||
else:
|
||||
size = os.path.getsize(full)
|
||||
except OSError:
|
||||
size = -1
|
||||
if size != e.get("size_bytes"):
|
||||
errors.append(f"size {e['name']}: manifest={e.get('size_bytes')} live={size}")
|
||||
|
||||
# (e) shell find cross-check on the cache bucket (dirs).
|
||||
find_out = subprocess.run(
|
||||
["find", PROFILE, "-maxdepth", "1", "-type", "d", "-printf", "%f"],
|
||||
capture_output=True, text=True).stdout.split()
|
||||
if sorted(x for x in find_out if x in CATEGORIES["cache"]) != sorted(CATEGORIES["cache"]):
|
||||
errors.append("cache bucket cross-check mismatch")
|
||||
|
||||
if errors:
|
||||
print("FAIL:")
|
||||
for e in errors:
|
||||
print(" -", e)
|
||||
return 1
|
||||
print(f"PASS: {len(manifest['entries'])} entries, 15 categories re-derived cleanly.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user