@pouchy_ai/world-sdk 0.19.0 → 0.20.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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @pouchy_ai/world-sdk
2
2
 
3
+ ## 0.20.0
4
+
5
+ - **Seven methods for routes that already existed.** `getWorldOverview`,
6
+ `getWorldCost`, `listWorldVersions`, `listStoryPackageVersions`,
7
+ `setWorldDisabled`, `setStoryPackageDisabled` (all `adminKey`-capable), and
8
+ `preflightWorld` (owner plane only — no `/admin` mirror).
9
+ - **Two of them the README already promised.** Its credential table listed
10
+ `overview` and `cost` as `adminKey` reads, and there was no method to make
11
+ them with. `getWorldOverview` is the one to reach for first: it carries
12
+ `ready` and the `blockers` behind a false, and unlike `preflightWorld` a
13
+ machine can call it.
14
+ - **`conformance.mjs` no longer fails a correctly-configured project.** Its
15
+ header claimed an admin key was enough; two of its own gates call
16
+ `replayLedgerToEnd` and `createScriptDraft`, whose routes have no `/admin`
17
+ mirror, so on a key alone they threw "needs an owner token". Those gates now
18
+ SKIP with the reason printed, and skips are counted apart from passes rather
19
+ than folded into the ratio.
20
+ - No API change — `WORLD_API_VERSION` stays at 1.24.0. Every route these
21
+ methods call has been served on both planes since Batch 1; only the client
22
+ was missing.
23
+
3
24
  ## 0.19.0
4
25
 
5
26
  - `getApprovedExport(environmentId, worldInstanceId, draftId, editorialId, exportId)`
package/README.md CHANGED
@@ -52,7 +52,10 @@ directions a beat could take, then commit the one the player picked. Off unless
52
52
  the world's published revision declares it; the `envelope` is server-side only.
53
53
 
54
54
  **Reading** — `getWorldState`, `getProgress`, `getTurn`, `listTurns`,
55
- `listTurnsSince`, `replayLedger`, `replayLedgerToEnd`, `scanConsistency`.
55
+ `listTurnsSince`, `replayLedger`, `replayLedgerToEnd`, `scanConsistency`,
56
+ `getWorldOverview`, `getWorldCost`, `listWorldVersions`,
57
+ `listStoryPackageVersions`, `setWorldDisabled`, `setStoryPackageDisabled`,
58
+ `preflightWorld`.
56
59
 
57
60
  **Which credential does what.** This matters more than it looks: one of them
58
61
  expires within the hour.
@@ -64,9 +67,22 @@ expires within the hour.
64
67
  | `adminToken` (Firebase ID token, ~1h) | everything above plus the content loop | outlive the hour |
65
68
 
66
69
  A server holds `secretKey` + `signing` + `adminKey` and needs **no browser login
67
- anywhere in its deployment** including `conformance.mjs`, which used to demand
68
- one. `adminToken` is for a person: the content loop's review step is a human
69
- decision, which is the point of it rather than an obstacle.
70
+ for the runtime loop**: author a story package and a world, mint sessions, drive
71
+ turns, and read everything back. `adminToken` is for a person, and the content
72
+ loop's review step is a human decision — the point of it rather than an obstacle.
73
+
74
+ **Which calls have no `/admin` mirror, and therefore need `adminToken`:** the
75
+ whole content loop (`*ScriptDraft*`, `*EditorialDraft*`, `createApprovedExport`,
76
+ `deriveStoryPackageCandidate`), the delivery-queue ACTIONS (`drainDeliveries`,
77
+ `requeueDelivery`, `rehydrateDelivery`, `resolveDeliveryGap`), `replayLedger`,
78
+ `archiveLedger`, `evaluateWorld`, and `preflightWorld`. Reading an approved
79
+ export back IS mirrored (`getApprovedExport`) — that is how a machine collects
80
+ what a person approved, with the id arriving on the `world.script_approved`
81
+ webhook.
82
+
83
+ `conformance.mjs` follows the same line: on an admin key alone it runs every
84
+ runtime gate and SKIPS the two whose routes are owner-plane, printing why.
85
+ Setting `POUCHY_ADMIN_TOKEN` as well runs all of them.
70
86
 
