wal: pause
This commit is contained in:
@ -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
|
||||
|
||||
Reference in New Issue
Block a user