@pouchy_ai/world-sdk 0.16.0 → 0.19.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,88 @@
1
1
  # @pouchy_ai/world-sdk
2
2
 
3
+ ## 0.19.0
4
+
5
+ - `getApprovedExport(environmentId, worldInstanceId, draftId, editorialId, exportId)`
6
+ — **read an approved script, body included, from a backend.** With `adminKey`
7
+ set it goes to the `/admin` mirror and needs no browser anywhere in your
8
+ deployment; same mechanism as `getProgress` and `getTurn`.
9
+ - **This closes the loop.** Until now nothing returned an export body: the
10
+ collection listing strips it, so the only response that ever carried a script
11
+ was the POST that minted one. Reading required the create verb and an
12
+ hour-lived browser token, which meant a person had to forward the file.
13
+ - **Every field the call needs is in the `world.script_approved` webhook** —
14
+ `exportId` at the top level, and `environmentId`, `worldInstanceId`,
15
+ `evidenceDraftId`, `editorialDraftId` inside `lineage`. Webhook → read, with
16
+ nobody in the middle. The payload still carries no script.
17
+ - **Creating an export is still a human act** and is deliberately NOT mirrored:
18
+ an export is a reviewer's signature on a specific text. `deriveStoryPackageCandidate`
19
+ is not mirrored either — it leads to published canon.
20
+ - New `ApprovedScriptExportSummary`, and `listApprovedExports` is now typed with
21
+ it instead of `unknown[]`. **The summary carries no `content`** — that is the
22
+ point of the split, and treating one as a full row is now a compile error
23
+ rather than a crash reading `content.exportDigest` off `undefined`.
24
+ - Any miss on the item read — wrong project, environment, instance, draft,
25
+ editorial, or no such id — answers the **same** 404, so refusals cannot be used
26
+ to map which objects exist.
27
+
28
+ ## 0.18.0
29
+
30
+ - `createApprovedExport({ version: 2 })` — the V2 production hand-off: a
31
+ synopsis, character notes, an episode split, the beats where the plot moved,
32
+ committed relation changes, and where the run diverged from its story.
33
+ - **Your existing calls do not change.** `version` defaults to 1, and omitting
34
+ it sends no `version` at all, so a caller written before this release receives
35
+ a byte-identical V1 export.
36
+ - **V1 is frozen.** `exportId` is a content hash you may be keying on for
37
+ idempotency; nothing was added to V1, no key was reordered in its canonical
38
+ string, and its `approvedscript1` domain prefix is unchanged. Golden vectors
39
+ generated before V2 existed hold the line.
40
+ - `ApprovedScriptExportRow.content` is now a union discriminated on
41
+ `contractVersion`. **Narrow before reading version-specific fields** — if you
42
+ read `content.synopsis` without checking, TypeScript will now tell you. A
43
+ stored row keeps the version it was written with, forever; nothing is
44
+ migrated.
45
+ - The two contracts hash under separate domains, so one approval exported both
46
+ ways yields two different, individually stable ids. That is correct: they
47
+ carry different content.
48
+ - An unrecognised `version` is a **400**, not a quiet fall back to 1 — a caller
49
+ who believes they asked for V2 must not silently ship against V1.
50
+ - Every V2 field is derived from committed material and **no model is called**.
51
+ `synopsis` is assembled from scene headings rather than written; a character
52
+ note quotes only an evidence-origin line; `episodes` is a mechanical split at
53
+ a fixed size, not an editorial judgement; `relationChanges` carries the
54
+ evidence layer's own wording rather than a role pair parsed out of prose.
55
+ - A rejected scene stays rejected: `keyMoments` and `relationChanges` cover only
56
+ turns belonging to surviving scenes. `divergence` and `lineage` are facts
57
+ about the run and are not filtered.
58
+ - `deriveStoryPackageCandidate` takes **version 1 exports only** and refuses a
59
+ V2 export with 422. It is the one path from an export into published canon,
60
+ and which V2 fields may become canon has not been decided.
61
+ - `world.script_approved` webhooks gain `contractVersion`. Still identifiers and
62
+ lineage only — never the script.
63
+
64
+ ## 0.17.0
65
+
66
+ - `scanConsistency({ layers })` — opt into the MODEL-ASSISTED layer: suspicions
67
+ the deterministic checks cannot prove.
68
+ - **Your existing calls do not change.** Without `layers` the response is
69
+ byte-identical to 0.16.0 — no new keys, and zero model calls server-side.
70
+ - **Suspicions arrive in `modelAssistedFindings`, never in `findings`.**
71
+ `WorldConsistencyFindingCode` is frozen. If you test
72
+ `severity === 'blocking'` over `findings`, that check keeps meaning exactly
73
+ what it meant; model-assisted severity has no `blocking` member at all.
74
+ - Three gates, all required: a server flag, a declaration on the published world
75
+ revision, and `layers`. `modelAssistedNote` tells you which one was shut.
76
+ - Findings are `potential` until a person decides. `findingId` is stable across
77
+ scans for the same judgement — derived from the code and the server-normalised
78
+ evidence, not from the model's wording — so a dismissal sticks.
79
+ - At most five model calls per scan, no retry, a wall-clock deadline, and
80
+ `modelAssistedNote: 'incomplete'` when the budget stopped it early. Rate
81
+ limited fail-closed: one scan per minute per project.
82
+ - `suspected_cross_ip_contamination` is an editorial opinion even once
83
+ confirmed. It is not evidence of isolation and should not gate a build, block
84
+ an export, or reject content.
85
+
3
86
  ## 0.16.0