71
87
  ```ts
72
88
  const world = new PouchyWorldClient({
package/conformance.mjs CHANGED
@@ -94,12 +94,26 @@ console.log(`Pouchy World conformance — ${scenario.label} scenario\n`);
94
94
  const results = [];
95
95
  let failed = false;
96
96
 
97
+ /** Marks a gate that CANNOT run on the credentials in hand.
98
+ *
99
+ * Not a pass and not a failure. Two gates below drive the content loop and
100
+ * the ledger replay, and neither route has an `/admin` mirror — they are
101
+ * owner-plane by design ("creating stays human, reading does not"). Reporting
102
+ * them as FAIL would make a correctly-configured project look broken; leaving
103
+ * them out silently would make a partial run look complete. */
104
+ class NeedsOwnerToken extends Error {}
105
+
97
106
  async function check(name, fn) {
98
107
  try {
99
108
  const detail = await fn();
100
109
  results.push({ name, ok: true, detail });
101
110
  console.log(`PASS ${name}${detail ? ` — ${detail}` : ''}`);
102
111
  } catch (err) {
112
+ if (err instanceof NeedsOwnerToken) {
113
+ results.push({ name, ok: null, detail: err.message });
114
+ console.log(`SKIP ${name} — ${err.message}`);
115
+ return;
116
+ }
103
117
  failed = true;
104
118
  const detail =
105
119
  err instanceof WorldApiError
@@ -114,14 +128,21 @@ async function check(name, fn) {
114
128
 
115
129
  const world = new PouchyWorldClient({
116
130
  projectId: env('POUCHY_PROJECT_ID'),
117
- // Batch 10: an ADMIN KEY is enough, and is the point.
131
+ // Batch 10: an ADMIN KEY carries the RUNTIME loop, which is the point.
118
132
  //
119
133
  // This harness used to demand POUCHY_ADMIN_TOKEN — a Firebase ID token,
120
134
  // browser-minted and good for about an hour. So the one check that proves a
121
135
  // project's world plane is wired correctly could not run without a person
122
136
  // opening a browser first, which is exactly the thing the rest of this SDK
123
- // stopped requiring. Either credential works now; the key is preferred and
124
- // needs no human.
137
+ // stopped requiring.
138
+ //
139
+ // CORRECTED: "either credential works" was too strong, and this file was the
140
+ // proof — two of its own gates call `replayLedgerToEnd` and
141
+ // `createScriptDraft`, whose routes have NO `/admin` mirror. On an admin key
142
+ // alone they threw "needs an owner token", so the harness could not pass a
143
+ // correctly-configured project. Those two gates now SKIP with the reason
144
+ // printed; every runtime gate runs on the key. Set POUCHY_ADMIN_TOKEN as
145
+ // well to run all of them.
125
146
  ...(process.env.POUCHY_ADMIN_KEY
126
147
  ? { adminKey: process.env.POUCHY_ADMIN_KEY }
127
148
  : { adminToken: env('POUCHY_ADMIN_TOKEN') }),
@@ -133,6 +154,15 @@ const world = new PouchyWorldClient({
133
154
  },
134
155
  ...(process.env.POUCHY_BASE_URL ? { baseUrl: process.env.POUCHY_BASE_URL } : {})
135
156
  });
157
+ /** Throws a SKIP for a gate whose route is owner-plane only. */
158
+ function requireOwnerToken(route) {
159
+ if (!process.env.POUCHY_ADMIN_TOKEN) {
160
+ throw new NeedsOwnerToken(
161
+ `${route} has no /admin mirror — set POUCHY_ADMIN_TOKEN to run this gate`
162
+ );
163
+ }
164
+ }
165
+
136
166
  const agentA = env('POUCHY_AGENT_A');
137
167
  const agentB = env('POUCHY_AGENT_B');
138
168
  const stamp = Date.now().toString(36);
@@ -237,6 +267,7 @@ await check('world state reads back (user projection, no private layer)', async
237
267
  });
238
268
 
239
269
  await check('replay verifies the ledger against the served state', async () => {
270
+ requireOwnerToken('/ledger/replay');
240
271
  const report = await world.replayLedgerToEnd(created.environmentId, instanceId);
241
272
  if (report.verdict !== 'consistent') {
242
273
  throw new Error(`${report.verdict}${report.detail ? `: ${report.detail}` : ''}`);
@@ -246,6 +277,7 @@ await check('replay verifies the ledger against the served state', async () => {
246
277
  });
247
278
 
248
279
  await check('a script draft generates and refuses export before review', async () => {
280
+ requireOwnerToken('/script-drafts');
249
281
  const draft = await world.createScriptDraft(created.environmentId, instanceId);
250
282
  if (draft.content?.humanReviewRequired !== true) {
251
283
  throw new Error('draft did not carry humanReviewRequired');
@@ -284,9 +316,24 @@ await check('a WRONG signature is refused (the second proof is real)', async ()
284
316
  });
285
317
 
286
318
  console.log('');
287
- console.log(`${results.filter((r) => r.ok).length}/${results.length} checks passed`);
319
+ // SKIPPED is counted apart from PASSED, never folded into a ratio. A gate that
320
+ // could not run is not a gate that passed, and "8/10 passed" would read as two
321
+ // failures — the opposite of what happened.
322
+ const passed = results.filter((r) => r.ok === true).length;
323
+ const skipped = results.filter((r) => r.ok === null).length;
324
+ const ran = results.length - skipped;
325
+ console.log(
326
+ `${passed}/${ran} checks passed` + (skipped ? ` · ${skipped} skipped (no owner token)` : '')
327
+ );
288
328
  if (failed) {
289
329
  console.error('Conformance FAILED — fix the first failure above before writing product code.');
290
330
  process.exit(1);
291
331
  }
292
- console.log('Conformance passed. The world plane is wired correctly for this project.');
332
+ if (skipped) {
333
+ console.log(
334
+ `The RUNTIME plane is wired correctly. ${skipped} content-loop gate(s) did not run: ` +
335
+ `their routes are owner-plane only. Set POUCHY_ADMIN_TOKEN to cover them too.`
336
+ );
337
+ } else {
338
+ console.log('Conformance passed. The world plane is wired correctly for this project.');
339
+ }
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * a project is running — which is exactly the field you reach for when a
5
5
  * customer's integration behaves like an older SDK than they say they have.
6
6
  * It sat at '0.1.0' for eight releases before anything compared the two. */
7
- export declare const WORLD_SDK_VERSION = "0.19.0";
7
+ export declare const WORLD_SDK_VERSION = "0.20.0";
8
8
  export declare const DEFAULT_BASE_URL = "https://pouchy.ai/v1";
9
9
  /** One direction a beat could take. The WHOLE of what a deliberation shows a
10
10
  * player: no reasoning, no role secrets, no simulated effects, no scores. */
@@ -892,6 +892,63 @@ export declare class PouchyWorldClient {
892
892
  contentHash: string;
893
893
  }>;
894
894
  getWorld(environmentId: string): Promise<unknown>;
895
+ /** The world's readiness and operational aggregate — what the dashboard's
896
+ * World Overview binds to.
897
+ *
898
+ * This is the call to make BEFORE driving anything: `ready` plus the
899
+ * `blockers` that explain a false, instance counts, and the delivery
900
+ * queue's shape. It was reachable on both planes from the start and simply
901
+ * had no method here, which meant the README's own credential table
902
+ * promised an `adminKey` read the SDK could not perform. */
903
+ getWorldOverview(environmentId: string): Promise<unknown>;
904
+ /** What this world has cost, by month.
905
+ *
906
+ * `months` is clamped server-side; omit it for the default window. */
907
+ getWorldCost(environmentId: string, months?: number): Promise<unknown>;
908
+ /** Every published revision of this world.
909
+ *
910
+ * An instance is pinned to the revision it was created on, so this is how a
911
+ * backend learns which definition a running story is actually playing. */
912
+ listWorldVersions(environmentId: string): Promise<{
913
+ versions: unknown[];
914
+ }>;
915
+ /** Every published revision of a story package.
916
+ *
917
+ * The response carries `hasUnsupported` / `unsupportedCount` as well as the
918
+ * versions: a history this build cannot read whole is reported as PARTIAL
919
+ * rather than passing its newest readable revision off as the newest one.
920
+ * Check that flag before treating the last entry as current. */
921
+ listStoryPackageVersions(packageId: string): Promise<{
922
+ versions: unknown[];
923
+ unsupported?: unknown;
924
+ unsupportedCount?: number;
925
+ hasUnsupported?: boolean;
926
+ }>;
927
+ /** Stop (or restart) a world.
928
+ *
929
+ * A disabled world refuses new sessions and new turns. Existing instances
930
+ * are not deleted and their ledgers are untouched — this is a door being
931
+ * shut, not history being erased, and passing `false` opens it again. */
932
+ setWorldDisabled(environmentId: string, disabled: boolean): Promise<{
933
+ environmentId: string;
934
+ disabled: boolean;
935
+ }>;
936
+ /** Stop (or restart) a story package.
937
+ *
938
+ * Worlds already PINNED to one of its revisions keep running: an instance
939
+ * holds its own revision for life. This stops new pins. */
940
+ setStoryPackageDisabled(packageId: string, disabled: boolean): Promise<{
941
+ packageId: string;
942
+ disabled: boolean;
943
+ }>;
944
+ /** The pre-launch checklist for a world: every reason it would refuse to
945
+ * run, gathered before you try.
946
+ *
947
+ * OWNER TOKEN ONLY — there is no `/admin` mirror for this route, so an
948
+ * unattended backend cannot call it. That is not an oversight to work
949
+ * around: `getWorldOverview` carries `ready` and `blockers` and IS
950
+ * mirrored, so a machine has the readiness answer it needs. */
951
+ preflightWorld(environmentId: string): Promise<unknown>;
895
952
  /** Publish the next world revision. Existing world INSTANCES keep the
896
953
  * revision they were created on — a published change reaches new instances
897
954
  * only, which is what keeps a running story from changing runtime or rules
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import { createHash, createHmac, randomUUID } from 'node:crypto';
23
23
  * a project is running — which is exactly the field you reach for when a
24
24
  * customer's integration behaves like an older SDK than they say they have.
25
25
  * It sat at '0.1.0' for eight releases before anything compared the two. */
26
- export const WORLD_SDK_VERSION = '0.19.0';
26
+ export const WORLD_SDK_VERSION = '0.20.0';
27
27
  export const DEFAULT_BASE_URL = 'https://pouchy.ai/v1';
28
28
  /** Read the commit turn id out of an envelope, so the signature covers the id
29
29
  * the server will commit under. The payload half is base64url JSON; the
@@ -287,6 +287,65 @@ export class PouchyWorldClient {
287
287
  getWorld(environmentId) {
288
288
  return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}`, `/admin/environments/${environmentId}`);
