@pouchy_ai/world-sdk 0.18.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,51 @@
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
+
24
+ ## 0.19.0
25
+
26
+ - `getApprovedExport(environmentId, worldInstanceId, draftId, editorialId, exportId)`
27
+ — **read an approved script, body included, from a backend.** With `adminKey`
28
+ set it goes to the `/admin` mirror and needs no browser anywhere in your
29
+ deployment; same mechanism as `getProgress` and `getTurn`.
30
+ - **This closes the loop.** Until now nothing returned an export body: the
31
+ collection listing strips it, so the only response that ever carried a script
32
+ was the POST that minted one. Reading required the create verb and an
33
+ hour-lived browser token, which meant a person had to forward the file.
34
+ - **Every field the call needs is in the `world.script_approved` webhook** —
35
+ `exportId` at the top level, and `environmentId`, `worldInstanceId`,
36
+ `evidenceDraftId`, `editorialDraftId` inside `lineage`. Webhook → read, with
37
+ nobody in the middle. The payload still carries no script.
38
+ - **Creating an export is still a human act** and is deliberately NOT mirrored:
39
+ an export is a reviewer's signature on a specific text. `deriveStoryPackageCandidate`
40
+ is not mirrored either — it leads to published canon.
41
+ - New `ApprovedScriptExportSummary`, and `listApprovedExports` is now typed with
42
+ it instead of `unknown[]`. **The summary carries no `content`** — that is the
43
+ point of the split, and treating one as a full row is now a compile error
44
+ rather than a crash reading `content.exportDigest` off `undefined`.
45
+ - Any miss on the item read — wrong project, environment, instance, draft,
46
+ editorial, or no such id — answers the **same** 404, so refusals cannot be used
47
+ to map which objects exist.
48
+
3
49
  ## 0.18.0
4
50
 