4
87
 
5
88
  - `scanConsistency()` — what a world's own record proves about itself. Ten
package/README.md CHANGED
@@ -160,12 +160,36 @@ copies, and `prune` — the one call here that deletes — refuses without
160
160
  `confirm`, and again unless the archive verifies and has outlived retention.
161
161
 
162
162
  **Production hand-off** — `createApprovedExport`, `listApprovedExports`,
163
- `deriveStoryPackageCandidate`. An approved editorial draft becomes a versioned
164
- `ApprovedScriptExportV1` carrying the whole chain: original story package →
165
- world instance → ledger range → evidence draft → editorial draft → reviewer.
166
- Idempotent on content, so replaying an export is a no-op. The Story Package
167
- candidate is validated and *returned*; publishing it is a separate act by a
168
- person, and only evidence-origin material becomes canon.
163
+ `getApprovedExport`, `deriveStoryPackageCandidate`. An approved editorial draft becomes a versioned
164
+ approved script export carrying the whole chain: original story package → world
165
+ instance → ledger range → evidence draft → editorial draft → reviewer.
166
+ Idempotent on content, so replaying an export is a no-op.
167
+
168
+ `version` picks the contract and **defaults to 1**. `ApprovedScriptExportV1` is
169
+ frozen: its `exportId` is a content hash integrators key on, so nothing is added
170
+ to it, ever. `version: 2` adds what a production workflow needs beyond the
171
+ scenes — a synopsis assembled from scene headings, character notes quoting only
172
+ committed lines, a mechanical episode split, the beats that completed a story
173
+ node, committed relation changes, and where the run diverged from its story. All
174
+ of it derived from committed material; **no model is called for any of it**. The
175
+ two contracts hash under separate domains, so one approval exported both ways
176
+ yields two different, individually stable ids. `content` is a union — narrow on
177
+ `contractVersion` before reading version-specific fields, and expect an
178
+ unrecognised `version` to be a 400 rather than a quiet fall back to 1.
179
+
180
+ `listApprovedExports` returns **summaries** — identity, provenance, a scene
181
+ count, and no script. `getApprovedExport` returns **one export with its body**,
182
+ and it is the call an unattended backend makes: with `adminKey` set it routes to
183
+ the `/admin` mirror, so no browser is involved anywhere in your deployment.
184
+ Everything it needs arrives in the `world.script_approved` webhook, so the loop
185
+ closes without a person in the middle. Creating an export is deliberately NOT on
186
+ that lane — an export is a reviewer's signature on a specific text.
187
+
188
+ The Story Package candidate is validated and *returned*; publishing it is a
189
+ separate act by a person, and only evidence-origin material becomes canon. It
190
+ takes **version 1 exports only** and refuses a V2 export with 422 — it is the
191
+ one path from an export into published canon, and which V2 fields may become
192
+ canon has not been decided.
169
193
 
