@volter/twin-moonshot 0.1.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/LICENSE +202 -0
- package/README.md +164 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +25 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +86 -0
- package/dist/src/moonshot-budget.d.ts +57 -0
- package/dist/src/moonshot-budget.js +142 -0
- package/dist/src/moonshot-capabilities.d.ts +4 -0
- package/dist/src/moonshot-capabilities.js +1200 -0
- package/dist/src/moonshot-conformance.d.ts +14 -0
- package/dist/src/moonshot-conformance.js +405 -0
- package/dist/src/moonshot-connector.d.ts +168 -0
- package/dist/src/moonshot-connector.js +416 -0
- package/dist/src/moonshot-models.d.ts +36 -0
- package/dist/src/moonshot-models.js +37 -0
- package/dist/src/moonshot-scenario.d.ts +54 -0
- package/dist/src/moonshot-scenario.js +175 -0
- package/dist/src/moonshot-server.d.ts +13 -0
- package/dist/src/moonshot-server.js +202 -0
- package/dist/src/moonshot-stub.d.ts +70 -0
- package/dist/src/moonshot-stub.js +222 -0
- package/dist/src/moonshot-twin.d.ts +144 -0
- package/dist/src/moonshot-twin.js +1647 -0
- package/dist/src/moonshot-types.d.ts +251 -0
- package/dist/src/moonshot-types.js +19 -0
- package/package.json +53 -0
- package/src/cli.ts +25 -0
- package/src/index.ts +129 -0
- package/src/moonshot-budget.ts +163 -0
- package/src/moonshot-capabilities.ts +1220 -0
- package/src/moonshot-conformance.ts +416 -0
- package/src/moonshot-connector.ts +465 -0
- package/src/moonshot-models.ts +89 -0
- package/src/moonshot-scenario.ts +194 -0
- package/src/moonshot-server.ts +220 -0
- package/src/moonshot-stub.ts +230 -0
- package/src/moonshot-twin.ts +1670 -0
- package/src/moonshot-types.ts +225 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ConformanceViolation = {
|
|
2
|
+
check: string;
|
|
3
|
+
detail: string;
|
|
4
|
+
};
|
|
5
|
+
export type MoonshotConformanceReport = {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
checksRun: number;
|
|
8
|
+
probes: number;
|
|
9
|
+
violations: ConformanceViolation[];
|
|
10
|
+
};
|
|
11
|
+
/** Run the offline conformance checks against a fresh temp root. */
|
|
12
|
+
export declare function checkMoonshotConformance(opts?: {
|
|
13
|
+
root?: string;
|
|
14
|
+
}): Promise<MoonshotConformanceReport>;
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// Moonshot twin conformance — an offline check with REAL TEETH: delete a handler branch and it
|
|
2
|
+
// goes RED. Moonshot publishes its own OpenAPI document, so the claimed surface has a first-party
|
|
3
|
+
// denominator; the harness still does not compare two constants (the false-green ADDING_A_TWIN.md
|
|
4
|
+
// §6 documents). Instead it runs THREE passes:
|
|
5
|
+
//
|
|
6
|
+
// 1. PROBES — one real request per CLAIMED endpoint, graded on the OUTCOME a live handler
|
|
7
|
+
// produces: an expected status set PLUS a predicate over the body. A handler that returned
|
|
8
|
+
// `{}`, or whose branch was deleted (so the router falls through to its not-found), fails.
|
|
9
|
+
// 2. BIJECTION — the probe table and the claimed-endpoint snapshot must match two ways, so a
|
|
10
|
+
// claim with no probe and a probe with no claim are both RED.
|
|
11
|
+
// 3. ROUTER_SURFACE — a HAND-AUTHORED census of every method/path pair `routeMoonshot` branches
|
|
12
|
+
// on, each carrying a live request that must actually REACH that branch (not fall through to
|
|
13
|
+
// the router's not-found). Plus MUST_NOT_SERVE: paths the vendor does not serve, which must
|
|
14
|
+
// answer the vendor-shaped not-found envelope.
|
|
15
|
+
//
|
|
16
|
+
// Fully offline + deterministic (drives the local handler against a temp root) so it runs without
|
|
17
|
+
// an API key. Honest scope: it checks the protocol envelope, NOT model output (a deterministic
|
|
18
|
+
// labeled stub by design).
|
|
19
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { handleMoonshotTwinRequest, MESSAGES_PREFIX, MOONSHOT_API_PREFIX } from "./moonshot-twin.js";
|
|
23
|
+
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
24
|
+
const CHAT = { model: 'kimi-k3', messages: [{ role: 'user', content: 'hi' }] };
|
|
25
|
+
const PROBES = [
|
|
26
|
+
{
|
|
27
|
+
key: 'POST /v1/chat/completions',
|
|
28
|
+
method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: CHAT, statuses: [200],
|
|
29
|
+
ok: (b) => isObj(b) && b.object === 'chat.completion' && Array.isArray(b.choices)
|
|
30
|
+
&& b.choices[0]?.message?.role === 'assistant'
|
|
31
|
+
&& typeof b.choices[0]?.message?.content === 'string'
|
|
32
|
+
&& isObj(b.usage) && typeof b.usage.total_tokens === 'number' && typeof b.usage.cached_tokens === 'number',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'POST /v1/responses',
|
|
36
|
+
method: 'POST', path: `${MOONSHOT_API_PREFIX}/responses`, body: { model: 'kimi-k3', input: 'hi' }, statuses: [200],
|
|
37
|
+
ok: (b) => isObj(b) && b.object === 'response' && b.status === 'completed' && Array.isArray(b.output)
|
|
38
|
+
&& b.output.some((o) => o.type === 'message')
|
|
39
|
+
&& isObj(b.usage) && typeof b.usage.total_tokens === 'number',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: 'POST /anthropic/v1/messages',
|
|
43
|
+
method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'hi' }], max_tokens: 100 }, statuses: [200],
|
|
44
|
+
ok: (b) => isObj(b) && b.type === 'message' && b.role === 'assistant' && Array.isArray(b.content)
|
|
45
|
+
&& b.content[0]?.type === 'thinking'
|
|
46
|
+
&& typeof b.stop_reason === 'string' && isObj(b.usage) && typeof b.usage.input_tokens === 'number',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'GET /v1/models',
|
|
50
|
+
method: 'GET', path: `${MOONSHOT_API_PREFIX}/models`, statuses: [200],
|
|
51
|
+
ok: (b) => isObj(b) && b.object === 'list' && Array.isArray(b.data) && b.data.some((m) => m.id === 'kimi-k3' && m.object === 'model' && m.owned_by === 'moonshot'),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'POST /v1/files',
|
|
55
|
+
method: 'POST', path: `${MOONSHOT_API_PREFIX}/files`, body: { purpose: 'batch', filename: 'in.jsonl', content: '{}' }, statuses: [200],
|
|
56
|
+
ok: (b) => isObj(b) && b.object === 'file' && b.purpose === 'batch' && b.status === 'ready' && String(b.id).startsWith('file_twin_'),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'GET /v1/files',
|
|
60
|
+
method: 'GET', path: `${MOONSHOT_API_PREFIX}/files`, statuses: [200],
|
|
61
|
+
seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'seed.jsonl', content: '{}' }); },
|
|
62
|
+
ok: (b) => isObj(b) && b.object === 'list' && b.data.some((f) => f.filename === 'seed.jsonl'),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: 'GET /v1/files/{file_id}',
|
|
66
|
+
method: 'GET', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1`, statuses: [200],
|
|
67
|
+
seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'one.jsonl', content: 'x' }); },
|
|
68
|
+
ok: (b) => isObj(b) && b.id === 'file_twin_1' && b.filename === 'one.jsonl',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: 'GET /v1/files/{file_id}/content',
|
|
72
|
+
method: 'GET', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1/content`, statuses: [200],
|
|
73
|
+
seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'one.jsonl', content: '{"custom_id":"a"}' }); },
|
|
74
|
+
ok: (b) => b === '{"custom_id":"a"}',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: 'DELETE /v1/files/{file_id}',
|
|
78
|
+
method: 'DELETE', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1`, statuses: [200],
|
|
79
|
+
seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'one.jsonl', content: 'x' }); },
|
|
80
|
+
ok: (b) => isObj(b) && b.deleted === true && b.id === 'file_twin_1' && b.object === 'file',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
key: 'POST /v1/batches',
|
|
84
|
+
method: 'POST', path: `${MOONSHOT_API_PREFIX}/batches`, body: { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' }, statuses: [200],
|
|
85
|
+
seed: async (h) => { await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'in.jsonl', content: 'x' }); },
|
|
86
|
+
ok: (b) => isObj(b) && b.object === 'batch' && b.status === 'validating' && b.endpoint === '/v1/chat/completions' && typeof b.expires_at === 'number',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: 'GET /v1/batches',
|
|
90
|
+
method: 'GET', path: `${MOONSHOT_API_PREFIX}/batches`, statuses: [200],
|
|
91
|
+
seed: async (h) => {
|
|
92
|
+
await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'in.jsonl', content: 'x' });
|
|
93
|
+
await h('POST', `${MOONSHOT_API_PREFIX}/batches`, { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' });
|
|
94
|
+
},
|
|
95
|
+
ok: (b) => isObj(b) && b.object === 'list' && b.data.some((x) => x.id === 'batch_twin_1'),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: 'GET /v1/batches/{batch_id}',
|
|
99
|
+
method: 'GET', path: `${MOONSHOT_API_PREFIX}/batches/batch_twin_1`, statuses: [200],
|
|
100
|
+
seed: async (h) => {
|
|
101
|
+
await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'in.jsonl', content: 'x' });
|
|
102
|
+
await h('POST', `${MOONSHOT_API_PREFIX}/batches`, { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' });
|
|
103
|
+
},
|
|
104
|
+
ok: (b) => isObj(b) && b.id === 'batch_twin_1' && b.input_file_id === 'file_twin_1',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
key: 'POST /v1/batches/{batch_id}/cancel',
|
|
108
|
+
method: 'POST', path: `${MOONSHOT_API_PREFIX}/batches/batch_twin_1/cancel`, statuses: [200],
|
|
109
|
+
seed: async (h) => {
|
|
110
|
+
await h('POST', `${MOONSHOT_API_PREFIX}/files`, { purpose: 'batch', filename: 'in.jsonl', content: 'x' });
|
|
111
|
+
await h('POST', `${MOONSHOT_API_PREFIX}/batches`, { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' });
|
|
112
|
+
},
|
|
113
|
+
ok: (b) => isObj(b) && b.status === 'cancelling' && typeof b.cancelling_at === 'number',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: 'GET /v1/users/me/balance',
|
|
117
|
+
method: 'GET', path: `${MOONSHOT_API_PREFIX}/users/me/balance`, statuses: [200],
|
|
118
|
+
ok: (b) => isObj(b) && b.code === 0 && b.scode === '0x0' && b.status === true
|
|
119
|
+
&& isObj(b.data) && typeof b.data.available_balance === 'number' && typeof b.data.voucher_balance === 'number' && typeof b.data.cash_balance === 'number',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: 'POST /v1/tokenizers/estimate-token-count',
|
|
123
|
+
method: 'POST', path: `${MOONSHOT_API_PREFIX}/tokenizers/estimate-token-count`, body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'hello world, estimate me' }] }, statuses: [200],
|
|
124
|
+
ok: (b) => isObj(b) && isObj(b.data) && typeof b.data.total_tokens === 'number' && b.data.total_tokens > 0,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: 'POST /v1/signatures/verify',
|
|
128
|
+
method: 'POST', path: `${MOONSHOT_API_PREFIX}/signatures/verify`, body: { nonce: 'n-1', timestamp: 1786338000123, model: 'kimi-k2.7-code', signature: 'reqsigv1_not-a-real-token' }, statuses: [200],
|
|
129
|
+
ok: (b) => isObj(b) && b.valid === false,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
key: 'POST /v1/tools/search',
|
|
133
|
+
method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/search`, body: { text_query: 'kimi api limits', limit: 3 }, statuses: [200],
|
|
134
|
+
ok: (b) => isObj(b) && Array.isArray(b.search_results) && b.search_results.length === 3
|
|
135
|
+
&& typeof b.search_results[0]?.url === 'string' && typeof b.search_results[0]?.snippet === 'string',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
key: 'POST /v1/tools/search_pro',
|
|
139
|
+
method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/search_pro`, body: { text_query: 'kimi api limits', sites: ['platform.kimi.ai'] }, statuses: [200],
|
|
140
|
+
ok: (b) => isObj(b) && Array.isArray(b.search_results) && b.search_results.length === 5
|
|
141
|
+
&& Array.isArray(b.search_results[0]?.chunks),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
key: 'POST /v1/tools/fetch',
|
|
145
|
+
method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/fetch`, body: { url: 'https://platform.kimi.ai/docs' }, statuses: [200],
|
|
146
|
+
ok: (b) => isObj(b) && b.url === 'https://platform.kimi.ai/docs' && typeof b.markdown === 'string' && b.markdown.includes('[twin-stub]'),
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
/**
|
|
150
|
+
* THE ROUTER CENSUS — hand-authored from `routeMoonshot`'s dispatch branches, paired with a
|
|
151
|
+
* request that REACHES that branch. Each entry is checked live: the router must BRANCH on it
|
|
152
|
+
* (i.e. not fall through to its own `Unknown request URL` not-found) — so deleting a handler
|
|
153
|
+
* branch reddens the census by name, independently of that endpoint's own probe.
|
|
154
|
+
*/
|
|
155
|
+
const ROUTER_SURFACE = [
|
|
156
|
+
{ key: 'GET /v1/models', method: 'GET', path: `${MOONSHOT_API_PREFIX}/models` },
|
|
157
|
+
{ key: 'POST /v1/chat/completions', method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: CHAT },
|
|
158
|
+
{ key: 'POST /v1/responses', method: 'POST', path: `${MOONSHOT_API_PREFIX}/responses`, body: { model: 'kimi-k3', input: 'x' } },
|
|
159
|
+
{ key: 'POST /v1/tokenizers/estimate-token-count', method: 'POST', path: `${MOONSHOT_API_PREFIX}/tokenizers/estimate-token-count`, body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }] } },
|
|
160
|
+
{ key: 'POST /v1/signatures/verify', method: 'POST', path: `${MOONSHOT_API_PREFIX}/signatures/verify`, body: { nonce: 'n', timestamp: 1, model: 'kimi-k3', signature: 's' } },
|
|
161
|
+
{ key: 'POST /v1/tools/search', method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/search`, body: { text_query: 'x' } },
|
|
162
|
+
{ key: 'POST /v1/tools/search_pro', method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/search_pro`, body: { text_query: 'x' } },
|
|
163
|
+
{ key: 'POST /v1/tools/fetch', method: 'POST', path: `${MOONSHOT_API_PREFIX}/tools/fetch`, body: { url: 'https://x.example' } },
|
|
164
|
+
{ key: 'GET /v1/users/me/balance', method: 'GET', path: `${MOONSHOT_API_PREFIX}/users/me/balance` },
|
|
165
|
+
{ key: 'POST /v1/files', method: 'POST', path: `${MOONSHOT_API_PREFIX}/files`, body: { purpose: 'batch', filename: 'a.jsonl', content: 'x' } },
|
|
166
|
+
{ key: 'GET /v1/files', method: 'GET', path: `${MOONSHOT_API_PREFIX}/files` },
|
|
167
|
+
{ key: 'GET /v1/files/{file_id}', method: 'GET', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1` },
|
|
168
|
+
{ key: 'GET /v1/files/{file_id}/content', method: 'GET', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1/content` },
|
|
169
|
+
{ key: 'DELETE /v1/files/{file_id}', method: 'DELETE', path: `${MOONSHOT_API_PREFIX}/files/file_twin_1` },
|
|
170
|
+
{ key: 'POST /v1/batches', method: 'POST', path: `${MOONSHOT_API_PREFIX}/batches`, body: { input_file_id: 'file_twin_1', endpoint: '/v1/chat/completions', completion_window: '24h' } },
|
|
171
|
+
{ key: 'GET /v1/batches', method: 'GET', path: `${MOONSHOT_API_PREFIX}/batches` },
|
|
172
|
+
{ key: 'GET /v1/batches/{batch_id}', method: 'GET', path: `${MOONSHOT_API_PREFIX}/batches/batch_twin_1` },
|
|
173
|
+
{ key: 'POST /v1/batches/{batch_id}/cancel', method: 'POST', path: `${MOONSHOT_API_PREFIX}/batches/batch_twin_1/cancel` },
|
|
174
|
+
{ key: 'POST /anthropic/v1/messages', method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: { model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 } },
|
|
175
|
+
];
|
|
176
|
+
/** The literal the router falls through to when NO branch matched. A census entry answering this
|
|
177
|
+
* means its branch is gone. Kept as a LITERAL — importing the handler's string would be a
|
|
178
|
+
* tautology that could not catch the message drifting. */
|
|
179
|
+
const ROUTER_MISS = 'Unknown request URL';
|
|
180
|
+
const ROUTER_SURFACE_KEYS = ROUTER_SURFACE.map((e) => e.key);
|
|
181
|
+
/**
|
|
182
|
+
* Surface the twin must NOT serve: the bare `/openai/v1` prefix (that is GROQ's shape, not
|
|
183
|
+
* Moonshot's), paths outside both prefixes, and the kimi-k2.x models on the kimi-k3-only
|
|
184
|
+
* endpoints (the OpenAPI's per-request model enums are the closed sets). Each must answer the
|
|
185
|
+
* vendor-shaped not-found/error envelope.
|
|
186
|
+
*/
|
|
187
|
+
const MUST_NOT_SERVE = [
|
|
188
|
+
{ method: 'POST', path: '/openai/v1/chat/completions', body: CHAT, why: 'the /openai/v1 prefix is GroqCloud\'s shape; Moonshot serves /v1' },
|
|
189
|
+
{ method: 'GET', path: '/v1/fine_tuning/jobs', why: 'OpenAI has this endpoint; Moonshot\'s OpenAPI does not' },
|
|
190
|
+
{ method: 'GET', path: '/v1/embeddings', why: 'Moonshot publishes no embeddings endpoint' },
|
|
191
|
+
{ method: 'GET', path: `${MOONSHOT_API_PREFIX}/models/kimi-k3`, why: 'Moonshot\'s OpenAPI declares NO /v1/models/{model} retrieve operation — serving a 200 here was a 200 for an unmodeled route' },
|
|
192
|
+
{ method: 'POST', path: `${MOONSHOT_API_PREFIX}/responses`, body: { model: 'kimi-k2.6', input: 'x' }, why: 'the Responses API is kimi-k3 only' },
|
|
193
|
+
{ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: { model: 'kimi-k2.7-code', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 }, why: 'the Messages API is kimi-k3 only' },
|
|
194
|
+
];
|
|
195
|
+
/** Run the offline conformance checks against a fresh temp root. */
|
|
196
|
+
export async function checkMoonshotConformance(opts = {}) {
|
|
197
|
+
const violations = [];
|
|
198
|
+
let checksRun = 0;
|
|
199
|
+
const fail = (check, detail) => violations.push({ check, detail });
|
|
200
|
+
// ── 1. PROBES: each in its OWN throwaway root, so a probe's seed can't leak into another. ──
|
|
201
|
+
for (const probe of PROBES) {
|
|
202
|
+
checksRun++;
|
|
203
|
+
// A FRESH root per probe, always. `opts.root` is the PARENT directory, never a shared root:
|
|
204
|
+
// sharing it let `nextId` ratchet across probes, so the `files/file_twin_1` probe read the id
|
|
205
|
+
// an earlier probe had minted and `world-moonshot conformance --root DIR` reported false
|
|
206
|
+
// violations (the groq pack's §9 round-two finding, back-applied here from the start).
|
|
207
|
+
const root = mkdtempSync(join(opts.root ?? tmpdir(), 'moonshot-conf-'));
|
|
208
|
+
const h = (method, path, body) => handleMoonshotTwinRequest({ method, path, ...(body === undefined ? {} : { body: JSON.stringify(body) }), root });
|
|
209
|
+
try {
|
|
210
|
+
if (probe.seed)
|
|
211
|
+
await probe.seed(h);
|
|
212
|
+
const res = await h(probe.method, probe.path, probe.body);
|
|
213
|
+
if (!probe.statuses.includes(res.status)) {
|
|
214
|
+
fail(`probe:${probe.key}`, `status ${res.status} (expected one of ${probe.statuses.join('/')}) body=${JSON.stringify(res.body).slice(0, 200)}`);
|
|
215
|
+
}
|
|
216
|
+
else if (!probe.ok(res.body, res.status)) {
|
|
217
|
+
fail(`probe:${probe.key}`, `body did not satisfy the live-handler predicate: ${JSON.stringify(res.body).slice(0, 300)}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
rmSync(root, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// ── 2. BIJECTION: probe table ⇄ router census, both directions. ──
|
|
225
|
+
checksRun++;
|
|
226
|
+
const probeKeys = new Set(PROBES.map((p) => p.key));
|
|
227
|
+
for (const key of ROUTER_SURFACE_KEYS)
|
|
228
|
+
if (!probeKeys.has(key))
|
|
229
|
+
fail('bijection', `router branch "${key}" has no probe`);
|
|
230
|
+
for (const key of probeKeys)
|
|
231
|
+
if (!ROUTER_SURFACE_KEYS.includes(key))
|
|
232
|
+
fail('bijection', `probe "${key}" is not in the hand-authored router census`);
|
|
233
|
+
if (PROBES.length !== new Set(PROBES.map((p) => p.key)).size)
|
|
234
|
+
fail('bijection', 'duplicate probe key');
|
|
235
|
+
if (ROUTER_SURFACE.length !== ROUTER_SURFACE_KEYS.length)
|
|
236
|
+
fail('bijection', 'the router census and its key list disagree');
|
|
237
|
+
// ── 2b. THE CENSUS HAS TEETH: every declared branch must actually be REACHED. A branch whose
|
|
238
|
+
// handler was deleted falls through to the router's own not-found, which is what this
|
|
239
|
+
// catches — independently of that endpoint's own probe.
|
|
240
|
+
for (const entry of ROUTER_SURFACE) {
|
|
241
|
+
checksRun++;
|
|
242
|
+
const root = mkdtempSync(join(opts.root ?? tmpdir(), 'moonshot-conf-'));
|
|
243
|
+
try {
|
|
244
|
+
const res = await handleMoonshotTwinRequest({ method: entry.method, path: entry.path, ...(entry.body === undefined ? {} : { body: JSON.stringify(entry.body) }), root });
|
|
245
|
+
const message = String(res.body?.error?.message ?? res.body?.error?.message ?? '');
|
|
246
|
+
if (message.includes(ROUTER_MISS)) {
|
|
247
|
+
fail('router_surface', `${entry.key} fell through to the router's not-found — the branch is gone (answered ${res.status}: ${message})`);
|
|
248
|
+
}
|
|
249
|
+
if (!ROUTER_SURFACE_KEYS.includes(entry.key))
|
|
250
|
+
fail('router_surface', `${entry.key} is exercised but not declared`);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
rmSync(root, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// ── 3. The twin must not serve surface it does not model (or the vendor does not have). ──
|
|
257
|
+
{
|
|
258
|
+
const root = opts.root ?? mkdtempSync(join(tmpdir(), 'moonshot-conf-'));
|
|
259
|
+
try {
|
|
260
|
+
for (const entry of MUST_NOT_SERVE) {
|
|
261
|
+
checksRun++;
|
|
262
|
+
const res = await handleMoonshotTwinRequest({ method: entry.method, path: entry.path, ...(entry.body === undefined ? {} : { body: JSON.stringify(entry.body) }), root });
|
|
263
|
+
if (res.status < 400)
|
|
264
|
+
fail('must_not_serve', `${entry.method} ${entry.path} answered ${res.status} — ${entry.why}`);
|
|
265
|
+
}
|
|
266
|
+
// ── 4. The error envelope on /v1 carries `error.message` (REQUIRED) plus only keys
|
|
267
|
+
// Moonshot's ErrorResponse schema declares: message, type, code. The key list is a
|
|
268
|
+
// LITERAL here — asserting against the handler's own constant would be a tautology.
|
|
269
|
+
checksRun++;
|
|
270
|
+
const ERROR_OBJECT_KEYS = new Set(['message', 'type', 'code']);
|
|
271
|
+
const bad = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ messages: [] }), root });
|
|
272
|
+
const eb = bad.body;
|
|
273
|
+
if (bad.status !== 400 || !isObj(eb?.error)) {
|
|
274
|
+
fail('error.envelope', `missing model did not yield a 400 with an error envelope (status ${bad.status})`);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
const undeclared = Object.keys(eb.error).filter((k) => !ERROR_OBJECT_KEYS.has(k));
|
|
278
|
+
if (undeclared.length)
|
|
279
|
+
fail('error.envelope', `error object carries key(s) Moonshot's ErrorResponse schema does not declare: ${undeclared.join(', ')}`);
|
|
280
|
+
if (typeof eb.error.message !== 'string' || !eb.error.message)
|
|
281
|
+
fail('error.envelope', 'error.message is not a non-empty string');
|
|
282
|
+
if (eb.error.type !== 'invalid_request_error')
|
|
283
|
+
fail('error.envelope', `error.type is ${String(eb.error.type)}`);
|
|
284
|
+
}
|
|
285
|
+
// ── 4b. The Messages surface answers its OWN envelope ({type:'error',error:{type,message}})
|
|
286
|
+
// on EVERY failure — the validation 400 AND the cross-cutting 401/429/503/405 that
|
|
287
|
+
// fire before routing (each of which used to leak the /v1 envelope regardless of
|
|
288
|
+
// prefix, the round-two finding). Anthropic's own error types apply here.
|
|
289
|
+
checksRun++;
|
|
290
|
+
const anthropicShape = (b) => {
|
|
291
|
+
const mb = b;
|
|
292
|
+
return mb?.type === 'error' && isObj(mb.error) && typeof mb.error.type === 'string' && typeof mb.error.message === 'string' && !('code' in mb.error);
|
|
293
|
+
};
|
|
294
|
+
const mErr = await handleMoonshotTwinRequest({ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }] }), root });
|
|
295
|
+
if (mErr.status !== 400)
|
|
296
|
+
fail('messages.error_envelope', `missing max_tokens answered ${mErr.status}, not 400`);
|
|
297
|
+
else {
|
|
298
|
+
if (!anthropicShape(mErr.body))
|
|
299
|
+
fail('messages.error_envelope', 'Messages error body is not the MessagesErrorResponse shape { type, error:{type,message} }');
|
|
300
|
+
if ('error' in mErr.body && isObj(mErr.body.error) && 'code' in mErr.body.error)
|
|
301
|
+
fail('messages.error_envelope', 'Messages error envelope carries a `code` key its schema does not declare');
|
|
302
|
+
}
|
|
303
|
+
const cross = [
|
|
304
|
+
['401', await handleMoonshotTwinRequest({ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 }), root, headers: { 'x-api-key': '' } }), 'authentication_error'],
|
|
305
|
+
['429', await handleMoonshotTwinRequest({ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 }), root, headers: { authorization: 'Bearer sk-twin', 'x-twin-force-rate-limit': '1' } }), 'rate_limit_error'],
|
|
306
|
+
['503', await handleMoonshotTwinRequest({ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 }), root, headers: { authorization: 'Bearer sk-twin', 'x-twin-force-server-unavailable': '1' } }), 'overloaded_error'],
|
|
307
|
+
['405', await handleMoonshotTwinRequest({ method: 'POST', path: `${MESSAGES_PREFIX}/messages`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 10 }), root, readOnly: true }), 'invalid_request_error'],
|
|
308
|
+
];
|
|
309
|
+
for (const [what, res, wantType] of cross) {
|
|
310
|
+
if (res.status !== Number(what))
|
|
311
|
+
fail('messages.error_envelope', `the ${what} cross-cutting failure answered ${res.status}`);
|
|
312
|
+
else if (!anthropicShape(res.body))
|
|
313
|
+
fail('messages.error_envelope', `the ${what} cross-cutting failure leaked a non-Anthropic envelope: ${JSON.stringify(res.body).slice(0, 120)}`);
|
|
314
|
+
else if (res.body.error.type !== wantType)
|
|
315
|
+
fail('messages.error_envelope', `the ${what} cross-cutting failure answered error.type ${res.body.error.type}, not ${wantType}`);
|
|
316
|
+
}
|
|
317
|
+
// ── 5. Per-model parameter gating, straight from the OpenAPI's per-request schemas: ──
|
|
318
|
+
// kimi-k2.7-code REJECTS thinking.type 'disabled'; kimi-k2.6 accepts it; kimi-k3 has
|
|
319
|
+
// no thinking parameter at all; reasoning_effort is kimi-k3-only.
|
|
320
|
+
checksRun++;
|
|
321
|
+
const k27Disabled = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ model: 'kimi-k2.7-code', messages: [{ role: 'user', content: 'x' }], thinking: { type: 'disabled' } }), root });
|
|
322
|
+
const k26Disabled = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ model: 'kimi-k2.6', messages: [{ role: 'user', content: 'x' }], thinking: { type: 'disabled' } }), root });
|
|
323
|
+
const k3Thinking = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], thinking: { type: 'enabled' } }), root });
|
|
324
|
+
const k3Effort = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], reasoning_effort: 'low' }), root });
|
|
325
|
+
const k26Effort = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ model: 'kimi-k2.6', messages: [{ role: 'user', content: 'x' }], reasoning_effort: 'low' }), root });
|
|
326
|
+
if (k27Disabled.status !== 400)
|
|
327
|
+
fail('chat.per_model_gating', `kimi-k2.7-code thinking.type 'disabled' answered ${k27Disabled.status}, not 400`);
|
|
328
|
+
if (k26Disabled.status !== 200)
|
|
329
|
+
fail('chat.per_model_gating', `kimi-k2.6 thinking.type 'disabled' answered ${k26Disabled.status}, not 200`);
|
|
330
|
+
if (k3Thinking.status !== 400)
|
|
331
|
+
fail('chat.per_model_gating', `kimi-k3 'thinking' answered ${k3Thinking.status}, not 400 (kimi-k3 uses reasoning_effort)`);
|
|
332
|
+
if (k3Effort.status !== 200)
|
|
333
|
+
fail('chat.per_model_gating', `kimi-k3 reasoning_effort answered ${k3Effort.status}, not 200`);
|
|
334
|
+
if (k26Effort.status !== 400)
|
|
335
|
+
fail('chat.per_model_gating', `kimi-k2.6 reasoning_effort answered ${k26Effort.status}, not 400`);
|
|
336
|
+
// ── 6. The streaming chunk sequence, including Moonshot's usage-bearing tail chunk. ──
|
|
337
|
+
checksRun++;
|
|
338
|
+
const events = [];
|
|
339
|
+
await handleMoonshotTwinRequest({
|
|
340
|
+
method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, root,
|
|
341
|
+
body: JSON.stringify({ ...CHAT, stream: true }),
|
|
342
|
+
sseSink: (e) => events.push(e),
|
|
343
|
+
});
|
|
344
|
+
const hasRole = events.some((e) => !e.done && e.data.choices?.[0]?.delta?.role === 'assistant');
|
|
345
|
+
const done = events.length > 0 && events[events.length - 1].done === true;
|
|
346
|
+
const firstObj = events[0]?.data?.object;
|
|
347
|
+
const tail = events[events.length - 2]?.data;
|
|
348
|
+
if (!hasRole)
|
|
349
|
+
fail('chat.stream', 'no role delta chunk');
|
|
350
|
+
if (!done)
|
|
351
|
+
fail('chat.stream', 'stream did not end with [DONE]');
|
|
352
|
+
if (firstObj !== 'chat.completion.chunk')
|
|
353
|
+
fail('chat.stream', `first chunk object is ${String(firstObj)}`);
|
|
354
|
+
if (!tail || !Array.isArray(tail.choices) || tail.choices.length !== 0 || !isObj(tail.usage)) {
|
|
355
|
+
fail('chat.stream', 'final chunk is not Moonshot\'s empty-choices usage tail');
|
|
356
|
+
}
|
|
357
|
+
// ── 7. The Anthropic-compatible streaming grammar: named events, message_start →
|
|
358
|
+
// thinking/text deltas → message_delta → message_stop, NO [DONE] sentinel. ──
|
|
359
|
+
checksRun++;
|
|
360
|
+
const mEvents = [];
|
|
361
|
+
await handleMoonshotTwinRequest({
|
|
362
|
+
method: 'POST', path: `${MESSAGES_PREFIX}/messages`, root,
|
|
363
|
+
body: JSON.stringify({ model: 'kimi-k3', messages: [{ role: 'user', content: 'x' }], max_tokens: 200, stream: true }),
|
|
364
|
+
messagesSseSink: (e) => mEvents.push(e),
|
|
365
|
+
});
|
|
366
|
+
const names = mEvents.filter((e) => !e.done).map((e) => e.event ?? '');
|
|
367
|
+
if (names[0] !== 'message_start')
|
|
368
|
+
fail('messages.stream', `first event is ${String(names[0])}, not message_start`);
|
|
369
|
+
if (!names.includes('content_block_delta') || !names.includes('content_block_stop'))
|
|
370
|
+
fail('messages.stream', 'no content_block deltas');
|
|
371
|
+
if (names[names.length - 1] !== 'message_stop')
|
|
372
|
+
fail('messages.stream', `last event is ${String(names[names.length - 1])}, not message_stop`);
|
|
373
|
+
if (mEvents.some((e) => e.done === false && e.data === undefined && e.event === undefined))
|
|
374
|
+
fail('messages.stream', 'unnamed event in the Anthropic grammar');
|
|
375
|
+
// ── 8. tool_calls envelope when tools are provided. ──
|
|
376
|
+
checksRun++;
|
|
377
|
+
const tool = await handleMoonshotTwinRequest({
|
|
378
|
+
method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, root,
|
|
379
|
+
body: JSON.stringify({ ...CHAT, tools: [{ type: 'function', function: { name: 'get_weather', parameters: { type: 'object', properties: { city: { type: 'string' } } } } }] }),
|
|
380
|
+
});
|
|
381
|
+
const choice = tool.body.choices?.[0];
|
|
382
|
+
if (choice?.finish_reason !== 'tool_calls')
|
|
383
|
+
fail('chat.tool_calls', 'finish_reason not tool_calls');
|
|
384
|
+
const calls = choice?.message?.tool_calls;
|
|
385
|
+
if (!Array.isArray(calls) || calls[0]?.type !== 'function' || calls[0]?.function?.name !== 'get_weather')
|
|
386
|
+
fail('chat.tool_calls', 'no get_weather function tool_call');
|
|
387
|
+
// ── 9. Thinking mode surfaces reasoning_content; disabled k2.6 does not. ──
|
|
388
|
+
checksRun++;
|
|
389
|
+
const k3 = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify(CHAT), root });
|
|
390
|
+
const k3Msg = k3.body.choices?.[0]?.message;
|
|
391
|
+
if (typeof k3Msg?.reasoning_content !== 'string' || !k3Msg.reasoning_content.includes('[twin-stub:kimi-k3]')) {
|
|
392
|
+
fail('chat.reasoning_content', 'kimi-k3 (always thinking) did not surface reasoning_content');
|
|
393
|
+
}
|
|
394
|
+
const k26off = await handleMoonshotTwinRequest({ method: 'POST', path: `${MOONSHOT_API_PREFIX}/chat/completions`, body: JSON.stringify({ model: 'kimi-k2.6', messages: [{ role: 'user', content: 'x' }], thinking: { type: 'disabled' } }), root });
|
|
395
|
+
const k26Msg = k26off.body.choices?.[0]?.message;
|
|
396
|
+
if ('reasoning_content' in (k26Msg ?? {}))
|
|
397
|
+
fail('chat.reasoning_content', "kimi-k2.6 with thinking.type 'disabled' surfaced reasoning_content");
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
if (opts.root === undefined)
|
|
401
|
+
rmSync(root, { recursive: true, force: true });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return { ok: violations.length === 0, checksRun, probes: PROBES.length, violations };
|
|
405
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { PerformContext, PushOutcome, RemoteExecute, SyncResource, TwinAction } from '@volter/world-core';
|
|
2
|
+
import { MoonshotBudget, type MoonshotBudgetOptions } from './moonshot-budget.js';
|
|
3
|
+
/**
|
|
4
|
+
* The injected real-Moonshot boundary. `request` issues ONE Moonshot REST call:
|
|
5
|
+
* method — 'GET' | 'POST' | 'DELETE'
|
|
6
|
+
* path — e.g. '/v1/files' or '/v1/batches/batch_123/cancel'
|
|
7
|
+
* body — JSON body for POST (omitted otherwise)
|
|
8
|
+
* Returns the parsed JSON (an object, a `{ data }` list, or an `{ error }` envelope).
|
|
9
|
+
*/
|
|
10
|
+
export type MoonshotExecute = (method: 'GET' | 'POST' | 'DELETE', path: string, body?: Record<string, unknown>) => Promise<{
|
|
11
|
+
data?: any;
|
|
12
|
+
error?: {
|
|
13
|
+
message?: string;
|
|
14
|
+
type?: string;
|
|
15
|
+
};
|
|
16
|
+
[k: string]: unknown;
|
|
17
|
+
}>;
|
|
18
|
+
/** Construction options for the live executor. `budget` cannot be null and cannot be loosened. */
|
|
19
|
+
export type LiveMoonshotOptions = {
|
|
20
|
+
/** Injected `fetch`, so a test can COUNT the requests the guard did or did not let through. */
|
|
21
|
+
fetchImpl?: typeof fetch;
|
|
22
|
+
/** An existing budget to share across executors. Omit and one is constructed. Cannot be null. */
|
|
23
|
+
budget?: MoonshotBudget;
|
|
24
|
+
/** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
|
|
25
|
+
budgetOptions?: MoonshotBudgetOptions;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A live executor against the real Moonshot REST API (the user's own API key). Sends the required
|
|
29
|
+
* `Authorization: Bearer` header. Never imported by the pack's own serve path — only constructed
|
|
30
|
+
* by a caller that opts into real I/O.
|
|
31
|
+
*
|
|
32
|
+
* THIS IS THE ONE PLACE this pack issues a live `api.moonshot.ai` request, and therefore the one
|
|
33
|
+
* place the rate budget has to be enforced. EVERY call is guarded: the budget is charged BEFORE
|
|
34
|
+
* the request goes out (`checkBudget`, which THROWS `MoonshotBudgetError` instead of returning
|
|
35
|
+
* when the ceiling or a cooldown says stop) and the response is fed back (`recordCall`) so a
|
|
36
|
+
* `retry-after` / 429 / `x-ratelimit-remaining: 0` signal becomes a persisted cooldown that makes
|
|
37
|
+
* every later call fail fast WITHOUT touching Moonshot. There is deliberately no OPTION to
|
|
38
|
+
* disable the guard, and no value a caller can pass for `budget` that yields an unguarded client.
|
|
39
|
+
* What that does NOT claim is immunity from a caller who WANTS one: a fresh `budgetOptions.path`
|
|
40
|
+
* per construction, or an injected clock, restores the allowance, because the same seam tests
|
|
41
|
+
* need cannot be denied to a determined caller in the same process. See `moonshot-budget.ts` and
|
|
42
|
+
* the kernel header for the limits of the guarantee.
|
|
43
|
+
*/
|
|
44
|
+
export declare function liveMoonshotExecute(apiKey: string, base?: string, opts?: LiveMoonshotOptions): MoonshotExecute;
|
|
45
|
+
/** Map a real-Moonshot model object → a twin sync resource. */
|
|
46
|
+
export declare function mapModel(m: Record<string, unknown>): SyncResource;
|
|
47
|
+
/** Map a real-Moonshot File object → a twin sync resource. */
|
|
48
|
+
export declare function mapFile(f: Record<string, unknown>): SyncResource;
|
|
49
|
+
/** Map a real-Moonshot Batch object → a twin sync resource. */
|
|
50
|
+
export declare function mapBatch(b: Record<string, unknown>): SyncResource;
|
|
51
|
+
/** Map the real-Moonshot balance → the twin's 'balance' resource (id 'me'). */
|
|
52
|
+
export declare function mapBalance(b: Record<string, unknown>): SyncResource;
|
|
53
|
+
/** Pull all modeled real collections via the executor and map them to twin sync resources. */
|
|
54
|
+
export declare function pullMoonshotState(execute: MoonshotExecute): Promise<SyncResource[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Pull from real Moonshot and fold into the twin (mirror seeding). Observation entries are
|
|
57
|
+
* content-addressed, so a re-pull of identical state appends nothing.
|
|
58
|
+
*
|
|
59
|
+
* `occurredAt` has NO pinned default on purpose: the kernel hashes an observed event over
|
|
60
|
+
* (occurredAt + post-state), so under a fixed poll time a vendor value that REVERTS across polls
|
|
61
|
+
* collides with its own earlier observation and the fold reports a phantom delta while the
|
|
62
|
+
* projection keeps the stale value (ADDING_A_TWIN.md §6). Callers pass a moving timestamp.
|
|
63
|
+
*/
|
|
64
|
+
export declare function syncMoonshotFromReal(execute: MoonshotExecute, opts: {
|
|
65
|
+
root?: string;
|
|
66
|
+
occurredAt: string;
|
|
67
|
+
}): Promise<{
|
|
68
|
+
observed: number;
|
|
69
|
+
deltasAppended: number;
|
|
70
|
+
}>;
|
|
71
|
+
/** Why this action cannot be pushed, or null if it can. Pure — no vendor call on its path. */
|
|
72
|
+
export declare function unpushableReason(op: string): string | null;
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the id the VENDOR knows this subject by.
|
|
75
|
+
*
|
|
76
|
+
* The kernel records the vendor's minted id at landing time (`vendorSubjectId` → the landed copy
|
|
77
|
+
* carries the vendor's id, `aliasOf` the local one), and `resolveSubjectId` answers it — so a
|
|
78
|
+
* later delete/cancel addresses the vendor by THAT. A locally-minted subject with no recorded
|
|
79
|
+
* alias is refused rather than guessed at — addressing the real account by the twin's own mint
|
|
80
|
+
* would DELETE or CANCEL a resource the vendor never had.
|
|
81
|
+
*/
|
|
82
|
+
export declare function externalIdFor(subjectType: string, subjectId: string, root?: string): string | null;
|
|
83
|
+
/**
|
|
84
|
+
* Resolve the REST (method, path) for ONE pending action — faithful to the real Moonshot REST
|
|
85
|
+
* surface:
|
|
86
|
+
* - <type>.create → POST <collection>
|
|
87
|
+
* - <type>.cancel → POST <collection>/:id/cancel
|
|
88
|
+
* - <type>.delete → DELETE <collection>/:id
|
|
89
|
+
*/
|
|
90
|
+
export declare function moonshotRequestForAction(action: Pick<TwinAction, 'operation' | 'subject'>,
|
|
91
|
+
/** The id the VENDOR knows this subject by. Required for anything but a create — see
|
|
92
|
+
* `externalIdFor`; passing the twin's own mint would address a resource the vendor never had. */
|
|
93
|
+
externalId?: string): {
|
|
94
|
+
method: 'GET' | 'POST' | 'DELETE';
|
|
95
|
+
path: string;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Push ONE pending action to REAL Moonshot via the injected executor. Returns the real external
|
|
99
|
+
* id (the object id from the response; for a create that's a freshly minted id, otherwise it
|
|
100
|
+
* echoes the subject). WRITES TO THE REAL ACCOUNT.
|
|
101
|
+
*/
|
|
102
|
+
export declare function pushMoonshotAction(execute: MoonshotExecute, action: Pick<TwinAction, 'operation' | 'subject' | 'fields'>, opts?: {
|
|
103
|
+
externalId?: string;
|
|
104
|
+
}): Promise<{
|
|
105
|
+
externalId: string;
|
|
106
|
+
}>;
|
|
107
|
+
/** The pack's executor over the kernel's: the same Moonshot call, carried by the head. The
|
|
108
|
+
* credential never enters this file — the kernel's executor applies it to the authorization
|
|
109
|
+
* header before this sees the request. */
|
|
110
|
+
export declare function moonshotExecuteOver(execute: RemoteExecute): MoonshotExecute;
|
|
111
|
+
/** The refresh adapter: pull the account's models / files / batches / balance through the executor. */
|
|
112
|
+
export declare function syncMoonshotFromRemote(execute: RemoteExecute, opts?: {
|
|
113
|
+
root?: string;
|
|
114
|
+
origin?: string;
|
|
115
|
+
occurredAt?: string;
|
|
116
|
+
}): Promise<ReturnType<typeof syncMoonshotFromReal>>;
|
|
117
|
+
/**
|
|
118
|
+
* The PERFORM adapter: one deployable entry crosses to Moonshot, or settles with the reason it
|
|
119
|
+
* never could. `file.create` is unpushable-BY-DESIGN (the real endpoint is multipart with the
|
|
120
|
+
* file body; this executor sends JSON) — it settles without crossing rather than throwing, so it
|
|
121
|
+
* cannot wedge every later batch action behind it. A locally-minted subject with no vendor alias
|
|
122
|
+
* THROWS (the head records the failure): addressing the real account by the twin's own mint would
|
|
123
|
+
* delete or cancel a resource the vendor never had.
|
|
124
|
+
*/
|
|
125
|
+
export declare function performMoonshotAction(execute: RemoteExecute, action: TwinAction, ctx: PerformContext): Promise<PushOutcome>;
|
|
126
|
+
/**
|
|
127
|
+
* Push the twin's PENDING local actions to real Moonshot and CONFIRM each. Idempotency: a
|
|
128
|
+
* confirmed action is no longer deployable, so a re-push enacts NOTHING.
|
|
129
|
+
*
|
|
130
|
+
* Every action this pack records is SINGLE-RESOURCE (one file, one batch), so confirming with
|
|
131
|
+
* `fields` alone is correct — see ADDING_A_TWIN.md §5 on compound actions, which this pack has
|
|
132
|
+
* none of. If a compound write is ever added here, it must ride its extra resources in
|
|
133
|
+
* `additionalObservations` or the push will silently delete them.
|
|
134
|
+
*/
|
|
135
|
+
export declare function pushPendingMoonshotActions(execute: MoonshotExecute, opts: {
|
|
136
|
+
root?: string;
|
|
137
|
+
occurredAt: string;
|
|
138
|
+
}): Promise<{
|
|
139
|
+
pushed: number;
|
|
140
|
+
confirmed: string[];
|
|
141
|
+
externalIds: Record<string, string>;
|
|
142
|
+
refused: Array<{
|
|
143
|
+
actionId: string;
|
|
144
|
+
operation: string;
|
|
145
|
+
reason: string;
|
|
146
|
+
}>;
|
|
147
|
+
}>;
|
|
148
|
+
/**
|
|
149
|
+
* FULL bi-directional sync over the injected client: (1) PUSH every deployable local entry to
|
|
150
|
+
* real Moonshot and confirm it, then (2) PULL all modeled collections back and fold them onto
|
|
151
|
+
* the root's log. Pushing first means the pull observes the twin's own writes as confirmed
|
|
152
|
+
* external state (no double-count). Re-running with nothing deployable and identical real state
|
|
153
|
+
* is a no-op.
|
|
154
|
+
*/
|
|
155
|
+
export declare function fullSyncMoonshot(execute: MoonshotExecute, opts: {
|
|
156
|
+
root?: string;
|
|
157
|
+
occurredAt: string;
|
|
158
|
+
}): Promise<{
|
|
159
|
+
pushed: number;
|
|
160
|
+
observed: number;
|
|
161
|
+
deltasAppended: number;
|
|
162
|
+
collections: number;
|
|
163
|
+
refused: Array<{
|
|
164
|
+
actionId: string;
|
|
165
|
+
operation: string;
|
|
166
|
+
reason: string;
|
|
167
|
+
}>;
|
|
168
|
+
}>;
|