-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathprotocol.zod.ts
More file actions
3564 lines (3389 loc) · 188 KB
/
Copy pathprotocol.zod.ts
File metadata and controls
3564 lines (3389 loc) · 188 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
import { ViewSchema } from '../ui/view.zod';
import { DiscoverySchema } from './discovery.zod';
import {
BatchUpdateRequestSchema,
BatchUpdateResponseSchema,
UpdateManyRequestSchema,
DeleteManyRequestSchema,
} from './batch.zod';
import { MetadataCacheRequestSchema, MetadataCacheResponseSchema } from './http-cache.zod';
import { QuerySchema, QUERY_DISTINCT_REMOVED } from '../data/query.zod';
import { retiredKey } from '../shared/retired-key';
import { MetadataItemNameSchema } from '../shared/identifiers.zod';
import { DroppedFieldsEventSchema } from '../data/data-engine.zod';
import {
AnalyticsQueryRequestSchema,
AnalyticsResultResponseSchema,
GetAnalyticsMetaRequestSchema,
AnalyticsMetadataResponseSchema
} from './analytics.zod';
import { RealtimePresenceSchema, TransportProtocol } from './realtime.zod';
import { ObjectPermissionSchema, EffectiveObjectPermissionSchema, FieldPermissionSchema } from '../security/permission.zod';
import { ActionDescriptorSchema } from '../automation/node-executor.zod';
import { TranslationDataSchema } from '../system/translation.zod';
// #5950 / #5882 — the ADR-0010 read-side protection envelope both metadata-item
// responses publish. Same three vocabularies the resolver filters against, so a
// value this spec cannot name is a value the resolver would have dropped.
import {
MetadataLockSchema,
MetadataLockSourceSchema,
MetadataProvenanceSchema,
} from '../kernel/metadata-protection.zod';
import { MetadataValidationResultSchema } from '../kernel/metadata-plugin.zod';
// [#10235] The per-column sortability projection the object read serves on its
// envelope — computed at serve time, never authorable; see sortability.zod.ts.
import { ObjectSortabilitySchema } from './sortability.zod';
import {
ListPackagesRequestSchema,
ListPackagesResponseSchema,
GetPackageRequestSchema,
GetPackageResponseSchema,
InstallPackageRequestSchema,
InstallPackageResponseSchema,
UninstallPackageRequestSchema,
UninstallPackageResponseSchema,
EnablePackageRequestSchema,
EnablePackageResponseSchema,
DisablePackageRequestSchema,
DisablePackageResponseSchema,
} from '../kernel/package-registry.zod';
import type {
ListPackagesRequest,
ListPackagesResponse,
GetPackageRequest,
GetPackageResponse,
InstallPackageRequest,
InstallPackageResponse,
UninstallPackageRequest,
UninstallPackageResponse,
EnablePackageRequest,
EnablePackageResponse,
DisablePackageRequest,
DisablePackageResponse,
InstalledPackage,
PackageStatus,
} from '../kernel/package-registry.zod';
import { lazySchema } from '../shared/lazy-schema';
export const AutomationTriggerRequestSchema = lazySchema(() => z.object({
trigger: z.string(),
payload: z.record(z.string(), z.unknown())
}));
export const AutomationTriggerResponseSchema = lazySchema(() => z.object({
success: z.boolean(),
jobId: z.string().optional(),
result: z.unknown().optional()
}));
/**
* Response for `GET /api/v1/automation/actions` (ADR-0018).
*
* Returns the live action/node registry — the platform's built-in actions plus
* any plugin-contributed ones — backing the designer palette and flow
* validation. Each entry is a canonical {@link ActionDescriptorSchema}.
*/
export const AutomationActionsResponseSchema = lazySchema(() => z.object({
actions: z.array(ActionDescriptorSchema).describe('Registered action descriptors (built-in + plugin)'),
total: z.number().int().nonnegative().describe('Number of descriptors returned (after any filters)'),
}));
/**
* ObjectStack Protocol - Zod Schema Definitions
*
* Defines the runtime-validated contract for interacting with ObjectStack metadata and data.
* Used by API adapters (HTTP, WebSocket, gRPC) to fetch data/metadata without knowing engine internals.
*
* This protocol enables:
* - Runtime request/response validation at API gateway level
* - Automatic API documentation generation
* - Type-safe RPC communication between microservices
* - Client SDK generation from schemas
*
* Architecture Alignment:
* - Salesforce: REST API Request/Response schemas
* - Kubernetes: API Resource schemas with runtime validation
* - GraphQL: Schema-first API design
*/
// ==========================================
// Discovery & Metadata Operations
// ==========================================
/**
* Get API Discovery Request
* No parameters needed
*/
export const GetDiscoveryRequestSchema = lazySchema(() => z.object({}));
/**
* Get API Discovery Response
* Derived from DiscoverySchema (single source of truth) for protocol-level use.
*
* All fields from DiscoverySchema are available but made optional (except `version`)
* to support progressive disclosure and backward compatibility with existing clients.
*
* - `routes` provides a flat endpoint map for client routing.
* - `services` is the single source of truth for service availability.
* - `apiName` is kept as an optional alias for `name` for backward compatibility.
*
* ## This schema is the CLIENT's tolerance, never a producer's licence (#4828)
*
* `.partial()` exists so a client can parse an OLDER server's response without
* exploding, and zod's default unknown-key strip lets it parse a NEWER one.
* That tolerance is correct at this boundary and wrong as a producer contract —
* and for a long time it was the only schema anything referenced, so it acted
* as both. The result was `declared ≠ enforced` in both directions at once:
* `getDiscovery()` never emitted the required `name`/`environment`/`locale` and
* still parsed clean, while the runtime dispatcher emitted `features` and
* `endpoints` — declared nowhere — and also parsed clean.
*
* So the two schemas now have separate jobs, and a gate compares them:
*
* - **{@link DiscoverySchema} is authoritative for PRODUCERS.** Every producer's
* live shape must satisfy it (`packages/metadata-protocol`, `packages/runtime`
* and `packages/rest` each carry a `discovery-schema-conformance.test.ts`).
* - **This schema is the CONSUMER's parse**, and its key set is the allowance
* those producer gates check against — i.e. `DiscoverySchema`'s keys plus the
* declared deprecated aliases. `./discovery.test.ts` pins that equivalence, so
* a key can never again appear on one side only.
*
* ## `apiName` retirement schedule (ADR-0087)
*
* `apiName` is the sole surviving deprecated alias here. `name` is canonical and
* REQUIRED by `DiscoverySchema`; as of protocol 17 every producer emits `name`,
* and `getDiscovery()` additionally emits `apiName` with the identical value so
* clients pinned to the alias keep working.
*
* - **Protocol 17 (now)**: both emitted; `name` canonical, `apiName` deprecated.
* - **Protocol 18**: producers stop emitting `apiName` and it is removed from
* this schema. Consumers migrate to `name` — a pure rename with no semantic
* change, which is why it needs no D2 conversion entry (that table converts
* AUTHORED metadata at load; a response payload has no load seam).
*
* Consumers to migrate before 18 — measured 2026-08-05 across `objectstack`,
* `objectui` and `cloud`: `packages/client/tests/integration/01-discovery.test.ts`
* (TC-DISC-001/002). No product code in any of the three repos reads `apiName`.
*
* @see DiscoverySchema in ./discovery.zod.ts — the canonical definition.
*/
export const GetDiscoveryResponseSchema = lazySchema(() => DiscoverySchema
.partial()
.required({ version: true })
.extend({
/**
* @deprecated Use `name` instead. Removed in protocol 18 — see the
* retirement schedule above. Emitted alongside `name` until then.
*/
apiName: z.string().optional().describe('API name (deprecated — use `name`; removed in protocol 18)'),
}));
/**
* Get Metadata Types Request
*/
export const GetMetaTypesRequestSchema = lazySchema(() => z.object({}));
/**
* Get Metadata Types Response
*
* Phase 3a-1: returns the bare `types` array (for backwards compatibility)
* plus rich per-type `entries` describing label, domain, file patterns, and
* runtime capability flags. Admin UIs (Metadata Directory, Resource list,
* Quick Find) consume `entries`; legacy code that only needs the type list
* keeps using `types`.
*/
export const GetMetaTypesResponseSchema = lazySchema(() => z.object({
types: z.array(z.string()).describe('Available metadata type names (e.g., "object", "plugin", "view")'),
entries: z.array(z.object({
type: z.string().describe('Singular type identifier'),
label: z.string().describe('Human-readable label'),
description: z.string().optional().describe('Brief description'),
filePatterns: z.array(z.string()).describe('Glob patterns used to discover artifacts of this type'),
supportsOverlay: z.boolean().describe('Loader can merge per-org overlays on top of artifact'),
allowOrgOverride: z.boolean().describe('Per-org overlay writes accepted at runtime (may be env-elevated)'),
allowRuntimeCreate: z.boolean().describe('New artifacts of this type can be created via runtime API'),
supportsVersioning: z.boolean().describe('History is tracked for this type'),
executionPinned: z.boolean().describe('Runtime transactions pin a specific historical version_hash (ADR-0009)'),
loadOrder: z.number().int().describe('Loading priority (lower = earlier)'),
domain: z.enum(['data', 'ui', 'automation', 'system', 'security', 'ai']).describe('Protocol domain'),
overrideSource: z.enum(['registry', 'env']).describe('Whether allowOrgOverride is set in the static registry or via OS_METADATA_WRITABLE env var'),
createSeed: z.unknown().optional().describe('Authoritative minimal valid create seed for this type — Studio/CLI/API derive create defaults from it (single source of truth in @objectstack/spec). Absent for canvas-create types whose shape is built interactively.'),
})).optional().describe('Enriched per-type registry entries (Phase 3a)'),
}));
/**
* Get Metadata Items Request
* Get all items of a specific metadata type
*
* **`environmentId` is deliberately NOT a member here — or on any meta-read
* request schema** (maintainer ruling 2026-08-18, #9741). It is the
* TRANSPORT-level multi-kernel routing key: the REST layer resolves the target
* kernel from it *before* the protocol call, and the implementation's own
* parameter types (`@objectstack/metadata-protocol`) never read it off the
* request. Its absence from these schemas is a recorded contract decision —
* "not part of the request" — not an undeclared-surface omission. Callers that
* must carry it alongside a request do so in a typed transport envelope at
* their own layer (see `TransportScopedMetaRequest` in `@objectstack/rest`),
* never by widening these shapes.
*/
export const GetMetaItemsRequestSchema = lazySchema(() => z.object({
type: z.string().describe('Metadata type name (e.g., "object", "plugin")'),
packageId: z.string().optional().describe('Optional package ID to filter items by'),
organizationId: z.string().optional().describe(
'Organization (tenant) scope for the read. Selects the org partition in the '
+ 'ADR-0005 overlay read order — org overlay wins over env-wide overlay wins '
+ 'over packaged artifact — so it decides which tenant\'s customization rows '
+ 'are merged into the list. Absent = environment-wide read: only env-level '
+ 'overlays apply and no org partition is consulted.',
),
previewDrafts: z.boolean().optional().describe(
'Draft-visibility switch (ADR-0033 draft-overlay preview): when true, '
+ 'pending `state=\'draft\'` rows are overlaid on the active list — draft '
+ 'wins on name collision, draft-only items appear, and each overlaid item '
+ 'is tagged `_draft: true` so UIs can badge the preview. Absent/false = '
+ 'published world only. Declaration ≠ authorization: this member only '
+ 'switches which rows are read, and ADR-0106 masking is unaffected — '
+ 'callers without draft-preview authorization are refused upstream '
+ '(admin-gated), not by this schema.',
),
}));
/**
* Get Metadata Items Response
*/
export const GetMetaItemsResponseSchema = lazySchema(() => z.object({
type: z.string().describe('Metadata type name'),
items: z.array(z.unknown()).describe('Array of metadata items'),
}));
/**
* Get Metadata Item Request
* Get a specific metadata item by type and name
*/
export const GetMetaItemRequestSchema = lazySchema(() => z.object({
type: z.string().describe('Metadata type name'),
name: z.string().describe('Item name (snake_case identifier)'),
packageId: z.string().optional().describe('Optional package ID to filter items by'),
organizationId: z.string().optional().describe(
'Organization (tenant) scope for the read. Selects the org partition in the '
+ 'ADR-0005 overlay read order — org overlay wins over env-wide overlay wins '
+ 'over packaged artifact — so it decides which tenant\'s customization row '
+ 'is served as the item. Absent = environment-wide read: only env-level '
+ 'overlays apply and no org partition is consulted.',
),
state: z.enum(['active', 'draft']).optional().describe(
'Draft-visibility switch — which lifecycle row to read (strict mode): '
+ '`\'draft\'` opens the pending draft buffer (Studio\'s editor read) and '
+ 'fails when no draft exists; absent or `\'active\'` reads the live '
+ 'published row. Distinct from `previewDrafts`, which FALLS BACK to the '
+ 'active row when no draft exists. Declaration ≠ authorization: this '
+ 'member only selects which stored row is read — ADR-0106 masking is '
+ 'unaffected, and draft access is gated upstream, not by this schema.',
),
previewDrafts: z.boolean().optional().describe(
'Draft-visibility switch (ADR-0033 draft-overlay preview, non-strict): '
+ 'when true and `state` is not `\'draft\'`, a pending draft row is '
+ 'preferred if one exists, else the read falls back to the active row — '
+ 'the render path degrades to the published value instead of erroring. A '
+ 'served draft is tagged `_draft: true` so UIs can badge it. Declaration '
+ '≠ authorization: this member only switches which row is read, and '
+ 'ADR-0106 masking is unaffected — draft preview is admin-gated '
+ 'upstream, not by this schema.',
),
}));
/**
* ADR-0010 read-side protection envelope — the flags a metadata READ publishes
* alongside the document, all derived from one `resolveLockState()` call.
*
* These are the UN-prefixed, envelope-level counterparts of the `_lock` /
* `_provenance` fields `MetadataProtectionFields` splices into the document
* itself: the document stores `_lock`, and the read RESOLVES it into `lock`
* plus the three `editable` / `deletable` / `resettable` verdicts Studio
* renders affordances from (ADR-0010 §5), so no consumer re-implements the
* lock algebra.
*
* Shared by {@link GetMetaItemResponseSchema} and
* {@link GetMetaItemLayeredResponseSchema} — both are produced by the SAME
* `resolveLockState` call in `metadata-protocol`, so a mixin is what keeps the
* two declarations from drifting apart key by key. Module-local on purpose: it
* is a shape these two responses share, not a new public vocabulary.
*
* Every key is optional HERE and tightened per-response where the producer
* guarantees presence — see each schema's note. Optionality is measured, not
* assumed: the six `lockReason` … `packageVersion` keys are spread only when
* `!== undefined` (they read off `_`-prefixed document fields that are
* themselves optional), so they are conditional on EVERY path.
*/
const MetadataProtectionEnvelopeFields = {
lock: MetadataLockSchema.optional().describe(
'Resolved lock verdict for this item (ADR-0010 §3.3). `none` means unlocked; '
+ '`no-overlay` / `no-delete` / `full` refuse the corresponding write with '
+ '403 `ITEM_LOCKED`. Resolved from the document\'s `_lock`, with the packaged '
+ 'artifact winning over any org overlay.',
),
lockReason: z.string().optional().describe(
'Human-readable explanation shown next to a refused write. Present only when '
+ 'the resolved item declares `_lockReason`.',
),
lockSource: MetadataLockSourceSchema.optional().describe(
'Which layer asserted the lock. Present only when the resolved item declares '
+ '`_lockSource`.',
),
lockDocsUrl: z.string().optional().describe(
'Documentation link surfaced beside `lockReason`. Present only when the '
+ 'resolved item declares `_lockDocsUrl`.',
),
provenance: MetadataProvenanceSchema.optional().describe(
'Where the item came from (package | org | env-forced). Present only when the '
+ 'resolved item declares `_provenance`.',
),
packageId: z.string().optional().describe(
'Owning package machine id. Present only when the resolved item declares '
+ '`_packageId`.',
),
packageVersion: z.string().optional().describe(
'Owning package version. Present only when the resolved item declares '
+ '`_packageVersion`.',
),
editable: z.boolean().optional().describe(
'Whether an overlay write is permitted — false iff `lock` is `no-overlay` or '
+ '`full`. A derived verdict: do not recompute it from `lock` client-side.',
),
deletable: z.boolean().optional().describe(
'Whether deleting the overlay is permitted — false iff `lock` is `no-delete` '
+ 'or `full`.',
),
resettable: z.boolean().optional().describe(
'Whether the item can be reset to its packaged default — true iff it is '
+ 'artifact-backed, i.e. there is a baseline to reset TO.',
),
} as const;
/**
* Get Metadata Item Response
*
* Describes the FULL body `GET /api/v1/meta/:type/:name` can return, not the
* three-key subset it used to claim (#5950 — the read-side twin of the write-side
* gap #5745 closed on {@link SaveMetaItemResponseSchema}).
*
* The declaration stopped at `{ type, name, item }` while the uncached branch
* served ten more keys: the ADR-0010 protection envelope, spread onto the wire
* verbatim by the REST layer (`rest-server.ts`'s `translateMetaEnvelope` does
* `{ ...envelope, item }`). `lock` in particular is the read half of the ADR-0008
* optimistic-concurrency story the write half already declares — so an SDK caller
* typed against this response could not see it, and reading it meant a cast, the
* consumer-side tolerance this repo rejects by Prime Directive #12.
*
* **Why every protection key is optional, measured rather than assumed.** This
* route reaches a body by two branches and they publish different amounts:
*
* - **cached** (`getMetaItemCached`, THE DEFAULT — `enableCache` defaults to
* `true`): the REST layer rebuilds the envelope as `{ type, name, item }` and
* deliberately resolves NO lock — it is the fast published-value path and
* never consults the lock resolver (`rest-server.ts`, the `cachedEnvelope`
* note). All ten keys are ABSENT.
* - **uncached** (`getMetaItem`): `lock`, `editable`, `deletable` and
* `resettable` are always set; the other six appear only when the resolved
* document carries the corresponding `_`-prefixed field.
*
* So `optional` here means "this deployment/branch did not publish it", NEVER
* "unlocked" — a consumer that needs the OCC carriers must read the uncached
* path and must not read absence as `lock: 'none'`. Declaring them required
* would make the default deployment's own response fail its own contract, which
* is the #5563 defect in mirror image.
*
* ⚠️ This is a DECLARATION change only — zero runtime behaviour is altered. That
* lock presence depends on a server-side cache setting is a separate, larger
* question (#5950 says so explicitly) and is deliberately NOT decided here.
*
* [#10235] `sortability` is the one envelope key that is NOT part of the
* protection family: the per-column sortability projection, present exactly
* when the served type is `object` (every branch — cached included, unlike the
* protection keys — because it is computed from the served document itself,
* never from the lock resolver). See `sortability.zod.ts` for the category
* set and the consumer contract.
*/
export const GetMetaItemResponseSchema = lazySchema(() => z.object({
type: z.string().describe('Metadata type name'),
name: z.string().describe('Item name'),
item: z.unknown().describe('Metadata item definition'),
sortability: ObjectSortabilitySchema.optional().describe(
'Per-column sortability projection — present exactly when `type` '
+ 'is `object`, on every serving branch. Computed at serve time from the '
+ 'served document via the spec\'s own storage predicates; consumers render '
+ 'sort affordances from this signal and never re-derive it from field '
+ '`type`. See `ObjectSortabilitySchema` for the closed category set.',
),
...MetadataProtectionEnvelopeFields,
}));
/**
* Get Metadata Item — LAYERED Request
*
* Request shape for `GET /api/v1/meta/:type/:name/layers` (the
* `getMetaItemLayered` protocol method). Mirrors the implementation's
* parameter type in `@objectstack/metadata-protocol` member for member — a
* declared-surface catch-up, not a new capability: the verb and every member
* here already ship and are enforced.
*/
export const GetMetaItemLayeredRequestSchema = lazySchema(() => z.object({
type: z.string().describe('Metadata type name'),
name: z.string().describe('Item name'),
packageId: z.string().optional().describe(
'Optional package ID — scopes the `code` layer so a same-name collision '
+ 'resolves to the requested package\'s artifact (ADR-0048).',
),
organizationId: z.string().optional().describe(
'Organization (tenant) scope for the read. Selects the org partition in the '
+ 'ADR-0005 overlay read order, so it decides which tenant\'s customization '
+ 'row is reported as the `overlay` layer (and merged into `effective`). '
+ 'Absent = environment-wide read: `overlay` reports the env-level row only.',
),
}));
/**
* Get Metadata Item — LAYERED Response
*
* The body of `GET /api/v1/meta/:type/:name/layers`: a three-layer diagnostic
* projection that shows the packaged baseline, the tenant's customization row
* and the merged result SIDE BY SIDE, which is what drives Studio's
* "code default vs override vs effective" comparison tabs.
*
* **Why this is a separate schema on a separate path** (#5882, ruled B by the
* maintainer 2026-08-06). This projection used to be reached by putting
* `?layers=true` on the ordinary read, so one route answered two unrelated
* resource representations while `packages/spec` declared only one of them —
* anything generating a client from the route table (SDK annotations, codegen,
* an AI-written integration) produced a parser that was simply wrong for the
* flagged call. Collapsing the three layers into
* {@link GetMetaItemResponseSchema}'s single `item` was never an option: seeing
* the layers apart IS the diagnostic. The rejected alternative was teaching the
* route declaration to express "two shapes, chosen by query flag"; that adds a
* new primitive every future tool must understand, and conditional response
* selection is precisely where codegen and AI clients go wrong. One path, one
* response shape — so the projection got its own path.
*
* The `?layers=` flag still answers this same body during its deprecation
* window, marked with `Deprecation` / `Link` response headers.
*
* **Required vs optional, measured against the producer.** Unlike the ordinary
* read there is exactly ONE producer path here (`getMetaItemLayered`; the
* layered view deliberately skips the cache), so the four resolved verdicts
* `lock` / `editable` / `deletable` / `resettable` are ALWAYS set and are
* required below. The six conditional protection keys stay optional for the
* same reason they are optional on the ordinary read.
*/
export const GetMetaItemLayeredResponseSchema = lazySchema(() => z.object({
type: z.string().describe('Metadata type name (canonical singular)'),
name: z.string().describe('Item name'),
code: z.unknown().describe(
'LAYER 1 — the packaged artifact baseline exactly as shipped, before any '
+ 'tenant customization. `null` when no artifact ships this item (it exists '
+ 'only as an overlay).',
),
overlay: z.unknown().describe(
'LAYER 2 — the stored customization row ALONE, not merged with `code`. '
+ '`null` when this tenant has not customized the item.',
),
overlayScope: z.enum(['org', 'env']).nullable().describe(
'Which scope the `overlay` row was read from — `org` for a tenant overlay, '
+ '`env` for an environment-level one. `null` exactly when `overlay` is null.',
),
effective: z.unknown().describe(
'LAYER 3 — the merged result, i.e. the value an ordinary '
+ '`GET /meta/:type/:name` would return under `item`. `null` when the item '
+ 'resolves to nothing at all.',
),
_diagnostics: MetadataValidationResultSchema.optional().describe(
'Load-time spec-validation verdict for `effective`, so the Studio edit page '
+ 'can raise invalid-metadata banners and inline field errors without a '
+ 'second round trip. ABSENT for metadata types that register no Zod schema '
+ '(function / service / router) — absence means "no opinion", never "valid".',
),
...MetadataProtectionEnvelopeFields,
// The four resolved verdicts are unconditional on this single-producer path —
// tightened from the mixin's optional baseline. See the note above.
lock: MetadataLockSchema.describe(
'Resolved lock verdict (ADR-0010 §3.3), artifact winning over overlay. Always '
+ 'present on this path.',
),
editable: z.boolean().describe('Whether an overlay write is permitted. Always present on this path.'),
deletable: z.boolean().describe('Whether deleting the overlay is permitted. Always present on this path.'),
resettable: z.boolean().describe('Whether the item can be reset to its packaged default. Always present on this path.'),
}));
/**
* One finding from the #4463 runtime authoring gate — the fourth door.
*
* The gate runs the SHARED author-time rule registry (`@objectstack/lint`'s
* `AUTHORING_RULES`, the same table `os validate` / `os build` / `os lint` run)
* over a body about to go `active`, and partitions its findings by severity.
* The `error` half becomes the 422 `invalid_metadata` envelope's `issues[]`;
* the rest are ADVISORY — they do not block the write, and #4463 D3 decided
* they ride the 2xx response instead of being discarded.
*
* This is that ONE element shape, declared once and used by both halves, which
* is the whole point of D3's "reuse the Zod envelope": a consumer reads the
* same six keys whether the verdict arrived on a refusal or on a success.
* `@objectstack/metadata-protocol` re-exports this type as its
* `RuntimeAuthoringIssue` rather than declaring a second interface (#4717).
*/
export const RuntimeAuthoringIssueSchema = lazySchema(() => z.object({
rule: z.string().describe(
'Stable diagnostic rule id (`flow-multi-write-unfiltered`, '
+ '`approval-expression-invalid`, …). Machine-readable and stable across '
+ 'releases — the key a renderer groups or suppresses by.',
),
path: z.string().describe(
'Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), '
+ 'so an editor can jump to the offending key. May be empty when the '
+ 'finding is about the item as a whole. For the collection-resident '
+ 'write types (`object` / `permission` / `book`) the TOP-LEVEL collection '
+ 'entry is keyed by NAME (`objects.acme_invoice.sharingModel`), never by '
+ 'an array index — the gate evaluates against a private per-write '
+ 'snapshot whose indexes no caller can resolve. Every other '
+ 'write type is the sole member of its own collection, so its `[0]` is '
+ 'trivially stable and stays positional (`flows[0]...`), as do nested '
+ 'positions inside one named item (`objects.acme_invoice.indexes[1]`), '
+ 'which index the author\'s own document.',
),
where: z.string().describe(
'Human-readable location — `flow "leave_approval" · node "approve"`. Prose '
+ 'for a person; use `path` for anything mechanical.',
),
message: z.string().describe('What is wrong, in the rule author\'s own words.'),
hint: z.string().describe('How to fix it.'),
severity: z.enum(['error', 'warning', 'info']).describe(
'How the gate treated this finding. `error` means the write was REFUSED '
+ '(these appear on the 422, never on a 2xx); `warning` / `info` are '
+ 'advisory — the write succeeded and the finding is FYI.',
),
}));
/**
* Save Metadata Item Request
* Create or update a metadata item — the request shape for
* `PUT /api/v1/meta/:type/:name` (the `saveMetaItem` protocol method).
*
* Declared member for member against the implementation's parameter type in
* `@objectstack/metadata-protocol` and the REST save door's actual sends — a
* declared-surface catch-up, not a new capability (#12004, the #11006
* maintainer-ruled pattern, 2026-08-22 option B, carried one door over
* exactly as #11679/PR #12003 carried it to the reset twin). `saveMetaItem`
* is a REQUIRED protocol member, so this was the sharpest instance of the
* request-shape gap: the schema declared 3 of the ~11 members the door
* sends, and the call-site literal had to stay behind an `as any` cast
* (removing it surfaced `TS2353` on the undeclared keys — pure
* request-shape smuggling, never member-existence feature detection).
* Every member below already ships and is read and enforced by the
* implementation.
*
* `name` carries the enforced item-name grammar (#12194 — lowercase
* snake_case segments, optionally dot-qualified; `shared/identifiers.zod.ts`
* is the single source). The implementation refuses an off-grammar name at
* the door with `400 INVALID_REQUEST`, so declared = enforced. The read and
* delete request shapes deliberately stay `z.string()`: pre-grammar residue
* rows must remain listable and clearable.
*
* Two members the implementation's parameter type family carries are
* deliberately NOT declared:
*
* - `environmentId` — the TRANSPORT-level multi-kernel routing key, OUT of
* protocol request shapes by the #9741 maintainer ruling (2026-08-18):
* `resolveProtocol(environmentId)` has already selected the target kernel
* before this method is entered, and `packages/rest` layers that one
* member on top of the declared shape via its `TransportScopedMetaRequest`
* wrapper, which is where a routing key belongs.
* - `source` — write-provenance for the history/audit rows
* (`'protocol.saveMetaItem'` by default). The implementation declares it,
* but NO door sends it: its only producer is the implementation's own
* internal `migrateStoredMetadata` call (`'migrate-stored'`), which does
* not travel through this contract. The publish-door precedent (#11426)
* leaves such a member undeclared until a producer on THIS contract pulls
* it — declaring it here would advertise a wire-authorable provenance
* channel the REST layer deliberately never reads.
*
* `writeFace` IS declared, and the distinction with `source` is the point:
* both are server-stated, but `writeFace` is sent by two doors and the
* duplicate-package internal call — three real producers on this parameter
* — and the implementation branches its refusal envelopes on it. See the
* member's own doc for why declaring it does not make it client-authorable.
*/
export const SaveMetaItemRequestSchema = lazySchema(() => z.object({
type: z.string().describe('Metadata type name'),
name: MetadataItemNameSchema.describe(
'Item name — lowercase snake_case segments, optionally dot-qualified '
+ '(`crm_lead`, `crm_lead.pipeline`). Slash-compound names are refused at '
+ 'the publish door.',
),
item: z.unknown().describe('Metadata item definition'),
organizationId: z.string().optional().describe(
'Organization (tenant) scope for the write. Load-bearing, not advisory: '
+ 'it selects the overlay partition (ADR-0005) the row lands in — an '
+ 'org-scoped save writes that tenant\'s own overlay, while an org-less '
+ 'save writes the environment-wide row every tenant reads — and it is '
+ 'the scope stamped on the write\'s audit row. An org-scoped write of a '
+ 'type whose registry entry declares `allowOrgOverride: false` is '
+ 'refused (403). Absent = environment-wide.',
),
parentVersion: z.string().nullable().optional().describe(
'ADR-0008 optimistic-concurrency pin: the version token the caller '
+ 'believes is current (on the REST door, the `If-Match` request '
+ 'header). Present as a string, a concurrent edit is reported as a 409 '
+ 'conflict instead of silently overwritten. ⚠️ `null` is NOT the same '
+ 'as absent: a present `null` asserts "no current row of this '
+ 'lifecycle" — the first-write pin, refused 409 when a row already '
+ 'exists — while an ABSENT key is unpinned: the implementation adopts '
+ 'the current row\'s hash as the parent (last-write-wins). Nullable '
+ 'because that is the implementation\'s parameter type, and unlike the '
+ 'reset twin (which folds a present `null` back to the current hash) '
+ 'this verb passes `null` through to the repository\'s conflict check '
+ 'unchanged.',
),
actor: z.string().optional().describe(
'Identity recorded on the write\'s history event (`recorded_by`, a '
+ 'lookup into `sys_user`) and audit row. On the REST door this is the '
+ 'request\'s authenticated identity (one producer) — never a '
+ 'caller-supplied header. Absent, the event is recorded actor-less '
+ '(null), deliberately not attributed to "system".',
),
force: z.boolean().optional().describe(
'Destructive-change acknowledgement (`?force=true` on the REST door): '
+ 'skips the safety diff that refuses an `object` save whose body drops '
+ 'fields or narrows types the stored item still carries (409 with the '
+ 'findings otherwise). Only `object` saves reach that diff, so the '
+ 'flag is inert for every other type. Absent = the guard runs.',
),
mode: z.enum(['draft', 'publish']).optional().describe(
'Per-item lifecycle (ADR-0005 drafts): `draft` stages the body as a '
+ 'pending draft overlay (`?mode=draft` on the REST door; the publish '
+ 'door promotes it later); `publish` or ABSENT writes straight to the '
+ 'live `active` row — the legacy default, kept so callers that '
+ 'predate the draft/publish split keep working. Any value other than '
+ '`draft` is read as `publish`.',
),
packageId: z.string().nullable().optional().describe(
'ADR-0048 — the software package to bind the saved row to '
+ '(`sys_metadata.package_id`; `?package=<id>` on the REST door, sent '
+ 'only when it names a real package). Set when authoring inside a '
+ 'Studio package workspace; a named read-only base package is refused. '
+ 'On create the row is stamped with this id; on update an existing '
+ 'binding is preserved, never silently re-bound. Absent = env-local '
+ 'overlay (no package stamp); it also scopes which row the unpinned '
+ 'parent-version resolution reads.',
),
writeFace: z.enum(['package-duplicate', 'meta-envelope', 'meta-dispatch']).optional().describe(
'Which write door a refusal is being rendered FOR — stated by the '
+ 'SERVER, never by a remote caller: every door builds this request '
+ 'field by field and never spreads a request body into it, so there is '
+ 'no path for a client to smuggle a face in, and a face arriving in a '
+ 'wire body is simply never read. Two refusals branch on it, for '
+ 'different questions: the 409 destructive-change remedy names the '
+ 'acknowledgement mechanism that actually exists on the refusing door '
+ '(`?force=true` on the REST doors; the dispatcher and the '
+ 'duplicate-package door have none), and the 422 invalid-metadata '
+ 'message adapts to whether a structured `issues[]` channel reaches '
+ 'the consumer beside it. Absent = the conservative default wording.',
),
}));
/**
* Save Metadata Item Response
*
* Describes the FULL body `PUT /api/v1/meta/:type/:name` returns, not a subset
* of it (#5745, settled by the #5563 maintainer ruling "补齐 spec 字段"). The
* declaration previously stopped at `{ success, message }`, so a `.parse()` of
* a real response silently STRIPPED `version` / `seq` / `state` — and
* `SaveMetaItemResponse` could not even name them at the type level. `version`
* in particular is the token the ADR-0008 optimistic-concurrency chain already
* runs on (echo it back as `If-Match` on the next write to get a 409 instead of
* a lost update), so leaving it undeclared meant the OCC carrier existed on the
* wire with no contract behind it.
*
* Presence was measured against `origin/main`, not assumed: the sole producer is
* `ObjectStackProtocolImplementation.saveMetaItem`, whose single success return
* is the repository write path, and the REST route hands that object to
* `res.json()` verbatim. That path always sets `version` / `seq` / `state`, so
* the three are REQUIRED here; `projectionApplied` is conditional on an
* ADR-0094 mutation projector being registered for the type, so it alone is
* optional. (A second, receipt-less legacy return used to exist and would have
* forced all three to be optional — it was proved unreachable and deleted in
* #5264 / PR #5782, which is why `required` is safe to state.)
*/
export const SaveMetaItemResponseSchema = lazySchema(() => z.object({
success: z.boolean(),
version: z.string().describe(
'Content hash of the just-committed body, and the token the ADR-0008 '
+ 'optimistic-concurrency chain runs on: send it back as the `If-Match` '
+ 'request header on the next write to that item and a concurrent edit is '
+ 'reported as 409 `metadata_conflict` instead of silently overwritten. '
+ 'Opaque to callers — echo it verbatim, never parse it. Currently emitted '
+ 'as `sha256:<64 hex chars>`, but the format is not part of this contract.',
),
seq: z.number().int().describe(
'Monotonic sequence number of the metadata event this write appended to '
+ 'the item history (sys_metadata_history.event_seq). Orders writes; unlike '
+ '`version` it is not an OCC token.',
),
state: z.enum(['draft', 'active']).describe(
'Lifecycle the body was written into: "draft" when the request asked for '
+ 'draft mode (`?mode=draft`), otherwise "active" (published and live). A '
+ 'draft is staged only — it is not served to the runtime until published.',
),
projectionApplied: z.object({
success: z.boolean().describe('False when the projector threw; the metadata write itself still succeeded.'),
error: z.string().optional().describe('Projector failure message, present only when `success` is false.'),
}).optional().describe(
'Outcome of the awaited ADR-0094 mutation projector — the post-persist step '
+ 'that materializes this metadata into its derived data-plane read model '
+ '(e.g. `permission` → `sys_permission_set`). Present ONLY when a projector '
+ 'is registered for this metadata type, which is why it is optional: its '
+ 'absence means "no projector ran", never "the projection failed". '
+ 'Best-effort by design — a projector failure is reported here and logged, '
+ 'never thrown, so a caller that needs the read model to be live must check '
+ '`projectionApplied.success` rather than rely on the 200.',
),
advisories: z.array(RuntimeAuthoringIssueSchema).optional().describe(
'Non-gating findings from the runtime authoring gate — the same '
+ 'shared author-time rules `os validate` / `os build` / `os lint` run, '
+ 'applied to this body on its way to `active`. The write SUCCEEDED; these '
+ 'are what the gate has to say about it anyway (closing D3). '
+ 'Present ONLY when at least one advisory was raised — an empty array is '
+ 'never emitted, so a clean save\'s response bytes are unchanged and '
+ 'absence means "nothing to report", never "the gate did not run". '
+ 'Advisory by construction: every entry has `severity` `warning` or '
+ '`info`, because an `error` finding refuses the write and arrives as the '
+ '422 `invalid_metadata` envelope instead of here. A caller that ignores '
+ 'this key behaves exactly as before. Runtime-only: the CLI surfaces the '
+ 'same findings on its own stdout, and a Studio / MCP / AI author has no '
+ 'CLI at all, which is the gap this key exists to close. The gate runs on '
+ 'both write doors (D1), and both report: '
+ '`POST /meta/:type/:name/publish` carries the same key on '
+ '`PublishMetaItemResponseSchema`.',
),
message: z.string().optional(),
}));
/**
* Publish Metadata Item Request
*
* Request shape for `POST /api/v1/meta/:type/:name/publish` (the
* `publishMetaItem` protocol method) — the promotion door: `saveMetaItem`
* (with `?mode=draft`) stages a body, and this verb promotes the pending
* DRAFT overlay to the live `active` row. Mirrors the implementation's
* parameter type in `@objectstack/metadata-protocol` member for member — a
* declared-surface catch-up, not a new capability (#11006, maintainer ruling
* 2026-08-22: option B, closing the half-declared door #7294 left — the
* response side was declared there while the request and the interface member
* were not). The verb and every member here already ship and are enforced.
*
* Two members the implementation's parameter type family carries are
* deliberately NOT declared:
*
* - `environmentId` — the TRANSPORT-level multi-kernel routing key, OUT of
* protocol request shapes by the #9741 maintainer ruling (2026-08-18):
* `resolveProtocol(environmentId)` has already selected the target kernel
* before this method is entered, and `packages/rest` layers that one member
* on top of the declared shape via its `TransportScopedMetaRequest` wrapper,
* which is where a routing key belongs.
* - `_skipSeedApply` — internal coordination between `publishPackageDrafts`
* and the per-item path (the batch door suppresses the per-item seed apply
* and loads every seed body in one later pass). Never read from the wire,
* so it is not part of the contract.
*/
export const PublishMetaItemRequestSchema = lazySchema(() => z.object({
type: z.string().describe('Metadata type name'),
name: MetadataItemNameSchema.describe(
'Item name — lowercase snake_case segments, optionally dot-qualified '
+ '(`crm_lead`, `crm_lead.pipeline`). The promotion door enforces the '
+ 'same grammar as `saveMetaItem`.',
),
organizationId: z.string().optional().describe(
'Organization (tenant) scope for the promotion. The implementation resolves '
+ 'the draft through the org partition (ADR-0005), so a draft '
+ 'authored org-scoped must be published under the same scope or the lookup '
+ 'answers 404 `[no_draft]`. Absent = environment-wide.',
),
actor: z.string().optional().describe(
'Identity recorded on the `op=\'publish\'` history event. On the REST door '
+ 'this is the request\'s authenticated identity (one producer) — '
+ 'never a caller-supplied header.',
),
message: z.string().optional().describe(
'Optional human-readable note recorded with the publish history event.',
),
packageId: z.string().nullable().optional().describe(
'ADR-0048 — the software package the draft being promoted was listed '
+ 'under, when the caller has one to state (`?package=<id>` on the REST '
+ 'door). ⚠️ `null` is NOT the same as absent, and the '
+ 'difference is load-bearing: the implementation branches on the KEY '
+ 'BEING PRESENT, so an ABSENT key keeps the historical "match any '
+ 'package" resolution while `null` pins the lookup to the '
+ 'package-UNBOUND row. Spread it in conditionally; a '
+ 'present-and-`undefined` key coerces to `null` downstream and makes a '
+ 'package-bound draft unfindable — a silent `no_draft` on the untouched '
+ 'path.',
),
}));
/**
* Publish Metadata Item Response
*
* Describes the FULL body `POST /api/v1/meta/:type/:name/publish` returns
* (#7294 — the #5745 discipline carried one door over). The publish door is the
* sibling write surface of the save door: `saveMetaItem` writes a body, and
* `publishMetaItem` promotes an already-written DRAFT body to `active`. Until
* this declaration the route was served with no contract behind it at all —
* the string `PublishMetaItem` appeared nowhere under `packages/spec/src/`, so
* `version` here sat in exactly the undeclared state `version` on the save
* response sat in before #5745, despite being the same ADR-0008
* optimistic-concurrency token with the same "echo it back as `If-Match`" job.
*
* Presence was measured against `origin/main`, not assumed. The sole producer
* is `ObjectStackProtocolImplementation.publishMetaItem`, which builds ONE
* response object and the REST route hands it to `res.json()` verbatim. That
* object literal always sets `success` / `version` / `seq`, so the three are
* REQUIRED; the three `*Applied` receipts are each attached only when the
* corresponding side effect ran, so each is optional — and their absence means
* "that side effect did not run", NEVER "it failed".
*
* **The three conditional receipts, and what makes each conditional**
* (`runPublishSideEffects`, phase 2 of the ADR-0067 D2 split):
*
* - `seedApplied` — only when the published type is `seed`. Publishing a seed
* is what makes its rows live, so the row materialization rides along.
* - `materializeApplied` — only when an ADR-0086 P2 publish materializer is
* registered for the type (e.g. `permission` → `sys_permission_set`).
* - `projectionApplied` — only when an ADR-0094 mutation projector is
* registered for the type. Same field, same meaning, as on
* {@link SaveMetaItemResponseSchema}: both write doors run the projector.
*
* All three are **best-effort by contract**: publishing the metadata always
* succeeds independently, and a side-effect failure is SURFACED here rather
* than thrown. So `success: true` on the envelope does not mean the data plane
* caught up — a caller that needs it live must read the receipt's own
* `success`, which is why every receipt carries one.
*
* **`advisories` is the fourth conditional key** (#9176, mirroring #4717 one
* door over): the #4463 runtime authoring gate runs on BOTH write doors by
* D1 — a draft→active promotion is gated exactly as a direct active save —
* and its non-blocking findings ride the 2xx of whichever door earned it.
* Until #9176 only the save door reported; the promotion call site received
* the gate's advisory return and discarded it, which mattered precisely
* because Studio's designer takes draft-then-publish on every edit, so the
* one door its authors actually use was the one that said nothing.
*/
export const PublishMetaItemResponseSchema = lazySchema(() => z.object({
success: z.boolean().describe(
'Always true on a 2xx — the draft was promoted. It does NOT cover the '
+ 'best-effort side effects below, each of which reports its own `success`.',
),
version: z.string().describe(
'Content hash of the just-promoted body, and the token the ADR-0008 '
+ 'optimistic-concurrency chain runs on: send it back as the `If-Match` '
+ 'request header on the next write to that item and a concurrent edit is '
+ 'reported as 409 `metadata_conflict` instead of silently overwritten. '
+ 'Opaque to callers — echo it verbatim, never parse it. Currently emitted '
+ 'as `sha256:<64 hex chars>`, but the format is not part of this contract.',
),
seq: z.number().int().describe(
'Monotonic sequence number of the `op=\'publish\'` metadata event this '
+ 'promotion appended to the item history (sys_metadata_history.event_seq). '
+ 'Orders writes; unlike `version` it is not an OCC token.',
),
seedApplied: z.object({
success: z.boolean().describe(
'False when the seed rows did not fully land. The publish itself still '
+ 'succeeded — check this rather than assuming data went live.',
),
inserted: z.number().int().describe('Rows created by the externalId-keyed upsert.'),
updated: z.number().int().describe('Rows updated by the externalId-keyed upsert.'),
error: z.string().optional().describe(
'Single failure message, present when the seed apply threw before the '
+ 'loader ran (including "no readable seed bodies").',
),
errors: z.array(z.unknown()).optional().describe(
'Per-record failures reported by the seed loader. Present only when the '
+ 'loader ran and returned a non-empty error list.',
),
}).optional().describe(
'Outcome of materializing a published `seed` body into data rows. Present '
+ 'ONLY when the published type is `seed` — publishing a seed is what makes '
+ 'its rows live, so the load rides along with the metadata promotion. '
+ 'Best-effort: a seed-load problem is surfaced here, never thrown, so a '
+ 'caller must check `seedApplied.success` instead of assuming the 200 '
+ 'covered the data. Absent on the batch path, which suppresses the '
+ 'per-item apply and loads every seed body in one later pass.',
),
materializeApplied: z.object({
success: z.boolean().describe('False when the materializer threw or reported failure; the publish still succeeded.'),
inserted: z.number().int().describe('Data-plane rows created by the materializer.'),
updated: z.number().int().describe('Data-plane rows updated by the materializer.'),
error: z.string().optional().describe('Materializer failure message, present only when `success` is false.'),
}).optional().describe(
'Outcome of the ADR-0086 P2 publish-time materializer — the step that '
+ 'projects the published body into its data-plane row (e.g. `permission` '
+ '→ `sys_permission_set`, under the owning package). Present ONLY when a '
+ 'materializer is registered for this metadata type, which is why it is '
+ 'optional: its absence means "no materializer ran", never "it failed". '
+ 'Best-effort, same contract as `seedApplied`.',
),
projectionApplied: z.object({
success: z.boolean().describe('False when the projector threw; the metadata promotion itself still succeeded.'),
error: z.string().optional().describe('Projector failure message, present only when `success` is false.'),
}).optional().describe(
'Outcome of the awaited ADR-0094 mutation projector — the post-persist step '
+ 'that materializes this metadata into its derived data-plane read model. '
+ 'The same receipt {@link SaveMetaItemResponseSchema} carries, because the '
+ 'projector runs on BOTH write doors: a direct active save and this '
+ 'draft→active promotion. Present ONLY when a projector is registered for '
+ 'this metadata type. Best-effort — a projector failure is reported here '
+ 'and logged, never thrown.',
),
advisories: z.array(RuntimeAuthoringIssueSchema).optional().describe(
'Non-gating findings from the runtime authoring gate — the same '
+ 'shared author-time rules `os validate` / `os build` / `os lint` run, '
+ 'applied to the DRAFT body this promotion carried to `active` ('
+ 'the same key `SaveMetaItemResponseSchema` carries, because the gate '
+ 'runs on both write doors, D1). The promotion SUCCEEDED; these '
+ 'are what the gate has to say about it anyway. Present ONLY when at '
+ 'least one advisory was raised — an empty array is never emitted, so a '
+ 'clean publish\'s response bytes are unchanged and absence means '
+ '"nothing to report", never "the gate did not run". Advisory by '
+ 'construction: every entry has `severity` `warning` or `info`, because '
+ 'an `error` finding refuses the promotion and arrives as the 422 '
+ '`invalid_metadata` envelope instead of here. A caller that ignores '
+ 'this key behaves exactly as before. This door is the one Studio\'s '
+ 'designer takes on every edit (draft save, then publish), and a Studio '
+ '/ MCP / AI author has no CLI at all — which is the gap this key exists '
+ 'to close.',
),
message: z.string().optional().describe(
'Human-readable receipt, e.g. `Published draft — type=view, name=cases '
+ '[seq=3]`. The producer sets it on every publish today; it stays optional '
+ 'to match the producer\'s own signature and its `SaveMetaItemResponse` '
+ 'twin, and because an absent human-readable string strips no data — the '
+ 'failure mode this key exists to prevent.',
),
}));
/**
* Publish Package Drafts Response — the "publish whole app" door.
*
* Describes the FULL body `POST /api/v1/packages/:id/publish-drafts` answers
* inside the dispatcher's `{ success, data }` envelope (#9406 — the #5745
* "declared = returned" discipline carried to the batch door, the same move
* #7294 made for the single-item `POST /meta/:type/:name/publish`). Until this
* declaration the batch response was only an inline TypeScript return type in
* `@objectstack/metadata-protocol` — no spec schema, no pin suite, no
* conformance gate — so a field added to or dropped from Studio's "publish
* whole app" response could not turn anything red (#9343's discarded
* advisories were the measured instance).
*
* **The wire face is the helper's return PLUS the route's mutations** —
* measured on the producer pair, not assumed:
*
* - `ObjectStackProtocolImplementation.publishPackageDrafts` builds the base
* object. Its three return sites always set `success` / `outcome` /
* `publishedCount` / `failedCount` / `published` / `failed` (so those six
* are REQUIRED), and
* attach `seedApplied` / `materializeApplied` / `probes` / `commitId` only
* on the happy path when the corresponding fact exists (so each is
* optional — absence means "did not apply", never "failed").
* - The REST door (`packages/runtime/src/domains/packages.ts`) then mutates
* that object before `res.json()`: it back-fills `seedApplied` for custom
* protocols that do not self-apply, and attaches the ADR-0045 visibility
* flip receipts `unhiddenApps` / `unhideError` and the `metadata:reloaded`