@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +1 -1
  3. package/scripts/bench/data-foundation.mjs +562 -0
  4. package/src/core/observability/adapters/common.js +75 -0
  5. package/src/core/observability/adapters/contextAdapter.js +55 -0
  6. package/src/core/observability/adapters/decisionAdapter.js +61 -0
  7. package/src/core/observability/adapters/routeAdapter.js +135 -0
  8. package/src/core/observability/analytics/digest.js +186 -0
  9. package/src/core/observability/analytics/fingerprints.js +126 -0
  10. package/src/core/observability/analytics/opportunities.js +329 -0
  11. package/src/core/observability/analytics/rebuild.js +56 -0
  12. package/src/core/observability/analytics/summary.js +298 -0
  13. package/src/core/observability/emit/config.js +29 -0
  14. package/src/core/observability/emit/recorder.js +297 -0
  15. package/src/core/observability/evaluation/aiPacket.js +230 -0
  16. package/src/core/observability/evaluation/optimizationKnowledge.js +172 -0
  17. package/src/core/observability/evaluation/replay.js +143 -0
  18. package/src/core/observability/evaluation/scorecard.js +445 -0
  19. package/src/core/observability/privacy/allowlist.js +185 -0
  20. package/src/core/observability/privacy/redaction.js +113 -0
  21. package/src/core/observability/privacy/sanitizeForSupport.js +133 -0
  22. package/src/core/observability/privacy/sanitizeObserved.js +134 -0
  23. package/src/core/observability/rollout.js +155 -0
  24. package/src/core/observability/schema/constants.js +66 -0
  25. package/src/core/observability/schema/registry.js +223 -0
  26. package/src/core/observability/schema/validate.js +227 -0
  27. package/src/core/observability/segments/internal.js +241 -0
  28. package/src/core/observability/segments/readSegments.js +215 -0
  29. package/src/core/observability/segments/recovery.js +123 -0
  30. package/src/core/observability/segments/retention.js +381 -0
  31. package/src/core/observability/support/import.js +402 -0
  32. package/src/core/observability/support/manifest.js +135 -0
  33. package/src/core/observability/support/paths.js +94 -0
  34. package/src/core/observability/support/projector.js +483 -0
  35. package/src/core/observability/support/renderer.js +130 -0
  36. package/src/core/observability/support/retention.js +155 -0
  37. package/src/core/runtimeConfig.js +6 -3
  38. package/src/decision/client.js +11 -3
  39. package/template_project/.claude/ukit/index/unic-decision.mjs +6 -2
  40. package/template_project/.omp/RULES.md +6 -6
  41. package/template_project/.omp/config.yml +6 -0
  42. package/template_project/docs/UKIT_INTERNALS.md +9 -0
  43. 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 UNIC Decision Agent (docs/pstack/UNIC_DECISION_SPEC §17). All
297
- // stages ship 'off' — zero outbound calls, deterministic policy stays
298
- // authoritative. `enabled:false` is the global emergency disable.
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',
@@ -1,9 +1,17 @@
1
1
  /**
2
2
  * client.js (TASK-002 / M07)
3
3
  *
4
- * Typed client for the UNIC Decision Agent: OpenAI-compatible
5
- * `POST <baseUrl>/v1/chat/completions` against the configured UNIC gateway
6
- * (docs/pstack/UNIC_DECISION_SPEC.md §3/§15/§16).
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 UNIC Decision Agent
6
- * (docs/pstack/UNIC_DECISION_SPEC.md §8/§9, CONTRACTS.md C13/C14).
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 exactly what makes UKit look like it only has one model. Keep direct
52
- execution for trivial/simple work; delegate when the tier differs or when a noisy lane would other-
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 three cost tiers — `smol`, `default`, `slow` — never `vision`,
77
- > which stays `unic-vision` (capability lane, not a cost tier). Old keys `lite`/`code`/`smart` remain
78
- > as compat aliases — edit each alias alongside its tier (smol↔lite, default↔code, slow↔smart).
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 exactly what makes UKit look like it only has one model. Keep direct
49
- execution for trivial/simple work; delegate when the tier differs or when a noisy lane would other-
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 three cost tiers — `smol`, `default`, `slow` — never `vision`,
74
- > which stays `unic-vision` (capability lane, not a cost tier). Old keys `lite`/`code`/`smart` remain
75
- > as compat aliases — edit each alias alongside its tier (smol↔lite, default↔code, slow↔smart).
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.