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.
Files changed (245) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +4 -2
  3. package/dist/artifacts/artifact.d.ts +12 -13
  4. package/dist/artifacts/artifact.d.ts.map +1 -1
  5. package/dist/artifacts/artifact.js +17 -23
  6. package/dist/artifacts/index.d.ts +13 -5
  7. package/dist/artifacts/index.d.ts.map +1 -1
  8. package/dist/artifacts/index.js +11 -4
  9. package/dist/artifacts/run-context.d.ts +3 -24
  10. package/dist/artifacts/run-context.d.ts.map +1 -1
  11. package/dist/artifacts/run-context.js +3 -39
  12. package/dist/bakeoff/engine.d.ts +29 -0
  13. package/dist/bakeoff/engine.d.ts.map +1 -1
  14. package/dist/bakeoff/engine.js +47 -0
  15. package/dist/bakeoff/eval-cases-rig.d.ts +36 -0
  16. package/dist/bakeoff/eval-cases-rig.d.ts.map +1 -0
  17. package/dist/bakeoff/eval-cases-rig.js +27 -0
  18. package/dist/bakeoff/eval-cases.d.ts +19 -0
  19. package/dist/bakeoff/eval-cases.d.ts.map +1 -1
  20. package/dist/bakeoff/eval-cases.js +18 -1
  21. package/dist/bakeoff/golden-judge.d.ts +47 -0
  22. package/dist/bakeoff/golden-judge.d.ts.map +1 -0
  23. package/dist/bakeoff/golden-judge.js +49 -0
  24. package/dist/bakeoff/quality.d.ts +30 -0
  25. package/dist/bakeoff/quality.d.ts.map +1 -1
  26. package/dist/bakeoff/quality.js +21 -0
  27. package/dist/bakeoff/replay.d.ts +11 -0
  28. package/dist/bakeoff/replay.d.ts.map +1 -1
  29. package/dist/bakeoff/replay.js +8 -0
  30. package/dist/bakeoff/report.d.ts +7 -0
  31. package/dist/bakeoff/report.d.ts.map +1 -1
  32. package/dist/bakeoff/report.js +26 -0
  33. package/dist/cli-judge.d.ts +12 -0
  34. package/dist/cli-judge.d.ts.map +1 -0
  35. package/dist/cli-judge.js +48 -0
  36. package/dist/cli.d.ts.map +1 -1
  37. package/dist/cli.js +14 -42
  38. package/dist/config.d.ts +54 -29
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +86 -47
  41. package/dist/detect/detector.d.ts +4 -2
  42. package/dist/detect/detector.d.ts.map +1 -1
  43. package/dist/detect/safe-harbor.d.ts +57 -0
  44. package/dist/detect/safe-harbor.d.ts.map +1 -0
  45. package/dist/detect/safe-harbor.js +278 -0
  46. package/dist/feedback/coverage.d.ts +11 -8
  47. package/dist/feedback/coverage.d.ts.map +1 -1
  48. package/dist/feedback/coverage.js +29 -16
  49. package/dist/feedback/grade.d.ts +51 -0
  50. package/dist/feedback/grade.d.ts.map +1 -0
  51. package/dist/feedback/grade.js +63 -0
  52. package/dist/gateway/http-protocol.d.ts +1 -1
  53. package/dist/gateway/http-protocol.d.ts.map +1 -1
  54. package/dist/gateway/http-protocol.js +1 -1
  55. package/dist/index.d.ts +44 -27
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +110 -43
  58. package/dist/init/observe-cli.js +1 -1
  59. package/dist/init/observe.d.ts +2 -2
  60. package/dist/init/observe.d.ts.map +1 -1
  61. package/dist/init/observe.js +11 -7
  62. package/dist/mask/arm.d.ts +112 -0
  63. package/dist/mask/arm.d.ts.map +1 -0
  64. package/dist/mask/arm.js +179 -0
  65. package/dist/mask/index.d.ts +2 -0
  66. package/dist/mask/index.d.ts.map +1 -1
  67. package/dist/mask/index.js +7 -1
  68. package/dist/mask/mask-policy-cli.d.ts +12 -4
  69. package/dist/mask/mask-policy-cli.d.ts.map +1 -1
  70. package/dist/mask/mask-policy-cli.js +34 -9
  71. package/dist/query/grammar.d.ts +2 -2
  72. package/dist/query/grammar.d.ts.map +1 -1
  73. package/dist/query/grammar.js +3 -1
  74. package/dist/query/saved.d.ts +13 -0
  75. package/dist/query/saved.d.ts.map +1 -1
  76. package/dist/query/saved.js +67 -1
  77. package/dist/receipts/types.d.ts +1 -1
  78. package/dist/receipts/types.d.ts.map +1 -1
  79. package/dist/receipts/types.js +1 -0
  80. package/dist/registry/build.d.ts +43 -1
  81. package/dist/registry/build.d.ts.map +1 -1
  82. package/dist/registry/build.js +52 -0
  83. package/dist/registry/pricing-shape.d.ts +29 -0
  84. package/dist/registry/pricing-shape.d.ts.map +1 -1
  85. package/dist/registry/pricing-shape.js +10 -1
  86. package/dist/registry/rows.d.ts.map +1 -1
  87. package/dist/registry/rows.js +1 -0
  88. package/dist/registry/select.d.ts +6 -1
  89. package/dist/registry/select.d.ts.map +1 -1
  90. package/dist/registry/select.js +11 -0
  91. package/dist/routes/call-config.d.ts +199 -0
  92. package/dist/routes/call-config.d.ts.map +1 -0
  93. package/dist/routes/call-config.js +343 -0
  94. package/dist/routes/loader.js +5 -3
  95. package/dist/routes/objective-loader.d.ts +12 -0
  96. package/dist/routes/objective-loader.d.ts.map +1 -1
  97. package/dist/routes/objective-loader.js +82 -0
  98. package/dist/routes/resolved-bundle.d.ts +50 -0
  99. package/dist/routes/resolved-bundle.d.ts.map +1 -1
  100. package/dist/routes/resolved-bundle.js +52 -0
  101. package/dist/routes/screen-loader.d.ts +28 -0
  102. package/dist/routes/screen-loader.d.ts.map +1 -1
  103. package/dist/routes/screen-loader.js +51 -0
  104. package/dist/run/accept.d.ts +2 -0
  105. package/dist/run/accept.d.ts.map +1 -1
  106. package/dist/run/accept.js +1 -0
  107. package/dist/run/dispatch.d.ts +42 -13
  108. package/dist/run/dispatch.d.ts.map +1 -1
  109. package/dist/run/dispatch.js +107 -25
  110. package/dist/run/grounding.d.ts +28 -2
  111. package/dist/run/grounding.d.ts.map +1 -1
  112. package/dist/run/grounding.js +54 -10
  113. package/dist/run/record-attempt.d.ts +4 -0
  114. package/dist/run/record-attempt.d.ts.map +1 -1
  115. package/dist/run/record-attempt.js +4 -0
  116. package/dist/run/routing-docs.js +1 -1
  117. package/dist/run/run.d.ts.map +1 -1
  118. package/dist/run/run.js +21 -26
  119. package/dist/savings/odometer.d.ts +4 -0
  120. package/dist/savings/odometer.d.ts.map +1 -1
  121. package/dist/savings/odometer.js +20 -1
  122. package/dist/savings/swap-execute.d.ts +7 -0
  123. package/dist/savings/swap-execute.d.ts.map +1 -1
  124. package/dist/savings/swap-execute.js +30 -0
  125. package/dist/savings/swaps-client.d.ts +10 -0
  126. package/dist/savings/swaps-client.d.ts.map +1 -1
  127. package/dist/screen/fixtures/probe-corpora.d.ts +23 -0
  128. package/dist/screen/fixtures/probe-corpora.d.ts.map +1 -0
  129. package/dist/screen/fixtures/probe-corpora.js +69 -0
  130. package/dist/screen/index.d.ts +21 -0
  131. package/dist/screen/index.d.ts.map +1 -0
  132. package/dist/screen/index.js +64 -0
  133. package/dist/screen/model-screen.d.ts +111 -0
  134. package/dist/screen/model-screen.d.ts.map +1 -0
  135. package/dist/screen/model-screen.js +178 -0
  136. package/dist/screen/patterns.d.ts +51 -0
  137. package/dist/screen/patterns.d.ts.map +1 -0
  138. package/dist/screen/patterns.js +151 -0
  139. package/dist/screen/phi-gate.d.ts +52 -0
  140. package/dist/screen/phi-gate.d.ts.map +1 -0
  141. package/dist/screen/phi-gate.js +86 -0
  142. package/dist/screen/policy.d.ts +71 -0
  143. package/dist/screen/policy.d.ts.map +1 -0
  144. package/dist/screen/policy.js +334 -0
  145. package/dist/screen/probe.d.ts +118 -0
  146. package/dist/screen/probe.d.ts.map +1 -0
  147. package/dist/screen/probe.js +132 -0
  148. package/dist/screen/screen-cli.d.ts +16 -0
  149. package/dist/screen/screen-cli.d.ts.map +1 -0
  150. package/dist/screen/screen-cli.js +178 -0
  151. package/dist/screen/screen.d.ts +99 -0
  152. package/dist/screen/screen.d.ts.map +1 -0
  153. package/dist/screen/screen.js +0 -0
  154. package/dist/screen/sidecar.d.ts +69 -0
  155. package/dist/screen/sidecar.d.ts.map +1 -0
  156. package/dist/screen/sidecar.js +145 -0
  157. package/dist/screen/types.d.ts +43 -2
  158. package/dist/screen/types.d.ts.map +1 -1
  159. package/dist/settings/api-fetcher.d.ts +14 -3
  160. package/dist/settings/api-fetcher.d.ts.map +1 -1
  161. package/dist/settings/api-fetcher.js +61 -10
  162. package/dist/settings/grading-guide.d.ts +76 -0
  163. package/dist/settings/grading-guide.d.ts.map +1 -0
  164. package/dist/settings/grading-guide.js +23 -0
  165. package/dist/settings/index.d.ts +3 -2
  166. package/dist/settings/index.d.ts.map +1 -1
  167. package/dist/settings/index.js +2 -1
  168. package/dist/settings/task-settings.d.ts +44 -7
  169. package/dist/settings/task-settings.d.ts.map +1 -1
  170. package/dist/settings/task-settings.js +53 -12
  171. package/dist/status.d.ts +26 -20
  172. package/dist/status.d.ts.map +1 -1
  173. package/dist/status.js +47 -29
  174. package/dist/telemetry/artifact-store.d.ts +5 -6
  175. package/dist/telemetry/artifact-store.d.ts.map +1 -1
  176. package/dist/telemetry/artifact-store.js +10 -24
  177. package/dist/telemetry/attempt-meta.d.ts +21 -0
  178. package/dist/telemetry/attempt-meta.d.ts.map +1 -1
  179. package/dist/telemetry/attempt-meta.js +9 -0
  180. package/dist/telemetry/exporter.d.ts +2 -2
  181. package/dist/telemetry/exporter.d.ts.map +1 -1
  182. package/dist/telemetry/exporter.js +22 -41
  183. package/dist/telemetry/grade-mappers.d.ts +22 -0
  184. package/dist/telemetry/grade-mappers.d.ts.map +1 -0
  185. package/dist/telemetry/grade-mappers.js +58 -0
  186. package/dist/telemetry/grades-sql.d.ts +53 -0
  187. package/dist/telemetry/grades-sql.d.ts.map +1 -0
  188. package/dist/telemetry/grades-sql.js +133 -0
  189. package/dist/telemetry/ingest-protocol.d.ts +29 -46
  190. package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
  191. package/dist/telemetry/ingest-protocol.js +16 -54
  192. package/dist/telemetry/store-migrations.d.ts.map +1 -1
  193. package/dist/telemetry/store-migrations.js +51 -0
  194. package/dist/telemetry/store.d.ts +9 -6
  195. package/dist/telemetry/store.d.ts.map +1 -1
  196. package/dist/telemetry/store.js +8 -50
  197. package/dist/tenancy/api-key-format.d.ts +30 -1
  198. package/dist/tenancy/api-key-format.d.ts.map +1 -1
  199. package/dist/tenancy/api-key-format.js +33 -1
  200. package/dist/types/provider.d.ts +3 -0
  201. package/dist/types/provider.d.ts.map +1 -1
  202. package/dist/types/provider.js +25 -0
  203. package/dist/types/route.d.ts +78 -2
  204. package/dist/types/route.d.ts.map +1 -1
  205. package/dist/types/telemetry.d.ts +43 -46
  206. package/dist/types/telemetry.d.ts.map +1 -1
  207. package/dist/validate-cli.d.ts.map +1 -1
  208. package/dist/validate-cli.js +32 -0
  209. package/dist/verify/verify.d.ts +5 -0
  210. package/dist/verify/verify.d.ts.map +1 -1
  211. package/dist/verify/verify.js +2 -0
  212. package/dist/watch/evaluate.d.ts +84 -1
  213. package/dist/watch/evaluate.d.ts.map +1 -1
  214. package/dist/watch/evaluate.js +28 -0
  215. package/dist/watch/monitor.d.ts +8 -2
  216. package/dist/watch/monitor.d.ts.map +1 -1
  217. package/dist/watch/monitor.js +13 -5
  218. package/dist/watch/regret-monitor-cli.d.ts +16 -0
  219. package/dist/watch/regret-monitor-cli.d.ts.map +1 -0
  220. package/dist/watch/regret-monitor-cli.js +137 -0
  221. package/dist/watch/regret-monitor.d.ts +168 -0
  222. package/dist/watch/regret-monitor.d.ts.map +1 -0
  223. package/dist/watch/regret-monitor.js +304 -0
  224. package/dist/watch/upgrade.d.ts +62 -0
  225. package/dist/watch/upgrade.d.ts.map +1 -0
  226. package/dist/watch/upgrade.js +57 -0
  227. package/docs/agent-skill/SKILL.md +109 -5
  228. package/docs/how-it-fits.md +3 -2
  229. package/docs/instrumentation.md +149 -21
  230. package/docs/llms.txt +64 -20
  231. package/docs/mcp-oracle.md +249 -0
  232. package/docs/quickstart.md +95 -7
  233. package/package.json +16 -3
  234. package/registry/leaderboard.json +74 -0
  235. package/registry/registry.json +150 -112
  236. package/screen.d.ts +16 -0
  237. package/templates/call-disposition-qa.json +77 -0
  238. package/templates/catalog-cleansing.json +77 -0
  239. package/templates/compliance-review.json +76 -0
  240. package/templates/cs-next-action.json +76 -0
  241. package/templates/document-extraction.json +78 -0
  242. package/templates/financial-classification.json +75 -0
  243. package/templates/lead-qualification.json +76 -0
  244. package/templates/medical-classification.json +71 -0
  245. 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"}