@ikenga/contract 0.17.0 → 0.18.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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +36 -11
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +10 -4
- package/dist/manifest.js.map +1 -1
- package/dist/ngwa.d.ts +1650 -0
- package/dist/ngwa.d.ts.map +1 -0
- package/dist/ngwa.js +225 -0
- package/dist/ngwa.js.map +1 -0
- package/dist/registry.d.ts +29 -11
- package/dist/registry.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/index.ts +1 -0
- package/src/manifest.test.ts +24 -0
- package/src/manifest.ts +12 -5
- package/src/ngwa.test.ts +330 -0
- package/src/ngwa.ts +262 -0
package/src/ngwa.test.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
NGWA_KINDS,
|
|
6
|
+
NGWA_SOURCES,
|
|
7
|
+
NGWA_STATES,
|
|
8
|
+
NgwaItemSchema,
|
|
9
|
+
NgwaOriginSchema,
|
|
10
|
+
NgwaPermsSummarySchema,
|
|
11
|
+
NgwaPlacementSchema,
|
|
12
|
+
NgwaRefSchema,
|
|
13
|
+
NgwaRuntimeSchema,
|
|
14
|
+
NgwaScopeSchema,
|
|
15
|
+
NgwaSnapshotSchema,
|
|
16
|
+
NgwaTrustSchema,
|
|
17
|
+
NgwaUsageSchema,
|
|
18
|
+
} from './ngwa.js';
|
|
19
|
+
|
|
20
|
+
// ─── WP-13 — G-NGWA-ITEM (drafts/ngwa-item.md §2, frozen Round 11) ──────────
|
|
21
|
+
|
|
22
|
+
const ORIGIN = {
|
|
23
|
+
source: 'registry',
|
|
24
|
+
url: 'https://registry.ikenga.dev/index.json',
|
|
25
|
+
ref: null,
|
|
26
|
+
resolved_version: '0.4.2',
|
|
27
|
+
publisher: 'ikenga-hq',
|
|
28
|
+
managed: true,
|
|
29
|
+
auto_update: false,
|
|
30
|
+
installed_at_ms: 1_726_000_000_000,
|
|
31
|
+
updated_at_ms: 1_726_900_000_000,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const PERMS = {
|
|
35
|
+
shell_execute: ['git *'],
|
|
36
|
+
fs_write_outside_sandbox: ['~/.claude/**'],
|
|
37
|
+
net: ['https://api.royalti.io/**'],
|
|
38
|
+
vault_keys: ['supabase.anon'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const TRUST = {
|
|
42
|
+
state: 'granted',
|
|
43
|
+
signed: true,
|
|
44
|
+
auto_trusted: false,
|
|
45
|
+
review_pending: false,
|
|
46
|
+
perms: PERMS,
|
|
47
|
+
last_granted_at_ms: 1_726_900_000_000,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const RUNTIME = {
|
|
51
|
+
state: 'running',
|
|
52
|
+
pid: 44_112,
|
|
53
|
+
uptime_s: 903,
|
|
54
|
+
restarts: 1,
|
|
55
|
+
last_err: null,
|
|
56
|
+
last_crash_ms: null,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const PLACEMENT = {
|
|
60
|
+
engine: 'claude',
|
|
61
|
+
scope: { kind: 'project', project_id: 'C--Users-x-royalti-co' },
|
|
62
|
+
path: '/Users/x/royalti-co/.claude/skills/groundwork',
|
|
63
|
+
mechanism: 'symlink-dir',
|
|
64
|
+
present: true,
|
|
65
|
+
link_target: '/Users/x/.ikenga/oba/skills/groundwork',
|
|
66
|
+
in_store: true,
|
|
67
|
+
managed_by: 'oba',
|
|
68
|
+
overridden_by: null,
|
|
69
|
+
format: 'md-yaml',
|
|
70
|
+
status: 'active',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const USAGE = {
|
|
74
|
+
source: 'transcript',
|
|
75
|
+
last_used_ms: 1_726_950_000_000,
|
|
76
|
+
count_7d: 12,
|
|
77
|
+
count_30d: 72,
|
|
78
|
+
tokens_30d: 1_240_991,
|
|
79
|
+
window_start_ms: 1_724_000_000_000,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const REF = {
|
|
83
|
+
kind: 'skill',
|
|
84
|
+
name: 'skill-core',
|
|
85
|
+
item_id: 'skill:personal:skill-core',
|
|
86
|
+
source: 'catalog',
|
|
87
|
+
ref: null,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const ITEM = {
|
|
91
|
+
id: 'com.ikenga.studio',
|
|
92
|
+
kind: 'app',
|
|
93
|
+
name: 'studio',
|
|
94
|
+
display_name: 'Studio',
|
|
95
|
+
description: 'Beat-detection workbench.',
|
|
96
|
+
version: '0.4.2',
|
|
97
|
+
latest_version: '0.5.0',
|
|
98
|
+
scope: { kind: 'personal' },
|
|
99
|
+
origin: ORIGIN,
|
|
100
|
+
state: 'enabled',
|
|
101
|
+
runtime: RUNTIME,
|
|
102
|
+
trust: TRUST,
|
|
103
|
+
placements: [PLACEMENT],
|
|
104
|
+
usage: USAGE,
|
|
105
|
+
requires: [REF],
|
|
106
|
+
required_by: [],
|
|
107
|
+
owner_pkg_id: null,
|
|
108
|
+
install_path: '/Users/x/.ikenga/pkgs/com.ikenga.studio',
|
|
109
|
+
engines: ['claude'],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const SOURCE_HEALTH = { ok: true, error: null, count: 3 };
|
|
113
|
+
|
|
114
|
+
const SNAPSHOT = {
|
|
115
|
+
items: [ITEM],
|
|
116
|
+
as_of_ms: 1_726_951_000_000,
|
|
117
|
+
sources: {
|
|
118
|
+
kernel: SOURCE_HEALTH,
|
|
119
|
+
oba: SOURCE_HEALTH,
|
|
120
|
+
engine_config: SOURCE_HEALTH,
|
|
121
|
+
engine_assets: { ok: false, error: 'registry snapshot unavailable', count: 0 },
|
|
122
|
+
trust: SOURCE_HEALTH,
|
|
123
|
+
usage: SOURCE_HEALTH,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Every object key anywhere in a parsed value must be snake_case (§2: the
|
|
128
|
+
* producer is a Rust command serializing with serde defaults and no renames). */
|
|
129
|
+
function assertSnakeCaseKeys(value: unknown, path = '$'): void {
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
value.forEach((v, i) => assertSnakeCaseKeys(v, `${path}[${i}]`));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (value === null || typeof value !== 'object') return;
|
|
135
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
136
|
+
assert.match(k, /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/, `key ${path}.${k} is not snake_case`);
|
|
137
|
+
assertSnakeCaseKeys(v, `${path}.${k}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const keys = (o: object) => Object.keys(o).sort();
|
|
142
|
+
|
|
143
|
+
// ── (a) every field in §2 is present, and keys are snake_case ──────────────
|
|
144
|
+
|
|
145
|
+
test('Ngwa: every field name in draft §2 is present on a parsed fixture', () => {
|
|
146
|
+
const item = NgwaItemSchema.parse(ITEM);
|
|
147
|
+
|
|
148
|
+
assert.deepEqual(keys(item), keys({
|
|
149
|
+
id: 0, kind: 0, name: 0, display_name: 0, description: 0, version: 0,
|
|
150
|
+
latest_version: 0, scope: 0, origin: 0, state: 0, runtime: 0, trust: 0,
|
|
151
|
+
placements: 0, usage: 0, requires: 0, required_by: 0, owner_pkg_id: 0,
|
|
152
|
+
install_path: 0, engines: 0,
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
assert.deepEqual(keys(NgwaOriginSchema.parse(ORIGIN)), keys({
|
|
156
|
+
source: 0, url: 0, ref: 0, resolved_version: 0, publisher: 0, managed: 0,
|
|
157
|
+
auto_update: 0, installed_at_ms: 0, updated_at_ms: 0,
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
assert.deepEqual(keys(NgwaRuntimeSchema.parse(RUNTIME)), keys({
|
|
161
|
+
state: 0, pid: 0, uptime_s: 0, restarts: 0, last_err: 0, last_crash_ms: 0,
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
assert.deepEqual(keys(NgwaTrustSchema.parse(TRUST)), keys({
|
|
165
|
+
state: 0, signed: 0, auto_trusted: 0, review_pending: 0, perms: 0,
|
|
166
|
+
last_granted_at_ms: 0,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
assert.deepEqual(keys(NgwaPermsSummarySchema.parse(PERMS)), keys({
|
|
170
|
+
shell_execute: 0, fs_write_outside_sandbox: 0, net: 0, vault_keys: 0,
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
assert.deepEqual(keys(NgwaPlacementSchema.parse(PLACEMENT)), keys({
|
|
174
|
+
engine: 0, scope: 0, path: 0, mechanism: 0, present: 0, link_target: 0,
|
|
175
|
+
in_store: 0, managed_by: 0, overridden_by: 0, format: 0, status: 0,
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
assert.deepEqual(keys(NgwaUsageSchema.parse(USAGE)), keys({
|
|
179
|
+
source: 0, last_used_ms: 0, count_7d: 0, count_30d: 0, tokens_30d: 0,
|
|
180
|
+
window_start_ms: 0,
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
assert.deepEqual(keys(NgwaRefSchema.parse(REF)), keys({
|
|
184
|
+
kind: 0, name: 0, item_id: 0, source: 0, ref: 0,
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
const snap = NgwaSnapshotSchema.parse(SNAPSHOT);
|
|
188
|
+
assert.deepEqual(keys(snap), ['as_of_ms', 'items', 'sources']);
|
|
189
|
+
assert.deepEqual(keys(snap.sources), [
|
|
190
|
+
'engine_assets', 'engine_config', 'kernel', 'oba', 'trust', 'usage',
|
|
191
|
+
]);
|
|
192
|
+
assert.deepEqual(keys(snap.sources.kernel), ['count', 'error', 'ok']);
|
|
193
|
+
|
|
194
|
+
// …and nothing anywhere in the parsed snapshot is camelCase.
|
|
195
|
+
assertSnakeCaseKeys(snap);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('Ngwa: the three frozen enum lists match §2 exactly', () => {
|
|
199
|
+
assert.deepEqual([...NGWA_KINDS], [
|
|
200
|
+
'app', 'engine', 'tool', 'sidecar',
|
|
201
|
+
'skill', 'agent', 'command', 'hook',
|
|
202
|
+
'bundle', 'schedule', 'workflow',
|
|
203
|
+
]);
|
|
204
|
+
assert.deepEqual([...NGWA_SOURCES], [
|
|
205
|
+
'builtin', 'registry', 'git', 'npx', 'local', 'dev', 'catalog',
|
|
206
|
+
]);
|
|
207
|
+
assert.deepEqual([...NGWA_STATES], [
|
|
208
|
+
'enabled', 'disabled', 'available', 'update', 'orphaned', 'broken',
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ── (b) invalid kind is rejected ───────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
test('Ngwa: an invalid kind is rejected', () => {
|
|
215
|
+
// `project` and `artifact` appear in the older ngwa-surface.html mock but are
|
|
216
|
+
// deliberately NOT in the union (§4) — neither has a producer.
|
|
217
|
+
assert.throws(() => NgwaItemSchema.parse({ ...ITEM, kind: 'project' }));
|
|
218
|
+
assert.throws(() => NgwaItemSchema.parse({ ...ITEM, kind: 'artifact' }));
|
|
219
|
+
assert.throws(() => NgwaItemSchema.parse({ ...ITEM, kind: 'APP' }));
|
|
220
|
+
// …while every frozen kind is accepted.
|
|
221
|
+
for (const k of NGWA_KINDS) {
|
|
222
|
+
assert.equal(NgwaItemSchema.parse({ ...ITEM, kind: k }).kind, k);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ── (c) invalid origin.source is rejected ──────────────────────────────────
|
|
227
|
+
|
|
228
|
+
test('Ngwa: an invalid origin.source is rejected', () => {
|
|
229
|
+
assert.throws(() =>
|
|
230
|
+
NgwaItemSchema.parse({ ...ITEM, origin: { ...ORIGIN, source: 'ftp' } }),
|
|
231
|
+
);
|
|
232
|
+
assert.throws(() => NgwaOriginSchema.parse({ ...ORIGIN, source: 'marketplace' }));
|
|
233
|
+
assert.throws(() => NgwaOriginSchema.parse({ ...ORIGIN, source: null }));
|
|
234
|
+
for (const s of NGWA_SOURCES) {
|
|
235
|
+
assert.equal(NgwaOriginSchema.parse({ ...ORIGIN, source: s }).source, s);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ── (d) usage: null vs a measured zero ─────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
test('Ngwa: usage null parses and is distinguishable from a zeroed NgwaUsage', () => {
|
|
242
|
+
const unmeasured = NgwaItemSchema.parse({ ...ITEM, usage: null });
|
|
243
|
+
assert.equal(unmeasured.usage, null);
|
|
244
|
+
|
|
245
|
+
const zeroed = NgwaItemSchema.parse({
|
|
246
|
+
...ITEM,
|
|
247
|
+
usage: {
|
|
248
|
+
source: 'transcript',
|
|
249
|
+
last_used_ms: null,
|
|
250
|
+
count_7d: 0,
|
|
251
|
+
count_30d: 0,
|
|
252
|
+
tokens_30d: 0,
|
|
253
|
+
window_start_ms: 1_724_000_000_000,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
assert.notEqual(zeroed.usage, null);
|
|
257
|
+
assert.equal(zeroed.usage?.count_7d, 0);
|
|
258
|
+
assert.equal(zeroed.usage?.count_30d, 0);
|
|
259
|
+
|
|
260
|
+
// The whole point of the nullable: "never measured" must not collapse into a
|
|
261
|
+
// measured zero (frame-workbench-v4.html:2679 — unmeasured reads "—", not 0).
|
|
262
|
+
assert.notDeepEqual(unmeasured.usage, zeroed.usage);
|
|
263
|
+
assert.equal(unmeasured.usage ?? 'dash', 'dash');
|
|
264
|
+
assert.notEqual(zeroed.usage ?? 'dash', 'dash');
|
|
265
|
+
|
|
266
|
+
// `usage` is required on the item — absent is not the same as null.
|
|
267
|
+
const { usage: _omit, ...withoutUsage } = ITEM;
|
|
268
|
+
assert.throws(() => NgwaItemSchema.parse(withoutUsage));
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ── Scope: the two-arm discriminated union ─────────────────────────────────
|
|
272
|
+
|
|
273
|
+
test('Ngwa: NgwaScope is a two-arm union discriminated on kind', () => {
|
|
274
|
+
assert.deepEqual(NgwaScopeSchema.parse({ kind: 'personal' }), { kind: 'personal' });
|
|
275
|
+
assert.deepEqual(NgwaScopeSchema.parse({ kind: 'project', project_id: 'p1' }), {
|
|
276
|
+
kind: 'project',
|
|
277
|
+
project_id: 'p1',
|
|
278
|
+
});
|
|
279
|
+
// project_id is required on the project arm, and absent from the personal one.
|
|
280
|
+
assert.throws(() => NgwaScopeSchema.parse({ kind: 'project' }));
|
|
281
|
+
assert.throws(() => NgwaScopeSchema.parse({ kind: 'workspace' }));
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ── Nullability and openness the draft calls out explicitly ────────────────
|
|
285
|
+
|
|
286
|
+
test('Ngwa: runtime is null for items with no supervisor child', () => {
|
|
287
|
+
const skill = NgwaItemSchema.parse({ ...ITEM, kind: 'skill', runtime: null });
|
|
288
|
+
assert.equal(skill.runtime, null);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('Ngwa: Ọba primitives carry trust not_applicable with perms null (§5)', () => {
|
|
292
|
+
const item = NgwaItemSchema.parse({
|
|
293
|
+
...ITEM,
|
|
294
|
+
kind: 'skill',
|
|
295
|
+
trust: {
|
|
296
|
+
state: 'not_applicable',
|
|
297
|
+
signed: false,
|
|
298
|
+
auto_trusted: false,
|
|
299
|
+
review_pending: false,
|
|
300
|
+
perms: null,
|
|
301
|
+
last_granted_at_ms: null,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
assert.equal(item.trust.state, 'not_applicable');
|
|
305
|
+
assert.equal(item.trust.perms, null);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('Ngwa: NgwaRef.kind stays open, mirroring RequiresEntry.kind', () => {
|
|
309
|
+
// §2: `NgwaKind | string` — a future primitive kind must not break a snapshot.
|
|
310
|
+
assert.equal(NgwaRefSchema.parse({ ...REF, kind: 'mcp' }).kind, 'mcp');
|
|
311
|
+
assert.equal(NgwaRefSchema.parse({ ...REF, kind: 'bundle' }).kind, 'bundle');
|
|
312
|
+
// …but NgwaRef.source is the closed NgwaSource union, nullable.
|
|
313
|
+
assert.equal(NgwaRefSchema.parse({ ...REF, source: null }).source, null);
|
|
314
|
+
assert.throws(() => NgwaRefSchema.parse({ ...REF, source: 'ftp' }));
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('Ngwa: an item with no placements is not an error (a pkg app has none)', () => {
|
|
318
|
+
const app = NgwaItemSchema.parse({ ...ITEM, placements: [], engines: [] });
|
|
319
|
+
assert.deepEqual(app.placements, []);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('Ngwa: placement present:false on an enabled item parses (the orphan case)', () => {
|
|
323
|
+
const orphan = NgwaItemSchema.parse({
|
|
324
|
+
...ITEM,
|
|
325
|
+
state: 'enabled',
|
|
326
|
+
placements: [{ ...PLACEMENT, present: false, link_target: null, format: null }],
|
|
327
|
+
});
|
|
328
|
+
assert.equal(orphan.placements[0]?.present, false);
|
|
329
|
+
assert.equal(orphan.placements[0]?.format, null);
|
|
330
|
+
});
|
package/src/ngwa.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Ngwa — the unified "installed thing" item, frozen as gate G-NGWA-ITEM
|
|
2
|
+
// (`plans/shell-ux-rearchitecture/drafts/ngwa-item.md` §2, Round 11).
|
|
3
|
+
//
|
|
4
|
+
// One `NgwaItem` joins five subsystems that no command joins today: the pkg
|
|
5
|
+
// kernel (`InstalledSummary`), the Ọba Claude-asset store (`ClaudeStoreEntry`),
|
|
6
|
+
// the engine-config scan (`claude_config.rs`), the `engine_assets` registry,
|
|
7
|
+
// and trust (`pkg/trust.rs` + `commands/pkg_trust.rs`). None of the five is a
|
|
8
|
+
// superset of another, so this type is a join, not a rename of any one of them.
|
|
9
|
+
//
|
|
10
|
+
// Producer: the Rust `ngwa_snapshot` command (WP-14). Consumers: WP-15
|
|
11
|
+
// installed+store, WP-16 scopes+health, WP-17 item detail, WP-18 trust sheet.
|
|
12
|
+
//
|
|
13
|
+
// KEY CASING IS `snake_case`, deliberately. The producer is a Rust command
|
|
14
|
+
// whose neighbouring struct (`InstalledSummary`) serializes with serde defaults
|
|
15
|
+
// and no renames; Ọba's camelCase renames are the exception in this codebase,
|
|
16
|
+
// not the rule, and matching serde defaults keeps `ngwa_snapshot` free of a
|
|
17
|
+
// rename attribute per field.
|
|
18
|
+
//
|
|
19
|
+
// The draft is FROZEN: any change to the shape below needs a new round in
|
|
20
|
+
// `04-discussion.md`, not an edit here.
|
|
21
|
+
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
|
|
24
|
+
// ── Kind ──────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Eleven kinds — two more than D-02's locked Kind facet. `bundle` and
|
|
27
|
+
* `sidecar` are real in the code (`Kind::Bundle`, `SidecarSpec`) and reachable
|
|
28
|
+
* through *More filters*; `workflow` has no producer in Phase 2 (Phase 4 /
|
|
29
|
+
* G-MANIFEST-V5) but stays in the union so adding one is not a contract break.
|
|
30
|
+
* `project` and `artifact` from the older `ngwa-surface.html` are NOT here —
|
|
31
|
+
* neither is in the locked facet list and neither has a producer. */
|
|
32
|
+
export const NGWA_KINDS = [
|
|
33
|
+
'app', 'engine', 'tool', 'sidecar', // from the pkg kernel
|
|
34
|
+
'skill', 'agent', 'command', 'hook', // from Ọba + the engine-config scan
|
|
35
|
+
'bundle', // Ọba bundle, or a pkg that only pulls requires[]
|
|
36
|
+
'schedule', // manifest cron[]
|
|
37
|
+
'workflow', // Phase 4 — no producer in Phase 2
|
|
38
|
+
] as const;
|
|
39
|
+
export const NgwaKindSchema = z.enum(NGWA_KINDS);
|
|
40
|
+
export type NgwaKind = (typeof NGWA_KINDS)[number];
|
|
41
|
+
|
|
42
|
+
// ── Scope ─────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/** `personal` is the kernel's `project_id: null` and Ọba's `workspace`. */
|
|
45
|
+
export const NgwaScopeSchema = z.discriminatedUnion('kind', [
|
|
46
|
+
z.object({ kind: z.literal('personal') }),
|
|
47
|
+
z.object({ kind: z.literal('project'), project_id: z.string() }),
|
|
48
|
+
]);
|
|
49
|
+
export type NgwaScope = z.infer<typeof NgwaScopeSchema>;
|
|
50
|
+
|
|
51
|
+
// ── Source (provenance) ───────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/** The union of the kernel's `InstallSource` (`builtin`/`registry`/`local`/
|
|
54
|
+
* `dev`) and Ọba's `ProvenanceSource` (`local`/`git`/`npx`/`catalog`). */
|
|
55
|
+
export const NGWA_SOURCES = [
|
|
56
|
+
'builtin', 'registry', 'git', 'npx', 'local', 'dev',
|
|
57
|
+
'catalog', // deprecated Ọba back-compat only (claude_store.rs:94-109)
|
|
58
|
+
] as const;
|
|
59
|
+
export const NgwaSourceSchema = z.enum(NGWA_SOURCES);
|
|
60
|
+
export type NgwaSource = z.infer<typeof NgwaSourceSchema>;
|
|
61
|
+
|
|
62
|
+
/** Provenance. The plan's single `source` string could not hold the url / ref /
|
|
63
|
+
* resolved version / publisher both provenance records carry, so it became an
|
|
64
|
+
* object; `origin.source` is the facet value. */
|
|
65
|
+
export const NgwaOriginSchema = z.object({
|
|
66
|
+
source: NgwaSourceSchema,
|
|
67
|
+
/** registry url | git remote | npm spec */
|
|
68
|
+
url: z.string().nullable(),
|
|
69
|
+
/** git ref */
|
|
70
|
+
ref: z.string().nullable(),
|
|
71
|
+
/** git SHA | npm version */
|
|
72
|
+
resolved_version: z.string().nullable(),
|
|
73
|
+
/** `InstallSource::Registry.publisher_key`, or catalog publisher */
|
|
74
|
+
publisher: z.string().nullable(),
|
|
75
|
+
/** vault-owned and deletable (Ọba `managed`) */
|
|
76
|
+
managed: z.boolean(),
|
|
77
|
+
auto_update: z.boolean(),
|
|
78
|
+
installed_at_ms: z.number().nullable(),
|
|
79
|
+
updated_at_ms: z.number().nullable(),
|
|
80
|
+
});
|
|
81
|
+
export type NgwaOrigin = z.infer<typeof NgwaOriginSchema>;
|
|
82
|
+
|
|
83
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export const NGWA_STATES = [
|
|
86
|
+
'enabled', 'disabled', 'available', 'update', 'orphaned', 'broken',
|
|
87
|
+
] as const;
|
|
88
|
+
export const NgwaStateSchema = z.enum(NGWA_STATES);
|
|
89
|
+
export type NgwaState = z.infer<typeof NgwaStateSchema>;
|
|
90
|
+
|
|
91
|
+
/** Live supervisor state. Only long-lived sidecars / MCP servers have one.
|
|
92
|
+
* Read from `sidecar_supervisor` directly — `PkgRowV2.state` never does, which
|
|
93
|
+
* is drift §10.5 that this type fixes for the new surface. */
|
|
94
|
+
export const NgwaRuntimeSchema = z.object({
|
|
95
|
+
state: z.enum([
|
|
96
|
+
'spawning', 'running', 'crashed', 'blocked', 'parked', 'stopped', 'shuttingdown',
|
|
97
|
+
]),
|
|
98
|
+
pid: z.number().nullable(),
|
|
99
|
+
uptime_s: z.number().nullable(),
|
|
100
|
+
restarts: z.number(),
|
|
101
|
+
last_err: z.string().nullable(),
|
|
102
|
+
last_crash_ms: z.number().nullable(),
|
|
103
|
+
});
|
|
104
|
+
export type NgwaRuntime = z.infer<typeof NgwaRuntimeSchema>;
|
|
105
|
+
|
|
106
|
+
// ── Trust ─────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/** The four sorted permission lists the trust snapshot hash is taken over
|
|
109
|
+
* (`trust.rs:61-67`, `159-182`). All-or-nothing: no per-permission row exists
|
|
110
|
+
* anywhere, which is the constraint WP-18 inherits. */
|
|
111
|
+
export const NgwaPermsSummarySchema = z.object({
|
|
112
|
+
shell_execute: z.array(z.string()),
|
|
113
|
+
fs_write_outside_sandbox: z.array(z.string()),
|
|
114
|
+
net: z.array(z.string()),
|
|
115
|
+
vault_keys: z.array(z.string()),
|
|
116
|
+
});
|
|
117
|
+
export type NgwaPermsSummary = z.infer<typeof NgwaPermsSummarySchema>;
|
|
118
|
+
|
|
119
|
+
/** Two distinct trust systems exist and must not be conflated: `pkg/trust.rs`
|
|
120
|
+
* is the per-call sensitive-permission gate (`state`), `commands/pkg_trust.rs`
|
|
121
|
+
* is the separate boot-time capability-diff review (`review_pending`). The
|
|
122
|
+
* D-02 facet values are derived, not read off one enum:
|
|
123
|
+
* `builtin` ← `auto_trusted`; `signed` ← `signed`; `unsigned` ← `!signed &&
|
|
124
|
+
* state !== 'needs_approval'`; `review` ← `state === 'needs_approval' ||
|
|
125
|
+
* review_pending`. Ọba primitives get `state: 'not_applicable'`, `perms: null`
|
|
126
|
+
* — a skill declares intent and is never granted anything. */
|
|
127
|
+
export const NgwaTrustSchema = z.object({
|
|
128
|
+
/** Mirrors PkgTrustState; 'not_applicable' for everything that is not a pkg. */
|
|
129
|
+
state: z.enum([
|
|
130
|
+
'auto_trusted', 'auto_granted', 'granted', 'needs_approval', 'not_applicable',
|
|
131
|
+
]),
|
|
132
|
+
/** manifest `signature` present, or minisign-verified catalog entry */
|
|
133
|
+
signed: z.boolean(),
|
|
134
|
+
/** provenance-trusted (builtin / dev) */
|
|
135
|
+
auto_trusted: z.boolean(),
|
|
136
|
+
/** a TrustReview row is waiting (pkg_trust.rs) */
|
|
137
|
+
review_pending: z.boolean(),
|
|
138
|
+
/** null when state is 'not_applicable' */
|
|
139
|
+
perms: NgwaPermsSummarySchema.nullable(),
|
|
140
|
+
last_granted_at_ms: z.number().nullable(),
|
|
141
|
+
});
|
|
142
|
+
export type NgwaTrust = z.infer<typeof NgwaTrustSchema>;
|
|
143
|
+
|
|
144
|
+
// ── Placement ─────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/** The real per-instance placement record, sourced primarily from the
|
|
147
|
+
* `claude_config` scan; `engine_assets` is a second contributor that marks a
|
|
148
|
+
* placement `managed_by: 'pkg'`. `present === false` on an `enabled` item is
|
|
149
|
+
* the orphan case WP-16 Health reports. */
|
|
150
|
+
export const NgwaPlacementSchema = z.object({
|
|
151
|
+
/** 'claude' | 'gemini' | 'codex' | ... */
|
|
152
|
+
engine: z.string(),
|
|
153
|
+
scope: NgwaScopeSchema,
|
|
154
|
+
path: z.string(),
|
|
155
|
+
mechanism: z.enum(['symlink-dir', 'file', 'settings-key']),
|
|
156
|
+
/** the target actually exists on disk */
|
|
157
|
+
present: z.boolean(),
|
|
158
|
+
link_target: z.string().nullable(),
|
|
159
|
+
/** the link resolves into the Ọba store */
|
|
160
|
+
in_store: z.boolean(),
|
|
161
|
+
managed_by: z.enum(['oba', 'pkg', 'user']),
|
|
162
|
+
/** path of the placement that shadows this one */
|
|
163
|
+
overridden_by: z.string().nullable(),
|
|
164
|
+
format: z.enum(['md-yaml', 'toml', 'json-embedded']).nullable(),
|
|
165
|
+
status: z.enum(['active', 'deprecated']),
|
|
166
|
+
});
|
|
167
|
+
export type NgwaPlacement = z.infer<typeof NgwaPlacementSchema>;
|
|
168
|
+
|
|
169
|
+
// ── Usage ─────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/** null on the item means "never measured" and MUST render as "—", never as 0
|
|
172
|
+
* (designs/frame-workbench-v4.html:2679 — "anything not measured reads —").
|
|
173
|
+
* Phase 2 sources this from transcript JSONL (DEC-24); `'hooks'` stays a legal
|
|
174
|
+
* value so a later increment can add it — principally for `hook` rows, which
|
|
175
|
+
* the transcript cannot see at all — without reopening the gate. */
|
|
176
|
+
export const NgwaUsageSchema = z.object({
|
|
177
|
+
source: z.enum(['hooks', 'transcript']),
|
|
178
|
+
last_used_ms: z.number().nullable(),
|
|
179
|
+
count_7d: z.number().nullable(),
|
|
180
|
+
count_30d: z.number().nullable(),
|
|
181
|
+
/** null when the chosen source carries no tokens */
|
|
182
|
+
tokens_30d: z.number().nullable(),
|
|
183
|
+
/** Earliest moment the source can see. Anything before it is unknown, not zero. */
|
|
184
|
+
window_start_ms: z.number(),
|
|
185
|
+
});
|
|
186
|
+
export type NgwaUsage = z.infer<typeof NgwaUsageSchema>;
|
|
187
|
+
|
|
188
|
+
// ── Dependency edges ──────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
export const NgwaRefSchema = z.object({
|
|
191
|
+
/** open, mirroring RequiresEntry.kind (manifest.rs:177-192) */
|
|
192
|
+
kind: z.union([NgwaKindSchema, z.string()]),
|
|
193
|
+
name: z.string(),
|
|
194
|
+
/** resolved NgwaItem.id, or null if unresolved */
|
|
195
|
+
item_id: z.string().nullable(),
|
|
196
|
+
source: NgwaSourceSchema.nullable(),
|
|
197
|
+
ref: z.string().nullable(),
|
|
198
|
+
});
|
|
199
|
+
export type NgwaRef = z.infer<typeof NgwaRefSchema>;
|
|
200
|
+
|
|
201
|
+
// ── The item ──────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
export const NgwaItemSchema = z.object({
|
|
204
|
+
/** Stable across a refresh. Pkg-backed: the manifest id. Everything else:
|
|
205
|
+
* `${kind}:${scope_key}:${name}` where scope_key is 'personal' | `project:<id>`. */
|
|
206
|
+
id: z.string(),
|
|
207
|
+
kind: NgwaKindSchema,
|
|
208
|
+
/** the on-disk / manifest name (stable, used for joins) */
|
|
209
|
+
name: z.string(),
|
|
210
|
+
/** manifest `name`, else `name` */
|
|
211
|
+
display_name: z.string(),
|
|
212
|
+
description: z.string().nullable(),
|
|
213
|
+
/** null for Ọba primitives with no resolved version */
|
|
214
|
+
version: z.string().nullable(),
|
|
215
|
+
/** registry index only; null otherwise */
|
|
216
|
+
latest_version: z.string().nullable(),
|
|
217
|
+
scope: NgwaScopeSchema,
|
|
218
|
+
origin: NgwaOriginSchema,
|
|
219
|
+
state: NgwaStateSchema,
|
|
220
|
+
runtime: NgwaRuntimeSchema.nullable(),
|
|
221
|
+
trust: NgwaTrustSchema,
|
|
222
|
+
/** An item with no placements is not an error: a pkg app has none. */
|
|
223
|
+
placements: z.array(NgwaPlacementSchema),
|
|
224
|
+
/** null = unmeasured. A measured zero is `{ count_7d: 0, … }`; the UI must
|
|
225
|
+
* distinguish them. */
|
|
226
|
+
usage: NgwaUsageSchema.nullable(),
|
|
227
|
+
requires: z.array(NgwaRefSchema),
|
|
228
|
+
/** Computed per snapshot by inverting `requires[]`, never stored — Ọba
|
|
229
|
+
* deliberately does not persist the reverse graph (claude_store.rs:120-122). */
|
|
230
|
+
required_by: z.array(NgwaRefSchema),
|
|
231
|
+
/** The pkg that contributed this item, when it is not itself a pkg. */
|
|
232
|
+
owner_pkg_id: z.string().nullable(),
|
|
233
|
+
install_path: z.string().nullable(),
|
|
234
|
+
/** Engines this item is placed in, derived from placements[]. Facet source. */
|
|
235
|
+
engines: z.array(z.string()),
|
|
236
|
+
});
|
|
237
|
+
export type NgwaItem = z.infer<typeof NgwaItemSchema>;
|
|
238
|
+
|
|
239
|
+
/** Per-source health entry, so the UI can say "Ọba unreadable" instead of
|
|
240
|
+
* silently rendering "no skills". */
|
|
241
|
+
export const NgwaSourceHealthSchema = z.object({
|
|
242
|
+
ok: z.boolean(),
|
|
243
|
+
error: z.string().nullable(),
|
|
244
|
+
count: z.number(),
|
|
245
|
+
});
|
|
246
|
+
export type NgwaSourceHealth = z.infer<typeof NgwaSourceHealthSchema>;
|
|
247
|
+
|
|
248
|
+
export const NgwaSnapshotSchema = z.object({
|
|
249
|
+
/** `id` is unique within a snapshot; WP-14 asserts uniqueness. */
|
|
250
|
+
items: z.array(NgwaItemSchema),
|
|
251
|
+
as_of_ms: z.number(),
|
|
252
|
+
/** Per-source health, so the UI can say "Ọba unreadable" instead of "no skills". */
|
|
253
|
+
sources: z.object({
|
|
254
|
+
kernel: NgwaSourceHealthSchema,
|
|
255
|
+
oba: NgwaSourceHealthSchema,
|
|
256
|
+
engine_config: NgwaSourceHealthSchema,
|
|
257
|
+
engine_assets: NgwaSourceHealthSchema,
|
|
258
|
+
trust: NgwaSourceHealthSchema,
|
|
259
|
+
usage: NgwaSourceHealthSchema,
|
|
260
|
+
}),
|
|
261
|
+
});
|
|
262
|
+
export type NgwaSnapshot = z.infer<typeof NgwaSnapshotSchema>;
|