Repository navigation
redaction, pkg/scan: when coverage is cut short, redaction paths return partial output without failing #770
Description
Summary
When coverage is cut short (caller ctx cancelled or expired, a validator size cap, a fired --validator-budget, an unparseable file), pkg/redact.Engine.Redact and pkg/scan.RedactFile return unredacted text with err == nil, and --stdin --enable-redaction exits 0 even under --fail-on-incomplete. Measured: a pre-cancelled ctx returns Redacted identical to the input (2,000/2,000 cards and SSNs in clear); a 5.6 MiB log under context.Background() keeps 5,518/5,518 ARN lines in clear; an unparseable .pdf is copied through verbatim. Only --file discloses (NOT FULLY EXAMINED, exit 3 under the flag). A fired time budget also discards every finding the validator made (SSN=1s: 0 of 20,000; a one-line A/B keeps 7,424–13,824) and runs every check on the file two extra times (CPU 5.38–6.12 s vs 3.23–3.40 s). pkg/scan.ScanText/ScanFile ignore their ctx entirely.
Reproduction
Measured on main @ 3df48ac (v2.5.3); line references are at that commit. main has since gained dependency patch bumps and #751, #752 and #759 (changelog CI, perfguard, build tooling), none of which changes detection or redaction.
printf 'Ticket 4471: caller verified SSN %s, reply to j7.example@example.org\n' "$(printf '5%02d-%02d-%04d' 17 17 1007)" > one.txt
ferret-scan --stdin --enable-redaction --checks SSN,EMAIL --validator-budget all=1ns --fail-on-incomplete < one.txt; echo "exit=$?"
ferret-scan --file one.txt --enable-redaction --checks SSN,EMAIL --validator-budget all=1ns --fail-on-incomplete; echo "exit=$?"--stdin: the line unchanged, a stderr warning, exit 0 (3/3 runs). --file: NOT FULLY EXAMINED … coverage cut short, exit 3.
Inputs and method
All values are synthetic:
- Cards: the body
453201+%09dof100000000+(i*7919)%899999999, closed with a computed Luhn digit, written as four space-separated groups of 4. - SSN-shaped values:
5%02d-%02d-%04dof(10+i%80, 10+i%89, 1000+i%8999). - E-mail: addresses at
example.org. - ARN:
arn:aws:s3:::synthetic-bucket-q7x2/logs.
Files:
records.txt: 20,000 lines (2,177,780 B) of the formRecord %d: customer card %s, SSN %s, contact j%d.example@example.org on file.infra.log: 5,734,445 B. One e-mail line, then the ARN on every 21st line and padding otherwise (5,518 ARN lines).broken.pdf: a%PDF-1.7header and one SSN-shaped value, with no PDF structure.
CPU is user+sys (getrusage, /usr/bin/time -p), from interleaved samples at load averages 6–66. Go harnesses use replace github.com/awslabs/ferret-scan/v2 => <checkout>.
Matrix A: surfaces on cut-short inputs
CLI runs use --enable-redaction --format json --output findings.json. "Copy" means the file in --redaction-output-dir. Exit codes are shown as without / with --fail-on-incomplete.
| input / trigger | route | exit | redacted output | disclosure |
|---|---|---|---|---|
| one.txt, no budget | --file / --stdin |
0 | copy / stdout, both values masked | — |
| clean file | --file |
0 | no copy | — |
| clean file | --stdin |
0 | stdout = input | — |
one.txt, all=1ns |
--file |
0 / 3 | no copy (0 matches) | NOT FULLY EXAMINED, stats.files_not_examined: 1 |
one.txt, all=1ns |
--stdin |
0 / 0 | stdout = input | stderr WARNING only; findings.json = {"results": []} |
one.txt, all=1ns, --pre-commit-mode |
--file |
0 / 3 | no copy | one-line notice (124 B, worded contents unreadable); findings.json 193–194 B with stats |
one.txt, all=1ns, --pre-commit-mode |
--stdin |
0 / 0 | stdout = input | 0 B stderr; findings.json 0 B |
broken.pdf, --checks SSN |
--file |
0 / 3 | no copy | NOT FULLY EXAMINED, cannot parse |
infra.log, CLOUD_RESOURCES,EMAIL |
--file |
0 / 3 | copy: e-mail masked, 5,518/5,518 ARN lines in clear | NOT FULLY EXAMINED |
infra.log, CLOUD_RESOURCES,EMAIL |
--stdin |
0 / 0 | stdout byte-identical to the copy | stderr WARNING only; no stats |
records.txt, --checks SSN, SSN=300ms |
--stdin |
0 / 0 | stdout = input, 20,000/20,000 SSNs | stderr WARNING only |
Detect-only --stdin already exits 3 under the flag, but in pre-commit mode it prints 0 B of disclosure too.
library call (Background() unless noted) |
result |
|---|---|
RedactFile(broken.pdf, SSN) |
err=nil, count=0, output = input. ScanFile reports Incomplete with cause cannot parse. |
RedactFile(infra.log, CLOUD_RESOURCES,EMAIL) |
err=nil, count=1, 5,518/5,518 ARN lines in clear |
RedactFile(infra.log, CLOUD_RESOURCES) |
err=nil, count=0, clean-file passthrough |
Engine.Redact(infra.log, CLOUD_RESOURCES) |
err=nil, 0 findings, Redacted == input |
Engine.Redact, 2,000 records |
Background(): 6,000 findings (1.12–1.17 s CPU). Pre-cancelled, or a 1, 100 or 200 ms deadline: err=nil, 0 findings, FindingsByType=map[], Redacted == input. A 2 s deadline (does not fire): 6,000. |
ScanText(infra.log, CLOUD_RESOURCES) |
err=nil, Incomplete=true, len(NotExamined)=0, reason validator declined oversize content: validator declined oversize content: cloud-resource scan skipped, … |
ScanText/ScanFile, 10,000 records, pre-cancelled or a 50 ms deadline |
30,000/30,000 findings, Incomplete=false, 1.64–2.07 s CPU. The ctx is ignored. |
examples/lambda-redact/handler.go enables every check (:78), passes the invocation ctx (:176), and returns res.Redacted whenever err == nil (:224-234). So a firing deadline, or a request over 5 MiB, returns the original text.
Matrix C: fired budgets
Run on records.txt with --format json --confidence all --limit 0, interleaved:
--checks |
budget | findings | files_not_examined |
CPU |
|---|---|---|---|---|
| SSN | none | 20,000 | absent | 2.46–3.30 s |
| SSN | SSN=300ms / 1s / 1.5s |
0 | 1 | 1.39–1.79 / 3.58–4.32 / 5.06–6.00 s |
| SSN | SSN=3s / 20s (does not fire) |
20,000 | absent | 2.47–3.14 s |
| SSN,EMAIL | none | 20,000 SSN + 20,000 e-mail | absent | 3.23–3.40 s |
| SSN,EMAIL | SSN=300ms / 1s |
e-mail only | 1 | 3.12–3.20 / 5.38–6.12 s |
With SSN=300ms and --checks SSN,EMAIL, --debug logs 3 process_content passes, 2 s and then 3 s apart, each ending in context deadline exceeded. It also logs 3 email_validator validate_content records, against 1 without a budget.
Binary A/B. The only change is at dual_path_bridge.go:962: errors.Is(r.err, execguard.ErrMatchBudgetExceeded) becomes execguard.IsCoverageCutShort(r.err) (plus dropping the then-unused errors import). Results:
| budget | HEAD keeps | widened keeps |
|---|---|---|
| none | 20,000 | 20,000 (identical one by one) |
SSN=300ms |
0 | 2,304–4,096 |
SSN=1s |
0 | 7,424–13,824 |
The widened gate is necessary but not sufficient:
- Lower scores. Each kept partial matches a complete-run finding on (line, text, type) (13,824/13,824 in one sample), but each 75 scores 55 and each 60 scores 40, so MEDIUM drops to LOW (12,870 and 949 of 13,824; the 5 at 100 are unchanged).
- Missing metadata. Partials lack the bridge metadata:
context_*,confidence_adjustment,original_confidence,semantic_contextandvalidation_path. - Caller-ctx path still loses everything.
Engine.Redacton 4,000 lines returns 0 findings at 100–300 ms deadlines (18/18 samples). - Cancellation leaves work running. A 50 ms deadline on 20,000 lines returns in 50–52 ms, but the abandoned validators then burn 138–204 ms of CPU over the next 4 s.
Root cause
A cut-short scan is treated as success or as "found nothing".
- A (sinks):
pkg/redact/engine.go:269-272dropsrunErr;internal/core/redact.go:131checks onlyUnredactedFiles, then passes through (:140-143);cmd/stdin.go:348returns before the--fail-on-incompleteresolution (:408-418) intoreturn 0(:556-559);:250drops the pre-commit warning. - B (ctx):
pkg/scan/scan.go:286,:322take_ context.Context;internal/core/scanner.go:415andinternal/parallel/worker_pool.go:168root oncontext.Background(). - C (partials):
internal/validators/dual_path_bridge.go:961-963keeps partials only onErrMatchBudgetExceeded, a hand copy ofexecguard.IsCoverageCutShort(execguard.go:87-105);worker_pool.go:353retries it:internal/resilience/errors.go:155,:183-198class a deadline retryable.
Detail
- No field to carry the loss.
redact.ResultandAuditRecord(types.go:221,:187) have no incomplete field, and neither doesRedactFileResult(redactfile.go:41-48). - Comments that promise the opposite.
redactfile.go:54: "safe to share".engine.go:293-295: "all-or-nothing".engine.go:262-264: "pass a derived context".validator_runner.go:60-63: callers "log the error and continue". That is safe for detection and unsafe for redaction.
- stdin.
cmd/stdin.go:245-249calls the exit code "advisory warning only".- The residual floor at :478-491 checks only values that were reported.
:369and:585buildFormatterOptionswithoutStats,NotExaminedorOutputToFile. File mode sets the last of these atcmd/main.go:2006.
- No library time bound.
ScanConfig(scanner.go:30) has noValidatorBudgets, andScanTextleavesContentScanConfig.ValidatorBudgets(scanner.go:354) unset.RedactFiletakes no ctx.core.ScanContentnever fillsNotExamined. Its branches (scanner.go:431-447) enumerate the family by hand, omitErrValidatorPanicked, and prepend the reason twice.internal/web/server.go:899passes no ctx, andr.Context()appears nowhere.Preprocessor.Process(filePath string)(preprocessor.go:256) takes no ctx, so extraction cannot be interrupted.
- Partials.
- The 18 validators that poll
LineLoopCancelledreturn their partials withctx.Err(). - The bridge sends them under
IsCoverageCutShort(:884-886), before contextual scoring (:892), and then the narrower gate drops them. - The intent is stated in PR feat(v2-refactor): per-validator time + match budgets (Move C) + secrets/cloudresources ctx #110,
V2_ARCHITECTURE.md:555andexecguard.go:208-211. keepMatchescame in with 7524437 and was missed when b11dc5a (fix(cloudresources): disclose the oversize refusal instead of returning an empty result #424) folded the other copies.- Both drains (
dual_path_bridge.go:1010-1017andvalidator_runner.go:327-343) stop atdefault:on a parentDone. DefaultValidatorRetryStrategy()(validator_runner.go:50-55) setsMaxRetries=2, with a 1 s interval, ×2.execguard/budget_test.go:62asserts partial survival one layer below the broken gate.
- The 18 validators that poll
Width of the class
Redaction sinks: 4 (3 fail open). pkg/ exports taking a ctx: 3, all of them (2 discard it, 1 fails open). Hand copies of the cut-short family outside execguard (non-test): 2. Non-blocking drains: 2. Retry wrappers: 1. Validators returning partials with ctx.Err(): 18. Tests feeding a sink a cut-short scan or dead ctx: 0.
Resolution
The cut-short contract (owned here; --file does this today, so both CLI paths share one rule):
- (a) Output never exceeds
--file's (a copy only when a value was masked).--stdinkeeps emitting its stream, as for clean input;RedactFilestops passing a not-examined file through as clean. - (b) Disclosure, always: stderr (incl. the pre-commit one-liner),
stats+ not-examined entry, libraryIncomplete/NotExamined. - (c) Signal: CLI exit 3 under
--fail-on-incomplete, else 0. Library sinks always return an error wrappingErrIncomplete, partial result alongside (io.Readerstyle).
Track A, sinks (lands first; independent of B and C):
Engine.Redact: a done ctx at entry or any non-nilrunErr→ non-nilResult(Incomplete,IncompleteReason) andErrIncomplete.RedactFile: any of the three--fail-on-incompletechannels names the file →scan.ErrIncomplete+RedactFileResult.Coverage *RedactCoverage(nil when complete; {{U38}}).--stdinredaction: both exits viaresolveIncompleteExitCode; pre-commit notice; cause into {{U33}}'s shared stdinFormatterOptionsconstructor.examples/lambda-redactand the README snippets checkerrbeforeRedacted.
Track B, caller ctx:
- Thread ctx through
ScanContent,ScanFile, the parallel processor and the web handler. ScanText/ScanFile: done at entry →nil, ctx.Err(); fired mid-scan → partialResult(Incomplete,NotExamined) and an error wrappingctx.Err(). Internal guards keep(Result{Incomplete}, nil).ScanTextfillsNotExamined, prefixes the reason once; addscan.RedactFileContext.
Track C, partials:
keepMatchesusesIsCoverageCutShort.- Score and annotate partials before the early send.
- Bounded grace drain (≤100 ms) on
Done. - Never retry a guard outcome (by type, before any message test).
scanner.go:431-447uses the predicate; an AST guard rejects hand copies.
Detail, compatibility
Engine.Redact.- Return
fmt.Errorf("%w: %w", ErrIncomplete, cause)for every non-nilrunErr: the cut-short family, the 5-minute ceiling (engine.go:265), and hard errors whose matchesRunValidatorsdiscards. IncompleteReasonis cause text only, never input bytes.Redactedhas the findings made before the cut applied.errors.Is(err, context.Canceled)anderrors.Is(err, context.DeadlineExceeded)keep working for caller ctxs.
- Return
RedactFile.- The three channels are
stats.IncompleteFiles,EmptyExtractionFilesandFailedFiles, the same ones--fail-on-incompletecounts. - Skip the clean-file passthrough for such a file, but keep the partial copy, as
--filedoes. - Wrap the taxonomy cause (
coverage cut short,cannot parse). - Images without body text are not in
EmptyExtractionFiles, so they are unaffected. - Carrier.
RedactCoverage{Incomplete, IncompleteReason, NotExamined}(thescan.Resultnames) behind the pointerCoverage, nil when complete. Slice fields onRedactFileResultitself would repeat the v2.4.5scan.Findingbreak ({{U38}} details 5: incompatible=1). Re-measured with-overlayat 3df48ac: the pointer shape staysComparable()and a map-key/==consumer builds; the slice shape fails withinvalid map key typeandstruct containing []…NotExaminedFile cannot be compared. The pointer compares by identity only on cut-short results, which also carry an error.RedactFileContextreturns the same type.
- The three channels are
- stdin (absorbs {{U39}} Part A).
- Pass
result.Incompleteand its reason intorunStdinRedaction. - The shared constructor ({{U33}}) sets
Stats,NotExaminedandOutputToFilethe way file mode does. - Rewrite :245-249.
- That is the whole stdin change: it matches
--file, with no stdin-only refusal. - If this lands before {{U33}}, the exit code and stderr parts ship alone and the stdin JSON criterion waits for it.
- Pass
- Example and README. Map
ErrIncompleteto a payload-freerequest_id=…: redaction incompleteerror. Fix README.md :211, :220, :226, :230, :247 and :253, which discard errors. - Threading.
ScanContentderives its 5-minute ceiling from the caller's ctx.ScanFile,ProcessFiles*and the worker pool take the caller's ctx as their root. These are internal signatures.- Pass
r.Context()atserver.go:899.
- Caller cancellation. This follows Go convention, so callers that check only
errstop. The guards keep the pkg/scan gives library consumers one bool and a prose string where the CLI has a structured not-examined taxonomy #391 detection contract. There are never fewer findings withIncomplete=false. RedactFileand budgets.RedactFilebecomes acontext.Background()wrapper.- Forwarding
ValidatorBudgetsonTextOptions/FileOptionsis optional. - The doc comments say that extraction cannot be interrupted.
keepMatches. Drop the now-unusederrorsimport.- Scoring partials. Apply contextual confidence and
validation_pathbefore :884-886, so a kept partial scores the same 75 its complete-run twin does, not 55. - Grace period.
- Validators poll every 256 lines (
ctxCheckStride). - The 138–204 ms of post-return CPU is the work being thrown away.
- A validator that misses the grace period is abandoned as now.
- Validators poll every 256 lines (
- No retry for guard outcomes.
resilience.ClassifyErrorreturns non-retryable forexecguard.IsCoverageCutShort(err), before thenet.Errorand substring checks.- Rewording the cause ({{U35}} item 6,
BudgetExceededError) therefore cannot change retry behaviour. - A budget is a bound, not a transient fault.
- AST guard. Over non-test files in
internal/,cmd/andpkg/, it fails onerrors.Is(x, S)outsideinternal/execguardfor S in {DeadlineExceeded,Canceled,ErrMatchBudgetExceeded,ErrContentTooLarge,ErrValidatorPanicked}. The one exception is aswitchthat only picks wording afterIsCoverageCutShorthas matched.
Compatibility.
- Suppression hashes.
- Tracks A and B change no finding's Text, line or type.
- Track C only adds findings, on cut-short runs. Complete runs are byte-identical, per the A/B.
hashVersionNoConfidence(internal/suppressions/suppression.go:246, the current write formula) excludes confidence, so a kept partial matches its twin's rule.- Legacy v1 rules (:243) include confidence. That is why item 9 is mandatory.
- A
--generate-suppressionsfile from a budgeted run gains entries.
- Behaviour.
- Withholding output is not proposed; it would change
--file,--stdinandRedactFiletogether. Library sinks error because a Go caller has no flag but checkserr. - stdin redaction exits 3 instead of 0, but only on a cut-short run under the flag.
- stdin JSON gains
stats(via {{U33}}). RedactFilewrites no passthrough for a not-examined file.- Library callers get errors where they used to get
(cleartext, nil). - An expired ctx passed to
ScanTextnow yields an error.
- Withholding output is not proposed; it would change
- API. All additive (list under {{U38}}):
redact.ErrIncomplete,scan.ErrIncomplete,Incomplete/IncompleteReasononredact.ResultandAuditRecord,scan.RedactCoverage+RedactFileResult.Coverage,scan.RedactFileContext, optionalValidatorBudgets. No exported signature changes. Of the touched structs onlyRedactFileResultis comparable today (the other six are not); the pointer keeps it so. The new error returns are behavioural, invisible to an API snapshot: own CHANGELOG callout. {{U39}}'s Python API must not ship before this. - Precision and performance.
- No pattern, keyword or score changes.
- Tracks A and B add no per-line cost.
- Item 11 removes two extra passes of every check for each fired budget.
Tests
Deterministic triggers only (dead ctx, size cap, unparseable bytes, stub validator, all=1ns), never a wall-clock budget racing real work. Unit tests per sink; a cmd table test over matrix A; stub tests for items 8–11; the AST guard; an unchanged goldencorpus.
Test list
pkg/redact:- Each of a pre-cancelled ctx, a past deadline, an over-cap
CLOUD_RESOURCESrequest and a stub hard error givesErrIncompleteandres.Incomplete. - The reason and
AuditRecordcontain no input bytes. - A complete request is byte-identical to today's output.
- Each of a pre-cancelled ctx, a past deadline, an over-cap
- Lambda example: a pre-cancelled ctx gives an error and an empty
Response, and no input bytes appear in the messages. pkg/scan:- Cancelling at entry gives
nil, context.Canceled. - A stub mid-scan cancel gives a partial result, an error, and
NotExamined[0].Cause == "coverage cut short". ScanTexton a size-capped input gives a non-emptyNotExamined.RedactFileon broken.pdf, on aCLOUD_RESOURCES-only over-cap log, and with a pre-cancelled ctx givesErrIncompleteand no output file.- The over-cap log plus one e-mail gives
ErrIncompleteand the partial copy. - Cut-short cases set
res.Coverage.Incompleteand list the file inres.Coverage.NotExamined; a complete run leavesres.Coverage == nil;RedactFileResultstaysComparable().
- Cancelling at entry gives
cmd: matrix A;--stdinstdout equals the--filecopy wherever a copy exists.- Track C:
- A stub that returns N matches with
DeadlineExceededkeeps all N through the bridge andRunValidators, with confidence and metadata equal to thenil-error case. - A partial sent 10 ms after
Doneis kept. A stub that never returns is abandoned within the bound. - Under
DefaultValidatorRetryStrategy(), both the cut check and its sibling run once. - A kept partial's
generateFindingHashequals its twin's.
- A stub that returns N matches with
- Goldencorpus: byte-identical, plus an unparseable-PDF case in
golden_file_redact_test.go. Time-cut recall depends on timing, so it is pinned by the stubs instead.
Docs & CHANGELOG
pkg/redact/doc.go, pkg/scan doc comments, README library section, README-Stdin.md, COVERAGE_DISCLOSURE.md, configuration.md:249, THREAT_MODEL.md:39, the Lambda README. CHANGELOG [Unreleased]: Security (Track A), Bug Fixes (B, C).
Doc changes
pkg/redact/doc.goand theengine.gocomments: documentErrIncomplete, and tell callers to checkerrbefore usingRedacted.scan.go: document the ctx semantics, and fix theResult.Incompletecomment at :240.redactfile.go:50-55: "safe to share" holds only whenerr == nil; documentCoverage.README.md(:205-260): stop discarding errors.docs/user-guides/README-Stdin.md: cover exit 3 for the gateway. The statement at :257 ("same exit-code semantics as file mode") then becomes true.docs/COVERAGE_DISCLOSURE.md, "The library and the web UI": addpkg/redact,RedactFileand the gateway, and correct thepkg/scanNotExaminedrow.docs/configuration.md:249and the--validator-budgethelp: a budget is stopped once, its partial results are kept, and the scan is reported incomplete.THREAT_MODEL.md:39: the exit-3 claim becomes true for stdin.- CHANGELOG:
- The Security entry names every surface.
- The Bug Fixes entry notes that budgeted runs gain findings and suppression entries, and that complete runs are unchanged.
Acceptance criteria
-
Engine.Redact(pre-cancelled ctx, past deadline, >5 MiBCLOUD_RESOURCESrequest):errors.Is(err, redact.ErrIncomplete),Result.IncompleteandAuditRecord().Incompletetrue, no input bytes in reason or error;examples/lambda-redactreturns an error and noredactedfield. -
RedactFile(unparseable.pdf; >5 MiBCLOUD_RESOURCES-only log):errors.Is(err, scan.ErrIncomplete),res.Coverage.Incompletetrue, no file at the output path; complete run:res.Coverage == nil. - one.txt
all=1ns(also--pre-commit-mode) and infra.log:--stdinand--fileredaction both exit 0 without--fail-on-incomplete, 3 with it, disclose on stderr; stdin JSON hasstats.files_not_examined == 1(with {{U33}}). -
ScanText/ScanFile: pre-cancelled ctx →nil, context.Canceled; stub mid-scan cancel →Incomplete,NotExamined[0].Cause == "coverage cut short", error wrappingctx.Err(); size-cappedScanText→ non-emptyNotExamined, reason prefix once. -
scan.RedactFileContextexists,RedactFilekeeps its signature,RedactFileResultstays comparable ({{U38}} snapshot),/scanpassesr.Context(). - records.txt,
--checks SSN,EMAIL --validator-budget SSN=<d>that fires: non-zero SSN findings, each identical (line, text, type, confidence) in the unbudgeted run;--debugshows 1email_validatorvalidate_contentrecord (today 3); exit 3 under--fail-on-incomplete. - A stub returning N matches with
DeadlineExceededyields N findings, confidence and metadata equal to thenil-error case. - The AST guard passes, and fails when
errors.Is(err, execguard.ErrMatchBudgetExceeded)is added outsideexecguard. - Goldencorpus byte-identical,
make pr-checklistgreen, docs and CHANGELOG entries present.
Related
- cloudresources silently skips content over 5MB: an ARN in a 6MB file is reported as a clean scan at exit 0 #414/fix(cloudresources): disclose the oversize refusal instead of returning an empty result #424 (oversize refusal disclosed, not at the sinks), pkg/scan gives library consumers one bool and a prose string where the CLI has a structured not-examined taxonomy #391/pkg/scan still exposes no not-examined taxonomy at v2.3.4 — #391 was closed as completed but the API is unchanged #510/pkg/scan and the web UI report every coverage loss as "no document text extracted", including losses where the text WAS read #432 (
pkg/scannot-examined taxonomy), A corrupt or truncated .pdf is counted as scanned and reported clean: pdf_metadata succeeds so the text extractor's error reaches no disclosure channel (exit 0 even with --fail-on-incomplete) #294 (unparseable.pdf), redaction refusals exit 0, so a cleartext leak has no machine-readable signal #441 (machine-readable redaction refusals), pkg/scan.RedactText returns the input unchanged while reporting the value redacted, for any consolidated finding #631 (sibling sink defect), gate integrity: three ways the tool reports success on work it did not do (consolidates exit resolver, coverage ledger, --exclude) #667 (gate side), feat(v2-refactor): per-validator time + match budgets (Move C) + secrets/cloudresources ctx #110 (keep-partials intent), feat(v2): surface degraded validator coverage on the file path (Phase 4) #104, feat(api): add pkg/scan — public detection + redaction API #143. - {{U33}} owns the stdin
statsblock and shared constructor (item 3). {{U35}}'s found-alongside line = items 11/8; {{U35}} item 6 (BudgetExceededError) is non-retryable by type under item 11. {{U39}} Part A is absorbed here. {{U38}} gates the newpkg/API. validators: a metadata-capable file scanned without METADATA loses every finding on any cut-short, silently at exit 0 #771: same "cut short = found nothing" root withoutMETADATA.
Audit item U07 · tier P0 · effort L · rank 6/51. Measurements pinned to 3df48acd (v2.5.3); a {{U##}} token is a sibling item not yet filed.
Activity
fix(redaction): fail a cut-short redaction on every sink, keep partials
chore(changelog): place the #770 bullet under Bug Fixes after the rebase
chore(changelog): collect release notes as one fragment per PR under …