Repository navigation
Expand file tree
/
Copy pathmetadata-plugin.zod.ts
More file actions
1440 lines (1343 loc) · 86.4 KB
/
Copy pathmetadata-plugin.zod.ts
File metadata and controls
1440 lines (1343 loc) · 86.4 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 { retiredKey } from '../shared/retired-key';
import { MetadataManagerConfigSchema } from './metadata-loader.zod';
import { SEMVER_2_0_0_VERSION_PATTERN } from './version-grammar';
import { ActionSchema } from '../ui/action.zod';
/**
* # Metadata Plugin Protocol
*
* Defines the specification for the **Metadata Plugin** — the central authority
* responsible for managing ALL metadata across the ObjectStack platform.
*
* ## Architecture
* The Metadata Plugin consolidates all scattered metadata operations into a single,
* cohesive plugin that "takes over" the entire platform's metadata management:
*
* ```
* ┌──────────────────────────────────────────────────────────────────┐
* │ Metadata Plugin │
* │ │
* │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
* │ │ Type Registry │ │ Loader │ │ Customization Layer │ │
* │ │ (all types) │ │ (file/db/s3)│ │ (overlay / merge) │ │
* │ └──────────────┘ └──────────────┘ └──────────────────────┘ │
* │ │
* │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
* │ │ Persistence │ │ Query │ │ Lifecycle │ │
* │ │ (db records) │ │ (search) │ │ (validate/deploy) │ │
* │ └──────────────┘ └──────────────┘ └──────────────────────┘ │
* └──────────────────────────────────────────────────────────────────┘
* ```
*
* ## Alignment
* - **Salesforce**: Metadata API (deploy, retrieve, describe)
* - **ServiceNow**: System Dictionary + Metadata API
* - **Kubernetes**: API Server + CRD Registry
*
* ## References
* - kernel/metadata-loader.zod.ts — MetadataManager wiring (datasource, cache, write gates)
* - system/metadata-persistence.zod.ts — Database record format + loader/watch envelope types
* - contracts/metadata-service.ts — Service interface
*/
// ==========================================
// Metadata Type Registry
// ==========================================
/**
* Platform Metadata Type Enum
*
* The canonical list of all metadata types managed by the platform.
* Each type maps to a specific Zod schema (e.g., ObjectSchema, ViewSchema).
* There is no declared-kind channel beyond this list (#8586): a plugin's kind
* enters the LIVE type set only as a side effect of registering an item of
* that kind (`SchemaRegistry.registerItem` / `MetadataManager.register`).
* A manifest's `contributes.kinds` stores kind DESCRIPTORS as items of the
* `kind` type (`SchemaRegistry.registerKind`); it does not extend this enum
* or the type registry.
*
* ## Naming Convention
* **IMPORTANT:** All metadata type names are in **SINGULAR** form:
* - ✅ Use: `'agent'`, `'tool'`, `'skill'`, `'view'`, `'flow'`, `'action'`
* - ❌ NOT: `'agents'`, `'tools'`, `'skills'`, `'views'`, `'flows'`, `'actions'`
*
* This convention applies to:
* - Protocol definitions (this enum)
* - UI plugin registrations (`metadataTypes`, `metadataIcons`)
* - Metadata service operations
* - File patterns (`*.agent.ts`, `*.tool.ts`)
*
* REST API endpoints continue to use plural forms per REST conventions:
* - `/api/v1/ai/agents`, `/api/v1/ai/conversations`
*/
import { lazySchema } from '../shared/lazy-schema';
export const MetadataTypeSchema = lazySchema(() => z.enum([
// Data Protocol
'object', // Business entity definition (ObjectSchema)
'object_extension', // Fields and rules a package adds to an object another package defines (ObjectExtensionSchema; ADR-0146 D10)
'field', // Standalone field definition (FieldSchema)
// ADR-0088: there is no `trigger` metadata type — sync data-layer logic is a
// `hook`; async automation is a `record_change` flow.
// (The `triggers` capability token in `requires:` is a different namespace.)
// ADR-0088 (#4509): there is no `validation` metadata type — validation rules
// are authored INLINE as `object.validations[]`, which is the only shape the
// engine evaluates. A standalone rule had no way to say what it validated
// (`ValidationRuleSchema` carries no object-binding key and every variant is
// strict), so an item authored here bound to nothing and intercepted no write
// — including `state_machine` rules, which ADR-0020 routes through this same
// inline shape. `ValidationRuleSchema` itself is unchanged and fully live.
'hook', // Data hooks (HookSchema)
'seed', // Seed/fixture data — runtime-draftable; publishing applies it (SeedSchema)
'mapping', // Import/export field mappings (MappingSchema) — consumed by POST /data/:object/import via mappingName (#2611); promoted to a kind per the ADR-0088 admission test once the consumer landed
'picklist', // Shared option list that select fields reference by name (PicklistSchema)
// UI Protocol
'view', // List/form views (ViewSchema)
'page', // Standalone pages (PageSchema)
'dashboard', // Dashboard layouts (DashboardSchema)
'app', // Application shell (AppSchema)
'action', // UI/Server actions (ActionSchema)
'report', // Report definitions (ReportSchema)
'dataset', // Analytics semantic layer — dimensions/measures (DatasetSchema, ADR-0021)
// Automation Protocol
'flow', // Visual logic flows (FlowSchema)
// ADR-0020: there is no `workflow` metadata type — record state machines are
// authored inline as a `state_machine` validation rule on the object (the
// `validations` discriminated union), enforced on the write path.
// ADR-0019: `approval` is no longer a metadata type — approvals are authored
// as Approval nodes inside a `flow`.
'job', // Background / scheduled jobs (JobSchema)
// System Protocol
'datasource', // Data connections (DatasourceSchema)
'external_catalog', // Cached remote schema snapshot for federated datasources (ADR-0015) — RUNTIME-CREATED by the Sync wizard (ADR-0062/0088); packages never ship one
'translation', // i18n resources (TranslationSchema)
// ADR-0088: `router`/`function`/`service` are NOT metadata kinds — they are
// code contributions: declarative `apis:` for an app, while `http.server`
// mounts stay the platform plugins' own primitive and are not an app-author
// surface (router — the manifest `contributes.routes` spelling never
// delivered and was retired, commit bc56e1881), `defineStack({ functions })` (function —
// `contributes.functions` was retired unread, commit be21955ba), and the
// plugin/service registry itself (service).
//
// [#5271, part of #5206] `api` is the ONE declarative endpoint ITEM kind, and
// it is NOT a reversal of the `router` retirement above: `router` was retired
// as a KIND because its delivered form (the platform's imperative `http.server`
// mount; the `contributes.routes` spelling never delivered and was retired,
// commit bc56e1881) is a code contribution. A single `ApiEndpoint` — a
// stable URL plus a policy layer over an existing pipeline — is a declarative
// artifact, and it passes all three clauses of ADR-0088's admission test:
// 1. INDEPENDENT LIFECYCLE — the endpoint matcher indexes, invalidates and
// re-judges one stored `api` item at a time (`buildEndpointIndex`,
// `MetadataManager.ENDPOINT_METADATA_TYPE`).
// 2. DECLARATIVE GOVERNABILITY — file patterns plus a resolved declaration
// schema (`ApiEndpointSchema`), see the registry entry below. ⚠️ This
// clause originally read "`allowRuntimeCreate: true` plus file
// patterns"; #5488 flipped that flag to `false` (maintainer ruling
// 2026-08-07), so governability now rests on the artifact route alone —
// which is the route that was ever governed. The admission test is
// unaffected: ADR-0088 asks whether the kind is DECLARATIVELY governed,
// not whether it is runtime-writable.
// 3. A REAL CONSUMER — #5040's E-series executor serves them and
// `/openapi.json` describes them; #5040 E8 proves it on a real boot.
// ADR-0088's own `router` row already anticipated this: "the endpoint
// executor is being built under #5040, after which declarative `apis:`
// becomes a third, real delivered form".
//
// The kind was live long before it was declared — artifact ingest has mapped
// `defineStack({ apis })` → `api` items (`ARTIFACT_FIELD_TO_TYPE`) all along,
// so this entry closes the MIRROR of `declared ≠ enforced`: enforced but
// undeclared. Until #5271 the type resolved no schema, so `saveMetaItem`
// stored ANY JSON under it unvalidated (#5206).
'api', // Declarative HTTP endpoints (ApiEndpointSchema, ADR-0121)
// #4616: the canonical schema is `EmailTemplateDefinitionSchema`
// (`system/email-template.zod.ts`), which is what `BUILTIN_METADATA_TYPE_SCHEMAS`
// resolves this kind to. This comment used to name `EmailTemplateSchema` — the
// legacy sub-shape spec 7.1.0 demoted when it fixed that Prime Directive #8
// double-declaration, and removed outright in #4616 — which is exactly how
// consumers kept wiring the wrong one.
'email_template', // Outbound email templates (EmailTemplateDefinitionSchema)
'doc', // Package documentation — flat Markdown items (DocSchema, ADR-0046)
'book', // Documentation navigation spine (BookSchema, ADR-0046 §6)
// Security Protocol
'permission', // Permission sets (PermissionSetSchema)
'position', // Positions — flat capability-distribution groups (ADR-0090 D3)
// [#5961] `capability` was ENFORCED BUT UNDECLARED — the same mirror of
// `declared ≠ enforced` that #5271 closed for `api`. `PLURAL_TO_SINGULAR`
// has mapped `capabilities` → `capability` since #5870, `AppPlugin`
// registers stack-declared capabilities under that exact name, and
// the declared-capability seeder (since retired, ADR-0131 D3) read them back —
// but the kind was absent
// from this enum, from `BUILTIN_METADATA_TYPE_SCHEMAS` and from
// `DEFAULT_METADATA_TYPE_REGISTRY`, so it resolved no schema and
// `PUT /api/v1/meta/capability/:name` stored ANY JSON on an authorization
// surface. ⚠️ `role` / `profile` / `policy` are a DIFFERENT question and are
// deliberately NOT admitted here: they have no `PLURAL_TO_SINGULAR` mapping,
// no declaration schema and no read-back seam — see #5961's ruling.
'capability', // Package-declared authorization capabilities (CapabilityDeclarationSchema, ADR-0066 D1)
// `sharing_rule` was a URL-map-only kind with no registry entry, so the two
// `/meta` doors read its absence as "runtime-creatable": `/meta/types`
// advertised `allowRuntimeCreate: true` and `PUT /meta/sharing_rule/:name`
// stored an environment-wide item the sharing service then enforced — a third
// authoring door beside the organization's `sys_sharing_rule` rows and the
// package's declaration, past both the same-name guard and the organization
// wall. Regime C (ADR-0131 D6) has two sources and only two; the registry
// entry declares the type code-only so both doors read one sealed answer.
'sharing_rule', // Package-declared sharing rules (SharingRuleSchema, ADR-0131 D6 regime C)
// [ADR-0142 D1] An ownership queue — a named set of people records sit in.
// Regime C exactly as `sharing_rule`: a package declares it (the stack's
// `queues`, read from the registry) and an organization authors its own
// (`sys_queue` rows); the registry entry below is code-only.
'queue', // Package-declared ownership queues (QueueSchema, ADR-0142 D1)
// [ADR-0142 D5] Declared routing of an inserted row to a queue or a user,
// evaluated by the engine once per insert. Declared only (merge question
// 11); switched off through the activation ledger.
'assignment_rule', // Package-declared assignment rules (AssignmentRuleSchema, ADR-0142 D5)
// AI Protocol
'agent', // AI agent definitions (AgentSchema)
'tool', // AI tool definitions (ToolSchema)
'skill', // AI skill definitions (SkillSchema)
]));
export type MetadataType = z.input<typeof MetadataTypeSchema>;
// ==========================================
// Type Registry Entry
// ==========================================
/**
* Metadata Type Registry Entry
*
* Describes a registered metadata type, including its validation schema,
* file patterns, and capabilities. Used by the metadata plugin to:
* 1. Discover metadata files on disk
* 2. Validate metadata payloads
* 3. Determine storage behavior
*/
/**
* Base shape of a metadata type registry entry, without invariants.
* Used internally where Zod operations (`.omit`, `.extend`) that don't
* compose with refinements are required.
*/
const MetadataTypeRegistryEntryBaseSchema = z.object({
/** Metadata type identifier (e.g., 'object', 'view') */
type: MetadataTypeSchema.describe('Metadata type identifier'),
/** Human-readable label */
label: z.string().describe('Display label for the metadata type'),
/** Brief description */
description: z.string().optional().describe('Description of the metadata type'),
/**
* File glob patterns for this type.
* Used to discover metadata files on disk.
*
* ⚠️ DISCOVERY IS THE LEAST EXERCISED OF THIS FIELD'S USES (#12075, recorded
* beside the declaration by commit b307bfd2a). The only reader that actually globs
* these patterns is `MetadataPlugin._loadFromFileSystem`
* (`packages/metadata/src/plugin.ts`), reached on exactly ONE branch:
* `eager` bootstrap (the default) AND no `options.artifactSource`.
* Re-measured on `origin/main`: both non-test `new MetadataPlugin(...)`
* sites — `runtime/src/standalone-stack.ts` and `cli/src/commands/serve.ts`
* — set an `artifactSource` unconditionally, and all nine tracked
* `objectstack.config.ts` declare their metadata in code, so nothing this
* repo boots, scaffolds or ships reaches the glob pass. Recorded
* disposition: no end-to-end dogfood is being minted for that path —
* startup focus, and a path with zero measured consumers does not earn one.
* Unexercised is NOT broken, only unmeasured. The full measurement,
* discriminator included, lives on the CLI's own read of this field
* (`packages/cli/src/utils/metadata-file-name.ts`).
*
* ⛔ Do not read that as "this field is inert". Its VALUES have live readers
* that never glob: the CLI derives every scaffolded file name from them
* (commit 50fb191dc, pinned by `cli/test/generate-file-name-registry-parity.test.ts`),
* and `codeOnlySourceHint` reads `filePatterns[0]` back as the prescription
* in a 403 `not_creatable` refusal — which the `capability` and `api`
* entries of `DEFAULT_METADATA_TYPE_REGISTRY` below already lean on.
*
* @example ["**\/*.object.ts", "**\/*.object.yml"]
*/
filePatterns: z.array(z.string()).describe('Glob patterns to discover files of this type'),
/**
* Whether a stored overlay row of an item a managed package ships may be
* REMOVED through the ordinary delete door without the type's
* environment-overlay permission ({@link allowEnvironmentOverlay}): the
* removal removes no managed definition, and changes what is served for
* `permission` alone (the repair carve-out for overlay rows stored before a
* type's permission was rolled back).
*
* It opens no save and it decides no read: whether an environment overlay of
* a packaged item is written or served is decided by
* `allowEnvironmentOverlay` alone (ADR-0131 D6). Its readers are the delete
* tier of the metadata protocol and the repository's delete gate.
*/
supportsOverlay: z.boolean().default(true).describe(
'Whether a stored overlay row of an item a managed package ships may be removed through the '
+ 'ordinary delete door without the type\'s environment-overlay permission: the removal removes '
+ 'no managed definition, and changes what is served for permission alone; it opens no save and '
+ 'it decides no read'
),
/**
* Whether an environment may overlay an item of this type that a managed
* package ships — ADR-0131 D6 regime O, ADR-0005's environment layer: an
* environment row of the same name replaces the packaged item whole.
*
* This is the single decider of that question at every door: the metadata
* protocol's save door, the managed-content seal on write AND on read (a
* stored environment overlay of a packaged item of a type without it is
* withheld from the served read), the repository's put gate, and Studio's
* write gate (`GET /api/v1/meta/types` carries it per entry). The
* `OS_METADATA_WRITABLE` hatch elevates the advertised value but never
* opens a managed item. Defaults to `false`: a type opts in deliberately,
* and a write against a packaged item of a type without it is refused with
* HTTP 403 `NOT_OVERRIDABLE`. Which types opt in is
* `DEFAULT_METADATA_TYPE_REGISTRY` below, and only there (ADR-0005: no
* parallel whitelists).
*
* Renamed from `allowOrgOverride` in 18: ADR-0131 D6 retired the
* per-organization overlay axis, and the key now names the one scope it
* governs.
*/
allowEnvironmentOverlay: z.boolean().default(false).describe(
'Whether an environment may overlay an item of this type that a managed package ships '
+ '(ADR-0131 D6 regime O). Decides the save door, the managed-content seal on write and on '
+ 'read, and Studio\'s write gate; the OS_METADATA_WRITABLE hatch does not reach a managed '
+ 'item. Default false.'
),
/**
* RENAMED in 18 to {@link allowEnvironmentOverlay} — tombstoned, not deleted.
*
* ADR-0131 D6 retired the per-organization overlay axis, so the key that
* said "per-org" was renamed to say the one scope it governs (ruling C on
* the ADR-0131 C5 split: no second key, and not the old name with a new
* meaning). `MetadataTypeRegistryEntryBaseSchema` is a plain `z.object`,
* so a bare deletion would strip an authored old key in silence (the
* ADR-0104 shape); the tombstone refuses it at `tsc` and at the parse with
* the rename. No D2 conversion: no manifest, stack, plugin configuration or
* stored row carries a registry entry — the registry is a TypeScript
* literal — so a conversion would have no seam that ever runs. Registered
* in `RETIRED_KEYS_BY_MAJOR[18]` with the D3 semantic entry
* `metadata-type-registry-allow-org-override-renamed`.
*
* ⚠️ A truthiness READER of the old key still compiles against this
* tombstone (the output is an optional `undefined`) and silently reads
* `undefined`, which closes every type. Read `allowEnvironmentOverlay`.
*/
allowOrgOverride: retiredKey(
'`MetadataTypeRegistryEntry.allowOrgOverride` was renamed to `allowEnvironmentOverlay` '
+ 'in @objectstack/spec 18 (ADR-0131 D6) — the per-organization overlay axis was retired, and '
+ 'the key now names the one scope it governs: whether an environment may overlay an item '
+ 'of this type that a managed package ships. Rename the key to `allowEnvironmentOverlay`; '
+ 'the value is unchanged.',
),
/**
* Whether metadata of this type can be created at runtime via API.
* Some types (e.g., 'object') may be restricted to deployment-only.
*/
allowRuntimeCreate: z.boolean().default(true).describe('Allow runtime creation via API'),
/**
* Whether this type supports versioning.
* When true, changes are tracked with version history.
*/
supportsVersioning: z.boolean().default(false).describe('Whether version history is tracked'),
/**
* Whether runtime transaction rows pin a specific historical version of
* this metadata by content hash (ADR-0009).
*
* Types with `executionPinned: true` give the strongest persistence
* guarantee in the system:
*
* 1. History rows for this type are **never** garbage-collected,
* regardless of `MetadataHistoryRetentionPolicy`.
* 2. `MetadataRepository.getByHash(ref, hash)` MUST return the
* pinned body for any hash that was ever HEAD.
* 3. Implies `supportsVersioning: true` (enforced via superRefine on
* the wrapping `MetadataTypeRegistryEntrySchema`).
*
* Use this flag for metadata describing executable business processes
* whose runtime invocations can pause across redeploys
* (`flow`, `workflow`, `approval`). Do **not** set it for types whose
* runtime state captures outputs rather than the source body
* (`agent`, `tool`).
*/
executionPinned: z.boolean().default(false).describe(
'Transaction rows reference a specific version_hash; history GC is disabled and getByHash() MUST resolve old hashes (ADR-0009)'
),
/**
* Priority order for loading (lower = earlier).
* Objects load before views, views before dashboards.
*/
loadOrder: z.number().int().min(0).default(100).describe('Loading priority (lower = earlier)'),
/** The domain this type belongs to */
domain: z.enum(['data', 'ui', 'automation', 'system', 'security', 'ai'])
.describe('Protocol domain'),
/**
* Declarative **type-level** actions for this metadata type.
*
* These are buttons the Studio metadata-admin engine renders for the type
* as a whole (or for a single item of the type) — the same `ActionSchema`
* business objects already use for row/header actions, reused verbatim so
* there is no third button mechanism to learn.
*
* The canonical example is the `datasource` **Test connection** button: an
* `ActionType: 'api'` action whose `url` calls the connection-probe
* endpoint. Built-in types declare actions here; plugins layer additional
* actions onto any type at runtime via `registerMetadataTypeActions()`
* (the engine merges declarative + registered when emitting
* `/api/v1/meta/types/:type`).
*
* Note: this is distinct from the per-record `actions` a business *object*
* carries (`ObjectSchema.actions`) — those act on rows of a user object,
* these act on definitions of a metadata type.
*/
actions: z.array(ActionSchema).optional().describe(
'Declarative type-level actions (e.g. datasource "Test connection"), reusing ActionSchema; merged with plugin-registered actions when emitted'
),
});
export const MetadataTypeRegistryEntrySchema = lazySchema(() =>
MetadataTypeRegistryEntryBaseSchema.superRefine((entry, ctx) => {
// ADR-0009 invariant: executionPinned ⇒ supportsVersioning.
// A type whose transaction rows pin a historical body MUST also be
// tracked in the history log, otherwise getByHash() has nothing to
// resolve against.
if (entry.executionPinned && !entry.supportsVersioning) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['executionPinned'],
message: `executionPinned requires supportsVersioning: true (type '${entry.type}')`,
});
}
})
);
export type MetadataTypeRegistryEntry = z.input<typeof MetadataTypeRegistryEntrySchema>;
/** Post-parse shape of {@link MetadataTypeRegistryEntry} — defaults applied, transforms run (ADR-0122). */
export type MetadataTypeRegistryEntryParsed = z.infer<typeof MetadataTypeRegistryEntrySchema>;
// ==========================================
// Metadata Query Protocol
// ==========================================
/**
* Metadata Query Schema
*
* Standard protocol for searching and filtering metadata items.
* Used by the metadata service to support advanced metadata discovery.
*/
export const MetadataQuerySchema = lazySchema(() => z.object({
/** Filter by metadata type(s) */
types: z.array(MetadataTypeSchema).optional().describe('Filter by metadata types'),
/** Filter by namespace(s) */
namespaces: z.array(z.string()).optional().describe('Filter by namespaces'),
/** Filter by package ID */
packageId: z.string().optional().describe('Filter by owning package'),
/** Full-text search across name, label, description */
search: z.string().optional().describe('Full-text search query'),
/** Filter by scope */
scope: z.enum(['system', 'platform', 'user']).optional().describe('Filter by scope'),
/** Filter by state */
state: z.enum(['draft', 'active', 'archived', 'deprecated']).optional().describe('Filter by lifecycle state'),
/** Filter by tags */
tags: z.array(z.string()).optional().describe('Filter by tags'),
/** Sort field */
sortBy: z.enum(['name', 'type', 'updatedAt', 'createdAt']).default('name').describe('Sort field'),
/** Sort direction */
sortOrder: z.enum(['asc', 'desc']).default('asc').describe('Sort direction'),
/** Pagination: page number (1-based) */
page: z.number().int().min(1).default(1).describe('Page number'),
/** Pagination: items per page */
pageSize: z.number().int().min(1).max(500).default(50).describe('Items per page'),
}));
export type MetadataQuery = z.input<typeof MetadataQuerySchema>;
/** Post-parse shape of {@link MetadataQuery} — defaults applied, transforms run (ADR-0122). */
export type MetadataQueryParsed = z.infer<typeof MetadataQuerySchema>;
/**
* Metadata Query Result
*/
export const MetadataQueryResultSchema = lazySchema(() => z.object({
/** Matched items */
items: z.array(z.object({
type: z.string().describe('Metadata type'),
name: z.string().describe('Item name'),
namespace: z.string().optional().describe('Namespace'),
label: z.string().optional().describe('Display label'),
scope: z.enum(['system', 'platform', 'user']).optional(),
state: z.enum(['draft', 'active', 'archived', 'deprecated']).optional(),
packageId: z.string().optional(),
updatedAt: z.string().datetime().optional(),
})).describe('Matched metadata items'),
/** Total count (for pagination) */
total: z.number().int().min(0).describe('Total matching items'),
/** Current page */
page: z.number().int().min(1).describe('Current page'),
/** Page size */
pageSize: z.number().int().min(1).describe('Page size'),
}));
export type MetadataQueryResult = z.input<typeof MetadataQueryResultSchema>;
// ==========================================
// Metadata Lifecycle Events
// ==========================================
// v17 dual-source cleanup (#4587): the `MetadataEvent(Schema)` pair that
// lived here (a `metadata.registered/…/exported` lifecycle-event envelope)
// was removed. It was the #4411-family dead copy: zero importers outside its
// own unit test across framework/cloud/objectui, and NOTHING ever emitted
// its event vocabulary. The bare names now belong to `@objectstack/spec/api`
// alone (`src/api/events.zod.ts`) — the realtime metadata change feed
// (`metadata.{type}.{created|updated|deleted}`) that `MetadataManager`
// publishes and the client SDK (`@objectstack/client`) subscribes to.
// Runtime *watch* events remain `MetadataWatchEvent` in
// `@objectstack/spec/system`; repository change-log events are
// `@objectstack/metadata-core`'s own `MetadataEvent` (ADR-0008).
// ==========================================
// Metadata Validation
// ==========================================
/**
* Metadata Validation Result
*/
export const MetadataValidationResultSchema = lazySchema(() => z.object({
/** Whether validation passed */
valid: z.boolean().describe('Whether the metadata is valid'),
/** Validation errors */
errors: z.array(z.object({
path: z.string().describe('JSON path to the invalid field'),
message: z.string().describe('Error description'),
code: z.string().optional().describe('Error code'),
})).optional().describe('Validation errors'),
/** Validation warnings (non-blocking) */
warnings: z.array(z.object({
path: z.string().describe('JSON path to the field'),
message: z.string().describe('Warning description'),
})).optional().describe('Validation warnings'),
}));
export type MetadataValidationResult = z.input<typeof MetadataValidationResultSchema>;
// ==========================================
// Metadata Plugin Configuration
// ==========================================
/**
* Metadata Plugin Configuration
*
* The unified configuration for the metadata plugin, combining
* storage, caching, customization, and type registry settings.
*/
export const MetadataPluginConfigSchema = lazySchema(() => z.object({
/**
* Storage configuration.
* References MetadataManagerConfigSchema for the underlying storage backend.
*/
storage: MetadataManagerConfigSchema.describe('Storage backend configuration'),
/**
* REMOVED in v17 (commit 9e0ba21a1, ADR-0049 enforce-or-remove; re-charter of #12057).
*
* `customizationPolicies` embedded the paper metadata-customization
* protocol's `CustomizationPolicySchema` (lockedFields / customizableFields
* whitelists) and was read by NOTHING: no code ever consulted a policy
* before accepting or refusing a customization, and the protocol it
* configured — the three-layer overlay of `metadata-customization.zod.ts` —
* was itself unreachable from any served surface (ADR-0126 §6 wall 4
* supersedes it: "nothing may build against it"). Authoring a policy got a
* clean parse and zero behaviour, zero diagnostics.
*
* Tombstoned rather than deleted for the same reason as `additionalTypes`
* below: `MetadataPluginConfigSchema` is not `.strict()`, so a plain
* deletion would silently strip the key (the #3726 / #3733 shape,
* ADR-0104). The mechanisms that actually govern customization are in the
* prescription.
*/
customizationPolicies: retiredKey(
'`config.customizationPolicies` was removed from `MetadataPluginConfig` in @objectstack/spec 17 ' +
'(ADR-0049 enforce-or-remove) — it never had an effect: no code ever read a ' +
'customization policy, and the overlay protocol it configured was itself unreachable from any ' +
'served surface (ADR-0126 supersedes it on the record). Delete the key. What a customization ' +
"may touch is governed by the real mechanisms: ADR-0005's environment overlay (opt-in via " +
'`allowEnvironmentOverlay` on `DEFAULT_METADATA_TYPE_REGISTRY`, enforced at the metadata ' +
"protocol's write doors and its served read) and ADR-0126's packaged-metadata model (clone + " +
'ledger disable).',
),
/**
* REMOVED in v17 (commit 9e0ba21a1, ADR-0049 enforce-or-remove; re-charter of #12057).
*
* `mergeStrategy` embedded the paper protocol's `MergeStrategyConfigSchema`
* (keep-custom / accept-incoming / three-way-merge) and was read by
* NOTHING: no 3-way merge engine ever existed, and package upgrades do not
* merge customizations — ADR-0126 §6 wall 3 separates the packaged BASE
* (upgrades rewrite it) from the customer's recorded choices (never
* touched by an upgrade). Same tombstone reasoning as above.
*/
mergeStrategy: retiredKey(
'`config.mergeStrategy` was removed from `MetadataPluginConfig` in @objectstack/spec 17 ' +
'(ADR-0049 enforce-or-remove) — it never had an effect: no 3-way merge engine ever ' +
'existed to read it, and package upgrades do not merge customizations (ADR-0126: upgrades ' +
'rewrite the packaged base; customer choices live in the ledger and are never merged). ' +
'Delete the key. There is no replacement — upgrade-vs-customization separation is the ' +
'model, not a configurable strategy.',
),
/**
* REMOVED in v17 (#8586, ADR-0049 enforce-or-remove).
*
* `additionalTypes` was declared, documented on four docs pages as THE way a
* plugin registers a custom metadata type, and read by NOTHING: the only
* production writer of the manager's type registry is
* `setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY)` (`packages/metadata/src/
* plugin.ts`), called exactly once, and it REPLACES the array outright.
* Measured: declared count == live count (27 == 27) — the live type set is
* exactly the built-in registry, whatever was authored here. The same
* silence trap as #4212's `onInstall`, one level down: write it per the
* docs, get no error, nothing happens.
*
* Tombstoned rather than deleted because `MetadataPluginConfigSchema` is not
* `.strict()`: a plain deletion would silently strip the key, replacing an
* inert declaration with an invisible one (the #3726 / #3733 shape,
* ADR-0104). How a kind ACTUALLY reaches the live set is in the
* prescription below.
*/
additionalTypes: retiredKey(
'`config.additionalTypes` was removed from `MetadataPluginConfig` in @objectstack/spec 17 ' +
'(ADR-0049 enforce-or-remove) — it never had an effect: the only production writer ' +
'of the metadata type registry is `setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY)`, which ' +
'replaces the array outright, so nothing ever merged these entries and the live type set ' +
'was exactly the built-in registry whatever you declared here. Delete the key. There is no ' +
'declared-kind channel: a kind enters the live metadata-type set as a side effect of ' +
'registering an ITEM of that kind (`SchemaRegistry.registerItem` during app/manifest ' +
'registration, or `MetadataManager.register` at runtime); bind its schema with ' +
"`registerMetadataTypeSchema(type, schema)` from your plugin's `init(ctx)` so " +
'`GET /api/v1/meta` serves a real JSON Schema for it.',
),
/**
* Enable metadata change events.
* When true, the plugin emits events on every metadata change.
*/
enableEvents: z.boolean().default(true).describe('Emit metadata change events'),
/**
* Enable metadata validation on write operations.
* When true, all metadata is validated against its type schema before saving.
*/
validateOnWrite: z.boolean().default(true).describe('Validate metadata on write'),
/**
* Enable metadata versioning.
* When true, changes to metadata are tracked with version history.
*/
enableVersioning: z.boolean().default(false).describe('Track metadata version history'),
/**
* Maximum number of metadata items to keep in memory cache.
*/
cacheMaxItems: z.number().int().min(0).default(10000).describe('Max items in memory cache'),
/**
* Bootstrap Mode
*
* Controls how the MetadataPlugin populates the in-memory registry at
* `start()` time. Tuning this is the single most effective lever for
* cold-start latency in production.
*
* - `eager` (default) — Scan filesystem and prime every type listed in
* `DEFAULT_METADATA_TYPE_REGISTRY` via `loadMany()` for each type.
* Best DX (everything is queryable immediately) at the cost of cold-start
* I/O proportional to the number of metadata files. Recommended for dev,
* Studio sessions, and small/medium production deployments.
*
* - `lazy` — Skip the filesystem priming pass entirely. Metadata is loaded
* on first access through `MetadataManager.load()` / `loadMany()` and
* cached thereafter via the loader read-through cache. Best for
* long-running servers with thousands of items where only a hot subset
* is queried per request.
*
* - `artifact-only` — Load exclusively from the artifact source configured
* on the plugin (`artifactSource: { mode: 'local-file' }`; the `path` may
* be a filesystem path or an `http(s)` artifact URL).
* The filesystem is not scanned, and the database loader is not consulted
* during bootstrap. Required for Edge / serverless / immutable-image
* deployments where the running process must not perform write-back or
* open watch handles.
*/
bootstrap: z.enum(['eager', 'lazy', 'artifact-only']).default('eager')
.describe('How metadata is primed at plugin start (eager / lazy / artifact-only)'),
}));
export type MetadataPluginConfig = z.input<typeof MetadataPluginConfigSchema>;
/** Post-parse shape of {@link MetadataPluginConfig} — defaults applied, transforms run (ADR-0122). */
export type MetadataPluginConfigParsed = z.infer<typeof MetadataPluginConfigSchema>;
// ==========================================
// Metadata Plugin Manifest
// ==========================================
/**
* Metadata Plugin Manifest
*
* The complete manifest for the Metadata Plugin, declaring its identity,
* capabilities, and configuration. This is the "contract" between the
* metadata plugin and the kernel.
*/
export const MetadataPluginManifestSchema = lazySchema(() => z.object({
/** Plugin identifier */
id: z.literal('com.objectstack.metadata').describe('Metadata plugin ID'),
/** Plugin name */
name: z.literal('ObjectStack Metadata Service').describe('Plugin name'),
/** Plugin version */
version: z.string().regex(SEMVER_2_0_0_VERSION_PATTERN).describe('Plugin version (SemVer 2.0.0 — e.g. 1.2.3, 2.0.0-beta.1)'),
/** Plugin type */
type: z.literal('standard').describe('Plugin type'),
/** Plugin description */
description: z.string().default('Core metadata management service for ObjectStack platform')
.describe('Plugin description'),
/**
* Capabilities this plugin provides.
* The kernel uses this to route metadata requests to this plugin.
*/
capabilities: z.object({
/** Supports CRUD operations on metadata */
crud: z.boolean().default(true).describe('Supports metadata CRUD'),
/** Supports metadata query/search */
query: z.boolean().default(true).describe('Supports metadata query'),
/** Supports the overlay/customization system */
overlay: z.boolean().default(true).describe('Supports customization overlays'),
/** Supports file watching for hot reload */
watch: z.boolean().default(false).describe('Supports file watching'),
/** Supports bulk import/export */
importExport: z.boolean().default(true).describe('Supports import/export'),
/** Supports metadata validation */
validation: z.boolean().default(true).describe('Supports schema validation'),
/** Supports metadata versioning */
versioning: z.boolean().default(false).describe('Supports version history'),
/** Supports metadata events */
events: z.boolean().default(true).describe('Emits metadata events'),
}).describe('Plugin capabilities'),
/** Plugin configuration */
config: MetadataPluginConfigSchema.optional().describe('Plugin configuration'),
}));
export type MetadataPluginManifest = z.input<typeof MetadataPluginManifestSchema>;
/** Post-parse shape of {@link MetadataPluginManifest} — defaults applied, transforms run (ADR-0122). */
export type MetadataPluginManifestParsed = z.infer<typeof MetadataPluginManifestSchema>;
// ==========================================
// Built-in Type Registry Defaults
// ==========================================
/**
* Default Type Registry
*
* The built-in metadata type registry with default configurations.
* This array is the TOTAL universe of declared metadata types (#8586): the
* only production writer of the manager's type registry is
* `setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY)`, and no channel extends
* it — `config.additionalTypes` was retired as inert (ADR-0049), and a
* manifest's `contributes.kinds` registers kind descriptors as ITEMS of the
* `kind` type, not entries here. A plugin's kind reaches the live set as a
* side effect of registering an item of that kind.
*
* The `allowEnvironmentOverlay` column was spelled `allowOrgOverride` before
* 18; the rationale notes below name it by its current spelling, including
* where they record a decision taken under the old one.
*/
export const DEFAULT_METADATA_TYPE_REGISTRY: MetadataTypeRegistryEntryParsed[] = [
// Data Protocol (load first)
//
// `object` and `field`: packaged items are LOCKED (`allowEnvironmentOverlay: false`).
// Runtime API rejects overlay writes against artifact-backed objects/fields
// with `403 not_overridable`. Tenants CAN create brand-new OBJECTS
// (`allowRuntimeCreate: true`); `field` is the exception and the entry below
// records why. Rationale: object schema = physical table
// DDL; per-org overlay of packaged objects creates upgrade conflicts and
// multi-tenant schema drift. New tenant-owned objects live in their own
// namespace and are free to evolve. (Mirrors Salesforce: standard objects
// are not modifiable per-org beyond layout/label; custom objects are full).
{ type: 'object', label: 'Object', filePatterns: ['**/*.object.ts', '**/*.object.yml', '**/*.object.json'], supportsOverlay: false, allowEnvironmentOverlay: false, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 10, domain: 'data' },
// [ADR-0146 D10, #28574] `object_extension` — an extension of an object
// another package defines (`ObjectExtensionSchema`: fields, rules, indexes,
// labels, merged by the ObjectQL registry as an `'extend'` contributor) is
// an item of the package that writes it: a base's paired `.custom` (Studio's
// add-field, N-23c) or any writable package whose dependency closure holds
// the target. One item per package per target, stored under the target's
// name; its body carries `extend` and no `name`. Draft, publish, history,
// rollback and delete are the doors every item uses, and the package's
// export carries the item as an `objectExtensions` entry.
//
// `filePatterns`: the `*.extension.ts` convention a code package's
// `defineObjectExtension` files follow (`examples/app-showcase/src/data/
// extensions/`). A code package declares its extensions in
// `objectExtensions`, and they are never stored as items of this type: the
// ObjectQL engine's `registerApp` registers each one as an `'extend'`
// contributor of its target object. The artifact loader maps that collection
// to this type name but files no item from it, because an extension carries
// `extend` and no `name`. This type's items are the stored rows the
// `/api/v1/meta` doors write. `supportsOverlay` /
// `allowEnvironmentOverlay: false`: an extension is never overlaid; a
// package changes its own item, and a second package writes a second item.
// The body is judged by the metadata-protocol doors with
// `ObjectExtensionSchema` itself; this type has no entry in
// `BUILTIN_METADATA_TYPE_SCHEMAS` (the ADR-0010 envelope there would change
// the schema a manifest's `objectExtensions` parses with).
{ type: 'object_extension', label: 'Object Extension', description: 'Fields and rules a package adds to an object another package defines', filePatterns: ['**/*.extension.ts'], supportsOverlay: false, allowEnvironmentOverlay: false, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 11, domain: 'data' },
// [#7893] `field` — the STANDALONE runtime-create door is RETIRED
// (`allowRuntimeCreate: false`), maintainer ruling 2026-08-12.
//
// WHY THIS FLAG IS `false` (the decision this entry records):
//
// `field` is the one declared type with NO STANDALONE EXISTENCE. Fields are
// authored INSIDE the object (`ObjectSchema.fields`, a `z.record(name,
// FieldSchema)`), so a `field` write mints a SEPARATE `sys_metadata` row
// keyed `('field', '<object>.<name>')` — and nothing composes fragment rows
// into their parent. Measured end-to-end through the real `HttpDispatcher` →
// `ObjectStackProtocolImplementation` → `SysMetadataRepository` (#7893):
//
// PUT /api/v1/meta/field/showcase_task.zz_probe -> 200, state=active,
// "Saved field 'showcase_task.zz_probe' (env-wide, state=active)"
// GET /api/v1/meta/object/showcase_task -> fields = [title, status]
// `zz_probe` ABSENT, forever
// GET /api/v1/meta/field/showcase_task.zz_probe -> 200, _diagnostics.valid=true
//
// So the row is self-readable and well-formed, and reaches no object's
// `fields` — therefore no ObjectQL query, no physical column, no consumer
// that matters. Self-readable and universally inert. `allowRuntimeCreate:
// true` declared a capability the platform never built: there is no
// composition step, `applyRegistryWriteThrough` routes only
// `type === 'object'`, and `filePatterns` (`**\/*.field.ts`) match nothing in
// any app. ADR-0049 calls a declared-but-unhonoured capability false
// compliance and requires enforce-or-remove; this is the remove side.
//
// ⚠️ THE READ SKIP IS NOT CAUSED BY `supportsOverlay: false`, and a fix that
// "corrects" that flag is a REGRESSION. `supportsOverlay` gates no read path
// at all — only `assertDeleteAllowed` consults it, so flipping it to `true`
// changes nothing on the read and silently WIDENS the delete authorization
// gate. The control that settles it is one line up: `object` carries the
// IDENTICAL pair (`supportsOverlay: false, allowRuntimeCreate: true`) and a
// runtime-created object is fully readable (measured: `GET
// /meta/object/runtime_thing` -> `fields: ['note']`). Same flags, opposite
// outcome, so the flag is not the cause — the missing composition step is.
//
// WHAT DOES NOT CHANGE, and why this is not a lost capability: adding a
// field at runtime is a core Studio/CRM operation and it REMAINS possible —
// through the object, which is where a field actually lives. `object` keeps
// `allowRuntimeCreate: true`, so `PUT /api/v1/meta/object/<name>` with the
// new field in `fields` both persists AND composes. What is withdrawn is a
// second, broken SPELLING of that operation, not the operation. The refusal
// says so: `codeOnlySourceHint` gives `field` the object-route prescription
// rather than reading its `filePatterns` back (see the metadata-protocol
// call site — the glob would name a file no loader has ever ingested).
//
// ⚠️ NOT borrowed from #5488 (`api`), whose mechanism this reuses but whose
// JUSTIFICATION does not transfer: that ruling rested on "zero business pull
// for Studio-authored runtime endpoints today", and "add a field" is the
// opposite of zero-pull. The justification here is this card's own ruling and
// the object route staying open, not an absence of demand.
//
// Options rejected, on the record: (1) BUILD the read path — a feature
// spanning >= 3 packages (a composition step that does not exist, ~20
// `gate.fields` call sites, physical schema/migrations, and cold boot via
// `loadMetaFromDb`); a separate card if ever wanted, implementation first,
// declaration second. (3) DOCUMENT it as inert — precisely the shape ADR-0049
// forbids.
//
// ⚠️ `allowEnvironmentOverlay: false` is UNCHANGED and #7743's overlay refusal
// STAYS: an artifact-backed field is still refused `403 NOT_OVERRIDABLE` via
// `isNestedArtifactField`. Making field OVERRIDES legal is a separate
// decision from making field CREATES work; this entry touches only the
// create tier. `OS_METADATA_WRITABLE=field` remains the one operator escape
// hatch, and `deleteMetaItem` is deliberately NOT gated by this refusal, so
// repair of rows written through the retired channel stays possible.
{ type: 'field', label: 'Field', filePatterns: ['**/*.field.ts', '**/*.field.yml'], supportsOverlay: false, allowEnvironmentOverlay: false, allowRuntimeCreate: false, supportsVersioning: false, executionPinned: false, loadOrder: 20, domain: 'data' },
// ADR-0088 (#4509) — the `validation` kind is RETIRED. It failed the
// admission test on its first clause: no independent lifecycle. A rule only
// means anything against an object, and the only shape the engine evaluates
// is `object.validations[]`; the standalone schema had no binding key to
// name its object with (and, being `.strict()` in all six variants, could not
// be given one by an author). Its `filePatterns` and `allowRuntimeCreate`
// retire with it. Author rules as `validations:` on the object.
{ type: 'hook', label: 'Hook', filePatterns: ['**/*.hook.ts', '**/*.hook.yml'], supportsOverlay: false, allowEnvironmentOverlay: false, allowRuntimeCreate: true, supportsVersioning: false, executionPinned: false, loadOrder: 30, domain: 'data' },
// `seed`: fixture / initialization data (SeedSchema = object + records + mode +
// externalId). Runtime-draftable so the AI (and any author) can stage seed
// rows as a DRAFT (ADR-0033) and PUBLISH them like any other artifact;
// publishing a seed draft is what actually loads the rows (runtime applies it
// via SeedLoaderService — see runtime publish-drafts handler). loadOrder is
// last in the data domain so every referenced object/field already exists.
// NOTE: distinct from the (analytics-bound) `dataset` name — see ADR.
{ type: 'seed', label: 'Seed Data', description: 'Fixture / initialization data applied on publish', filePatterns: ['**/*.seed.ts', '**/*.seed.yml', '**/*.seed.json'], supportsOverlay: false, allowEnvironmentOverlay: false, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 95, domain: 'data' },
// `mapping` (#2611, admitted per ADR-0088): reusable import/export field
// mappings, consumed by the import endpoint via `mappingName`.
// `allowRuntimeCreate: true` so the import wizard can SAVE a hand-built
// mapping as a named artifact; packaged mappings stay locked
// (`allowEnvironmentOverlay: false`) like every artifact-backed item.
// `picklist`: a shared option list select fields reference by name
// (`Field.select({ picklist })`, `data/picklist.zod.ts`). PACKAGE-OWNED, so
// both runtime doors are closed: `allowRuntimeCreate: false` refuses a
// runtime create with 403 `not_creatable` and reads the prescription back
// from `filePatterns[0]`, and `allowEnvironmentOverlay: false` refuses an
// environment overlay of a packaged picklist — a deployment appending values
// is a later phase with its own admission (ADR-0005, additive only), not
// declared here. Another package
// adds values through `picklistExtensions`, on the artifact route.
// `loadOrder: 8` — before `object` (10), whose fields reference it.
{ type: 'picklist', label: 'Picklist', description: 'Shared option list that select fields reference by name', filePatterns: ['**/*.picklist.ts', '**/*.picklist.yml', '**/*.picklist.json'], supportsOverlay: false, allowEnvironmentOverlay: false, allowRuntimeCreate: false, supportsVersioning: false, executionPinned: false, loadOrder: 8, domain: 'data' },
{ type: 'mapping', label: 'Import Mapping', description: 'Reusable import/export field mapping (rename + transforms), referenced by name at import', filePatterns: ['**/*.mapping.ts', '**/*.mapping.yml', '**/*.mapping.json'], supportsOverlay: false, allowEnvironmentOverlay: false, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 96, domain: 'data' },
// UI Protocol
// `view/page/dashboard/action/report`: UI artifacts benefit from version
// history (label says it all — admins iterate frequently, want to roll back).
// `app`: tenants may author custom navigation apps (Salesforce Lightning App
// parity), so `allowRuntimeCreate: true`.
//
// `allowEnvironmentOverlay` in this section is governed by ADR-0005's amendment
// table (`docs/adr/0005-metadata-customization-overlay.md:53-64`), which
// whitelists exactly `view`/`dashboard`/`report` ("Pure presentation. Safe
// per-org override.") and says ❌ for `page`/`app`/`action` ("Conservative
// default — these bind to routes and side-effects. Promote individually if
// a concrete need appears."). `page`/`app`/`action`/`dataset` were ROLLED
// BACK from an unratified `true` by the 2026-08-08 maintainer ruling on
// ADR-0005, landed in commit ee58392e1 (same verdict family as `flow`, #6283): no promotion ADR exists,
// and no live org-scoped overlay rows were found in-repo. `dataset` is
// absent from the table, so it takes the amendment's default for new types
// — `allowEnvironmentOverlay: false` until an admission pair (overlay schema + a
// WRITTEN render-only rationale) is ratified. Promotion of any of these is
// an ADR-0005 revision, not a registry edit.
//
// NOT closed: `allowRuntimeCreate` stays `true` on all four — a tenant may
// still author a BRAND-NEW page/app/action/dataset (the ADR-0005 two-tier
// model); what is closed is per-org overlay of a PACKAGED item, now a loud
// `403 not_overridable` at the write. ADR-0045's publish visibility flip
// (`runtime/domains/packages.ts`) keeps working: it rewrites apps
// MATERIALIZED into `sys_metadata` (DB-only provenance), which rides the
// `allowRuntimeCreate` tier, not this flag.
{ type: 'view', label: 'View', filePatterns: ['**/*.view.ts', '**/*.view.yml', '**/*.view.json'], supportsOverlay: true, allowEnvironmentOverlay: true, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 50, domain: 'ui' },
{ type: 'page', label: 'Page', filePatterns: ['**/*.page.ts', '**/*.page.yml'], supportsOverlay: true, allowEnvironmentOverlay: false, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 50, domain: 'ui' },
{ type: 'dashboard', label: 'Dashboard', filePatterns: ['**/*.dashboard.ts', '**/*.dashboard.yml', '**/*.dashboard.json'], supportsOverlay: true, allowEnvironmentOverlay: true, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 60, domain: 'ui' },
{ type: 'app', label: 'Application', filePatterns: ['**/*.app.ts', '**/*.app.yml', '**/*.app.json'], supportsOverlay: true, allowEnvironmentOverlay: false, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 70, domain: 'ui' },
// `action` was additionally the #6283 `flow` shape exactly: its own row
// declares `supportsOverlay: false`, so `allowEnvironmentOverlay: true` granted a
// write nothing could ever read back — the #6190 phantom. (ADR-0005, commit ee58392e1)
{ type: 'action', label: 'Action', filePatterns: ['**/*.action.ts', '**/*.action.yml'], supportsOverlay: false, allowEnvironmentOverlay: false, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 50, domain: 'ui' },
{ type: 'report', label: 'Report', filePatterns: ['**/*.report.ts', '**/*.report.yml'], supportsOverlay: true, allowEnvironmentOverlay: true, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 60, domain: 'ui' },
// ADR-0021: dataset is the analytics semantic layer that report/dashboard bind to.
// loadOrder 55 < report/dashboard (60) so datasets register before their consumers.
{ type: 'dataset', label: 'Dataset', description: 'Analytics semantic layer — dimensions & measures', filePatterns: ['**/*.dataset.ts', '**/*.dataset.yml', '**/*.dataset.json'], supportsOverlay: true, allowEnvironmentOverlay: false, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 55, domain: 'ui' },
// Automation Protocol — flow is executionPinned (ADR-0009).
// ADR-0019: there is no `approval` metadata type — approvals are Approval
// nodes inside a `flow`, so they load and version with their enclosing flow.
// ADR-0020: there is no `workflow` metadata type — record state machines are
// a `state_machine` validation rule on the object, not a standalone artifact.
//
// `allowEnvironmentOverlay: false` — ROLLED BACK from `true` (#6283, settling the
// contract half of #6155 Q1=B; the unreviewed flip itself is recorded in
// #6191, commit ba252da0b). ADR-0005's amendment table
// (`docs/adr/0005-metadata-customization-overlay.md:57`) has never said
// anything else about this row:
//
// | automation | `flow`, `workflow`, `approval` | ❌ | Carry execution
// | side-effects (events, jobs, audit). Per-org variants are a deployment,
// | not an overlay. |
//
// That ADR is what `OVERLAY_ALLOWED_TYPES` derives from (Prime Directive #8),
// so `true` here was the registry contradicting the document it is supposed
// to be the machine-readable form of — and contradicting its OWN row, which
// declares `supportsOverlay: false`: the loader cannot merge a per-org flow
// overlay, so the write permission granted a write nothing could ever read
// back. #6190 measured exactly that phantom: an org-scoped flow overlay wrote
// successfully and lost its binding on the next cold start. Rolling the flag
// back turns that silent phantom into a loud `403 not_overridable` at the
// moment of the write.
//