@jinn-network/core 0.1.0-canary.3afae198
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canonical-json.d.ts +1 -0
- package/dist/canonical-json.js +67 -0
- package/dist/captured-task.d.ts +140 -0
- package/dist/captured-task.js +122 -0
- package/dist/contribution-store.d.ts +97 -0
- package/dist/contribution-store.js +770 -0
- package/dist/corpus-read/acquire.d.ts +39 -0
- package/dist/corpus-read/acquire.js +212 -0
- package/dist/corpus-read/cache.d.ts +14 -0
- package/dist/corpus-read/cache.js +17 -0
- package/dist/corpus-read/capture-meta.d.ts +16 -0
- package/dist/corpus-read/capture-meta.js +18 -0
- package/dist/corpus-read/create-corpus.d.ts +2 -0
- package/dist/corpus-read/create-corpus.js +97 -0
- package/dist/corpus-read/fetch-artifact.d.ts +21 -0
- package/dist/corpus-read/fetch-artifact.js +32 -0
- package/dist/corpus-read/fetch.d.ts +12 -0
- package/dist/corpus-read/fetch.js +24 -0
- package/dist/corpus-read/http-discovery.d.ts +9 -0
- package/dist/corpus-read/http-discovery.js +128 -0
- package/dist/corpus-read/index.d.ts +10 -0
- package/dist/corpus-read/index.js +10 -0
- package/dist/corpus-read/ipfs.d.ts +14 -0
- package/dist/corpus-read/ipfs.js +81 -0
- package/dist/corpus-read/route-resolver.d.ts +16 -0
- package/dist/corpus-read/route-resolver.js +19 -0
- package/dist/corpus-read/types.d.ts +177 -0
- package/dist/corpus-read/types.js +42 -0
- package/dist/envelope.d.ts +134 -0
- package/dist/envelope.js +162 -0
- package/dist/evidence-adapter.d.ts +26 -0
- package/dist/evidence-adapter.js +321 -0
- package/dist/evidence-filesystem.d.ts +40 -0
- package/dist/evidence-filesystem.js +267 -0
- package/dist/evidence-index.d.ts +117 -0
- package/dist/evidence-index.js +1083 -0
- package/dist/evidence-store-lock.d.ts +2 -0
- package/dist/evidence-store-lock.js +208 -0
- package/dist/execution-envelope.d.ts +3142 -0
- package/dist/execution-envelope.js +203 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/manifest.d.ts +75 -0
- package/dist/manifest.js +184 -0
- package/dist/paired.d.ts +68 -0
- package/dist/paired.js +92 -0
- package/dist/scrub/apply-dispositions.d.ts +74 -0
- package/dist/scrub/apply-dispositions.js +309 -0
- package/dist/scrub/build.d.ts +75 -0
- package/dist/scrub/build.js +143 -0
- package/dist/scrub/checksummed-instruments-detector.d.ts +13 -0
- package/dist/scrub/checksummed-instruments-detector.js +119 -0
- package/dist/scrub/data/bip39-english.d.ts +2 -0
- package/dist/scrub/data/bip39-english.js +2051 -0
- package/dist/scrub/data/gitleaks-rules.d.ts +56 -0
- package/dist/scrub/data/gitleaks-rules.js +67 -0
- package/dist/scrub/emit-scrub.d.ts +52 -0
- package/dist/scrub/emit-scrub.js +122 -0
- package/dist/scrub/eval/findings-from-scrub.d.ts +18 -0
- package/dist/scrub/eval/findings-from-scrub.js +99 -0
- package/dist/scrub/eval/fixtures.d.ts +14 -0
- package/dist/scrub/eval/fixtures.js +159 -0
- package/dist/scrub/eval/index.d.ts +6 -0
- package/dist/scrub/eval/index.js +6 -0
- package/dist/scrub/eval/local-corpus.d.ts +15 -0
- package/dist/scrub/eval/local-corpus.js +29 -0
- package/dist/scrub/eval/metrics.d.ts +19 -0
- package/dist/scrub/eval/metrics.js +77 -0
- package/dist/scrub/eval/run-bench.d.ts +8 -0
- package/dist/scrub/eval/run-bench.js +125 -0
- package/dist/scrub/eval/types.d.ts +69 -0
- package/dist/scrub/eval/types.js +7 -0
- package/dist/scrub/finding.d.ts +38 -0
- package/dist/scrub/finding.js +7 -0
- package/dist/scrub/git-identity-detector.d.ts +16 -0
- package/dist/scrub/git-identity-detector.js +116 -0
- package/dist/scrub/gitleaks-detector.d.ts +13 -0
- package/dist/scrub/gitleaks-detector.js +45 -0
- package/dist/scrub/gliner-detector.d.ts +65 -0
- package/dist/scrub/gliner-detector.js +129 -0
- package/dist/scrub/index.d.ts +26 -0
- package/dist/scrub/index.js +26 -0
- package/dist/scrub/ip-address-detector.d.ts +12 -0
- package/dist/scrub/ip-address-detector.js +81 -0
- package/dist/scrub/key-policy.d.ts +30 -0
- package/dist/scrub/key-policy.js +72 -0
- package/dist/scrub/known-identity-detector.d.ts +66 -0
- package/dist/scrub/known-identity-detector.js +284 -0
- package/dist/scrub/layer2.d.ts +18 -0
- package/dist/scrub/layer2.js +28 -0
- package/dist/scrub/ml-pii-stage.d.ts +42 -0
- package/dist/scrub/ml-pii-stage.js +119 -0
- package/dist/scrub/pii-build.d.ts +47 -0
- package/dist/scrub/pii-build.js +67 -0
- package/dist/scrub/pipeline.d.ts +77 -0
- package/dist/scrub/pipeline.js +225 -0
- package/dist/scrub/plain-patterns-stage.d.ts +33 -0
- package/dist/scrub/plain-patterns-stage.js +88 -0
- package/dist/scrub/policy.d.ts +28 -0
- package/dist/scrub/policy.js +156 -0
- package/dist/scrub/provenance.d.ts +53 -0
- package/dist/scrub/provenance.js +83 -0
- package/dist/scrub/reject-classes-detector.d.ts +11 -0
- package/dist/scrub/reject-classes-detector.js +195 -0
- package/dist/scrub/reject-publish-error.d.ts +30 -0
- package/dist/scrub/reject-publish-error.js +47 -0
- package/dist/scrub/review-queue.d.ts +90 -0
- package/dist/scrub/review-queue.js +259 -0
- package/dist/scrub/secretlint-stage.d.ts +21 -0
- package/dist/scrub/secretlint-stage.js +264 -0
- package/dist/scrub/transformers-detector.d.ts +41 -0
- package/dist/scrub/transformers-detector.js +77 -0
- package/dist/scrub/types.d.ts +36 -0
- package/dist/scrub/types.js +1 -0
- package/dist/scrub/url-credentials-detector.d.ts +9 -0
- package/dist/scrub/url-credentials-detector.js +66 -0
- package/dist/session-provenance.d.ts +75 -0
- package/dist/session-provenance.js +20 -0
- package/dist/skill-artifact.d.ts +442 -0
- package/dist/skill-artifact.js +136 -0
- package/dist/trajectory/hash-chain.d.ts +18 -0
- package/dist/trajectory/hash-chain.js +47 -0
- package/dist/trajectory/index.d.ts +4 -0
- package/dist/trajectory/index.js +4 -0
- package/dist/trajectory/schema.d.ts +681 -0
- package/dist/trajectory/schema.js +119 -0
- package/dist/trajectory/transcript-parsers/aider-history.d.ts +42 -0
- package/dist/trajectory/transcript-parsers/aider-history.js +181 -0
- package/dist/trajectory/transcript-parsers/claude-code-jsonl.d.ts +37 -0
- package/dist/trajectory/transcript-parsers/claude-code-jsonl.js +142 -0
- package/dist/trajectory/transcript-parsers/codex-session.d.ts +42 -0
- package/dist/trajectory/transcript-parsers/codex-session.js +235 -0
- package/dist/trajectory/transcript-parsers/continue-devdata.d.ts +41 -0
- package/dist/trajectory/transcript-parsers/continue-devdata.js +190 -0
- package/dist/trajectory/transcript-parsers/cursor-sqlite.d.ts +38 -0
- package/dist/trajectory/transcript-parsers/cursor-sqlite.js +118 -0
- package/dist/trajectory/transcript-parsers/gemini-session.d.ts +35 -0
- package/dist/trajectory/transcript-parsers/gemini-session.js +137 -0
- package/dist/trajectory/transcript-parsers/index.d.ts +7 -0
- package/dist/trajectory/transcript-parsers/index.js +7 -0
- package/dist/trajectory/transcript-parsers/types.d.ts +101 -0
- package/dist/trajectory/transcript-parsers/types.js +32 -0
- package/dist/trajectory/transcript-to-spans/attrs.d.ts +4 -0
- package/dist/trajectory/transcript-to-spans/attrs.js +30 -0
- package/dist/trajectory/transcript-to-spans/claude-code-stream-json.d.ts +8 -0
- package/dist/trajectory/transcript-to-spans/claude-code-stream-json.js +162 -0
- package/dist/trajectory/transcript-to-spans/codex-exec-json.d.ts +8 -0
- package/dist/trajectory/transcript-to-spans/codex-exec-json.js +253 -0
- package/dist/trajectory/transcript-to-spans/index.d.ts +4 -0
- package/dist/trajectory/transcript-to-spans/index.js +4 -0
- package/dist/trajectory/transcript-to-spans/types.d.ts +18 -0
- package/dist/trajectory/transcript-to-spans/types.js +1 -0
- package/dist/window.d.ts +12 -0
- package/dist/window.js +5 -0
- package/package.json +72 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { assertNoRejectPublish, shouldRejectPublish } from './apply-dispositions.js';
|
|
2
|
+
import type { Detector } from './finding.js';
|
|
3
|
+
import { type PolicyTable } from './policy.js';
|
|
4
|
+
import { type PolicyHashInput } from './provenance.js';
|
|
5
|
+
import { type ReviewQueueStore } from './review-queue.js';
|
|
6
|
+
import type { Attributes, ScrubResult, ScrubStage } from './types.js';
|
|
7
|
+
export { shouldRejectPublish, assertNoRejectPublish };
|
|
8
|
+
export interface ScrubPipelineOptions {
|
|
9
|
+
policy?: PolicyTable;
|
|
10
|
+
/** Check-mode: any non-pass disposition rejects (design §6.5). */
|
|
11
|
+
checkMode?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Review-queue store for flag resolutions (#1973). Defaults to the
|
|
14
|
+
* operator-local `~/.jinn-client/scrub-review/queue.jsonl` store in
|
|
15
|
+
* redact-mode. Pass an in-memory store in tests.
|
|
16
|
+
*/
|
|
17
|
+
reviewStore?: ReviewQueueStore;
|
|
18
|
+
/**
|
|
19
|
+
* When true (default in redact-mode), unresolved flags enqueue + throw
|
|
20
|
+
* {@link UnresolvedFlagError}. Check-mode leaves this off.
|
|
21
|
+
*/
|
|
22
|
+
failClosedOnUnresolvedFlags?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Inputs for the published policy hash (#1974). When omitted, emit helpers
|
|
25
|
+
* still bump `schemaVersion` but leave `policyHash` unset.
|
|
26
|
+
*/
|
|
27
|
+
provenance?: Omit<PolicyHashInput, 'policy' | 'detectors'>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Runs one detector inventory, then one disposition pass (#1969).
|
|
31
|
+
*
|
|
32
|
+
* Nested values (#1378): after the flat detect→dispose pass, surviving
|
|
33
|
+
* object/array values are walked and every string leaf is scrubbed through
|
|
34
|
+
* the same detect→dispose path, keyed by the TOP-LEVEL attribute key.
|
|
35
|
+
*
|
|
36
|
+
* Constructor also accepts legacy {@link ScrubStage} arrays (stage.scrub chain)
|
|
37
|
+
* so existing unit tests and injectors keep working through the migration.
|
|
38
|
+
*/
|
|
39
|
+
export declare class ScrubPipeline {
|
|
40
|
+
private readonly policy;
|
|
41
|
+
private readonly checkMode;
|
|
42
|
+
private readonly detectors;
|
|
43
|
+
private readonly stages;
|
|
44
|
+
private readonly reviewStore;
|
|
45
|
+
private readonly failClosedOnUnresolvedFlags;
|
|
46
|
+
private readonly provenanceExtras;
|
|
47
|
+
constructor(detectorsOrStages: Array<Detector | ScrubStage>, opts?: ScrubPipelineOptions);
|
|
48
|
+
get components(): Array<{
|
|
49
|
+
name: string;
|
|
50
|
+
version: string;
|
|
51
|
+
}>;
|
|
52
|
+
/** Disposition policy table used by this pipeline (#1974). */
|
|
53
|
+
get policyTable(): PolicyTable;
|
|
54
|
+
/**
|
|
55
|
+
* Additive provenance fields for a redaction manifest (#1974). Always
|
|
56
|
+
* includes `schemaVersion`; `policyHash` is present when provenance
|
|
57
|
+
* extras were supplied at construction.
|
|
58
|
+
*/
|
|
59
|
+
manifestProvenance(perClassCounts?: Record<string, number>): {
|
|
60
|
+
schemaVersion: number;
|
|
61
|
+
policyHash?: string;
|
|
62
|
+
perClassCounts?: Record<string, number>;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Reproducible policy hash, or undefined when provenance inputs were not
|
|
66
|
+
* configured (legacy stage-only pipelines).
|
|
67
|
+
*/
|
|
68
|
+
policyHash(): string | undefined;
|
|
69
|
+
run(attributes: Attributes): Promise<ScrubResult>;
|
|
70
|
+
private assertAndEnqueueUnresolvedFlags;
|
|
71
|
+
/** Legacy ScrubStage chain (tests / residual injectors). */
|
|
72
|
+
private runLegacyStages;
|
|
73
|
+
private runStagesFlat;
|
|
74
|
+
private scrubNestedLegacy;
|
|
75
|
+
private runDetectDispose;
|
|
76
|
+
private scrubNested;
|
|
77
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { applyDispositions, assertNoRejectPublish, shouldRejectPublish } from './apply-dispositions.js';
|
|
2
|
+
import { DEFAULT_POLICY } from './policy.js';
|
|
3
|
+
import { REDACTION_MANIFEST_SCHEMA_VERSION, computePolicyHash, mergePerClassCounts, } from './provenance.js';
|
|
4
|
+
import { UnresolvedFlagError, createReviewQueueStore, } from './review-queue.js';
|
|
5
|
+
export { shouldRejectPublish, assertNoRejectPublish };
|
|
6
|
+
function isDetector(item) {
|
|
7
|
+
return typeof item.detect === 'function';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Runs one detector inventory, then one disposition pass (#1969).
|
|
11
|
+
*
|
|
12
|
+
* Nested values (#1378): after the flat detect→dispose pass, surviving
|
|
13
|
+
* object/array values are walked and every string leaf is scrubbed through
|
|
14
|
+
* the same detect→dispose path, keyed by the TOP-LEVEL attribute key.
|
|
15
|
+
*
|
|
16
|
+
* Constructor also accepts legacy {@link ScrubStage} arrays (stage.scrub chain)
|
|
17
|
+
* so existing unit tests and injectors keep working through the migration.
|
|
18
|
+
*/
|
|
19
|
+
export class ScrubPipeline {
|
|
20
|
+
policy;
|
|
21
|
+
checkMode;
|
|
22
|
+
detectors;
|
|
23
|
+
stages;
|
|
24
|
+
reviewStore;
|
|
25
|
+
failClosedOnUnresolvedFlags;
|
|
26
|
+
provenanceExtras;
|
|
27
|
+
constructor(detectorsOrStages, opts = {}) {
|
|
28
|
+
this.policy = opts.policy ?? DEFAULT_POLICY;
|
|
29
|
+
this.checkMode = opts.checkMode ?? false;
|
|
30
|
+
this.reviewStore = opts.reviewStore;
|
|
31
|
+
this.failClosedOnUnresolvedFlags =
|
|
32
|
+
opts.failClosedOnUnresolvedFlags ?? !this.checkMode;
|
|
33
|
+
this.provenanceExtras = opts.provenance;
|
|
34
|
+
if (detectorsOrStages.length > 0 && detectorsOrStages.every(isDetector)) {
|
|
35
|
+
this.detectors = detectorsOrStages;
|
|
36
|
+
this.stages = null;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
this.detectors = null;
|
|
40
|
+
this.stages = detectorsOrStages;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
get components() {
|
|
44
|
+
const items = this.detectors ?? this.stages ?? [];
|
|
45
|
+
return items.map((d) => ({ name: d.name, version: d.version }));
|
|
46
|
+
}
|
|
47
|
+
/** Disposition policy table used by this pipeline (#1974). */
|
|
48
|
+
get policyTable() {
|
|
49
|
+
return this.policy;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Additive provenance fields for a redaction manifest (#1974). Always
|
|
53
|
+
* includes `schemaVersion`; `policyHash` is present when provenance
|
|
54
|
+
* extras were supplied at construction.
|
|
55
|
+
*/
|
|
56
|
+
manifestProvenance(perClassCounts) {
|
|
57
|
+
const out = { schemaVersion: REDACTION_MANIFEST_SCHEMA_VERSION };
|
|
58
|
+
const hash = this.policyHash();
|
|
59
|
+
if (hash)
|
|
60
|
+
out.policyHash = hash;
|
|
61
|
+
if (perClassCounts && Object.keys(perClassCounts).length > 0) {
|
|
62
|
+
out.perClassCounts = perClassCounts;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Reproducible policy hash, or undefined when provenance inputs were not
|
|
68
|
+
* configured (legacy stage-only pipelines).
|
|
69
|
+
*/
|
|
70
|
+
policyHash() {
|
|
71
|
+
if (!this.provenanceExtras)
|
|
72
|
+
return undefined;
|
|
73
|
+
return computePolicyHash({
|
|
74
|
+
policy: this.policy,
|
|
75
|
+
detectors: this.components,
|
|
76
|
+
modelId: this.provenanceExtras.modelId,
|
|
77
|
+
labels: this.provenanceExtras.labels,
|
|
78
|
+
allowlistDigest: this.provenanceExtras.allowlistDigest,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async run(attributes) {
|
|
82
|
+
if (this.stages) {
|
|
83
|
+
return this.runLegacyStages(attributes);
|
|
84
|
+
}
|
|
85
|
+
const flat = await this.runDetectDispose(attributes);
|
|
86
|
+
const out = {};
|
|
87
|
+
const redactions = [...flat.redactions];
|
|
88
|
+
const findings = [...flat.findings];
|
|
89
|
+
const unresolvedFlags = [...flat.unresolvedFlags];
|
|
90
|
+
let perClassCounts = { ...flat.perClassCounts };
|
|
91
|
+
let rejected = flat.rejected;
|
|
92
|
+
for (const [key, value] of Object.entries(flat.attributes)) {
|
|
93
|
+
out[key] =
|
|
94
|
+
typeof value === 'string'
|
|
95
|
+
? value
|
|
96
|
+
: await this.scrubNested(key, value, redactions, findings, unresolvedFlags, (nestedCounts) => {
|
|
97
|
+
perClassCounts =
|
|
98
|
+
mergePerClassCounts(perClassCounts, nestedCounts) ?? perClassCounts;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Check-mode: trust the disposition `rejected` bit. Do not re-derive from
|
|
102
|
+
// raw redactions.length — allowlist-pass records are auditable passes.
|
|
103
|
+
if (this.checkMode && flat.rejected)
|
|
104
|
+
rejected = true;
|
|
105
|
+
const result = {
|
|
106
|
+
attributes: out,
|
|
107
|
+
redactions,
|
|
108
|
+
findings,
|
|
109
|
+
rejected,
|
|
110
|
+
unresolvedFlags,
|
|
111
|
+
perClassCounts,
|
|
112
|
+
};
|
|
113
|
+
// Publish altitude: reject-publish classes abort loudly with a class name.
|
|
114
|
+
// Check-mode distill maps redactions → rejection reasons without throwing.
|
|
115
|
+
if (!this.checkMode)
|
|
116
|
+
assertNoRejectPublish(result, this.policy);
|
|
117
|
+
if (!this.checkMode && this.failClosedOnUnresolvedFlags) {
|
|
118
|
+
this.assertAndEnqueueUnresolvedFlags(result.unresolvedFlags ?? [], attributes);
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
assertAndEnqueueUnresolvedFlags(unresolved, attributes) {
|
|
123
|
+
if (unresolved.length === 0)
|
|
124
|
+
return;
|
|
125
|
+
// Lazy default: operator-local queue so unattended publishes leave a
|
|
126
|
+
// reviewable trail. Tests inject `reviewStore` (memory) or disable
|
|
127
|
+
// fail-closed via `failClosedOnUnresolvedFlags: false`.
|
|
128
|
+
const store = this.reviewStore ?? createReviewQueueStore();
|
|
129
|
+
const enqueued = store.enqueue(unresolved.map((finding) => {
|
|
130
|
+
const value = attributes[finding.span.key];
|
|
131
|
+
const text = typeof value === 'string' ? value : '';
|
|
132
|
+
const snippet = text.slice(Math.max(0, finding.span.start - 40), Math.min(text.length, finding.span.end + 40));
|
|
133
|
+
return {
|
|
134
|
+
finding,
|
|
135
|
+
context: { attributeKey: finding.span.key, snippet },
|
|
136
|
+
};
|
|
137
|
+
}));
|
|
138
|
+
throw new UnresolvedFlagError(unresolved, enqueued.map((e) => e.id));
|
|
139
|
+
}
|
|
140
|
+
/** Legacy ScrubStage chain (tests / residual injectors). */
|
|
141
|
+
async runLegacyStages(attributes) {
|
|
142
|
+
const flat = await this.runStagesFlat(attributes);
|
|
143
|
+
const out = {};
|
|
144
|
+
const redactions = [...flat.redactions];
|
|
145
|
+
for (const [key, value] of Object.entries(flat.attributes)) {
|
|
146
|
+
out[key] = typeof value === 'string' ? value : await this.scrubNestedLegacy(key, value, redactions);
|
|
147
|
+
}
|
|
148
|
+
return { attributes: out, redactions };
|
|
149
|
+
}
|
|
150
|
+
async runStagesFlat(attributes) {
|
|
151
|
+
let current = attributes;
|
|
152
|
+
const redactions = [];
|
|
153
|
+
for (const stage of this.stages) {
|
|
154
|
+
const result = await stage.scrub(current);
|
|
155
|
+
current = result.attributes;
|
|
156
|
+
redactions.push(...result.redactions);
|
|
157
|
+
}
|
|
158
|
+
return { attributes: current, redactions };
|
|
159
|
+
}
|
|
160
|
+
async scrubNestedLegacy(key, value, redactions) {
|
|
161
|
+
if (typeof value === 'string') {
|
|
162
|
+
const result = await this.runStagesFlat({ [key]: value });
|
|
163
|
+
redactions.push(...result.redactions);
|
|
164
|
+
const scrubbed = result.attributes[key];
|
|
165
|
+
return typeof scrubbed === 'string' ? scrubbed : value;
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(value)) {
|
|
168
|
+
const items = [];
|
|
169
|
+
for (const item of value)
|
|
170
|
+
items.push(await this.scrubNestedLegacy(key, item, redactions));
|
|
171
|
+
return items;
|
|
172
|
+
}
|
|
173
|
+
if (value !== null && typeof value === 'object') {
|
|
174
|
+
const proto = Object.getPrototypeOf(value);
|
|
175
|
+
if (proto !== Object.prototype && proto !== null)
|
|
176
|
+
return value;
|
|
177
|
+
const entries = {};
|
|
178
|
+
for (const [k, v] of Object.entries(value)) {
|
|
179
|
+
entries[k] = await this.scrubNestedLegacy(key, v, redactions);
|
|
180
|
+
}
|
|
181
|
+
return entries;
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
async runDetectDispose(attributes) {
|
|
186
|
+
const findings = [];
|
|
187
|
+
for (const detector of this.detectors) {
|
|
188
|
+
findings.push(...(await detector.detect(attributes)));
|
|
189
|
+
}
|
|
190
|
+
return applyDispositions(attributes, findings, {
|
|
191
|
+
policy: this.policy,
|
|
192
|
+
checkMode: this.checkMode,
|
|
193
|
+
reviewStore: this.reviewStore,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async scrubNested(key, value, redactions, findings, unresolvedFlags, onCounts) {
|
|
197
|
+
if (typeof value === 'string') {
|
|
198
|
+
const result = await this.runDetectDispose({ [key]: value });
|
|
199
|
+
redactions.push(...result.redactions);
|
|
200
|
+
findings.push(...result.findings);
|
|
201
|
+
unresolvedFlags.push(...result.unresolvedFlags);
|
|
202
|
+
onCounts(result.perClassCounts);
|
|
203
|
+
const scrubbed = result.attributes[key];
|
|
204
|
+
return typeof scrubbed === 'string' ? scrubbed : value;
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(value)) {
|
|
207
|
+
const items = [];
|
|
208
|
+
for (const item of value) {
|
|
209
|
+
items.push(await this.scrubNested(key, item, redactions, findings, unresolvedFlags, onCounts));
|
|
210
|
+
}
|
|
211
|
+
return items;
|
|
212
|
+
}
|
|
213
|
+
if (value !== null && typeof value === 'object') {
|
|
214
|
+
const proto = Object.getPrototypeOf(value);
|
|
215
|
+
if (proto !== Object.prototype && proto !== null)
|
|
216
|
+
return value;
|
|
217
|
+
const entries = {};
|
|
218
|
+
for (const [k, v] of Object.entries(value)) {
|
|
219
|
+
entries[k] = await this.scrubNested(key, v, redactions, findings, unresolvedFlags, onCounts);
|
|
220
|
+
}
|
|
221
|
+
return entries;
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plain-patterns detector — deterministic regex findings for shapes the
|
|
3
|
+
* probabilistic detectors miss (#1330 / #1415 / #1959 / #1969):
|
|
4
|
+
*
|
|
5
|
+
* - email (B1)
|
|
6
|
+
* - POSIX home-directory paths (D1)
|
|
7
|
+
* - AWS/GCP credential-ID prefixes (A1)
|
|
8
|
+
* - Ethereum-style wallet addresses `0x`+40 hex (C1)
|
|
9
|
+
*
|
|
10
|
+
* Emits findings only — disposition owns stubs. Wallet + credential-ID shapes
|
|
11
|
+
* are always registered in the shared inventory (#1969); policy decides.
|
|
12
|
+
*/
|
|
13
|
+
import { type KeyPolicy } from './key-policy.js';
|
|
14
|
+
import type { Detector } from './finding.js';
|
|
15
|
+
import type { ScrubStage } from './types.js';
|
|
16
|
+
export interface PlainPatternsOptions {
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated (#1969) Credential IDs are always in the shared inventory.
|
|
19
|
+
* Kept so call sites that pass `{ credentialIds: true }` still type-check.
|
|
20
|
+
*/
|
|
21
|
+
credentialIds?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* @deprecated (#1969) Wallet addresses are always in the shared inventory.
|
|
24
|
+
* Kept so call sites that pass `{ walletAddresses: true }` still type-check.
|
|
25
|
+
*/
|
|
26
|
+
walletAddresses?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare function plainPatternsDetector(policy: KeyPolicy, _opts?: PlainPatternsOptions): Detector;
|
|
29
|
+
/**
|
|
30
|
+
* Legacy ScrubStage wrapper: detect + apply default dispositions. New callers
|
|
31
|
+
* should use {@link plainPatternsDetector} via ScrubPipeline.
|
|
32
|
+
*/
|
|
33
|
+
export declare function plainPatternsStage(policy: KeyPolicy, opts?: PlainPatternsOptions): ScrubStage;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plain-patterns detector — deterministic regex findings for shapes the
|
|
3
|
+
* probabilistic detectors miss (#1330 / #1415 / #1959 / #1969):
|
|
4
|
+
*
|
|
5
|
+
* - email (B1)
|
|
6
|
+
* - POSIX home-directory paths (D1)
|
|
7
|
+
* - AWS/GCP credential-ID prefixes (A1)
|
|
8
|
+
* - Ethereum-style wallet addresses `0x`+40 hex (C1)
|
|
9
|
+
*
|
|
10
|
+
* Emits findings only — disposition owns stubs. Wallet + credential-ID shapes
|
|
11
|
+
* are always registered in the shared inventory (#1969); policy decides.
|
|
12
|
+
*/
|
|
13
|
+
import { applyDispositions } from './apply-dispositions.js';
|
|
14
|
+
import { classifyKey } from './key-policy.js';
|
|
15
|
+
const VERSION = '0.2.0';
|
|
16
|
+
/** Plain email shape. */
|
|
17
|
+
const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])+)+/g;
|
|
18
|
+
/** POSIX home-dir path segment carrying a username. */
|
|
19
|
+
const HOME_PATH_PATTERN = /\/(?:Users|home)\/[^/\s"'`]+/g;
|
|
20
|
+
/**
|
|
21
|
+
* Bare AWS access-key ID — the fixed four-char prefixes + 16 key chars that
|
|
22
|
+
* secretlint's aws rule only scans for under `enableIDScanRule: true`.
|
|
23
|
+
*/
|
|
24
|
+
const AWS_KEY_ID_PATTERN = /\b(?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}\b/g;
|
|
25
|
+
/**
|
|
26
|
+
* GCP API key: fixed `AIza` prefix + 35 key chars. No trailing `\b` — the key
|
|
27
|
+
* charset includes `-`, which breaks word-boundary semantics at the end.
|
|
28
|
+
*/
|
|
29
|
+
const GCP_API_KEY_PATTERN = /\bAIza[0-9A-Za-z_-]{35}/g;
|
|
30
|
+
/**
|
|
31
|
+
* Ethereum-style wallet address: `0x` + exactly 40 hex chars (#1959 / C1).
|
|
32
|
+
* Bare 40-hex git SHAs and `0x`+64 tx hashes must survive.
|
|
33
|
+
*/
|
|
34
|
+
const ETH_ADDRESS_PATTERN = /\b0x[a-fA-F0-9]{40}\b/g;
|
|
35
|
+
const RULES = [
|
|
36
|
+
{ pattern: EMAIL_PATTERN, class: 'B1', evidence: 'email' },
|
|
37
|
+
{ pattern: HOME_PATH_PATTERN, class: 'D1', evidence: 'home-path' },
|
|
38
|
+
{ pattern: AWS_KEY_ID_PATTERN, class: 'A1', evidence: 'aws-access-key-id' },
|
|
39
|
+
{ pattern: GCP_API_KEY_PATTERN, class: 'A1', evidence: 'gcp-api-key' },
|
|
40
|
+
{ pattern: ETH_ADDRESS_PATTERN, class: 'C1', evidence: 'eth-address' },
|
|
41
|
+
];
|
|
42
|
+
function collectMatches(text, key, rule, detector) {
|
|
43
|
+
const findings = [];
|
|
44
|
+
const re = new RegExp(rule.pattern.source, rule.pattern.flags.includes('g') ? rule.pattern.flags : `${rule.pattern.flags}g`);
|
|
45
|
+
let match;
|
|
46
|
+
while ((match = re.exec(text)) !== null) {
|
|
47
|
+
findings.push({
|
|
48
|
+
class: rule.class,
|
|
49
|
+
span: { key, start: match.index, end: match.index + match[0].length },
|
|
50
|
+
confidence: 'VERY_HIGH',
|
|
51
|
+
evidence: [rule.evidence],
|
|
52
|
+
detector,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return findings;
|
|
56
|
+
}
|
|
57
|
+
export function plainPatternsDetector(policy, _opts = {}) {
|
|
58
|
+
const meta = { name: 'plain-patterns', version: VERSION };
|
|
59
|
+
return {
|
|
60
|
+
...meta,
|
|
61
|
+
detect(attributes) {
|
|
62
|
+
const findings = [];
|
|
63
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
64
|
+
if (typeof value !== 'string' || classifyKey(key, policy) !== 'content')
|
|
65
|
+
continue;
|
|
66
|
+
for (const rule of RULES) {
|
|
67
|
+
findings.push(...collectMatches(value, key, rule, meta));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return findings;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Legacy ScrubStage wrapper: detect + apply default dispositions. New callers
|
|
76
|
+
* should use {@link plainPatternsDetector} via ScrubPipeline.
|
|
77
|
+
*/
|
|
78
|
+
export function plainPatternsStage(policy, opts = {}) {
|
|
79
|
+
const detector = plainPatternsDetector(policy, opts);
|
|
80
|
+
return {
|
|
81
|
+
name: detector.name,
|
|
82
|
+
version: detector.version,
|
|
83
|
+
scrub(attributes) {
|
|
84
|
+
const findings = detector.detect(attributes);
|
|
85
|
+
return applyDispositions(attributes, findings);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned disposition policy (#1969 / design §6.5).
|
|
3
|
+
*
|
|
4
|
+
* disposition(class, band) -> redact | reject-publish | flag | pass
|
|
5
|
+
*
|
|
6
|
+
* Defaults follow §3.2's rightmost columns. C2 stays `pass` until the flag
|
|
7
|
+
* review surface exists (do not redact 0x+64 tx hashes).
|
|
8
|
+
*/
|
|
9
|
+
import type { Band, Finding, ScrubClass } from './finding.js';
|
|
10
|
+
export type Disposition = 'redact' | 'reject-publish' | 'flag' | 'pass';
|
|
11
|
+
export declare const POLICY_VERSION = "1.0.0";
|
|
12
|
+
/** Per-band dispositions for one class. Missing bands fall back to `pass`. */
|
|
13
|
+
export type ClassDispositionRow = Partial<Record<Band, Disposition>>;
|
|
14
|
+
export interface PolicyTable {
|
|
15
|
+
version: string;
|
|
16
|
+
dispositions: Partial<Record<ScrubClass, ClassDispositionRow>>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Default policy. High-confidence deterministic classes redact; reject classes
|
|
20
|
+
* (A4/A5) reject-publish; A2 mid-band flags; C2 passes pending flag surface.
|
|
21
|
+
*/
|
|
22
|
+
export declare const DEFAULT_POLICY: PolicyTable;
|
|
23
|
+
export declare function resolveDisposition(scrubClass: ScrubClass, band: Band, policy?: PolicyTable): Disposition;
|
|
24
|
+
/**
|
|
25
|
+
* Check-mode mapping (design §6.5): any non-`pass` disposition → reject.
|
|
26
|
+
* One mapping line — not a second pipeline.
|
|
27
|
+
*/
|
|
28
|
+
export declare function checkModeRejects(findings: Finding[], policy?: PolicyTable): boolean;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned disposition policy (#1969 / design §6.5).
|
|
3
|
+
*
|
|
4
|
+
* disposition(class, band) -> redact | reject-publish | flag | pass
|
|
5
|
+
*
|
|
6
|
+
* Defaults follow §3.2's rightmost columns. C2 stays `pass` until the flag
|
|
7
|
+
* review surface exists (do not redact 0x+64 tx hashes).
|
|
8
|
+
*/
|
|
9
|
+
export const POLICY_VERSION = '1.0.0';
|
|
10
|
+
/**
|
|
11
|
+
* Default policy. High-confidence deterministic classes redact; reject classes
|
|
12
|
+
* (A4/A5) reject-publish; A2 mid-band flags; C2 passes pending flag surface.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_POLICY = {
|
|
15
|
+
version: POLICY_VERSION,
|
|
16
|
+
dispositions: {
|
|
17
|
+
A1: {
|
|
18
|
+
VERY_HIGH: 'redact',
|
|
19
|
+
HIGH: 'redact',
|
|
20
|
+
MEDIUM: 'redact',
|
|
21
|
+
LOW: 'flag',
|
|
22
|
+
VERY_LOW: 'pass',
|
|
23
|
+
},
|
|
24
|
+
A2: {
|
|
25
|
+
VERY_HIGH: 'redact',
|
|
26
|
+
HIGH: 'redact',
|
|
27
|
+
MEDIUM: 'flag',
|
|
28
|
+
LOW: 'pass',
|
|
29
|
+
VERY_LOW: 'pass',
|
|
30
|
+
},
|
|
31
|
+
A3: {
|
|
32
|
+
VERY_HIGH: 'redact',
|
|
33
|
+
HIGH: 'redact',
|
|
34
|
+
MEDIUM: 'redact',
|
|
35
|
+
LOW: 'flag',
|
|
36
|
+
VERY_LOW: 'pass',
|
|
37
|
+
},
|
|
38
|
+
A4: {
|
|
39
|
+
VERY_HIGH: 'reject-publish',
|
|
40
|
+
HIGH: 'reject-publish',
|
|
41
|
+
MEDIUM: 'reject-publish',
|
|
42
|
+
LOW: 'flag',
|
|
43
|
+
VERY_LOW: 'pass',
|
|
44
|
+
},
|
|
45
|
+
A5: {
|
|
46
|
+
VERY_HIGH: 'reject-publish',
|
|
47
|
+
HIGH: 'reject-publish',
|
|
48
|
+
MEDIUM: 'reject-publish',
|
|
49
|
+
LOW: 'flag',
|
|
50
|
+
VERY_LOW: 'pass',
|
|
51
|
+
},
|
|
52
|
+
B1: {
|
|
53
|
+
VERY_HIGH: 'redact',
|
|
54
|
+
HIGH: 'redact',
|
|
55
|
+
MEDIUM: 'redact',
|
|
56
|
+
LOW: 'flag',
|
|
57
|
+
VERY_LOW: 'pass',
|
|
58
|
+
},
|
|
59
|
+
B2: {
|
|
60
|
+
VERY_HIGH: 'redact',
|
|
61
|
+
HIGH: 'redact',
|
|
62
|
+
MEDIUM: 'redact',
|
|
63
|
+
LOW: 'flag',
|
|
64
|
+
VERY_LOW: 'pass',
|
|
65
|
+
},
|
|
66
|
+
B3: {
|
|
67
|
+
VERY_HIGH: 'redact',
|
|
68
|
+
HIGH: 'flag',
|
|
69
|
+
MEDIUM: 'flag',
|
|
70
|
+
LOW: 'pass',
|
|
71
|
+
VERY_LOW: 'pass',
|
|
72
|
+
},
|
|
73
|
+
B4: {
|
|
74
|
+
VERY_HIGH: 'redact',
|
|
75
|
+
HIGH: 'redact',
|
|
76
|
+
MEDIUM: 'flag',
|
|
77
|
+
LOW: 'pass',
|
|
78
|
+
VERY_LOW: 'pass',
|
|
79
|
+
},
|
|
80
|
+
B5: {
|
|
81
|
+
VERY_HIGH: 'redact',
|
|
82
|
+
HIGH: 'redact',
|
|
83
|
+
MEDIUM: 'flag',
|
|
84
|
+
LOW: 'pass',
|
|
85
|
+
VERY_LOW: 'pass',
|
|
86
|
+
},
|
|
87
|
+
B6: {
|
|
88
|
+
VERY_HIGH: 'flag',
|
|
89
|
+
HIGH: 'flag',
|
|
90
|
+
MEDIUM: 'flag',
|
|
91
|
+
LOW: 'pass',
|
|
92
|
+
VERY_LOW: 'pass',
|
|
93
|
+
},
|
|
94
|
+
B7: {
|
|
95
|
+
VERY_HIGH: 'redact',
|
|
96
|
+
HIGH: 'redact',
|
|
97
|
+
MEDIUM: 'flag',
|
|
98
|
+
LOW: 'pass',
|
|
99
|
+
VERY_LOW: 'pass',
|
|
100
|
+
},
|
|
101
|
+
C1: {
|
|
102
|
+
VERY_HIGH: 'redact',
|
|
103
|
+
HIGH: 'redact',
|
|
104
|
+
MEDIUM: 'redact',
|
|
105
|
+
LOW: 'flag',
|
|
106
|
+
VERY_LOW: 'pass',
|
|
107
|
+
},
|
|
108
|
+
// C2: §3.2 default is flag, but provenance receipts (0x+64 tx hashes) must
|
|
109
|
+
// survive — leave as pass (locked Q / deferred from review-queue landing).
|
|
110
|
+
C2: {
|
|
111
|
+
VERY_HIGH: 'pass',
|
|
112
|
+
HIGH: 'pass',
|
|
113
|
+
MEDIUM: 'pass',
|
|
114
|
+
LOW: 'pass',
|
|
115
|
+
VERY_LOW: 'pass',
|
|
116
|
+
},
|
|
117
|
+
D1: {
|
|
118
|
+
VERY_HIGH: 'redact',
|
|
119
|
+
HIGH: 'redact',
|
|
120
|
+
MEDIUM: 'redact',
|
|
121
|
+
LOW: 'flag',
|
|
122
|
+
VERY_LOW: 'pass',
|
|
123
|
+
},
|
|
124
|
+
D2: {
|
|
125
|
+
VERY_HIGH: 'redact',
|
|
126
|
+
HIGH: 'redact',
|
|
127
|
+
MEDIUM: 'flag',
|
|
128
|
+
LOW: 'pass',
|
|
129
|
+
VERY_LOW: 'pass',
|
|
130
|
+
},
|
|
131
|
+
D3: {
|
|
132
|
+
VERY_HIGH: 'redact',
|
|
133
|
+
HIGH: 'redact',
|
|
134
|
+
MEDIUM: 'flag',
|
|
135
|
+
LOW: 'pass',
|
|
136
|
+
VERY_LOW: 'pass',
|
|
137
|
+
},
|
|
138
|
+
E1: {
|
|
139
|
+
VERY_HIGH: 'flag',
|
|
140
|
+
HIGH: 'flag',
|
|
141
|
+
MEDIUM: 'flag',
|
|
142
|
+
LOW: 'pass',
|
|
143
|
+
VERY_LOW: 'pass',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
export function resolveDisposition(scrubClass, band, policy = DEFAULT_POLICY) {
|
|
148
|
+
return policy.dispositions[scrubClass]?.[band] ?? 'pass';
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Check-mode mapping (design §6.5): any non-`pass` disposition → reject.
|
|
152
|
+
* One mapping line — not a second pipeline.
|
|
153
|
+
*/
|
|
154
|
+
export function checkModeRejects(findings, policy = DEFAULT_POLICY) {
|
|
155
|
+
return findings.some((f) => resolveDisposition(f.class, f.confidence, policy) !== 'pass');
|
|
156
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scrub provenance for the published redaction manifest (#1974 / design §6.7).
|
|
3
|
+
*
|
|
4
|
+
* Locked Q5: additive optional fields on the existing redaction-manifest shape
|
|
5
|
+
* (`schemaVersion` 2) — no TraceEnvelope rev. Old envelopes remain readable.
|
|
6
|
+
*/
|
|
7
|
+
import type { ScrubClass } from './finding.js';
|
|
8
|
+
import type { Disposition, PolicyTable } from './policy.js';
|
|
9
|
+
/** Manifest schema version that includes optional provenance fields. */
|
|
10
|
+
export declare const REDACTION_MANIFEST_SCHEMA_VERSION = 2;
|
|
11
|
+
/** Disposition kinds counted in `perClassCounts` (pass is omitted). */
|
|
12
|
+
export type CountedDisposition = 'redact' | 'flag' | 'reject';
|
|
13
|
+
export interface PolicyHashInput {
|
|
14
|
+
/** Versioned disposition policy table. */
|
|
15
|
+
policy: PolicyTable;
|
|
16
|
+
/** Detector inventory (name + pinned version), order-insensitive. */
|
|
17
|
+
detectors: ReadonlyArray<{
|
|
18
|
+
name: string;
|
|
19
|
+
version: string;
|
|
20
|
+
}>;
|
|
21
|
+
/** ML model id when the ML tier is present; null otherwise. */
|
|
22
|
+
modelId: string | null;
|
|
23
|
+
/** Zero-shot / NER label set hashed into the policy digest. */
|
|
24
|
+
labels: readonly string[];
|
|
25
|
+
/** Digest of the instance allowlist (see {@link computeAllowlistDigest}). */
|
|
26
|
+
allowlistDigest: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Stable key for one class × applied disposition in `perClassCounts`.
|
|
30
|
+
* Reject-publish dispositions are recorded as `reject`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function perClassCountKey(scrubClass: ScrubClass, disposition: CountedDisposition): string;
|
|
33
|
+
/** Map a policy disposition onto the counted kind (or null for pass). */
|
|
34
|
+
export declare function countedDisposition(disposition: Disposition): CountedDisposition | null;
|
|
35
|
+
/**
|
|
36
|
+
* sha256 hex over a canonical JSON of allowlist entries sorted by
|
|
37
|
+
* (value, kind). Provenance notes are excluded so the digest is about
|
|
38
|
+
* *what* is allowlisted, not the human rationale string.
|
|
39
|
+
*/
|
|
40
|
+
export declare function computeAllowlistDigest(entries: ReadonlyArray<{
|
|
41
|
+
value: string;
|
|
42
|
+
kind: string;
|
|
43
|
+
}>): string;
|
|
44
|
+
/**
|
|
45
|
+
* Reproducible policy/composition hash: same inputs → same hex digest.
|
|
46
|
+
* Payload is RFC 8785 canonical JSON of policy + sorted detectors + model
|
|
47
|
+
* id + sorted labels + allowlist digest, then sha256.
|
|
48
|
+
*/
|
|
49
|
+
export declare function computePolicyHash(input: PolicyHashInput): string;
|
|
50
|
+
/** Increment one class:disposition counter (mutates `counts`). */
|
|
51
|
+
export declare function incrementPerClassCount(counts: Record<string, number>, scrubClass: ScrubClass, disposition: Disposition): void;
|
|
52
|
+
/** Sum two per-class count maps (missing → empty). */
|
|
53
|
+
export declare function mergePerClassCounts(a?: Record<string, number>, b?: Record<string, number>): Record<string, number> | undefined;
|