modelrig 0.8.1 → 0.9.0
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/CHANGELOG.md +145 -0
- package/README.md +4 -2
- package/dist/artifacts/artifact.d.ts +12 -13
- package/dist/artifacts/artifact.d.ts.map +1 -1
- package/dist/artifacts/artifact.js +17 -23
- package/dist/artifacts/index.d.ts +13 -5
- package/dist/artifacts/index.d.ts.map +1 -1
- package/dist/artifacts/index.js +11 -4
- package/dist/artifacts/run-context.d.ts +3 -24
- package/dist/artifacts/run-context.d.ts.map +1 -1
- package/dist/artifacts/run-context.js +3 -39
- package/dist/bakeoff/engine.d.ts +29 -0
- package/dist/bakeoff/engine.d.ts.map +1 -1
- package/dist/bakeoff/engine.js +47 -0
- package/dist/bakeoff/eval-cases-rig.d.ts +36 -0
- package/dist/bakeoff/eval-cases-rig.d.ts.map +1 -0
- package/dist/bakeoff/eval-cases-rig.js +27 -0
- package/dist/bakeoff/eval-cases.d.ts +19 -0
- package/dist/bakeoff/eval-cases.d.ts.map +1 -1
- package/dist/bakeoff/eval-cases.js +18 -1
- package/dist/bakeoff/golden-judge.d.ts +47 -0
- package/dist/bakeoff/golden-judge.d.ts.map +1 -0
- package/dist/bakeoff/golden-judge.js +49 -0
- package/dist/bakeoff/quality.d.ts +30 -0
- package/dist/bakeoff/quality.d.ts.map +1 -1
- package/dist/bakeoff/quality.js +21 -0
- package/dist/bakeoff/replay.d.ts +11 -0
- package/dist/bakeoff/replay.d.ts.map +1 -1
- package/dist/bakeoff/replay.js +8 -0
- package/dist/bakeoff/report.d.ts +7 -0
- package/dist/bakeoff/report.d.ts.map +1 -1
- package/dist/bakeoff/report.js +26 -0
- package/dist/cli-judge.d.ts +12 -0
- package/dist/cli-judge.d.ts.map +1 -0
- package/dist/cli-judge.js +48 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -42
- package/dist/config.d.ts +54 -29
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +86 -47
- package/dist/detect/detector.d.ts +4 -2
- package/dist/detect/detector.d.ts.map +1 -1
- package/dist/detect/safe-harbor.d.ts +57 -0
- package/dist/detect/safe-harbor.d.ts.map +1 -0
- package/dist/detect/safe-harbor.js +278 -0
- package/dist/feedback/coverage.d.ts +11 -8
- package/dist/feedback/coverage.d.ts.map +1 -1
- package/dist/feedback/coverage.js +29 -16
- package/dist/feedback/grade.d.ts +51 -0
- package/dist/feedback/grade.d.ts.map +1 -0
- package/dist/feedback/grade.js +63 -0
- package/dist/gateway/http-protocol.d.ts +1 -1
- package/dist/gateway/http-protocol.d.ts.map +1 -1
- package/dist/gateway/http-protocol.js +1 -1
- package/dist/index.d.ts +44 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +110 -43
- package/dist/init/observe-cli.js +1 -1
- package/dist/init/observe.d.ts +2 -2
- package/dist/init/observe.d.ts.map +1 -1
- package/dist/init/observe.js +11 -7
- package/dist/mask/arm.d.ts +112 -0
- package/dist/mask/arm.d.ts.map +1 -0
- package/dist/mask/arm.js +179 -0
- package/dist/mask/index.d.ts +2 -0
- package/dist/mask/index.d.ts.map +1 -1
- package/dist/mask/index.js +7 -1
- package/dist/mask/mask-policy-cli.d.ts +12 -4
- package/dist/mask/mask-policy-cli.d.ts.map +1 -1
- package/dist/mask/mask-policy-cli.js +34 -9
- package/dist/query/grammar.d.ts +2 -2
- package/dist/query/grammar.d.ts.map +1 -1
- package/dist/query/grammar.js +3 -1
- package/dist/query/saved.d.ts +13 -0
- package/dist/query/saved.d.ts.map +1 -1
- package/dist/query/saved.js +67 -1
- package/dist/receipts/types.d.ts +1 -1
- package/dist/receipts/types.d.ts.map +1 -1
- package/dist/receipts/types.js +1 -0
- package/dist/registry/build.d.ts +43 -1
- package/dist/registry/build.d.ts.map +1 -1
- package/dist/registry/build.js +52 -0
- package/dist/registry/pricing-shape.d.ts +29 -0
- package/dist/registry/pricing-shape.d.ts.map +1 -1
- package/dist/registry/pricing-shape.js +10 -1
- package/dist/registry/rows.d.ts.map +1 -1
- package/dist/registry/rows.js +1 -0
- package/dist/registry/select.d.ts +6 -1
- package/dist/registry/select.d.ts.map +1 -1
- package/dist/registry/select.js +11 -0
- package/dist/routes/call-config.d.ts +199 -0
- package/dist/routes/call-config.d.ts.map +1 -0
- package/dist/routes/call-config.js +343 -0
- package/dist/routes/loader.js +5 -3
- package/dist/routes/objective-loader.d.ts +12 -0
- package/dist/routes/objective-loader.d.ts.map +1 -1
- package/dist/routes/objective-loader.js +82 -0
- package/dist/routes/resolved-bundle.d.ts +50 -0
- package/dist/routes/resolved-bundle.d.ts.map +1 -1
- package/dist/routes/resolved-bundle.js +52 -0
- package/dist/routes/screen-loader.d.ts +28 -0
- package/dist/routes/screen-loader.d.ts.map +1 -1
- package/dist/routes/screen-loader.js +51 -0
- package/dist/run/accept.d.ts +2 -0
- package/dist/run/accept.d.ts.map +1 -1
- package/dist/run/accept.js +1 -0
- package/dist/run/dispatch.d.ts +42 -13
- package/dist/run/dispatch.d.ts.map +1 -1
- package/dist/run/dispatch.js +107 -25
- package/dist/run/grounding.d.ts +28 -2
- package/dist/run/grounding.d.ts.map +1 -1
- package/dist/run/grounding.js +54 -10
- package/dist/run/record-attempt.d.ts +4 -0
- package/dist/run/record-attempt.d.ts.map +1 -1
- package/dist/run/record-attempt.js +4 -0
- package/dist/run/routing-docs.js +1 -1
- package/dist/run/run.d.ts.map +1 -1
- package/dist/run/run.js +21 -26
- package/dist/savings/odometer.d.ts +4 -0
- package/dist/savings/odometer.d.ts.map +1 -1
- package/dist/savings/odometer.js +20 -1
- package/dist/savings/swap-execute.d.ts +7 -0
- package/dist/savings/swap-execute.d.ts.map +1 -1
- package/dist/savings/swap-execute.js +30 -0
- package/dist/savings/swaps-client.d.ts +10 -0
- package/dist/savings/swaps-client.d.ts.map +1 -1
- package/dist/screen/fixtures/probe-corpora.d.ts +23 -0
- package/dist/screen/fixtures/probe-corpora.d.ts.map +1 -0
- package/dist/screen/fixtures/probe-corpora.js +69 -0
- package/dist/screen/index.d.ts +21 -0
- package/dist/screen/index.d.ts.map +1 -0
- package/dist/screen/index.js +64 -0
- package/dist/screen/model-screen.d.ts +111 -0
- package/dist/screen/model-screen.d.ts.map +1 -0
- package/dist/screen/model-screen.js +178 -0
- package/dist/screen/patterns.d.ts +51 -0
- package/dist/screen/patterns.d.ts.map +1 -0
- package/dist/screen/patterns.js +151 -0
- package/dist/screen/phi-gate.d.ts +52 -0
- package/dist/screen/phi-gate.d.ts.map +1 -0
- package/dist/screen/phi-gate.js +86 -0
- package/dist/screen/policy.d.ts +71 -0
- package/dist/screen/policy.d.ts.map +1 -0
- package/dist/screen/policy.js +334 -0
- package/dist/screen/probe.d.ts +118 -0
- package/dist/screen/probe.d.ts.map +1 -0
- package/dist/screen/probe.js +132 -0
- package/dist/screen/screen-cli.d.ts +16 -0
- package/dist/screen/screen-cli.d.ts.map +1 -0
- package/dist/screen/screen-cli.js +178 -0
- package/dist/screen/screen.d.ts +99 -0
- package/dist/screen/screen.d.ts.map +1 -0
- package/dist/screen/screen.js +0 -0
- package/dist/screen/sidecar.d.ts +69 -0
- package/dist/screen/sidecar.d.ts.map +1 -0
- package/dist/screen/sidecar.js +145 -0
- package/dist/screen/types.d.ts +43 -2
- package/dist/screen/types.d.ts.map +1 -1
- package/dist/settings/api-fetcher.d.ts +14 -3
- package/dist/settings/api-fetcher.d.ts.map +1 -1
- package/dist/settings/api-fetcher.js +61 -10
- package/dist/settings/grading-guide.d.ts +76 -0
- package/dist/settings/grading-guide.d.ts.map +1 -0
- package/dist/settings/grading-guide.js +23 -0
- package/dist/settings/index.d.ts +3 -2
- package/dist/settings/index.d.ts.map +1 -1
- package/dist/settings/index.js +2 -1
- package/dist/settings/task-settings.d.ts +44 -7
- package/dist/settings/task-settings.d.ts.map +1 -1
- package/dist/settings/task-settings.js +53 -12
- package/dist/status.d.ts +26 -20
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +47 -29
- package/dist/telemetry/artifact-store.d.ts +5 -6
- package/dist/telemetry/artifact-store.d.ts.map +1 -1
- package/dist/telemetry/artifact-store.js +10 -24
- package/dist/telemetry/attempt-meta.d.ts +21 -0
- package/dist/telemetry/attempt-meta.d.ts.map +1 -1
- package/dist/telemetry/attempt-meta.js +9 -0
- package/dist/telemetry/exporter.d.ts +2 -2
- package/dist/telemetry/exporter.d.ts.map +1 -1
- package/dist/telemetry/exporter.js +22 -41
- package/dist/telemetry/grade-mappers.d.ts +22 -0
- package/dist/telemetry/grade-mappers.d.ts.map +1 -0
- package/dist/telemetry/grade-mappers.js +58 -0
- package/dist/telemetry/grades-sql.d.ts +53 -0
- package/dist/telemetry/grades-sql.d.ts.map +1 -0
- package/dist/telemetry/grades-sql.js +133 -0
- package/dist/telemetry/ingest-protocol.d.ts +29 -46
- package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
- package/dist/telemetry/ingest-protocol.js +16 -54
- package/dist/telemetry/store-migrations.d.ts.map +1 -1
- package/dist/telemetry/store-migrations.js +51 -0
- package/dist/telemetry/store.d.ts +9 -6
- package/dist/telemetry/store.d.ts.map +1 -1
- package/dist/telemetry/store.js +8 -50
- package/dist/tenancy/api-key-format.d.ts +30 -1
- package/dist/tenancy/api-key-format.d.ts.map +1 -1
- package/dist/tenancy/api-key-format.js +33 -1
- package/dist/types/provider.d.ts +3 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/provider.js +25 -0
- package/dist/types/route.d.ts +78 -2
- package/dist/types/route.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +43 -46
- package/dist/types/telemetry.d.ts.map +1 -1
- package/dist/validate-cli.d.ts.map +1 -1
- package/dist/validate-cli.js +32 -0
- package/dist/verify/verify.d.ts +5 -0
- package/dist/verify/verify.d.ts.map +1 -1
- package/dist/verify/verify.js +2 -0
- package/dist/watch/evaluate.d.ts +84 -1
- package/dist/watch/evaluate.d.ts.map +1 -1
- package/dist/watch/evaluate.js +28 -0
- package/dist/watch/monitor.d.ts +8 -2
- package/dist/watch/monitor.d.ts.map +1 -1
- package/dist/watch/monitor.js +13 -5
- package/dist/watch/regret-monitor-cli.d.ts +16 -0
- package/dist/watch/regret-monitor-cli.d.ts.map +1 -0
- package/dist/watch/regret-monitor-cli.js +137 -0
- package/dist/watch/regret-monitor.d.ts +168 -0
- package/dist/watch/regret-monitor.d.ts.map +1 -0
- package/dist/watch/regret-monitor.js +304 -0
- package/dist/watch/upgrade.d.ts +62 -0
- package/dist/watch/upgrade.d.ts.map +1 -0
- package/dist/watch/upgrade.js +57 -0
- package/docs/agent-skill/SKILL.md +109 -5
- package/docs/how-it-fits.md +3 -2
- package/docs/instrumentation.md +149 -21
- package/docs/llms.txt +64 -20
- package/docs/mcp-oracle.md +249 -0
- package/docs/quickstart.md +95 -7
- package/package.json +16 -3
- package/registry/leaderboard.json +74 -0
- package/registry/registry.json +150 -112
- package/screen.d.ts +16 -0
- package/templates/call-disposition-qa.json +77 -0
- package/templates/catalog-cleansing.json +77 -0
- package/templates/compliance-review.json +76 -0
- package/templates/cs-next-action.json +76 -0
- package/templates/document-extraction.json +78 -0
- package/templates/financial-classification.json +75 -0
- package/templates/lead-qualification.json +76 -0
- package/templates/medical-classification.json +71 -0
- package/templates/ticket-triage.json +76 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Screening — policy loading & validation (WIRED 2026-08-25, SC-1a). Spec §4.1.
|
|
4
|
+
*
|
|
5
|
+
* Runs at ROUTE LOAD (same place `require:` / `mask:` are validated),
|
|
6
|
+
* synchronously — the route loader is sync, so the shape check, the detector
|
|
7
|
+
* build, and the hash are too. An unknown detector, an unknown action, or a
|
|
8
|
+
* `sources` key that LOOSENS a default tier is a LOAD error (teaching message,
|
|
9
|
+
* CE-2 fixHint style), never a runtime surprise (spec §4.1, §4.3 row 1).
|
|
10
|
+
*
|
|
11
|
+
* The single discipline shared with mask/policy.ts: validate the raw shape first
|
|
12
|
+
* (returns teaching strings, never throws), then parse, then resolve (builds
|
|
13
|
+
* detectors + the stable `policyHash`). A configured-but-unavailable optional
|
|
14
|
+
* classifier is NOT a load error — it degrades to T0 with `degraded: true`
|
|
15
|
+
* (spec §4.3); only a malformed policy fails the load.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.validateScreenPolicyShape = validateScreenPolicyShape;
|
|
19
|
+
exports.parseScreenPolicy = parseScreenPolicy;
|
|
20
|
+
exports.screenCacheDir = screenCacheDir;
|
|
21
|
+
exports.classifierModelPath = classifierModelPath;
|
|
22
|
+
exports.resolveScreenPolicy = resolveScreenPolicy;
|
|
23
|
+
exports.policyHashOf = policyHashOf;
|
|
24
|
+
const node_crypto_1 = require("node:crypto");
|
|
25
|
+
const node_os_1 = require("node:os");
|
|
26
|
+
const node_path_1 = require("node:path");
|
|
27
|
+
const provenance_1 = require("./provenance");
|
|
28
|
+
const patterns_1 = require("./patterns");
|
|
29
|
+
const safe_harbor_1 = require("../detect/safe-harbor");
|
|
30
|
+
const sidecar_1 = require("./sidecar");
|
|
31
|
+
const model_screen_1 = require("./model-screen");
|
|
32
|
+
const MODES = new Set(["off", "detect", "enforce"]);
|
|
33
|
+
const ACTIONS = new Set(["flag", "strip", "block", "quarantine"]);
|
|
34
|
+
const TIERS = new Set(["trusted", "semi", "untrusted"]);
|
|
35
|
+
const SOURCES = new Set([
|
|
36
|
+
"template",
|
|
37
|
+
"variable",
|
|
38
|
+
"search",
|
|
39
|
+
"tool_result",
|
|
40
|
+
"proxy_user",
|
|
41
|
+
"judge_meta",
|
|
42
|
+
"unknown",
|
|
43
|
+
]);
|
|
44
|
+
const CITATIONS = new Set(["allowlist", "off"]);
|
|
45
|
+
/** Trust rank — HIGHER is MORE trusted. A `sources` override may only keep or
|
|
46
|
+
* LOWER a source's rank (tighten); RAISING it (loosening, e.g. search→semi) is a
|
|
47
|
+
* load error (spec §2.1 "may tighten … but never loosen"). */
|
|
48
|
+
function trustRank(t) {
|
|
49
|
+
return t === "trusted" ? 2 : t === "semi" ? 1 : 0;
|
|
50
|
+
}
|
|
51
|
+
function isRecord(v) {
|
|
52
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
53
|
+
}
|
|
54
|
+
/** A detector id is known iff it is the injection pack, a `classifier:` tier id,
|
|
55
|
+
* a SC-2 Safe Harbor id (`pii:<slug>@v1` / `safe-harbor@v1`), or a
|
|
56
|
+
* `sidecar:<digest>` id (SC-2 §3). The optional peer/weights (classifier) and
|
|
57
|
+
* the endpoint reachability (sidecar) decide AVAILABILITY at resolve/run time,
|
|
58
|
+
* not validity here — an id typo must still fail the load. */
|
|
59
|
+
function isKnownDetectorId(id) {
|
|
60
|
+
return (id === patterns_1.PATTERNS_ID ||
|
|
61
|
+
id.startsWith("classifier:") ||
|
|
62
|
+
id.startsWith("sidecar:") ||
|
|
63
|
+
id.startsWith("model:screen@") ||
|
|
64
|
+
(0, safe_harbor_1.isSafeHarborId)(id));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate the raw YAML `screen:` block shape (used by the route loader and by
|
|
68
|
+
* `modelrig validate`). Returns teaching-style problem strings (CE-2 fixHint
|
|
69
|
+
* pattern) — EMPTY means valid. Never throws.
|
|
70
|
+
*/
|
|
71
|
+
function validateScreenPolicyShape(raw) {
|
|
72
|
+
const problems = [];
|
|
73
|
+
if (!isRecord(raw)) {
|
|
74
|
+
problems.push("screen must be a block with at least a `mode` (off|detect|enforce)");
|
|
75
|
+
return problems;
|
|
76
|
+
}
|
|
77
|
+
if (raw.mode !== undefined && (typeof raw.mode !== "string" || !MODES.has(raw.mode))) {
|
|
78
|
+
problems.push("screen.mode must be one of off | detect | enforce");
|
|
79
|
+
}
|
|
80
|
+
if (raw.sources !== undefined) {
|
|
81
|
+
if (!isRecord(raw.sources)) {
|
|
82
|
+
problems.push("screen.sources must be a map of content source → trust tier (trusted|semi|untrusted)");
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
for (const [source, tier] of Object.entries(raw.sources)) {
|
|
86
|
+
if (!SOURCES.has(source)) {
|
|
87
|
+
problems.push(`screen.sources key "${source}" is not a known content source — allowed: ${[...SOURCES].join(", ")}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (typeof tier !== "string" || !TIERS.has(tier)) {
|
|
91
|
+
problems.push(`screen.sources.${source} must be trusted | semi | untrusted`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (trustRank(tier) > trustRank((0, provenance_1.defaultTier)(source))) {
|
|
95
|
+
problems.push(`screen.sources.${source}: "${tier}" LOOSENS the default tier "${(0, provenance_1.defaultTier)(source)}" — ` +
|
|
96
|
+
"a source may only be tightened (made more untrusted), never loosened (spec §2.1).");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (raw.detectors !== undefined) {
|
|
102
|
+
if (!Array.isArray(raw.detectors)) {
|
|
103
|
+
problems.push("screen.detectors must be a list of detector ids (e.g. patterns@v1, classifier:defender@int8)");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
for (const d of raw.detectors) {
|
|
107
|
+
if (typeof d !== "string" || !isKnownDetectorId(d)) {
|
|
108
|
+
problems.push(`screen.detectors entry "${String(d)}" is not a known detector — allowed: patterns@v1, ` +
|
|
109
|
+
"classifier:<name>, safe-harbor@v1 (or a pii:<slug>@v1 Safe Harbor id), sidecar:<digest>, model:screen@<model>");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (raw.sidecar !== undefined)
|
|
115
|
+
validateSidecarShape(raw.sidecar, problems);
|
|
116
|
+
if (raw.on !== undefined) {
|
|
117
|
+
if (!isRecord(raw.on)) {
|
|
118
|
+
problems.push("screen.on must be a map of trust tier → action (flag|strip|block|quarantine)");
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
for (const [tier, action] of Object.entries(raw.on)) {
|
|
122
|
+
if (!TIERS.has(tier)) {
|
|
123
|
+
problems.push(`screen.on key "${tier}" is not a trust tier — allowed: trusted, semi, untrusted`);
|
|
124
|
+
}
|
|
125
|
+
else if (typeof action !== "string" || !ACTIONS.has(action)) {
|
|
126
|
+
problems.push(`screen.on.${tier} must be one of flag | strip | block | quarantine`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (raw.threshold !== undefined && (typeof raw.threshold !== "number" || raw.threshold < 0 || raw.threshold > 1)) {
|
|
132
|
+
problems.push("screen.threshold must be a number in [0, 1] (default 0.8)");
|
|
133
|
+
}
|
|
134
|
+
if (raw.output !== undefined)
|
|
135
|
+
validateOutputShape(raw.output, problems);
|
|
136
|
+
return problems;
|
|
137
|
+
}
|
|
138
|
+
/** Validate the optional `screen.sidecar` block (SC-2 §3). A malformed sidecar
|
|
139
|
+
* is a LOAD error (teaching message) — a `phi` route that references a sidecar
|
|
140
|
+
* must have a real one. */
|
|
141
|
+
function validateSidecarShape(sidecar, problems) {
|
|
142
|
+
if (!isRecord(sidecar)) {
|
|
143
|
+
problems.push("screen.sidecar must be a block { digest, endpoint, timeout_ms?, types? }");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (typeof sidecar.digest !== "string" || sidecar.digest === "") {
|
|
147
|
+
problems.push("screen.sidecar.digest must be a non-empty string (the customer's image/config digest)");
|
|
148
|
+
}
|
|
149
|
+
if (typeof sidecar.endpoint !== "string" || !/^https?:\/\//.test(sidecar.endpoint)) {
|
|
150
|
+
problems.push("screen.sidecar.endpoint must be an http(s) URL to your own de-id service");
|
|
151
|
+
}
|
|
152
|
+
if (sidecar.timeout_ms !== undefined && (typeof sidecar.timeout_ms !== "number" || sidecar.timeout_ms <= 0)) {
|
|
153
|
+
problems.push("screen.sidecar.timeout_ms must be a positive number of milliseconds");
|
|
154
|
+
}
|
|
155
|
+
if (sidecar.types !== undefined && (!Array.isArray(sidecar.types) || sidecar.types.some((t) => typeof t !== "string"))) {
|
|
156
|
+
problems.push("screen.sidecar.types must be a list of span type labels (strings)");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function validateOutputShape(output, problems) {
|
|
160
|
+
if (!isRecord(output)) {
|
|
161
|
+
problems.push("screen.output must be a block { canary?, strip_unicode_tags?, citations? }");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (output.canary !== undefined && typeof output.canary !== "boolean") {
|
|
165
|
+
problems.push("screen.output.canary must be a boolean");
|
|
166
|
+
}
|
|
167
|
+
if (output.strip_unicode_tags !== undefined && typeof output.strip_unicode_tags !== "boolean") {
|
|
168
|
+
problems.push("screen.output.strip_unicode_tags must be a boolean");
|
|
169
|
+
}
|
|
170
|
+
if (output.citations !== undefined && (typeof output.citations !== "string" || !CITATIONS.has(output.citations))) {
|
|
171
|
+
problems.push("screen.output.citations must be allowlist | off");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Parse a validated raw block into a typed {@link ScreenPolicy}. Assumes
|
|
175
|
+
* {@link validateScreenPolicyShape} already passed (caller checks first). */
|
|
176
|
+
function parseScreenPolicy(raw) {
|
|
177
|
+
const output = {};
|
|
178
|
+
if (isRecord(raw.output)) {
|
|
179
|
+
if (typeof raw.output.canary === "boolean")
|
|
180
|
+
output.canary = raw.output.canary;
|
|
181
|
+
if (typeof raw.output.strip_unicode_tags === "boolean")
|
|
182
|
+
output.strip_unicode_tags = raw.output.strip_unicode_tags;
|
|
183
|
+
if (raw.output.citations === "allowlist" || raw.output.citations === "off")
|
|
184
|
+
output.citations = raw.output.citations;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
mode: raw.mode ?? "off",
|
|
188
|
+
...(isRecord(raw.sources) ? { sources: raw.sources } : {}),
|
|
189
|
+
...(Array.isArray(raw.detectors) ? { detectors: raw.detectors } : {}),
|
|
190
|
+
...(isRecord(raw.on) ? { on: raw.on } : {}),
|
|
191
|
+
...(typeof raw.threshold === "number" ? { threshold: raw.threshold } : {}),
|
|
192
|
+
...(Object.keys(output).length > 0 ? { output } : {}),
|
|
193
|
+
...(isRecord(raw.sidecar) ? { sidecar: parseSidecar(raw.sidecar) } : {}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** Parse a validated `screen.sidecar` block into the typed config (assumes
|
|
197
|
+
* {@link validateSidecarShape} passed). */
|
|
198
|
+
function parseSidecar(raw) {
|
|
199
|
+
return {
|
|
200
|
+
digest: raw.digest,
|
|
201
|
+
endpoint: raw.endpoint,
|
|
202
|
+
...(typeof raw.timeout_ms === "number" ? { timeout_ms: raw.timeout_ms } : {}),
|
|
203
|
+
...(Array.isArray(raw.types) ? { types: raw.types } : {}),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** Local cache dir for optional classifier weights (`modelrig screen pull`
|
|
207
|
+
* writes here; the resolver reads here). Weights are NEVER bundled or re-hosted
|
|
208
|
+
* (A3) — this is the customer's local store under the home dir. (A configurable
|
|
209
|
+
* location can ride RigConfig in SC-1b; env is read only in config.ts.) */
|
|
210
|
+
function screenCacheDir() {
|
|
211
|
+
return (0, node_path_1.join)((0, node_os_1.homedir)(), ".modelrig", "screen");
|
|
212
|
+
}
|
|
213
|
+
/** The on-disk model path a classifier id resolves to in the local cache. */
|
|
214
|
+
function classifierModelPath(id) {
|
|
215
|
+
return (0, node_path_1.join)(screenCacheDir(), `${id.replace(/[^a-zA-Z0-9._@-]/g, "_")}.onnx`);
|
|
216
|
+
}
|
|
217
|
+
/** Canonical JSON of a policy for hashing — keys sorted so a reformat never
|
|
218
|
+
* changes the hash. The hash pins WHAT screened this inference on the receipt. */
|
|
219
|
+
function canonicalPolicy(policy) {
|
|
220
|
+
const norm = {
|
|
221
|
+
mode: policy.mode,
|
|
222
|
+
sources: policy.sources ?? {},
|
|
223
|
+
detectors: [...(policy.detectors ?? [patterns_1.PATTERNS_ID])].sort(),
|
|
224
|
+
on: policy.on ?? {},
|
|
225
|
+
threshold: policy.threshold ?? 0.8,
|
|
226
|
+
output: {
|
|
227
|
+
canary: policy.output?.canary ?? false,
|
|
228
|
+
strip_unicode_tags: policy.output?.strip_unicode_tags ?? false,
|
|
229
|
+
citations: policy.output?.citations ?? "off",
|
|
230
|
+
},
|
|
231
|
+
// Conditional so a policy WITHOUT a sidecar hashes exactly as before SC-2
|
|
232
|
+
// (no key added ⇒ byte-identical canonical JSON ⇒ unchanged policy_hash).
|
|
233
|
+
...(policy.sidecar
|
|
234
|
+
? {
|
|
235
|
+
sidecar: {
|
|
236
|
+
digest: policy.sidecar.digest,
|
|
237
|
+
endpoint: policy.sidecar.endpoint,
|
|
238
|
+
timeout_ms: policy.sidecar.timeout_ms ?? null,
|
|
239
|
+
types: policy.sidecar.types ? [...policy.sidecar.types].sort() : null,
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
: {}),
|
|
243
|
+
};
|
|
244
|
+
return JSON.stringify(norm, Object.keys(norm).sort());
|
|
245
|
+
}
|
|
246
|
+
function resolveScreenPolicy(policy, opts = {}) {
|
|
247
|
+
const detectors = [(0, patterns_1.createPatternDetector)()];
|
|
248
|
+
const detectorMeta = [{ id: patterns_1.PATTERNS_ID, version: "v1" }];
|
|
249
|
+
let degraded = false;
|
|
250
|
+
const configured = policy.detectors ?? [patterns_1.PATTERNS_ID];
|
|
251
|
+
for (const id of configured) {
|
|
252
|
+
if (id === patterns_1.PATTERNS_ID)
|
|
253
|
+
continue;
|
|
254
|
+
// SC-3 T3 model screen (spec-sc3 §2): a metered inference on the customer's
|
|
255
|
+
// provider. Built only when the dispatch layer injected the screen call;
|
|
256
|
+
// otherwise the optional tier is unavailable at load ⇒ degrade to the
|
|
257
|
+
// surviving detectors (never a load error), mirroring the classifier path.
|
|
258
|
+
if (id.startsWith("model:screen@")) {
|
|
259
|
+
const wiring = opts.modelScreen;
|
|
260
|
+
if (wiring === undefined) {
|
|
261
|
+
degraded = true;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const model = id.slice("model:screen@".length);
|
|
265
|
+
detectors.push((0, model_screen_1.createModelScreenDetector)({
|
|
266
|
+
id: (0, model_screen_1.modelScreenReceiptId)(id),
|
|
267
|
+
model,
|
|
268
|
+
dailyCap: wiring.dailyCap,
|
|
269
|
+
screenCall: wiring.call,
|
|
270
|
+
...(wiring.flagConfidence !== undefined ? { flagConfidence: wiring.flagConfidence } : {}),
|
|
271
|
+
...(wiring.meter !== undefined ? { meter: wiring.meter } : {}),
|
|
272
|
+
...(wiring.now !== undefined ? { now: wiring.now } : {}),
|
|
273
|
+
}));
|
|
274
|
+
detectorMeta.push({ id: (0, model_screen_1.modelScreenReceiptId)(id), version: model });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
// SC-2 deterministic Safe Harbor detectors (opt-in via the detectors list —
|
|
278
|
+
// NEVER the custody scrub floor). The pack expands to all ten; a single
|
|
279
|
+
// `pii:<slug>@v1` id resolves to one. Deterministic ⇒ always available.
|
|
280
|
+
if ((0, safe_harbor_1.isSafeHarborId)(id)) {
|
|
281
|
+
for (const det of (0, safe_harbor_1.safeHarborDetectorsFor)(id))
|
|
282
|
+
detectors.push(det);
|
|
283
|
+
for (const meta of (0, safe_harbor_1.safeHarborIdentities)(id))
|
|
284
|
+
detectorMeta.push(meta);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// The sidecar id is honoured via the `screen.sidecar` block below (its
|
|
288
|
+
// presence in `detectors` is the phi-gate signal; the endpoint rides the
|
|
289
|
+
// block), so a bare `sidecar:<digest>` id here is a no-op to avoid a double
|
|
290
|
+
// build.
|
|
291
|
+
if (id.startsWith("sidecar:"))
|
|
292
|
+
continue;
|
|
293
|
+
if (!id.startsWith("classifier:"))
|
|
294
|
+
continue;
|
|
295
|
+
try {
|
|
296
|
+
const det = (0, patterns_1.createClassifierDetector)({ id, modelPath: classifierModelPath(id), runtime: "onnx" });
|
|
297
|
+
detectors.push(det);
|
|
298
|
+
detectorMeta.push({ id, version: classifierVersion(id) });
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
if (err instanceof patterns_1.ScreenDetectorUnavailableError) {
|
|
302
|
+
degraded = true; // spec §4.3: T0 only, receipt degraded — never a block.
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// SC-2 sidecar (spec-sc2 §3): the `screen.sidecar` block builds a
|
|
309
|
+
// `sidecar:<digest>` detector. Reachability is a RUNTIME condition — an
|
|
310
|
+
// unreachable/timed-out sidecar throws at detect(), which the shipped seam
|
|
311
|
+
// turns into the fail-closed matrix (degrade under detect, pre-dispatch block
|
|
312
|
+
// under enforce). No local availability check here (unlike a classifier's
|
|
313
|
+
// model file); the digest identity always appears so the phi-gate sees it.
|
|
314
|
+
if (policy.sidecar) {
|
|
315
|
+
const id = (0, sidecar_1.sidecarReceiptId)(policy.sidecar.digest);
|
|
316
|
+
detectors.push((0, sidecar_1.createSidecarDetector)({
|
|
317
|
+
id,
|
|
318
|
+
endpoint: policy.sidecar.endpoint,
|
|
319
|
+
...(policy.sidecar.timeout_ms !== undefined ? { timeoutMs: policy.sidecar.timeout_ms } : {}),
|
|
320
|
+
...(policy.sidecar.types !== undefined ? { types: policy.sidecar.types } : {}),
|
|
321
|
+
}));
|
|
322
|
+
detectorMeta.push({ id, version: "sidecar" });
|
|
323
|
+
}
|
|
324
|
+
return { policy, detectors, detectorMeta, policyHash: policyHashOf(policy), degraded };
|
|
325
|
+
}
|
|
326
|
+
/** sha256 over the canonical policy JSON — the `policy_hash` a receipt carries. */
|
|
327
|
+
function policyHashOf(policy) {
|
|
328
|
+
return (0, node_crypto_1.createHash)("sha256").update(canonicalPolicy(policy)).digest("hex");
|
|
329
|
+
}
|
|
330
|
+
/** The version segment of a `classifier:name@version` id, or "unknown". */
|
|
331
|
+
function classifierVersion(id) {
|
|
332
|
+
const at = id.lastIndexOf("@");
|
|
333
|
+
return at > 0 ? id.slice(at + 1) : "unknown";
|
|
334
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SC-3 — `modelrig screen probe` core (spec-sc3 §3). Runs each configured
|
|
3
|
+
* `Detector` over (a) the license-clean LABELLED fixture corpora → TPR / FPR /
|
|
4
|
+
* latency, and (b) the org's BENIGN captures (the customer's own known-benign
|
|
5
|
+
* traffic) → the FP rate on your traffic, and populates the reserved
|
|
6
|
+
* `DetectorRegistryEntry.measured` shape (detect/detector.ts) — the "probed, not
|
|
7
|
+
* declared" doctrine (research §3.1 principle 6) applied to detectors.
|
|
8
|
+
*
|
|
9
|
+
* METRICS ONLY — NAMED LEAK INVARIANT, EXTENDED (spec-sc3 §3, acceptance b): the
|
|
10
|
+
* `measured` record holds numbers, a per-corpus DIGEST (a one-way hash, never
|
|
11
|
+
* content), a license label, and a detector version — and NOTHING ELSE. No
|
|
12
|
+
* fixture value, no captured content byte, and no matched span leaves the process
|
|
13
|
+
* into this record. A property test JSON-serializes the record over random
|
|
14
|
+
* content-bearing corpora and asserts none of that content appears.
|
|
15
|
+
*
|
|
16
|
+
* DETERMINISM (spec-sc3 §3): on a fixed corpus + fixed detector version the
|
|
17
|
+
* accuracy metrics (TPR/FPR/n) and the corpus digests are reproducible — corpora
|
|
18
|
+
* are evaluated in a stable (id-sorted) order, and the digest pins the exact
|
|
19
|
+
* inputs a published number was measured against. Latency is wall-clock by
|
|
20
|
+
* default; inject `clock` for a reproducible latency in tests.
|
|
21
|
+
*/
|
|
22
|
+
import type { ContentSource, Detector, DetectorRegistryEntry, TrustTier } from "../detect/detector";
|
|
23
|
+
/** The default policy threshold a span must clear to count as a DETECTION (the
|
|
24
|
+
* same 0.8 the screen engine defaults to; a probe run may override it). */
|
|
25
|
+
export declare const PROBE_DEFAULT_THRESHOLD = 0.8;
|
|
26
|
+
/** One labelled fixture item. `positive: true` ⇒ the item SHOULD be flagged
|
|
27
|
+
* (a true injection/PII shape); `false` ⇒ a benign negative. */
|
|
28
|
+
export interface LabelledItem {
|
|
29
|
+
readonly text: string;
|
|
30
|
+
readonly source: ContentSource;
|
|
31
|
+
readonly tier: TrustTier;
|
|
32
|
+
readonly positive: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** A license-clean labelled corpus. `id` + `license` are PUBLIC labels (never
|
|
35
|
+
* content); the digest is computed over the items so a published number is
|
|
36
|
+
* verifiable against the exact inputs. */
|
|
37
|
+
export interface LabelledCorpus {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
/** SPDX-style license label — only license-clean corpora may be probed and
|
|
40
|
+
* published (spec-sc3 §3 / acceptance f). Recorded on the receipt. */
|
|
41
|
+
readonly license: string;
|
|
42
|
+
readonly items: readonly LabelledItem[];
|
|
43
|
+
}
|
|
44
|
+
/** A known-benign capture item (the org's own traffic). No label — every flag is
|
|
45
|
+
* a FALSE POSITIVE by construction, which is exactly the FP rate we want. */
|
|
46
|
+
export interface BenignItem {
|
|
47
|
+
readonly text: string;
|
|
48
|
+
readonly source: ContentSource;
|
|
49
|
+
readonly tier: TrustTier;
|
|
50
|
+
}
|
|
51
|
+
/** Per-corpus measured metrics — counts + rates + a digest + a license. */
|
|
52
|
+
export interface PerCorpusMetric {
|
|
53
|
+
readonly corpusId: string;
|
|
54
|
+
/** sha256 over the canonical item shapes — a HASH, never the content. */
|
|
55
|
+
readonly digest: string;
|
|
56
|
+
readonly license: string;
|
|
57
|
+
/** Total items evaluated. */
|
|
58
|
+
readonly n: number;
|
|
59
|
+
/** Positives / negatives in the corpus (so TPR/FPR are interpretable). */
|
|
60
|
+
readonly positives: number;
|
|
61
|
+
readonly negatives: number;
|
|
62
|
+
/** True-positive rate = detected among positives / positives (null if none). */
|
|
63
|
+
readonly tpr: number | null;
|
|
64
|
+
/** False-positive rate = detected among negatives / negatives (null if none). */
|
|
65
|
+
readonly fpr: number | null;
|
|
66
|
+
/** Total detector wall-time over this corpus, ms (integer). */
|
|
67
|
+
readonly latencyMs: number;
|
|
68
|
+
}
|
|
69
|
+
/** The org-benign FP measurement, or the teaching state when there is nothing to
|
|
70
|
+
* sample (a metadata-posture org has no captures — NOT an error, spec-sc3 §8). */
|
|
71
|
+
export type OrgBenignMetric = {
|
|
72
|
+
readonly sampled: true;
|
|
73
|
+
readonly n: number;
|
|
74
|
+
readonly fpRate: number;
|
|
75
|
+
readonly sampledAt: string;
|
|
76
|
+
} | {
|
|
77
|
+
readonly sampled: false;
|
|
78
|
+
readonly note: "no captures to sample";
|
|
79
|
+
};
|
|
80
|
+
/** The reserved `DetectorRegistryEntry.measured` payload SC-3 populates. Metrics
|
|
81
|
+
* only — the leak invariant is STRUCTURAL: every field is a number, a hash, a
|
|
82
|
+
* license label, a version, or an ISO date. */
|
|
83
|
+
export interface ProbeMeasured {
|
|
84
|
+
readonly detectorId: string;
|
|
85
|
+
readonly detectorVersion: string;
|
|
86
|
+
readonly threshold: number;
|
|
87
|
+
readonly perCorpus: readonly PerCorpusMetric[];
|
|
88
|
+
readonly orgBenign: OrgBenignMetric;
|
|
89
|
+
}
|
|
90
|
+
export interface ProbeInput {
|
|
91
|
+
readonly detector: Detector;
|
|
92
|
+
/** The version label pinned on the receipt (e.g. "v1", "sidecar", a model id). */
|
|
93
|
+
readonly detectorVersion: string;
|
|
94
|
+
readonly corpora: readonly LabelledCorpus[];
|
|
95
|
+
/** The org's benign captures, when a capture source exists. Absent / empty ⇒
|
|
96
|
+
* the "no captures to sample" teaching state. */
|
|
97
|
+
readonly benign?: readonly BenignItem[];
|
|
98
|
+
/** Detection threshold (default {@link PROBE_DEFAULT_THRESHOLD}). */
|
|
99
|
+
readonly threshold?: number;
|
|
100
|
+
/** Injectable monotonic clock in ms (default performance.now) — for a
|
|
101
|
+
* reproducible latency in tests. */
|
|
102
|
+
readonly clock?: () => number;
|
|
103
|
+
/** Injectable wall clock (default Date) — for a reproducible `sampledAt`. */
|
|
104
|
+
readonly now?: () => Date;
|
|
105
|
+
}
|
|
106
|
+
/** Canonical, offset-stable digest of a corpus's item SHAPES. A one-way hash of
|
|
107
|
+
* the fixture inputs — recorded so a published metric is verifiable against the
|
|
108
|
+
* exact corpus, WITHOUT the corpus content ever entering the record. */
|
|
109
|
+
export declare function digestCorpus(items: readonly LabelledItem[]): string;
|
|
110
|
+
/**
|
|
111
|
+
* Probe one detector over the labelled corpora + the org benign captures →
|
|
112
|
+
* `ProbeMeasured`. Corpora are evaluated in a STABLE id-sorted order so the
|
|
113
|
+
* record is reproducible on a fixed corpus. Metrics only — never a value.
|
|
114
|
+
*/
|
|
115
|
+
export declare function probeDetector(input: ProbeInput): Promise<ProbeMeasured>;
|
|
116
|
+
/** Populate the reserved `DetectorRegistryEntry.measured` shape from a probe. */
|
|
117
|
+
export declare function buildRegistryEntry(detector: Detector, measured: ProbeMeasured): DetectorRegistryEntry;
|
|
118
|
+
//# sourceMappingURL=probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/screen/probe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SC-3 — `modelrig screen probe` core (spec-sc3 §3). Runs each configured
|
|
4
|
+
* `Detector` over (a) the license-clean LABELLED fixture corpora → TPR / FPR /
|
|
5
|
+
* latency, and (b) the org's BENIGN captures (the customer's own known-benign
|
|
6
|
+
* traffic) → the FP rate on your traffic, and populates the reserved
|
|
7
|
+
* `DetectorRegistryEntry.measured` shape (detect/detector.ts) — the "probed, not
|
|
8
|
+
* declared" doctrine (research §3.1 principle 6) applied to detectors.
|
|
9
|
+
*
|
|
10
|
+
* METRICS ONLY — NAMED LEAK INVARIANT, EXTENDED (spec-sc3 §3, acceptance b): the
|
|
11
|
+
* `measured` record holds numbers, a per-corpus DIGEST (a one-way hash, never
|
|
12
|
+
* content), a license label, and a detector version — and NOTHING ELSE. No
|
|
13
|
+
* fixture value, no captured content byte, and no matched span leaves the process
|
|
14
|
+
* into this record. A property test JSON-serializes the record over random
|
|
15
|
+
* content-bearing corpora and asserts none of that content appears.
|
|
16
|
+
*
|
|
17
|
+
* DETERMINISM (spec-sc3 §3): on a fixed corpus + fixed detector version the
|
|
18
|
+
* accuracy metrics (TPR/FPR/n) and the corpus digests are reproducible — corpora
|
|
19
|
+
* are evaluated in a stable (id-sorted) order, and the digest pins the exact
|
|
20
|
+
* inputs a published number was measured against. Latency is wall-clock by
|
|
21
|
+
* default; inject `clock` for a reproducible latency in tests.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.PROBE_DEFAULT_THRESHOLD = void 0;
|
|
25
|
+
exports.digestCorpus = digestCorpus;
|
|
26
|
+
exports.probeDetector = probeDetector;
|
|
27
|
+
exports.buildRegistryEntry = buildRegistryEntry;
|
|
28
|
+
const node_crypto_1 = require("node:crypto");
|
|
29
|
+
/** The default policy threshold a span must clear to count as a DETECTION (the
|
|
30
|
+
* same 0.8 the screen engine defaults to; a probe run may override it). */
|
|
31
|
+
exports.PROBE_DEFAULT_THRESHOLD = 0.8;
|
|
32
|
+
/** Canonical, offset-stable digest of a corpus's item SHAPES. A one-way hash of
|
|
33
|
+
* the fixture inputs — recorded so a published metric is verifiable against the
|
|
34
|
+
* exact corpus, WITHOUT the corpus content ever entering the record. */
|
|
35
|
+
function digestCorpus(items) {
|
|
36
|
+
const canonical = items.map((it) => [it.source, it.tier, it.positive ? 1 : 0, it.text]);
|
|
37
|
+
return `sha256:${(0, node_crypto_1.createHash)("sha256").update(JSON.stringify(canonical), "utf8").digest("hex")}`;
|
|
38
|
+
}
|
|
39
|
+
/** Did the detector FLAG this input? True iff it returns ≥1 span at or above the
|
|
40
|
+
* threshold. The spans (offsets/types) are consumed here and discarded — only
|
|
41
|
+
* the boolean survives into a count. */
|
|
42
|
+
async function flagged(detector, input, threshold) {
|
|
43
|
+
const spans = await detector.detect(input);
|
|
44
|
+
for (const s of spans)
|
|
45
|
+
if (s.confidence >= threshold)
|
|
46
|
+
return true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/** Evaluate one labelled corpus → its metric. Latency is summed from `clock`. */
|
|
50
|
+
async function evaluateCorpus(detector, corpus, threshold, clock) {
|
|
51
|
+
let positives = 0;
|
|
52
|
+
let negatives = 0;
|
|
53
|
+
let truePos = 0;
|
|
54
|
+
let falsePos = 0;
|
|
55
|
+
let latency = 0;
|
|
56
|
+
for (const item of corpus.items) {
|
|
57
|
+
const input = { text: item.text, source: item.source, tier: item.tier };
|
|
58
|
+
const t0 = clock();
|
|
59
|
+
const hit = await flagged(detector, input, threshold);
|
|
60
|
+
latency += clock() - t0;
|
|
61
|
+
if (item.positive) {
|
|
62
|
+
positives += 1;
|
|
63
|
+
if (hit)
|
|
64
|
+
truePos += 1;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
negatives += 1;
|
|
68
|
+
if (hit)
|
|
69
|
+
falsePos += 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
corpusId: corpus.id,
|
|
74
|
+
digest: digestCorpus(corpus.items),
|
|
75
|
+
license: corpus.license,
|
|
76
|
+
n: corpus.items.length,
|
|
77
|
+
positives,
|
|
78
|
+
negatives,
|
|
79
|
+
tpr: positives > 0 ? round(truePos / positives) : null,
|
|
80
|
+
fpr: negatives > 0 ? round(falsePos / negatives) : null,
|
|
81
|
+
latencyMs: Math.round(latency),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Evaluate the org's benign captures → the FP rate on your traffic. Any flag on
|
|
85
|
+
* a known-benign item is a false positive. */
|
|
86
|
+
async function evaluateBenign(detector, benign, threshold, now) {
|
|
87
|
+
if (benign.length === 0)
|
|
88
|
+
return { sampled: false, note: "no captures to sample" };
|
|
89
|
+
let falsePos = 0;
|
|
90
|
+
for (const item of benign) {
|
|
91
|
+
const input = { text: item.text, source: item.source, tier: item.tier };
|
|
92
|
+
if (await flagged(detector, input, threshold))
|
|
93
|
+
falsePos += 1;
|
|
94
|
+
}
|
|
95
|
+
return { sampled: true, n: benign.length, fpRate: round(falsePos / benign.length), sampledAt: now().toISOString() };
|
|
96
|
+
}
|
|
97
|
+
function round(n, dp = 4) {
|
|
98
|
+
const f = 10 ** dp;
|
|
99
|
+
return Math.round(n * f) / f;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Probe one detector over the labelled corpora + the org benign captures →
|
|
103
|
+
* `ProbeMeasured`. Corpora are evaluated in a STABLE id-sorted order so the
|
|
104
|
+
* record is reproducible on a fixed corpus. Metrics only — never a value.
|
|
105
|
+
*/
|
|
106
|
+
async function probeDetector(input) {
|
|
107
|
+
const threshold = input.threshold ?? exports.PROBE_DEFAULT_THRESHOLD;
|
|
108
|
+
const clock = input.clock ?? (() => performanceNow());
|
|
109
|
+
const now = input.now ?? (() => new Date());
|
|
110
|
+
const ordered = [...input.corpora].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
111
|
+
const perCorpus = [];
|
|
112
|
+
for (const corpus of ordered) {
|
|
113
|
+
perCorpus.push(await evaluateCorpus(input.detector, corpus, threshold, clock));
|
|
114
|
+
}
|
|
115
|
+
const orgBenign = await evaluateBenign(input.detector, input.benign ?? [], threshold, now);
|
|
116
|
+
return {
|
|
117
|
+
detectorId: input.detector.id,
|
|
118
|
+
detectorVersion: input.detectorVersion,
|
|
119
|
+
threshold,
|
|
120
|
+
perCorpus,
|
|
121
|
+
orgBenign,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Populate the reserved `DetectorRegistryEntry.measured` shape from a probe. */
|
|
125
|
+
function buildRegistryEntry(detector, measured) {
|
|
126
|
+
return { detector: { id: detector.id, kind: detector.kind }, measured };
|
|
127
|
+
}
|
|
128
|
+
/** performance.now with a Date fallback — never throws if perf is unavailable. */
|
|
129
|
+
function performanceNow() {
|
|
130
|
+
const perf = globalThis.performance;
|
|
131
|
+
return perf !== undefined ? perf.now() : Date.now();
|
|
132
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `modelrig screen …` (SC-1a, spec §9) — extracted from cli.ts to keep it under
|
|
3
|
+
* the 700-LOC cap (same discipline as mask-policy-cli.ts).
|
|
4
|
+
*
|
|
5
|
+
* modelrig screen check <file|-> dry-run: scan content with the T0 pattern
|
|
6
|
+
* pack and print COUNTS by type × source.
|
|
7
|
+
* NEVER prints a matched value (spec §1.4).
|
|
8
|
+
* modelrig screen pull <detector> resolve the LOCAL cache path for an optional
|
|
9
|
+
* classifier's weights and, with --url, fetch
|
|
10
|
+
* them there. ModelRig never bundles or
|
|
11
|
+
* re-hosts weights (A3).
|
|
12
|
+
*/
|
|
13
|
+
import type { Flags } from "../cli";
|
|
14
|
+
/** Dispatch `modelrig screen <subcommand>`. */
|
|
15
|
+
export declare function runScreenCli(flags: Flags): Promise<number>;
|
|
16
|
+
//# sourceMappingURL=screen-cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screen-cli.d.ts","sourceRoot":"","sources":["../../src/screen/screen-cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AA4JpC,+CAA+C;AAC/C,wBAAsB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAahE"}
|