@miller-tech/uap 2.1.7 → 2.11.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/config/capacity-policy.json +32 -0
- package/config/classify-thresholds.json +9 -0
- package/config/supervise-policy.json +13 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +87 -2
- package/dist/bin/cli.js.map +1 -1
- package/dist/capacity/policy.d.ts +67 -0
- package/dist/capacity/policy.d.ts.map +1 -0
- package/dist/capacity/policy.js +133 -0
- package/dist/capacity/policy.js.map +1 -0
- package/dist/capacity/probe.d.ts +23 -0
- package/dist/capacity/probe.d.ts.map +1 -0
- package/dist/capacity/probe.js +93 -0
- package/dist/capacity/probe.js.map +1 -0
- package/dist/classify/baseline.d.ts +24 -0
- package/dist/classify/baseline.d.ts.map +1 -0
- package/dist/classify/baseline.js +140 -0
- package/dist/classify/baseline.js.map +1 -0
- package/dist/classify/index.d.ts +30 -0
- package/dist/classify/index.d.ts.map +1 -0
- package/dist/classify/index.js +84 -0
- package/dist/classify/index.js.map +1 -0
- package/dist/classify/shadow.d.ts +34 -0
- package/dist/classify/shadow.d.ts.map +1 -0
- package/dist/classify/shadow.js +33 -0
- package/dist/classify/shadow.js.map +1 -0
- package/dist/classify/types.d.ts +64 -0
- package/dist/classify/types.d.ts.map +1 -0
- package/dist/classify/types.js +37 -0
- package/dist/classify/types.js.map +1 -0
- package/dist/cli/classify.d.ts +24 -0
- package/dist/cli/classify.d.ts.map +1 -0
- package/dist/cli/classify.js +94 -0
- package/dist/cli/classify.js.map +1 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +26 -0
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/doctor.d.ts +9 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +49 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/review.d.ts +12 -0
- package/dist/cli/review.d.ts.map +1 -0
- package/dist/cli/review.js +118 -0
- package/dist/cli/review.js.map +1 -0
- package/dist/cli/supervise.d.ts +8 -0
- package/dist/cli/supervise.d.ts.map +1 -0
- package/dist/cli/supervise.js +76 -0
- package/dist/cli/supervise.js.map +1 -0
- package/dist/delivery/convergence-loop.d.ts +8 -0
- package/dist/delivery/convergence-loop.d.ts.map +1 -1
- package/dist/delivery/convergence-loop.js +1 -0
- package/dist/delivery/convergence-loop.js.map +1 -1
- package/dist/delivery/delivery-result.d.ts.map +1 -1
- package/dist/delivery/delivery-result.js +5 -0
- package/dist/delivery/delivery-result.js.map +1 -1
- package/dist/delivery/gate-evidence.d.ts +143 -0
- package/dist/delivery/gate-evidence.d.ts.map +1 -0
- package/dist/delivery/gate-evidence.js +239 -0
- package/dist/delivery/gate-evidence.js.map +1 -0
- package/dist/delivery/index.d.ts +1 -0
- package/dist/delivery/index.d.ts.map +1 -1
- package/dist/delivery/index.js +1 -0
- package/dist/delivery/index.js.map +1 -1
- package/dist/evals/routing-corpus.d.ts +35 -0
- package/dist/evals/routing-corpus.d.ts.map +1 -0
- package/dist/evals/routing-corpus.js +142 -0
- package/dist/evals/routing-corpus.js.map +1 -0
- package/dist/evals/routing-eval.d.ts +104 -0
- package/dist/evals/routing-eval.d.ts.map +1 -0
- package/dist/evals/routing-eval.js +231 -0
- package/dist/evals/routing-eval.js.map +1 -0
- package/dist/evals/tfidf.d.ts +35 -0
- package/dist/evals/tfidf.d.ts.map +1 -0
- package/dist/evals/tfidf.js +132 -0
- package/dist/evals/tfidf.js.map +1 -0
- package/dist/review/prepass.d.ts +88 -0
- package/dist/review/prepass.d.ts.map +1 -0
- package/dist/review/prepass.js +428 -0
- package/dist/review/prepass.js.map +1 -0
- package/dist/review/visual-captures.d.ts +60 -0
- package/dist/review/visual-captures.d.ts.map +1 -0
- package/dist/review/visual-captures.js +222 -0
- package/dist/review/visual-captures.js.map +1 -0
- package/dist/supervise/assess.d.ts +73 -0
- package/dist/supervise/assess.d.ts.map +1 -0
- package/dist/supervise/assess.js +215 -0
- package/dist/supervise/assess.js.map +1 -0
- package/dist/supervise/config.d.ts +13 -0
- package/dist/supervise/config.d.ts.map +1 -0
- package/dist/supervise/config.js +81 -0
- package/dist/supervise/config.js.map +1 -0
- package/dist/supervise/loop.d.ts +78 -0
- package/dist/supervise/loop.d.ts.map +1 -0
- package/dist/supervise/loop.js +311 -0
- package/dist/supervise/loop.js.map +1 -0
- package/dist/supervise/observe.d.ts +31 -0
- package/dist/supervise/observe.d.ts.map +1 -0
- package/dist/supervise/observe.js +172 -0
- package/dist/supervise/observe.js.map +1 -0
- package/dist/supervise/policy.d.ts +16 -0
- package/dist/supervise/policy.d.ts.map +1 -0
- package/dist/supervise/policy.js +147 -0
- package/dist/supervise/policy.js.map +1 -0
- package/dist/supervise/types.d.ts +109 -0
- package/dist/supervise/types.d.ts.map +1 -0
- package/dist/supervise/types.js +11 -0
- package/dist/supervise/types.js.map +1 -0
- package/docs/INDEX.md +9 -3
- package/docs/architecture/adr/0001-automode-risk-floor.md +75 -0
- package/docs/architecture/adr/0002-evidence-bound-ship.md +81 -0
- package/docs/guides/CAPACITY_POLICY.md +98 -0
- package/docs/performance/automode-risk-calibration.md +72 -0
- package/docs/performance/reliability-ladder.md +78 -0
- package/docs/plans/system1-uplift-2026-09-18.md +191 -0
- package/docs/reference/automode-risk.md +134 -0
- package/docs/reference/evidence-bound-ship.md +145 -0
- package/docs/reference/review-prepass.md +77 -0
- package/docs/reference/routing-evals.md +71 -0
- package/docs/reference/semantic-supervisor.md +140 -0
- package/docs/reference/system1-classifier.md +103 -0
- package/docs/reference/visual-captures.md +113 -0
- package/package.json +5 -3
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
- package/src/policies/enforcers/enforcement_self_protect.py +25 -2
- package/src/policies/enforcers/evidence_bound_ship.py +257 -0
- package/src/policies/enforcers/expert_review_required.py +120 -0
- package/src/policies/schemas/policies/evidence-bound-ship.md +106 -0
- package/src/policies/schemas/policies/visual-verification.md +8 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +782 -19
- package/tools/agents/scripts/project_telemetry.py +52 -0
- package/tools/agents/scripts/tool_risk.py +789 -0
- package/tools/agents/scripts/tool_risk_calibrate.py +255 -0
- package/tools/agents/tests/fixtures/tool_risk_cases.json +124 -0
- package/tools/agents/tests/test_enforcer_escape_hatches.py +13 -0
- package/tools/agents/tests/test_gate_evidence.py +56 -0
- package/tools/agents/tests/test_tool_risk.py +521 -0
- package/tools/agents/tests/test_verbatim_compact.py +629 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capacity policy as code (uplift 0.5).
|
|
3
|
+
*
|
|
4
|
+
* Generalizes the 2026-09-18 llama.cpp OOM-fix discipline: every UAP-managed
|
|
5
|
+
* service declares its resource budget, its headroom requirement, and what
|
|
6
|
+
* GREEN/RED/DARK mean for it. `uap doctor` probes each declared service and
|
|
7
|
+
* reports violations. The policy is data (config/capacity-policy.json); the
|
|
8
|
+
* judgment is deterministic code; fail-open posture is per-probe (a missing
|
|
9
|
+
* systemctl/nvidia-smi yields UNKNOWN, never a fabricated GREEN).
|
|
10
|
+
*/
|
|
11
|
+
export class PolicyError extends Error {
|
|
12
|
+
}
|
|
13
|
+
/** systemd unit names: strict charset, never a leading dash. A name like
|
|
14
|
+
* "--host=x.service" ends in .service but is parsed by systemctl as a FLAG —
|
|
15
|
+
* --host would open an SSH connection to an attacker-chosen host (CWE-88).
|
|
16
|
+
* The probe also passes `--` before the unit; validation is the first wall. */
|
|
17
|
+
const UNIT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@-]*\.service$/;
|
|
18
|
+
/** Parse + validate a policy document. Malformed input throws loudly with
|
|
19
|
+
* the offending path — a capacity policy that half-loads is worse than none. */
|
|
20
|
+
export function parsePolicy(text, source = 'policy') {
|
|
21
|
+
let data;
|
|
22
|
+
try {
|
|
23
|
+
data = JSON.parse(text);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
throw new PolicyError(`${source}: invalid JSON — ${err.message}`);
|
|
27
|
+
}
|
|
28
|
+
const policy = data;
|
|
29
|
+
if (policy?.version !== 1)
|
|
30
|
+
throw new PolicyError(`${source}: "version": 1 required`);
|
|
31
|
+
if (!Array.isArray(policy.services) || policy.services.length === 0) {
|
|
32
|
+
throw new PolicyError(`${source}: "services" must be a non-empty array`);
|
|
33
|
+
}
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const [i, svc] of policy.services.entries()) {
|
|
36
|
+
const where = `${source} services[${i}]`;
|
|
37
|
+
if (!svc || typeof svc !== 'object')
|
|
38
|
+
throw new PolicyError(`${where}: must be an object`);
|
|
39
|
+
if (typeof svc.name !== 'string' || !svc.name.trim()) {
|
|
40
|
+
throw new PolicyError(`${where}: "name" is required`);
|
|
41
|
+
}
|
|
42
|
+
if (seen.has(svc.name))
|
|
43
|
+
throw new PolicyError(`${where}: duplicate service "${svc.name}"`);
|
|
44
|
+
seen.add(svc.name);
|
|
45
|
+
if (typeof svc.systemd?.unit !== 'string' || !UNIT_NAME_RE.test(svc.systemd.unit)) {
|
|
46
|
+
throw new PolicyError(`${where}: systemd.unit must match ${UNIT_NAME_RE} (no flags, whitespace, or leading dashes)`);
|
|
47
|
+
}
|
|
48
|
+
if (svc.systemd.scope !== 'user' && svc.systemd.scope !== 'system') {
|
|
49
|
+
throw new PolicyError(`${where}: systemd.scope must be "user" or "system"`);
|
|
50
|
+
}
|
|
51
|
+
for (const [section, keys] of [
|
|
52
|
+
['budget', ['vramMiB', 'rssMiB']],
|
|
53
|
+
['headroom', ['gpuMinFreeMiB']],
|
|
54
|
+
]) {
|
|
55
|
+
const block = svc[section];
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const v = block?.[key];
|
|
58
|
+
if (v !== undefined && (typeof v !== 'number' || !Number.isFinite(v) || v < 0)) {
|
|
59
|
+
throw new PolicyError(`${where}: ${section}.${key} must be a non-negative number`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const rb = svc.restartBudget;
|
|
64
|
+
if (rb !== undefined) {
|
|
65
|
+
for (const key of ['knownRestarts', 'allowedNew']) {
|
|
66
|
+
const v = rb[key];
|
|
67
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 0) {
|
|
68
|
+
throw new PolicyError(`${where}: restartBudget.${key} must be a non-negative integer`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return policy;
|
|
74
|
+
}
|
|
75
|
+
/** Pure health computation over a probed state — the deterministic core,
|
|
76
|
+
* fully testable without systemd or a GPU. */
|
|
77
|
+
export function computeHealth(svc, probed, probeAvailable) {
|
|
78
|
+
const reasons = [];
|
|
79
|
+
if (!probeAvailable.systemd) {
|
|
80
|
+
return { health: 'UNKNOWN', reasons: ['systemctl unavailable — cannot probe the unit'] };
|
|
81
|
+
}
|
|
82
|
+
const state = probed.activeState ?? 'unknown';
|
|
83
|
+
// DARK: not serving. The discipline from the OOM fix: a service that is
|
|
84
|
+
// down or crash-looping is not "degraded", it is absent.
|
|
85
|
+
if (state === 'failed' || state === 'inactive' || state === 'unknown') {
|
|
86
|
+
return { health: 'DARK', reasons: [`unit ${svc.systemd.unit} is ${state} — not serving`] };
|
|
87
|
+
}
|
|
88
|
+
// RED: activating (esp. subState=auto-restart) means the unit is NOT yet
|
|
89
|
+
// serving — a Restart=always crash loop sits exactly here. Down-looping is
|
|
90
|
+
// never GREEN, with or without a declared restartBudget.
|
|
91
|
+
if (state === 'activating' || probed.subState === 'auto-restart') {
|
|
92
|
+
return {
|
|
93
|
+
health: 'RED',
|
|
94
|
+
reasons: [`unit ${svc.systemd.unit} is ${state}/${probed.subState ?? '?'} — not yet serving (crash-loop candidate)`],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// RED: serving, but a budget is violated.
|
|
98
|
+
const rb = svc.restartBudget;
|
|
99
|
+
if (rb && probed.nRestarts !== undefined) {
|
|
100
|
+
const allowed = rb.knownRestarts + rb.allowedNew;
|
|
101
|
+
if (probed.nRestarts > allowed) {
|
|
102
|
+
reasons.push(`NRestarts=${probed.nRestarts} exceeds budget ${allowed} ` +
|
|
103
|
+
`(${rb.knownRestarts} known + ${rb.allowedNew} new) — new crashes since the baseline`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (svc.headroom?.gpuMinFreeMiB !== undefined) {
|
|
107
|
+
if (!probeAvailable.gpu || probed.gpuFreeMiB === undefined) {
|
|
108
|
+
reasons.push('gpu headroom declared but nvidia-smi unavailable — headroom unverified');
|
|
109
|
+
}
|
|
110
|
+
else if (probed.gpuFreeMiB < svc.headroom.gpuMinFreeMiB) {
|
|
111
|
+
reasons.push(`GPU free ${probed.gpuFreeMiB} MiB below required headroom ${svc.headroom.gpuMinFreeMiB} MiB — OOM territory`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// RSS budget: enforced against the unit's MemoryCurrent. Doctrine: a
|
|
115
|
+
// declared budget that cannot be verified is not healthy (RED), matching
|
|
116
|
+
// the unverified-headroom rule. budget.vramMiB is NOT compared against the
|
|
117
|
+
// process's total GPU footprint — it declares the service's own configured
|
|
118
|
+
// allocator limit (e.g. --vbr-vram), which the server enforces internally;
|
|
119
|
+
// the host-side invariant the doctor owns is gpuMinFreeMiB headroom.
|
|
120
|
+
if (svc.budget?.rssMiB !== undefined) {
|
|
121
|
+
if (probed.memoryCurrentMiB === undefined) {
|
|
122
|
+
reasons.push('rssMiB budget declared but MemoryCurrent unavailable — budget unverified');
|
|
123
|
+
}
|
|
124
|
+
else if (probed.memoryCurrentMiB > svc.budget.rssMiB) {
|
|
125
|
+
reasons.push(`RSS ${probed.memoryCurrentMiB} MiB exceeds declared budget ${svc.budget.rssMiB} MiB`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (reasons.length > 0)
|
|
129
|
+
return { health: 'RED', reasons };
|
|
130
|
+
reasons.push(`active (${probed.subState ?? 'running'}), all budgets inside policy`);
|
|
131
|
+
return { health: 'GREEN', reasons };
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/capacity/policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type CapacityPolicy, type Health, type ServiceReport } from './policy.js';
|
|
2
|
+
export type ProbeState = ServiceReport['probed'];
|
|
3
|
+
/** systemctl show <unit> for the declared scope. Undefined fields when the
|
|
4
|
+
* unit or systemctl is missing. `--` terminates option parsing so even a
|
|
5
|
+
* validated unit name is never reinterpreted as a flag. */
|
|
6
|
+
export declare function probeSystemd(unit: string, scope: 'user' | 'system'): ProbeState | null;
|
|
7
|
+
/** Free VRAM (MiB) on GPU 0 via nvidia-smi; null when no GPU/tool. */
|
|
8
|
+
export declare function probeGpuFreeMiB(): number | null;
|
|
9
|
+
/** Policy resolution: explicit path, project config/, then user config. */
|
|
10
|
+
export declare function loadPolicy(projectDir: string, explicitPath?: string): {
|
|
11
|
+
policy: CapacityPolicy;
|
|
12
|
+
path: string;
|
|
13
|
+
};
|
|
14
|
+
export interface DoctorDeps {
|
|
15
|
+
probeSystemd: typeof probeSystemd;
|
|
16
|
+
probeGpuFreeMiB: typeof probeGpuFreeMiB;
|
|
17
|
+
}
|
|
18
|
+
/** Probe every declared service and compute its health. Deps are injectable
|
|
19
|
+
* so the matrix is testable without a host. */
|
|
20
|
+
export declare function runDoctor(policy: CapacityPolicy, deps?: DoctorDeps): ServiceReport[];
|
|
21
|
+
/** Worst-health rollup for exit codes: DARK > RED > UNKNOWN > GREEN. */
|
|
22
|
+
export declare function worstHealth(reports: ServiceReport[]): Health;
|
|
23
|
+
//# sourceMappingURL=probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/capacity/probe.ts"],"names":[],"mappings":"AASA,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,UAAU,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;AAIjD;;2DAE2D;AAC3D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,IAAI,CA6BtF;AAED,sEAAsE;AACtE,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAa/C;AAED,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAgB9G;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,OAAO,YAAY,CAAC;IAClC,eAAe,EAAE,OAAO,eAAe,CAAC;CACzC;AAED;+CAC+C;AAC/C,wBAAgB,SAAS,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,GAAE,UAA8C,GAAG,aAAa,EAAE,CAYvH;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,MAAM,CAK5D"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host probes for the capacity doctor: systemd unit state and GPU headroom.
|
|
3
|
+
* execFileSync with argument arrays only (no shell); every probe fails soft
|
|
4
|
+
* to an unavailable flag — the health computation decides what that means.
|
|
5
|
+
*/
|
|
6
|
+
import { execFileSync } from 'child_process';
|
|
7
|
+
import { existsSync, readFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { parsePolicy, computeHealth, } from './policy.js';
|
|
11
|
+
const PROBE_TIMEOUT_MS = 5000; // a wedged driver/D-Bus must fail soft, not hang
|
|
12
|
+
/** systemctl show <unit> for the declared scope. Undefined fields when the
|
|
13
|
+
* unit or systemctl is missing. `--` terminates option parsing so even a
|
|
14
|
+
* validated unit name is never reinterpreted as a flag. */
|
|
15
|
+
export function probeSystemd(unit, scope) {
|
|
16
|
+
// Flags BEFORE `--`, unit AFTER it: `show unit -p ...` would make `-p` a
|
|
17
|
+
// unit name (systemctl then reports the real unit as inactive — false DARK).
|
|
18
|
+
const args = scope === 'user' ? ['--user', 'show'] : ['show'];
|
|
19
|
+
try {
|
|
20
|
+
const out = execFileSync('systemctl', [...args, '-p', 'ActiveState,SubState,NRestarts,MainPID,MemoryCurrent', '--', unit], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: PROBE_TIMEOUT_MS });
|
|
21
|
+
const props = Object.fromEntries(out.split('\n').filter(Boolean).map((l) => {
|
|
22
|
+
const eq = l.indexOf('=');
|
|
23
|
+
return [l.slice(0, eq), l.slice(eq + 1)];
|
|
24
|
+
}));
|
|
25
|
+
const restarts = Number(props.NRestarts);
|
|
26
|
+
const pid = Number(props.MainPID);
|
|
27
|
+
const mem = Number(props.MemoryCurrent); // bytes; "[not set]" → NaN
|
|
28
|
+
return {
|
|
29
|
+
activeState: props.ActiveState || 'unknown',
|
|
30
|
+
subState: props.SubState || undefined,
|
|
31
|
+
nRestarts: Number.isFinite(restarts) && props.NRestarts ? restarts : undefined,
|
|
32
|
+
mainPid: Number.isFinite(pid) && pid > 0 ? pid : undefined,
|
|
33
|
+
memoryCurrentMiB: Number.isFinite(mem) && mem > 0 ? Math.round(mem / 1048576) : undefined,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null; // no systemctl / timeout (container, macOS) — UNKNOWN, not GREEN
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Free VRAM (MiB) on GPU 0 via nvidia-smi; null when no GPU/tool. */
|
|
41
|
+
export function probeGpuFreeMiB() {
|
|
42
|
+
try {
|
|
43
|
+
const out = execFileSync('nvidia-smi', ['--query-gpu=memory.free', '--format=csv,noheader,nounits'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: PROBE_TIMEOUT_MS });
|
|
44
|
+
const first = out.split('\n')[0]?.trim();
|
|
45
|
+
const n = Number(first);
|
|
46
|
+
return Number.isFinite(n) ? n : null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Policy resolution: explicit path, project config/, then user config. */
|
|
53
|
+
export function loadPolicy(projectDir, explicitPath) {
|
|
54
|
+
const candidates = explicitPath
|
|
55
|
+
? [explicitPath]
|
|
56
|
+
: [
|
|
57
|
+
join(projectDir, 'config', 'capacity-policy.json'),
|
|
58
|
+
join(homedir(), '.config', 'uap', 'capacity-policy.json'),
|
|
59
|
+
];
|
|
60
|
+
for (const path of candidates) {
|
|
61
|
+
if (existsSync(path)) {
|
|
62
|
+
return { policy: parsePolicy(readFileSync(path, 'utf-8'), path), path };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`no capacity policy found (looked: ${candidates.join(', ')}) — ` +
|
|
66
|
+
'declare one at config/capacity-policy.json; see docs/guides/CAPACITY_POLICY.md');
|
|
67
|
+
}
|
|
68
|
+
/** Probe every declared service and compute its health. Deps are injectable
|
|
69
|
+
* so the matrix is testable without a host. */
|
|
70
|
+
export function runDoctor(policy, deps = { probeSystemd, probeGpuFreeMiB }) {
|
|
71
|
+
// One GPU probe per run, shared across services (nvidia-smi is not cheap).
|
|
72
|
+
const gpuFree = deps.probeGpuFreeMiB();
|
|
73
|
+
return policy.services.map((svc) => {
|
|
74
|
+
const sys = deps.probeSystemd(svc.systemd.unit, svc.systemd.scope);
|
|
75
|
+
const probed = { ...(sys ?? {}), gpuFreeMiB: gpuFree ?? undefined };
|
|
76
|
+
const { health, reasons } = computeHealth(svc, probed, {
|
|
77
|
+
systemd: sys !== null,
|
|
78
|
+
gpu: gpuFree !== null,
|
|
79
|
+
});
|
|
80
|
+
return { name: svc.name, health, reasons, probed };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Worst-health rollup for exit codes: DARK > RED > UNKNOWN > GREEN. */
|
|
84
|
+
export function worstHealth(reports) {
|
|
85
|
+
if (reports.some((r) => r.health === 'DARK'))
|
|
86
|
+
return 'DARK';
|
|
87
|
+
if (reports.some((r) => r.health === 'RED'))
|
|
88
|
+
return 'RED';
|
|
89
|
+
if (reports.some((r) => r.health === 'UNKNOWN'))
|
|
90
|
+
return 'UNKNOWN';
|
|
91
|
+
return 'GREEN';
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe.js","sourceRoot":"","sources":["../../src/capacity/probe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline System-1 backend: deterministic, CPU-only, dependency-free.
|
|
3
|
+
*
|
|
4
|
+
* Token-density scoring against per-question seed vocabularies, squashed
|
|
5
|
+
* through a logistic curve. It is deliberately simple: its job is to be the
|
|
6
|
+
* always-available local floor (airgap-pure, <50ms p95 trivially) and the
|
|
7
|
+
* shadow-mode reference that a trained embedding head must beat before
|
|
8
|
+
* promotion. Seeded from UAP's own incident vocabulary (the 2026-09-18 OOM
|
|
9
|
+
* timeline, gate outcomes, escalation telemetry language).
|
|
10
|
+
*
|
|
11
|
+
* v1 scope: the baseline answers ONLY the built-in questions (it keys its
|
|
12
|
+
* vocabulary off the question name). A custom question gets a ClassifyError,
|
|
13
|
+
* not a silently mis-scoped answer — the trained head is the generalizer.
|
|
14
|
+
*/
|
|
15
|
+
import { AssessResult, ClassifierBackend, Question } from './types.js';
|
|
16
|
+
export declare class BaselineBackend implements ClassifierBackend {
|
|
17
|
+
readonly name = "baseline-tfidf-v1";
|
|
18
|
+
assess(state: string, questions: Question[]): AssessResult;
|
|
19
|
+
private answer;
|
|
20
|
+
private noul;
|
|
21
|
+
private score;
|
|
22
|
+
private choice;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=baseline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../../src/classify/baseline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAEL,YAAY,EACZ,iBAAiB,EAGjB,QAAQ,EACT,MAAM,YAAY,CAAC;AA+CpB,qBAAa,eAAgB,YAAW,iBAAiB;IACvD,QAAQ,CAAC,IAAI,uBAAuB;IAEpC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,YAAY;IAS1D,OAAO,CAAC,MAAM;IAmBd,OAAO,CAAC,IAAI;IAcZ,OAAO,CAAC,KAAK;IAUb,OAAO,CAAC,MAAM;CAmCf"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline System-1 backend: deterministic, CPU-only, dependency-free.
|
|
3
|
+
*
|
|
4
|
+
* Token-density scoring against per-question seed vocabularies, squashed
|
|
5
|
+
* through a logistic curve. It is deliberately simple: its job is to be the
|
|
6
|
+
* always-available local floor (airgap-pure, <50ms p95 trivially) and the
|
|
7
|
+
* shadow-mode reference that a trained embedding head must beat before
|
|
8
|
+
* promotion. Seeded from UAP's own incident vocabulary (the 2026-09-18 OOM
|
|
9
|
+
* timeline, gate outcomes, escalation telemetry language).
|
|
10
|
+
*
|
|
11
|
+
* v1 scope: the baseline answers ONLY the built-in questions (it keys its
|
|
12
|
+
* vocabulary off the question name). A custom question gets a ClassifyError,
|
|
13
|
+
* not a silently mis-scoped answer — the trained head is the generalizer.
|
|
14
|
+
*/
|
|
15
|
+
import { ClassifyError, BUILTIN_QUESTIONS, } from './types.js';
|
|
16
|
+
/** Tokens that mark elevated escalation risk in a state description. */
|
|
17
|
+
const ESCALATION_TOKENS = [
|
|
18
|
+
'error', 'failed', 'failure', 'crash', 'oom', 'sigabrt', 'killed', 'stuck',
|
|
19
|
+
'loop', 'looping', 'restarting', 'timeout', 'timed', 'denied', 'refused',
|
|
20
|
+
'corrupt', 'unrecoverable', 'exception', 'fatal', 'panic', 'deadlock',
|
|
21
|
+
'blocked', 'cannot', 'unable', 'unknown', 'missing', 'broken', 'regression',
|
|
22
|
+
'conflict', 'diverged', 'mismatch', 'unexpected', 'anomaly', 'leak',
|
|
23
|
+
];
|
|
24
|
+
/** Tokens that mark destructive/irreversible operations (task-risk score). */
|
|
25
|
+
const DESTRUCTIVE_TOKENS = [
|
|
26
|
+
'delete', 'remove', 'rm', 'drop', 'truncate', 'destroy', 'wipe', 'purge',
|
|
27
|
+
'overwrite', 'reset', 'force', 'push', 'deploy', 'migrate', 'production',
|
|
28
|
+
'secrets', 'credentials', 'rotate', 'revoke', 'sudo', 'chmod', 'chown',
|
|
29
|
+
];
|
|
30
|
+
const STOP = new Set([
|
|
31
|
+
'the', 'a', 'an', 'and', 'or', 'of', 'to', 'in', 'on', 'for', 'with',
|
|
32
|
+
'is', 'are', 'was', 'were', 'be', 'been', 'it', 'this', 'that', 'at',
|
|
33
|
+
'by', 'as', 'from', 'not', 'no', 'we', 'i', 'you', 'they',
|
|
34
|
+
]);
|
|
35
|
+
function tokenize(text) {
|
|
36
|
+
return text
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.split(/[^a-z0-9]+/)
|
|
39
|
+
.filter((t) => t.length > 1 && !STOP.has(t));
|
|
40
|
+
}
|
|
41
|
+
/** Sigmoid around a calibrated midpoint. */
|
|
42
|
+
function sigmoid(x) {
|
|
43
|
+
return 1 / (1 + Math.exp(-x));
|
|
44
|
+
}
|
|
45
|
+
/** hits / sqrt(length): sub-linear length normalization — long clean prose
|
|
46
|
+
* dilutes a few keyword hits, but a wall of repeated keywords still
|
|
47
|
+
* saturates (numerator grows linearly). Saturation resistance, if wanted,
|
|
48
|
+
* must come from the calibrated head, not from this ratio. */
|
|
49
|
+
function hitRatio(tokens, vocab) {
|
|
50
|
+
if (tokens.length === 0)
|
|
51
|
+
return 0;
|
|
52
|
+
const set = new Set(vocab);
|
|
53
|
+
const hits = tokens.filter((t) => set.has(t)).length;
|
|
54
|
+
return hits / Math.sqrt(tokens.length);
|
|
55
|
+
}
|
|
56
|
+
export class BaselineBackend {
|
|
57
|
+
name = 'baseline-tfidf-v1';
|
|
58
|
+
assess(state, questions) {
|
|
59
|
+
const tokens = tokenize(state);
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const q of questions) {
|
|
62
|
+
out[q.name] = this.answer(q, tokens);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
answer(q, tokens) {
|
|
67
|
+
// Question-blind scoring would silently answer a custom noul question
|
|
68
|
+
// with the escalation vocabulary — refuse instead (the trained head is
|
|
69
|
+
// the generalizer; see module header).
|
|
70
|
+
if (!BUILTIN_QUESTIONS.some((b) => b.name === q.name)) {
|
|
71
|
+
throw new ClassifyError(`baseline backend answers only the built-in questions; got "${q.name}"`);
|
|
72
|
+
}
|
|
73
|
+
switch (q.kind) {
|
|
74
|
+
case 'noul':
|
|
75
|
+
return this.noul(tokens);
|
|
76
|
+
case 'score':
|
|
77
|
+
return this.score(tokens);
|
|
78
|
+
case 'choice':
|
|
79
|
+
return this.choice(q, tokens);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
noul(tokens) {
|
|
83
|
+
const r = hitRatio(tokens, ESCALATION_TOKENS);
|
|
84
|
+
// Midpoint ~0.35 ratio → p 0.5; steep enough to separate clean states.
|
|
85
|
+
const probability = sigmoid((r - 0.35) * 8);
|
|
86
|
+
// Confidence scales with evidence mass; empty states are low-confidence.
|
|
87
|
+
const confidence = Math.min(1, tokens.length / 40) * (0.5 + Math.abs(probability - 0.5));
|
|
88
|
+
return {
|
|
89
|
+
value: probability >= 0.5,
|
|
90
|
+
probability,
|
|
91
|
+
confidence,
|
|
92
|
+
backend: this.name,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
score(tokens) {
|
|
96
|
+
const r = hitRatio(tokens, DESTRUCTIVE_TOKENS);
|
|
97
|
+
// Map 0..~0.8 ratio onto 1..5 with a soft curve.
|
|
98
|
+
const raw = 1 + 4 * Math.min(1, r / 0.8);
|
|
99
|
+
const value = Math.round(raw);
|
|
100
|
+
const probability = 1 - Math.min(1, Math.abs(raw - value) * 2);
|
|
101
|
+
const confidence = Math.min(1, tokens.length / 40);
|
|
102
|
+
return { value, probability, confidence, backend: this.name };
|
|
103
|
+
}
|
|
104
|
+
choice(q, tokens) {
|
|
105
|
+
const options = q.options ?? [];
|
|
106
|
+
if (options.length === 0) {
|
|
107
|
+
return { value: '', probability: 0, confidence: 0, backend: this.name };
|
|
108
|
+
}
|
|
109
|
+
// Deterministic scoring: 'escalate'/'stop' track escalation + destructive
|
|
110
|
+
// density, 'verify-first' tracks moderate signals, 'proceed' is the rest.
|
|
111
|
+
const esc = hitRatio(tokens, ESCALATION_TOKENS);
|
|
112
|
+
const dst = hitRatio(tokens, DESTRUCTIVE_TOKENS);
|
|
113
|
+
const scores = {
|
|
114
|
+
escalate: 2 * esc + dst,
|
|
115
|
+
// stop must beat escalate on pure-destructive-no-incident states, so
|
|
116
|
+
// it gets an escalation-independent margin (escalate >= dst always).
|
|
117
|
+
stop: dst > 0.6 && esc < 0.2 ? 2 * dst + 0.1 : 0,
|
|
118
|
+
'verify-first': esc > 0.1 && esc < 0.5 ? 0.4 : 0.1,
|
|
119
|
+
proceed: Math.max(0.05, 0.5 - esc - dst),
|
|
120
|
+
};
|
|
121
|
+
let best = options[0];
|
|
122
|
+
let bestScore = -Infinity;
|
|
123
|
+
let total = 0;
|
|
124
|
+
for (const opt of options) {
|
|
125
|
+
const s = scores[opt] ?? 0.05;
|
|
126
|
+
total += s;
|
|
127
|
+
if (s > bestScore) {
|
|
128
|
+
bestScore = s;
|
|
129
|
+
best = opt;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
value: best,
|
|
134
|
+
probability: total > 0 ? bestScore / total : 1 / options.length,
|
|
135
|
+
confidence: Math.min(1, tokens.length / 40),
|
|
136
|
+
backend: this.name,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/classify/baseline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifier entry point: backend selection + threshold application.
|
|
3
|
+
*
|
|
4
|
+
* Backend chain (first available wins):
|
|
5
|
+
* 1. trained head at ~/.config/uap/classifier-head.json (future; §6 v2)
|
|
6
|
+
* 2. baseline-tfidf-v1 (always available, airgap-pure)
|
|
7
|
+
* Thresholds come from reviewed policy config (config/classify-thresholds.json
|
|
8
|
+
* in the project, or ~/.config/uap/), never from model output.
|
|
9
|
+
*/
|
|
10
|
+
import { AssessResult, ClassifierBackend, Question, QuestionThresholds } from './types.js';
|
|
11
|
+
export * from './types.js';
|
|
12
|
+
export { BaselineBackend } from './baseline.js';
|
|
13
|
+
export { recordShadow, shadowLogPath, type ShadowRecord } from './shadow.js';
|
|
14
|
+
export declare function defaultBackend(): ClassifierBackend;
|
|
15
|
+
export declare function thresholdPaths(projectDir: string): string[];
|
|
16
|
+
export declare function loadThresholds(projectDir: string, explicitPath?: string): Record<string, QuestionThresholds>;
|
|
17
|
+
export interface DecidedAssessment {
|
|
18
|
+
name: string;
|
|
19
|
+
value: AssessResult[string]['value'];
|
|
20
|
+
probability: number;
|
|
21
|
+
confidence: number;
|
|
22
|
+
/** True when confidence < floor: the caller must defer to its heuristic. */
|
|
23
|
+
defer: boolean;
|
|
24
|
+
backend: string;
|
|
25
|
+
}
|
|
26
|
+
/** Assess + apply reviewed thresholds. The returned `defer` flag is the
|
|
27
|
+
* shadow-mode contract: below the confidence floor, the classifier advises
|
|
28
|
+
* but the heuristic decides. */
|
|
29
|
+
export declare function assessWithThresholds(state: string, questions: Question[], thresholds: Record<string, QuestionThresholds>, backend?: ClassifierBackend): DecidedAssessment[];
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/classify/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,EACL,YAAY,EACZ,iBAAiB,EAGjB,QAAQ,EACR,kBAAkB,EAEnB,MAAM,YAAY,CAAC;AAEpB,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7E,wBAAgB,cAAc,IAAI,iBAAiB,CAKlD;AAED,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAK3D;AAED,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CA4BpC;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;gCAEgC;AAChC,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,QAAQ,EAAE,EACrB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,EAC9C,OAAO,GAAE,iBAAoC,GAC5C,iBAAiB,EAAE,CAqBrB"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifier entry point: backend selection + threshold application.
|
|
3
|
+
*
|
|
4
|
+
* Backend chain (first available wins):
|
|
5
|
+
* 1. trained head at ~/.config/uap/classifier-head.json (future; §6 v2)
|
|
6
|
+
* 2. baseline-tfidf-v1 (always available, airgap-pure)
|
|
7
|
+
* Thresholds come from reviewed policy config (config/classify-thresholds.json
|
|
8
|
+
* in the project, or ~/.config/uap/), never from model output.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { BaselineBackend } from './baseline.js';
|
|
14
|
+
import { ClassifyError, DEFAULT_THRESHOLDS, } from './types.js';
|
|
15
|
+
export * from './types.js';
|
|
16
|
+
export { BaselineBackend } from './baseline.js';
|
|
17
|
+
export { recordShadow, shadowLogPath } from './shadow.js';
|
|
18
|
+
export function defaultBackend() {
|
|
19
|
+
// v1: the trained-head slot exists in the chain but no head format is
|
|
20
|
+
// committed yet — landing small, one dimension end-to-end. When a head
|
|
21
|
+
// ships it inserts here without touching consumers.
|
|
22
|
+
return new BaselineBackend();
|
|
23
|
+
}
|
|
24
|
+
export function thresholdPaths(projectDir) {
|
|
25
|
+
return [
|
|
26
|
+
join(projectDir, 'config', 'classify-thresholds.json'),
|
|
27
|
+
join(homedir(), '.config', 'uap', 'classify-thresholds.json'),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
export function loadThresholds(projectDir, explicitPath) {
|
|
31
|
+
const candidates = explicitPath ? [explicitPath] : thresholdPaths(projectDir);
|
|
32
|
+
for (const p of candidates) {
|
|
33
|
+
if (!existsSync(p))
|
|
34
|
+
continue;
|
|
35
|
+
let cfg;
|
|
36
|
+
try {
|
|
37
|
+
cfg = JSON.parse(readFileSync(p, 'utf-8'));
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
throw new ClassifyError(`threshold config at ${p} is not valid JSON: ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
if (cfg.version !== 1 || typeof cfg.questions !== 'object' || cfg.questions === null) {
|
|
43
|
+
throw new ClassifyError(`threshold config at ${p}: expected {version: 1, questions: {...}}`);
|
|
44
|
+
}
|
|
45
|
+
for (const [name, t] of Object.entries(cfg.questions)) {
|
|
46
|
+
const tauOk = t.tau === undefined || (Number.isFinite(t.tau) && t.tau >= 0 && t.tau <= 1);
|
|
47
|
+
const floorOk = Number.isFinite(t.confidenceFloor) && t.confidenceFloor >= 0 && t.confidenceFloor <= 1;
|
|
48
|
+
if (!tauOk || !floorOk) {
|
|
49
|
+
throw new ClassifyError(`threshold config at ${p}: questions.${name} needs confidenceFloor (and optional tau) in [0,1]`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return cfg.questions;
|
|
53
|
+
}
|
|
54
|
+
if (explicitPath) {
|
|
55
|
+
throw new ClassifyError(`threshold config not found: ${explicitPath}`);
|
|
56
|
+
}
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
/** Assess + apply reviewed thresholds. The returned `defer` flag is the
|
|
60
|
+
* shadow-mode contract: below the confidence floor, the classifier advises
|
|
61
|
+
* but the heuristic decides. */
|
|
62
|
+
export function assessWithThresholds(state, questions, thresholds, backend = defaultBackend()) {
|
|
63
|
+
const result = backend.assess(state, questions);
|
|
64
|
+
return questions.map((q) => {
|
|
65
|
+
const a = result[q.name];
|
|
66
|
+
if (!a) {
|
|
67
|
+
throw new ClassifyError(`backend "${backend.name}" returned no assessment for "${q.name}"`);
|
|
68
|
+
}
|
|
69
|
+
const t = thresholds[q.name] ?? DEFAULT_THRESHOLDS;
|
|
70
|
+
let value = a.value;
|
|
71
|
+
if (q.kind === 'noul') {
|
|
72
|
+
value = a.probability >= (t.tau ?? DEFAULT_THRESHOLDS.tau);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
name: q.name,
|
|
76
|
+
value,
|
|
77
|
+
probability: a.probability,
|
|
78
|
+
confidence: a.confidence,
|
|
79
|
+
defer: a.confidence < t.confidenceFloor,
|
|
80
|
+
backend: a.backend,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/classify/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow-mode logging (uplift §6 risk mitigation): the classifier assesses,
|
|
3
|
+
* heuristics decide, divergence is logged. Promotion to active only after
|
|
4
|
+
* measured agreement.
|
|
5
|
+
*
|
|
6
|
+
* Privacy: the raw state text is NEVER persisted — only a content hash, the
|
|
7
|
+
* word count, and the assessment (no raw-text surface; note the hash is
|
|
8
|
+
* equality-revealing for low-entropy boilerplate states — acceptable for a
|
|
9
|
+
* local-only join key). Calibration replays join outcome labels by hash;
|
|
10
|
+
* the text itself stays in the session where it was produced. The log is
|
|
11
|
+
* local-only (.uap/, git-ignored, mode 0600).
|
|
12
|
+
*/
|
|
13
|
+
import { AssessResult, Question } from './types.js';
|
|
14
|
+
export interface ShadowRecord {
|
|
15
|
+
/** Schema version — the first calibration replay will want one. */
|
|
16
|
+
v: 1;
|
|
17
|
+
ts: string;
|
|
18
|
+
/** SHA-256 of the state text — join key for later outcome labels. */
|
|
19
|
+
stateHash: string;
|
|
20
|
+
wordCount: number;
|
|
21
|
+
questions: string[];
|
|
22
|
+
result: AssessResult;
|
|
23
|
+
/** The heuristic decision that actually fired while the classifier was in
|
|
24
|
+
* shadow — the divergence the promotion gate measures. Optional because
|
|
25
|
+
* the CLI's ad-hoc --shadow has no heuristic in the loop; Wave 1 consumers
|
|
26
|
+
* (supervisor, AutoMode) must populate it. */
|
|
27
|
+
heuristicDecision?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
export declare function shadowLogPath(projectDir: string): string;
|
|
30
|
+
export declare function recordShadow(projectDir: string, state: string, questions: Question[], result: AssessResult, opts?: {
|
|
31
|
+
logPath?: string;
|
|
32
|
+
heuristicDecision?: Record<string, unknown>;
|
|
33
|
+
}): void;
|
|
34
|
+
//# sourceMappingURL=shadow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../src/classify/shadow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,CAAC,EAAE,CAAC,CAAC;IACL,EAAE,EAAE,MAAM,CAAC;IACX,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB;;;kDAG8C;IAC9C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7C;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,YAAY,CAC1B,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,QAAQ,EAAE,EACrB,MAAM,EAAE,YAAY,EACpB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAO,GAC3E,IAAI,CAaN"}
|