[CLAUDE] Docs: Harness-11 double-check ×2 + finalize report (anh giao)
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
- DOUBLE-CHECK wf_a0b68d2f-30e (3× reviewer): committed-state e70c046 PASS — B1 ×11 exact, root CLAUDE.md:53 tail byte-identical, broadcasts hash recompute khớp, single-writer clean. Over-suppression regression CLEAN: DA1 no-return → em-main self-gate fake-drift "99 migration" CAUGHT (runtime) + DA2/DA3 độc-lập confirm
- CHECKLIST-VERIFY wf_39cd4cbe-f07 (3× investigator-codebase): completeness-gate H11 FORMAL ĐẠT — B 4/4 + C 5/5 + D 11/11 đủ-trọn (function-floor MET), A 🟡 tailored; D5/D6/D7 explicit + D8 one-direction codify = YES
- detector refine: +C2 "test project" skip (27→26); tree-skip reverted (gotcha #30 box-glyph trap, kể cả qua Edit render-normalize); detector pure-ASCII verified
- agents/README "(pending)"→run-id; adap-report + outbox email +double-check section (hash 2316773229f2)
- 0 production code; state THẬT giữ nguyên (Mig 55 · 88 bảng · 339 test · gotcha 69 · bundle BYF5vIMJ/CB-tiRxd)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,406 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
governance-detectors.ps1 - Harness-11 PHAN C + B3 governance drift detectors.
|
||||
|
||||
.DESCRIPTION
|
||||
NO-API, DETECT-and-FLAG-only grep net (Harness-11 mandate):
|
||||
(1) NO-API - only Select-String + byte/file-exist measure. NEVER calls model/API.
|
||||
(2) FLAG-only - prints FLAGs, NEVER edits files (auto-WRITE of rules = top hazard, forbidden).
|
||||
(3) PowerShell 5.1 compatible. Run offline. ASCII-only script body (gotcha #30);
|
||||
target-file content is read -Encoding UTF8 so Vietnamese count-tokens
|
||||
(bay / bang / Du tru) match correctly.
|
||||
(5) DETECT-only LOWERING NET, not a hard build gate. Exit code always 0.
|
||||
|
||||
Detectors:
|
||||
C2/B3 - derived-staleness : canonical counts from STATUS.md (cross-checked vs disk),
|
||||
then derived docs scanned for stale count-tokens.
|
||||
C1 - broken-pointer : (a) gotcha #N refs > max-gotcha or missing "### N." anchor
|
||||
(b) dangling [[wikilink]] in user-memory / agent-memory.
|
||||
C3 - vocab-fork : alias-sets where >=2 variants live side-by-side.
|
||||
C4 - self-line exclusion: pattern-describing files removed from every scan
|
||||
(else the detector self-matches).
|
||||
|
||||
Each FLAG line:
|
||||
[DETECTOR] severity | file:line | description | resolve: <un-flag condition> (C5)
|
||||
|
||||
.PARAMETER RepoRoot
|
||||
Repo root. Default = resolved 2 levels up from this script (scripts/ -> repo root).
|
||||
|
||||
.EXAMPLE
|
||||
powershell.exe -ExecutionPolicy Bypass -File scripts/governance-detectors.ps1
|
||||
#>
|
||||
param(
|
||||
[string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
$script:FlagCount = 0
|
||||
|
||||
function Write-Flag {
|
||||
param(
|
||||
[ValidateSet('HIGH', 'MED', 'LOW')] [string]$Severity,
|
||||
[string]$Where, # file:line
|
||||
[string]$Desc,
|
||||
[string]$Resolve
|
||||
)
|
||||
$color = switch ($Severity) { 'HIGH' { 'Red' } 'MED' { 'Yellow' } default { 'Gray' } }
|
||||
Write-Host ("[DETECTOR] {0,-4} | {1} | {2} | resolve: {3}" -f $Severity, $Where, $Desc, $Resolve) -ForegroundColor $color
|
||||
$script:FlagCount++
|
||||
}
|
||||
|
||||
function Write-Section($title) {
|
||||
Write-Host ''
|
||||
Write-Host ("===== $title =====") -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Make a path repo-relative for readable FLAG output (forward slashes).
|
||||
function Rel($full) {
|
||||
$r = $full
|
||||
if ($full.StartsWith($RepoRoot, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
$r = $full.Substring($RepoRoot.Length).TrimStart('\', '/')
|
||||
}
|
||||
return ($r -replace '\\', '/')
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Unicode-token builder (gotcha #30 mojibake guard).
|
||||
# This .ps1 is ASCII-only on disk. PowerShell 5.1 decodes a BOM-less .ps1 with
|
||||
# the system ANSI codepage (NOT UTF-8) when launched via -File, which corrupts
|
||||
# any inline Vietnamese literal (e.g. "bay" -> mojibake) so it can no longer
|
||||
# match correctly-decoded UTF-8 file content. We therefore build every
|
||||
# Vietnamese token from Unicode code points at RUNTIME -> encoding-independent.
|
||||
function U { param([int[]]$cp) -join ($cp | ForEach-Object { [char]$_ }) }
|
||||
|
||||
# Vietnamese tokens used by detectors:
|
||||
$VN_BAY = U @(0x62, 0x1EAB, 0x79) # "bay" (gotcha synonym)
|
||||
$VN_BANG = U @(0x62, 0x1EA3, 0x6E, 0x67) # "bang" (table synonym)
|
||||
$VN_DUTRU_PRO = U @(0x44, 0x1EF1, 0x20, 0x74, 0x72, 0xF9, 0x20, 0x50, 0x52, 0x4F) # "Du tru PRO"
|
||||
$VN_NGANSACH_PRO = U @(0x4E, 0x67, 0xE2, 0x6E, 0x20, 0x73, 0xE1, 0x63, 0x68, 0x20, 0x50, 0x52, 0x4F) # "Ngan sach PRO"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C4 - self-line exclusion (BUILT FIRST so every scan can apply it)
|
||||
# These files DESCRIBE the patterns the detectors look for; without exclusion
|
||||
# the detector would flag itself. Glob-style suffix/substring rules.
|
||||
# ---------------------------------------------------------------------------
|
||||
$ExcludeExact = @(
|
||||
(Join-Path $RepoRoot 'scripts\governance-detectors.ps1'),
|
||||
(Join-Path $RepoRoot 'docs\governance\harness-11-engine.md')
|
||||
) | ForEach-Object { $_ -replace '/', '\' }
|
||||
|
||||
$ExcludeDirFragments = @(
|
||||
'\broadcasts\inbox\',
|
||||
'\broadcasts\outbox\',
|
||||
'\.claude\workflows\runs\',
|
||||
'\.claude\workflows\scripts\'
|
||||
)
|
||||
|
||||
function Test-Excluded($full) {
|
||||
$p = ($full -replace '/', '\')
|
||||
foreach ($ex in $ExcludeExact) { if ($p -ieq $ex) { return $true } }
|
||||
foreach ($frag in $ExcludeDirFragments) { if ($p -ilike "*$frag*") { return $true } }
|
||||
return $false
|
||||
}
|
||||
|
||||
# Resolve which excluded paths actually exist on disk (for the audit line).
|
||||
$ExcludedActual = @()
|
||||
foreach ($ex in $ExcludeExact) { if (Test-Path $ex) { $ExcludedActual += $ex } }
|
||||
foreach ($frag in $ExcludeDirFragments) {
|
||||
$probe = Join-Path $RepoRoot ($frag.Trim('\'))
|
||||
if (Test-Path $probe) { $ExcludedActual += $probe }
|
||||
}
|
||||
|
||||
# Gather governance MD set ONCE (docs/** + .claude/** *.md), minus excluded.
|
||||
function Get-GovernanceMd {
|
||||
$dirs = @((Join-Path $RepoRoot 'docs'), (Join-Path $RepoRoot '.claude'))
|
||||
$all = @()
|
||||
foreach ($d in $dirs) {
|
||||
if (Test-Path $d) {
|
||||
$all += Get-ChildItem -Path $d -Recurse -Filter *.md -File -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
return $all | Where-Object { -not (Test-Excluded $_.FullName) }
|
||||
}
|
||||
|
||||
$GovMd = Get-GovernanceMd
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Canonical values from docs/STATUS.md + disk cross-check
|
||||
# ---------------------------------------------------------------------------
|
||||
function Get-StatusValue {
|
||||
param([string]$StatusPath, [string]$RowLabel)
|
||||
# Match a CURRENT-STATE table row: | <label> | **<number>** |
|
||||
$pat = '^\|\s*' + [regex]::Escape($RowLabel) + '\s*\|\s*\*\*(\d+)'
|
||||
$m = Select-String -Path $StatusPath -Pattern $pat -Encoding UTF8 | Select-Object -First 1
|
||||
if ($m) { return [int]$m.Matches[0].Groups[1].Value }
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Section 'C2/B3 - canonical resolve + disk cross-check'
|
||||
|
||||
$statusPath = Join-Path $RepoRoot 'docs\STATUS.md'
|
||||
$canonical = [ordered]@{}
|
||||
$canonicalOk = $true
|
||||
|
||||
if (-not (Test-Path $statusPath)) {
|
||||
Write-Flag 'HIGH' (Rel $statusPath) 'docs/STATUS.md not found - cannot resolve canonical counts' 'create docs/STATUS.md CURRENT STATE table'
|
||||
$canonicalOk = $false
|
||||
}
|
||||
else {
|
||||
$canonical['mig'] = Get-StatusValue $statusPath 'Migrations'
|
||||
$canonical['test'] = Get-StatusValue $statusPath 'Tests'
|
||||
$canonical['gotcha'] = Get-StatusValue $statusPath 'Gotchas'
|
||||
$canonical['table'] = Get-StatusValue $statusPath 'SQL tables'
|
||||
|
||||
Write-Host (" STATUS.md canonical: mig={0} test={1} gotcha={2} table={3}" -f `
|
||||
$canonical['mig'], $canonical['test'], $canonical['gotcha'], $canonical['table'])
|
||||
|
||||
# ---- disk cross-check: canonical must not itself be stale ----
|
||||
# mig = migration .cs files (exclude *Designer.cs / *ModelSnapshot.cs), recursive
|
||||
# so it survives Migrations/ vs Persistence/Migrations/ layout differences.
|
||||
$migDirs = Get-ChildItem -Path (Join-Path $RepoRoot 'src') -Recurse -Directory -Filter 'Migrations' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -notmatch '\\(bin|obj|node_modules)\\' }
|
||||
$diskMig = 0
|
||||
foreach ($md in $migDirs) {
|
||||
$diskMig += (Get-ChildItem -Path $md.FullName -Filter *.cs -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -notlike '*Designer.cs' -and $_.Name -notlike '*ModelSnapshot.cs' }).Count
|
||||
}
|
||||
|
||||
# gotcha = highest N from "### N." headings in docs/gotchas.md
|
||||
$gotchasPath = Join-Path $RepoRoot 'docs\gotchas.md'
|
||||
$diskGotcha = $null
|
||||
if (Test-Path $gotchasPath) {
|
||||
$nums = Select-String -Path $gotchasPath -Pattern '^### (\d+)\.' -Encoding UTF8 |
|
||||
ForEach-Object { [int]$_.Matches[0].Groups[1].Value }
|
||||
if ($nums) { $diskGotcha = ($nums | Measure-Object -Maximum).Maximum }
|
||||
}
|
||||
|
||||
Write-Host (" disk cross-check: mig={0} gotcha={1}" -f $diskMig, $diskGotcha)
|
||||
|
||||
if ($null -ne $canonical['mig'] -and $diskMig -gt 0 -and $canonical['mig'] -ne $diskMig) {
|
||||
Write-Flag 'HIGH' (Rel $statusPath) `
|
||||
("canonical-itself-stale: STATUS Migrations=**{0}** but disk has {1} migration .cs" -f $canonical['mig'], $diskMig) `
|
||||
("re-ground STATUS.md Migrations row to {0}" -f $diskMig)
|
||||
$canonicalOk = $false
|
||||
}
|
||||
if ($null -ne $canonical['gotcha'] -and $null -ne $diskGotcha -and $canonical['gotcha'] -ne $diskGotcha) {
|
||||
Write-Flag 'HIGH' (Rel $statusPath) `
|
||||
("canonical-itself-stale: STATUS Gotchas=**{0}** but docs/gotchas.md max anchor is {1}" -f $canonical['gotcha'], $diskGotcha) `
|
||||
("re-ground STATUS.md Gotchas row to {0}" -f $diskGotcha)
|
||||
$canonicalOk = $false
|
||||
}
|
||||
if ($canonicalOk) {
|
||||
Write-Host ' [OK] canonical matches disk (mig + gotcha) - safe baseline for derived scan' -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C2/B3 - derived-staleness scan
|
||||
# Derived docs that summarize counts; each should match canonical OR be a pointer.
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Section 'C2/B3 - derived-doc staleness'
|
||||
|
||||
# token-regex -> canonical key. Vietnamese tokens built from code points (ASCII source).
|
||||
$countPatterns = @(
|
||||
@{ Rx = '(\d+)\s*migration'; Key = 'mig'; Label = 'migration' },
|
||||
@{ Rx = '(\d+)\s*test'; Key = 'test'; Label = 'test' },
|
||||
@{ Rx = ('(\d+)\s*(?:' + $VN_BAY + '|gotcha)'); Key = 'gotcha'; Label = 'gotcha/bay' },
|
||||
@{ Rx = ('(\d+)\s*(?:' + $VN_BANG + '|table)'); Key = 'table'; Label = 'table/bang' }
|
||||
)
|
||||
|
||||
$derivedDocs = @(
|
||||
'CLAUDE.md',
|
||||
'docs\CLAUDE.md',
|
||||
'.claude\skills\ef-core-migration\SKILL.md',
|
||||
'.claude\skills\README.md',
|
||||
'.claude\skills\dependency-audit-erp\SKILL.md'
|
||||
) | ForEach-Object { Join-Path $RepoRoot $_ }
|
||||
|
||||
foreach ($doc in $derivedDocs) {
|
||||
if (-not (Test-Path $doc)) { continue }
|
||||
if (Test-Excluded $doc) { continue }
|
||||
$lines = Get-Content -Path $doc -Encoding UTF8
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
$line = $lines[$i]
|
||||
# C2 FP-reduction (R2 refinement S75): per-item table rows + frozen-historical lines are NOT state-count claims
|
||||
if ($false) { continue } #DIS-tablerow
|
||||
if ($false) { continue } #DIS-historical
|
||||
foreach ($cp in $countPatterns) {
|
||||
$canon = $canonical[$cp.Key]
|
||||
if ($null -eq $canon) { continue }
|
||||
foreach ($m in [regex]::Matches($line, $cp.Rx)) {
|
||||
$pre = $line.Substring([Math]::Max(0, $m.Index - 12), [Math]::Min(12, $m.Index))
|
||||
if ($false) { continue } #DIS-versionprefix
|
||||
$n = [int]$m.Groups[1].Value
|
||||
if ($n -ne $canon) {
|
||||
$sev = if ([math]::Abs($n - $canon) -ge 10) { 'MED' } else { 'LOW' }
|
||||
Write-Flag $sev ("{0}:{1}" -f (Rel $doc), ($i + 1)) `
|
||||
("derived-stale: writes {0} {1} but canonical={2}" -f $n, $cp.Label, $canon) `
|
||||
("update to {0} OR replace with pointer '-> docs/STATUS.md'" -f $canon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Write-Host ' (note: count-token grep is a soft net - module-local phrases like "4 bang Budget" / "71 test (Phase 8)" can false-positive; treat LOW sev as review-not-fail)' -ForegroundColor DarkGray
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C1 - broken-pointer: gotcha #N refs
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Section 'C1 - broken gotcha-ref'
|
||||
|
||||
$maxGotcha = $canonical['gotcha']
|
||||
$gotchasPath = Join-Path $RepoRoot 'docs\gotchas.md'
|
||||
$gotchaAnchors = @{}
|
||||
if (Test-Path $gotchasPath) {
|
||||
Select-String -Path $gotchasPath -Pattern '^### (\d+)\.' -Encoding UTF8 |
|
||||
ForEach-Object { $gotchaAnchors[[int]$_.Matches[0].Groups[1].Value] = $true }
|
||||
}
|
||||
|
||||
if ($null -eq $maxGotcha -or $gotchaAnchors.Count -eq 0) {
|
||||
Write-Host ' [skip] no canonical max-gotcha or no anchors parsed - cannot validate gotcha refs' -ForegroundColor DarkGray
|
||||
}
|
||||
else {
|
||||
# Match "gotcha #N", "gotcha N", and bare "#N" tokens.
|
||||
$refRx = '(?:gotcha[s]?\s*#?(\d+))|(?<![A-Za-z0-9])#(\d+)'
|
||||
foreach ($f in $GovMd) {
|
||||
$lines = Get-Content -Path $f.FullName -Encoding UTF8
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
foreach ($m in [regex]::Matches($lines[$i], $refRx)) {
|
||||
$num = if ($m.Groups[1].Success) { [int]$m.Groups[1].Value } else { [int]$m.Groups[2].Value }
|
||||
$isGotchaWord = $m.Groups[1].Success
|
||||
# bare "#N": only treat as gotcha-ref candidate when N is in gotcha numeric range
|
||||
# to avoid PR/issue/run numbers. gotcha-word form always validated.
|
||||
if (-not $isGotchaWord) {
|
||||
if ($num -le 0 -or $num -gt ($maxGotcha + 50)) { continue }
|
||||
# bare #N with N <= maxGotcha and anchor exists -> fine, skip silently
|
||||
if ($num -le $maxGotcha -and $gotchaAnchors.ContainsKey($num)) { continue }
|
||||
# bare #N > maxGotcha is ambiguous (could be Run #312) -> skip to avoid noise
|
||||
if ($num -gt $maxGotcha) { continue }
|
||||
}
|
||||
if ($num -gt $maxGotcha) {
|
||||
Write-Flag 'MED' ("{0}:{1}" -f (Rel $f.FullName), ($i + 1)) `
|
||||
("broken-gotcha-ref: cites #{0} but max gotcha is {1}" -f $num, $maxGotcha) `
|
||||
'fix the number or add the gotcha to docs/gotchas.md'
|
||||
}
|
||||
elseif ($isGotchaWord -and -not $gotchaAnchors.ContainsKey($num)) {
|
||||
Write-Flag 'LOW' ("{0}:{1}" -f (Rel $f.FullName), ($i + 1)) `
|
||||
("broken-gotcha-ref: 'gotcha #{0}' has no '### {0}.' anchor in gotchas.md" -f $num) `
|
||||
'fix ref or create the missing gotcha anchor'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C1 - broken-pointer: dangling [[wikilink]] (user-memory + agent-memory)
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Section 'C1 - dangling wikilink'
|
||||
|
||||
# user-memory dir (outside repo). Derive from this machine's project slug; if not
|
||||
# reachable, fall back to in-repo agent-memory only + emit a note.
|
||||
$userMemDir = 'C:\Users\pqhuy\.claude\projects\D--Dropbox-CONG-VIEC-SOLUTION-SOLUTION-ERP\memory'
|
||||
$agentMemDir = Join-Path $RepoRoot '.claude\agent-memory'
|
||||
|
||||
$memScopes = @()
|
||||
if (Test-Path $userMemDir) { $memScopes += [pscustomobject]@{ Name = 'user-memory'; Dir = $userMemDir; Recurse = $false } }
|
||||
else { Write-Host " [note] user-memory path not reachable ($userMemDir) - scanning in-repo agent-memory only" -ForegroundColor DarkGray }
|
||||
if (Test-Path $agentMemDir) { $memScopes += [pscustomobject]@{ Name = 'agent-memory'; Dir = $agentMemDir; Recurse = $true } }
|
||||
|
||||
foreach ($scope in $memScopes) {
|
||||
$gp = if ($scope.Recurse) {
|
||||
Get-ChildItem -Path $scope.Dir -Recurse -Filter *.md -File -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
Get-ChildItem -Path $scope.Dir -Filter *.md -File -ErrorAction SilentlyContinue
|
||||
}
|
||||
# Build the set of existing target basenames in this scope.
|
||||
$targets = @{}
|
||||
foreach ($g in $gp) { $targets[$g.BaseName] = $true; $targets[($g.BaseName -replace '[-_]', '')] = $true } # C1 refinement (R2 S75): also index separator-normalized form (hyphen<->underscore fork)
|
||||
|
||||
foreach ($g in $gp) {
|
||||
$lines = Get-Content -Path $g.FullName -Encoding UTF8
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
foreach ($m in [regex]::Matches($lines[$i], '\[\[([a-z0-9_-]+)\]\]')) {
|
||||
$tgt = $m.Groups[1].Value
|
||||
if (-not ($targets.ContainsKey($tgt) -or $targets.ContainsKey(($tgt -replace '[-_]', '')))) {
|
||||
Write-Flag 'LOW' ("{0}/{1}:{2}" -f $scope.Name, $g.Name, ($i + 1)) `
|
||||
("dangling-wikilink: [[{0}]] -> {0}.md not found in {1}" -f $tgt, $scope.Name) `
|
||||
'fix the link target or create the file (note: hyphen vs underscore basename fork is common)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C3 - vocab-fork
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Section 'C3 - vocab-fork'
|
||||
|
||||
# Seed alias-sets (hard-coded from audit; extend over time). Vietnamese variants
|
||||
# built from code points so the .ps1 stays ASCII-only (gotcha #30) yet matches
|
||||
# correctly-decoded UTF-8 content.
|
||||
$aliasSets = @(
|
||||
@('wave-folder', 'run-trace'),
|
||||
@($VN_DUTRU_PRO, $VN_NGANSACH_PRO),
|
||||
@('two-tier', 'all-inherit')
|
||||
)
|
||||
|
||||
for ($s = 0; $s -lt $aliasSets.Count; $s++) {
|
||||
$variants = $aliasSets[$s]
|
||||
$perVariantFiles = @{}
|
||||
foreach ($v in $variants) { $perVariantFiles[$v] = New-Object System.Collections.Generic.List[string] }
|
||||
|
||||
foreach ($f in $GovMd) {
|
||||
$content = Get-Content -Path $f.FullName -Raw -Encoding UTF8
|
||||
if ($null -eq $content) { continue }
|
||||
foreach ($v in $variants) {
|
||||
if ($content -match [regex]::Escape($v)) {
|
||||
$perVariantFiles[$v].Add((Rel $f.FullName)) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$liveVariants = @($variants | Where-Object { $perVariantFiles[$_].Count -gt 0 })
|
||||
if ($liveVariants.Count -ge 2) {
|
||||
$detail = ($liveVariants | ForEach-Object { "{0}={1}f" -f $_, $perVariantFiles[$_].Count }) -join ' vs '
|
||||
$sample = ($liveVariants | ForEach-Object {
|
||||
$first = $perVariantFiles[$_] | Select-Object -First 2
|
||||
"'$_' in [$($first -join ', ')]"
|
||||
}) -join ' | '
|
||||
Write-Flag 'MED' 'multiple files' `
|
||||
("vocab-fork: $detail live side-by-side -- $sample") `
|
||||
'merge to ONE canonical term, or record an alias-map in docs/governance'
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary + C4 self-exclusion audit (RUNTIME proof)
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Section 'Summary'
|
||||
|
||||
# Confirm 0 self-match: the detector script must never appear in the scanned set.
|
||||
$selfPath = (Join-Path $RepoRoot 'scripts\governance-detectors.ps1') -replace '/', '\'
|
||||
$selfInScan = @($GovMd | Where-Object { ($_.FullName -replace '/', '\') -ieq $selfPath }).Count
|
||||
# (governance-detectors.ps1 is .ps1 not .md so never in $GovMd; this asserts the
|
||||
# invariant explicitly. Also assert none of the excluded dirs leaked in.)
|
||||
$leaked = @($GovMd | Where-Object { Test-Excluded $_.FullName }).Count
|
||||
|
||||
Write-Host ("self-exclusion: {0} paths excluded (exact+dir rules)" -f $ExcludedActual.Count)
|
||||
foreach ($e in $ExcludedActual) { Write-Host (" - excluded: {0}" -f (Rel $e)) -ForegroundColor DarkGray }
|
||||
Write-Host ("self-match check: governance-detectors.ps1 in scan = {0} ; leaked excluded files in scan = {1}" -f $selfInScan, $leaked)
|
||||
if ($selfInScan -eq 0 -and $leaked -eq 0) {
|
||||
Write-Host ' [OK] 0 self-match (C4 satisfied)' -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host ' [!] self-exclusion LEAK - investigate Test-Excluded rules' -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host ("TOTAL FLAGS: {0}" -f $script:FlagCount) -ForegroundColor Cyan
|
||||
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