170
194
  **Editorial layer** — `createEditorialDraft`, `getEditorialDraft`,
171
195
  `listEditorialDrafts`, `setEditorialStatus`, `decideEditorialScene`,
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.16.0";
7
+ export declare const WORLD_SDK_VERSION = "0.19.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. */
@@ -528,44 +528,158 @@ export interface WorldDrainReport {
528
528
  blocked: number;
529
529
  saturated: boolean;
530
530
  }
531
- /** The production hand-off (Batch 7). Every line keeps the origin the SERVER
532
- * verified: `evidence` with the turn it came from, or `suggestion` because
533
- * nothing committed matched it whatever the model claimed. A suggestion is
534
- * never laundered into a fact downstream. */
531
+ /** The production hand-off, version 1 (Batch 7) — FROZEN. Every line keeps the
532
+ * origin the SERVER verified: `evidence` with the turn it came from, or
533
+ * `suggestion` because nothing committed matched it, whatever the model
534
+ * claimed. A suggestion is never laundered into a fact downstream.
535
+ *
536
+ * Frozen means frozen: `exportId` is a content hash, integrators key on it for
537
+ * idempotency, and a field added here would change the id of every export
538
+ * minted from an approval that was already exported. New material goes to V2. */
539
+ export interface ApprovedScriptExportContentV1 {
540
+ contractVersion: 1;
541
+ exportId: string;
542
+ title: string;
543
+ lineage: Record<string, unknown>;
544
+ review: {
545
+ reviewer: string;
546
+ approvedAt: string;
547
+ sceneDecisions: Record<string, unknown>;
548
+ };
549
+ characters: Array<{
550
+ roleId: string;
551
+ storyRoleId?: string;
552
+ lines: number;
553
+ }>;
554
+ scenes: Array<Record<string, unknown>>;
555
+ stateChanges: Array<{
556
+ summary: string;
557
+ sourceTurnId: string;
558
+ }>;
559
+ suggestionRatio: number;
560
+ exportDigest: string;
561
+ }
562
+ /** Version 2 (Batch 10) — what a production workflow needs beyond the scenes.
563
+ *
564
+ * Every added field is DERIVED from material the pipeline already committed;
565
+ * no model is called to produce any of it. In particular `synopsis` is
566
+ * ASSEMBLED from the surviving scene headings rather than written, and a
567
+ * character note quotes only an evidence-origin line.
568
+ *
569
+ * Declared field by field rather than extending V1. The two contracts hash
570
+ * under separate domains (`approvedscript1` / `approvedscript2`), so one
571
+ * approval exported both ways yields two different, individually stable ids —
572
+ * which is correct, since the two carry different content. */
573
+ export interface ApprovedScriptExportContentV2 {
574
+ contractVersion: 2;
575
+ exportId: string;
576
+ title: string;
577
+ lineage: Record<string, unknown>;
578
+ review: {
579
+ reviewer: string;
580
+ approvedAt: string;
581
+ /** Whether `approvedAt` was stamped when the draft entered `approved`, or
582
+ * stood in from the row's last-touch time because it predates the stamp. */
583
+ approvalTimeSource: 'stamped' | 'legacy_updated_at';
584
+ sceneDecisions: Record<string, unknown>;
585
+ };
586
+ characters: Array<{
587
+ roleId: string;
588
+ storyRoleId?: string;
589
+ lines: number;
590
+ }>;
591
+ scenes: Array<Record<string, unknown>>;
592
+ stateChanges: Array<{
593
+ summary: string;
594
+ sourceTurnId: string;
595
+ }>;
596
+ suggestionRatio: number;
597
+ /** Scene headings in order. Assembled, not authored. */
598
+ synopsis?: string;
599
+ characterNotes?: Array<{
600
+ roleId: string;
601
+ storyRoleId?: string;
602
+ lines: number;
603
+ sceneIds: string[];
604
+ /** Their first COMMITTED line. Absent when they only ever spoke suggested
605
+ * lines — a model's connective sentence must not read as characterisation. */
606
+ firstLine?: string;
607
+ }>;
608
+ /** A MECHANICAL split at a fixed number of scenes, not an editorial one: a
609
+ * reproducible starting point for a producer to move. */
610
+ episodes?: Array<{
611
+ episodeId: string;
612
+ title: string;
613
+ sceneIds: string[];
614
+ synopsis: string;
615
+ }>;
616
+ /** Beats that completed at least one story node — where the plot moved. Not
617
+ * "moments somebody found dramatic": that judgement stays a person's. */
618
+ keyMoments?: Array<{
619
+ sourceTurnId: string;
620
+ seq: number;
621
+ completedNodeIds: string[];
622
+ summary: string;
623
+ }>;
624
+ /** Committed `set_relation` changes, in the evidence layer's own wording and
625
+ * with the turn they came from. Not parsed back into a role pair: inventing
626
+ * structure from prose is the guessing this pipeline reserves for people. */
627
+ relationChanges?: Array<{
628
+ summary: string;
629
+ sourceTurnId: string;
630
+ }>;
631
+ /** Where the run stands against the story it started from. Facts only — a
632
+ * node never completed, or a branch condition reached. Never what "should"
633
+ * have happened. */
634
+ divergence?: Array<{
635
+ kind: 'unresolved_objective' | 'branch_reached';
636
+ nodeId?: string;
637
+ branchId?: string;
638
+ description: string;
639
+ }>;
640
+ exportDigest: string;
641
+ }
642
+ /** Both contracts, discriminated on `content.contractVersion`. Narrow before
643
+ * reading anything version-specific; a stored row keeps the version it was
644
+ * written with, forever. */
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
+ }
535
669
  export interface ApprovedScriptExportRow {
536
670
  exportId: string;
537
671
  draftId: string;
538
672
  editorialId: string;
539
673
  worldInstanceId: string;
540
674
  environmentId: string;
541
- content: {
542
- contractVersion: number;
543
- exportId: string;
544
- title: string;
545
- lineage: Record<string, unknown>;
546
- review: {
547
- reviewer: string;
548
- approvedAt: string;
549
- sceneDecisions: Record<string, unknown>;
550
- };
551
- characters: Array<{
552
- roleId: string;
553
- storyRoleId?: string;
554
- lines: number;
555
- }>;
556
- scenes: Array<Record<string, unknown>>;
557
- stateChanges: Array<{
558
- summary: string;
559
- sourceTurnId: string;
560
- }>;
561
- suggestionRatio: number;
562
- exportDigest: string;
563
- };
675
+ content: ApprovedScriptExportContent;
564
676
  createdAt: string;
565
677
  createdBy: string;
566
678
  deliveredAt?: string;
567
679
  deliveryAttempted?: number;
568
680
  deliveryOk?: number;
681
+ approvalTimeSource?: 'stamped' | 'legacy_updated_at';
682
+ approvedContentDigest?: string;
569
683
  }
