wal: pause

This commit is contained in:
pqhuy1987
2026-07-18 01:00:30 +07:00
parent a5ccffc8b8
commit 3a394b02cc
10 changed files with 313 additions and 21 deletions

View File

@ -62,6 +62,25 @@ function Write-Section($title) {
Write-Host ("===== $title =====") -ForegroundColor Cyan
}
# INFORM-only flag sink for NEW detectors (C6 cite-2-tier, H24-4 pending-flip).
# Counted SEPARATELY in $script:InformCount and NEVER folded into $script:FlagCount
# (= the baseline TOTAL). Rationale (anti-Goodhart, owner-set): a brand-new net
# whose LOW hits are read by judgement on day one must not move the audited TOTAL
# down/up -- folding it would let "flag count fell" masquerade as progress and would
# hide whether the thing that changed was a false positive or a real witness. Fold +
# severity-raise is a POST-triage, owner-gated decision (see detector headers). Same
# [DETECTOR] line shape as Write-Flag so `comm before/after` cleanly isolates new lines.
$script:InformCount = 0
function Write-InformFlag {
param(
[string]$Where, # file:line
[string]$Desc,
[string]$Resolve
)
Write-Host ("[DETECTOR] {0,-4} | {1} | {2} | resolve: {3}" -f 'LOW', $Where, $Desc, $Resolve) -ForegroundColor Gray
$script:InformCount++
}
# Make a path repo-relative for readable FLAG output (forward slashes).
function Rel($full) {
$r = $full
@ -1097,6 +1116,246 @@ else {
}
}
# ---------------------------------------------------------------------------
# C6 - cite-2-tier (INFORM-only, NEW; count SEPARATE, NOT folded into TOTAL)
#
# Two-tier citation check over a NARROW scope. A "cite" is a path:line token whose
# path ends in md|ps1|js|cs|ts|tsx. Tier-1: the file does not resolve -> cite-dead-file.
# Tier-2: it resolves but the cited line > EOF (ReadAllLines.Length) -> cite-line-past-eof.
# Both LOW (suite convention: an un-triaged net reads by judgement on day one; MED-noise
# + fold-into-TOTAL are a later, owner-gated call after triage).
#
# SCOPE (deliberately narrow, spec C2): docs/governance/*.md TOP-LEVEL only
# (adap-reports/ EXCLUDED -- ~90 lineage cites there are frozen history, not live
# claims) + .claude/commands/*.md + .claude/agents/*.md.
#
# THREE EXCLUSIONS, all measured on disk before shipping:
# (a) QUOTED cites -- inline-backtick via Test-Quoted (S123 citation-trap law: a
# prose matcher MUST use the enclosure use/mention discriminator) AND fenced
# ``` blocks via a fence-state toggle. MEASURED: 172/185 cites in scope sit
# inside backticks/fences (fable-real-runbook.md tables + code fence :103-109);
# scanning them bare would be ~172 false dead-file flags.
# (b) LINEAGE: the adap-reports/ dir (dir-exclude) + any line carrying the frozen
# marker U+1F9CA (built by code point, gotcha #30). fable-real-runbook.md:88 is
# exactly such a line (cite next to the retired-marker note). Declared: lineage
# OUTSIDE these two proxies accepts a first-run false positive; tune after triage.
# (c) SELF-DEF: this script is .ps1 + already in ExcludeExact; the run's spec + sub-MD
# live under .claude/workflows/runs/ which is already an ExcludeDirFragment -- so
# both are out of scope by construction AND re-checked via Test-Excluded per file.
#
# RESOLUTION (spec: repo-rel + familiar roots .claude/, docs/): try repo-rel exact,
# then .claude/<path> and docs/<path> prefix-joins, then -- for a BARE basename -- a
# recursive basename index built once over .claude/ + docs/. That index is why a bare
# hmw.js (lives at .claude/workflows/hmw.js) or reviewer.md (.claude/agents/reviewer.md)
# resolves instead of false-flagging (measured: 172/185 cites are bare basenames).
#
# HEADER LIMITS (declared, spec m-1):
# (a) soft-drift WITHIN eof is invisible (a stale :5 that should be :7 still resolves
# and is <= EOF -> not caught; the S108 +-1/2 class);
# (b) multi-root resolve can MASK a dead cite when a same-basename file exists at
# another root (index returns the first match; Ambiguous is noted in the flag);
# (c) a range cite file.md:12-15 is checked at its FIRST line (12) only.
# ---------------------------------------------------------------------------
Write-Section 'C6 - cite-2-tier (INFORM-only)'
$FROZEN_MARK = U @(0xD83E, 0xDDCA) # U+1F9CA frozen/lineage marker (surrogate pair)
$FenceTriple = U @(0x60, 0x60, 0x60) # ``` (ASCII backticks; built to avoid quoting ambiguity)
$FenceRx = '^\s{0,3}' + $FenceTriple
$c6Scope = @()
$govTop = Join-Path $RepoRoot 'docs\governance'
if (Test-Path $govTop) { $c6Scope += Get-ChildItem -Path $govTop -Filter *.md -File -ErrorAction SilentlyContinue }
$cmdDir = Join-Path $RepoRoot '.claude\commands'
if (Test-Path $cmdDir) { $c6Scope += Get-ChildItem -Path $cmdDir -Filter *.md -File -ErrorAction SilentlyContinue }
$agtDir = Join-Path $RepoRoot '.claude\agents'
if (Test-Path $agtDir) { $c6Scope += Get-ChildItem -Path $agtDir -Filter *.md -File -ErrorAction SilentlyContinue }
$c6Scope = @($c6Scope | Where-Object { -not (Test-Excluded $_.FullName) })
# Recursive basename index over the two familiar roots for BARE-basename resolution.
$citeBasenameIndex = @{}
foreach ($rt in @('.claude', 'docs')) {
$rtPath = Join-Path $RepoRoot $rt
if (-not (Test-Path $rtPath)) { continue }
Get-ChildItem -Path $rtPath -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -imatch '^\.(md|ps1|js|cs|ts|tsx)$' -and $_.FullName -notmatch '[\\/](bin|obj|node_modules)[\\/]' } |
ForEach-Object {
$bn = $_.Name
if (-not $citeBasenameIndex.ContainsKey($bn)) { $citeBasenameIndex[$bn] = New-Object System.Collections.Generic.List[string] }
$citeBasenameIndex[$bn].Add($_.FullName) | Out-Null
}
}
function Resolve-Cite {
param([string]$Path)
$norm = $Path -replace '/', '\'
foreach ($c in @(
(Join-Path $RepoRoot $norm),
(Join-Path $RepoRoot (Join-Path '.claude' $norm)),
(Join-Path $RepoRoot (Join-Path 'docs' $norm)))) {
if (Test-Path -LiteralPath $c -PathType Leaf) {
return [pscustomobject]@{ Exists = $true; File = $c; Ambiguous = $false }
}
}
if ($norm -notmatch '[\\/]') {
$bn = [System.IO.Path]::GetFileName($norm)
if ($citeBasenameIndex.ContainsKey($bn)) {
$lst = $citeBasenameIndex[$bn]
return [pscustomobject]@{ Exists = $true; File = $lst[0]; Ambiguous = ($lst.Count -gt 1) }
}
}
return [pscustomobject]@{ Exists = $false; File = $null; Ambiguous = $false }
}
# cite = <path>.<ext>:<line>. Leading '.' allowed (so .claude/... paths resolve). Range
# :12-15 captured at first line only (limit c). Lookbehind stops mid-word matches.
$c6CiteRx = '(?<![A-Za-z0-9])(\.?[A-Za-z0-9_][A-Za-z0-9_./\\-]*\.(?:md|ps1|js|cs|tsx?)):(\d+)'
$c6EofCache = @{}
$c6Flags = 0
$c6Scanned = 0
foreach ($f in $c6Scope) {
$lines = Get-Content -Path $f.FullName -Encoding UTF8 -ErrorAction SilentlyContinue
if ($null -eq $lines) { continue }
$c6Scanned++
$inFence = $false
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
if ($line -match $FenceRx) { $inFence = -not $inFence; continue } # fence toggle: skip the ``` line
if ($inFence) { continue } # inside ``` block: mentioned, not used
if ($line.Contains($FROZEN_MARK)) { continue } # lineage: frozen-marker line
foreach ($m in [regex]::Matches($line, $c6CiteRx)) {
if (Test-Quoted $line $m.Index) { continue } # inline `...`: mentioned, not used
$citePath = $m.Groups[1].Value
$citeLine = [int]$m.Groups[2].Value
$r = Resolve-Cite $citePath
if (-not $r.Exists) {
Write-InformFlag ("{0}:{1}" -f (Rel $f.FullName), ($i + 1)) `
("cite-dead-file: '{0}:{1}' resolves to no file (repo-rel + .claude/ + docs/ + basename-index)" -f $citePath, $citeLine) `
'fix the path or add the missing file (range cites are checked at their first line)'
$c6Flags++
}
else {
if (-not $c6EofCache.ContainsKey($r.File)) {
try { $c6EofCache[$r.File] = [System.IO.File]::ReadAllLines($r.File).Length }
catch { $c6EofCache[$r.File] = -1 }
}
$eof = $c6EofCache[$r.File]
if ($eof -ge 0 -and $citeLine -gt $eof) {
$amb = if ($r.Ambiguous) { ' (basename ambiguous across roots -- may mask a dead cite)' } else { '' }
Write-InformFlag ("{0}:{1}" -f (Rel $f.FullName), ($i + 1)) `
("cite-line-past-eof: '{0}:{1}' but {2} has {3} lines{4}" -f $citePath, $citeLine, (Rel $r.File), $eof, $amb) `
'fix the line number (target file is shorter) or update the cite'
$c6Flags++
}
}
}
}
}
Write-Host (" C6 scanned {0} file(s) (governance-top + commands + agents; adap-reports excluded)" -f $c6Scanned)
Write-Host (" C6 cite-2-tier flags = {0} [INFORM-only, LOW, NOT folded into TOTAL baseline]" -f $c6Flags) -ForegroundColor DarkGray
# ---------------------------------------------------------------------------
# H24-4 - pending-flip (INFORM-only, NEW; count SEPARATE, NOT folded into TOTAL)
#
# Cross-source staleness: a HANDOFF "waiting-for-owner" note whose referenced mark has
# ALREADY flipped (stamped Active/Active-High, or RESOLVED) in ACTIVE-MARKS.md. The
# HANDOFF still says "waiting" while the ledger says "decided" => pending-flip-stale.
#
# SOURCE A = docs/HANDOFF.md, CURRENT SEGMENT ONLY. Boundary = the FIRST line that
# STARTS WITH "**Prev S" (regex ^, NOT substring). M-1 (measured): the current segment
# carries "... segment Prev S134 ..." in the MIDDLE of a line -- a substring match would
# cut the segment early and drop live lines; the ^-anchor does not. STATUS.md is OUT of
# scope v1 (M-2): its In-Progress section is now only S117/S113 lineage -> scanning it
# is background noise; widening is a post-triage call.
#
# MATCH (M-3): split each line on separators " <U+00B7> " (space-middot-space) or " ; "
# -> logic fragments. A waiting token (CHO GAT | CHO ANH | cho-ky, built by code point)
# AND a mark code (RC-... signature OR [carry:slug]) must sit in the SAME fragment. A
# token inside `...`/fence is MENTIONED not used (Test-Quoted + fence).
#
# SOURCE B = .claude/governance/ACTIVE-MARKS.md. The mark's OWN table row (first cell)
# decides status: Active/Active-High -> stamped ; SUPERSEDED/DISABLE -> IM ; a RESOLVED
# note mentioning it -> resolved. stamped|resolved => FLAG ; superseded|not-found => IM.
#
# LIVE first-run EXPECTATION = 0 flags by design (measured: current segment has 0
# waiting tokens). Value is in FUTURE sessions. Declared out-of-reach class: a proposal
# that landed in CODE but was never marked (S137) is a human-eye H24 view audit, not
# machine-visible here.
# ---------------------------------------------------------------------------
Write-Section 'H24-4 - pending-flip (INFORM-only)'
$h244Flags = 0
$handoffP = Join-Path $RepoRoot 'docs\HANDOFF.md'
$marksP = Join-Path $RepoRoot '.claude\governance\ACTIVE-MARKS.md'
if (-not (Test-Path $handoffP)) {
Write-Host ' [skip] no docs/HANDOFF.md - no pending surface' -ForegroundColor DarkGray
}
elseif (-not (Test-Path $marksP)) {
Write-Host ' [skip] no .claude/governance/ACTIVE-MARKS.md - cannot cross-check mark status' -ForegroundColor DarkGray
}
else {
$clsO = '[' + (U @(0x1EDC, 0x1EDD)) + ']' # O-horn-grave upper/lower
$clsA = '[' + (U @(0x1EAC, 0x1EAD)) + ']' # A-circumflex-dot upper/lower
$clsY = '[' + (U @(0x00FD, 0x00DD)) + ']' # y-acute lower/upper
$WaitRx = '[Cc][Hh]' + $clsO + '[ \-]([Gg]' + $clsA + '[Tt]|[Aa][Nn][Hh]|[Kk]' + $clsY + ')'
$MidSep = ' ' + (U @(0x00B7)) + ' '
$SepRx = [regex]::Escape($MidSep) + '| ; '
$CodeRx = '(?:RC-[A-Za-z0-9][A-Za-z0-9-]*)|(?:\[carry:[a-z0-9][a-z0-9._-]*\])'
$BacktickCh = U @(0x60)
$script:marksLines = @(Get-Content -Path $marksP -Encoding UTF8 -ErrorAction SilentlyContinue)
function Get-MarkStatus {
param([string]$Code)
$ownRowRx = '^\|\s*' + $BacktickCh + [regex]::Escape($Code) + $BacktickCh
$status = 'none'
foreach ($ml in $script:marksLines) {
if ($ml -match $ownRowRx) {
if ($ml -match '(?i)SUPERSEDED|DISABLE') { return 'superseded' }
if ($ml -match 'Active') { $status = 'stamped' }
}
elseif (($ml -match '(?i)RESOLVED') -and ($ml.Contains($Code))) {
if ($status -eq 'none') { $status = 'resolved' }
}
}
return $status
}
$handLines = @(Get-Content -Path $handoffP -Encoding UTF8 -ErrorAction SilentlyContinue)
$boundary = $handLines.Count
for ($i = 0; $i -lt $handLines.Count; $i++) {
if ($handLines[$i] -match '^\*\*Prev S') { $boundary = $i; break }
}
Write-Host (" HANDOFF current-segment = lines 1..{0} (boundary '**Prev S' at line {1})" -f $boundary, ($boundary + 1))
$inFence = $false
for ($i = 0; $i -lt $boundary; $i++) {
$line = $handLines[$i]
if ($line -match $FenceRx) { $inFence = -not $inFence; continue }
if ($inFence) { continue }
foreach ($wm in [regex]::Matches($line, $WaitRx)) {
if (Test-Quoted $line $wm.Index) { continue }
# fragment enclosing the token = span between the nearest separators.
$left = 0; $right = $line.Length
foreach ($sm in [regex]::Matches($line, $SepRx)) {
if (($sm.Index + $sm.Length) -le $wm.Index) { $left = $sm.Index + $sm.Length }
elseif ($sm.Index -ge ($wm.Index + $wm.Length)) { $right = $sm.Index; break }
}
$frag = $line.Substring($left, $right - $left)
foreach ($cm in [regex]::Matches($frag, $CodeRx)) {
$st = Get-MarkStatus $cm.Value
if ($st -eq 'stamped' -or $st -eq 'resolved') {
Write-InformFlag ("docs/HANDOFF.md:{0}" -f ($i + 1)) `
("pending-flip-stale: current segment still waits on '{0}' but ACTIVE-MARKS has it {1}" -f $cm.Value, $st) `
'close/re-scope the HANDOFF waiting item, or re-open the mark if the wait is real'
$h244Flags++
}
}
}
}
Write-Host (" H24-4 pending-flip flags = {0} [INFORM-only, LOW, NOT folded; 0 = designed first-run]" -f $h244Flags) -ForegroundColor DarkGray
}
# ---------------------------------------------------------------------------
# Summary + C4 self-exclusion audit (RUNTIME proof)
# ---------------------------------------------------------------------------
@ -1120,6 +1379,7 @@ if ($selfInScan -eq 0 -and $leaked -eq 0) {
Write-Host ''
Write-Host ("TOTAL FLAGS: {0}" -f $script:FlagCount) -ForegroundColor Cyan
Write-Host ("INFORM-ONLY (new nets C6 cite-2-tier + H24-4 pending-flip): {0} - counted SEPARATELY, NOT in TOTAL above (anti-Goodhart, owner-set; fold+sev-raise is post-triage)" -f $script:InformCount) -ForegroundColor DarkGray
Write-Host 'NOTE: DETECT-only lowering net. Exit 0 always (never fails build). FLAGs are advisory.' -ForegroundColor DarkGray
exit 0