289
289
  }
290
+ /** The world's readiness and operational aggregate — what the dashboard's
291
+ * World Overview binds to.
292
+ *
293
+ * This is the call to make BEFORE driving anything: `ready` plus the
294
+ * `blockers` that explain a false, instance counts, and the delivery
295
+ * queue's shape. It was reachable on both planes from the start and simply
296
+ * had no method here, which meant the README's own credential table
297
+ * promised an `adminKey` read the SDK could not perform. */
298
+ getWorldOverview(environmentId) {
299
+ return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}/overview`, `/admin/environments/${environmentId}/overview`);
300
+ }
301
+ /** What this world has cost, by month.
302
+ *
303
+ * `months` is clamped server-side; omit it for the default window. */
304
+ getWorldCost(environmentId, months) {
305
+ const query = months === undefined ? '' : `?months=${encodeURIComponent(String(months))}`;
306
+ return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}/cost`, `/admin/environments/${environmentId}/cost`, undefined, query);
307
+ }
308
+ /** Every published revision of this world.
309
+ *
310
+ * An instance is pinned to the revision it was created on, so this is how a
311
+ * backend learns which definition a running story is actually playing. */
312
+ listWorldVersions(environmentId) {
313
+ return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}/versions`, `/admin/environments/${environmentId}/versions`);
314
+ }
315
+ /** Every published revision of a story package.
316
+ *
317
+ * The response carries `hasUnsupported` / `unsupportedCount` as well as the
318
+ * versions: a history this build cannot read whole is reported as PARTIAL
319
+ * rather than passing its newest readable revision off as the newest one.
320
+ * Check that flag before treating the last entry as current. */
321
+ listStoryPackageVersions(packageId) {
322
+ return this.machine('GET', `/projects/${this.projectId}/story-packages/${packageId}/versions`, `/admin/story-packages/${packageId}/versions`);
323
+ }
324
+ /** Stop (or restart) a world.
325
+ *
326
+ * A disabled world refuses new sessions and new turns. Existing instances
327
+ * are not deleted and their ledgers are untouched — this is a door being
328
+ * shut, not history being erased, and passing `false` opens it again. */
329
+ setWorldDisabled(environmentId, disabled) {
330
+ return this.machine('POST', `/projects/${this.projectId}/environments/${environmentId}/disable`, `/admin/environments/${environmentId}/disable`, { disabled });
331
+ }
332
+ /** Stop (or restart) a story package.
333
+ *
334
+ * Worlds already PINNED to one of its revisions keep running: an instance
335
+ * holds its own revision for life. This stops new pins. */
336
+ setStoryPackageDisabled(packageId, disabled) {
337
+ return this.machine('POST', `/projects/${this.projectId}/story-packages/${packageId}/disable`, `/admin/story-packages/${packageId}/disable`, { disabled });
338
+ }
339
+ /** The pre-launch checklist for a world: every reason it would refuse to
340
+ * run, gathered before you try.
341
+ *
342
+ * OWNER TOKEN ONLY — there is no `/admin` mirror for this route, so an
343
+ * unattended backend cannot call it. That is not an oversight to work
344
+ * around: `getWorldOverview` carries `ready` and `blockers` and IS
345
+ * mirrored, so a machine has the readiness answer it needs. */
346
+ preflightWorld(environmentId) {
347
+ return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/preflight`);
348
+ }
290
349
  /** Publish the next world revision. Existing world INSTANCES keep the
291
350
  * revision they were created on — a published change reaches new instances
292
351
  * only, which is what keeps a running story from changing runtime or rules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/world-sdk",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Server-side TypeScript client for Pouchy World \u2014 story packages, world definitions, world sessions, coordinated turns, trusted events, replay verification and script drafts. Node only: it holds a project Secret Key and a source signing key, which never belong in a browser or a mobile app.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",