5
51
  - `createApprovedExport({ version: 2 })` — the V2 production hand-off: a
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({
@@ -160,7 +176,7 @@ copies, and `prune` — the one call here that deletes — refuses without
160
176
  `confirm`, and again unless the archive verifies and has outlived retention.
161
177
 
162
178
  **Production hand-off** — `createApprovedExport`, `listApprovedExports`,
163
- `deriveStoryPackageCandidate`. An approved editorial draft becomes a versioned
179
+ `getApprovedExport`, `deriveStoryPackageCandidate`. An approved editorial draft becomes a versioned
164
180
  approved script export carrying the whole chain: original story package → world
165
181
  instance → ledger range → evidence draft → editorial draft → reviewer.
166
182
  Idempotent on content, so replaying an export is a no-op.
@@ -177,6 +193,14 @@ yields two different, individually stable ids. `content` is a union — narrow o
177
193
  `contractVersion` before reading version-specific fields, and expect an
178
194
  unrecognised `version` to be a 400 rather than a quiet fall back to 1.
179
195
 
196
+ `listApprovedExports` returns **summaries** — identity, provenance, a scene
197
+ count, and no script. `getApprovedExport` returns **one export with its body**,
198
+ and it is the call an unattended backend makes: with `adminKey` set it routes to
199
+ the `/admin` mirror, so no browser is involved anywhere in your deployment.
200
+ Everything it needs arrives in the `world.script_approved` webhook, so the loop
201
+ closes without a person in the middle. Creating an export is deliberately NOT on
202
+ that lane — an export is a reviewer's signature on a specific text.
203
+
180
204
  The Story Package candidate is validated and *returned*; publishing it is a
181
205
  separate act by a person, and only evidence-origin material becomes canon. It
182
206
  takes **version 1 exports only** and refuses a V2 export with 422 — it is the
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.18.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. */
@@ -643,6 +643,29 @@ export interface ApprovedScriptExportContentV2 {
643
643
  * reading anything version-specific; a stored row keeps the version it was
644
644
  * written with, forever. */
645
645
  export type ApprovedScriptExportContent = ApprovedScriptExportContentV1 | ApprovedScriptExportContentV2;
646
+ /** Identity and provenance for every export on an editorial draft, WITHOUT the
647
+ * script. This is what `listApprovedExports` returns.
648
+ *
649
+ * Split from `ApprovedScriptExportRow` deliberately, and the difference is the
650
+ * whole script. Treating a summary as a full row reads `content.exportDigest`
651
+ * off `undefined`; keeping them apart makes that a compile error rather than a
652
+ * crash in front of a user. To get the body, call `getApprovedExport`. */
653
+ export interface ApprovedScriptExportSummary {
654
+ exportId: string;
655
+ draftId: string;
656
+ editorialId: string;
657
+ worldInstanceId: string;
658
+ environmentId: string;
659
+ /** How many scenes the body carries — the shape of what is not here. */
660
+ scenes: number;
661
+ createdAt: string;
662
+ createdBy: string;
663
+ deliveredAt?: string;
664
+ deliveryAttempted?: number;
665
+ deliveryOk?: number;
666
+ approvalTimeSource?: 'stamped' | 'legacy_updated_at';
667
+ approvedContentDigest?: string;
668
+ }
646
669
  export interface ApprovedScriptExportRow {
647
670
  exportId: string;
648
671
  draftId: string;
@@ -655,6 +678,8 @@ export interface ApprovedScriptExportRow {
655
678
  deliveredAt?: string;
656
679
  deliveryAttempted?: number;
657
680
  deliveryOk?: number;
681
+ approvalTimeSource?: 'stamped' | 'legacy_updated_at';
682
+ approvedContentDigest?: string;
658
683
  }
659
684
  /** What `POST /v1/sessions` actually returns for a WORLD mint.
660
685
  *
@@ -867,6 +892,63 @@ export declare class PouchyWorldClient {
867
892
  contentHash: string;
868
893
  }>;
869
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>;
870
952
  /** Publish the next world revision. Existing world INSTANCES keep the
871
953
  * revision they were created on — a published change reaches new instances
872
954
  * only, which is what keeps a running story from changing runtime or rules
@@ -1012,8 +1094,30 @@ export declare class PouchyWorldClient {
1012
1094
  version?: 1 | 2;
1013
1095
  }): Promise<ApprovedScriptExportRow>;
1014
1096
  listApprovedExports(environmentId: string, worldInstanceId: string, draftId: string, editorialId: string): Promise<{
1015
- exports: unknown[];
1097
+ exports: ApprovedScriptExportSummary[];
1016
1098
  }>;
1099
+ /** Read ONE approved export, script included — the machine lane's read.
1100
+ *
1101
+ * This is the call an unattended backend makes. With `adminKey` set it goes
1102
+ * to the `/admin` mirror and needs no browser anywhere in your deployment;
1103
+ * with an `adminToken` it goes to the owner plane. Same mechanism as
1104
+ * `getProgress` and `getTurn`.
1105
+ *
1106
+ * Every field it needs is in the `world.script_approved` webhook you
1107
+ * received: `exportId` at the top level, and `environmentId`,
1108
+ * `worldInstanceId`, `evidenceDraftId` and `editorialDraftId` inside
1109
+ * `lineage`. So the webhook → read loop closes without a person in it.
1110
+ *
1111
+ * It READS. It does not create, approve or re-approve: minting an export is
1112
+ * a reviewer signing off on a specific text, and that stays on the owner
1113
+ * plane with a human behind it. What comes back cannot change — `exportId`
1114
+ * is the content digest of the body it returns.
1115
+ *
1116
+ * Narrow `content.contractVersion` before reading version-specific fields.
1117
+ * A miss of any kind — wrong instance, wrong draft, wrong id — is one 404;
1118
+ * they are not told apart, so a caller cannot map an id space by reading
1119
+ * refusals. */
1120
+ getApprovedExport(environmentId: string, worldInstanceId: string, draftId: string, editorialId: string, exportId: string): Promise<ApprovedScriptExportRow>;
1017
1121
  /** The next Story Package as a CANDIDATE — validated, and returned rather
1018
1122
  * than published. Publishing it is a separate act by a person, through
1019
1123
  * `createStoryPackage`. Only evidence-origin material becomes canon.
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.18.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
@@ -475,6 +534,30 @@ export class PouchyWorldClient {
475
534
  listApprovedExports(environmentId, worldInstanceId, draftId, editorialId) {
476
535
  return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial/${editorialId}/approved-export`);
477
536
  }
537
+ /** Read ONE approved export, script included — the machine lane's read.
538
+ *
539
+ * This is the call an unattended backend makes. With `adminKey` set it goes
540
+ * to the `/admin` mirror and needs no browser anywhere in your deployment;
541
+ * with an `adminToken` it goes to the owner plane. Same mechanism as
542
+ * `getProgress` and `getTurn`.
543
+ *
544
+ * Every field it needs is in the `world.script_approved` webhook you
545
+ * received: `exportId` at the top level, and `environmentId`,
546
+ * `worldInstanceId`, `evidenceDraftId` and `editorialDraftId` inside
547
+ * `lineage`. So the webhook → read loop closes without a person in it.
548
+ *
549
+ * It READS. It does not create, approve or re-approve: minting an export is
550
+ * a reviewer signing off on a specific text, and that stays on the owner
551
+ * plane with a human behind it. What comes back cannot change — `exportId`
552
+ * is the content digest of the body it returns.
553
+ *
554
+ * Narrow `content.contractVersion` before reading version-specific fields.
555
+ * A miss of any kind — wrong instance, wrong draft, wrong id — is one 404;
556
+ * they are not told apart, so a caller cannot map an id space by reading
557
+ * refusals. */
558
+ getApprovedExport(environmentId, worldInstanceId, draftId, editorialId, exportId) {
559
+ return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial/${editorialId}/approved-export/${encodeURIComponent(exportId)}`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial/${editorialId}/approved-export/${encodeURIComponent(exportId)}`);
560
+ }
478
561
  /** The next Story Package as a CANDIDATE — validated, and returned rather
479
562
  * than published. Publishing it is a separate act by a person, through
480
563
  * `createStoryPackage`. Only evidence-origin material becomes canon.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/world-sdk",
3
- "version": "0.18.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",