[CLAUDE] Docs: S153 closeout — adopt TRỌN Ctx soft-memory + OWNER-VETO 3-vai + RENAME hub-form (squash 22 wal:)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m39s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m39s
Pipeline anh lệnh: /check-email + /adap-apply -> /fable-real invest (spec 9-task/24-checklist) -> hmw Opus-5-MAX 2 đợt + lead-solo T6-T8 -> checklist verify. Thư ① 9c909007 (model-default Opus 5 MAX): TRỌN — filing 2-tuyến + probe-sau-restart 2-nguồn claude-opus-5[1m] + thư hub d432f5eab5cd + caveat (e) đóng. Thư ② 0df10df4 (pause-tiep Ctx soft-memory, sàn 7-khoản): EXECUTED - máy mind-check: scripts/session_ctx.py +542 dòng (7 phép + 2 phụ, 4 mức, draft/closed/--root/--session PIN, exit 0/1/3) - template .claude/templates/session-mind-template.md + wire 4 lệnh (pause C-bis+2bis+§1-carve-out, tiep §2.6+§3-ter, session-start Phase 3.5, session-end §L.b(l)+6.3-bis) - memory-budget: mind_ctx_kb=32 + tiep_reload + ĐÓNG ghost-wire session_ctx_kb (H18 class-2 sống S146->S153) - fault-inject 18/18 + 2 fix hậu-T4 (FI 6/6): GAP-#9 closed-mode E-0-con-trỏ CỜ->TRƯỢT (chặn gradient thưởng xoá-con-trỏ-trung-thực, họ bẫy F-1 của chính thư hub) · secrets-sweep +--root parity - số ĐO THẬT pause +117ms thay ước lệch 2.2-5.4x OWNER 2 quyết trong-phiên: - (38) VETO: 1-vai -> hub-form 3-vai (ctx-curator @pause + carve-out pause §1 · ctx-verifier @tiep · ctx-audit @2-bookend) - RENAME theo chuẩn hub/sisters để audit chéo: ctx-pause-check->ctx-curator, ctx-reload->ctx-verifier (đo đĩa 3 nhà; hub≠sister => SE theo HUB-form, tiền lệ S149). Roster 20->23, VALID_ROLES 23. Gap còn lại: ring5-audit (V5 truth-floor) hub CÓ, SE+VIPIX chưa -> slot (41). Bookend 2 đầu (12 spawn) + trio auto 1 vòng: - @open: H1 7f · H2 4f · H24 5+3 FLAG · ring1 28Đ/4T · ring2 8/8Đ + M-1 MISS thật · trio eval-MIXED-14/refine-3A-7B-4N/audit-25Đ-3T (re-chạy 7 instrument, 24 số byte-exact) - @close: H1 6f · H2 8f (baseline S154 = 16/10, tự sửa bản ~23/11) · H24 4+2 FLAG · ring1 47Đ/5T/1KC/53 · ring2 6/6Đ + 21 phép phụ - lead xử 11 mục: 9 FLAG H24 + 3 RESIDUAL ring2 + F-r2 (plugin 3 hard-site) + M-1 cả 2 persona + M9 14 sổ diary + F-3/F-5/F-6 + nit Sàn-5 + BÁC F-4 (fan-out không vào _index) #53 x14-cận-dưới, 0 mất dữ liệu (WRITE-lane derive-from-body · Agent-resume · SendMessage-resume x6). Datum: trio return-only hết miễn nhiễm (3/3 CLEAN -> 1/3). 4-A suýt xảy ra thật (bản committed của 2 file H24 là bản cắt 24%/48%). completeness-gate: 5 vòng đủ · phép ĐẠT 4 / TRƯỢT 0 / vacuous 0. State: Mig 67 · 89 bảng · 532 test · gotcha 83 · Sub-agents 23 · counter 27 · bundle FROZEN · governance-only 0 prod-code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@ -1,7 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""session_ctx.py -- session-model derive helpers (SE port, S148 2026-07-24).
|
||||
|
||||
SE PORT of AI_INFRA scripts/session_ctx.py -- 🔴 TRIMMED ON PURPOSE, KHAI RO:
|
||||
SE PORT of AI_INFRA scripts/session_ctx.py -- [!] TRIMMED ON PURPOSE, KHAI RO:
|
||||
(S153 1-char fix: the marker here USED to be a U+1F534 emoji = the ONLY byte that broke this
|
||||
file's OWN declared "ASCII-only source" invariant at the bottom of this docstring. Docstring
|
||||
only -- 0 behaviour delta for machine-block / secrets-sweep.)
|
||||
hub's original (~26 KB) also carries jsonl transcript parsing, anh-message containment
|
||||
verification, token-overhead accounting and a context-cap getter. SE ports only the TWO
|
||||
capabilities its rituals actually call:
|
||||
@ -42,6 +45,19 @@ _RUNID_RE = re.compile(r'\b(wf_[A-Za-z0-9_-]+)')
|
||||
SECRET_PATTERNS = ('voyage', 'sk-', 'gitea_pat', 'cfut_')
|
||||
_SECRET_RES = [(p, re.compile(r'\b' + re.escape(p), re.I)) for p in SECRET_PATTERNS]
|
||||
|
||||
# CANONICAL SINGLE-TRUTH for the `ts:` KEY-LINE probe (S153, ctx soft-memory 0df10df4).
|
||||
# [!] ANY future reader that needs "the most recent session marker timestamp" (the C10-LATENESS
|
||||
# gap in scripts/governance-detectors.ps1 openly declares it has "no reliable source" of ts)
|
||||
# MUST import THIS constant -- do NOT hand-roll a second ts regex anywhere. Two regexes = two
|
||||
# truths, which is precisely what rao-2 of the _mind rule-block forbids.
|
||||
# WHY the probe exists at all: SE markers `_pause-<i>.md` / `_tiep-<i>.md` / `_snapshot-<k>.md`
|
||||
# carry `ts:` as their FIRST field BY DESIGN, so a future latest-marker reader will grep `_*`
|
||||
# without filtering by filename => a `ts:` key line inside `_mind-s-<N>.md` would be read as a
|
||||
# marker timestamp. Hence `_mind` content must never contain one.
|
||||
# Matches a KEY line only (line-start, optional indent, optional space before colon); a ts that
|
||||
# lives INSIDE a heading ("## MIND-1 -- 2026-07-26T...") is NOT a key line and must not match.
|
||||
_TS_KEY_RE = re.compile(r'^[ \t]*ts[ \t]*:', re.I | re.M)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# git helpers
|
||||
@ -176,6 +192,497 @@ def secrets_sweep(session_dir):
|
||||
return hits
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# (c) mind-check -- gate for the SOFT-memory file _mind-s-<N>.md
|
||||
# S153, adopt of broadcast 0df10df4 (ctx soft-memory). 7 standard checks + 2 aux.
|
||||
# 4 levels per check: dat / TRUOT / co / bo-qua-co-khai.
|
||||
# exit 0 = 0 TRUOT | 1 = >=1 TRUOT | 3 = config-or-state (fail-loud, never a default).
|
||||
# ---------------------------------------------------------------------------
|
||||
_MIND_RULES_END = '<!-- MIND-RULES-END -->'
|
||||
_MIND_TOP_MARKER = '<!-- MIND-TOP -->'
|
||||
|
||||
# Real blocks only: `^## MIND-<digits>`. The rule-block schema writes `## MIND-<k>` with a
|
||||
# literal `<k>` placeholder precisely so this parser cannot false-match it (same trick as
|
||||
# .claude/templates/session-context-template.md ENTRY-SCHEMA).
|
||||
_MIND_BLOCK_RE = re.compile(r'^##[ \t]+MIND-(\d+)\b', re.M)
|
||||
|
||||
# rao-1: anh's VERBATIM words live in ONE place only (_context FLOW `> anh:`); the soft file
|
||||
# may only POINT at `PAUSE-<k>`. Copying them here would fork the single-source.
|
||||
_ANH_LINE_RE = re.compile(r'^[ \t]*>[ \t]*anh[ \t]*:', re.I | re.M)
|
||||
|
||||
# rao-3 exception: the ONLY accepted masked-secret label, spelled with diacritics exactly as the
|
||||
# rule-block emits it. Non-ASCII expressed as \uXXXX so the SOURCE stays ASCII (module header).
|
||||
# `[da che <8-hex>]` (ASCII fallback) is DELIBERATELY NOT accepted: on a secret gate an
|
||||
# over-strict miss costs an edit, a lenient miss costs a permanent leak (blocks are immutable).
|
||||
_MASK_LABEL_RE = re.compile(r'\[[ \t]*\u0111\u00e3[ \t]+che[ \t]+[0-9a-fA-F]{8}[ \t]*\]')
|
||||
|
||||
# aux-9 structure: A-E section labels. Form FAMILY (not one hardcoded shape) because the template
|
||||
# is authored in a PARALLEL lane -- this code binds to the SPEC markers (MIND-RULES-*/MIND-TOP/
|
||||
# `## MIND-<k>`), and stays permissive about how A..E are decorated. Accepted:
|
||||
# `### A -- x` | `**A.** x` | `- **A** -- x` | `A) x` | `#### A: x`
|
||||
_SECTION_LETTER_RE = re.compile(
|
||||
r'^[ \t]{0,3}(?:#{2,6}[ \t]*)?(?:[-*][ \t]+)?(?:\*\*[ \t]*)?([A-E])(?:[ \t]*\*\*)?'
|
||||
r'[ \t]*[.):\u2014-]',
|
||||
re.M)
|
||||
|
||||
# aux-9: every idea in section D must carry ONE of the 4 stance labels (moi-neu / dang-cai /
|
||||
# gan-chot / treo-cho-anh) -- \uXXXX-escaped, ASCII source.
|
||||
_D_LABELS = ('m\u1edbi-n\u00eau', '\u0111ang-c\u00e3i', 'g\u1ea7n-ch\u1ed1t', 'treo-ch\u1edd-anh')
|
||||
# An empty section must be DECLARED empty -- `(trong ...` with the diacritic o-circumflex-acute.
|
||||
_EMPTY_DECL_RE = re.compile(r'\([ \t]*tr\u1ed1ng')
|
||||
_BULLET_RE = re.compile(r'^[ \t]*(?:[-*+][ \t]+|\d+[.)][ \t]+)')
|
||||
|
||||
_BACKTICK_RE = re.compile(r'`([^`\n]+)`')
|
||||
# Self-DECLARED counters (session-end writes them into `_end`). CROSS-CHECK ONLY -- never a
|
||||
# counting source; disk wins, a mismatch is an INFO flag (hub measured real under-declaration).
|
||||
_SELF_DECL_RES = (('mind-blocks', re.compile(r'mind-blocks[ \t]*[=:][ \t]*(\d+)')),
|
||||
('_pause', re.compile(r'_pause[ \t]*[=:][ \t]*(\d+)')))
|
||||
|
||||
DAT = 'dat'
|
||||
TRUOT = 'TRUOT'
|
||||
CO = 'co'
|
||||
SKIP = 'bo-qua-co-khai'
|
||||
|
||||
|
||||
class MindConfigError(Exception):
|
||||
"""Config / state error -> exit 3. Raised INSTEAD of assuming a default value."""
|
||||
|
||||
|
||||
def _ascii_safe(s, limit=110):
|
||||
"""ASCII-only rendering of an excerpt. A VN-diacritic line printed to a cp1252 console
|
||||
raises UnicodeEncodeError = the gate would DIE mid-check; backslash-escape instead."""
|
||||
s = s.strip().replace('\t', ' ')
|
||||
if len(s) > limit:
|
||||
s = s[:limit] + '...'
|
||||
return s.encode('ascii', 'backslashreplace').decode('ascii')
|
||||
|
||||
|
||||
def _is_int(v):
|
||||
return isinstance(v, int) and not isinstance(v, bool)
|
||||
|
||||
|
||||
def _require_budget_kb(repo_root, keys):
|
||||
"""{key: cap_in_BYTES} read LIVE from .claude/agent-memory/memory-budget.json (value is KB,
|
||||
reader multiplies by 1024). Missing file / missing key / non-int -> MindConfigError.
|
||||
|
||||
NO DEFAULT, ever (house rule, h24_cadence._note): a hardcoded ceiling would silently
|
||||
re-create the drift that single-source config exists to prevent. The message NAMES the
|
||||
missing key -- a nameless 'config error' sends the reader hunting."""
|
||||
p = Path(repo_root) / '.claude' / 'agent-memory' / 'memory-budget.json'
|
||||
if not p.is_file():
|
||||
raise MindConfigError('config absent: %s' % p)
|
||||
try:
|
||||
data = json.loads(p.read_text(encoding='utf-8'))
|
||||
except (OSError, ValueError) as e:
|
||||
raise MindConfigError('config unreadable: %s (%s)' % (p, e))
|
||||
missing = [k for k in keys if not _is_int(data.get(k))]
|
||||
if missing:
|
||||
raise MindConfigError('missing key(s) %s in %s -- CAM default (khuon H24-2 fail-loud)'
|
||||
% (', '.join(missing), p))
|
||||
return {k: int(data[k]) * 1024 for k in keys}
|
||||
|
||||
|
||||
def _split_content_region(text):
|
||||
"""(region, line_offset, rules_end_found) -- content = everything BELOW <!-- MIND-RULES-END -->.
|
||||
|
||||
enclosure use-vs-mention: the rule block ITSELF spells out the forbidden strings in order to
|
||||
explain them, so a flat grep over the whole file would flag the very rules that forbid them
|
||||
(citation-trap class). Marker absent => scan the WHOLE file + raise a flag: a file without the
|
||||
marker is off-template, and the safe failure direction is LOUD, not lenient."""
|
||||
i = text.find(_MIND_RULES_END)
|
||||
if i < 0:
|
||||
return text, 0, False
|
||||
nl = text.find('\n', i)
|
||||
start = len(text) if nl < 0 else nl + 1
|
||||
return text[start:], text.count('\n', 0, start), True
|
||||
|
||||
|
||||
def _parse_blocks(region, line_offset):
|
||||
"""Ordered-by-POSITION list of {num, line, text}. Newest-on-TOP => blocks[0] is the newest.
|
||||
(Opposite direction from _context FLOW, which appends newest-at-BOTTOM.)"""
|
||||
ms = list(_MIND_BLOCK_RE.finditer(region))
|
||||
out = []
|
||||
for i, m in enumerate(ms):
|
||||
end = ms[i + 1].start() if i + 1 < len(ms) else len(region)
|
||||
out.append({'num': int(m.group(1)),
|
||||
'line': line_offset + region.count('\n', 0, m.start()) + 1,
|
||||
'text': region[m.start():end]})
|
||||
return out
|
||||
|
||||
|
||||
def _block_sections(block_text):
|
||||
"""{letter: section_text} for the FIRST occurrence of each A-E label (see form family)."""
|
||||
found = [(m.group(1), m.start()) for m in _SECTION_LETTER_RE.finditer(block_text)]
|
||||
secs = {}
|
||||
for i, (letter, pos) in enumerate(found):
|
||||
end = found[i + 1][1] if i + 1 < len(found) else len(block_text)
|
||||
secs.setdefault(letter, block_text[pos:end])
|
||||
return secs
|
||||
|
||||
|
||||
def _pointer_tokens(sec_text):
|
||||
"""Backticked path-ish tokens in section E (`verdict + con-tro` per line)."""
|
||||
toks = []
|
||||
for m in _BACKTICK_RE.finditer(sec_text):
|
||||
t = m.group(1).strip()
|
||||
if not t or ' ' in t or '\t' in t or '/' not in t:
|
||||
continue
|
||||
if t.startswith('http://') or t.startswith('https://'):
|
||||
continue
|
||||
toks.append(t)
|
||||
return toks
|
||||
|
||||
|
||||
def _classify_pointer(token, repo_root):
|
||||
"""ok | pending | broken | placeholder (F-1 three-state, + declared 4th for placeholders)."""
|
||||
if any(c in token for c in '<>*{}'):
|
||||
return 'placeholder'
|
||||
raw = token[2:] if token.startswith('./') else token
|
||||
p = Path(raw)
|
||||
if not p.is_absolute():
|
||||
p = Path(repo_root) / raw
|
||||
if p.exists():
|
||||
return 'ok'
|
||||
return 'pending' if p.parent.is_dir() else 'broken'
|
||||
|
||||
|
||||
def _count_pause_on_disk(session_dir):
|
||||
"""p = DISK count, dual-accept both marker forms.
|
||||
`_pause-<i>.md` (hub form, S148+) + `pause-*.md` (legacy <=S147). The glob `pause-*` does NOT
|
||||
match `_pause-1.md` (leading underscore) => adding them cannot double-count; same
|
||||
life-saving property the ORPHAN-L probe relies on (tiep.md:93)."""
|
||||
session_dir = Path(session_dir)
|
||||
if not session_dir.is_dir():
|
||||
return 0, 0, 0
|
||||
hub = len([p for p in session_dir.glob('_pause-*.md') if p.is_file()])
|
||||
legacy = len([p for p in session_dir.glob('pause-*.md') if p.is_file()])
|
||||
return hub + legacy, hub, legacy
|
||||
|
||||
|
||||
def _self_declared(session_dir):
|
||||
"""{name: n} parsed from `_end` (e.g. `markers: _pause=1 . mind-blocks=2`). Cross-check ONLY."""
|
||||
end = Path(session_dir) / '_end'
|
||||
if not end.is_file():
|
||||
return None
|
||||
text = end.read_text(encoding='utf-8', errors='replace')
|
||||
out = {}
|
||||
for name, rx in _SELF_DECL_RES:
|
||||
m = rx.search(text)
|
||||
if m:
|
||||
out[name] = int(m.group(1))
|
||||
return out
|
||||
|
||||
|
||||
def _chk(checks, cid, name, level, detail=()):
|
||||
checks.append({'id': cid, 'name': name, 'level': level, 'detail': list(detail)})
|
||||
|
||||
|
||||
# --- the three rao (barriers), shared by draft-mode and full-mode -------------------------
|
||||
def _check_rao(checks, region, line_offset, label):
|
||||
hits = [(line_offset + region.count('\n', 0, m.start()) + 1,
|
||||
region[m.start():region.find('\n', m.start()) if region.find('\n', m.start()) >= 0
|
||||
else len(region)])
|
||||
for m in _ANH_LINE_RE.finditer(region)]
|
||||
_chk(checks, '1', 'rao-1 loi-anh-verbatim', TRUOT if hits else DAT,
|
||||
['%s:%d %s' % (label, ln, _ascii_safe(txt)) for ln, txt in hits]
|
||||
or ['0 hit `> anh:` duoi RULES-END (verbatim chi song o _context FLOW)'])
|
||||
|
||||
ts_hits = [line_offset + region.count('\n', 0, m.start()) + 1
|
||||
for m in _TS_KEY_RE.finditer(region)]
|
||||
_chk(checks, '2', 'rao-2 ts-key-line', TRUOT if ts_hits else DAT,
|
||||
['%s:%d key-line `ts:` (canonical regex session_ctx.py:_TS_KEY_RE)' % (label, ln)
|
||||
for ln in ts_hits]
|
||||
or ['0 key-line `ts:` (heading-embedded ts is NOT a key line -- by design)'])
|
||||
|
||||
sec_hits = []
|
||||
for i, line in enumerate(region.splitlines(), 1):
|
||||
if _MASK_LABEL_RE.search(line):
|
||||
continue
|
||||
for pat, rx in _SECRET_RES:
|
||||
if rx.search(line):
|
||||
sec_hits.append((line_offset + i, pat))
|
||||
break
|
||||
_chk(checks, '3', 'rao-3 secret Category-5', TRUOT if sec_hits else DAT,
|
||||
# snippet DELIBERATELY not printed: this gate must not itself leak the secret into a log.
|
||||
['%s:%d [%s] (snippet KHONG in -- chong ro-ri log)' % (label, ln, pat)
|
||||
for ln, pat in sec_hits]
|
||||
or ['0 hit / %d pattern; ngoai-le nhan che 8-hex duoc mien' % len(SECRET_PATTERNS)])
|
||||
|
||||
|
||||
def _check_pointer(checks, blocks, repo_root, closed):
|
||||
"""(4) F-1 three-state on the pointers of section E of the TOP block."""
|
||||
if not blocks:
|
||||
_chk(checks, '4', 'con-tro E block-top', SKIP, ['0 block => 0 con-tro de cham'])
|
||||
return
|
||||
top = blocks[0]
|
||||
secs = _block_sections(top['text'])
|
||||
e_text = secs.get('E')
|
||||
if e_text is None:
|
||||
_chk(checks, '4', 'con-tro E block-top', SKIP,
|
||||
['block MIND-%d: khong tim thay muc E (cau-truc do aux-9 cham)' % top['num']])
|
||||
return
|
||||
if _EMPTY_DECL_RE.search(e_text) and not _pointer_tokens(e_text):
|
||||
_chk(checks, '4', 'con-tro E block-top', SKIP,
|
||||
['block MIND-%d: muc E khai `(trong ...)` => 0 con-tro (khai, khong xanh-im)'
|
||||
% top['num']])
|
||||
return
|
||||
toks = _pointer_tokens(e_text)
|
||||
if not toks:
|
||||
# GAP-#9 (T4 fault-inject @S153): closed-mode MUST fail here. If this stayed CO at close,
|
||||
# deleting a truthful pending pointer (-> this branch, exit 0) would be CHEAPER than keeping
|
||||
# it (pending -> TRUOT at close) => the gradient rewards the WRONG path (family of F-1).
|
||||
lvl = TRUOT if closed else CO
|
||||
_chk(checks, '4', 'con-tro E block-top', lvl,
|
||||
['block MIND-%d: muc E co noi-dung ma 0 con-tro backtick-path%s'
|
||||
% (top['num'],
|
||||
' => TRUOT o cua DONG (chan duong-re-nhat-de-xanh = xoa con-tro)' if closed else '')])
|
||||
return
|
||||
detail, worst = [], DAT
|
||||
for t in toks:
|
||||
state = _classify_pointer(t, repo_root)
|
||||
if state == 'ok':
|
||||
detail.append('dat %s' % _ascii_safe(t, 90))
|
||||
elif state == 'placeholder':
|
||||
detail.append('%s %s (placeholder <>/* -- khong resolve duoc, khai)' % (SKIP, _ascii_safe(t, 90)))
|
||||
elif state == 'pending':
|
||||
# anti-Goodhart (PHAN B #9): the cheapest way to go green must be the RIGHT way.
|
||||
detail.append('%s %s -- cha CO, file chua sinh => CHO file sinh o cua DONG; '
|
||||
'CAM xoa con-tro de lam xanh'
|
||||
% ((TRUOT if closed else CO), _ascii_safe(t, 90)))
|
||||
if closed:
|
||||
worst = TRUOT
|
||||
elif worst != TRUOT:
|
||||
worst = CO
|
||||
else:
|
||||
detail.append('%s %s -- cha KHONG co => hong that (moi mode)' % (TRUOT, _ascii_safe(t, 90)))
|
||||
worst = TRUOT
|
||||
detail.append('KHAI (nguyen van thu SS4): TEN-TEP-SAI trong thu-muc THAT doc thanh '
|
||||
'"dang-cho" o cua mo, toi cua dong moi siet -- day la KE, khong phai tinh nang')
|
||||
_chk(checks, '4', 'con-tro E block-top', worst, detail)
|
||||
|
||||
|
||||
def _check_numbering(checks, blocks):
|
||||
"""(5) unique + top >= every number below. NO count formula is enforced: the house may change
|
||||
how numbers are allocated and this check must still work (spec Muc-3 of the broadcast)."""
|
||||
if not blocks:
|
||||
_chk(checks, '5', 'so-hieu block', SKIP, ['0 block'])
|
||||
return
|
||||
nums = [b['num'] for b in blocks]
|
||||
dups = sorted({n for n in nums if nums.count(n) > 1})
|
||||
top = nums[0]
|
||||
detail = ['day (tren->duoi): %s' % ', '.join(str(n) for n in nums)]
|
||||
level = DAT
|
||||
if dups:
|
||||
level = TRUOT
|
||||
detail.append('%s so-hieu TRUNG: %s' % (TRUOT, ', '.join(str(d) for d in dups)))
|
||||
below_max = max(nums[1:]) if len(nums) > 1 else None
|
||||
if below_max is not None and top < below_max:
|
||||
level = TRUOT
|
||||
detail.append('%s block top MIND-%d < so duoi MIND-%d (moi-nhat-o-TREN bi vi pham)'
|
||||
% (TRUOT, top, below_max))
|
||||
if level == DAT:
|
||||
detail.append('duy-nhat + top >= moi so duoi (khong neo cong-thuc dem)')
|
||||
_chk(checks, '5', 'so-hieu block', level, detail)
|
||||
|
||||
|
||||
def _check_structure(checks, blocks, label):
|
||||
"""aux-9: 5 sections A-E present (or declared empty) + every D idea carries a stance label."""
|
||||
if not blocks:
|
||||
_chk(checks, '9', 'aux cau-truc A-E + nhan D', SKIP, ['0 block'])
|
||||
return
|
||||
level, detail = DAT, []
|
||||
for b in blocks:
|
||||
secs = _block_sections(b['text'])
|
||||
missing = [c for c in 'ABCDE' if c not in secs]
|
||||
if missing:
|
||||
level = TRUOT
|
||||
detail.append('%s MIND-%d (%s:%d) thieu muc: %s [dang nhan: `### A --` | `**A.**` | '
|
||||
'`- **A** --` | `A)` | `#### A:`]'
|
||||
% (TRUOT, b['num'], label, b['line'], ', '.join(missing)))
|
||||
continue
|
||||
d_text = secs['D']
|
||||
d_lines = d_text.splitlines()[1:]
|
||||
# An empty-declaration line is a DECLARATION, not an idea -- exempt it even when it is
|
||||
# written as a bullet (`- (trong -- khai)`), otherwise the rule "muc trong ghi (trong --
|
||||
# khai)" would itself be un-satisfiable. Caught by the clean fixture, not by reasoning.
|
||||
ideas = [(i, ln) for i, ln in enumerate(d_lines, 1)
|
||||
if _BULLET_RE.match(ln) and not _EMPTY_DECL_RE.search(ln)]
|
||||
if not ideas:
|
||||
if not _EMPTY_DECL_RE.search(d_text):
|
||||
level = TRUOT
|
||||
detail.append('%s MIND-%d muc D: 0 y ma cung KHONG khai `(trong ...)`'
|
||||
% (TRUOT, b['num']))
|
||||
continue
|
||||
bad = [ln for _, ln in ideas if not any(lb in ln for lb in _D_LABELS)]
|
||||
if bad:
|
||||
level = TRUOT
|
||||
detail.append('%s MIND-%d muc D: %d/%d y thieu nhan {moi-neu|dang-cai|gan-chot|'
|
||||
'treo-cho-anh} -- vd: %s'
|
||||
% (TRUOT, b['num'], len(bad), len(ideas), _ascii_safe(bad[0], 70)))
|
||||
if level == DAT:
|
||||
detail.append('%d block: 5 muc A-E du + moi y muc D co nhan' % len(blocks))
|
||||
_chk(checks, '9', 'aux cau-truc A-E + nhan D', level, detail)
|
||||
|
||||
|
||||
def _check_context_cap(checks, session_dir, session_n, cap_bytes):
|
||||
"""aux-8: the FIRST REAL READER of session_ctx_kb (closes the H18 ghost-wire: the key's own
|
||||
note asked its future reader to (a) read the key (b) x1024 (c) drop the caveat line)."""
|
||||
ctx = Path(session_dir) / ('_context-s-%d.md' % session_n)
|
||||
if not ctx.is_file():
|
||||
_chk(checks, '8', 'aux tran _context', SKIP, ['%s: khong co tep' % ctx.name])
|
||||
return
|
||||
size = ctx.stat().st_size
|
||||
over = size > cap_bytes
|
||||
_chk(checks, '8', 'aux tran _context', CO if over else DAT,
|
||||
['%s = %dB vs session_ctx_kb x1024 = %dB%s'
|
||||
% (ctx.name, size, cap_bytes, ' => VUOT' if over else '')])
|
||||
|
||||
|
||||
def mind_check(session_n, root=None, draft=None, force_closed=False):
|
||||
"""Gate _mind-s-<N>.md. Returns (rc, lines). Raises MindConfigError for rc=3 cases.
|
||||
|
||||
`--session` is PINNED by the caller on purpose (no default-latest): two windows running side
|
||||
by side would race for "the latest" and audit each other's file."""
|
||||
repo_root = Path(root or ROOT)
|
||||
session_dir = repo_root / '.claude' / 'sessions' / ('session-%d' % session_n)
|
||||
canonical = session_dir / ('_mind-s-%d.md' % session_n)
|
||||
closed = bool(force_closed) or (session_dir / '_end').is_file()
|
||||
mode = 'draft' if draft else ('closed' if closed else 'open')
|
||||
checks = []
|
||||
out = ['[mind-check] session-%d | mode=%s | root=%s' % (session_n, mode, repo_root)]
|
||||
|
||||
if draft:
|
||||
dpath = Path(draft)
|
||||
if not dpath.is_file():
|
||||
alt = repo_root / draft
|
||||
if not alt.is_file():
|
||||
raise MindConfigError('draft file absent: %s' % dpath)
|
||||
dpath = alt
|
||||
# Draft-mode consumes NO mind_ctx_kb (checks 6/7 are skipped) => requiring it would be a
|
||||
# fail-loud on a key this run never reads. It DOES read session_ctx_kb (aux-8).
|
||||
caps = _require_budget_kb(repo_root, ['session_ctx_kb'])
|
||||
text = dpath.read_text(encoding='utf-8', errors='replace')
|
||||
region, line_offset, _ = _split_content_region(text)
|
||||
out.append(' draft : %s (%dB)' % (dpath, dpath.stat().st_size))
|
||||
out.append(' canon : %s%s' % (canonical, '' if canonical.is_file() else ' (chua co)'))
|
||||
_check_rao(checks, region, line_offset, dpath.name)
|
||||
dblocks = _parse_blocks(region, line_offset)
|
||||
on_disk = 0
|
||||
if canonical.is_file():
|
||||
creg, coff, _ = _split_content_region(
|
||||
canonical.read_text(encoding='utf-8', errors='replace'))
|
||||
on_disk = len(_parse_blocks(creg, coff))
|
||||
if not dblocks:
|
||||
_chk(checks, '5', 'so-hieu draft vs count-on-disk', TRUOT,
|
||||
['draft khong co heading `## MIND-<n>` nao'])
|
||||
else:
|
||||
want = on_disk
|
||||
got = dblocks[0]['num']
|
||||
_chk(checks, '5', 'so-hieu draft vs count-on-disk', DAT if got == want else TRUOT,
|
||||
['draft MIND-%d vs count-on-disk %d%s'
|
||||
% (got, want, '' if got == want else ' => LECH (so-hieu = DEM block TRUOC chen)')])
|
||||
_check_pointer(checks, dblocks, repo_root, closed=False)
|
||||
_chk(checks, '6', 'bat-bien |block| vs p', SKIP, ['draft-mode: cham sau khi chen'])
|
||||
_chk(checks, '7', 'tran mind_ctx_kb', SKIP,
|
||||
['draft-mode: do tren tep THAT sau khi chen (khong doc mind_ctx_kb luot nay)'])
|
||||
_check_context_cap(checks, session_dir, session_n, caps['session_ctx_kb'])
|
||||
_check_structure(checks, dblocks, dpath.name)
|
||||
return _render(out, checks, 'draft (kiem NHAP truoc khi chen -- loi khong bao gio vao tep)')
|
||||
|
||||
# ---- full mode -----------------------------------------------------------------------
|
||||
if not canonical.is_file():
|
||||
cands = sorted(p.name for p in session_dir.glob('*mind*')) if session_dir.is_dir() else []
|
||||
why = ['thieu: %s' % canonical]
|
||||
if not session_dir.is_dir():
|
||||
why.append('thu-muc phien KHONG ton tai: %s' % session_dir)
|
||||
# tep-sai-phai-NEU-TEN: "khong co gi de kiem" must never hide a typo'd filename.
|
||||
why.append('glob `*mind*` trong thu-muc: %s'
|
||||
% (', '.join(cands) if cands else '(0 ung vien)'))
|
||||
if cands:
|
||||
why.append('=> co ung vien TEN SAI o tren; canonical dung phai la %s' % canonical.name)
|
||||
level = TRUOT if closed else SKIP
|
||||
if closed:
|
||||
why.append('closed-mode (co `_end`) => TRUOT: phien da dong ma khong co lop MEM')
|
||||
else:
|
||||
why.append('open-mode => bo-qua CO KHAI (chua doc memory-budget.json: khong co gi de do)')
|
||||
_chk(checks, '0', 'ton tai _mind-s-%d.md' % session_n, level, why)
|
||||
return _render(out, checks, 'full/%s (tep canonical vang)' % mode)
|
||||
|
||||
caps = _require_budget_kb(repo_root, ['mind_ctx_kb', 'session_ctx_kb'])
|
||||
text = canonical.read_text(encoding='utf-8', errors='replace')
|
||||
size = canonical.stat().st_size
|
||||
region, line_offset, rules_end = _split_content_region(text)
|
||||
out.append(' target: %s (%dB)' % (canonical, size))
|
||||
if not rules_end:
|
||||
_chk(checks, '0', 'enclosure %s' % _MIND_RULES_END, CO,
|
||||
['marker VANG => quet TRON tep (huong hong NGHIEM); tep dang off-template'])
|
||||
else:
|
||||
_chk(checks, '0', 'enclosure %s' % _MIND_RULES_END, DAT,
|
||||
['chi quet DUOI marker (use-vs-mention: khoi luat chua chinh chuoi bi cam)'])
|
||||
if _MIND_TOP_MARKER not in text:
|
||||
_chk(checks, '0b', 'marker %s' % _MIND_TOP_MARKER, CO,
|
||||
['vang => khong biet chen block moi o dau (moi-nhat-o-TREN)'])
|
||||
|
||||
_check_rao(checks, region, line_offset, canonical.name)
|
||||
blocks = _parse_blocks(region, line_offset)
|
||||
_check_pointer(checks, blocks, repo_root, closed)
|
||||
_check_numbering(checks, blocks)
|
||||
|
||||
p, hub, legacy = _count_pause_on_disk(session_dir)
|
||||
n = len(blocks)
|
||||
ok = n in (p, p + 1)
|
||||
detail = ['|block| = %d ; p (DEM DIA) = %d [_pause-*.md=%d + pause-*.md=%d dual-accept]'
|
||||
% (n, p, hub, legacy),
|
||||
'ky vong |block| thuoc {%d, %d} (block-0 + 1 refresh/pause)' % (p, p + 1)]
|
||||
decl = _self_declared(session_dir)
|
||||
info = []
|
||||
if decl is None:
|
||||
detail.append('cross-check tu-khai: khong co `_end` => bo qua (dia van la nguon duy nhat)')
|
||||
else:
|
||||
for key, want in (('mind-blocks', n), ('_pause', p)):
|
||||
if key in decl and decl[key] != want:
|
||||
info.append('co-INFO lech tu-khai: `_end` %s=%d vs DIA %d (DIA THANG)'
|
||||
% (key, decl[key], want))
|
||||
detail.append('cross-check tu-khai `_end`: %s'
|
||||
% (', '.join('%s=%d' % (k, v) for k, v in sorted(decl.items())) or '(0 field)'))
|
||||
detail.extend(info)
|
||||
_chk(checks, '6', 'bat-bien |block| vs p', TRUOT if not ok else (CO if info else DAT), detail)
|
||||
|
||||
cap = caps['mind_ctx_kb']
|
||||
over = size > cap
|
||||
d7 = ['%dB vs mind_ctx_kb x1024 = %dB (doc LIVE, 0 hardcode)%s'
|
||||
% (size, cap, ' => VUOT' if over else '')]
|
||||
if over:
|
||||
if n < 3:
|
||||
d7.append('TU-CHOI nen: chi %d block (<3) => giuong co vuot tran, KHONG nen' % n)
|
||||
else:
|
||||
d7.append('huong-dan: nen block CU NHAT (tru block-0 va block top); verbatim con o git')
|
||||
_chk(checks, '7', 'tran mind_ctx_kb', CO if over else DAT, d7)
|
||||
|
||||
_check_context_cap(checks, session_dir, session_n, caps['session_ctx_kb'])
|
||||
_check_structure(checks, blocks, canonical.name)
|
||||
return _render(out, checks, 'full/%s' % mode)
|
||||
|
||||
|
||||
def _render(out, checks, mode_note):
|
||||
"""Deterministic 4-level report. rc = 1 iff >=1 TRUOT (co / bo-qua-co-khai never gate)."""
|
||||
order = {'0': 0, '0b': 1, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6, '6': 7, '7': 8,
|
||||
'8': 9, '9': 10}
|
||||
checks = sorted(checks, key=lambda c: order.get(c['id'], 99))
|
||||
tally = {DAT: 0, TRUOT: 0, CO: 0, SKIP: 0}
|
||||
for c in checks:
|
||||
tally[c['level']] = tally.get(c['level'], 0) + 1
|
||||
tag = '[phu-%s]' % c['id'] if c['id'] in ('8', '9') else '(%s)' % c['id']
|
||||
out.append(' %-8s %-30s : %s' % (tag, c['name'], c['level']))
|
||||
for d in c['detail']:
|
||||
out.append(' - %s' % d)
|
||||
rc = 1 if tally[TRUOT] else 0
|
||||
out.append(' mode-note: %s' % mode_note)
|
||||
out.append(' verdict: dat=%d %s=%d co=%d %s=%d => exit %d'
|
||||
% (tally[DAT], TRUOT, tally[TRUOT], tally[CO], SKIP, tally[SKIP], rc))
|
||||
return rc, out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -191,6 +698,22 @@ def main(argv=None):
|
||||
|
||||
ss = sub.add_parser('secrets-sweep', help='Category-5 pre-commit gate over a session dir')
|
||||
ss.add_argument('--session', type=int, required=True, metavar='N')
|
||||
# S153 T4 #7 spec-drift fix: seam parity with mind-check (--root was mind-check-only, so the
|
||||
# literal command in PHAN B #7 could not run as written; T4 had to copy the script into the tree).
|
||||
ss.add_argument('--root', default=None, metavar='PATH',
|
||||
help='repo root override = fault-inject seam (mirror mind-check --root)')
|
||||
|
||||
# S153 ctx soft-memory (0df10df4). --session is REQUIRED with NO default-latest on purpose:
|
||||
# two windows running in parallel would race over "the latest" session.
|
||||
mc = sub.add_parser('mind-check', help='gate the soft-memory file _mind-s-<N>.md')
|
||||
mc.add_argument('--session', type=int, required=True, metavar='N')
|
||||
mc.add_argument('--draft', default=None, metavar='FILE',
|
||||
help='check a DRAFT block before inserting it (checks 1-3 + numbering; '
|
||||
'skips 6/7) -- an error must never reach the immutable file')
|
||||
mc.add_argument('--closed', action='store_true',
|
||||
help='force closed-mode (auto-on when `_end` exists); flag = fault-inject seam')
|
||||
mc.add_argument('--root', default=None, metavar='PATH',
|
||||
help='repo root override = fault-inject seam (mirror -RepoRoot of the .ps1 family)')
|
||||
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
@ -201,7 +724,8 @@ def main(argv=None):
|
||||
return 0
|
||||
|
||||
if args.cmd == 'secrets-sweep':
|
||||
session_dir = DEFAULT_SESSIONS_ROOT / f"session-{args.session}"
|
||||
base = (Path(args.root) / '.claude' / 'sessions') if args.root else DEFAULT_SESSIONS_ROOT
|
||||
session_dir = base / f"session-{args.session}"
|
||||
hits = secrets_sweep(session_dir)
|
||||
if not hits:
|
||||
print(f"[secrets-sweep] session-{args.session}: 0 hit "
|
||||
@ -213,6 +737,20 @@ def main(argv=None):
|
||||
print(f" {h['file']}:{h['line']} [{h['pattern']}] {h['snippet']}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if args.cmd == 'mind-check':
|
||||
try:
|
||||
rc, lines = mind_check(args.session, root=args.root, draft=args.draft,
|
||||
force_closed=args.closed)
|
||||
except MindConfigError as e:
|
||||
# exit 3 = config/state. FAIL-LOUD by design: measuring with an assumed ceiling would
|
||||
# be worse than measuring nothing, because it looks like a pass.
|
||||
print(f"[mind-check] CONFIG/STATE ERROR: {e}", file=sys.stderr)
|
||||
return 3
|
||||
stream = sys.stderr if rc else sys.stdout
|
||||
for ln in lines:
|
||||
print(ln, file=stream)
|
||||
return rc
|
||||
|
||||
return 2
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user