@ngockhoale/ukit 3.0.5 → 3.0.7
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 +17 -0
- package/package.json +1 -1
- package/scripts/bench/data-foundation.mjs +562 -0
- package/src/core/observability/adapters/common.js +75 -0
- package/src/core/observability/adapters/contextAdapter.js +55 -0
- package/src/core/observability/adapters/decisionAdapter.js +61 -0
- package/src/core/observability/adapters/routeAdapter.js +135 -0
- package/src/core/observability/analytics/digest.js +186 -0
- package/src/core/observability/analytics/fingerprints.js +126 -0
- package/src/core/observability/analytics/opportunities.js +329 -0
- package/src/core/observability/analytics/rebuild.js +56 -0
- package/src/core/observability/analytics/summary.js +298 -0
- package/src/core/observability/emit/config.js +29 -0
- package/src/core/observability/emit/recorder.js +297 -0
- package/src/core/observability/evaluation/aiPacket.js +230 -0
- package/src/core/observability/evaluation/optimizationKnowledge.js +172 -0
- package/src/core/observability/evaluation/replay.js +143 -0
- package/src/core/observability/evaluation/scorecard.js +445 -0
- package/src/core/observability/privacy/allowlist.js +185 -0
- package/src/core/observability/privacy/redaction.js +113 -0
- package/src/core/observability/privacy/sanitizeForSupport.js +133 -0
- package/src/core/observability/privacy/sanitizeObserved.js +134 -0
- package/src/core/observability/rollout.js +155 -0
- package/src/core/observability/schema/constants.js +66 -0
- package/src/core/observability/schema/registry.js +223 -0
- package/src/core/observability/schema/validate.js +227 -0
- package/src/core/observability/segments/internal.js +241 -0
- package/src/core/observability/segments/readSegments.js +215 -0
- package/src/core/observability/segments/recovery.js +123 -0
- package/src/core/observability/segments/retention.js +381 -0
- package/src/core/observability/support/import.js +402 -0
- package/src/core/observability/support/manifest.js +135 -0
- package/src/core/observability/support/paths.js +94 -0
- package/src/core/observability/support/projector.js +483 -0
- package/src/core/observability/support/renderer.js +130 -0
- package/src/core/observability/support/retention.js +155 -0
- package/src/core/runtimeConfig.js +6 -3
- package/src/decision/client.js +11 -3
- package/template_project/.claude/ukit/index/unic-decision.mjs +6 -2
- package/template_project/.omp/RULES.md +6 -6
- package/template_project/.omp/config.yml +6 -0
- package/template_project/docs/UKIT_INTERNALS.md +9 -0
- package/template_project/instructions/overlays/omp-rules.md +6 -6
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* retention.js (TASK-009, SPEC §5 DF-FR10) — support-view cap/age pruning.
|
|
3
|
+
*
|
|
4
|
+
* pruneSupportView(dir, policy?) → { ok: true, removed: string[], bytes }
|
|
5
|
+
* | { ok: false, reason }
|
|
6
|
+
*
|
|
7
|
+
* This is the retention owner for the user-facing `UKit Support` folder —
|
|
8
|
+
* deliberately SEPARATE from segments/retention.js, which governs the
|
|
9
|
+
* private canonical store. The two stores have different caps, different
|
|
10
|
+
* audiences, and different deletion safety rules.
|
|
11
|
+
*
|
|
12
|
+
* Ownership rule (the safety boundary): a file inside the support dir is
|
|
13
|
+
* prunable only when it is a regular file AND
|
|
14
|
+
* - listed in the current manifest.json `files` map, or
|
|
15
|
+
* - matches the digest naming convention `digest-<16 hex>.md`.
|
|
16
|
+
* Symlinks, directories, foreign-named files, and — when no manifest
|
|
17
|
+
* exists — static-named files are never touched. User-owned contents
|
|
18
|
+
* survive pruning unconditionally.
|
|
19
|
+
*
|
|
20
|
+
* Eviction order under the byte cap mirrors the projector's priority:
|
|
21
|
+
* digests first (oldest mtime first), then records.jsonl, then SUMMARY.md,
|
|
22
|
+
* README.md, and manifest.json last — core self-description outlives
|
|
23
|
+
* derived content.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
|
|
29
|
+
import { ioReason } from '../segments/internal.js';
|
|
30
|
+
|
|
31
|
+
export const STATIC_OWNED_NAMES = new Set([
|
|
32
|
+
'README.md',
|
|
33
|
+
'SUMMARY.md',
|
|
34
|
+
'manifest.json',
|
|
35
|
+
'records.jsonl',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export const DIGEST_NAME_RE = /^digest-[0-9a-f]{16}\.md$/;
|
|
39
|
+
|
|
40
|
+
/** Names this module may ever treat as owned. */
|
|
41
|
+
export function isOwnedName(name) {
|
|
42
|
+
return STATIC_OWNED_NAMES.has(name) || DIGEST_NAME_RE.test(name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const DEFAULT_SUPPORT_POLICY = Object.freeze({
|
|
46
|
+
maxBytes: 8 * 1024 * 1024,
|
|
47
|
+
maxAgeMs: 30 * 24 * 60 * 60 * 1000,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function isPlainObject(value) {
|
|
51
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readManifestFiles(dir) {
|
|
55
|
+
try {
|
|
56
|
+
const raw = await fs.promises.readFile(path.join(dir, 'manifest.json'), 'utf8');
|
|
57
|
+
const manifest = JSON.parse(raw);
|
|
58
|
+
if (isPlainObject(manifest) && isPlainObject(manifest.files)) {
|
|
59
|
+
return new Set(Object.keys(manifest.files));
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// missing/corrupt manifest → only convention-named digests stay owned
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Eviction priority: lower number = evicted sooner. */
|
|
68
|
+
function evictionRank(name) {
|
|
69
|
+
if (DIGEST_NAME_RE.test(name)) return 0;
|
|
70
|
+
if (name === 'records.jsonl') return 1;
|
|
71
|
+
if (name === 'SUMMARY.md') return 2;
|
|
72
|
+
if (name === 'README.md') return 3;
|
|
73
|
+
return 4; // manifest.json and anything else owned
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} dir resolved support directory
|
|
78
|
+
* @param {{ maxBytes?: number, maxAgeMs?: number, now?: number }} [policy]
|
|
79
|
+
*/
|
|
80
|
+
export async function pruneSupportView(dir, policy = {}) {
|
|
81
|
+
const maxBytes = Number.isFinite(policy.maxBytes) ? policy.maxBytes : DEFAULT_SUPPORT_POLICY.maxBytes;
|
|
82
|
+
const maxAgeMs = Number.isFinite(policy.maxAgeMs) ? policy.maxAgeMs : DEFAULT_SUPPORT_POLICY.maxAgeMs;
|
|
83
|
+
const now = Number.isFinite(policy.now) ? policy.now : Date.now();
|
|
84
|
+
|
|
85
|
+
let dirStat;
|
|
86
|
+
try {
|
|
87
|
+
dirStat = await fs.promises.lstat(dir);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err && err.code === 'ENOENT') return { ok: true, removed: [], bytes: 0 };
|
|
90
|
+
return { ok: false, reason: ioReason(err) };
|
|
91
|
+
}
|
|
92
|
+
if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) {
|
|
93
|
+
return { ok: false, reason: 'support_path_not_directory' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const manifestOwned = await readManifestFiles(dir);
|
|
97
|
+
|
|
98
|
+
let entries;
|
|
99
|
+
try {
|
|
100
|
+
entries = await fs.promises.readdir(dir);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
return { ok: false, reason: ioReason(err) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Snapshot owned regular files with stats. Never follow symlinks.
|
|
106
|
+
const owned = [];
|
|
107
|
+
for (const name of entries) {
|
|
108
|
+
const ownedByManifest = manifestOwned !== null && manifestOwned.has(name);
|
|
109
|
+
const ownedByConvention = DIGEST_NAME_RE.test(name);
|
|
110
|
+
if (!ownedByManifest && !ownedByConvention) continue;
|
|
111
|
+
let st;
|
|
112
|
+
try {
|
|
113
|
+
st = await fs.promises.lstat(path.join(dir, name));
|
|
114
|
+
} catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!st.isFile()) continue; // symlink/dir at an owned name → not ours to touch
|
|
118
|
+
owned.push({ name, size: st.size, mtimeMs: st.mtimeMs });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const removed = [];
|
|
122
|
+
const remove = async (entry) => {
|
|
123
|
+
try {
|
|
124
|
+
await fs.promises.rm(path.join(dir, entry.name), { force: true });
|
|
125
|
+
removed.push(entry.name);
|
|
126
|
+
return true;
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Age pass: owned files past the age cap go first.
|
|
133
|
+
const survivors = [];
|
|
134
|
+
for (const entry of owned) {
|
|
135
|
+
if (now - entry.mtimeMs > maxAgeMs) {
|
|
136
|
+
await remove(entry);
|
|
137
|
+
} else {
|
|
138
|
+
survivors.push(entry);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Cap pass: evict lowest-priority owned content until under maxBytes.
|
|
143
|
+
let total = survivors.reduce((sum, e) => sum + e.size, 0);
|
|
144
|
+
if (total > maxBytes) {
|
|
145
|
+
const order = survivors
|
|
146
|
+
.slice()
|
|
147
|
+
.sort((a, b) => evictionRank(a.name) - evictionRank(b.name) || a.mtimeMs - b.mtimeMs);
|
|
148
|
+
for (const entry of order) {
|
|
149
|
+
if (total <= maxBytes) break;
|
|
150
|
+
if (await remove(entry)) total -= entry.size;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { ok: true, removed, bytes: Math.max(0, total) };
|
|
155
|
+
}
|
|
@@ -293,9 +293,12 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
293
293
|
fastPath: { stage: 'off' },
|
|
294
294
|
escalation: { stage: 'off' },
|
|
295
295
|
},
|
|
296
|
-
// C52 M07
|
|
297
|
-
//
|
|
298
|
-
//
|
|
296
|
+
// C52 M07: `unic-decision` is the owner's local non-LLM Lava/JEV model in
|
|
297
|
+
// UNIC Provider. Its OpenAI-compatible API is transport only, not proof of
|
|
298
|
+
// remote hosting or generative-LLM billing. Prefer bounded typed decisions
|
|
299
|
+
// when staged on; all stages currently ship 'off' (no decision request),
|
|
300
|
+
// deterministic safety/completion policy stays authoritative.
|
|
301
|
+
// `enabled:false` is the global emergency disable.
|
|
299
302
|
decisionPlane: {
|
|
300
303
|
enabled: true,
|
|
301
304
|
stage: 'off',
|
package/src/decision/client.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* client.js (TASK-002 / M07)
|
|
3
3
|
*
|
|
4
|
-
* Typed client for the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Typed client for the owner's LOCAL `unic-decision` model in UNIC Provider.
|
|
5
|
+
* It uses Lava/JEV technology and is NOT an LLM. OpenAI-compatible
|
|
6
|
+
* `POST <baseUrl>/v1/chat/completions` is only its interoperability transport,
|
|
7
|
+
* not evidence of remote hosting, generative semantics, or LLM pricing.
|
|
8
|
+
* See docs/pstack/UNIC_DECISION_SPEC.md (identity, §3/§15/§16).
|
|
9
|
+
*
|
|
10
|
+
* Prefer bounded classify/select/rank/score questions when decisionPlane.stage
|
|
11
|
+
* enables the caller; derive candidates in code, pass compact state and typed
|
|
12
|
+
* questions to createDecisionClient(...).requestBatch({ statePacket, questions }),
|
|
13
|
+
* then validate the outcome and retain deterministic safety/permission gates.
|
|
14
|
+
* The client does not itself switch a disabled rollout stage on.
|
|
7
15
|
*
|
|
8
16
|
* - Endpoint + credentials resolve via gatewayProbe env indirection only —
|
|
9
17
|
* no literal keys, no new credential store.
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* unic-decision.mjs (TASK-005 / M07, SPEC §5 FR-011)
|
|
4
4
|
*
|
|
5
|
-
* Standalone installed-side adapter for the
|
|
6
|
-
*
|
|
5
|
+
* Standalone installed-side adapter for the owner's local, non-LLM
|
|
6
|
+
* Lava/JEV `unic-decision` model in UNIC Provider. OpenAI-compatible
|
|
7
|
+
* /v1/chat/completions is a transport for easy integration, not evidence of
|
|
8
|
+
* an external generative LLM or LLM pricing. Prefer bounded typed semantic
|
|
9
|
+
* decisions when the caller's decisionPlane stage is enabled; this adapter
|
|
10
|
+
* does not change the rollout stage or override deterministic safety gates.
|
|
7
11
|
*
|
|
8
12
|
* Modes:
|
|
9
13
|
* node unic-decision.mjs --health-probe one no-tools request; the ONLY
|
|
@@ -48,9 +48,8 @@ Your own model does not change mid-turn. A tier only applies when work is handed
|
|
|
48
48
|
- Images: `@vision` only. `@default`/`@slow` cannot see images on the UNIC gateway and must never
|
|
49
49
|
guess at their contents — hand every image to `ukit-vision-analyst` first.
|
|
50
50
|
|
|
51
|
-
Doing everything inline is
|
|
52
|
-
|
|
53
|
-
wise flood this context.
|
|
51
|
+
Doing everything inline is what makes UKit look like it only has one model. Keep direct execution
|
|
52
|
+
for trivial/simple work; delegate when the tier differs or a noisy lane would flood this context.
|
|
54
53
|
|
|
55
54
|
## 7. Safe Patch
|
|
56
55
|
|
|
@@ -73,6 +72,7 @@ last thing you emit is one short line naming what is unfinished and what you nee
|
|
|
73
72
|
a status — the user cannot distinguish it from a crash.
|
|
74
73
|
|
|
75
74
|
> Maintainer note, not a per-turn rule: `modelRoles` in `.omp/config.yml` ships UNIC gateway names.
|
|
76
|
-
> On a non-UNIC provider, edit only the
|
|
77
|
-
>
|
|
78
|
-
>
|
|
75
|
+
> On a non-UNIC provider, edit only the cost tiers `smol`/`default`/`slow` — never `vision`
|
|
76
|
+
> (`unic-vision`) or `decision` (`unic-decision`, the LOCAL JEV/Lava typed-decision model —
|
|
77
|
+
> `docs/pstack/UNIC_DECISION_GUIDE.md`); both are capability lanes. Aliases `lite`/`code`/`smart`
|
|
78
|
+
> map smol↔lite, default↔code, slow↔smart — edit each alongside its tier.
|
|
@@ -13,6 +13,12 @@ modelRoles:
|
|
|
13
13
|
default: unic-code
|
|
14
14
|
slow: unic-smart
|
|
15
15
|
vision: unic-vision
|
|
16
|
+
# `decision` is a capability lane like `vision`, not a cost tier: it binds to
|
|
17
|
+
# the LOCAL JEV/Lava typed-decision model `unic-decision` (bounded classify/
|
|
18
|
+
# select/rank/score calls — see docs/pstack/UNIC_DECISION_GUIDE.md). It is NOT
|
|
19
|
+
# a handoff tier, so tier maps (handoff-model-guard.sh tierOf) never see it.
|
|
20
|
+
# On a non-UNIC provider leave it unset — there is no cost-tier alias for it.
|
|
21
|
+
decision: unic-decision
|
|
16
22
|
# Back-compat aliases — INDEPENDENT keys, not references. A maintainer overriding a
|
|
17
23
|
# cost tier above must edit the matching alias too (smol↔lite, default↔code,
|
|
18
24
|
# slow↔smart), or the alias keeps resolving to the old unic-* value.
|
|
@@ -140,6 +140,15 @@ the root contract is the only carrier — keep both in sync with
|
|
|
140
140
|
|
|
141
141
|
## 3-Tier Model Routing — full detail
|
|
142
142
|
|
|
143
|
+
`unic-decision` is **not** another LLM tier. The owner built this local,
|
|
144
|
+
near-zero-cost Lava/JEV model in UNIC Provider for fast typed semantic
|
|
145
|
+
decisions; the OpenAI-compatible API is a convenience transport, not a
|
|
146
|
+
generative-LLM or external-hosting claim. When `decisionPlane.stage` enables
|
|
147
|
+
it, use bounded candidate choices/rankings/scores through
|
|
148
|
+
`.claude/ukit/index/unic-decision.mjs`; keep safety, authorization and
|
|
149
|
+
completion gates deterministic. The shipped stage is `off` until explicitly
|
|
150
|
+
promoted. See `docs/pstack/UNIC_DECISION_SPEC.md` in the UKit development repo.
|
|
151
|
+
|
|
143
152
|
UKit routes tasks to one of three model tiers based on task complexity. The main session
|
|
144
153
|
model never changes mid-turn: a tier only takes effect when work is handed to an agent
|
|
145
154
|
whose own definition binds that model.
|
|
@@ -45,9 +45,8 @@ Your own model does not change mid-turn. A tier only applies when work is handed
|
|
|
45
45
|
- Images: `@vision` only. `@default`/`@slow` cannot see images on the UNIC gateway and must never
|
|
46
46
|
guess at their contents — hand every image to `ukit-vision-analyst` first.
|
|
47
47
|
|
|
48
|
-
Doing everything inline is
|
|
49
|
-
|
|
50
|
-
wise flood this context.
|
|
48
|
+
Doing everything inline is what makes UKit look like it only has one model. Keep direct execution
|
|
49
|
+
for trivial/simple work; delegate when the tier differs or a noisy lane would flood this context.
|
|
51
50
|
|
|
52
51
|
## 7. Safe Patch
|
|
53
52
|
|
|
@@ -70,6 +69,7 @@ last thing you emit is one short line naming what is unfinished and what you nee
|
|
|
70
69
|
a status — the user cannot distinguish it from a crash.
|
|
71
70
|
|
|
72
71
|
> Maintainer note, not a per-turn rule: `modelRoles` in `.omp/config.yml` ships UNIC gateway names.
|
|
73
|
-
> On a non-UNIC provider, edit only the
|
|
74
|
-
>
|
|
75
|
-
>
|
|
72
|
+
> On a non-UNIC provider, edit only the cost tiers `smol`/`default`/`slow` — never `vision`
|
|
73
|
+
> (`unic-vision`) or `decision` (`unic-decision`, the LOCAL JEV/Lava typed-decision model —
|
|
74
|
+
> `docs/pstack/UNIC_DECISION_GUIDE.md`); both are capability lanes. Aliases `lite`/`code`/`smart`
|
|
75
|
+
> map smol↔lite, default↔code, slow↔smart — edit each alongside its tier.
|