[CLAUDE] Docs: S123 citation-trap guard + H24-3 + DUAL-ACCEPT + luat retire-legacy
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m22s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m22s
Anh chot 4 viec governance. Detector 46->45 = DOI THANH-PHAN (-2 duong-gia +1 duong-THAT), khong phai "flag giam". Mark moi RC-pqhuy1987-15-07-2026-17-23-10. - backtick-guard H24-1: discriminator = ENCLOSURE (use vs mention), KHONG port duoc charset-guard cua H24-2 vi vi-du byte-identical voi claim. Helper DUY-NHAT Test-Quoted. Fault-inject 5/5 (2 anti-Goodhart). - H24-3 session-label lag (MED, moi): va am-gia CHUNG-MINH-DUOC cua H24-1 (so NGAY ma ngay = moi nhat => 0 flag trong khi nhan lui 3 phien; tai-phat lan 4). Fault-inject 6/6 + 6/6 sau fix. - DUAL-ACCEPT (dang-3 RETIRED) + LUAT RETIRE LEGACY: go nhanh legacy khi tap di-san RONG, giu khi CON nguoi thu-huong. Repo that 0 orphan/26. Fault-inject 7/7. - tiep.md:66 stale-at-birth + sweep view-residual-asym 6/6 be-mat LUAT sach. Reviewer PASS_WITH_FIXES bat 6 loi that, lead verify 4/4 doc-lap: gen-2 citation-trap (lead viet 30 dong chung-minh dinh-luat roi de H24-3 KHONG guard) - greedy .* lay CUOI khong phai MAX - ly-do retire ap cho dang-2 thi giet C8 - "retire HOAN-THANH mark" = nguy-bien - "5/5" = 4/4. Goodhart DO DUOC (ngoai diff): W4/S122 "va" permission-matrix:16 bang doi FORMAT anchor => khop 0 pattern => H24-1 mat chinh positive-control no dung quanh; flag-count GIAM nen trong nhu thanh-cong. Outward: addendum R5 (DUNG adopt TRI-ACCEPT) + email hub ed3786a10f12 (selftest-stamp MATCH x2). _index reconcile: 2 email S122 gui ma chua log. #53 garble x4/3 agent - harvest-curator ghi diary TRONG luc lam nen vot duoc tu dia ngay; reviewer tu rut luat "ghi diary TRUOC return" tu garble-1 va nho dung no ma garble-2 vot duoc. Prompt-dung KHONG chan duoc #53. Lead sai 4 ca tron o truc dem-ve-minh: "25"!=26 - "5/5"!=4/4 - viet "7/7 PASS" TRUOC khi chay - "0 proxy" sai. State GIU NGUYEN: Mig 66 - 89 bang - 509 test - gotcha 82 - menu 54. Con no: STATUS:6 CO Y chua bump (H24-3 dang FLAG, giu cho hien ra). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -126,6 +126,15 @@ foreach ($frag in $ExcludeDirFragments) {
|
||||
}
|
||||
|
||||
# Gather governance MD set ONCE (docs/** + .claude/** *.md), minus excluded.
|
||||
# VENDOR-SKIP (S123 review): this walks the FILESYSTEM, so it does NOT honour .gitignore.
|
||||
# Measured: docs/_user-guide/node_modules/ holds 30 gitignored .md that were being scanned
|
||||
# as if they were governance docs. Zero anchors in them today => zero impact so far, but
|
||||
# H24-1 sets moc-phai to the MAX anchor in the corpus: ONE vendor README carrying a future
|
||||
# "**Last updated:** 2027-xx-xx" would push the right edge past every real doc and silence
|
||||
# the detector for good -- the exact strangle documented at the H24-1 banner below.
|
||||
# The '(bin|obj|node_modules)' filter already existed further down for the code-scan; it
|
||||
# was simply never applied here. Same class, one site patched, sibling missed -- which is
|
||||
# the S123 lesson in miniature: patch a class, then grep every same-class site.
|
||||
function Get-GovernanceMd {
|
||||
$dirs = @((Join-Path $RepoRoot 'docs'), (Join-Path $RepoRoot '.claude'))
|
||||
$all = @()
|
||||
@ -134,7 +143,9 @@ function Get-GovernanceMd {
|
||||
$all += Get-ChildItem -Path $d -Recurse -Filter *.md -File -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
return $all | Where-Object { -not (Test-Excluded $_.FullName) }
|
||||
return $all |
|
||||
Where-Object { $_.FullName -notmatch '[\\/](bin|obj|node_modules)[\\/]' } |
|
||||
Where-Object { -not (Test-Excluded $_.FullName) }
|
||||
}
|
||||
|
||||
$GovMd = Get-GovernanceMd
|
||||
@ -527,14 +538,76 @@ $AnchorPatterns = @(
|
||||
# detectors' baselines, which is not this lane's call.
|
||||
$TitleFreshSkip = @('\docs\_archive\')
|
||||
|
||||
# BACKTICK-GUARD (S123) -- the self-reference trap, measured not guessed.
|
||||
# A doc that DEFINES the stale-anchor anti-pattern must QUOTE a stale anchor to
|
||||
# explain it. That quote is byte-identical to a live anchor -- being identical is
|
||||
# the POINT of a good example -- so a pattern-matcher flags the teacher.
|
||||
# Measured on disk @S123: 4 H24-1 flags, 2 are this exact case:
|
||||
# .claude/agents/lead-view-auditor.md:45 quotes the anchor as the EXAMPLE
|
||||
# for its own class `view-stale-header`
|
||||
# docs/governance/adap-reports/...harness-24...:78 quotes it while EXPLAINING that
|
||||
# very false-positive <- 3rd generation
|
||||
# The defect PROPAGATES BY CITATION: every doc written ABOUT the false-positive
|
||||
# becomes one. Root instance = permission-matrix/SKILL.md:16 (a REAL defect, fixed W4).
|
||||
#
|
||||
# WHY NOT the H24-2 mechanism (charset-discriminator, :670-672): that guard works
|
||||
# because the format-spec literal "[carry:<slug>]" carries a '<' that a real key can
|
||||
# NEVER contain. Here there is no such character -- example and claim are byte-equal.
|
||||
# A DIFFERENT discriminator is required: not the anchor's SHAPE, but its ENCLOSURE.
|
||||
#
|
||||
# THE RULE: an anchor sitting inside an inline-code span (`...`) is being MENTIONED,
|
||||
# not USED (use/mention distinction). Count backticks left of the match; ODD => we are
|
||||
# inside an open span => quoted example => keep looking. Measured 4/4 ON DISK TODAY:
|
||||
# BARE -> docs/rag-setup-plan.md:4 . form-engine/SKILL.md:15 = TRUE POSITIVE, still fires
|
||||
# QUOTED -> lead-view-auditor.md:45 . adap-report...:78 = FALSE POSITIVE, now skipped
|
||||
#
|
||||
# 4/4, NOT the "5/5" an earlier draft of this comment claimed (fixed S123 after review).
|
||||
# The would-be 5th case, permission-matrix/SKILL.md:16, IS NOT ON DISK ANY MORE and does
|
||||
# NOT fire -- W4/S122 "fixed" it by rewriting the anchor into "**Status (cap-nhat ...):**",
|
||||
# a form that matches NEITHER $AnchorPatterns entry. So it does not even parse, let alone
|
||||
# flag. Verified: both patterns return False against that line.
|
||||
# -> ADJACENT HAZARD, worth naming: that file left the MEASUREMENT SET instead of getting
|
||||
# fresh. H24-1 thereby lost the exact positive-control it was built around (see the
|
||||
# anchor_patterns note below: pattern #2 exists BECAUSE of permission-matrix). Flag
|
||||
# count fell, which reads like success -- Goodhart in its quietest form. Do NOT
|
||||
# "resolve" a title-stale flag by reshaping the anchor; refresh the date or declare
|
||||
# the doc frozen-historical.
|
||||
# -> pattern #2 now matches ONLY form-engine/SKILL.md:15. If that one is ever reshaped
|
||||
# too, pattern #2 becomes dead code that exists purely to catch quoted examples.
|
||||
#
|
||||
# WHY NO head-N-lines scope on top: position is a PROXY, and the backtick rule already
|
||||
# separates the measured set exactly. Stacking a proxy buys zero measured precision while
|
||||
# adding a NEW blind spot -- a doc that legitimately anchors below line N goes silently
|
||||
# unwatched. Mark RC-pqhuy1987-20-06-2026-10-29-11 (proxy-instead-of-signal = false
|
||||
# economy) applies: use the real signal, not its shadow.
|
||||
#
|
||||
# HONEST LIMIT (do not read this as closed): the guard reads INLINE spans only. An
|
||||
# anchor quoted inside a fenced ``` block has ZERO backticks on its own line => reads
|
||||
# BARE => still a false positive. Measured 0 such cases today (383 files scanned, 6 anchor
|
||||
# hits, none inside a fence), so it is left unhandled rather than fixed blind. Same for
|
||||
# escaped \` and ``double-tick`` spans.
|
||||
|
||||
# Test-Quoted -- SHARED by H24-1 (Get-AnchorDate) and H24-3 (session-label).
|
||||
# Deliberately a FUNCTION, not two inline copies: the citation trap is a CLASS, and the
|
||||
# S123 review caught H24-3 shipping WITHOUT this guard in the very diff that proved the
|
||||
# class exists. One greppable definition is how the next detector inherits the fix instead
|
||||
# of re-earning it. If you add a detector that pattern-matches prose, call this.
|
||||
function Test-Quoted {
|
||||
param([string]$Line, [int]$Index)
|
||||
return ((([regex]::Matches($Line.Substring(0, $Index), '`')).Count % 2) -eq 1)
|
||||
}
|
||||
|
||||
function Get-AnchorDate {
|
||||
param([string]$Path)
|
||||
$ls = Get-Content -Path $Path -Encoding UTF8 -ErrorAction SilentlyContinue
|
||||
if ($null -eq $ls) { return $null }
|
||||
for ($i = 0; $i -lt $ls.Count; $i++) {
|
||||
foreach ($rx in $AnchorPatterns) {
|
||||
$m = [regex]::Match($ls[$i], $rx)
|
||||
if ($m.Success) {
|
||||
# Matches (all), not Match (first): one line may carry a quoted example
|
||||
# BEFORE a live anchor. Taking only the first match would let the example
|
||||
# shadow the real claim on that line.
|
||||
foreach ($m in [regex]::Matches($ls[$i], $rx)) {
|
||||
if (Test-Quoted $ls[$i] $m.Index) { continue } # inside `...` => mentioned, not used
|
||||
$raw = $m.Groups[1].Value
|
||||
$dt = [datetime]::MinValue
|
||||
$ok = [datetime]::TryParseExact($raw, 'yyyy-MM-dd',
|
||||
@ -584,6 +657,123 @@ else {
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# H24-3 - session-label lag (do-lech-nhan-phien) [NEW S123]
|
||||
#
|
||||
# WHY THIS EXISTS -- H24-1 has a PROVEN false-negative, and this is it.
|
||||
# docs/STATUS.md:6 read "**Last updated:** 2026-07-15 (S119 ...)" while :35 read
|
||||
# "## Recently Done (S122 ...)". Measured: S119 x3 hits in the mega-line, S120/S121/
|
||||
# S122 = 0 hits => the header label had fallen 3 sessions behind the content.
|
||||
# H24-1 CANNOT see this: it compares DATES, and the date 2026-07-15 was the NEWEST
|
||||
# in the corpus => 0 flags. The doc was simultaneously the freshest thing on disk
|
||||
# and wrong about itself.
|
||||
#
|
||||
# NOT a one-off: the same :6-lag was caught by hand at S116, S117, S118 and again at
|
||||
# S123 -- 4 recurrences. Three of those were fixed by bumping the number, which is
|
||||
# why it came back a 4th time. Per the owner's own S122 finding: what stops a repeat
|
||||
# is a LAW THAT CAN SEE, not a memory that must remember. So the axis changes from
|
||||
# DATE (H24-1) to SESSION LABEL (here).
|
||||
#
|
||||
# THE RULE: within ONE file, the "(S<N>)" on the **Last updated** line must not be
|
||||
# older than the newest "(S<M>)" carried by a heading. Both numbers live in the same
|
||||
# file, so there is no cross-file inference and no ratio-band -- a mismatch is wrong
|
||||
# BY CONSTRUCTION. Hence MED, not the LOW that H24-1's noisier date-axis earns.
|
||||
# -> HONEST CORRECTION (S123 review): an earlier draft justified MED with "0 proxy".
|
||||
# That was WRONG. $labelN is the FIRST regex hit, which is a PROXY for "this file's
|
||||
# OWN label" -- and it breaks precisely when the file QUOTES someone else's label.
|
||||
# MED survives because Test-Quoted removes that failure mode, NOT because no proxy
|
||||
# was ever there. Naming the proxy is the point; pretending it is absent is how the
|
||||
# next reader stops looking for it.
|
||||
#
|
||||
# TWO BUGS THIS DETECTOR SHIPPED AND THE S123 REVIEW CAUGHT -- both worth remembering:
|
||||
# (a) NO Test-Quoted guard. The same diff that added this detector ALSO added 30 lines
|
||||
# proving "every doc written ABOUT the false-positive becomes one" -- and then built
|
||||
# a new prose-matcher without the guard. Generation-2 of the very trap. The canonical
|
||||
# trigger would have been the S123 adap-report DESCRIBING this detector.
|
||||
# => the lesson is mechanical, not moral: patch a trap CLASS, then grep every
|
||||
# same-class matcher IN THAT DIFF. A lesson sitting in context does not self-fire.
|
||||
# (b) GREEDY '.*' took the LAST "(S<N>" on a line, not the MAX. Isolated repro:
|
||||
# "## Tong hop (S122 ...) va lich su (S110 ...)" -> regex yielded S110, max is S122
|
||||
# => could print "[ok] label S115 >= newest heading S110", a false-negative stated
|
||||
# as a fact about the file. Fixed by scanning ALL matches per line and taking max.
|
||||
# The 6/6 fault-inject missed it because every case put ONE "(S<N>" per heading:
|
||||
# N/N PASS proves the axes TOUCHED, never the axis never tried. Ask instead:
|
||||
# "which axis has no case at all?" (corpus has 0 such headings today => was LATENT).
|
||||
#
|
||||
# Measured @S123 on the live corpus (both directions, no tuning):
|
||||
# docs/STATUS.md label S119 vs max-heading S122 -> 119 < 122 => FLAG (the real defect)
|
||||
# docs/HANDOFF.md label S122 vs max-heading S84 -> 122 > 84 => silent
|
||||
# HANDOFF needs NO special case: its only S-heading is an archive pointer
|
||||
# ("Session detail cu (S84 -> tro ve truoc)"), and a pointer to old sessions is
|
||||
# exactly what a CURRENT label should outrank. The rule reads that correctly on its own.
|
||||
#
|
||||
# HONEST LIMIT: this catches a label that lags its OWN headings. A file whose label
|
||||
# AND headings are both stale together stays silent -- consistency is not freshness.
|
||||
# That gap is real and left open rather than papered over with a second proxy.
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Section 'H24-3 - session-label lag'
|
||||
|
||||
$SessLabelRx = '\*\*Last updated:\*\*[^(]*\(S(\d+)'
|
||||
# Heading side is scanned in TWO steps, NOT one regex: '^#{2,}\s' decides "is this a
|
||||
# heading?", then EVERY '\(S(\d+)' on that line is collected and max-ed. The old
|
||||
# single-regex '^#{2,}\s+.*\(S(\d+)' looked equivalent and was not -- greedy '.*' made it
|
||||
# "the LAST (S<N> on the line", which is a different question from "the newest".
|
||||
$SessHeadingLineRx = '^#{2,}\s'
|
||||
$SessNumRx = '\(S(\d+)'
|
||||
|
||||
$sessChecked = 0
|
||||
foreach ($f in $GovMd) {
|
||||
$p = ($f.FullName -replace '/', '\')
|
||||
$skip = $false
|
||||
# Same frozen-by-design carve-out as H24-1: an archive's old label IS the record.
|
||||
foreach ($frag in $TitleFreshSkip) { if ($p -ilike "*$frag*") { $skip = $true } }
|
||||
if ($skip) { continue }
|
||||
|
||||
$ls = Get-Content -Path $f.FullName -Encoding UTF8 -ErrorAction SilentlyContinue
|
||||
if ($null -eq $ls) { continue }
|
||||
|
||||
$labelN = $null; $labelLine = 0
|
||||
$maxHeadN = $null; $maxHeadLine = 0
|
||||
for ($i = 0; $i -lt $ls.Count; $i++) {
|
||||
# LABEL: first UNQUOTED hit wins. Matches (all) + Test-Quoted, so a line that
|
||||
# quotes an example label before carrying a real one still yields the real one.
|
||||
if ($null -eq $labelN) {
|
||||
foreach ($lm in [regex]::Matches($ls[$i], $SessLabelRx)) {
|
||||
if (Test-Quoted $ls[$i] $lm.Index) { continue }
|
||||
$labelN = [int]$lm.Groups[1].Value; $labelLine = $i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
# HEADING: MAX over ALL unquoted hits ON the line (not the first, not the last).
|
||||
# $SessHeadingRx anchors '^#{2,}' so it only matches at line start; iterating
|
||||
# Matches() would re-scan the same line, so strip the anchor and scan the tail.
|
||||
if ($ls[$i] -match $SessHeadingLineRx) {
|
||||
foreach ($hm in [regex]::Matches($ls[$i], $SessNumRx)) {
|
||||
if (Test-Quoted $ls[$i] $hm.Index) { continue }
|
||||
$hn = [int]$hm.Groups[1].Value
|
||||
if (($null -eq $maxHeadN) -or ($hn -gt $maxHeadN)) { $maxHeadN = $hn; $maxHeadLine = $i + 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# No label, or no S-heading to compare against => this doc makes no session claim.
|
||||
if (($null -eq $labelN) -or ($null -eq $maxHeadN)) { continue }
|
||||
$sessChecked++
|
||||
|
||||
if ($labelN -lt $maxHeadN) {
|
||||
Write-Flag 'MED' ("{0}:{1}" -f (Rel $f.FullName), $labelLine) `
|
||||
("session-label lag: header label says S{0} but the file's own newest heading is S{1} (line {2}, {3} session(s) ahead)" -f `
|
||||
$labelN, $maxHeadN, $maxHeadLine, ($maxHeadN - $labelN)) `
|
||||
'bump the header label to match the newest section, OR drop the session label from the header so it cannot go stale (B1 collapse)'
|
||||
}
|
||||
else {
|
||||
Write-Host (" [ok] {0}: label S{1} >= newest heading S{2}" -f (Rel $f.FullName), $labelN, $maxHeadN) -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
if ($sessChecked -eq 0) {
|
||||
Write-Host ' [skip] no doc carries BOTH a session label and an S-heading - nothing to compare' -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# H24-2 - carry-age (INFORM-only)
|
||||
# A [carry:<slug>] key that survives >= M CONSECUTIVE most-recent carry-lines is
|
||||
|
||||
Reference in New Issue
Block a user