entangled-worlds-orbiters-shared 2.16.0 → 3.0.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/README.md +2 -4
- package/dist/faust/recipes.js +10 -6
- package/dist/faust/voiceProgram.d.ts +2 -0
- package/dist/faust/voiceProgram.js +16 -9
- package/package.json +2 -7
- package/src/faust/recipes.ts +9 -5
- package/src/faust/voiceProgram.ts +18 -10
- package/dist/capabilities/errors.d.ts +0 -79
- package/dist/capabilities/errors.js +0 -87
- package/dist/capabilities/index.d.ts +0 -9
- package/dist/capabilities/index.js +0 -9
- package/dist/capabilities/registry.d.ts +0 -106
- package/dist/capabilities/registry.js +0 -215
- package/dist/capabilities/schema.d.ts +0 -52
- package/dist/capabilities/schema.js +0 -139
- package/src/capabilities/errors.ts +0 -115
- package/src/capabilities/index.ts +0 -22
- package/src/capabilities/registry.ts +0 -307
- package/src/capabilities/schema.ts +0 -164
package/README.md
CHANGED
|
@@ -81,10 +81,8 @@ import * as EntangledWorlds from 'entangled-worlds-orbiters-shared';
|
|
|
81
81
|
- Real binaries (close pair from component masses, a distant companion for wide pairs) and an optional debris disc; `mainSequenceTemperature` / `Radius` / `Luminosity` helpers
|
|
82
82
|
- Perf + look harness: `npm run bench:star` (see `bench/star/results/REPORT.md`)
|
|
83
83
|
|
|
84
|
-
### Capabilities
|
|
85
|
-
|
|
86
|
-
- `validate`, `assertValid` - JSON-Schema-subset validator used for capability inputs
|
|
87
|
-
- Error taxonomy - `ValidationError`, `UnavailableError`, `PrecheckError`, …
|
|
84
|
+
### Capabilities → moved
|
|
85
|
+
The capability registry (`createCapabilityRegistry`, `validate` / `assertValid`, the error taxonomy) moved to its own package, **`plantasia.space-orchidea/capabilities`** (removed here in 3.0.0).
|
|
88
86
|
|
|
89
87
|
### Studio session (`entangled-worlds-orbiters-shared/studio`)
|
|
90
88
|
- `createHistory` - Snapshot undo/redo with injected snapshot/restore
|
package/dist/faust/recipes.js
CHANGED
|
@@ -70,20 +70,24 @@ export function readHeadShape(value, sources) {
|
|
|
70
70
|
if (typeof file !== 'string' || !Object.hasOwn(sources, file))
|
|
71
71
|
throw new Error(`module ${index}: no such source`);
|
|
72
72
|
const last = index === value.length - 1;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const constants = notes.constants ?? {};
|
|
73
|
+
// Program constants, an instrument's or a note module's: a few safe names, each a small whole number.
|
|
74
|
+
const constantsOf = (given) => {
|
|
75
|
+
const constants = given ?? {};
|
|
77
76
|
if (typeof constants !== 'object' || Array.isArray(constants))
|
|
78
77
|
throw new Error(`module ${index}: constants`);
|
|
79
78
|
const entries = Object.entries(constants);
|
|
80
79
|
if (entries.length > 8 || entries.some(([name, n]) => !/^[A-Z][A-Z0-9_]{0,23}$/.test(name) || !whole(n, MAX_CONSTANT)))
|
|
81
80
|
throw new Error(`module ${index}: constants`);
|
|
82
|
-
return
|
|
81
|
+
return entries.length ? { constants: Object.fromEntries(entries) } : {};
|
|
82
|
+
};
|
|
83
|
+
if (last) {
|
|
84
|
+
if (notes?.kind !== 'instrument' || !whole(notes.polyphony, MAX_NOTE_LANES))
|
|
85
|
+
throw new Error(`module ${index}: a head ends with an instrument of 1 to ${MAX_NOTE_LANES} voices`);
|
|
86
|
+
return { dspFileName: file, notes: { kind: 'instrument', polyphony: notes.polyphony, ...constantsOf(notes.constants) } };
|
|
83
87
|
}
|
|
84
88
|
if ((notes?.kind !== 'generator' && notes?.kind !== 'midi') || !whole(notes.lanesPerLane, MAX_NOTE_LANES))
|
|
85
89
|
throw new Error(`module ${index}: a note module with 1 to ${MAX_NOTE_LANES} lanes per lane`);
|
|
86
|
-
return { dspFileName: file, notes: { kind: notes.kind, lanesPerLane: notes.lanesPerLane } };
|
|
90
|
+
return { dspFileName: file, notes: { kind: notes.kind, lanesPerLane: notes.lanesPerLane, ...constantsOf(notes.constants) } };
|
|
87
91
|
});
|
|
88
92
|
}
|
|
89
93
|
const isHead = (head) => {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* lanes so far and multiplies them by its `lanesPerLane`, inside its note-domain bypass; the
|
|
42
42
|
* instrument plays every lane that reaches it. `SLOT` is the module's place, its part of the seed;
|
|
43
43
|
* a module that adds lanes (a structural `lanesOut`) also gets `LANES_OUT`, its lanes out per lane in,
|
|
44
|
+
* and a note module with program constants (an optional section compiled in or out) gets them too,
|
|
44
45
|
* since `notes(n)` is told only n.
|
|
45
46
|
*
|
|
46
47
|
* Between the note modules a lane carries a fourth signal, the beat its note started on (what Random
|
|
@@ -62,6 +63,7 @@ export type SlotNotes = {
|
|
|
62
63
|
} | {
|
|
63
64
|
kind: 'generator' | 'midi';
|
|
64
65
|
lanesPerLane: number;
|
|
66
|
+
constants?: Readonly<Record<string, number>>;
|
|
65
67
|
};
|
|
66
68
|
/** One slot of a composed voice: the module's `.dsp`, by file name. */
|
|
67
69
|
export interface VoiceSlot {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* lanes so far and multiplies them by its `lanesPerLane`, inside its note-domain bypass; the
|
|
42
42
|
* instrument plays every lane that reaches it. `SLOT` is the module's place, its part of the seed;
|
|
43
43
|
* a module that adds lanes (a structural `lanesOut`) also gets `LANES_OUT`, its lanes out per lane in,
|
|
44
|
+
* and a note module with program constants (an optional section compiled in or out) gets them too,
|
|
44
45
|
* since `notes(n)` is told only n.
|
|
45
46
|
*
|
|
46
47
|
* Between the note modules a lane carries a fourth signal, the beat its note started on (what Random
|
|
@@ -117,7 +118,7 @@ function noteModules(slots, head) {
|
|
|
117
118
|
}
|
|
118
119
|
const k = wholeAtLeastOne(notes.lanesPerLane, `slot ${index}: lanesPerLane`);
|
|
119
120
|
// A module that adds lanes is told how many it puts out per lane in (an echo's repeats + 1).
|
|
120
|
-
const env = k === 1 ? `SLOT = ${index};` : `SLOT = ${index}; LANES_OUT = ${k}
|
|
121
|
+
const env = (k === 1 ? `SLOT = ${index};` : `SLOT = ${index}; LANES_OUT = ${k};`) + constantsEnv(notes.constants, index);
|
|
121
122
|
const fx = `library("${slot.dspFileName}")[${env}].notes(${lanes})`;
|
|
122
123
|
parts.push(`hgroup("${slotGroupId(index)}", nt.bypass(${lanes}, ${k}, ${fx}))`);
|
|
123
124
|
lanes *= k;
|
|
@@ -134,13 +135,16 @@ function noteModules(slots, head) {
|
|
|
134
135
|
function instrumentPart(slots, head, lanes) {
|
|
135
136
|
// The instrument's place salts its random draws too (an LFO's Random), as every note module's does;
|
|
136
137
|
// its program constants (unison's `U`) shape it, as its polyphony does.
|
|
137
|
-
const constants =
|
|
138
|
-
|
|
138
|
+
const constants = constantsEnv(slots[head].notes.constants, head);
|
|
139
|
+
return `hgroup("${slotGroupId(head)}", library("${slots[head].dspFileName}")[SLOT = ${head};${constants}].play(${lanes}))`;
|
|
140
|
+
}
|
|
141
|
+
/** A slot's program constants as library environment entries (` U = 2;`), each a safe name and a whole number. */
|
|
142
|
+
function constantsEnv(constants, index) {
|
|
143
|
+
return Object.entries(constants ?? {}).map(([name, value]) => {
|
|
139
144
|
if (!/^[A-Z][A-Z0-9_]*$/.test(name))
|
|
140
|
-
throw new Error(`[voiceProgram] slot ${
|
|
141
|
-
return ` ${name} = ${wholeAtLeastOne(value, `slot ${
|
|
145
|
+
throw new Error(`[voiceProgram] slot ${index}: unsafe constant ${name}`);
|
|
146
|
+
return ` ${name} = ${wholeAtLeastOne(value, `slot ${index}: ${name}`)};`;
|
|
142
147
|
}).join('');
|
|
143
|
-
return `hgroup("${slotGroupId(head)}", library("${slots[head].dspFileName}")[SLOT = ${head};${constants}].play(${lanes}))`;
|
|
144
148
|
}
|
|
145
149
|
/** The entry, the note modules and the instrument, up to `head` (the instrument): lanes all the way. */
|
|
146
150
|
function noteChain(slots, head) {
|
|
@@ -167,8 +171,8 @@ export function voiceCacheKey(slots) {
|
|
|
167
171
|
// A note module's lane count is structural too: it shapes the program.
|
|
168
172
|
const shape = (slots ?? []).map((slot) => {
|
|
169
173
|
const notes = slot?.notes;
|
|
170
|
-
const constants = notes
|
|
171
|
-
const lanes = !notes ? '' : notes.kind === 'instrument' ? `#p${notes.polyphony}${constants}` : `#${notes.kind}${notes.lanesPerLane}`;
|
|
174
|
+
const constants = notes ? constantsKey(notes.constants) : '';
|
|
175
|
+
const lanes = !notes ? '' : notes.kind === 'instrument' ? `#p${notes.polyphony}${constants}` : `#${notes.kind}${notes.lanesPerLane}${constants}`;
|
|
172
176
|
return String(slot?.dspFileName ?? '') + lanes;
|
|
173
177
|
}).join('>');
|
|
174
178
|
return `v${fnv(shape)}`;
|
|
@@ -238,7 +242,10 @@ export function buildHeadPrograms(slots) {
|
|
|
238
242
|
// FAUST roots a program's controls in a group named after it only when it has several groups at the
|
|
239
243
|
// top: named here, both programs' addresses start as the one program's do, whatever each holds.
|
|
240
244
|
const root = (chain) => `vgroup("${HEAD_PROGRAM_NAME}", ${chain})`;
|
|
241
|
-
const shape = slots.slice(0, head).map((slot) =>
|
|
245
|
+
const shape = slots.slice(0, head).map((slot) => {
|
|
246
|
+
const n = slot.notes;
|
|
247
|
+
return `${slot.dspFileName}#${n.kind}${n.lanesPerLane}${constantsKey(n.constants)}`;
|
|
248
|
+
}).join('>');
|
|
242
249
|
return {
|
|
243
250
|
notes: {
|
|
244
251
|
key: `n${fileKey(`${shape}>#p${notes.polyphony}`)}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "entangled-worlds-orbiters-shared",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Shared library for entangled worlds orbiters - contains core world rendering, moons system, and UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -38,10 +38,6 @@
|
|
|
38
38
|
"types": "./dist/clock/curves.d.ts",
|
|
39
39
|
"import": "./dist/clock/curves.js"
|
|
40
40
|
},
|
|
41
|
-
"./capabilities": {
|
|
42
|
-
"types": "./dist/capabilities/index.d.ts",
|
|
43
|
-
"import": "./dist/capabilities/index.js"
|
|
44
|
-
},
|
|
45
41
|
"./faust": {
|
|
46
42
|
"types": "./dist/faust/index.d.ts",
|
|
47
43
|
"import": "./dist/faust/index.js"
|
|
@@ -129,14 +125,13 @@
|
|
|
129
125
|
"prepare": "npm run build:ts",
|
|
130
126
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
131
127
|
"test:clock": "node --test --import tsx src/clock/*.test.ts",
|
|
132
|
-
"test:capabilities": "node --test --import tsx src/capabilities/*.test.js",
|
|
133
128
|
"test:notes": "node --test --import tsx src/notes/*.test.ts",
|
|
134
129
|
"test:faust": "node --test --import tsx src/faust/*.test.ts",
|
|
135
130
|
"test:studio": "node --test --import tsx src/studio/*.test.js",
|
|
136
131
|
"test:uploads": "node --test --import tsx src/uploads/*.test.js",
|
|
137
132
|
"test:audio": "node --test --import tsx src/audio/*.test.ts",
|
|
138
133
|
"test:render": "node --test src/render/*.test.js",
|
|
139
|
-
"test": "npm run test:clock && npm run test:
|
|
134
|
+
"test": "npm run test:clock && npm run test:notes && npm run test:faust && npm run test:studio && npm run test:uploads && npm run test:audio && npm run test:render && npm run test:moons && npm run test:star",
|
|
140
135
|
"test:moons": "node --test src/moons/*.test.js",
|
|
141
136
|
"test:star": "node --test --import tsx src/star/*.test.ts",
|
|
142
137
|
"bench:star:build": "esbuild bench/star/main.ts --bundle --format=esm --target=es2022 --outfile=bench/star/build/main.js --log-level=warning",
|
package/src/faust/recipes.ts
CHANGED
|
@@ -90,16 +90,20 @@ export function readHeadShape(value: unknown, sources: Readonly<Record<string, s
|
|
|
90
90
|
const notes = (slot as VoiceSlot | null)?.notes as SlotNotes | undefined;
|
|
91
91
|
if (typeof file !== 'string' || !Object.hasOwn(sources, file)) throw new Error(`module ${index}: no such source`);
|
|
92
92
|
const last = index === value.length - 1;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const constants =
|
|
93
|
+
// Program constants, an instrument's or a note module's: a few safe names, each a small whole number.
|
|
94
|
+
const constantsOf = (given: unknown): { constants?: Record<string, number> } => {
|
|
95
|
+
const constants = given ?? {};
|
|
96
96
|
if (typeof constants !== 'object' || Array.isArray(constants)) throw new Error(`module ${index}: constants`);
|
|
97
97
|
const entries = Object.entries(constants);
|
|
98
98
|
if (entries.length > 8 || entries.some(([name, n]) => !/^[A-Z][A-Z0-9_]{0,23}$/.test(name) || !whole(n, MAX_CONSTANT))) throw new Error(`module ${index}: constants`);
|
|
99
|
-
return
|
|
99
|
+
return entries.length ? { constants: Object.fromEntries(entries) } : {};
|
|
100
|
+
};
|
|
101
|
+
if (last) {
|
|
102
|
+
if (notes?.kind !== 'instrument' || !whole(notes.polyphony, MAX_NOTE_LANES)) throw new Error(`module ${index}: a head ends with an instrument of 1 to ${MAX_NOTE_LANES} voices`);
|
|
103
|
+
return { dspFileName: file, notes: { kind: 'instrument', polyphony: notes.polyphony, ...constantsOf(notes.constants) } };
|
|
100
104
|
}
|
|
101
105
|
if ((notes?.kind !== 'generator' && notes?.kind !== 'midi') || !whole(notes.lanesPerLane, MAX_NOTE_LANES)) throw new Error(`module ${index}: a note module with 1 to ${MAX_NOTE_LANES} lanes per lane`);
|
|
102
|
-
return { dspFileName: file, notes: { kind: notes.kind, lanesPerLane: notes.lanesPerLane } };
|
|
106
|
+
return { dspFileName: file, notes: { kind: notes.kind, lanesPerLane: notes.lanesPerLane, ...constantsOf(notes.constants) } };
|
|
103
107
|
});
|
|
104
108
|
}
|
|
105
109
|
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* lanes so far and multiplies them by its `lanesPerLane`, inside its note-domain bypass; the
|
|
42
42
|
* instrument plays every lane that reaches it. `SLOT` is the module's place, its part of the seed;
|
|
43
43
|
* a module that adds lanes (a structural `lanesOut`) also gets `LANES_OUT`, its lanes out per lane in,
|
|
44
|
+
* and a note module with program constants (an optional section compiled in or out) gets them too,
|
|
44
45
|
* since `notes(n)` is told only n.
|
|
45
46
|
*
|
|
46
47
|
* Between the note modules a lane carries a fourth signal, the beat its note started on (what Random
|
|
@@ -55,7 +56,7 @@ export const MAX_NOTE_LANES = 16;
|
|
|
55
56
|
* What a slot does with notes: a notes instrument's polyphony (and its program constants, when it has
|
|
56
57
|
* any), or a generator's or MIDI effect's lanes out per lane in.
|
|
57
58
|
*/
|
|
58
|
-
export type SlotNotes = { kind: 'instrument'; polyphony: number; constants?: Readonly<Record<string, number>>; copies?: number } | { kind: 'generator' | 'midi'; lanesPerLane: number };
|
|
59
|
+
export type SlotNotes = { kind: 'instrument'; polyphony: number; constants?: Readonly<Record<string, number>>; copies?: number } | { kind: 'generator' | 'midi'; lanesPerLane: number; constants?: Readonly<Record<string, number>> };
|
|
59
60
|
|
|
60
61
|
/** Faust identifiers: the group name goes into generated source, so keep it tame. */
|
|
61
62
|
const SAFE_SEGMENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -143,7 +144,7 @@ function noteModules(slots: readonly VoiceSlot[], head: number): { parts: string
|
|
|
143
144
|
}
|
|
144
145
|
const k = wholeAtLeastOne(notes.lanesPerLane, `slot ${index}: lanesPerLane`);
|
|
145
146
|
// A module that adds lanes is told how many it puts out per lane in (an echo's repeats + 1).
|
|
146
|
-
const env = k === 1 ? `SLOT = ${index};` : `SLOT = ${index}; LANES_OUT = ${k}
|
|
147
|
+
const env = (k === 1 ? `SLOT = ${index};` : `SLOT = ${index}; LANES_OUT = ${k};`) + constantsEnv(notes.constants, index);
|
|
147
148
|
const fx = `library("${slot.dspFileName}")[${env}].notes(${lanes})`;
|
|
148
149
|
parts.push(`hgroup("${slotGroupId(index)}", nt.bypass(${lanes}, ${k}, ${fx}))`);
|
|
149
150
|
lanes *= k;
|
|
@@ -160,14 +161,18 @@ function noteModules(slots: readonly VoiceSlot[], head: number): { parts: string
|
|
|
160
161
|
function instrumentPart(slots: readonly VoiceSlot[], head: number, lanes: number): string {
|
|
161
162
|
// The instrument's place salts its random draws too (an LFO's Random), as every note module's does;
|
|
162
163
|
// its program constants (unison's `U`) shape it, as its polyphony does.
|
|
163
|
-
const constants =
|
|
164
|
-
.map(([name, value]) => {
|
|
165
|
-
if (!/^[A-Z][A-Z0-9_]*$/.test(name)) throw new Error(`[voiceProgram] slot ${head}: unsafe constant ${name}`);
|
|
166
|
-
return ` ${name} = ${wholeAtLeastOne(value, `slot ${head}: ${name}`)};`;
|
|
167
|
-
}).join('');
|
|
164
|
+
const constants = constantsEnv((slots[head].notes as { constants?: Readonly<Record<string, number>> }).constants, head);
|
|
168
165
|
return `hgroup("${slotGroupId(head)}", library("${slots[head].dspFileName}")[SLOT = ${head};${constants}].play(${lanes}))`;
|
|
169
166
|
}
|
|
170
167
|
|
|
168
|
+
/** A slot's program constants as library environment entries (` U = 2;`), each a safe name and a whole number. */
|
|
169
|
+
function constantsEnv(constants: Readonly<Record<string, number>> | undefined, index: number): string {
|
|
170
|
+
return Object.entries(constants ?? {}).map(([name, value]) => {
|
|
171
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(name)) throw new Error(`[voiceProgram] slot ${index}: unsafe constant ${name}`);
|
|
172
|
+
return ` ${name} = ${wholeAtLeastOne(value, `slot ${index}: ${name}`)};`;
|
|
173
|
+
}).join('');
|
|
174
|
+
}
|
|
175
|
+
|
|
171
176
|
/** The entry, the note modules and the instrument, up to `head` (the instrument): lanes all the way. */
|
|
172
177
|
function noteChain(slots: readonly VoiceSlot[], head: number): string[] {
|
|
173
178
|
const { parts, lanes } = noteModules(slots, head);
|
|
@@ -194,8 +199,8 @@ export function voiceCacheKey(slots: readonly VoiceSlot[] | null | undefined): s
|
|
|
194
199
|
// A note module's lane count is structural too: it shapes the program.
|
|
195
200
|
const shape = (slots ?? []).map((slot) => {
|
|
196
201
|
const notes = slot?.notes;
|
|
197
|
-
const constants = notes
|
|
198
|
-
const lanes = !notes ? '' : notes.kind === 'instrument' ? `#p${notes.polyphony}${constants}` : `#${notes.kind}${notes.lanesPerLane}`;
|
|
202
|
+
const constants = notes ? constantsKey(notes.constants) : '';
|
|
203
|
+
const lanes = !notes ? '' : notes.kind === 'instrument' ? `#p${notes.polyphony}${constants}` : `#${notes.kind}${notes.lanesPerLane}${constants}`;
|
|
199
204
|
return String(slot?.dspFileName ?? '') + lanes;
|
|
200
205
|
}).join('>');
|
|
201
206
|
return `v${fnv(shape)}`;
|
|
@@ -287,7 +292,10 @@ export function buildHeadPrograms(slots: readonly VoiceSlot[]): HeadPrograms {
|
|
|
287
292
|
// FAUST roots a program's controls in a group named after it only when it has several groups at the
|
|
288
293
|
// top: named here, both programs' addresses start as the one program's do, whatever each holds.
|
|
289
294
|
const root = (chain: string) => `vgroup("${HEAD_PROGRAM_NAME}", ${chain})`;
|
|
290
|
-
const shape = slots.slice(0, head).map((slot) =>
|
|
295
|
+
const shape = slots.slice(0, head).map((slot) => {
|
|
296
|
+
const n = slot.notes as Extract<SlotNotes, { kind: 'generator' | 'midi' }>;
|
|
297
|
+
return `${slot.dspFileName}#${n.kind}${n.lanesPerLane}${constantsKey(n.constants)}`;
|
|
298
|
+
}).join('>');
|
|
291
299
|
return {
|
|
292
300
|
notes: {
|
|
293
301
|
key: `n${fileKey(`${shape}>#p${notes.polyphony}`)}`,
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Capability layer error taxonomy (shared machinery).
|
|
3
|
-
*
|
|
4
|
-
* This is the ONE generic capability machinery, app-agnostic and layer-neutral
|
|
5
|
-
* (`src/capabilities/` imports nothing app-level — see `.agents/decisions/`). Both the studio
|
|
6
|
-
* registry (`src/studio/capabilities/`, the credit-metered superset caller) and the edit-mode
|
|
7
|
-
* registry (`src/core/edit/capabilities/`, the lean preview caller) build on it. It supersedes
|
|
8
|
-
* the previously duplicated `studio/capabilities/errors.js` + `core/edit/capabilities/errors.ts`.
|
|
9
|
-
*
|
|
10
|
-
* One base class so callers — today the UI, tomorrow the agent loop — branch on `err.code`
|
|
11
|
-
* instead of string-matching messages. Every error carries the offending capability name when
|
|
12
|
-
* known, so failures are traceable without a stack dive.
|
|
13
|
-
*/
|
|
14
|
-
export type CapabilityErrorCode = 'CAPABILITY_ERROR' | 'VALIDATION' | 'UNAVAILABLE' | 'PRECHECK_FAILED' | 'NOT_IMPLEMENTED' | 'INSUFFICIENT_CREDITS' | 'UNKNOWN_CAPABILITY';
|
|
15
|
-
export interface SchemaIssue {
|
|
16
|
-
path: string;
|
|
17
|
-
message: string;
|
|
18
|
-
}
|
|
19
|
-
export declare class CapabilityError extends Error {
|
|
20
|
-
code: CapabilityErrorCode;
|
|
21
|
-
capability: string | null;
|
|
22
|
-
cause?: unknown;
|
|
23
|
-
constructor(message: string, { code, capability, cause }?: {
|
|
24
|
-
code?: CapabilityErrorCode;
|
|
25
|
-
capability?: string | null;
|
|
26
|
-
cause?: unknown;
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
/** Input args failed the capability's inputSchema. Thrown before any write. */
|
|
30
|
-
export declare class ValidationError extends CapabilityError {
|
|
31
|
-
issues: SchemaIssue[];
|
|
32
|
-
constructor(message: string, { capability, issues }?: {
|
|
33
|
-
capability?: string | null;
|
|
34
|
-
issues?: SchemaIssue[];
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
/** Capability is declared but not invokable yet (status: 'planned' / available()===false). */
|
|
38
|
-
export declare class UnavailableError extends CapabilityError {
|
|
39
|
-
reason: string | null;
|
|
40
|
-
constructor(message: string, { capability, reason }?: {
|
|
41
|
-
capability?: string | null;
|
|
42
|
-
reason?: string | null;
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Capability's precheck rejected the args against live state (no-op target, missing element,
|
|
47
|
-
* guard condition). Thrown before pushHistory / any write.
|
|
48
|
-
*/
|
|
49
|
-
export declare class PrecheckError extends CapabilityError {
|
|
50
|
-
reason: string | null;
|
|
51
|
-
constructor(message: string, { capability, reason }?: {
|
|
52
|
-
capability?: string | null;
|
|
53
|
-
reason?: string | null;
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Wired capability whose execute() is not implemented (generative stubs). Distinct from
|
|
58
|
-
* UnavailableError: "active but unfinished", reachable only if status was flipped to 'active'
|
|
59
|
-
* before the implementation landed.
|
|
60
|
-
*/
|
|
61
|
-
export declare class NotImplementedError extends CapabilityError {
|
|
62
|
-
constructor(capability: string, message?: string | null);
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Reserved for the future generative path: the backend credit check failed (402-style).
|
|
66
|
-
* Declared now so the agent loop can catch it without a follow-up refactor.
|
|
67
|
-
*/
|
|
68
|
-
export declare class InsufficientCreditsError extends CapabilityError {
|
|
69
|
-
required: number | null;
|
|
70
|
-
available: number | null;
|
|
71
|
-
constructor(message: string, { capability, required, available }?: {
|
|
72
|
-
capability?: string | null;
|
|
73
|
-
required?: number | null;
|
|
74
|
-
available?: number | null;
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
export declare class UnknownCapabilityError extends CapabilityError {
|
|
78
|
-
constructor(name: string);
|
|
79
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Capability layer error taxonomy (shared machinery).
|
|
3
|
-
*
|
|
4
|
-
* This is the ONE generic capability machinery, app-agnostic and layer-neutral
|
|
5
|
-
* (`src/capabilities/` imports nothing app-level — see `.agents/decisions/`). Both the studio
|
|
6
|
-
* registry (`src/studio/capabilities/`, the credit-metered superset caller) and the edit-mode
|
|
7
|
-
* registry (`src/core/edit/capabilities/`, the lean preview caller) build on it. It supersedes
|
|
8
|
-
* the previously duplicated `studio/capabilities/errors.js` + `core/edit/capabilities/errors.ts`.
|
|
9
|
-
*
|
|
10
|
-
* One base class so callers — today the UI, tomorrow the agent loop — branch on `err.code`
|
|
11
|
-
* instead of string-matching messages. Every error carries the offending capability name when
|
|
12
|
-
* known, so failures are traceable without a stack dive.
|
|
13
|
-
*/
|
|
14
|
-
export class CapabilityError extends Error {
|
|
15
|
-
code;
|
|
16
|
-
capability;
|
|
17
|
-
cause;
|
|
18
|
-
constructor(message, { code = 'CAPABILITY_ERROR', capability = null, cause = null } = {}) {
|
|
19
|
-
super(message);
|
|
20
|
-
this.name = 'CapabilityError';
|
|
21
|
-
this.code = code;
|
|
22
|
-
this.capability = capability;
|
|
23
|
-
if (cause)
|
|
24
|
-
this.cause = cause;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/** Input args failed the capability's inputSchema. Thrown before any write. */
|
|
28
|
-
export class ValidationError extends CapabilityError {
|
|
29
|
-
issues;
|
|
30
|
-
constructor(message, { capability = null, issues = [] } = {}) {
|
|
31
|
-
super(message, { code: 'VALIDATION', capability });
|
|
32
|
-
this.name = 'ValidationError';
|
|
33
|
-
this.issues = issues;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/** Capability is declared but not invokable yet (status: 'planned' / available()===false). */
|
|
37
|
-
export class UnavailableError extends CapabilityError {
|
|
38
|
-
reason;
|
|
39
|
-
constructor(message, { capability = null, reason = null } = {}) {
|
|
40
|
-
super(message, { code: 'UNAVAILABLE', capability });
|
|
41
|
-
this.name = 'UnavailableError';
|
|
42
|
-
this.reason = reason;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Capability's precheck rejected the args against live state (no-op target, missing element,
|
|
47
|
-
* guard condition). Thrown before pushHistory / any write.
|
|
48
|
-
*/
|
|
49
|
-
export class PrecheckError extends CapabilityError {
|
|
50
|
-
reason;
|
|
51
|
-
constructor(message, { capability = null, reason = null } = {}) {
|
|
52
|
-
super(message, { code: 'PRECHECK_FAILED', capability });
|
|
53
|
-
this.name = 'PrecheckError';
|
|
54
|
-
this.reason = reason;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Wired capability whose execute() is not implemented (generative stubs). Distinct from
|
|
59
|
-
* UnavailableError: "active but unfinished", reachable only if status was flipped to 'active'
|
|
60
|
-
* before the implementation landed.
|
|
61
|
-
*/
|
|
62
|
-
export class NotImplementedError extends CapabilityError {
|
|
63
|
-
constructor(capability, message = null) {
|
|
64
|
-
super(message || `Capability "${capability}" is not implemented yet`, { code: 'NOT_IMPLEMENTED', capability });
|
|
65
|
-
this.name = 'NotImplementedError';
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Reserved for the future generative path: the backend credit check failed (402-style).
|
|
70
|
-
* Declared now so the agent loop can catch it without a follow-up refactor.
|
|
71
|
-
*/
|
|
72
|
-
export class InsufficientCreditsError extends CapabilityError {
|
|
73
|
-
required;
|
|
74
|
-
available;
|
|
75
|
-
constructor(message, { capability = null, required = null, available = null } = {}) {
|
|
76
|
-
super(message, { code: 'INSUFFICIENT_CREDITS', capability });
|
|
77
|
-
this.name = 'InsufficientCreditsError';
|
|
78
|
-
this.required = required;
|
|
79
|
-
this.available = available;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
export class UnknownCapabilityError extends CapabilityError {
|
|
83
|
-
constructor(name) {
|
|
84
|
-
super(`Unknown capability "${name}"`, { code: 'UNKNOWN_CAPABILITY', capability: name });
|
|
85
|
-
this.name = 'UnknownCapabilityError';
|
|
86
|
-
}
|
|
87
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared capability machinery — app-agnostic, layer-neutral registry + validator +
|
|
3
|
-
* error taxonomy. Both `src/studio/capabilities/` and `src/core/edit/capabilities/` build their
|
|
4
|
-
* own registry instances and descriptors on top of this. Imports nothing app-level (see
|
|
5
|
-
* `.agents/decisions/`). This is the generic core of the AI-ready capability seam.
|
|
6
|
-
*/
|
|
7
|
-
export { createCapabilityRegistry, type CapabilityRegistry, type CapabilityDescriptor, type CapabilityInfo, type CapabilityKind, type CapabilityStatus, type Reactivity, type InvokeMeta, type InvokeMode, type PrecheckResult, type RegistryCtx, } from './registry.js';
|
|
8
|
-
export { validate, assertValid, type JsonSchema, type JsonType } from './schema.js';
|
|
9
|
-
export * from './errors.js';
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared capability machinery — app-agnostic, layer-neutral registry + validator +
|
|
3
|
-
* error taxonomy. Both `src/studio/capabilities/` and `src/core/edit/capabilities/` build their
|
|
4
|
-
* own registry instances and descriptors on top of this. Imports nothing app-level (see
|
|
5
|
-
* `.agents/decisions/`). This is the generic core of the AI-ready capability seam.
|
|
6
|
-
*/
|
|
7
|
-
export { createCapabilityRegistry, } from './registry.js';
|
|
8
|
-
export { validate, assertValid } from './schema.js';
|
|
9
|
-
export * from './errors.js';
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Capability registry — the single, named, validated chokepoint for edits (shared machinery).
|
|
3
|
-
*
|
|
4
|
-
* The ONE registry both callers build on (supersedes the duplicated
|
|
5
|
-
* `studio/capabilities/registry.js` + `core/edit/capabilities/registry.ts`). It is the SUPERSET:
|
|
6
|
-
* the studio uses the full pipeline (precheck → reversible history → credit cost → generative),
|
|
7
|
-
* the edit-mode caller uses the lean subset (validation + named dispatch + the preview/commit
|
|
8
|
-
* `mode`) and simply leaves the extra features unset. App-agnostic: `src/capabilities/` imports
|
|
9
|
-
* nothing app-level (see `.agents/decisions/`); all state/functions arrive via the injected ctx.
|
|
10
|
-
*
|
|
11
|
-
* Today the UI calls invoke()/invokeSync() with source:'ui'; tomorrow the agent loop calls the
|
|
12
|
-
* SAME registry with source:'agent'. Every mutation flows through one pipeline so history,
|
|
13
|
-
* validation, and availability are enforced in exactly one place.
|
|
14
|
-
*
|
|
15
|
-
* invoke() pipeline:
|
|
16
|
-
* 1. resolve unknown name -> UnknownCapabilityError
|
|
17
|
-
* 2. assertAvailable status:'planned' / available(ctx)===false -> UnavailableError (no writes)
|
|
18
|
-
* 3. validate args vs inputSchema -> ValidationError (no writes)
|
|
19
|
-
* 4. precheck PURE, no writes. Returns { ok, reason, willMutate }. !ok -> PrecheckError
|
|
20
|
-
* 5. commit only now: if reversible && willMutate !== false -> ctx.pushHistory()
|
|
21
|
-
* 6. execute the actual write
|
|
22
|
-
*
|
|
23
|
-
* `mode` is the edit-mode preview/commit distinction (ignored by studio capabilities):
|
|
24
|
-
* 'preview' → live side effect only, no host broadcast 'commit' → side effect + emit.
|
|
25
|
-
*/
|
|
26
|
-
import { type JsonSchema } from './schema.js';
|
|
27
|
-
export type CapabilityKind = 'mutation' | 'query' | 'generative';
|
|
28
|
-
export type Reactivity = 'live' | 'structural';
|
|
29
|
-
export type CapabilityStatus = 'active' | 'planned';
|
|
30
|
-
export type InvokeMode = 'preview' | 'commit';
|
|
31
|
-
export interface InvokeMeta {
|
|
32
|
-
source: string;
|
|
33
|
-
mode: InvokeMode;
|
|
34
|
-
}
|
|
35
|
-
export interface PrecheckResult {
|
|
36
|
-
ok: boolean;
|
|
37
|
-
reason?: string | null;
|
|
38
|
-
/** false suppresses the no-op history snapshot at the commit boundary. */
|
|
39
|
-
willMutate?: boolean;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* The optional history hook the registry calls at the commit boundary for reversible mutations.
|
|
43
|
-
* A caller's ctx may include it (the studio does) or not (edit-mode has no history). It is NOT a
|
|
44
|
-
* generic constraint — the ctx type is free-form per caller — the registry just reads pushHistory
|
|
45
|
-
* off it when present.
|
|
46
|
-
*/
|
|
47
|
-
export interface RegistryCtx {
|
|
48
|
-
pushHistory?: () => void;
|
|
49
|
-
}
|
|
50
|
-
export interface CapabilityDescriptor<Ctx = Record<string, unknown>, Args = Record<string, unknown>> {
|
|
51
|
-
name: string;
|
|
52
|
-
kind: CapabilityKind;
|
|
53
|
-
description?: string;
|
|
54
|
-
inputSchema?: JsonSchema;
|
|
55
|
-
status?: CapabilityStatus;
|
|
56
|
-
reversible?: boolean;
|
|
57
|
-
/** Cost class of applying the edit: 'live' (cheap in-place) vs 'structural' (rebuild). */
|
|
58
|
-
reactivity?: Reactivity;
|
|
59
|
-
available?: (ctx: Ctx) => boolean;
|
|
60
|
-
cost?: (args: Args, ctx: Ctx) => number;
|
|
61
|
-
precheck?: (args: Args, ctx: Ctx, meta: InvokeMeta) => PrecheckResult | Promise<PrecheckResult>;
|
|
62
|
-
execute: (args: Args, ctx: Ctx, meta: InvokeMeta) => unknown;
|
|
63
|
-
}
|
|
64
|
-
/** Public, function-free view of a capability — what the agent tool-catalog builder consumes. */
|
|
65
|
-
export interface CapabilityInfo {
|
|
66
|
-
name: string;
|
|
67
|
-
kind: CapabilityKind;
|
|
68
|
-
status: CapabilityStatus;
|
|
69
|
-
description: string;
|
|
70
|
-
inputSchema: JsonSchema;
|
|
71
|
-
reversible: boolean;
|
|
72
|
-
reactivity: Reactivity | null;
|
|
73
|
-
}
|
|
74
|
-
interface NormalizedCapability<Ctx> {
|
|
75
|
-
name: string;
|
|
76
|
-
kind: CapabilityKind;
|
|
77
|
-
status: CapabilityStatus;
|
|
78
|
-
description: string;
|
|
79
|
-
inputSchema: JsonSchema;
|
|
80
|
-
reversible: boolean;
|
|
81
|
-
reactivity: Reactivity | null;
|
|
82
|
-
available: (ctx: Ctx) => boolean;
|
|
83
|
-
cost: (args: never, ctx: Ctx) => number;
|
|
84
|
-
precheck: (args: never, ctx: Ctx, meta: InvokeMeta) => PrecheckResult | Promise<PrecheckResult>;
|
|
85
|
-
execute: (args: never, ctx: Ctx, meta: InvokeMeta) => unknown;
|
|
86
|
-
}
|
|
87
|
-
export interface CapabilityRegistry<Ctx = Record<string, unknown>> {
|
|
88
|
-
register: (descriptor: CapabilityDescriptor<Ctx>) => NormalizedCapability<Ctx>;
|
|
89
|
-
registerAll: (descriptors: CapabilityDescriptor<Ctx>[]) => CapabilityRegistry<Ctx>;
|
|
90
|
-
has: (name: string) => boolean;
|
|
91
|
-
get: (name: string) => NormalizedCapability<Ctx> | null;
|
|
92
|
-
/** Public catalog view of one capability by name (null if unknown). */
|
|
93
|
-
describe: (name: string) => CapabilityInfo | null;
|
|
94
|
-
list: (opts?: {
|
|
95
|
-
includePlanned?: boolean;
|
|
96
|
-
kind?: CapabilityKind | null;
|
|
97
|
-
}) => CapabilityInfo[];
|
|
98
|
-
isAvailable: (name: string) => boolean;
|
|
99
|
-
cost: (name: string, args?: Record<string, unknown>) => number;
|
|
100
|
-
invoke: (name: string, args?: Record<string, unknown>, opts?: Partial<InvokeMeta>) => Promise<unknown>;
|
|
101
|
-
invokeSync: (name: string, args?: Record<string, unknown>, opts?: Partial<InvokeMeta>) => unknown;
|
|
102
|
-
}
|
|
103
|
-
export declare function createCapabilityRegistry<Ctx = Record<string, unknown>>({ ctx }?: {
|
|
104
|
-
ctx?: Ctx;
|
|
105
|
-
}): CapabilityRegistry<Ctx>;
|
|
106
|
-
export {};
|