570
684
  /** What `POST /v1/sessions` actually returns for a WORLD mint.
571
685
  *
@@ -684,6 +798,44 @@ export interface WorldConsistencyCoverage {
684
798
  skipped: number;
685
799
  reason: 'evidence_field_absent' | 'scan_incomplete';
686
800
  }
801
+ /** The MODEL-ASSISTED layer. World API 1.20.
802
+ *
803
+ * A SEPARATE closed set from `WorldConsistencyFindingCode`, which stays frozen
804
+ * as the deterministic vocabulary. Keeping them apart is not tidiness: code
805
+ * written against 1.19 reasonably does `if (f.severity === 'blocking') fail()`
806
+ * over `findings`, and a suspicion about a character's voice must never be
807
+ * able to fail that build. Suspicions arrive in their own array, and their
808
+ * severity has no `'blocking'` member at all.
809
+ *
810
+ * These are judgements, not proofs. Nothing here should gate a merge, block an
811
+ * export, or reject content — `suspected_cross_ip_contamination` least of all:
812
+ * even confirmed it is an editorial opinion, not evidence of isolation. */
813
+ export type WorldModelAssistedFindingCode = 'suspected_canon_conflict' | 'suspected_revival' | 'suspected_persona_conflict' | 'suspected_secret_leak' | 'suspected_cross_ip_contamination';
814
+ export interface WorldModelAssistedFinding {
815
+ code: WorldModelAssistedFindingCode;
816
+ layer: 'model_assisted';
817
+ /** `potential` until a person decides. Confirming records an opinion against
818
+ * the finding; it changes nothing about the story, canon, ledger or state. */
819
+ status: 'potential' | 'confirmed' | 'dismissed';
820
+ severity: 'warning' | 'info';
821
+ /** Stable across scans for the same judgement, so a dismissal sticks. */
822
+ findingId: string;
823
+ /** Assembled server-side from committed rows. The model picks which rows;
824
+ * it never writes evidence. */
825
+ evidence: Array<{
826
+ turnId?: string;
827
+ roleId?: string;
828
+ }>;
829
+ /** The model's own words, bounded, and typed apart from evidence. */
830
+ suggestion?: {
831
+ kind: 'author_note';
832
+ text: string;
833
+ };
834
+ reviewedBy?: string;
835
+ reviewedAt?: string;
836
+ }
837
+ /** Why the model layer produced less than a full pass, when it was asked for. */
838
+ export type WorldModelLayerNote = 'disabled_server' | 'not_declared' | 'rate_limited' | 'unavailable' | 'incomplete';
687
839
  export type WorldConsistencyReport = {
688
840
  status: 'complete' | 'incomplete';
689
841
  contractVersion: number;
@@ -693,6 +845,10 @@ export type WorldConsistencyReport = {
693
845
  findingsTruncated: boolean;
694
846
  /** Present only when `status` is `incomplete`. Pass it back to continue. */
695
847
  cursor?: number;
848
+ /** Present ONLY when you asked for `model_assisted`. A default call
849
+ * carries neither this key nor `modelAssistedNote`. */
850
+ modelAssistedFindings?: WorldModelAssistedFinding[];
851
+ modelAssistedNote?: WorldModelLayerNote;
696
852
  } | {
697
853
  status: 'rate_limited';
698
854
  retryAfterSec: number;
@@ -772,6 +928,7 @@ export declare class PouchyWorldClient {
772
928
  */
773
929
  scanConsistency(environmentId: string, worldInstanceId: string, opts?: {
774
930
  cursor?: number;
931
+ layers?: Array<'deterministic' | 'model_assisted'>;
775
932
  }): Promise<WorldConsistencyReport>;
776
933
  /** Where the story is, and whether it can go on. World API 1.18.
777
934
  *
@@ -867,16 +1024,51 @@ export declare class PouchyWorldClient {
867
1024
  * (Batch 7). Idempotent on content — the same approved review exports to
868
1025
  * the same id forever, so replaying one is a no-op rather than a second
869
1026
  * script. `notify` also sends `world.script_approved` to the project's
870
- * webhooks, carrying lineage and identifiers but never the script body. */
1027
+ * webhooks, carrying lineage, identifiers and `contractVersion` but never
1028
+ * the script body.
1029
+ *
1030
+ * `version` DEFAULTS TO 1 and omitting it sends no `version` at all, so a
1031
+ * caller written before V2 existed keeps receiving byte-identical exports.
1032
+ * Pass `version: 2` for the production contract (synopsis, character notes,
1033
+ * episodes, key moments, relation changes, divergence). Any other value is
1034
+ * refused by the server rather than coerced. */
871
1035
  createApprovedExport(environmentId: string, worldInstanceId: string, draftId: string, editorialId: string, options?: {
872
1036
  notify?: boolean;
1037
+ version?: 1 | 2;
873
1038
  }): Promise<ApprovedScriptExportRow>;
874
1039
  listApprovedExports(environmentId: string, worldInstanceId: string, draftId: string, editorialId: string): Promise<{
875
- exports: unknown[];
1040
+ exports: ApprovedScriptExportSummary[];
876
1041
  }>;
1042
+ /** Read ONE approved export, script included — the machine lane's read.
1043
+ *
1044
+ * This is the call an unattended backend makes. With `adminKey` set it goes
1045
+ * to the `/admin` mirror and needs no browser anywhere in your deployment;
1046
+ * with an `adminToken` it goes to the owner plane. Same mechanism as
1047
+ * `getProgress` and `getTurn`.
1048
+ *
1049
+ * Every field it needs is in the `world.script_approved` webhook you
1050
+ * received: `exportId` at the top level, and `environmentId`,
1051
+ * `worldInstanceId`, `evidenceDraftId` and `editorialDraftId` inside
1052
+ * `lineage`. So the webhook → read loop closes without a person in it.
1053
+ *
1054
+ * It READS. It does not create, approve or re-approve: minting an export is
1055
+ * a reviewer signing off on a specific text, and that stays on the owner
1056
+ * plane with a human behind it. What comes back cannot change — `exportId`
1057
+ * is the content digest of the body it returns.
1058
+ *
1059
+ * Narrow `content.contractVersion` before reading version-specific fields.
1060
+ * A miss of any kind — wrong instance, wrong draft, wrong id — is one 404;
1061
+ * they are not told apart, so a caller cannot map an id space by reading
1062
+ * refusals. */
1063
+ getApprovedExport(environmentId: string, worldInstanceId: string, draftId: string, editorialId: string, exportId: string): Promise<ApprovedScriptExportRow>;
877
1064
  /** The next Story Package as a CANDIDATE — validated, and returned rather
878
1065
  * than published. Publishing it is a separate act by a person, through
879
- * `createStoryPackage`. Only evidence-origin material becomes canon. */
1066
+ * `createStoryPackage`. Only evidence-origin material becomes canon.
1067
+ *
1068
+ * VERSION 1 EXPORTS ONLY. A version 2 export is refused with 422: this is
1069
+ * the one path from an export into published canon, and which V2 fields may
1070
+ * become canon has not been decided. Export the approval as version 1 and
1071
+ * derive from that. */
880
1072
  deriveStoryPackageCandidate(environmentId: string, worldInstanceId: string, draftId: string, editorialId: string, exportId: string, options?: {
881
1073
  name?: string;
882
1074
  summary?: string;
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.16.0';
26
+ export const WORLD_SDK_VERSION = '0.19.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
@@ -324,6 +324,10 @@ export class PouchyWorldClient {
324
324
  const q = new URLSearchParams();
325
325
  if (opts.cursor !== undefined)
326
326
  q.set('cursor', String(opts.cursor));
327
+ // Omitted means deterministic only, with zero model calls — the default
328
+ // stays what it was, so upgrading the SDK changes no behaviour.
329
+ if (opts.layers?.length)
330
+ q.set('layers', opts.layers.join(','));
327
331
  const suffix = q.toString() ? `?${q}` : '';
328
332
  return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/consistency`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/consistency`, suffix);
329
333
  }
@@ -454,16 +458,55 @@ export class PouchyWorldClient {
454
458
  * (Batch 7). Idempotent on content — the same approved review exports to
455
459
  * the same id forever, so replaying one is a no-op rather than a second
456
460
  * script. `notify` also sends `world.script_approved` to the project's
457
- * webhooks, carrying lineage and identifiers but never the script body. */
461
+ * webhooks, carrying lineage, identifiers and `contractVersion` but never
462
+ * the script body.
463
+ *
464
+ * `version` DEFAULTS TO 1 and omitting it sends no `version` at all, so a
465
+ * caller written before V2 existed keeps receiving byte-identical exports.
466
+ * Pass `version: 2` for the production contract (synopsis, character notes,
467
+ * episodes, key moments, relation changes, divergence). Any other value is
468
+ * refused by the server rather than coerced. */
458
469
  createApprovedExport(environmentId, worldInstanceId, draftId, editorialId, options = {}) {
459
- return this.owner('POST', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial/${editorialId}/approved-export`, options.notify ? { notify: true } : {});
470
+ return this.owner('POST', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial/${editorialId}/approved-export`, {
471
+ ...(options.notify ? { notify: true } : {}),
472
+ ...(options.version === undefined ? {} : { version: options.version })
473
+ });
460
474
  }
461
475
  listApprovedExports(environmentId, worldInstanceId, draftId, editorialId) {
462
476
  return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial/${editorialId}/approved-export`);
463
477
  }
478
+ /** Read ONE approved export, script included — the machine lane's read.
479
+ *
480
+ * This is the call an unattended backend makes. With `adminKey` set it goes
481
+ * to the `/admin` mirror and needs no browser anywhere in your deployment;
482
+ * with an `adminToken` it goes to the owner plane. Same mechanism as
483
+ * `getProgress` and `getTurn`.
484
+ *
485
+ * Every field it needs is in the `world.script_approved` webhook you
486
+ * received: `exportId` at the top level, and `environmentId`,
487
+ * `worldInstanceId`, `evidenceDraftId` and `editorialDraftId` inside
488
+ * `lineage`. So the webhook → read loop closes without a person in it.
489
+ *
490
+ * It READS. It does not create, approve or re-approve: minting an export is
491
+ * a reviewer signing off on a specific text, and that stays on the owner
492
+ * plane with a human behind it. What comes back cannot change — `exportId`
493
+ * is the content digest of the body it returns.
494
+ *
495
+ * Narrow `content.contractVersion` before reading version-specific fields.
496
+ * A miss of any kind — wrong instance, wrong draft, wrong id — is one 404;
497
+ * they are not told apart, so a caller cannot map an id space by reading
498
+ * refusals. */
499
+ getApprovedExport(environmentId, worldInstanceId, draftId, editorialId, exportId) {
500
+ 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)}`);
501
+ }
464
502
  /** The next Story Package as a CANDIDATE — validated, and returned rather
465
503
  * than published. Publishing it is a separate act by a person, through
466
- * `createStoryPackage`. Only evidence-origin material becomes canon. */
504
+ * `createStoryPackage`. Only evidence-origin material becomes canon.
505
+ *
506
+ * VERSION 1 EXPORTS ONLY. A version 2 export is refused with 422: this is
507
+ * the one path from an export into published canon, and which V2 fields may
508
+ * become canon has not been decided. Export the approval as version 1 and
509
+ * derive from that. */
467
510
  deriveStoryPackageCandidate(environmentId, worldInstanceId, draftId, editorialId, exportId, options = {}) {
468
511
  return this.owner('POST', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial/${editorialId}/approved-export/candidate?exportId=${encodeURIComponent(exportId)}`, options);
469
512
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/world-sdk",
3
- "version": "0.16.0",
3
+ "version": "0.19.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",